diff --git a/.agents/docs/agent-adapters.md b/.agents/docs/agent-adapters.md index bcb04219..144baa7e 100644 --- a/.agents/docs/agent-adapters.md +++ b/.agents/docs/agent-adapters.md @@ -49,19 +49,21 @@ Every provider is a folder under `src/supervisor/agents//` with the same i Opening two provider folders side-by-side answers "what does this provider do differently" by file-name alignment alone. -Model/effort lists below are the **statically declared defaults**. Several providers ship `models: []` / `efforts: []` and fill them at runtime from a capabilities probe (Codex/Gemini/Copilot/Grok/OpenCode) — the listed values are illustrative, not authoritative. Read the provider's `detection.ts` for the live source of truth. - -| Provider | Models | Efforts | Live Input | Structured Session | -| ------------ | ------------------------------------------------------ | ---------------------------------------- | --------------------- | ---------------------- | -| Claude | opus-4-8, fable-5, opus-4-7, opus-4-6, sonnet, haiku | low, medium, high, xHigh, max, ultracode | terminal | No | -| Codex | (probed dynamically via app-server) | (probed dynamically) | terminal / GUI server | Yes (stdio app-server) | -| Gemini | (probed dynamically via ACP) | (probed dynamically) | terminal | No | -| Copilot | (probed via ACP) | (probed via ACP) | terminal | Yes (ACP) | -| Cursor | auto, composer-\*, GPT/Opus/Sonnet variants | (embedded in model name) | terminal | No | -| Grok | grok-build (probed via ACP) | (none) | terminal | Yes (ACP) | -| OpenCode | (probed dynamically via SDK) | (probed dynamically) | terminal / GUI server | Yes (SDK server) | -| Antigravity | auto (managed by `agy`) | (none) | terminal | No | -| Command Code | Kimi/Claude/GPT/Gemini/GLM/… (static, `--list-models`) | (none) | terminal | No | +Model/effort lists below are the **statically declared defaults**. Several providers ship `models: []` / `efforts: []` and fill them at runtime from a capabilities probe (Codex/Gemini/Copilot/Grok/OpenCode, and Cursor via its CLI `--list-models`) — the listed values are illustrative, not authoritative. Read the provider's `detection.ts` for the live source of truth. + +The **Structured Session** column reflects whether the adapter implements `createStructuredSession` (i.e. supports a `"gui"` presentation mode); it is not a model-list default and is authoritative. + +| Provider | Models | Efforts | Live Input | Structured Session | +| ------------ | ------------------------------------------------------------------------ | ---------------------------------------- | --------------------- | ---------------------- | +| Claude | opus-4-8, fable-5, opus-4-7, opus-4-6, sonnet, haiku | low, medium, high, xHigh, max, ultracode | terminal | Yes (SDK) | +| Codex | (probed dynamically via app-server) | (probed dynamically) | terminal / GUI server | Yes (stdio app-server) | +| Gemini | (probed dynamically via ACP) | (probed dynamically) | terminal | Yes (ACP) | +| Copilot | (probed via ACP) | (probed via ACP) | terminal | Yes (ACP) | +| Cursor | auto, composer-\*, GPT/Opus/Sonnet variants (probed via `--list-models`) | (embedded in model name) | terminal | Yes (ACP) | +| Grok | grok-build (probed via ACP) | (none) | terminal | Yes (ACP) | +| OpenCode | (probed dynamically via SDK) | (probed dynamically) | terminal / GUI server | Yes (SDK server) | +| Antigravity | auto (managed by `agy`) | (none) | terminal | No | +| Command Code | Kimi/Claude/GPT/Gemini/GLM/… (static, `--list-models`) | (none) | terminal | No | ## Adding a New Provider — Full Checklist @@ -116,7 +118,7 @@ forgotten. ### 3. Shared contracts -- [ ] `src/shared/contracts/agentInstance.ts` — add the kind to `KNOWN_AGENT_DRIVERS`. +- [ ] `src/shared/contracts/agentInstance.ts` — add the kind to `KNOWN_AGENT_DRIVERS`. (Note: this map currently has **no runtime readers** — the shipped Grok provider omits its entry with no observable effect — so today this is for completeness, not feature-gating.) ### 4. Renderer provider — `src/renderer/components/providers//` @@ -173,7 +175,7 @@ The codebase is provider-agnostic by design (targeting 5-10 providers). Each pro - WSL projects are detected via `ProjectLocation.kind === "wsl"`. - Commands are wrapped: `wsl.exe -d --cd -- `. - `batchWslCommandsAsync()` combines multiple commands into one `wsl.exe` invocation to avoid ~800-1000ms per-spawn overhead. -- Shell detection (`resolveWslShellPath`) is cached per distro with `/bin/sh` fallback. +- Shell detection (`resolveWslShellPath`) is cached per distro with a `/bin/bash` fallback (chosen over `/bin/sh` so rc files — nvm/fnm/asdf — still get sourced). - Agent install detection runs per-environment (Windows and each active WSL distro independently). ## Hook Runtime Resolution @@ -190,7 +192,7 @@ Three layers, in order of cost: 2. **Login-shell probe.** macOS GUI apps don't inherit the user's interactive PATH (no Homebrew, no nvm) — so on POSIX we spawn `$SHELL -lic` with sentinel markers (`__LC_NODE_PATH__:`, `__LC_NODE_VERSION__:`) to extract the user's `node` past any rc-file noise. On Windows, Electron inherits PATH from the registry already, so `where.exe node` is enough. If the binary version is ≥ `MIN_ACCEPTED_NODE_MAJOR`, that's our pick. 3. **Background install.** When 1 + 2 both miss, the resolver fires `installNativeRuntime` (download → SHA256-verify → `tar -xJf` for `.tar.xz` / `tar.exe -xf` for `.zip`) and immediately returns null. The current install pass falls back to `ELECTRON_RUN_AS_NODE=1`; next supervisor boot picks up the managed runtime via the fast path. -Result is memoized for the supervisor lifetime (one promise shared across all 5 providers) and cleared on restart. `resolveNativeNode` is the public entry point; `managedNodePath` is exported for tests. +Result is memoized for the supervisor lifetime (one promise per base dir, shared across whatever providers resolve against that dir) and cleared on restart. `resolveNativeNode` is the public entry point; `managedNodePath` is exported for tests. ### WSL — `src/supervisor/wsl/runtime/index.ts` diff --git a/.agents/docs/architecture.md b/.agents/docs/architecture.md index c90936be..cfe11146 100644 --- a/.agents/docs/architecture.md +++ b/.agents/docs/architecture.md @@ -3,15 +3,15 @@ ## Layers - `src/main/`: Electron shell (`main.ts`), context-isolated preload bridge (`preload.ts`), SQLite database (`db.ts`, `db.schema.ts` via Drizzle ORM + better-sqlite3). -- `src/supervisor/`: Forked Node process owning all agent PTY sessions, git operations, terminal log persistence, agent status caching, and commit message generation. Entry point: `index.ts` (IPC dispatcher with Zod-validated payloads). -- `src/renderer/`: React 19 + HeroUI v3 + xterm.js. Zustand stores for state, persisted to SQLite via the preload bridge. +- `src/supervisor/`: Forked Node process owning all agent PTY sessions, git operations, terminal log persistence, agent status caching, and commit message generation. Entry point: `index.ts` (IPC dispatcher that trusts pre-validated payloads — Zod validation runs caller-side in the main process before dispatch, not in the supervisor). +- `src/renderer/`: React 19 + HeroUI v3 + xterm.js. Zustand stores for state; a couple persist to SQLite via the preload bridge (`appStore`, `threadTodoDockStore`), several persist to `localStorage`, and many are ephemeral — see the State Management table for the per-store breakdown. - `src/shared/`: Zod schemas, TypeScript types, IPC contracts (`ipc.ts`), and pure helpers (ANSI stripping, WSL path utilities, worktree path computation, theme resolution, agent status filtering). ## IPC Architecture -The main process forks the supervisor as a child process. Communication is UUID-keyed request/reply over `process.send()` + `process.on("message")`. The supervisor also emits fire-and-forget events (thread output, state changes, agent statuses) that the main process forwards to the renderer via `ipcRenderer`. +The main process forks the supervisor as a child process. Communication is UUID-keyed request/reply over `process.send()` + `process.on("message")`. The supervisor also emits fire-and-forget events (thread output, state changes, agent statuses) that the main process forwards to the renderer via `webContents.send` (received in the renderer through `ipcRenderer.on`). -The preload bridge (`window.lightcode`) wraps all IPC into typed async methods defined by the `LightcodeBridge` interface in `src/shared/ipc.ts`. +The preload bridge (`window.lightcode`) wraps all IPC into typed async methods defined by the `LightcodeBridge` type, declared in `src/shared/ipc/bridge.ts` and re-exported via the `src/shared/ipc.ts` barrel. ## State Management @@ -59,7 +59,7 @@ Native modules (`node-pty`, `better-sqlite3`, `electron`) are excluded from bund ## Database -SQLite via Drizzle ORM (`src/main/db.ts`). Tables: `projects`, `threads`, `appState`, `threadRuntimeItems`, `threadContextUsage`, `threadCompletedTurns`. The renderer reads/writes through preload bridge methods (`dbGetProjects`, `dbUpsertThread`, `dbSyncAll`, etc.). Zustand persistence uses a custom `dbStorage` backend. +SQLite via Drizzle ORM (`src/main/db.ts`, `db.schema.ts`). Drizzle tables: `projects`, `threads`, `appState`, `projectNotes`, `threadRuntimeItems`, `threadContextUsage`, `threadCompletedTurns`. Plus `usage_events`, created via raw `CREATE TABLE` DDL in `db.ts` rather than the Drizzle schema. The renderer reads/writes through preload bridge methods (`dbGetProjects`, `dbUpsertThread`, `dbSyncAll`, etc.). Zustand persistence uses a custom `dbStorage` backend. ## Git Integration diff --git a/.agents/docs/editing-rules.md b/.agents/docs/editing-rules.md index a9e71e09..e6599970 100644 --- a/.agents/docs/editing-rules.md +++ b/.agents/docs/editing-rules.md @@ -71,8 +71,10 @@ const getProjectThreads = createArrayKeyedMap((threads: Thread[]) => buildProjectThreadsMap(threads, (t) => !t.archived), ); -// selector — O(1) after the first caller for a given array identity -const threads = getProjectThreads(allThreads).get(projectId) ?? EMPTY_THREADS; +// selector — O(1) after the first caller for a given array identity. +// The returned getter takes (array, key) and returns the value directly; +// the internal Map's .get is not exposed to callers. +const threads = getProjectThreads(allThreads, projectId) ?? EMPTY_THREADS; ``` Precedent: `state/derivations.ts` (`createArrayKeyedMap`), consumed by `hooks/uiSelectors.ts` and `state/gitSelectors.ts`. @@ -117,7 +119,7 @@ A component that renders a tab strip, a file tree, or a PR card often ends up su Fix it by splitting the component so each subcomponent subscribes only to what **it** renders: - `FileEditorPane` → `TabStripHeader` (tab list) + `EditorBody` (active buffer content). -- `ProjectTreeView` → `TreeChildren` (per-directory entries) + `TreeEntryRow` (per-path flags) + `TreeSearchBar` (debounced query). +- `ProjectTreeView` → `TreeChildren` (per-directory entries) + `TreeEntryRow` (per-path flags); the debounced search query is owned by the `useProjectTree` hook. - `GitReviewSidebar` → `PrSection(prKey)` + per-file `FileRow(path)`. The row component's props are the **identifier** (`path`, `key`, `id`) and maybe stable callbacks — never the entity payload. @@ -165,7 +167,7 @@ If a child needs a store value, the child reads it via a hook. Do not pass `sele ## Vite Configuration - Use Vite 8 Rolldown-native config (e.g. `rolldownOptions`) over older Rollup-first patterns. -- Manual chunks are defined for xterm, git-diff, ui (HeroUI + React Aria), framework (React + Zustand + Zod), and vendor. +- Manual chunks are defined for xterm, git-diff, monaco, shiki, ui (HeroUI + React Aria), framework (React + Zustand + Zod), and vendor. ## Cleanup diff --git a/.agents/docs/ui-patterns.md b/.agents/docs/ui-patterns.md index 3daaed7f..c348961b 100644 --- a/.agents/docs/ui-patterns.md +++ b/.agents/docs/ui-patterns.md @@ -36,7 +36,7 @@ Match the canonical dialog look — do not restyle. Reference: `CreatePrModal`, ## ACP Composer Behavior -- **Inline file mentions stay text-first, then serialize to structured segments.** `MentionInput` + `serializeMentions` accept raw `@path` tokens, so repo-relative references like `@.agents/docs/ui-patterns.md` become `{ kind: "file" }` prompt segments on submit without requiring a picker chip. +- **Inline file mentions stay text-first, then serialize to structured segments.** `MentionInput` + the `serializeMentions.ts` helpers (`serializeToSegments` / `flattenSegments`) accept raw `@path` tokens, so repo-relative references like `@.agents/docs/ui-patterns.md` become `{ kind: "file" }` prompt segments on submit without requiring a picker chip. - **ACP resource paths resolve from the active project root.** Relative file mentions and attachments are normalized before ACP conversion so Windows, WSL, spaces, and `file://` URIs stay valid. - **GUI ACP threads support steering via follow-up submits while working.** A new submit during `working` interrupts the active turn, queues the follow-up as structured `{ prompt, segments, config }`, and drains queued turns FIFO once the session returns to `idle` / `needs_reply`. - **Terminal presentation stays PTY-owned.** Do not add queueing or fake stop/steering UI to terminal-native threads; the terminal surface remains the source of truth there. @@ -94,8 +94,8 @@ Three modes: light, dark, system. Resolved via `useResolvedAppearance()` hook in ## Layout -- **AppShell**: Collapsible sidebar (220-500px, collapsed to 48px icon rail) + main content + optional right panel (320-700px). Drag-to-resize with localStorage persistence. -- **SplitPaneContainer**: Horizontal multi-pane for side-by-side threads (1-3 panes, equal initial distribution, 15% minimum each). +- **AppShell**: Collapsible sidebar (240-500px, collapsed to 48px icon rail) + main content + optional right panel (320-1100px). Drag-to-resize with localStorage persistence. +- **SplitPaneContainer**: Recursive horizontal/vertical multi-pane tree for side-by-side threads (unbounded pane count, equal initial distribution, 15% minimum each). - Resizable panels use **local state only** (not Zustand) to avoid resize lag. ## Module Loading diff --git a/README.md b/README.md index ec7e51bb..eab7d0fe 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

