From 0ff680bd2cb9256920876a69224eab2ddf1a2ad5 Mon Sep 17 00:00:00 2001 From: Anuj-72 Date: Fri, 17 Jul 2026 03:51:38 +0530 Subject: [PATCH 01/13] feat(vscode): scaffold VS Code extension for issue #35 Set up extensions/vscode/ as a thin client over the atlas-proxy HTTP API: extension manifest with settings (proxy URL, service token, permission mode, status bar) and command stubs, TypeScript + esbuild build, eslint, vitest test harness, and the src layout for the client/session/ui modules landing in follow-up commits. No functional behavior yet. --- .gitignore | 1 + extensions/vscode/.gitignore | 9 + extensions/vscode/.vscode/extensions.json | 5 + extensions/vscode/.vscode/launch.json | 21 + extensions/vscode/.vscode/settings.json | 13 + extensions/vscode/.vscode/tasks.json | 45 + extensions/vscode/.vscodeignore | 12 + extensions/vscode/README.md | 58 + extensions/vscode/esbuild.js | 56 + extensions/vscode/eslint.config.mjs | 27 + extensions/vscode/media/.gitkeep | 1 + extensions/vscode/package-lock.json | 4956 +++++++++++++++++++++ extensions/vscode/package.json | 106 + extensions/vscode/src/extension.ts | 18 + extensions/vscode/tsconfig.json | 19 + 15 files changed, 5347 insertions(+) create mode 100644 extensions/vscode/.gitignore create mode 100644 extensions/vscode/.vscode/extensions.json create mode 100644 extensions/vscode/.vscode/launch.json create mode 100644 extensions/vscode/.vscode/settings.json create mode 100644 extensions/vscode/.vscode/tasks.json create mode 100644 extensions/vscode/.vscodeignore create mode 100644 extensions/vscode/README.md create mode 100644 extensions/vscode/esbuild.js create mode 100644 extensions/vscode/eslint.config.mjs create mode 100644 extensions/vscode/media/.gitkeep create mode 100644 extensions/vscode/package-lock.json create mode 100644 extensions/vscode/package.json create mode 100644 extensions/vscode/src/extension.ts create mode 100644 extensions/vscode/tsconfig.json diff --git a/.gitignore b/.gitignore index ddb819db..ad798339 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ htmlcov/ .hypothesis/ *.egg-info/ dist/ +out/ build/ eggs/ *.egg diff --git a/extensions/vscode/.gitignore b/extensions/vscode/.gitignore new file mode 100644 index 00000000..0ecb344f --- /dev/null +++ b/extensions/vscode/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +out/ +.vscode-test/ +*.vsix + +# Root .gitignore ignores these repo-wide; the extension needs them committed. +!package-lock.json +!.vscode/ diff --git a/extensions/vscode/.vscode/extensions.json b/extensions/vscode/.vscode/extensions.json new file mode 100644 index 00000000..259410a7 --- /dev/null +++ b/extensions/vscode/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers"] +} diff --git a/extensions/vscode/.vscode/launch.json b/extensions/vscode/.vscode/launch.json new file mode 100644 index 00000000..c42edc04 --- /dev/null +++ b/extensions/vscode/.vscode/launch.json @@ -0,0 +1,21 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}" + } + ] +} diff --git a/extensions/vscode/.vscode/settings.json b/extensions/vscode/.vscode/settings.json new file mode 100644 index 00000000..1bcc8be2 --- /dev/null +++ b/extensions/vscode/.vscode/settings.json @@ -0,0 +1,13 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": false, // set this to true to hide the "out" folder with the compiled JS files + "dist": false // set this to true to hide the "dist" folder with the compiled JS files + }, + "search.exclude": { + "out": true, // set this to false to include "out" folder in search results + "dist": true // set this to false to include "dist" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "js/ts.tsc.autoDetect": "off" +} \ No newline at end of file diff --git a/extensions/vscode/.vscode/tasks.json b/extensions/vscode/.vscode/tasks.json new file mode 100644 index 00000000..8cc4d865 --- /dev/null +++ b/extensions/vscode/.vscode/tasks.json @@ -0,0 +1,45 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "label": "watch", + "dependsOn": [ + "npm: watch:tsc", + "npm: watch:esbuild" + ], + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "type": "npm", + "script": "watch:esbuild", + "group": "build", + "problemMatcher": "$esbuild-watch", + "isBackground": true, + "label": "npm: watch:esbuild", + "presentation": { + "group": "watch", + "reveal": "never" + } + }, + { + "type": "npm", + "script": "watch:tsc", + "group": "build", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "label": "npm: watch:tsc", + "presentation": { + "group": "watch", + "reveal": "never" + } + } + ] +} diff --git a/extensions/vscode/.vscodeignore b/extensions/vscode/.vscodeignore new file mode 100644 index 00000000..34e33d1e --- /dev/null +++ b/extensions/vscode/.vscodeignore @@ -0,0 +1,12 @@ +.vscode/** +.vscode-test/** +out/** +test/** +node_modules/** +src/** +.gitignore +esbuild.js +**/tsconfig.json +**/eslint.config.mjs +**/*.map +**/*.ts diff --git a/extensions/vscode/README.md b/extensions/vscode/README.md new file mode 100644 index 00000000..2e433a3c --- /dev/null +++ b/extensions/vscode/README.md @@ -0,0 +1,58 @@ +# ATLAS VS Code Extension + +A VS Code client for the [ATLAS](https://github.com/itigges22/ATLAS) agent proxy — a thin UI layer wrapping `atlas-proxy`'s agent loop (chat, tool calls, permission gating, diffs) with no agent logic in the extension itself. + +**Status: Work in progress.** Tracking [issue #35](https://github.com/itigges22/ATLAS/issues/35). Currently scaffold-only — chat UI, permission flow, and diff rendering land in upcoming commits. + +## How it works + +The extension is a thin SSE client over the proxy HTTP API (see `docs/API.md`): + +* `POST /v1/agent` — streams a turn (text tokens, tool calls/results, permission requests) as server-sent events +* `POST /v1/permission` — answers permission requests raised mid-turn +* `POST /cancel` — cancels the in-flight turn +* `GET /ready` — status bar connectivity polling + +The TUI (`tui/`) is the reference client; the extension mirrors its session conventions (client-minted `session_id` per turn, `session_allowed_tools` re-sent each turn, cancel = abort + best-effort `POST /cancel`). + +## Settings + +* `atlas.proxyUrl` — base URL of the ATLAS proxy server (default `http://localhost:8090`) +* `atlas.serviceToken` — dev-override bearer token (prefer the `ATLAS: Set Service Token` command, which stores it in SecretStorage instead of plaintext settings) +* `atlas.permissionMode` — `default` / `accept-edits` / `yolo` +* `atlas.statusBar.enabled`, `atlas.statusBar.pollIntervalSec` — status bar connectivity polling + +## Commands + +* `ATLAS: Open Chat` +* `ATLAS: Cancel Current Turn` +* `ATLAS: Set Service Token` +* `ATLAS: New Conversation` + +## Known limitations + +* The proxy applies tool calls to its own mounted workspace (`ATLAS_WORKSPACE_DIR`). If the VS Code workspace folder is not the same directory, edits land elsewhere — the extension detects the mismatch heuristically and warns, but cannot verify the proxy's mount (no endpoint exposes it yet). + +## Development + +```bash +npm install +npm run compile # type-check + lint + bundle +npm test # vitest unit tests +``` + +Press `F5` in VS Code to launch an Extension Development Host for testing against a live proxy (`atlas up`). + +## Layout + +``` +src/ +├── extension.ts # activate(), command registration +├── client/ # proxy HTTP/SSE client (atlasClient, sse parser, API types) +├── session/ # turn lifecycle (session_id, history, permission flow) +├── ui/ # chat webview, status bar, diff provider +├── workspace/ # workspace-mismatch heuristic +└── util/ # error-envelope mapping +media/ # webview assets +test/ # vitest unit tests + mock proxy fixture +``` diff --git a/extensions/vscode/esbuild.js b/extensions/vscode/esbuild.js new file mode 100644 index 00000000..cc2be598 --- /dev/null +++ b/extensions/vscode/esbuild.js @@ -0,0 +1,56 @@ +const esbuild = require("esbuild"); + +const production = process.argv.includes('--production'); +const watch = process.argv.includes('--watch'); + +/** + * @type {import('esbuild').Plugin} + */ +const esbuildProblemMatcherPlugin = { + name: 'esbuild-problem-matcher', + + setup(build) { + build.onStart(() => { + console.log('[watch] build started'); + }); + build.onEnd((result) => { + result.errors.forEach(({ text, location }) => { + console.error(`✘ [ERROR] ${text}`); + console.error(` ${location.file}:${location.line}:${location.column}:`); + }); + console.log('[watch] build finished'); + }); + }, +}; + +async function main() { + const ctx = await esbuild.context({ + entryPoints: [ + 'src/extension.ts' + ], + bundle: true, + format: 'cjs', + minify: production, + sourcemap: !production, + sourcesContent: false, + platform: 'node', + outfile: 'dist/extension.js', + external: ['vscode'], + logLevel: 'silent', + plugins: [ + /* add to the end of plugins array */ + esbuildProblemMatcherPlugin, + ], + }); + if (watch) { + await ctx.watch(); + } else { + await ctx.rebuild(); + await ctx.dispose(); + } +} + +main().catch(e => { + console.error(e); + process.exit(1); +}); diff --git a/extensions/vscode/eslint.config.mjs b/extensions/vscode/eslint.config.mjs new file mode 100644 index 00000000..7c51b0c0 --- /dev/null +++ b/extensions/vscode/eslint.config.mjs @@ -0,0 +1,27 @@ +import typescriptEslint from "typescript-eslint"; + +export default [{ + files: ["**/*.ts"], +}, { + plugins: { + "@typescript-eslint": typescriptEslint.plugin, + }, + + languageOptions: { + parser: typescriptEslint.parser, + ecmaVersion: 2022, + sourceType: "module", + }, + + rules: { + "@typescript-eslint/naming-convention": ["warn", { + selector: "import", + format: ["camelCase", "PascalCase"], + }], + + curly: "warn", + eqeqeq: "warn", + "no-throw-literal": "warn", + semi: "warn", + }, +}]; \ No newline at end of file diff --git a/extensions/vscode/media/.gitkeep b/extensions/vscode/media/.gitkeep new file mode 100644 index 00000000..95db929c --- /dev/null +++ b/extensions/vscode/media/.gitkeep @@ -0,0 +1 @@ +# Placeholder — webview assets (chat.js, chat.css, icon) land with the chat view commit. diff --git a/extensions/vscode/package-lock.json b/extensions/vscode/package-lock.json new file mode 100644 index 00000000..6420d718 --- /dev/null +++ b/extensions/vscode/package-lock.json @@ -0,0 +1,4956 @@ +{ + "name": "atlas-vscode", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "atlas-vscode", + "version": "0.0.1", + "license": "AGPL-3.0-only", + "devDependencies": { + "@types/node": "^18.19.0", + "@types/vscode": "^1.100.0", + "esbuild": "^0.28.1", + "eslint": "^10.5.0", + "npm-run-all": "^4.1.5", + "typescript": "^6.0.3", + "typescript-eslint": "^8.61.1", + "vitest": "^3.2.0" + }, + "engines": { + "node": ">=18", + "vscode": "^1.100.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "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/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/vscode": { + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", + "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", + "integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/type-utils": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.64.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz", + "integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", + "integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.64.0", + "@typescript-eslint/types": "^8.64.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz", + "integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", + "integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz", + "integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz", + "integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", + "integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.64.0", + "@typescript-eslint/tsconfig-utils": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz", + "integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz", + "integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.64.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "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.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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-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/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/chalk/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/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "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/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/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/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "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": "10.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", + "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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/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/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/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.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/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "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/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "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": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "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-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "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/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/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/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/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm-run-all/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/npm-run-all/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm-run-all/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/npm-run-all/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/npm-run-all/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/npm-run-all/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/npm-run-all/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/npm-run-all/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/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "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/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "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/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "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/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/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "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/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.padend": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", + "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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/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/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.64.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz", + "integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.64.0", + "@typescript-eslint/parser": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "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/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "extraneous": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/vite-node/node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "extraneous": true, + "license": "MIT" + }, + "node_modules/vite-node/node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "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/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "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/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json new file mode 100644 index 00000000..e4dc5a20 --- /dev/null +++ b/extensions/vscode/package.json @@ -0,0 +1,106 @@ +{ + "name": "atlas-vscode", + "displayName": "ATLAS", + "description": "VS Code client for the ATLAS agent proxy — chat, tool calls, permission gating, and diff review over the proxy HTTP API.", + "version": "0.0.1", + "publisher": "atlas", + "license": "AGPL-3.0-only", + "repository": { + "type": "git", + "url": "https://github.com/itigges22/ATLAS.git", + "directory": "extensions/vscode" + }, + "engines": { + "vscode": "^1.100.0", + "node": ">=18" + }, + "categories": [ + "AI", + "Other" + ], + "activationEvents": [], + "main": "./dist/extension.js", + "contributes": { + "commands": [ + { + "command": "atlas.openChat", + "title": "ATLAS: Open Chat" + }, + { + "command": "atlas.cancelTurn", + "title": "ATLAS: Cancel Current Turn" + }, + { + "command": "atlas.setToken", + "title": "ATLAS: Set Service Token" + }, + { + "command": "atlas.newConversation", + "title": "ATLAS: New Conversation" + } + ], + "configuration": { + "title": "ATLAS", + "properties": { + "atlas.proxyUrl": { + "type": "string", + "default": "http://localhost:8090", + "description": "Base URL of the ATLAS proxy server." + }, + "atlas.serviceToken": { + "type": "string", + "default": "", + "description": "Dev-override bearer token for the ATLAS proxy. Prefer the 'ATLAS: Set Service Token' command (stores in SecretStorage) — this field is plaintext in settings.json." + }, + "atlas.permissionMode": { + "type": "string", + "enum": [ + "default", + "accept-edits", + "yolo" + ], + "default": "default", + "enumDescriptions": [ + "Pause and prompt for every file-write tool call.", + "Auto-allow edit/write tools, still prompt for other sensitive actions.", + "Auto-allow everything. Use with caution." + ], + "description": "Controls how ATLAS permission requests are handled." + }, + "atlas.statusBar.enabled": { + "type": "boolean", + "default": true, + "description": "Show ATLAS connection status in the status bar." + }, + "atlas.statusBar.pollIntervalSec": { + "type": "number", + "default": 15, + "minimum": 5, + "description": "How often (seconds) to poll /ready for status bar state." + } + } + } + }, + "scripts": { + "vscode:prepublish": "npm run package", + "compile": "npm run check-types && npm run lint && node esbuild.js", + "watch": "npm-run-all -p watch:*", + "watch:esbuild": "node esbuild.js --watch", + "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", + "package": "npm run check-types && npm run lint && node esbuild.js --production", + "check-types": "tsc --noEmit", + "lint": "eslint src", + "test": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@types/node": "^18.19.0", + "@types/vscode": "^1.100.0", + "esbuild": "^0.28.1", + "eslint": "^10.5.0", + "npm-run-all": "^4.1.5", + "typescript": "^6.0.3", + "typescript-eslint": "^8.61.1", + "vitest": "^3.2.0" + } +} diff --git a/extensions/vscode/src/extension.ts b/extensions/vscode/src/extension.ts new file mode 100644 index 00000000..9743fb19 --- /dev/null +++ b/extensions/vscode/src/extension.ts @@ -0,0 +1,18 @@ +import * as vscode from 'vscode'; + +// Command IDs contributed in package.json. Real implementations land with the +// chat view (atlas.openChat / atlas.newConversation), turn manager +// (atlas.cancelTurn), and client auth (atlas.setToken) in upcoming commits. +const COMMANDS = ['atlas.openChat', 'atlas.cancelTurn', 'atlas.setToken', 'atlas.newConversation'] as const; + +export function activate(context: vscode.ExtensionContext) { + for (const command of COMMANDS) { + context.subscriptions.push( + vscode.commands.registerCommand(command, () => { + void vscode.window.showInformationMessage(`ATLAS: '${command}' is not implemented yet (scaffold).`); + }), + ); + } +} + +export function deactivate() {} diff --git a/extensions/vscode/tsconfig.json b/extensions/vscode/tsconfig.json new file mode 100644 index 00000000..bb38c16b --- /dev/null +++ b/extensions/vscode/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "Node16", + "target": "ES2022", + "lib": [ + "ES2022" + ], + "types": [ + "node" + ], + "sourceMap": true, + "strict": true, + "noEmit": true + }, + "include": [ + "src", + "test" + ] +} From e5209b500b91c261aba8e511a9f8df0bbbce3747 Mon Sep 17 00:00:00 2001 From: Anuj-72 Date: Fri, 17 Jul 2026 20:46:33 +0530 Subject: [PATCH 02/13] feat(vscode): add proxy client core with SSE parser and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the typed client layer over the atlas-proxy public API: - src/client/types.ts: request/event/error shapes from docs/API.md, matching the proxy's Go struct field tags - src/client/sse.ts: pure SSE frame parser mirroring tui/chat.go (comment skipping, [DONE] sentinel, malformed-frame tolerance, chunk-split and multibyte-safe buffering) - src/client/atlasClient.ts: /v1/agent streaming, /cancel (best-effort), /v1/permission (404-as-resolved), /ready, /version; bearer auth; stable error-envelope mapping via AtlasApiError - test/: vitest suites (27 tests) plus a mockProxy fixture — a real http.Server streaming canned SSE, including a permission pause-and-resume flow driven through the client tsconfig gains skipLibCheck: vitest's bundled declarations reference DOM WebSocket types absent from lib ES2022. --- extensions/vscode/src/client/atlasClient.ts | 173 +++++++++++++ extensions/vscode/src/client/sse.ts | 84 +++++++ extensions/vscode/src/client/types.ts | 187 ++++++++++++++ extensions/vscode/test/atlasClient.test.ts | 246 +++++++++++++++++++ extensions/vscode/test/fixtures/mockProxy.ts | 165 +++++++++++++ extensions/vscode/test/sse.test.ts | 117 +++++++++ extensions/vscode/tsconfig.json | 5 +- 7 files changed, 976 insertions(+), 1 deletion(-) create mode 100644 extensions/vscode/src/client/atlasClient.ts create mode 100644 extensions/vscode/src/client/sse.ts create mode 100644 extensions/vscode/src/client/types.ts create mode 100644 extensions/vscode/test/atlasClient.test.ts create mode 100644 extensions/vscode/test/fixtures/mockProxy.ts create mode 100644 extensions/vscode/test/sse.test.ts diff --git a/extensions/vscode/src/client/atlasClient.ts b/extensions/vscode/src/client/atlasClient.ts new file mode 100644 index 00000000..863fe153 --- /dev/null +++ b/extensions/vscode/src/client/atlasClient.ts @@ -0,0 +1,173 @@ +// HTTP client for the atlas-proxy public API: /v1/agent (SSE), +// /cancel, /v1/permission, /ready, /version. Zero runtime deps — +// Node 18+ global fetch + AbortController. No vscode imports so the +// mock-proxy integration tests run under plain vitest. +// +// Conventions mirrored from tui/chat.go: +// - Authorization: Bearer on every request when a token is set. +// - /v1/permission 404 = request already resolved = success. +// - /cancel is best-effort defense-in-depth; aborting the SSE request +// is the primary cancel mechanism. + +import { parseSSEStream } from './sse'; +import type { + AgentRequest, + ChatEvent, + ErrorEnvelope, + PermissionDecisionRequest, + ReadyResponse, + VersionResponse, +} from './types'; + +export interface AtlasClientOptions { + /** Base URL of the proxy, e.g. http://localhost:8090. */ + baseUrl: string; + /** Bearer token; empty string sends no Authorization header. */ + token?: string; +} + +/** Error carrying the proxy's stable error envelope when one was parseable. + * Callers switch on `code` (closed set), never on the human `detail`. */ +export class AtlasApiError extends Error { + readonly status: number; + readonly code: string; + readonly detail: string; + + constructor(status: number, envelope: Partial, fallbackBody: string) { + const code = envelope.error ?? ''; + const detail = envelope.detail ?? fallbackBody; + super(code ? `${code}: ${detail}` : `HTTP ${status}: ${detail}`); + this.name = 'AtlasApiError'; + this.status = status; + this.code = code; + this.detail = detail; + } +} + +async function toApiError(response: Response): Promise { + const body = await response.text().catch(() => ''); + let envelope: Partial = {}; + try { + envelope = JSON.parse(body) as ErrorEnvelope; + } catch { + // non-JSON body (reverse proxy error page etc.) — keep raw text + } + return new AtlasApiError(response.status, envelope, body.trim()); +} + +export class AtlasClient { + private readonly baseUrl: string; + private readonly token: string; + + constructor(options: AtlasClientOptions) { + this.baseUrl = options.baseUrl.replace(/\/+$/, ''); + this.token = options.token ?? ''; + } + + private headers(json = true): Record { + const headers: Record = {}; + if (json) { + headers['Content-Type'] = 'application/json'; + } + if (this.token !== '') { + headers['Authorization'] = `Bearer ${this.token}`; + } + return headers; + } + + /** + * POST /v1/agent and stream the turn's events. The generator completes + * on the `[DONE]` sentinel; aborting `signal` cancels the request (the + * caller should also fire cancelTurn as defense-in-depth). + * + * Non-200 responses throw AtlasApiError before any event is yielded. + */ + async *sendAgentTurn( + request: AgentRequest, + signal?: AbortSignal, + ): AsyncGenerator { + const response = await fetch(`${this.baseUrl}/v1/agent`, { + method: 'POST', + headers: { ...this.headers(), Accept: 'text/event-stream' }, + body: JSON.stringify(request), + signal, + }); + if (!response.ok) { + throw await toApiError(response); + } + if (!response.body) { + throw new AtlasApiError(response.status, {}, 'empty response body'); + } + yield* parseSSEStream(response.body); + } + + /** + * POST /cancel for an in-flight turn. Best-effort: connection failures + * and non-200s are swallowed — the SSE abort is the primary mechanism. + * Returns true when the proxy reported `cancelled: true`. + */ + async cancelTurn(sessionId: string): Promise { + if (sessionId === '') { + return false; + } + try { + const response = await fetch(`${this.baseUrl}/cancel`, { + method: 'POST', + headers: this.headers(), + body: JSON.stringify({ session_id: sessionId }), + }); + if (!response.ok) { + return false; // 404 = nothing in flight (idempotent) + } + const body = (await response.json()) as { cancelled?: boolean }; + return body.cancelled === true; + } catch { + return false; + } + } + + /** + * POST /v1/permission answering a permission_request event. A 404 means + * the pending request already resolved (cancelled or timed out) — treated + * as success per the TUI convention. Other failures throw AtlasApiError. + */ + async postPermissionDecision(decision: PermissionDecisionRequest): Promise { + const response = await fetch(`${this.baseUrl}/v1/permission`, { + method: 'POST', + headers: this.headers(), + body: JSON.stringify(decision), + }); + if (response.ok || response.status === 404) { + await response.body?.cancel(); + return; + } + throw await toApiError(response); + } + + /** + * GET /ready. Returns the gate body for both 200 and 503 (same shape). + * Network failure throws — the status bar maps that to "unreachable". + */ + async getReady(): Promise { + const response = await fetch(`${this.baseUrl}/ready`, { + method: 'GET', + headers: this.headers(false), + }); + if (response.ok || response.status === 503) { + return (await response.json()) as ReadyResponse; + } + throw await toApiError(response); + } + + /** GET /version — api_version, protocol_version, error_codes. */ + async getVersion(): Promise { + const response = await fetch(`${this.baseUrl}/version`, { + method: 'GET', + headers: this.headers(false), + }); + if (!response.ok) { + throw await toApiError(response); + } + return (await response.json()) as VersionResponse; + } +} diff --git a/extensions/vscode/src/client/sse.ts b/extensions/vscode/src/client/sse.ts new file mode 100644 index 00000000..b95338dc --- /dev/null +++ b/extensions/vscode/src/client/sse.ts @@ -0,0 +1,84 @@ +// Pure SSE frame parser for the /v1/agent chat stream. Mirrors +// tui/chat.go's parseChatSSE: line-oriented, skips `:` comments (the +// proxy sends `: connected` on open), takes `data:` frames, stops on +// the `[DONE]` sentinel, and skips malformed frames rather than +// killing the turn. No vscode imports — unit-testable under vitest. + +import type { ChatEvent } from './types'; + +/** The SSE terminator the proxy writes after the final event. */ +export const DONE_SENTINEL = '[DONE]'; + +/** + * Parse a byte stream of SSE lines into ChatEvents. + * + * Yields each well-formed `data: {"type":...,"data":...}` frame and + * returns when the `[DONE]` sentinel arrives or the stream ends. + * Chunk boundaries are arbitrary — a frame may be split anywhere, + * including mid-multibyte-character (the streaming TextDecoder holds + * partial sequences across chunks). + */ +export async function* parseSSEStream( + stream: AsyncIterable, +): AsyncGenerator { + const decoder = new TextDecoder('utf-8'); + let buffer = ''; + + for await (const chunk of stream) { + buffer += decoder.decode(chunk, { stream: true }); + + // Consume complete lines; keep the trailing partial in the buffer. + let newlineIndex: number; + while ((newlineIndex = buffer.indexOf('\n')) !== -1) { + const line = buffer.slice(0, newlineIndex); + buffer = buffer.slice(newlineIndex + 1); + const event = parseSSELine(line); + if (event === DONE) { + return; + } + if (event) { + yield event; + } + } + } + + // Stream ended without [DONE]; flush a final unterminated line if any. + buffer += decoder.decode(); + if (buffer.length > 0) { + const event = parseSSELine(buffer); + if (event && event !== DONE) { + yield event; + } + } +} + +/** Internal marker distinguishing the [DONE] sentinel from a skipped line. */ +const DONE = Symbol('done'); + +function parseSSELine(rawLine: string): ChatEvent | typeof DONE | null { + const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine; + // Blank separators and `:` comments (": connected", ": heartbeat"). + if (line === '' || line.startsWith(':')) { + return null; + } + if (!line.startsWith('data:')) { + return null; + } + const data = line.slice('data:'.length).trim(); + if (data === '') { + return null; + } + if (data === DONE_SENTINEL) { + return DONE; + } + let event: ChatEvent; + try { + event = JSON.parse(data) as ChatEvent; + } catch { + return null; // skip malformed frame, don't kill the turn + } + if (!event || typeof event.type !== 'string' || event.type === '') { + return null; + } + return event; +} diff --git a/extensions/vscode/src/client/types.ts b/extensions/vscode/src/client/types.ts new file mode 100644 index 00000000..e5ffd1bb --- /dev/null +++ b/extensions/vscode/src/client/types.ts @@ -0,0 +1,187 @@ +// Request/response and SSE event shapes for the atlas-proxy public client +// API. Mirrors docs/API.md and the Go structs in proxy/types.go / +// tui/chat.go — field tags there are the source of truth. + +/** One prior-turn message replayed to the proxy on each /v1/agent call. + * The proxy caps history at the most recent 40 entries. */ +export interface HistoryMessage { + role: 'user' | 'assistant'; + content: string; +} + +/** Permission mode for a turn (docs/API.md POST /v1/agent `mode`). */ +export type PermissionMode = 'default' | 'accept-edits' | 'yolo'; + +/** POST /v1/agent request body. Field names MUST match the anonymous + * struct in proxy/agent.go's handleAgent (see tui/chat.go agentRequest). */ +export interface AgentRequest { + message: string; + working_dir: string; + mode: PermissionMode; + /** Required for /cancel and /v1/permission — the proxy keys the cancel + * handle and pending permission requests by this id. */ + session_id: string; + history?: HistoryMessage[]; + /** Tools the user approved for the whole session; re-sent every turn. */ + session_allowed_tools?: string[]; +} + +/** POST /v1/permission request body. */ +export interface PermissionDecisionRequest { + session_id: string; + tool_call_id: string; + decision: 'allow' | 'deny'; + scope: 'once' | 'session'; +} + +/** One SSE event on the /v1/agent stream: {"type":"","data":{...}}. + * `data` stays loosely typed at the envelope level — narrow via the + * payload interfaces below after switching on `type`. Unknown types must + * be tolerated (forward compatibility). */ +export interface ChatEvent { + type: string; + data: unknown; +} + +// --- Payloads for the events the extension renders (docs/API.md table). --- + +export interface TextEventData { + content: string; +} + +export interface ReasoningTokenEventData { + text: string; +} + +export interface ToolCallEventData { + name: string; + args: unknown; + turn: number; +} + +export interface ToolResultEventData { + tool: string; + success: boolean; + data: unknown; + error?: string; + /** Go duration string, e.g. "245ms". */ + elapsed?: string; +} + +export interface PermissionRequestEventData { + tool_name: string; + args: unknown; + /** Human-readable description of the pending call. */ + message: string; + /** Echo back on POST /v1/permission. */ + tool_call_id: string; +} + +export interface PermissionDeniedEventData { + tool: string; +} + +export interface LlmCallStartEventData { + turn: number; + messages: number; + prompt_tokens: number; +} + +export interface LlmFirstTokenEventData { + prompt_ms: number; +} + +export interface LlmCallEndEventData { + turn: number; + tokens: number; + total_tokens: number; + ms: number; + chars?: number; + error?: string; +} + +export interface LlmPromptProgressEventData { + processed: number; + total: number; + /** 0–1 float. */ + pct: number; + elapsed_ms: number; +} + +export interface DoneEventData { + /** Empty for a text-shaped turn. */ + summary: string; +} + +export interface ErrorEventData { + error: string; +} + +export interface PlanStep { + id: string; + action: string; + target: string; + why: string; +} + +export interface PlanLoadedEventData { + steps: PlanStep[]; + verify_step: string; + rationale: string; + winning_score: number; + /** 0 for the initial plan, 1+ for revisions. */ + revision: number; +} + +// --- Non-stream endpoint payloads. --- + +/** POST /cancel response. 200 → cancelled:true; 404 → cancelled:false. */ +export interface CancelResponse { + cancelled: boolean; +} + +/** POST /v1/permission response. 200 → delivered:true; 404 → the request + * already resolved (treated as success by convention — see tui/chat.go + * postPermissionDecision). */ +export interface PermissionDecisionResponse { + delivered: boolean; +} + +/** GET /ready body — 200 when all gates pass, 503 (same shape) otherwise. */ +export interface ReadyResponse { + ready: boolean; + inference: boolean; + lens_ready: boolean; + sandbox: boolean; + v3: boolean; +} + +/** GET /version body. */ +export interface VersionResponse { + api_version: string; + protocol_version: number; + error_codes: string[]; +} + +/** Closed error-code set from docs/API.md. Switch on `error`, never on + * `detail` — the human message may change between versions. */ +export type ErrorCode = + | 'unauthorized' + | 'invalid_input' + | 'unsupported_operation' + | 'permission_denied' + | 'timeout' + | 'cancelled' + | 'dependency_unavailable' + | 'incompatible_artifact' + | 'resource_limit' + | 'sandbox_policy_rejected' + | 'model_failure' + | 'internal_error'; + +/** Stable error envelope on non-2xx JSON responses. */ +export interface ErrorEnvelope { + error: ErrorCode | string; + detail?: string; + api_version?: string; +} diff --git a/extensions/vscode/test/atlasClient.test.ts b/extensions/vscode/test/atlasClient.test.ts new file mode 100644 index 00000000..5a430dc9 --- /dev/null +++ b/extensions/vscode/test/atlasClient.test.ts @@ -0,0 +1,246 @@ +// Integration tests for AtlasClient against the mockProxy fixture: +// happy-path turn, bearer auth, permission pause + resume, permission +// 404-as-success, error envelope, cancel/abort, /ready 200 and 503. + +import { afterEach, describe, expect, it } from 'vitest'; +import { AtlasApiError, AtlasClient } from '../src/client/atlasClient'; +import type { AgentRequest, ChatEvent } from '../src/client/types'; +import { MockProxy } from './fixtures/mockProxy'; + +const BASE_REQUEST: AgentRequest = { + message: 'hello', + working_dir: '.', + mode: 'default', + session_id: 'test-abc123', +}; + +let proxy: MockProxy | undefined; + +afterEach(async () => { + await proxy?.stop(); + proxy = undefined; +}); + +async function startProxy(options: ConstructorParameters[0] = {}): Promise { + proxy = new MockProxy(options); + await proxy.start(); + return proxy; +} + +describe('AtlasClient.sendAgentTurn', () => { + it('streams a happy-path turn to completion', async () => { + const mock = await startProxy({ + agentEvents: [ + { type: 'turn_start', data: { turn: 1, messages: 1, trimmed: false } }, + { type: 'text', data: { content: 'hi there' } }, + { type: 'done', data: { summary: '' } }, + ], + }); + const client = new AtlasClient({ baseUrl: mock.url }); + + const events: ChatEvent[] = []; + for await (const event of client.sendAgentTurn(BASE_REQUEST)) { + events.push(event); + } + expect(events.map((e) => e.type)).toEqual(['turn_start', 'text', 'done']); + expect(mock.requests[0].body).toMatchObject({ message: 'hello', session_id: 'test-abc123' }); + }); + + it('sends the bearer token on every request', async () => { + const mock = await startProxy({ agentEvents: [] }); + const client = new AtlasClient({ baseUrl: mock.url, token: 'sk-test' }); + + for await (const _ of client.sendAgentTurn(BASE_REQUEST)) { + // drain + } + await client.cancelTurn('test-abc123'); + expect(mock.requests).toHaveLength(2); + for (const request of mock.requests) { + expect(request.headers.authorization).toBe('Bearer sk-test'); + } + }); + + it('omits the Authorization header when no token is set', async () => { + const mock = await startProxy({ agentEvents: [] }); + const client = new AtlasClient({ baseUrl: mock.url }); + for await (const _ of client.sendAgentTurn(BASE_REQUEST)) { + // drain + } + expect(mock.requests[0].headers.authorization).toBeUndefined(); + }); + + it('throws AtlasApiError with the envelope code on non-200', async () => { + const mock = await startProxy({ + agentError: { status: 401, body: { error: 'unauthorized', detail: 'bad token', api_version: '1.0.0' } }, + }); + const client = new AtlasClient({ baseUrl: mock.url }); + + const iterate = async () => { + for await (const _ of client.sendAgentTurn(BASE_REQUEST)) { + // unreachable + } + }; + await expect(iterate()).rejects.toSatisfy((error: unknown) => { + expect(error).toBeInstanceOf(AtlasApiError); + const apiError = error as AtlasApiError; + expect(apiError.status).toBe(401); + expect(apiError.code).toBe('unauthorized'); + expect(apiError.detail).toBe('bad token'); + return true; + }); + }); + + it('keeps the code empty on a non-JSON error body', async () => { + const mock = await startProxy({ agentError: { status: 502, body: 'Bad Gateway' } }); + const client = new AtlasClient({ baseUrl: mock.url }); + const iterate = async () => { + for await (const _ of client.sendAgentTurn(BASE_REQUEST)) { + // unreachable + } + }; + await expect(iterate()).rejects.toSatisfy((error: unknown) => { + const apiError = error as AtlasApiError; + expect(apiError.status).toBe(502); + expect(apiError.code).toBe(''); + return true; + }); + }); + + it('aborts the stream via AbortSignal', async () => { + const mock = await startProxy({ + agentEvents: [ + { type: 'text', data: { content: 'one' } }, + { type: 'text', data: { content: 'two' } }, + { type: 'text', data: { content: 'three' } }, + ], + frameDelayMs: 50, + }); + const client = new AtlasClient({ baseUrl: mock.url }); + const controller = new AbortController(); + + const events: ChatEvent[] = []; + const iterate = async () => { + for await (const event of client.sendAgentTurn(BASE_REQUEST, controller.signal)) { + events.push(event); + controller.abort(); // abort after the first event + } + }; + await expect(iterate()).rejects.toThrow(); + expect(events).toHaveLength(1); + }); + + it('resumes after a permission pause when the decision is posted', async () => { + const mock = await startProxy({ + agentEvents: [ + { + type: 'permission_request', + data: { tool_name: 'edit_file', args: {}, message: 'edit app.py?', tool_call_id: 'call_0' }, + }, + { type: 'tool_result', data: { tool: 'edit_file', success: true, data: {}, elapsed: '10ms' } }, + { type: 'done', data: { summary: '' } }, + ], + pauseAfterIndex: 0, + }); + const client = new AtlasClient({ baseUrl: mock.url }); + + const events: ChatEvent[] = []; + for await (const event of client.sendAgentTurn(BASE_REQUEST)) { + events.push(event); + if (event.type === 'permission_request') { + // Answer mid-stream, like the real UI flow. + await client.postPermissionDecision({ + session_id: BASE_REQUEST.session_id, + tool_call_id: 'call_0', + decision: 'allow', + scope: 'once', + }); + } + } + expect(events.map((e) => e.type)).toEqual(['permission_request', 'tool_result', 'done']); + const permissionPost = mock.requests.find((r) => r.url === '/v1/permission'); + expect(permissionPost?.body).toEqual({ + session_id: 'test-abc123', + tool_call_id: 'call_0', + decision: 'allow', + scope: 'once', + }); + }); +}); + +describe('AtlasClient.postPermissionDecision', () => { + it('treats 404 (already resolved) as success', async () => { + const mock = await startProxy({ permissionStatus: 404 }); + const client = new AtlasClient({ baseUrl: mock.url }); + await expect( + client.postPermissionDecision({ + session_id: 's', + tool_call_id: 'call_0', + decision: 'allow', + scope: 'session', + }), + ).resolves.toBeUndefined(); + }); + + it('throws on other failures', async () => { + const mock = await startProxy({ permissionStatus: 500 }); + const client = new AtlasClient({ baseUrl: mock.url }); + await expect( + client.postPermissionDecision({ session_id: 's', tool_call_id: 'c', decision: 'deny', scope: 'once' }), + ).rejects.toBeInstanceOf(AtlasApiError); + }); +}); + +describe('AtlasClient.cancelTurn', () => { + it('returns true when the proxy cancels', async () => { + const mock = await startProxy(); + const client = new AtlasClient({ baseUrl: mock.url }); + await expect(client.cancelTurn('test-abc123')).resolves.toBe(true); + expect(mock.requests[0].body).toEqual({ session_id: 'test-abc123' }); + }); + + it('is best-effort: returns false on connection failure', async () => { + // Port 1 is unassignable — connection refused. + const client = new AtlasClient({ baseUrl: 'http://127.0.0.1:1' }); + await expect(client.cancelTurn('s')).resolves.toBe(false); + }); + + it('returns false for an empty session id without a request', async () => { + const mock = await startProxy(); + const client = new AtlasClient({ baseUrl: mock.url }); + await expect(client.cancelTurn('')).resolves.toBe(false); + expect(mock.requests).toHaveLength(0); + }); +}); + +describe('AtlasClient.getReady', () => { + it('returns the gate body on 200', async () => { + const mock = await startProxy(); + const client = new AtlasClient({ baseUrl: mock.url }); + await expect(client.getReady()).resolves.toMatchObject({ ready: true }); + }); + + it('returns the gate body on 503 (degraded)', async () => { + const mock = await startProxy({ + ready: { status: 503, body: { ready: false, inference: true, lens_ready: false, sandbox: true, v3: true } }, + }); + const client = new AtlasClient({ baseUrl: mock.url }); + await expect(client.getReady()).resolves.toMatchObject({ ready: false, lens_ready: false }); + }); +}); + +describe('AtlasClient.getVersion', () => { + it('returns version info', async () => { + const mock = await startProxy(); + const client = new AtlasClient({ baseUrl: mock.url }); + await expect(client.getVersion()).resolves.toMatchObject({ api_version: '1.0.0', protocol_version: 1 }); + }); +}); + +describe('AtlasClient base URL handling', () => { + it('strips trailing slashes from the base URL', async () => { + const mock = await startProxy(); + const client = new AtlasClient({ baseUrl: `${mock.url}///` }); + await client.getVersion(); + expect(mock.requests[0].url).toBe('/version'); + }); +}); diff --git a/extensions/vscode/test/fixtures/mockProxy.ts b/extensions/vscode/test/fixtures/mockProxy.ts new file mode 100644 index 00000000..8cdcc67c --- /dev/null +++ b/extensions/vscode/test/fixtures/mockProxy.ts @@ -0,0 +1,165 @@ +// Real http.Server fixture streaming canned SSE the way atlas-proxy +// does: `: connected` comment first, `data: {...}\n\n` frames, then +// `data: [DONE]\n\n`. Drives integration tests of AtlasClient without +// a live proxy. + +import * as http from 'node:http'; +import type { AddressInfo } from 'node:net'; +import type { ChatEvent } from '../../src/client/types'; + +export interface RecordedRequest { + method: string; + url: string; + headers: http.IncomingHttpHeaders; + body: unknown; +} + +export interface MockProxyOptions { + /** Events streamed on POST /v1/agent, in order. */ + agentEvents?: ChatEvent[]; + /** Pause the /v1/agent stream after this event index until a + * POST /v1/permission arrives (simulates a permission_request block). */ + pauseAfterIndex?: number; + /** Respond to /v1/agent with this status + error envelope, no stream. */ + agentError?: { status: number; body: unknown }; + /** Status for POST /v1/permission (default 200). */ + permissionStatus?: number; + /** Body for GET /ready (default all-true) and its status (default 200). */ + ready?: { status: number; body: unknown }; + /** Milliseconds between streamed frames (default 0 — immediate). */ + frameDelayMs?: number; + /** Omit the trailing [DONE] sentinel (simulates a dropped connection). */ + omitDone?: boolean; +} + +export class MockProxy { + readonly requests: RecordedRequest[] = []; + private server: http.Server | undefined; + private options: MockProxyOptions; + private resumeSignal: (() => void) | undefined; + + constructor(options: MockProxyOptions = {}) { + this.options = options; + } + + get url(): string { + const address = this.server?.address() as AddressInfo | null; + if (!address) { + throw new Error('mock proxy not started'); + } + return `http://127.0.0.1:${address.port}`; + } + + async start(): Promise { + this.server = http.createServer((request, response) => { + void this.handle(request, response); + }); + await new Promise((resolve) => this.server!.listen(0, '127.0.0.1', resolve)); + } + + async stop(): Promise { + const server = this.server; + this.server = undefined; + if (server) { + server.closeAllConnections(); + await new Promise((resolve) => server.close(() => resolve())); + } + } + + private async handle(request: http.IncomingMessage, response: http.ServerResponse): Promise { + const chunks: Buffer[] = []; + for await (const chunk of request) { + chunks.push(chunk as Buffer); + } + const rawBody = Buffer.concat(chunks).toString('utf-8'); + let body: unknown = undefined; + if (rawBody !== '') { + try { + body = JSON.parse(rawBody); + } catch { + body = rawBody; + } + } + this.requests.push({ + method: request.method ?? '', + url: request.url ?? '', + headers: request.headers, + body, + }); + + const route = `${request.method} ${request.url}`; + switch (route) { + case 'POST /v1/agent': + return this.handleAgent(response); + case 'POST /v1/permission': + return this.handlePermission(response); + case 'POST /cancel': + return json(response, 200, { cancelled: true }); + case 'GET /ready': { + const ready = this.options.ready ?? { + status: 200, + body: { ready: true, inference: true, lens_ready: true, sandbox: true, v3: true }, + }; + return json(response, ready.status, ready.body); + } + case 'GET /version': + return json(response, 200, { + api_version: '1.0.0', + protocol_version: 1, + error_codes: ['unauthorized', 'invalid_input'], + }); + default: + return json(response, 404, { error: 'invalid_input', detail: `no route ${route}` }); + } + } + + private async handleAgent(response: http.ServerResponse): Promise { + if (this.options.agentError) { + return json(response, this.options.agentError.status, this.options.agentError.body); + } + response.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }); + response.write(': connected\n\n'); + + const events = this.options.agentEvents ?? []; + for (let index = 0; index < events.length; index++) { + if (this.options.frameDelayMs) { + await sleep(this.options.frameDelayMs); + } + response.write(`data: ${JSON.stringify(events[index])}\n\n`); + if (index === this.options.pauseAfterIndex) { + // Block until a permission decision lands, like the real + // agent loop pausing on a destructive tool call. + await new Promise((resolve) => { + this.resumeSignal = resolve; + }); + } + } + if (!this.options.omitDone) { + response.write('data: [DONE]\n\n'); + } + response.end(); + } + + private handlePermission(response: http.ServerResponse): void { + const status = this.options.permissionStatus ?? 200; + json(response, status, { delivered: status === 200 }); + if (this.resumeSignal) { + const resume = this.resumeSignal; + this.resumeSignal = undefined; + resume(); + } + } +} + +function json(response: http.ServerResponse, status: number, body: unknown): void { + response.writeHead(status, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify(body)); +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/extensions/vscode/test/sse.test.ts b/extensions/vscode/test/sse.test.ts new file mode 100644 index 00000000..a6b350dd --- /dev/null +++ b/extensions/vscode/test/sse.test.ts @@ -0,0 +1,117 @@ +// Unit tests for the pure SSE frame parser. Coverage per plan: comments, +// chunk splits mid-frame, [DONE], >1MB frames, malformed frames, CRLF. + +import { describe, expect, it } from 'vitest'; +import { parseSSEStream } from '../src/client/sse'; +import type { ChatEvent } from '../src/client/types'; + +const encoder = new TextEncoder(); + +/** Turn strings into an async byte stream, one chunk per string. */ +async function* chunks(...parts: string[]): AsyncGenerator { + for (const part of parts) { + yield encoder.encode(part); + } +} + +async function collect(stream: AsyncIterable): Promise { + const events: ChatEvent[] = []; + for await (const event of parseSSEStream(stream)) { + events.push(event); + } + return events; +} + +function frame(event: ChatEvent): string { + return `data: ${JSON.stringify(event)}\n\n`; +} + +describe('parseSSEStream', () => { + it('parses a simple stream and stops at [DONE]', async () => { + const events = await collect( + chunks( + ': connected\n\n', + frame({ type: 'text', data: { content: 'hello' } }), + frame({ type: 'done', data: { summary: '' } }), + 'data: [DONE]\n\n', + ), + ); + expect(events).toEqual([ + { type: 'text', data: { content: 'hello' } }, + { type: 'done', data: { summary: '' } }, + ]); + }); + + it('skips comments and blank lines', async () => { + const events = await collect( + chunks(': connected\n\n: heartbeat\n\n', frame({ type: 'text', data: { content: 'x' } }), 'data: [DONE]\n\n'), + ); + expect(events).toHaveLength(1); + }); + + it('reassembles frames split across arbitrary chunk boundaries', async () => { + const full = frame({ type: 'tool_call', data: { name: 'read_file', args: { path: 'a.py' }, turn: 1 } }); + // Split mid-"data:", mid-JSON, and mid-newline. + const events = await collect(chunks('da', 'ta: {"type":"tool_call","da', full.slice(full.indexOf('"da') + 3), 'data: [DONE]\n\n')); + expect(events).toEqual([{ type: 'tool_call', data: { name: 'read_file', args: { path: 'a.py' }, turn: 1 } }]); + }); + + it('handles a multibyte character split across chunks', async () => { + const bytes = encoder.encode(frame({ type: 'text', data: { content: 'héllo→' } }) + 'data: [DONE]\n\n'); + // Split inside the é (2-byte UTF-8 sequence). + const splitAt = 25; + async function* twoChunks(): AsyncGenerator { + yield bytes.slice(0, splitAt); + yield bytes.slice(splitAt); + } + const events = await collect(twoChunks()); + expect(events).toEqual([{ type: 'text', data: { content: 'héllo→' } }]); + }); + + it('parses a >1MB frame', async () => { + const big = 'x'.repeat(1_200_000); + const events = await collect( + chunks(frame({ type: 'tool_result', data: { tool: 'read_file', success: true, data: big } }), 'data: [DONE]\n\n'), + ); + expect(events).toHaveLength(1); + expect((events[0].data as { data: string }).data).toHaveLength(1_200_000); + }); + + it('skips malformed JSON frames without killing the stream', async () => { + const events = await collect( + chunks('data: {not json}\n\n', frame({ type: 'text', data: { content: 'ok' } }), 'data: [DONE]\n\n'), + ); + expect(events).toEqual([{ type: 'text', data: { content: 'ok' } }]); + }); + + it('skips frames with a missing or empty type', async () => { + const events = await collect( + chunks('data: {"data":{"content":"no type"}}\n\n', 'data: {"type":"","data":{}}\n\n', 'data: [DONE]\n\n'), + ); + expect(events).toEqual([]); + }); + + it('handles CRLF line endings', async () => { + const events = await collect( + chunks(': connected\r\n\r\ndata: {"type":"text","data":{"content":"crlf"}}\r\n\r\ndata: [DONE]\r\n\r\n'), + ); + expect(events).toEqual([{ type: 'text', data: { content: 'crlf' } }]); + }); + + it('yields events already received when the stream ends without [DONE]', async () => { + const events = await collect(chunks(frame({ type: 'text', data: { content: 'partial' } }))); + expect(events).toEqual([{ type: 'text', data: { content: 'partial' } }]); + }); + + it('flushes a final unterminated data line at end of stream', async () => { + const events = await collect(chunks('data: {"type":"text","data":{"content":"tail"}}')); + expect(events).toEqual([{ type: 'text', data: { content: 'tail' } }]); + }); + + it('ignores events after [DONE]', async () => { + const events = await collect( + chunks('data: [DONE]\n\n', frame({ type: 'text', data: { content: 'late' } })), + ); + expect(events).toEqual([]); + }); +}); diff --git a/extensions/vscode/tsconfig.json b/extensions/vscode/tsconfig.json index bb38c16b..32fda6d4 100644 --- a/extensions/vscode/tsconfig.json +++ b/extensions/vscode/tsconfig.json @@ -10,7 +10,10 @@ ], "sourceMap": true, "strict": true, - "noEmit": true + "noEmit": true, + // vitest's bundled .d.ts references DOM WebSocket types absent from + // lib ES2022 — skip type-checking declaration files in node_modules. + "skipLibCheck": true }, "include": [ "src", From a779c41a53847e633b323ca5bd8b8c325e350194 Mon Sep 17 00:00:00 2001 From: Anuj-72 Date: Sat, 18 Jul 2026 19:37:53 +0530 Subject: [PATCH 03/13] feat(vscode): add chat sidebar with streaming turns and session management Add a webview chat view backed by a vscode-free TurnManager that mirrors the TUI's protocol conventions: per-turn 24-char hex session ids, rolling history capped at 40 entries with assistant text re-wrapped in the {"type":"text"} envelope, session_allowed_tools re-sent on every turn, and cancel wired to stream abort plus best-effort POST /cancel. The webview is a dumb renderer: all state lives in the extension host, which replays the transcript into any re-created view. Rendering is textContent-only under a nonce'd CSP. The service token is read from SecretStorage (atlas.setToken command) with the plaintext setting as a dev override; 401s surface a "Set Token" action. permission_request is interim auto-deny (with a visible note) so turns never sit on the server-side permission timeout; the approve/deny UI lands in the next commit. Covered by 12 TurnManager unit tests (39 total). --- extensions/vscode/media/chat.css | 150 ++++++++++ extensions/vscode/media/chat.js | 164 +++++++++++ extensions/vscode/media/icon.svg | 7 + extensions/vscode/package.json | 18 ++ extensions/vscode/src/extension.ts | 51 +++- extensions/vscode/src/session/turnManager.ts | 122 ++++++++ extensions/vscode/src/ui/chatView.ts | 275 +++++++++++++++++++ extensions/vscode/test/turnManager.test.ts | 250 +++++++++++++++++ 8 files changed, 1025 insertions(+), 12 deletions(-) create mode 100644 extensions/vscode/media/chat.css create mode 100644 extensions/vscode/media/chat.js create mode 100644 extensions/vscode/media/icon.svg create mode 100644 extensions/vscode/src/session/turnManager.ts create mode 100644 extensions/vscode/src/ui/chatView.ts create mode 100644 extensions/vscode/test/turnManager.test.ts diff --git a/extensions/vscode/media/chat.css b/extensions/vscode/media/chat.css new file mode 100644 index 00000000..fe7a1d47 --- /dev/null +++ b/extensions/vscode/media/chat.css @@ -0,0 +1,150 @@ +/* ATLAS chat panel — theme-aware via VS Code CSS variables. */ + +html, +body { + height: 100%; + margin: 0; + padding: 0; + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); + color: var(--vscode-foreground); + background: var(--vscode-sideBar-background); +} + +body { + display: flex; + flex-direction: column; +} + +#messages { + flex: 1; + overflow-y: auto; + padding: 8px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.msg { + padding: 6px 10px; + border-radius: 6px; + white-space: pre-wrap; + word-break: break-word; + max-width: 95%; +} + +.msg.user { + align-self: flex-end; + background: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border, transparent); +} + +.msg.assistant { + align-self: flex-start; + background: var(--vscode-editor-inactiveSelectionBackground); +} + +.chip { + align-self: flex-start; + font-size: 0.9em; + padding: 3px 8px; + border-radius: 10px; + border: 1px solid var(--vscode-widget-border, var(--vscode-input-border, transparent)); + background: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + max-width: 95%; + overflow-wrap: anywhere; +} + +.chip.ok .chip-status { + color: var(--vscode-testing-iconPassed, #73c991); +} + +.chip.fail .chip-status { + color: var(--vscode-testing-iconFailed, #f14c4c); +} + +.chip-status { + margin-right: 5px; +} + +.chip-label { + font-family: var(--vscode-editor-font-family); +} + +.chip-elapsed { + margin-left: 6px; + opacity: 0.7; +} + +.chip-error { + margin-top: 3px; + color: var(--vscode-errorForeground); + white-space: pre-wrap; +} + +.note { + align-self: center; + font-style: italic; + opacity: 0.75; + font-size: 0.9em; + text-align: center; +} + +.error-card { + align-self: stretch; + padding: 6px 10px; + border-radius: 4px; + border: 1px solid var(--vscode-inputValidation-errorBorder, var(--vscode-errorForeground)); + background: var(--vscode-inputValidation-errorBackground, transparent); + color: var(--vscode-errorForeground); + white-space: pre-wrap; +} + +#composer { + display: flex; + flex-direction: column; + gap: 4px; + padding: 8px; + border-top: 1px solid var(--vscode-widget-border, var(--vscode-input-border, transparent)); +} + +#input { + resize: vertical; + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); + color: var(--vscode-input-foreground); + background: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border, transparent); + border-radius: 4px; + padding: 6px; +} + +#input:focus { + outline: 1px solid var(--vscode-focusBorder); +} + +#actions { + display: flex; + justify-content: flex-end; + gap: 6px; +} + +button { + font-family: var(--vscode-font-family); + color: var(--vscode-button-foreground); + background: var(--vscode-button-background); + border: none; + border-radius: 4px; + padding: 4px 12px; + cursor: pointer; +} + +button:hover { + background: var(--vscode-button-hoverBackground); +} + +#stop { + background: var(--vscode-button-secondaryBackground, var(--vscode-button-background)); + color: var(--vscode-button-secondaryForeground, var(--vscode-button-foreground)); +} diff --git a/extensions/vscode/media/chat.js b/extensions/vscode/media/chat.js new file mode 100644 index 00000000..4f56eebf --- /dev/null +++ b/extensions/vscode/media/chat.js @@ -0,0 +1,164 @@ +// Webview-side renderer for the ATLAS chat panel. Deliberately dumb: all +// state lives in the extension host (src/ui/chatView.ts); this script only +// renders the messages it is posted, in order. On load it sends {type: +// "ready"} and the host replays the full transcript. + +/* global acquireVsCodeApi */ + +(function () { + 'use strict'; + + const vscode = acquireVsCodeApi(); + + const messagesEl = document.getElementById('messages'); + const inputEl = document.getElementById('input'); + const sendEl = document.getElementById('send'); + const stopEl = document.getElementById('stop'); + + /** The assistant bubble currently receiving streamed text, if any. */ + let openAssistantEl = null; + /** Tool chips awaiting a tool_result, keyed by tool name (FIFO per name). */ + const pendingChips = new Map(); + + function scrollToBottom() { + messagesEl.scrollTop = messagesEl.scrollHeight; + } + + function appendBlock(className, text) { + const el = document.createElement('div'); + el.className = className; + if (text !== undefined) { + el.textContent = text; + } + messagesEl.appendChild(el); + scrollToBottom(); + return el; + } + + function closeAssistantBubble() { + openAssistantEl = null; + } + + function assistantBubble() { + if (!openAssistantEl) { + openAssistantEl = appendBlock('msg assistant', ''); + } + return openAssistantEl; + } + + function addToolChip(name, detail) { + const chip = document.createElement('div'); + chip.className = 'chip pending'; + const label = document.createElement('span'); + label.className = 'chip-label'; + label.textContent = detail ? name + ' ' + detail : name; + const status = document.createElement('span'); + status.className = 'chip-status'; + status.textContent = '⋯'; // ⋯ spinner placeholder + chip.appendChild(status); + chip.appendChild(label); + messagesEl.appendChild(chip); + scrollToBottom(); + + if (!pendingChips.has(name)) { + pendingChips.set(name, []); + } + pendingChips.get(name).push(chip); + } + + function resolveToolChip(tool, success, elapsed, error) { + const queue = pendingChips.get(tool); + const chip = queue && queue.length > 0 ? queue.shift() : null; + if (!chip) { + // Result without a rendered call (e.g. replay edge) — show standalone. + appendBlock('chip ' + (success ? 'ok' : 'fail'), (success ? '✓ ' : '✗ ') + tool + (error ? ': ' + error : '')); + return; + } + chip.classList.remove('pending'); + chip.classList.add(success ? 'ok' : 'fail'); + const status = chip.querySelector('.chip-status'); + status.textContent = success ? '✓' : '✗'; + if (elapsed) { + const time = document.createElement('span'); + time.className = 'chip-elapsed'; + time.textContent = elapsed; + chip.appendChild(time); + } + if (!success && error) { + const detail = document.createElement('div'); + detail.className = 'chip-error'; + detail.textContent = error; + chip.appendChild(detail); + } + } + + function setBusy(busy) { + sendEl.hidden = busy; + stopEl.hidden = !busy; + inputEl.disabled = busy; + } + + function resetAll() { + messagesEl.textContent = ''; + pendingChips.clear(); + openAssistantEl = null; + } + + window.addEventListener('message', (event) => { + const message = event.data; + switch (message.type) { + case 'userMessage': + closeAssistantBubble(); + appendBlock('msg user', message.text); + break; + case 'assistantDelta': + assistantBubble().textContent += message.text; + scrollToBottom(); + break; + case 'toolCall': + closeAssistantBubble(); + addToolChip(message.name, message.detail); + break; + case 'toolResult': + resolveToolChip(message.tool, message.success, message.elapsed, message.error); + break; + case 'note': + closeAssistantBubble(); + appendBlock('note', message.text); + break; + case 'turnDone': + closeAssistantBubble(); + break; + case 'turnError': + closeAssistantBubble(); + appendBlock('error-card', message.message); + break; + case 'busy': + setBusy(message.value); + break; + case 'reset': + resetAll(); + break; + } + }); + + function submit() { + const text = inputEl.value.trim(); + if (text === '') { + return; + } + inputEl.value = ''; + vscode.postMessage({ type: 'submit', text: text }); + } + + sendEl.addEventListener('click', submit); + stopEl.addEventListener('click', () => vscode.postMessage({ type: 'cancel' })); + inputEl.addEventListener('keydown', (event) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + submit(); + } + }); + + vscode.postMessage({ type: 'ready' }); +})(); diff --git a/extensions/vscode/media/icon.svg b/extensions/vscode/media/icon.svg new file mode 100644 index 00000000..0ebdbf78 --- /dev/null +++ b/extensions/vscode/media/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index e4dc5a20..d1f3d246 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -39,6 +39,24 @@ "title": "ATLAS: New Conversation" } ], + "viewsContainers": { + "activitybar": [ + { + "id": "atlas", + "title": "ATLAS", + "icon": "media/icon.svg" + } + ] + }, + "views": { + "atlas": [ + { + "type": "webview", + "id": "atlas.chatView", + "name": "Chat" + } + ] + }, "configuration": { "title": "ATLAS", "properties": { diff --git a/extensions/vscode/src/extension.ts b/extensions/vscode/src/extension.ts index 9743fb19..47651323 100644 --- a/extensions/vscode/src/extension.ts +++ b/extensions/vscode/src/extension.ts @@ -1,18 +1,45 @@ import * as vscode from 'vscode'; - -// Command IDs contributed in package.json. Real implementations land with the -// chat view (atlas.openChat / atlas.newConversation), turn manager -// (atlas.cancelTurn), and client auth (atlas.setToken) in upcoming commits. -const COMMANDS = ['atlas.openChat', 'atlas.cancelTurn', 'atlas.setToken', 'atlas.newConversation'] as const; +import { ChatViewProvider, TOKEN_SECRET_KEY } from './ui/chatView'; export function activate(context: vscode.ExtensionContext) { - for (const command of COMMANDS) { - context.subscriptions.push( - vscode.commands.registerCommand(command, () => { - void vscode.window.showInformationMessage(`ATLAS: '${command}' is not implemented yet (scaffold).`); - }), - ); - } + const chat = new ChatViewProvider(context.extensionUri, context.secrets); + + context.subscriptions.push( + vscode.window.registerWebviewViewProvider(ChatViewProvider.viewType, chat, { + webviewOptions: { retainContextWhenHidden: true }, + }), + + vscode.commands.registerCommand('atlas.openChat', () => { + void vscode.commands.executeCommand(`${ChatViewProvider.viewType}.focus`); + }), + + vscode.commands.registerCommand('atlas.cancelTurn', () => { + chat.cancelTurn(); + }), + + vscode.commands.registerCommand('atlas.newConversation', () => { + chat.newConversation(); + }), + + vscode.commands.registerCommand('atlas.setToken', async () => { + const token = await vscode.window.showInputBox({ + title: 'ATLAS: Set Service Token', + prompt: 'Bearer token for the ATLAS proxy (leave empty to clear).', + password: true, + ignoreFocusOut: true, + }); + if (token === undefined) { + return; // dismissed + } + if (token === '') { + await context.secrets.delete(TOKEN_SECRET_KEY); + void vscode.window.showInformationMessage('ATLAS: service token cleared.'); + } else { + await context.secrets.store(TOKEN_SECRET_KEY, token); + void vscode.window.showInformationMessage('ATLAS: service token saved to Secret Storage.'); + } + }), + ); } export function deactivate() {} diff --git a/extensions/vscode/src/session/turnManager.ts b/extensions/vscode/src/session/turnManager.ts new file mode 100644 index 00000000..be498228 --- /dev/null +++ b/extensions/vscode/src/session/turnManager.ts @@ -0,0 +1,122 @@ +// Turn/session state for the chat: session_id minting, rolling history, +// session-approved tools, and cancellation. Conventions mirror the TUI +// reference client (tui/chat.go) — see the plan and docs/API.md. +// +// Deliberately vscode-free so it is unit-testable under plain vitest. + +import { randomBytes } from 'node:crypto'; +import type { AgentRequest, ChatEvent, HistoryMessage, PermissionMode } from '../client/types'; + +/** The proxy caps replayed history at the most recent 40 entries; we cap + * client-side too so the request body never grows unbounded (tui/chat.go). */ +export const HISTORY_LIMIT = 40; + +/** Minimal client surface TurnManager needs — satisfied by AtlasClient, + * kept structural so tests can pass a fake. */ +export interface TurnClient { + sendAgentTurn(request: AgentRequest, signal?: AbortSignal): AsyncGenerator; + cancelTurn(sessionId: string): Promise; +} + +/** Client-minted per-turn session id: 12 random bytes, hex-encoded + * (24 chars) — same recipe as tui/chat.go. */ +export function mintSessionId(): string { + return randomBytes(12).toString('hex'); +} + +export class TurnManager { + private history: HistoryMessage[] = []; + /** Tools approved with scope "session"; re-sent on every later turn. */ + readonly sessionAllowedTools = new Set(); + + private abort: AbortController | undefined; + private activeClient: TurnClient | undefined; + private activeSessionId = ''; + + /** True while a turn's event stream is being consumed. */ + get busy(): boolean { + return this.abort !== undefined; + } + + /** session_id of the in-flight (or most recent) turn — needed to route + * POST /v1/permission decisions. */ + get sessionId(): string { + return this.activeSessionId; + } + + /** Run one agent turn: mint a session id, send message + rolling history, + * and yield the raw event stream. History is finalized in `finally` so it + * updates on done, error, and cancel alike (partial assistant text kept). */ + async *runTurn(client: TurnClient, message: string, mode: PermissionMode): AsyncGenerator { + if (this.abort) { + throw new Error('a turn is already in progress'); + } + const abort = new AbortController(); + this.abort = abort; + this.activeClient = client; + this.activeSessionId = mintSessionId(); + + const request: AgentRequest = { + message, + // The proxy overrides working_dir with ATLAS_WORKSPACE_DIR; "." is + // the conventional client value (tui/chat.go). + working_dir: '.', + mode, + session_id: this.activeSessionId, + history: [...this.history], + }; + if (this.sessionAllowedTools.size > 0) { + request.session_allowed_tools = [...this.sessionAllowedTools]; + } + + let assistantText = ''; + try { + for await (const event of client.sendAgentTurn(request, abort.signal)) { + if (event.type === 'text') { + const data = event.data as { content?: unknown }; + if (typeof data?.content === 'string') { + assistantText += data.content; + } + } + yield event; + } + } finally { + this.history.push({ role: 'user', content: message }); + if (assistantText !== '') { + // TUI convention: assistant history entries are re-wrapped in + // the {"type":"text","content":...} envelope (tui/chat.go). + this.history.push({ + role: 'assistant', + content: JSON.stringify({ type: 'text', content: assistantText }), + }); + } + if (this.history.length > HISTORY_LIMIT) { + this.history = this.history.slice(-HISTORY_LIMIT); + } + this.abort = undefined; + this.activeClient = undefined; + } + } + + /** Cancel the in-flight turn: abort the local stream (primary) and fire a + * best-effort POST /cancel (defense-in-depth — TUI convention). No-op when + * idle. */ + cancel(): void { + if (!this.abort) { + return; + } + const client = this.activeClient; + const sessionId = this.activeSessionId; + this.abort.abort(); + if (client && sessionId) { + void client.cancelTurn(sessionId); + } + } + + /** Start a fresh conversation: drop history and session-approved tools. + * Callers should cancel() first if a turn is in flight. */ + reset(): void { + this.history = []; + this.sessionAllowedTools.clear(); + } +} diff --git a/extensions/vscode/src/ui/chatView.ts b/extensions/vscode/src/ui/chatView.ts new file mode 100644 index 00000000..1299ca66 --- /dev/null +++ b/extensions/vscode/src/ui/chatView.ts @@ -0,0 +1,275 @@ +// Chat sidebar: a WebviewViewProvider that owns all turn state and treats +// the webview as a dumb renderer. Every message posted to the webview is +// also appended to a transcript so a re-created webview (sidebar closed and +// reopened, window reload of the view) can be replayed from scratch. + +import * as vscode from 'vscode'; +import { AtlasApiError, AtlasClient } from '../client/atlasClient'; +import type { + ErrorEventData, + PermissionMode, + PermissionRequestEventData, + TextEventData, + ToolCallEventData, + ToolResultEventData, +} from '../client/types'; +import { TurnManager } from '../session/turnManager'; + +/** SecretStorage key for the service token ('ATLAS: Set Service Token'). */ +export const TOKEN_SECRET_KEY = 'atlas.serviceToken'; + +/** Messages the extension posts INTO the webview (media/chat.js). */ +type OutboundMessage = + | { type: 'userMessage'; text: string } + | { type: 'assistantDelta'; text: string } + | { type: 'toolCall'; name: string; detail: string } + | { type: 'toolResult'; tool: string; success: boolean; elapsed?: string; error?: string } + | { type: 'note'; text: string } + | { type: 'turnDone' } + | { type: 'turnError'; message: string } + | { type: 'reset' } + | { type: 'busy'; value: boolean }; + +/** Messages the webview posts back to the extension. */ +type InboundMessage = { type: 'ready' } | { type: 'submit'; text: string } | { type: 'cancel' }; + +/** Condense tool args to a single short line for the tool chip. */ +function condenseArgs(args: unknown): string { + if (args === null || args === undefined) { + return ''; + } + let text: string; + try { + text = typeof args === 'string' ? args : JSON.stringify(args); + } catch { + return ''; + } + return text.length > 120 ? `${text.slice(0, 117)}...` : text; +} + +export class ChatViewProvider implements vscode.WebviewViewProvider { + static readonly viewType = 'atlas.chatView'; + + private view: vscode.WebviewView | undefined; + /** Replayed into any freshly created webview; `busy` is transient and + * excluded. */ + private transcript: OutboundMessage[] = []; + private readonly turns = new TurnManager(); + private readonly output: vscode.OutputChannel; + + constructor( + private readonly extensionUri: vscode.Uri, + private readonly secrets: vscode.SecretStorage, + ) { + this.output = vscode.window.createOutputChannel('ATLAS'); + } + + resolveWebviewView(view: vscode.WebviewView): void { + this.view = view; + view.webview.options = { + enableScripts: true, + localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, 'media')], + }; + view.webview.html = this.renderHtml(view.webview); + view.webview.onDidReceiveMessage((message: InboundMessage) => { + switch (message.type) { + case 'ready': + this.replay(); + break; + case 'submit': + void this.runTurn(message.text); + break; + case 'cancel': + this.cancelTurn(); + break; + } + }); + } + + cancelTurn(): void { + this.turns.cancel(); + } + + newConversation(): void { + this.turns.cancel(); + this.turns.reset(); + this.transcript = []; + this.postTransient({ type: 'reset' }); + } + + private async runTurn(text: string): Promise { + const message = text.trim(); + if (message === '') { + return; + } + if (this.turns.busy) { + this.post({ type: 'note', text: 'A turn is already in progress — cancel it first.' }); + return; + } + + const config = vscode.workspace.getConfiguration('atlas'); + const baseUrl = config.get('proxyUrl', 'http://localhost:8090'); + const mode = config.get('permissionMode', 'default'); + // Plaintext setting is a dev override; SecretStorage is the real home. + const token = config.get('serviceToken', '') || (await this.secrets.get(TOKEN_SECRET_KEY)) || ''; + const client = new AtlasClient({ baseUrl, token }); + + this.post({ type: 'userMessage', text: message }); + this.postTransient({ type: 'busy', value: true }); + try { + for await (const event of this.turns.runTurn(client, message, mode)) { + this.dispatch(event.type, event.data, client); + } + this.post({ type: 'turnDone' }); + } catch (error) { + this.handleTurnFailure(error); + } finally { + this.postTransient({ type: 'busy', value: false }); + } + } + + private dispatch(type: string, data: unknown, client: AtlasClient): void { + switch (type) { + case 'text': { + const payload = data as TextEventData; + if (typeof payload?.content === 'string') { + this.post({ type: 'assistantDelta', text: payload.content }); + } + break; + } + case 'tool_call': { + const payload = data as ToolCallEventData; + this.post({ type: 'toolCall', name: payload.name, detail: condenseArgs(payload.args) }); + break; + } + case 'tool_result': { + const payload = data as ToolResultEventData; + this.post({ + type: 'toolResult', + tool: payload.tool, + success: payload.success, + elapsed: payload.elapsed, + error: payload.error, + }); + break; + } + case 'permission_request': { + // Interim commit-3 behavior: deny immediately and say so, so + // the turn never sits on the 600s server-side timeout. The + // real approve/deny flow lands in the next commit. + const payload = data as PermissionRequestEventData; + this.post({ + type: 'note', + text: `Permission request for '${payload.tool_name}' auto-denied — the approval UI lands in an upcoming commit.`, + }); + void client + .postPermissionDecision({ + session_id: this.turns.sessionId, + tool_call_id: payload.tool_call_id, + decision: 'deny', + scope: 'once', + }) + .catch((error: unknown) => this.log('permission deny failed', error)); + break; + } + case 'error': { + const payload = data as ErrorEventData; + this.post({ type: 'turnError', message: payload.error || 'unknown stream error' }); + break; + } + case 'done': + // Bubble finalization happens on generator completion. + break; + default: + // Forward compatibility: unknown event types are logged, never fatal. + this.log(`unhandled event '${type}'`, data); + } + } + + private handleTurnFailure(error: unknown): void { + if (error instanceof Error && error.name === 'AbortError') { + this.post({ type: 'note', text: 'Turn cancelled.' }); + return; + } + if (error instanceof AtlasApiError) { + this.post({ type: 'turnError', message: `${error.code || 'request failed'}: ${error.detail || error.message}` }); + if (error.code === 'unauthorized') { + void vscode.window + .showErrorMessage('ATLAS proxy rejected the request (unauthorized).', 'Set Token') + .then((choice) => { + if (choice === 'Set Token') { + void vscode.commands.executeCommand('atlas.setToken'); + } + }); + } + return; + } + const message = error instanceof Error ? error.message : String(error); + this.post({ type: 'turnError', message: `Could not reach the ATLAS proxy: ${message}` }); + } + + /** Post to the webview and record for replay. */ + private post(message: OutboundMessage): void { + this.transcript.push(message); + void this.view?.webview.postMessage(message); + } + + /** Post without recording (busy toggles, reset marker). */ + private postTransient(message: OutboundMessage): void { + void this.view?.webview.postMessage(message); + } + + private replay(): void { + for (const message of this.transcript) { + void this.view?.webview.postMessage(message); + } + this.postTransient({ type: 'busy', value: this.turns.busy }); + } + + private log(context: string, detail: unknown): void { + let rendered: string; + try { + rendered = JSON.stringify(detail); + } catch { + rendered = String(detail); + } + this.output.appendLine(`${context}: ${rendered}`); + } + + private renderHtml(webview: vscode.Webview): string { + const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'media', 'chat.js')); + const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, 'media', 'chat.css')); + const nonce = getNonce(); + return ` + + + + + + + ATLAS Chat + + +
+
+ +
+ + +
+
+ + +`; + } +} + +function getNonce(): string { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} diff --git a/extensions/vscode/test/turnManager.test.ts b/extensions/vscode/test/turnManager.test.ts new file mode 100644 index 00000000..d1a70092 --- /dev/null +++ b/extensions/vscode/test/turnManager.test.ts @@ -0,0 +1,250 @@ +// Unit tests for TurnManager: session_id minting, request shape, rolling +// history (40-cap, assistant envelope re-wrap), session_allowed_tools +// re-send, busy guard, and cancel. + +import { describe, expect, it } from 'vitest'; +import type { AgentRequest, ChatEvent } from '../src/client/types'; +import { HISTORY_LIMIT, mintSessionId, TurnManager, type TurnClient } from '../src/session/turnManager'; + +/** Fake client that records requests and replays canned events. */ +function fakeClient(events: ChatEvent[] = []): TurnClient & { requests: AgentRequest[]; cancelled: string[] } { + return { + requests: [], + cancelled: [], + async *sendAgentTurn(request: AgentRequest): AsyncGenerator { + this.requests.push(request); + for (const event of events) { + yield event; + } + }, + async cancelTurn(sessionId: string): Promise { + this.cancelled.push(sessionId); + return true; + }, + }; +} + +async function drain(turn: AsyncGenerator): Promise { + const out: ChatEvent[] = []; + for await (const event of turn) { + out.push(event); + } + return out; +} + +const text = (content: string): ChatEvent => ({ type: 'text', data: { content } }); +const done: ChatEvent = { type: 'done', data: { summary: '' } }; + +/** Rejects with an AbortError when the signal aborts — including when it + * already aborted before this was awaited (listener-only fakes hang there). */ +function abortableHang(signal?: AbortSignal): Promise { + return new Promise((_, reject) => { + const fail = () => { + const error = new Error('aborted'); + error.name = 'AbortError'; + reject(error); + }; + if (signal?.aborted) { + fail(); + return; + } + signal?.addEventListener('abort', fail); + }); +} + +describe('mintSessionId', () => { + it('mints 24-char hex ids, unique per call', () => { + const a = mintSessionId(); + const b = mintSessionId(); + expect(a).toMatch(/^[0-9a-f]{24}$/); + expect(b).toMatch(/^[0-9a-f]{24}$/); + expect(a).not.toBe(b); + }); +}); + +describe('TurnManager', () => { + it('sends the conventional request shape and yields all events', async () => { + const client = fakeClient([text('hi'), done]); + const turns = new TurnManager(); + + const events = await drain(turns.runTurn(client, 'hello', 'default')); + + expect(events).toHaveLength(2); + const request = client.requests[0]; + expect(request.message).toBe('hello'); + expect(request.working_dir).toBe('.'); + expect(request.mode).toBe('default'); + expect(request.session_id).toMatch(/^[0-9a-f]{24}$/); + expect(request.history).toEqual([]); + expect(request.session_allowed_tools).toBeUndefined(); + expect(turns.sessionId).toBe(request.session_id); + }); + + it('mints a fresh session_id per turn', async () => { + const client = fakeClient([done]); + const turns = new TurnManager(); + await drain(turns.runTurn(client, 'one', 'default')); + await drain(turns.runTurn(client, 'two', 'default')); + expect(client.requests[0].session_id).not.toBe(client.requests[1].session_id); + }); + + it('replays history with assistant text re-wrapped in the {"type":"text"} envelope', async () => { + const client = fakeClient([text('Hello '), text('world'), done]); + const turns = new TurnManager(); + + await drain(turns.runTurn(client, 'greet me', 'default')); + await drain(turns.runTurn(client, 'again', 'default')); + + expect(client.requests[1].history).toEqual([ + { role: 'user', content: 'greet me' }, + { role: 'assistant', content: JSON.stringify({ type: 'text', content: 'Hello world' }) }, + ]); + }); + + it('records no assistant entry for a text-free turn', async () => { + const client = fakeClient([{ type: 'error', data: { error: 'boom' } }]); + const turns = new TurnManager(); + + await drain(turns.runTurn(client, 'do it', 'default')); + await drain(turns.runTurn(client, 'retry', 'default')); + + // History sent on turn N covers turns < N only; turn 1 produced no text. + expect(client.requests[1].history).toEqual([{ role: 'user', content: 'do it' }]); + }); + + it(`caps history at the most recent ${HISTORY_LIMIT} entries`, async () => { + const client = fakeClient([text('r'), done]); + const turns = new TurnManager(); + + // 25 turns × 2 entries (user + assistant) = 50 recorded > 40 cap. + for (let i = 0; i < 25; i++) { + await drain(turns.runTurn(client, `msg ${i}`, 'default')); + } + await drain(turns.runTurn(client, 'final', 'default')); + + const history = client.requests.at(-1)!.history!; + expect(history).toHaveLength(HISTORY_LIMIT); + // Oldest surviving entries come from the tail of the run, not turn 0. + expect(history[0].content).not.toContain('msg 0'); + // Final pair is turn 24's user message + its re-wrapped assistant reply. + expect(history.at(-2)).toEqual({ role: 'user', content: 'msg 24' }); + expect(history.at(-1)).toEqual({ + role: 'assistant', + content: JSON.stringify({ type: 'text', content: 'r' }), + }); + }); + + it('re-sends sessionAllowedTools on every turn once populated', async () => { + const client = fakeClient([done]); + const turns = new TurnManager(); + turns.sessionAllowedTools.add('edit_file'); + turns.sessionAllowedTools.add('write_file'); + + await drain(turns.runTurn(client, 'one', 'default')); + await drain(turns.runTurn(client, 'two', 'default')); + + expect(client.requests[0].session_allowed_tools).toEqual(['edit_file', 'write_file']); + expect(client.requests[1].session_allowed_tools).toEqual(['edit_file', 'write_file']); + }); + + it('rejects a second concurrent turn', async () => { + const client = fakeClient(); + let release!: () => void; + const gate = new Promise((resolve) => (release = resolve)); + client.sendAgentTurn = async function* (request: AgentRequest) { + this.requests.push(request); + await gate; + yield done; + }; + const turns = new TurnManager(); + + const first = drain(turns.runTurn(client, 'slow', 'default')); + // Give the first generator a tick to start the stream. + await new Promise((resolve) => setImmediate(resolve)); + expect(turns.busy).toBe(true); + await expect(drain(turns.runTurn(client, 'second', 'default'))).rejects.toThrow(/already in progress/); + + release(); + await first; + expect(turns.busy).toBe(false); + }); + + it('cancel aborts the stream and fires best-effort POST /cancel', async () => { + const client = fakeClient(); + client.sendAgentTurn = async function* (request: AgentRequest, signal?: AbortSignal) { + this.requests.push(request); + yield text('partial'); + await abortableHang(signal); + }; + const turns = new TurnManager(); + + const consumed = (async () => { + const seen: ChatEvent[] = []; + try { + for await (const event of turns.runTurn(client, 'go', 'default')) { + seen.push(event); + turns.cancel(); + } + throw new Error('stream should have aborted'); + } catch (error) { + expect((error as Error).name).toBe('AbortError'); + } + return seen; + })(); + + const seen = await consumed; + expect(seen).toHaveLength(1); + expect(client.cancelled).toEqual([client.requests[0].session_id]); + expect(turns.busy).toBe(false); + }); + + it('keeps partial assistant text in history after a cancelled turn', async () => { + const client = fakeClient(); + client.sendAgentTurn = async function* (request: AgentRequest, signal?: AbortSignal) { + this.requests.push(request); + yield text('half an ans'); + await abortableHang(signal); + }; + const turns = new TurnManager(); + + try { + for await (const _event of turns.runTurn(client, 'go', 'default')) { + turns.cancel(); + } + } catch { + // expected abort + } + + // Next turn replays the partial text. + client.sendAgentTurn = async function* (request: AgentRequest) { + this.requests.push(request); + yield done; + }; + await drain(turns.runTurn(client, 'next', 'default')); + expect(client.requests[1].history).toEqual([ + { role: 'user', content: 'go' }, + { role: 'assistant', content: JSON.stringify({ type: 'text', content: 'half an ans' }) }, + ]); + }); + + it('cancel when idle is a no-op', () => { + const client = fakeClient(); + const turns = new TurnManager(); + turns.cancel(); + expect(client.cancelled).toEqual([]); + }); + + it('reset clears history and session-approved tools', async () => { + const client = fakeClient([text('a'), done]); + const turns = new TurnManager(); + turns.sessionAllowedTools.add('edit_file'); + await drain(turns.runTurn(client, 'one', 'default')); + + turns.reset(); + await drain(turns.runTurn(client, 'two', 'default')); + + const request = client.requests[1]; + expect(request.history).toEqual([]); + expect(request.session_allowed_tools).toBeUndefined(); + }); +}); From 8acb96fcc4c38460e5c4f20e1b4009e5943ba3cf Mon Sep 17 00:00:00 2001 From: Anuj-72 Date: Sun, 19 Jul 2026 00:04:41 +0530 Subject: [PATCH 04/13] feat(vscode): add interactive permission flow with webview cards and native prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/session/permissionFlow.ts: vscode-free permission state machine — first-answer-wins settling, session allowlist auto-allow (scope "once", matching TUI), remote permission_denied dismissal by tool name, turn-end cleanup, advisory POSTs with swallowed errors - src/ui/chatView.ts: wire PermissionFlow into event dispatch (replaces interim auto-deny), dual-surface prompts (inline webview card + native notification), transcript-replay-safe prompt/resolution messages - media/chat.js + chat.css: permission cards with Allow Once / Allow for Session / Deny actions and resolved-state rendering - test/permissionFlow.test.ts: 11 tests covering auto-allow, scopes, race arbitration, remote denial, turn end, POST failure paths --- extensions/vscode/media/chat.css | 50 ++++ extensions/vscode/media/chat.js | 77 ++++++ .../vscode/src/session/permissionFlow.ts | 164 ++++++++++++ extensions/vscode/src/ui/chatView.ts | 108 ++++++-- extensions/vscode/test/permissionFlow.test.ts | 235 ++++++++++++++++++ 5 files changed, 617 insertions(+), 17 deletions(-) create mode 100644 extensions/vscode/src/session/permissionFlow.ts create mode 100644 extensions/vscode/test/permissionFlow.test.ts diff --git a/extensions/vscode/media/chat.css b/extensions/vscode/media/chat.css index fe7a1d47..3bd0bc8c 100644 --- a/extensions/vscode/media/chat.css +++ b/extensions/vscode/media/chat.css @@ -101,6 +101,56 @@ body { white-space: pre-wrap; } +.permission-card { + align-self: stretch; + padding: 8px 10px; + border-radius: 4px; + border: 1px solid var(--vscode-inputValidation-warningBorder, var(--vscode-editorWarning-foreground, #cca700)); + background: var(--vscode-inputValidation-warningBackground, transparent); + display: flex; + flex-direction: column; + gap: 6px; +} + +.permission-card.resolved { + opacity: 0.75; + border-color: var(--vscode-widget-border, var(--vscode-input-border, transparent)); + background: transparent; +} + +.permission-title { + font-weight: 600; +} + +.permission-message { + white-space: pre-wrap; + word-break: break-word; +} + +.permission-args { + font-family: var(--vscode-editor-font-family); + font-size: 0.9em; + opacity: 0.8; + overflow-wrap: anywhere; +} + +.permission-actions { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.permission-actions .deny { + background: var(--vscode-button-secondaryBackground, var(--vscode-button-background)); + color: var(--vscode-button-secondaryForeground, var(--vscode-button-foreground)); +} + +.permission-outcome { + font-style: italic; + font-size: 0.9em; + opacity: 0.85; +} + #composer { display: flex; flex-direction: column; diff --git a/extensions/vscode/media/chat.js b/extensions/vscode/media/chat.js index 4f56eebf..90455593 100644 --- a/extensions/vscode/media/chat.js +++ b/extensions/vscode/media/chat.js @@ -19,6 +19,8 @@ let openAssistantEl = null; /** Tool chips awaiting a tool_result, keyed by tool name (FIFO per name). */ const pendingChips = new Map(); + /** Open permission cards keyed by prompt id. */ + const permissionCards = new Map(); function scrollToBottom() { messagesEl.scrollTop = messagesEl.scrollHeight; @@ -92,6 +94,73 @@ } } + function addPermissionCard(id, tool, detail, message) { + const card = document.createElement('div'); + card.className = 'permission-card'; + + const title = document.createElement('div'); + title.className = 'permission-title'; + title.textContent = 'Permission: ' + tool; + card.appendChild(title); + + if (message) { + const body = document.createElement('div'); + body.className = 'permission-message'; + body.textContent = message; + card.appendChild(body); + } + if (detail) { + const args = document.createElement('div'); + args.className = 'permission-args'; + args.textContent = detail; + card.appendChild(args); + } + + const actions = document.createElement('div'); + actions.className = 'permission-actions'; + const buttons = [ + ['Allow Once', 'allow-once'], + ['Allow for Session', 'allow-session'], + ['Deny', 'deny'], + ]; + for (const pair of buttons) { + const button = document.createElement('button'); + button.type = 'button'; + button.textContent = pair[0]; + if (pair[1] === 'deny') { + button.className = 'deny'; + } + button.addEventListener('click', () => { + vscode.postMessage({ type: 'permissionAnswer', id: id, choice: pair[1] }); + }); + actions.appendChild(button); + } + card.appendChild(actions); + + messagesEl.appendChild(card); + permissionCards.set(id, card); + scrollToBottom(); + } + + function resolvePermissionCard(id, outcome) { + const card = permissionCards.get(id); + permissionCards.delete(id); + if (!card) { + // Replay path: the prompt message was recorded before its resolution — + // both replay in order, so a missing card only means a pruned DOM. + return; + } + const actions = card.querySelector('.permission-actions'); + if (actions) { + actions.remove(); + } + const result = document.createElement('div'); + result.className = 'permission-outcome'; + result.textContent = outcome; + card.classList.add('resolved'); + card.appendChild(result); + } + function setBusy(busy) { sendEl.hidden = busy; stopEl.hidden = !busy; @@ -101,6 +170,7 @@ function resetAll() { messagesEl.textContent = ''; pendingChips.clear(); + permissionCards.clear(); openAssistantEl = null; } @@ -126,6 +196,13 @@ closeAssistantBubble(); appendBlock('note', message.text); break; + case 'permissionPrompt': + closeAssistantBubble(); + addPermissionCard(message.id, message.tool, message.detail, message.message); + break; + case 'permissionResolved': + resolvePermissionCard(message.id, message.outcome); + break; case 'turnDone': closeAssistantBubble(); break; diff --git a/extensions/vscode/src/session/permissionFlow.ts b/extensions/vscode/src/session/permissionFlow.ts new file mode 100644 index 00000000..d33caf0e --- /dev/null +++ b/extensions/vscode/src/session/permissionFlow.ts @@ -0,0 +1,164 @@ +// Permission flow for mid-turn permission_request events. Pure logic, no +// vscode imports — the UI (native notification + webview card) is injected +// via PermissionUi so the flow runs under plain vitest. +// +// Conventions mirrored from tui/model.go: +// - A tool already in sessionAllowedTools is auto-answered allow/"once" +// (NOT "session" — the allowlist itself is re-sent on the next turn's +// request; the proxy does not persist it), fire-and-forget, no prompt. +// - "Allow for session" adds the tool to the allowlist AND answers with +// scope "session" so the in-flight turn honors it too. +// - A proxy-side permission_denied (timeout, cancel) can arrive while the +// prompt is still up — the pending prompt is dismissed by tool name. +// - Decision POSTs are advisory: the proxy has its own deny fail-safe, so +// post failures are reported to the UI for logging, never rethrown. + +import type { PermissionDecisionRequest, PermissionRequestEventData } from '../client/types'; + +/** Minimal poster surface — satisfied by AtlasClient. */ +export interface PermissionPoster { + postPermissionDecision(decision: PermissionDecisionRequest): Promise; +} + +/** What the user picked on a prompt. */ +export type PermissionChoice = 'allow-once' | 'allow-session' | 'deny'; + +/** Why a pending prompt went away. */ +export type DismissReason = 'answered' | 'denied-remote' | 'turn-ended'; + +/** UI surface driven by the flow. All callbacks are synchronous fire-and- + * forget from the flow's point of view. */ +export interface PermissionUi { + /** Show the prompt (notification and/or inline card). */ + onPrompt(pending: PendingPermission): void; + /** Remove/resolve the prompt UI. When reason is 'answered', + * `pending.choice` carries what the user picked. */ + onDismiss(pending: PendingPermission, reason: DismissReason): void; + /** A session-allowlisted tool was auto-answered without a prompt. */ + onAutoAllow(toolName: string): void; + /** A decision POST failed (non-404). Advisory — log and move on. */ + onPostError(toolName: string, error: unknown): void; +} + +/** One live permission prompt. Multiple UI surfaces may call settle(); + * the first answer wins and later calls are no-ops. */ +export class PendingPermission { + private settled = false; + private settledChoice: PermissionChoice | undefined; + + constructor( + /** Stable id for routing webview button clicks back to this prompt. */ + readonly id: number, + readonly sessionId: string, + readonly request: PermissionRequestEventData, + private readonly onSettle: (pending: PendingPermission, choice: PermissionChoice) => void, + ) {} + + get isSettled(): boolean { + return this.settled; + } + + /** The winning choice, once settled by a user answer. */ + get choice(): PermissionChoice | undefined { + return this.settledChoice; + } + + /** Record the user's answer. Returns true when this call decided the + * request, false when another surface (or a remote deny) beat it. */ + settle(choice: PermissionChoice): boolean { + if (this.settled) { + return false; + } + this.settled = true; + this.settledChoice = choice; + this.onSettle(this, choice); + return true; + } + + /** Mark settled without a user choice (remote deny / turn end) so a + * late button click becomes a no-op. */ + dismiss(): void { + this.settled = true; + } +} + +export class PermissionFlow { + private pending: PendingPermission[] = []; + private nextId = 1; + + constructor( + /** Shared with TurnManager — additions here are re-sent as + * session_allowed_tools on every later turn. */ + private readonly sessionAllowedTools: Set, + private readonly ui: PermissionUi, + ) {} + + /** Handle a permission_request event from the stream. */ + handleRequest(poster: PermissionPoster, sessionId: string, request: PermissionRequestEventData): void { + if (this.sessionAllowedTools.has(request.tool_name)) { + this.post(poster, sessionId, request, 'allow', 'once'); + this.ui.onAutoAllow(request.tool_name); + return; + } + const pending = new PendingPermission(this.nextId++, sessionId, request, (settled, choice) => { + this.remove(settled); + if (choice === 'allow-session') { + this.sessionAllowedTools.add(request.tool_name); + } + this.post(poster, sessionId, request, choice === 'deny' ? 'deny' : 'allow', choice === 'allow-session' ? 'session' : 'once'); + this.ui.onDismiss(settled, 'answered'); + }); + this.pending.push(pending); + this.ui.onPrompt(pending); + } + + /** Handle a permission_denied event: the proxy resolved the request on + * its side (timeout or cancel) — dismiss the oldest matching prompt. */ + handleDenied(toolName: string): void { + const pending = this.pending.find((p) => p.request.tool_name === toolName); + if (!pending) { + return; + } + pending.dismiss(); + this.remove(pending); + this.ui.onDismiss(pending, 'denied-remote'); + } + + /** The turn is over (done, error, or cancel) — nothing left to answer. */ + endTurn(): void { + const open = this.pending; + this.pending = []; + for (const pending of open) { + pending.dismiss(); + this.ui.onDismiss(pending, 'turn-ended'); + } + } + + /** Route a webview answer by prompt id. Unknown/settled ids are no-ops + * (stale card after a reload, or another surface answered first). */ + settleById(id: number, choice: PermissionChoice): boolean { + const pending = this.pending.find((p) => p.id === id); + return pending ? pending.settle(choice) : false; + } + + private remove(pending: PendingPermission): void { + this.pending = this.pending.filter((p) => p !== pending); + } + + private post( + poster: PermissionPoster, + sessionId: string, + request: PermissionRequestEventData, + decision: 'allow' | 'deny', + scope: 'once' | 'session', + ): void { + void poster + .postPermissionDecision({ + session_id: sessionId, + tool_call_id: request.tool_call_id, + decision, + scope, + }) + .catch((error: unknown) => this.ui.onPostError(request.tool_name, error)); + } +} diff --git a/extensions/vscode/src/ui/chatView.ts b/extensions/vscode/src/ui/chatView.ts index 1299ca66..3bbc254b 100644 --- a/extensions/vscode/src/ui/chatView.ts +++ b/extensions/vscode/src/ui/chatView.ts @@ -7,12 +7,19 @@ import * as vscode from 'vscode'; import { AtlasApiError, AtlasClient } from '../client/atlasClient'; import type { ErrorEventData, + PermissionDeniedEventData, PermissionMode, PermissionRequestEventData, TextEventData, ToolCallEventData, ToolResultEventData, } from '../client/types'; +import { + PendingPermission, + PermissionFlow, + type DismissReason, + type PermissionChoice, +} from '../session/permissionFlow'; import { TurnManager } from '../session/turnManager'; /** SecretStorage key for the service token ('ATLAS: Set Service Token'). */ @@ -25,13 +32,19 @@ type OutboundMessage = | { type: 'toolCall'; name: string; detail: string } | { type: 'toolResult'; tool: string; success: boolean; elapsed?: string; error?: string } | { type: 'note'; text: string } + | { type: 'permissionPrompt'; id: number; tool: string; detail: string; message: string } + | { type: 'permissionResolved'; id: number; outcome: string } | { type: 'turnDone' } | { type: 'turnError'; message: string } | { type: 'reset' } | { type: 'busy'; value: boolean }; /** Messages the webview posts back to the extension. */ -type InboundMessage = { type: 'ready' } | { type: 'submit'; text: string } | { type: 'cancel' }; +type InboundMessage = + | { type: 'ready' } + | { type: 'submit'; text: string } + | { type: 'cancel' } + | { type: 'permissionAnswer'; id: number; choice: PermissionChoice }; /** Condense tool args to a single short line for the tool chip. */ function condenseArgs(args: unknown): string { @@ -55,6 +68,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { * excluded. */ private transcript: OutboundMessage[] = []; private readonly turns = new TurnManager(); + private readonly permissions: PermissionFlow; private readonly output: vscode.OutputChannel; constructor( @@ -62,6 +76,12 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { private readonly secrets: vscode.SecretStorage, ) { this.output = vscode.window.createOutputChannel('ATLAS'); + this.permissions = new PermissionFlow(this.turns.sessionAllowedTools, { + onPrompt: (pending) => this.showPermissionPrompt(pending), + onDismiss: (pending, reason) => this.resolvePermissionPrompt(pending, reason), + onAutoAllow: (toolName) => this.post({ type: 'note', text: `'${toolName}' auto-allowed (approved for this session).` }), + onPostError: (toolName, error) => this.log(`permission decision POST failed for '${toolName}'`, error), + }); } resolveWebviewView(view: vscode.WebviewView): void { @@ -82,6 +102,10 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { case 'cancel': this.cancelTurn(); break; + case 'permissionAnswer': + // Stale/settled ids are no-ops inside the flow (first answer wins). + this.permissions.settleById(message.id, message.choice); + break; } }); } @@ -124,6 +148,9 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { } catch (error) { this.handleTurnFailure(error); } finally { + // Any prompt still open has nothing left to answer — the proxy + // resolves pending requests when the turn ends. + this.permissions.endTurn(); this.postTransient({ type: 'busy', value: false }); } } @@ -154,22 +181,18 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { break; } case 'permission_request': { - // Interim commit-3 behavior: deny immediately and say so, so - // the turn never sits on the 600s server-side timeout. The - // real approve/deny flow lands in the next commit. const payload = data as PermissionRequestEventData; - this.post({ - type: 'note', - text: `Permission request for '${payload.tool_name}' auto-denied — the approval UI lands in an upcoming commit.`, - }); - void client - .postPermissionDecision({ - session_id: this.turns.sessionId, - tool_call_id: payload.tool_call_id, - decision: 'deny', - scope: 'once', - }) - .catch((error: unknown) => this.log('permission deny failed', error)); + this.permissions.handleRequest(client, this.turns.sessionId, payload); + break; + } + case 'permission_denied': { + // Proxy-side resolution (timeout/cancel) — may arrive while our + // prompt is still up; the flow dismisses it by tool name. The + // denied row itself renders here (TUI convention: the local deny + // path renders NO row to avoid duplicating this event). + const payload = data as PermissionDeniedEventData; + this.permissions.handleDenied(payload.tool); + this.post({ type: 'note', text: `Permission denied for '${payload.tool}'.` }); break; } case 'error': { @@ -208,7 +231,58 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { this.post({ type: 'turnError', message: `Could not reach the ATLAS proxy: ${message}` }); } - /** Post to the webview and record for replay. */ + /** Show a permission prompt on both surfaces: an inline webview card + * (buttons post permissionAnswer back) and a native notification. First + * answer wins — PendingPermission.settle() ignores the loser. */ + private showPermissionPrompt(pending: PendingPermission): void { + this.post({ + type: 'permissionPrompt', + id: pending.id, + tool: pending.request.tool_name, + detail: condenseArgs(pending.request.args), + message: pending.request.message || '', + }); + const label = pending.request.message || `ATLAS wants to run '${pending.request.tool_name}'.`; + void vscode.window + .showInformationMessage(label, 'Allow Once', 'Allow for Session', 'Deny') + .then((choice) => { + if (choice === undefined) { + return; // dismissed — the card (or the timeout) decides + } + const map: Record = { + 'Allow Once': 'allow-once', + 'Allow for Session': 'allow-session', + Deny: 'deny', + }; + pending.settle(map[choice]); + }); + } + + /** Collapse the prompt card into its outcome line. A user deny renders no + * extra row — the proxy's permission_denied event carries that (TUI + * convention, avoids the duplicate). */ + private resolvePermissionPrompt(pending: PendingPermission, reason: DismissReason): void { + let outcome: string; + switch (reason) { + case 'answered': + outcome = + pending.choice === 'allow-session' + ? 'allowed for session' + : pending.choice === 'allow-once' + ? 'allowed once' + : 'denied'; + break; + case 'denied-remote': + outcome = 'resolved by proxy'; + break; + case 'turn-ended': + outcome = 'turn ended'; + break; + } + this.post({ type: 'permissionResolved', id: pending.id, outcome }); + } + + private post(message: OutboundMessage): void { this.transcript.push(message); void this.view?.webview.postMessage(message); diff --git a/extensions/vscode/test/permissionFlow.test.ts b/extensions/vscode/test/permissionFlow.test.ts new file mode 100644 index 00000000..043c4024 --- /dev/null +++ b/extensions/vscode/test/permissionFlow.test.ts @@ -0,0 +1,235 @@ +// Unit tests for PermissionFlow: auto-allow for session-approved tools, +// prompt/settle round trips (once/session/deny), first-answer-wins, +// remote permission_denied dismissal, turn-end cleanup, and advisory +// POST error reporting. + +import { describe, expect, it } from 'vitest'; +import type { PermissionDecisionRequest, PermissionRequestEventData } from '../src/client/types'; +import { + PermissionFlow, + type DismissReason, + type PendingPermission, + type PermissionPoster, + type PermissionUi, +} from '../src/session/permissionFlow'; + +function request(tool: string, id = `call-${tool}`): PermissionRequestEventData { + return { tool_name: tool, args: { path: 'a.py' }, message: `run ${tool}?`, tool_call_id: id }; +} + +function fakePoster(failWith?: Error): PermissionPoster & { decisions: PermissionDecisionRequest[] } { + return { + decisions: [], + async postPermissionDecision(decision: PermissionDecisionRequest): Promise { + this.decisions.push(decision); + if (failWith) { + throw failWith; + } + }, + }; +} + +interface UiEvent { + kind: 'prompt' | 'dismiss' | 'auto-allow' | 'post-error'; + pending?: PendingPermission; + reason?: DismissReason; + toolName?: string; + error?: unknown; +} + +function fakeUi(): PermissionUi & { events: UiEvent[] } { + return { + events: [], + onPrompt(pending) { + this.events.push({ kind: 'prompt', pending }); + }, + onDismiss(pending, reason) { + this.events.push({ kind: 'dismiss', pending, reason }); + }, + onAutoAllow(toolName) { + this.events.push({ kind: 'auto-allow', toolName }); + }, + onPostError(toolName, error) { + this.events.push({ kind: 'post-error', toolName, error }); + }, + }; +} + +/** Let queued microtasks (fire-and-forget POST .catch chains) run. */ +const settle = () => new Promise((resolve) => setImmediate(resolve)); + +describe('PermissionFlow', () => { + it('auto-answers allow/once for a session-allowed tool, no prompt', async () => { + const poster = fakePoster(); + const ui = fakeUi(); + const allowed = new Set(['edit_file']); + const flow = new PermissionFlow(allowed, ui); + + flow.handleRequest(poster, 'sess-1', request('edit_file')); + await settle(); + + // TUI convention: auto-allow posts scope "once", not "session". + expect(poster.decisions).toEqual([ + { session_id: 'sess-1', tool_call_id: 'call-edit_file', decision: 'allow', scope: 'once' }, + ]); + expect(ui.events).toEqual([{ kind: 'auto-allow', toolName: 'edit_file' }]); + }); + + it('prompts for an unlisted tool and posts allow/once on allow-once', async () => { + const poster = fakePoster(); + const ui = fakeUi(); + const allowed = new Set(); + const flow = new PermissionFlow(allowed, ui); + + flow.handleRequest(poster, 'sess-1', request('write_file')); + expect(ui.events).toHaveLength(1); + expect(ui.events[0].kind).toBe('prompt'); + const pending = ui.events[0].pending!; + expect(pending.request.tool_name).toBe('write_file'); + + expect(pending.settle('allow-once')).toBe(true); + await settle(); + + expect(poster.decisions).toEqual([ + { session_id: 'sess-1', tool_call_id: 'call-write_file', decision: 'allow', scope: 'once' }, + ]); + // Allow-once must NOT touch the session allowlist. + expect(allowed.size).toBe(0); + expect(ui.events.at(-1)).toMatchObject({ kind: 'dismiss', reason: 'answered' }); + expect(pending.choice).toBe('allow-once'); + }); + + it('allow-session adds to the allowlist and posts scope "session"', async () => { + const poster = fakePoster(); + const ui = fakeUi(); + const allowed = new Set(); + const flow = new PermissionFlow(allowed, ui); + + flow.handleRequest(poster, 'sess-2', request('ast_edit')); + ui.events[0].pending!.settle('allow-session'); + await settle(); + + expect(allowed.has('ast_edit')).toBe(true); + expect(poster.decisions).toEqual([ + { session_id: 'sess-2', tool_call_id: 'call-ast_edit', decision: 'allow', scope: 'session' }, + ]); + }); + + it('deny posts deny/once and does not touch the allowlist', async () => { + const poster = fakePoster(); + const ui = fakeUi(); + const allowed = new Set(); + const flow = new PermissionFlow(allowed, ui); + + flow.handleRequest(poster, 'sess-3', request('run_command')); + ui.events[0].pending!.settle('deny'); + await settle(); + + expect(poster.decisions).toEqual([ + { session_id: 'sess-3', tool_call_id: 'call-run_command', decision: 'deny', scope: 'once' }, + ]); + expect(allowed.size).toBe(0); + }); + + it('first answer wins — the second settle is a no-op', async () => { + const poster = fakePoster(); + const ui = fakeUi(); + const flow = new PermissionFlow(new Set(), ui); + + flow.handleRequest(poster, 'sess-4', request('write_file')); + const pending = ui.events[0].pending!; + + expect(pending.settle('allow-once')).toBe(true); + expect(pending.settle('deny')).toBe(false); + await settle(); + + expect(poster.decisions).toHaveLength(1); + expect(poster.decisions[0].decision).toBe('allow'); + expect(pending.choice).toBe('allow-once'); + }); + + it('remote permission_denied dismisses the matching prompt; later settle is a no-op', async () => { + const poster = fakePoster(); + const ui = fakeUi(); + const flow = new PermissionFlow(new Set(), ui); + + flow.handleRequest(poster, 'sess-5', request('write_file')); + const pending = ui.events[0].pending!; + + flow.handleDenied('write_file'); + expect(ui.events.at(-1)).toMatchObject({ kind: 'dismiss', reason: 'denied-remote' }); + + expect(pending.settle('allow-once')).toBe(false); + await settle(); + expect(poster.decisions).toHaveLength(0); + }); + + it('permission_denied for a tool with no open prompt is a no-op', () => { + const ui = fakeUi(); + const flow = new PermissionFlow(new Set(), ui); + flow.handleDenied('write_file'); + expect(ui.events).toHaveLength(0); + }); + + it('endTurn dismisses all open prompts and blocks late answers', async () => { + const poster = fakePoster(); + const ui = fakeUi(); + const flow = new PermissionFlow(new Set(), ui); + + flow.handleRequest(poster, 'sess-6', request('write_file', 'call-1')); + flow.handleRequest(poster, 'sess-6', request('edit_file', 'call-2')); + const first = ui.events[0].pending!; + + flow.endTurn(); + const dismissals = ui.events.filter((e) => e.kind === 'dismiss'); + expect(dismissals).toHaveLength(2); + expect(dismissals.every((e) => e.reason === 'turn-ended')).toBe(true); + + expect(first.settle('allow-once')).toBe(false); + await settle(); + expect(poster.decisions).toHaveLength(0); + }); + + it('settleById routes to the right prompt; unknown ids are no-ops', async () => { + const poster = fakePoster(); + const ui = fakeUi(); + const flow = new PermissionFlow(new Set(), ui); + + flow.handleRequest(poster, 'sess-7', request('write_file', 'call-1')); + flow.handleRequest(poster, 'sess-7', request('edit_file', 'call-2')); + const second = ui.events[1].pending!; + + expect(flow.settleById(9999, 'deny')).toBe(false); + expect(flow.settleById(second.id, 'allow-once')).toBe(true); + await settle(); + + expect(poster.decisions).toEqual([ + { session_id: 'sess-7', tool_call_id: 'call-2', decision: 'allow', scope: 'once' }, + ]); + }); + + it('reports POST failures via onPostError instead of throwing', async () => { + const boom = new Error('proxy 500'); + const poster = fakePoster(boom); + const ui = fakeUi(); + const flow = new PermissionFlow(new Set(), ui); + + flow.handleRequest(poster, 'sess-8', request('write_file')); + ui.events[0].pending!.settle('allow-once'); + await settle(); + + expect(ui.events.at(-1)).toMatchObject({ kind: 'post-error', toolName: 'write_file', error: boom }); + }); + + it('auto-allow POST failure also routes to onPostError', async () => { + const boom = new Error('proxy 500'); + const poster = fakePoster(boom); + const ui = fakeUi(); + const flow = new PermissionFlow(new Set(['edit_file']), ui); + + flow.handleRequest(poster, 'sess-9', request('edit_file')); + await settle(); + + expect(ui.events.at(-1)).toMatchObject({ kind: 'post-error', toolName: 'edit_file', error: boom }); + }); +}); From f26a0bbadaa9c57ff3a803e519b8f9bd9eed6e98 Mon Sep 17 00:00:00 2001 From: Anuj-72 Date: Sun, 19 Jul 2026 16:27:02 +0530 Subject: [PATCH 05/13] feat(vscode): render diffs for write_file/edit_file/ast_edit Predict edits client-side and surface them via native vscode.diff and a read-only atlas-diff: virtual doc, for both permission review and applied changes. --- extensions/vscode/README.md | 8 +- extensions/vscode/media/chat.css | 19 ++ extensions/vscode/media/chat.js | 70 +++-- extensions/vscode/src/extension.ts | 6 +- extensions/vscode/src/session/editPreview.ts | 296 +++++++++++++++++++ extensions/vscode/src/ui/chatView.ts | 202 +++++++++++-- extensions/vscode/src/ui/diffProvider.ts | 61 ++++ extensions/vscode/test/editPreview.test.ts | 164 ++++++++++ 8 files changed, 784 insertions(+), 42 deletions(-) create mode 100644 extensions/vscode/src/session/editPreview.ts create mode 100644 extensions/vscode/src/ui/diffProvider.ts create mode 100644 extensions/vscode/test/editPreview.test.ts diff --git a/extensions/vscode/README.md b/extensions/vscode/README.md index 2e433a3c..66f5711b 100644 --- a/extensions/vscode/README.md +++ b/extensions/vscode/README.md @@ -2,7 +2,13 @@ A VS Code client for the [ATLAS](https://github.com/itigges22/ATLAS) agent proxy — a thin UI layer wrapping `atlas-proxy`'s agent loop (chat, tool calls, permission gating, diffs) with no agent logic in the extension itself. -**Status: Work in progress.** Tracking [issue #35](https://github.com/itigges22/ATLAS/issues/35). Currently scaffold-only — chat UI, permission flow, and diff rendering land in upcoming commits. +**Status: Work in progress.** Tracking [issue #35](https://github.com/itigges22/ATLAS/issues/35). Chat, permission flow, and diff review are implemented; status bar and the workspace-mismatch warning land in upcoming commits. + +## Diff review + +* Permission prompts for `write_file` / `edit_file` / `ast_edit` offer **View Diff** — a side-by-side preview predicted client-side from the local file (`ast_edit` results carry no content, so its predictions use a best-effort selector splice and are labeled approximate). +* After a successful edit, the tool chip offers **View change** — the exact applied diff (pre-call snapshot vs on-disk), which also compensates for approximate `ast_edit` predictions. Falls back to `edit_file`'s server-computed `diff_preview` when the file is not readable in this workspace. +* `move_file` / `delete_file` / `run_command` prompt without a diff. ## How it works diff --git a/extensions/vscode/media/chat.css b/extensions/vscode/media/chat.css index 3bd0bc8c..ae0e8db2 100644 --- a/extensions/vscode/media/chat.css +++ b/extensions/vscode/media/chat.css @@ -134,6 +134,12 @@ body { overflow-wrap: anywhere; } +.permission-note { + font-style: italic; + font-size: 0.9em; + opacity: 0.8; +} + .permission-actions { display: flex; gap: 6px; @@ -145,6 +151,19 @@ body { color: var(--vscode-button-secondaryForeground, var(--vscode-button-foreground)); } +.permission-actions .view-diff { + background: var(--vscode-button-secondaryBackground, var(--vscode-button-background)); + color: var(--vscode-button-secondaryForeground, var(--vscode-button-foreground)); +} + +.chip-diff { + margin-left: 6px; + padding: 1px 8px; + font-size: 0.9em; + background: var(--vscode-button-secondaryBackground, var(--vscode-button-background)); + color: var(--vscode-button-secondaryForeground, var(--vscode-button-foreground)); +} + .permission-outcome { font-style: italic; font-size: 0.9em; diff --git a/extensions/vscode/media/chat.js b/extensions/vscode/media/chat.js index 90455593..51143bd1 100644 --- a/extensions/vscode/media/chat.js +++ b/extensions/vscode/media/chat.js @@ -68,33 +68,43 @@ pendingChips.get(name).push(chip); } - function resolveToolChip(tool, success, elapsed, error) { + function resolveToolChip(tool, success, elapsed, error, diffId) { const queue = pendingChips.get(tool); - const chip = queue && queue.length > 0 ? queue.shift() : null; + let chip = queue && queue.length > 0 ? queue.shift() : null; if (!chip) { // Result without a rendered call (e.g. replay edge) — show standalone. - appendBlock('chip ' + (success ? 'ok' : 'fail'), (success ? '✓ ' : '✗ ') + tool + (error ? ': ' + error : '')); - return; - } - chip.classList.remove('pending'); - chip.classList.add(success ? 'ok' : 'fail'); - const status = chip.querySelector('.chip-status'); - status.textContent = success ? '✓' : '✗'; - if (elapsed) { - const time = document.createElement('span'); - time.className = 'chip-elapsed'; - time.textContent = elapsed; - chip.appendChild(time); + chip = appendBlock('chip ' + (success ? 'ok' : 'fail'), (success ? '✓ ' : '✗ ') + tool + (error ? ': ' + error : '')); + } else { + chip.classList.remove('pending'); + chip.classList.add(success ? 'ok' : 'fail'); + const status = chip.querySelector('.chip-status'); + status.textContent = success ? '✓' : '✗'; + if (elapsed) { + const time = document.createElement('span'); + time.className = 'chip-elapsed'; + time.textContent = elapsed; + chip.appendChild(time); + } + if (!success && error) { + const detail = document.createElement('div'); + detail.className = 'chip-error'; + detail.textContent = error; + chip.appendChild(detail); + } } - if (!success && error) { - const detail = document.createElement('div'); - detail.className = 'chip-error'; - detail.textContent = error; - chip.appendChild(detail); + if (diffId !== undefined && diffId !== null) { + const view = document.createElement('button'); + view.type = 'button'; + view.className = 'chip-diff'; + view.textContent = 'View change'; + view.addEventListener('click', () => { + vscode.postMessage({ type: 'viewAppliedDiff', id: diffId }); + }); + chip.appendChild(view); } } - function addPermissionCard(id, tool, detail, message) { + function addPermissionCard(id, tool, detail, message, canDiff, note) { const card = document.createElement('div'); card.className = 'permission-card'; @@ -115,9 +125,25 @@ args.textContent = detail; card.appendChild(args); } + if (note) { + const hint = document.createElement('div'); + hint.className = 'permission-note'; + hint.textContent = note; + card.appendChild(hint); + } const actions = document.createElement('div'); actions.className = 'permission-actions'; + if (canDiff) { + const view = document.createElement('button'); + view.type = 'button'; + view.className = 'view-diff'; + view.textContent = 'View Diff'; + view.addEventListener('click', () => { + vscode.postMessage({ type: 'viewPermissionDiff', id: id }); + }); + actions.appendChild(view); + } const buttons = [ ['Allow Once', 'allow-once'], ['Allow for Session', 'allow-session'], @@ -190,7 +216,7 @@ addToolChip(message.name, message.detail); break; case 'toolResult': - resolveToolChip(message.tool, message.success, message.elapsed, message.error); + resolveToolChip(message.tool, message.success, message.elapsed, message.error, message.diffId); break; case 'note': closeAssistantBubble(); @@ -198,7 +224,7 @@ break; case 'permissionPrompt': closeAssistantBubble(); - addPermissionCard(message.id, message.tool, message.detail, message.message); + addPermissionCard(message.id, message.tool, message.detail, message.message, message.canDiff, message.note); break; case 'permissionResolved': resolvePermissionCard(message.id, message.outcome); diff --git a/extensions/vscode/src/extension.ts b/extensions/vscode/src/extension.ts index 47651323..2152b187 100644 --- a/extensions/vscode/src/extension.ts +++ b/extensions/vscode/src/extension.ts @@ -1,10 +1,14 @@ import * as vscode from 'vscode'; import { ChatViewProvider, TOKEN_SECRET_KEY } from './ui/chatView'; +import { DiffProvider } from './ui/diffProvider'; export function activate(context: vscode.ExtensionContext) { - const chat = new ChatViewProvider(context.extensionUri, context.secrets); + const diffs = new DiffProvider(); + const chat = new ChatViewProvider(context.extensionUri, context.secrets, diffs); context.subscriptions.push( + diffs.register(), + vscode.window.registerWebviewViewProvider(ChatViewProvider.viewType, chat, { webviewOptions: { retainContextWhenHidden: true }, }), diff --git a/extensions/vscode/src/session/editPreview.ts b/extensions/vscode/src/session/editPreview.ts new file mode 100644 index 00000000..250f1b9c --- /dev/null +++ b/extensions/vscode/src/session/editPreview.ts @@ -0,0 +1,296 @@ +// Client-side prediction of what a file-writing tool call will do, computed +// at permission time from the CURRENT local file. Needed because the diff has +// to be shown BEFORE the user allows the call — and for ast_edit even the +// result carries no new content (proxy/types.go AstEditOutput is ok/selector/ +// language/byte counts only), so the post-state can only be predicted here. +// +// Accuracy contract per tool: +// - write_file: exact (disk-or-empty vs `content`). +// - edit_file: exact when old_str occurs (first occurrence, or all with +// replace_all); otherwise a snippet diff of old_str vs new_str plus a +// note — the proxy will likely reject that call anyway. +// - ast_edit: best-effort regex/indent splice for python function:NAME / +// class:NAME (decorator-aware) and a naive top-level scan for HTML. +// Tree-sitter is the server-side authority (v3-service), so every +// ast_edit prediction is labeled approximate. The exact view comes later: +// chatView snapshots the file at tool_call time and diffs snapshot vs +// on-disk after the tool_result. +// +// Deliberately vscode-free so it runs under plain vitest. + +/** The three tools whose permission prompts and tool chips get diffs. + * move_file / delete_file / run_command are notification-only. */ +export const FILE_EDIT_TOOLS = new Set(['write_file', 'edit_file', 'ast_edit']); + +export interface EditPrediction { + /** Target path exactly as the model sent it (proxy-workspace-relative). */ + path: string; + /** 'file': left/right are whole-file states. 'snippet': left/right are + * just old_str/new_str (edit_file whose old_str is absent). */ + kind: 'file' | 'snippet'; + left: string; + right: string; + /** True when the prediction is a best-effort guess rather than the exact + * post-state (all ast_edit predictions, and edit_file snippet mode). */ + approximate: boolean; + note?: string; +} + +function stringField(args: unknown, key: string): string | undefined { + if (typeof args !== 'object' || args === null) { + return undefined; + } + const value = (args as Record)[key]; + return typeof value === 'string' ? value : undefined; +} + +/** Path a file-edit tool call targets, or undefined for other tools / + * malformed args. Used to read the local file and to key snapshots. */ +export function editTargetPath(tool: string, args: unknown): string | undefined { + if (!FILE_EDIT_TOOLS.has(tool)) { + return undefined; + } + return stringField(args, 'path'); +} + +/** Predict the post-edit file state. `current` is the local file content, or + * undefined when the file does not exist in this workspace. Returns undefined + * for non-edit tools or args missing required fields. */ +export function predictEdit(tool: string, args: unknown, current: string | undefined): EditPrediction | undefined { + const path = editTargetPath(tool, args); + if (path === undefined) { + return undefined; + } + switch (tool) { + case 'write_file': { + const content = stringField(args, 'content'); + if (content === undefined) { + return undefined; + } + return { + path, + kind: 'file', + left: current ?? '', + right: content, + approximate: false, + note: current === undefined ? 'new file' : undefined, + }; + } + case 'edit_file': { + const oldStr = stringField(args, 'old_str'); + const newStr = stringField(args, 'new_str'); + if (oldStr === undefined || newStr === undefined) { + return undefined; + } + return predictEditFile(path, current, oldStr, newStr, isReplaceAll(args)); + } + case 'ast_edit': { + const selector = stringField(args, 'selector'); + const content = stringField(args, 'content'); + if (selector === undefined || content === undefined) { + return undefined; + } + return predictAstEdit(path, current, selector, content); + } + default: + return undefined; + } +} + +function isReplaceAll(args: unknown): boolean { + if (typeof args !== 'object' || args === null) { + return false; + } + return (args as Record)['replace_all'] === true; +} + +function predictEditFile( + path: string, + current: string | undefined, + oldStr: string, + newStr: string, + replaceAll: boolean, +): EditPrediction { + if (current === undefined) { + return { + path, + kind: 'snippet', + left: oldStr, + right: newStr, + approximate: true, + note: 'target file not found in this workspace — showing old_str vs new_str', + }; + } + const index = current.indexOf(oldStr); + if (oldStr === '' || index === -1) { + return { + path, + kind: 'snippet', + left: oldStr, + right: newStr, + approximate: true, + note: 'old_str not found in the file — showing old_str vs new_str (the proxy will likely reject this edit)', + }; + } + // split/join instead of String.replace: the replacement string must be + // spliced literally ($& and friends have no meaning here). + const right = replaceAll + ? current.split(oldStr).join(newStr) + : current.slice(0, index) + newStr + current.slice(index + oldStr.length); + return { path, kind: 'file', left: current, right, approximate: false }; +} + +function predictAstEdit(path: string, current: string | undefined, selector: string, content: string): EditPrediction { + if (current === undefined) { + return { + path, + kind: 'file', + left: '', + right: content, + approximate: true, + note: 'target file not found in this workspace — showing the proposed node content only', + }; + } + const fn = /^function:([A-Za-z_]\w*)$/.exec(selector.trim()); + const cls = /^class:([A-Za-z_]\w*)$/.exec(selector.trim()); + const tag = /^<([A-Za-z][\w-]*)>$/.exec(selector.trim()); + if (fn || cls) { + const spliced = splicePythonNode(current, fn ? 'function' : 'class', (fn ?? cls)![1], content); + if (spliced) { + return { + path, + kind: 'file', + left: current, + right: spliced.right, + approximate: true, + note: spliced.note ?? 'predicted client-side — tree-sitter runs server-side', + }; + } + } else if (tag) { + let body = content; + if (tag[1].toLowerCase() === 'html') { + // Mirror the proxy's -selector quirk: the element replace + // does not cover a preceding , so the proxy strips a + // leading doctype from the content (proxy/tools.go). + body = stripLeadingDoctype(body); + } + const spliced = spliceHtmlElement(current, tag[1], body); + if (spliced !== undefined) { + return { + path, + kind: 'file', + left: current, + right: spliced, + approximate: true, + note: 'predicted client-side — tree-sitter runs server-side', + }; + } + } + return { + path, + kind: 'file', + left: current, + right: content, + approximate: true, + note: `selector '${selector}' not matched locally — showing the proposed content against the whole file`, + }; +} + +function stripLeadingDoctype(content: string): string { + const match = /^\s*]*>\r?\n?/i.exec(content); + return match ? content.slice(match[0].length) : content; +} + +function escapeRegExp(text: string): string { + return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** Replace the named python function/class (plus contiguous decorator lines + * directly above it) with `content`. Indent-scan only — no parser. */ +function splicePythonNode( + source: string, + kind: 'function' | 'class', + name: string, + content: string, +): { right: string; note?: string } | undefined { + const lines = source.split('\n'); + const head = + kind === 'function' + ? new RegExp(`^([ \\t]*)(?:async[ \\t]+)?def[ \\t]+${escapeRegExp(name)}[ \\t]*\\(`) + : new RegExp(`^([ \\t]*)class[ \\t]+${escapeRegExp(name)}[ \\t]*[(:]`); + const matches: number[] = []; + for (let i = 0; i < lines.length; i++) { + if (head.test(lines[i])) { + matches.push(i); + } + } + if (matches.length === 0) { + return undefined; + } + const defLine = matches[0]; + const indent = /^[ \t]*/.exec(lines[defLine])![0]; + + // Decorators are part of the node (proxy behavior: "decorators included + // automatically"). Naive: contiguous same-indent lines starting with '@'. + let start = defLine; + while (start > 0 && lines[start - 1].startsWith(`${indent}@`)) { + start--; + } + + // Body ends at the last line indented deeper than the def; blank lines + // inside the body are skipped, trailing blanks stay outside the node. + let lastNonEmpty = defLine; + for (let i = defLine + 1; i < lines.length; i++) { + if (lines[i].trim() === '') { + continue; + } + const lineIndent = /^[ \t]*/.exec(lines[i])![0]; + if (lineIndent.length <= indent.length) { + break; + } + lastNonEmpty = i; + } + + const right = [...lines.slice(0, start), ...content.split('\n'), ...lines.slice(lastNonEmpty + 1)].join('\n'); + const note = + matches.length > 1 + ? `${matches.length} definitions named '${name}' — the first one is shown replaced` + : undefined; + return { right, note }; +} + +/** Replace the first top-level ... element (nesting-aware for the + * same tag name, self-closing handled) with `content`. Naive text scan. */ +function spliceHtmlElement(source: string, tag: string, content: string): string | undefined { + const tokenRe = new RegExp(`<(/?)${escapeRegExp(tag)}(?=[\\s/>])`, 'gi'); + let depth = 0; + let start = -1; + let match: RegExpExecArray | null; + while ((match = tokenRe.exec(source)) !== null) { + const gt = source.indexOf('>', match.index); + if (gt === -1) { + return undefined; + } + if (match[1] === '') { + const selfClosing = source[gt - 1] === '/'; + if (depth === 0) { + start = match.index; + if (selfClosing) { + return source.slice(0, start) + content + source.slice(gt + 1); + } + } + if (!selfClosing) { + depth++; + } + } else { + depth--; + if (depth === 0 && start !== -1) { + return source.slice(0, start) + content + source.slice(gt + 1); + } + if (depth < 0) { + return undefined; + } + } + } + return undefined; +} diff --git a/extensions/vscode/src/ui/chatView.ts b/extensions/vscode/src/ui/chatView.ts index 3bbc254b..b701d107 100644 --- a/extensions/vscode/src/ui/chatView.ts +++ b/extensions/vscode/src/ui/chatView.ts @@ -3,6 +3,8 @@ // also appended to a transcript so a re-created webview (sidebar closed and // reopened, window reload of the view) can be replayed from scratch. +import { promises as fs } from 'node:fs'; +import * as path from 'node:path'; import * as vscode from 'vscode'; import { AtlasApiError, AtlasClient } from '../client/atlasClient'; import type { @@ -14,6 +16,7 @@ import type { ToolCallEventData, ToolResultEventData, } from '../client/types'; +import { editTargetPath, predictEdit, type EditPrediction } from '../session/editPreview'; import { PendingPermission, PermissionFlow, @@ -21,6 +24,7 @@ import { type PermissionChoice, } from '../session/permissionFlow'; import { TurnManager } from '../session/turnManager'; +import { DiffProvider } from './diffProvider'; /** SecretStorage key for the service token ('ATLAS: Set Service Token'). */ export const TOKEN_SECRET_KEY = 'atlas.serviceToken'; @@ -30,9 +34,9 @@ type OutboundMessage = | { type: 'userMessage'; text: string } | { type: 'assistantDelta'; text: string } | { type: 'toolCall'; name: string; detail: string } - | { type: 'toolResult'; tool: string; success: boolean; elapsed?: string; error?: string } + | { type: 'toolResult'; tool: string; success: boolean; elapsed?: string; error?: string; diffId?: number } | { type: 'note'; text: string } - | { type: 'permissionPrompt'; id: number; tool: string; detail: string; message: string } + | { type: 'permissionPrompt'; id: number; tool: string; detail: string; message: string; canDiff: boolean; note?: string } | { type: 'permissionResolved'; id: number; outcome: string } | { type: 'turnDone' } | { type: 'turnError'; message: string } @@ -44,7 +48,28 @@ type InboundMessage = | { type: 'ready' } | { type: 'submit'; text: string } | { type: 'cancel' } - | { type: 'permissionAnswer'; id: number; choice: PermissionChoice }; + | { type: 'permissionAnswer'; id: number; choice: PermissionChoice } + | { type: 'viewPermissionDiff'; id: number } + | { type: 'viewAppliedDiff'; id: number }; + +/** File state captured when a file-edit tool_call streams past, keyed by + * tool name FIFO (tool_result carries only the tool name — same matching + * scheme the webview uses for its chips). */ +interface EditSnapshot { + path: string; + before: string | undefined; +} + +/** What a resolved "View change" chip button opens: the exact applied + * change (snapshot vs on-disk), falling back to edit_file's server-computed + * diff_preview when the file is not readable in this workspace. */ +interface AppliedDiff { + tool: string; + path: string; + before: string | undefined; + after: string | undefined; + preview?: string; +} /** Condense tool args to a single short line for the tool chip. */ function condenseArgs(args: unknown): string { @@ -71,9 +96,21 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { private readonly permissions: PermissionFlow; private readonly output: vscode.OutputChannel; + /** Pre-edit file states awaiting their tool_result, FIFO per tool name. */ + private snapshots = new Map(); + /** Applied changes viewable from resolved tool chips, by diff id. */ + private appliedDiffs = new Map(); + /** Permission-time predictions viewable while the prompt is open. */ + private permissionDiffs = new Map(); + private nextDiffId = 1; + /** Hand-off slot: dispatch() computes the prediction, then handleRequest + * synchronously calls back into showPermissionPrompt which claims it. */ + private promptPrediction: EditPrediction | undefined; + constructor( private readonly extensionUri: vscode.Uri, private readonly secrets: vscode.SecretStorage, + private readonly diffs: DiffProvider, ) { this.output = vscode.window.createOutputChannel('ATLAS'); this.permissions = new PermissionFlow(this.turns.sessionAllowedTools, { @@ -106,6 +143,12 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { // Stale/settled ids are no-ops inside the flow (first answer wins). this.permissions.settleById(message.id, message.choice); break; + case 'viewPermissionDiff': + void this.openPermissionDiff(message.id); + break; + case 'viewAppliedDiff': + void this.openAppliedDiff(message.id); + break; } }); } @@ -118,6 +161,9 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { this.turns.cancel(); this.turns.reset(); this.transcript = []; + this.snapshots.clear(); + this.appliedDiffs.clear(); + this.permissionDiffs.clear(); this.postTransient({ type: 'reset' }); } @@ -142,7 +188,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { this.postTransient({ type: 'busy', value: true }); try { for await (const event of this.turns.runTurn(client, message, mode)) { - this.dispatch(event.type, event.data, client); + await this.dispatch(event.type, event.data, client); } this.post({ type: 'turnDone' }); } catch (error) { @@ -151,11 +197,12 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { // Any prompt still open has nothing left to answer — the proxy // resolves pending requests when the turn ends. this.permissions.endTurn(); + this.snapshots.clear(); this.postTransient({ type: 'busy', value: false }); } } - private dispatch(type: string, data: unknown, client: AtlasClient): void { + private async dispatch(type: string, data: unknown, client: AtlasClient): Promise { switch (type) { case 'text': { const payload = data as TextEventData; @@ -167,22 +214,45 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { case 'tool_call': { const payload = data as ToolCallEventData; this.post({ type: 'toolCall', name: payload.name, detail: condenseArgs(payload.args) }); + // Snapshot the target now (before permission/execution) so a + // successful result can show the exact applied change. At + // tool_call time rather than permission time: accept-edits + // and yolo turns never raise a permission_request. + const target = editTargetPath(payload.name, payload.args); + if (target !== undefined) { + const before = await this.readLocalFile(target); + const queue = this.snapshots.get(payload.name) ?? []; + queue.push({ path: target, before }); + this.snapshots.set(payload.name, queue); + } break; } case 'tool_result': { const payload = data as ToolResultEventData; + const diffId = await this.recordAppliedDiff(payload); this.post({ type: 'toolResult', tool: payload.tool, success: payload.success, elapsed: payload.elapsed, error: payload.error, + diffId, }); break; } case 'permission_request': { const payload = data as PermissionRequestEventData; - this.permissions.handleRequest(client, this.turns.sessionId, payload); + // Predict the edit from the CURRENT local file so the prompt + // can offer "View Diff" before the user decides. ast_edit has + // no post-content in its result, so this is the only pre-view. + const target = editTargetPath(payload.tool_name, payload.args); + const current = target === undefined ? undefined : await this.readLocalFile(target); + this.promptPrediction = predictEdit(payload.tool_name, payload.args, current); + try { + this.permissions.handleRequest(client, this.turns.sessionId, payload); + } finally { + this.promptPrediction = undefined; + } break; } case 'permission_denied': { @@ -209,6 +279,60 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { } } + /** Consume the pending snapshot for a tool_result and, when there is + * something to show, capture the on-disk "after" state and register a + * viewable applied diff. Returns its id, or undefined. */ + private async recordAppliedDiff(payload: ToolResultEventData): Promise { + const queue = this.snapshots.get(payload.tool); + const snapshot = queue && queue.length > 0 ? queue.shift() : undefined; + if (!snapshot || !payload.success) { + return undefined; + } + // Read immediately: the proxy writes before emitting the result, and + // a later read could already include the NEXT edit to the same file. + const after = await this.readLocalFile(snapshot.path); + const preview = + payload.tool === 'edit_file' ? (payload.data as { diff_preview?: string } | undefined)?.diff_preview : undefined; + if (after === undefined && !preview) { + return undefined; // nothing viewable (likely a workspace mismatch) + } + const diffId = this.nextDiffId++; + this.appliedDiffs.set(diffId, { tool: payload.tool, path: snapshot.path, before: snapshot.before, after, preview }); + return diffId; + } + + private async openAppliedDiff(id: number): Promise { + const applied = this.appliedDiffs.get(id); + if (!applied) { + return; + } + if (applied.after !== undefined) { + await this.diffs.openDiff( + `ATLAS: ${applied.tool} ${applied.path} (applied)`, + applied.path, + applied.before ?? '', + applied.after, + ); + } else if (applied.preview) { + await this.diffs.openPreview(applied.path, applied.preview); + } + } + + private async openPermissionDiff(id: number): Promise { + const entry = this.permissionDiffs.get(id); + if (!entry) { + return; + } + const { tool, prediction } = entry; + const qualifier = prediction.kind === 'snippet' ? 'snippet' : prediction.approximate ? 'proposed · approximate' : 'proposed'; + await this.diffs.openDiff( + `ATLAS: ${tool} ${prediction.path} (${qualifier})`, + prediction.path, + prediction.left, + prediction.right, + ); + } + private handleTurnFailure(error: unknown): void { if (error instanceof Error && error.name === 'AbortError') { this.post({ type: 'note', text: 'Turn cancelled.' }); @@ -235,33 +359,55 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { * (buttons post permissionAnswer back) and a native notification. First * answer wins — PendingPermission.settle() ignores the loser. */ private showPermissionPrompt(pending: PendingPermission): void { + const prediction = this.promptPrediction; + if (prediction) { + this.permissionDiffs.set(pending.id, { tool: pending.request.tool_name, prediction }); + } this.post({ type: 'permissionPrompt', id: pending.id, tool: pending.request.tool_name, detail: condenseArgs(pending.request.args), message: pending.request.message || '', + canDiff: prediction !== undefined, + note: prediction?.note, }); + this.showPermissionNotification(pending, prediction !== undefined); + } + + /** Native notification arm of the prompt. "View Diff" opens the diff and + * re-raises the notification (a notification consumes itself on any + * click) so the user can still answer from it. */ + private showPermissionNotification(pending: PendingPermission, canDiff: boolean): void { const label = pending.request.message || `ATLAS wants to run '${pending.request.tool_name}'.`; - void vscode.window - .showInformationMessage(label, 'Allow Once', 'Allow for Session', 'Deny') - .then((choice) => { - if (choice === undefined) { - return; // dismissed — the card (or the timeout) decides + const buttons = canDiff + ? ['View Diff', 'Allow Once', 'Allow for Session', 'Deny'] + : ['Allow Once', 'Allow for Session', 'Deny']; + void vscode.window.showInformationMessage(label, ...buttons).then((choice) => { + if (choice === undefined) { + return; // dismissed — the card (or the timeout) decides + } + if (choice === 'View Diff') { + void this.openPermissionDiff(pending.id); + if (!pending.isSettled) { + this.showPermissionNotification(pending, canDiff); } - const map: Record = { - 'Allow Once': 'allow-once', - 'Allow for Session': 'allow-session', - Deny: 'deny', - }; - pending.settle(map[choice]); - }); + return; + } + const map: Record = { + 'Allow Once': 'allow-once', + 'Allow for Session': 'allow-session', + Deny: 'deny', + }; + pending.settle(map[choice]); + }); } /** Collapse the prompt card into its outcome line. A user deny renders no * extra row — the proxy's permission_denied event carries that (TUI * convention, avoids the duplicate). */ private resolvePermissionPrompt(pending: PendingPermission, reason: DismissReason): void { + this.permissionDiffs.delete(pending.id); let outcome: string; switch (reason) { case 'answered': @@ -282,6 +428,26 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { this.post({ type: 'permissionResolved', id: pending.id, outcome }); } + /** Read a proxy-workspace-relative path from the local workspace folder. + * undefined when there is no folder, the file does not exist, or it is + * not readable as UTF-8 text. */ + private async readLocalFile(target: string): Promise { + let absolute: string; + if (path.isAbsolute(target)) { + absolute = target; + } else { + const folder = vscode.workspace.workspaceFolders?.[0]; + if (!folder) { + return undefined; + } + absolute = path.join(folder.uri.fsPath, target); + } + try { + return await fs.readFile(absolute, 'utf8'); + } catch { + return undefined; + } + } private post(message: OutboundMessage): void { this.transcript.push(message); diff --git a/extensions/vscode/src/ui/diffProvider.ts b/extensions/vscode/src/ui/diffProvider.ts new file mode 100644 index 00000000..6b575279 --- /dev/null +++ b/extensions/vscode/src/ui/diffProvider.ts @@ -0,0 +1,61 @@ +// Virtual documents for diff rendering. Registers the `atlas-diff:` scheme +// (TextDocumentContentProvider — read-only by construction) and opens native +// side-by-side diffs via the built-in `vscode.diff` command. Used for +// permission-time predictions (editPreview.ts), post-result applied-change +// views, and edit_file's server-computed diff_preview text. + +import * as vscode from 'vscode'; + +export class DiffProvider implements vscode.TextDocumentContentProvider { + static readonly scheme = 'atlas-diff'; + + private readonly docs = new Map(); + private readonly order: string[] = []; + private seq = 0; + + register(): vscode.Disposable { + return vscode.workspace.registerTextDocumentContentProvider(DiffProvider.scheme, this); + } + + provideTextDocumentContent(uri: vscode.Uri): string { + return this.docs.get(uri.toString()) ?? ''; + } + + /** Open a native side-by-side diff. `filePath` is the tool's target path; + * its basename is kept in both virtual URIs so VS Code's language + * detection (and syntax highlighting) applies to both panes. */ + async openDiff(title: string, filePath: string, left: string, right: string): Promise { + const name = basename(filePath); + const leftUri = this.stash(`before/${name}`, left); + const rightUri = this.stash(`after/${name}`, right); + await vscode.commands.executeCommand('vscode.diff', leftUri, rightUri, title); + } + + /** Open a unified-diff text blob (edit_file's diff_preview) as a + * read-only virtual doc; the .diff suffix gets diff highlighting. */ + async openPreview(filePath: string, content: string): Promise { + const uri = this.stash(`preview/${basename(filePath)}.diff`, content); + const doc = await vscode.workspace.openTextDocument(uri); + await vscode.window.showTextDocument(doc, { preview: true }); + } + + private stash(name: string, content: string): vscode.Uri { + const uri = vscode.Uri.from({ scheme: DiffProvider.scheme, path: `/${this.seq++}/${name}` }); + const key = uri.toString(); + this.docs.set(key, content); + this.order.push(key); + // Bounded memory: an editor still open on an evicted doc just + // renders empty on the next provider read. + while (this.order.length > 64) { + this.docs.delete(this.order.shift() as string); + } + return uri; + } +} + +function basename(filePath: string): string { + const clean = filePath.replace(/[\\/]+$/, ''); + const index = Math.max(clean.lastIndexOf('/'), clean.lastIndexOf('\\')); + const name = index === -1 ? clean : clean.slice(index + 1); + return name === '' ? 'file' : name; +} diff --git a/extensions/vscode/test/editPreview.test.ts b/extensions/vscode/test/editPreview.test.ts new file mode 100644 index 00000000..77b69d6e --- /dev/null +++ b/extensions/vscode/test/editPreview.test.ts @@ -0,0 +1,164 @@ +// Unit tests for the permission-time edit predictions: write_file (new and +// existing file), edit_file (old_str found / not found / replace_all / +// literal-$ splice), and the ast_edit best-effort splices (python function +// incl. decorators and async, class, html tag incl. nesting and the +// doctype quirk, no-match fallback). + +import { describe, expect, it } from 'vitest'; +import { editTargetPath, predictEdit } from '../src/session/editPreview'; + +const PY = [ + 'import os', + '', + "@app.route('/dash')", + '@login_required', + 'def dashboard():', + ' users = get_users()', + '', + ' return render(users)', + '', + 'def other():', + ' pass', +].join('\n'); + +const HTML = [ + '', + '', + 't', + '', + '
inner
', + '', + '', +].join('\n'); + +describe('editTargetPath', () => { + it('returns the path for the three file-edit tools only', () => { + expect(editTargetPath('write_file', { path: 'a.py', content: 'x' })).toBe('a.py'); + expect(editTargetPath('edit_file', { path: 'a.py' })).toBe('a.py'); + expect(editTargetPath('ast_edit', { path: 'a.py' })).toBe('a.py'); + expect(editTargetPath('run_command', { command: 'ls' })).toBeUndefined(); + expect(editTargetPath('move_file', { source: 'a', destination: 'b' })).toBeUndefined(); + expect(editTargetPath('delete_file', { path: 'a.py' })).toBeUndefined(); + }); + + it('rejects malformed args', () => { + expect(editTargetPath('write_file', null)).toBeUndefined(); + expect(editTargetPath('write_file', { path: 42 })).toBeUndefined(); + }); +}); + +describe('predictEdit: write_file', () => { + it('new file: empty left, content right, exact', () => { + const p = predictEdit('write_file', { path: 'new.py', content: 'x = 1\n' }, undefined)!; + expect(p).toMatchObject({ kind: 'file', left: '', right: 'x = 1\n', approximate: false }); + expect(p.note).toBe('new file'); + }); + + it('existing file: current left, content right', () => { + const p = predictEdit('write_file', { path: 'a.py', content: 'new' }, 'old')!; + expect(p).toMatchObject({ kind: 'file', left: 'old', right: 'new', approximate: false }); + expect(p.note).toBeUndefined(); + }); + + it('missing content field is not previewable', () => { + expect(predictEdit('write_file', { path: 'a.py' }, 'old')).toBeUndefined(); + }); +}); + +describe('predictEdit: edit_file', () => { + it('old_str found: first occurrence replaced only', () => { + const p = predictEdit('edit_file', { path: 'a.txt', old_str: 'aa', new_str: 'bb' }, 'aa x aa')!; + expect(p).toMatchObject({ kind: 'file', approximate: false, right: 'bb x aa' }); + }); + + it('replace_all replaces every occurrence', () => { + const p = predictEdit('edit_file', { path: 'a.txt', old_str: 'aa', new_str: 'bb', replace_all: true }, 'aa x aa')!; + expect(p.right).toBe('bb x bb'); + }); + + it('splices new_str literally (no $-pattern semantics)', () => { + const p = predictEdit('edit_file', { path: 'a.txt', old_str: 'X', new_str: "$& $' $1", replace_all: false }, 'a X b')!; + expect(p.right).toBe("a $& $' $1 b"); + }); + + it('old_str not found: snippet diff plus note', () => { + const p = predictEdit('edit_file', { path: 'a.txt', old_str: 'zz', new_str: 'bb' }, 'aa x aa')!; + expect(p).toMatchObject({ kind: 'snippet', left: 'zz', right: 'bb', approximate: true }); + expect(p.note).toContain('old_str not found'); + }); + + it('file missing locally: snippet diff plus workspace note', () => { + const p = predictEdit('edit_file', { path: 'a.txt', old_str: 'zz', new_str: 'bb' }, undefined)!; + expect(p.kind).toBe('snippet'); + expect(p.note).toContain('not found in this workspace'); + }); +}); + +describe('predictEdit: ast_edit python', () => { + it('function: replaces the def block including its decorators', () => { + const content = "@app.route('/dash')\ndef dashboard():\n return quick()"; + const p = predictEdit('ast_edit', { path: 'app.py', selector: 'function:dashboard', content }, PY)!; + expect(p.kind).toBe('file'); + expect(p.approximate).toBe(true); + expect(p.right).toContain('return quick()'); + // Old decorators and body are gone; sibling function untouched. + expect(p.right).not.toContain('@login_required'); + expect(p.right).not.toContain('get_users'); + expect(p.right).toContain('def other():'); + expect(p.right).toContain('import os'); + }); + + it('function: matches async def', () => { + const source = 'async def fetch():\n return await go()\n\nprint(1)'; + const p = predictEdit('ast_edit', { path: 'a.py', selector: 'function:fetch', content: 'async def fetch():\n return 2' }, source)!; + expect(p.right).toBe('async def fetch():\n return 2\n\nprint(1)'); + }); + + it('class: replaces the class body, keeps surroundings', () => { + const source = 'class A:\n def m(self):\n pass\n\nclass B(Base):\n x = 1\n\ntail = 2'; + const p = predictEdit('ast_edit', { path: 'a.py', selector: 'class:B', content: 'class B(Base):\n x = 9' }, source)!; + expect(p.right).toBe('class A:\n def m(self):\n pass\n\nclass B(Base):\n x = 9\n\ntail = 2'); + }); + + it('no match: whole file vs content, labeled approximate', () => { + const p = predictEdit('ast_edit', { path: 'app.py', selector: 'function:missing', content: 'def missing():\n pass' }, PY)!; + expect(p).toMatchObject({ kind: 'file', left: PY, approximate: true }); + expect(p.right).toBe('def missing():\n pass'); + expect(p.note).toContain('not matched locally'); + }); + + it('duplicate names: first one replaced, note says so', () => { + const source = 'def f():\n pass\n\ndef f():\n return 1'; + const p = predictEdit('ast_edit', { path: 'a.py', selector: 'function:f', content: 'def f():\n return 9' }, source)!; + expect(p.right).toBe('def f():\n return 9\n\ndef f():\n return 1'); + expect(p.note).toContain('2 definitions'); + }); +}); + +describe('predictEdit: ast_edit html', () => { + it(': nesting-aware replace of the whole element', () => { + const p = predictEdit('ast_edit', { path: 'i.html', selector: '', content: '

new

' }, HTML)!; + expect(p.right).toContain('

new

'); + expect(p.right).not.toContain('outer'); + expect(p.right).toContain('t'); + }); + + it('
: matching close found past the nested same-tag', () => { + const p = predictEdit('ast_edit', { path: 'i.html', selector: '
', content: '
x
' }, HTML)!; + expect(p.right).toContain('
x
\n'); + expect(p.right).not.toContain('inner'); + }); + + it(': strips the leading doctype from content (proxy quirk)', () => { + const content = '\nn'; + const p = predictEdit('ast_edit', { path: 'i.html', selector: '', content }, HTML)!; + // Original doctype stays, content's duplicate is dropped. + expect(p.right).toBe('\nn'); + }); + + it('tag absent: whole-file approximate fallback', () => { + const p = predictEdit('ast_edit', { path: 'i.html', selector: '', content: '
' }, HTML)!; + expect(p.left).toBe(HTML); + expect(p.note).toContain('not matched locally'); + }); +}); From 81923ca811d1b5360e3527ed11fd1c3da4a40af9 Mon Sep 17 00:00:00 2001 From: Anuj-72 Date: Sun, 19 Jul 2026 19:42:20 +0530 Subject: [PATCH 06/13] feat(vscode): add status bar, mismatch detection, error mapping, progress, CI Poll /ready for a status bar item (pauses while streaming), warn on workspace/proxy mount mismatch, map all 12 proxy error codes to friendly messages, show a live progress line, and add a paths-filtered CI workflow for the extension. --- .github/workflows/vscode-extension.yml | 56 +++++ extensions/vscode/README.md | 2 +- extensions/vscode/media/chat.css | 75 +++++++ extensions/vscode/media/chat.js | 94 +++++++++ extensions/vscode/package.json | 8 + extensions/vscode/src/client/atlasClient.ts | 17 ++ extensions/vscode/src/client/types.ts | 86 ++++++++ extensions/vscode/src/extension.ts | 24 ++- extensions/vscode/src/ui/chatView.ts | 223 +++++++++++++++++--- extensions/vscode/src/ui/statusBar.ts | 176 +++++++++++++++ extensions/vscode/src/util/errors.ts | 60 ++++++ extensions/vscode/src/workspace/mismatch.ts | 196 +++++++++++++++++ extensions/vscode/test/errors.test.ts | 80 +++++++ extensions/vscode/test/mismatch.test.ts | 173 +++++++++++++++ 14 files changed, 1240 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/vscode-extension.yml create mode 100644 extensions/vscode/src/ui/statusBar.ts create mode 100644 extensions/vscode/src/util/errors.ts create mode 100644 extensions/vscode/src/workspace/mismatch.ts create mode 100644 extensions/vscode/test/errors.test.ts create mode 100644 extensions/vscode/test/mismatch.test.ts diff --git a/.github/workflows/vscode-extension.yml b/.github/workflows/vscode-extension.yml new file mode 100644 index 00000000..eb77e7be --- /dev/null +++ b/.github/workflows/vscode-extension.yml @@ -0,0 +1,56 @@ +name: vscode-extension + +# Code-level checks for the VS Code extension (extensions/vscode/) — +# type-check, lint, unit tests, and a production bundle. Kept out of +# test.yml's Go/Python matrix: the extension is its own npm ecosystem and +# only needs to run when its files change. + +on: + push: + branches: [main, dev] + paths: + - 'extensions/vscode/**' + - '.github/workflows/vscode-extension.yml' + pull_request: + branches: [main, dev] + paths: + - 'extensions/vscode/**' + - '.github/workflows/vscode-extension.yml' + +concurrency: + group: vscode-extension-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build-and-test: + name: lint + test + build + runs-on: ubuntu-latest + defaults: + run: + working-directory: extensions/vscode + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: '20' + cache: npm + cache-dependency-path: extensions/vscode/package-lock.json + + - name: install + run: npm ci + + - name: type-check + run: npm run check-types + + - name: lint + run: npm run lint + + - name: test + run: npm test + + - name: bundle (production) + run: node esbuild.js --production diff --git a/extensions/vscode/README.md b/extensions/vscode/README.md index 66f5711b..26a41aeb 100644 --- a/extensions/vscode/README.md +++ b/extensions/vscode/README.md @@ -2,7 +2,7 @@ A VS Code client for the [ATLAS](https://github.com/itigges22/ATLAS) agent proxy — a thin UI layer wrapping `atlas-proxy`'s agent loop (chat, tool calls, permission gating, diffs) with no agent logic in the extension itself. -**Status: Work in progress.** Tracking [issue #35](https://github.com/itigges22/ATLAS/issues/35). Chat, permission flow, and diff review are implemented; status bar and the workspace-mismatch warning land in upcoming commits. +**Status: Work in progress.** Tracking [issue #35](https://github.com/itigges22/ATLAS/issues/35). Chat, permission flow, diff review, status bar, and the workspace-mismatch warning are implemented. ## Diff review diff --git a/extensions/vscode/media/chat.css b/extensions/vscode/media/chat.css index ae0e8db2..7287cfa4 100644 --- a/extensions/vscode/media/chat.css +++ b/extensions/vscode/media/chat.css @@ -91,6 +91,81 @@ body { text-align: center; } +.badge { + align-self: flex-start; + font-size: 0.85em; + padding: 2px 8px; + border-radius: 3px; + border-left: 3px solid var(--vscode-editorInfo-foreground, #3794ff); + background: var(--vscode-textBlockQuote-background, transparent); + opacity: 0.9; +} + +details.reasoning { + align-self: flex-start; + max-width: 95%; + font-size: 0.9em; + opacity: 0.75; +} + +details.reasoning summary { + cursor: pointer; + font-style: italic; +} + +.reasoning-body { + white-space: pre-wrap; + word-break: break-word; + padding: 4px 0 0 12px; +} + +details.plan-card { + align-self: stretch; + padding: 6px 10px; + border-radius: 4px; + border: 1px solid var(--vscode-widget-border, var(--vscode-input-border, transparent)); + background: var(--vscode-editor-inactiveSelectionBackground); + font-size: 0.9em; +} + +details.plan-card summary { + cursor: pointer; + font-weight: 600; +} + +.plan-steps { + display: flex; + flex-direction: column; + gap: 2px; + padding: 4px 0 0 4px; +} + +.plan-step { + display: flex; + gap: 6px; + align-items: baseline; +} + +.plan-step.done { + opacity: 0.65; + text-decoration: line-through; +} + +.plan-box { + flex: none; +} + +#progress { + padding: 2px 10px; + font-size: 0.85em; + font-style: italic; + opacity: 0.75; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + border-top: 1px solid var(--vscode-widget-border, var(--vscode-input-border, transparent)); +} + .error-card { align-self: stretch; padding: 6px 10px; diff --git a/extensions/vscode/media/chat.js b/extensions/vscode/media/chat.js index 51143bd1..0f204ad4 100644 --- a/extensions/vscode/media/chat.js +++ b/extensions/vscode/media/chat.js @@ -17,10 +17,19 @@ /** The assistant bubble currently receiving streamed text, if any. */ let openAssistantEl = null; + /** The collapsible "Thinking…" section currently receiving reasoning + * deltas, if any. Closed alongside the assistant bubble. */ + let openReasoningEl = null; /** Tool chips awaiting a tool_result, keyed by tool name (FIFO per name). */ const pendingChips = new Map(); /** Open permission cards keyed by prompt id. */ const permissionCards = new Map(); + /** Current plan checklist: step id -> row element. A new planLoaded + * (revision) replaces the map — old checklists stay in the log, frozen. */ + let planSteps = new Map(); + + /** Single transient progress line pinned above the composer. */ + const progressEl = document.getElementById('progress'); function scrollToBottom() { messagesEl.scrollTop = messagesEl.scrollHeight; @@ -39,6 +48,7 @@ function closeAssistantBubble() { openAssistantEl = null; + openReasoningEl = null; } function assistantBubble() { @@ -48,6 +58,68 @@ return openAssistantEl; } + function reasoningSection() { + if (!openReasoningEl) { + const details = document.createElement('details'); + details.className = 'reasoning'; + const summary = document.createElement('summary'); + summary.textContent = 'Thinking…'; + details.appendChild(summary); + const body = document.createElement('div'); + body.className = 'reasoning-body'; + details.appendChild(body); + messagesEl.appendChild(details); + scrollToBottom(); + openReasoningEl = body; + } + return openReasoningEl; + } + + function addPlanChecklist(steps, revision) { + planSteps = new Map(); + const card = document.createElement('details'); + card.className = 'plan-card'; + card.open = true; + const summary = document.createElement('summary'); + summary.textContent = revision > 0 ? 'Plan (revision ' + revision + ')' : 'Plan'; + card.appendChild(summary); + const list = document.createElement('div'); + list.className = 'plan-steps'; + for (const step of steps) { + const row = document.createElement('div'); + row.className = 'plan-step'; + const box = document.createElement('span'); + box.className = 'plan-box'; + box.textContent = '☐'; + row.appendChild(box); + const label = document.createElement('span'); + label.textContent = step.label; + row.appendChild(label); + list.appendChild(row); + planSteps.set(step.id, row); + } + card.appendChild(list); + messagesEl.appendChild(card); + scrollToBottom(); + } + + function checkPlanStep(stepId) { + const row = planSteps.get(stepId); + if (!row) { + return; + } + row.classList.add('done'); + const box = row.querySelector('.plan-box'); + if (box) { + box.textContent = '☑'; + } + } + + function setProgress(text) { + progressEl.textContent = text; + progressEl.hidden = text === ''; + } + function addToolChip(name, detail) { const chip = document.createElement('div'); chip.className = 'chip pending'; @@ -197,7 +269,10 @@ messagesEl.textContent = ''; pendingChips.clear(); permissionCards.clear(); + planSteps.clear(); openAssistantEl = null; + openReasoningEl = null; + setProgress(''); } window.addEventListener('message', (event) => { @@ -211,6 +286,10 @@ assistantBubble().textContent += message.text; scrollToBottom(); break; + case 'reasoningDelta': + reasoningSection().textContent += message.text; + scrollToBottom(); + break; case 'toolCall': closeAssistantBubble(); addToolChip(message.name, message.detail); @@ -222,6 +301,19 @@ closeAssistantBubble(); appendBlock('note', message.text); break; + case 'badge': + appendBlock('badge', message.text); + break; + case 'planLoaded': + closeAssistantBubble(); + addPlanChecklist(message.steps, message.revision); + break; + case 'planStep': + checkPlanStep(message.stepId); + break; + case 'progress': + setProgress(message.text); + break; case 'permissionPrompt': closeAssistantBubble(); addPermissionCard(message.id, message.tool, message.detail, message.message, message.canDiff, message.note); @@ -231,9 +323,11 @@ break; case 'turnDone': closeAssistantBubble(); + setProgress(''); break; case 'turnError': closeAssistantBubble(); + setProgress(''); appendBlock('error-card', message.message); break; case 'busy': diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index d1f3d246..4ec5d365 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -37,6 +37,14 @@ { "command": "atlas.newConversation", "title": "ATLAS: New Conversation" + }, + { + "command": "atlas.refreshStatus", + "title": "ATLAS: Refresh Status & Calibration" + }, + { + "command": "atlas.statusMenu", + "title": "ATLAS: Status Menu" } ], "viewsContainers": { diff --git a/extensions/vscode/src/client/atlasClient.ts b/extensions/vscode/src/client/atlasClient.ts index 863fe153..c0b96e6b 100644 --- a/extensions/vscode/src/client/atlasClient.ts +++ b/extensions/vscode/src/client/atlasClient.ts @@ -12,6 +12,7 @@ import { parseSSEStream } from './sse'; import type { AgentRequest, + CalibrationStatusResponse, ChatEvent, ErrorEnvelope, PermissionDecisionRequest, @@ -170,4 +171,20 @@ export class AtlasClient { } return (await response.json()) as VersionResponse; } + + /** + * GET /v1/calibration/status — lens/ASA verdict for the loaded model. + * Uncached server-side (each call re-probes the lens service), so callers + * hit it only at activation and on explicit user refresh. + */ + async getCalibrationStatus(): Promise { + const response = await fetch(`${this.baseUrl}/v1/calibration/status`, { + method: 'GET', + headers: this.headers(false), + }); + if (!response.ok) { + throw await toApiError(response); + } + return (await response.json()) as CalibrationStatusResponse; + } } diff --git a/extensions/vscode/src/client/types.ts b/extensions/vscode/src/client/types.ts index e5ffd1bb..d5f952ae 100644 --- a/extensions/vscode/src/client/types.ts +++ b/extensions/vscode/src/client/types.ts @@ -133,6 +133,76 @@ export interface PlanLoadedEventData { revision: number; } +/** plan_adherence fires after each tool call. Match shape carries + * step_index/step_id; miss shape carries tool/off_streak (+ neutral=true + * for recon tools that leave the off-streak unchanged). */ +export interface PlanAdherenceEventData { + matched: boolean; + step_index?: number; + step_id?: string; + step_action?: string; + satisfied: number; + total: number; + tool?: string; + off_streak?: number; + neutral?: boolean; +} + +export interface PlanReviseEventData { + reason: string; + /** 1-indexed. */ + revision: number; +} + +/** v3_progress — fallback for V3 stages without a dedicated typed event. */ +export interface V3ProgressEventData { + message: string; +} + +/** Common shape of the typed V3 stage events (v3_phase / v3_sandbox / + * v3_repair) — every one carries stage + detail plus stage-specific extras + * the progress line does not need. */ +export interface V3StageEventData { + stage: string; + detail: string; +} + +export interface V3LensVetoEventData { + stage: string; + detail: string; + /** Candidate index. */ + index: number; + gx_score_min: number; + first_off_rails_idx: number; +} + +export interface V3StructuralVetoEventData { + stage: string; + detail: string; + /** Candidate index. */ + index: number; + n_unresolved: number; + unresolved_calls: string[]; + n_calls_total: number; +} + +export interface AgentLensScoreEventData { + tool: 'write_file' | 'edit_file'; + turn: number; + n_tokens: number; + first_off_rails_idx: number; + gx_score_min: number; + gx_score_mean: number; + latency_ms: number; +} + +export interface AgentLensInterventionEventData { + turn: number; + tool: string; + /** The corrective injected into the next LLM call. */ + reason: string; +} + // --- Non-stream endpoint payloads. --- /** POST /cancel response. 200 → cancelled:true; 404 → cancelled:false. */ @@ -163,6 +233,22 @@ export interface VersionResponse { error_codes: string[]; } +/** GET /v1/calibration/status — lens + ASA compat verdict for the loaded + * model. Called once at activation and on manual refresh only: every call + * re-probes the lens service (~50–200 ms, docs/API.md). */ +export interface CalibrationStatusResponse { + lens: { + verdict: 'supported' | 'no-artifacts' | 'incomplete-artifacts' | 'uncalibrated' | 'dim-mismatch' | 'unreachable' | string; + hint?: string; + }; + asa: { + verdict: 'supported' | 'missing' | 'unverified' | 'incompatible' | string; + hint?: string; + }; + /** The seven status dimensions `atlas doctor` renders. */ + dimensions?: { name: string; status: string; detail: string }[]; +} + /** Closed error-code set from docs/API.md. Switch on `error`, never on * `detail` — the human message may change between versions. */ export type ErrorCode = diff --git a/extensions/vscode/src/extension.ts b/extensions/vscode/src/extension.ts index 2152b187..a5b7aad2 100644 --- a/extensions/vscode/src/extension.ts +++ b/extensions/vscode/src/extension.ts @@ -1,13 +1,17 @@ import * as vscode from 'vscode'; import { ChatViewProvider, TOKEN_SECRET_KEY } from './ui/chatView'; import { DiffProvider } from './ui/diffProvider'; +import { StatusBar } from './ui/statusBar'; export function activate(context: vscode.ExtensionContext) { const diffs = new DiffProvider(); - const chat = new ChatViewProvider(context.extensionUri, context.secrets, diffs); + const chat = new ChatViewProvider(context.extensionUri, context.secrets, diffs, context.workspaceState); + const statusBar = new StatusBar(() => chat.makeClient()); + chat.attachStatusBar(statusBar); context.subscriptions.push( diffs.register(), + statusBar, vscode.window.registerWebviewViewProvider(ChatViewProvider.viewType, chat, { webviewOptions: { retainContextWhenHidden: true }, @@ -25,6 +29,14 @@ export function activate(context: vscode.ExtensionContext) { chat.newConversation(); }), + vscode.commands.registerCommand('atlas.statusMenu', () => { + void statusBar.showMenu(); + }), + + vscode.commands.registerCommand('atlas.refreshStatus', () => { + void statusBar.refresh(); + }), + vscode.commands.registerCommand('atlas.setToken', async () => { const token = await vscode.window.showInputBox({ title: 'ATLAS: Set Service Token', @@ -42,8 +54,18 @@ export function activate(context: vscode.ExtensionContext) { await context.secrets.store(TOKEN_SECRET_KEY, token); void vscode.window.showInformationMessage('ATLAS: service token saved to Secret Storage.'); } + void statusBar.refresh(); // token change flips 401 state immediately + }), + + // Poll interval / enabled flag changes take effect without a reload. + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration('atlas.statusBar')) { + statusBar.applyConfig(); + } }), ); + + statusBar.start(); } export function deactivate() {} diff --git a/extensions/vscode/src/ui/chatView.ts b/extensions/vscode/src/ui/chatView.ts index b701d107..abcdeb3f 100644 --- a/extensions/vscode/src/ui/chatView.ts +++ b/extensions/vscode/src/ui/chatView.ts @@ -6,15 +6,23 @@ import { promises as fs } from 'node:fs'; import * as path from 'node:path'; import * as vscode from 'vscode'; -import { AtlasApiError, AtlasClient } from '../client/atlasClient'; +import { AtlasClient } from '../client/atlasClient'; import type { + AgentLensInterventionEventData, ErrorEventData, + LlmPromptProgressEventData, PermissionDeniedEventData, PermissionMode, PermissionRequestEventData, + PlanAdherenceEventData, + PlanLoadedEventData, + PlanReviseEventData, + ReasoningTokenEventData, TextEventData, ToolCallEventData, ToolResultEventData, + V3ProgressEventData, + V3StageEventData, } from '../client/types'; import { editTargetPath, predictEdit, type EditPrediction } from '../session/editPreview'; import { @@ -24,20 +32,31 @@ import { type PermissionChoice, } from '../session/permissionFlow'; import { TurnManager } from '../session/turnManager'; +import { renderError } from '../util/errors'; +import { MismatchDetector } from '../workspace/mismatch'; import { DiffProvider } from './diffProvider'; +import type { StatusBar } from './statusBar'; /** SecretStorage key for the service token ('ATLAS: Set Service Token'). */ export const TOKEN_SECRET_KEY = 'atlas.serviceToken'; +/** workspaceState key for the mismatch warning's "Don't show again". */ +export const MISMATCH_DISMISSED_KEY = 'atlas.mismatchWarningDismissed'; + /** Messages the extension posts INTO the webview (media/chat.js). */ type OutboundMessage = | { type: 'userMessage'; text: string } | { type: 'assistantDelta'; text: string } + | { type: 'reasoningDelta'; text: string } | { type: 'toolCall'; name: string; detail: string } | { type: 'toolResult'; tool: string; success: boolean; elapsed?: string; error?: string; diffId?: number } | { type: 'note'; text: string } + | { type: 'badge'; text: string } | { type: 'permissionPrompt'; id: number; tool: string; detail: string; message: string; canDiff: boolean; note?: string } | { type: 'permissionResolved'; id: number; outcome: string } + | { type: 'planLoaded'; steps: { id: string; label: string }[]; revision: number } + | { type: 'planStep'; stepId: string } + | { type: 'progress'; text: string } | { type: 'turnDone' } | { type: 'turnError'; message: string } | { type: 'reset' } @@ -107,10 +126,16 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { * synchronously calls back into showPermissionPrompt which claims it. */ private promptPrediction: EditPrediction | undefined; + /** Passive workspace-mismatch heuristic (see workspace/mismatch.ts). */ + private readonly mismatch: MismatchDetector; + /** Optional status bar — polling pauses while a turn streams. */ + private statusBar: StatusBar | undefined; + constructor( private readonly extensionUri: vscode.Uri, private readonly secrets: vscode.SecretStorage, private readonly diffs: DiffProvider, + private readonly workspaceState: vscode.Memento, ) { this.output = vscode.window.createOutputChannel('ATLAS'); this.permissions = new PermissionFlow(this.turns.sessionAllowedTools, { @@ -119,6 +144,25 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { onAutoAllow: (toolName) => this.post({ type: 'note', text: `'${toolName}' auto-allowed (approved for this session).` }), onPostError: (toolName, error) => this.log(`permission decision POST failed for '${toolName}'`, error), }); + this.mismatch = new MismatchDetector({ + stat: (relative) => this.statLocalFile(relative), + onMismatch: () => this.warnMismatch(), + }); + } + + /** Late-bound (the status bar is created after the view provider). */ + attachStatusBar(statusBar: StatusBar): void { + this.statusBar = statusBar; + } + + /** Build a client from current settings + stored token. Shared with the + * status bar poller via extension.ts. */ + async makeClient(): Promise { + const config = vscode.workspace.getConfiguration('atlas'); + const baseUrl = config.get('proxyUrl', 'http://localhost:8090'); + // Plaintext setting is a dev override; SecretStorage is the real home. + const token = config.get('serviceToken', '') || (await this.secrets.get(TOKEN_SECRET_KEY)) || ''; + return new AtlasClient({ baseUrl, token }); } resolveWebviewView(view: vscode.WebviewView): void { @@ -178,14 +222,12 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { } const config = vscode.workspace.getConfiguration('atlas'); - const baseUrl = config.get('proxyUrl', 'http://localhost:8090'); const mode = config.get('permissionMode', 'default'); - // Plaintext setting is a dev override; SecretStorage is the real home. - const token = config.get('serviceToken', '') || (await this.secrets.get(TOKEN_SECRET_KEY)) || ''; - const client = new AtlasClient({ baseUrl, token }); + const client = await this.makeClient(); this.post({ type: 'userMessage', text: message }); this.postTransient({ type: 'busy', value: true }); + this.statusBar?.setStreaming(true); try { for await (const event of this.turns.runTurn(client, message, mode)) { await this.dispatch(event.type, event.data, client); @@ -198,7 +240,10 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { // resolves pending requests when the turn ends. this.permissions.endTurn(); this.snapshots.clear(); + this.mismatch.reset(); + this.postTransient({ type: 'progress', text: '' }); this.postTransient({ type: 'busy', value: false }); + this.statusBar?.setStreaming(false); } } @@ -211,6 +256,13 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { } break; } + case 'reasoning_token': { + const payload = data as ReasoningTokenEventData; + if (typeof payload?.text === 'string') { + this.post({ type: 'reasoningDelta', text: payload.text }); + } + break; + } case 'tool_call': { const payload = data as ToolCallEventData; this.post({ type: 'toolCall', name: payload.name, detail: condenseArgs(payload.args) }); @@ -225,6 +277,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { queue.push({ path: target, before }); this.snapshots.set(payload.name, queue); } + await this.mismatch.recordToolCall(payload.name, payload.args); break; } case 'tool_result': { @@ -238,6 +291,9 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { error: payload.error, diffId, }); + // Fire-and-forget: the check sleeps its settle delay before + // stat-ing, and the stream must not wait on it. + void this.mismatch.recordToolResult(payload.tool, payload.success); break; } case 'permission_request': { @@ -265,6 +321,70 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { this.post({ type: 'note', text: `Permission denied for '${payload.tool}'.` }); break; } + case 'plan_loaded': { + const payload = data as PlanLoadedEventData; + if (Array.isArray(payload?.steps)) { + this.post({ + type: 'planLoaded', + steps: payload.steps.map((step) => ({ id: step.id, label: `${step.action} ${step.target}`.trim() })), + revision: payload.revision ?? 0, + }); + } + break; + } + case 'plan_adherence': { + const payload = data as PlanAdherenceEventData; + if (payload?.matched && typeof payload.step_id === 'string') { + this.post({ type: 'planStep', stepId: payload.step_id }); + } + break; + } + case 'plan_revise': { + const payload = data as PlanReviseEventData; + this.post({ type: 'note', text: `Plan going off track — revising (${payload?.reason || 'off-plan streak'}).` }); + break; + } + case 'llm_call_start': + this.postProgress('Thinking…'); + break; + case 'llm_prompt_progress': { + const payload = data as LlmPromptProgressEventData; + const pct = typeof payload?.pct === 'number' && payload.pct > 0 ? ` ${Math.round(payload.pct * 100)}%` : ''; + this.postProgress(`Processing prompt${pct}…`); + break; + } + case 'llm_first_token': + case 'llm_call_end': + this.postProgress(''); + break; + case 'v3_progress': { + const payload = data as V3ProgressEventData; + if (typeof payload?.message === 'string') { + this.postProgress(`V3: ${payload.message}`); + } + break; + } + case 'v3_phase': + case 'v3_sandbox': + case 'v3_repair': { + const payload = data as V3StageEventData; + if (typeof payload?.stage === 'string') { + this.postProgress(`V3: ${payload.stage}${payload.detail ? ` — ${payload.detail}` : ''}`); + } + break; + } + case 'v3_lens_veto': + case 'v3_structural_veto': { + const payload = data as V3StageEventData; + const why = type === 'v3_lens_veto' ? 'lens quality veto' : 'unresolved-call veto'; + this.post({ type: 'badge', text: `Candidate rejected (${why})${payload?.detail ? `: ${payload.detail}` : ''}` }); + break; + } + case 'agent_lens_intervention': { + const payload = data as AgentLensInterventionEventData; + this.post({ type: 'badge', text: `Lens intervention: ${payload?.reason || 'corrective queued'}` }); + break; + } case 'error': { const payload = data as ErrorEventData; this.post({ type: 'turnError', message: payload.error || 'unknown stream error' }); @@ -273,12 +393,28 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { case 'done': // Bubble finalization happens on generator completion. break; + // High-volume / TUI-internal streams the panel deliberately drops: + // llm_token duplicates the JSON tool-call content, v3 token streams + // churn the DOM for no user signal, agent_lens_score fires per write. + case 'llm_token': + case 'v3_token': + case 'v3_llm_start': + case 'v3_llm_end': + case 'v3_reasoning_token': + case 'agent_lens_score': + break; default: // Forward compatibility: unknown event types are logged, never fatal. this.log(`unhandled event '${type}'`, data); } } + /** Single-line progress indicator under the newest message. Transient: + * replay after a reload would show a stale spinner. */ + private postProgress(text: string): void { + this.postTransient({ type: 'progress', text }); + } + /** Consume the pending snapshot for a tool_result and, when there is * something to show, capture the on-disk "after" state and register a * viewable applied diff. Returns its id, or undefined. */ @@ -338,21 +474,17 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { this.post({ type: 'note', text: 'Turn cancelled.' }); return; } - if (error instanceof AtlasApiError) { - this.post({ type: 'turnError', message: `${error.code || 'request failed'}: ${error.detail || error.message}` }); - if (error.code === 'unauthorized') { - void vscode.window - .showErrorMessage('ATLAS proxy rejected the request (unauthorized).', 'Set Token') - .then((choice) => { - if (choice === 'Set Token') { - void vscode.commands.executeCommand('atlas.setToken'); - } - }); - } - return; + const rendered = renderError(error); + this.post({ type: 'turnError', message: rendered.message }); + if (rendered.action === 'set-token') { + void vscode.window.showErrorMessage(rendered.message, 'Set Token').then((choice) => { + if (choice === 'Set Token') { + void vscode.commands.executeCommand('atlas.setToken'); + } + }); + } else if (rendered.prominent) { + void vscode.window.showErrorMessage(rendered.message); } - const message = error instanceof Error ? error.message : String(error); - this.post({ type: 'turnError', message: `Could not reach the ATLAS proxy: ${message}` }); } /** Show a permission prompt on both surfaces: an inline webview card @@ -432,15 +564,9 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { * undefined when there is no folder, the file does not exist, or it is * not readable as UTF-8 text. */ private async readLocalFile(target: string): Promise { - let absolute: string; - if (path.isAbsolute(target)) { - absolute = target; - } else { - const folder = vscode.workspace.workspaceFolders?.[0]; - if (!folder) { - return undefined; - } - absolute = path.join(folder.uri.fsPath, target); + const absolute = this.resolveLocalPath(target); + if (absolute === undefined) { + return undefined; } try { return await fs.readFile(absolute, 'utf8'); @@ -449,6 +575,46 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { } } + private resolveLocalPath(target: string): string | undefined { + if (path.isAbsolute(target)) { + return target; + } + const folder = vscode.workspace.workspaceFolders?.[0]; + return folder === undefined ? undefined : path.join(folder.uri.fsPath, target); + } + + /** Stat arm of the mismatch heuristic. Unresolvable (no folder) maps to + * absent — the detector's verdicts treat that conservatively. */ + private async statLocalFile(target: string): Promise<{ exists: boolean; mtimeMs?: number }> { + const absolute = this.resolveLocalPath(target); + if (absolute === undefined) { + return { exists: false }; + } + try { + const stat = await fs.stat(absolute); + return { exists: true, mtimeMs: stat.mtimeMs }; + } catch { + return { exists: false }; + } + } + + /** One-time warning when local disk state contradicts a successful file + * op — the proxy is likely mounted on a different directory. */ + private warnMismatch(): void { + if (this.workspaceState.get(MISMATCH_DISMISSED_KEY, false)) { + return; + } + const message = + 'ATLAS applied a file change, but this workspace does not reflect it. ' + + 'The proxy is likely mounted on a different directory — restart ATLAS from this folder (see README).'; + this.post({ type: 'note', text: message }); + void vscode.window.showWarningMessage(message, "Don't Show Again").then((choice) => { + if (choice === "Don't Show Again") { + void this.workspaceState.update(MISMATCH_DISMISSED_KEY, true); + } + }); + } + private post(message: OutboundMessage): void { this.transcript.push(message); void this.view?.webview.postMessage(message); @@ -492,6 +658,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
+