diff --git a/design/qwen-code-electron-desktop-implementation-plan.md b/design/qwen-code-electron-desktop-implementation-plan.md new file mode 100644 index 000000000..6b8079ebb --- /dev/null +++ b/design/qwen-code-electron-desktop-implementation-plan.md @@ -0,0 +1,226 @@ +# Qwen Code Electron Desktop Implementation Plan + +This plan tracks the incremental MVP implementation for the Electron desktop +client described in +`docs/design/qwen-code-electron-desktop/qwen-code-electron-desktop-architecture.md`. +The architecture document is the source of truth; this file records execution +order, verification, decisions, and remaining work. + +## Ground Rules + +- Use Electron only; do not introduce Tauri. +- Keep the desktop shell thin: Electron main owns windows, native IPC, and the + local server lifecycle. +- Reuse Qwen Code ACP, core services, and shared web UI as later slices reach + those layers. +- Renderer must run with `nodeIntegration: false`, context isolation enabled, + and a preload whitelist. +- The local server must bind only `127.0.0.1`, use a random token, and reject + unauthorized requests. +- Every completed slice must leave passing targeted checks and a conventional + commit. + +## Task Breakdown + +### Slice 1: Desktop Workspace Skeleton and Health Service + +- Status: complete +- Goal: add the first runnable desktop package with Electron main/preload, + React renderer, and a local authenticated `/health` endpoint. +- Files: + - `packages/desktop/package.json` + - `packages/desktop/tsconfig*.json` + - `packages/desktop/vite.config.ts` + - `packages/desktop/src/main/**` + - `packages/desktop/src/preload/**` + - `packages/desktop/src/server/**` + - `packages/desktop/src/renderer/**` + - `scripts/build.js` + - `package-lock.json` +- Acceptance criteria: + - `packages/desktop` is recognized as an npm workspace. + - Main starts `DesktopServer` before creating the window. + - Preload exposes only typed `qwenDesktop` methods. + - Renderer fetches server info through preload and calls `/health` with a + bearer token. + - `/health` returns success only for valid token and allowed origin. + - Desktop build is included after reusable packages in the root build order. +- Verification: + - `npm install --workspace=packages/desktop` + - `npm run test --workspace=packages/desktop` + - `npm run typecheck --workspace=packages/desktop` + - `npm run build --workspace=packages/desktop` + +### Slice 2: Desktop Server Runtime Surface + +- Status: pending +- Goal: add `/api/runtime` and typed error responses that expose CLI path, + platform, desktop version, and auth/account placeholders without spawning ACP. +- Files: + - `packages/desktop/src/server/http/router.ts` + - `packages/desktop/src/server/services/runtimeService.ts` + - `packages/desktop/src/renderer/api/client.ts` + - `packages/desktop/src/renderer/App.tsx` +- Acceptance criteria: + - Runtime route is token protected. + - Renderer shows runtime summary without exposing secrets. + - Tests cover success, unauthorized, and unknown route errors. +- Verification: + - `npm run test --workspace=packages/desktop` + - `npm run typecheck --workspace=packages/desktop` + - `npm run build --workspace=packages/desktop` + +### Slice 3: ACP Process Client Wrapper + +- Status: pending +- Goal: implement a desktop-local ACP child-process client around + `qwen --acp --channel=Desktop`. +- Files: + - `packages/desktop/src/server/acp/AcpProcessClient.ts` + - `packages/desktop/src/server/acp/AcpEventRouter.ts` + - `packages/desktop/src/server/services/sessionService.ts` +- Acceptance criteria: + - Development mode can spawn the repository CLI ACP entrypoint. + - Production path is isolated behind a resolver for packaged `dist/cli.js`. + - Tests mock ACP transport and cover initialize, list, new, load, and close. +- Verification: + - `npm run test --workspace=packages/desktop` + - `npm run typecheck --workspace=packages/desktop` + - targeted ACP smoke command when credentials are not required + +### Slice 4: Session REST API + +- Status: pending +- Goal: add session create/list/load/delete/rename endpoints backed by ACP. +- Files: + - `packages/desktop/src/server/http/router.ts` + - `packages/desktop/src/server/services/sessionService.ts` + - `packages/desktop/src/renderer/stores/sessionStore.ts` +- Acceptance criteria: + - Session routes enforce token and origin rules. + - Renderer can create a session for a selected cwd and list existing sessions. + - Failed ACP operations return typed retryable/non-retryable errors. +- Verification: + - `npm run test --workspace=packages/desktop` + - `npm run typecheck --workspace=packages/desktop` + - manual DesktopServer smoke with fake ACP client + +### Slice 5: WebSocket Chat Loop + +- Status: pending +- Goal: add per-session WS connections and send user prompts through ACP. +- Files: + - `packages/desktop/src/server/ws/SessionSocketHub.ts` + - `packages/desktop/src/server/acp/AcpEventRouter.ts` + - `packages/desktop/src/renderer/api/websocket.ts` + - `packages/desktop/src/renderer/stores/chatStore.ts` +- Acceptance criteria: + - WS handshake validates session id and token. + - One active prompt per session is enforced. + - Renderer receives normalized assistant/tool/usage events. +- Verification: + - `npm run test --workspace=packages/desktop` + - fake ACP integration test for prompt and stream completion + +### Slice 6: Permission Bridge + +- Status: pending +- Goal: route ACP permission and ask-user-question callbacks to renderer and + resolve responses with timeout cancellation. +- Files: + - `packages/desktop/src/server/acp/permissionBridge.ts` + - `packages/desktop/src/server/ws/SessionSocketHub.ts` + - `packages/desktop/src/renderer/stores/chatStore.ts` +- Acceptance criteria: + - Permission requests are visible to the active session. + - Closing a WS connection cancels pending requests. + - Timeout defaults to deny/cancel. +- Verification: + - `npm run test --workspace=packages/desktop` + - renderer store tests for allow, deny, and timeout state + +### Slice 7: Settings, Auth, Model, and Mode UI + +- Status: pending +- Goal: expose settings/auth/model/mode controls while reusing Qwen Code + configuration semantics. +- Files: + - `packages/desktop/src/server/services/settingsService.ts` + - `packages/desktop/src/server/services/runtimeService.ts` + - `packages/desktop/src/renderer/stores/settingsStore.ts` + - `packages/desktop/src/renderer/stores/modelStore.ts` +- Acceptance criteria: + - Settings writes target the existing Qwen settings locations. + - Auth actions go through ACP or shared settings writer logic. + - Approval mode values remain `plan/default/auto-edit/yolo`. +- Verification: + - `npm run test --workspace=packages/desktop` + - temp HOME/QWEN_RUNTIME_DIR settings tests + +### Slice 8: Packaging and Smoke Test + +- Status: pending +- Goal: package a desktop app that can launch the bundled CLI ACP child. +- Files: + - `packages/desktop/electron-builder.*` + - `scripts/build.js` + - `scripts/copy_bundle_assets.js` +- Acceptance criteria: + - Packaged app starts renderer and DesktopServer. + - Production ACP launch uses `ELECTRON_RUN_AS_NODE=1`. + - Required CLI bundle and native/vendor resources are present. +- Verification: + - `npm run build` + - `npm run typecheck` + - desktop packaging smoke command + +## Decision Log + +- 2026-04-25: Use a main-process hosted `DesktopServer` for MVP, matching the + architecture recommendation and keeping the HTTP/WS boundary ready for a + future `utilityProcess` move. +- 2026-04-25: Use the latest stable Electron line available during this slice. + Electron releases list Electron 41.3.0 with Node.js 24.15.0, satisfying the + repository runtime requirement of Node >=20. +- 2026-04-25: Implement the first server routes with Node built-ins instead of + adding Express/Fastify. The current surface is small and this avoids + committing to an HTTP framework before the ACP routing shape is known. +- 2026-04-25: Allow CORS preflight without bearer auth, but only for allowed + app origins. Actual REST requests remain bearer-token protected. + +## Verification Log + +- 2026-04-25 Slice 1: + - `npm install --ignore-scripts --workspace=@qwen-code/desktop` passed. + - `npx prettier --check design/qwen-code-electron-desktop-implementation-plan.md scripts/build.js packages/desktop` passed. + - `npm run lint --workspace=packages/desktop` passed. + - `npm run test --workspace=packages/desktop` passed: 1 file, 4 tests. + - `npm run typecheck --workspace=packages/desktop` passed. + - `npm run build --workspace=packages/desktop` passed. + - `npm exec --workspace=packages/desktop -- electron --version` passed: + `v41.3.0`. + - `npm run typecheck` passed across workspaces. + - `npm run build` passed across the configured build order. Existing VS Code + companion lint warnings were reported by its build script, with no errors. + +## Self Review Notes + +- 2026-04-25 Slice 1: + - Security boundary checked: renderer uses `nodeIntegration: false`, + context isolation, a bundled preload whitelist, and no arbitrary IPC. + - Local server checked: binds `127.0.0.1`, generates a random token by + default, requires bearer auth for real routes, and rejects non-local + origins. + - CORS preflight intentionally remains unauthenticated but origin-gated so + packaged `file://` and dev `127.0.0.1` renderers can send authorization + headers. + - Fixed self-review issues before completion: guarded app startup behind the + Electron single-instance lock, tightened bearer parsing, and removed unused + direct WebSocket dependencies until the WS slice. + +## Remaining Work + +- Commit Slice 1. +- Continue with Slice 2 runtime route and typed server error surface. +- Continue through the ACP, session, WebSocket, permission, settings, and + packaging slices until the architecture MVP is fully verified. diff --git a/package-lock.json b/package-lock.json index ecb84a6b5..b29b93aee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -236,6 +236,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -711,6 +712,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -734,10 +736,78 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@electron/get/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.6", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", @@ -2175,6 +2245,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2823,6 +2894,10 @@ "resolved": "packages/channels/weixin", "link": true }, + "node_modules/@qwen-code/desktop": { + "resolved": "packages/desktop", + "link": true + }, "node_modules/@qwen-code/qwen-code": { "resolved": "packages/cli", "link": true @@ -3215,6 +3290,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@storybook/addon-a11y": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.2.0.tgz", @@ -3478,6 +3566,19 @@ "node": ">=6" } }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@teddyzhu/clipboard": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/@teddyzhu/clipboard/-/clipboard-0.0.5.tgz", @@ -3597,6 +3698,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3763,6 +3865,19 @@ "@types/node": "*" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/chai": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", @@ -3907,6 +4022,13 @@ "integrity": "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==", "license": "MIT" }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -3928,6 +4050,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -4068,6 +4200,7 @@ "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4078,6 +4211,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4099,6 +4233,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/semver": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", @@ -4283,6 +4427,7 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -4528,6 +4673,7 @@ "integrity": "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.6.1", @@ -4678,6 +4824,7 @@ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", @@ -4851,6 +4998,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5265,8 +5413,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/array-includes": { "version": "3.1.9", @@ -5736,6 +5883,15 @@ "node": ">= 0.8" } }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/boxen": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", @@ -5814,6 +5970,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -5877,6 +6034,51 @@ "node": ">=8" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -6284,6 +6486,19 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/code-excerpt": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", @@ -6474,7 +6689,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -6792,6 +7006,35 @@ "dev": true, "license": "MIT" }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -6855,6 +7098,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -6940,6 +7193,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -7185,6 +7446,25 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/electron": { + "version": "41.3.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-41.3.0.tgz", + "integrity": "sha512-2Q5aeocmFdeheZGDUTrAvSR3t+n0c3d104AJWWEnt7syJU0tE4VdibMYaPtQ47QuXSoUf0/xSsfUUvu/uSXIfg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^24.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.262", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", @@ -7192,6 +7472,23 @@ "dev": true, "license": "ISC" }, + "node_modules/electron/node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -7238,6 +7535,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -7450,6 +7757,14 @@ "benchmarks" ] }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/esbuild": { "version": "0.25.6", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", @@ -7552,6 +7867,7 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8256,7 +8572,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -8318,7 +8633,6 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -8328,7 +8642,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -8338,7 +8651,6 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -8546,7 +8858,6 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", - "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -8565,7 +8876,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -8574,15 +8884,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/finalhandler/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -9084,6 +9392,25 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, "node_modules/global-directory": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", @@ -9141,6 +9468,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -9394,6 +9747,13 @@ "entities": "^4.4.0" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -9433,6 +9793,20 @@ "node": ">= 14" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -9639,6 +10013,7 @@ "resolved": "https://registry.npmjs.org/ink/-/ink-6.2.3.tgz", "integrity": "sha512-fQkfEJjKbLXIcVWEE3MvpYSnwtbbmRsmeNDNz1pIuOFlwE+UF2gsy228J36OXKZGWJWZJKUigphBSqCNMcARtg==", "license": "MIT", + "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.0", "ansi-escapes": "^7.0.0", @@ -10616,6 +10991,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -10773,6 +11149,14 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -11312,6 +11696,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/lowlight": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", @@ -11409,6 +11803,20 @@ "node": ">= 18" } }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -11495,7 +11903,6 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -11557,6 +11964,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -11909,6 +12326,19 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-normalize-package-bin": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", @@ -12461,6 +12891,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -12678,8 +13118,7 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/path-type": { "version": "3.0.0", @@ -12876,6 +13315,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13035,6 +13475,7 @@ "integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -13097,6 +13538,16 @@ "dev": true, "license": "MIT" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -13287,6 +13738,19 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/qwen-code-vscode-ide-companion": { "resolved": "packages/vscode-ide-companion", "link": true @@ -13350,6 +13814,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13360,6 +13825,7 @@ "integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -13437,6 +13903,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13824,6 +14291,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -13844,6 +14318,19 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -13923,6 +14410,33 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/roarr/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/rollup": { "version": "4.44.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz", @@ -14155,6 +14669,14 @@ "node": ">=10" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -14224,6 +14746,37 @@ "node": ">= 0.8" } }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -14620,6 +15173,7 @@ "integrity": "sha512-fIQnFtpksRRgHR1CO1onGX3djaog4qsW/c5U8arqYTkUEr2TaWpn05mIJDOBoPJFlOdqFrB4Ttv0PZJxV7avhw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", @@ -14983,6 +15537,19 @@ "node": ">= 6" } }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -15308,6 +15875,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -15507,7 +16075,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.20.3", @@ -15515,6 +16084,7 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -15673,6 +16243,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15996,7 +16567,6 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4.0" } @@ -16039,6 +16609,7 @@ "integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", @@ -16152,6 +16723,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16165,6 +16737,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -16683,6 +17256,7 @@ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -16853,6 +17427,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -17023,6 +17598,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", @@ -17681,6 +18257,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", @@ -18075,6 +18652,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -18204,6 +18782,519 @@ "zod": "^3.25 || ^4" } }, + "packages/desktop": { + "name": "@qwen-code/desktop", + "version": "0.15.2", + "dependencies": { + "@qwen-code/webui": "file:../webui", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/node": "^20.11.24", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.2.0", + "electron": "^41.3.0", + "esbuild": "^0.25.0", + "typescript": "^5.3.3", + "vite": "^5.0.0", + "vitest": "^3.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "packages/desktop/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "packages/desktop/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "packages/desktop/node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "packages/sdk-typescript": { "name": "@qwen-code/sdk", "version": "0.1.6", @@ -18837,6 +19928,7 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -19317,6 +20409,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -20443,6 +21536,7 @@ "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", @@ -21679,6 +22773,7 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -22652,6 +23747,7 @@ "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -22666,6 +23762,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/packages/desktop/package.json b/packages/desktop/package.json new file mode 100644 index 000000000..72cbb81ba --- /dev/null +++ b/packages/desktop/package.json @@ -0,0 +1,38 @@ +{ + "name": "@qwen-code/desktop", + "version": "0.15.2", + "description": "Electron desktop client for Qwen Code", + "private": true, + "type": "module", + "main": "dist/main/main.js", + "scripts": { + "build": "npm run build:main && npm run build:preload && npm run build:renderer", + "build:main": "tsc --project tsconfig.main.json", + "build:preload": "esbuild src/preload/index.ts --bundle --platform=node --format=cjs --external:electron --outfile=dist/preload/index.cjs", + "build:renderer": "vite build", + "dev:renderer": "vite --host 127.0.0.1", + "lint": "eslint src --ext .ts,.tsx", + "start": "electron .", + "test": "vitest run", + "typecheck": "tsc --noEmit --project tsconfig.main.json && tsc --noEmit --project tsconfig.renderer.json" + }, + "dependencies": { + "@qwen-code/webui": "file:../webui", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/node": "^20.11.24", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.2.0", + "electron": "^41.3.0", + "esbuild": "^0.25.0", + "typescript": "^5.3.3", + "vite": "^5.0.0", + "vitest": "^3.2.4" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/desktop/src/main/ipc/registerIpc.ts b/packages/desktop/src/main/ipc/registerIpc.ts new file mode 100644 index 000000000..bacfeba5e --- /dev/null +++ b/packages/desktop/src/main/ipc/registerIpc.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ipcMain, type BrowserWindow } from 'electron'; +import type { DesktopServerInfo } from '../../shared/desktopApi.js'; +import { IPC_CHANNELS } from '../../shared/ipcChannels.js'; +import { selectDirectory } from '../native/dialogs.js'; +import { openPath, showItemInFolder } from '../native/shell.js'; + +interface RegisterIpcOptions { + getServerInfo(): DesktopServerInfo; + getMainWindow(): BrowserWindow | null; +} + +export function registerIpc(options: RegisterIpcOptions): void { + ipcMain.handle(IPC_CHANNELS.getServerInfo, () => options.getServerInfo()); + ipcMain.handle(IPC_CHANNELS.selectDirectory, () => + selectDirectory(options.getMainWindow()), + ); + ipcMain.handle(IPC_CHANNELS.openPath, async (_event, path: unknown) => { + await openPath(requireString(path, 'path')); + }); + ipcMain.handle(IPC_CHANNELS.showItemInFolder, (_event, path: unknown) => { + showItemInFolder(requireString(path, 'path')); + }); + ipcMain.handle(IPC_CHANNELS.windowMinimize, () => { + options.getMainWindow()?.minimize(); + }); + ipcMain.handle(IPC_CHANNELS.windowMaximize, () => { + const window = options.getMainWindow(); + if (!window) { + return; + } + + if (window.isMaximized()) { + window.unmaximize(); + } else { + window.maximize(); + } + }); + ipcMain.handle(IPC_CHANNELS.windowClose, () => { + options.getMainWindow()?.close(); + }); + ipcMain.handle( + IPC_CHANNELS.windowIsMaximized, + () => options.getMainWindow()?.isMaximized() ?? false, + ); +} + +function requireString(value: unknown, label: string): string { + if (typeof value !== 'string' || value.length === 0) { + throw new TypeError(`${label} must be a non-empty string.`); + } + + return value; +} diff --git a/packages/desktop/src/main/lifecycle/AppLifecycle.ts b/packages/desktop/src/main/lifecycle/AppLifecycle.ts new file mode 100644 index 000000000..7bd9f4379 --- /dev/null +++ b/packages/desktop/src/main/lifecycle/AppLifecycle.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +export function shouldQuitWhenWindowsClosed( + platform = process.platform, +): boolean { + return platform !== 'darwin'; +} diff --git a/packages/desktop/src/main/main.ts b/packages/desktop/src/main/main.ts new file mode 100644 index 000000000..88fc91764 --- /dev/null +++ b/packages/desktop/src/main/main.ts @@ -0,0 +1,113 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { app, BrowserWindow, dialog, session } from 'electron'; +import { shouldQuitWhenWindowsClosed } from './lifecycle/AppLifecycle.js'; +import { registerIpc } from './ipc/registerIpc.js'; +import { createMainWindow } from './windows/MainWindow.js'; +import { startDesktopServer } from '../server/index.js'; +import type { DesktopServer } from '../server/types.js'; + +let desktopServer: DesktopServer | undefined; +let mainWindow: BrowserWindow | null = null; + +const gotSingleInstanceLock = app.requestSingleInstanceLock(); +if (!gotSingleInstanceLock) { + app.quit(); +} else { + app.on('second-instance', () => { + if (!mainWindow) { + return; + } + + if (mainWindow.isMinimized()) { + mainWindow.restore(); + } + mainWindow.focus(); + }); + + app + .whenReady() + .then(bootstrap) + .catch((error: unknown) => { + dialog.showErrorBox( + 'Qwen Code failed to start', + error instanceof Error ? error.message : String(error), + ); + app.exit(1); + }); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + void createOrFocusMainWindow(); + } + }); + + app.on('window-all-closed', () => { + if (shouldQuitWhenWindowsClosed()) { + app.quit(); + } + }); + + app.on('before-quit', () => { + void desktopServer?.close(); + }); +} + +async function bootstrap(): Promise { + app.setName('Qwen Code'); + registerContentSecurityPolicy(); + + desktopServer = await startDesktopServer(); + registerIpc({ + getServerInfo: () => { + if (!desktopServer) { + throw new Error('Desktop server is not running.'); + } + + return desktopServer.info; + }, + getMainWindow: () => mainWindow, + }); + + await createOrFocusMainWindow(); +} + +async function createOrFocusMainWindow(): Promise { + if (mainWindow) { + mainWindow.focus(); + return; + } + + mainWindow = await createMainWindow(); + mainWindow.on('closed', () => { + mainWindow = null; + }); +} + +function registerContentSecurityPolicy(): void { + session.defaultSession.webRequest.onHeadersReceived((details, callback) => { + callback({ + responseHeaders: { + ...details.responseHeaders, + 'Content-Security-Policy': [createContentSecurityPolicy()], + }, + }); + }); +} + +function createContentSecurityPolicy(): string { + return [ + "default-src 'self'", + "script-src 'self'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data:", + "font-src 'self' data:", + "connect-src 'self' http://127.0.0.1:* ws://127.0.0.1:*", + "object-src 'none'", + "base-uri 'self'", + ].join('; '); +} diff --git a/packages/desktop/src/main/native/dialogs.ts b/packages/desktop/src/main/native/dialogs.ts new file mode 100644 index 000000000..e18738036 --- /dev/null +++ b/packages/desktop/src/main/native/dialogs.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { dialog, type BrowserWindow, type OpenDialogOptions } from 'electron'; + +export async function selectDirectory( + owner: BrowserWindow | null, +): Promise { + const options: OpenDialogOptions = { + properties: ['openDirectory', 'createDirectory'], + }; + const result = owner + ? await dialog.showOpenDialog(owner, options) + : await dialog.showOpenDialog(options); + + if (result.canceled || result.filePaths.length === 0) { + return null; + } + + return result.filePaths[0] ?? null; +} diff --git a/packages/desktop/src/main/native/shell.ts b/packages/desktop/src/main/native/shell.ts new file mode 100644 index 000000000..1bb7a6a79 --- /dev/null +++ b/packages/desktop/src/main/native/shell.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { shell } from 'electron'; + +export async function openPath(path: string): Promise { + const errorMessage = await shell.openPath(path); + if (errorMessage) { + throw new Error(errorMessage); + } +} + +export function showItemInFolder(path: string): void { + shell.showItemInFolder(path); +} + +export async function openExternalUrl(url: string): Promise { + if (!isAllowedExternalUrl(url)) { + throw new Error('External URL scheme is not allowed.'); + } + + await shell.openExternal(url); +} + +function isAllowedExternalUrl(url: string): boolean { + try { + const parsed = new URL(url); + return parsed.protocol === 'https:' || parsed.protocol === 'http:'; + } catch { + return false; + } +} diff --git a/packages/desktop/src/main/windows/MainWindow.ts b/packages/desktop/src/main/windows/MainWindow.ts new file mode 100644 index 000000000..b5328f6f6 --- /dev/null +++ b/packages/desktop/src/main/windows/MainWindow.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BrowserWindow } from 'electron'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { openExternalUrl } from '../native/shell.js'; + +const mainDir = dirname(fileURLToPath(import.meta.url)); +const preloadPath = join(mainDir, '../preload/index.cjs'); +const rendererIndexPath = join(mainDir, '../renderer/index.html'); + +export async function createMainWindow(): Promise { + const mainWindow = new BrowserWindow({ + width: 1240, + height: 820, + minWidth: 960, + minHeight: 640, + title: 'Qwen Code', + backgroundColor: '#101214', + show: false, + webPreferences: { + contextIsolation: true, + nodeIntegration: false, + preload: preloadPath, + sandbox: false, + webSecurity: true, + }, + }); + + mainWindow.once('ready-to-show', () => { + mainWindow.show(); + }); + + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + void openExternalUrl(url); + return { action: 'deny' }; + }); + + const rendererUrl = process.env['QWEN_DESKTOP_RENDERER_URL']; + if (rendererUrl) { + await mainWindow.loadURL(rendererUrl); + } else { + await mainWindow.loadFile(rendererIndexPath); + } + + return mainWindow; +} diff --git a/packages/desktop/src/preload/index.ts b/packages/desktop/src/preload/index.ts new file mode 100644 index 000000000..5db4210da --- /dev/null +++ b/packages/desktop/src/preload/index.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { contextBridge, ipcRenderer } from 'electron'; +import type { + DesktopServerInfo, + QwenDesktopApi, +} from '../shared/desktopApi.js'; +import { IPC_CHANNELS } from '../shared/ipcChannels.js'; + +const api: QwenDesktopApi = { + getServerInfo: () => + ipcRenderer.invoke( + IPC_CHANNELS.getServerInfo, + ) as Promise, + selectDirectory: () => + ipcRenderer.invoke(IPC_CHANNELS.selectDirectory) as Promise, + openPath: async (path: string) => { + await ipcRenderer.invoke(IPC_CHANNELS.openPath, path); + }, + showItemInFolder: async (path: string) => { + await ipcRenderer.invoke(IPC_CHANNELS.showItemInFolder, path); + }, + window: { + minimize: async () => { + await ipcRenderer.invoke(IPC_CHANNELS.windowMinimize); + }, + maximize: async () => { + await ipcRenderer.invoke(IPC_CHANNELS.windowMaximize); + }, + close: async () => { + await ipcRenderer.invoke(IPC_CHANNELS.windowClose); + }, + isMaximized: () => + ipcRenderer.invoke(IPC_CHANNELS.windowIsMaximized) as Promise, + }, +}; + +contextBridge.exposeInMainWorld('qwenDesktop', api); diff --git a/packages/desktop/src/renderer/App.tsx b/packages/desktop/src/renderer/App.tsx new file mode 100644 index 000000000..4f9e88386 --- /dev/null +++ b/packages/desktop/src/renderer/App.tsx @@ -0,0 +1,145 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useEffect, useMemo, useState } from 'react'; +import { + loadDesktopStatus, + type DesktopConnectionStatus, +} from './api/client.js'; + +type LoadState = + | { state: 'loading' } + | { state: 'ready'; status: DesktopConnectionStatus } + | { state: 'error'; message: string }; + +export function App() { + const [loadState, setLoadState] = useState({ state: 'loading' }); + + useEffect(() => { + let disposed = false; + + const load = async () => { + try { + const status = await loadDesktopStatus(); + if (!disposed) { + setLoadState({ state: 'ready', status }); + } + } catch (error) { + if (!disposed) { + setLoadState({ + state: 'error', + message: + error instanceof Error + ? error.message + : 'Unable to reach desktop service.', + }); + } + } + }; + + void load(); + + return () => { + disposed = true; + }; + }, []); + + const statusLabel = useMemo(() => { + if (loadState.state === 'ready') { + return 'Connected'; + } + + if (loadState.state === 'error') { + return 'Offline'; + } + + return 'Starting'; + }, [loadState]); + + return ( +
+ + +
+
+
+