One window for all your AI coding agents.
- Run Claude, Codex, OpenCode, Gemini, Antigravity, Cursor, and Copilot side-by-side. Terminal and chat, any layout. + Run Claude, Codex, OpenCode, Gemini, Grok, Antigravity, Cursor, Command Code, and Copilot side-by-side. Terminal and chat, any layout.

@@ -25,7 +25,7 @@ ## Supported Agents -**Claude** · **Codex** · **OpenCode** · **Gemini** · **Antigravity** · **Cursor** · **Copilot** and any agent from the [ACP registry](https://agentclientprotocol.com). +**Claude** · **Codex** · **OpenCode** · **Gemini** · **Grok** · **Antigravity** · **Cursor** · **Command Code** · **Copilot** and any agent from the [ACP registry](https://agentclientprotocol.com). ## Why Lightcode? diff --git a/docs/superpowers/specs/2026-06-08-create-project-design.md b/docs/superpowers/specs/2026-06-08-create-project-design.md index cae63765..99f015ca 100644 --- a/docs/superpowers/specs/2026-06-08-create-project-design.md +++ b/docs/superpowers/specs/2026-06-08-create-project-design.md @@ -1,12 +1,13 @@ # Create Project — Design Date: 2026-06-08 +Status: **Implemented** — shipped in PR #149 (commit `5927b617`); the repo-clone flow was added later in PR #167. The sections below have been reconciled with the shipped code (see notes inline where the implementation diverged from the original design). ## Goal Implement a unified "create project" flow with: -- A `+` menu offering **Start from scratch** and **Use an existing folder**. +- A `+` menu offering **Start from scratch**, **Clone a repository**, and **Use an existing folder**. - A runtime picker for **Native / WSL** (WSL shown only when distros exist). - A folder picker whose default is preselected as **last-used → home**, scoped **per runtime**. @@ -26,31 +27,28 @@ Implement a unified "create project" flow with: - `ProjectLocation` (`src/shared/contracts/common.ts`): discriminated union `windows | wsl | posix`. - Helpers (`src/shared/wsl.ts`): `parseWslUncPath`, `getProjectName`, `toWslUncPath`, `getProjectFsPath`. - IPC: `pickFolder(defaultPath?)`, `listWslDistros()` already exist (`procedures/app.ts`, `localHandlers.ts`). -- Settings: JSON at `~/.lightcode/settings.json`; renderer store `sharedSettingsStore.ts`; schema `src/shared/settings.ts`. **No last-used directory persisted today.** **No create-directory IPC today.** +- Settings: JSON at `~/.lightcode/settings.json`; renderer store `sharedSettingsStore.ts`; schema `src/shared/settings.ts`. At design time: no last-used directory persisted, no create-directory IPC. -The screenshots' "Start from scratch" / "Use an existing folder" menu and "Name project" modal do **not** exist in code yet — they are the target. +This section captures the **pre-implementation baseline**. All of the targets below — the "Start from scratch" / "Use an existing folder" menu, the "Name project" modal, per-runtime last-used-dir persistence (`lastUsedProjectDirs`), and the `createProjectDirectory` IPC — have since shipped. ## Architecture ### Entry points -- Sidebar `+` (`SidebarHeaderControls`) and the `WelcomeOverlay` CTA both render `CreateProjectMenu` (two items). +- Sidebar `+` (`SidebarHeaderControls`) and the `WelcomeOverlay` CTA both render `CreateProjectMenu` (three items: Start from scratch / Clone a repository / Use an existing folder). - "Start from scratch" → `panelStore.openCreateProjectModal()` (the scratch modal, mounted once in `AppOverlays`). +- "Clone a repository" → `panelStore.openCloneProjectModal()` → `CloneProjectModal` (GitHub-account browse + paste-a-URL modes; `cloneRepo` IPC). Added after the original design — see Out of scope note. - "Use an existing folder" → `addExistingProject()` → native `pickFolder` → create project. No modal. ### `CreateProjectModal` (scratch only) -HeroUI `Modal` (mirrors `CreatePrModal`). Fields: +HeroUI `Modal` (mirrors `CreatePrModal`). The modal is **scratch-only** — there is no `mode` toggle and no "existing folder" variant (the existing-folder path bypasses this modal entirely, as above). Fields as shipped: - **Runtime selector** — visible only when WSL distros exist. Options: `Native` + one per distro. Hidden on macOS/Linux (runtime = `posix`). Changing it re-resolves the default location. -- **Location** (read-only path + **Browse** → `pickFolder(defaultPath)`): - - scratch: "Parent folder". - - existing: "Folder". +- **Location** (read-only path, label "Location", + **Browse** → `pickFolder(defaultPath)`): - Default on open / runtime change: `lastUsedProjectDirs[runtimeKey]` → else runtime home (`homeDir` native; `\\wsl.localhost\\home` for WSL). -- **Name** (text input, placeholder "New project"): - - scratch: required; legal single path segment. - - existing: prefilled with basename of picked folder; editable; display-name only. -- **Footer**: Cancel / Save. Save disabled until valid. Scratch shows a live preview of the final path. +- **Name** (text input, label "Project name", placeholder "New project"): required; legal single path segment. +- **Footer**: Cancel / **Create project**. The create button is disabled until valid; the modal shows a live preview of the final path. `runtimeKey` = `"native"` for windows/posix native, else the distro name. @@ -72,8 +70,8 @@ HeroUI `Modal` (mirrors `CreatePrModal`). Fields: ### State / components / files -- `panelStore`: `createProjectModal: { open, mode }` + `openCreateProject(mode)` / `closeCreateProject()`. -- New files: `CreateProjectMenu`, `CreateProjectModal`, `useCreateProjectFlow` controller (with pure `deriveLocationFromPath`, name validation, target-path build). +- `panelStore`: separate boolean flags `createProjectModalOpen` / `cloneProjectModalOpen`, each with no-arg openers/closers (`openCreateProjectModal()` / `closeCreateProjectModal()` / `openCloneProjectModal()` / `closeCloneProjectModal()`). There is no `{ open, mode }` object or `mode` argument — scratch and clone are distinct modals. +- New files: `CreateProjectMenu`, `CreateProjectModal`, `CloneProjectModal`, the flow module `src/renderer/actions/createProjectActions.ts`, and pure helpers in `src/shared/createProject.ts` (`deriveLocationFromPath`, `validateProjectName`, `buildScratchTargetPath`). (Implemented as an actions module + shared helpers, not the originally-proposed `useCreateProjectFlow` hook.) - Edits: `SidebarHeaderControls.tsx`, `WelcomeOverlay.tsx`, `settings.ts`, `sharedSettingsStore.ts`, preload + bridge type, IPC procedure map + handler, `AppOverlays`. ## Testing (TDD) @@ -85,6 +83,6 @@ HeroUI `Modal` (mirrors `CreatePrModal`). Fields: ## Out of scope -- Cloning a repo from a URL. +- ~~Cloning a repo from a URL.~~ — was out of scope at design time, but later **shipped** (`CloneProjectModal` with GitHub + URL modes, `cloneRepo` IPC → `git clone` / `gh repo clone`). - Multi-folder / monorepo workspace projects. - Renaming/migrating existing projects' runtimes. diff --git a/docs/superpowers/specs/2026-06-08-notes-panel-design.md b/docs/superpowers/specs/2026-06-08-notes-panel-design.md index c919a398..2bc863e5 100644 --- a/docs/superpowers/specs/2026-06-08-notes-panel-design.md +++ b/docs/superpowers/specs/2026-06-08-notes-panel-design.md @@ -1,6 +1,7 @@ # Notes & To-dos Panel — Design **Date:** 2026-06-08 +**Status:** **Implemented** (shipped in PR #151). Reconciled with the shipped code below. **Goal:** Add a per-project Notes/To-dos panel to the right panel (iCloud Notes–style), reusing a popular editor library, with the ability to spin up a new thread from a to-do item or from selected note text. @@ -54,22 +55,25 @@ edited note docs don't rewrite the entire projects+threads payload on each keyst Standalone Zustand store (NOT persisted through the app-store middleware). Per-project cache `{ status, doc, todos }`. Actions: `ensureLoaded`, `setDoc`, `addTodo`, `toggleTodo`, -`updateTodoText`, `removeTodo`, `reorderTodos`, `flush`, `flushAll`. Mutations schedule a +`updateTodoText`, `removeTodo`, `moveTodo(projectId, fromIndex, toIndex)`, `flush`, `flushAll`. Mutations schedule a ~600ms debounced `dbSetProjectNotes`; `flush`/`flushAll` (panel close, beforeunload) write immediately. Missing-bridge (tests) is handled gracefully — in-memory only. ## New-thread seeding — `src/renderer/actions/notesActions.ts` -`newThreadFromText(projectId, text)`: +`newThreadFromText(projectId, text)` (shipped as two steps — the originally-planned +"merge into draft text" step was dropped; preservation of already-typed text is handled +downstream by caret insertion instead): -1. Merge `text` into the project's draft text (append to any in-progress prompt; preserve - file/attachment segments). -2. Push a one-shot **composer seed** (`draftSlice.setComposerSeed`) so the draft composer - applies it whether it mounts fresh or is already open. -3. `openNewThread(projectId)` (respects side-by-side `newThreadMode`). +1. Push a one-shot **composer seed** (`draftSlice.setComposerSeed`) carrying the trimmed + text, so the draft composer applies it whether it mounts fresh or is already open. The + seed is kept separate from `draftContents` (it does not touch the stored draft). +2. `openNewThread(projectId)` (respects side-by-side `newThreadMode`). `ThreadDraftComposerArea` consumes a pending seed on mount and on nonce change via the -existing `MentionInput` ref (`restoreFromSegments`), then clears it. +existing `MentionInput` ref (`insertText`, which inserts at the caret so anything the user +already typed is preserved), then clears it. (`restoreFromSegments` is a separate path used +to restore a previously saved draft, not the seed.) ## UI @@ -96,8 +100,9 @@ existing `MentionInput` ref (`restoreFromSegments`), then clears it. ## Styling ProseMirror/editor styles (placeholder, list spacing, focus) added to -`src/renderer/styles.css`, scoped to a `.lc-notes-editor` wrapper using existing design -tokens (`--foreground`, `--muted`, `--border`, `--accent`). +`src/renderer/styles.css`, scoped to the `.lc-notes-prose` contenteditable root using existing +design tokens (`--foreground`, `--muted`, `--border`, `--accent`). (`.lc-notes-editor` is only +a wrapper `div` className with no styles attached.) ## Testing diff --git a/package.json b/package.json index 213b97cb..d7af56e3 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "postinstall": "electron-rebuild --only better-sqlite3 && node scripts/ensure-native-deps.mjs", "setup:native": "electron-rebuild --only better-sqlite3 && node scripts/ensure-native-deps.mjs", "dev": "concurrently -k -n renderer,electron,app -c cyan,magenta,yellow \"pnpm run dev:renderer\" \"pnpm run dev:electron\" \"pnpm run dev:app\"", + "devtools": "node scripts/dev-react-devtools.mjs", + "dev:devtools": "cross-env LIGHTCODE_REACT_DEVTOOLS=1 concurrently -n react-devtools,app -c green,gray \"pnpm run devtools\" \"pnpm run dev\"", "website:dev": "pnpm --dir website dev", "website:build": "pnpm --dir website build", "website:start": "pnpm --dir website start", @@ -133,6 +135,7 @@ "oxlint": "^1.62.0", "oxlint-tsgolint": "^0.22.1", "postcss": "^8.5.13", + "react-devtools": "^7.0.1", "tsdown": "^0.21.10", "typescript": "^6.0.3", "vite": "^8.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40bd12f6..f57e121e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -264,6 +264,9 @@ importers: postcss: specifier: ^8.5.13 version: 8.5.13 + react-devtools: + specifier: ^7.0.1 + version: 7.0.1 tsdown: specifier: ^0.21.10 version: 0.21.10(@typescript/native-preview@7.0.0-dev.20260501.1)(typescript@6.0.3) @@ -2829,6 +2832,9 @@ packages: '@types/mysql@2.15.27': resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + '@types/node@16.18.126': + resolution: {integrity: sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==} + '@types/node@24.12.2': resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} @@ -3079,10 +3085,17 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-align@2.0.0: + resolution: {integrity: sha512-TdlOggdA/zURfMYa7ABC66j+oqfMew58KpJMbUlH3bcZP1b+cBHIHDDn5uH9INsxrHBPjsqM0tDB4jPTF/vgJA==} + ansi-escapes@7.3.0: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} + ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -3091,6 +3104,10 @@ packages: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -3209,6 +3226,10 @@ packages: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + boxen@1.3.0: + resolution: {integrity: sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==} + engines: {node: '>=4'} + brace-expansion@1.1.13: resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} @@ -3268,9 +3289,17 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + camelcase@4.1.0: + resolution: {integrity: sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==} + engines: {node: '>=4'} + caniuse-lite@1.0.30001787: resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} + capture-stack-trace@1.0.2: + resolution: {integrity: sha512-X/WM2UQs6VMHUtjUDnZTRI+i1crWteJySFzr9UpGoQa4WQffXVTTXuekjl7TjZRlcF2XfjgITT0HxZ9RnxeT0w==} + engines: {node: '>=0.10.0'} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -3278,6 +3307,10 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + chalk@3.0.0: resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} engines: {node: '>=8'} @@ -3320,6 +3353,9 @@ packages: chromium-pickle-js@0.2.0: resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + ci-info@1.6.0: + resolution: {integrity: sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==} + ci-info@4.3.1: resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} @@ -3331,6 +3367,10 @@ packages: cjs-module-lexer@2.2.0: resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + cli-boxes@1.0.0: + resolution: {integrity: sha512-3Fo5wu8Ytle8q9iCzS4D2MWVL2X7JVWRiS1BnXbTFDhS9c/REkM9vd1AmabsoZoY5/dGi5TT9iKL8Kb6DeBRQg==} + engines: {node: '>=0.10.0'} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -3357,10 +3397,16 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -3399,6 +3445,10 @@ packages: engines: {node: '>=18'} hasBin: true + configstore@3.1.5: + resolution: {integrity: sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==} + engines: {node: '>=4'} + content-disposition@1.1.0: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} @@ -3438,6 +3488,10 @@ packages: crc@3.8.0: resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} + create-error-class@3.0.2: + resolution: {integrity: sha512-gYTKKexFO3kh200H1Nit76sRwRtOY32vQd3jpAQKpLtZqyNsSQNfI4N7o3eP2wUjV35pTWKRYqFUDBvUha/Pkw==} + engines: {node: '>=0.10.0'} + cross-dirname@0.1.0: resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} @@ -3446,10 +3500,17 @@ packages: engines: {node: '>=20'} hasBin: true + cross-spawn@5.1.0: + resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-random-string@1.0.0: + resolution: {integrity: sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==} + engines: {node: '>=4'} + css-tree@3.2.1: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} @@ -3650,6 +3711,10 @@ packages: resolution: {integrity: sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A==} engines: {node: '>=0.10.0'} + default-gateway@6.0.3: + resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==} + engines: {node: '>= 10'} + defer-to-connect@2.0.1: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} engines: {node: '>=10'} @@ -3714,6 +3779,10 @@ packages: dompurify@3.4.10: resolution: {integrity: sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==} + dot-prop@4.2.1: + resolution: {integrity: sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==} + engines: {node: '>=4'} + dotenv-expand@11.0.7: resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} engines: {node: '>=12'} @@ -3855,6 +3924,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer3@0.1.5: + resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -3884,6 +3956,11 @@ packages: resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} engines: {node: '>=8.0.0'} + electron@23.3.13: + resolution: {integrity: sha512-BaXtHEb+KYKLouUXlUVDa/lj9pj4F5kiE0kwFdJV84Y2EU7euIDgPthfKtchhr5MVHmjtavRMIV/zAwEiSQ9rQ==} + engines: {node: '>= 12.20.55'} + hasBin: true + electron@41.7.0: resolution: {integrity: sha512-U6KAKivjk6YQ0Z5+eloJBjwhbHRE206gvy1UBMw2bSluWtMh5waeXMvX6AT/Ujm5ymYXVJOp7g9N7vOFw16wBQ==} engines: {node: '>= 12.20.55'} @@ -3972,6 +4049,10 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -4008,6 +4089,14 @@ packages: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} + execa@0.7.0: + resolution: {integrity: sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==} + engines: {node: '>=4'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -4182,10 +4271,18 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@3.0.0: + resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} + engines: {node: '>=4'} + get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + get-tsconfig@4.13.7: resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} @@ -4200,6 +4297,10 @@ packages: resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} engines: {node: '>=10.0'} + global-dirs@0.1.1: + resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} + engines: {node: '>=4'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -4212,6 +4313,10 @@ packages: resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} engines: {node: '>=10.19.0'} + got@6.7.1: + resolution: {integrity: sha512-Y/K3EDuiQN9rTZhBvPRWMLXIKdeD1Rj0nzunfoi0Yyn5WBEbzxXKU9Ub2X41oZBagVWOBU3MuDonFMgPWQFnwg==} + engines: {node: '>=4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -4221,6 +4326,10 @@ packages: hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -4319,6 +4428,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -4351,6 +4464,10 @@ packages: resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==} engines: {node: '>=18'} + import-lazy@2.1.0: + resolution: {integrity: sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==} + engines: {node: '>=4'} + import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} @@ -4358,6 +4475,10 @@ packages: resolution: {integrity: sha512-bDxwDdF04gm550DfZHgffvlX+9kUlcz32UD0AeBTmVPFiWkrexF2XVmiuFFbDhiFuP8fQkrkvI2KdSNPYWAXkQ==} engines: {node: '>=20.19.0'} + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -4381,6 +4502,10 @@ packages: react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + internal-ip@6.2.0: + resolution: {integrity: sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==} + engines: {node: '>=10'} + internmap@1.0.1: resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} @@ -4395,6 +4520,10 @@ packages: resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} engines: {node: '>= 12'} + ip-regex@4.3.0: + resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} + engines: {node: '>=8'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -4408,9 +4537,17 @@ packages: is-buffer@1.1.6: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + is-ci@1.2.1: + resolution: {integrity: sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==} + hasBin: true + is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -4422,10 +4559,30 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-installed-globally@0.1.0: + resolution: {integrity: sha512-ERNhMg+i/XgDwPIPF3u24qpajVreaiSuvpb1Uu0jugw7KKcxGyCX8cgp8P5fwTmAuXku6beDHHECdKArjlg7tw==} + engines: {node: '>=4'} + + is-ip@3.1.0: + resolution: {integrity: sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==} + engines: {node: '>=8'} + + is-npm@1.0.0: + resolution: {integrity: sha512-9r39FIr3d+KD9SbX0sfMsHzb5PP3uimOiwr3YupUaUFG4W0l1U57Rx3utpttV7qz5U3jmrO5auUa04LU9pyHsg==} + engines: {node: '>=0.10.0'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + + is-path-inside@1.0.1: + resolution: {integrity: sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g==} + engines: {node: '>=0.10.0'} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -4436,6 +4593,22 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-redirect@1.0.0: + resolution: {integrity: sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==} + engines: {node: '>=0.10.0'} + + is-retry-allowed@1.2.0: + resolution: {integrity: sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==} + engines: {node: '>=0.10.0'} + + is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + isbinaryfile@4.0.10: resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} engines: {node: '>= 8.0.0'} @@ -4546,6 +4719,10 @@ packages: resolution: {integrity: sha512-sOPIi4hISFnY7twwV97ca1TsxpBtXq0URu/LL1AvxwccPG/RIBBlKS7a/f/EL6w8lTNaS0EFs/F+IdSOaqYpng==} engines: {node: '>=20.10.0', npm: '>=10.2.3'} + latest-version@3.1.0: + resolution: {integrity: sha512-Be1YRHWWlZaSsrz2U+VInk+tO0EwLIyV+23RhWLINJYwg/UIikxjlj3MhH37/6/EDCAusjajvMkMMUXRaMWl/w==} + engines: {node: '>=4'} + layout-base@1.0.2: resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} @@ -4671,6 +4848,10 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lowercase-keys@1.0.1: + resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} + engines: {node: '>=0.10.0'} + lowercase-keys@2.0.0: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} @@ -4682,6 +4863,9 @@ packages: resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} engines: {node: 20 || >=22} + lru-cache@4.1.5: + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -4706,6 +4890,10 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-dir@1.3.0: + resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==} + engines: {node: '>=4'} + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -4792,6 +4980,9 @@ packages: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + mermaid@11.14.0: resolution: {integrity: sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==} @@ -4904,6 +5095,10 @@ packages: engines: {node: '>=4.0.0'} hasBin: true + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -5047,6 +5242,14 @@ packages: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} + npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -5069,6 +5272,10 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -5118,10 +5325,26 @@ packages: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} + p-event@4.2.0: + resolution: {integrity: sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==} + engines: {node: '>=8'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + package-json@4.0.1: + resolution: {integrity: sha512-q/R5GrMek0vzgoomq6rm9OX+3PQve8sLwTirmK30YB3Cu0Bbt9OX9M/SIUnroN5BGJkzwGsFwDaRGD9EwBOlCA==} + engines: {node: '>=4'} + package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} @@ -5145,6 +5368,13 @@ packages: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} + path-is-inside@1.0.2: + resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} + + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -5184,6 +5414,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} @@ -5244,6 +5478,10 @@ packages: deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true + prepend-http@1.0.4: + resolution: {integrity: sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==} + engines: {node: '>=0.10.0'} + pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -5323,6 +5561,9 @@ packages: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} + pseudomap@1.0.2: + resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -5365,6 +5606,13 @@ packages: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-devtools-core@7.0.1: + resolution: {integrity: sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw==} + + react-devtools@7.0.1: + resolution: {integrity: sha512-I2UXoJlsqNeN3uCrlrJw0V+K6HTHU5Q1x+BWJlF99hBC6A/lKhe+IuITZXyKb8BluMReSBYUhUYGkJGgr2KPQQ==} + hasBin: true + react-dom@19.2.5: resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} peerDependencies: @@ -5432,6 +5680,13 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + registry-auth-token@3.4.0: + resolution: {integrity: sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==} + + registry-url@3.1.0: + resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==} + engines: {node: '>=0.10.0'} + rehype-harden@1.1.8: resolution: {integrity: sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw==} @@ -5587,6 +5842,10 @@ packages: semver-compare@1.0.0: resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + semver-diff@2.1.0: + resolution: {integrity: sha512-gL8F8L4ORwsS0+iQ34yCYv///jsOq0ZL7WP55d1HnJ32o7tyFYEFQZQA22mrLIacZdU6xecaBBZ+uEiffGNyXw==} + engines: {node: '>=0.10.0'} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -5619,10 +5878,18 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} + shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} @@ -5731,6 +5998,10 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} + string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -5749,6 +6020,10 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -5757,6 +6032,14 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -5791,6 +6074,10 @@ packages: resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} engines: {node: '>= 8.0'} + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -5850,6 +6137,14 @@ packages: resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} engines: {node: '>=6.0.0'} + term-size@1.2.0: + resolution: {integrity: sha512-7dPUZQGy/+m3/wjVz3ZW5dobSoD/02NxJpoXUX0WIyjfVS3l0c+b/+9phIDFA7FHzkYtwtMFgeGZ/Y8jVTeqQQ==} + engines: {node: '>=4'} + + timed-out@4.0.1: + resolution: {integrity: sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==} + engines: {node: '>=0.10.0'} + tiny-async-pool@1.3.0: resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} @@ -6006,6 +6301,10 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unique-string@1.0.0: + resolution: {integrity: sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==} + engines: {node: '>=4'} + unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -6047,15 +6346,27 @@ packages: synckit: optional: true + unzip-response@2.0.1: + resolution: {integrity: sha512-N0XH6lqDtFH84JxptQoZYmloF4nzrQqqrAymNj+/gW60AO2AZgOcf4O/nUXJcYfyQkqvMo9lSupBZmmgvuVXlw==} + engines: {node: '>=4'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' + update-notifier@2.5.0: + resolution: {integrity: sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==} + engines: {node: '>=4'} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-parse-lax@1.0.0: + resolution: {integrity: sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==} + engines: {node: '>=0.10.0'} + use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -6227,6 +6538,10 @@ packages: resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -6247,6 +6562,10 @@ packages: engines: {node: '>=8'} hasBin: true + widest-line@2.0.1: + resolution: {integrity: sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==} + engines: {node: '>=4'} + wrap-ansi@10.0.0: resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} engines: {node: '>=20'} @@ -6262,6 +6581,25 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + write-file-atomic@2.4.3: + resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} + + ws@7.5.11: + resolution: {integrity: sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xdg-basedir@3.0.0: + resolution: {integrity: sha512-1Dly4xqlulvPD3fZUQJLY+FUIeqN3N2MM3uqe4rCJftAvOjFa3jFGfctOgluGx4ahPbUCsZkmJILiP0Vi4T6lQ==} + engines: {node: '>=4'} + xml-lexer@0.2.2: resolution: {integrity: sha512-G0i98epIwiUEiKmMcavmVdhtymW+pCAohMRgybyIME9ygfVu8QheIi+YoQh3ngiThsT0SQzJT4R0sKDEv8Ou0w==} @@ -6287,6 +6625,9 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yallist@2.1.2: + resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -8871,6 +9212,8 @@ snapshots: dependencies: '@types/node': 25.9.1 + '@types/node@16.18.126': {} + '@types/node@24.12.2': dependencies: undici-types: 7.16.0 @@ -9114,14 +9457,24 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-align@2.0.0: + dependencies: + string-width: 2.1.1 + ansi-escapes@7.3.0: dependencies: environment: 1.1.0 + ansi-regex@3.0.1: {} + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -9272,6 +9625,16 @@ snapshots: boolean@3.2.0: {} + boxen@1.3.0: + dependencies: + ansi-align: 2.0.0 + camelcase: 4.1.0 + chalk: 2.4.2 + cli-boxes: 1.0.0 + string-width: 2.1.1 + term-size: 1.2.0 + widest-line: 2.0.1 + brace-expansion@1.1.13: dependencies: balanced-match: 1.0.2 @@ -9360,12 +9723,22 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + camelcase@4.1.0: {} + caniuse-lite@1.0.30001787: {} + capture-stack-trace@1.0.2: {} + ccount@2.0.1: {} chai@6.2.2: {} + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + chalk@3.0.0: dependencies: ansi-styles: 4.3.0 @@ -9405,12 +9778,16 @@ snapshots: chromium-pickle-js@0.2.0: {} + ci-info@1.6.0: {} + ci-info@4.3.1: {} ci-info@4.4.0: {} cjs-module-lexer@2.2.0: {} + cli-boxes@1.0.0: {} + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -9440,10 +9817,16 @@ snapshots: clsx@2.1.1: {} + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + color-convert@2.0.1: dependencies: color-name: 1.1.4 + color-name@1.1.3: {} + color-name@1.1.4: {} combined-stream@1.0.8: @@ -9474,6 +9857,15 @@ snapshots: tree-kill: 1.2.2 yargs: 17.7.2 + configstore@3.1.5: + dependencies: + dot-prop: 4.2.1 + graceful-fs: 4.2.11 + make-dir: 1.3.0 + unique-string: 1.0.0 + write-file-atomic: 2.4.3 + xdg-basedir: 3.0.0 + content-disposition@1.1.0: {} content-type@1.0.5: {} @@ -9507,6 +9899,10 @@ snapshots: buffer: 5.7.1 optional: true + create-error-class@3.0.2: + dependencies: + capture-stack-trace: 1.0.2 + cross-dirname@0.1.0: optional: true @@ -9515,12 +9911,20 @@ snapshots: '@epic-web/invariant': 1.0.0 cross-spawn: 7.0.6 + cross-spawn@5.1.0: + dependencies: + lru-cache: 4.1.5 + shebang-command: 1.2.0 + which: 1.3.1 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + crypto-random-string@1.0.0: {} + css-tree@3.2.1: dependencies: mdn-data: 2.27.1 @@ -9744,6 +10148,10 @@ snapshots: kind-of: 3.2.2 rename-keys: 1.2.0 + default-gateway@6.0.3: + dependencies: + execa: 5.1.1 + defer-to-connect@2.0.1: {} define-data-property@1.1.4: @@ -9821,6 +10229,10 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 + dot-prop@4.2.1: + dependencies: + is-obj: 1.0.1 + dotenv-expand@11.0.7: dependencies: dotenv: 16.6.1 @@ -9851,6 +10263,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer3@0.1.5: {} + ee-first@1.1.1: {} ejs@3.1.10: @@ -9922,6 +10336,14 @@ snapshots: transitivePeerDependencies: - supports-color + electron@23.3.13: + dependencies: + '@electron/get': 2.0.3 + '@types/node': 16.18.126 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + electron@41.7.0: dependencies: '@electron/get': 2.0.3 @@ -10021,6 +10443,8 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} + escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} @@ -10045,6 +10469,28 @@ snapshots: dependencies: eventsource-parser: 3.1.0 + execa@0.7.0: + dependencies: + cross-spawn: 5.1.0 + get-stream: 3.0.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + expand-template@2.0.3: {} expect-type@1.3.0: {} @@ -10241,10 +10687,14 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stream@3.0.0: {} + get-stream@5.2.0: dependencies: pump: 3.0.4 + get-stream@6.0.1: {} + get-tsconfig@4.13.7: dependencies: resolve-pkg-maps: 1.0.0 @@ -10269,6 +10719,10 @@ snapshots: semver: 7.7.4 serialize-error: 7.0.1 + global-dirs@0.1.1: + dependencies: + ini: 1.3.8 + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -10290,12 +10744,30 @@ snapshots: p-cancelable: 2.1.1 responselike: 2.0.1 + got@6.7.1: + dependencies: + '@types/keyv': 3.1.4 + '@types/responselike': 1.0.3 + create-error-class: 3.0.2 + duplexer3: 0.1.5 + get-stream: 3.0.0 + is-redirect: 1.0.0 + is-retry-allowed: 1.2.0 + is-stream: 1.1.0 + lowercase-keys: 1.0.1 + safe-buffer: 5.2.1 + timed-out: 4.0.1 + unzip-response: 2.0.1 + url-parse-lax: 1.0.0 + graceful-fs@4.2.11: {} guid-typescript@1.0.9: {} hachure-fill@0.5.2: {} + has-flag@3.0.0: {} + has-flag@4.0.0: {} has-property-descriptors@1.0.2: @@ -10465,6 +10937,8 @@ snapshots: transitivePeerDependencies: - supports-color + human-signals@2.1.0: {} + husky@9.1.7: {} iconv-corefoundation@1.1.7: @@ -10501,10 +10975,14 @@ snapshots: cjs-module-lexer: 2.2.0 module-details-from-path: 1.0.4 + import-lazy@2.1.0: {} + import-meta-resolve@4.2.0: {} import-without-cache@0.3.3: {} + imurmurhash@0.1.4: {} + indent-string@4.0.0: {} inflight@1.0.6: @@ -10528,6 +11006,13 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) + internal-ip@6.2.0: + dependencies: + default-gateway: 6.0.3 + ipaddr.js: 1.9.1 + is-ip: 3.1.0 + p-event: 4.2.0 + internmap@1.0.1: {} internmap@2.0.3: {} @@ -10541,6 +11026,8 @@ snapshots: ip-address@10.2.0: {} + ip-regex@4.3.0: {} + ipaddr.js@1.9.1: {} is-alphabetical@2.0.1: {} @@ -10552,8 +11039,14 @@ snapshots: is-buffer@1.1.6: {} + is-ci@1.2.1: + dependencies: + ci-info: 1.6.0 + is-decimal@2.0.1: {} + is-fullwidth-code-point@2.0.0: {} + is-fullwidth-code-point@3.0.0: {} is-fullwidth-code-point@5.1.0: @@ -10562,14 +11055,39 @@ snapshots: is-hexadecimal@2.0.1: {} + is-installed-globally@0.1.0: + dependencies: + global-dirs: 0.1.1 + is-path-inside: 1.0.1 + + is-ip@3.1.0: + dependencies: + ip-regex: 4.3.0 + + is-npm@1.0.0: {} + is-number@7.0.0: {} + is-obj@1.0.1: {} + + is-path-inside@1.0.1: + dependencies: + path-is-inside: 1.0.2 + is-plain-obj@4.1.0: {} is-potential-custom-element-name@1.0.1: {} is-promise@4.0.0: {} + is-redirect@1.0.0: {} + + is-retry-allowed@1.2.0: {} + + is-stream@1.1.0: {} + + is-stream@2.0.1: {} + isbinaryfile@4.0.10: {} isbinaryfile@5.0.7: {} @@ -10693,6 +11211,10 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 + latest-version@3.1.0: + dependencies: + package-json: 4.0.1 + layout-base@1.0.2: {} layout-base@2.0.1: {} @@ -10793,6 +11315,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + lowercase-keys@1.0.1: {} + lowercase-keys@2.0.0: {} lowlight@3.3.0: @@ -10803,6 +11327,11 @@ snapshots: lru-cache@11.3.5: {} + lru-cache@4.1.5: + dependencies: + pseudomap: 1.0.2 + yallist: 2.1.2 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -10825,6 +11354,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@1.3.0: + dependencies: + pify: 3.0.0 + markdown-table@3.0.4: {} marked@14.0.0: {} @@ -11005,6 +11538,8 @@ snapshots: merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} + mermaid@11.14.0: dependencies: '@braintree/sanitize-url': 7.1.2 @@ -11239,6 +11774,8 @@ snapshots: mime@2.6.0: {} + mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} mimic-response@1.0.1: {} @@ -11372,6 +11909,14 @@ snapshots: normalize-url@6.1.0: {} + npm-run-path@2.0.2: + dependencies: + path-key: 2.0.1 + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -11388,6 +11933,10 @@ snapshots: dependencies: wrappy: 1.0.2 + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -11479,10 +12028,27 @@ snapshots: p-cancelable@2.1.1: {} + p-event@4.2.0: + dependencies: + p-timeout: 3.2.0 + + p-finally@1.0.0: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + package-json@4.0.1: + dependencies: + got: 6.7.1 + registry-auth-token: 3.4.0 + registry-url: 3.1.0 + semver: 5.7.2 + package-manager-detector@1.6.0: {} parse-entities@4.0.2: @@ -11509,6 +12075,10 @@ snapshots: path-is-absolute@1.0.1: {} + path-is-inside@1.0.2: {} + + path-key@2.0.1: {} + path-key@3.1.1: {} path-to-regexp@8.4.2: {} @@ -11537,6 +12107,8 @@ snapshots: picomatch@4.0.4: {} + pify@3.0.0: {} + pify@4.0.1: {} pkce-challenge@5.0.1: {} @@ -11604,6 +12176,8 @@ snapshots: tar-fs: 2.1.4 tunnel-agent: 0.6.0 + prepend-http@1.0.4: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -11730,6 +12304,8 @@ snapshots: proxy-from-env@2.1.0: {} + pseudomap@1.0.2: {} + pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -11811,6 +12387,27 @@ snapshots: react-stately: 3.46.0(react@19.2.7) use-sync-external-store: 1.6.0(react@19.2.7) + react-devtools-core@7.0.1: + dependencies: + shell-quote: 1.8.4 + ws: 7.5.11 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + react-devtools@7.0.1: + dependencies: + cross-spawn: 5.1.0 + electron: 23.3.13 + internal-ip: 6.2.0 + minimist: 1.2.8 + react-devtools-core: 7.0.1 + update-notifier: 2.5.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + react-dom@19.2.5(react@19.2.5): dependencies: react: 19.2.5 @@ -11919,6 +12516,15 @@ snapshots: dependencies: regex-utilities: 2.3.0 + registry-auth-token@3.4.0: + dependencies: + rc: 1.2.8 + safe-buffer: 5.2.1 + + registry-url@3.1.0: + dependencies: + rc: 1.2.8 + rehype-harden@1.1.8: dependencies: unist-util-visit: 5.1.0 @@ -12129,6 +12735,10 @@ snapshots: semver-compare@1.0.0: {} + semver-diff@2.1.0: + dependencies: + semver: 5.7.2 + semver@5.7.2: {} semver@6.3.1: {} @@ -12197,10 +12807,16 @@ snapshots: '@img/sharp-win32-ia32': 0.34.5 '@img/sharp-win32-x64': 0.34.5 + shebang-command@1.2.0: + dependencies: + shebang-regex: 1.0.0 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 + shebang-regex@1.0.0: {} + shebang-regex@3.0.0: {} shell-quote@1.8.4: {} @@ -12330,6 +12946,11 @@ snapshots: string-argv@0.3.2: {} + string-width@2.1.1: + dependencies: + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -12356,6 +12977,10 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 + strip-ansi@4.0.0: + dependencies: + ansi-regex: 3.0.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -12364,6 +12989,10 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-eof@1.0.0: {} + + strip-final-newline@2.0.0: {} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -12393,6 +13022,10 @@ snapshots: transitivePeerDependencies: - supports-color + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -12463,6 +13096,12 @@ snapshots: mkdirp: 0.5.6 rimraf: 2.6.3 + term-size@1.2.0: + dependencies: + execa: 0.7.0 + + timed-out@4.0.1: {} + tiny-async-pool@1.3.0: dependencies: semver: 5.7.2 @@ -12599,6 +13238,10 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 + unique-string@1.0.0: + dependencies: + crypto-random-string: 1.0.0 + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -12636,16 +13279,35 @@ snapshots: dependencies: rolldown: 1.0.0-rc.17 + unzip-response@2.0.1: {} + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.1 + update-notifier@2.5.0: + dependencies: + boxen: 1.3.0 + chalk: 2.4.2 + configstore: 3.1.5 + import-lazy: 2.1.0 + is-ci: 1.2.1 + is-installed-globally: 0.1.0 + is-npm: 1.0.0 + latest-version: 3.1.0 + semver-diff: 2.1.0 + xdg-basedir: 3.0.0 + uri-js@4.4.1: dependencies: punycode: 2.3.1 + url-parse-lax@1.0.0: + dependencies: + prepend-http: 1.0.4 + use-sync-external-store@1.6.0(react@19.2.5): dependencies: react: 19.2.5 @@ -12784,6 +13446,10 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + which@1.3.1: + dependencies: + isexe: 2.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -12801,6 +13467,10 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + widest-line@2.0.1: + dependencies: + string-width: 2.1.1 + wrap-ansi@10.0.0: dependencies: ansi-styles: 6.2.3 @@ -12821,6 +13491,16 @@ snapshots: wrappy@1.0.2: {} + write-file-atomic@2.4.3: + dependencies: + graceful-fs: 4.2.11 + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + ws@7.5.11: {} + + xdg-basedir@3.0.0: {} + xml-lexer@0.2.2: dependencies: eventemitter3: 2.0.3 @@ -12840,6 +13520,8 @@ snapshots: y18n@5.0.8: {} + yallist@2.1.2: {} + yallist@3.1.1: {} yallist@4.0.0: {} diff --git a/scripts/dev-react-devtools.mjs b/scripts/dev-react-devtools.mjs new file mode 100644 index 00000000..201d2e34 --- /dev/null +++ b/scripts/dev-react-devtools.mjs @@ -0,0 +1,11 @@ +// react-devtools spawns its own bundled Electron. Electron-based hosts +// (VS Code, the Lightcode app itself, Claude Code) set ELECTRON_RUN_AS_NODE=1 +// in their child processes. If that leaks into our dev shell, react-devtools' +// Electron starts as plain Node and `require("electron").app` is undefined, so +// it crashes at startup (app.js:17, "Cannot read properties of undefined"). +// Delete the variable before spawning, mirroring scripts/dev-launch.mjs. +delete process.env.ELECTRON_RUN_AS_NODE; + +import { execSync } from "node:child_process"; + +execSync("react-devtools", { stdio: "inherit" }); diff --git a/src/renderer/components/providers/ProviderIcon.tsx b/src/renderer/components/providers/ProviderIcon.tsx index d6b2b2c4..deaa3cb3 100644 --- a/src/renderer/components/providers/ProviderIcon.tsx +++ b/src/renderer/components/providers/ProviderIcon.tsx @@ -1,6 +1,7 @@ import type { CSSProperties, ReactNode } from "react"; import { baseAgentKind } from "@/shared/contracts"; import type { StatusTone } from "./statusTone"; +import { syncMaskScanPhase } from "./syncMaskScanPhase"; import { getUtilityTaskCandidates, getUtilityTaskDefaultsHint, @@ -61,6 +62,7 @@ function ExternalProviderIcon(props: { src: string; tone: StatusTone; className? /> {props.tone === "working" ? ( diff --git a/src/renderer/components/providers/StatusIcon.tsx b/src/renderer/components/providers/StatusIcon.tsx index 3bda239b..8a9765a7 100644 --- a/src/renderer/components/providers/StatusIcon.tsx +++ b/src/renderer/components/providers/StatusIcon.tsx @@ -1,4 +1,5 @@ import type { StatusTone } from "./statusTone"; +import { syncMaskScanPhase } from "./syncMaskScanPhase"; export function StatusIcon(props: { tone: StatusTone; @@ -75,6 +76,7 @@ export function StatusIcon(props: { {tone === "working" && maskUrl ? (

