History

bring back electron (+ websocket/package changes) (#10)

* feat: scaffold apps/server package and WebSocket protocol types - Create apps/server with package.json, tsconfig, tsup config - Copy codexAppServerManager, providerManager, fixPath from desktop (no Electron deps) - Add WebSocket message protocol schemas (WsRequest, WsResponse, WsPush) to @acme/contracts - Define WS_METHODS and WS_CHANNELS constants for type-safe routing Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com> * feat: implement WebSocket + HTTP server and CLI entry point - wsServer.ts: WebSocket server routes NativeApi methods, serves static files - Provider events pushed to all connected clients - SPA fallback for client-side routing - Server welcome message with cwd/projectName on connect - index.ts: CLI entry point - Auto-detects available port (default 3773) - Resolves renderer dist for static serving - Opens browser on startup via 'open' package - Graceful shutdown on SIGINT/SIGTERM Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com> * feat: WebSocket transport and NativeApi implementation for browser - wsTransport.ts: WebSocket client with request/response, push subscriptions, auto-reconnect with exponential backoff - wsNativeApi.ts: Full NativeApi implementation over WebSocket transport - All provider methods routed through WS_METHODS - Provider events streamed via WS_CHANNELS.providerEvent - Todos/terminal/agent stubbed (not needed for web mode) - dialogs.pickFolder returns null (text input fallback in UI) - session-logic.ts: readNativeApi() now auto-detects environment - Prefers Electron preload bridge if available (window.nativeApi) - Falls back to WebSocket transport for browser mode - onServerWelcome() hook for renderer to receive cwd on connect Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com> * feat: adapt renderer for browser mode - App.tsx: Add AutoProjectBootstrap that listens for server welcome and auto-creates project + thread from server's cwd - Sidebar: Conditionalize drag region (Electron only), hide Browse button in browser - ChatView: Conditionalize drag region, adjust padding for browser - DiffPanel: Same drag region treatment - index.html: Update title to 'CodeThing' - env.ts: isElectron detection utility Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com> * feat: monorepo & CLI integration - Root package.json: Add dev, dev:server, dev:web, start scripts - dev runs both server and renderer in parallel via turbo - VITE_WS_URL injected so renderer connects to server in dev mode - turbo.json: Add WS env vars to globalEnv - vite.config.ts: Inject VITE_WS_URL at build time for WS server discovery - wsTransport.ts: Use VITE_WS_URL env var in dev, fall back to page URL in prod - Server dev script: Uses tsx for direct TypeScript execution with VITE_DEV_SERVER_URL Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com> * feat: remove Electron desktop app, clean up contracts and docs - Delete apps/desktop/ entirely (replaced by apps/server) - Remove IPC_CHANNELS constant from contracts (no longer needed) - Keep NativeApi interface (still the renderer's API contract) - Keep EDITORS constant (used by both server and renderer) - Update README.md with new architecture diagram and scripts - Update AGENTS.md with new package roles and architecture Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com> * test: add WebSocket server integration tests - Tests welcome message on connect with cwd/projectName - Tests server.getConfig method - Tests providers.listSessions returns empty array - Tests error response for unknown methods - Tests graceful handling of invalid JSON - All 73 tests pass across 12 test files Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com> * fix: address lint warnings in new files - wsTransport.ts: Use addEventListener instead of on-function assignments - wsServer.ts: Replace filter(Boolean).pop() with direct array access - wsServer.test.ts: Use addEventListener for WebSocket events Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com> * fix: race condition in auto-project bootstrap + cleanup - wsNativeApi: Cache last welcome payload and replay to late subscribers. Fixes race where WS connect + welcome arrives before React effect fires. - App.tsx: Add bootstrappedRef to prevent duplicate project creation, skip bootstrap in Electron mode - wsTransport: Use standard import.meta.env for VITE_WS_URL access - CLAUDE.md: Fix header (was incorrectly '# AGENTS.md') Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com> * fix: use tsx command directly in server dev script Use 'tsx src/index.ts' instead of 'node --import tsx' for simpler dev execution Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com> * chore: clean up stale Electron references and config - Remove smoke-test script (was desktop-only) - Remove dist-electron from turbo outputs and .gitignore - Rename ELECTRON_RENDERER_PORT → PORT in vite.config.ts - Clean up turbo globalEnv (remove ELECTRON_RENDERER_PORT, WS_SERVER_PORT) - Update CI workflow to use bun 1.3.9 (matches package.json engines) Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com> * feat: make t3 package publishable for npx - Remove private:true so package can be published to npm - Add files field to limit published files to dist/ - Move @acme/contracts to devDependencies (bundled by tsup) - Add @acme/renderer devDependency for turbo build ordering - Add bundle-client.mjs: copies renderer build into dist/client/ - Update resolveStaticDir to check dist/client/ first (npx scenario), then fall back to monorepo layout (../../renderer/dist) - Full build pipeline: contracts → renderer → server + bundle client Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com> * fix tests * turborepo and better logging * bring back electron * fix * Promise catching fixes ---------

Theo Browne committed 9 months ago

Commit 8ebd2471 parent e7c429251 files · +2477 −1234Browse files

Unified · Split

51 files
  1. .github/workflows/ci.yml +10 −2
  2. .gitignore +0 −1
  3. AGENTS.md +10 −9
  4. README.md +68 −41
  5. apps/desktop/.gitignore +1 −0
  6. apps/desktop/package.json +5 −8
  7. apps/desktop/scripts/dev-electron.mjs +2 −2
  8. apps/desktop/scripts/smoke-test.mjs +19 −42
  9. apps/desktop/src/fixPath.ts +1 −1
  10. apps/desktop/src/ipcHelpers.test.ts +0 −104
  11. apps/desktop/src/ipcHelpers.ts +0 −24
  12. apps/desktop/src/main.ts +156 −290
  13. apps/desktop/src/preload.ts +6 −52
  14. apps/desktop/src/processManager.test.ts +0 −48
  15. apps/desktop/src/processManager.ts +0 −144
  16. apps/desktop/src/todoStore.ts +0 −106
  17. apps/renderer/index.html +1 −1
  18. apps/renderer/src/App.tsx +118 −4
  19. apps/renderer/src/components/ChatView.tsx +3 −2
  20. apps/renderer/src/components/DiffPanel.tsx +2 −1
  21. apps/renderer/src/components/Sidebar.tsx +112 −43
  22. apps/renderer/src/env.ts +8 −0
  23. apps/renderer/src/session-logic.ts +16 −3
  24. apps/renderer/src/store.test.ts +57 −3
  25. apps/renderer/src/store.ts +57 −40
  26. apps/renderer/src/vite-env.d.ts +6 −0
  27. apps/renderer/src/wsNativeApi.ts +102 −0
  28. apps/renderer/src/wsTransport.ts +197 −0
  29. apps/renderer/vite.config.ts +7 −1
  30. apps/server/package.json +34 −0
  31. apps/server/scripts/bundle-client.mjs +21 −0
  32. apps/server/src/codexAppServerManager.test.ts +0 −0
  33. apps/server/src/codexAppServerManager.ts +0 −0
  34. apps/server/src/fixPath.ts +18 −0
  35. apps/server/src/index.ts +176 −0
  36. apps/server/src/logger.ts +103 −0
  37. apps/server/src/projectRegistry.ts +172 −0
  38. apps/server/src/providerManager.test.ts +0 −0
  39. apps/server/src/providerManager.ts +0 −0
  40. apps/server/src/wsServer.test.ts +347 −0
  41. apps/server/src/wsServer.ts +373 −0
  42. apps/server/tsconfig.json +9 −0
  43. apps/server/tsup.config.ts +13 −0
  44. bun.lock +56 −232
  45. package.json +8 −2
  46. packages/contracts/src/index.ts +2 −0
  47. packages/contracts/src/ipc.ts +11 −22
  48. packages/contracts/src/project.test.ts +49 −0
  49. packages/contracts/src/project.ts +30 −0
  50. packages/contracts/src/ws.ts +78 −0
  51. turbo.json +13 −6