Local service

+

{statusLabel}

+
+ +
+ +
+
+
+

Conversation

+ Idle +
+
No session selected
+
+ +
+
+

Runtime

+
+ +
+
+
+
+ ); +} + +function StatusPill({ state }: { state: LoadState['state'] }) { + return {state}; +} + +function RuntimeDetails({ loadState }: { loadState: LoadState }) { + if (loadState.state === 'loading') { + return
Checking service
; + } + + if (loadState.state === 'error') { + return
{loadState.message}
; + } + + return ( +
+
+
Server
+
{loadState.status.serverUrl}
+
+
+
Health
+
{loadState.status.health.service}
+
+
+
Uptime
+
{loadState.status.health.uptimeMs} ms
+
+
+ ); +} diff --git a/packages/desktop/src/renderer/api/client.ts b/packages/desktop/src/renderer/api/client.ts new file mode 100644 index 000000000..171b079cd --- /dev/null +++ b/packages/desktop/src/renderer/api/client.ts @@ -0,0 +1,61 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { DesktopServerInfo } from '../../shared/desktopApi.js'; + +export interface DesktopHealth { + ok: true; + service: 'qwen-desktop'; + uptimeMs: number; + timestamp: string; +} + +export interface DesktopConnectionStatus { + serverUrl: string; + health: DesktopHealth; +} + +export async function loadDesktopStatus(): Promise { + const serverInfo = await getServerInfo(); + const healthUrl = new URL('/health', serverInfo.url); + const response = await fetch(healthUrl, { + headers: { + Authorization: `Bearer ${serverInfo.token}`, + }, + }); + const payload = (await response.json()) as unknown; + + if (!response.ok || !isDesktopHealth(payload)) { + throw new Error('Desktop service health check failed.'); + } + + return { + serverUrl: serverInfo.url, + health: payload, + }; +} + +async function getServerInfo(): Promise { + if (!window.qwenDesktop) { + throw new Error('Desktop preload API is unavailable.'); + } + + return window.qwenDesktop.getServerInfo(); +} + +function isDesktopHealth(value: unknown): value is DesktopHealth { + if (!value || typeof value !== 'object') { + return false; + } + + const candidate = value as Partial; + return ( + candidate.ok === true && + candidate.service === 'qwen-desktop' && + typeof candidate.uptimeMs === 'number' && + typeof candidate.timestamp === 'string' + ); +} diff --git a/packages/desktop/src/renderer/index.html b/packages/desktop/src/renderer/index.html new file mode 100644 index 000000000..70074cdd8 --- /dev/null +++ b/packages/desktop/src/renderer/index.html @@ -0,0 +1,12 @@ + + + + + + Qwen Code + + +
+ + + diff --git a/packages/desktop/src/renderer/main.tsx b/packages/desktop/src/renderer/main.tsx new file mode 100644 index 000000000..7817e314c --- /dev/null +++ b/packages/desktop/src/renderer/main.tsx @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App.js'; +import './styles.css'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error('Renderer root element was not found.'); +} + +createRoot(rootElement).render( + + + , +); diff --git a/packages/desktop/src/renderer/styles.css b/packages/desktop/src/renderer/styles.css new file mode 100644 index 000000000..55b5b74cd --- /dev/null +++ b/packages/desktop/src/renderer/styles.css @@ -0,0 +1,264 @@ +* { + box-sizing: border-box; +} + +:root { + color: #eef0ed; + background: #101214; + font-family: + ui-sans-serif, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; +} + +body { + min-width: 320px; + min-height: 100vh; + margin: 0; + background: + linear-gradient(135deg, rgba(48, 77, 64, 0.28), transparent 38%), + linear-gradient(180deg, #17191b 0%, #101214 100%); +} + +button, +input, +textarea { + font: inherit; +} + +#root { + min-height: 100vh; +} + +.desktop-shell { + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + min-height: 100vh; +} + +.sidebar { + display: flex; + flex-direction: column; + gap: 22px; + min-width: 0; + padding: 22px; + border-right: 1px solid rgba(238, 240, 237, 0.09); + background: rgba(18, 20, 21, 0.92); +} + +.brand-lockup { + display: flex; + align-items: center; + gap: 12px; + min-height: 48px; +} + +.brand-mark { + display: grid; + width: 42px; + height: 42px; + place-items: center; + border: 1px solid rgba(238, 240, 237, 0.16); + background: #e7bd73; + color: #161311; + font-weight: 800; +} + +.brand-lockup h1, +.brand-lockup p, +.topbar h2, +.topbar p, +.panel h3 { + margin: 0; +} + +.brand-lockup h1 { + font-size: 17px; + font-weight: 750; +} + +.brand-lockup p, +.eyebrow { + color: #9ca39b; + font-size: 12px; + letter-spacing: 0; + text-transform: uppercase; +} + +.sidebar-section { + display: flex; + flex-direction: column; + gap: 10px; +} + +.sidebar-section-fill { + flex: 1; +} + +.sidebar-section h2 { + margin: 0; + color: #b7bdb6; + font-size: 12px; + font-weight: 700; + letter-spacing: 0; + text-transform: uppercase; +} + +.empty-row { + min-height: 42px; + padding: 12px; + border: 1px solid rgba(238, 240, 237, 0.09); + color: #858c84; + font-size: 13px; +} + +.workbench { + display: flex; + min-width: 0; + flex-direction: column; + padding: 24px; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + min-height: 64px; + padding-bottom: 22px; +} + +.topbar h2 { + font-size: 28px; + font-weight: 760; +} + +.status-pill { + min-width: 86px; + padding: 7px 10px; + border: 1px solid rgba(238, 240, 237, 0.12); + color: #d8dcd6; + font-size: 12px; + font-weight: 700; + text-align: center; + text-transform: uppercase; +} + +.status-pill-ready { + border-color: rgba(99, 214, 157, 0.45); + color: #96e6ba; +} + +.status-pill-error { + border-color: rgba(235, 111, 91, 0.55); + color: #ff9a84; +} + +.workspace-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) 320px; + gap: 18px; + min-height: 0; + flex: 1; +} + +.panel { + min-width: 0; + border: 1px solid rgba(238, 240, 237, 0.1); + background: rgba(20, 23, 23, 0.74); +} + +.panel-main { + display: flex; + min-height: 520px; + flex-direction: column; +} + +.panel-side { + min-height: 240px; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 48px; + padding: 0 16px; + border-bottom: 1px solid rgba(238, 240, 237, 0.08); +} + +.panel-header h3 { + font-size: 14px; + font-weight: 750; +} + +.panel-header span { + color: #858c84; + font-size: 12px; +} + +.conversation-empty { + display: grid; + flex: 1; + min-height: 260px; + place-items: center; + color: #858c84; + font-size: 14px; +} + +.runtime-row, +.runtime-details { + padding: 16px; +} + +.runtime-details { + display: grid; + gap: 14px; + margin: 0; +} + +.runtime-details div { + display: grid; + gap: 4px; + min-width: 0; +} + +.runtime-details dt { + color: #9ca39b; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; +} + +.runtime-details dd { + min-width: 0; + margin: 0; + overflow-wrap: anywhere; + color: #eef0ed; + font-size: 13px; +} + +.muted { + color: #858c84; +} + +.error-text { + color: #ff9a84; +} + +@media (max-width: 860px) { + .desktop-shell { + grid-template-columns: 1fr; + } + + .sidebar { + border-right: 0; + border-bottom: 1px solid rgba(238, 240, 237, 0.09); + } + + .workspace-grid { + grid-template-columns: 1fr; + } +} diff --git a/packages/desktop/src/renderer/vite-env.d.ts b/packages/desktop/src/renderer/vite-env.d.ts new file mode 100644 index 000000000..3cb0fbfc1 --- /dev/null +++ b/packages/desktop/src/renderer/vite-env.d.ts @@ -0,0 +1,11 @@ +/// + +import type { QwenDesktopApi } from '../shared/desktopApi'; + +declare global { + interface Window { + qwenDesktop: QwenDesktopApi; + } +} + +export {}; diff --git a/packages/desktop/src/server/http/auth.ts b/packages/desktop/src/server/http/auth.ts new file mode 100644 index 000000000..12409cc32 --- /dev/null +++ b/packages/desktop/src/server/http/auth.ts @@ -0,0 +1,93 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { randomBytes, timingSafeEqual } from 'node:crypto'; +import type { IncomingHttpHeaders } from 'node:http'; + +const LOCAL_HTTP_ORIGIN = /^http:\/\/(?:127\.0\.0\.1|localhost):\d+$/u; + +export function createServerToken(): string { + return randomBytes(32).toString('base64url'); +} + +export function getSingleHeader( + value: string | string[] | undefined, +): string | undefined { + if (Array.isArray(value)) { + return value[0]; + } + + return value; +} + +export function getBearerToken( + authorization: string | string[] | undefined, +): string | undefined { + const header = getSingleHeader(authorization); + if (!header) { + return undefined; + } + + const parts = header.trim().split(/\s+/u); + if (parts.length !== 2) { + return undefined; + } + + const [scheme, token] = parts; + if (scheme?.toLowerCase() !== 'bearer' || !token) { + return undefined; + } + + return token; +} + +export function isAuthorized( + headers: IncomingHttpHeaders, + expectedToken: string, +): boolean { + const token = getBearerToken(headers.authorization); + if (!token) { + return false; + } + + return safeEqual(token, expectedToken); +} + +export function isAllowedOrigin(origin: string | undefined): boolean { + if (!origin || origin === 'null' || origin.startsWith('file://')) { + return true; + } + + return LOCAL_HTTP_ORIGIN.test(origin); +} + +export function createCorsHeaders( + origin: string | undefined, +): Record { + const headers: Record = { + 'Access-Control-Allow-Headers': 'authorization, content-type', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS', + 'Access-Control-Max-Age': '600', + Vary: 'Origin', + }; + + if (origin && isAllowedOrigin(origin)) { + headers['Access-Control-Allow-Origin'] = origin; + } + + return headers; +} + +function safeEqual(left: string, right: string): boolean { + const leftBuffer = Buffer.from(left); + const rightBuffer = Buffer.from(right); + + if (leftBuffer.length !== rightBuffer.length) { + return false; + } + + return timingSafeEqual(leftBuffer, rightBuffer); +} diff --git a/packages/desktop/src/server/index.test.ts b/packages/desktop/src/server/index.test.ts new file mode 100644 index 000000000..8b7573b51 --- /dev/null +++ b/packages/desktop/src/server/index.test.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { afterEach, describe, expect, it } from 'vitest'; +import { startDesktopServer } from './index.js'; +import type { DesktopServer } from './types.js'; + +const servers: DesktopServer[] = []; + +afterEach(async () => { + await Promise.all(servers.splice(0).map((server) => server.close())); +}); + +describe('DesktopServer', () => { + it('binds to localhost and serves authenticated health checks', async () => { + const server = await createTestServer(); + + expect(server.info.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/u); + expect(server.info.token).toBe('test-token'); + + const unauthorized = await getJson(server, '/health'); + expect(unauthorized.status).toBe(401); + expect(unauthorized.body).toMatchObject({ + ok: false, + code: 'unauthorized', + }); + + const authorized = await getJson(server, '/health', { + Authorization: 'Bearer test-token', + }); + expect(authorized.status).toBe(200); + expect(authorized.body).toMatchObject({ + ok: true, + service: 'qwen-desktop', + }); + }); + + it('rejects non-local origins before token checks', async () => { + const server = await createTestServer(); + + const response = await getJson(server, '/health', { + Authorization: 'Bearer test-token', + Origin: 'https://example.com', + }); + + expect(response.status).toBe(403); + expect(response.body).toMatchObject({ + ok: false, + code: 'origin_forbidden', + }); + }); + + it('allows app preflight requests without exposing the route', async () => { + const server = await createTestServer(); + + const response = await fetch(`${server.info.url}/health`, { + method: 'OPTIONS', + headers: { + Origin: 'http://127.0.0.1:5173', + 'Access-Control-Request-Headers': 'authorization', + }, + }); + + expect(response.status).toBe(204); + expect(response.headers.get('access-control-allow-origin')).toBe( + 'http://127.0.0.1:5173', + ); + expect(response.headers.get('access-control-allow-headers')).toContain( + 'authorization', + ); + }); + + it('returns a typed error for unknown authenticated routes', async () => { + const server = await createTestServer(); + + const response = await getJson(server, '/api/missing', { + Authorization: 'Bearer test-token', + }); + + expect(response.status).toBe(404); + expect(response.body).toMatchObject({ + ok: false, + code: 'not_found', + }); + }); +}); + +async function createTestServer(): Promise { + const server = await startDesktopServer({ + token: 'test-token', + now: () => new Date('2026-04-25T00:00:00.000Z'), + }); + servers.push(server); + return server; +} + +async function getJson( + server: DesktopServer, + path: string, + headers: Record = {}, +): Promise<{ status: number; body: unknown }> { + const response = await fetch(`${server.info.url}${path}`, { headers }); + return { + status: response.status, + body: (await response.json()) as unknown, + }; +} diff --git a/packages/desktop/src/server/index.ts b/packages/desktop/src/server/index.ts new file mode 100644 index 000000000..38e0ee443 --- /dev/null +++ b/packages/desktop/src/server/index.ts @@ -0,0 +1,173 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + createServer, + type IncomingMessage, + type ServerResponse, +} from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { + createCorsHeaders, + createServerToken, + getSingleHeader, + isAllowedOrigin, + isAuthorized, +} from './http/auth.js'; +import type { + DesktopErrorResponse, + DesktopHealthResponse, + DesktopServer, + DesktopServerOptions, +} from './types.js'; + +interface HandlerContext { + token: string; + startedAt: number; + now: () => Date; +} + +export async function startDesktopServer( + options: DesktopServerOptions = {}, +): Promise { + const token = options.token ?? createServerToken(); + const now = options.now ?? (() => new Date()); + const startedAt = now().getTime(); + const server = createServer((request, response) => { + handleRequest(request, response, { token, startedAt, now }); + }); + + await new Promise((resolve, reject) => { + const handleError = (error: Error) => { + server.off('listening', handleListening); + reject(error); + }; + const handleListening = () => { + server.off('error', handleError); + resolve(); + }; + + server.once('error', handleError); + server.once('listening', handleListening); + server.listen(0, '127.0.0.1'); + }); + + const address = server.address(); + if (!isAddressInfo(address)) { + await closeHttpServer(server); + throw new Error('Desktop server did not bind to a TCP address.'); + } + + return { + info: { + url: `http://127.0.0.1:${address.port}`, + token, + }, + close: () => closeHttpServer(server), + }; +} + +function handleRequest( + request: IncomingMessage, + response: ServerResponse, + context: HandlerContext, +) { + const origin = getSingleHeader(request.headers.origin); + if (!isAllowedOrigin(origin)) { + sendJson(response, origin, 403, { + ok: false, + code: 'origin_forbidden', + message: 'Request origin is not allowed.', + }); + return; + } + + if (request.method === 'OPTIONS') { + response.writeHead(204, createCorsHeaders(origin)); + response.end(); + return; + } + + if (!isAuthorized(request.headers, context.token)) { + sendJson(response, origin, 401, { + ok: false, + code: 'unauthorized', + message: 'Missing or invalid desktop server token.', + }); + return; + } + + const requestUrl = parseRequestUrl(request); + if (!requestUrl) { + sendJson(response, origin, 400, { + ok: false, + code: 'bad_request', + message: 'Request URL is invalid.', + }); + return; + } + + if (request.method === 'GET' && requestUrl.pathname === '/health') { + sendJson(response, origin, 200, { + ok: true, + service: 'qwen-desktop', + uptimeMs: Math.max(0, context.now().getTime() - context.startedAt), + timestamp: context.now().toISOString(), + }); + return; + } + + sendJson(response, origin, 404, { + ok: false, + code: 'not_found', + message: 'Route not found.', + }); +} + +function parseRequestUrl(request: IncomingMessage): URL | undefined { + try { + return new URL(request.url ?? '/', 'http://127.0.0.1'); + } catch { + return undefined; + } +} + +function sendJson( + response: ServerResponse, + origin: string | undefined, + statusCode: number, + payload: DesktopHealthResponse | DesktopErrorResponse, +) { + response.writeHead(statusCode, { + ...createCorsHeaders(origin), + 'Content-Type': 'application/json; charset=utf-8', + }); + response.end(`${JSON.stringify(payload)}\n`); +} + +function isAddressInfo( + address: string | AddressInfo | null, +): address is AddressInfo { + return typeof address === 'object' && address !== null; +} + +async function closeHttpServer( + server: ReturnType, +): Promise { + if (!server.listening) { + return; + } + + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); +} diff --git a/packages/desktop/src/server/types.ts b/packages/desktop/src/server/types.ts new file mode 100644 index 000000000..f3c6119d4 --- /dev/null +++ b/packages/desktop/src/server/types.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { DesktopServerInfo } from '../shared/desktopApi.js'; + +export type { DesktopServerInfo }; + +export interface DesktopServer { + info: DesktopServerInfo; + close(): Promise; +} + +export interface DesktopServerOptions { + token?: string; + now?: () => Date; +} + +export interface DesktopHealthResponse { + ok: true; + service: 'qwen-desktop'; + uptimeMs: number; + timestamp: string; +} + +export interface DesktopErrorResponse { + ok: false; + code: string; + message: string; +} diff --git a/packages/desktop/src/shared/desktopApi.ts b/packages/desktop/src/shared/desktopApi.ts new file mode 100644 index 000000000..57dfe50b7 --- /dev/null +++ b/packages/desktop/src/shared/desktopApi.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface DesktopServerInfo { + url: string; + token: string; +} + +export interface DesktopWindowApi { + minimize(): Promise; + maximize(): Promise; + close(): Promise; + isMaximized(): Promise; +} + +export interface QwenDesktopApi { + getServerInfo(): Promise; + selectDirectory(): Promise; + openPath(path: string): Promise; + showItemInFolder(path: string): Promise; + window: DesktopWindowApi; +} diff --git a/packages/desktop/src/shared/ipcChannels.ts b/packages/desktop/src/shared/ipcChannels.ts new file mode 100644 index 000000000..dcf6964a8 --- /dev/null +++ b/packages/desktop/src/shared/ipcChannels.ts @@ -0,0 +1,16 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +export const IPC_CHANNELS = { + getServerInfo: 'qwen-desktop:get-server-info', + selectDirectory: 'qwen-desktop:select-directory', + openPath: 'qwen-desktop:open-path', + showItemInFolder: 'qwen-desktop:show-item-in-folder', + windowMinimize: 'qwen-desktop:window:minimize', + windowMaximize: 'qwen-desktop:window:maximize', + windowClose: 'qwen-desktop:window:close', + windowIsMaximized: 'qwen-desktop:window:is-maximized', +} as const; diff --git a/packages/desktop/tsconfig.json b/packages/desktop/tsconfig.json new file mode 100644 index 000000000..ada446967 --- /dev/null +++ b/packages/desktop/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.main.json" + }, + { + "path": "./tsconfig.renderer.json" + } + ] +} diff --git a/packages/desktop/tsconfig.main.json b/packages/desktop/tsconfig.main.json new file mode 100644 index 000000000..152658bb9 --- /dev/null +++ b/packages/desktop/tsconfig.main.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": false, + "declaration": false, + "declarationMap": false, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "types": ["node", "vitest/globals"] + }, + "include": [ + "src/main/**/*.ts", + "src/preload/**/*.ts", + "src/server/**/*.ts", + "src/shared/**/*.ts" + ], + "exclude": ["**/*.test.ts", "dist", "node_modules"] +} diff --git a/packages/desktop/tsconfig.renderer.json b/packages/desktop/tsconfig.renderer.json new file mode 100644 index 000000000..2ad4e2bb3 --- /dev/null +++ b/packages/desktop/tsconfig.renderer.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/renderer", "src/shared/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/desktop/vite.config.ts b/packages/desktop/vite.config.ts new file mode 100644 index 000000000..e1b0eb6c2 --- /dev/null +++ b/packages/desktop/vite.config.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +const packageRoot = dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + root: resolve(packageRoot, 'src/renderer'), + plugins: [react()], + build: { + outDir: resolve(packageRoot, 'dist/renderer'), + emptyOutDir: true, + sourcemap: true, + }, + server: { + host: '127.0.0.1', + }, +}); diff --git a/packages/desktop/vitest.config.ts b/packages/desktop/vitest.config.ts new file mode 100644 index 000000000..3a234b833 --- /dev/null +++ b/packages/desktop/vitest.config.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + restoreMocks: true, + }, +}); diff --git a/scripts/build.js b/scripts/build.js index f471bf25e..eb4ee42ec 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -42,6 +42,7 @@ execSync('npm run generate', { stdio: 'inherit', cwd: root }); // 6. webui (shared UI components - used by vscode companion) // 7. sdk (no internal dependencies) // 8. vscode-ide-companion (depends on webui) +// 9. desktop (depends on webui and launches the built CLI in later slices) const buildOrder = [ 'packages/core', 'packages/web-templates', @@ -54,6 +55,7 @@ const buildOrder = [ 'packages/webui', 'packages/sdk-typescript', 'packages/vscode-ide-companion', + 'packages/desktop', ]; for (const workspace of buildOrder) {