diff --git a/src/renderer/components/thread/ChatPane/parts/items/PlanProposal.tsx b/src/renderer/components/thread/ChatPane/parts/items/PlanProposal.tsx index 47761d6e..43a47606 100644 --- a/src/renderer/components/thread/ChatPane/parts/items/PlanProposal.tsx +++ b/src/renderer/components/thread/ChatPane/parts/items/PlanProposal.tsx @@ -5,6 +5,7 @@ import type { ToolCallPayload } from "@/shared/contracts"; import type { RuntimeChatItem } from "@/renderer/state/slices/runtimeEventSlice"; import { ItemMarkdown } from "./ItemMarkdown"; import { PathDisplay } from "@/renderer/components/common/PathDisplay"; +import { useShimmer } from "@/renderer/thinkingAnimator"; import { chatMessageSurfaceClass } from "./chatMessageSurface"; interface PlanProposalProps { @@ -26,6 +27,7 @@ export const PlanProposal = memo(function PlanProposal({ item }: PlanProposalPro const plan = readString(args, "plan"); const planFilePath = readString(args, "planFilePath") ?? readString(args, "plan_filename"); const isStreaming = item.state !== "completed"; + const thinkingTextRef = useShimmer(isStreaming); if (!plan && !isStreaming && !planFilePath) return null; @@ -37,6 +39,7 @@ export const PlanProposal = memo(function PlanProposal({ item }: PlanProposalPro className={`size-3 shrink-0 ${isStreaming ? "lightcode-plan-proposal-icon" : ""}`} /> diff --git a/src/renderer/components/thread/ChatPane/parts/items/Reasoning.tsx b/src/renderer/components/thread/ChatPane/parts/items/Reasoning.tsx index c280152f..e4dcbb6c 100644 --- a/src/renderer/components/thread/ChatPane/parts/items/Reasoning.tsx +++ b/src/renderer/components/thread/ChatPane/parts/items/Reasoning.tsx @@ -12,6 +12,7 @@ import { Brain, ChevronDown } from "lucide-react"; import type { RuntimeChatItem } from "@/renderer/state/slices/runtimeEventSlice"; import { useChatPaneActions } from "../../chatPaneActionsContext"; import { isElementAtBottom } from "../../chatScrollGeometry"; +import { useBrainThinking, useShimmer } from "@/renderer/thinkingAnimator"; import { chatMessageSurfaceClass } from "./chatMessageSurface"; import { ItemMarkdown } from "./ItemMarkdown"; @@ -86,6 +87,9 @@ export const Reasoning = memo(function Reasoning({ item }: ReasoningProps) { return () => observer.disconnect(); }, [shouldAutoScroll]); + const thinkingTextRef = useShimmer(isStreaming); + const brainRef = useBrainThinking(isStreaming); + if (!isStreaming) { // Compact toggle — visually distinct from tool-call accordions: no border // tile, dotted left rule when expanded, italic body. Equal vertical @@ -120,8 +124,16 @@ export const Reasoning = memo(function Reasoning({ item }: ReasoningProps) {
- - + + Thinking
diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx index b68e0b03..32d2c4cb 100644 --- a/src/renderer/main.tsx +++ b/src/renderer/main.tsx @@ -1,5 +1,6 @@ import { createRoot, type Root } from "react-dom/client"; import "./styles.css"; +import "./uiAnimationActivity"; import { readBridge } from "./bridge"; import { captureRendererException, initializeRendererSentry } from "./diagnostics/sentry"; import { getAppName } from "@/shared/appName"; diff --git a/src/renderer/styles.css b/src/renderer/styles.css index 4c7d2e4b..9341c815 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -1300,8 +1300,6 @@ html[data-platform="darwin"] .lightcode-content-over-drag-region--drag { width: 100%; height: 100%; overflow: visible; - will-change: filter, transform; - transform: translate3d(0, 0, 0); } .lightcode-provider-icon__fill { @@ -1318,13 +1316,6 @@ html[data-platform="darwin"] .lightcode-content-over-drag-region--drag { -webkit-mask-position: center; -webkit-mask-repeat: no-repeat; -webkit-mask-size: contain; - will-change: filter, transform; - transform: translate3d(0, 0, 0); -} - -.lightcode-provider-icon svg { - will-change: filter, transform; - transform: translate3d(0, 0, 0); } .lightcode-provider-icon--external .lightcode-provider-icon__mask { @@ -1370,7 +1361,12 @@ html[data-platform="darwin"] .lightcode-content-over-drag-region--drag { transparent 100% ); transform: translate3d(-33.333%, 0, 0); - animation: lightcode-provider-icon-mask-scan 1.45s linear infinite; + /* Throttle the shine to 15fps with steps() rather than advancing every display + refresh. The compositor only produces a new frame when the stepped transform + actually changes, so a 120Hz ProMotion panel draws ~15 frames/s here instead + of ~120 — an ~8x cut in GPU compositing for a sweep this subtle and slow. + 24 steps / 1.6s = 15fps. Raise the step count for a smoother sweep. */ + animation: lightcode-provider-icon-mask-scan 1.6s steps(24, jump-end) infinite; will-change: transform; } @@ -1404,6 +1400,33 @@ html[data-platform="darwin"] .lightcode-content-over-drag-region--drag { } } +/* Pause the always-on status-icon animations when the window is hidden or + unfocused (attributes toggled by uiAnimationActivity.ts). The static glow and + colour stay, so a backgrounded window still shows which threads are busy — we + just stop driving the GPU compositor while nobody is looking. Drop the + `data-app-unfocused` selectors if you want the sweep to keep running while the + window is visible-but-unfocused. */ +html[data-app-hidden] .lightcode-provider-icon__mask-scan::before, +html[data-app-unfocused] .lightcode-provider-icon__mask-scan::before, +html[data-app-hidden] .lightcode-provider-icon--attention, +html[data-app-unfocused] .lightcode-provider-icon--attention, +html[data-app-hidden] .lightcode-provider-icon--finished, +html[data-app-unfocused] .lightcode-provider-icon--finished { + animation-play-state: paused; +} + +/* Honour the OS "reduce motion" setting — drop the moving sweep/pulse entirely + and let the static glow + colour signal status. (The thinking shimmer + brain + are JS-driven via thinkingAnimator.ts, which checks prefers-reduced-motion + itself and paints a single static frame.) */ +@media (prefers-reduced-motion: reduce) { + .lightcode-provider-icon__mask-scan::before, + .lightcode-provider-icon--attention, + .lightcode-provider-icon--finished { + animation: none; + } +} + .lightcode-provider-icon__shell { fill: currentColor; opacity: 0.32; @@ -1432,6 +1455,12 @@ html[data-platform="darwin"] .lightcode-content-over-drag-region--drag { .lightcode-provider-icon__mask:not(.lightcode-provider-icon__mask-scan) { filter: drop-shadow(0 0 0.25rem color-mix(in oklab, var(--working-tone) 45%, transparent)) drop-shadow(0 0 0.5rem color-mix(in oklab, var(--working-tone) 22%, transparent)); + /* Promote to a compositor layer ONLY while working — the glow drop-shadow and + the shine sweep need it. Idle/gray icons (often dozens across the sidebar + + recent-threads lists) no longer each carry a standing `will-change` layer + and its GPU memory. */ + will-change: filter, transform; + transform: translate3d(0, 0, 0); } .lightcode-provider-icon--error { @@ -1495,53 +1524,28 @@ html[data-platform="darwin"] .lightcode-content-over-drag-region--drag { pointer-events: none; } -@keyframes lightcode-brain-firing { - 0%, - 100% { - opacity: 0.45; - } - 50% { - opacity: 1; - } -} - .lightcode-brain-thinking { filter: drop-shadow(0 0 0.1rem color-mix(in oklab, currentColor 32%, transparent)); - will-change: transform; - transform: translateZ(0); } +/* The per-path "firing" opacities are driven from a shared 15fps timer + (thinkingAnimator.ts), NOT a CSS animation: a continuously-active opacity + animation on SVG paths can't composite, so it would force a main-thread style + recalc + frame on every display refresh (~120fps). 0.55 is the static + fallback shown before the timer first paints. */ .lightcode-brain-thinking path { opacity: 0.55; - will-change: opacity; -} - -.lightcode-brain-thinking path:nth-child(3n + 1) { - animation: lightcode-brain-firing 1.8s ease-in-out infinite; - animation-delay: 0s; -} - -.lightcode-brain-thinking path:nth-child(3n + 2) { - animation: lightcode-brain-firing 1.8s ease-in-out infinite; - animation-delay: 0.6s; -} - -.lightcode-brain-thinking path:nth-child(3n) { - animation: lightcode-brain-firing 1.8s ease-in-out infinite; - animation-delay: 1.2s; -} - -@keyframes lightcode-text-shimmer { - 0% { - background-position: 0% 0; - } - 100% { - background-position: -200% 0; - } } -/* "Thinking" label that pairs with the brain. The highlight shimmers in - a continuous, smooth 60fps compositor sweep. */ +/* "Thinking"/"Working for"/"Compacting"/"Proposed plan" label that pairs with + the brain. The highlight sweeps via `background-position` — a main-thread + *paint* property that can't composite, so an always-active CSS animation here + repaints AND recalcs style every display refresh (~120fps). Instead the sweep + is driven from a shared 15fps timer (thinkingAnimator.ts) that writes + `background-position-x` only ~15×/s, letting the frame pipeline idle between + ticks. `contain: paint` keeps each repaint scoped to the label box; the + `background-position: 0% 0` below is the static fallback before the timer + runs. */ .lightcode-thinking-text { position: relative; display: inline-block; @@ -1561,8 +1565,6 @@ html[data-platform="darwin"] .lightcode-content-over-drag-region--drag { -webkit-background-clip: text; color: transparent; -webkit-text-fill-color: transparent; - animation: lightcode-text-shimmer 2.2s linear infinite; - will-change: background-position; } @keyframes lightcode-plan-check-1-anim { diff --git a/src/renderer/thinkingAnimator.test.tsx b/src/renderer/thinkingAnimator.test.tsx new file mode 100644 index 00000000..301d2a38 --- /dev/null +++ b/src/renderer/thinkingAnimator.test.tsx @@ -0,0 +1,84 @@ +import { cleanup, render } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { useBrainThinking, useShimmer } from "./thinkingAnimator"; + +function ShimmerProbe({ active }: { active: boolean }) { + const ref = useShimmer(active); + return ; +} + +function BrainProbe({ active }: { active: boolean }) { + const ref = useBrainThinking(active); + return ( + + + + + + ); +} + +describe("thinkingAnimator", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(0); + }); + + afterEach(() => { + cleanup(); + vi.useRealTimers(); + vi.restoreAllMocks(); + document.documentElement.removeAttribute("data-app-hidden"); + document.documentElement.removeAttribute("data-app-unfocused"); + }); + + it("drives the shimmer background-position from a shared timer", () => { + const { getByTestId } = render(); + const el = getByTestId("shimmer") as HTMLSpanElement; + + // The registration effect paints one frame immediately at t=0. + expect(el.style.backgroundPositionX).toMatch(/%$/); + expect(parseFloat(el.style.backgroundPositionX)).toBeCloseTo(0, 5); + + // One 67ms tick → now=67 → -200 * (67/2200) ≈ -6.1%. + vi.advanceTimersByTime(67); + expect(parseFloat(el.style.backgroundPositionX)).toBeCloseTo(-6.1, 1); + }); + + it("stops writing once the element unmounts", () => { + const { getByTestId, unmount } = render(); + const el = getByTestId("shimmer") as HTMLSpanElement; + vi.advanceTimersByTime(67); + const afterFirst = el.style.backgroundPositionX; + + unmount(); + // No registered elements left; further ticks must not throw or write. + expect(() => vi.advanceTimersByTime(1000)).not.toThrow(); + expect(el.style.backgroundPositionX).toBe(afterFirst); + }); + + it("does not animate while inactive", () => { + const { getByTestId } = render(); + const el = getByTestId("shimmer") as HTMLSpanElement; + vi.advanceTimersByTime(500); + expect(el.style.backgroundPositionX).toBe(""); + }); + + it("freezes ticks while the app is backgrounded", () => { + document.documentElement.setAttribute("data-app-hidden", ""); + const { getByTestId } = render(); + const el = getByTestId("shimmer") as HTMLSpanElement; + const initial = el.style.backgroundPositionX; // painted once on register + vi.advanceTimersByTime(1000); + expect(el.style.backgroundPositionX).toBe(initial); // ticks are no-ops while hidden + }); + + it("fires the three brain path groups out of phase", () => { + const { getByTestId } = render(); + // At t=0 the groups (delays 0 / 0.6s / 1.2s) sit at different points of the + // 0.45→1→0.45 pulse, so group 0 differs from groups 1 and 2. + expect(parseFloat(getByTestId("p0").style.opacity)).toBeCloseTo(0.45, 2); + expect(parseFloat(getByTestId("p1").style.opacity)).toBeCloseTo(0.817, 2); + expect(parseFloat(getByTestId("p2").style.opacity)).toBeCloseTo(0.817, 2); + }); +}); diff --git a/src/renderer/thinkingAnimator.ts b/src/renderer/thinkingAnimator.ts new file mode 100644 index 00000000..b828a804 --- /dev/null +++ b/src/renderer/thinkingAnimator.ts @@ -0,0 +1,176 @@ +// Shared 15fps driver for the "thinking" UI animations — the +// `.lightcode-thinking-text` shimmer ("Working for"/"Thinking"/"Compacting"/ +// "Proposed plan") and the `.lightcode-brain-thinking` firing. +// +// Why this exists instead of CSS animations: +// Both effects animate properties that CANNOT run on the compositor — +// `background-position` (the shimmer) and per-path `opacity` on an SVG (the +// brain). A continuously-active CSS animation on such a property forces Blink to +// re-run style recalc + the entire frame pipeline EVERY display refresh (~120fps +// on ProMotion) for as long as the animation is active, even though the value +// only needs to change ~15×/s. Traces showed UpdateLayoutTree ≡ +// serviceScriptedAnimations ≡ ~124/s during streaming — a main-thread frame +// every refresh — purely from these two animations. `steps()` throttles the +// paint but NOT that per-frame recalc loop, because the animation is still +// "active" every frame. +// +// Driving the exact same values from a single `setInterval` at 15fps means the +// element is only dirtied ~15×/s; between ticks nothing is invalidated, so the +// renderer's frame pipeline goes idle. The visuals are identical (same gradient +// sweep, same staggered brain firing), and a shared wall-clock phase keeps every +// instance perfectly in sync. The timer pauses while the window is hidden or +// unfocused (delegating that policy to uiAnimationActivity.ts), and honours +// `prefers-reduced-motion` by painting one static frame. + +import { useEffect, useRef } from "react"; +import type { RefObject } from "react"; + +const FPS = 15; +const TICK_MS = Math.round(1000 / FPS); // ~67ms +const SHIMMER_PERIOD_MS = 2200; // matches the previous 2.2s background-position sweep +const BRAIN_PERIOD_MS = 1800; // matches the previous 1.8s opacity pulse +const BRAIN_GROUP_DELAY_MS = 600; // matches the previous 0s / 0.6s / 1.2s stagger + +const shimmerEls = new Set(); +// Brains map to their path list, cached once at registration so the tick loop +// doesn't re-run querySelectorAll on every frame. +const brainPaths = new Map(); +let timer: ReturnType | null = null; + +function hasRegistered(): boolean { + return shimmerEls.size > 0 || brainPaths.size > 0; +} + +function prefersReducedMotion(): boolean { + return ( + typeof window !== "undefined" && + typeof window.matchMedia === "function" && + window.matchMedia("(prefers-reduced-motion: reduce)").matches + ); +} + +// Single source of truth for "should UI animations run": uiAnimationActivity.ts +// toggles these attributes on visibilitychange/focus/blur, and styles.css uses +// the same attributes to pause the compositor-driven icon animations. Reading +// them here keeps the JS-driven thinking animations gated by the exact same +// policy — change "when to pause" in one place and both follow. +function isAppActive(): boolean { + if (typeof document === "undefined") return false; + const root = document.documentElement; + return !root.hasAttribute("data-app-hidden") && !root.hasAttribute("data-app-unfocused"); +} + +// Triangle 0→1→0 over the period, mapped to 0.45..1 (matches the old +// @keyframes lightcode-brain-firing: 0%/100% = 0.45, 50% = 1). +function brainOpacity(phase01: number): number { + const tri = 1 - Math.abs(1 - 2 * phase01); + return 0.45 + 0.55 * tri; +} + +function paintFrame(now: number): void { + if (shimmerEls.size > 0) { + // background-position-x sweeps 0% → -200% (with 200% size + repeat-x this loops seamlessly). + const phase = (now % SHIMMER_PERIOD_MS) / SHIMMER_PERIOD_MS; + const x = `${(-200 * phase).toFixed(1)}%`; + for (const el of shimmerEls) el.style.backgroundPositionX = x; + } + for (const paths of brainPaths.values()) { + for (let i = 0; i < paths.length; i++) { + const delay = (i % 3) * BRAIN_GROUP_DELAY_MS; + const t = (((now - delay) % BRAIN_PERIOD_MS) + BRAIN_PERIOD_MS) % BRAIN_PERIOD_MS; + paths[i]!.style.opacity = brainOpacity(t / BRAIN_PERIOD_MS).toFixed(3); + } + } +} + +function tick(): void { + // Freeze (skip writes) while backgrounded/unfocused — nothing to drive, and + // the static frozen frame is fine. Next tick resumes within ~67ms on refocus. + if (!isAppActive()) return; + paintFrame(Date.now()); +} + +function ensureRunning(): void { + if (timer !== null) return; + if (prefersReducedMotion()) { + paintFrame(0); // one static frame; no loop + return; + } + if (typeof setInterval !== "function") return; + paintFrame(Date.now()); + timer = setInterval(tick, TICK_MS); +} + +function maybeStop(): void { + if (timer !== null && !hasRegistered()) { + clearInterval(timer); + timer = null; + } +} + +function addShimmer(el: HTMLElement): void { + shimmerEls.add(el); +} +function removeShimmer(el: HTMLElement): void { + shimmerEls.delete(el); +} +function addBrain(el: SVGSVGElement): void { + brainPaths.set(el, Array.from(el.querySelectorAll("path"))); +} +function removeBrain(el: SVGSVGElement): void { + brainPaths.delete(el); +} + +// Shared registration effect for every animated element: while `active`, add the +// element to its registry and keep the shared timer running; clean up on unmount +// or when it goes inactive. +function useAnimatedElement( + ref: RefObject, + active: boolean, + add: (el: T) => void, + remove: (el: T) => void, +): void { + useEffect(() => { + const el = ref.current; + if (!active || !el) return; + add(el); + ensureRunning(); + return () => { + remove(el); + maybeStop(); + }; + }, [ref, active, add, remove]); +} + +/** + * Register a `.lightcode-thinking-text` element to receive the shimmer sweep + * while `active`. Attach the returned ref to the element. No-op when inactive, + * so the timer only runs while a label is actually shimmering. + */ +export function useShimmer( + active: boolean, +): RefObject { + const ref = useRef(null); + useAnimatedElement(ref, active, addShimmer, removeShimmer); + return ref; +} + +/** Same as {@link useShimmer} but drives an element you already hold a ref to. */ +export function useShimmerRef( + ref: RefObject, + active: boolean, +): void { + useAnimatedElement(ref, active, addShimmer, removeShimmer); +} + +/** + * Register a `.lightcode-brain-thinking` SVG to receive the staggered per-path + * opacity firing while `active`. Attach the returned ref to the icon. Gating on + * `active` (not just mount) matters because a Reasoning block can transition + * streaming→completed without unmounting — the firing must stop with it. + */ +export function useBrainThinking(active: boolean): RefObject { + const ref = useRef(null); + useAnimatedElement(ref, active, addBrain, removeBrain); + return ref; +} diff --git a/src/renderer/uiAnimationActivity.ts b/src/renderer/uiAnimationActivity.ts new file mode 100644 index 00000000..f747c797 --- /dev/null +++ b/src/renderer/uiAnimationActivity.ts @@ -0,0 +1,32 @@ +// Pauses the always-on, compositor-driven status-icon animations (the "working" +// shine sweep and the attention/finished pulses) whenever the app window is +// hidden or unfocused. Those animations otherwise keep the GPU compositor +// producing frames at the full display refresh (~120Hz on a ProMotion panel) +// for as long as any thread is working — even while the window sits in the +// background. The static glow/colour on the icons is untouched, so a +// backgrounded window still shows which threads are busy; we just stop driving +// the GPU while nobody is looking at it. +// +// Chromium already stops compositing a fully hidden/occluded page (the window's +// `backgroundThrottling` default), but a window that is merely *unfocused* +// (another app in front, or a different window on the same screen) stays +// "visible" and keeps animating at full cost. This guard closes that gap by +// toggling attributes that styles.css keys `animation-play-state: paused` off. + +function syncAnimationActivity(): void { + if (typeof document === "undefined") return; + const root = document.documentElement; + root.toggleAttribute("data-app-hidden", document.visibilityState === "hidden"); + // `hasFocus()` is false when the renderer's window isn't the key window + // (including when DevTools is focused in dev — harmless, the sweep just pauses). + root.toggleAttribute("data-app-unfocused", !document.hasFocus()); +} + +if (typeof document !== "undefined" && typeof window !== "undefined") { + document.addEventListener("visibilitychange", syncAnimationActivity); + window.addEventListener("focus", syncAnimationActivity); + window.addEventListener("blur", syncAnimationActivity); + syncAnimationActivity(); +} + +export {}; diff --git a/vite.config.ts b/vite.config.ts index 1109b56c..63982f2c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,5 @@ import { resolve } from "node:path"; -import { defineConfig, loadEnv } from "vite"; +import { defineConfig, loadEnv, type Plugin } from "vite"; import babel from "@rolldown/plugin-babel"; import react, { reactCompilerPreset } from "@vitejs/plugin-react"; @@ -30,8 +30,38 @@ function buildPostHogEnvDefines(mode: string): Record { // src/shared/channel.config-parity.test.ts. const lightcodeChannel = process.env.LIGHTCODE_CHANNEL === "nightly" ? "nightly" : "stable"; +// Dev-only: connect the renderer to the standalone React DevTools app for +// inspecting/profiling rerenders. The React DevTools *browser extension* uses +// `chrome.scripting` (Manifest V3), which Electron doesn't implement — under +// Electron the extension panels load but never find the React tree +// (facebook/react#25843). The supported alternative is the standalone +// `react-devtools` app (run via `pnpm devtools`), which serves a backend on +// :8097 that the page connects to. The hook must be installed *before* React +// loads, so we inject a classic