.github/workflows/ci.yml+10−2
@@ -8,7 +8,7 @@ on: · 7 unmodified lines
88
99 jobs:
1010 quality:
11− name: Lint, Typecheck, Test
11+ name: Lint, Typecheck, Test, Build
1212 runs-on: ubuntu-latest
1313 steps:
1414 - name: Checkout
@@ -17,7 +17,7 @@ jobs: · 2 unmodified lines
1717 - name: Setup Bun
1818 uses: oven-sh/setup-bun@v2
1919 with:
20− bun-version: 1.1.38
20+ bun-version: 1.3.9
2121
2222 - name: Cache Bun and Turbo
2323 uses: actions/cache@v4
@@ -40,3 +40,11 @@ jobs: · 16 unmodified lines
4040
4141 - name: Test
4242 run: bun run test
43+
44+ - name: Build desktop pipeline
45+ run: bun run build:desktop
46+
47+ - name: Verify preload bundle output
48+ run: |
49+ test -f apps/desktop/dist-electron/preload.js
50+ grep -nE "desktopBridge|getWsUrl|PICK_FOLDER_CHANNEL|wsUrl" apps/desktop/dist-electron/preload.js
.gitignore+0−1
@@ -5,7 +5,6 @@ node_modules · 4 unmodified lines
55 *.log
66 *.tsbuildinfo
77 apps/*/dist
8−apps/*/dist-electron
98 packages/*/dist
109 .env
1110 .env.local
AGENTS.md+10−9
@@ -1,7 +1,7 @@
1−# AGENTS.md
1+# CLAUDE.md
22
33 ## Project Snapshot
4−CodeThing is a minimal GUI for using code agents like Codex and Claude Code (coming soon).
4+CodeThing is a minimal web GUI for using code agents like Codex and Claude Code (coming soon).
55
66 This repository is a VERY EARLY WIP. Proposing sweeping changes that improve long-term maintainability is encouraged.
77
@@ -13,17 +13,18 @@ This repository is a VERY EARLY WIP. Proposing sweeping changes that improve lon · 5 unmodified lines
1313 If a tradeoff is required, choose correctness and robustness over short-term convenience.
1414
1515 ## Package Roles
16−- `apps/desktop`: Electron main/preload runtime. Owns provider orchestration, process/session lifecycle, and native IPC boundaries.
17−- `apps/renderer`: React/Vite UI. Owns session UX, conversation/event rendering, and client-side state.
18−- `packages/contracts`: Shared Zod schemas and TypeScript contracts for provider events, IPC payloads, and model/session types.
16+- `apps/server`: Node.js WebSocket server. Wraps Codex app-server (JSON-RPC over stdio), serves the React web app, and manages provider sessions.
17+- `apps/renderer`: React/Vite UI. Owns session UX, conversation/event rendering, and client-side state. Connects to the server via WebSocket.
18+- `packages/contracts`: Shared Zod schemas and TypeScript contracts for provider events, WebSocket protocol, and model/session types.
1919
2020 ## Codex App Server (Important)
21−CodeThing is currently Codex-first. The desktop app starts `codex app-server` (JSON-RPC over stdio) per provider session, then streams structured events into the renderer through the provider APIs.
21+CodeThing is currently Codex-first. The server starts `codex app-server` (JSON-RPC over stdio) per provider session, then streams structured events to the browser through WebSocket push messages.
2222
2323 How we use it in this codebase:
24−- Session startup/resume and turn lifecycle are brokered in `apps/desktop/src/codexAppServerManager.ts`.
25−- Provider dispatch and thread event logging are coordinated in `apps/desktop/src/providerManager.ts`.
26−- Renderer consumes provider event streams via `nativeApi.providers.onEvent`.
24+- Session startup/resume and turn lifecycle are brokered in `apps/server/src/codexAppServerManager.ts`.
25+- Provider dispatch and thread event logging are coordinated in `apps/server/src/providerManager.ts`.
26+- WebSocket server routes NativeApi methods in `apps/server/src/wsServer.ts`.
27+- Renderer consumes provider event streams via WebSocket push on channel `providers.event`.
2728
2829 Docs:
2930 - Codex App Server docs: https://developers.openai.com/codex/sdk/#app-server
README.md+68−41
@@ -1,16 +1,38 @@
1−# CodeThing (Electron + Vite + Bun)
2−
3−CodeThing is a desktop shell for coding agents. This first implementation is:
4−
5−1. Codex-first: connects to `codex app-server` and streams turn/item events.
6−2. Provider-ready: renderer speaks a provider abstraction so Claude Code can plug in later.
7−3. Typed end-to-end: contracts validate payloads at preload/main boundaries.
1+# CodeThing
2+
3+CodeThing is a minimal web GUI for coding agents. Currently Codex-first, with Claude Code support coming soon.
4+
5+Run `npx t3` in any project directory to launch the web interface.
6+Run `bun run dev:desktop` to launch the Electron desktop app in this monorepo.
7+
8+## Architecture
9+
10+CodeThing runs as a **Node.js WebSocket server** that wraps `codex app-server` (JSON-RPC over stdio) and serves a React web app.
11+
12+```
13+┌─────────────────────────────────┐
14+│ Browser (React + Vite) │
15+│ Connected via WebSocket │
16+└──────────┬──────────────────────┘
17+ │ ws://localhost:3773
18+┌──────────▼──────────────────────┐
19+│ apps/server (Node.js) │
20+│ WebSocket + HTTP static server │
21+│ ProviderManager │
22+│ CodexAppServerManager │
23+└──────────┬──────────────────────┘
24+ │ JSON-RPC over stdio
25+┌──────────▼──────────────────────┐
26+│ codex app-server │
27+└─────────────────────────────────┘
28+```
829
930 ## Workspace layout
1031
11−- `/apps/desktop`: Electron main + preload process, includes provider and Codex session managers.
12−- `/apps/renderer`: React + Vite UI for session control, conversation, and protocol event stream.
13−- `/packages/contracts`: shared Zod schemas + TypeScript types for IPC and provider events.
32+- `/apps/server`: Node.js WebSocket server. Wraps Codex app-server, serves the built renderer, and opens the browser on start.
33+- `/apps/renderer`: React + Vite UI. Session control, conversation, and provider event rendering. Connects to the server via WebSocket.
34+- `/apps/desktop`: Electron shell. Spawns a desktop-scoped `t3` backend process and loads the shared renderer.
35+- `/packages/contracts`: Shared Zod schemas and TypeScript contracts for provider events, WebSocket protocol, and model/session types.
1436
1537 ## Codex prerequisites
1638
@@ -18,50 +40,55 @@ CodeThing is a desktop shell for coding agents. This first implementation is: · 1 unmodified line
1840 - Authenticate Codex before running CodeThing (for example via API key or ChatGPT auth supported by Codex).
1941 - CodeThing starts the server via `codex app-server` per session.
2042
21−## Security and boundary model
43+## Quick start
2244
23−- `nodeIntegration: false`
24−- `contextIsolation: true`
25−- `sandbox: true`
26−- Renderer talks only to `window.nativeApi` exposed by preload.
27−- Preload and main both validate inputs using shared Zod schemas.
45+```bash
46+# Development (with hot reload)
47+bun run dev
2848
29−`sandbox: true` above is Electron renderer sandboxing. It is separate from Codex execution sandbox policy (`read-only`, `workspace-write`, `danger-full-access`) used when starting provider sessions.
49+# Desktop development
50+bun run dev:desktop
3051
31−## Runtime modes
52+# Production
53+bun run build
54+bun run start
3255
33−CodeThing has a global runtime mode switch in the sidebar:
56+# Or from any project directory after publishing:
57+npx t3
58+```
3459
35−- `Full access` (default): starts new sessions with `approvalPolicy: never` and `sandboxMode: danger-full-access`.
36−- `Approval required`: starts new sessions with `approvalPolicy: on-request` and `sandboxMode: workspace-write`, then prompts in-app for command/file approvals.
60+## Scripts
3761
38−Mode changes apply across all threads. Existing live sessions are restarted so old and new threads use the selected mode.
62+- `bun run dev` — Starts contracts, server, and web dev tasks via Turborepo's parallel task runner.
63+- `bun run dev:server` — Starts just the WebSocket server (uses tsx for TS execution).
64+- `bun run dev:web` — Starts just the Vite dev server for the renderer.
65+- `bun run start` — Runs the production server (serves built renderer as static files).
66+- `bun run build` — Builds contracts, renderer, and server through Turbo.
67+- `bun run typecheck` — Strict TypeScript checks for all packages.
68+- `bun run test` — Runs workspace tests.
3969
40−## Scripts
70+## Runtime modes
4171
42−- `bun run dev`: starts contract build/watch, renderer dev server, and Electron process.
43−- `bun run build`: builds contracts, renderer, and desktop bundles through Turbo.
44−- `bun run typecheck`: strict TypeScript checks for all packages.
45−- `bun run test`: runs workspace tests.
72+CodeThing has a global runtime mode switch in the chat toolbar:
4673
47−## CI quality gates
74+- **Full access** (default): starts sessions with `approvalPolicy: never` and `sandboxMode: danger-full-access`.
75+- **Supervised**: starts sessions with `approvalPolicy: on-request` and `sandboxMode: workspace-write`, then prompts in-app for command/file approvals.
4876
49−- `.github/workflows/ci.yml` runs `bun run lint`, `bun run typecheck`, and `bun run test` on pull requests and pushes to `main`.
77+## Provider architecture
5078
51−Optional:
79+The renderer communicates with the server via WebSocket using a simple JSON-RPC-style protocol:
5280
53−- `ELECTRON_RENDERER_PORT=5180 bun run dev` if `5173` is already in use.
81+- **Request/Response**: `{ id, method, params }` → `{ id, result }` or `{ id, error }`
82+- **Push events**: `{ type: "push", channel, data }` for streaming provider events
5483
55−## Provider architecture
84+Methods mirror the `NativeApi` interface defined in `@acme/contracts`:
5685
57−The renderer now depends on `nativeApi.providers.*`:
86+- `providers.startSession`, `providers.sendTurn`, `providers.interruptTurn`
87+- `providers.respondToRequest`, `providers.stopSession`, `providers.listSessions`
88+- `shell.openInEditor`, `server.getConfig`
5889
59−1. `startSession`
60−2. `sendTurn`
61−3. `interruptTurn`
62−4. `respondToRequest`
63−5. `stopSession`
64−6. `listSessions`
65−7. `onEvent`
90+Codex is the only implemented provider. `claudeCode` is reserved in contracts/UI.
6691
67−Codex is the only implemented provider right now. `claudeCode` is reserved in contracts/UI but returns a not-implemented error in main-process dispatch.
92+## CI quality gates
93+
94+- `.github/workflows/ci.yml` runs `bun run lint`, `bun run typecheck`, and `bun run test` on pull requests and pushes to `main`.
apps/desktop/.gitignore+1−0
@@ -0,0 +1 @@
1+dist-electron/
apps/desktop/package.json+5−8
@@ -4,28 +4,25 @@ · 3 unmodified lines
44 "private": true,
55 "main": "dist-electron/main.js",
66 "scripts": {
7− "dev": "concurrently -k -n BUNDLE,ELECTRON \"bun run dev:bundle\" \"bun run dev:electron\"",
7+ "dev": "bun run --cwd ../server build && concurrently -k -n BUNDLE,RENDERER,ELECTRON \"bun run dev:bundle\" \"bun run --cwd ../renderer dev\" \"bun run dev:electron\"",
88 "dev:bundle": "tsup --watch",
99 "dev:electron": "bun run scripts/dev-electron.mjs",
1010 "build": "tsup",
1111 "start": "electron dist-electron/main.js",
12− "postinstall": "electron-rebuild",
1312 "typecheck": "tsc --noEmit",
14− "test": "vitest run",
13+ "test": "vitest run --passWithNoTests",
1514 "smoke-test": "node scripts/smoke-test.mjs"
1615 },
1716 "dependencies": {
18− "@acme/contracts": "workspace:*",
19− "electron": "33.4.11",
20− "node-pty": "^1.0.0"
17+ "electron": "33.4.11"
2118 },
2219 "devDependencies": {
23− "concurrently": "^9.1.2",
24− "@electron/rebuild": "^3.7.0",
2520 "@types/node": "^22.10.2",
21+ "concurrently": "^9.1.2",
2622 "electronmon": "^2.0.2",
2723 "tsup": "^8.3.5",
2824 "typescript": "^5.7.3",
25+ "vitest": "^3.0.0",
2926 "wait-on": "^8.0.2"
3027 }
3128 }
apps/desktop/scripts/dev-electron.mjs+2−2
@@ -10,11 +10,11 @@ await waitOn({ · 9 unmodified lines
1010 `tcp:${port}`,
1111 "file:dist-electron/main.js",
1212 "file:dist-electron/preload.js",
13+ "file:../server/dist/index.js",
1314 ],
1415 });
1516
16−const command =
17− process.platform === "win32" ? "electronmon.cmd" : "electronmon";
17+const command = process.platform === "win32" ? "electronmon.cmd" : "electronmon";
1818 const child = spawn(command, ["dist-electron/main.js"], {
1919 stdio: "inherit",
2020 env: {
apps/desktop/scripts/smoke-test.mjs+19−42
@@ -1,84 +1,61 @@
1−/**
2− * Smoke test: builds desktop + renderer, launches Electron against the
3− * production bundle, waits for the renderer to confirm it loaded, then exits.
4− *
5− * Catches the two categories of regression we've hit:
6− * 1. Module resolution failures (preload can't find @acme/contracts, etc.)
7− * 2. CSP / script blocking (React fails to mount)
8− *
9− * The test works by injecting a tiny check via ELECTRON_ENABLE_LOGGING —
10− * Electron forwards renderer console.log to the main process stdout when
11− * that env var is set. We look for React's "Download the React DevTools"
12− * message as proof that React successfully mounted (it only fires after
13− * the first render). For production mode (no React DevTools message), we
14− * instead check that no fatal errors appeared.
15− */
16−import { spawn, execSync } from "node:child_process";
1+import { execSync, spawn } from "node:child_process";
172 import { dirname, resolve } from "node:path";
183 import { fileURLToPath } from "node:url";
194
205 const __dirname = dirname(fileURLToPath(import.meta.url));
21−const root = resolve(__dirname, "../../..");
226 const desktopDir = resolve(__dirname, "..");
7+const rootDir = resolve(__dirname, "../../..");
238 const electronBin = resolve(desktopDir, "node_modules/.bin/electron");
249 const mainJs = resolve(desktopDir, "dist-electron/main.js");
2510
26−// ── Build first ──────────────────────────────────────────────────────
27−console.log("Building contracts + renderer + desktop...");
28−execSync("bun run build", { cwd: root, stdio: "inherit" });
11+console.log("Building contracts + renderer + server + desktop...");
12+execSync("bun run build", { cwd: rootDir, stdio: "inherit" });
2913
30−// ── Launch Electron (production mode — no VITE_DEV_SERVER_URL) ──────
31−console.log("\nLaunching Electron (production mode)...");
14+console.log("\nLaunching Electron smoke test...");
3215
3316 const child = spawn(electronBin, [mainJs], {
3417 stdio: ["pipe", "pipe", "pipe"],
3518 env: {
3619 ...process.env,
37− VITE_DEV_SERVER_URL: "", // ensure production path
20+ VITE_DEV_SERVER_URL: "",
3821 ELECTRON_ENABLE_LOGGING: "1",
3922 },
4023 });
4124
4225 let output = "";
43−
44−child.stdout.on("data", (d) => {
45− output += d.toString();
26+child.stdout.on("data", (chunk) => {
27+ output += chunk.toString();
4628 });
47−child.stderr.on("data", (d) => {
48− output += d.toString();
29+child.stderr.on("data", (chunk) => {
30+ output += chunk.toString();
4931 });
5032
51−const TIMEOUT_MS = 8_000;
52−
53−const timer = setTimeout(() => {
33+const timeout = setTimeout(() => {
5434 child.kill();
55−}, TIMEOUT_MS);
35+}, 8_000);
5636
5737 child.on("exit", () => {
58− clearTimeout(timer);
38+ clearTimeout(timeout);
5939
60− // Fatal patterns that indicate broken builds
6140 const fatalPatterns = [
6241 "Cannot find module",
6342 "MODULE_NOT_FOUND",
64− "Refused to execute", // CSP blocking scripts
65− "can't detect preamble", // @vitejs/plugin-react failure
43+ "Refused to execute",
6644 "Uncaught Error",
6745 "Uncaught TypeError",
6846 "Uncaught ReferenceError",
6947 ];
70−
71− const failures = fatalPatterns.filter((p) => output.includes(p));
48+ const failures = fatalPatterns.filter((pattern) => output.includes(pattern));
7249
7350 if (failures.length > 0) {
74− console.error("\n❌ Smoke test FAILED. Matched fatal patterns:");
75− for (const f of failures) {
76− console.error(` • ${f}`);
51+ console.error("\nDesktop smoke test failed:");
52+ for (const failure of failures) {
53+ console.error(` - ${failure}`);
7754 }
7855 console.error("\nFull output:\n" + output);
7956 process.exit(1);
8057 }
8158
82− console.log("✅ Smoke test passed — no fatal errors detected");
59+ console.log("Desktop smoke test passed.");
8360 process.exit(0);
8461 });
apps/desktop/src/fixPath.ts+1−1
@@ -13,6 +13,6 @@ export function fixPath(): void { · 12 unmodified lines
1313 process.env.PATH = result;
1414 }
1515 } catch {
16− // Silently ignore — keep default PATH
16+ // Keep inherited PATH if shell lookup fails.
1717 }
1818 }
apps/desktop/src/ipcHelpers.test.ts+0−104
@@ -1,104 +0,0 @@
1−import { describe, expect, it, vi } from "vitest";
2−
3−import { withParsedArgs, withParsedPayload } from "./ipcHelpers";
4−
5−describe("withParsedPayload", () => {
6− it("parses payload and passes typed input to handler", async () => {
7− const handler = vi.fn(async (_event: unknown, payload: { value: string }) =>
8− payload.value.toUpperCase(),
9− );
10− const wrapped = withParsedPayload(
11− {
12− parse(payload: unknown): { value: string } {
13− if (
14− !payload ||
15− typeof payload !== "object" ||
16− typeof (payload as { value?: unknown }).value !== "string"
17− ) {
18− throw new Error("Invalid payload");
19− }
20−
21− return { value: (payload as { value: string }).value };
22− },
23− },
24− handler,
25− );
26−
27− const result = await wrapped({}, { value: "hello" });
28− expect(result).toBe("HELLO");
29− expect(handler).toHaveBeenCalledWith({}, { value: "hello" });
30− });
31−
32− it("throws and does not call handler on invalid payload", async () => {
33− const handler = vi.fn(async () => "ok");
34− const wrapped = withParsedPayload(
35− {
36− parse(payload: unknown): { value: string } {
37− if (
38− !payload ||
39− typeof payload !== "object" ||
40− typeof (payload as { value?: unknown }).value !== "string"
41− ) {
42− throw new Error("Invalid payload");
43− }
44−
45− return { value: (payload as { value: string }).value };
46− },
47− },
48− handler,
49− );
50−
51− expect(() => wrapped({}, { value: 123 })).toThrow();
52− expect(handler).not.toHaveBeenCalled();
53− });
54−});
55−
56−describe("withParsedArgs", () => {
57− it("parses tuple arguments before invoking handler", () => {
58− const handler = vi.fn((_event: unknown, sessionId: string, data: string) => {
59− return `${sessionId}:${data}`;
60− });
61− const wrapped = withParsedArgs(
62− {
63− parse(args: unknown[]): [string, string] {
64− const [sessionId, data] = args;
65− if (typeof sessionId !== "string" || sessionId.length === 0) {
66− throw new Error("Invalid sessionId");
67− }
68− if (typeof data !== "string") {
69− throw new Error("Invalid data");
70− }
71−
72− return [sessionId, data];
73− },
74− },
75− handler,
76− );
77−
78− expect(wrapped({}, "abc", "input")).toBe("abc:input");
79− expect(handler).toHaveBeenCalledWith({}, "abc", "input");
80− });
81−
82− it("throws and does not call handler when args are invalid", () => {
83− const handler = vi.fn();
84− const wrapped = withParsedArgs(
85− {
86− parse(args: unknown[]): [string, string] {
87− const [sessionId, data] = args;
88− if (typeof sessionId !== "string" || sessionId.length === 0) {
89− throw new Error("Invalid sessionId");
90− }
91− if (typeof data !== "string") {
92− throw new Error("Invalid data");
93− }
94−
95− return [sessionId, data];
96− },
97− },
98− handler,
99− );
100−
101− expect(() => wrapped({}, 123, "input")).toThrow();
102− expect(handler).not.toHaveBeenCalled();
103− });
104−});
apps/desktop/src/ipcHelpers.ts+0−24
@@ -1,24 +0,0 @@
1−type MaybePromise<T> = T | Promise<T>;
2−type Parser<T> = {
3− parse: (value: unknown) => T;
4−};
5−type ArgsParser<T extends unknown[]> = {
6− parse: (value: unknown[]) => T;
7−};
8−
9−export function withParsedPayload<TPayload, TResult>(
10− schema: Parser<TPayload>,
11− handler: (event: unknown, payload: TPayload) => MaybePromise<TResult>,
12−): (event: unknown, payload: unknown) => MaybePromise<TResult> {
13− return (event, payload) => handler(event, schema.parse(payload));
14−}
15−
16−export function withParsedArgs<TArgs extends unknown[], TResult>(
17− schema: ArgsParser<TArgs>,
18− handler: (event: unknown, ...args: TArgs) => MaybePromise<TResult>,
19−): (event: unknown, ...args: unknown[]) => MaybePromise<TResult> {
20− return (event, ...args) => {
21− const parsedArgs = schema.parse(args);
22− return handler(event, ...parsedArgs);
23− };
24−}
apps/desktop/src/main.ts+156−290
@@ -1,111 +1,134 @@
1−import { fixPath } from "./fixPath";
2−fixPath();
3−
4−import { spawn } from "node:child_process";
1+import { spawn, type ChildProcess } from "node:child_process";
2+import { randomBytes } from "node:crypto";
3+import fs from "node:fs";
4+import net from "node:net";
5+import os from "node:os";
56 import path from "node:path";
6−import { BrowserWindow, app, dialog, ipcMain, session, shell } from "electron";
77
8−import {
9− EDITORS,
10− IPC_CHANNELS,
11− type TerminalCommandInput,
12− type TerminalCommandResult,
13− agentConfigSchema,
14− agentSessionIdSchema,
15− newTodoInputSchema,
16− providerInterruptTurnInputSchema,
17− providerRespondToRequestInputSchema,
18− providerSendTurnInputSchema,
19− providerSessionStartInputSchema,
20− providerStopSessionInputSchema,
21− terminalCommandInputSchema,
22− todoIdSchema,
23−} from "@acme/contracts";
24−import { withParsedArgs, withParsedPayload } from "./ipcHelpers";
25−import { ProcessManager } from "./processManager";
26−import { ProviderManager } from "./providerManager";
27−import { TodoStore } from "./todoStore";
8+import { app, BrowserWindow, dialog, ipcMain } from "electron";
289
29−const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);
10+import { fixPath } from "./fixPath";
3011
31−let todoStore: TodoStore;
32−const processManager = new ProcessManager();
33−const providerManager = new ProviderManager();
34−const agentWriteArgsParser = {
35− parse(args: unknown[]): [string, string] {
36− const [sessionId, data] = args;
37− if (typeof data !== "string") {
38− throw new Error("agent:write data must be a string");
39− }
12+fixPath();
4013
41− return [agentSessionIdSchema.parse(sessionId), data];
42− },
43−};
14+const PICK_FOLDER_CHANNEL = "desktop:pick-folder";
15+const ROOT_DIR = path.resolve(__dirname, "../../..");
16+const BACKEND_ENTRY = path.join(ROOT_DIR, "apps/server/dist/index.js");
17+const RENDERER_ENTRY = path.join(ROOT_DIR, "apps/renderer/dist/index.html");
18+const STATE_DIR = path.join(os.homedir(), ".t3", "userdata");
19+const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);
4420
45−function createWindow(): BrowserWindow {
46− const window = new BrowserWindow({
47− width: 1100,
48− height: 780,
49− minWidth: 840,
50− minHeight: 620,
51− show: false,
52− autoHideMenuBar: true,
53− titleBarStyle: "hiddenInset",
54− trafficLightPosition: { x: 16, y: 18 },
55− webPreferences: {
56− preload: path.join(__dirname, "preload.js"),
57− contextIsolation: true,
58− nodeIntegration: false,
59− sandbox: true,
60− },
21+let mainWindow: BrowserWindow | null = null;
22+let backendProcess: ChildProcess | null = null;
23+let backendPort = 0;
24+let backendAuthToken = "";
25+let backendWsUrl = "";
26+let restartAttempt = 0;
27+let restartTimer: ReturnType<typeof setTimeout> | null = null;
28+let isQuitting = false;
29+
30+async function reserveLoopbackPort(): Promise<number> {
31+ return new Promise((resolve, reject) => {
32+ const probe = net.createServer();
33+ probe.listen(0, "127.0.0.1", () => {
34+ const address = probe.address();
35+ const port = typeof address === "object" && address !== null ? address.port : 0;
36+ probe.close(() => {
37+ if (port > 0) {
38+ resolve(port);
39+ return;
40+ }
41+ reject(new Error("Failed to reserve backend port"));
42+ });
43+ });
44+ probe.on("error", reject);
6145 });
46+}
6247
63− window.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
48+function backendEnv(): NodeJS.ProcessEnv {
49+ return {
50+ ...process.env,
51+ CODETHING_MODE: "desktop",
52+ CODETHING_NO_BROWSER: "1",
53+ CODETHING_PORT: String(backendPort),
54+ CODETHING_STATE_DIR: STATE_DIR,
55+ CODETHING_AUTH_TOKEN: backendAuthToken,
56+ };
57+}
6458
65− window.once("ready-to-show", () => {
66− window.show();
67− });
59+function scheduleBackendRestart(reason: string): void {
60+ if (isQuitting || restartTimer) return;
6861
69− setupEventForwarding(window);
62+ const delayMs = Math.min(500 * 2 ** restartAttempt, 10_000);
63+ restartAttempt += 1;
64+ console.error(`[desktop] backend exited unexpectedly (${reason}); restarting in ${delayMs}ms`);
7065
71− if (isDevelopment) {
72− void window.loadURL(process.env.VITE_DEV_SERVER_URL as string);
73− return window;
66+ restartTimer = setTimeout(() => {
67+ restartTimer = null;
68+ startBackend();
69+ }, delayMs);
70+}
71+
72+function startBackend(): void {
73+ if (isQuitting || backendProcess) return;
74+
75+ if (!fs.existsSync(BACKEND_ENTRY)) {
76+ scheduleBackendRestart(`missing server entry at ${BACKEND_ENTRY}`);
77+ return;
7478 }
7579
76− void window.loadFile(path.join(__dirname, "../../renderer/dist/index.html"));
77− return window;
78−}
80+ const child = spawn(process.execPath, [BACKEND_ENTRY], {
81+ cwd: ROOT_DIR,
82+ env: backendEnv(),
83+ stdio: "inherit",
84+ });
85+ backendProcess = child;
7986
80−function registerIpcHandlers(): void {
81− // Todo handlers
82− ipcMain.handle(IPC_CHANNELS.todosList, async () => {
83− return todoStore.list();
87+ child.once("spawn", () => {
88+ restartAttempt = 0;
89+ });
90+
91+ child.on("error", (error) => {
92+ if (backendProcess === child) {
93+ backendProcess = null;
94+ }
95+ scheduleBackendRestart(error.message);
96+ });
97+
98+ child.on("exit", (code, signal) => {
99+ if (backendProcess === child) {
100+ backendProcess = null;
101+ }
102+ if (isQuitting) return;
103+ const reason = `code=${code ?? "null"} signal=${signal ?? "null"}`;
104+ scheduleBackendRestart(reason);
84105 });
106+}
85107
86− ipcMain.handle(
87− IPC_CHANNELS.todosAdd,
88− withParsedPayload(newTodoInputSchema, async (_event, payload) => {
89− return todoStore.add(payload);
90− }),
91− );
108+function stopBackend(): void {
109+ if (restartTimer) {
110+ clearTimeout(restartTimer);
111+ restartTimer = null;
112+ }
92113
93− ipcMain.handle(
94− IPC_CHANNELS.todosToggle,
95− withParsedPayload(todoIdSchema, async (_event, id) => {
96− return todoStore.toggle(id);
97− }),
98− );
114+ const child = backendProcess;
115+ backendProcess = null;
116+ if (!child) return;
99117
100− ipcMain.handle(
101− IPC_CHANNELS.todosRemove,
102− withParsedPayload(todoIdSchema, async (_event, id) => {
103− return todoStore.remove(id);
104− }),
105− );
118+ if (!child.killed) {
119+ child.kill("SIGTERM");
120+ setTimeout(() => {
121+ if (!child.killed) {
122+ child.kill("SIGKILL");
123+ }
124+ }, 2_000).unref();
125+ }
126+}
106127
107− ipcMain.handle(IPC_CHANNELS.dialogPickFolder, async () => {
108− const owner = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0];
128+function registerIpcHandlers(): void {
129+ ipcMain.removeHandler(PICK_FOLDER_CHANNEL);
130+ ipcMain.handle(PICK_FOLDER_CHANNEL, async () => {
131+ const owner = BrowserWindow.getFocusedWindow() ?? mainWindow;
109132 const result = owner
110133 ? await dialog.showOpenDialog(owner, {
111134 properties: ["openDirectory", "createDirectory"],
@@ -113,233 +136,76 @@ function registerIpcHandlers(): void { · 1 unmodified line
113136 : await dialog.showOpenDialog({
114137 properties: ["openDirectory", "createDirectory"],
115138 });
116−
117139 if (result.canceled) return null;
118140 return result.filePaths[0] ?? null;
119141 });
142+}
120143
121− // Terminal handlers
122− ipcMain.handle(
123− IPC_CHANNELS.terminalRun,
124− withParsedPayload(terminalCommandInputSchema, async (_event, payload) => {
125− return runTerminalCommand(payload);
126− }),
127− );
128−
129− // Shell handlers
130− ipcMain.handle(
131− IPC_CHANNELS.shellOpenInEditor,
132− async (_event, cwd: string, editor: string) => {
133− if (!cwd) throw new Error("cwd is required");
134− const editorDef = EDITORS.find((e) => e.id === editor);
135− if (!editorDef) throw new Error(`Unknown editor: ${editor}`);
136− if (!editorDef.command) {
137− const error = await shell.openPath(cwd);
138− if (error) throw new Error(error);
139− return;
140− }
141− const child = spawn(editorDef.command, [cwd], {
142− detached: true,
143− stdio: "ignore",
144− });
145− child.on("error", () => {
146− /* ignore spawn failures for detached editors */
147− });
148− child.unref();
144+function createWindow(): BrowserWindow {
145+ const window = new BrowserWindow({
146+ width: 1100,
147+ height: 780,
148+ minWidth: 840,
149+ minHeight: 620,
150+ show: false,
151+ autoHideMenuBar: true,
152+ titleBarStyle: "hiddenInset",
153+ trafficLightPosition: { x: 16, y: 18 },
154+ webPreferences: {
155+ preload: path.join(__dirname, "preload.js"),
156+ contextIsolation: true,
157+ nodeIntegration: false,
158+ sandbox: true,
149159 },
150− );
151−
152− // Agent handlers
153− ipcMain.handle(
154− IPC_CHANNELS.agentSpawn,
155− withParsedPayload(agentConfigSchema, async (_event, config) => {
156− return processManager.spawn(config);
157− }),
158− );
159−
160− ipcMain.handle(
161− IPC_CHANNELS.agentKill,
162− withParsedPayload(agentSessionIdSchema, async (_event, sessionId) => {
163− processManager.kill(sessionId);
164− }),
165− );
166−
167− ipcMain.handle(
168− IPC_CHANNELS.agentWrite,
169− withParsedArgs(agentWriteArgsParser, async (_event, sessionId, data) => {
170− processManager.write(sessionId, data);
171− }),
172− );
173−
174− // Provider handlers
175− ipcMain.handle(
176− IPC_CHANNELS.providerSessionStart,
177− withParsedPayload(providerSessionStartInputSchema, async (_event, payload) => {
178− return providerManager.startSession(payload);
179− }),
180− );
181−
182− ipcMain.handle(
183− IPC_CHANNELS.providerTurnStart,
184− withParsedPayload(providerSendTurnInputSchema, async (_event, payload) => {
185− return providerManager.sendTurn(payload);
186− }),
187− );
188−
189− ipcMain.handle(
190− IPC_CHANNELS.providerTurnInterrupt,
191− withParsedPayload(providerInterruptTurnInputSchema, async (_event, payload) => {
192− await providerManager.interruptTurn(payload);
193− }),
194− );
195−
196− ipcMain.handle(
197− IPC_CHANNELS.providerRequestRespond,
198− withParsedPayload(
199− providerRespondToRequestInputSchema,
200− async (_event, payload) => {
201− await providerManager.respondToRequest(payload);
202− },
203− ),
204− );
205−
206− ipcMain.handle(
207− IPC_CHANNELS.providerSessionStop,
208− withParsedPayload(providerStopSessionInputSchema, async (_event, payload) => {
209− providerManager.stopSession(payload);
210− }),
211− );
212−
213− ipcMain.handle(IPC_CHANNELS.providerSessionList, async () => {
214− return providerManager.listSessions();
215160 });
216−}
217−
218−async function runTerminalCommand(input: TerminalCommandInput): Promise<TerminalCommandResult> {
219− const shellPath =
220− process.platform === "win32"
221− ? (process.env.ComSpec ?? "cmd.exe")
222− : (process.env.SHELL ?? "/bin/sh");
223−
224− const args =
225− process.platform === "win32" ? ["/d", "/s", "/c", input.command] : ["-lc", input.command];
226−
227− return new Promise((resolve, reject) => {
228− const child = spawn(shellPath, args, {
229− cwd: input.cwd,
230− env: process.env,
231− stdio: ["ignore", "pipe", "pipe"],
232− });
233−
234− let stdout = "";
235− let stderr = "";
236− let timedOut = false;
237−
238− const timeout = setTimeout(() => {
239− timedOut = true;
240− child.kill("SIGTERM");
241− setTimeout(() => {
242− if (!child.killed) {
243− child.kill("SIGKILL");
244− }
245− }, 1_000).unref();
246− }, input.timeoutMs ?? 30_000);
247−
248− child.stdout?.on("data", (chunk: Buffer) => {
249− stdout += chunk.toString();
250− });
251−
252− child.stderr?.on("data", (chunk: Buffer) => {
253− stderr += chunk.toString();
254− });
255−
256− child.on("error", (error) => {
257− clearTimeout(timeout);
258− reject(error);
259− });
260161
261− child.on("close", (code, signal) => {
262− clearTimeout(timeout);
263− resolve({
264− stdout,
265− stderr,
266− code: code ?? null,
267− signal: signal ?? null,
268− timedOut,
269− });
270− });
162+ window.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
163+ window.once("ready-to-show", () => {
164+ window.show();
271165 });
272−}
273−
274−function setupEventForwarding(window: BrowserWindow): void {
275− const onOutput = (chunk: unknown) => {
276− if (!window.isDestroyed()) {
277− window.webContents.send(IPC_CHANNELS.agentOutput, chunk);
278− }
279− };
280166
281− const onExit = (exit: unknown) => {
282− if (!window.isDestroyed()) {
283− window.webContents.send(IPC_CHANNELS.agentExit, exit);
284− }
285− };
286−
287− const onProviderEvent = (event: unknown) => {
288− if (!window.isDestroyed()) {
289− window.webContents.send(IPC_CHANNELS.providerEvent, event);
167+ if (isDevelopment) {
168+ void window.loadURL(process.env.VITE_DEV_SERVER_URL as string);
169+ } else {
170+ if (!fs.existsSync(RENDERER_ENTRY)) {
171+ throw new Error(`Renderer bundle missing at ${RENDERER_ENTRY}`);
290172 }
291− };
292−
293− processManager.on("output", onOutput);
294− processManager.on("exit", onExit);
295− providerManager.on("event", onProviderEvent);
173+ void window.loadFile(RENDERER_ENTRY);
174+ }
296175
297176 window.on("closed", () => {
298− processManager.off("output", onOutput);
299− processManager.off("exit", onExit);
300− providerManager.off("event", onProviderEvent);
177+ if (mainWindow === window) {
178+ mainWindow = null;
179+ }
301180 });
302−}
303−
304−function setupCSP(): void {
305− session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
306− const csp = isDevelopment
307− ? "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://localhost:* http://localhost:*"
308− : "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'";
309181
310− callback({
311− responseHeaders: {
312− ...details.responseHeaders,
313− "Content-Security-Policy": [csp],
314− },
315− });
316− });
182+ return window;
317183 }
318184
319185 async function bootstrap(): Promise<void> {
320− setupCSP();
321−
322− todoStore = new TodoStore(path.join(app.getPath("userData"), "todos.json"));
323− await todoStore.init();
186+ backendPort = await reserveLoopbackPort();
187+ backendAuthToken = randomBytes(24).toString("hex");
188+ backendWsUrl = `ws://127.0.0.1:${backendPort}/?token=${encodeURIComponent(backendAuthToken)}`;
189+ process.env.CODETHING_DESKTOP_WS_URL = backendWsUrl;
324190
325191 registerIpcHandlers();
326− createWindow();
327−
328− app.on("activate", () => {
329− if (BrowserWindow.getAllWindows().length === 0) {
330− createWindow();
331− }
332− });
192+ startBackend();
193+ mainWindow = createWindow();
333194 }
334195
335196 app.on("before-quit", () => {
336− processManager.killAll();
337− providerManager.stopAll();
338− providerManager.dispose();
197+ isQuitting = true;
198+ stopBackend();
339199 });
340200
341201 app.whenReady().then(() => {
342202 void bootstrap();
203+
204+ app.on("activate", () => {
205+ if (BrowserWindow.getAllWindows().length === 0) {
206+ mainWindow = createWindow();
207+ }
208+ });
343209 });
344210
345211 app.on("window-all-closed", () => {
apps/desktop/src/preload.ts+6−52
@@ -1,55 +1,9 @@
11 import { contextBridge, ipcRenderer } from "electron";
22
3−import { IPC_CHANNELS, type NativeApi } from "@acme/contracts";
3+const PICK_FOLDER_CHANNEL = "desktop:pick-folder";
4+const wsUrl = process.env.CODETHING_DESKTOP_WS_URL ?? null;
45
5−const nativeApi: NativeApi = {
6− todos: {
7− list: () => ipcRenderer.invoke(IPC_CHANNELS.todosList),
8− add: (input) => ipcRenderer.invoke(IPC_CHANNELS.todosAdd, input),
9− toggle: (id) => ipcRenderer.invoke(IPC_CHANNELS.todosToggle, id),
10− remove: (id) => ipcRenderer.invoke(IPC_CHANNELS.todosRemove, id),
11− },
12− dialogs: {
13− pickFolder: () => ipcRenderer.invoke(IPC_CHANNELS.dialogPickFolder),
14− },
15− terminal: {
16− run: (input) => ipcRenderer.invoke(IPC_CHANNELS.terminalRun, input),
17− },
18− agent: {
19− spawn: (config) => ipcRenderer.invoke(IPC_CHANNELS.agentSpawn, config),
20− kill: (sessionId) => ipcRenderer.invoke(IPC_CHANNELS.agentKill, sessionId),
21− write: (sessionId, data) => ipcRenderer.invoke(IPC_CHANNELS.agentWrite, sessionId, data),
22− onOutput: (callback) => {
23− const listener = (_event: Electron.IpcRendererEvent, chunk: unknown) =>
24− callback(chunk as Parameters<typeof callback>[0]);
25− ipcRenderer.on(IPC_CHANNELS.agentOutput, listener);
26− return () => ipcRenderer.removeListener(IPC_CHANNELS.agentOutput, listener);
27− },
28− onExit: (callback) => {
29− const listener = (_event: Electron.IpcRendererEvent, exit: unknown) =>
30− callback(exit as Parameters<typeof callback>[0]);
31− ipcRenderer.on(IPC_CHANNELS.agentExit, listener);
32− return () => ipcRenderer.removeListener(IPC_CHANNELS.agentExit, listener);
33− },
34− },
35− providers: {
36− startSession: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerSessionStart, input),
37− sendTurn: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerTurnStart, input),
38− interruptTurn: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerTurnInterrupt, input),
39− respondToRequest: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerRequestRespond, input),
40− stopSession: (input) => ipcRenderer.invoke(IPC_CHANNELS.providerSessionStop, input),
41− listSessions: () => ipcRenderer.invoke(IPC_CHANNELS.providerSessionList),
42− onEvent: (callback) => {
43− const listener = (_event: Electron.IpcRendererEvent, payload: unknown) =>
44− callback(payload as Parameters<typeof callback>[0]);
45− ipcRenderer.on(IPC_CHANNELS.providerEvent, listener);
46− return () => ipcRenderer.removeListener(IPC_CHANNELS.providerEvent, listener);
47− },
48− },
49− shell: {
50− openInEditor: (cwd: string, editor: string) =>
51− ipcRenderer.invoke(IPC_CHANNELS.shellOpenInEditor, cwd, editor),
52− },
53−};
54−
55−contextBridge.exposeInMainWorld("nativeApi", nativeApi);
6+contextBridge.exposeInMainWorld("desktopBridge", {
7+ getWsUrl: () => wsUrl,
8+ pickFolder: () => ipcRenderer.invoke(PICK_FOLDER_CHANNEL) as Promise<string | null>,
9+});
apps/desktop/src/processManager.test.ts+0−48
@@ -1,48 +0,0 @@
1−import { describe, expect, it } from "vitest";
2−
3−import { ProcessManager } from "./processManager";
4−
5−describe("ProcessManager", () => {
6− it("can be instantiated", () => {
7− const pm = new ProcessManager();
8− expect(pm).toBeInstanceOf(ProcessManager);
9− });
10−
11− it("spawns a process and receives output", async () => {
12− const pm = new ProcessManager();
13− const chunks: string[] = [];
14−
15− const done = new Promise<void>((resolve) => {
16− pm.on("output", (chunk) => {
17− chunks.push(chunk.data);
18− });
19− pm.on("exit", () => {
20− resolve();
21− });
22− });
23−
24− const sessionId = pm.spawn({ command: "echo", args: ["hello"] });
25− expect(typeof sessionId).toBe("string");
26− expect(sessionId.length).toBeGreaterThan(0);
27−
28− await done;
29−
30− expect(chunks.join("").trim()).toBe("hello");
31− });
32−
33− it("can kill a spawned process", async () => {
34− const pm = new ProcessManager();
35−
36− const exited = new Promise<number | null>((resolve) => {
37− pm.on("exit", (exit) => {
38− resolve(exit.code);
39− });
40− });
41−
42− const sessionId = pm.spawn({ command: "sleep", args: ["10"] });
43− pm.kill(sessionId);
44−
45− const code = await exited;
46− expect(code).not.toBe(0);
47− });
48−});
apps/desktop/src/processManager.ts+0−144
@@ -1,144 +0,0 @@
1−import { type ChildProcess, spawn } from "node:child_process";
2−import { randomUUID } from "node:crypto";
3−import { EventEmitter } from "node:events";
4−
5−import {
6− type AgentConfig,
7− type AgentExit,
8− type OutputChunk,
9− agentConfigSchema,
10−} from "@acme/contracts";
11−import type { IPty } from "node-pty";
12−
13−export interface ProcessManagerEvents {
14− output: [chunk: OutputChunk];
15− exit: [exit: AgentExit];
16−}
17−
18−export class ProcessManager extends EventEmitter<ProcessManagerEvents> {
19− private sessions = new Map<string, ChildProcess>();
20− private ptySessions = new Map<string, IPty>();
21−
22− spawn(raw: AgentConfig): string {
23− const config = agentConfigSchema.parse(raw);
24− const sessionId = randomUUID();
25−
26− if (config.usePty) {
27− return this.spawnPty(sessionId, config);
28− }
29−
30− return this.spawnProcess(sessionId, config);
31− }
32−
33− private spawnProcess(sessionId: string, config: AgentConfig): string {
34− const child = spawn(config.command, config.args, {
35− cwd: config.cwd,
36− env: config.env ? { ...process.env, ...config.env } : process.env,
37− stdio: ["pipe", "pipe", "pipe"],
38− });
39−
40− this.sessions.set(sessionId, child);
41−
42− child.stdout?.on("data", (data: Buffer) => {
43− this.emit("output", {
44− sessionId,
45− stream: "stdout",
46− data: data.toString(),
47− });
48− });
49−
50− child.stderr?.on("data", (data: Buffer) => {
51− this.emit("output", {
52− sessionId,
53− stream: "stderr",
54− data: data.toString(),
55− });
56− });
57−
58− child.on("exit", (code, signal) => {
59− this.sessions.delete(sessionId);
60− this.emit("exit", {
61− sessionId,
62− code: code ?? null,
63− signal: signal ?? null,
64− });
65− });
66−
67− return sessionId;
68− }
69−
70− private spawnPty(sessionId: string, config: AgentConfig): string {
71− const pty = require("node-pty") as typeof import("node-pty");
72−
73− const ptyProcess = pty.spawn(config.command, config.args, {
74− name: "xterm-256color",
75− cols: 120,
76− rows: 30,
77− cwd: config.cwd ?? process.cwd(),
78− env: (config.env ? { ...process.env, ...config.env } : process.env) as Record<string, string>,
79− });
80−
81− this.ptySessions.set(sessionId, ptyProcess);
82−
83− ptyProcess.onData((data) => {
84− this.emit("output", {
85− sessionId,
86− stream: "stdout",
87− data,
88− });
89− });
90−
91− ptyProcess.onExit(({ exitCode, signal }) => {
92− this.ptySessions.delete(sessionId);
93− this.emit("exit", {
94− sessionId,
95− code: exitCode,
96− signal: signal !== undefined ? String(signal) : null,
97− });
98− });
99−
100− return sessionId;
101− }
102−
103− write(sessionId: string, data: string): void {
104− const child = this.sessions.get(sessionId);
105− if (child) {
106− child.stdin?.write(data);
107− return;
108− }
109−
110− const pty = this.ptySessions.get(sessionId);
111− if (pty) {
112− pty.write(data);
113− return;
114− }
115−
116− throw new Error(`No session: ${sessionId}`);
117− }
118−
119− kill(sessionId: string): void {
120− const child = this.sessions.get(sessionId);
121− if (child) {
122− child.kill();
123− return;
124− }
125−
126− const pty = this.ptySessions.get(sessionId);
127− if (pty) {
128− pty.kill();
129− return;
130− }
131− }
132−
133− killAll(): void {
134− for (const child of this.sessions.values()) {
135− child.kill();
136− }
137− this.sessions.clear();
138−
139− for (const pty of this.ptySessions.values()) {
140− pty.kill();
141− }
142− this.ptySessions.clear();
143− }
144−}
apps/desktop/src/todoStore.ts+0−106
@@ -1,106 +0,0 @@
1−import { randomUUID } from "node:crypto";
2−import { promises as fs } from "node:fs";
3−import path from "node:path";
4−
5−import {
6− type NewTodoInput,
7− type Todo,
8− newTodoInputSchema,
9− todoIdSchema,
10− todoListSchema,
11−} from "@acme/contracts";
12−
13−export class TodoStore {
14− private todos: Todo[] = [];
15− private queue: Promise<void> = Promise.resolve();
16−
17− public constructor(private readonly filePath: string) {}
18−
19− public async init(): Promise<void> {
20− return this.runExclusive(async () => {
21− await fs.mkdir(path.dirname(this.filePath), { recursive: true });
22−
23− try {
24− const raw = await fs.readFile(this.filePath, "utf8");
25− const parsed = todoListSchema.safeParse(JSON.parse(raw));
26−
27− if (parsed.success) {
28− this.todos = parsed.data;
29− return;
30− }
31− } catch (error) {
32− if (!isNotFoundError(error)) {
33− throw error;
34− }
35− }
36−
37− this.todos = [];
38− await this.persist();
39− });
40− }
41−
42− public async list(): Promise<Todo[]> {
43− return this.runExclusive(async () => [...this.todos]);
44− }
45−
46− public async add(input: NewTodoInput): Promise<Todo[]> {
47− return this.runExclusive(async () => {
48− const { title } = newTodoInputSchema.parse(input);
49− const todo: Todo = {
50− id: randomUUID(),
51− title,
52− completed: false,
53− createdAt: new Date().toISOString(),
54− };
55−
56− this.todos = [todo, ...this.todos];
57− await this.persist();
58− return [...this.todos];
59− });
60− }
61−
62− public async toggle(id: string): Promise<Todo[]> {
63− return this.runExclusive(async () => {
64− const parsedId = todoIdSchema.parse(id);
65− this.todos = this.todos.map((todo) =>
66− todo.id === parsedId ? { ...todo, completed: !todo.completed } : todo,
67− );
68−
69− await this.persist();
70− return [...this.todos];
71− });
72− }
73−
74− public async remove(id: string): Promise<Todo[]> {
75− return this.runExclusive(async () => {
76− const parsedId = todoIdSchema.parse(id);
77− this.todos = this.todos.filter((todo) => todo.id !== parsedId);
78−
79− await this.persist();
80− return [...this.todos];
81− });
82− }
83−
84− private async persist(): Promise<void> {
85− await fs.writeFile(this.filePath, JSON.stringify(this.todos, null, 2), "utf8");
86− }
87−
88− private async runExclusive<T>(operation: () => Promise<T>): Promise<T> {
89− const next = this.queue.then(operation, operation);
90− this.queue = next.then(
91− () => undefined,
92− () => undefined,
93− );
94−
95− return next;
96− }
97−}
98−
99−function isNotFoundError(error: unknown): boolean {
100− return (
101− typeof error === "object" &&
102− error !== null &&
103− "code" in error &&
104− (error as { code?: string }).code === "ENOENT"
105− );
106−}
apps/renderer/index.html+1−1
@@ -3,7 +3,7 @@ · 2 unmodified lines
33 <head>
44 <meta charset="UTF-8" />
55 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6− <title>Electron Todo</title>
6+ <title>CodeThing</title>
77 </head>
88 <body>
99 <div id="root"></div>
apps/renderer/src/App.tsx+118−4
@@ -3,8 +3,11 @@ import { useEffect, useMemo, useRef } from "react"; · 2 unmodified lines
33 import ChatView from "./components/ChatView";
44 import DiffPanel from "./components/DiffPanel";
55 import Sidebar from "./components/Sidebar";
6+import { isElectron } from "./env";
7+import { DEFAULT_MODEL } from "./model-logic";
68 import { readNativeApi } from "./session-logic";
79 import { StoreProvider, useStore } from "./store";
10+import { onServerWelcome } from "./wsNativeApi";
811
912 function EventRouter() {
1013 const api = useMemo(() => readNativeApi(), []);
@@ -25,6 +28,118 @@ function EventRouter() { · 14 unmodified lines
2528 return null;
2629 }
2730
31+function AutoProjectBootstrap() {
32+ const { state, dispatch } = useStore();
33+ const bootstrappedRef = useRef(false);
34+
35+ useEffect(() => {
36+ // Browser mode bootstraps from server welcome.
37+ // Electron bootstraps from persisted projects via DesktopProjectBootstrap.
38+ if (isElectron) return;
39+
40+ return onServerWelcome((payload) => {
41+ if (bootstrappedRef.current) return;
42+
43+ // Don't create duplicate projects for the same cwd
44+ const existing = state.projects.find((p) => p.cwd === payload.cwd);
45+ if (existing) {
46+ bootstrappedRef.current = true;
47+ // Ensure a thread is active
48+ const existingThread = state.threads.find((t) => t.projectId === existing.id);
49+ if (existingThread && !state.activeThreadId) {
50+ dispatch({
51+ type: "SET_ACTIVE_THREAD",
52+ threadId: existingThread.id,
53+ });
54+ }
55+ return;
56+ }
57+
58+ bootstrappedRef.current = true;
59+
60+ // Create project + thread from server cwd
61+ const projectId = crypto.randomUUID();
62+ dispatch({
63+ type: "ADD_PROJECT",
64+ project: {
65+ id: projectId,
66+ name: payload.projectName,
67+ cwd: payload.cwd,
68+ model: DEFAULT_MODEL,
69+ expanded: true,
70+ },
71+ });
72+ dispatch({
73+ type: "ADD_THREAD",
74+ thread: {
75+ id: crypto.randomUUID(),
76+ codexThreadId: null,
77+ projectId,
78+ title: "New thread",
79+ model: DEFAULT_MODEL,
80+ session: null,
81+ messages: [],
82+ events: [],
83+ error: null,
84+ createdAt: new Date().toISOString(),
85+ },
86+ });
87+ });
88+ }, [state.projects, state.threads, state.activeThreadId, dispatch]);
89+
90+ return null;
91+}
92+
93+function DesktopProjectBootstrap() {
94+ const api = useMemo(() => readNativeApi(), []);
95+ const { dispatch } = useStore();
96+ const bootstrappedRef = useRef(false);
97+
98+ useEffect(() => {
99+ if (!isElectron || !api || bootstrappedRef.current) return;
100+
101+ let disposed = false;
102+ let retryDelayMs = 500;
103+ let retryTimer: ReturnType<typeof setTimeout> | null = null;
104+
105+ const attemptBootstrap = async () => {
106+ try {
107+ const projects = await api.projects.list();
108+ if (disposed) return;
109+ dispatch({
110+ type: "SYNC_PROJECTS",
111+ projects: projects.map((project) => ({
112+ id: project.id,
113+ name: project.name,
114+ cwd: project.cwd,
115+ model: DEFAULT_MODEL,
116+ expanded: true,
117+ })),
118+ });
119+ bootstrappedRef.current = true;
120+ } catch {
121+ if (disposed) return;
122+ retryTimer = setTimeout(() => {
123+ retryTimer = null;
124+ void attemptBootstrap();
125+ }, retryDelayMs);
126+ retryDelayMs = Math.min(retryDelayMs * 2, 5_000);
127+ }
128+ };
129+
130+ void attemptBootstrap();
131+
132+ return () => {
133+ disposed = true;
134+ if (retryTimer) {
135+ clearTimeout(retryTimer);
136+ }
137+ };
138+ }, [api, dispatch]);
139+
140+ return null;
141+}
142+
28143 function Layout() {
29144 const api = useMemo(() => readNativeApi(), []);
30145 const { state } = useStore();
@@ -32,11 +147,8 @@ function Layout() { · 1 unmodified line
32147 if (!api) {
33148 return (
34149 <div className="flex h-screen flex-col bg-background text-foreground">
35− <div className="drag-region h-[52px] shrink-0" />
36150 <div className="flex flex-1 items-center justify-center">
37− <p className="text-sm text-muted-foreground">
38− Native bridge unavailable. Launch through Electron.
39− </p>
151+ <p className="text-sm text-muted-foreground">Connecting to CodeThing server...</p>
40152 </div>
41153 </div>
42154 );
@@ -45,6 +157,8 @@ function Layout() { · 2 unmodified lines
45157 return (
46158 <div className="flex h-screen overflow-hidden bg-background text-foreground">
47159 <EventRouter />
160+ <AutoProjectBootstrap />
161+ <DesktopProjectBootstrap />
48162 <Sidebar />
49163 <ChatView />
50164 {state.diffOpen && <DiffPanel />}
apps/renderer/src/components/ChatView.tsx+3−2
@@ -14,6 +14,7 @@ import { · 13 unmodified lines
1414 } from "react";
1515
1616 import { EDITORS, type EditorId } from "@acme/contracts";
17+import { isElectron } from "../env";
1718 import { buildBootstrapInput } from "../historyBootstrap";
1819 import {
1920 DEFAULT_MODEL,
@@ -563,7 +564,7 @@ export default function ChatView() { · 543 unmodified lines
563564 if (!activeThread) {
564565 return (
565566 <div className="flex flex-1 flex-col bg-background text-muted-foreground/40">
566− <div className="drag-region h-[52px] shrink-0" />
567+ {isElectron && <div className="drag-region h-[52px] shrink-0" />}
567568 <div className="flex flex-1 items-center justify-center">
568569 <div className="text-center">
569570 <p className="text-sm">Select a thread or create a new one to get started.</p>
@@ -576,7 +577,7 @@ export default function ChatView() { · 6 unmodified lines
576577 return (
577578 <div className="flex flex-1 flex-col bg-background">
578579 {/* Top bar */}
579− <header className="drag-region flex items-center justify-between border-b border-border px-5 pt-[28px] pb-3">
580+ <header className={`flex items-center justify-between border-b border-border px-5 pb-3 ${isElectron ? "drag-region pt-[28px]" : "pt-3"}`}>
580581 <div className="flex items-center gap-3">
581582 <h2 className="text-sm font-medium text-foreground">
582583 {activeThread.title}
apps/renderer/src/components/DiffPanel.tsx+2−1
@@ -1,3 +1,4 @@
1+import { isElectron } from "../env";
12 import { useStore } from "../store";
23
34 export default function DiffPanel() {
@@ -6,7 +7,7 @@ export default function DiffPanel() { · 2 unmodified lines
67 return (
78 <aside className="flex h-full w-[400px] shrink-0 flex-col border-l border-border bg-card">
89 {/* Header */}
9− <div className="drag-region flex items-center justify-between border-b border-border px-4 pt-[28px] pb-3">
10+ <div className={`flex items-center justify-between border-b border-border px-4 pb-3 ${isElectron ? "drag-region pt-[28px]" : "pt-3"}`}>
1011 <h3 className="text-xs font-medium text-foreground">Uncommitted changes</h3>
1112 <button
1213 type="button"
apps/renderer/src/components/Sidebar.tsx+112−43
@@ -1,5 +1,6 @@
11 import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
22 import { useCallback, useEffect, useMemo, useState } from "react";
3+import { isElectron } from "../env";
34 import { useTheme } from "../hooks/useTheme";
45 import { DEFAULT_MODEL } from "../model-logic";
56 import { readNativeApi } from "../session-logic";
@@ -17,6 +18,13 @@ function formatRelativeTime(iso: string): string { · 11 unmodified lines
1718 return `${Math.floor(hours / 24)}d ago`;
1819 }
1920
21+function inferProjectName(cwd: string): string {
22+ const normalized = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
23+ const parts = normalized.split("/");
24+ const last = parts[parts.length - 1];
25+ return last && last.length > 0 ? last : "project";
26+}
27+
2028 function threadStatusLabel(
2129 status: "connecting" | "ready" | "running" | "error" | "closed" | undefined,
2230 ): "Working" | "Connecting" | null {
@@ -32,22 +40,7 @@ export default function Sidebar() { · 9 unmodified lines
3240 const [addingProject, setAddingProject] = useState(false);
3341 const [newCwd, setNewCwd] = useState("");
3442 const [isPickingFolder, setIsPickingFolder] = useState(false);
35−
36− const handleAddProject = () => {
37− const cwd = newCwd.trim();
38− if (!cwd) return;
39− const name = cwd.split("/").filter(Boolean).pop() ?? "project";
40− const project: Project = {
41− id: crypto.randomUUID(),
42− name,
43− cwd,
44− model: DEFAULT_MODEL,
45− expanded: true,
46− };
47− dispatch({ type: "ADD_PROJECT", project });
48− setNewCwd("");
49− setAddingProject(false);
50− };
43+ const [isAddingProject, setIsAddingProject] = useState(false);
5144
5245 const handleNewThread = useCallback(
5346 (projectId: string) => {
@@ -58,9 +51,7 @@ export default function Sidebar() { · 4 unmodified lines
5851 codexThreadId: null,
5952 projectId,
6053 title: "New thread",
61− model:
62− state.projects.find((p) => p.id === projectId)?.model ??
63− DEFAULT_MODEL,
54+ model: state.projects.find((p) => p.id === projectId)?.model ?? DEFAULT_MODEL,
6455 session: null,
6556 messages: [],
6657 events: [],
@@ -72,13 +63,98 @@ export default function Sidebar() { · 5 unmodified lines
7263 [dispatch, state.projects],
7364 );
7465
66+ const focusMostRecentThreadForProject = useCallback(
67+ (projectId: string) => {
68+ const latestThread = state.threads
69+ .filter((thread) => thread.projectId === projectId)
70+ .toSorted((a, b) => {
71+ const byDate = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
72+ if (byDate !== 0) return byDate;
73+ return b.id.localeCompare(a.id);
74+ })[0];
75+ if (!latestThread) return;
76+
77+ dispatch({
78+ type: "SET_ACTIVE_THREAD",
79+ threadId: latestThread.id,
80+ });
81+ },
82+ [dispatch, state.threads],
83+ );
84+
85+ const addProjectFromPath = useCallback(
86+ async (rawCwd: string) => {
87+ const cwd = rawCwd.trim();
88+ if (!cwd || isAddingProject) return;
89+
90+ setIsAddingProject(true);
91+ try {
92+ if (isElectron && api) {
93+ const result = await api.projects.add({ cwd });
94+ const project: Project = {
95+ id: result.project.id,
96+ name: result.project.name,
97+ cwd: result.project.cwd,
98+ model: DEFAULT_MODEL,
99+ expanded: true,
100+ };
101+ const existingById = state.projects.find((p) => p.id === project.id);
102+ const existingByCwd = state.projects.find((p) => p.cwd === project.cwd);
103+ if (!existingById && !existingByCwd) {
104+ dispatch({ type: "ADD_PROJECT", project });
105+ }
106+ const resolvedProjectId = existingByCwd?.id ?? project.id;
107+
108+ if (result.created) {
109+ handleNewThread(resolvedProjectId);
110+ } else {
111+ focusMostRecentThreadForProject(resolvedProjectId);
112+ }
113+ } else {
114+ const existing = state.projects.find((project) => project.cwd === cwd);
115+ if (existing) {
116+ focusMostRecentThreadForProject(existing.id);
117+ return;
118+ }
119+
120+ const name = inferProjectName(cwd);
121+ const project: Project = {
122+ id: crypto.randomUUID(),
123+ name,
124+ cwd,
125+ model: DEFAULT_MODEL,
126+ expanded: true,
127+ };
128+ dispatch({ type: "ADD_PROJECT", project });
129+ handleNewThread(project.id);
130+ }
131+ } finally {
132+ setIsAddingProject(false);
133+ setNewCwd("");
134+ setAddingProject(false);
135+ }
136+ },
137+ [
138+ api,
139+ dispatch,
140+ focusMostRecentThreadForProject,
141+ handleNewThread,
142+ isAddingProject,
143+ state.projects,
144+ ],
145+ );
146+
147+ const handleAddProject = () => {
148+ void addProjectFromPath(newCwd);
149+ };
150+
75151 const handlePickFolder = async () => {
76152 if (!api || isPickingFolder) return;
77153 setIsPickingFolder(true);
78154 try {
79155 const pickedPath = await api.dialogs.pickFolder();
80156 if (!pickedPath) return;
81− setNewCwd(pickedPath);
157+ await addProjectFromPath(pickedPath);
82158 } finally {
83159 setIsPickingFolder(false);
84160 }
@@ -107,9 +183,7 @@ export default function Sidebar() { · 22 unmodified lines
107183 }
108184 }
109185
110− const activeThread = state.threads.find(
111− (t) => t.id === state.activeThreadId,
112− );
186+ const activeThread = state.threads.find((t) => t.id === state.activeThreadId);
113187 const projectId = activeThread?.projectId ?? state.projects[0]?.id;
114188 if (!projectId) return;
115189
@@ -125,10 +199,10 @@ export default function Sidebar() { · 9 unmodified lines
125199
126200 return (
127201 <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-border bg-card">
128− {/* Drag region / traffic light space */}
129− <div className="drag-region h-[52px] shrink-0" />
202+ {/* Drag region / traffic light space (Electron only) */}
203+ {isElectron && <div className="drag-region h-[52px] shrink-0" />}
130204 {/* Branding */}
131− <div className="flex items-center gap-2 px-4 pb-4">
205+ <div className={`flex items-center gap-2 px-4 pb-4 ${isElectron ? "" : "pt-4"}`}>
132206 <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-xs font-bold text-primary-foreground">
133207 CT
134208 </div>
@@ -176,10 +250,8 @@ export default function Sidebar() { · 41 unmodified lines
176250 {state.projects.map((project) => {
177251 const threads = state.threads
178252 .filter((t) => t.projectId === project.id)
179− .sort((a, b) => {
180− const byDate =
181− new Date(b.createdAt).getTime() -
182− new Date(a.createdAt).getTime();
253+ .toSorted((a, b) => {
254+ const byDate = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
183255 if (byDate !== 0) return byDate;
184256 return b.id.localeCompare(a.id);
185257 });
@@ -205,15 +277,15 @@ export default function Sidebar() { · 19 unmodified lines
205277 <div className="ml-2 border-l border-border/80 pl-2">
206278 {threads.map((thread) => {
207279 const isActive = state.activeThreadId === thread.id;
208− const threadStatus = threadStatusLabel(
209− thread.session?.status,
210− );
280+ const threadStatus = threadStatusLabel(thread.session?.status);
211281 return (
212282 <button
213283 key={thread.id}
214284 type="button"
215285 className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 ${
216− isActive ? "bg-accent text-foreground" : "text-muted-foreground hover:bg-secondary"
286+ isActive
287+ ? "bg-accent text-foreground"
288+ : "text-muted-foreground hover:bg-secondary"
217289 }`}
218290 onClick={() =>
219291 dispatch({
@@ -226,14 +298,10 @@ export default function Sidebar() { · 6 unmodified lines
226298 {threadStatus && (
227299 <span className="inline-flex items-center gap-1 text-[10px] text-sky-600 dark:text-sky-300/80">
228300 <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-500 dark:bg-sky-300/80" />
229− <span className="hidden xl:inline">
230− {threadStatus}
231− </span>
301+ <span className="hidden xl:inline">{threadStatus}</span>
232302 </span>
233303 )}
234− <span className="min-w-0 flex-1 truncate text-xs">
235− {thread.title}
236− </span>
304+ <span className="min-w-0 flex-1 truncate text-xs">{thread.title}</span>
237305 </div>
238306 <span className="shrink-0 text-[10px] text-muted-foreground/40">
239307 {formatRelativeTime(thread.createdAt)}
@@ -281,12 +349,12 @@ export default function Sidebar() { · 41 unmodified lines
281349 if (e.key === "Escape") setAddingProject(false);
282350 }}
283351 />
284− {api && (
352+ {isElectron && api && (
285353 <button
286354 type="button"
287355 className="mb-2 flex w-full items-center justify-center rounded-md border border-border px-2 py-1.5 text-xs text-muted-foreground transition-colors duration-150 hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60"
288356 onClick={() => void handlePickFolder()}
289− disabled={isPickingFolder}
357+ disabled={isPickingFolder || isAddingProject}
290358 >
291359 {isPickingFolder ? "Picking folder..." : "Browse for folder"}
292360 </button>
@@ -296,8 +364,9 @@ export default function Sidebar() { · 3 unmodified lines
296364 type="button"
297365 className="flex-1 rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors duration-150 hover:bg-primary/90"
298366 onClick={handleAddProject}
367+ disabled={isAddingProject}
299368 >
300− Add
369+ {isAddingProject ? "Adding..." : "Add"}
301370 </button>
302371 <button
303372 type="button"
apps/renderer/src/env.ts+8−0
@@ -0,0 +1,8 @@
1+/**
2+ * True when running inside the Electron preload bridge, false in a regular browser.
3+ * The preload script sets window.nativeApi via contextBridge before any renderer
4+ * code executes, so this is reliable at module load time.
5+ */
6+export const isElectron =
7+ typeof window !== "undefined" &&
8+ (window.desktopBridge !== undefined || window.nativeApi !== undefined);
apps/renderer/src/session-logic.ts+16−3
@@ -1,5 +1,6 @@
1−import type { ProviderEvent, ProviderKind, ProviderSession } from "@acme/contracts";
1+import type { NativeApi, ProviderEvent, ProviderKind, ProviderSession } from "@acme/contracts";
22 import type { ChatMessage, SessionPhase } from "./types";
3+import { createWsNativeApi } from "./wsNativeApi";
34
45 export const PROVIDER_OPTIONS: Array<{
56 value: ProviderKind;
@@ -10,9 +11,21 @@ export const PROVIDER_OPTIONS: Array<{ · 4 unmodified lines
1011 { value: "claudeCode", label: "Claude Code (soon)", available: false },
1112 ];
1213
13−export function readNativeApi() {
14+let cachedApi: NativeApi | undefined;
15+
16+export function readNativeApi(): NativeApi | undefined {
1417 if (typeof window === "undefined") return undefined;
15− return window.nativeApi;
18+ if (cachedApi) return cachedApi;
19+
20+ // Prefer Electron preload bridge if available
21+ if (window.nativeApi) {
22+ cachedApi = window.nativeApi;
23+ return cachedApi;
24+ }
25+
26+ // Fall back to WebSocket transport
27+ cachedApi = createWsNativeApi();
28+ return cachedApi;
1629 }
1730
1831 export function asObject(value: unknown): Record<string, unknown> | undefined {
apps/renderer/src/store.test.ts+57−3
@@ -4,9 +4,7 @@ import { describe, expect, it } from "vitest"; · 3 unmodified lines
44 import { type AppState, reducer } from "./store";
55 import type { Thread } from "./types";
66
7−function makeSession(
8− overrides: Partial<ProviderSession> = {},
9−): ProviderSession {
7+function makeSession(overrides: Partial<ProviderSession> = {}): ProviderSession {
108 return {
119 sessionId: "sess-1",
1210 provider: "codex",
@@ -131,4 +129,60 @@ describe("store reducer thread continuity", () => { · 118 unmodified lines
131129 expect(next.threads[0]?.codexThreadId).toBe("thr_new");
132130 expect(next.threads[0]?.session?.threadId).toBe("thr_new");
133131 });
132+
133+ it("reconciles project ids by cwd when syncing backend projects", () => {
134+ const state: AppState = {
135+ projects: [
136+ {
137+ id: "project-old-a",
138+ name: "A",
139+ cwd: "/tmp/a",
140+ model: "gpt-5.3-codex",
141+ expanded: false,
142+ },
143+ {
144+ id: "project-old-b",
145+ name: "B",
146+ cwd: "/tmp/b",
147+ model: "gpt-5.3-codex",
148+ expanded: true,
149+ },
150+ ],
151+ threads: [
152+ makeThread({
153+ id: "thread-a",
154+ projectId: "project-old-a",
155+ }),
156+ makeThread({
157+ id: "thread-b",
158+ projectId: "project-old-b",
159+ }),
160+ ],
161+ activeThreadId: "thread-b",
162+ runtimeMode: "full-access",
163+ diffOpen: false,
164+ };
165+
166+ const next = reducer(state, {
167+ type: "SYNC_PROJECTS",
168+ projects: [
169+ {
170+ id: "project-new-a",
171+ name: "A",
172+ cwd: "/tmp/a",
173+ model: "gpt-5.3-codex",
174+ expanded: true,
175+ },
176+ ],
177+ });
178+
179+ expect(next.projects).toHaveLength(1);
180+ expect(next.projects[0]?.id).toBe("project-new-a");
181+ // Preserve existing project UI preferences by cwd
182+ expect(next.projects[0]?.expanded).toBe(false);
183+ expect(next.threads).toHaveLength(1);
184+ expect(next.threads[0]?.id).toBe("thread-a");
185+ expect(next.threads[0]?.projectId).toBe("project-new-a");
186+ expect(next.activeThreadId).toBe("thread-a");
187+ });
134188 });
apps/renderer/src/store.ts+57−40
@@ -11,23 +11,14 @@ import { · 10 unmodified lines
1111 import type { ProviderEvent, ProviderSession } from "@acme/contracts";
1212 import { resolveModelSlug } from "./model-logic";
1313 import { hydratePersistedState, toPersistedState } from "./persistenceSchema";
14−import {
15− applyEventToMessages,
16− asObject,
17− asString,
18− evolveSession,
19−} from "./session-logic";
20−import {
21− DEFAULT_RUNTIME_MODE,
22− type Project,
23− type RuntimeMode,
24− type Thread,
25−} from "./types";
14+import { applyEventToMessages, asObject, asString, evolveSession } from "./session-logic";
15+import { DEFAULT_RUNTIME_MODE, type Project, type RuntimeMode, type Thread } from "./types";
2616
2717 // ── Actions ──────────────────────────────────────────────────────────
2818
2919 type Action =
3020 | { type: "ADD_PROJECT"; project: Project }
21+ | { type: "SYNC_PROJECTS"; projects: Project[] }
3122 | { type: "TOGGLE_PROJECT"; projectId: string }
3223 | { type: "ADD_THREAD"; thread: Thread }
3324 | { type: "SET_ACTIVE_THREAD"; threadId: string }
@@ -98,10 +89,7 @@ function persistState(state: AppState): void { · 64 unmodified lines
9889 if (typeof window === "undefined") return;
9990
10091 try {
101− window.localStorage.setItem(
102− PERSISTED_STATE_KEY,
103− JSON.stringify(toPersistedState(state)),
104− );
92+ window.localStorage.setItem(PERSISTED_STATE_KEY, JSON.stringify(toPersistedState(state)));
10593 for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) {
10694 window.localStorage.removeItem(legacyKey);
10795 }
@@ -143,10 +131,7 @@ function getEventThreadId(event: ProviderEvent): string | undefined { · 35 unmodified lines
143131 );
144132 }
145133
146−function shouldIgnoreForeignThreadEvent(
147− thread: Thread,
148− event: ProviderEvent,
149−): boolean {
134+function shouldIgnoreForeignThreadEvent(thread: Thread, event: ProviderEvent): boolean {
150135 const eventThreadId = getEventThreadId(event);
151136 if (!eventThreadId) {
152137 return false;
@@ -175,10 +160,7 @@ function durationMs(startIso: string, endIso: string): number | undefined { · 22 unmodified lines
175160 return end - start;
176161 }
177162
178−function updateTurnFields(
179− thread: Thread,
180− event: ProviderEvent,
181−): Partial<Thread> {
163+function updateTurnFields(thread: Thread, event: ProviderEvent): Partial<Thread> {
182164 if (event.method === "turn/started") {
183165 return {
184166 latestTurnId: getEventTurnId(event) ?? thread.latestTurnId,
@@ -195,9 +177,7 @@ function updateTurnFields( · 10 unmodified lines
195177 ? thread.latestTurnStartedAt
196178 : undefined;
197179 const elapsed =
198− startedAt && startedAt.length > 0
199− ? durationMs(startedAt, event.createdAt)
200− : undefined;
180+ startedAt && startedAt.length > 0 ? durationMs(startedAt, event.createdAt) : undefined;
201181
202182 return {
203183 latestTurnId: completedTurnId ?? thread.latestTurnId,
@@ -214,6 +194,9 @@ function updateTurnFields( · 10 unmodified lines
214194 export function reducer(state: AppState, action: Action): AppState {
215195 switch (action.type) {
216196 case "ADD_PROJECT":
197+ if (state.projects.some((project) => project.cwd === action.project.cwd)) {
198+ return state;
199+ }
217200 return {
218201 ...state,
219202 projects: [
@@ -225,6 +208,48 @@ export function reducer(state: AppState, action: Action): AppState { · 5 unmodified lines
225208 ],
226209 };
227210
211+ case "SYNC_PROJECTS": {
212+ const previousByCwd = new Map(
213+ state.projects.map((project) => [project.cwd, project] as const),
214+ );
215+ const nextProjects = action.projects.map((project) => {
216+ const previous = previousByCwd.get(project.cwd);
217+ return {
218+ ...project,
219+ model: resolveModelSlug(previous?.model ?? project.model),
220+ expanded: previous?.expanded ?? project.expanded,
221+ };
222+ });
223+ const previousProjectById = new Map(
224+ state.projects.map((project) => [project.id, project] as const),
225+ );
226+ const nextProjectIdByCwd = new Map(
227+ nextProjects.map((project) => [project.cwd, project.id] as const),
228+ );
229+ const nextThreads = state.threads
230+ .map((thread) => {
231+ const previousProject = previousProjectById.get(thread.projectId);
232+ if (!previousProject) return null;
233+ const mappedProjectId = nextProjectIdByCwd.get(previousProject.cwd);
234+ if (!mappedProjectId) return null;
235+ return {
236+ ...thread,
237+ projectId: mappedProjectId,
238+ };
239+ })
240+ .filter((thread): thread is Thread => thread !== null);
241+ const activeThreadId = nextThreads.some((thread) => thread.id === state.activeThreadId)
242+ ? state.activeThreadId
243+ : (nextThreads[0]?.id ?? null);
244+
245+ return {
246+ ...state,
247+ projects: nextProjects,
248+ threads: nextThreads,
249+ activeThreadId,
250+ };
251+ }
252+
228253 case "TOGGLE_PROJECT":
229254 return {
230255 ...state,
@@ -265,24 +290,16 @@ export function reducer(state: AppState, action: Action): AppState { · 34 unmodified lines
265290 ...(() => {
266291 const eventThreadId = getEventThreadId(event);
267292 const shouldRebindIdentity =
268− event.method === "thread/started" &&
269− t.session?.status === "connecting";
293+ event.method === "thread/started" && t.session?.status === "connecting";
270294 return {
271295 codexThreadId: shouldRebindIdentity
272− ? eventThreadId ?? t.codexThreadId
273− : t.codexThreadId ?? eventThreadId ?? null,
274− error:
275− (event.kind === "error" && event.message
276− ? event.message
277− : t.error),
296+ ? (eventThreadId ?? t.codexThreadId)
297+ : (t.codexThreadId ?? eventThreadId ?? null),
298+ error: event.kind === "error" && event.message ? event.message : t.error,
278299 };
279300 })(),
280301 session: t.session ? evolveSession(t.session, event) : t.session,
281− messages: applyEventToMessages(
282− t.messages,
283− event,
284− activeAssistantItemRef,
285− ),
302+ messages: applyEventToMessages(t.messages, event, activeAssistantItemRef),
286303 events: [event, ...t.events],
287304 ...updateTurnFields(t, event),
288305 })),
apps/renderer/src/vite-env.d.ts+6−0
@@ -2,8 +2,14 @@ · 1 unmodified line
22
33 import type { NativeApi } from "@acme/contracts";
44
5+interface DesktopBridge {
6+ getWsUrl: () => string | null;
7+ pickFolder: () => Promise<string | null>;
8+}
9+
510 declare global {
611 interface Window {
712 nativeApi?: NativeApi;
13+ desktopBridge?: DesktopBridge;
814 }
915 }
apps/renderer/src/wsNativeApi.ts+102−0
@@ -0,0 +1,102 @@
1+import { type NativeApi, WS_CHANNELS, WS_METHODS, type WsWelcomePayload } from "@acme/contracts";
2+
3+import { WsTransport } from "./wsTransport";
4+
5+let instance: { api: NativeApi; transport: WsTransport } | null = null;
6+const welcomeListeners = new Set<(payload: WsWelcomePayload) => void>();
7+let lastWelcome: WsWelcomePayload | null = null;
8+
9+/**
10+ * Subscribe to the server welcome message. If a welcome was already received
11+ * before this call, the listener fires synchronously with the cached payload.
12+ * This avoids the race between WebSocket connect and React effect registration.
13+ */
14+export function onServerWelcome(listener: (payload: WsWelcomePayload) => void): () => void {
15+ welcomeListeners.add(listener);
16+
17+ // Replay cached welcome for late subscribers
18+ if (lastWelcome) {
19+ try {
20+ listener(lastWelcome);
21+ } catch {
22+ // Swallow listener errors
23+ }
24+ }
25+
26+ return () => {
27+ welcomeListeners.delete(listener);
28+ };
29+}
30+
31+export function createWsNativeApi(): NativeApi {
32+ if (instance) return instance.api;
33+
34+ const transport = new WsTransport();
35+
36+ // Listen for server welcome and forward to registered listeners.
37+ // Also cache it so late subscribers (React effects) get it immediately.
38+ transport.subscribe(WS_CHANNELS.serverWelcome, (data) => {
39+ const payload = data as WsWelcomePayload;
40+ lastWelcome = payload;
41+ for (const listener of welcomeListeners) {
42+ try {
43+ listener(payload);
44+ } catch {
45+ // Swallow listener errors
46+ }
47+ }
48+ });
49+
50+ const api: NativeApi = {
51+ todos: {
52+ list: async () => [],
53+ add: async () => [],
54+ toggle: async () => [],
55+ remove: async () => [],
56+ },
57+ dialogs: {
58+ pickFolder: async () => {
59+ if (!window.desktopBridge) return null;
60+ return window.desktopBridge.pickFolder();
61+ },
62+ },
63+ terminal: {
64+ run: async () => ({
65+ stdout: "",
66+ stderr: "Terminal not available in web mode",
67+ code: 1,
68+ signal: null,
69+ timedOut: false,
70+ }),
71+ },
72+ agent: {
73+ spawn: async () => "",
74+ kill: async () => {},
75+ write: async () => {},
76+ onOutput: () => () => {},
77+ onExit: () => () => {},
78+ },
79+ providers: {
80+ startSession: (input) => transport.request(WS_METHODS.providersStartSession, input),
81+ sendTurn: (input) => transport.request(WS_METHODS.providersSendTurn, input),
82+ interruptTurn: (input) => transport.request(WS_METHODS.providersInterruptTurn, input),
83+ respondToRequest: (input) => transport.request(WS_METHODS.providersRespondToRequest, input),
84+ stopSession: (input) => transport.request(WS_METHODS.providersStopSession, input),
85+ listSessions: () => transport.request(WS_METHODS.providersListSessions),
86+ onEvent: (callback) =>
87+ transport.subscribe(WS_CHANNELS.providerEvent, callback as (data: unknown) => void),
88+ },
89+ projects: {
90+ list: () => transport.request(WS_METHODS.projectsList),
91+ add: (input) => transport.request(WS_METHODS.projectsAdd, input),
92+ remove: (input) => transport.request(WS_METHODS.projectsRemove, input),
93+ },
94+ shell: {
95+ openInEditor: (cwd, editor) =>
96+ transport.request(WS_METHODS.shellOpenInEditor, { cwd, editor }),
97+ },
98+ };
99+
100+ instance = { api, transport };
101+ return api;
102+}
apps/renderer/src/wsTransport.ts+197−0
@@ -0,0 +1,197 @@
1+import type { WsPush, WsRequest, WsResponse } from "@acme/contracts";
2+
3+type PushListener = (data: unknown) => void;
4+
5+interface PendingRequest {
6+ resolve: (result: unknown) => void;
7+ reject: (error: Error) => void;
8+ timeout: ReturnType<typeof setTimeout>;
9+}
10+
11+const REQUEST_TIMEOUT_MS = 60_000;
12+const RECONNECT_DELAYS_MS = [500, 1_000, 2_000, 4_000, 8_000];
13+
14+export class WsTransport {
15+ private ws: WebSocket | null = null;
16+ private nextId = 1;
17+ private readonly pending = new Map<string, PendingRequest>();
18+ private readonly listeners = new Map<string, Set<PushListener>>();
19+ private reconnectAttempt = 0;
20+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
21+ private disposed = false;
22+ private readonly url: string;
23+
24+ constructor(url?: string) {
25+ const bridgeUrl = window.desktopBridge?.getWsUrl();
26+ // In dev mode, VITE_WS_URL points to the server's WebSocket endpoint.
27+ // In production, the page is served by the WS server on the same host:port.
28+ const envUrl = import.meta.env.VITE_WS_URL as string | undefined;
29+ this.url =
30+ url ??
31+ (bridgeUrl && bridgeUrl.length > 0
32+ ? bridgeUrl
33+ : envUrl && envUrl.length > 0
34+ ? envUrl
35+ : `ws://${window.location.hostname}:${window.location.port}`);
36+ this.connect();
37+ }
38+
39+ async request<T = unknown>(method: string, params?: unknown): Promise<T> {
40+ const id = String(this.nextId++);
41+ const message: WsRequest = { id, method, ...(params !== undefined ? { params } : {}) };
42+
43+ return new Promise<T>((resolve, reject) => {
44+ const timeout = setTimeout(() => {
45+ this.pending.delete(id);
46+ reject(new Error(`Request timed out: ${method}`));
47+ }, REQUEST_TIMEOUT_MS);
48+
49+ this.pending.set(id, {
50+ resolve: resolve as (result: unknown) => void,
51+ reject,
52+ timeout,
53+ });
54+
55+ this.send(message);
56+ });
57+ }
58+
59+ subscribe(channel: string, listener: PushListener): () => void {
60+ let channelListeners = this.listeners.get(channel);
61+ if (!channelListeners) {
62+ channelListeners = new Set();
63+ this.listeners.set(channel, channelListeners);
64+ }
65+ channelListeners.add(listener);
66+
67+ return () => {
68+ channelListeners!.delete(listener);
69+ if (channelListeners!.size === 0) {
70+ this.listeners.delete(channel);
71+ }
72+ };
73+ }
74+
75+ dispose() {
76+ this.disposed = true;
77+ if (this.reconnectTimer !== null) {
78+ clearTimeout(this.reconnectTimer);
79+ this.reconnectTimer = null;
80+ }
81+ for (const pending of this.pending.values()) {
82+ clearTimeout(pending.timeout);
83+ pending.reject(new Error("Transport disposed"));
84+ }
85+ this.pending.clear();
86+ this.ws?.close();
87+ this.ws = null;
88+ }
89+
90+ private connect() {
91+ if (this.disposed) return;
92+
93+ const ws = new WebSocket(this.url);
94+
95+ ws.addEventListener("open", () => {
96+ this.ws = ws;
97+ this.reconnectAttempt = 0;
98+ });
99+
100+ ws.addEventListener("message", (event) => {
101+ this.handleMessage(event.data);
102+ });
103+
104+ ws.addEventListener("close", () => {
105+ this.ws = null;
106+ this.scheduleReconnect();
107+ });
108+
109+ ws.addEventListener("error", () => {
110+ // close event will fire after error
111+ });
112+ }
113+
114+ private handleMessage(raw: unknown) {
115+ let parsed: unknown;
116+ try {
117+ parsed = JSON.parse(String(raw));
118+ } catch {
119+ return;
120+ }
121+
122+ if (!parsed || typeof parsed !== "object") return;
123+
124+ const message = parsed as Record<string, unknown>;
125+
126+ // Push event
127+ if (message.type === "push") {
128+ const push = message as unknown as WsPush;
129+ const channelListeners = this.listeners.get(push.channel);
130+ if (channelListeners) {
131+ for (const listener of channelListeners) {
132+ try {
133+ listener(push.data);
134+ } catch {
135+ // Swallow listener errors
136+ }
137+ }
138+ }
139+ return;
140+ }
141+
142+ // Response to a request
143+ if (typeof message.id === "string") {
144+ const response = message as unknown as WsResponse;
145+ const pending = this.pending.get(response.id);
146+ if (!pending) return;
147+
148+ clearTimeout(pending.timeout);
149+ this.pending.delete(response.id);
150+
151+ if (response.error) {
152+ pending.reject(new Error(response.error.message));
153+ } else {
154+ pending.resolve(response.result);
155+ }
156+ }
157+ }
158+
159+ private send(message: WsRequest) {
160+ if (this.ws?.readyState === WebSocket.OPEN) {
161+ this.ws.send(JSON.stringify(message));
162+ return;
163+ }
164+
165+ // If not connected, wait for connection
166+ const waitForOpen = () => {
167+ const check = setInterval(() => {
168+ if (this.disposed) {
169+ clearInterval(check);
170+ return;
171+ }
172+ if (this.ws?.readyState === WebSocket.OPEN) {
173+ clearInterval(check);
174+ this.ws.send(JSON.stringify(message));
175+ }
176+ }, 50);
177+
178+ // Give up after timeout (the pending request will time out on its own)
179+ setTimeout(() => clearInterval(check), REQUEST_TIMEOUT_MS);
180+ };
181+ waitForOpen();
182+ }
183+
184+ private scheduleReconnect() {
185+ if (this.disposed) return;
186+
187+ const delay =
188+ RECONNECT_DELAYS_MS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)] ??
189+ RECONNECT_DELAYS_MS[0]!;
190+
191+ this.reconnectAttempt++;
192+ this.reconnectTimer = setTimeout(() => {
193+ this.reconnectTimer = null;
194+ this.connect();
195+ }, delay);
196+ }
197+}
apps/renderer/vite.config.ts+7−1
@@ -11,8 +11,14 @@ export default defineConfig({ · 10 unmodified lines
1111 }),
1212 tailwindcss(),
1313 ],
14+ define: {
15+ // In dev mode, tell the renderer where the WebSocket server lives
16+ "import.meta.env.VITE_WS_URL": JSON.stringify(
17+ process.env.VITE_WS_URL ?? "",
18+ ),
19+ },
1420 server: {
15− port: Number(process.env.ELECTRON_RENDERER_PORT ?? 5173),
21+ port: Number(process.env.PORT ?? 5173),
1622 strictPort: true,
1723 },
1824 build: {
apps/server/package.json+34−0
@@ -0,0 +1,34 @@
1+{
2+ "name": "t3",
3+ "version": "0.1.0",
4+ "bin": {
5+ "t3": "./dist/index.js"
6+ },
7+ "files": [
8+ "dist"
9+ ],
10+ "type": "module",
11+ "main": "./dist/index.js",
12+ "scripts": {
13+ "dev": "VITE_DEV_SERVER_URL=http://localhost:5173 tsx src/index.ts",
14+ "dev:desktop": "CODETHING_MODE=desktop CODETHING_NO_BROWSER=1 tsx src/index.ts",
15+ "build": "tsup && node scripts/bundle-client.mjs",
16+ "start": "node dist/index.js",
17+ "start:desktop": "CODETHING_MODE=desktop CODETHING_NO_BROWSER=1 node dist/index.js",
18+ "typecheck": "tsc --noEmit",
19+ "test": "vitest run"
20+ },
21+ "dependencies": {
22+ "open": "^10.1.0",
23+ "ws": "^8.18.0"
24+ },
25+ "devDependencies": {
26+ "@acme/contracts": "workspace:*",
27+ "@acme/renderer": "workspace:*",
28+ "@types/node": "^22.10.2",
29+ "@types/ws": "^8.5.13",
30+ "tsup": "^8.3.5",
31+ "tsx": "^4.19.0",
32+ "typescript": "^5.7.3"
33+ }
34+}
apps/server/scripts/bundle-client.mjs+21−0
@@ -0,0 +1,21 @@
1+/**
2+ * Copies the built renderer into dist/client/ so the published npm package
3+ * includes the web UI. This runs as a post-build step.
4+ */
5+import fs from "node:fs";
6+import path from "node:path";
7+import { fileURLToPath } from "node:url";
8+
9+const __dirname = path.dirname(fileURLToPath(import.meta.url));
10+const rendererDist = path.resolve(__dirname, "../../renderer/dist");
11+const target = path.resolve(__dirname, "../dist/client");
12+
13+if (!fs.existsSync(rendererDist)) {
14+ console.log(
15+ "⚠ Renderer dist not found — skipping client bundle. Run `bun run --cwd apps/renderer build` first.",
16+ );
17+ process.exit(0);
18+}
19+
20+fs.cpSync(rendererDist, target, { recursive: true });
21+console.log("✓ Bundled renderer into dist/client");
apps/server/src/codexAppServerManager.test.tsrenamed from apps/desktop/src/codexAppServerManager.test.ts+0−0

File renamed without changes.

apps/server/src/codexAppServerManager.tsrenamed from apps/desktop/src/codexAppServerManager.ts+0−0

File renamed without changes.

apps/server/src/fixPath.ts+18−0
@@ -0,0 +1,18 @@
1+import { execFileSync } from "node:child_process";
2+
3+export function fixPath(): void {
4+ if (process.platform !== "darwin") return;
5+
6+ try {
7+ const shell = process.env.SHELL ?? "/bin/zsh";
8+ const result = execFileSync(shell, ["-ilc", "echo -n $PATH"], {
9+ encoding: "utf8",
10+ timeout: 5000,
11+ });
12+ if (result) {
13+ process.env.PATH = result;
14+ }
15+ } catch {
16+ // Silently ignore — keep default PATH
17+ }
18+}
apps/server/src/index.ts+176−0
@@ -0,0 +1,176 @@
1+import fs from "node:fs";
2+import net from "node:net";
3+import os from "node:os";
4+import path from "node:path";
5+import { fileURLToPath } from "node:url";
6+
7+import { fixPath } from "./fixPath";
8+import { createLogger } from "./logger";
9+import { ProjectRegistry } from "./projectRegistry";
10+import { createServer } from "./wsServer";
11+
12+fixPath();
13+
14+const DEFAULT_PORT = 3773;
15+const cwd = process.cwd();
16+const logger = createLogger("server");
17+
18+type RuntimeMode = "web" | "desktop";
19+
20+function parseBooleanEnv(value: string | undefined): boolean | undefined {
21+ if (value === undefined) return undefined;
22+ const normalized = value.trim().toLowerCase();
23+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
24+ if (["0", "false", "no", "off"].includes(normalized)) return false;
25+ return undefined;
26+}
27+
28+function parsePort(value: string | undefined): number | undefined {
29+ if (!value) return undefined;
30+ const parsed = Number(value);
31+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
32+ throw new Error(`Invalid CODETHING_PORT: ${value}`);
33+ }
34+ return parsed;
35+}
36+
37+function expandHomePath(input: string): string {
38+ if (input === "~") return os.homedir();
39+ if (input.startsWith("~/")) {
40+ return path.join(os.homedir(), input.slice(2));
41+ }
42+ return input;
43+}
44+
45+function resolveStateDir(raw: string | undefined): string {
46+ if (!raw || raw.trim().length === 0) {
47+ return path.join(os.homedir(), ".t3", "userdata");
48+ }
49+ return path.resolve(expandHomePath(raw.trim()));
50+}
51+
52+async function findAvailablePort(preferred: number): Promise<number> {
53+ return new Promise((resolve, reject) => {
54+ const server = net.createServer();
55+ server.listen(preferred, () => {
56+ server.close(() => resolve(preferred));
57+ });
58+ server.on("error", () => {
59+ // Preferred port busy — let the OS pick one
60+ const fallback = net.createServer();
61+ fallback.listen(0, () => {
62+ const addr = fallback.address();
63+ const port = typeof addr === "object" && addr !== null ? addr.port : 0;
64+ fallback.close(() => {
65+ if (port > 0) resolve(port);
66+ else reject(new Error("Could not find an available port."));
67+ });
68+ });
69+ fallback.on("error", reject);
70+ });
71+ });
72+}
73+
74+function resolveStaticDir(): string | undefined {
75+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
76+
77+ // Check for bundled client (npm publish / npx scenario):
78+ // dist/client/ lives alongside dist/index.js
79+ const bundledClient = path.resolve(__dirname, "client");
80+ try {
81+ const stat = fs.statSync(path.join(bundledClient, "index.html"));
82+ if (stat.isFile()) return bundledClient;
83+ } catch {
84+ // Not bundled — check monorepo layout
85+ }
86+
87+ // Monorepo layout: apps/server/dist/index.js → apps/renderer/dist/
88+ const monorepoClient = path.resolve(__dirname, "../../renderer/dist");
89+ try {
90+ const stat = fs.statSync(path.join(monorepoClient, "index.html"));
91+ if (stat.isFile()) return monorepoClient;
92+ } catch {
93+ // Not found — probably dev mode, Vite will serve
94+ }
95+
96+ return undefined;
97+}
98+
99+async function main() {
100+ const mode: RuntimeMode = process.env.CODETHING_MODE === "desktop" ? "desktop" : "web";
101+ const requestedPort = parsePort(process.env.CODETHING_PORT);
102+ const port =
103+ requestedPort ?? (mode === "desktop" ? DEFAULT_PORT : await findAvailablePort(DEFAULT_PORT));
104+ const stateDir = resolveStateDir(process.env.CODETHING_STATE_DIR);
105+ const projectRegistry = new ProjectRegistry(stateDir);
106+ const devUrl = process.env.VITE_DEV_SERVER_URL;
107+ const noBrowser = parseBooleanEnv(process.env.CODETHING_NO_BROWSER) ?? mode === "desktop";
108+ const authToken = process.env.CODETHING_AUTH_TOKEN;
109+ const staticDir = devUrl ? undefined : resolveStaticDir();
110+
111+ if (!devUrl && !staticDir) {
112+ logger.warn("renderer bundle missing and no VITE_DEV_SERVER_URL; web UI unavailable", {
113+ hint: "Run `bun run --cwd apps/renderer build` or set VITE_DEV_SERVER_URL for dev mode.",
114+ });
115+ }
116+
117+ const server = createServer({
118+ port,
119+ host: mode === "desktop" ? "127.0.0.1" : undefined,
120+ cwd,
121+ staticDir,
122+ devUrl,
123+ projectRegistry,
124+ authToken,
125+ });
126+ await server.start();
127+
128+ const url = `http://localhost:${port}`;
129+ logger.info("CodeThing running", {
130+ url,
131+ cwd,
132+ mode,
133+ stateDir,
134+ authEnabled: Boolean(authToken),
135+ });
136+
137+ // Open browser (dynamic import because `open` is ESM-only)
138+ if (!noBrowser) {
139+ try {
140+ const open = await import("open");
141+ const target = devUrl ?? url;
142+ await open.default(target);
143+ } catch {
144+ logger.info("browser auto-open unavailable", {
145+ hint: `Open ${devUrl ?? url} in your browser.`,
146+ });
147+ }
148+ }
149+
150+ // Graceful shutdown
151+ let shuttingDown = false;
152+ async function shutdown() {
153+ if (shuttingDown) return;
154+ shuttingDown = true;
155+ logger.info("shutting down");
156+ try {
157+ await server.stop();
158+ process.exit(0);
159+ } catch (error) {
160+ logger.error("failed to stop server cleanly", { error });
161+ process.exit(1);
162+ }
163+ }
164+
165+ process.on("SIGINT", () => {
166+ void shutdown();
167+ });
168+ process.on("SIGTERM", () => {
169+ void shutdown();
170+ });
171+}
172+
173+main().catch((err) => {
174+ logger.error("failed to start CodeThing", { error: err });
175+ process.exit(1);
176+});
apps/server/src/logger.ts+103−0
@@ -0,0 +1,103 @@
1+import util from "node:util";
2+
3+type LogLevel = "info" | "warn" | "error" | "event";
4+
5+type LogContext = Record<string, unknown>;
6+
7+const ANSI = {
8+ reset: "\u001b[0m",
9+ dim: "\u001b[2m",
10+ cyan: "\u001b[36m",
11+ yellow: "\u001b[33m",
12+ red: "\u001b[31m",
13+ magenta: "\u001b[35m",
14+} as const;
15+
16+const LEVEL_LABEL: Record<LogLevel, string> = {
17+ info: "INFO",
18+ warn: "WARN",
19+ error: "ERROR",
20+ event: "EVENT",
21+};
22+
23+const LEVEL_COLOR: Record<LogLevel, string> = {
24+ info: ANSI.cyan,
25+ warn: ANSI.yellow,
26+ error: ANSI.red,
27+ event: ANSI.magenta,
28+};
29+
30+function useColors() {
31+ return Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined;
32+}
33+
34+function colorize(value: string, color: string, enabled: boolean) {
35+ return enabled ? `${color}${value}${ANSI.reset}` : value;
36+}
37+
38+function timeStamp() {
39+ return new Date().toISOString().slice(11, 23);
40+}
41+
42+function formatValue(value: unknown) {
43+ if (typeof value === "string") {
44+ return JSON.stringify(value);
45+ }
46+ if (
47+ typeof value === "number" ||
48+ typeof value === "boolean" ||
49+ value === null ||
50+ value === undefined
51+ ) {
52+ return String(value);
53+ }
54+ return util.inspect(value, {
55+ depth: 4,
56+ breakLength: Infinity,
57+ compact: true,
58+ maxArrayLength: 25,
59+ maxStringLength: 320,
60+ });
61+}
62+
63+function formatContext(context: LogContext | undefined) {
64+ if (!context) return "";
65+ const entries = Object.entries(context).filter(([, value]) => value !== undefined);
66+ if (entries.length === 0) return "";
67+ return entries.map(([key, value]) => `${key}=${formatValue(value)}`).join(" ");
68+}
69+
70+function write(level: LogLevel, scope: string, message: string, context?: LogContext) {
71+ const colorEnabled = useColors();
72+ const ts = colorize(timeStamp(), ANSI.dim, colorEnabled);
73+ const levelLabel = colorize(LEVEL_LABEL[level], LEVEL_COLOR[level], colorEnabled);
74+ const contextText = formatContext(context);
75+ const line = `${ts} ${levelLabel} [${scope}] ${message}${contextText ? ` ${contextText}` : ""}`;
76+
77+ if (level === "warn") {
78+ console.warn(line);
79+ return;
80+ }
81+ if (level === "error") {
82+ console.error(line);
83+ return;
84+ }
85+ console.log(line);
86+}
87+
88+export function createLogger(scope: string) {
89+ return {
90+ info(message: string, context?: LogContext) {
91+ write("info", scope, message, context);
92+ },
93+ warn(message: string, context?: LogContext) {
94+ write("warn", scope, message, context);
95+ },
96+ error(message: string, context?: LogContext) {
97+ write("error", scope, message, context);
98+ },
99+ event(message: string, context?: LogContext) {
100+ write("event", scope, message, context);
101+ },
102+ };
103+}
apps/server/src/projectRegistry.ts+172−0
@@ -0,0 +1,172 @@
1+import { randomUUID } from "node:crypto";
2+import fs from "node:fs";
3+import path from "node:path";
4+
5+import {
6+ type ProjectAddInput,
7+ type ProjectAddResult,
8+ type ProjectListResult,
9+ type ProjectRecord,
10+ type ProjectRemoveInput,
11+ projectAddInputSchema,
12+ projectRecordSchema,
13+ projectRemoveInputSchema,
14+} from "@acme/contracts";
15+
16+function normalizeCwd(rawCwd: string): string {
17+ const resolved = path.resolve(rawCwd.trim());
18+ const normalized = path.normalize(resolved);
19+ if (process.platform === "win32") {
20+ return normalized.toLowerCase();
21+ }
22+ return normalized;
23+}
24+
25+function isDirectory(cwd: string): boolean {
26+ try {
27+ return fs.statSync(cwd).isDirectory();
28+ } catch {
29+ return false;
30+ }
31+}
32+
33+function inferProjectName(cwd: string): string {
34+ const name = path.basename(cwd);
35+ return name.length > 0 ? name : "project";
36+}
37+
38+export class ProjectRegistry {
39+ private readonly stateDir: string;
40+ private readonly filePath: string;
41+ private projects: ProjectRecord[];
42+
43+ constructor(stateDir: string) {
44+ this.stateDir = stateDir;
45+ this.filePath = path.join(this.stateDir, "projects.json");
46+ fs.mkdirSync(this.stateDir, { recursive: true });
47+ this.projects = this.loadFromDisk();
48+ }
49+
50+ list(): ProjectListResult {
51+ return this.projects.map((project) => ({ ...project }));
52+ }
53+
54+ add(raw: ProjectAddInput): ProjectAddResult {
55+ const input = projectAddInputSchema.parse(raw);
56+ const normalizedCwd = normalizeCwd(input.cwd);
57+ if (!isDirectory(normalizedCwd)) {
58+ throw new Error(`Project path does not exist: ${normalizedCwd}`);
59+ }
60+
61+ const existing = this.projects.find((project) => normalizeCwd(project.cwd) === normalizedCwd);
62+ if (existing) {
63+ return { project: { ...existing }, created: false };
64+ }
65+
66+ const now = new Date().toISOString();
67+ const project: ProjectRecord = {
68+ id: randomUUID(),
69+ cwd: normalizedCwd,
70+ name: inferProjectName(normalizedCwd),
71+ createdAt: now,
72+ updatedAt: now,
73+ };
74+
75+ this.projects = [project, ...this.projects];
76+ this.persist();
77+ return { project: { ...project }, created: true };
78+ }
79+
80+ remove(raw: ProjectRemoveInput): void {
81+ const input = projectRemoveInputSchema.parse(raw);
82+ const next = this.projects.filter((project) => project.id !== input.id);
83+ if (next.length === this.projects.length) {
84+ return;
85+ }
86+
87+ this.projects = next;
88+ this.persist();
89+ }
90+
91+ private loadFromDisk(): ProjectRecord[] {
92+ const loaded = this.readPersistedProjects();
93+ const deduped = new Map<string, ProjectRecord>();
94+ const normalizedProjects: ProjectRecord[] = [];
95+
96+ for (const project of loaded) {
97+ const normalizedCwd = normalizeCwd(project.cwd);
98+ if (!isDirectory(normalizedCwd)) {
99+ continue;
100+ }
101+ if (deduped.has(normalizedCwd)) {
102+ continue;
103+ }
104+
105+ const normalizedProject: ProjectRecord = {
106+ ...project,
107+ cwd: normalizedCwd,
108+ name:
109+ project.name.trim().length > 0 ? project.name.trim() : inferProjectName(normalizedCwd),
110+ };
111+ deduped.set(normalizedCwd, normalizedProject);
112+ normalizedProjects.push(normalizedProject);
113+ }
114+
115+ const changed =
116+ normalizedProjects.length !== loaded.length ||
117+ normalizedProjects.some((project, index) => {
118+ const source = loaded[index];
119+ if (!source) return true;
120+ return (
121+ source.id !== project.id ||
122+ source.cwd !== project.cwd ||
123+ source.name !== project.name ||
124+ source.createdAt !== project.createdAt ||
125+ source.updatedAt !== project.updatedAt
126+ );
127+ });
128+
129+ if (changed) {
130+ this.projects = normalizedProjects;
131+ this.persist();
132+ }
133+
134+ return normalizedProjects;
135+ }
136+
137+ private readPersistedProjects(): ProjectRecord[] {
138+ try {
139+ const raw = fs.readFileSync(this.filePath, "utf8");
140+ const parsed = JSON.parse(raw) as { projects?: unknown };
141+ if (!parsed || !Array.isArray(parsed.projects)) {
142+ return [];
143+ }
144+
145+ const projects: ProjectRecord[] = [];
146+ for (const candidate of parsed.projects) {
147+ const row = projectRecordSchema.safeParse(candidate);
148+ if (!row.success) {
149+ continue;
150+ }
151+ projects.push(row.data);
152+ }
153+ return projects;
154+ } catch {
155+ return [];
156+ }
157+ }
158+
159+ private persist(): void {
160+ const payload = JSON.stringify(
161+ {
162+ version: 1,
163+ projects: this.projects,
164+ },
165+ null,
166+ 2,
167+ );
168+ const tempFile = `${this.filePath}.tmp-${process.pid}-${Date.now()}`;
169+ fs.writeFileSync(tempFile, payload);
170+ fs.renameSync(tempFile, this.filePath);
171+ }
172+}
apps/server/src/providerManager.test.tsrenamed from apps/desktop/src/providerManager.test.ts+0−0

File renamed without changes.

apps/server/src/providerManager.tsrenamed from apps/desktop/src/providerManager.ts+0−0

File renamed without changes.

apps/server/src/wsServer.test.ts+347−0
@@ -0,0 +1,347 @@
1+import fs from "node:fs";
2+import os from "node:os";
3+import path from "node:path";
4+
5+import { describe, expect, it, afterEach, vi } from "vitest";
6+import { createServer } from "./wsServer";
7+import WebSocket from "ws";
8+
9+import { WS_CHANNELS, WS_METHODS, type WsPush, type WsResponse } from "@acme/contracts";
10+import { ProjectRegistry } from "./projectRegistry";
11+
12+interface PendingMessages {
13+ queue: unknown[];
14+ waiters: Array<(message: unknown) => void>;
15+}
16+
17+const pendingBySocket = new WeakMap<WebSocket, PendingMessages>();
18+
19+function connectWs(port: number, token?: string): Promise<WebSocket> {
20+ return new Promise((resolve, reject) => {
21+ const query = token ? `?token=${encodeURIComponent(token)}` : "";
22+ const ws = new WebSocket(`ws://127.0.0.1:${port}/${query}`);
23+ const pending: PendingMessages = { queue: [], waiters: [] };
24+ pendingBySocket.set(ws, pending);
25+
26+ ws.on("message", (raw) => {
27+ const parsed = JSON.parse(String(raw));
28+ const waiter = pending.waiters.shift();
29+ if (waiter) {
30+ waiter(parsed);
31+ return;
32+ }
33+ pending.queue.push(parsed);
34+ });
35+
36+ ws.once("open", () => resolve(ws));
37+ ws.once("error", () => reject(new Error("WebSocket connection failed")));
38+ });
39+}
40+
41+function waitForMessage(ws: WebSocket): Promise<unknown> {
42+ const pending = pendingBySocket.get(ws);
43+ if (!pending) {
44+ return Promise.reject(new Error("WebSocket not initialized"));
45+ }
46+
47+ const queued = pending.queue.shift();
48+ if (queued !== undefined) {
49+ return Promise.resolve(queued);
50+ }
51+
52+ return new Promise((resolve) => {
53+ pending.waiters.push(resolve);
54+ });
55+}
56+
57+async function sendRequest(ws: WebSocket, method: string, params?: unknown): Promise<WsResponse> {
58+ const id = crypto.randomUUID();
59+ const message = JSON.stringify({ id, method, ...(params !== undefined ? { params } : {}) });
60+ ws.send(message);
61+
62+ // Wait for response with matching id
63+ while (true) {
64+ const parsed = (await waitForMessage(ws)) as Record<string, unknown>;
65+ if (parsed.id === id) {
66+ return parsed as WsResponse;
67+ }
68+ }
69+}
70+
71+describe("WebSocket Server", () => {
72+ let server: ReturnType<typeof createServer> | null = null;
73+ const connections: WebSocket[] = [];
74+ const tempDirs: string[] = [];
75+
76+ function makeTempDir(prefix: string): string {
77+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
78+ tempDirs.push(dir);
79+ return dir;
80+ }
81+
82+ function createTestServer(
83+ options: {
84+ cwd?: string;
85+ devUrl?: string;
86+ authToken?: string;
87+ stateDir?: string;
88+ } = {},
89+ ): ReturnType<typeof createServer> {
90+ const stateDir = options.stateDir ?? makeTempDir("codething-ws-state-");
91+ return createServer({
92+ port: 0,
93+ cwd: options.cwd ?? "/test/project",
94+ ...(options.devUrl ? { devUrl: options.devUrl } : {}),
95+ ...(options.authToken ? { authToken: options.authToken } : {}),
96+ projectRegistry: new ProjectRegistry(stateDir),
97+ });
98+ }
99+
100+ afterEach(async () => {
101+ for (const ws of connections) {
102+ ws.close();
103+ }
104+ connections.length = 0;
105+ if (server) {
106+ await server.stop();
107+ }
108+ server = null;
109+ for (const dir of tempDirs.splice(0, tempDirs.length)) {
110+ fs.rmSync(dir, { recursive: true, force: true });
111+ }
112+ vi.restoreAllMocks();
113+ });
114+
115+ it("sends welcome message on connect", async () => {
116+ server = createTestServer({ cwd: "/test/project" });
117+ // Get the actual port after listen
118+ await server.start();
119+ const addr = server.httpServer.address();
120+ const port = typeof addr === "object" && addr !== null ? addr.port : 0;
121+ expect(port).toBeGreaterThan(0);
122+
123+ const ws = await connectWs(port);
124+ connections.push(ws);
125+
126+ const message = (await waitForMessage(ws)) as WsPush;
127+ expect(message.type).toBe("push");
128+ expect(message.channel).toBe(WS_CHANNELS.serverWelcome);
129+ expect(message.data).toEqual({
130+ cwd: "/test/project",
131+ projectName: "project",
132+ });
133+ });
134+
135+ it("logs outbound websocket push events in dev mode", async () => {
136+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {
137+ // Keep test output clean while verifying websocket logs.
138+ });
139+
140+ server = createTestServer({
141+ cwd: "/test/project",
142+ devUrl: "http://localhost:5173",
143+ });
144+ await server.start();
145+ const addr = server.httpServer.address();
146+ const port = typeof addr === "object" && addr !== null ? addr.port : 0;
147+ expect(port).toBeGreaterThan(0);
148+
149+ const ws = await connectWs(port);
150+ connections.push(ws);
151+ await waitForMessage(ws);
152+
153+ expect(
154+ logSpy.mock.calls.some(([message]) => {
155+ if (typeof message !== "string") return false;
156+ return (
157+ message.includes("[ws]") &&
158+ message.includes("outgoing push") &&
159+ message.includes(`channel="${WS_CHANNELS.serverWelcome}"`)
160+ );
161+ }),
162+ ).toBe(true);
163+ });
164+
165+ it("responds to server.getConfig", async () => {
166+ server = createTestServer({ cwd: "/my/workspace" });
167+ await server.start();
168+ const addr = server.httpServer.address();
169+ const port = typeof addr === "object" && addr !== null ? addr.port : 0;
170+
171+ const ws = await connectWs(port);
172+ connections.push(ws);
173+
174+ // Consume welcome message
175+ await waitForMessage(ws);
176+
177+ const response = await sendRequest(ws, WS_METHODS.serverGetConfig);
178+ expect(response.error).toBeUndefined();
179+ expect(response.result).toEqual({ cwd: "/my/workspace" });
180+ });
181+
182+ it("returns error for unknown methods", async () => {
183+ server = createTestServer({ cwd: "/test" });
184+ await server.start();
185+ const addr = server.httpServer.address();
186+ const port = typeof addr === "object" && addr !== null ? addr.port : 0;
187+
188+ const ws = await connectWs(port);
189+ connections.push(ws);
190+
191+ // Consume welcome
192+ await waitForMessage(ws);
193+
194+ const response = await sendRequest(ws, "nonexistent.method");
195+ expect(response.error).toBeDefined();
196+ expect(response.error!.message).toContain("Unknown method");
197+ });
198+
199+ it("responds to providers.listSessions with empty array", async () => {
200+ server = createTestServer({ cwd: "/test" });
201+ await server.start();
202+ const addr = server.httpServer.address();
203+ const port = typeof addr === "object" && addr !== null ? addr.port : 0;
204+
205+ const ws = await connectWs(port);
206+ connections.push(ws);
207+
208+ // Consume welcome
209+ await waitForMessage(ws);
210+
211+ const response = await sendRequest(ws, WS_METHODS.providersListSessions);
212+ expect(response.error).toBeUndefined();
213+ expect(response.result).toEqual([]);
214+ });
215+
216+ it("handles invalid JSON gracefully", async () => {
217+ server = createTestServer({ cwd: "/test" });
218+ await server.start();
219+ const addr = server.httpServer.address();
220+ const port = typeof addr === "object" && addr !== null ? addr.port : 0;
221+
222+ const ws = await connectWs(port);
223+ connections.push(ws);
224+
225+ // Consume welcome
226+ await waitForMessage(ws);
227+
228+ // Send garbage
229+ ws.send("not json at all");
230+
231+ const response = (await waitForMessage(ws)) as WsResponse;
232+ expect(response.error).toBeDefined();
233+ expect(response.error!.message).toContain("Invalid request format");
234+ });
235+
236+ it("supports projects list/add/dedupe/remove", async () => {
237+ const stateDir = makeTempDir("codething-ws-projects-state-");
238+ const firstProjectCwd = makeTempDir("codething-ws-project-a-");
239+
240+ server = createTestServer({ stateDir, cwd: "/test" });
241+ await server.start();
242+ const addr = server.httpServer.address();
243+ const port = typeof addr === "object" && addr !== null ? addr.port : 0;
244+
245+ const ws = await connectWs(port);
246+ connections.push(ws);
247+ await waitForMessage(ws);
248+
249+ const emptyList = await sendRequest(ws, WS_METHODS.projectsList);
250+ expect(emptyList.error).toBeUndefined();
251+ expect(emptyList.result).toEqual([]);
252+
253+ const created = await sendRequest(ws, WS_METHODS.projectsAdd, {
254+ cwd: firstProjectCwd,
255+ });
256+ expect(created.error).toBeUndefined();
257+ expect((created.result as { created: boolean }).created).toBe(true);
258+
259+ const duplicate = await sendRequest(ws, WS_METHODS.projectsAdd, {
260+ cwd: firstProjectCwd,
261+ });
262+ expect(duplicate.error).toBeUndefined();
263+ expect((duplicate.result as { created: boolean }).created).toBe(false);
264+
265+ const listed = await sendRequest(ws, WS_METHODS.projectsList);
266+ expect(listed.error).toBeUndefined();
267+ const listedProjects = listed.result as Array<{ id: string; cwd: string }>;
268+ expect(listedProjects).toHaveLength(1);
269+
270+ const projectId = listedProjects[0]?.id;
271+ expect(projectId).toBeTruthy();
272+ if (!projectId) return;
273+
274+ const removed = await sendRequest(ws, WS_METHODS.projectsRemove, {
275+ id: projectId,
276+ });
277+ expect(removed.error).toBeUndefined();
278+
279+ const afterRemove = await sendRequest(ws, WS_METHODS.projectsList);
280+ expect(afterRemove.error).toBeUndefined();
281+ expect(afterRemove.result).toEqual([]);
282+ });
283+
284+ it("prunes missing projects on startup", async () => {
285+ const stateDir = makeTempDir("codething-ws-prune-state-");
286+ const existing = makeTempDir("codething-ws-existing-project-");
287+ const missing = path.join(stateDir, "definitely-missing");
288+ const now = new Date().toISOString();
289+ const projectsFile = path.join(stateDir, "projects.json");
290+
291+ fs.writeFileSync(
292+ projectsFile,
293+ JSON.stringify(
294+ {
295+ version: 1,
296+ projects: [
297+ {
298+ id: "project-existing",
299+ cwd: existing,
300+ name: "existing",
301+ createdAt: now,
302+ updatedAt: now,
303+ },
304+ {
305+ id: "project-missing",
306+ cwd: missing,
307+ name: "missing",
308+ createdAt: now,
309+ updatedAt: now,
310+ },
311+ ],
312+ },
313+ null,
314+ 2,
315+ ),
316+ );
317+
318+ server = createTestServer({ stateDir, cwd: "/test" });
319+ await server.start();
320+ const addr = server.httpServer.address();
321+ const port = typeof addr === "object" && addr !== null ? addr.port : 0;
322+
323+ const ws = await connectWs(port);
324+ connections.push(ws);
325+ await waitForMessage(ws);
326+
327+ const response = await sendRequest(ws, WS_METHODS.projectsList);
328+ expect(response.error).toBeUndefined();
329+ const listed = response.result as Array<{ id: string }>;
330+ expect(listed).toHaveLength(1);
331+ expect(listed[0]?.id).toBe("project-existing");
332+ });
333+
334+ it("rejects websocket connections without a valid auth token", async () => {
335+ server = createTestServer({ cwd: "/test", authToken: "secret-token" });
336+ await server.start();
337+ const addr = server.httpServer.address();
338+ const port = typeof addr === "object" && addr !== null ? addr.port : 0;
339+
340+ await expect(connectWs(port)).rejects.toThrow("WebSocket connection failed");
341+
342+ const authorizedWs = await connectWs(port, "secret-token");
343+ connections.push(authorizedWs);
344+ const welcome = (await waitForMessage(authorizedWs)) as WsPush;
345+ expect(welcome.channel).toBe(WS_CHANNELS.serverWelcome);
346+ });
347+});
apps/server/src/wsServer.ts+373−0
@@ -0,0 +1,373 @@
1+import { spawn } from "node:child_process";
2+import fs from "node:fs";
3+import http from "node:http";
4+import os from "node:os";
5+import path from "node:path";
6+import type { Duplex } from "node:stream";
7+
8+import {
9+ EDITORS,
10+ WS_CHANNELS,
11+ WS_METHODS,
12+ type WsPush,
13+ type WsRequest,
14+ type WsResponse,
15+ wsRequestSchema,
16+} from "@acme/contracts";
17+import { WebSocketServer, type WebSocket } from "ws";
18+
19+import { createLogger } from "./logger";
20+import { ProjectRegistry } from "./projectRegistry";
21+import { ProviderManager } from "./providerManager";
22+
23+const MIME_TYPES: Record<string, string> = {
24+ ".html": "text/html; charset=utf-8",
25+ ".js": "application/javascript; charset=utf-8",
26+ ".css": "text/css; charset=utf-8",
27+ ".json": "application/json; charset=utf-8",
28+ ".png": "image/png",
29+ ".jpg": "image/jpeg",
30+ ".svg": "image/svg+xml",
31+ ".ico": "image/x-icon",
32+ ".woff": "font/woff",
33+ ".woff2": "font/woff2",
34+ ".ttf": "font/ttf",
35+ ".map": "application/json",
36+};
37+
38+export interface ServerOptions {
39+ port: number;
40+ host?: string | undefined;
41+ cwd: string;
42+ staticDir?: string | undefined;
43+ devUrl?: string | undefined;
44+ logWebSocketEvents?: boolean | undefined;
45+ projectRegistry?: ProjectRegistry | undefined;
46+ authToken?: string | undefined;
47+}
48+
49+function parseBooleanEnv(value: string | undefined): boolean | undefined {
50+ if (value === undefined) return undefined;
51+ const normalized = value.trim().toLowerCase();
52+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
53+ if (["0", "false", "no", "off"].includes(normalized)) return false;
54+ return undefined;
55+}
56+
57+export function createServer(options: ServerOptions) {
58+ const {
59+ port,
60+ host,
61+ cwd,
62+ staticDir,
63+ devUrl,
64+ logWebSocketEvents: explicitLogWsEvents,
65+ projectRegistry: providedRegistry,
66+ authToken,
67+ } = options;
68+ const providerManager = new ProviderManager();
69+ const projectRegistry =
70+ providedRegistry ?? new ProjectRegistry(path.join(os.homedir(), ".t3", "userdata"));
71+ const clients = new Set<WebSocket>();
72+ const logger = createLogger("ws");
73+ const logWebSocketEvents =
74+ explicitLogWsEvents ?? parseBooleanEnv(process.env.CODETHING_LOG_WS_EVENTS) ?? Boolean(devUrl);
75+
76+ function logOutgoingPush(push: WsPush, recipients: number) {
77+ if (!logWebSocketEvents) return;
78+ logger.event("outgoing push", {
79+ channel: push.channel,
80+ recipients,
81+ payload: push.data,
82+ });
83+ }
84+
85+ // Forward provider events to all connected WebSocket clients
86+ providerManager.on("event", (event) => {
87+ const push: WsPush = {
88+ type: "push",
89+ channel: WS_CHANNELS.providerEvent,
90+ data: event,
91+ };
92+ const message = JSON.stringify(push);
93+ let recipients = 0;
94+ for (const client of clients) {
95+ if (client.readyState === client.OPEN) {
96+ client.send(message);
97+ recipients += 1;
98+ }
99+ }
100+ logOutgoingPush(push, recipients);
101+ });
102+
103+ // HTTP server — serves static files or redirects to Vite dev server
104+ const httpServer = http.createServer((req, res) => {
105+ // In dev mode, redirect to Vite dev server
106+ if (devUrl) {
107+ res.writeHead(302, { Location: devUrl });
108+ res.end();
109+ return;
110+ }
111+
112+ // Serve static files from the renderer build
113+ if (!staticDir) {
114+ res.writeHead(503, { "Content-Type": "text/plain" });
115+ res.end("No static directory configured and no dev URL set.");
116+ return;
117+ }
118+
119+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
120+ let filePath = path.join(staticDir, url.pathname);
121+
122+ // SPA fallback: if no file extension and not found, serve index.html
123+ const ext = path.extname(filePath);
124+ if (!ext) {
125+ filePath = path.join(filePath, "index.html");
126+ }
127+
128+ fs.stat(filePath, (err, stats) => {
129+ if (err || !stats?.isFile()) {
130+ // SPA fallback
131+ const indexPath = path.join(staticDir, "index.html");
132+ fs.readFile(indexPath, (readErr, data) => {
133+ if (readErr) {
134+ res.writeHead(404, { "Content-Type": "text/plain" });
135+ res.end("Not Found");
136+ return;
137+ }
138+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
139+ res.end(data);
140+ });
141+ return;
142+ }
143+
144+ const fileExt = path.extname(filePath);
145+ const contentType = MIME_TYPES[fileExt] ?? "application/octet-stream";
146+
147+ fs.readFile(filePath, (readErr, data) => {
148+ if (readErr) {
149+ res.writeHead(500, { "Content-Type": "text/plain" });
150+ res.end("Internal Server Error");
151+ return;
152+ }
153+ res.writeHead(200, { "Content-Type": contentType });
154+ res.end(data);
155+ });
156+ });
157+ });
158+
159+ // WebSocket server — upgrades from the HTTP server
160+ const wss = new WebSocketServer({ noServer: true });
161+
162+ function rejectUpgrade(socket: Duplex, statusCode: number, message: string): void {
163+ socket.write(
164+ `HTTP/1.1 ${statusCode} ${statusCode === 401 ? "Unauthorized" : "Bad Request"}\r\n` +
165+ "Connection: close\r\n" +
166+ "Content-Type: text/plain\r\n" +
167+ `Content-Length: ${Buffer.byteLength(message)}\r\n` +
168+ "\r\n" +
169+ message,
170+ );
171+ socket.destroy();
172+ }
173+
174+ httpServer.on("upgrade", (request, socket, head) => {
175+ if (authToken) {
176+ let providedToken: string | null = null;
177+ try {
178+ const url = new URL(request.url ?? "/", `http://localhost:${port}`);
179+ providedToken = url.searchParams.get("token");
180+ } catch {
181+ rejectUpgrade(socket, 400, "Invalid WebSocket URL");
182+ return;
183+ }
184+
185+ if (providedToken !== authToken) {
186+ rejectUpgrade(socket, 401, "Unauthorized WebSocket connection");
187+ return;
188+ }
189+ }
190+
191+ wss.handleUpgrade(request, socket, head, (ws) => {
192+ wss.emit("connection", ws, request);
193+ });
194+ });
195+
196+ wss.on("connection", (ws) => {
197+ clients.add(ws);
198+
199+ // Send welcome message with project info
200+ const segments = cwd.split(/[/\\]/).filter(Boolean);
201+ const projectName = segments[segments.length - 1] ?? "project";
202+
203+ const welcome: WsPush = {
204+ type: "push",
205+ channel: WS_CHANNELS.serverWelcome,
206+ data: { cwd, projectName },
207+ };
208+ logOutgoingPush(welcome, 1);
209+ ws.send(JSON.stringify(welcome));
210+
211+ ws.on("message", (raw) => {
212+ void handleMessage(ws, raw);
213+ });
214+
215+ ws.on("close", () => {
216+ clients.delete(ws);
217+ });
218+
219+ ws.on("error", () => {
220+ clients.delete(ws);
221+ });
222+ });
223+
224+ async function handleMessage(ws: WebSocket, raw: unknown) {
225+ let request: WsRequest;
226+ try {
227+ const parsed = JSON.parse(String(raw));
228+ request = wsRequestSchema.parse(parsed);
229+ } catch {
230+ const errorResponse: WsResponse = {
231+ id: "unknown",
232+ error: { message: "Invalid request format" },
233+ };
234+ ws.send(JSON.stringify(errorResponse));
235+ return;
236+ }
237+
238+ try {
239+ const result = await routeRequest(request);
240+ const response: WsResponse = { id: request.id, result };
241+ ws.send(JSON.stringify(response));
242+ } catch (err) {
243+ const message = err instanceof Error ? err.message : "Unknown server error";
244+ const response: WsResponse = {
245+ id: request.id,
246+ error: { message },
247+ };
248+ ws.send(JSON.stringify(response));
249+ }
250+ }
251+
252+ async function routeRequest(request: WsRequest): Promise<unknown> {
253+ switch (request.method) {
254+ case WS_METHODS.providersStartSession:
255+ return providerManager.startSession(request.params as never);
256+
257+ case WS_METHODS.providersSendTurn:
258+ return providerManager.sendTurn(request.params as never);
259+
260+ case WS_METHODS.providersInterruptTurn:
261+ return providerManager.interruptTurn(request.params as never);
262+
263+ case WS_METHODS.providersRespondToRequest:
264+ return providerManager.respondToRequest(request.params as never);
265+
266+ case WS_METHODS.providersStopSession: {
267+ providerManager.stopSession(request.params as never);
268+ return undefined;
269+ }
270+
271+ case WS_METHODS.providersListSessions:
272+ return providerManager.listSessions();
273+
274+ case WS_METHODS.projectsList:
275+ return projectRegistry.list();
276+
277+ case WS_METHODS.projectsAdd:
278+ return projectRegistry.add(request.params as never);
279+
280+ case WS_METHODS.projectsRemove:
281+ projectRegistry.remove(request.params as never);
282+ return undefined;
283+
284+ case WS_METHODS.shellOpenInEditor: {
285+ const params = request.params as {
286+ cwd: string;
287+ editor: string;
288+ };
289+ if (!params?.cwd) throw new Error("cwd is required");
290+ const editorDef = EDITORS.find((e) => e.id === params.editor);
291+ if (!editorDef) throw new Error(`Unknown editor: ${params.editor}`);
292+ if (!editorDef.command) {
293+ // Can't open file manager programmatically from server, just succeed silently
294+ return undefined;
295+ }
296+ const child = spawn(editorDef.command, [params.cwd], {
297+ detached: true,
298+ stdio: "ignore",
299+ });
300+ child.on("error", () => {
301+ /* ignore spawn failures for detached editors */
302+ });
303+ child.unref();
304+ return undefined;
305+ }
306+
307+ case WS_METHODS.serverGetConfig:
308+ return { cwd };
309+
310+ default:
311+ throw new Error(`Unknown method: ${request.method}`);
312+ }
313+ }
314+
315+ function start() {
316+ return new Promise<void>((resolve, reject) => {
317+ const onError = (error: Error) => {
318+ httpServer.off("error", onError);
319+ reject(error);
320+ };
321+ httpServer.once("error", onError);
322+ const onListening = () => {
323+ httpServer.off("error", onError);
324+ resolve();
325+ };
326+ if (host) {
327+ httpServer.listen(port, host, onListening);
328+ return;
329+ }
330+ httpServer.listen(port, onListening);
331+ });
332+ }
333+
334+ async function stop(): Promise<void> {
335+ providerManager.stopAll();
336+ providerManager.dispose();
337+
338+ for (const client of clients) {
339+ client.close();
340+ }
341+ clients.clear();
342+
343+ const isServerNotRunningError = (error: unknown): boolean => {
344+ if (!(error instanceof Error)) return false;
345+ const maybeCode = (error as NodeJS.ErrnoException).code;
346+ return maybeCode === "ERR_SERVER_NOT_RUNNING" || error.message.toLowerCase().includes("not running");
347+ };
348+
349+ const closeWebSocketServer = new Promise<void>((resolve, reject) => {
350+ wss.close((error) => {
351+ if (error && !isServerNotRunningError(error)) {
352+ reject(error);
353+ return;
354+ }
355+ resolve();
356+ });
357+ });
358+
359+ const closeHttpServer = new Promise<void>((resolve, reject) => {
360+ httpServer.close((error) => {
361+ if (error && !isServerNotRunningError(error)) {
362+ reject(error);
363+ return;
364+ }
365+ resolve();
366+ });
367+ });
368+
369+ await Promise.all([closeWebSocketServer, closeHttpServer]);
370+ }
371+
372+ return { start, stop, httpServer };
373+}
apps/server/tsconfig.json+9−0
@@ -0,0 +1,9 @@
1+{
2+ "extends": "../../tsconfig.base.json",
3+ "compilerOptions": {
4+ "composite": true,
5+ "types": ["node"],
6+ "lib": ["ES2023"]
7+ },
8+ "include": ["src", "tsup.config.ts"]
9+}
apps/server/tsup.config.ts+13−0
@@ -0,0 +1,13 @@
1+import { defineConfig } from "tsup";
2+
3+export default defineConfig({
4+ entry: ["src/index.ts"],
5+ format: "esm",
6+ outDir: "dist",
7+ sourcemap: true,
8+ clean: true,
9+ noExternal: ["@acme/contracts"],
10+ banner: {
11+ js: '#!/usr/bin/env node\n',
12+ },
13+});
bun.lock+56−232
@@ -16,17 +16,15 @@ · 15 unmodified lines
1616 "name": "@acme/desktop",
1717 "version": "0.0.0",
1818 "dependencies": {
19− "@acme/contracts": "workspace:*",
2019 "electron": "33.4.11",
21− "node-pty": "^1.0.0",
2220 },
2321 "devDependencies": {
24− "@electron/rebuild": "^3.7.0",
2522 "@types/node": "^22.10.2",
2623 "concurrently": "^9.1.2",
2724 "electronmon": "^2.0.2",
2825 "tsup": "^8.3.5",
2926 "typescript": "^5.7.3",
27+ "vitest": "^3.0.0",
3028 "wait-on": "^8.0.2",
3129 },
3230 },
@@ -55,6 +53,26 @@ · 22 unmodified lines
5553 "vite": "^6.0.5",
5654 },
5755 },
56+ "apps/server": {
57+ "name": "t3",
58+ "version": "0.1.0",
59+ "bin": {
60+ "t3": "./dist/index.js",
61+ },
62+ "dependencies": {
63+ "open": "^10.1.0",
64+ "ws": "^8.18.0",
65+ },
66+ "devDependencies": {
67+ "@acme/contracts": "workspace:*",
68+ "@acme/renderer": "workspace:*",
69+ "@types/node": "^22.10.2",
70+ "@types/ws": "^8.5.13",
71+ "tsup": "^8.3.5",
72+ "tsx": "^4.19.0",
73+ "typescript": "^5.7.3",
74+ },
75+ },
5876 "packages/contracts": {
5977 "name": "@acme/contracts",
6078 "version": "0.0.0",
@@ -114,10 +132,6 @@ · 53 unmodified lines
114132
115133 "@electron/get": ["@electron/get@2.0.3", "", { "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" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="],
116134
117− "@electron/node-gyp": ["@electron/node-gyp@github:electron/node-gyp#06b29aa", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": "./bin/node-gyp.js" }, "electron-node-gyp-06b29aa"],
118−
119− "@electron/rebuild": ["@electron/rebuild@3.7.2", "", { "dependencies": { "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", "@malept/cross-spawn-promise": "^2.0.0", "chalk": "^4.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "fs-extra": "^10.0.0", "got": "^11.7.0", "node-abi": "^3.45.0", "node-api-version": "^0.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^6.0.5", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg=="],
120−
121135 "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
122136
123137 "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
@@ -170,8 +184,6 @@ · 46 unmodified lines
170184
171185 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
172186
173− "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
174−
175187 "@hapi/address": ["@hapi/address@5.1.1", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA=="],
176188
177189 "@hapi/formula": ["@hapi/formula@3.0.2", "", {}, "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw=="],
@@ -194,12 +206,6 @@ · 16 unmodified lines
194206
195207 "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
196208
197− "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="],
198−
199− "@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="],
200−
201− "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="],
202−
203209 "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jmUfF7cNJPw57bEK7sMIqrYRgn4LH428tSgtgLTCtjuGuu1ShREyrkeB7y8HtkXRfhBs4lVY+HMLhqElJvZ6ww=="],
204210
205211 "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-S6vlV8S7jbjzJOSjfVg2CimUC0r7/aHDLdUm/3+/B/SU/s1jV7ivqWkMv1/8EB43d1BBwT9JQ60ZMTkBqeXSFA=="],
@@ -320,8 +326,6 @@ · 114 unmodified lines
320326
321327 "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
322328
323− "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="],
324−
325329 "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
326330
327331 "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
@@ -362,6 +366,8 @@ · 34 unmodified lines
362366
363367 "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
364368
369+ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
370+
365371 "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
366372
367373 "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
@@ -382,16 +388,8 @@ · 14 unmodified lines
382388
383389 "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
384390
385− "abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="],
386−
387391 "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
388392
389− "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
390−
391− "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
392−
393− "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
394−
395393 "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
396394
397395 "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@@ -402,38 +400,28 @@ · 4 unmodified lines
402400
403401 "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
404402
405− "axios": ["axios@1.13.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg=="],
403+ "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="],
406404
407405 "babel-plugin-react-compiler": ["babel-plugin-react-compiler@19.0.0-beta-ebf51a3-20250411", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-q84bNR9JG1crykAlJUt5Ud0/5BUyMFuQww/mrwIQDFBaxsikqBDj3f/FNDsVd2iR26A1HvXKWPEIfgJDv8/V2g=="],
408406
409407 "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
410408
411− "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
412−
413− "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
414−
415409 "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="],
416410
417− "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
418−
419411 "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
420412
421− "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
422−
423413 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
424414
425415 "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
426416
427− "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
428−
429417 "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
430418
419+ "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
420+
431421 "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
432422
433423 "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
434424
435− "cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="],
436−
437425 "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
438426
439427 "cacheable-request": ["cacheable-request@7.0.4", "", { "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" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="],
@@ -460,18 +448,8 @@ · 20 unmodified lines
460448
461449 "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
462450
463− "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
464−
465− "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
466−
467− "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
468−
469− "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
470−
471451 "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
472452
473− "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
474−
475453 "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
476454
477455 "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -484,8 +462,6 @@ · 6 unmodified lines
484462
485463 "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
486464
487− "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
488−
489465 "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="],
490466
491467 "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
@@ -494,8 +470,6 @@ · 2 unmodified lines
494470
495471 "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
496472
497− "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
498−
499473 "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
500474
501475 "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
@@ -506,12 +480,16 @@ · 4 unmodified lines
506480
507481 "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
508482
509− "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
483+ "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
484+
485+ "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
510486
511487 "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
512488
513489 "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
514490
491+ "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
492+
515493 "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
516494
517495 "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
@@ -534,16 +512,12 @@ · 16 unmodified lines
534512
535513 "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
536514
537− "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="],
538−
539515 "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
540516
541517 "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
542518
543519 "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
544520
545− "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
546−
547521 "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
548522
549523 "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
@@ -568,8 +542,6 @@ · 18 unmodified lines
568542
569543 "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
570544
571− "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
572−
573545 "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
574546
575547 "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
@@ -586,11 +558,7 @@ · 10 unmodified lines
586558
587559 "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
588560
589− "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
590−
591− "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
592−
593− "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
561+ "fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
594562
595563 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
596564
@@ -606,7 +574,7 @@ · 9 unmodified lines
606574
607575 "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
608576
609− "glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
577+ "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
610578
611579 "global-agent": ["global-agent@3.0.0", "", { "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" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
612580
@@ -642,55 +610,31 @@ · 29 unmodified lines
642610
643611 "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
644612
645− "http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="],
646−
647613 "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
648614
649− "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
650−
651− "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
652−
653− "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
654−
655− "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
656−
657615 "import-from": ["import-from@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ=="],
658616
659− "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
660−
661− "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
662−
663− "infer-owner": ["infer-owner@1.0.4", "", {}, "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A=="],
664−
665− "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
666−
667− "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
668−
669617 "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
670618
671− "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
672−
673619 "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
674620
675621 "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
676622
677623 "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
678624
625+ "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
626+
679627 "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
680628
681629 "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
682630
683− "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
684−
685− "is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="],
631+ "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
686632
687633 "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
688634
689635 "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
690636
691− "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
692−
693− "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
637+ "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
694638
695639 "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
696640
@@ -708,7 +652,7 @@ · 11 unmodified lines
708652
709653 "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
710654
711− "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
655+ "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
712656
713657 "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
714658
@@ -746,8 +690,6 @@ · 31 unmodified lines
746690
747691 "lodash.difference": ["lodash.difference@4.5.0", "", {}, "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="],
748692
749− "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
750−
751693 "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
752694
753695 "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
@@ -756,14 +698,12 @@ · 2 unmodified lines
756698
757699 "lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="],
758700
759− "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
701+ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
760702
761703 "lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
762704
763705 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
764706
765− "make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="],
766−
767707 "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
768708
769709 "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
@@ -862,30 +802,10 @@ · 92 unmodified lines
862802
863803 "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
864804
865− "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
866−
867805 "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
868806
869− "minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
870−
871807 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
872808
873− "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
874−
875− "minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="],
876−
877− "minipass-fetch": ["minipass-fetch@2.1.2", "", { "dependencies": { "minipass": "^3.1.6", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA=="],
878−
879− "minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="],
880−
881− "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="],
882−
883− "minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="],
884−
885− "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
886−
887− "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
888−
889809 "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
890810
891811 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -894,20 +814,8 @@ · 2 unmodified lines
894814
895815 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
896816
897− "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
898−
899− "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="],
900−
901− "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
902−
903− "node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="],
904−
905− "node-pty": ["node-pty@1.1.0", "", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg=="],
906−
907817 "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
908818
909− "nopt": ["nopt@6.0.0", "", { "dependencies": { "abbrev": "^1.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g=="],
910−
911819 "normalize-path": ["normalize-path@2.1.1", "", { "dependencies": { "remove-trailing-separator": "^1.0.1" } }, "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w=="],
912820
913821 "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="],
@@ -918,9 +826,7 @@ · 4 unmodified lines
918826
919827 "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
920828
921− "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
922−
923− "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
829+ "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
924830
925831 "oxfmt": ["oxfmt@0.28.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.28.0", "@oxfmt/darwin-x64": "0.28.0", "@oxfmt/linux-arm64-gnu": "0.28.0", "@oxfmt/linux-arm64-musl": "0.28.0", "@oxfmt/linux-x64-gnu": "0.28.0", "@oxfmt/linux-x64-musl": "0.28.0", "@oxfmt/win32-arm64": "0.28.0", "@oxfmt/win32-x64": "0.28.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-3+hhBqPE6Kp22KfJmnstrZbl+KdOVSEu1V0ABaFIg1rYLtrMgrupx9znnHgHLqKxAVHebjTdiCJDk30CXOt6cw=="],
926832
@@ -928,14 +834,8 @@ · 1 unmodified line
928834
929835 "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
930836
931− "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
932−
933837 "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
934838
935− "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
936−
937− "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
938−
939839 "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
940840
941841 "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
@@ -956,14 +856,8 @@ · 14 unmodified lines
956856
957857 "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
958858
959− "proc-log": ["proc-log@2.0.1", "", {}, "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw=="],
960−
961859 "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
962860
963− "promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
964−
965− "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
966−
967861 "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
968862
969863 "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
@@ -980,10 +874,6 @@ · 10 unmodified lines
980874
981875 "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
982876
983− "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="],
984−
985− "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
986−
987877 "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
988878
989879 "rehype-highlight": ["rehype-highlight@7.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-text": "^4.0.0", "lowlight": "^3.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA=="],
@@ -1004,50 +894,32 @@ · 14 unmodified lines
1004894
1005895 "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
1006896
1007− "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
1008−
1009− "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
1010−
1011− "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
897+ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
1012898
1013− "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
899+ "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
1014900
1015901 "roarr": ["roarr@2.15.4", "", { "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" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
1016902
1017903 "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
1018904
905+ "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
906+
1019907 "runtime-required": ["runtime-required@1.1.0", "", {}, "sha512-yX97f5E0WfNpcQnfVjap6vzQcvErkYYCx6eTK4siqGEdC8lglwypUFgZVTX7ShvIlgfkC4XGFl9O1KTYcff0pw=="],
1020908
1021909 "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
1022910
1023− "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
1024−
1025− "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
1026−
1027911 "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
1028912
1029− "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
913+ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
1030914
1031915 "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="],
1032916
1033917 "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="],
1034918
1035− "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
1036−
1037− "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
1038−
1039919 "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
1040920
1041921 "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
1042922
1043− "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
1044−
1045− "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
1046−
1047− "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
1048−
1049− "socks-proxy-agent": ["socks-proxy-agent@7.0.0", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww=="],
1050−
1051923 "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
1052924
1053925 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
@@ -1056,16 +928,12 @@ · 2 unmodified lines
1056928
1057929 "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
1058930
1059− "ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="],
1060−
1061931 "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
1062932
1063933 "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
1064934
1065935 "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
1066936
1067− "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
1068−
1069937 "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
1070938
1071939 "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -1082,12 +950,12 @@ · 10 unmodified lines
1082950
1083951 "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
1084952
953+ "t3": ["t3@workspace:apps/server"],
954+
1085955 "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
1086956
1087957 "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
1088958
1089− "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
1090−
1091959 "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
1092960
1093961 "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
@@ -1118,6 +986,8 @@ · 24 unmodified lines
1118986
1119987 "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="],
1120988
989+ "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
990+
1121991 "turbo": ["turbo@2.8.3", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.3", "turbo-darwin-arm64": "2.8.3", "turbo-linux-64": "2.8.3", "turbo-linux-arm64": "2.8.3", "turbo-windows-64": "2.8.3", "turbo-windows-arm64": "2.8.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-8Osxz5Tu/Dw2kb31EAY+nhq/YZ3wzmQSmYa1nIArqxgCAldxv9TPlrAiaBUDVnKA4aiPn0OFBD1ACcpc5VFOAQ=="],
1122992
1123993 "turbo-darwin-64": ["turbo-darwin-64@2.8.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-4kXRLfcygLOeNcP6JquqRLmGB/ATjjfehiojL2dJkL7GFm3SPSXbq7oNj8UbD8XriYQ5hPaSuz59iF1ijPHkTw=="],
@@ -1142,10 +1012,6 @@ · 18 unmodified lines
11421012
11431013 "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
11441014
1145− "unique-filename": ["unique-filename@2.0.1", "", { "dependencies": { "unique-slug": "^3.0.0" } }, "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A=="],
1146−
1147− "unique-slug": ["unique-slug@3.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w=="],
1148−
11491015 "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
11501016
11511017 "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
@@ -1158,14 +1024,12 @@ · 6 unmodified lines
11581024
11591025 "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
11601026
1161− "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
1027+ "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
11621028
11631029 "unixify": ["unixify@1.0.0", "", { "dependencies": { "normalize-path": "^2.1.1" } }, "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg=="],
11641030
11651031 "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
11661032
1167− "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
1168−
11691033 "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
11701034
11711035 "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
@@ -1180,19 +1044,19 @@ · 8 unmodified lines
11801044
11811045 "watchboy": ["watchboy@0.4.3", "", { "dependencies": { "lodash.difference": "^4.5.0", "micromatch": "^4.0.2", "pify": "^4.0.1", "unixify": "^1.0.0" } }, "sha512-GHs1HxwvxSMBsqd/WfTOZhj5gBdMqf5HQpfgtKxDfZRxrlYPDdVLRB61LCeRzJaWANmvSIMlfmRVDwVmJFgAKA=="],
11821046
1183− "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
1184−
1185− "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
1186−
11871047 "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
11881048
11891049 "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
11901050
11911051 "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
11921052
1053+ "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
1054+
1055+ "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
1056+
11931057 "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
11941058
1195− "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
1059+ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
11961060
11971061 "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
11981062
@@ -1206,16 +1070,6 @@ · 7 unmodified lines
12061070
12071071 "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
12081072
1209− "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
1210−
1211− "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
1212−
1213− "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
1214−
1215− "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
1216−
1217− "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
1218−
12191073 "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
12201074
12211075 "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
@@ -1230,8 +1084,6 @@ · 8 unmodified lines
12301084
12311085 "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
12321086
1233− "cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
1234−
12351087 "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
12361088
12371089 "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
@@ -1240,46 +1092,20 @@ · 2 unmodified lines
12401092
12411093 "electronmon/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
12421094
1243− "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
1244−
1245− "make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
1095+ "global-agent/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
12461096
12471097 "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
12481098
12491099 "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
12501100
1251− "minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
1252−
1253− "minipass-fetch/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
1254−
1255− "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
1256−
1257− "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
1258−
1259− "minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
1260−
1261− "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
1262−
12631101 "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
12641102
1265− "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
1266−
1267− "ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
1268−
12691103 "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
12701104
12711105 "vitest/tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
12721106
1273− "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
1274−
1275− "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
1276−
1277− "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
1278−
12791107 "electronmon/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
12801108
1281− "rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
1282−
12831109 "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
12841110
12851111 "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
@@ -1331,7 +1157,5 @@ · 45 unmodified lines
13311157 "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
13321158
13331159 "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
1334−
1335− "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
13361160 }
13371161 }
package.json+8−2
@@ -6,13 +6,19 @@ · 5 unmodified lines
66 "packages/*"
77 ],
88 "scripts": {
9− "dev": "bun run build:contracts && turbo run dev --parallel",
9+ "dev": "CODETHING_LOG_WS_EVENTS=1 VITE_WS_URL=ws://localhost:3773 turbo run dev --ui=tui --filter=@acme/contracts --filter=@acme/renderer --filter=t3 --parallel",
10+ "dev:server": "bun run build:contracts && bun run --cwd apps/server dev",
11+ "dev:web": "VITE_WS_URL=ws://localhost:3773 bun run --cwd apps/renderer dev",
12+ "dev:desktop": "bun run --cwd apps/desktop dev",
13+ "start": "node apps/server/dist/index.js",
14+ "start:desktop": "bun run --cwd apps/desktop start",
1015 "build": "turbo run build",
16+ "build:desktop": "bun run build:contracts && bun run --cwd apps/renderer build && bun run --cwd apps/server build && bun run --cwd apps/desktop build",
1117 "typecheck": "turbo run typecheck",
1218 "lint": "oxlint --report-unused-disable-directives",
1319 "test": "turbo run test",
20+ "test:desktop-smoke": "bun run --cwd apps/desktop smoke-test",
1421 "fmt": "oxfmt",
15− "smoke-test": "turbo run smoke-test",
1622 "build:contracts": "bun run --cwd packages/contracts build"
1723 },
1824 "devDependencies": {
packages/contracts/src/index.ts+2−0
@@ -4,3 +4,5 @@ export * from "./agent"; · 3 unmodified lines
44 export * from "./terminal";
55 export * from "./provider";
66 export * from "./model";
7+export * from "./ws";
8+export * from "./project";
packages/contracts/src/ipc.ts+11−22
@@ -9,6 +9,12 @@ import type { · 8 unmodified lines
99 ProviderStopSessionInput,
1010 ProviderTurnStartResult,
1111 } from "./provider";
12+import type {
13+ ProjectAddInput,
14+ ProjectAddResult,
15+ ProjectListResult,
16+ ProjectRemoveInput,
17+} from "./project";
1218 import type { TerminalCommandInput, TerminalCommandResult } from "./terminal";
1319 import type { NewTodoInput, Todo } from "./todo";
1420
@@ -19,28 +25,6 @@ export const EDITORS = [ · 4 unmodified lines
1925
2026 export type EditorId = (typeof EDITORS)[number]["id"];
2127
22−export const IPC_CHANNELS = {
23− todosList: "todos:list",
24− todosAdd: "todos:add",
25− todosToggle: "todos:toggle",
26− todosRemove: "todos:remove",
27− dialogPickFolder: "dialog:pick-folder",
28− terminalRun: "terminal:run",
29− agentSpawn: "agent:spawn",
30− agentKill: "agent:kill",
31− agentWrite: "agent:write",
32− agentOutput: "agent:output",
33− agentExit: "agent:exit",
34− providerSessionStart: "provider:session:start",
35− providerTurnStart: "provider:turn:start",
36− providerTurnInterrupt: "provider:turn:interrupt",
37− providerSessionStop: "provider:session:stop",
38− providerSessionList: "provider:session:list",
39− providerRequestRespond: "provider:request:respond",
40− providerEvent: "provider:event",
41− shellOpenInEditor: "shell:open-in-editor",
42−} as const;
43−
4428 export interface NativeApi {
4529 todos: {
4630 list: () => Promise<Todo[]>;
@@ -70,6 +54,11 @@ export interface NativeApi { · 23 unmodified lines
7054 listSessions: () => Promise<ProviderSession[]>;
7155 onEvent: (callback: (event: ProviderEvent) => void) => () => void;
7256 };
57+ projects: {
58+ list: () => Promise<ProjectListResult>;
59+ add: (input: ProjectAddInput) => Promise<ProjectAddResult>;
60+ remove: (input: ProjectRemoveInput) => Promise<void>;
61+ };
7362 shell: {
7463 openInEditor: (cwd: string, editor: EditorId) => Promise<void>;
7564 };
packages/contracts/src/project.test.ts+49−0
@@ -0,0 +1,49 @@
1+import { describe, expect, it } from "vitest";
2+
3+import {
4+ projectAddInputSchema,
5+ projectAddResultSchema,
6+ projectListResultSchema,
7+ projectRemoveInputSchema,
8+} from "./project";
9+
10+describe("project contracts", () => {
11+ it("parses project list result", () => {
12+ const result = projectListResultSchema.parse([
13+ {
14+ id: "project-1",
15+ cwd: "/tmp/project",
16+ name: "project",
17+ createdAt: "2026-02-10T00:00:00.000Z",
18+ updatedAt: "2026-02-10T00:00:00.000Z",
19+ },
20+ ]);
21+
22+ expect(result).toHaveLength(1);
23+ expect(result[0]?.id).toBe("project-1");
24+ });
25+
26+ it("trims add input cwd", () => {
27+ const parsed = projectAddInputSchema.parse({ cwd: " /tmp/project " });
28+ expect(parsed.cwd).toBe("/tmp/project");
29+ });
30+
31+ it("requires add result created flag", () => {
32+ const parsed = projectAddResultSchema.parse({
33+ project: {
34+ id: "project-1",
35+ cwd: "/tmp/project",
36+ name: "project",
37+ createdAt: "2026-02-10T00:00:00.000Z",
38+ updatedAt: "2026-02-10T00:00:00.000Z",
39+ },
40+ created: true,
41+ });
42+ expect(parsed.created).toBe(true);
43+ });
44+
45+ it("parses remove input", () => {
46+ const parsed = projectRemoveInputSchema.parse({ id: "project-1" });
47+ expect(parsed.id).toBe("project-1");
48+ });
49+});
packages/contracts/src/project.ts+30−0
@@ -0,0 +1,30 @@
1+import { z } from "zod";
2+
3+export const projectRecordSchema = z.object({
4+ id: z.string().min(1),
5+ cwd: z.string().min(1),
6+ name: z.string().min(1),
7+ createdAt: z.string().datetime(),
8+ updatedAt: z.string().datetime(),
9+});
10+
11+export const projectListResultSchema = z.array(projectRecordSchema);
12+
13+export const projectAddInputSchema = z.object({
14+ cwd: z.string().trim().min(1),
15+});
16+
17+export const projectAddResultSchema = z.object({
18+ project: projectRecordSchema,
19+ created: z.boolean(),
20+});
21+
22+export const projectRemoveInputSchema = z.object({
23+ id: z.string().min(1),
24+});
25+
26+export type ProjectRecord = z.infer<typeof projectRecordSchema>;
27+export type ProjectListResult = z.infer<typeof projectListResultSchema>;
28+export type ProjectAddInput = z.input<typeof projectAddInputSchema>;
29+export type ProjectAddResult = z.infer<typeof projectAddResultSchema>;
30+export type ProjectRemoveInput = z.input<typeof projectRemoveInputSchema>;
packages/contracts/src/ws.ts+78−0
@@ -0,0 +1,78 @@
1+import { z } from "zod";
2+
3+// ── WebSocket RPC Method Names ───────────────────────────────────────
4+
5+export const WS_METHODS = {
6+ // Provider methods (mirrors NativeApi.providers)
7+ providersStartSession: "providers.startSession",
8+ providersSendTurn: "providers.sendTurn",
9+ providersInterruptTurn: "providers.interruptTurn",
10+ providersRespondToRequest: "providers.respondToRequest",
11+ providersStopSession: "providers.stopSession",
12+ providersListSessions: "providers.listSessions",
13+
14+ // Project registry methods
15+ projectsList: "projects.list",
16+ projectsAdd: "projects.add",
17+ projectsRemove: "projects.remove",
18+
19+ // Shell methods
20+ shellOpenInEditor: "shell.openInEditor",
21+
22+ // Server meta
23+ serverGetConfig: "server.getConfig",
24+} as const;
25+
26+// ── Push Event Channels ──────────────────────────────────────────────
27+
28+export const WS_CHANNELS = {
29+ providerEvent: "providers.event",
30+ serverWelcome: "server.welcome",
31+} as const;
32+
33+// ── Client → Server (request) ────────────────────────────────────────
34+
35+export const wsRequestSchema = z.object({
36+ id: z.string().min(1),
37+ method: z.string().min(1),
38+ params: z.unknown().optional(),
39+});
40+
41+export type WsRequest = z.infer<typeof wsRequestSchema>;
42+
43+// ── Server → Client (response to request) ────────────────────────────
44+
45+export const wsResponseSchema = z.object({
46+ id: z.string().min(1),
47+ result: z.unknown().optional(),
48+ error: z
49+ .object({
50+ message: z.string(),
51+ })
52+ .optional(),
53+});
54+
55+export type WsResponse = z.infer<typeof wsResponseSchema>;
56+
57+// ── Server → Client (push event) ─────────────────────────────────────
58+
59+export const wsPushSchema = z.object({
60+ type: z.literal("push"),
61+ channel: z.string().min(1),
62+ data: z.unknown(),
63+});
64+
65+export type WsPush = z.infer<typeof wsPushSchema>;
66+
67+// ── Union of all server → client messages ─────────────────────────────
68+
69+export type WsServerMessage = WsResponse | WsPush;
70+
71+// ── Server welcome payload ───────────────────────────────────────────
72+
73+export const wsWelcomePayloadSchema = z.object({
74+ cwd: z.string().min(1),
75+ projectName: z.string().min(1),
76+});
77+
78+export type WsWelcomePayload = z.infer<typeof wsWelcomePayloadSchema>;
turbo.json+13−6
@@ -1,12 +1,24 @@
11 {
22 "$schema": "https://turbo.build/schema.json",
3− "globalEnv": ["ELECTRON_RENDERER_PORT"],
3+ "globalEnv": [
4+ "PORT",
5+ "VITE_WS_URL",
6+ "VITE_DEV_SERVER_URL",
7+ "CODETHING_LOG_WS_EVENTS",
8+ "CODETHING_MODE",
9+ "CODETHING_PORT",
10+ "CODETHING_NO_BROWSER",
11+ "CODETHING_STATE_DIR",
12+ "CODETHING_AUTH_TOKEN",
13+ "CODETHING_DESKTOP_WS_URL"
14+ ],
415 "tasks": {
516 "build": {
617 "dependsOn": ["^build"],
718 "outputs": ["dist/**", "dist-electron/**"]
819 },
920 "dev": {
21+ "dependsOn": ["@acme/contracts#build"],
1022 "cache": false,
1123 "persistent": true
1224 },
@@ -17,11 +29,6 @@ · 4 unmodified lines
1729 "test": {
1830 "dependsOn": ["^build"],
1931 "outputs": []
20− },
21− "smoke-test": {
22− "dependsOn": ["build"],
23− "cache": false,
24− "outputs": []
2532 }
2633 }
2734 }

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t