diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d31376ac30..2fbe37e918 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -18,7 +18,7 @@ Closes # ### Reasoning - + ### Goals diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e0fd69103..efe337f7a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,55 @@ # Changelog +## 0.4.0 - 2026-08-13 + +### Breaking + +- Removed `paseo chat` and `paseo loop` ([#3053](https://github.com/getpaseo/paseo/pull/3053)) + +### Added + +- Added host-wide agent profiles for reusable provider, model, mode, thinking, and feature settings ([#3208](https://github.com/getpaseo/paseo/pull/3208)) +- Added workspace file search to Cmd/Ctrl+P ([#3059](https://github.com/getpaseo/paseo/pull/3059)) +- Added file and folder actions to Files and Changes ([#3027](https://github.com/getpaseo/paseo/pull/3027) by [@nikuscs](https://github.com/nikuscs)) +- Added interactive Mermaid diagrams to chats and Markdown previews ([#2306](https://github.com/getpaseo/paseo/pull/2306) by [@dmeledon](https://github.com/dmeledon)) +- Added live task progress above the composer and in the timeline ([#3227](https://github.com/getpaseo/paseo/pull/3227)) +- Added a model setting for generated workspace titles, branch names, commits, and pull request drafts ([#3215](https://github.com/getpaseo/paseo/pull/3215)) +- Added the supported `@getpaseo/client` TypeScript SDK ([#3141](https://github.com/getpaseo/paseo/pull/3141)) +- Added provider installation diagnostics to the CLI ([#3243](https://github.com/getpaseo/paseo/pull/3243)) +- Added workspace rename to the CLI ([#3209](https://github.com/getpaseo/paseo/pull/3209) by [@martinhanzik](https://github.com/martinhanzik)) + +### Improved + +- Added a centered reading layout and formatted YAML front matter to Markdown previews ([#3240](https://github.com/getpaseo/paseo/pull/3240)) +- Added sidebar grouping to the Command Center ([#3063](https://github.com/getpaseo/paseo/pull/3063) by [@cleiter](https://github.com/cleiter)) +- Added every built-in theme to theme shortcuts ([#3214](https://github.com/getpaseo/paseo/pull/3214)) +- Removed periodic Git polling for idle workspaces ([#3323](https://github.com/getpaseo/paseo/pull/3323)) +- Set provider catalog refreshes to a configurable two-minute deadline ([#3322](https://github.com/getpaseo/paseo/pull/3322)) + +### Fixed + +- Fixed terminal sessions being lost after host sleep or daemon worker stalls ([#3235](https://github.com/getpaseo/paseo/pull/3235), [#3263](https://github.com/getpaseo/paseo/pull/3263)) +- Fixed daemon hang when archiving a workspace ([#3107](https://github.com/getpaseo/paseo/pull/3107)) +- Fixed New Workspace crash on Android when projects span multiple hosts ([#3241](https://github.com/getpaseo/paseo/pull/3241)) +- Fixed provider cache exhausting Android local storage ([#3234](https://github.com/getpaseo/paseo/pull/3234)) +- Fixed dictation recordings being lost during connection interruptions ([#3159](https://github.com/getpaseo/paseo/pull/3159)) +- Fixed new workspaces reusing an existing worktree ([#3224](https://github.com/getpaseo/paseo/pull/3224)) +- Fixed new worktrees ignoring the setup command saved in Project Settings ([#3233](https://github.com/getpaseo/paseo/pull/3233)) +- Fixed new worktrees becoming dirty when the selected base contained a different `paseo.json` ([#3311](https://github.com/getpaseo/paseo/pull/3311)) +- Fixed removed hosts continuing to receive push notifications ([#3176](https://github.com/getpaseo/paseo/pull/3176)) +- Fixed delegated-agent notifications stopping after permission prompts or workspace closure ([#3177](https://github.com/getpaseo/paseo/pull/3177), [#3192](https://github.com/getpaseo/paseo/pull/3192) by [@thomasvan](https://github.com/thomasvan), [@wilgon456](https://github.com/wilgon456)) +- Fixed OMP agent startup when model catalogs exceed the protocol-v1 frame limit ([#3184](https://github.com/getpaseo/paseo/pull/3184) by [@pi3123](https://github.com/pi3123)) +- Fixed OpenCode models being unable to return to their default variant ([#3281](https://github.com/getpaseo/paseo/pull/3281)) +- Fixed completed Codex subagents remaining marked active ([#3188](https://github.com/getpaseo/paseo/pull/3188) by [@Strainy](https://github.com/Strainy)) +- Fixed Codex compaction leaving turns marked as working ([#3211](https://github.com/getpaseo/paseo/pull/3211) by [@edihasaj](https://github.com/edihasaj)) +- Fixed Cursor usage with current Cursor logins ([#2704](https://github.com/getpaseo/paseo/pull/2704) by [@QuteSaltyFish](https://github.com/QuteSaltyFish)) +- Fixed SVG project icons rendering as blank on mobile ([#2579](https://github.com/getpaseo/paseo/pull/2579) by [@colonelpanic8](https://github.com/colonelpanic8)) +- Fixed crash when persisted cache was incompatible ([#3289](https://github.com/getpaseo/paseo/pull/3289)) +- Fixed open delegated agents being archived with their parent ([#3279](https://github.com/getpaseo/paseo/pull/3279)) +- Fixed agent creation when profiles carried modes unsupported by the selected provider ([#3331](https://github.com/getpaseo/paseo/pull/3331)) +- Fixed Claude agents waiting on Paseo's ten-minute MCP timeout ([#3315](https://github.com/getpaseo/paseo/pull/3315)) +- Fixed Copy resume command for Hermes agents ([#3300](https://github.com/getpaseo/paseo/pull/3300) by [@desmond-rai](https://github.com/desmond-rai)) + ## 0.3.1 - 2026-08-09 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4035db1a38..8eaaf5c0c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Thank you for taking the time to contribute to Paseo. Paseo is an opinionated product, built on freedom and flexibility: any agent provider, any device, running on your own machine. It is meant to be composable, so you can build the workflow you want. Read more about the product vision [here](docs/product.md). -Given Paseo's scope, contributing to to it takes a lot of context that is very hard to transfer. That's why product, design, architecture, and workflow decisions are currently all made by the maintainer. +Given Paseo's scope, contributing to it takes a lot of context that is very hard to transfer. That's why product, design, architecture, and workflow decisions are currently all made by the maintainer. I pick what to build based on whether it fits the product, how many workflows it improves, whether it keeps things composable, whether we can hold the quality bar, and whether I want to build it. @@ -72,8 +72,8 @@ Here is the criteria I use to decide: - PRs that were explicitly approved in a discussion are preferred. - Unsolicited PR can be closed without a detailed review. - Your PR can be narrowed, refactored, or redesigned. -- Your PR might be accpeted but not merged immediately. -- You wil be attributed for your work in the changelog, even if I redesign your PR +- Your PR might be accepted but not merged immediately. +- You will be attributed for your work in the changelog, even if I redesign your PR ## QA evidence diff --git a/README.ja.md b/README.ja.md index ff159843ec..0f35d47c57 100644 --- a/README.ja.md +++ b/README.ja.md @@ -118,7 +118,6 @@ npx skills add getpaseo/paseo どのエージェントとの会話でも使用できます。 - `/paseo-handoff` — エージェント間で作業を引き継ぎます。私はこれを使って Claude で計画し、Codex に実装を引き継いでいます。 -- `/paseo-loop` — 明確な受け入れ基準に沿ってエージェントをループさせます(Ralph loops とも呼ばれます)。検証役を追加することもできます。 - `/paseo-advisor` — 単一のエージェントをアドバイザーとして起動し、作業を委任せずにセカンドオピニオンを得ます。 - `/paseo-committee` — 対照的な2つのエージェントで委員会を構成し、一歩引いた視点で根本原因を分析して計画を作成します。 diff --git a/README.ko.md b/README.ko.md index ec1bc64cf2..9975a7dd63 100644 --- a/README.ko.md +++ b/README.ko.md @@ -128,7 +128,6 @@ npx skills add getpaseo/paseo 그런 다음 어떤 에이전트 대화에서든 아래 명령을 사용할 수 있습니다. - `/paseo-handoff` — 에이전트 간에 작업을 넘깁니다. Claude로 계획을 세운 뒤 Codex에 구현을 넘길 때 이 기능을 씁니다. -- `/paseo-loop` — 명확한 완료 조건을 기준으로 에이전트를 반복 실행합니다(Ralph loop). 검증 에이전트를 추가할 수도 있습니다. - `/paseo-advisor` — 작업 자체를 넘기지 않고, 에이전트 하나를 조언자로 띄워 두 번째 의견을 받습니다. - `/paseo-committee` — 서로 다른 관점의 에이전트 두 개로 위원회를 구성해, 한 발 물러나 근본 원인을 분석하고 계획을 세웁니다. diff --git a/README.md b/README.md index f16d2c6c43..1b26f31800 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,6 @@ npx skills add getpaseo/paseo Then use them in any agent conversation: - `/paseo-handoff` — hand off work between agents. I use this to plan with Claude and then handoff to Codex to implement. -- `/paseo-loop` — loop an agent against clear acceptance criteria (aka Ralph loops), optionally with a verifier. - `/paseo-advisor` — spin up a single agent as an advisor for a second opinion, without delegating the work itself. - `/paseo-committee` — form a committee of two contrasting agents to step back, do root cause analysis, and produce a plan. diff --git a/README.zh-CN.md b/README.zh-CN.md index dd9e2eae65..fef0f4ff2c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -118,7 +118,6 @@ npx skills add getpaseo/paseo 然后在任意 agent 对话中使用: - `/paseo-handoff` — 在 agents 之间交接工作。我会用它先和 Claude 规划,再交给 Codex 实现。 -- `/paseo-loop` — 让 agent 按明确验收标准循环工作(也叫 Ralph loops),也可以加 verifier。 - `/paseo-advisor` — 启动单个 agent 作为 advisor,提供第二意见,但不把工作委托出去。 - `/paseo-committee` — 组建两个风格互补的 agents,让它们后退一步做根因分析并产出计划。 diff --git a/docs/agent-lifecycle.md b/docs/agent-lifecycle.md index eeed68322d..4da30bf0e0 100644 --- a/docs/agent-lifecycle.md +++ b/docs/agent-lifecycle.md @@ -44,11 +44,26 @@ Agents can launch other agents via the agent-scoped `create_agent` MCP tool. Age - **Subagents** — exist as part of the creating agent's work, appear in that agent's subagent track, and are archived with it. - **Detached agents** — stand on their own after an explicit detach transition, do not appear in the former parent's subagent track, and are not archived with it. +Parent archive detaches a subagent instead of archiving it when either condition holds: + +- The child belongs to another workspace. +- The child is currently open in an agent tab. + +All other children archive with the parent. After the workspace layout hydrates, the client marks +every managed subagent present in its tabs with `paseo.open-agent-tab.=true` through the +generic agent metadata update. This includes background and restored tabs; navigation does not own +the marker. Closing a tab sets that client's label to `false`. Any `true` client label keeps the child +open. Detach clears the parent and every open-tab label. The surviving child therefore becomes a +normal root agent immediately, and closing its still-open tab archives it. + Runtime ownership is resolved from explicit workspace ID and caller context, never from `cwd`. Workspace creation is a separate operation with `local | worktree` isolation; agent creation only selects an existing workspace. -Users can also detach an existing subagent from the subagents track. Detach is deliberately a manual lifecycle gesture, not an agent-facing MCP tool. It removes the `paseo.parent-agent-id` label only: it does not stop, archive, move, or restart the agent. The agent keeps its current `cwd` and `workspaceId`, leaves the former parent's track, and behaves like a root agent for tab close, workspace activity, and future parent archive. +Users can also detach an existing subagent from the subagents track. Detach is deliberately a manual lifecycle gesture, not an agent-facing MCP tool. It removes the parent and open-tab lifecycle labels: it does not stop, archive, move, or restart the agent. The agent keeps its current `cwd` and `workspaceId`, leaves the former parent's track, and behaves like a root agent for tab close, workspace activity, and future parent archive. `notifyOnFinish` defaults to `true` for agent-scoped creation and background prompt follow-ups because most delegated work needs to report back to the creating agent. Set it to `false` only for truly fire-and-forget agents or prompts. +Permission requests are notification checkpoints, not the end of that subscription. The caller is notified again after a permission response when the child finishes, errors, or requests another permission. +The permission notification includes the normalized request plus the child and request IDs, so the caller can inspect it and respond without fetching agent status. +A watched child that closes before its finish event also notifies the caller so delegated work cannot disappear silently during archive or workspace teardown. ## Provider-managed child agents @@ -71,7 +86,7 @@ Archiving runs through `AgentManager.archiveAgent` (`packages/server/src/server/ 2. Set `archivedAt` and normalize `lastStatus` away from `running`/`initializing` 3. Notify subscribers 4. Close the runtime (kills the process if still running) -5. **Cascade-archive children** — any agent whose `paseo.parent-agent-id` label matches the archived agent gets archived too, recursively +5. **Resolve children** — detach cross-workspace and open-tab children; cascade-archive the rest recursively Cascade is what keeps subagent fleets from outliving their orchestrator. @@ -106,7 +121,7 @@ These are two distinct concepts that used to be conflated: Closing a tab on a **root agent** still archives — the tab is the agent's home, so closing it means "I'm done with this agent." A confirm dialog protects against archiving a running agent by accident. -Closing a tab on a **subagent** (any agent with `parentAgentId`) is **layout-only**. The agent stays unarchived and stays in its parent's track. The user can re-open the tab from the track at any time. This is implemented in `handleCloseAgentTab` (`packages/app/src/screens/workspace/workspace-screen.tsx`). +Closing a tab on a **subagent** (any agent with `parentAgentId`) is **layout-only**. The app clears the current client's open-tab label before removing the tab. Another client's open tab remains protected. The agent stays unarchived and stays in its parent's track, so a later parent archive cascades to it when no client still has it open. The user can re-open the tab from the track at any time. Single and bulk tab close apply the same policy. The asymmetry is intentional: a subagent's persistent relationship lives in the parent's track. Same-workspace subagents are not auto-opened as tabs; the user opens one from that track when needed. A cross-workspace subagent is also auto-opened as a tab in its own workspace so opening that workspace does not appear empty. It remains in the parent's track until it is actually detached. @@ -152,7 +167,7 @@ Archived Paseo subagents disappear from the track, by design. To remove one from The track header's **Archive finished** action hides finished provider-owned rows in the current app session. Their native sessions and timelines are untouched, and managed Paseo subagents are not archived by this bulk action. If a hidden provider child starts running again, the app brings it back to the track. -To keep the agent alive but remove it from the parent's track, use **detach**. The daemon clears the parent label, emits the normal agent update, and every client reclassifies the agent from subagent to root/sibling from that updated snapshot. +To keep the agent alive but remove it from the parent's track, use **detach**. The daemon clears the relationship lifecycle labels, emits the normal agent update, and every client reclassifies the agent from subagent to root/sibling from that updated snapshot. ## Why this shape @@ -186,11 +201,12 @@ $PASEO_HOME/agents/{cwd-with-dashes}/{agent-id}.json Each agent is a single JSON file. Fields relevant to this doc: -| Field | Type | Meaning | -| --------------------------------- | ------------- | ---------------------------------------------------------------------------------- | -| `id` | `string` | Stable identifier | -| `archivedAt` | `string?` | Soft-delete timestamp (ISO 8601) | -| `labels["paseo.parent-agent-id"]` | `string?` | Parent agent ID, set automatically for agent-scoped creation and removed by detach | -| `lastStatus` | `AgentStatus` | `initializing` / `idle` / `running` / `error` / `closed` | +| Field | Type | Meaning | +| -------------------------------------------- | ------------- | ---------------------------------------------------------------------------------- | +| `id` | `string` | Stable identifier | +| `archivedAt` | `string?` | Soft-delete timestamp (ISO 8601) | +| `labels["paseo.parent-agent-id"]` | `string?` | Parent agent ID, set automatically for agent-scoped creation and removed by detach | +| `labels["paseo.open-agent-tab."]` | `string?` | `"true"` protects an open tab on that client; detach clears every matching label | +| `lastStatus` | `AgentStatus` | `initializing` / `idle` / `running` / `error` / `closed` | See [`docs/data-model.md`](./data-model.md) for the full agent record. diff --git a/docs/architecture.md b/docs/architecture.md index 820324d359..4fc6d7db61 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -69,6 +69,7 @@ not retain non-Git directories. | `server/bootstrap.ts` | Daemon initialization: HTTP server, WS server, agent manager, storage, relay | | `server/websocket-server.ts` | WebSocket connection management, hello handshake, binary frame routing | | `server/session.ts` | Per-client session state, timeline subscriptions, terminal operations | +| `server/directory-sync/` | Daemon-global latest-state sequences for projects, workspaces, and agents | | `server/agent/agent-manager.ts` | Agent lifecycle state machine, timeline tracking, subscriber management | | `server/agent/agent-storage.ts` | File-backed JSON persistence at `$PASEO_HOME/agents/` | | `server/agent/tools/` | Transport-neutral catalog for workspaces, agents, permissions, and automation | @@ -78,8 +79,6 @@ not retain non-Git directories. | `server/provider-probe/` | Invisible runtime-bound workspaces for pre-creation provider discovery | | `server/relay-transport.ts` | Outbound relay connection with E2E encryption | | `server/schedule/` | Cron-based scheduled agents | -| `server/loop-service.ts` | Looping agent runs that retry until an exit condition | -| `server/chat/` | Chat rooms for agent-to-agent and human-to-agent messaging | ### `packages/protocol` — Wire schemas and shared protocol types @@ -114,19 +113,27 @@ Cross-platform React Native app that connects to one or more daemons. - Expo Router navigation (`/h/[serverId]/workspace/[workspaceId]`, `/h/[serverId]/agent/[agentId]`, etc.). The `workspaceId` URL segment is an opaque workspace id, not a directly meaningful filesystem path. - `HostRuntimeController` manages saved host connections, reconnection, and per-host runtime state -- `runtime/replica-cache` keeps a non-authoritative per-host display replica in AsyncStorage: only the last focused agent, its workspace, and a short timeline tail. It restores before navigation becomes ready, leaves remote hydration flags false, and is atomically replaced by the normal snapshot-plus-delta synchronization path. +- `runtime/replica-cache` keeps the complete project, workspace, and active-agent directory plus one short focused timeline tail in AsyncStorage. It restores before navigation becomes ready and leaves remote hydration flags false. +- `runtime/directory-sync` owns directory reconciliation. On reconnect it passes the persisted per-entity cursor through `project.list`, `fetch_workspaces`, and `fetch_agents`; the daemon returns each entity's latest projection when its sequence is newer, plus tombstones. - `SessionContext` wraps the daemon client for the active session - Composer UI and submit/draft behavior live in `packages/app/src/composer/`; screens and panels should integrate it from there instead of dropping composer internals into `components/`, `hooks/`, or `screens/workspace/` - Timeline reducers in `timeline/session-stream-reducers.ts` handle compaction, gap detection, sequence-based deduplication - Timeline sync correctness is documented in [docs/timeline-sync.md](timeline-sync.md): live streams are for immediacy, `fetch_agent_timeline_request` is authoritative, and catch-up is paged but complete. - Voice features: dictation (STT) and voice agent (realtime) -The replica cache exists only to paint stale data immediately while the host connects. It does not -own mutations, infer deletions, or replace daemon reconciliation. Pending permission requests are -not restored from it. AsyncStorage is not encrypted, so the cached timeline tail may contain source -code, prompts, and tool output; encrypted-at-rest storage is a separate product/security decision. -Its serialized payload has a 1 MiB byte budget and evicts whole host snapshots in least-recently- -written order; a single oversized host is omitted rather than partially restored. +The replica cache paints stale data immediately while the host connects. Directory cursors are +reconciliation checkpoints; cached entities remain non-authoritative until the daemon answers. +Pending permission requests are not restored from it. AsyncStorage is not encrypted, so the cached +timeline tail may contain source code, prompts, and tool output; encrypted-at-rest storage is a +separate product/security decision. Its serialized payload has a 32 MiB byte budget and evicts whole +host snapshots in least-recently-written order; a single oversized host is omitted rather than +partially restored. Browser and Electron builds store it in IndexedDB. Native builds use +AsyncStorage, and Android reserves 64 MiB for that database. + +The three directory entity types have independent monotonic sequences and share one daemon +generation. The daemon retains only the latest projection per entity and bounded tombstones, not an +event log. A missing, expired, or previous-generation cursor receives a full snapshot. Projects are +independent records; a project with no workspaces does not need a workspace placeholder. ### `packages/cli` — Command-line client @@ -134,13 +141,11 @@ Commander.js CLI with Docker-style commands. Common agent operations are also ex - `paseo agent ls/run/import/attach/logs/stop/delete/send/inspect/wait/archive/reload/update/mode` - `paseo daemon start/stop/restart/status/pair/set-password` -- `paseo chat ls/create/inspect/post/read/wait/delete` - `paseo terminal ls/create/capture/send-keys/kill` - `paseo script ls/start/stop` -- `paseo loop run/ls/inspect/logs/stop` - `paseo schedule create/ls/inspect/update/pause/resume/run-once/logs/delete` - `paseo heartbeat create/update/delete` -- `paseo workspace create/ls/archive` +- `paseo workspace create/ls/rename/archive` - `paseo permit allow/deny/ls` - `paseo provider ls/models` - hidden legacy `paseo worktree create/ls/archive` compatibility alias @@ -399,9 +404,7 @@ $PASEO_HOME/ ├── projects/projects.json # Project registry ├── projects/workspaces.json # Workspace registry ├── projects/icons/ # Custom project icon images -├── chat/ # Chat rooms ├── schedules/ # Scheduled-agent definitions and runs -├── loops/ # Loop runs and logs ├── config.json # Daemon config (mutable) ├── daemon-keypair.json # Daemon identity for relay/E2EE ├── push-tokens.json # Mobile push tokens diff --git a/docs/custom-providers.md b/docs/custom-providers.md index e31fefe068..924d1bc066 100644 --- a/docs/custom-providers.md +++ b/docs/custom-providers.md @@ -2,7 +2,7 @@ Paseo supports configuring custom agent providers through `config.json` (located at `$PASEO_HOME/config.json`, typically `~/.paseo/config.json`). You can extend built-in providers with different API backends, add ACP-compatible agents, set custom binaries, disable providers, and create multiple profiles for the same underlying provider. -All provider configuration lives under `agents.providers` in config.json: +Provider definitions live under `agents.providers` in config.json: ```json { @@ -17,6 +17,20 @@ All provider configuration lives under `agents.providers` in config.json: Provider IDs must be lowercase alphanumeric with hyphens (`/^[a-z][a-z0-9-]*$/`). +Each provider catalog refresh waits up to 2 minutes. If a provider loads many plugins or a large +agent catalog during startup, raise the limit in milliseconds: + +```json +{ + "agents": { + "catalogRefreshTimeoutMs": 180000 + } +} +``` + +The limit applies independently to every provider refresh and covers availability plus the entire +catalog probe. `PASEO_PROVIDER_REFRESH_TIMEOUT_MS` sets it when the config field is absent. + --- ## Table of Contents @@ -245,6 +259,8 @@ requires_openai_auth = false You can create multiple entries that extend the same built-in provider. Each gets its own entry in the provider list with independent credentials, models, and environment. +"Profile" here means a provider alias, and it is not an **Agent profile** — that is a named bundle of provider, model, mode, thinking option and features, stored under `daemon.agentProfiles`. See [glossary.md](glossary.md) for all four senses of the word. + Example: two different Anthropic accounts as separate profiles: ```json diff --git a/docs/data-model.md b/docs/data-model.md index fefe08990f..b935102be0 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -58,10 +58,6 @@ $PASEO_HOME/ │ └── {agentId}.json # One file per agent ├── schedules/ │ └── {scheduleId}.json # One file per schedule -├── chat/ -│ └── rooms.json # All rooms + messages -├── loops/ -│ └── loops.json # All loop records ├── projects/ │ ├── projects.json # Project registry │ ├── workspaces.json # Workspace registry @@ -93,7 +89,7 @@ Each agent is stored as a separate JSON file, grouped by project directory. | `lastActivityAt` | `string?` (ISO 8601) | Last activity timestamp | | `lastUserMessageAt` | `string?` (ISO 8601) | Last user message timestamp | | `title` | `string?` | User-visible title | -| `labels` | `Record` | Key-value labels (default `{}`). `paseo.parent-agent-id` is set automatically for agent-scoped creation and removed by detach — see [agent-lifecycle.md](./agent-lifecycle.md) | +| `labels` | `Record` | Key-value labels (default `{}`). Paseo uses `paseo.parent-agent-id` for parentage and client-scoped `paseo.open-agent-tab.*` labels while managed subagent tabs are open — see [agent-lifecycle.md](./agent-lifecycle.md) | | `lastStatus` | `AgentStatus` | One of: `"initializing"`, `"idle"`, `"running"`, `"error"`, `"closed"`. `closed` means the record is resumable but has no live provider runtime; archive remains represented separately by `archivedAt`. | | `lastModeId` | `string?` | Last active mode ID | | `config` | `SerializableConfig?` | Agent session configuration (see below) | @@ -104,7 +100,7 @@ Each agent is stored as a separate JSON file, grouped by project directory. | `requiresAttention` | `boolean?` | Whether the agent needs user attention | | `attentionReason` | `"finished" \| "error" \| "permission"?` | Why attention is needed | | `attentionTimestamp` | `string?` (ISO 8601) | When attention was flagged | -| `internal` | `boolean?` | Whether this is a system-internal agent (loop workers, etc.) | +| `internal` | `boolean?` | Whether this is a system-internal agent | | `archivedAt` | `string?` (ISO 8601) | Soft-delete timestamp | ### Nested: SerializableConfig @@ -193,6 +189,8 @@ Single file, validated with `PersistedConfigSchema`. mcp: { enabled: boolean, injectIntoAgents: boolean }, git: { maxProcessesPerSecond: number, maxProcessConcurrency: number }, appendSystemPrompt: string, // appended to supported provider system/developer prompts + terminalProfiles: TerminalProfile[], // named shell commands; omitted means DEFAULT_TERMINAL_PROFILES + agentProfiles: AgentProfile[], // named agent launch bundles; omitted means none cors: { allowedOrigins: string[] }, relay: { enabled: boolean, endpoint: string, publicEndpoint: string, useTls: boolean, publicUseTls: boolean }, // new homes materialize enabled: false auth: { password: string } // bcrypt hash, optional @@ -239,6 +237,21 @@ Single file, validated with `PersistedConfigSchema`. All fields are optional with sensible defaults. +### Profile lists + +`terminalProfiles` and `agentProfiles` are both whole-list fields: a config patch replaces the +array, never merges entries, so a client sends the complete next list on every add, edit, reorder +and remove. List order is the display order. + +Absent and empty mean different things for terminal profiles — omitting the key falls back to +`DEFAULT_TERMINAL_PROFILES`, while `[]` means the user removed them all. Agent profiles have no +defaults, so both mean none. + +`PersistedConfigSchema` parses strictly, so a daemon that predates a field drops it on write +rather than storing something it cannot describe. That is why the client gates the agent profiles +UI on `server_info.features.agentProfiles` instead of letting a save appear to succeed against an +older daemon. + ### Git process limits Git process limits are global to one daemon. The start-rate limit defaults to `64` processes per @@ -362,133 +375,7 @@ One file per schedule. ID is 8 hex characters. --- -## 4. Chat - -**Path:** `$PASEO_HOME/chat/rooms.json` - -Single file containing all rooms and messages. - -```json -{ - "rooms": [ ... ], - "messages": [ ... ] -} -``` - -### ChatRoom - -| Field | Type | Description | -| ----------- | ------------------- | ----------------------------------- | -| `id` | `string` (UUID) | | -| `name` | `string` | Unique room name (case-insensitive) | -| `purpose` | `string?` | Room description | -| `createdAt` | `string` (ISO 8601) | | -| `updatedAt` | `string` (ISO 8601) | Updated on each new message | - -### ChatMessage - -| Field | Type | Description | -| ------------------ | ------------------- | ----------------------------------- | -| `id` | `string` (UUID) | | -| `roomId` | `string` | FK to ChatRoom.id | -| `authorAgentId` | `string` | Agent ID of the author | -| `body` | `string` | Message text (supports `@mentions`) | -| `replyToMessageId` | `string?` | FK to another ChatMessage.id | -| `mentionAgentIds` | `string[]` | Extracted `@mention` agent IDs | -| `createdAt` | `string` (ISO 8601) | | - ---- - -## 5. Loop - -**Path:** `$PASEO_HOME/loops/loops.json` - -Single file containing an array of all loop records. Writes are direct (not atomic) and serialized through an in-memory queue. On daemon startup any record with `status: "running"` is recovered as `"stopped"` with an interruption log entry. - -| Field | Type | Description | -| ----------------------- | --------------------------------------------------- | ------------------------------------------ | -| `id` | `string` | 8-char UUID prefix | -| `name` | `string?` | Human-readable name | -| `prompt` | `string` | Worker prompt | -| `cwd` | `string` | Working directory | -| `provider` | `string` | Default provider | -| `model` | `string?` | Default model | -| `modeId` | `string?` | Default mode ID | -| `workerProvider` | `string?` | Override provider for workers | -| `workerModel` | `string?` | Override model for workers | -| `verifierProvider` | `string?` | Override provider for verifiers | -| `verifierModel` | `string?` | Override model for verifiers | -| `verifierModeId` | `string?` | Override mode ID for verifiers | -| `verifyPrompt` | `string?` | LLM verification prompt | -| `verifyChecks` | `string[]` | Shell commands to run as checks | -| `archive` | `boolean` | Whether to archive worker agents after use | -| `sleepMs` | `number` | Delay between iterations (ms) | -| `maxIterations` | `number?` | Cap on iterations | -| `maxTimeMs` | `number?` | Total time budget (ms) | -| `status` | `"running" \| "succeeded" \| "failed" \| "stopped"` | | -| `createdAt` | `string` (ISO 8601) | | -| `updatedAt` | `string` (ISO 8601) | | -| `startedAt` | `string` (ISO 8601) | | -| `completedAt` | `string?` (ISO 8601) | | -| `stopRequestedAt` | `string?` (ISO 8601) | | -| `iterations` | `LoopIteration[]` | | -| `logs` | `LoopLogEntry[]` | | -| `nextLogSeq` | `number` | Monotonic log sequence counter | -| `activeIteration` | `number?` | Currently executing iteration index | -| `activeWorkerAgentId` | `string?` | Currently running worker agent | -| `activeVerifierAgentId` | `string?` | Currently running verifier agent | - -### Nested: LoopIteration - -| Field | Type | Description | -| ------------------- | --------------------------------------------------- | ------------------------ | -| `index` | `number` | 1-based iteration index | -| `workerAgentId` | `string?` | Agent ID of the worker | -| `workerStartedAt` | `string` (ISO 8601) | | -| `workerCompletedAt` | `string?` (ISO 8601) | | -| `verifierAgentId` | `string?` | Agent ID of the verifier | -| `status` | `"running" \| "succeeded" \| "failed" \| "stopped"` | | -| `workerOutcome` | `"completed" \| "failed" \| "canceled"?` | | -| `failureReason` | `string?` | | -| `verifyChecks` | `LoopVerifyCheckResult[]` | Shell check results | -| `verifyPrompt` | `LoopVerifyPromptResult?` | LLM verification result | - -### Nested: LoopLogEntry - -| Field | Type | -| ----------- | ---------------------------------------------------- | -| `seq` | `number` (monotonic) | -| `timestamp` | `string` (ISO 8601) | -| `iteration` | `number?` | -| `source` | `"loop" \| "worker" \| "verifier" \| "verify-check"` | -| `level` | `"info" \| "error"` | -| `text` | `string` | - -### Nested: LoopVerifyCheckResult - -| Field | Type | -| ------------- | ------------------- | -| `command` | `string` | -| `exitCode` | `number` | -| `passed` | `boolean` | -| `stdout` | `string` | -| `stderr` | `string` | -| `startedAt` | `string` (ISO 8601) | -| `completedAt` | `string` (ISO 8601) | - -### Nested: LoopVerifyPromptResult - -| Field | Type | -| ----------------- | ------------------- | -| `passed` | `boolean` | -| `reason` | `string` | -| `verifierAgentId` | `string?` | -| `startedAt` | `string` (ISO 8601) | -| `completedAt` | `string` (ISO 8601) | - ---- - -## 6. Project Registry +## 4. Project Registry **Path:** `$PASEO_HOME/projects/projects.json` @@ -523,7 +410,7 @@ workspace together with its owning project. --- -## 7. Workspace Registry +## 5. Workspace Registry **Path:** `$PASEO_HOME/projects/workspaces.json` @@ -564,7 +451,7 @@ than treating it as valid. --- -## 8. Push Token Store +## 6. Push Token Store **Path:** `$PASEO_HOME/push-tokens.json` @@ -578,7 +465,7 @@ Simple set of Expo push notification tokens. Loaded with permissive parsing (fil --- -## 9. Daemon meta files +## 7. Daemon meta files These small files are not validated as full Zod schemas but are persisted under `$PASEO_HOME` for daemon identity and runtime coordination. diff --git a/docs/development.md b/docs/development.md index b4c0d3ce1b..b517844bb1 100644 --- a/docs/development.md +++ b/docs/development.md @@ -23,6 +23,10 @@ Desktop dev launches its desktop-managed daemon with `PASEO_NODE_ENV=development so development-only providers such as Mock Load Test are available. Packaged desktop launches always force the daemon to production mode. +The web and desktop dev launchers pass the current Git branch to Metro as +`EXPO_PUBLIC_PASEO_DEV_BUILD_LABEL`. The expanded desktop sidebar shows it in +the titlebar row. Production builds leave the variable unset and show no label. + `npm run dev` is only a shorthand for `npm run dev:server`. Keep `127.0.0.1:6767` for the packaged app and production-style `~/.paseo` state. ## Nix desktop package @@ -127,9 +131,11 @@ desktop-only environment inherited by terminals opened inside Paseo from couplin a new worktree instance to the parent desktop instance's profile or single-instance lock. -Electron remains in the desktop dev runner's process group. Closing or stopping the -workspace-script terminal must terminate Electron with Metro; detaching Electron -leaves an orphan holding the worktree's single-instance lock and broken output pipes. +The desktop workspace script `exec`s the dev runner so the terminal owns the runner +PID. Terminal shutdown reaches the runner as `SIGHUP`; the runner stops Metro and +asks Electron to quit through its normal app lifecycle. Do not add an npm wrapper or +detach Electron: either change leaves an orphan holding the worktree's single-instance +lock and broken output pipes. With desktop dev running, verify the real BrowserWindow, titlebar clearance, fullscreen transition, and 751-pixel settings split with: @@ -302,9 +308,9 @@ For runtime-selected workspaces, lifecycle commands run inside the selected runt resolved from `PATH` on macOS/Linux, and PowerShell with `-NoProfile` on Windows. They inherit the daemon environment plus Paseo's lifecycle variables; login and interactive shell startup files are not loaded, and Bash's `BASH_ENV` -hook is unset. Daemon-run loop verify checks and ACP single-string terminal -commands use the same non-login Bash behavior on macOS/Linux, but preserve their -existing `cmd.exe /c` string semantics on Windows. Service scripts are separate: +hook is unset. ACP single-string terminal commands use the same non-login Bash +behavior on macOS/Linux, but preserve their existing `cmd.exe /c` string semantics +on Windows. Service scripts are separate: they launch in a terminal and receive the service environment described below. Because the shell differs per platform, a lifecycle command that must run @@ -456,7 +462,7 @@ install. Use `npm run cli` to run the in-repo CLI from source (`npx tsx packages/cli/src/index.ts`). The script wraps the CLI with `scripts/dev-home.sh`, so it automatically uses this checkout's `.dev/paseo-home` and dev daemon endpoint unless you pass an explicit override. The globally installed `paseo` binary on macOS is a symlink into the installed Paseo desktop app, not this checkout — use it to drive the desktop's built-in daemon, but use `npm run cli` when you want to talk to the CLI you are editing. -Canonical automation uses `paseo workspace create/ls/archive`, `paseo heartbeat create/update/delete`, and the full `paseo schedule` group. MCP heartbeat automation is intentionally smaller: create and delete only. Detach remains an explicit user lifecycle action rather than an agent tool. `paseo run --new-workspace local|worktree` composes workspace creation with agent creation. The old `paseo worktree` and `paseo run --worktree` forms are hidden compatibility aliases. +Canonical automation uses `paseo workspace create/ls/rename/archive`, `paseo heartbeat create/update/delete`, and the full `paseo schedule` group. MCP heartbeat automation is intentionally smaller: create and delete only. Detach remains an explicit user lifecycle action rather than an agent tool. `paseo run --new-workspace local|worktree` composes workspace creation with agent creation. The old `paseo worktree` and `paseo run --worktree` forms are hidden compatibility aliases. ```bash npm run cli -- ls -a -g # List all agents globally diff --git a/docs/file-observation.md b/docs/file-observation.md index 1418422eaa..f246372f8a 100644 --- a/docs/file-observation.md +++ b/docs/file-observation.md @@ -16,4 +16,6 @@ Read aggregate health from the owning observer service. Runtime metrics include Git owns Git-ignore evaluation. The observer accepts absolute excluded roots and applies updates without replacing the observation or exposing its watcher topology. This keeps tracked files inside otherwise ignored directories observable and keeps Git policy out of the filesystem module. +Workspace Git verifies each repository metadata subscription with a one-shot canary inside the Git directory. If the event does not round-trip through the subscription callback, treat the watcher as unavailable and enter degraded polling. Refresh working-tree Git-ignore exclusions from ignore-file events and watcher recovery, never from a healthy-watcher timer. + The real-filesystem contracts and daemon auto-archive lifecycle run in the normal server test suite. Use the scripts only for manual performance and soak work: `npm run measure:file-observer --workspace=@getpaseo/server` measures burst and sustained-create behavior, and `npm run repro:file-observer-teardown --workspace=@getpaseo/server` runs the teardown soak. diff --git a/docs/glossary.md b/docs/glossary.md index 267a5a6ef7..c4c4c19662 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -26,14 +26,16 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here - **Workspace status bucket** — Aggregate activity signal for a workspace row. Same-`cwd` workspaces intentionally share agent and terminal status buckets, while tab, agent, and terminal visibility remains scoped by `workspaceId`. - **Agent session** — One running instance of an agent inside a workspace (one provider, one model, one cwd, one timeline). The conceptual unit; in the UI this opens as a tab. Moving toward this as the canonical term over "Agent". Code: `AgentSnapshotPayload` (`packages/protocol/src/messages.ts:608`). - **Session** — Two senses: (a) per-client connection to a daemon, internal; (b) user-facing agent session, see **Agent session**. Code: `Session` (`packages/server/src/server/session.ts`) for (a). Don't confuse with: provider-side agent session log. -- **Profile** — Internal name for the persisted shape of a host. Code: `HostProfile` (`packages/app/src/types/host-connection.ts:37`). Never user-facing. +- **Profile** — Internal name for the persisted shape of a host. Code: `HostProfile` (`packages/app/src/types/host-connection.ts:37`). Never user-facing. Three other things are called "profile" in this codebase, so always qualify: **Agent profile**, **Terminal profile**, and the provider aliases in [custom-providers.md](custom-providers.md). Bare "profile" is forbidden in UI copy. +- **Agent profile** — A named launch bundle: provider, model, mode, thinking option, feature values, and free-text notes. Applying one writes those values into the **Agent controls** and is then forgotten — there is no selected profile, no default profile, and no drift tracking. The notes are written for orchestrating agents and are what the `list_profiles` MCP tool surfaces. Stored host-wide in daemon config as `agentProfiles`. UI: "Agent profiles". Code: `AgentProfile` (`packages/protocol/src/messages.ts:161`). Carries no system prompt, because a per-agent system prompt can only be set at creation. Forbidden: "Favourite", which this replaced. +- **Terminal profile** — A named shell command a terminal can launch. Stored host-wide in daemon config as `terminalProfiles`. UI: "Terminal profiles". Code: `TerminalProfile` (`packages/protocol/src/messages.ts:135`), `resolveTerminalProfiles` (`packages/protocol/src/terminal-profiles.ts`). Also selectable as a New Workspace launch target, where the alternative is the chat agent. - **Provider** — Agent backend (Claude Code, Codex, Copilot, OpenCode, Pi, Oh My Pi). UI: "Provider". Code: `ProviderSnapshotEntry` (`packages/protocol/src/messages.ts:198`). - **Model** — A specific LLM offered by a provider. UI: "Model" / "Select model". Code: `AgentModelDefinition` (`packages/protocol/src/messages.ts:187`). - **Tab** — UI surface representing one session inside a workspace. Not a conceptual unit; use **Agent session** when talking about the model. Code: `WorkspaceTabDescriptor` (`packages/app/src/screens/workspace/workspace-tabs-types.ts`). - **Terminal** — Workspace-scoped PTY shell streamed over the binary mux channel. UI: "Terminal". Code: `TerminalStreamFrame` (`packages/protocol/src/terminal-stream-protocol.ts`). - **Workspace script** — A named command a workspace can run, declared in `paseo.json`. Two types: `service` and `script` (a command that runs and finishes). UI: "Scripts" for the panel and the menu, both of which list either type. Code: `WorkspaceScriptPayload` (`packages/protocol/src/messages.ts:3126`). Don't confuse with **Service**, which is one of the two types rather than a synonym for the pair. - **Service** — A workspace script of type `service`: long-running, may bind a port, and carries `health` of `healthy | unhealthy | null` once the daemon has observed it. The only type a sidebar workspace row reports, because a running plain script says nothing about whether the workspace is up; the row names the service and turns danger when its health fails, and where several run the failing one takes the row's single slot. UI: "Services" in the sidebar's display preferences (**Show → Services**). Code: `selectWorkspaceServiceSummary` (`packages/app/src/components/sidebar/workspace-meta-row/service-summary.ts`), `WorkspaceScriptHealthSchema` (`packages/protocol/src/messages.ts:3124`). Forbidden: "Service" as a label for the Scripts panel, which lists both types. -- **Schedule** — Cron-style trigger that creates new agents. UI: CLI/MCP (`paseo schedule`, `create_schedule`). Don't confuse with: Heartbeat (cron prompt back into the same agent) or Loop (iterative re-execution of one agent). +- **Schedule** — Cron-style trigger that creates new agents. UI: CLI/MCP (`paseo schedule`, `create_schedule`). Don't confuse with: Heartbeat (cron prompt back into the same agent). - **Heartbeat** — Ephemeral cron prompt sent back into the same agent/conversation. Agent surfaces expose create, update cron, and delete only. Use for reminders and babysitting where status should return inline. - **Mode** — Provider-specific operational mode (plan, default, full-access, …). UI: icon-only. Code: `modeId` in `AgentSessionConfig` (`packages/protocol/src/messages.ts:257`). - **Attachment** — External or local context bound to an agent prompt: forge issue/change request, review context, uploaded file, text, or image. UI: "Attach issue or PR/MR". Code: `AgentAttachment` (`packages/protocol/src/messages.ts:782`). diff --git a/docs/hub.md b/docs/hub.md index 336b6600cc..de13853218 100644 --- a/docs/hub.md +++ b/docs/hub.md @@ -95,11 +95,11 @@ Hub authentication rejection or close code `4403` permanently revokes the local daemon deletes its credential, stops reconnecting, and retains only the relationship ID, Hub origin, scopes, and a sanitized reason for status reporting. -`paseo hub disconnect` disables socket reconnect before requesting remote revocation. If the Hub is -offline, the daemon persists `disconnecting` and retries revocation across daemon restarts without -opening a Hub socket. This also covers an enrollment whose request may have succeeded but whose -response was lost. `--force` removes local authority immediately and warns that remote revocation may -still be pending. +`paseo hub disconnect` disables socket reconnect and execution authority before making one bounded +remote revocation request. The daemon then removes the local relationship whether the request +succeeds or fails. A failed request returns a warning that server-side revocation may remain pending. +`--force` skips the remote request. Legacy persisted `disconnecting` records are removed on startup; +the daemon does not retry revocation in the background. `paseo hub logout` removes only the active human CLI credential and preserves credentials for other origins. Interactive logout inspects and optionally disconnects a same-origin daemon before deleting the login; a failed requested disconnect preserves the login. JSON and noninteractive logout never prompt or disconnect implicitly. diff --git a/docs/product.md b/docs/product.md index e8a7a553b5..eac71c160c 100644 --- a/docs/product.md +++ b/docs/product.md @@ -12,7 +12,7 @@ The development workflow is shifting from manually editing files to orchestratin Freedom and flexibility. Every design decision follows from this: -- **Multi-provider** — Use any coding agent harness. Pick the right model for each job, switch freely as the landscape shifts. No vendor-lock in. +- **Multi-provider** — Use any coding agent harness. Pick the right model for each job, switch freely as the landscape shifts. No vendor lock-in. - **Cross-device** — Desktop, mobile, web, CLI. Start work at your desk, check progress from your phone, script from the terminal. - **Self-hosted** — The daemon runs on your machine. Your code, your keys, your environment. No inference markup, no cloud dependency. - **Respectful** - No telemetry, no forced cloud, no forced accounts diff --git a/docs/providers.md b/docs/providers.md index 9c1cfe3ae4..5d591e7b51 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -85,6 +85,8 @@ Rewind accepts the canonical wire `messageId` and resolves it to the provider id Submitted user-message wire items carry the same Paseo ID in `messageId` and `clientMessageId`. Provider adapters attach `clientMessageId` only to the echo for that foreground submission; provider history and externally initiated user rows do not have a Paseo client ID. +Provider adapters must terminalize every transient timeline row before emitting the turn's terminal event. Codex may omit the completed `contextCompaction` item when a turn ends during compaction, so its adapter closes any pending root compaction before forwarding `turn_completed`, `turn_failed`, or `turn_canceled`. A terminal turn must never leave the client showing an operation as still loading. + Draft metadata lookups should avoid creating provider sessions when the upstream provider has top-level APIs for that metadata. Prefer `AgentClient.fetchCatalog`, `listCommands`, or `listFeatures` over creating a scratch `AgentSession`; scratch sessions can show up as empty native sessions in provider import/history UIs. `fetchCatalog` is the single discovery API for models and modes — provider implementations may use one process, separate upstream calls, or static data internally, but callers outside the provider do not get separate runtime model/mode probes. Draft command listing and scratch-session feature listing require an explicit draft model. Do not resolve a default model through catalog discovery. A client-level `listFeatures` implementation may return features from an incomplete, model-less draft and owns which features are valid in that state. Provider session import has its own contract. The picker calls `listImportableSessions` and receives rows only: provider handle, cwd, title, prompt previews, and last activity. Import calls `importSession({ providerHandleId, cwd })` for the selected row and must not call listing again. The provider returns the resumed session, storage config, persistence handle, and hydrated timeline for that one native session; `AgentManager.importProviderSession` seeds the daemon timeline and publishes the Paseo agent only after it is ready. @@ -105,6 +107,13 @@ The daemon keeps provider snapshots per bound workspace, with separate legacy cw Provider catalog probes receive a discriminated `FetchCatalogOptions`: `{ scope: "global", force }` for global catalog refreshes, or `{ scope: "workspace", cwd, force }` after the workspace runtime has bound its private placement. Providers decide what global means for their runtime; do not infer global by comparing a cwd to the user's home directory. +`ProviderSnapshotManager` owns one refresh deadline per provider. The deadline starts before the +availability check and covers that check plus the complete catalog probe. Providers that make +multiple catalog requests must not apply this deadline separately to each request. The manager +aborts the shared refresh signal at the deadline. Providers name active catalog operations and +finish subprocess, server, or session cleanup before rejecting. Timeout errors list the operations +that were still active when the deadline expired. + Snapshot reads may probe providers only while the requested cwd scope is cold. Once an entry is warm, its `ready`, `error`, or `unavailable` state stays cached until an explicit refresh. Do not add TTL revalidation, focus-triggered refreshes, selector-open refreshes, or config-reload refreshes. Selector-open refetches may read an already-loading or stale React Query, but they must not force provider probing on their own. Capable clients receive a compact, content-addressed snapshot. Model rows derive their provider from the containing entry and reference snapshot-level thinking sets. The app persists that compact shape per server and workspace identity, or per cwd for the legacy path, then sends its hash on the next pull; an unchanged response carries no catalog body. Keep the legacy encoding for clients without the capability. The hash covers the complete client-visible compact snapshot, including status and `fetchedAt`, so explicit refreshes invalidate it even when the discovered catalog is otherwise equal. diff --git a/docs/refactors/session-decomposition-plan.md b/docs/refactors/session-decomposition-plan.md index ff8bbfab49..8228e6ee10 100644 --- a/docs/refactors/session-decomposition-plan.md +++ b/docs/refactors/session-decomposition-plan.md @@ -19,23 +19,22 @@ The first carves shipped as **deep modules with a narrow Host seam**, not the `d - **#1640 — VoiceSession** (`session/voice/voice-session.ts`, seam `VoiceSessionHost`): the STT/TTS/dictation/turn-detection subsystem. _(Originally landed at `server/voice/`; relocated under `session/` so all session subsystems share one home.)_ - **#1644 — CheckoutSession, read side** (`session/checkout/checkout-session.ts`, seam `CheckoutSessionHost`, port `CheckoutDiffSubscriber`): status, branch validate/suggest, diff subscribe/unsubscribe, manual refresh. The workspace-git observer already delegates `emitStatusUpdate`/`scheduleDiffRefresh` to it. +- **ScheduleSession** (`session/schedule/schedule-session.ts`, seam `ScheduleSessionHost`): scheduled-agent request handling. **Next carve — CheckoutSession mutation side (Slice 4 below).** The 17 checkout _write_ handlers still inline in `session.ts` (`dispatchCheckoutMessage`, ~2010) — branch switch/rename, commit, merge, merge-from-base, pull, push, PR create/merge, github set-auto-merge/get-check-details, PR status, PR timeline, github search, stash save/pop/list — move into the existing `session/checkout/checkout-session.ts` behind `CheckoutSessionHost`. The Slice-3 observer entanglement the table fears is already resolved: #1644 moved the status/diff read side into CheckoutSession, so the workspace observer delegates today and this no longer blocks on the WorkspaceController split. ### Why this is safe at the dispatch seam (verified) -`dispatchInboundMessage` builds `a() ?? b() ?? ... ?? dispatchMiscMessage()` and short-circuits on the first non-`undefined` **Promise object** (not its resolved value). Message-type spaces are **disjoint** (no duplicate `case` labels across switches), so at most one dispatcher matches any message — collapsing to delegation cannot change which handler runs. `dispatchTerminalMessage` (2150-2153) already proves this. Two quirks preserved verbatim: schedule/\* is reached via the chat dispatcher's OWN `default` arm (2183), not the top-level `??`; and `start_workspace_script_request` (a workspace type) is special-cased before terminal delegation (2150). +`dispatchInboundMessage` builds `a() ?? b() ?? ... ?? dispatchMiscMessage()` and short-circuits on the first non-`undefined` **Promise object** (not its resolved value). Message-type spaces are **disjoint** (no duplicate `case` labels across switches), so at most one dispatcher matches any message — collapsing to delegation cannot change which handler runs. `dispatchTerminalMessage` (2150-2153) already proves this. `start_workspace_script_request` (a workspace type) is special-cased before terminal delegation (2150). Rejected alternatives: **feature-module** (free functions + wide context bag) cannot own the live state machines (workspaceUpdatesSubscription, agentUpdatesSubscription, ~25 voice fields) and adds a competing idiom; **mixin-composition** preserves the shared-`this` god object verbatim and requires widening ~325 private fields to protected. ## Slice ordering (least-coupled first) -The task recommended **git/checkout as the first slice — OVERRIDDEN.** Verification: `emitCheckoutStatusUpdate` is called from exactly ONE site (session.ts:4915), inside the workspace-owned `syncWorkspaceGitObserver` callback that ALSO fires workspace effects over shared watch-target maps. Extracting checkout first forces splitting the hardest workspace/git seam before workspace is touched. The strictly safer first cuts are **chat-schedule-loop** (only knot: `handleChatPostRequest`; touches no shared observer/git/voice state) and **provider-catalog** (one shared collaborator + injected predicates). +The task recommended **git/checkout as the first slice — OVERRIDDEN.** Verification: `emitCheckoutStatusUpdate` is called from exactly ONE site (session.ts:4915), inside the workspace-owned `syncWorkspaceGitObserver` callback that ALSO fires workspace effects over shared watch-target maps. Extracting checkout first forces splitting the hardest workspace/git seam before workspace is touched. The safer first cut is **provider-catalog** (one shared collaborator + injected predicates). | # | Slice | Effort | Risk | | --- | ----------------------------------------------------------------------------- | ------ | ------ | -| 0 | Test net + disjointness tripwire (no extraction) | M | low | -| 1 | ChatScheduleLoopController — **STOP FOR REVIEW after green** | M | low | | 2 | ProviderCatalogController | M | medium | | 3 | Split shared workspace-git observer + agent-subscribe fan-out (no controller) | M | high | | 4 | GitCheckoutController | L | medium | @@ -47,34 +46,6 @@ The task recommended **git/checkout as the first slice — OVERRIDDEN.** Verific --- -## Slice 0 — Test net + disjointness tripwire (prerequisite) - -No production code moves. Add `session.dispatch-seam.test.ts`. This is the gate the whole plan rests on, because chat/schedule/loop have **zero** handleMessage coverage today (verified). - -Write RED-then-GREEN against the **current in-place** Session: - -- `chat/post` happy path (asserts `chat/post` response emitted) + fanout-limit error path (asserts the `chat/post` error envelope, NOT a bubbled `rpc_error`). -- one `schedule/*` and one `loop/*` round-trip. -- a handler that throws **synchronously** emits `rpc_error{code:"handler_error"}` + an `activity_log` error frame. -- a handler that **rejects async** emits the SAME pair. -- a table-driven assertion that the union of all controllers' owned-type `ReadonlySet`s is pairwise disjoint and covers the dispatched `SessionInboundMessage` union (grows as controllers land). - -**Tests:** `session.dispatch-seam.test.ts`, `session.test.ts`. - ---- - -## Slice 1 — ChatScheduleLoopController ← STOP FOR HUMAN REVIEW after this ships green - -**Move:** all 21 handlers (`handleChat*` ×7, `handleSchedule*` ×9, `handleLoop*` ×5), the three rpc-error emitters (`emitChatRpcError`/`emitScheduleRpcError`/`emitLoopRpcError` — **kept separate, not merged**), `toScheduleSummary` → `packages/server/src/server/chat/chat-schedule-loop-controller.ts`. Collapse `dispatchChatScheduleLoopMessage` + `dispatchScheduleMessage` to `return this.chatScheduleLoopController.dispatch(msg)`. - -**SessionContext surface:** `emit`, `sessionLogger`, `clientId` (authorAgentId fallback), `chatService`, `scheduleService`, `loopService`, and a narrow agent-control port `{ listAgents, resolveAgentIdentifier, agentStorage.list }` for `handleChatPostRequest` mention fanout. - -**Owned-type set MUST include all 7 `chat/*` + 5 `loop/*` + 9 `schedule/*` types** — schedule/\* is currently routed via the chat dispatcher's own `default` arm, so it must stay inside this one controller, or schedule requests silently no-op. - -**Behavior note:** least-coupled domain. Move the three rpc-error emitters verbatim (they differ in default code + the `ChatServiceError` branch). **Tests:** `session.dispatch-seam.test.ts`, `loop-service.test.ts`, `session.test.ts`. - ---- - ## Slice 2 — ProviderCatalogController **Move:** 7 provider handlers + `emitProviderDisabledResponse` + `getProviderSnapshotEntryForRead` → `packages/server/src/server/provider/provider-catalog-controller.ts`. Move the `providers_snapshot_update` PUSH wiring (1235-1254) into the controller's `start()`/`dispose()`. Collapse `dispatchProviderMessage`. diff --git a/docs/release.md b/docs/release.md index 96ecd3d362..1556de9493 100644 --- a/docs/release.md +++ b/docs/release.md @@ -464,26 +464,37 @@ No prefix (`v`), no extra text. `Release Notes Sync` matches the `## X.Y.Z` (or ## Changelog ownership - **The agent running the release writes the changelog entry — beta or stable.** The release context and final wording stay with that agent. +- **Commit history is only an index of the changes. Never draft the changelog from commit subjects or diffs alone.** For every PR in the release range, read the full PR description and every issue it links to before deciding what changed, why users care, or how changes should be grouped. Use the implementation only to verify the resulting understanding. - For the first beta or a direct stable release, draft from the previous stable tag to the release source. For later betas, draft from the previous beta tag to the release source. Promotion replaces the beta series with one entry drafted from the previous stable tag to the release source. Review the result against the changelog policy below, show it to the user, and wait for approval before committing it. -## Changelog voice +## Changelog wording -The changelog is shown on the Paseo homepage. Write it for **end users**, not developers. +The changelog is shown on the Paseo homepage. Each bullet is a compact factual record of +product behavior that changed. -- **Frame everything from the user's perspective.** Describe what changed in the app, not what changed in the code. Users care that "workspaces load instantly" — not that a component no longer remounts. -- **Never mention component names, internal modules, or implementation details.** No `WorkingIndicator`, no `accumulatedUsage`, no `reconcileAndEmitWorkspaceUpdates`. Also no "virtualized lists", no "remount", no "memoization", no "debounced", no "fuzzy ranking", no "controlled input", no "uncontrolled input" — these are implementation words masquerading as user-facing copy. -- **Concrete WRONG → RIGHT examples** (real mistakes from past releases): +- **Name the exact change.** Prefer `Added `, `Removed `, + `Changed `, or `Fixed when `. +- **Keep the scope exact.** A conditional bug is not a general reliability problem. Do not + broaden one failure into claims that Paseo is now faster, smoother, responsive, or reliable. +- **Use concrete product and runtime terms.** Git polling, persisted cache, provider catalog, + and WebSocket reconnects can identify the affected behavior. Component names, internal + modules, code symbols, and implementation techniques cannot: omit `WorkingIndicator`, + `reconcileAndEmitWorkspaceUpdates`, remounts, memoization, and controlled inputs. +- **State the consequence only when the change itself is unclear.** Keep the condition that + makes the consequence true. Do not replace a precise change with a broad benefit claim. +- **Do not invent context.** Mention an upgrade, platform, workload, or user action only when + the PR or linked issue establishes that scope. - | Wrong (implementation-facing) | Right (user-facing) | - | ----------------------------------------------------------------------------------- | ----------------------------------------------------------- | - | Switching layouts no longer remounts the active agent | Splitting a pane no longer loses your scroll position | - | Model, mode, and thinking pickers — searchable virtualized lists with fuzzy ranking | Mobile model selector is faster and more straightforward | - | Text inputs in mobile sheets no longer flicker while typing fast | Typing in mobile sheets no longer flickers | - | Compact web sheets no longer crash when swiped to dismiss | Sheets on mobile web no longer crash when swiped to dismiss | - | Reduced re-renders in the agent list | Agent list scrolls smoothly | - | Added debouncing to the search input | Search results no longer lag behind typing | +| Avoid | Write | +| ------------------------------------------------------------ | ----------------------------------------------------- | +| Paseo stays responsive with many idle Git workspaces | Removed periodic Git polling for idle workspaces | +| Incompatible saved app data no longer crashes after upgrades | Fixed crash when persisted cache was incompatible | +| Splitting layouts no longer remounts the active agent | Fixed scroll position resetting when splitting a pane | +| Mobile model selector is faster and more straightforward | Added search to the mobile model selector | - Test: would a non-developer reader recognise what changed when using the app? If they'd need an engineer to translate ("what's a remount?"), the bullet is still implementation-facing — rewrite it as the symptom the user experiences. +Test each bullet against the source PR and issue: can a reviewer point to the exact behavior +that changed, the failure that was fixed, or the capability that was added? If the bullet only +claims a general improvement, rewrite it with the concrete change. - **Use the entry's release scope.** Include changes within the matching range in **Changelog scope**. - **Collapse internal iterations within that scope.** Present a feature added and fixed in one range as working. A later beta can describe a fix to behavior delivered in an earlier beta; promotion folds the complete beta series into the final stable behavior. @@ -498,8 +509,7 @@ Every bullet must be scannable at a glance. The changelog is not release documen - **One line per bullet.** If a bullet wraps to three lines in a narrow column, it's too long. - **Split bullets that pack multiple distinct changes.** If a bullet uses "and", "plus", a comma list, or an em-dash to chain several independent improvements, break them into separate bullets — even when they share a theme or author. One bullet = one user-facing change. - **Trim qualifying clauses.** Drop "with a hint shown when…", "matching the CLI's behaviour", "across common install shapes". If the detail doesn't change whether a user cares, cut it. -- **Lead with what the user can do, not the mechanism.** The reader cares about the capability, not how it works under the hood. Do not explain LAN vs WAN, TLS handshakes, IPC, the daemon-relay topology, or any internal concept the user has not asked about. "Self-hosted relays can use a different TLS setting for the public endpoint" — not "Self-hosted relays support a separate TLS setting for the public endpoint, so the daemon can reach the relay over the LAN while the phone reaches it over the public secure address." If a feature genuinely needs background to be understood, it belongs in product docs, with a one-line teaser in the changelog. -- **Lead with the outcome.** "Windows: agents launch reliably from npm `.cmd` shims…" is better than "Windows: agents launch reliably across common install shapes. Claude, Codex, and OpenCode now start correctly…". +- **Stop after identifying the change.** Do not explain LAN/WAN topology, TLS handshakes, IPC, or other architecture in a changelog bullet. Put necessary background in product docs. - **Attribution follows the split.** When you split a dense bullet, move each PR/author to the bullet it belongs to. Never duplicate the same PR across multiple bullets. ## Changelog attribution @@ -567,6 +577,7 @@ Each beta entry records what its testers receive. Promotion produces the single ### Beta release - [ ] The resolved release source is the intended commit (default `origin/main`) and its existing CI is green +- [ ] Every PR in the release range has been opened, and its full description and every linked issue have been read before drafting the changelog - [ ] Add a new `CHANGELOG.md` entry for this beta (heading `## X.Y.Z-beta.N - YYYY-MM-DD`), review it against the changelog policy, get approval, and commit it before cutting the release - [ ] The diff from the previous stable to the resolved release source is classified as patch or minor, with the target version and rationale approved - [ ] Release preparation stayed local until the approved release command pushed the complete branch and tag @@ -587,6 +598,7 @@ Each beta entry records what its testers receive. Promotion produces the single - [ ] Run the pre-release sanity check (see above) and address any findings - [ ] The diff from the previous stable to the resolved release source is classified as patch or minor, with the target version and rationale approved - [ ] The resolved release source is the intended commit (default `origin/main`) and its existing CI is green +- [ ] Every PR in the release range has been opened, and its full description and every linked issue have been read before drafting the changelog - [ ] Ensure the approved release inputs are committed locally and the git worktree is clean before running any release command - [ ] Ensure local `npm run typecheck` passes on that exact commit before running any release command - [ ] Update `CHANGELOG.md` with user-facing release notes (features, fixes — not refactors). Promotion replaces every `## X.Y.Z-beta.N` entry in the series with one `## X.Y.Z - YYYY-MM-DD` entry covering the full release diff --git a/nix/npm-deps.hash b/nix/npm-deps.hash index cb16a408df..ddbe6bd11e 100644 --- a/nix/npm-deps.hash +++ b/nix/npm-deps.hash @@ -1 +1 @@ -sha256-tB0WZG6F6VFMS5zFqo73giSAn7SqfkJISF5fMjDioJQ= +sha256-qPIKchcbGkJHFSqgeM/BUSmu7TiVfKy3IEgUcMNNJYc= diff --git a/package-lock.json b/package-lock.json index 2d2385bc6e..6dcf97b5d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "paseo", - "version": "0.3.1", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "paseo", - "version": "0.3.1", + "version": "0.4.0", "hasInstallScript": true, "license": "AGPL-3.0-or-later", "workspaces": [ @@ -45,8 +45,6 @@ }, "node_modules/@0no-co/graphql.web": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.2.0.tgz", - "integrity": "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw==", "license": "MIT", "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" @@ -59,8 +57,6 @@ }, "node_modules/@agentclientprotocol/sdk": { "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.17.1.tgz", - "integrity": "sha512-yjyIn8POL18IOXioLySYiL0G44kZ/IZctAls7vS3AC3X+qLhFXbWmzABSZehwRnWFShMXT+ODa/HJG1+mGXZ1A==", "license": "Apache-2.0", "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" @@ -68,8 +64,6 @@ }, "node_modules/@ai-sdk/gateway": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.1.tgz", - "integrity": "sha512-vPVIbnP35ZnayS937XLo85vynR85fpBQWHCdUweq7apzqFOTU2YkUd4V3msebEHbQ2Zro60ZShDDy9SMiyWTqA==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "2.0.0", @@ -85,8 +79,6 @@ }, "node_modules/@ai-sdk/provider": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", - "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", "license": "Apache-2.0", "dependencies": { "json-schema": "^0.4.0" @@ -97,8 +89,6 @@ }, "node_modules/@ai-sdk/provider-utils": { "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.12.tgz", - "integrity": "sha512-ZtbdvYxdMoria+2SlNarEk6Hlgyf+zzcznlD55EAl+7VZvJaSg2sqPvwArY7L6TfDEDJsnCq0fdhBSkYo0Xqdg==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "2.0.0", @@ -112,10 +102,21 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@anthropic-ai/sdk": { "version": "0.104.2", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.104.2.tgz", - "integrity": "sha512-s1wEVDAtEwkS7Ajgep6PZKJLFqybRkmD3Byz+iVVsSpbDY0gjROXE9aOft6V3PMqynn3NTcycV5whga9tCzmKA==", "license": "MIT", "dependencies": { "json-schema-to-ts": "^3.1.1", @@ -135,8 +136,6 @@ }, "node_modules/@babel/cli": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.28.6.tgz", - "integrity": "sha512-6EUNcuBbNkj08Oj4gAZ+BUU8yLCgKzgVX4gaTh09Ya2C8ICM4P+G30g4m3akRxSYAp3A/gnWchrNst7px4/nUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -165,8 +164,6 @@ }, "node_modules/@babel/cli/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -176,8 +173,6 @@ }, "node_modules/@babel/cli/node_modules/commander": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", "dev": true, "license": "MIT", "engines": { @@ -186,9 +181,6 @@ }, "node_modules/@babel/cli/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -208,8 +200,6 @@ }, "node_modules/@babel/cli/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -221,8 +211,6 @@ }, "node_modules/@babel/code-frame": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -235,8 +223,6 @@ }, "node_modules/@babel/compat-data": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -244,8 +230,6 @@ }, "node_modules/@babel/core": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -274,8 +258,6 @@ }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -283,8 +265,6 @@ }, "node_modules/@babel/generator": { "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", @@ -299,8 +279,6 @@ }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "license": "MIT", "dependencies": { "@babel/types": "^7.27.3" @@ -311,8 +289,6 @@ }, "node_modules/@babel/helper-compilation-targets": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", @@ -327,8 +303,6 @@ }, "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -336,8 +310,6 @@ }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -345,14 +317,10 @@ }, "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -372,8 +340,6 @@ }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -381,8 +347,6 @@ }, "node_modules/@babel/helper-create-regexp-features-plugin": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", - "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -398,8 +362,6 @@ }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -407,8 +369,6 @@ }, "node_modules/@babel/helper-define-polyfill-provider": { "version": "0.6.7", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.7.tgz", - "integrity": "sha512-6Fqi8MtQ/PweQ9xvux65emkLQ83uB+qAVtfHkC9UodyHMIZdxNI01HjLCLUtybElp2KY2XNE0nOgyP1E1vXw9w==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", @@ -423,8 +383,6 @@ }, "node_modules/@babel/helper-globals": { "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -432,8 +390,6 @@ }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.5", @@ -445,8 +401,6 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.6", @@ -458,8 +412,6 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.28.6", @@ -475,8 +427,6 @@ }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "license": "MIT", "dependencies": { "@babel/types": "^7.27.1" @@ -487,8 +437,6 @@ }, "node_modules/@babel/helper-plugin-utils": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -496,8 +444,6 @@ }, "node_modules/@babel/helper-remap-async-to-generator": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", @@ -513,8 +459,6 @@ }, "node_modules/@babel/helper-replace-supers": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", @@ -530,8 +474,6 @@ }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -543,8 +485,6 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -552,8 +492,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -561,8 +499,6 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -570,8 +506,6 @@ }, "node_modules/@babel/helper-wrap-function": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", - "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", @@ -584,8 +518,6 @@ }, "node_modules/@babel/helpers": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", @@ -597,8 +529,6 @@ }, "node_modules/@babel/highlight": { "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", - "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", @@ -612,8 +542,6 @@ }, "node_modules/@babel/highlight/node_modules/ansi-styles": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -624,8 +552,6 @@ }, "node_modules/@babel/highlight/node_modules/chalk": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -638,8 +564,6 @@ }, "node_modules/@babel/highlight/node_modules/color-convert": { "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -647,14 +571,10 @@ }, "node_modules/@babel/highlight/node_modules/color-name": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, "node_modules/@babel/highlight/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -662,8 +582,6 @@ }, "node_modules/@babel/highlight/node_modules/has-flag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "license": "MIT", "engines": { "node": ">=4" @@ -671,8 +589,6 @@ }, "node_modules/@babel/highlight/node_modules/supports-color": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -683,8 +599,6 @@ }, "node_modules/@babel/parser": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -698,8 +612,6 @@ }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", - "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -715,8 +627,6 @@ }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "dev": true, "license": "MIT", "dependencies": { @@ -731,8 +641,6 @@ }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "dev": true, "license": "MIT", "dependencies": { @@ -747,8 +655,6 @@ }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "dev": true, "license": "MIT", "dependencies": { @@ -765,8 +671,6 @@ }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", - "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", "dev": true, "license": "MIT", "dependencies": { @@ -782,8 +686,6 @@ }, "node_modules/@babel/plugin-proposal-decorators": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz", - "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==", "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", @@ -799,8 +701,6 @@ }, "node_modules/@babel/plugin-proposal-export-default-from": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.27.1.tgz", - "integrity": "sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -814,8 +714,6 @@ }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, "license": "MIT", "engines": { @@ -827,8 +725,6 @@ }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -839,8 +735,6 @@ }, "node_modules/@babel/plugin-syntax-bigint": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -851,8 +745,6 @@ }, "node_modules/@babel/plugin-syntax-class-properties": { "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" @@ -863,8 +755,6 @@ }, "node_modules/@babel/plugin-syntax-class-static-block": { "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -878,8 +768,6 @@ }, "node_modules/@babel/plugin-syntax-decorators": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", - "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -893,8 +781,6 @@ }, "node_modules/@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -905,8 +791,6 @@ }, "node_modules/@babel/plugin-syntax-export-default-from": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.28.6.tgz", - "integrity": "sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -920,8 +804,6 @@ }, "node_modules/@babel/plugin-syntax-flow": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", - "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -935,8 +817,6 @@ }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", - "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", "dev": true, "license": "MIT", "dependencies": { @@ -951,8 +831,6 @@ }, "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", - "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -966,8 +844,6 @@ }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -978,8 +854,6 @@ }, "node_modules/@babel/plugin-syntax-json-strings": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -990,8 +864,6 @@ }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1005,8 +877,6 @@ }, "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -1017,8 +887,6 @@ }, "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1029,8 +897,6 @@ }, "node_modules/@babel/plugin-syntax-numeric-separator": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -1041,8 +907,6 @@ }, "node_modules/@babel/plugin-syntax-object-rest-spread": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1053,8 +917,6 @@ }, "node_modules/@babel/plugin-syntax-optional-catch-binding": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1065,8 +927,6 @@ }, "node_modules/@babel/plugin-syntax-optional-chaining": { "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -1077,8 +937,6 @@ }, "node_modules/@babel/plugin-syntax-private-property-in-object": { "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -1092,8 +950,6 @@ }, "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -1107,8 +963,6 @@ }, "node_modules/@babel/plugin-syntax-typescript": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1122,8 +976,6 @@ }, "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dev": true, "license": "MIT", "dependencies": { @@ -1139,8 +991,6 @@ }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1154,8 +1004,6 @@ }, "node_modules/@babel/plugin-transform-async-generator-functions": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", - "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -1171,8 +1019,6 @@ }, "node_modules/@babel/plugin-transform-async-to-generator": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", - "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.28.6", @@ -1188,8 +1034,6 @@ }, "node_modules/@babel/plugin-transform-block-scoped-functions": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "dev": true, "license": "MIT", "dependencies": { @@ -1204,8 +1048,6 @@ }, "node_modules/@babel/plugin-transform-block-scoping": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", - "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1219,8 +1061,6 @@ }, "node_modules/@babel/plugin-transform-class-properties": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", - "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", @@ -1235,8 +1075,6 @@ }, "node_modules/@babel/plugin-transform-class-static-block": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", - "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", @@ -1251,8 +1089,6 @@ }, "node_modules/@babel/plugin-transform-classes": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", - "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -1271,8 +1107,6 @@ }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", - "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -1287,8 +1121,6 @@ }, "node_modules/@babel/plugin-transform-destructuring": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1303,8 +1135,6 @@ }, "node_modules/@babel/plugin-transform-dotall-regex": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", - "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", "dev": true, "license": "MIT", "dependencies": { @@ -1320,8 +1150,6 @@ }, "node_modules/@babel/plugin-transform-duplicate-keys": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1336,8 +1164,6 @@ }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", "dev": true, "license": "MIT", "dependencies": { @@ -1353,8 +1179,6 @@ }, "node_modules/@babel/plugin-transform-dynamic-import": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", "dev": true, "license": "MIT", "dependencies": { @@ -1369,8 +1193,6 @@ }, "node_modules/@babel/plugin-transform-explicit-resource-management": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", - "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", "dev": true, "license": "MIT", "dependencies": { @@ -1386,8 +1208,6 @@ }, "node_modules/@babel/plugin-transform-exponentiation-operator": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", - "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", "dev": true, "license": "MIT", "dependencies": { @@ -1402,8 +1222,6 @@ }, "node_modules/@babel/plugin-transform-export-namespace-from": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1417,8 +1235,6 @@ }, "node_modules/@babel/plugin-transform-flow-strip-types": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", - "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1433,8 +1249,6 @@ }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1449,8 +1263,6 @@ }, "node_modules/@babel/plugin-transform-function-name": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", @@ -1466,8 +1278,6 @@ }, "node_modules/@babel/plugin-transform-json-strings": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", - "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1482,8 +1292,6 @@ }, "node_modules/@babel/plugin-transform-literals": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1497,8 +1305,6 @@ }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", - "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1512,8 +1318,6 @@ }, "node_modules/@babel/plugin-transform-member-expression-literals": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1528,8 +1332,6 @@ }, "node_modules/@babel/plugin-transform-modules-amd": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "dev": true, "license": "MIT", "dependencies": { @@ -1545,8 +1347,6 @@ }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.28.6", @@ -1561,8 +1361,6 @@ }, "node_modules/@babel/plugin-transform-modules-systemjs": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1580,8 +1378,6 @@ }, "node_modules/@babel/plugin-transform-modules-umd": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "dev": true, "license": "MIT", "dependencies": { @@ -1597,8 +1393,6 @@ }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -1613,8 +1407,6 @@ }, "node_modules/@babel/plugin-transform-new-target": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1629,8 +1421,6 @@ }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", - "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1644,8 +1434,6 @@ }, "node_modules/@babel/plugin-transform-numeric-separator": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", - "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1659,8 +1447,6 @@ }, "node_modules/@babel/plugin-transform-object-rest-spread": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", - "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.28.6", @@ -1678,8 +1464,6 @@ }, "node_modules/@babel/plugin-transform-object-super": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "dev": true, "license": "MIT", "dependencies": { @@ -1695,8 +1479,6 @@ }, "node_modules/@babel/plugin-transform-optional-catch-binding": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", - "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1710,8 +1492,6 @@ }, "node_modules/@babel/plugin-transform-optional-chaining": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", - "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -1726,8 +1506,6 @@ }, "node_modules/@babel/plugin-transform-parameters": { "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1741,8 +1519,6 @@ }, "node_modules/@babel/plugin-transform-private-methods": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", - "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", @@ -1757,8 +1533,6 @@ }, "node_modules/@babel/plugin-transform-private-property-in-object": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", - "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -1774,8 +1548,6 @@ }, "node_modules/@babel/plugin-transform-property-literals": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1790,8 +1562,6 @@ }, "node_modules/@babel/plugin-transform-react-display-name": { "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", - "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1805,8 +1575,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", - "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -1824,8 +1592,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-development": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", - "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", "license": "MIT", "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" @@ -1839,8 +1605,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1854,8 +1618,6 @@ }, "node_modules/@babel/plugin-transform-react-jsx-source": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1869,8 +1631,6 @@ }, "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", - "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", @@ -1885,8 +1645,6 @@ }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", - "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1900,8 +1658,6 @@ }, "node_modules/@babel/plugin-transform-regexp-modifiers": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", - "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", "dev": true, "license": "MIT", "dependencies": { @@ -1917,8 +1673,6 @@ }, "node_modules/@babel/plugin-transform-reserved-words": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", "dev": true, "license": "MIT", "dependencies": { @@ -1933,8 +1687,6 @@ }, "node_modules/@babel/plugin-transform-runtime": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz", - "integrity": "sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.28.6", @@ -1953,8 +1705,6 @@ }, "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1962,8 +1712,6 @@ }, "node_modules/@babel/plugin-transform-shorthand-properties": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1977,8 +1725,6 @@ }, "node_modules/@babel/plugin-transform-spread": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", - "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -1993,8 +1739,6 @@ }, "node_modules/@babel/plugin-transform-sticky-regex": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -2008,8 +1752,6 @@ }, "node_modules/@babel/plugin-transform-template-literals": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -2023,8 +1765,6 @@ }, "node_modules/@babel/plugin-transform-typeof-symbol": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "dev": true, "license": "MIT", "dependencies": { @@ -2039,8 +1779,6 @@ }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", - "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -2058,8 +1796,6 @@ }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "dev": true, "license": "MIT", "dependencies": { @@ -2074,8 +1810,6 @@ }, "node_modules/@babel/plugin-transform-unicode-property-regex": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", - "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", "dev": true, "license": "MIT", "dependencies": { @@ -2091,8 +1825,6 @@ }, "node_modules/@babel/plugin-transform-unicode-regex": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", @@ -2107,8 +1839,6 @@ }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", - "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2124,8 +1854,6 @@ }, "node_modules/@babel/preset-env": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz", - "integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==", "dev": true, "license": "MIT", "dependencies": { @@ -2209,8 +1937,6 @@ }, "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { "version": "0.14.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.1.tgz", - "integrity": "sha512-ENp89vM9Pw4kv/koBb5N2f9bDZsR0hpf3BdPMOg/pkS3pwO4dzNnQZVXtBbeyAadgm865DmQG2jMMLqmZXvuCw==", "dev": true, "license": "MIT", "dependencies": { @@ -2223,8 +1949,6 @@ }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -2233,8 +1957,6 @@ }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dev": true, "license": "MIT", "dependencies": { @@ -2248,8 +1970,6 @@ }, "node_modules/@babel/preset-react": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", - "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -2268,8 +1988,6 @@ }, "node_modules/@babel/preset-typescript": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", - "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -2287,8 +2005,6 @@ }, "node_modules/@babel/runtime": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2296,8 +2012,6 @@ }, "node_modules/@babel/template": { "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.28.6", @@ -2310,8 +2024,6 @@ }, "node_modules/@babel/traverse": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -2329,8 +2041,6 @@ "node_modules/@babel/traverse--for-generate-function-map": { "name": "@babel/traverse", "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -2347,8 +2057,6 @@ }, "node_modules/@babel/types": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -2360,15 +2068,23 @@ }, "node_modules/@blazediff/core": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", - "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==", "dev": true, "license": "MIT" }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, "node_modules/@clack/core": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.1.0.tgz", - "integrity": "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==", "license": "MIT", "dependencies": { "sisteransi": "^1.0.5" @@ -2376,8 +2092,6 @@ }, "node_modules/@clack/prompts": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.1.0.tgz", - "integrity": "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==", "license": "MIT", "dependencies": { "@clack/core": "1.1.0", @@ -2386,8 +2100,6 @@ }, "node_modules/@cloudflare/kv-asset-handler": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", - "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", "license": "MIT OR Apache-2.0", "engines": { "node": ">=22.0.0" @@ -2395,8 +2107,6 @@ }, "node_modules/@cloudflare/unenv-preset": { "version": "2.16.1", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", - "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", "license": "MIT OR Apache-2.0", "peerDependencies": { "unenv": "2.0.0-rc.24", @@ -2410,8 +2120,6 @@ }, "node_modules/@cloudflare/vite-plugin": { "version": "1.36.3", - "resolved": "https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.36.3.tgz", - "integrity": "sha512-n3SZhEZQxk4B2xHaCwgXfc7oLkx/4leTxL254P09DK2Qoj1VVOqVELo62CsUYq5dZS+okMdeWqNa13xEOKMs4g==", "license": "MIT", "dependencies": { "@cloudflare/unenv-preset": "2.16.1", @@ -2427,8 +2135,6 @@ }, "node_modules/@cloudflare/vite-plugin/node_modules/ws": { "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -2446,26 +2152,8 @@ } } }, - "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260507.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260507.1.tgz", - "integrity": "sha512-S85aMwcaPJUjKWDiG6iMMnioKWtPLACa6m0j/EhHR1GYfVpnxb974cBc6d25L+sf7jHWHJI2u5hGp0UTJ7MtXQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, "node_modules/@cloudflare/workerd-darwin-arm64": { "version": "1.20260507.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260507.1.tgz", - "integrity": "sha512-GMEBu8Zp9Q97HLnf7bWJN4KjWpN5MxpeqdvHjBGWNl8UYprJI0k+Jkp89+Wh5S8vIon+HoVbDfOzPa7VwgL6Eg==", "cpu": [ "arm64" ], @@ -2478,66 +2166,14 @@ "node": ">=16" } }, - "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260507.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260507.1.tgz", - "integrity": "sha512-QlrKEBdgA3uVc0Ok0Q3+0/CW0CTjgj5ySir1i1YY5FXVv0X6GpwtnB5umjunjF2MFprss+L+iFGZzxcSvMC1nA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260507.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260507.1.tgz", - "integrity": "sha512-eGbbupEtK2nh9V9Dhcx3vv3GTKeXqSVNgAEYVCCN0NGS9tl9HbMoHRX/4JL181FKXROMigWBCQVL//qPhsAzBQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260507.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260507.1.tgz", - "integrity": "sha512-dmClJ/E0BAcuDetQIZFqbeAXejWrG5pysGRMQ6T83Y0IW/7IAamY2zFEkAJ10I5xwZsdHuYsZtzlOxpEXpJs7A==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, "node_modules/@cloudflare/workers-types": { "version": "4.20260509.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260509.1.tgz", - "integrity": "sha512-jFlTTD+0MK/01TdL5sHIsQ8RqzfmvBsGl4hSp87INv2+JIs/JF6EL9J8enuCz6z3fNdfOKISNbGCIrzZRXVrcw==", "license": "MIT OR Apache-2.0", "optional": true, "peer": true }, "node_modules/@codemirror/autocomplete": { "version": "6.20.3", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", - "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", "license": "MIT", "peer": true, "dependencies": { @@ -2549,8 +2185,6 @@ }, "node_modules/@codemirror/commands": { "version": "6.10.4", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz", - "integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -2561,8 +2195,6 @@ }, "node_modules/@codemirror/language": { "version": "6.12.4", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz", - "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -2575,8 +2207,6 @@ }, "node_modules/@codemirror/legacy-modes": { "version": "6.5.3", - "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.3.tgz", - "integrity": "sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0" @@ -2584,8 +2214,6 @@ }, "node_modules/@codemirror/search": { "version": "6.7.1", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz", - "integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -2595,8 +2223,6 @@ }, "node_modules/@codemirror/state": { "version": "6.7.1", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz", - "integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==", "license": "MIT", "dependencies": { "@marijn/find-cluster-break": "^1.0.0" @@ -2604,8 +2230,6 @@ }, "node_modules/@codemirror/view": { "version": "6.43.6", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz", - "integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.7.0", @@ -2616,8 +2240,6 @@ }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -2628,8 +2250,6 @@ }, "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -2638,8 +2258,6 @@ }, "node_modules/@develar/schema-utils": { "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", "dev": true, "license": "MIT", "dependencies": { @@ -2656,8 +2274,6 @@ }, "node_modules/@develar/schema-utils/node_modules/ajv": { "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2673,8 +2289,6 @@ }, "node_modules/@develar/schema-utils/node_modules/ajv-keywords": { "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2683,15 +2297,11 @@ }, "node_modules/@develar/schema-utils/node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/@dnd-kit/accessibility": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -2702,8 +2312,6 @@ }, "node_modules/@dnd-kit/core": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", "dependencies": { "@dnd-kit/accessibility": "^3.1.1", @@ -2717,8 +2325,6 @@ }, "node_modules/@dnd-kit/sortable": { "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", - "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", "license": "MIT", "dependencies": { "@dnd-kit/utilities": "^3.2.2", @@ -2731,8 +2337,6 @@ }, "node_modules/@dnd-kit/utilities": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -2743,8 +2347,6 @@ }, "node_modules/@egjs/hammerjs": { "version": "2.0.17", - "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", - "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", "license": "MIT", "dependencies": { "@types/hammerjs": "^2.0.36" @@ -2755,8 +2357,6 @@ }, "node_modules/@electron/asar": { "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", - "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", "dev": true, "license": "MIT", "dependencies": { @@ -2773,8 +2373,6 @@ }, "node_modules/@electron/asar/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2784,8 +2382,6 @@ }, "node_modules/@electron/asar/node_modules/commander": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", "dev": true, "license": "MIT", "engines": { @@ -2794,9 +2390,6 @@ }, "node_modules/@electron/asar/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -2816,8 +2409,6 @@ }, "node_modules/@electron/asar/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2829,8 +2420,6 @@ }, "node_modules/@electron/fuses": { "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", - "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", "dev": true, "license": "MIT", "dependencies": { @@ -2844,8 +2433,6 @@ }, "node_modules/@electron/fuses/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2860,8 +2447,6 @@ }, "node_modules/@electron/get": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2882,8 +2467,6 @@ }, "node_modules/@electron/get/node_modules/fs-extra": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", "dependencies": { @@ -2897,8 +2480,6 @@ }, "node_modules/@electron/get/node_modules/jsonfile": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", "optionalDependencies": { @@ -2907,8 +2488,6 @@ }, "node_modules/@electron/get/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -2917,8 +2496,6 @@ }, "node_modules/@electron/get/node_modules/universalify": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", "engines": { @@ -2927,8 +2504,6 @@ }, "node_modules/@electron/notarize": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", "dev": true, "license": "MIT", "dependencies": { @@ -2942,8 +2517,6 @@ }, "node_modules/@electron/notarize/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2958,8 +2531,6 @@ }, "node_modules/@electron/osx-sign": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", - "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2980,8 +2551,6 @@ }, "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", "dev": true, "license": "MIT", "engines": { @@ -2993,8 +2562,6 @@ }, "node_modules/@electron/rebuild": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", - "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", "dev": true, "license": "MIT", "dependencies": { @@ -3021,8 +2588,6 @@ }, "node_modules/@electron/rebuild/node_modules/chownr": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3031,8 +2596,6 @@ }, "node_modules/@electron/rebuild/node_modules/minizlib": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -3044,8 +2607,6 @@ }, "node_modules/@electron/rebuild/node_modules/tar": { "version": "7.5.12", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.12.tgz", - "integrity": "sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -3061,8 +2622,6 @@ }, "node_modules/@electron/rebuild/node_modules/yallist": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3071,8 +2630,6 @@ }, "node_modules/@electron/universal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", - "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", "dev": true, "license": "MIT", "dependencies": { @@ -3090,8 +2647,6 @@ }, "node_modules/@electron/universal/node_modules/fs-extra": { "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", "dev": true, "license": "MIT", "dependencies": { @@ -3105,8 +2660,6 @@ }, "node_modules/@electron/universal/node_modules/minimatch": { "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { @@ -3119,114 +2672,13 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@epic-web/invariant": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", - "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", "dev": true, "license": "MIT" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -3239,346 +2691,8 @@ "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3596,8 +2710,6 @@ }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3609,8 +2721,6 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -3619,8 +2729,6 @@ }, "node_modules/@eslint/config-array": { "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3634,8 +2742,6 @@ }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -3645,8 +2751,6 @@ }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3658,8 +2762,6 @@ }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3671,8 +2773,6 @@ }, "node_modules/@eslint/core": { "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3684,8 +2784,6 @@ }, "node_modules/@eslint/eslintrc": { "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { @@ -3708,8 +2806,6 @@ }, "node_modules/@eslint/eslintrc/node_modules/ajv": { "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -3725,8 +2821,6 @@ }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -3736,15 +2830,11 @@ }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3756,8 +2846,6 @@ }, "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { @@ -3769,8 +2857,6 @@ }, "node_modules/@eslint/js": { "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -3782,8 +2868,6 @@ }, "node_modules/@eslint/object-schema": { "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3792,8 +2876,6 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3806,8 +2888,6 @@ }, "node_modules/@expo/apple-utils": { "version": "2.1.13", - "resolved": "https://registry.npmjs.org/@expo/apple-utils/-/apple-utils-2.1.13.tgz", - "integrity": "sha512-nt3efiJhAWTHl9ikKYrHEuv3dhqCdicsHFRE9LmvtcVsPhXl9bAsm0gbACoLPr7ClP8664H/S6SdVJOD/tw0jg==", "dev": true, "license": "MIT", "bin": { @@ -3816,21 +2896,17 @@ }, "node_modules/@expo/bunyan": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@expo/bunyan/-/bunyan-4.0.1.tgz", - "integrity": "sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==", "dev": true, + "engines": [ + "node >=0.10.0" + ], "license": "MIT", "dependencies": { "uuid": "^8.0.0" - }, - "engines": { - "node": ">=0.10.0" } }, "node_modules/@expo/bunyan/node_modules/uuid": { "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, "license": "MIT", "bin": { @@ -3839,8 +2915,6 @@ }, "node_modules/@expo/cli": { "version": "54.0.23", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.23.tgz", - "integrity": "sha512-km0h72SFfQCmVycH/JtPFTVy69w6Lx1cHNDmfLfQqgKFYeeHTjx7LVDP4POHCtNxFP2UeRazrygJhlh4zz498g==", "license": "MIT", "dependencies": { "@0no-co/graphql.web": "^1.0.8", @@ -3926,8 +3000,6 @@ }, "node_modules/@expo/cli/node_modules/@expo/code-signing-certificates": { "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", - "integrity": "sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==", "license": "MIT", "dependencies": { "node-forge": "^1.3.3" @@ -3935,8 +3007,6 @@ }, "node_modules/@expo/cli/node_modules/@expo/config": { "version": "12.0.13", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.13.tgz", - "integrity": "sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "~7.10.4", @@ -3956,8 +3026,6 @@ }, "node_modules/@expo/cli/node_modules/@expo/config-plugins": { "version": "54.0.4", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.4.tgz", - "integrity": "sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q==", "license": "MIT", "dependencies": { "@expo/config-types": "^54.0.10", @@ -3978,14 +3046,10 @@ }, "node_modules/@expo/cli/node_modules/@expo/config-types": { "version": "54.0.10", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-54.0.10.tgz", - "integrity": "sha512-/J16SC2an1LdtCZ67xhSkGXpALYUVUNyZws7v+PVsFZxClYehDSoKLqyRaGkpHlYrCc08bS0RF5E0JV6g50psA==", "license": "MIT" }, "node_modules/@expo/cli/node_modules/@expo/config/node_modules/@babel/code-frame": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "license": "MIT", "dependencies": { "@babel/highlight": "^7.10.4" @@ -3993,8 +3057,6 @@ }, "node_modules/@expo/cli/node_modules/@expo/env": { "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.11.tgz", - "integrity": "sha512-xV+ps6YCW7XIPVUwFVCRN2nox09dnRwy8uIjwHWTODu0zFw4kp4omnVkl0OOjuu2XOe7tdgAHxikrkJt9xB/7Q==", "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -4006,8 +3068,6 @@ }, "node_modules/@expo/cli/node_modules/@expo/image-utils": { "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.12.tgz", - "integrity": "sha512-3KguH7kyKqq7pNwLb9j6BBdD/bjmNwXZG/HPWT6GWIXbwrvAJt2JNyYTP5agWJ8jbbuys1yuCzmkX+TU6rmI7A==", "license": "MIT", "dependencies": { "@expo/spawn-async": "^1.7.2", @@ -4021,8 +3081,6 @@ }, "node_modules/@expo/cli/node_modules/@expo/json-file": { "version": "10.0.12", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.12.tgz", - "integrity": "sha512-inbDycp1rMAelAofg7h/mMzIe+Owx6F7pur3XdQ3EPTy00tme+4P6FWgHKUcjN8dBSrnbRNpSyh5/shzHyVCyQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -4031,8 +3089,6 @@ }, "node_modules/@expo/cli/node_modules/@expo/osascript": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.4.2.tgz", - "integrity": "sha512-/XP7PSYF2hzOZzqfjgkoWtllyeTN8dW3aM4P6YgKcmmPikKL5FdoyQhti4eh6RK5a5VrUXJTOlTNIpIHsfB5Iw==", "license": "MIT", "dependencies": { "@expo/spawn-async": "^1.7.2" @@ -4043,8 +3099,6 @@ }, "node_modules/@expo/cli/node_modules/@expo/plist": { "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.8.tgz", - "integrity": "sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ==", "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.8.8", @@ -4054,8 +3108,6 @@ }, "node_modules/@expo/cli/node_modules/@expo/prebuild-config": { "version": "54.0.8", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-54.0.8.tgz", - "integrity": "sha512-EA7N4dloty2t5Rde+HP0IEE+nkAQiu4A/+QGZGT9mFnZ5KKjPPkqSyYcRvP5bhQE10D+tvz6X0ngZpulbMdbsg==", "license": "MIT", "dependencies": { "@expo/config": "~12.0.13", @@ -4075,14 +3127,10 @@ }, "node_modules/@expo/cli/node_modules/@react-native/normalize-colors": { "version": "0.81.5", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz", - "integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==", "license": "MIT" }, "node_modules/@expo/cli/node_modules/@urql/core": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@urql/core/-/core-5.2.0.tgz", - "integrity": "sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A==", "license": "MIT", "dependencies": { "@0no-co/graphql.web": "^1.0.13", @@ -4091,8 +3139,6 @@ }, "node_modules/@expo/cli/node_modules/@urql/exchange-retry": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@urql/exchange-retry/-/exchange-retry-1.3.2.tgz", - "integrity": "sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg==", "license": "MIT", "dependencies": { "@urql/core": "^5.1.2", @@ -4104,8 +3150,6 @@ }, "node_modules/@expo/cli/node_modules/@xmldom/xmldom": { "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -4113,8 +3157,6 @@ }, "node_modules/@expo/cli/node_modules/ansi-regex": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "license": "MIT", "engines": { "node": ">=6" @@ -4122,8 +3164,6 @@ }, "node_modules/@expo/cli/node_modules/ansi-styles": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -4134,8 +3174,6 @@ }, "node_modules/@expo/cli/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -4143,8 +3181,6 @@ }, "node_modules/@expo/cli/node_modules/chownr": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -4152,8 +3188,6 @@ }, "node_modules/@expo/cli/node_modules/cli-cursor": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", "license": "MIT", "dependencies": { "restore-cursor": "^2.0.0" @@ -4164,8 +3198,6 @@ }, "node_modules/@expo/cli/node_modules/color-convert": { "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -4173,14 +3205,10 @@ }, "node_modules/@expo/cli/node_modules/color-name": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, "node_modules/@expo/cli/node_modules/commander": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "license": "MIT", "engines": { "node": ">= 6" @@ -4188,8 +3216,6 @@ }, "node_modules/@expo/cli/node_modules/dotenv": { "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -4200,8 +3226,6 @@ }, "node_modules/@expo/cli/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -4209,8 +3233,6 @@ }, "node_modules/@expo/cli/node_modules/getenv": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", - "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", "license": "MIT", "engines": { "node": ">=6" @@ -4218,8 +3240,6 @@ }, "node_modules/@expo/cli/node_modules/glob": { "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "license": "BlueOak-1.0.0", "dependencies": { "minimatch": "^10.2.2", @@ -4235,8 +3255,6 @@ }, "node_modules/@expo/cli/node_modules/glob/node_modules/brace-expansion": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -4247,8 +3265,6 @@ }, "node_modules/@expo/cli/node_modules/glob/node_modules/minimatch": { "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.2" @@ -4262,8 +3278,6 @@ }, "node_modules/@expo/cli/node_modules/has-flag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "license": "MIT", "engines": { "node": ">=4" @@ -4271,8 +3285,6 @@ }, "node_modules/@expo/cli/node_modules/log-symbols": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", "license": "MIT", "dependencies": { "chalk": "^2.0.1" @@ -4283,8 +3295,6 @@ }, "node_modules/@expo/cli/node_modules/log-symbols/node_modules/chalk": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -4297,8 +3307,6 @@ }, "node_modules/@expo/cli/node_modules/lru-cache": { "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -4306,8 +3314,6 @@ }, "node_modules/@expo/cli/node_modules/mimic-fn": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "license": "MIT", "engines": { "node": ">=4" @@ -4315,8 +3321,6 @@ }, "node_modules/@expo/cli/node_modules/minimatch": { "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.2" @@ -4330,8 +3334,6 @@ }, "node_modules/@expo/cli/node_modules/minizlib": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -4342,8 +3344,6 @@ }, "node_modules/@expo/cli/node_modules/node-forge": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -4351,8 +3351,6 @@ }, "node_modules/@expo/cli/node_modules/onetime": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", "license": "MIT", "dependencies": { "mimic-fn": "^1.0.0" @@ -4363,8 +3361,6 @@ }, "node_modules/@expo/cli/node_modules/ora": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", - "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", "license": "MIT", "dependencies": { "chalk": "^2.4.2", @@ -4380,8 +3376,6 @@ }, "node_modules/@expo/cli/node_modules/ora/node_modules/chalk": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -4394,8 +3388,6 @@ }, "node_modules/@expo/cli/node_modules/path-scurry": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -4410,8 +3402,6 @@ }, "node_modules/@expo/cli/node_modules/picomatch": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", "license": "MIT", "engines": { "node": ">=10" @@ -4422,16 +3412,12 @@ }, "node_modules/@expo/cli/node_modules/qrcode-terminal": { "version": "0.11.0", - "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz", - "integrity": "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==", "bin": { "qrcode-terminal": "bin/qrcode-terminal.js" } }, "node_modules/@expo/cli/node_modules/restore-cursor": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", "license": "MIT", "dependencies": { "onetime": "^2.0.0", @@ -4443,14 +3429,10 @@ }, "node_modules/@expo/cli/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/@expo/cli/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "license": "MIT", "engines": { "node": ">=8" @@ -4458,8 +3440,6 @@ }, "node_modules/@expo/cli/node_modules/strip-ansi": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "license": "MIT", "dependencies": { "ansi-regex": "^4.1.0" @@ -4470,8 +3450,6 @@ }, "node_modules/@expo/cli/node_modules/sucrase": { "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -4492,8 +3470,6 @@ }, "node_modules/@expo/cli/node_modules/supports-color": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -4504,8 +3480,6 @@ }, "node_modules/@expo/cli/node_modules/tar": { "version": "7.5.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", - "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -4520,8 +3494,6 @@ }, "node_modules/@expo/cli/node_modules/undici": { "version": "6.23.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", - "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "license": "MIT", "engines": { "node": ">=18.17" @@ -4529,8 +3501,6 @@ }, "node_modules/@expo/cli/node_modules/xmlbuilder": { "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "license": "MIT", "engines": { "node": ">=8.0" @@ -4538,8 +3508,6 @@ }, "node_modules/@expo/cli/node_modules/yallist": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -4547,8 +3515,6 @@ }, "node_modules/@expo/code-signing-certificates": { "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz", - "integrity": "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw==", "dev": true, "license": "MIT", "dependencies": { @@ -4558,8 +3524,6 @@ }, "node_modules/@expo/config": { "version": "10.0.6", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-10.0.6.tgz", - "integrity": "sha512-xXkfPElrtxznkOZxFASJ7OPa6E9IHSjcZwj5BQ6XUF2dz5M7AFa2h5sXM8AalSaDU5tEBSgoUOjTh5957TlR8g==", "dev": true, "license": "MIT", "dependencies": { @@ -4580,8 +3544,6 @@ }, "node_modules/@expo/config-plugins": { "version": "9.0.12", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-9.0.12.tgz", - "integrity": "sha512-/Ko/NM+GzvJyRkq8PITm8ms0KY5v0wmN1OQFYRMkcJqOi3PjlhndW+G6bHpJI9mkQXBaUnHwAiGLqIC3+MQ5Wg==", "dev": true, "license": "MIT", "dependencies": { @@ -4603,8 +3565,6 @@ }, "node_modules/@expo/config-plugins/node_modules/@babel/code-frame": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "license": "MIT", "dependencies": { @@ -4613,8 +3573,6 @@ }, "node_modules/@expo/config-plugins/node_modules/@expo/json-file": { "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-9.0.2.tgz", - "integrity": "sha512-yAznIUrybOIWp3Uax7yRflB0xsEpvIwIEqIjao9SGi2Gaa+N0OamWfe0fnXBSWF+2zzF4VvqwT4W5zwelchfgw==", "dev": true, "license": "MIT", "dependencies": { @@ -4625,8 +3583,6 @@ }, "node_modules/@expo/config-plugins/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", "engines": { @@ -4635,15 +3591,11 @@ }, "node_modules/@expo/config-types": { "version": "52.0.5", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-52.0.5.tgz", - "integrity": "sha512-AMDeuDLHXXqd8W+0zSjIt7f37vUd/BP8p43k68NHpyAvQO+z8mbQZm3cNQVAMySeayK2XoPigAFB1JF2NFajaA==", "dev": true, "license": "MIT" }, "node_modules/@expo/config/node_modules/@babel/code-frame": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "license": "MIT", "dependencies": { @@ -4652,8 +3604,6 @@ }, "node_modules/@expo/config/node_modules/@expo/json-file": { "version": "9.1.5", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-9.1.5.tgz", - "integrity": "sha512-prWBhLUlmcQtvN6Y7BpW2k9zXGd3ySa3R6rAguMJkp1z22nunLN64KYTUWfijFlprFoxm9r2VNnGkcbndAlgKA==", "dev": true, "license": "MIT", "dependencies": { @@ -4663,8 +3613,6 @@ }, "node_modules/@expo/devcert": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@expo/devcert/-/devcert-1.2.1.tgz", - "integrity": "sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==", "license": "MIT", "dependencies": { "@expo/sudo-prompt": "^9.3.1", @@ -4673,8 +3621,6 @@ }, "node_modules/@expo/devcert/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "license": "MIT", "dependencies": { "ms": "^2.1.1" @@ -4682,8 +3628,6 @@ }, "node_modules/@expo/devtools": { "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@expo/devtools/-/devtools-0.1.8.tgz", - "integrity": "sha512-SVLxbuanDjJPgc0sy3EfXUMLb/tXzp6XIHkhtPVmTWJAp+FOr6+5SeiCfJrCzZFet0Ifyke2vX3sFcKwEvCXwQ==", "license": "MIT", "dependencies": { "chalk": "^4.1.2" @@ -4703,8 +3647,6 @@ }, "node_modules/@expo/eas-build-job": { "version": "1.0.243", - "resolved": "https://registry.npmjs.org/@expo/eas-build-job/-/eas-build-job-1.0.243.tgz", - "integrity": "sha512-wLU9q0pzIKibr4jLMV/U6BYszk5T85p/WmRYUIqH7nhmy2dDUJHz0WlP/i/ZWAsnrHplwidbCZvZGoxwgJsISA==", "dev": true, "license": "MIT", "dependencies": { @@ -4716,8 +3658,6 @@ }, "node_modules/@expo/eas-build-job/node_modules/joi": { "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4730,8 +3670,6 @@ }, "node_modules/@expo/eas-json": { "version": "16.30.0", - "resolved": "https://registry.npmjs.org/@expo/eas-json/-/eas-json-16.30.0.tgz", - "integrity": "sha512-GeWdhJTPcOh7xhox+ymsUYqsrPTB+dTyjaKdvdhZNHoNYwZADEEq2DyDHUJJPiUbPvld03qbg/mrSaVMYhxP9w==", "dev": true, "license": "MIT", "dependencies": { @@ -4753,8 +3691,6 @@ }, "node_modules/@expo/eas-json/node_modules/@babel/code-frame": { "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "license": "MIT", "dependencies": { @@ -4767,8 +3703,6 @@ }, "node_modules/@expo/eas-json/node_modules/@babel/code-frame/node_modules/chalk": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4782,8 +3716,6 @@ }, "node_modules/@expo/eas-json/node_modules/@expo/eas-build-job": { "version": "1.0.231", - "resolved": "https://registry.npmjs.org/@expo/eas-build-job/-/eas-build-job-1.0.231.tgz", - "integrity": "sha512-jktsZ7IJYcC9lbgUWftbFCpfrhcGwJX4q0qSubUrqvFTMuUyLjCzJqyIZ4znbXLXfJSUPDJNAd4H0wBqKJA9kA==", "dev": true, "license": "MIT", "dependencies": { @@ -4795,8 +3727,6 @@ }, "node_modules/@expo/eas-json/node_modules/@expo/eas-build-job/node_modules/joi": { "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4809,8 +3739,6 @@ }, "node_modules/@expo/eas-json/node_modules/@expo/eas-build-job/node_modules/semver": { "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -4822,8 +3750,6 @@ }, "node_modules/@expo/eas-json/node_modules/ansi-styles": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "license": "MIT", "dependencies": { @@ -4835,8 +3761,6 @@ }, "node_modules/@expo/eas-json/node_modules/color-convert": { "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "license": "MIT", "dependencies": { @@ -4845,15 +3769,11 @@ }, "node_modules/@expo/eas-json/node_modules/color-name": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true, "license": "MIT" }, "node_modules/@expo/eas-json/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", "engines": { @@ -4862,8 +3782,6 @@ }, "node_modules/@expo/eas-json/node_modules/fs-extra": { "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, "license": "MIT", "dependencies": { @@ -4877,8 +3795,6 @@ }, "node_modules/@expo/eas-json/node_modules/has-flag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "license": "MIT", "engines": { @@ -4887,8 +3803,6 @@ }, "node_modules/@expo/eas-json/node_modules/semver": { "version": "7.5.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", - "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", "dev": true, "license": "ISC", "dependencies": { @@ -4903,8 +3817,6 @@ }, "node_modules/@expo/eas-json/node_modules/supports-color": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "license": "MIT", "dependencies": { @@ -4916,15 +3828,11 @@ }, "node_modules/@expo/eas-json/node_modules/tslib": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", "dev": true, "license": "0BSD" }, "node_modules/@expo/env": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-1.0.7.tgz", - "integrity": "sha512-qSTEnwvuYJ3umapO9XJtrb1fAqiPlmUUg78N0IZXXGwQRt+bkp0OBls+Y5Mxw/Owj8waAM0Z3huKKskRADR5ow==", "dev": true, "license": "MIT", "dependencies": { @@ -4937,8 +3845,6 @@ }, "node_modules/@expo/env/node_modules/dotenv": { "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -4950,8 +3856,6 @@ }, "node_modules/@expo/env/node_modules/getenv": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", - "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", "dev": true, "license": "MIT", "engines": { @@ -4960,8 +3864,6 @@ }, "node_modules/@expo/fingerprint": { "version": "0.15.4", - "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.15.4.tgz", - "integrity": "sha512-eYlxcrGdR2/j2M6pEDXo9zU9KXXF1vhP+V+Tl+lyY+bU8lnzrN6c637mz6Ye3em2ANy8hhUR03Raf8VsT9Ogng==", "license": "MIT", "dependencies": { "@expo/spawn-async": "^1.7.2", @@ -4982,8 +3884,6 @@ }, "node_modules/@expo/fingerprint/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -4991,8 +3891,6 @@ }, "node_modules/@expo/fingerprint/node_modules/getenv": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", - "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", "license": "MIT", "engines": { "node": ">=6" @@ -5000,8 +3898,6 @@ }, "node_modules/@expo/fingerprint/node_modules/glob": { "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "license": "BlueOak-1.0.0", "dependencies": { "minimatch": "^10.2.2", @@ -5017,8 +3913,6 @@ }, "node_modules/@expo/fingerprint/node_modules/glob/node_modules/brace-expansion": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -5029,8 +3923,6 @@ }, "node_modules/@expo/fingerprint/node_modules/glob/node_modules/minimatch": { "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.2" @@ -5044,8 +3936,6 @@ }, "node_modules/@expo/fingerprint/node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "license": "MIT", "engines": { "node": ">= 4" @@ -5053,8 +3943,6 @@ }, "node_modules/@expo/fingerprint/node_modules/lru-cache": { "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -5062,8 +3950,6 @@ }, "node_modules/@expo/fingerprint/node_modules/minimatch": { "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.2" @@ -5077,8 +3963,6 @@ }, "node_modules/@expo/fingerprint/node_modules/path-scurry": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -5093,8 +3977,6 @@ }, "node_modules/@expo/image-utils": { "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.6.5.tgz", - "integrity": "sha512-RsS/1CwJYzccvlprYktD42KjyfWZECH6PPIEowvoSmXfGLfdViwcUEI4RvBfKX5Jli6P67H+6YmHvPTbGOboew==", "dev": true, "license": "MIT", "dependencies": { @@ -5112,8 +3994,6 @@ }, "node_modules/@expo/image-utils/node_modules/fs-extra": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.0.tgz", - "integrity": "sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g==", "dev": true, "license": "MIT", "dependencies": { @@ -5128,8 +4008,6 @@ }, "node_modules/@expo/image-utils/node_modules/universalify": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", - "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", "dev": true, "license": "MIT", "engines": { @@ -5138,8 +4016,6 @@ }, "node_modules/@expo/json-file": { "version": "8.3.3", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-8.3.3.tgz", - "integrity": "sha512-eZ5dld9AD0PrVRiIWpRkm5aIoWBw3kAyd8VkuWEy92sEthBKDDDHAnK2a0dw0Eil6j7rK7lS/Qaq/Zzngv2h5A==", "dev": true, "license": "MIT", "dependencies": { @@ -5150,8 +4026,6 @@ }, "node_modules/@expo/json-file/node_modules/@babel/code-frame": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "license": "MIT", "dependencies": { @@ -5160,8 +4034,6 @@ }, "node_modules/@expo/logger": { "version": "1.0.221", - "resolved": "https://registry.npmjs.org/@expo/logger/-/logger-1.0.221.tgz", - "integrity": "sha512-arNkhzq4Ludz9J7PIYWTVYXuT1bO/A8prR8HYAviKuF6fEZfitemNVriGSetQASOE8Vnv3lmBZv6YIwLiFuv/Q==", "dev": true, "license": "BUSL-1.1", "dependencies": { @@ -5171,8 +4043,6 @@ }, "node_modules/@expo/metro": { "version": "54.2.0", - "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-54.2.0.tgz", - "integrity": "sha512-h68TNZPGsk6swMmLm9nRSnE2UXm48rWwgcbtAHVMikXvbxdS41NDHHeqg1rcQ9AbznDRp6SQVC2MVpDnsRKU1w==", "license": "MIT", "dependencies": { "metro": "0.83.3", @@ -5193,8 +4063,6 @@ }, "node_modules/@expo/metro-config": { "version": "54.0.14", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.14.tgz", - "integrity": "sha512-hxpLyDfOR4L23tJ9W1IbJJsG7k4lv2sotohBm/kTYyiG+pe1SYCAWsRmgk+H42o/wWf/HQjE5k45S5TomGLxNA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -5230,8 +4098,6 @@ }, "node_modules/@expo/metro-config/node_modules/@expo/config": { "version": "12.0.13", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.13.tgz", - "integrity": "sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "~7.10.4", @@ -5251,8 +4117,6 @@ }, "node_modules/@expo/metro-config/node_modules/@expo/config-plugins": { "version": "54.0.4", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.4.tgz", - "integrity": "sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q==", "license": "MIT", "dependencies": { "@expo/config-types": "^54.0.10", @@ -5273,14 +4137,10 @@ }, "node_modules/@expo/metro-config/node_modules/@expo/config-types": { "version": "54.0.10", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-54.0.10.tgz", - "integrity": "sha512-/J16SC2an1LdtCZ67xhSkGXpALYUVUNyZws7v+PVsFZxClYehDSoKLqyRaGkpHlYrCc08bS0RF5E0JV6g50psA==", "license": "MIT" }, "node_modules/@expo/metro-config/node_modules/@expo/config/node_modules/@babel/code-frame": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "license": "MIT", "dependencies": { "@babel/highlight": "^7.10.4" @@ -5288,8 +4148,6 @@ }, "node_modules/@expo/metro-config/node_modules/@expo/env": { "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.11.tgz", - "integrity": "sha512-xV+ps6YCW7XIPVUwFVCRN2nox09dnRwy8uIjwHWTODu0zFw4kp4omnVkl0OOjuu2XOe7tdgAHxikrkJt9xB/7Q==", "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -5301,8 +4159,6 @@ }, "node_modules/@expo/metro-config/node_modules/@expo/json-file": { "version": "10.0.12", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.12.tgz", - "integrity": "sha512-inbDycp1rMAelAofg7h/mMzIe+Owx6F7pur3XdQ3EPTy00tme+4P6FWgHKUcjN8dBSrnbRNpSyh5/shzHyVCyQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -5311,8 +4167,6 @@ }, "node_modules/@expo/metro-config/node_modules/@expo/plist": { "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.8.tgz", - "integrity": "sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ==", "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.8.8", @@ -5322,8 +4176,6 @@ }, "node_modules/@expo/metro-config/node_modules/@xmldom/xmldom": { "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -5331,8 +4183,6 @@ }, "node_modules/@expo/metro-config/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -5340,8 +4190,6 @@ }, "node_modules/@expo/metro-config/node_modules/commander": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "license": "MIT", "engines": { "node": ">= 6" @@ -5349,8 +4197,6 @@ }, "node_modules/@expo/metro-config/node_modules/dotenv": { "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -5361,8 +4207,6 @@ }, "node_modules/@expo/metro-config/node_modules/getenv": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", - "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", "license": "MIT", "engines": { "node": ">=6" @@ -5370,8 +4214,6 @@ }, "node_modules/@expo/metro-config/node_modules/glob": { "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "license": "BlueOak-1.0.0", "dependencies": { "minimatch": "^10.2.2", @@ -5387,8 +4229,6 @@ }, "node_modules/@expo/metro-config/node_modules/glob/node_modules/brace-expansion": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -5399,8 +4239,6 @@ }, "node_modules/@expo/metro-config/node_modules/glob/node_modules/minimatch": { "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.2" @@ -5414,8 +4252,6 @@ }, "node_modules/@expo/metro-config/node_modules/lru-cache": { "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -5423,8 +4259,6 @@ }, "node_modules/@expo/metro-config/node_modules/minimatch": { "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.2" @@ -5438,8 +4272,6 @@ }, "node_modules/@expo/metro-config/node_modules/path-scurry": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -5454,8 +4286,6 @@ }, "node_modules/@expo/metro-config/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "license": "MIT", "engines": { "node": ">=8" @@ -5463,8 +4293,6 @@ }, "node_modules/@expo/metro-config/node_modules/sucrase": { "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -5485,8 +4313,6 @@ }, "node_modules/@expo/metro-config/node_modules/xmlbuilder": { "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "license": "MIT", "engines": { "node": ">=8.0" @@ -5494,8 +4320,6 @@ }, "node_modules/@expo/metro-runtime": { "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-6.1.2.tgz", - "integrity": "sha512-nvM+Qv45QH7pmYvP8JB1G8JpScrWND3KrMA6ZKe62cwwNiX/BjHU28Ear0v/4bQWXlOY0mv6B8CDIm8JxXde9g==", "license": "MIT", "dependencies": { "anser": "^1.4.9", @@ -5517,8 +4341,6 @@ }, "node_modules/@expo/multipart-body-parser": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@expo/multipart-body-parser/-/multipart-body-parser-2.0.0.tgz", - "integrity": "sha512-yS/wsqlj0d8ZKETEN7ro3dZtjdMhpte8wp+xUzjUQC3jizxcE0E62xgvGquJObiYUMGoCF5qRYr2t78STPEaSw==", "dev": true, "license": "MIT", "dependencies": { @@ -5527,8 +4349,6 @@ }, "node_modules/@expo/npm-proofread": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@expo/npm-proofread/-/npm-proofread-1.0.1.tgz", - "integrity": "sha512-yDyBlNIgg+rKoKCOVM9ZyAtgqRx6gHw+JtmZTrYYW4auAfwS6kE/iInmCpVXPqRy0OhJHgXj6q5PCgWELLVMeg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5540,8 +4360,6 @@ }, "node_modules/@expo/npm-proofread/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -5550,8 +4368,6 @@ }, "node_modules/@expo/osascript": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.1.4.tgz", - "integrity": "sha512-LcPjxJ5FOFpqPORm+5MRLV0CuYWMthJYV6eerF+lQVXKlvgSn3EOqaHC3Vf3H+vmB0f6G4kdvvFtg40vG4bIhA==", "dev": true, "license": "MIT", "dependencies": { @@ -5564,8 +4380,6 @@ }, "node_modules/@expo/package-manager": { "version": "1.9.10", - "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.9.10.tgz", - "integrity": "sha512-axJm+NOj3jVxep49va/+L3KkF3YW/dkV+RwzqUJedZrv4LeTqOG4rhrCaCPXHTvLqCTDKu6j0Xyd28N7mnxsGA==", "license": "MIT", "dependencies": { "@expo/json-file": "^10.0.8", @@ -5578,8 +4392,6 @@ }, "node_modules/@expo/package-manager/node_modules/@expo/json-file": { "version": "10.0.12", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.12.tgz", - "integrity": "sha512-inbDycp1rMAelAofg7h/mMzIe+Owx6F7pur3XdQ3EPTy00tme+4P6FWgHKUcjN8dBSrnbRNpSyh5/shzHyVCyQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -5588,8 +4400,6 @@ }, "node_modules/@expo/package-manager/node_modules/ansi-regex": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "license": "MIT", "engines": { "node": ">=6" @@ -5597,8 +4407,6 @@ }, "node_modules/@expo/package-manager/node_modules/ansi-styles": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -5609,8 +4417,6 @@ }, "node_modules/@expo/package-manager/node_modules/cli-cursor": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", "license": "MIT", "dependencies": { "restore-cursor": "^2.0.0" @@ -5621,8 +4427,6 @@ }, "node_modules/@expo/package-manager/node_modules/color-convert": { "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -5630,14 +4434,10 @@ }, "node_modules/@expo/package-manager/node_modules/color-name": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, "node_modules/@expo/package-manager/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -5645,8 +4445,6 @@ }, "node_modules/@expo/package-manager/node_modules/has-flag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "license": "MIT", "engines": { "node": ">=4" @@ -5654,8 +4452,6 @@ }, "node_modules/@expo/package-manager/node_modules/log-symbols": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", "license": "MIT", "dependencies": { "chalk": "^2.0.1" @@ -5666,8 +4462,6 @@ }, "node_modules/@expo/package-manager/node_modules/log-symbols/node_modules/chalk": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -5680,8 +4474,6 @@ }, "node_modules/@expo/package-manager/node_modules/mimic-fn": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "license": "MIT", "engines": { "node": ">=4" @@ -5689,8 +4481,6 @@ }, "node_modules/@expo/package-manager/node_modules/onetime": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", "license": "MIT", "dependencies": { "mimic-fn": "^1.0.0" @@ -5701,8 +4491,6 @@ }, "node_modules/@expo/package-manager/node_modules/ora": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", - "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", "license": "MIT", "dependencies": { "chalk": "^2.4.2", @@ -5718,8 +4506,6 @@ }, "node_modules/@expo/package-manager/node_modules/ora/node_modules/chalk": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -5732,8 +4518,6 @@ }, "node_modules/@expo/package-manager/node_modules/restore-cursor": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", "license": "MIT", "dependencies": { "onetime": "^2.0.0", @@ -5745,14 +4529,10 @@ }, "node_modules/@expo/package-manager/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/@expo/package-manager/node_modules/strip-ansi": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "license": "MIT", "dependencies": { "ansi-regex": "^4.1.0" @@ -5763,8 +4543,6 @@ }, "node_modules/@expo/package-manager/node_modules/supports-color": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -5775,8 +4553,6 @@ }, "node_modules/@expo/pkcs12": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@expo/pkcs12/-/pkcs12-0.1.3.tgz", - "integrity": "sha512-96MePEGppKi08vawrTPw8kMCRdsbrDbV900MlI8rrP9F57DfDl/y1P52bwIDBYCEHE3XtPMo7s1xkG0BKOLCVg==", "dev": true, "license": "MIT", "dependencies": { @@ -5785,8 +4561,6 @@ }, "node_modules/@expo/plist": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.2.0.tgz", - "integrity": "sha512-F/IZJQaf8OIVnVA6XWUeMPC3OH6MV00Wxf0WC0JhTQht2QgjyHUa3U5Gs3vRtDq8tXNsZneOQRDVwpaOnd4zTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5797,8 +4571,6 @@ }, "node_modules/@expo/plugin-help": { "version": "5.1.23", - "resolved": "https://registry.npmjs.org/@expo/plugin-help/-/plugin-help-5.1.23.tgz", - "integrity": "sha512-s0uH6cPplLj73ZVie40EYUhl7X7q9kRR+8IfZWDod3wUtVGOFInxuCPX9Jpv1UwwBgbRu2cLisqr8m45LrFgxw==", "dev": true, "license": "MIT", "dependencies": { @@ -5810,8 +4582,6 @@ }, "node_modules/@expo/plugin-help/node_modules/@oclif/core": { "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@oclif/core/-/core-2.16.0.tgz", - "integrity": "sha512-dL6atBH0zCZl1A1IXCKJgLPrM/wR7K+Wi401E/IvqsK8m2iCHW+0TEOGrans/cuN3oTW+uxIyJFHJ8Im0k4qBw==", "dev": true, "license": "MIT", "dependencies": { @@ -5850,8 +4620,6 @@ }, "node_modules/@expo/plugin-help/node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", "engines": { @@ -5860,8 +4628,6 @@ }, "node_modules/@expo/plugin-help/node_modules/js-yaml": { "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -5874,8 +4640,6 @@ }, "node_modules/@expo/plugin-warn-if-update-available": { "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@expo/plugin-warn-if-update-available/-/plugin-warn-if-update-available-2.5.1.tgz", - "integrity": "sha512-B65QSIZ+TgFHnVXsTw+1Q6djsJByWwnIjYfoG8ZV9wizOC01gbAw1cOZ/YtrJ2BrDnzFQtM8qecjlmZ7C3MPLw==", "dev": true, "license": "MIT", "dependencies": { @@ -5894,8 +4658,6 @@ }, "node_modules/@expo/plugin-warn-if-update-available/node_modules/@oclif/core": { "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@oclif/core/-/core-2.16.0.tgz", - "integrity": "sha512-dL6atBH0zCZl1A1IXCKJgLPrM/wR7K+Wi401E/IvqsK8m2iCHW+0TEOGrans/cuN3oTW+uxIyJFHJ8Im0k4qBw==", "dev": true, "license": "MIT", "dependencies": { @@ -5934,8 +4696,6 @@ }, "node_modules/@expo/plugin-warn-if-update-available/node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", "engines": { @@ -5944,8 +4704,6 @@ }, "node_modules/@expo/plugin-warn-if-update-available/node_modules/js-yaml": { "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -5958,8 +4716,6 @@ }, "node_modules/@expo/prebuild-config": { "version": "8.0.17", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-8.0.17.tgz", - "integrity": "sha512-HM+XpDox3fAZuXZXvy55VRcBbsZSDijGf8jI8i/pexgWvtsnt1ouelPXRuE1pXDicMX+lZO83QV+XkyLmBEXYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5978,8 +4734,6 @@ }, "node_modules/@expo/prebuild-config/node_modules/@babel/code-frame": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "license": "MIT", "dependencies": { @@ -5988,8 +4742,6 @@ }, "node_modules/@expo/prebuild-config/node_modules/@expo/json-file": { "version": "9.1.5", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-9.1.5.tgz", - "integrity": "sha512-prWBhLUlmcQtvN6Y7BpW2k9zXGd3ySa3R6rAguMJkp1z22nunLN64KYTUWfijFlprFoxm9r2VNnGkcbndAlgKA==", "dev": true, "license": "MIT", "dependencies": { @@ -5999,15 +4751,11 @@ }, "node_modules/@expo/prebuild-config/node_modules/@react-native/normalize-colors": { "version": "0.76.2", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.76.2.tgz", - "integrity": "sha512-ICoOpaTLPsFQjNLSM00NgQr6wal300cZZonHVSDXKntX+BfkLeuCHRtr/Mn+klTtW+/1v2/2FRm9dXjvyGf9Dw==", "dev": true, "license": "MIT" }, "node_modules/@expo/prebuild-config/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6022,8 +4770,6 @@ }, "node_modules/@expo/results": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@expo/results/-/results-1.0.0.tgz", - "integrity": "sha512-qECzzXX5oJot3m2Gu9pfRDz50USdBieQVwYAzeAtQRUTD3PVeTK1tlRUoDcrK8PSruDLuVYdKkLebX4w/o55VA==", "dev": true, "license": "MIT", "engines": { @@ -6032,8 +4778,6 @@ }, "node_modules/@expo/rudder-sdk-node": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@expo/rudder-sdk-node/-/rudder-sdk-node-1.1.1.tgz", - "integrity": "sha512-uy/hS/awclDJ1S88w9UGpc6Nm9XnNUjzOAAib1A3PVAnGQIwebg8DpFqOthFBTlZxeuV/BKbZ5jmTbtNZkp1WQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6051,8 +4795,6 @@ }, "node_modules/@expo/rudder-sdk-node/node_modules/uuid": { "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, "license": "MIT", "bin": { @@ -6061,20 +4803,14 @@ }, "node_modules/@expo/schema-utils": { "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-0.1.8.tgz", - "integrity": "sha512-9I6ZqvnAvKKDiO+ZF8BpQQFYWXOJvTAL5L/227RUbWG1OVZDInFifzCBiqAZ3b67NRfeAgpgvbA7rejsqhY62A==", "license": "MIT" }, "node_modules/@expo/sdk-runtime-versions": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz", - "integrity": "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==", "license": "MIT" }, "node_modules/@expo/spawn-async": { "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.7.2.tgz", - "integrity": "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3" @@ -6085,8 +4821,6 @@ }, "node_modules/@expo/steps": { "version": "1.0.231", - "resolved": "https://registry.npmjs.org/@expo/steps/-/steps-1.0.231.tgz", - "integrity": "sha512-o7Wj6W32S4H4geWC0b3RL5finHmiPT+e8SMP4/4Il2R0oc9er8jLXaj1tQ3fTZvpnlqoMWcFn5fAvaGnxfYDPA==", "dev": true, "license": "BUSL-1.1", "dependencies": { @@ -6109,8 +4843,6 @@ }, "node_modules/@expo/steps/node_modules/@expo/eas-build-job": { "version": "1.0.231", - "resolved": "https://registry.npmjs.org/@expo/eas-build-job/-/eas-build-job-1.0.231.tgz", - "integrity": "sha512-jktsZ7IJYcC9lbgUWftbFCpfrhcGwJX4q0qSubUrqvFTMuUyLjCzJqyIZ4znbXLXfJSUPDJNAd4H0wBqKJA9kA==", "dev": true, "license": "MIT", "dependencies": { @@ -6122,8 +4854,6 @@ }, "node_modules/@expo/steps/node_modules/fs-extra": { "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", "dev": true, "license": "MIT", "dependencies": { @@ -6137,8 +4867,6 @@ }, "node_modules/@expo/steps/node_modules/joi": { "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6151,21 +4879,15 @@ }, "node_modules/@expo/sudo-prompt": { "version": "9.3.2", - "resolved": "https://registry.npmjs.org/@expo/sudo-prompt/-/sudo-prompt-9.3.2.tgz", - "integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==", "license": "MIT" }, "node_modules/@expo/timeago.js": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@expo/timeago.js/-/timeago.js-1.0.0.tgz", - "integrity": "sha512-PD45CGlCL8kG0U3YcH1NvYxQThw5XAS7qE9bgP4L7dakm8lsMz+p8BQ1IjBFMmImawVWsV3py6JZINaEebXLnw==", "dev": true, "license": "MIT" }, "node_modules/@expo/vector-icons": { "version": "15.1.1", - "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.1.1.tgz", - "integrity": "sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==", "license": "MIT", "peerDependencies": { "expo-font": ">=14.0.4", @@ -6175,14 +4897,10 @@ }, "node_modules/@expo/ws-tunnel": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-1.0.6.tgz", - "integrity": "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==", "license": "MIT" }, "node_modules/@expo/xcpretty": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.1.tgz", - "integrity": "sha512-KZNxZvnGCtiM2aYYZ6Wz0Ix5r47dAvpNLApFtZWnSoERzAdOMzVBOPysBoM0JlF6FKWZ8GPqgn6qt3dV/8Zlpg==", "license": "BSD-3-Clause", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -6195,8 +4913,6 @@ }, "node_modules/@floating-ui/core": { "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.11" @@ -6204,8 +4920,6 @@ }, "node_modules/@floating-ui/react-native": { "version": "0.10.9", - "resolved": "https://registry.npmjs.org/@floating-ui/react-native/-/react-native-0.10.9.tgz", - "integrity": "sha512-8lLF/hrfCLPSeMljMbFi/cxVjtFTZyg1XHFiuZtXirbBiEfTvCgD0a1YUo2fpG4wa9Fa8Nz4HSq4TmdV+JuxwQ==", "license": "MIT", "dependencies": { "@floating-ui/core": "^1.0.0" @@ -6217,8 +4931,6 @@ }, "node_modules/@floating-ui/utils": { "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, "node_modules/@getpaseo/app": { @@ -6275,8 +4987,6 @@ }, "node_modules/@gorhom/bottom-sheet": { "version": "5.2.14", - "resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-5.2.14.tgz", - "integrity": "sha512-uLQFlDjp9z+jrOFcMSEldPqL5JdaXL3vXOh+juhwoNvXgTsEorJLjHTugXu+YccAG/0KJnShzKCrb71MHBsvJg==", "license": "MIT", "dependencies": { "@gorhom/portal": "1.0.14", @@ -6301,8 +5011,6 @@ }, "node_modules/@gorhom/portal": { "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@gorhom/portal/-/portal-1.0.14.tgz", - "integrity": "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==", "license": "MIT", "dependencies": { "nanoid": "^3.3.1" @@ -6314,8 +5022,6 @@ }, "node_modules/@hapi/address": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", - "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6327,36 +5033,26 @@ }, "node_modules/@hapi/address/node_modules/@hapi/hoek": { "version": "11.0.7", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", - "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@hapi/formula": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", - "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@hapi/hoek": { "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@hapi/pinpoint": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", - "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@hapi/tlds": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", - "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -6365,8 +5061,6 @@ }, "node_modules/@hapi/topo": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6375,8 +5069,6 @@ }, "node_modules/@hono/node-server": { "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -6387,8 +5079,6 @@ }, "node_modules/@humanfs/core": { "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6397,8 +5087,6 @@ }, "node_modules/@humanfs/node": { "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -6411,8 +5099,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6425,8 +5111,6 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6437,16 +5121,29 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@ide/backoff": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz", - "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==", "license": "MIT" }, "node_modules/@img/colour": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "engines": { "node": ">=18" @@ -6454,8 +5151,6 @@ }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], @@ -6474,32 +5169,8 @@ "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, "node_modules/@img/sharp-libvips-darwin-arm64": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], @@ -6512,406 +5183,8 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, "license": "ISC", "dependencies": { @@ -6928,8 +5201,6 @@ }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -6941,8 +5212,6 @@ }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -6954,15 +5223,11 @@ }, "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { @@ -6979,8 +5244,6 @@ }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { @@ -6995,8 +5258,6 @@ }, "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7013,8 +5274,6 @@ }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -7025,8 +5284,6 @@ }, "node_modules/@isaacs/ttlcache": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", - "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", "license": "ISC", "engines": { "node": ">=12" @@ -7034,8 +5291,6 @@ }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "license": "ISC", "dependencies": { "camelcase": "^5.3.1", @@ -7050,8 +5305,6 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "license": "MIT", "engines": { "node": ">=6" @@ -7059,8 +5312,6 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -7072,8 +5323,6 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -7085,8 +5334,6 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -7097,8 +5344,6 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -7112,8 +5357,6 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -7124,8 +5367,6 @@ }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "license": "MIT", "engines": { "node": ">=8" @@ -7133,8 +5374,6 @@ }, "node_modules/@jest/console": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, "license": "MIT", "dependencies": { @@ -7151,8 +5390,6 @@ }, "node_modules/@jest/console/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", "engines": { @@ -7161,8 +5398,6 @@ }, "node_modules/@jest/create-cache-key-function": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", - "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", "license": "MIT", "dependencies": { "@jest/types": "^29.6.3" @@ -7173,8 +5408,6 @@ }, "node_modules/@jest/diff-sequences": { "version": "30.4.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", - "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", "dev": true, "license": "MIT", "engines": { @@ -7183,8 +5416,6 @@ }, "node_modules/@jest/environment": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "license": "MIT", "dependencies": { "@jest/fake-timers": "^29.7.0", @@ -7198,8 +5429,6 @@ }, "node_modules/@jest/expect": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7212,8 +5441,6 @@ }, "node_modules/@jest/expect-utils": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, "license": "MIT", "dependencies": { @@ -7225,8 +5452,6 @@ }, "node_modules/@jest/fake-timers": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -7242,8 +5467,6 @@ }, "node_modules/@jest/get-type": { "version": "30.1.0", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", - "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", "dev": true, "license": "MIT", "engines": { @@ -7252,8 +5475,6 @@ }, "node_modules/@jest/globals": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7268,8 +5489,6 @@ }, "node_modules/@jest/schemas": { "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -7280,8 +5499,6 @@ }, "node_modules/@jest/test-result": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, "license": "MIT", "dependencies": { @@ -7296,8 +5513,6 @@ }, "node_modules/@jest/transform": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -7322,14 +5537,10 @@ }, "node_modules/@jest/transform/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/@jest/transform/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "license": "MIT", "engines": { "node": ">=8" @@ -7337,8 +5548,6 @@ }, "node_modules/@jest/transform/node_modules/write-file-atomic": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -7350,8 +5559,6 @@ }, "node_modules/@jest/types": { "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -7367,8 +5574,6 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -7377,8 +5582,6 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -7387,8 +5590,6 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -7396,8 +5597,6 @@ }, "node_modules/@jridgewell/source-map": { "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -7406,14 +5605,10 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -7422,14 +5617,10 @@ }, "node_modules/@lezer/common": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", - "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", "license": "MIT" }, "node_modules/@lezer/cpp": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.5.tgz", - "integrity": "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -7439,8 +5630,6 @@ }, "node_modules/@lezer/css": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.1.tgz", - "integrity": "sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -7450,8 +5639,6 @@ }, "node_modules/@lezer/go": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz", - "integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -7461,8 +5648,6 @@ }, "node_modules/@lezer/highlight": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", - "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "license": "MIT", "dependencies": { "@lezer/common": "^1.3.0" @@ -7470,8 +5655,6 @@ }, "node_modules/@lezer/html": { "version": "1.3.13", - "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", - "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -7481,8 +5664,6 @@ }, "node_modules/@lezer/java": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz", - "integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -7492,8 +5673,6 @@ }, "node_modules/@lezer/javascript": { "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", - "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -7503,8 +5682,6 @@ }, "node_modules/@lezer/json": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", - "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -7514,8 +5691,6 @@ }, "node_modules/@lezer/lr": { "version": "1.4.8", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", - "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" @@ -7523,8 +5698,6 @@ }, "node_modules/@lezer/markdown": { "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz", - "integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==", "license": "MIT", "dependencies": { "@lezer/common": "^1.5.0", @@ -7533,8 +5706,6 @@ }, "node_modules/@lezer/php": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.5.tgz", - "integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -7544,8 +5715,6 @@ }, "node_modules/@lezer/python": { "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", - "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -7555,8 +5724,6 @@ }, "node_modules/@lezer/rust": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz", - "integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -7566,8 +5733,6 @@ }, "node_modules/@lezer/xml": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", - "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -7577,8 +5742,6 @@ }, "node_modules/@lezer/yaml": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", - "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -7588,8 +5751,6 @@ }, "node_modules/@malept/cross-spawn-promise": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", "dev": true, "funding": [ { @@ -7611,8 +5772,6 @@ }, "node_modules/@malept/flatpak-bundler": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7627,8 +5786,6 @@ }, "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7643,8 +5800,6 @@ }, "node_modules/@mapbox/node-pre-gyp": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", - "integrity": "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7665,8 +5820,6 @@ }, "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -7675,8 +5828,6 @@ }, "node_modules/@mapbox/node-pre-gyp/node_modules/chownr": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -7685,8 +5836,6 @@ }, "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { @@ -7699,8 +5848,6 @@ }, "node_modules/@mapbox/node-pre-gyp/node_modules/minizlib": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -7712,8 +5859,6 @@ }, "node_modules/@mapbox/node-pre-gyp/node_modules/tar": { "version": "7.5.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", - "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -7729,8 +5874,6 @@ }, "node_modules/@mapbox/node-pre-gyp/node_modules/yallist": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -7739,20 +5882,33 @@ }, "node_modules/@marijn/find-cluster-break": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", - "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "license": "MIT" }, + "node_modules/@mattermost/react-native-paste-input": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@mattermost/react-native-paste-input/-/react-native-paste-input-2.0.1.tgz", + "integrity": "sha512-Q9zjXvjwxVQ1M8ylxr9Y+wUpG7xb1wW4u+I34oPvF3etTyBrGtVyA4Bt7Xphgc29eW6wQFUxTIbzZc3E6W2MQg==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": ">=0.76.0" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz", + "integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, "node_modules/@mixmark-io/domino": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", - "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", "license": "BSD-2-Clause" }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9", @@ -7791,8 +5947,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { "mime-types": "^3.0.0", @@ -7804,8 +5958,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -7820,8 +5972,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/ajv-formats": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -7837,8 +5987,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", @@ -7861,8 +6009,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "license": "MIT", "engines": { "node": ">=18" @@ -7874,8 +6020,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", "engines": { "node": ">=18" @@ -7887,8 +6031,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", "engines": { "node": ">=6.6.0" @@ -7896,8 +6038,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/express": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", @@ -7939,8 +6079,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -7960,8 +6098,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -7969,8 +6105,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -7985,8 +6119,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -7994,8 +6126,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", "engines": { "node": ">=18" @@ -8006,8 +6136,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -8022,8 +6150,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -8031,8 +6157,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -8046,8 +6170,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -8061,8 +6183,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/send": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", "dependencies": { "debug": "^4.4.3", @@ -8087,8 +6207,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -8106,8 +6224,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { "content-type": "^2.0.0", @@ -8124,8 +6240,6 @@ }, "node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "license": "MIT", "engines": { "node": ">=18" @@ -8135,35 +6249,14 @@ "url": "https://opencollective.com/express" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, "node_modules/@nicolo-ribaudo/chokidar-2": { "version": "2.1.8-no-fsevents.3", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", - "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", "dev": true, "license": "MIT", "optional": true }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { @@ -8176,8 +6269,6 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -8186,8 +6277,6 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { @@ -8200,8 +6289,6 @@ }, "node_modules/@nolyfill/is-core-module": { "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", "dev": true, "license": "MIT", "engines": { @@ -8210,8 +6297,6 @@ }, "node_modules/@npmcli/agent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", "dev": true, "license": "ISC", "dependencies": { @@ -8227,8 +6312,6 @@ }, "node_modules/@npmcli/agent/node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -8237,8 +6320,6 @@ }, "node_modules/@npmcli/agent/node_modules/http-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { @@ -8251,8 +6332,6 @@ }, "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { @@ -8265,15 +6344,11 @@ }, "node_modules/@npmcli/agent/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, "node_modules/@npmcli/fs": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", "dev": true, "license": "ISC", "dependencies": { @@ -8285,8 +6360,6 @@ }, "node_modules/@oclif/core": { "version": "1.26.2", - "resolved": "https://registry.npmjs.org/@oclif/core/-/core-1.26.2.tgz", - "integrity": "sha512-6jYuZgXvHfOIc9GIaS4T3CIKGTjPmfAxuMcbCbMRKJJl4aq/4xeRlEz0E8/hz8HxvxZBGvN2GwAUHlrGWQVrVw==", "dev": true, "license": "MIT", "dependencies": { @@ -8325,8 +6398,6 @@ }, "node_modules/@oclif/core/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8341,8 +6412,6 @@ }, "node_modules/@oclif/core/node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", "engines": { @@ -8351,8 +6420,6 @@ }, "node_modules/@oclif/core/node_modules/js-yaml": { "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -8365,15 +6432,11 @@ }, "node_modules/@oclif/linewrap": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@oclif/linewrap/-/linewrap-1.0.0.tgz", - "integrity": "sha512-Ups2dShK52xXa8w6iBWLgcjPJWjais6KPJQq3gQ/88AY6BXoTX+MIGFPrWQO1KLMiQfoTpcLnUwloN4brrVUHw==", "dev": true, "license": "ISC" }, "node_modules/@oclif/plugin-autocomplete": { "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-2.3.10.tgz", - "integrity": "sha512-Ow1AR8WtjzlyCtiWWPgzMyT8SbcDJFr47009riLioHa+MHX2BCDtVn2DVnN/E6b9JlPV5ptQpjefoRSNWBesmg==", "dev": true, "license": "MIT", "dependencies": { @@ -8387,8 +6450,6 @@ }, "node_modules/@oclif/plugin-autocomplete/node_modules/@oclif/core": { "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@oclif/core/-/core-2.16.0.tgz", - "integrity": "sha512-dL6atBH0zCZl1A1IXCKJgLPrM/wR7K+Wi401E/IvqsK8m2iCHW+0TEOGrans/cuN3oTW+uxIyJFHJ8Im0k4qBw==", "dev": true, "license": "MIT", "dependencies": { @@ -8427,8 +6488,6 @@ }, "node_modules/@oclif/plugin-autocomplete/node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", "engines": { @@ -8437,8 +6496,6 @@ }, "node_modules/@oclif/plugin-autocomplete/node_modules/js-yaml": { "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -8451,9 +6508,6 @@ }, "node_modules/@oclif/screen": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@oclif/screen/-/screen-3.0.8.tgz", - "integrity": "sha512-yx6KAqlt3TAHBduS2fMQtJDL2ufIHnDRArrJEOoTTuizxqmjLT+psGYOHpmMl3gvQpFJ11Hs76guUUktzAF9Bg==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dev": true, "license": "MIT", "engines": { @@ -8462,8 +6516,6 @@ }, "node_modules/@oozcitak/dom": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-2.0.2.tgz", - "integrity": "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==", "license": "MIT", "dependencies": { "@oozcitak/infra": "^2.0.2", @@ -8476,8 +6528,6 @@ }, "node_modules/@oozcitak/infra": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-2.0.2.tgz", - "integrity": "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==", "license": "MIT", "dependencies": { "@oozcitak/util": "^10.0.0" @@ -8488,8 +6538,6 @@ }, "node_modules/@oozcitak/url": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-3.0.0.tgz", - "integrity": "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==", "license": "MIT", "dependencies": { "@oozcitak/infra": "^2.0.2", @@ -8501,8 +6549,6 @@ }, "node_modules/@oozcitak/util": { "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-10.0.0.tgz", - "integrity": "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==", "license": "MIT", "engines": { "node": ">=20.0" @@ -8510,45 +6556,13 @@ }, "node_modules/@opentelemetry/api": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", "engines": { "node": ">=8.0.0" } }, - "node_modules/@oxc-resolver/binding-android-arm-eabi": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.19.1.tgz", - "integrity": "sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@oxc-resolver/binding-android-arm64": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.19.1.tgz", - "integrity": "sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, "node_modules/@oxc-resolver/binding-darwin-arm64": { "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.19.1.tgz", - "integrity": "sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==", "cpu": [ "arm64" ], @@ -8559,285 +6573,8 @@ "darwin" ] }, - "node_modules/@oxc-resolver/binding-darwin-x64": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.19.1.tgz", - "integrity": "sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxc-resolver/binding-freebsd-x64": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.19.1.tgz", - "integrity": "sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.19.1.tgz", - "integrity": "sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.19.1.tgz", - "integrity": "sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.19.1.tgz", - "integrity": "sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-arm64-musl": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.19.1.tgz", - "integrity": "sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.19.1.tgz", - "integrity": "sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.19.1.tgz", - "integrity": "sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.19.1.tgz", - "integrity": "sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.19.1.tgz", - "integrity": "sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-x64-gnu": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.19.1.tgz", - "integrity": "sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-linux-x64-musl": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.19.1.tgz", - "integrity": "sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxc-resolver/binding-openharmony-arm64": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.19.1.tgz", - "integrity": "sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@oxc-resolver/binding-wasm32-wasi": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.19.1.tgz", - "integrity": "sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.19.1.tgz", - "integrity": "sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oxc-resolver/binding-win32-ia32-msvc": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.19.1.tgz", - "integrity": "sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oxc-resolver/binding-win32-x64-msvc": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.19.1.tgz", - "integrity": "sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.46.0.tgz", - "integrity": "sha512-b1doV4WRcJU+BESSlCvCjV+5CEr/T6h0frArAdV26Nir+gGNFNaylvDiiMPfF1pxeV0txZEs38ojzJaxBYg+ng==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.46.0.tgz", - "integrity": "sha512-v6+HhjsoV3GO0u2u9jLSAZrvWfTraDxKofUIQ7/ktS7tzS+epVsxdHmeM+XxuNcAY/nWxxU1Sg4JcGTNRXraBA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@oxfmt/binding-darwin-arm64": { "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.46.0.tgz", - "integrity": "sha512-3eeooJGrqGIlI5MyryDZsAcKXSmKIgAD4yYtfRrRJzXZ0UTFZtiSveIur56YPrGMYZwT4XyVhHsMqrNwr1XeFA==", "cpu": [ "arm64" ], @@ -8851,282 +6588,8 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.46.0.tgz", - "integrity": "sha512-QG8BDM0CXWbu84k2SKmCqfEddPQPFiBicwtYnLqHRWZZl57HbtOLRMac/KTq2NO4AEc4ICCBpFxJIV9zcqYfkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.46.0.tgz", - "integrity": "sha512-9DdCqS/n2ncu/Chazvt3cpgAjAmIGQDz7hFKSrNItMApyV/Ja9mz3hD4JakIE3nS8PW9smEbPWnb389QLBY4nw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.46.0.tgz", - "integrity": "sha512-Dgs7VeE2jT0LHMhw6tPEt0xQYe54kBqHEovmWsv4FVQlegCOvlIJNx0S8n4vj8WUtpT+Z6BD2HhKJPLglLxvZg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.46.0.tgz", - "integrity": "sha512-Zxn3adhTH13JKnU4xXJj8FeEfF680XjXh3gSShKl57HCMBRde2tUJTgogV/1MSHA80PJEVrDa7r66TLVq3Ia7Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.46.0.tgz", - "integrity": "sha512-+TWipjrgVM8D7aIdDD0tlr3teLTTvQTn7QTE5BpT10H1Fj82gfdn9X6nn2sDgx/MepuSCfSnzFNJq2paLL0OiA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.46.0.tgz", - "integrity": "sha512-aAUPBWJ1lGwwnxZUEDLJ94+Iy6MuwJwPxUgO4sCA5mEEyDk7b+cDQ+JpX1VR150Zoyd+D49gsrUzpUK5h587Eg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.46.0.tgz", - "integrity": "sha512-ufBCJukyFX/UDrokP/r6BGDoTInnsDs7bxyzKAgMiZlt2Qu8GPJSJ6Zm6whIiJzKk0naxA8ilwmbO1LMw6Htxw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.46.0.tgz", - "integrity": "sha512-eqtlC2YmPqjun76R1gVfGLuKWx7NuEnLEAudZ7n6ipSKbCZTqIKSs1b5Y8K/JHZsRpLkeSmAAjig5HOIg8fQzQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.46.0.tgz", - "integrity": "sha512-yccVOO2nMXkQLGgy0He3EQEwKD7NF0zEk+/OWmroznkqXyJdN6bfK0LtNnr6/14Bh3FjpYq7bP33l/VloCnxpA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.46.0.tgz", - "integrity": "sha512-aAf7fG23OQCey6VRPj9IeCraoYtpgtx0ZyJ1CXkPyT1wjzBE7c3xtuxHe/AdHaJfVVb/SXpSk8Gl1LzyQupSqw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.46.0.tgz", - "integrity": "sha512-q0JPsTMyJNjYrBvYFDz4WbVsafNZaPCZv4RnFypRotLqpKROtBZcEaXQW4eb9YmvLU3NckVemLJnzkSZSdmOxw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.46.0.tgz", - "integrity": "sha512-7LsLY9Cw57GPkhSR+duI3mt9baRczK/DtHYSldQ4BEU92da9igBQNl4z7Vq5U9NNPsh1FmpKvv1q9WDtiUQR1A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.46.0.tgz", - "integrity": "sha512-lHiBOz8Duaku7JtRNLlps3j++eOaICPZSd8FCVmTDM4DFOPT71Bjn7g6iar1z7StXlKRweUKxWUs4sA+zWGDXg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.46.0.tgz", - "integrity": "sha512-/5ktYUliP89RhgC37DBH1x20U5zPSZMy3cMEcO0j3793rbHP9MWsknBwQB6eozRzWmYrh0IFM/p20EbPvDlYlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.46.0.tgz", - "integrity": "sha512-3WTnoiuIr8XvV0DIY7SN+1uJSwKf4sPpcbHfobcRT9JutGcLaef/miyBB87jxd3aqH+mS0+G5lsgHuXLUwjjpQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.46.0.tgz", - "integrity": "sha512-IXxiQpkYnOwNfP23vzwSfhdpxJzyiPTY7eTn6dn3DsriKddESzM8i6kfq9R7CD/PUJwCvQT22NgtygBeug3KoA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@oxlint-tsgolint/darwin-arm64": { "version": "0.22.1", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-0.22.1.tgz", - "integrity": "sha512-4150Lpgc1YM09GcjA6GSrra1JoPjC7aOpfywLjWEY4vW0Sd1qKzqHF1WRaiw0/qUZ40OATYdv3aRd7ipPkWQbw==", "cpu": [ "arm64" ], @@ -9137,114 +6600,8 @@ "darwin" ] }, - "node_modules/@oxlint-tsgolint/darwin-x64": { - "version": "0.22.1", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-0.22.1.tgz", - "integrity": "sha512-vFWcPWYOgZs4HWcgS1EjUZg33NLcNfEYU49KGImmCfZWkflENrmBYV4HN/C0YeAPum6ZZ/goPSvQrB/cOD+NfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oxlint-tsgolint/linux-arm64": { - "version": "0.22.1", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-0.22.1.tgz", - "integrity": "sha512-6LiUpP0Zir3+29FvBm7Y28q/dBjSHqTZ5MhG1Ckw4fGhI4cAvbcwXaKvbjx1TP7rRmBNOoq/M5xdpHjTb+GAew==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint-tsgolint/linux-x64": { - "version": "0.22.1", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-0.22.1.tgz", - "integrity": "sha512-fuX1hEQfpHauUbXADsfqVhRzrUrGabzGXbj5wsp2vKhV5uk/Rze8Mba9GdjFGECzvXudMGqHqxB4r6jGRdhxVA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oxlint-tsgolint/win32-arm64": { - "version": "0.22.1", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-0.22.1.tgz", - "integrity": "sha512-8SZidAj+jrbZf9ZjBEYW0tiNZ+KasqB2zgW26qdiPpQSF/DzURnPmXz651IeA9YsmbVdHGIooEHUmev6QJdquA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oxlint-tsgolint/win32-x64": { - "version": "0.22.1", - "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-0.22.1.tgz", - "integrity": "sha512-QweSk9H5lFh5Y+WUf2Kq/OAN88V6+62ZwGhP38gqdRotI90luXSMkruFTj7Q2rYrzH4ZVNaSqx7NY8JpSfIzqg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.61.0.tgz", - "integrity": "sha512-6eZBPgiigK5txqoVgRqxbaxiom4lM8AP8CyKPPvpzKnQ3iFRFOIDc+0AapF+qsUSwjOzr5SGk4SxQDpQhkSJMQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-android-arm64": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.61.0.tgz", - "integrity": "sha512-CkwLR69MUnyv5wjzebvbbtTSUwqLxM35CXE79bHqDIK+NtKmPEUpStTcLQRZMCo4MP0qRT6TXIQVpK0ZVScnMA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@oxlint/binding-darwin-arm64": { "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.61.0.tgz", - "integrity": "sha512-8JbefTkbmvqkqWjmQrHke+MdpgT2UghhD/ktM4FOQSpGeCgbMToJEKdl9zwhr/YWTl92i4QI1KiTwVExpcUN8A==", "cpu": [ "arm64" ], @@ -9258,288 +6615,12 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.61.0.tgz", - "integrity": "sha512-uWpoxDT47hTnDLcdEh5jVbso8rlTTu5o0zuqa9J8E0JAKmIWn7kGFEIB03Pycn2hd2vKxybPGLhjURy/9We5FQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.61.0.tgz", - "integrity": "sha512-K/o4hEyW7flfMel0iBVznmMBt7VIMHGdjADocHKpK1DUF9erpWnJ+BSSWd2W0c8K3mPtpph+CuHzRU6CI3l9jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.61.0.tgz", - "integrity": "sha512-P6040ZkcyweJ0Po9yEFqJCdvZnf3VNCGs1SIHgXDf8AAQNC6ID/heXQs9iSgo2FH7gKaKq32VWc59XZwL34C5Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.61.0.tgz", - "integrity": "sha512-bwxrGCzTZkuB+THv2TQ1aTkVEfv5oz8sl+0XZZCpoYzErJD8OhPQOTA0ENPd1zJz8QsVdSzSrS2umKtPq4/JXg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.61.0.tgz", - "integrity": "sha512-vkhb9/wKguMkLlrm3FoJW/Xmdv31GgYAE+x8lxxQ+7HeOxXUySI0q36a3NTVIuQUdLzxCI1zzMGsk1o37FOe3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.61.0.tgz", - "integrity": "sha512-bl1dQh8LnVqsj6oOQAcxwbuOmNJkwc4p6o//HTBZhNTzJy21TLDwAviMqUFNUxDHkPGpmdKTSN4tWTjLryP8xg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.61.0.tgz", - "integrity": "sha512-QoOX6KB2IiEpyOj/HKqaxi+NQHPnOgNgnr22n9N4ANJCzXkUlj1UmeAbFb4PpqdlHIzvGDM5xZ0OKtcLq9RhiQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.61.0.tgz", - "integrity": "sha512-1TGcTerjY6p152wCof3oKElccq3xHljS/Mucp04gV/4ATpP6nO7YNnp7opEg6SHkv2a57/b4b8Ndm9znJ1/qAw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.61.0.tgz", - "integrity": "sha512-65wXEmZIrX2ADwC8i/qFL4EWLSbeuBpAm3suuX1vu4IQkKd+wLT/HU/BOl84kp91u2SxPkPDyQgu4yrqp8vwVA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.61.0.tgz", - "integrity": "sha512-TVvhgMvor7Qa6COeXxCJ7ENOM+lcAOGsQ0iUdPSCv2hxb9qSHLQ4XF1h50S6RE1gBOJ0WV3rNukg4JJJP1LWRA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.61.0.tgz", - "integrity": "sha512-SjpS5uYuFoDnDdZPwZE59ndF95AsY47R5MliuneTWR1pDm2CxGJaYXbKULI71t5TVfLQUWmrHEGRL9xvuq6dnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.61.0.tgz", - "integrity": "sha512-gGfAeGD4sNJGILZbc/yKcIimO9wQnPMoYp9swAaKeEtwsSQAbU+rsdQze5SBtIP6j0QDzeYd4XSSUCRCF+LIeQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.61.0.tgz", - "integrity": "sha512-OlVT0LrG/ct33EVtWRyR+B/othwmDWeRxfi13wUdPeb3lAT5TgTcFDcfLfarZtzB4W1nWF/zICMgYdkggX2WmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.61.0.tgz", - "integrity": "sha512-vI//NZPJk6DToiovPtaiwD4iQ7kO1r5ReWQD0sOOyKRtP3E2f6jxin4uvwi3OvDzHA2EFfd7DcZl5dtkQh7g1w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.61.0.tgz", - "integrity": "sha512-0ySj4/4zd2XjePs3XAQq7IigIstN4LPQZgCyigX5/ERMLjdWAJfnxcTsrtxZxuij8guJW8foXuHmhGxW0H4dDA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.61.0.tgz", - "integrity": "sha512-0xgSiyeqDLDZxXoe9CVJrOx3TUVsfyoOY7cNi03JbItNcC9WCZqrSNdrAbHONxhSPaVh/lzfnDcON1RqSUMhHw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@pinojs/redact": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", - "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, "license": "MIT", "optional": true, @@ -9549,8 +6630,6 @@ }, "node_modules/@pkgr/core": { "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", "dev": true, "license": "MIT", "engines": { @@ -9562,8 +6641,6 @@ }, "node_modules/@playwright/test": { "version": "1.58.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", - "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -9578,15 +6655,11 @@ }, "node_modules/@polka/url": { "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "dev": true, "license": "MIT" }, "node_modules/@poppinss/colors": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", - "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", "license": "MIT", "dependencies": { "kleur": "^4.1.5" @@ -9594,8 +6667,6 @@ }, "node_modules/@poppinss/colors/node_modules/kleur": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", "license": "MIT", "engines": { "node": ">=6" @@ -9603,8 +6674,6 @@ }, "node_modules/@poppinss/dumper": { "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", - "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", "license": "MIT", "dependencies": { "@poppinss/colors": "^4.1.5", @@ -9614,8 +6683,6 @@ }, "node_modules/@poppinss/dumper/node_modules/supports-color": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", - "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", "license": "MIT", "engines": { "node": ">=18" @@ -9626,20 +6693,14 @@ }, "node_modules/@poppinss/exception": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", - "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", "license": "MIT" }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -9664,8 +6725,6 @@ }, "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -9682,8 +6741,6 @@ }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -9697,8 +6754,6 @@ }, "node_modules/@radix-ui/react-context": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -9712,8 +6767,6 @@ }, "node_modules/@radix-ui/react-dialog": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -9748,8 +6801,6 @@ }, "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -9766,8 +6817,6 @@ }, "node_modules/@radix-ui/react-direction": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -9781,8 +6830,6 @@ }, "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -9808,8 +6855,6 @@ }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -9823,8 +6868,6 @@ }, "node_modules/@radix-ui/react-focus-scope": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -9848,8 +6891,6 @@ }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -9866,8 +6907,6 @@ }, "node_modules/@radix-ui/react-portal": { "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3", @@ -9890,8 +6929,6 @@ }, "node_modules/@radix-ui/react-presence": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -9914,8 +6951,6 @@ }, "node_modules/@radix-ui/react-primitive": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.2.3" @@ -9937,8 +6972,6 @@ }, "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -9955,8 +6988,6 @@ }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -9986,8 +7017,6 @@ }, "node_modules/@radix-ui/react-slot": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", - "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -10004,8 +7033,6 @@ }, "node_modules/@radix-ui/react-tabs": { "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -10034,8 +7061,6 @@ }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -10049,8 +7074,6 @@ }, "node_modules/@radix-ui/react-use-controllable-state": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", "license": "MIT", "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", @@ -10068,8 +7091,6 @@ }, "node_modules/@radix-ui/react-use-effect-event": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -10086,8 +7107,6 @@ }, "node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" @@ -10104,8 +7123,6 @@ }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -10119,8 +7136,6 @@ }, "node_modules/@react-native-async-storage/async-storage": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", - "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", "license": "MIT", "dependencies": { "merge-options": "^3.0.4" @@ -10131,8 +7146,6 @@ }, "node_modules/@react-native-masked-view/masked-view": { "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@react-native-masked-view/masked-view/-/masked-view-0.3.2.tgz", - "integrity": "sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ==", "license": "MIT", "peerDependencies": { "react": ">=16", @@ -10141,8 +7154,6 @@ }, "node_modules/@react-native/assets-registry": { "version": "0.81.5", - "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz", - "integrity": "sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w==", "license": "MIT", "engines": { "node": ">= 20.19.4" @@ -10150,8 +7161,6 @@ }, "node_modules/@react-native/babel-plugin-codegen": { "version": "0.81.5", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.81.5.tgz", - "integrity": "sha512-oF71cIH6je3fSLi6VPjjC3Sgyyn57JLHXs+mHWc9MoCiJJcM4nqsS5J38zv1XQ8d3zOW2JtHro+LF0tagj2bfQ==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.3", @@ -10163,8 +7172,6 @@ }, "node_modules/@react-native/babel-preset": { "version": "0.81.5", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.81.5.tgz", - "integrity": "sha512-UoI/x/5tCmi+pZ3c1+Ypr1DaRMDLI3y+Q70pVLLVgrnC3DHsHRIbHcCHIeG/IJvoeFqFM2sTdhSOLJrf8lOPrA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -10222,8 +7229,6 @@ }, "node_modules/@react-native/babel-preset/node_modules/react-refresh": { "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10231,8 +7236,6 @@ }, "node_modules/@react-native/codegen": { "version": "0.81.5", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.81.5.tgz", - "integrity": "sha512-a2TDA03Up8lpSa9sh5VRGCQDXgCTOyDOFH+aqyinxp1HChG8uk89/G+nkJ9FPd0rqgi25eCTR16TWdS3b+fA6g==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -10252,8 +7255,6 @@ }, "node_modules/@react-native/codegen/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -10262,9 +7263,6 @@ }, "node_modules/@react-native/codegen/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -10283,8 +7281,6 @@ }, "node_modules/@react-native/codegen/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -10295,8 +7291,6 @@ }, "node_modules/@react-native/community-cli-plugin": { "version": "0.81.5", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.81.5.tgz", - "integrity": "sha512-yWRlmEOtcyvSZ4+OvqPabt+NS36vg0K/WADTQLhrYrm9qdZSuXmq8PmdJWz/68wAqKQ+4KTILiq2kjRQwnyhQw==", "license": "MIT", "dependencies": { "@react-native/dev-middleware": "0.81.5", @@ -10325,8 +7319,6 @@ }, "node_modules/@react-native/debugger-frontend": { "version": "0.81.5", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.81.5.tgz", - "integrity": "sha512-bnd9FSdWKx2ncklOetCgrlwqSGhMHP2zOxObJbOWXoj7GHEmih4MKarBo5/a8gX8EfA1EwRATdfNBQ81DY+h+w==", "license": "BSD-3-Clause", "engines": { "node": ">= 20.19.4" @@ -10334,8 +7326,6 @@ }, "node_modules/@react-native/dev-middleware": { "version": "0.81.5", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.81.5.tgz", - "integrity": "sha512-WfPfZzboYgo/TUtysuD5xyANzzfka8Ebni6RIb2wDxhb56ERi7qDrE4xGhtPsjCL4pQBXSVxyIlCy0d8I6EgGA==", "license": "MIT", "dependencies": { "@isaacs/ttlcache": "^1.4.1", @@ -10356,8 +7346,6 @@ }, "node_modules/@react-native/dev-middleware/node_modules/ws": { "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", - "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", "license": "MIT", "dependencies": { "async-limiter": "~1.0.0" @@ -10365,8 +7353,6 @@ }, "node_modules/@react-native/gradle-plugin": { "version": "0.81.5", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.81.5.tgz", - "integrity": "sha512-hORRlNBj+ReNMLo9jme3yQ6JQf4GZpVEBLxmTXGGlIL78MAezDZr5/uq9dwElSbcGmLEgeiax6e174Fie6qPLg==", "license": "MIT", "engines": { "node": ">= 20.19.4" @@ -10374,8 +7360,6 @@ }, "node_modules/@react-native/js-polyfills": { "version": "0.81.5", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.81.5.tgz", - "integrity": "sha512-fB7M1CMOCIUudTRuj7kzxIBTVw2KXnsgbQ6+4cbqSxo8NmRRhA0Ul4ZUzZj3rFd3VznTL4Brmocv1oiN0bWZ8w==", "license": "MIT", "engines": { "node": ">= 20.19.4" @@ -10383,8 +7367,6 @@ }, "node_modules/@react-native/metro-babel-transformer": { "version": "0.85.3", - "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.85.3.tgz", - "integrity": "sha512-omuKq+r7jM4XvCMIlNMPP7Up3SyB8o5EAdZtF7YXniKyq7UOMBqhYHFqgsdOXr0lT+3ADf7VCJG3sb82jlBrrQ==", "license": "MIT", "peer": true, "dependencies": { @@ -10402,8 +7384,6 @@ }, "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/babel-plugin-codegen": { "version": "0.85.3", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.85.3.tgz", - "integrity": "sha512-Wc94zGfeFG8Njf9SHMPfYZP04kjigkOps6F1TYTvd7ZVXuGxqseCDgxc50LWcOhOCLypI9n3oVVqz81C3p44ZA==", "license": "MIT", "peer": true, "dependencies": { @@ -10416,8 +7396,6 @@ }, "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/babel-preset": { "version": "0.85.3", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.85.3.tgz", - "integrity": "sha512-fD7fxEhkJB/aF57tWoXjaAWpklfrExYZS3k6aXPP3BQ77DZY7gvf/b7dbirwjID6NVnP1JDRJyTuPBGr0K/vlw==", "license": "MIT", "peer": true, "dependencies": { @@ -10464,8 +7442,6 @@ }, "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/codegen": { "version": "0.85.3", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.85.3.tgz", - "integrity": "sha512-/JkS1lGLyzBWP1FbgDwaqEf7qShIC6pUC1M0a/YMAd/v4iqR24MRkQWe7jkYvcBQ2LpEhs5NGE9InhxSv21zCA==", "license": "MIT", "peer": true, "dependencies": { @@ -10486,8 +7462,6 @@ }, "node_modules/@react-native/metro-babel-transformer/node_modules/babel-plugin-syntax-hermes-parser": { "version": "0.33.3", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.33.3.tgz", - "integrity": "sha512-/Z9xYdaJ1lC0pT9do6TqCqhOSLfZ5Ot8D5za1p+feEfWYupCOfGbhhEXN9r2ZgJtDNUNRw/Z+T2CvAGKBqtqWA==", "license": "MIT", "peer": true, "dependencies": { @@ -10496,15 +7470,11 @@ }, "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-estree": { "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", - "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", "license": "MIT", "peer": true }, "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-parser": { "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", - "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", "license": "MIT", "peer": true, "dependencies": { @@ -10513,8 +7483,6 @@ }, "node_modules/@react-native/metro-babel-transformer/node_modules/react-refresh": { "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "license": "MIT", "peer": true, "engines": { @@ -10523,8 +7491,6 @@ }, "node_modules/@react-native/metro-config": { "version": "0.85.3", - "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.85.3.tgz", - "integrity": "sha512-sVo6HepUmCcpdfozEf91lA0FjpLNNZYu/Zi9FiYiAQTK8pzATXDVTqhvdxpFrQn435p5eUTSbllvbH/KN+bnyA==", "license": "MIT", "peer": true, "dependencies": { @@ -10539,8 +7505,6 @@ }, "node_modules/@react-native/metro-config/node_modules/@react-native/js-polyfills": { "version": "0.85.3", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.85.3.tgz", - "integrity": "sha512-U2+aMshIXf1uFn77tpBb/xhHWB9vkVrMpt7kkucAugF8hJKYTDGB587X7WwelHduK2KBfhl4giSv0rzZGoef9A==", "license": "MIT", "peer": true, "engines": { @@ -10549,8 +7513,6 @@ }, "node_modules/@react-native/metro-config/node_modules/accepts": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "peer": true, "dependencies": { @@ -10563,8 +7525,6 @@ }, "node_modules/@react-native/metro-config/node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", "peer": true, "engines": { @@ -10573,22 +7533,16 @@ }, "node_modules/@react-native/metro-config/node_modules/ci-info": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "license": "MIT", "peer": true }, "node_modules/@react-native/metro-config/node_modules/hermes-estree": { "version": "0.35.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz", - "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==", "license": "MIT", "peer": true }, "node_modules/@react-native/metro-config/node_modules/hermes-parser": { "version": "0.35.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz", - "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==", "license": "MIT", "peer": true, "dependencies": { @@ -10597,8 +7551,6 @@ }, "node_modules/@react-native/metro-config/node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "peer": true, "dependencies": { @@ -10611,8 +7563,6 @@ }, "node_modules/@react-native/metro-config/node_modules/metro": { "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.84.4.tgz", - "integrity": "sha512-8ETTubqfD6ornDy2zYDvRcKnVDOXdFJsjetYDBsY4oAsb6NJkiwFR+FaMESyGppFmQUyBQA4H4sFGxzcQSGtFA==", "license": "MIT", "peer": true, "dependencies": { @@ -10665,8 +7615,6 @@ }, "node_modules/@react-native/metro-config/node_modules/metro-babel-transformer": { "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.84.4.tgz", - "integrity": "sha512-rvCfz8snl9h20VcvpOHxZuHP1SlAkv4HXbzw7nyyVwu6Eqo5PRerbakQ9XmUCOsRy70spJ37O+G1TK8oMzo48g==", "license": "MIT", "peer": true, "dependencies": { @@ -10682,8 +7630,6 @@ }, "node_modules/@react-native/metro-config/node_modules/metro-cache": { "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.84.4.tgz", - "integrity": "sha512-gpcFQdSLUwUCk71saKoE64jLFbx2nwTfVCcPSULMNT8QYq0p1eZZE29Jvd0HtT/UlhC3ZOutLxJME5xqD2JUZg==", "license": "MIT", "peer": true, "dependencies": { @@ -10698,8 +7644,6 @@ }, "node_modules/@react-native/metro-config/node_modules/metro-cache-key": { "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.84.4.tgz", - "integrity": "sha512-wVO79aGrkYImpnaVS4+d5RrRBRPX31QtvKB3wKGBuiNSznduZTQHzsrJZRroFJSwnygrzdsGUtDQPuqqFjFdvw==", "license": "MIT", "peer": true, "dependencies": { @@ -10711,8 +7655,6 @@ }, "node_modules/@react-native/metro-config/node_modules/metro-config": { "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.84.4.tgz", - "integrity": "sha512-PMotGDjXcXLWo2TMRH+VR99phFNgYTwqh4OoieIKK3yTJa1Jmkl+fZJxDO0jfBvNF+WESHciHvpNuBtXaF3B0Q==", "license": "MIT", "peer": true, "dependencies": { @@ -10731,8 +7673,6 @@ }, "node_modules/@react-native/metro-config/node_modules/metro-core": { "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.84.4.tgz", - "integrity": "sha512-HONpWC5LGXZn3ffkd4Hu6AIrfE7j4Z0g0wMo/goV24WOB3lhuFZ40KgvaDiSw8iyQHloMYay5N/wPX+z8oN/PQ==", "license": "MIT", "peer": true, "dependencies": { @@ -10746,8 +7686,6 @@ }, "node_modules/@react-native/metro-config/node_modules/metro-file-map": { "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.84.4.tgz", - "integrity": "sha512-KSVDi/u60hKPx++NLu3MTIvyjzNoJnFAF8PQFxaj1jiSka/wjw+Ua6sNuJ0TDHQv+7AAoFQxeMgaRAe8Yic5wQ==", "license": "MIT", "peer": true, "dependencies": { @@ -10767,8 +7705,6 @@ }, "node_modules/@react-native/metro-config/node_modules/metro-minify-terser": { "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.84.4.tgz", - "integrity": "sha512-5qpbaVOMC7CPitIpuewzVeGw7E+C3ykbv2mqTjQLl85Z3annSVGlSCTcsZjqXZzjupfK4Ztj3dDc4kc44NZwtQ==", "license": "MIT", "peer": true, "dependencies": { @@ -10781,8 +7717,6 @@ }, "node_modules/@react-native/metro-config/node_modules/metro-resolver": { "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.84.4.tgz", - "integrity": "sha512-1qLgbxQ5ZGhhutuPot1Yp348ofDsATL2WkrHF65TobqTT9K3P9qJXw38bomk7ncp5B7OYMfWwtyBZo1lCV792A==", "license": "MIT", "peer": true, "dependencies": { @@ -10794,8 +7728,6 @@ }, "node_modules/@react-native/metro-config/node_modules/metro-runtime": { "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.4.tgz", - "integrity": "sha512-Jibypds4g7AhzdRKY+kDoj51s5EXMwgyp5ddtlreDAsWefMdOx+agWqgm0H2XSZ/ueanHHVM89fnf5OJnlxa8Q==", "license": "MIT", "peer": true, "dependencies": { @@ -10808,8 +7740,6 @@ }, "node_modules/@react-native/metro-config/node_modules/metro-source-map": { "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.4.tgz", - "integrity": "sha512-jbWkPxIesVuo1IWkvezmMJld6iu8nD62GsrZiV6jP37AOdbo4OBq1FJ+qkOg8sV05wAHB//jAbziuW0SlJfW4g==", "license": "MIT", "peer": true, "dependencies": { @@ -10829,8 +7759,6 @@ }, "node_modules/@react-native/metro-config/node_modules/metro-symbolicate": { "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.4.tgz", - "integrity": "sha512-OnfpacxUqGPZQ27t8qK9mFa7uqHIlVWeqRqkCbvMvreEBiamEeOn8krKtcwgP5M4cYDPwuSmCTopHMVthqG4zA==", "license": "MIT", "peer": true, "dependencies": { @@ -10850,8 +7778,6 @@ }, "node_modules/@react-native/metro-config/node_modules/metro-transform-plugins": { "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.84.4.tgz", - "integrity": "sha512-kehr6HbAecqD0/a3xLXobELdPaAmRAl8bel0qagPF4vhZtux93nS8S4eq2kgKt6J2GnQpVjSoW1PXdst04mwow==", "license": "MIT", "peer": true, "dependencies": { @@ -10868,8 +7794,6 @@ }, "node_modules/@react-native/metro-config/node_modules/metro-transform-worker": { "version": "0.84.4", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.84.4.tgz", - "integrity": "sha512-W1IYMvvXTu4MxYr7d9h7CeG2vpIr3bmLLIavkPY4O1ilzDrvS8z/NEe6y+pC44Ff7raMXQgYSfdqDUwN/i39gg==", "license": "MIT", "peer": true, "dependencies": { @@ -10893,8 +7817,6 @@ }, "node_modules/@react-native/metro-config/node_modules/mime-types": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "peer": true, "dependencies": { @@ -10910,8 +7832,6 @@ }, "node_modules/@react-native/metro-config/node_modules/negotiator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "peer": true, "engines": { @@ -10920,8 +7840,6 @@ }, "node_modules/@react-native/metro-config/node_modules/ob1": { "version": "0.84.4", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.4.tgz", - "integrity": "sha512-eJXMpz4aQHXF/YBB9ddqZDIS+ooO91hObo9FoW/xBkr54/zCwYYCDqT/O54vNo8kOkWs5Ou/y28NgdrV0edQNA==", "license": "MIT", "peer": true, "dependencies": { @@ -10933,8 +7851,6 @@ }, "node_modules/@react-native/metro-config/node_modules/source-map": { "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "license": "BSD-3-Clause", "peer": true, "engines": { @@ -10943,8 +7859,6 @@ }, "node_modules/@react-native/metro-config/node_modules/ws": { "version": "7.5.11", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", - "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", "license": "MIT", "peer": true, "engines": { @@ -10965,14 +7879,10 @@ }, "node_modules/@react-native/normalize-colors": { "version": "0.81.6", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.6.tgz", - "integrity": "sha512-/OCgUysHIFhfmZxbJAydVc58l2SGIZbWpbQXBrYEYch8YElBbDFQ8IUtyogB7YJJQ8ewHZFj93rQGaECgkvvcw==", "license": "MIT" }, "node_modules/@react-native/virtualized-lists": { "version": "0.81.5", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz", - "integrity": "sha512-UVXgV/db25OPIvwZySeToXD/9sKKhOdkcWmmf4Jh8iBZuyfML+/5CasaZ1E7Lqg6g3uqVQq75NqIwkYmORJMPw==", "license": "MIT", "dependencies": { "invariant": "^2.2.4", @@ -10994,8 +7904,6 @@ }, "node_modules/@react-navigation/bottom-tabs": { "version": "7.15.5", - "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.15.5.tgz", - "integrity": "sha512-wQHredlCrRmShWQ1vF4HUcLdaiJ8fUgnbaeQH7BJ7MQVQh4mdzab0IOY/4QSmUyNRB350oyu1biTycyQ5FKWMQ==", "license": "MIT", "dependencies": { "@react-navigation/elements": "^2.9.10", @@ -11012,8 +7920,6 @@ }, "node_modules/@react-navigation/core": { "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.16.1.tgz", - "integrity": "sha512-xhquoyhKdqDfiL7LuupbwYnmauUGfVFGDEJO34m26k8zSN1eDjQ2stBZcHN8ILOI1PrG9885nf8ZmfaQxPS0ww==", "license": "MIT", "dependencies": { "@react-navigation/routers": "^7.5.3", @@ -11031,8 +7937,6 @@ }, "node_modules/@react-navigation/elements": { "version": "2.9.10", - "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.10.tgz", - "integrity": "sha512-N8tuBekzTRb0pkMHFJGvmC6Q5OisSbt6gzvw7RHMnp4NDo5auVllT12sWFaTXf8mTduaLKNSrD/NZNaOqThCBg==", "license": "MIT", "dependencies": { "color": "^4.2.3", @@ -11054,8 +7958,6 @@ }, "node_modules/@react-navigation/native": { "version": "7.1.33", - "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.33.tgz", - "integrity": "sha512-DpFdWGcgLajKZ1TuIvDNQsblN2QaUFWpTQaB8v7WRP9Mix8H/6TFoIrZd93pbymI2hybd6UYrD+lI408eWVcfw==", "license": "MIT", "dependencies": { "@react-navigation/core": "^7.16.1", @@ -11071,8 +7973,6 @@ }, "node_modules/@react-navigation/native-stack": { "version": "7.14.4", - "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.14.4.tgz", - "integrity": "sha512-HFEnM5Q7JY3FmmiolD/zvgY+9sxZAyVGPZJoz7BdTvJmi1VHOdplf24YiH45mqeitlGnaOlvNT55rH4abHJ5eA==", "license": "MIT", "dependencies": { "@react-navigation/elements": "^2.9.10", @@ -11090,8 +7990,6 @@ }, "node_modules/@react-navigation/routers": { "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.3.tgz", - "integrity": "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==", "license": "MIT", "dependencies": { "nanoid": "^3.3.11" @@ -11099,8 +7997,6 @@ }, "node_modules/@replit/codemirror-lang-csharp": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-csharp/-/codemirror-lang-csharp-6.2.0.tgz", - "integrity": "sha512-6utbaWkoymhoAXj051mkRp+VIJlpwUgCX9Toevz3YatiZsz512fw3OVCedXQx+WcR0wb6zVHjChnuxqfCLtFVQ==", "license": "MIT", "peerDependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -11114,8 +8010,6 @@ }, "node_modules/@replit/codemirror-vim": { "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.3.0.tgz", - "integrity": "sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==", "license": "MIT", "peerDependencies": { "@codemirror/commands": "6.x.x", @@ -11127,8 +8021,6 @@ }, "node_modules/@rollup/pluginutils": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -11150,43 +8042,11 @@ }, "node_modules/@rollup/pluginutils/node_modules/estree-walker": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true, "license": "MIT" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true - }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -11197,325 +8057,13 @@ ], "peer": true }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "peer": true - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true - }, "node_modules/@rtsao/scc": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true, "license": "MIT" }, "node_modules/@segment/ajv-human-errors": { "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@segment/ajv-human-errors/-/ajv-human-errors-2.16.0.tgz", - "integrity": "sha512-cHNfZcbHrmuYOA7/Sn7HlIDHanamiRTZtngfxcAuFaKQjP7cSqsVHjLz38FI2FQ8JDLz3syGLaz10Gn2ddo7+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -11524,8 +8072,6 @@ }, "node_modules/@segment/loosely-validate-event": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz", - "integrity": "sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw==", "dev": true, "dependencies": { "component-type": "^1.2.1", @@ -11534,8 +8080,6 @@ }, "node_modules/@shikijs/core": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz", - "integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==", "license": "MIT", "dependencies": { "@shikijs/primitive": "4.0.2", @@ -11550,8 +8094,6 @@ }, "node_modules/@shikijs/engine-javascript": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz", - "integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==", "license": "MIT", "dependencies": { "@shikijs/types": "4.0.2", @@ -11564,8 +8106,6 @@ }, "node_modules/@shikijs/engine-oniguruma": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz", - "integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==", "license": "MIT", "dependencies": { "@shikijs/types": "4.0.2", @@ -11577,8 +8117,6 @@ }, "node_modules/@shikijs/langs": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz", - "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==", "license": "MIT", "dependencies": { "@shikijs/types": "4.0.2" @@ -11589,8 +8127,6 @@ }, "node_modules/@shikijs/primitive": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz", - "integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==", "license": "MIT", "dependencies": { "@shikijs/types": "4.0.2", @@ -11603,8 +8139,6 @@ }, "node_modules/@shikijs/themes": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz", - "integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==", "license": "MIT", "dependencies": { "@shikijs/types": "4.0.2" @@ -11615,8 +8149,6 @@ }, "node_modules/@shikijs/types": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", - "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", @@ -11628,14 +8160,10 @@ }, "node_modules/@shikijs/vscode-textmate": { "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "license": "MIT" }, "node_modules/@sideway/address": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -11644,28 +8172,20 @@ }, "node_modules/@sideway/formula": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@sinclair/typebox": { "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", "license": "MIT" }, "node_modules/@sindresorhus/is": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", - "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", "license": "MIT", "engines": { "node": ">=18" @@ -11676,8 +8196,6 @@ }, "node_modules/@sinonjs/commons": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" @@ -11685,8 +8203,6 @@ }, "node_modules/@sinonjs/fake-timers": { "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0" @@ -11694,26 +8210,18 @@ }, "node_modules/@speed-highlight/core": { "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.15.tgz", - "integrity": "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==", "license": "CC0-1.0" }, "node_modules/@stablelib/base64": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", - "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", "license": "MIT" }, "node_modules/@standard-schema/spec": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "dev": true, "license": "MIT", "dependencies": { @@ -11725,8 +8233,6 @@ }, "node_modules/@tailwindcss/node": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", - "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", "dev": true, "license": "MIT", "dependencies": { @@ -11741,8 +8247,6 @@ }, "node_modules/@tailwindcss/oxide": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", - "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", "dev": true, "license": "MIT", "engines": { @@ -11763,27 +8267,8 @@ "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", - "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, "node_modules/@tailwindcss/oxide-darwin-arm64": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", - "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", "cpu": [ "arm64" ], @@ -11797,193 +8282,8 @@ "node": ">= 20" } }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", - "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", - "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", - "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", - "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", - "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", - "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", - "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", - "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", - "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", - "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, "node_modules/@tailwindcss/vite": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", - "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", "dev": true, "license": "MIT", "dependencies": { @@ -11997,8 +8297,6 @@ }, "node_modules/@tanstack/query-core": { "version": "5.90.20", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", - "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", "license": "MIT", "funding": { "type": "github", @@ -12007,8 +8305,6 @@ }, "node_modules/@tanstack/react-query": { "version": "5.90.21", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", - "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", "license": "MIT", "dependencies": { "@tanstack/query-core": "5.90.20" @@ -12023,8 +8319,6 @@ }, "node_modules/@tanstack/react-store": { "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz", - "integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==", "license": "MIT", "dependencies": { "@tanstack/store": "0.9.3", @@ -12041,8 +8335,6 @@ }, "node_modules/@tanstack/react-virtual": { "version": "3.13.21", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.21.tgz", - "integrity": "sha512-SYXFrmrbPgXBvf+HsOsKhFgqSe4M6B29VHOsX9Jih9TlNkNkDWx0hWMiMLUghMEzyUz772ndzdEeCEBx+3GIZw==", "license": "MIT", "dependencies": { "@tanstack/virtual-core": "3.13.21" @@ -12058,8 +8350,6 @@ }, "node_modules/@tanstack/store": { "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz", - "integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==", "license": "MIT", "funding": { "type": "github", @@ -12068,8 +8358,6 @@ }, "node_modules/@tanstack/virtual-core": { "version": "3.13.21", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.21.tgz", - "integrity": "sha512-ww+fmLHyCbPSf7JNbWZP3g7wl6SdNo3ah5Aiw+0e9FDErkVHLKprYUrwTm7dF646FtEkN/KkAKPYezxpmvOjxw==", "license": "MIT", "funding": { "type": "github", @@ -12078,8 +8366,6 @@ }, "node_modules/@testing-library/dom": { "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", "dependencies": { @@ -12098,8 +8384,6 @@ }, "node_modules/@testing-library/dom/node_modules/ansi-styles": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { @@ -12111,8 +8395,6 @@ }, "node_modules/@testing-library/dom/node_modules/pretty-format": { "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12126,15 +8408,11 @@ }, "node_modules/@testing-library/dom/node_modules/react-is": { "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, "license": "MIT" }, "node_modules/@testing-library/react": { "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", "dev": true, "license": "MIT", "dependencies": { @@ -12161,8 +8439,6 @@ }, "node_modules/@tootallnate/once": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "dev": true, "license": "MIT", "engines": { @@ -12171,61 +8447,36 @@ }, "node_modules/@tsconfig/node10": { "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node18": { "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.6.tgz", - "integrity": "sha512-eAWQzAjPj18tKnDzmWstz4OyWewLUNBm9tdoN9LayzoboRktYx3Enk1ZXPmThj55L7c4VWYq/Bzq0A51znZfhw==", "dev": true, "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/aria-query": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", @@ -12237,8 +8488,6 @@ }, "node_modules/@types/babel__generator": { "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -12246,8 +8495,6 @@ }, "node_modules/@types/babel__template": { "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -12256,8 +8503,6 @@ }, "node_modules/@types/babel__traverse": { "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "license": "MIT", "dependencies": { "@babel/types": "^7.28.2" @@ -12265,8 +8510,6 @@ }, "node_modules/@types/body-parser": { "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "dev": true, "license": "MIT", "dependencies": { @@ -12276,8 +8519,6 @@ }, "node_modules/@types/bunyan": { "version": "1.8.11", - "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.11.tgz", - "integrity": "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12286,8 +8527,6 @@ }, "node_modules/@types/cacheable-request": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "dev": true, "license": "MIT", "dependencies": { @@ -12299,8 +8538,6 @@ }, "node_modules/@types/chai": { "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { @@ -12310,8 +8547,6 @@ }, "node_modules/@types/cli-progress": { "version": "3.11.6", - "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz", - "integrity": "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==", "dev": true, "license": "MIT", "dependencies": { @@ -12320,18 +8555,267 @@ }, "node_modules/@types/connect": { "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "license": "MIT", "dependencies": { "@types/ms": "*" @@ -12339,21 +8823,15 @@ }, "node_modules/@types/deep-eql": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/estree-jsx": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", "license": "MIT", "dependencies": { "@types/estree": "*" @@ -12361,8 +8839,6 @@ }, "node_modules/@types/express": { "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, "license": "MIT", "dependencies": { @@ -12374,8 +8850,6 @@ }, "node_modules/@types/express-serve-static-core": { "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", "dev": true, "license": "MIT", "dependencies": { @@ -12387,18 +8861,20 @@ }, "node_modules/@types/fs-extra": { "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -12406,14 +8882,10 @@ }, "node_modules/@types/hammerjs": { "version": "2.0.46", - "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", - "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", "license": "MIT" }, "node_modules/@types/hast": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -12421,28 +8893,20 @@ }, "node_modules/@types/http-cache-semantics": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", "dev": true, "license": "MIT" }, "node_modules/@types/http-errors": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" @@ -12450,8 +8914,6 @@ }, "node_modules/@types/istanbul-reports": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" @@ -12459,8 +8921,6 @@ }, "node_modules/@types/jest": { "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12470,8 +8930,6 @@ }, "node_modules/@types/jsdom": { "version": "20.0.1", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", - "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12482,22 +8940,16 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/json5": { "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true, "license": "MIT" }, "node_modules/@types/keyv": { "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "dev": true, "license": "MIT", "dependencies": { @@ -12506,15 +8958,11 @@ }, "node_modules/@types/linkify-it": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", "dev": true, "license": "MIT" }, "node_modules/@types/markdown-it": { "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", - "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "dev": true, "license": "MIT", "dependencies": { @@ -12524,8 +8972,6 @@ }, "node_modules/@types/mdast": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -12533,42 +8979,30 @@ }, "node_modules/@types/mdurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "dev": true, "license": "MIT" }, "node_modules/@types/mime": { "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true, "license": "MIT" }, "node_modules/@types/mime-types": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==", "dev": true, "license": "MIT" }, "node_modules/@types/minimist": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "dev": true, "license": "MIT" }, "node_modules/@types/ms": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, "node_modules/@types/node": { "version": "20.19.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", - "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -12576,15 +9010,11 @@ }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true, "license": "MIT" }, "node_modules/@types/plist": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", "dev": true, "license": "MIT", "optional": true, @@ -12595,15 +9025,11 @@ }, "node_modules/@types/prop-types": { "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", "dev": true, "license": "MIT" }, "node_modules/@types/qrcode": { "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", - "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", "dev": true, "license": "MIT", "dependencies": { @@ -12612,22 +9038,16 @@ }, "node_modules/@types/qs": { "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -12635,8 +9055,6 @@ }, "node_modules/@types/react-dom": { "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", "peerDependencies": { @@ -12645,8 +9063,6 @@ }, "node_modules/@types/responselike": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "dev": true, "license": "MIT", "dependencies": { @@ -12655,8 +9071,6 @@ }, "node_modules/@types/send": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12665,8 +9079,6 @@ }, "node_modules/@types/serve-static": { "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", "dev": true, "license": "MIT", "dependencies": { @@ -12677,8 +9089,6 @@ }, "node_modules/@types/serve-static/node_modules/@types/send": { "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", "dev": true, "license": "MIT", "dependencies": { @@ -12688,49 +9098,42 @@ }, "node_modules/@types/stack-utils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "license": "MIT" }, "node_modules/@types/tough-cookie": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/turndown": { "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.6.tgz", - "integrity": "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==", "dev": true, "license": "MIT" }, "node_modules/@types/unist": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, "node_modules/@types/uuid": { "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", "dev": true, "license": "MIT" }, "node_modules/@types/verror": { "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", - "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", "dev": true, "license": "MIT", "optional": true }, "node_modules/@types/ws": { "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, "license": "MIT", "dependencies": { @@ -12739,8 +9142,6 @@ }, "node_modules/@types/yargs": { "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "license": "MIT", "dependencies": { "@types/yargs-parser": "*" @@ -12748,14 +9149,10 @@ }, "node_modules/@types/yargs-parser": { "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "license": "MIT" }, "node_modules/@types/yauzl": { "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, "license": "MIT", "optional": true, @@ -12765,8 +9162,6 @@ }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", - "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { @@ -12794,8 +9189,6 @@ }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -12804,8 +9197,6 @@ }, "node_modules/@typescript-eslint/parser": { "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", - "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", "dependencies": { @@ -12829,8 +9220,6 @@ }, "node_modules/@typescript-eslint/project-service": { "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", - "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12851,8 +9240,6 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", - "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { @@ -12869,8 +9256,6 @@ }, "node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", - "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", "engines": { @@ -12886,8 +9271,6 @@ }, "node_modules/@typescript-eslint/type-utils": { "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", - "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, "license": "MIT", "dependencies": { @@ -12911,8 +9294,6 @@ }, "node_modules/@typescript-eslint/types": { "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", - "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", "engines": { @@ -12925,8 +9306,6 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", - "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, "license": "MIT", "dependencies": { @@ -12953,8 +9332,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -12963,8 +9340,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { @@ -12976,8 +9351,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -12992,8 +9365,6 @@ }, "node_modules/@typescript-eslint/utils": { "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", - "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", "dependencies": { @@ -13016,8 +9387,6 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", - "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, "license": "MIT", "dependencies": { @@ -13034,8 +9403,6 @@ }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -13047,8 +9414,6 @@ }, "node_modules/@typescript/native-preview": { "version": "7.0.0-dev.20260423.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260423.1.tgz", - "integrity": "sha512-9WD7TJJlGvt9PQqJI/+44dVP4oqGQFIkYrpXt7nlQ0WgNIErN52x/XhxmJ4nWft06qejgPiUbPo4aYRNOmIHXg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -13069,8 +9434,6 @@ }, "node_modules/@typescript/native-preview-darwin-arm64": { "version": "7.0.0-dev.20260423.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260423.1.tgz", - "integrity": "sha512-wbLr6o5fROaCYt6cOpFhbe92FJAOdhAHwm/s8I/IyN5HbL1ULgel/wHaZiR+ws+27rgruNUiCENzTUg9vSz2bA==", "cpu": [ "arm64" ], @@ -13084,146 +9447,12 @@ "node": ">=16.20.0" } }, - "node_modules/@typescript/native-preview-darwin-x64": { - "version": "7.0.0-dev.20260423.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260423.1.tgz", - "integrity": "sha512-13MpNT+4MgkgrfiW2u03rnER5aB3yz9fA0bWEYh6IH3rIqA2AR3Dntp3QXW4sQrZf0SriXqHe2R7X3HCT5xmqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/native-preview-linux-arm": { - "version": "7.0.0-dev.20260423.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260423.1.tgz", - "integrity": "sha512-CxUA15qbPQRvz2nanBpiv1h4tgXTCJJwqOtgKMSdIuPkow8dyYW3ba5oLoH/jZhS4792XislX659hlFrfiU6CQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/native-preview-linux-arm64": { - "version": "7.0.0-dev.20260423.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260423.1.tgz", - "integrity": "sha512-ICIkJDTqmn0R4Vs811+Ht2RYTk1OCrAhHCu0JthmhR216T1Tqgi5DWRoCprp3RL1qU6fLnxxrIpEbNlNN7XFYA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/native-preview-linux-x64": { - "version": "7.0.0-dev.20260423.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260423.1.tgz", - "integrity": "sha512-cWLFS4R8dOU1YuUJ/2VLeGMVIjgI3GGb/f9rRY5MbWHq5l3NNZh8y1QwAOrTh3+g3q6+znArfxVnD2hZHUz8Mw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/native-preview-win32-arm64": { - "version": "7.0.0-dev.20260423.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260423.1.tgz", - "integrity": "sha512-OWaGUI4+dHqYZv+k6sITx9Y27FNy3XzNFk4OrOiYtBkIO/xrb9TPMP4A5XI4n5zwRLIv3xne9g039xgRbaeyoQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/native-preview-win32-x64": { - "version": "7.0.0-dev.20260423.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260423.1.tgz", - "integrity": "sha512-5MQjO/qdLwXpjW7Dy/1lNv7Vtpvo6bhCkbjan4PoRN5/eeyqEqDWxdf8AGE4btLmHqyIjEHRuYf7kp2tlAr6lQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, "node_modules/@unrs/resolver-binding-darwin-arm64": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", "cpu": [ "arm64" ], @@ -13234,250 +9463,18 @@ "darwin" ] }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@urql/core": { "version": "4.0.11", - "resolved": "https://registry.npmjs.org/@urql/core/-/core-4.0.11.tgz", - "integrity": "sha512-FFdY97vF5xnUrElcGw9erOLvtu+KGMLfwrLNDfv4IPgdp2IBsiGe+Kb7Aypfd3kH//BETewVSLm3+y2sSzjX6A==", "dev": true, "license": "MIT", "dependencies": { @@ -13487,8 +9484,6 @@ }, "node_modules/@urql/exchange-retry": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@urql/exchange-retry/-/exchange-retry-1.2.0.tgz", - "integrity": "sha512-1O/biKiVhhn0EtvDF4UOvz325K4RrLupfL8rHcmqD2TBLv4qVDWQuzx4JGa1FfqjjRb+C9TNZ6w19f32Mq85Ug==", "dev": true, "license": "MIT", "dependencies": { @@ -13498,8 +9493,6 @@ }, "node_modules/@vercel/nft": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-1.5.0.tgz", - "integrity": "sha512-IWTDeIoWhQ7ZtRO/JRKH+jhmeQvZYhtGPmzw/QGDY+wDCQqfm25P9yIdoAFagu4fWsK4IwZXDFIjrmp5rRm/sA==", "dev": true, "license": "MIT", "dependencies": { @@ -13525,8 +9518,6 @@ }, "node_modules/@vercel/nft/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -13535,8 +9526,6 @@ }, "node_modules/@vercel/nft/node_modules/brace-expansion": { "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -13548,15 +9537,11 @@ }, "node_modules/@vercel/nft/node_modules/estree-walker": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true, "license": "MIT" }, "node_modules/@vercel/nft/node_modules/glob": { "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -13573,8 +9558,6 @@ }, "node_modules/@vercel/nft/node_modules/lru-cache": { "version": "11.3.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", - "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -13583,8 +9566,6 @@ }, "node_modules/@vercel/nft/node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -13599,8 +9580,6 @@ }, "node_modules/@vercel/nft/node_modules/path-scurry": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -13616,8 +9595,6 @@ }, "node_modules/@vercel/oidc": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.3.tgz", - "integrity": "sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==", "license": "Apache-2.0", "engines": { "node": ">= 20" @@ -13625,8 +9602,6 @@ }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, "license": "MIT", "dependencies": { @@ -13646,15 +9621,11 @@ }, "node_modules/@vitejs/plugin-react/node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true, "license": "MIT" }, "node_modules/@vitest/browser": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.7.tgz", - "integrity": "sha512-N2JFGfXoEGVAut+kHeru9dD4BUMq/q5xDvBARNl0tUsly3m5KglLOu8VO/6MkDfOlgxXTycojkt6gBKsuyR+IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13676,8 +9647,6 @@ }, "node_modules/@vitest/browser-playwright": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.7.tgz", - "integrity": "sha512-OlTlJej7YN6VwV7zJJoNeaCsctF+JXpzpZ4oBHUbrQFfIq+0KW2f07rprCLh9N/zRIZ0v4Mchn1QDDmWMUhPKw==", "dev": true, "license": "MIT", "dependencies": { @@ -13700,8 +9669,6 @@ }, "node_modules/@vitest/expect": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", - "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", "dev": true, "license": "MIT", "dependencies": { @@ -13718,8 +9685,6 @@ }, "node_modules/@vitest/mocker": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", - "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", "dev": true, "license": "MIT", "dependencies": { @@ -13745,8 +9710,6 @@ }, "node_modules/@vitest/pretty-format": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", - "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", "dev": true, "license": "MIT", "dependencies": { @@ -13758,8 +9721,6 @@ }, "node_modules/@vitest/runner": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", - "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", "dev": true, "license": "MIT", "dependencies": { @@ -13772,8 +9733,6 @@ }, "node_modules/@vitest/snapshot": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", - "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", "dev": true, "license": "MIT", "dependencies": { @@ -13788,8 +9747,6 @@ }, "node_modules/@vitest/spy": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", - "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", "dev": true, "license": "MIT", "funding": { @@ -13798,8 +9755,6 @@ }, "node_modules/@vitest/utils": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", - "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", "dev": true, "license": "MIT", "dependencies": { @@ -13813,9 +9768,6 @@ }, "node_modules/@xmldom/xmldom": { "version": "0.7.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", - "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", - "deprecated": "this version is no longer supported, please update to at least 0.8.*", "dev": true, "license": "MIT", "engines": { @@ -13824,8 +9776,6 @@ }, "node_modules/@xterm/headless": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.0.0.tgz", - "integrity": "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw==", "license": "MIT", "workspaces": [ "addons/*" @@ -13833,30 +9783,21 @@ }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/7zip-bin": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", "dev": true, "license": "MIT" }, "node_modules/abab": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", "dev": true, "license": "BSD-3-Clause" }, "node_modules/abbrev": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", "dev": true, "license": "ISC", "engines": { @@ -13865,8 +9806,6 @@ }, "node_modules/abort-controller": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "license": "MIT", "dependencies": { "event-target-shim": "^5.0.0" @@ -13877,8 +9816,6 @@ }, "node_modules/accepts": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -13890,8 +9827,6 @@ }, "node_modules/acorn": { "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -13902,8 +9837,6 @@ }, "node_modules/acorn-globals": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -13913,8 +9846,6 @@ }, "node_modules/acorn-import-attributes": { "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -13923,8 +9854,6 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -13933,8 +9862,6 @@ }, "node_modules/acorn-walk": { "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", "dev": true, "license": "MIT", "dependencies": { @@ -13946,8 +9873,6 @@ }, "node_modules/agent-base": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13959,8 +9884,6 @@ }, "node_modules/ai": { "version": "5.0.78", - "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.78.tgz", - "integrity": "sha512-ec77fmQwJGLduswMrW4AAUGSOiu8dZaIwMmWHHGKsrMUFFS6ugfkTyx0srtuKYHNRRLRC2dT7cPirnUl98VnxA==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/gateway": "2.0.1", @@ -13977,8 +9900,6 @@ }, "node_modules/ajv": { "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -13993,8 +9914,6 @@ }, "node_modules/ajv-formats": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", "dependencies": { @@ -14011,14 +9930,10 @@ }, "node_modules/anser": { "version": "1.4.10", - "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", - "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", "license": "MIT" }, "node_modules/ansi-escapes": { "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "license": "MIT", "dependencies": { "type-fest": "^0.21.3" @@ -14032,8 +9947,6 @@ }, "node_modules/ansi-escapes/node_modules/type-fest": { "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -14044,8 +9957,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -14053,8 +9964,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -14068,15 +9977,11 @@ }, "node_modules/ansicolors": { "version": "0.3.2", - "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", - "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", "dev": true, "license": "MIT" }, "node_modules/ansis": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", - "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", "license": "ISC", "engines": { "node": ">=14" @@ -14084,14 +9989,10 @@ }, "node_modules/any-promise": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -14103,8 +10004,6 @@ }, "node_modules/anymatch/node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -14115,15 +10014,11 @@ }, "node_modules/app-builder-bin": { "version": "5.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", - "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", "dev": true, "license": "MIT" }, "node_modules/app-builder-lib": { "version": "26.8.1", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", - "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -14174,8 +10069,6 @@ }, "node_modules/app-builder-lib/node_modules/@electron/get": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", - "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14196,8 +10089,6 @@ }, "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", "dependencies": { @@ -14211,8 +10102,6 @@ }, "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -14221,8 +10110,6 @@ }, "node_modules/app-builder-lib/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -14231,8 +10118,6 @@ }, "node_modules/app-builder-lib/node_modules/brace-expansion": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { @@ -14244,8 +10129,6 @@ }, "node_modules/app-builder-lib/node_modules/chownr": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -14254,8 +10137,6 @@ }, "node_modules/app-builder-lib/node_modules/ci-info": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", "dev": true, "funding": [ { @@ -14270,8 +10151,6 @@ }, "node_modules/app-builder-lib/node_modules/dotenv": { "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -14283,8 +10162,6 @@ }, "node_modules/app-builder-lib/node_modules/isexe": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -14293,8 +10170,6 @@ }, "node_modules/app-builder-lib/node_modules/jsonfile": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", "optionalDependencies": { @@ -14303,8 +10178,6 @@ }, "node_modules/app-builder-lib/node_modules/minimatch": { "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -14319,8 +10192,6 @@ }, "node_modules/app-builder-lib/node_modules/minizlib": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -14332,8 +10203,6 @@ }, "node_modules/app-builder-lib/node_modules/tar": { "version": "7.5.12", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.12.tgz", - "integrity": "sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -14349,8 +10218,6 @@ }, "node_modules/app-builder-lib/node_modules/universalify": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", "engines": { @@ -14359,8 +10226,6 @@ }, "node_modules/app-builder-lib/node_modules/which": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, "license": "ISC", "dependencies": { @@ -14375,8 +10240,6 @@ }, "node_modules/app-builder-lib/node_modules/yallist": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -14385,14 +10248,10 @@ }, "node_modules/arg": { "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "license": "MIT" }, "node_modules/argparse": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" @@ -14400,8 +10259,6 @@ }, "node_modules/aria-hidden": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -14412,8 +10269,6 @@ }, "node_modules/aria-query": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -14422,8 +10277,6 @@ }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "license": "MIT", "dependencies": { @@ -14439,14 +10292,10 @@ }, "node_modules/array-flatten": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, "node_modules/array-includes": { "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14468,8 +10317,6 @@ }, "node_modules/array-union": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, "license": "MIT", "engines": { @@ -14478,8 +10325,6 @@ }, "node_modules/array.prototype.findlast": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14499,8 +10344,6 @@ }, "node_modules/array.prototype.findlastindex": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14521,8 +10364,6 @@ }, "node_modules/array.prototype.flat": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, "license": "MIT", "dependencies": { @@ -14540,8 +10381,6 @@ }, "node_modules/array.prototype.flatmap": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "license": "MIT", "dependencies": { @@ -14559,8 +10398,6 @@ }, "node_modules/array.prototype.tosorted": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "license": "MIT", "dependencies": { @@ -14576,8 +10413,6 @@ }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14598,8 +10433,6 @@ }, "node_modules/arrify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", "dev": true, "license": "MIT", "engines": { @@ -14608,14 +10441,10 @@ }, "node_modules/asap": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, "node_modules/asn1": { "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14624,8 +10453,6 @@ }, "node_modules/assert": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -14637,8 +10464,6 @@ }, "node_modules/assert-plus": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true, "license": "MIT", "optional": true, @@ -14648,8 +10473,6 @@ }, "node_modules/assertion-error": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { @@ -14658,8 +10481,6 @@ }, "node_modules/astral-regex": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "license": "MIT", "engines": { @@ -14668,15 +10489,11 @@ }, "node_modules/async": { "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true, "license": "MIT" }, "node_modules/async-exit-hook": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", "dev": true, "license": "MIT", "engines": { @@ -14685,8 +10502,6 @@ }, "node_modules/async-function": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, "license": "MIT", "engines": { @@ -14695,28 +10510,20 @@ }, "node_modules/async-limiter": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "license": "MIT" }, "node_modules/async-sema": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", - "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==", "dev": true, "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, "license": "MIT" }, "node_modules/at-least-node": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, "license": "ISC", "engines": { @@ -14725,8 +10532,6 @@ }, "node_modules/atomic-sleep": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", "license": "MIT", "engines": { "node": ">=8.0.0" @@ -14734,8 +10539,6 @@ }, "node_modules/available-typed-arrays": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -14749,8 +10552,6 @@ }, "node_modules/axios": { "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14761,8 +10562,6 @@ }, "node_modules/b4a": { "version": "1.8.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", - "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", "dev": true, "license": "Apache-2.0", "peerDependencies": { @@ -14776,8 +10575,6 @@ }, "node_modules/babel-dead-code-elimination": { "version": "1.0.12", - "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.12.tgz", - "integrity": "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==", "license": "MIT", "dependencies": { "@babel/core": "^7.23.7", @@ -14788,8 +10585,6 @@ }, "node_modules/babel-jest": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "license": "MIT", "dependencies": { "@jest/transform": "^29.7.0", @@ -14809,8 +10604,6 @@ }, "node_modules/babel-jest/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "license": "MIT", "engines": { "node": ">=8" @@ -14818,8 +10611,6 @@ }, "node_modules/babel-plugin-dynamic-import-node": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14828,8 +10619,6 @@ }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -14844,8 +10633,6 @@ }, "node_modules/babel-plugin-jest-hoist": { "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", @@ -14859,8 +10646,6 @@ }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.16", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.16.tgz", - "integrity": "sha512-xaVwwSfebXf0ooE11BJovZYKhFjIvQo7TsyVpETuIeH2JHv0k/T6Y5j22pPTvqYqmpkxdlPAJlyJ0tfOJAoMxw==", "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", @@ -14873,8 +10658,6 @@ }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -14882,8 +10665,6 @@ }, "node_modules/babel-plugin-polyfill-corejs3": { "version": "0.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", - "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5", @@ -14895,8 +10676,6 @@ }, "node_modules/babel-plugin-polyfill-regenerator": { "version": "0.6.7", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.7.tgz", - "integrity": "sha512-OTYbUlSwXhNgr4g6efMZgsO8//jA61P7ZbRX3iTT53VON8l+WQS8IAUEVo4a4cWknrg2W8Cj4gQhRYNCJ8GkAA==", "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.7" @@ -14907,8 +10686,6 @@ }, "node_modules/babel-plugin-react-compiler": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", - "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "license": "MIT", "dependencies": { "@babel/types": "^7.26.0" @@ -14916,14 +10693,10 @@ }, "node_modules/babel-plugin-react-native-web": { "version": "0.21.2", - "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz", - "integrity": "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==", "license": "MIT" }, "node_modules/babel-plugin-syntax-hermes-parser": { "version": "0.29.1", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.29.1.tgz", - "integrity": "sha512-2WFYnoWGdmih1I1J5eIqxATOeycOqRwYxAQBu3cUu/rhwInwHUg7k60AFNbuGjSDL8tje5GDrAnxzRLcu2pYcA==", "license": "MIT", "dependencies": { "hermes-parser": "0.29.1" @@ -14931,8 +10704,6 @@ }, "node_modules/babel-plugin-transform-flow-enums": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", - "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", "license": "MIT", "dependencies": { "@babel/plugin-syntax-flow": "^7.12.1" @@ -14940,8 +10711,6 @@ }, "node_modules/babel-preset-current-node-syntax": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", @@ -14966,8 +10735,6 @@ }, "node_modules/babel-preset-expo": { "version": "54.0.10", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-54.0.10.tgz", - "integrity": "sha512-wTt7POavLFypLcPW/uC5v8y+mtQKDJiyGLzYCjqr9tx0Qc3vCXcDKk1iCFIj/++Iy5CWhhTflEa7VvVPNWeCfw==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.25.9", @@ -15009,8 +10776,6 @@ }, "node_modules/babel-preset-jest": { "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "license": "MIT", "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", @@ -15025,14 +10790,10 @@ }, "node_modules/badgin": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz", - "integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==", "license": "MIT" }, "node_modules/bail": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", "license": "MIT", "funding": { "type": "github", @@ -15041,14 +10802,10 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, "node_modules/bare-events": { "version": "2.8.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", - "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "dev": true, "license": "Apache-2.0", "peerDependencies": { @@ -15062,8 +10819,6 @@ }, "node_modules/base64-js": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -15082,8 +10837,6 @@ }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -15094,8 +10847,6 @@ }, "node_modules/bcryptjs": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", - "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", "license": "BSD-3-Clause", "bin": { "bcrypt": "bin/bcrypt" @@ -15103,8 +10854,6 @@ }, "node_modules/better-opn": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", - "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", "license": "MIT", "dependencies": { "open": "^8.0.4" @@ -15115,8 +10864,6 @@ }, "node_modules/better-opn/node_modules/open": { "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "license": "MIT", "dependencies": { "define-lazy-prop": "^2.0.0", @@ -15132,8 +10879,6 @@ }, "node_modules/big-integer": { "version": "1.6.52", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", - "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", "license": "Unlicense", "engines": { "node": ">=0.6" @@ -15141,8 +10886,6 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "license": "MIT", "optional": true, @@ -15155,8 +10898,6 @@ }, "node_modules/bindings": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15165,14 +10906,10 @@ }, "node_modules/blake3-wasm": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", - "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", "license": "MIT" }, "node_modules/body-parser": { "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -15195,8 +10932,6 @@ }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -15204,8 +10939,6 @@ }, "node_modules/body-parser/node_modules/iconv-lite": { "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -15216,29 +10949,20 @@ }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/boolbase": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, "node_modules/boolean": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dev": true, "license": "MIT", "optional": true }, "node_modules/bplist-creator": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", - "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", "license": "MIT", "dependencies": { "stream-buffers": "2.2.x" @@ -15246,8 +10970,6 @@ }, "node_modules/bplist-parser": { "version": "0.3.2", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", - "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", "license": "MIT", "dependencies": { "big-integer": "1.6.x" @@ -15258,8 +10980,6 @@ }, "node_modules/brace-expansion": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -15267,8 +10987,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -15279,8 +10997,6 @@ }, "node_modules/browserslist": { "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -15312,8 +11028,6 @@ }, "node_modules/bs-logger": { "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", "dev": true, "license": "MIT", "dependencies": { @@ -15325,8 +11039,6 @@ }, "node_modules/bser": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "license": "Apache-2.0", "dependencies": { "node-int64": "^0.4.0" @@ -15334,8 +11046,6 @@ }, "node_modules/buffer": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -15358,8 +11068,6 @@ }, "node_modules/buffer-crc32": { "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, "license": "MIT", "engines": { @@ -15368,21 +11076,15 @@ }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/buffer-from": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, "node_modules/builder-util": { "version": "26.8.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", - "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", "dev": true, "license": "MIT", "dependencies": { @@ -15406,8 +11108,6 @@ }, "node_modules/builder-util-runtime": { "version": "9.5.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", - "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", "license": "MIT", "dependencies": { "debug": "^4.3.4", @@ -15419,8 +11119,6 @@ }, "node_modules/builder-util/node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -15429,8 +11127,6 @@ }, "node_modules/builder-util/node_modules/http-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { @@ -15443,8 +11139,6 @@ }, "node_modules/builder-util/node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { @@ -15457,8 +11151,6 @@ }, "node_modules/bunyan": { "version": "1.8.15", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", - "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", "dev": true, "engines": [ "node >=0.10.0" @@ -15476,8 +11168,6 @@ }, "node_modules/bytes": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -15485,8 +11175,6 @@ }, "node_modules/cacache": { "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", "dev": true, "license": "ISC", "dependencies": { @@ -15509,8 +11197,6 @@ }, "node_modules/cacache/node_modules/chownr": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -15519,8 +11205,6 @@ }, "node_modules/cacache/node_modules/fs-minipass": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, "license": "ISC", "dependencies": { @@ -15532,15 +11216,11 @@ }, "node_modules/cacache/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, "node_modules/cacache/node_modules/minizlib": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -15552,8 +11232,6 @@ }, "node_modules/cacache/node_modules/tar": { "version": "7.5.12", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.12.tgz", - "integrity": "sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -15569,8 +11247,6 @@ }, "node_modules/cacache/node_modules/yallist": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -15579,8 +11255,6 @@ }, "node_modules/cacheable-lookup": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "dev": true, "license": "MIT", "engines": { @@ -15589,8 +11263,6 @@ }, "node_modules/cacheable-request": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "dev": true, "license": "MIT", "dependencies": { @@ -15608,8 +11280,6 @@ }, "node_modules/cacheable-request/node_modules/get-stream": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "license": "MIT", "dependencies": { @@ -15624,8 +11294,6 @@ }, "node_modules/call-bind": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -15642,8 +11310,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -15655,8 +11321,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -15671,8 +11335,6 @@ }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -15681,8 +11343,6 @@ }, "node_modules/camelcase": { "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "license": "MIT", "engines": { "node": ">=10" @@ -15693,8 +11353,6 @@ }, "node_modules/camelcase-keys": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz", - "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==", "dev": true, "license": "MIT", "dependencies": { @@ -15712,8 +11370,6 @@ }, "node_modules/camelize": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", - "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -15721,8 +11377,6 @@ }, "node_modules/caniuse-lite": { "version": "1.0.30001777", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", - "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", "funding": [ { "type": "opencollective", @@ -15741,8 +11395,6 @@ }, "node_modules/cardinal": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", - "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", "dev": true, "license": "MIT", "dependencies": { @@ -15755,8 +11407,6 @@ }, "node_modules/ccount": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", "license": "MIT", "funding": { "type": "github", @@ -15765,8 +11415,6 @@ }, "node_modules/chai": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { @@ -15775,8 +11423,6 @@ }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -15791,8 +11437,6 @@ }, "node_modules/chalk/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -15803,8 +11447,6 @@ }, "node_modules/char-regex": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.2.tgz", - "integrity": "sha512-cbGOjAptfM2LVmWhwRFHEKTPkLwNddVmuqYZQt895yXwAsWsXObCG+YN4DGQ/JBtT4GP1a1lPPdio2z413LmTg==", "dev": true, "license": "MIT", "engines": { @@ -15813,8 +11455,6 @@ }, "node_modules/character-entities": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", "license": "MIT", "funding": { "type": "github", @@ -15823,8 +11463,6 @@ }, "node_modules/character-entities-html4": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", "license": "MIT", "funding": { "type": "github", @@ -15833,8 +11471,6 @@ }, "node_modules/character-entities-legacy": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", "license": "MIT", "funding": { "type": "github", @@ -15843,8 +11479,6 @@ }, "node_modules/character-reference-invalid": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", "license": "MIT", "funding": { "type": "github", @@ -15853,8 +11487,6 @@ }, "node_modules/charenc": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -15863,8 +11495,6 @@ }, "node_modules/chokidar": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "license": "MIT", "optional": true, @@ -15889,8 +11519,6 @@ }, "node_modules/chownr": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true, "license": "ISC", "engines": { @@ -15899,15 +11527,11 @@ }, "node_modules/chroma-js": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.2.0.tgz", - "integrity": "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw==", "dev": true, "license": "(BSD-3-Clause AND Apache-2.0)" }, "node_modules/chrome-launcher": { "version": "0.15.2", - "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", - "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", "license": "Apache-2.0", "dependencies": { "@types/node": "*", @@ -15924,8 +11548,6 @@ }, "node_modules/chromium-edge-launcher": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", - "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", "license": "Apache-2.0", "dependencies": { "@types/node": "*", @@ -15938,15 +11560,11 @@ }, "node_modules/chromium-pickle-js": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", "dev": true, "license": "MIT" }, "node_modules/ci-info": { "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "funding": [ { "type": "github", @@ -15960,8 +11578,6 @@ }, "node_modules/clean-stack": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.1.tgz", - "integrity": "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==", "dev": true, "license": "MIT", "dependencies": { @@ -15976,8 +11592,6 @@ }, "node_modules/cli-cursor": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, "license": "MIT", "dependencies": { @@ -15989,8 +11603,6 @@ }, "node_modules/cli-progress": { "version": "3.12.0", - "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", - "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", "dev": true, "license": "MIT", "dependencies": { @@ -16002,8 +11614,6 @@ }, "node_modules/cli-spinners": { "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "license": "MIT", "engines": { "node": ">=6" @@ -16014,8 +11624,6 @@ }, "node_modules/cli-truncate": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", "dev": true, "license": "MIT", "optional": true, @@ -16032,8 +11640,6 @@ }, "node_modules/cli-truncate/node_modules/slice-ansi": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", "dev": true, "license": "MIT", "optional": true, @@ -16048,14 +11654,10 @@ }, "node_modules/client-only": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -16068,8 +11670,6 @@ }, "node_modules/clone": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "license": "MIT", "engines": { "node": ">=0.8" @@ -16077,8 +11677,6 @@ }, "node_modules/clone-response": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "dev": true, "license": "MIT", "dependencies": { @@ -16090,15 +11688,11 @@ }, "node_modules/collect-v8-coverage": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", - "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", "dev": true, "license": "MIT" }, "node_modules/color": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1", @@ -16110,8 +11704,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -16122,14 +11714,10 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, "node_modules/color-string": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "license": "MIT", "dependencies": { "color-name": "^1.0.0", @@ -16138,14 +11726,10 @@ }, "node_modules/colorette": { "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", "dependencies": { @@ -16157,8 +11741,6 @@ }, "node_modules/comma-separated-tokens": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", "license": "MIT", "funding": { "type": "github", @@ -16167,8 +11749,6 @@ }, "node_modules/commander": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "license": "MIT", "engines": { "node": ">= 10" @@ -16176,8 +11756,6 @@ }, "node_modules/compare-version": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", "dev": true, "license": "MIT", "engines": { @@ -16186,8 +11764,6 @@ }, "node_modules/component-type": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/component-type/-/component-type-1.2.2.tgz", - "integrity": "sha512-99VUHREHiN5cLeHm3YLq312p6v+HUEcwtLCAtelvUDI6+SH5g5Cr85oNR2S1o6ywzL0ykMbuwLzM2ANocjEOIA==", "dev": true, "license": "MIT", "funding": { @@ -16196,8 +11772,6 @@ }, "node_modules/compressible": { "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" @@ -16208,8 +11782,6 @@ }, "node_modules/compression": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -16226,8 +11798,6 @@ }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -16235,14 +11805,10 @@ }, "node_modules/compression/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/compression/node_modules/negotiator": { "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -16250,14 +11816,10 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, "node_modules/concurrently": { "version": "9.2.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", - "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", "dev": true, "license": "MIT", "dependencies": { @@ -16281,8 +11843,6 @@ }, "node_modules/connect": { "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -16296,8 +11856,6 @@ }, "node_modules/connect/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -16305,14 +11863,10 @@ }, "node_modules/connect/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/consola": { "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "dev": true, "license": "MIT", "engines": { @@ -16321,8 +11875,6 @@ }, "node_modules/content-disposition": { "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -16333,8 +11885,6 @@ }, "node_modules/content-type": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -16342,14 +11892,10 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, "node_modules/cookie": { "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -16357,20 +11903,14 @@ }, "node_modules/cookie-es": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", - "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", "license": "MIT" }, "node_modules/cookie-signature": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, "node_modules/core-js-compat": { "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", - "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", "license": "MIT", "dependencies": { "browserslist": "^4.28.1" @@ -16382,15 +11922,11 @@ }, "node_modules/core-util-is": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true, "license": "MIT" }, "node_modules/cors": { "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", "dependencies": { "object-assign": "^4", @@ -16404,10 +11940,17 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/crc": { "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", "dev": true, "license": "MIT", "optional": true, @@ -16417,8 +11960,6 @@ }, "node_modules/crc/node_modules/buffer": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, "funding": [ { @@ -16443,21 +11984,15 @@ }, "node_modules/create-require": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true, "license": "MIT" }, "node_modules/crelt": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "license": "MIT" }, "node_modules/cross-env": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", - "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", "dev": true, "license": "MIT", "dependencies": { @@ -16474,8 +12009,6 @@ }, "node_modules/cross-fetch": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", "license": "MIT", "dependencies": { "node-fetch": "^2.7.0" @@ -16483,8 +12016,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -16497,8 +12028,6 @@ }, "node_modules/crypt": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -16507,8 +12036,6 @@ }, "node_modules/crypto-random-string": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true, "license": "MIT", "engines": { @@ -16517,8 +12044,6 @@ }, "node_modules/css-color-keywords": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", "license": "ISC", "engines": { "node": ">=4" @@ -16526,8 +12051,6 @@ }, "node_modules/css-in-js-utils": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", - "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", "license": "MIT", "dependencies": { "hyphenate-style-name": "^1.0.3" @@ -16535,8 +12058,6 @@ }, "node_modules/css-select": { "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -16551,8 +12072,6 @@ }, "node_modules/css-to-react-native": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", - "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", "license": "MIT", "dependencies": { "camelize": "^1.0.0", @@ -16562,8 +12081,6 @@ }, "node_modules/css-tree": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", "license": "MIT", "dependencies": { "mdn-data": "2.0.14", @@ -16575,8 +12092,6 @@ }, "node_modules/css-tree/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -16584,8 +12099,6 @@ }, "node_modules/css-what": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -16596,15 +12109,11 @@ }, "node_modules/cssom": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", "dev": true, "license": "MIT" }, "node_modules/cssstyle": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", "dev": true, "license": "MIT", "dependencies": { @@ -16616,21 +12125,514 @@ }, "node_modules/cssstyle/node_modules/cssom": { "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true, "license": "MIT" }, "node_modules/csstype": { "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/cytoscape": { + "version": "3.34.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.1.tgz", + "integrity": "sha512-Lr0RvH9H75y9ar8h9Toy6u4lxRSCcxUq+hHcQ26sVWo6BnaQp1gwEZOYqwuYTZhyW7npyKnNLP8oJ2p1/3OZ7g==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, "node_modules/data-urls": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16644,8 +12646,6 @@ }, "node_modules/data-urls/node_modules/tr46": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "dev": true, "license": "MIT", "dependencies": { @@ -16657,8 +12657,6 @@ }, "node_modules/data-urls/node_modules/webidl-conversions": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -16667,8 +12665,6 @@ }, "node_modules/data-urls/node_modules/whatwg-mimetype": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true, "license": "MIT", "engines": { @@ -16677,8 +12673,6 @@ }, "node_modules/data-urls/node_modules/whatwg-url": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16691,8 +12685,6 @@ }, "node_modules/data-view-buffer": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16709,8 +12701,6 @@ }, "node_modules/data-view-byte-length": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16727,8 +12717,6 @@ }, "node_modules/data-view-byte-offset": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16745,17 +12733,19 @@ }, "node_modules/dateformat": { "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", "license": "MIT", "engines": { "node": "*" } }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -16771,8 +12761,6 @@ }, "node_modules/decamelize": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz", - "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==", "dev": true, "license": "MIT", "engines": { @@ -16784,8 +12772,6 @@ }, "node_modules/decamelize-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", "dev": true, "license": "MIT", "dependencies": { @@ -16801,8 +12787,6 @@ }, "node_modules/decamelize-keys/node_modules/decamelize": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true, "license": "MIT", "engines": { @@ -16811,8 +12795,6 @@ }, "node_modules/decamelize-keys/node_modules/map-obj": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", "dev": true, "license": "MIT", "engines": { @@ -16821,15 +12803,11 @@ }, "node_modules/decimal.js": { "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, "license": "MIT" }, "node_modules/decode-named-character-reference": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", - "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -16841,8 +12819,6 @@ }, "node_modules/decode-uri-component": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "license": "MIT", "engines": { "node": ">=0.10" @@ -16850,8 +12826,6 @@ }, "node_modules/decompress-response": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16866,8 +12840,6 @@ }, "node_modules/decompress-response/node_modules/mimic-response": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "license": "MIT", "engines": { @@ -16879,8 +12851,6 @@ }, "node_modules/deep-extend": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", "engines": { "node": ">=4.0.0" @@ -16888,15 +12858,11 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/deep-rename-keys": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/deep-rename-keys/-/deep-rename-keys-0.2.1.tgz", - "integrity": "sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A==", "dev": true, "license": "MIT", "dependencies": { @@ -16909,8 +12875,6 @@ }, "node_modules/deep-rename-keys/node_modules/kind-of": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16922,8 +12886,6 @@ }, "node_modules/deepmerge": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -16931,8 +12893,6 @@ }, "node_modules/defaults": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "license": "MIT", "dependencies": { "clone": "^1.0.2" @@ -16943,8 +12903,6 @@ }, "node_modules/defer-to-connect": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "dev": true, "license": "MIT", "engines": { @@ -16953,8 +12911,6 @@ }, "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -16970,8 +12926,6 @@ }, "node_modules/define-lazy-prop": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "license": "MIT", "engines": { "node": ">=8" @@ -16979,8 +12933,6 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -16994,10 +12946,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", "engines": { @@ -17006,8 +12965,6 @@ }, "node_modules/depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -17015,8 +12972,6 @@ }, "node_modules/dequal": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "license": "MIT", "engines": { "node": ">=6" @@ -17024,8 +12979,6 @@ }, "node_modules/destroy": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "license": "MIT", "engines": { "node": ">= 0.8", @@ -17034,8 +12987,6 @@ }, "node_modules/detect-libc": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -17043,22 +12994,16 @@ }, "node_modules/detect-node": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true, "license": "MIT", "optional": true }, "node_modules/detect-node-es": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, "node_modules/devlop": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "license": "MIT", "dependencies": { "dequal": "^2.0.0" @@ -17070,8 +13015,6 @@ }, "node_modules/diff": { "version": "8.0.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", - "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -17079,8 +13022,6 @@ }, "node_modules/diff-sequences": { "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, "license": "MIT", "engines": { @@ -17089,14 +13030,10 @@ }, "node_modules/dijkstrajs": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, "node_modules/dir-compare": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", - "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", "dev": true, "license": "MIT", "dependencies": { @@ -17106,8 +13043,6 @@ }, "node_modules/dir-compare/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -17117,8 +13052,6 @@ }, "node_modules/dir-compare/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -17130,8 +13063,6 @@ }, "node_modules/dir-glob": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, "license": "MIT", "dependencies": { @@ -17143,8 +13074,6 @@ }, "node_modules/dmg-builder": { "version": "26.8.1", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", - "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "dev": true, "license": "MIT", "dependencies": { @@ -17160,8 +13089,6 @@ }, "node_modules/dmg-license": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", "dev": true, "license": "MIT", "optional": true, @@ -17187,8 +13114,6 @@ }, "node_modules/dmg-license/node_modules/ajv": { "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "optional": true, @@ -17205,16 +13130,12 @@ }, "node_modules/dmg-license/node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT", "optional": true }, "node_modules/doctrine": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -17226,15 +13147,11 @@ }, "node_modules/dom-accessibility-api": { "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, "license": "MIT" }, "node_modules/dom-serializer": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", @@ -17247,8 +13164,6 @@ }, "node_modules/domelementtype": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "funding": [ { "type": "github", @@ -17259,9 +13174,6 @@ }, "node_modules/domexception": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "deprecated": "Use your platform's native DOMException instead", "dev": true, "license": "MIT", "dependencies": { @@ -17273,8 +13185,6 @@ }, "node_modules/domexception/node_modules/webidl-conversions": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -17283,8 +13193,6 @@ }, "node_modules/domhandler": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" @@ -17298,15 +13206,20 @@ }, "node_modules/domino": { "version": "2.1.6", - "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", - "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==", "dev": true, "license": "BSD-2-Clause" }, + "node_modules/dompurify": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -17319,8 +13232,6 @@ }, "node_modules/dotenv": { "version": "17.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", - "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -17331,8 +13242,6 @@ }, "node_modules/dotenv-expand": { "version": "11.0.7", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", - "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", "license": "BSD-2-Clause", "dependencies": { "dotenv": "^16.4.5" @@ -17346,8 +13255,6 @@ }, "node_modules/dotenv-expand/node_modules/dotenv": { "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -17358,8 +13265,6 @@ }, "node_modules/dtrace-provider": { "version": "0.8.8", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", - "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", "dev": true, "hasInstallScript": true, "license": "BSD-2-Clause", @@ -17373,8 +13278,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -17387,8 +13290,6 @@ }, "node_modules/eas-cli": { "version": "16.32.0", - "resolved": "https://registry.npmjs.org/eas-cli/-/eas-cli-16.32.0.tgz", - "integrity": "sha512-4S40wdwi5+jAyn2c+sqvIIvT/IRwXfSbx2OvZrutYAfxeOiz9n2Hg/evJvArG60/iYT7Mvn5ZVzRJx/dEWksCw==", "dev": true, "license": "MIT", "dependencies": { @@ -17488,8 +13389,6 @@ }, "node_modules/eas-cli/node_modules/diff": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -17498,8 +13397,6 @@ }, "node_modules/eas-cli/node_modules/dotenv": { "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -17511,8 +13408,6 @@ }, "node_modules/eas-cli/node_modules/fast-glob": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "license": "MIT", "dependencies": { @@ -17528,8 +13423,6 @@ }, "node_modules/eas-cli/node_modules/fs-extra": { "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, "license": "MIT", "dependencies": { @@ -17543,8 +13436,6 @@ }, "node_modules/eas-cli/node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", "engines": { @@ -17553,8 +13444,6 @@ }, "node_modules/eas-cli/node_modules/nanoid": { "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -17572,8 +13461,6 @@ }, "node_modules/eas-cli/node_modules/node-fetch": { "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "dev": true, "license": "MIT", "dependencies": { @@ -17593,8 +13480,6 @@ }, "node_modules/eas-cli/node_modules/semver": { "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "license": "ISC", "dependencies": { @@ -17609,8 +13494,6 @@ }, "node_modules/eas-cli/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", "engines": { @@ -17619,15 +13502,11 @@ }, "node_modules/eas-cli/node_modules/tslib": { "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true, "license": "0BSD" }, "node_modules/eas-cli/node_modules/yaml": { "version": "2.6.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", - "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", "dev": true, "license": "ISC", "bin": { @@ -17639,15 +13518,11 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, "license": "MIT" }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -17656,14 +13531,10 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, "node_modules/ejs": { "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -17678,8 +13549,6 @@ }, "node_modules/electron": { "version": "41.2.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-41.2.0.tgz", - "integrity": "sha512-0OKLiymqfV0WK68RBXqAm3Myad2TpI5wwxLCBEUcH5Nugo3YfSk7p1Js/AL9266qTz5xZioUnxt9hG8FFwax0g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -17697,8 +13566,6 @@ }, "node_modules/electron-builder": { "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", - "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", "dev": true, "license": "MIT", "dependencies": { @@ -17723,8 +13590,6 @@ }, "node_modules/electron-builder/node_modules/ci-info": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "dev": true, "funding": [ { @@ -17739,8 +13604,6 @@ }, "node_modules/electron-log": { "version": "5.4.3", - "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.3.tgz", - "integrity": "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ==", "license": "MIT", "engines": { "node": ">= 14" @@ -17748,8 +13611,6 @@ }, "node_modules/electron-publish": { "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", - "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", "dev": true, "license": "MIT", "dependencies": { @@ -17765,8 +13626,6 @@ }, "node_modules/electron-publish/node_modules/mime": { "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, "license": "MIT", "bin": { @@ -17778,14 +13637,10 @@ }, "node_modules/electron-to-chromium": { "version": "1.5.307", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", - "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", "license": "ISC" }, "node_modules/electron-updater": { "version": "6.8.3", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz", - "integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==", "license": "MIT", "dependencies": { "builder-util-runtime": "9.5.1", @@ -17800,8 +13655,6 @@ }, "node_modules/electron/node_modules/@types/node": { "version": "24.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", - "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, "license": "MIT", "dependencies": { @@ -17810,15 +13663,11 @@ }, "node_modules/electron/node_modules/undici-types": { "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, "node_modules/emittery": { "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true, "license": "MIT", "engines": { @@ -17830,14 +13679,10 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -17845,8 +13690,6 @@ }, "node_modules/end-of-stream": { "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -17854,8 +13697,6 @@ }, "node_modules/enhanced-resolve": { "version": "5.20.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", - "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -17868,8 +13709,6 @@ }, "node_modules/entities": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -17880,8 +13719,6 @@ }, "node_modules/env-editor": { "version": "0.4.2", - "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", - "integrity": "sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==", "license": "MIT", "engines": { "node": ">=8" @@ -17889,8 +13726,6 @@ }, "node_modules/env-paths": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", - "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", "dev": true, "license": "MIT", "engines": { @@ -17899,15 +13734,11 @@ }, "node_modules/env-string": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/env-string/-/env-string-1.0.1.tgz", - "integrity": "sha512-/DhCJDf5DSFK32joQiWRpWrT0h7p3hVQfMKxiBb7Nt8C8IF8BYyPtclDnuGGLOoj16d/8udKeiE7JbkotDmorQ==", "dev": true, "license": "MIT" }, "node_modules/envinfo": { "version": "7.11.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", - "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", "dev": true, "license": "MIT", "bin": { @@ -17919,15 +13750,11 @@ }, "node_modules/err-code": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "dev": true, "license": "MIT" }, "node_modules/error-ex": { "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -17936,8 +13763,6 @@ }, "node_modules/error-stack-parser": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", "license": "MIT", "dependencies": { "stackframe": "^1.3.4" @@ -17945,8 +13770,6 @@ }, "node_modules/error-stack-parser-es": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", - "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" @@ -17954,8 +13777,6 @@ }, "node_modules/es-abstract": { "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", "dependencies": { @@ -18023,8 +13844,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -18032,8 +13851,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -18041,8 +13858,6 @@ }, "node_modules/es-iterator-helpers": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", - "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", "dev": true, "license": "MIT", "dependencies": { @@ -18069,15 +13884,11 @@ }, "node_modules/es-module-lexer": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -18088,8 +13899,6 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", "dependencies": { @@ -18104,8 +13913,6 @@ }, "node_modules/es-shim-unscopables": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, "license": "MIT", "dependencies": { @@ -18117,8 +13924,6 @@ }, "node_modules/es-to-primitive": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "license": "MIT", "dependencies": { @@ -18133,18 +13938,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] + }, "node_modules/es6-error": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true, "license": "MIT", "optional": true }, "node_modules/esbuild": { "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -18184,8 +13996,6 @@ }, "node_modules/escalade": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", "engines": { "node": ">=6" @@ -18193,14 +14003,10 @@ }, "node_modules/escape-html": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "license": "MIT", "engines": { "node": ">=10" @@ -18211,8 +14017,6 @@ }, "node_modules/escodegen": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -18233,8 +14037,6 @@ }, "node_modules/escodegen/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "optional": true, @@ -18245,8 +14047,6 @@ }, "node_modules/eslint": { "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { @@ -18305,8 +14105,6 @@ }, "node_modules/eslint-compat-utils": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", - "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -18321,8 +14119,6 @@ }, "node_modules/eslint-config-expo": { "version": "10.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-expo/-/eslint-config-expo-10.0.0.tgz", - "integrity": "sha512-/XC/DvniUWTzU7Ypb/cLDhDD4DXqEio4lug1ObD/oQ9Hcx3OVOR8Mkp4u6U4iGoZSJyIQmIk3WVHe/P1NYUXKw==", "dev": true, "license": "MIT", "dependencies": { @@ -18341,8 +14137,6 @@ }, "node_modules/eslint-config-expo/node_modules/globals": { "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "dev": true, "license": "MIT", "engines": { @@ -18354,8 +14148,6 @@ }, "node_modules/eslint-config-prettier": { "version": "8.10.2", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", - "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", "dev": true, "license": "MIT", "optional": true, @@ -18369,8 +14161,6 @@ }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, "license": "MIT", "dependencies": { @@ -18381,8 +14171,6 @@ }, "node_modules/eslint-import-resolver-node/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -18391,8 +14179,6 @@ }, "node_modules/eslint-import-resolver-typescript": { "version": "3.10.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", - "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", "dev": true, "license": "ISC", "dependencies": { @@ -18426,8 +14212,6 @@ }, "node_modules/eslint-module-utils": { "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -18444,8 +14228,6 @@ }, "node_modules/eslint-module-utils/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -18454,8 +14236,6 @@ }, "node_modules/eslint-plugin-es": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", - "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -18474,8 +14254,6 @@ }, "node_modules/eslint-plugin-es-x": { "version": "7.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", - "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", "dev": true, "funding": [ "https://github.com/sponsors/ota-meshi", @@ -18496,8 +14274,6 @@ }, "node_modules/eslint-plugin-expo": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-expo/-/eslint-plugin-expo-1.0.0.tgz", - "integrity": "sha512-qLtunR+cNFtC+jwYCBia5c/PJurMjSLMOV78KrEOyQK02ohZapU4dCFFnS2hfrJuw0zxfsjVkjqg3QBqi933QA==", "dev": true, "license": "MIT", "dependencies": { @@ -18514,8 +14290,6 @@ }, "node_modules/eslint-plugin-import": { "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { @@ -18548,8 +14322,6 @@ }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -18559,8 +14331,6 @@ }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -18569,8 +14339,6 @@ }, "node_modules/eslint-plugin-import/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -18582,8 +14350,6 @@ }, "node_modules/eslint-plugin-import/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -18592,8 +14358,6 @@ }, "node_modules/eslint-plugin-n": { "version": "17.24.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.24.0.tgz", - "integrity": "sha512-/gC7/KAYmfNnPNOb3eu8vw+TdVnV0zhdQwexsw6FLXbhzroVj20vRn2qL8lDWDGnAQ2J8DhdfvXxX9EoxvERvw==", "dev": true, "license": "MIT", "dependencies": { @@ -18619,8 +14383,6 @@ }, "node_modules/eslint-plugin-n/node_modules/globals": { "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", "dev": true, "license": "MIT", "engines": { @@ -18632,8 +14394,6 @@ }, "node_modules/eslint-plugin-n/node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -18642,8 +14402,6 @@ }, "node_modules/eslint-plugin-node": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", - "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", "dev": true, "license": "MIT", "dependencies": { @@ -18663,8 +14421,6 @@ }, "node_modules/eslint-plugin-node/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -18674,8 +14430,6 @@ }, "node_modules/eslint-plugin-node/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -18687,8 +14441,6 @@ }, "node_modules/eslint-plugin-node/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -18697,8 +14449,6 @@ }, "node_modules/eslint-plugin-prettier": { "version": "5.5.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", - "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", "dev": true, "license": "MIT", "dependencies": { @@ -18728,8 +14478,6 @@ }, "node_modules/eslint-plugin-react": { "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", "dependencies": { @@ -18761,8 +14509,6 @@ }, "node_modules/eslint-plugin-react-hooks": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "license": "MIT", "engines": { @@ -18774,8 +14520,6 @@ }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -18785,8 +14529,6 @@ }, "node_modules/eslint-plugin-react/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -18798,8 +14540,6 @@ }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", - "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", "dev": true, "license": "MIT", "dependencies": { @@ -18822,8 +14562,6 @@ }, "node_modules/eslint-plugin-react/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -18832,8 +14570,6 @@ }, "node_modules/eslint-scope": { "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -18849,8 +14585,6 @@ }, "node_modules/eslint-utils": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, "license": "MIT", "dependencies": { @@ -18865,8 +14599,6 @@ }, "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -18875,8 +14607,6 @@ }, "node_modules/eslint-visitor-keys": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -18888,8 +14618,6 @@ }, "node_modules/eslint/node_modules/ajv": { "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -18905,8 +14633,6 @@ }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -18916,8 +14642,6 @@ }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -18929,15 +14653,11 @@ }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -18949,8 +14669,6 @@ }, "node_modules/espree": { "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -18967,8 +14685,6 @@ }, "node_modules/esprima": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -18980,8 +14696,6 @@ }, "node_modules/esquery": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -18993,8 +14707,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -19006,8 +14718,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -19016,8 +14726,6 @@ }, "node_modules/estree-util-is-identifier-name": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", "license": "MIT", "funding": { "type": "opencollective", @@ -19026,8 +14734,6 @@ }, "node_modules/estree-walker": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -19036,8 +14742,6 @@ }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -19046,8 +14750,6 @@ }, "node_modules/etag": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -19055,8 +14757,6 @@ }, "node_modules/event-target-shim": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "license": "MIT", "engines": { "node": ">=6" @@ -19064,15 +14764,11 @@ }, "node_modules/eventemitter3": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", - "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==", "dev": true, "license": "MIT" }, "node_modules/events": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, "license": "MIT", "engines": { @@ -19081,8 +14777,6 @@ }, "node_modules/events-universal": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -19091,8 +14785,6 @@ }, "node_modules/eventsource": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "license": "MIT", "dependencies": { "eventsource-parser": "^3.0.1" @@ -19103,8 +14795,6 @@ }, "node_modules/eventsource-parser": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -19112,15 +14802,11 @@ }, "node_modules/exec-async": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz", - "integrity": "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==", "dev": true, "license": "MIT" }, "node_modules/expect": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, "license": "MIT", "dependencies": { @@ -19136,8 +14822,6 @@ }, "node_modules/expect-type": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -19146,8 +14830,6 @@ }, "node_modules/expo": { "version": "54.0.33", - "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz", - "integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", @@ -19198,8 +14880,6 @@ }, "node_modules/expo-application": { "version": "7.0.8", - "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.8.tgz", - "integrity": "sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q==", "license": "MIT", "peerDependencies": { "expo": "*" @@ -19207,8 +14887,6 @@ }, "node_modules/expo-asset": { "version": "12.0.12", - "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.12.tgz", - "integrity": "sha512-CsXFCQbx2fElSMn0lyTdRIyKlSXOal6ilLJd+yeZ6xaC7I9AICQgscY5nj0QcwgA+KYYCCEQEBndMsmj7drOWQ==", "license": "MIT", "dependencies": { "@expo/image-utils": "^0.8.8", @@ -19222,8 +14900,6 @@ }, "node_modules/expo-asset/node_modules/@expo/image-utils": { "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.12.tgz", - "integrity": "sha512-3KguH7kyKqq7pNwLb9j6BBdD/bjmNwXZG/HPWT6GWIXbwrvAJt2JNyYTP5agWJ8jbbuys1yuCzmkX+TU6rmI7A==", "license": "MIT", "dependencies": { "@expo/spawn-async": "^1.7.2", @@ -19237,8 +14913,6 @@ }, "node_modules/expo-asset/node_modules/getenv": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", - "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", "license": "MIT", "engines": { "node": ">=6" @@ -19246,8 +14920,6 @@ }, "node_modules/expo-audio": { "version": "1.0.16", - "resolved": "https://registry.npmjs.org/expo-audio/-/expo-audio-1.0.16.tgz", - "integrity": "sha512-j7otyjtO+8PVbemoCoRBr2Em0Kv9to3bfz5UpI5tDVVb5gD1dkn7sjv6/W6tWNqx14WLR1Wijh/ACecVv1Py+g==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -19258,8 +14930,6 @@ }, "node_modules/expo-build-properties": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/expo-build-properties/-/expo-build-properties-1.0.10.tgz", - "integrity": "sha512-mFCZbrbrv0AP5RB151tAoRzwRJelqM7bCJzCkxpu+owOyH+p/rFC/q7H5q8B9EpVWj8etaIuszR+gKwohpmu1Q==", "license": "MIT", "dependencies": { "ajv": "^8.11.0", @@ -19271,8 +14941,6 @@ }, "node_modules/expo-camera": { "version": "17.0.10", - "resolved": "https://registry.npmjs.org/expo-camera/-/expo-camera-17.0.10.tgz", - "integrity": "sha512-w1RBw83mAGVk4BPPwNrCZyFop0VLiVSRE3c2V9onWbdFwonpRhzmB4drygG8YOUTl1H3wQvALJHyMPTbgsK1Jg==", "license": "MIT", "dependencies": { "invariant": "^2.2.4" @@ -19291,8 +14959,6 @@ }, "node_modules/expo-constants": { "version": "18.0.13", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz", - "integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==", "license": "MIT", "dependencies": { "@expo/config": "~12.0.13", @@ -19305,8 +14971,6 @@ }, "node_modules/expo-constants/node_modules/@babel/code-frame": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "license": "MIT", "dependencies": { "@babel/highlight": "^7.10.4" @@ -19314,8 +14978,6 @@ }, "node_modules/expo-constants/node_modules/@expo/config": { "version": "12.0.13", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.13.tgz", - "integrity": "sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "~7.10.4", @@ -19335,8 +14997,6 @@ }, "node_modules/expo-constants/node_modules/@expo/config-plugins": { "version": "54.0.4", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.4.tgz", - "integrity": "sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q==", "license": "MIT", "dependencies": { "@expo/config-types": "^54.0.10", @@ -19357,14 +15017,10 @@ }, "node_modules/expo-constants/node_modules/@expo/config-types": { "version": "54.0.10", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-54.0.10.tgz", - "integrity": "sha512-/J16SC2an1LdtCZ67xhSkGXpALYUVUNyZws7v+PVsFZxClYehDSoKLqyRaGkpHlYrCc08bS0RF5E0JV6g50psA==", "license": "MIT" }, "node_modules/expo-constants/node_modules/@expo/env": { "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.11.tgz", - "integrity": "sha512-xV+ps6YCW7XIPVUwFVCRN2nox09dnRwy8uIjwHWTODu0zFw4kp4omnVkl0OOjuu2XOe7tdgAHxikrkJt9xB/7Q==", "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -19376,8 +15032,6 @@ }, "node_modules/expo-constants/node_modules/@expo/json-file": { "version": "10.0.12", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.12.tgz", - "integrity": "sha512-inbDycp1rMAelAofg7h/mMzIe+Owx6F7pur3XdQ3EPTy00tme+4P6FWgHKUcjN8dBSrnbRNpSyh5/shzHyVCyQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -19386,8 +15040,6 @@ }, "node_modules/expo-constants/node_modules/@expo/json-file/node_modules/@babel/code-frame": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -19400,8 +15052,6 @@ }, "node_modules/expo-constants/node_modules/@expo/plist": { "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.8.tgz", - "integrity": "sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ==", "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.8.8", @@ -19411,8 +15061,6 @@ }, "node_modules/expo-constants/node_modules/@xmldom/xmldom": { "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -19420,8 +15068,6 @@ }, "node_modules/expo-constants/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -19429,8 +15075,6 @@ }, "node_modules/expo-constants/node_modules/brace-expansion": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -19441,8 +15085,6 @@ }, "node_modules/expo-constants/node_modules/commander": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "license": "MIT", "engines": { "node": ">= 6" @@ -19450,8 +15092,6 @@ }, "node_modules/expo-constants/node_modules/dotenv": { "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -19462,8 +15102,6 @@ }, "node_modules/expo-constants/node_modules/getenv": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", - "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", "license": "MIT", "engines": { "node": ">=6" @@ -19471,8 +15109,6 @@ }, "node_modules/expo-constants/node_modules/glob": { "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "license": "BlueOak-1.0.0", "dependencies": { "minimatch": "^10.2.2", @@ -19488,8 +15124,6 @@ }, "node_modules/expo-constants/node_modules/lru-cache": { "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -19497,8 +15131,6 @@ }, "node_modules/expo-constants/node_modules/minimatch": { "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.2" @@ -19512,8 +15144,6 @@ }, "node_modules/expo-constants/node_modules/path-scurry": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -19528,8 +15158,6 @@ }, "node_modules/expo-constants/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "license": "MIT", "engines": { "node": ">=8" @@ -19537,8 +15165,6 @@ }, "node_modules/expo-constants/node_modules/sucrase": { "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -19559,8 +15185,6 @@ }, "node_modules/expo-constants/node_modules/xmlbuilder": { "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "license": "MIT", "engines": { "node": ">=8.0" @@ -19568,8 +15192,6 @@ }, "node_modules/expo-crypto": { "version": "15.0.8", - "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-15.0.8.tgz", - "integrity": "sha512-aF7A914TB66WIlTJvl5J6/itejfY78O7dq3ibvFltL9vnTALJ/7LYHvLT4fwmx9yUNS6ekLBtDGWivFWnj2Fcw==", "license": "MIT", "dependencies": { "base64-js": "^1.3.0" @@ -19580,8 +15202,6 @@ }, "node_modules/expo-dev-client": { "version": "6.0.20", - "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.20.tgz", - "integrity": "sha512-5XjoVlj1OxakNxy55j/AUaGPrDOlQlB6XdHLLWAw61w5ffSpUDHDnuZzKzs9xY1eIaogOqTOQaAzZ2ddBkdXLA==", "license": "MIT", "dependencies": { "expo-dev-launcher": "6.0.20", @@ -19596,8 +15216,6 @@ }, "node_modules/expo-dev-launcher": { "version": "6.0.20", - "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.20.tgz", - "integrity": "sha512-a04zHEeT9sB0L5EB38fz7sNnUKJ2Ar1pXpcyl60Ki8bXPNCs9rjY7NuYrDkP/irM8+1DklMBqHpyHiLyJ/R+EA==", "license": "MIT", "dependencies": { "ajv": "^8.11.0", @@ -19610,8 +15228,6 @@ }, "node_modules/expo-dev-menu": { "version": "7.0.18", - "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.18.tgz", - "integrity": "sha512-4kTdlHrnZCAWCT6tZRQHSSjZ7vECFisL4T+nsG/GJDo/jcHNaOVGV5qPV9wzlTxyMk3YOPggRw4+g7Ownrg5eA==", "license": "MIT", "dependencies": { "expo-dev-menu-interface": "2.0.0" @@ -19622,8 +15238,6 @@ }, "node_modules/expo-dev-menu-interface": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz", - "integrity": "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==", "license": "MIT", "peerDependencies": { "expo": "*" @@ -19631,8 +15245,6 @@ }, "node_modules/expo-document-picker": { "version": "14.0.8", - "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-14.0.8.tgz", - "integrity": "sha512-3tyQKpPqWWFlI8p9RiMX1+T1Zge5mEKeBuXWp1h8PEItFMUDSiOJbQ112sfdC6Hxt8wSxreV9bCRl/NgBdt+fA==", "license": "MIT", "peerDependencies": { "expo": "*" @@ -19640,8 +15252,6 @@ }, "node_modules/expo-file-system": { "version": "19.0.21", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz", - "integrity": "sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -19650,8 +15260,6 @@ }, "node_modules/expo-font": { "version": "14.0.11", - "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz", - "integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==", "license": "MIT", "dependencies": { "fontfaceobserver": "^2.1.0" @@ -19664,8 +15272,6 @@ }, "node_modules/expo-gradle-jvmargs": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/expo-gradle-jvmargs/-/expo-gradle-jvmargs-1.1.2.tgz", - "integrity": "sha512-VJRecrYlklVXEwafLyuZKUCnYEY9vJYvUy639LN3c5krlmunkYQphh/YzXDng8C9oihz6mr+cPbwEn6sv6fXyw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -19674,8 +15280,6 @@ }, "node_modules/expo-haptics": { "version": "15.0.8", - "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-15.0.8.tgz", - "integrity": "sha512-lftutojy8Qs8zaDzzjwM3gKHFZ8bOOEZDCkmh2Ddpe95Ra6kt2izeOfOfKuP/QEh0MZ1j9TfqippyHdRd1ZM9g==", "license": "MIT", "peerDependencies": { "expo": "*" @@ -19683,8 +15287,6 @@ }, "node_modules/expo-image": { "version": "3.0.11", - "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-3.0.11.tgz", - "integrity": "sha512-4TudfUCLgYgENv+f48omnU8tjS2S0Pd9EaON5/s1ZUBRwZ7K8acEr4NfvLPSaeXvxW24iLAiyQ7sV7BXQH3RoA==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -19700,8 +15302,6 @@ }, "node_modules/expo-image-loader": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-6.0.0.tgz", - "integrity": "sha512-nKs/xnOGw6ACb4g26xceBD57FKLFkSwEUTDXEDF3Gtcu3MqF3ZIYd3YM+sSb1/z9AKV1dYT7rMSGVNgsveXLIQ==", "license": "MIT", "peerDependencies": { "expo": "*" @@ -19709,8 +15309,6 @@ }, "node_modules/expo-image-manipulator": { "version": "14.0.8", - "resolved": "https://registry.npmjs.org/expo-image-manipulator/-/expo-image-manipulator-14.0.8.tgz", - "integrity": "sha512-sXsXjm7rIxLWZe0j2A41J/Ph53PpFJRdyzJ3EQ/qetxLUvS2m3K1sP5xy37px43qCf0l79N/i6XgFgenFV36/Q==", "license": "MIT", "dependencies": { "expo-image-loader": "~6.0.0" @@ -19721,8 +15319,6 @@ }, "node_modules/expo-image-picker": { "version": "17.0.10", - "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-17.0.10.tgz", - "integrity": "sha512-a2xrowp2trmvXyUWgX3O6Q2rZaa2C59AqivKI7+bm+wLvMfTEbZgldLX4rEJJhM8xtmEDTNU+lzjtObwzBRGaw==", "license": "MIT", "dependencies": { "expo-image-loader": "~6.0.0" @@ -19733,14 +15329,10 @@ }, "node_modules/expo-json-utils": { "version": "0.15.0", - "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", - "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==", "license": "MIT" }, "node_modules/expo-keep-awake": { "version": "15.0.8", - "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz", - "integrity": "sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -19749,8 +15341,6 @@ }, "node_modules/expo-linking": { "version": "8.0.11", - "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.11.tgz", - "integrity": "sha512-+VSaNL5om3kOp/SSKO5qe6cFgfSIWnnQDSbA7XLs3ECkYzXRquk5unxNS3pg7eK5kNUmQ4kgLI7MhTggAEUBLA==", "license": "MIT", "dependencies": { "expo-constants": "~18.0.12", @@ -19763,8 +15353,6 @@ }, "node_modules/expo-localization": { "version": "17.0.9", - "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-17.0.9.tgz", - "integrity": "sha512-k5eYHr7iLMob3M8cHH6bgDrDRmqnZG8xKGLhpx8ST+LsFXmSgYpJ/t0VwWm0gz64C/K669XhObdG3D6QwCGqhw==", "license": "MIT", "dependencies": { "rtl-detect": "^1.0.2" @@ -19776,8 +15364,6 @@ }, "node_modules/expo-manifests": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-1.0.10.tgz", - "integrity": "sha512-oxDUnURPcL4ZsOBY6X1DGWGuoZgVAFzp6PISWV7lPP2J0r8u1/ucuChBgpK7u1eLGFp6sDIPwXyEUCkI386XSQ==", "license": "MIT", "dependencies": { "@expo/config": "~12.0.11", @@ -19789,8 +15375,6 @@ }, "node_modules/expo-manifests/node_modules/@babel/code-frame": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "license": "MIT", "dependencies": { "@babel/highlight": "^7.10.4" @@ -19798,8 +15382,6 @@ }, "node_modules/expo-manifests/node_modules/@expo/config": { "version": "12.0.13", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.13.tgz", - "integrity": "sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "~7.10.4", @@ -19819,8 +15401,6 @@ }, "node_modules/expo-manifests/node_modules/@expo/config-plugins": { "version": "54.0.4", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.4.tgz", - "integrity": "sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q==", "license": "MIT", "dependencies": { "@expo/config-types": "^54.0.10", @@ -19841,14 +15421,10 @@ }, "node_modules/expo-manifests/node_modules/@expo/config-types": { "version": "54.0.10", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-54.0.10.tgz", - "integrity": "sha512-/J16SC2an1LdtCZ67xhSkGXpALYUVUNyZws7v+PVsFZxClYehDSoKLqyRaGkpHlYrCc08bS0RF5E0JV6g50psA==", "license": "MIT" }, "node_modules/expo-manifests/node_modules/@expo/json-file": { "version": "10.0.12", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.12.tgz", - "integrity": "sha512-inbDycp1rMAelAofg7h/mMzIe+Owx6F7pur3XdQ3EPTy00tme+4P6FWgHKUcjN8dBSrnbRNpSyh5/shzHyVCyQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -19857,8 +15433,6 @@ }, "node_modules/expo-manifests/node_modules/@expo/json-file/node_modules/@babel/code-frame": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -19871,8 +15445,6 @@ }, "node_modules/expo-manifests/node_modules/@expo/plist": { "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.8.tgz", - "integrity": "sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ==", "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.8.8", @@ -19882,8 +15454,6 @@ }, "node_modules/expo-manifests/node_modules/@xmldom/xmldom": { "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -19891,8 +15461,6 @@ }, "node_modules/expo-manifests/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -19900,8 +15468,6 @@ }, "node_modules/expo-manifests/node_modules/brace-expansion": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -19912,8 +15478,6 @@ }, "node_modules/expo-manifests/node_modules/commander": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "license": "MIT", "engines": { "node": ">= 6" @@ -19921,8 +15485,6 @@ }, "node_modules/expo-manifests/node_modules/getenv": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", - "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", "license": "MIT", "engines": { "node": ">=6" @@ -19930,8 +15492,6 @@ }, "node_modules/expo-manifests/node_modules/glob": { "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "license": "BlueOak-1.0.0", "dependencies": { "minimatch": "^10.2.2", @@ -19947,8 +15507,6 @@ }, "node_modules/expo-manifests/node_modules/lru-cache": { "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -19956,8 +15514,6 @@ }, "node_modules/expo-manifests/node_modules/minimatch": { "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.2" @@ -19971,8 +15527,6 @@ }, "node_modules/expo-manifests/node_modules/path-scurry": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -19987,8 +15541,6 @@ }, "node_modules/expo-manifests/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "license": "MIT", "engines": { "node": ">=8" @@ -19996,8 +15548,6 @@ }, "node_modules/expo-manifests/node_modules/sucrase": { "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -20018,8 +15568,6 @@ }, "node_modules/expo-manifests/node_modules/xmlbuilder": { "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "license": "MIT", "engines": { "node": ">=8.0" @@ -20027,8 +15575,6 @@ }, "node_modules/expo-modules-autolinking": { "version": "3.0.24", - "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.24.tgz", - "integrity": "sha512-TP+6HTwhL7orDvsz2VzauyQlXJcAWyU3ANsZ7JGL4DQu8XaZv/A41ZchbtAYLfozNA2Ya1Hzmhx65hXryBMjaQ==", "license": "MIT", "dependencies": { "@expo/spawn-async": "^1.7.2", @@ -20043,8 +15589,6 @@ }, "node_modules/expo-modules-core": { "version": "3.0.29", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-3.0.29.tgz", - "integrity": "sha512-LzipcjGqk8gvkrOUf7O2mejNWugPkf3lmd9GkqL9WuNyeN2fRwU0Dn77e3ZUKI3k6sI+DNwjkq4Nu9fNN9WS7Q==", "license": "MIT", "dependencies": { "invariant": "^2.2.4" @@ -20056,8 +15600,6 @@ }, "node_modules/expo-notifications": { "version": "0.32.16", - "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.32.16.tgz", - "integrity": "sha512-QQD/UA6v7LgvwIJ+tS7tSvqJZkdp0nCSj9MxsDk/jU1GttYdK49/5L2LvE/4U0H7sNBz1NZAyhDZozg8xgBLXw==", "license": "MIT", "dependencies": { "@expo/image-utils": "^0.8.8", @@ -20076,8 +15618,6 @@ }, "node_modules/expo-notifications/node_modules/@expo/image-utils": { "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.12.tgz", - "integrity": "sha512-3KguH7kyKqq7pNwLb9j6BBdD/bjmNwXZG/HPWT6GWIXbwrvAJt2JNyYTP5agWJ8jbbuys1yuCzmkX+TU6rmI7A==", "license": "MIT", "dependencies": { "@expo/spawn-async": "^1.7.2", @@ -20091,8 +15631,6 @@ }, "node_modules/expo-notifications/node_modules/getenv": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", - "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", "license": "MIT", "engines": { "node": ">=6" @@ -20100,8 +15638,6 @@ }, "node_modules/expo-router": { "version": "6.0.23", - "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.23.tgz", - "integrity": "sha512-qCxVAiCrCyu0npky6azEZ6dJDMt77OmCzEbpF6RbUTlfkaCA417LvY14SBkk0xyGruSxy/7pvJOI6tuThaUVCA==", "license": "MIT", "dependencies": { "@expo/metro-runtime": "^6.1.2", @@ -20171,8 +15707,6 @@ }, "node_modules/expo-router/node_modules/semver": { "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -20183,8 +15717,6 @@ }, "node_modules/expo-server": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.5.tgz", - "integrity": "sha512-IGR++flYH70rhLyeXF0Phle56/k4cee87WeQ4mamS+MkVAVP+dDlOHf2nN06Z9Y2KhU0Gp1k+y61KkghF7HdhA==", "license": "MIT", "engines": { "node": ">=20.16.0" @@ -20192,8 +15724,6 @@ }, "node_modules/expo-sharing": { "version": "14.0.8", - "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-14.0.8.tgz", - "integrity": "sha512-A1pPr2iBrxypFDCWVAESk532HK+db7MFXbvO2sCV9ienaFXAk7lIBm6bkqgE6vzRd9O3RGdEGzYx80cYlc089Q==", "license": "MIT", "peerDependencies": { "expo": "*" @@ -20201,8 +15731,6 @@ }, "node_modules/expo-splash-screen": { "version": "31.0.13", - "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-31.0.13.tgz", - "integrity": "sha512-1epJLC1cDlwwj089R2h8cxaU5uk4ONVAC+vzGiTZH4YARQhL4Stlz1MbR6yAS173GMosvkE6CAeihR7oIbCkDA==", "license": "MIT", "dependencies": { "@expo/prebuild-config": "^54.0.8" @@ -20213,8 +15741,6 @@ }, "node_modules/expo-splash-screen/node_modules/@expo/config": { "version": "12.0.13", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.13.tgz", - "integrity": "sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "~7.10.4", @@ -20234,8 +15760,6 @@ }, "node_modules/expo-splash-screen/node_modules/@expo/config-plugins": { "version": "54.0.4", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.4.tgz", - "integrity": "sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q==", "license": "MIT", "dependencies": { "@expo/config-types": "^54.0.10", @@ -20256,14 +15780,10 @@ }, "node_modules/expo-splash-screen/node_modules/@expo/config-types": { "version": "54.0.10", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-54.0.10.tgz", - "integrity": "sha512-/J16SC2an1LdtCZ67xhSkGXpALYUVUNyZws7v+PVsFZxClYehDSoKLqyRaGkpHlYrCc08bS0RF5E0JV6g50psA==", "license": "MIT" }, "node_modules/expo-splash-screen/node_modules/@expo/config/node_modules/@babel/code-frame": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "license": "MIT", "dependencies": { "@babel/highlight": "^7.10.4" @@ -20271,8 +15791,6 @@ }, "node_modules/expo-splash-screen/node_modules/@expo/image-utils": { "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.12.tgz", - "integrity": "sha512-3KguH7kyKqq7pNwLb9j6BBdD/bjmNwXZG/HPWT6GWIXbwrvAJt2JNyYTP5agWJ8jbbuys1yuCzmkX+TU6rmI7A==", "license": "MIT", "dependencies": { "@expo/spawn-async": "^1.7.2", @@ -20286,8 +15804,6 @@ }, "node_modules/expo-splash-screen/node_modules/@expo/json-file": { "version": "10.0.12", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.12.tgz", - "integrity": "sha512-inbDycp1rMAelAofg7h/mMzIe+Owx6F7pur3XdQ3EPTy00tme+4P6FWgHKUcjN8dBSrnbRNpSyh5/shzHyVCyQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -20296,8 +15812,6 @@ }, "node_modules/expo-splash-screen/node_modules/@expo/plist": { "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.8.tgz", - "integrity": "sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ==", "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.8.8", @@ -20307,8 +15821,6 @@ }, "node_modules/expo-splash-screen/node_modules/@expo/prebuild-config": { "version": "54.0.8", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-54.0.8.tgz", - "integrity": "sha512-EA7N4dloty2t5Rde+HP0IEE+nkAQiu4A/+QGZGT9mFnZ5KKjPPkqSyYcRvP5bhQE10D+tvz6X0ngZpulbMdbsg==", "license": "MIT", "dependencies": { "@expo/config": "~12.0.13", @@ -20328,14 +15840,10 @@ }, "node_modules/expo-splash-screen/node_modules/@react-native/normalize-colors": { "version": "0.81.5", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz", - "integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==", "license": "MIT" }, "node_modules/expo-splash-screen/node_modules/@xmldom/xmldom": { "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -20343,8 +15851,6 @@ }, "node_modules/expo-splash-screen/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -20352,8 +15858,6 @@ }, "node_modules/expo-splash-screen/node_modules/brace-expansion": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -20364,8 +15868,6 @@ }, "node_modules/expo-splash-screen/node_modules/commander": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "license": "MIT", "engines": { "node": ">= 6" @@ -20373,8 +15875,6 @@ }, "node_modules/expo-splash-screen/node_modules/getenv": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", - "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", "license": "MIT", "engines": { "node": ">=6" @@ -20382,8 +15882,6 @@ }, "node_modules/expo-splash-screen/node_modules/glob": { "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "license": "BlueOak-1.0.0", "dependencies": { "minimatch": "^10.2.2", @@ -20399,8 +15897,6 @@ }, "node_modules/expo-splash-screen/node_modules/lru-cache": { "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -20408,8 +15904,6 @@ }, "node_modules/expo-splash-screen/node_modules/minimatch": { "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.2" @@ -20423,8 +15917,6 @@ }, "node_modules/expo-splash-screen/node_modules/path-scurry": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -20439,8 +15931,6 @@ }, "node_modules/expo-splash-screen/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "license": "MIT", "engines": { "node": ">=8" @@ -20448,8 +15938,6 @@ }, "node_modules/expo-splash-screen/node_modules/sucrase": { "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -20470,8 +15958,6 @@ }, "node_modules/expo-splash-screen/node_modules/xmlbuilder": { "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "license": "MIT", "engines": { "node": ">=8.0" @@ -20479,8 +15965,6 @@ }, "node_modules/expo-system-ui": { "version": "6.0.9", - "resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-6.0.9.tgz", - "integrity": "sha512-eQTYGzw1V4RYiYHL9xDLYID3Wsec2aZS+ypEssmF64D38aDrqbDgz1a2MSlHLQp2jHXSs3FvojhZ9FVela1Zcg==", "license": "MIT", "dependencies": { "@react-native/normalize-colors": "0.81.5", @@ -20499,14 +15983,10 @@ }, "node_modules/expo-system-ui/node_modules/@react-native/normalize-colors": { "version": "0.81.5", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz", - "integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==", "license": "MIT" }, "node_modules/expo-updates-interface": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz", - "integrity": "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==", "license": "MIT", "peerDependencies": { "expo": "*" @@ -20514,8 +15994,6 @@ }, "node_modules/expo/node_modules/@babel/code-frame": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "license": "MIT", "dependencies": { "@babel/highlight": "^7.10.4" @@ -20523,8 +16001,6 @@ }, "node_modules/expo/node_modules/@expo/config": { "version": "12.0.13", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.13.tgz", - "integrity": "sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "~7.10.4", @@ -20544,8 +16020,6 @@ }, "node_modules/expo/node_modules/@expo/config-plugins": { "version": "54.0.4", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.4.tgz", - "integrity": "sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q==", "license": "MIT", "dependencies": { "@expo/config-types": "^54.0.10", @@ -20566,14 +16040,10 @@ }, "node_modules/expo/node_modules/@expo/config-types": { "version": "54.0.10", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-54.0.10.tgz", - "integrity": "sha512-/J16SC2an1LdtCZ67xhSkGXpALYUVUNyZws7v+PVsFZxClYehDSoKLqyRaGkpHlYrCc08bS0RF5E0JV6g50psA==", "license": "MIT" }, "node_modules/expo/node_modules/@expo/json-file": { "version": "10.0.12", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.12.tgz", - "integrity": "sha512-inbDycp1rMAelAofg7h/mMzIe+Owx6F7pur3XdQ3EPTy00tme+4P6FWgHKUcjN8dBSrnbRNpSyh5/shzHyVCyQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -20582,8 +16052,6 @@ }, "node_modules/expo/node_modules/@expo/json-file/node_modules/@babel/code-frame": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -20596,8 +16064,6 @@ }, "node_modules/expo/node_modules/@expo/plist": { "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.8.tgz", - "integrity": "sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ==", "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.8.8", @@ -20607,8 +16073,6 @@ }, "node_modules/expo/node_modules/@xmldom/xmldom": { "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -20616,8 +16080,6 @@ }, "node_modules/expo/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -20625,8 +16087,6 @@ }, "node_modules/expo/node_modules/brace-expansion": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -20637,8 +16097,6 @@ }, "node_modules/expo/node_modules/commander": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "license": "MIT", "engines": { "node": ">= 6" @@ -20646,8 +16104,6 @@ }, "node_modules/expo/node_modules/getenv": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", - "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", "license": "MIT", "engines": { "node": ">=6" @@ -20655,8 +16111,6 @@ }, "node_modules/expo/node_modules/glob": { "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "license": "BlueOak-1.0.0", "dependencies": { "minimatch": "^10.2.2", @@ -20672,8 +16126,6 @@ }, "node_modules/expo/node_modules/lru-cache": { "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -20681,8 +16133,6 @@ }, "node_modules/expo/node_modules/minimatch": { "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.2" @@ -20696,8 +16146,6 @@ }, "node_modules/expo/node_modules/path-scurry": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -20712,8 +16160,6 @@ }, "node_modules/expo/node_modules/react-refresh": { "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -20721,8 +16167,6 @@ }, "node_modules/expo/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "license": "MIT", "engines": { "node": ">=8" @@ -20730,8 +16174,6 @@ }, "node_modules/expo/node_modules/sucrase": { "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -20752,8 +16194,6 @@ }, "node_modules/expo/node_modules/xmlbuilder": { "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "license": "MIT", "engines": { "node": ">=8.0" @@ -20761,14 +16201,10 @@ }, "node_modules/exponential-backoff": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "license": "Apache-2.0" }, "node_modules/express": { "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -20813,8 +16249,6 @@ }, "node_modules/express-rate-limit": { "version": "8.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", - "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", "license": "MIT", "dependencies": { "ip-address": "^10.2.0" @@ -20831,8 +16265,6 @@ }, "node_modules/express/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -20840,8 +16272,6 @@ }, "node_modules/express/node_modules/finalhandler": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -20858,26 +16288,18 @@ }, "node_modules/express/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/exsolve": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "license": "MIT" }, "node_modules/extend": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, "node_modules/extract-zip": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -20897,8 +16319,6 @@ }, "node_modules/extract-zip/node_modules/get-stream": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "license": "MIT", "dependencies": { @@ -20913,8 +16333,6 @@ }, "node_modules/extsprintf": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", "dev": true, "engines": [ "node >=0.6.0" @@ -20924,34 +16342,24 @@ }, "node_modules/fast-copy": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", - "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, "node_modules/fast-diff": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true, "license": "Apache-2.0" }, "node_modules/fast-fifo": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "dev": true, "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -20967,33 +16375,23 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fast-safe-stringify": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "license": "MIT" }, "node_modules/fast-sha256": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", - "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", "license": "Unlicense" }, "node_modules/fast-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "funding": [ { "type": "github", @@ -21008,8 +16406,6 @@ }, "node_modules/fastq": { "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -21018,8 +16414,6 @@ }, "node_modules/fb-watchman": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "license": "Apache-2.0", "dependencies": { "bser": "2.1.1" @@ -21027,8 +16421,6 @@ }, "node_modules/fbjs": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", - "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", "license": "MIT", "dependencies": { "cross-fetch": "^3.1.5", @@ -21042,14 +16434,10 @@ }, "node_modules/fbjs-css-vars": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", - "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", "license": "MIT" }, "node_modules/fbjs/node_modules/promise": { "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", "license": "MIT", "dependencies": { "asap": "~2.0.3" @@ -21057,8 +16445,6 @@ }, "node_modules/fd-package-json": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", - "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", "dev": true, "license": "MIT", "dependencies": { @@ -21067,8 +16453,6 @@ }, "node_modules/fd-slicer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, "license": "MIT", "dependencies": { @@ -21077,8 +16461,6 @@ }, "node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", "engines": { "node": ">=12.0.0" @@ -21094,28 +16476,20 @@ }, "node_modules/fetch-retry": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-4.1.1.tgz", - "integrity": "sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA==", "dev": true, "license": "MIT" }, "node_modules/fetchdts": { "version": "0.1.7", - "resolved": "https://registry.npmjs.org/fetchdts/-/fetchdts-0.1.7.tgz", - "integrity": "sha512-YoZjBdafyLIop9lSxXVI33oLD5kN31q4Td+CasofLLYeLXRFeOsuOw0Uo+XNRi9PZlbfdlN2GmRtm4tCEQ9/KA==", "license": "MIT" }, "node_modules/fflate": { "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", "dev": true, "license": "MIT" }, "node_modules/figures": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, "license": "MIT", "dependencies": { @@ -21130,8 +16504,6 @@ }, "node_modules/figures/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", "engines": { @@ -21140,8 +16512,6 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -21153,15 +16523,11 @@ }, "node_modules/file-uri-to-path": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "dev": true, "license": "MIT" }, "node_modules/filelist": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", - "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -21170,8 +16536,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -21182,8 +16546,6 @@ }, "node_modules/filter-obj": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -21191,8 +16553,6 @@ }, "node_modules/finalhandler": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -21209,8 +16569,6 @@ }, "node_modules/finalhandler/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -21218,8 +16576,6 @@ }, "node_modules/finalhandler/node_modules/encodeurl": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -21227,14 +16583,10 @@ }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/finalhandler/node_modules/on-finished": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -21245,8 +16597,6 @@ }, "node_modules/finalhandler/node_modules/statuses": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -21254,8 +16604,6 @@ }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -21271,8 +16619,6 @@ }, "node_modules/find-yarn-workspace-root": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", - "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -21281,8 +16627,6 @@ }, "node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -21295,21 +16639,15 @@ }, "node_modules/flatted": { "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/flow-enums-runtime": { "version": "0.0.6", - "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", - "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", "license": "MIT" }, "node_modules/follow-redirects": { "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "dev": true, "funding": [ { @@ -21329,14 +16667,10 @@ }, "node_modules/fontfaceobserver": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz", - "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==", "license": "BSD-2-Clause" }, "node_modules/for-each": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -21350,8 +16684,6 @@ }, "node_modules/foreground-child": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, "license": "ISC", "dependencies": { @@ -21367,8 +16699,6 @@ }, "node_modules/form-data": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { @@ -21384,8 +16714,6 @@ }, "node_modules/formatly": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz", - "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==", "dev": true, "license": "MIT", "dependencies": { @@ -21400,8 +16728,6 @@ }, "node_modules/forwarded": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -21409,8 +16735,6 @@ }, "node_modules/framer-motion": { "version": "12.35.2", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.35.2.tgz", - "integrity": "sha512-dhfuEMaNo0hc+AEqyHiIfiJRNb9U9UQutE9FoKm5pjf7CMitp9xPEF1iWZihR1q86LBmo6EJ7S8cN8QXEy49AA==", "license": "MIT", "dependencies": { "motion-dom": "^12.35.2", @@ -21436,8 +16760,6 @@ }, "node_modules/freeport-async": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz", - "integrity": "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==", "license": "MIT", "engines": { "node": ">=8" @@ -21445,8 +16767,6 @@ }, "node_modules/fresh": { "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -21454,8 +16774,6 @@ }, "node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -21468,8 +16786,6 @@ }, "node_modules/fs-minipass": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "dev": true, "license": "ISC", "dependencies": { @@ -21481,8 +16797,6 @@ }, "node_modules/fs-minipass/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "license": "ISC", "dependencies": { @@ -21494,22 +16808,15 @@ }, "node_modules/fs-readdir-recursive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", "dev": true, "license": "MIT" }, "node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -21521,8 +16828,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -21530,8 +16835,6 @@ }, "node_modules/function.prototype.name": { "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -21551,8 +16854,6 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, "license": "MIT", "funding": { @@ -21561,8 +16862,6 @@ }, "node_modules/generator-function": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -21570,8 +16869,6 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -21579,8 +16876,6 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -21588,8 +16883,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -21612,8 +16905,6 @@ }, "node_modules/get-nonce": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", "license": "MIT", "engines": { "node": ">=6" @@ -21621,8 +16912,6 @@ }, "node_modules/get-package-type": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "license": "MIT", "engines": { "node": ">=8.0.0" @@ -21630,8 +16919,6 @@ }, "node_modules/get-port": { "version": "6.1.2", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-6.1.2.tgz", - "integrity": "sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==", "dev": true, "license": "MIT", "engines": { @@ -21643,8 +16930,6 @@ }, "node_modules/get-port-cli": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-port-cli/-/get-port-cli-3.0.0.tgz", - "integrity": "sha512-060GMr81KapTzSobWNrQVAqHeUaFRZhPj/lNnzdCcfVodFN497wRgEamnTCNgldJuiR6TXxdtkFidcYQ/nSVDA==", "dev": true, "license": "MIT", "dependencies": { @@ -21663,8 +16948,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -21676,8 +16959,6 @@ }, "node_modules/get-symbol-description": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "license": "MIT", "dependencies": { @@ -21694,8 +16975,6 @@ }, "node_modules/get-tsconfig": { "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -21707,8 +16986,6 @@ }, "node_modules/getenv": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/getenv/-/getenv-1.0.0.tgz", - "integrity": "sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==", "dev": true, "license": "MIT", "engines": { @@ -21717,15 +16994,10 @@ }, "node_modules/github-slugger": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", - "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", "license": "ISC" }, "node_modules/glob": { "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -21745,8 +17017,6 @@ }, "node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -21758,8 +17028,6 @@ }, "node_modules/glob/node_modules/minimatch": { "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { @@ -21774,8 +17042,6 @@ }, "node_modules/global-agent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "dev": true, "license": "BSD-3-Clause", "optional": true, @@ -21793,8 +17059,6 @@ }, "node_modules/global-agent/node_modules/serialize-error": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", "dev": true, "license": "MIT", "optional": true, @@ -21810,8 +17074,6 @@ }, "node_modules/global-agent/node_modules/type-fest": { "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true, "license": "(MIT OR CC0-1.0)", "optional": true, @@ -21824,8 +17086,6 @@ }, "node_modules/globals": { "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { @@ -21837,8 +17097,6 @@ }, "node_modules/globalthis": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -21854,8 +17112,6 @@ }, "node_modules/globby": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "license": "MIT", "dependencies": { @@ -21875,8 +17131,6 @@ }, "node_modules/globby/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", "engines": { @@ -21885,21 +17139,15 @@ }, "node_modules/globrex": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "dev": true, "license": "MIT" }, "node_modules/golden-fleece": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/golden-fleece/-/golden-fleece-1.0.9.tgz", - "integrity": "sha512-YSwLaGMOgSBx9roJlNLL12c+FRiw7VECphinc6mGucphc/ZxTHgdEz6gmJqH6NOzYEd/yr64hwjom5pZ+tJVpg==", "dev": true }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -21910,8 +17158,6 @@ }, "node_modules/got": { "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", "dev": true, "license": "MIT", "dependencies": { @@ -21936,8 +17182,6 @@ }, "node_modules/got/node_modules/@sindresorhus/is": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true, "license": "MIT", "engines": { @@ -21949,14 +17193,10 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, "node_modules/gradle-to-js": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/gradle-to-js/-/gradle-to-js-2.0.1.tgz", - "integrity": "sha512-is3hDn9zb8XXnjbEeAEIqxTpLHUiGBqjegLmXPuyMBfKAggpadWFku4/AP8iYAGBX6qR9/5UIUIp47V0XI3aMw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -21968,8 +17208,6 @@ }, "node_modules/graphql": { "version": "16.8.1", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", - "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", "devOptional": true, "license": "MIT", "engines": { @@ -21978,8 +17216,6 @@ }, "node_modules/graphql-tag": { "version": "2.12.6", - "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", - "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", "dev": true, "license": "MIT", "dependencies": { @@ -21995,8 +17231,6 @@ "node_modules/h3-v2": { "name": "h3", "version": "2.0.1-rc.20", - "resolved": "https://registry.npmjs.org/h3/-/h3-2.0.1-rc.20.tgz", - "integrity": "sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==", "license": "MIT", "dependencies": { "rou3": "^0.8.1", @@ -22017,10 +17251,14 @@ } } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/hard-rejection": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", "dev": true, "license": "MIT", "engines": { @@ -22029,8 +17267,6 @@ }, "node_modules/has-bigints": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, "license": "MIT", "engines": { @@ -22042,8 +17278,6 @@ }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", "engines": { "node": ">=8" @@ -22051,8 +17285,6 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -22063,8 +17295,6 @@ }, "node_modules/has-proto": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, "license": "MIT", "dependencies": { @@ -22079,8 +17309,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -22091,8 +17319,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -22106,8 +17332,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -22118,8 +17342,6 @@ }, "node_modules/hast-util-from-html": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", - "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -22136,8 +17358,6 @@ }, "node_modules/hast-util-from-parse5": { "version": "8.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", - "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -22156,8 +17376,6 @@ }, "node_modules/hast-util-heading-rank": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", - "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -22169,8 +17387,6 @@ }, "node_modules/hast-util-is-element": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", - "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -22182,8 +17398,6 @@ }, "node_modules/hast-util-parse-selector": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -22195,8 +17409,6 @@ }, "node_modules/hast-util-to-html": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", - "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -22218,8 +17430,6 @@ }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -22245,8 +17455,6 @@ }, "node_modules/hast-util-to-string": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", - "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -22258,8 +17466,6 @@ }, "node_modules/hast-util-whitespace": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -22271,8 +17477,6 @@ }, "node_modules/hastscript": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", - "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -22288,20 +17492,14 @@ }, "node_modules/help-me": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", - "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", "license": "MIT" }, "node_modules/hermes-estree": { "version": "0.29.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz", - "integrity": "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==", "license": "MIT" }, "node_modules/hermes-parser": { "version": "0.29.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.29.1.tgz", - "integrity": "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==", "license": "MIT", "dependencies": { "hermes-estree": "0.29.1" @@ -22309,8 +17507,6 @@ }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" @@ -22318,14 +17514,10 @@ }, "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/hono": { "version": "4.12.18", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", - "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -22333,8 +17525,6 @@ }, "node_modules/hosted-git-info": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, "license": "ISC", "dependencies": { @@ -22346,8 +17536,6 @@ }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", "dev": true, "license": "MIT", "dependencies": { @@ -22359,9 +17547,6 @@ }, "node_modules/html-encoding-sniffer/node_modules/whatwg-encoding": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "dev": true, "license": "MIT", "dependencies": { @@ -22373,8 +17558,6 @@ }, "node_modules/html-parse-stringify": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", "license": "MIT", "dependencies": { "void-elements": "3.1.0" @@ -22382,8 +17565,6 @@ }, "node_modules/html-url-attributes": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", - "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -22392,8 +17573,6 @@ }, "node_modules/html-void-elements": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", "license": "MIT", "funding": { "type": "github", @@ -22402,15 +17581,11 @@ }, "node_modules/http-cache-semantics": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/http-call": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/http-call/-/http-call-5.3.0.tgz", - "integrity": "sha512-ahwimsC23ICE4kPl9xTBjKB4inbRaeLyZeRunC/1Jy/Z6X8tv22MEAjK+KBOMSVLaqXPTTmd8638waVIKLGx2w==", "dev": true, "license": "ISC", "dependencies": { @@ -22427,8 +17602,6 @@ }, "node_modules/http-call/node_modules/parse-json": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", "dev": true, "license": "MIT", "dependencies": { @@ -22441,8 +17614,6 @@ }, "node_modules/http-errors": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { "depd": "~2.0.0", @@ -22461,8 +17632,6 @@ }, "node_modules/http-proxy-agent": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "dev": true, "license": "MIT", "dependencies": { @@ -22476,8 +17645,6 @@ }, "node_modules/http2-wrapper": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", "dev": true, "license": "MIT", "dependencies": { @@ -22490,8 +17657,6 @@ }, "node_modules/https-proxy-agent": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "license": "MIT", "dependencies": { @@ -22504,8 +17669,6 @@ }, "node_modules/hyperlinker": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hyperlinker/-/hyperlinker-1.0.0.tgz", - "integrity": "sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ==", "dev": true, "license": "MIT", "engines": { @@ -22514,14 +17677,10 @@ }, "node_modules/hyphenate-style-name": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", - "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", "license": "BSD-3-Clause" }, "node_modules/i18next": { "version": "26.3.0", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.0.tgz", - "integrity": "sha512-gHSgGpUXVmuqE2El1W61DmxeyeTlFfZgdJRWMo9jScAn5pu7TuTuiccb1zh3E2J9hEBVGJ23+96x0ieBhfuIHA==", "funding": [ { "type": "individual", @@ -22548,8 +17707,6 @@ }, "node_modules/iconv-corefoundation": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", "dev": true, "license": "MIT", "optional": true, @@ -22566,17 +17723,12 @@ }, "node_modules/iconv-corefoundation/node_modules/node-addon-api": { "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", "dev": true, "license": "MIT", "optional": true }, "node_modules/iconv-lite": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -22587,8 +17739,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -22607,8 +17757,6 @@ }, "node_modules/ignore": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", "dev": true, "license": "MIT", "engines": { @@ -22617,8 +17765,6 @@ }, "node_modules/image-size": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", "license": "MIT", "dependencies": { "queue": "6.0.2" @@ -22632,15 +17778,11 @@ }, "node_modules/immediate": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "dev": true, "license": "MIT" }, "node_modules/import-fresh": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -22656,18 +17798,24 @@ }, "node_modules/import-fresh/node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "license": "MIT", "engines": { "node": ">=0.8.19" @@ -22675,8 +17823,6 @@ }, "node_modules/indent-string": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", "dev": true, "license": "MIT", "engines": { @@ -22688,9 +17834,6 @@ }, "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -22699,26 +17842,18 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/inline-style-parser": { "version": "0.2.7", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", - "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, "node_modules/inline-style-prefixer": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz", - "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==", "license": "MIT", "dependencies": { "css-in-js-utils": "^3.1.0" @@ -22726,8 +17861,6 @@ }, "node_modules/inspect-webkit": { "version": "0.0.5", - "resolved": "https://registry.npmjs.org/inspect-webkit/-/inspect-webkit-0.0.5.tgz", - "integrity": "sha512-584wP/2nJO1LX74nqHP2j0tQzlK9ZTi+D0Z9qeLQjtUR/LCMXQHGX8M0vrsqBwTeGakF7q5GMFpg81nxUYlCvw==", "dev": true, "license": "MIT", "bin": { @@ -22739,8 +17872,6 @@ }, "node_modules/internal-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", "dependencies": { @@ -22752,10 +17883,17 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/invariant": { "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" @@ -22763,8 +17901,6 @@ }, "node_modules/ip-address": { "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" @@ -22772,8 +17908,6 @@ }, "node_modules/ipaddr.js": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", "engines": { "node": ">= 0.10" @@ -22781,8 +17915,6 @@ }, "node_modules/is-alphabetical": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", "license": "MIT", "funding": { "type": "github", @@ -22791,8 +17923,6 @@ }, "node_modules/is-alphanumerical": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", @@ -22805,8 +17935,6 @@ }, "node_modules/is-arguments": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -22821,8 +17949,6 @@ }, "node_modules/is-array-buffer": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", "dependencies": { @@ -22839,15 +17965,11 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, "license": "MIT" }, "node_modules/is-async-function": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -22866,8 +17988,6 @@ }, "node_modules/is-bigint": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -22882,8 +18002,6 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "license": "MIT", "optional": true, @@ -22896,8 +18014,6 @@ }, "node_modules/is-boolean-object": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "license": "MIT", "dependencies": { @@ -22913,15 +18029,11 @@ }, "node_modules/is-buffer": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true, "license": "MIT" }, "node_modules/is-bun-module": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -22930,8 +18042,6 @@ }, "node_modules/is-callable": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -22942,8 +18052,6 @@ }, "node_modules/is-core-module": { "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -22957,8 +18065,6 @@ }, "node_modules/is-data-view": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "license": "MIT", "dependencies": { @@ -22975,8 +18081,6 @@ }, "node_modules/is-date-object": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "license": "MIT", "dependencies": { @@ -22992,8 +18096,6 @@ }, "node_modules/is-decimal": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", "license": "MIT", "funding": { "type": "github", @@ -23002,8 +18104,6 @@ }, "node_modules/is-docker": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "license": "MIT", "bin": { "is-docker": "cli.js" @@ -23017,8 +18117,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -23027,8 +18125,6 @@ }, "node_modules/is-finalizationregistry": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, "license": "MIT", "dependencies": { @@ -23043,8 +18139,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { "node": ">=8" @@ -23052,8 +18146,6 @@ }, "node_modules/is-generator-function": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.4", @@ -23071,8 +18163,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -23084,8 +18174,6 @@ }, "node_modules/is-hexadecimal": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", "license": "MIT", "funding": { "type": "github", @@ -23094,8 +18182,6 @@ }, "node_modules/is-interactive": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "dev": true, "license": "MIT", "engines": { @@ -23104,8 +18190,6 @@ }, "node_modules/is-map": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, "license": "MIT", "engines": { @@ -23117,8 +18201,6 @@ }, "node_modules/is-nan": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", "license": "MIT", "dependencies": { "call-bind": "^1.0.0", @@ -23133,8 +18215,6 @@ }, "node_modules/is-negative-zero": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "license": "MIT", "engines": { @@ -23146,8 +18226,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "engines": { "node": ">=0.12.0" @@ -23155,8 +18233,6 @@ }, "node_modules/is-number-object": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { @@ -23172,8 +18248,6 @@ }, "node_modules/is-plain-obj": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, "license": "MIT", "engines": { @@ -23182,21 +18256,15 @@ }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, "license": "MIT" }, "node_modules/is-promise": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -23213,8 +18281,6 @@ }, "node_modules/is-retry-allowed": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", - "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", "dev": true, "license": "MIT", "engines": { @@ -23223,8 +18289,6 @@ }, "node_modules/is-set": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, "license": "MIT", "engines": { @@ -23236,8 +18300,6 @@ }, "node_modules/is-shared-array-buffer": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", "dependencies": { @@ -23252,8 +18314,6 @@ }, "node_modules/is-stream": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "license": "MIT", "engines": { @@ -23265,8 +18325,6 @@ }, "node_modules/is-string": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { @@ -23282,8 +18340,6 @@ }, "node_modules/is-symbol": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", "dependencies": { @@ -23300,8 +18356,6 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -23315,8 +18369,6 @@ }, "node_modules/is-unicode-supported": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, "license": "MIT", "engines": { @@ -23328,8 +18380,6 @@ }, "node_modules/is-weakmap": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, "license": "MIT", "engines": { @@ -23341,8 +18391,6 @@ }, "node_modules/is-weakref": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, "license": "MIT", "dependencies": { @@ -23357,8 +18405,6 @@ }, "node_modules/is-weakset": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", "dependencies": { @@ -23374,8 +18420,6 @@ }, "node_modules/is-wsl": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "license": "MIT", "dependencies": { "is-docker": "^2.0.0" @@ -23386,15 +18430,11 @@ }, "node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true, "license": "MIT" }, "node_modules/isbinaryfile": { "version": "5.0.7", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", - "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", "dev": true, "license": "MIT", "engines": { @@ -23406,8 +18446,6 @@ }, "node_modules/isbot": { "version": "5.1.35", - "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.35.tgz", - "integrity": "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg==", "license": "Unlicense", "engines": { "node": ">=18" @@ -23415,14 +18453,10 @@ }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "license": "BSD-3-Clause", "engines": { "node": ">=8" @@ -23430,8 +18464,6 @@ }, "node_modules/istanbul-lib-instrument": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", @@ -23446,8 +18478,6 @@ }, "node_modules/istanbul-lib-instrument/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -23455,8 +18485,6 @@ }, "node_modules/iterator.prototype": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "license": "MIT", "dependencies": { @@ -23473,8 +18501,6 @@ }, "node_modules/jackspeak": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -23489,8 +18515,6 @@ }, "node_modules/jake": { "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -23507,8 +18531,6 @@ }, "node_modules/jest-diff": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "license": "MIT", "dependencies": { @@ -23523,8 +18545,6 @@ }, "node_modules/jest-environment-jsdom": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", - "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", "dev": true, "license": "MIT", "dependencies": { @@ -23551,8 +18571,6 @@ }, "node_modules/jest-environment-node": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -23568,8 +18586,6 @@ }, "node_modules/jest-get-type": { "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -23577,8 +18593,6 @@ }, "node_modules/jest-haste-map": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -23602,8 +18616,6 @@ }, "node_modules/jest-matcher-utils": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, "license": "MIT", "dependencies": { @@ -23618,8 +18630,6 @@ }, "node_modules/jest-message-util": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.13", @@ -23638,8 +18648,6 @@ }, "node_modules/jest-message-util/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "license": "MIT", "engines": { "node": ">=8" @@ -23647,8 +18655,6 @@ }, "node_modules/jest-mock": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -23661,8 +18667,6 @@ }, "node_modules/jest-regex-util": { "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -23670,8 +18674,6 @@ }, "node_modules/jest-snapshot": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, "license": "MIT", "dependencies": { @@ -23703,8 +18705,6 @@ "node_modules/jest-snapshot-prettier": { "name": "prettier", "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, "license": "MIT", "bin": { @@ -23719,8 +18719,6 @@ }, "node_modules/jest-util": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -23736,8 +18734,6 @@ }, "node_modules/jest-util/node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -23748,8 +18744,6 @@ }, "node_modules/jest-validate": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -23765,8 +18759,6 @@ }, "node_modules/jest-watch-select-projects": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jest-watch-select-projects/-/jest-watch-select-projects-2.0.0.tgz", - "integrity": "sha512-j00nW4dXc2NiCW6znXgFLF9g8PJ0zP25cpQ1xRro/HU2GBfZQFZD0SoXnAlaoKkIY4MlfTMkKGbNXFpvCdjl1w==", "dev": true, "license": "MIT", "dependencies": { @@ -23777,8 +18769,6 @@ }, "node_modules/jest-watch-select-projects/node_modules/chalk": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "dev": true, "license": "MIT", "dependencies": { @@ -23791,8 +18781,6 @@ }, "node_modules/jest-watch-select-projects/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -23804,8 +18792,6 @@ }, "node_modules/jest-watch-typeahead": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-2.2.1.tgz", - "integrity": "sha512-jYpYmUnTzysmVnwq49TAxlmtOAwp8QIqvZyoofQFn8fiWhEDZj33ZXzg3JA4nGnzWFm1hbWf3ADpteUokvXgFA==", "dev": true, "license": "MIT", "dependencies": { @@ -23826,8 +18812,6 @@ }, "node_modules/jest-watch-typeahead/node_modules/ansi-escapes": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", - "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", "dev": true, "license": "MIT", "engines": { @@ -23839,8 +18823,6 @@ }, "node_modules/jest-watch-typeahead/node_modules/ansi-regex": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -23852,8 +18834,6 @@ }, "node_modules/jest-watch-typeahead/node_modules/slash": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, "license": "MIT", "engines": { @@ -23865,8 +18845,6 @@ }, "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { @@ -23881,8 +18859,6 @@ }, "node_modules/jest-watcher": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, "license": "MIT", "dependencies": { @@ -23901,8 +18877,6 @@ }, "node_modules/jest-watcher/node_modules/char-regex": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, "license": "MIT", "engines": { @@ -23911,8 +18885,6 @@ }, "node_modules/jest-watcher/node_modules/string-length": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", "dev": true, "license": "MIT", "dependencies": { @@ -23925,8 +18897,6 @@ }, "node_modules/jest-worker": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -23940,14 +18910,10 @@ }, "node_modules/jimp-compact": { "version": "0.16.1", - "resolved": "https://registry.npmjs.org/jimp-compact/-/jimp-compact-0.16.1.tgz", - "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==", "license": "MIT" }, "node_modules/jiti": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -23955,8 +18921,6 @@ }, "node_modules/jks-js": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jks-js/-/jks-js-1.1.0.tgz", - "integrity": "sha512-irWi8S2V029Vic63w0/TYa8NIZwXu9oeMtHQsX51JDIVBo0lrEaOoyM8ALEEh5PVKD6TrA26FixQK6TzT7dHqA==", "dev": true, "license": "MIT", "dependencies": { @@ -23967,8 +18931,6 @@ }, "node_modules/joi": { "version": "17.11.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", - "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -23981,15 +18943,11 @@ }, "node_modules/join-component": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/join-component/-/join-component-1.1.0.tgz", - "integrity": "sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ==", "dev": true, "license": "MIT" }, "node_modules/jose": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", - "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -23997,8 +18955,6 @@ }, "node_modules/joycon": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", "license": "MIT", "engines": { "node": ">=10" @@ -24006,20 +18962,14 @@ }, "node_modules/js-base64": { "version": "3.7.8", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", - "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", "license": "BSD-3-Clause" }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -24030,20 +18980,14 @@ }, "node_modules/js-yaml/node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, "node_modules/jsc-safe-url": { "version": "0.2.4", - "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", - "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", "license": "0BSD" }, "node_modules/jsdom": { "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -24088,8 +19032,6 @@ }, "node_modules/jsdom/node_modules/tr46": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "dev": true, "license": "MIT", "dependencies": { @@ -24101,8 +19043,6 @@ }, "node_modules/jsdom/node_modules/webidl-conversions": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -24111,9 +19051,6 @@ }, "node_modules/jsdom/node_modules/whatwg-encoding": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "dev": true, "license": "MIT", "dependencies": { @@ -24125,8 +19062,6 @@ }, "node_modules/jsdom/node_modules/whatwg-mimetype": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true, "license": "MIT", "engines": { @@ -24135,8 +19070,6 @@ }, "node_modules/jsdom/node_modules/whatwg-url": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -24149,8 +19082,6 @@ }, "node_modules/jsep": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", - "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "dev": true, "license": "MIT", "engines": { @@ -24159,8 +19090,6 @@ }, "node_modules/jsesc": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -24171,35 +19100,25 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-parse-better-errors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, "license": "MIT" }, "node_modules/json-schema": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/json-schema-to-ts": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", - "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", @@ -24211,20 +19130,14 @@ }, "node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, "node_modules/json-schema-typed": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, "node_modules/json-stable-stringify": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", - "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", "dev": true, "license": "MIT", "dependencies": { @@ -24243,23 +19156,17 @@ }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json-stringify-safe": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true, "license": "ISC", "optional": true }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -24270,8 +19177,6 @@ }, "node_modules/jsonfile": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "license": "MIT", "dependencies": { "universalify": "^2.0.0" @@ -24282,8 +19187,6 @@ }, "node_modules/jsonify": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", - "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", "dev": true, "license": "Public Domain", "funding": { @@ -24292,8 +19195,6 @@ }, "node_modules/jsonwebtoken": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", "dev": true, "license": "MIT", "dependencies": { @@ -24309,8 +19210,6 @@ }, "node_modules/jsx-ast-utils": { "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -24325,8 +19224,6 @@ }, "node_modules/jszip": { "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "dev": true, "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { @@ -24338,8 +19235,6 @@ }, "node_modules/jwa": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "dev": true, "license": "MIT", "dependencies": { @@ -24350,8 +19245,6 @@ }, "node_modules/jws": { "version": "3.2.3", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", - "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "dev": true, "license": "MIT", "dependencies": { @@ -24359,27 +19252,51 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/keychain": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/keychain/-/keychain-1.5.0.tgz", - "integrity": "sha512-liyp4r+93RI7EB2jhwaRd4MWfdgHH6shuldkaPMkELCJjMFvOOVXuTvw1pGqFfhsrgA6OqfykWWPQgBjQakVag==", "dev": true, "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/kind-of": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, "license": "MIT", "engines": { @@ -24388,8 +19305,6 @@ }, "node_modules/klaw-sync": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", - "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", "dev": true, "license": "MIT", "dependencies": { @@ -24398,8 +19313,6 @@ }, "node_modules/kleur": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "license": "MIT", "engines": { "node": ">=6" @@ -24407,8 +19320,6 @@ }, "node_modules/knip": { "version": "5.86.0", - "resolved": "https://registry.npmjs.org/knip/-/knip-5.86.0.tgz", - "integrity": "sha512-tGpRCbP+L+VysXnAp1bHTLQ0k/SdC3M3oX18+Cpiqax1qdS25iuCPzpK8LVmAKARZv0Ijri81Wq09Rzk0JTl+Q==", "dev": true, "funding": [ { @@ -24450,23 +19361,23 @@ }, "node_modules/lan-network": { "version": "0.1.7", - "resolved": "https://registry.npmjs.org/lan-network/-/lan-network-0.1.7.tgz", - "integrity": "sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ==", "license": "MIT", "bin": { "lan-network": "dist/lan-network-cli.js" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/lazy-val": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", "license": "MIT" }, "node_modules/lefthook": { "version": "2.1.6", - "resolved": "https://registry.npmjs.org/lefthook/-/lefthook-2.1.6.tgz", - "integrity": "sha512-w9sBoR0mdN+kJc3SB85VzpiAAl451/rxdCRcZlwW71QLjkeH3EBQFgc4VMj5apePychYDHAlqEWTB8J8JK/j1Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -24488,8 +19399,6 @@ }, "node_modules/lefthook-darwin-arm64": { "version": "2.1.6", - "resolved": "https://registry.npmjs.org/lefthook-darwin-arm64/-/lefthook-darwin-arm64-2.1.6.tgz", - "integrity": "sha512-hyB7eeiX78BS66f70byTJacDLC/xV1vgMv9n+idFUsrM7J3Udd/ag9Ag5NP3t0eN0EqQqAtrNnt35EH01lxnRQ==", "cpu": [ "arm64" ], @@ -24500,136 +19409,8 @@ "darwin" ] }, - "node_modules/lefthook-darwin-x64": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/lefthook-darwin-x64/-/lefthook-darwin-x64-2.1.6.tgz", - "integrity": "sha512-5Ka6cFxiH83krt+OMRQtmS6zqoZR5SLXSudLjTbZA1c3ZqF0+dqkeb4XcB6plx6WR0GFizabuc6Bi3iXPIe1eQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/lefthook-freebsd-arm64": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/lefthook-freebsd-arm64/-/lefthook-freebsd-arm64-2.1.6.tgz", - "integrity": "sha512-VswyOg5CVN3rMaOJ2HtnkltiMKgFHW/wouWxXsV8RxSa4tgWOKxM0EmSXi8qc2jX+LRga6B0uOY6toXS01zWxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/lefthook-freebsd-x64": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/lefthook-freebsd-x64/-/lefthook-freebsd-x64-2.1.6.tgz", - "integrity": "sha512-vXsCUFYuVwrVWwcypB7Zt2Hf+5pl1V1la7ZfvGYZaTRURu0zF/XUnMF/nOz/PebGv0f4x/iOWXWwP7E42xRWsg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/lefthook-linux-arm64": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/lefthook-linux-arm64/-/lefthook-linux-arm64-2.1.6.tgz", - "integrity": "sha512-WDJiQhJdZOvKORZd+kF/ms2l6NSsXzdA9ahflyr65V90AC4jES223W8VtEMbGPUtHuGWMEZ/v/XvwlWv0Ioz9g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/lefthook-linux-x64": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/lefthook-linux-x64/-/lefthook-linux-x64-2.1.6.tgz", - "integrity": "sha512-C18nCd7nTX1AVL4TcvwMmLAO1VI1OuGluIOTjiPkBQ746Ls1HhL5rl//jMPACmT28YmxIQJ2ZcLPNmhvEVBZvw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/lefthook-openbsd-arm64": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/lefthook-openbsd-arm64/-/lefthook-openbsd-arm64-2.1.6.tgz", - "integrity": "sha512-mZOMxM8HiPxVFXDO3PtCUbH4GB8rkveXhsgXF27oAZTYVzQ3gO9vT6r/pxit6msqRXz3fvcwimLVJgb8eRsa8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/lefthook-openbsd-x64": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/lefthook-openbsd-x64/-/lefthook-openbsd-x64-2.1.6.tgz", - "integrity": "sha512-sG9ALLZSnnMOfXu+B7SmxFhJhuoAh4bqi5En5aaHJET48TqrLOcWWZuH+7ArFM6gr/U5KfSUvdmHFmY8WqCcIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/lefthook-windows-arm64": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/lefthook-windows-arm64/-/lefthook-windows-arm64-2.1.6.tgz", - "integrity": "sha512-lD8yFWY4Csuljd0Rqs7EQaySC0VvDf7V3rN1FhRMUISTRDHutebIom1Loc8ckQPvKYGC6mftT9k0GvipsS+Brw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/lefthook-windows-x64": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/lefthook-windows-x64/-/lefthook-windows-x64-2.1.6.tgz", - "integrity": "sha512-q4z2n3xucLscoWiyMwFViEj3N8MDSkPulMwcJYuCYFHoPhP1h+icqNu7QRLGYj6AnVrCQweiUJY3Tb2X+GbD/A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/leven": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "license": "MIT", "engines": { "node": ">=6" @@ -24637,8 +19418,6 @@ }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -24651,8 +19430,6 @@ }, "node_modules/lezer-elixir": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/lezer-elixir/-/lezer-elixir-1.1.3.tgz", - "integrity": "sha512-Ymc58/WhxdZS9yEOlnKbF3rdeBdFcPm4OEm26KMqA1Za9vztXi7I5qwGw1KxYmm3Nv0iDHq//EQyBwSEzKG9Mg==", "license": "Apache-2.0", "dependencies": { "@lezer/highlight": "^1.2.0", @@ -24661,8 +19438,6 @@ }, "node_modules/lie": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -24671,8 +19446,6 @@ }, "node_modules/lighthouse-logger": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", - "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", "license": "Apache-2.0", "dependencies": { "debug": "^2.6.9", @@ -24681,8 +19454,6 @@ }, "node_modules/lighthouse-logger/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -24690,14 +19461,10 @@ }, "node_modules/lighthouse-logger/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/lightningcss": { "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -24722,30 +19489,8 @@ "lightningcss-win32-x64-msvc": "1.30.1" } }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lightningcss-darwin-arm64": { "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", "cpu": [ "arm64" ], @@ -24762,196 +19507,12 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, "node_modules/linkify-it": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", "license": "MIT", "dependencies": { "uc.micro": "^1.0.1" @@ -24959,8 +19520,6 @@ }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -24975,69 +19534,53 @@ }, "node_modules/lodash": { "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", "license": "MIT" }, "node_modules/lodash.get": { "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", "dev": true, "license": "MIT" }, "node_modules/lodash.isequal": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", "license": "MIT" }, "node_modules/lodash.memoize": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, "node_modules/lodash.throttle": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", "license": "MIT" }, "node_modules/log-symbols": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "license": "MIT", "dependencies": { @@ -25053,8 +19596,6 @@ }, "node_modules/longest-streak": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", "license": "MIT", "funding": { "type": "github", @@ -25063,8 +19604,6 @@ }, "node_modules/loose-envify": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -25075,8 +19614,6 @@ }, "node_modules/lowercase-keys": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "dev": true, "license": "MIT", "engines": { @@ -25085,8 +19622,6 @@ }, "node_modules/lru-cache": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -25097,8 +19632,6 @@ }, "node_modules/lucide-react": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz", - "integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -25106,8 +19639,6 @@ }, "node_modules/lucide-react-native": { "version": "0.546.0", - "resolved": "https://registry.npmjs.org/lucide-react-native/-/lucide-react-native-0.546.0.tgz", - "integrity": "sha512-v/qe5JnOHKIhccqfE7Ln78wd0mn5YgmCBvkTX2ca8sWeoym3ZYZcY/mapFsxssarutO4bMI/bRtTqiusGbpOUw==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", @@ -25117,8 +19648,6 @@ }, "node_modules/lz-string": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", "bin": { @@ -25127,8 +19656,6 @@ }, "node_modules/magic-string": { "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -25136,8 +19663,6 @@ }, "node_modules/make-dir": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dev": true, "license": "MIT", "dependencies": { @@ -25150,8 +19675,6 @@ }, "node_modules/make-dir/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -25160,15 +19683,11 @@ }, "node_modules/make-error": { "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true, "license": "ISC" }, "node_modules/make-fetch-happen": { "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", "dev": true, "license": "ISC", "dependencies": { @@ -25190,8 +19709,6 @@ }, "node_modules/make-fetch-happen/node_modules/negotiator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, "license": "MIT", "engines": { @@ -25200,8 +19717,6 @@ }, "node_modules/make-fetch-happen/node_modules/proc-log": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", "dev": true, "license": "ISC", "engines": { @@ -25210,8 +19725,6 @@ }, "node_modules/makeerror": { "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "license": "BSD-3-Clause", "dependencies": { "tmpl": "1.0.5" @@ -25219,8 +19732,6 @@ }, "node_modules/map-obj": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", "dev": true, "license": "MIT", "engines": { @@ -25232,8 +19743,6 @@ }, "node_modules/markdown-it": { "version": "10.0.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", - "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -25248,30 +19757,34 @@ }, "node_modules/markdown-it/node_modules/entities": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", "license": "BSD-2-Clause" }, "node_modules/markdown-table": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/marky": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", - "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", "license": "Apache-2.0" }, "node_modules/matcher": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", "dev": true, "license": "MIT", "optional": true, @@ -25284,8 +19797,6 @@ }, "node_modules/material-icon-theme": { "version": "5.32.0", - "resolved": "https://registry.npmjs.org/material-icon-theme/-/material-icon-theme-5.32.0.tgz", - "integrity": "sha512-SxJxCcnk6cJIbd+AxmoeghXJ24joXGmUzjiGci16sX4mXZdXprGEzM6ZZ0VHGAofxNlMqznEbExINwFLsxf8eQ==", "dev": true, "license": "MIT", "dependencies": { @@ -25303,8 +19814,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -25312,8 +19821,6 @@ }, "node_modules/md5": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -25324,8 +19831,6 @@ }, "node_modules/mdast-util-directive": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", - "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -25345,8 +19850,6 @@ }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -25361,8 +19864,6 @@ }, "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", "engines": { "node": ">=12" @@ -25373,8 +19874,6 @@ }, "node_modules/mdast-util-from-markdown": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", - "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -25397,8 +19896,6 @@ }, "node_modules/mdast-util-gfm": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", - "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^2.0.0", @@ -25416,8 +19913,6 @@ }, "node_modules/mdast-util-gfm-autolink-literal": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", - "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -25433,8 +19928,6 @@ }, "node_modules/mdast-util-gfm-footnote": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -25450,8 +19943,6 @@ }, "node_modules/mdast-util-gfm-strikethrough": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -25465,8 +19956,6 @@ }, "node_modules/mdast-util-gfm-table": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -25482,8 +19971,6 @@ }, "node_modules/mdast-util-gfm-task-list-item": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -25498,8 +19985,6 @@ }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -25516,8 +20001,6 @@ }, "node_modules/mdast-util-mdx-jsx": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -25540,8 +20023,6 @@ }, "node_modules/mdast-util-mdxjs-esm": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -25558,8 +20039,6 @@ }, "node_modules/mdast-util-phrasing": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -25572,8 +20051,6 @@ }, "node_modules/mdast-util-to-hast": { "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -25593,8 +20070,6 @@ }, "node_modules/mdast-util-to-markdown": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -25614,8 +20089,6 @@ }, "node_modules/mdast-util-to-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0" @@ -25627,20 +20100,14 @@ }, "node_modules/mdn-data": { "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", "license": "CC0-1.0" }, "node_modules/mdurl": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", "license": "MIT" }, "node_modules/media-typer": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -25648,14 +20115,10 @@ }, "node_modules/memoize-one": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, "node_modules/meow": { "version": "10.1.5", - "resolved": "https://registry.npmjs.org/meow/-/meow-10.1.5.tgz", - "integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==", "dev": true, "license": "MIT", "dependencies": { @@ -25681,8 +20144,6 @@ }, "node_modules/merge-descriptors": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -25690,8 +20151,6 @@ }, "node_modules/merge-options": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", - "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", "license": "MIT", "dependencies": { "is-plain-obj": "^2.1.0" @@ -25702,8 +20161,6 @@ }, "node_modules/merge-options/node_modules/is-plain-obj": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "license": "MIT", "engines": { "node": ">=8" @@ -25711,24 +20168,60 @@ }, "node_modules/merge-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "11.16.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.1.tgz", + "integrity": "sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.2.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.20", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.45", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/methods": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -25736,8 +20229,6 @@ }, "node_modules/metro": { "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.3.tgz", - "integrity": "sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.24.7", @@ -25790,8 +20281,6 @@ }, "node_modules/metro-babel-transformer": { "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.3.tgz", - "integrity": "sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -25805,14 +20294,10 @@ }, "node_modules/metro-babel-transformer/node_modules/hermes-estree": { "version": "0.32.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", - "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", "license": "MIT" }, "node_modules/metro-babel-transformer/node_modules/hermes-parser": { "version": "0.32.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", - "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", "license": "MIT", "dependencies": { "hermes-estree": "0.32.0" @@ -25820,8 +20305,6 @@ }, "node_modules/metro-cache": { "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.3.tgz", - "integrity": "sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q==", "license": "MIT", "dependencies": { "exponential-backoff": "^3.1.1", @@ -25835,8 +20318,6 @@ }, "node_modules/metro-cache-key": { "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.3.tgz", - "integrity": "sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -25847,8 +20328,6 @@ }, "node_modules/metro-cache/node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", "engines": { "node": ">= 14" @@ -25856,8 +20335,6 @@ }, "node_modules/metro-cache/node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -25869,8 +20346,6 @@ }, "node_modules/metro-config": { "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.3.tgz", - "integrity": "sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA==", "license": "MIT", "dependencies": { "connect": "^3.6.5", @@ -25888,8 +20363,6 @@ }, "node_modules/metro-core": { "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.3.tgz", - "integrity": "sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", @@ -25902,8 +20375,6 @@ }, "node_modules/metro-file-map": { "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.3.tgz", - "integrity": "sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -25922,8 +20393,6 @@ }, "node_modules/metro-minify-terser": { "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.3.tgz", - "integrity": "sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", @@ -25935,8 +20404,6 @@ }, "node_modules/metro-resolver": { "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.3.tgz", - "integrity": "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -25947,8 +20414,6 @@ }, "node_modules/metro-runtime": { "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.3.tgz", - "integrity": "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.0", @@ -25960,8 +20425,6 @@ }, "node_modules/metro-source-map": { "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.3.tgz", - "integrity": "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.3", @@ -25981,8 +20444,6 @@ }, "node_modules/metro-source-map/node_modules/source-map": { "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -25990,8 +20451,6 @@ }, "node_modules/metro-symbolicate": { "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.3.tgz", - "integrity": "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6", @@ -26010,8 +20469,6 @@ }, "node_modules/metro-symbolicate/node_modules/source-map": { "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -26019,8 +20476,6 @@ }, "node_modules/metro-transform-plugins": { "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.3.tgz", - "integrity": "sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -26036,8 +20491,6 @@ }, "node_modules/metro-transform-worker": { "version": "0.83.3", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.3.tgz", - "integrity": "sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -26060,20 +20513,14 @@ }, "node_modules/metro/node_modules/ci-info": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "license": "MIT" }, "node_modules/metro/node_modules/hermes-estree": { "version": "0.32.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", - "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", "license": "MIT" }, "node_modules/metro/node_modules/hermes-parser": { "version": "0.32.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", - "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", "license": "MIT", "dependencies": { "hermes-estree": "0.32.0" @@ -26081,8 +20528,6 @@ }, "node_modules/metro/node_modules/source-map": { "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -26090,8 +20535,6 @@ }, "node_modules/metro/node_modules/ws": { "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "license": "MIT", "engines": { "node": ">=8.3.0" @@ -26111,8 +20554,6 @@ }, "node_modules/micromark": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", "funding": [ { "type": "GitHub Sponsors", @@ -26146,8 +20587,6 @@ }, "node_modules/micromark-core-commonmark": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", "funding": [ { "type": "GitHub Sponsors", @@ -26180,8 +20619,6 @@ }, "node_modules/micromark-extension-directive": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", - "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -26199,8 +20636,6 @@ }, "node_modules/micromark-extension-gfm": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", "license": "MIT", "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", @@ -26219,8 +20654,6 @@ }, "node_modules/micromark-extension-gfm-autolink-literal": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", @@ -26235,8 +20668,6 @@ }, "node_modules/micromark-extension-gfm-footnote": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -26255,8 +20686,6 @@ }, "node_modules/micromark-extension-gfm-strikethrough": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -26273,8 +20702,6 @@ }, "node_modules/micromark-extension-gfm-table": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -26290,8 +20717,6 @@ }, "node_modules/micromark-extension-gfm-tagfilter": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", "license": "MIT", "dependencies": { "micromark-util-types": "^2.0.0" @@ -26303,8 +20728,6 @@ }, "node_modules/micromark-extension-gfm-task-list-item": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", - "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -26320,8 +20743,6 @@ }, "node_modules/micromark-factory-destination": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", "funding": [ { "type": "GitHub Sponsors", @@ -26341,8 +20762,6 @@ }, "node_modules/micromark-factory-label": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", "funding": [ { "type": "GitHub Sponsors", @@ -26363,8 +20782,6 @@ }, "node_modules/micromark-factory-space": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", "funding": [ { "type": "GitHub Sponsors", @@ -26383,8 +20800,6 @@ }, "node_modules/micromark-factory-title": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", "funding": [ { "type": "GitHub Sponsors", @@ -26405,8 +20820,6 @@ }, "node_modules/micromark-factory-whitespace": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", "funding": [ { "type": "GitHub Sponsors", @@ -26427,8 +20840,6 @@ }, "node_modules/micromark-util-character": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", "funding": [ { "type": "GitHub Sponsors", @@ -26447,8 +20858,6 @@ }, "node_modules/micromark-util-chunked": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", "funding": [ { "type": "GitHub Sponsors", @@ -26466,8 +20875,6 @@ }, "node_modules/micromark-util-classify-character": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", "funding": [ { "type": "GitHub Sponsors", @@ -26487,8 +20894,6 @@ }, "node_modules/micromark-util-combine-extensions": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", "funding": [ { "type": "GitHub Sponsors", @@ -26507,8 +20912,6 @@ }, "node_modules/micromark-util-decode-numeric-character-reference": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", "funding": [ { "type": "GitHub Sponsors", @@ -26526,8 +20929,6 @@ }, "node_modules/micromark-util-decode-string": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", "funding": [ { "type": "GitHub Sponsors", @@ -26548,8 +20949,6 @@ }, "node_modules/micromark-util-encode": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", "funding": [ { "type": "GitHub Sponsors", @@ -26564,8 +20963,6 @@ }, "node_modules/micromark-util-html-tag-name": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", "funding": [ { "type": "GitHub Sponsors", @@ -26580,8 +20977,6 @@ }, "node_modules/micromark-util-normalize-identifier": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", "funding": [ { "type": "GitHub Sponsors", @@ -26599,8 +20994,6 @@ }, "node_modules/micromark-util-resolve-all": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", "funding": [ { "type": "GitHub Sponsors", @@ -26618,8 +21011,6 @@ }, "node_modules/micromark-util-sanitize-uri": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", "funding": [ { "type": "GitHub Sponsors", @@ -26639,8 +21030,6 @@ }, "node_modules/micromark-util-subtokenize": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", "funding": [ { "type": "GitHub Sponsors", @@ -26661,8 +21050,6 @@ }, "node_modules/micromark-util-symbol": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", "funding": [ { "type": "GitHub Sponsors", @@ -26677,8 +21064,6 @@ }, "node_modules/micromark-util-types": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "funding": [ { "type": "GitHub Sponsors", @@ -26693,8 +21078,6 @@ }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -26706,8 +21089,6 @@ }, "node_modules/micromatch/node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -26718,8 +21099,6 @@ }, "node_modules/mime": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "dev": true, "license": "MIT", "bin": { @@ -26731,8 +21110,6 @@ }, "node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -26740,8 +21117,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -26752,8 +21127,6 @@ }, "node_modules/mime-types/node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -26761,8 +21134,6 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, "license": "MIT", "engines": { @@ -26771,8 +21142,6 @@ }, "node_modules/mimic-function": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "license": "MIT", "engines": { "node": ">=18" @@ -26783,8 +21152,6 @@ }, "node_modules/mimic-response": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true, "license": "MIT", "engines": { @@ -26793,8 +21160,6 @@ }, "node_modules/min-indent": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, "license": "MIT", "engines": { @@ -26803,8 +21168,6 @@ }, "node_modules/miniflare": { "version": "4.20260507.1", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260507.1.tgz", - "integrity": "sha512-PSXBiLExTdZ4UGO/raKCHQauUpYL7F880ZRB7j0+78Rv8h7TsdN2E/iEDK9sK2Y+SPQ5wJSeAa+rDeVKoZZoEw==", "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", @@ -26823,8 +21186,6 @@ }, "node_modules/miniflare/node_modules/ws": { "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -26844,8 +21205,6 @@ }, "node_modules/minimatch": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", "dev": true, "license": "ISC", "dependencies": { @@ -26857,8 +21216,6 @@ }, "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -26866,8 +21223,6 @@ }, "node_modules/minimist-options": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", "dev": true, "license": "MIT", "dependencies": { @@ -26881,8 +21236,6 @@ }, "node_modules/minipass": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -26890,8 +21243,6 @@ }, "node_modules/minipass-collect": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, "license": "ISC", "dependencies": { @@ -26903,8 +21254,6 @@ }, "node_modules/minipass-fetch": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -26921,8 +21270,6 @@ }, "node_modules/minipass-flush": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", "dev": true, "license": "ISC", "dependencies": { @@ -26934,8 +21281,6 @@ }, "node_modules/minipass-flush/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "license": "ISC", "dependencies": { @@ -26947,8 +21292,6 @@ }, "node_modules/minipass-pipeline": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "dev": true, "license": "ISC", "dependencies": { @@ -26960,8 +21303,6 @@ }, "node_modules/minipass-pipeline/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "license": "ISC", "dependencies": { @@ -26973,8 +21314,6 @@ }, "node_modules/minipass-sized": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", "dev": true, "license": "ISC", "dependencies": { @@ -26986,8 +21325,6 @@ }, "node_modules/minipass-sized/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "license": "ISC", "dependencies": { @@ -26999,8 +21336,6 @@ }, "node_modules/minizlib": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", - "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", "dev": true, "license": "MIT", "dependencies": { @@ -27013,8 +21348,6 @@ }, "node_modules/minizlib/node_modules/rimraf": { "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", "dev": true, "license": "ISC", "dependencies": { @@ -27029,8 +21362,6 @@ }, "node_modules/mkdirp": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -27041,14 +21372,10 @@ }, "node_modules/mnemonic-id": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/mnemonic-id/-/mnemonic-id-3.2.7.tgz", - "integrity": "sha512-kysx9gAGbvrzuFYxKkcRjnsg/NK61ovJOV4F1cHTRl9T5leg+bo6WI0pWIvOFh1Z/yDL0cjA5R3EEGPPLDv/XA==", "license": "MIT" }, "node_modules/moment": { "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "dev": true, "license": "MIT", "optional": true, @@ -27058,8 +21385,6 @@ }, "node_modules/motion-dom": { "version": "12.35.2", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.35.2.tgz", - "integrity": "sha512-pWXFMTwvGDbx1Fe9YL5HZebv2NhvGBzRtiNUv58aoK7+XrsuaydQ0JGRKK2r+bTKlwgSWwWxHbP5249Qr/BNpg==", "license": "MIT", "dependencies": { "motion-utils": "^12.29.2" @@ -27067,14 +21392,10 @@ }, "node_modules/motion-utils": { "version": "12.29.2", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", - "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", "license": "MIT" }, "node_modules/mrmime": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, "license": "MIT", "engines": { @@ -27083,28 +21404,20 @@ }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/multipasta": { "version": "0.2.7", - "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", - "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", "dev": true, "license": "MIT" }, "node_modules/mute-stream": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true, "license": "ISC" }, "node_modules/mv": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", "dev": true, "license": "MIT", "optional": true, @@ -27119,8 +21432,6 @@ }, "node_modules/mv/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "optional": true, @@ -27131,9 +21442,6 @@ }, "node_modules/mv/node_modules/glob": { "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "optional": true, @@ -27150,8 +21458,6 @@ }, "node_modules/mv/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "optional": true, @@ -27164,8 +21470,6 @@ }, "node_modules/mv/node_modules/mkdirp": { "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", "optional": true, @@ -27178,9 +21482,6 @@ }, "node_modules/mv/node_modules/rimraf": { "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", "optional": true, @@ -27193,8 +21494,6 @@ }, "node_modules/mz": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -27204,16 +21503,12 @@ }, "node_modules/nan": { "version": "2.25.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", - "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", "dev": true, "license": "MIT", "optional": true }, "node_modules/nanoid": { "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -27230,8 +21525,6 @@ }, "node_modules/napi-postinstall": { "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", "dev": true, "license": "MIT", "bin": { @@ -27246,15 +21539,11 @@ }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/natural-orderby": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-2.0.3.tgz", - "integrity": "sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q==", "dev": true, "license": "MIT", "engines": { @@ -27263,8 +21552,6 @@ }, "node_modules/ncp": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", "dev": true, "license": "MIT", "optional": true, @@ -27274,8 +21561,6 @@ }, "node_modules/negotiator": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -27283,14 +21568,10 @@ }, "node_modules/nested-error-stacks": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz", - "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==", "license": "MIT" }, "node_modules/node-abi": { "version": "4.28.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", - "integrity": "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g==", "dev": true, "license": "MIT", "dependencies": { @@ -27302,14 +21583,10 @@ }, "node_modules/node-addon-api": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, "node_modules/node-api-version": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", - "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -27318,8 +21595,6 @@ }, "node_modules/node-exports-info": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", - "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", "dev": true, "license": "MIT", "dependencies": { @@ -27337,8 +21612,6 @@ }, "node_modules/node-exports-info/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -27347,8 +21620,6 @@ }, "node_modules/node-fetch": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" @@ -27367,8 +21638,6 @@ }, "node_modules/node-forge": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", "dev": true, "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { @@ -27377,8 +21646,6 @@ }, "node_modules/node-gyp": { "version": "11.5.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", - "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", "dev": true, "license": "MIT", "dependencies": { @@ -27402,8 +21669,6 @@ }, "node_modules/node-gyp-build": { "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "dev": true, "license": "MIT", "bin": { @@ -27414,8 +21679,6 @@ }, "node_modules/node-gyp/node_modules/chownr": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -27424,8 +21687,6 @@ }, "node_modules/node-gyp/node_modules/isexe": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -27434,8 +21695,6 @@ }, "node_modules/node-gyp/node_modules/minizlib": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -27447,8 +21706,6 @@ }, "node_modules/node-gyp/node_modules/proc-log": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", "dev": true, "license": "ISC", "engines": { @@ -27457,8 +21714,6 @@ }, "node_modules/node-gyp/node_modules/tar": { "version": "7.5.12", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.12.tgz", - "integrity": "sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -27474,8 +21729,6 @@ }, "node_modules/node-gyp/node_modules/which": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, "license": "ISC", "dependencies": { @@ -27490,8 +21743,6 @@ }, "node_modules/node-gyp/node_modules/yallist": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -27500,14 +21751,10 @@ }, "node_modules/node-int64": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "license": "MIT" }, "node_modules/node-pty": { "version": "1.2.0-beta.15", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.15.tgz", - "integrity": "sha512-vORSzHXi4Ofl7HemVWpuudLqCPdaQb4LfpRCUpE5HPxhp4JYscl8zZwxh11p26v2wvW24WMwnMfLjhRLixrfxA==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -27516,14 +21763,10 @@ }, "node_modules/node-releases": { "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "license": "MIT" }, "node_modules/node-rsa": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", - "integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==", "dev": true, "license": "MIT", "dependencies": { @@ -27532,8 +21775,6 @@ }, "node_modules/node-stream-zip": { "version": "1.15.0", - "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", - "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", "dev": true, "license": "MIT", "engines": { @@ -27546,8 +21787,6 @@ }, "node_modules/nopt": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", "dev": true, "license": "ISC", "dependencies": { @@ -27562,8 +21801,6 @@ }, "node_modules/normalize-package-data": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -27578,8 +21815,6 @@ }, "node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -27587,8 +21822,6 @@ }, "node_modules/normalize-url": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true, "license": "MIT", "engines": { @@ -27600,8 +21833,6 @@ }, "node_modules/npm-package-arg": { "version": "11.0.3", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", - "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", "license": "ISC", "dependencies": { "hosted-git-info": "^7.0.0", @@ -27615,8 +21846,6 @@ }, "node_modules/npm-package-arg/node_modules/hosted-git-info": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", "license": "ISC", "dependencies": { "lru-cache": "^10.0.1" @@ -27627,14 +21856,10 @@ }, "node_modules/npm-package-arg/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, "node_modules/nth-check": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" @@ -27645,21 +21870,15 @@ }, "node_modules/nullthrows": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", - "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", "license": "MIT" }, "node_modules/nwsapi": { "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", "dev": true, "license": "MIT" }, "node_modules/ob1": { "version": "0.83.3", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.3.tgz", - "integrity": "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA==", "license": "MIT", "dependencies": { "flow-enums-runtime": "^0.0.6" @@ -27670,8 +21889,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -27679,8 +21896,6 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -27691,8 +21906,6 @@ }, "node_modules/object-is": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -27707,8 +21920,6 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -27716,8 +21927,6 @@ }, "node_modules/object-treeify": { "version": "1.1.33", - "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", - "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", "dev": true, "license": "MIT", "engines": { @@ -27726,8 +21935,6 @@ }, "node_modules/object.assign": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -27746,8 +21953,6 @@ }, "node_modules/object.entries": { "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", "dev": true, "license": "MIT", "dependencies": { @@ -27762,8 +21967,6 @@ }, "node_modules/object.fromentries": { "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "license": "MIT", "dependencies": { @@ -27781,8 +21984,6 @@ }, "node_modules/object.groupby": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, "license": "MIT", "dependencies": { @@ -27796,8 +21997,6 @@ }, "node_modules/object.values": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "license": "MIT", "dependencies": { @@ -27815,8 +22014,6 @@ }, "node_modules/obug": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -27826,8 +22023,6 @@ }, "node_modules/on-exit-leak-free": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -27835,8 +22030,6 @@ }, "node_modules/on-finished": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -27847,8 +22040,6 @@ }, "node_modules/on-headers": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -27856,8 +22047,6 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", "dependencies": { "wrappy": "1" @@ -27865,8 +22054,6 @@ }, "node_modules/onetime": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "license": "MIT", "dependencies": { @@ -27881,14 +22068,10 @@ }, "node_modules/oniguruma-parser": { "version": "0.12.2", - "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", - "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", "license": "MIT" }, "node_modules/oniguruma-to-es": { "version": "4.3.6", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", - "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", "license": "MIT", "dependencies": { "oniguruma-parser": "^0.12.2", @@ -27898,8 +22081,6 @@ }, "node_modules/open": { "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", "license": "MIT", "dependencies": { "is-docker": "^2.0.0", @@ -27914,8 +22095,6 @@ }, "node_modules/opentype.js": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-0.8.0.tgz", - "integrity": "sha512-FQHR4oGP+a0m/f6yHoRpBOIbn/5ZWxKd4D/djHVJu8+KpBTYrJda0b7mLcgDEMWXE9xBCJm+qb0yv6FcvPjukg==", "license": "MIT", "dependencies": { "tiny-inflate": "^1.0.2" @@ -27926,8 +22105,6 @@ }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -27944,8 +22121,6 @@ }, "node_modules/ora": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.1.0.tgz", - "integrity": "sha512-9tXIMPvjZ7hPTbk8DFq1f7Kow/HU/pQYB60JbNq+QnGwcyhWVZaQ4hM9zQDEsPxw/muLpgiHSaumUZxCAmod/w==", "dev": true, "license": "MIT", "dependencies": { @@ -27967,8 +22142,6 @@ }, "node_modules/own-keys": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", "dev": true, "license": "MIT", "dependencies": { @@ -27985,8 +22158,6 @@ }, "node_modules/oxc-resolver": { "version": "11.19.1", - "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.19.1.tgz", - "integrity": "sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==", "dev": true, "license": "MIT", "funding": { @@ -28017,8 +22188,6 @@ }, "node_modules/oxfmt": { "version": "0.46.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.46.0.tgz", - "integrity": "sha512-CopwJOwPAjZ9p76fCvz+mSOJTw9/NY3cSksZK3VO/bUQ8UoEcketNgUuYS0UB3p+R9XnXe7wGGXUmyFxc7QxJA==", "dev": true, "license": "MIT", "dependencies": { @@ -28057,8 +22226,6 @@ }, "node_modules/oxfmt/node_modules/tinypool": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", - "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", "dev": true, "license": "MIT", "engines": { @@ -28067,8 +22234,6 @@ }, "node_modules/oxlint": { "version": "1.61.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.61.0.tgz", - "integrity": "sha512-ZC0ALuhDZ6ivOFG+sy0D0pEDN49EvsId98zVlmYdkcXHsEM14m/qTNUEsUpiFiCVbpIxYtVBmmLE87nsbUHohQ==", "dev": true, "license": "MIT", "bin": { @@ -28112,8 +22277,6 @@ }, "node_modules/oxlint-tsgolint": { "version": "0.22.1", - "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-0.22.1.tgz", - "integrity": "sha512-YUSGSLUnoolsu8gxISEDio3q1rtsCozwfOzASUn3DT2mR2EeQ93uEEnen7s+6LpF+lyTQFln1pQfqwBh/fsVEg==", "dev": true, "license": "MIT", "bin": { @@ -28130,8 +22293,6 @@ }, "node_modules/p-cancelable": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "dev": true, "license": "MIT", "engines": { @@ -28140,8 +22301,6 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -28155,8 +22314,6 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -28171,8 +22328,6 @@ }, "node_modules/p-map": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "dev": true, "license": "MIT", "engines": { @@ -28184,8 +22339,6 @@ }, "node_modules/p-memoize": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/p-memoize/-/p-memoize-8.0.0.tgz", - "integrity": "sha512-jdZ10MCxavHoIHwJ5oweOtYy6ElPixEHaMkz0AuaEMovR1MRpVvYFzIEHRxgMEpXYzNpRVByFAniAzwmd1/uug==", "license": "MIT", "dependencies": { "mimic-function": "^5.0.1", @@ -28200,8 +22353,6 @@ }, "node_modules/p-memoize/node_modules/type-fest": { "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -28212,8 +22363,6 @@ }, "node_modules/p-throttle": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/p-throttle/-/p-throttle-8.1.0.tgz", - "integrity": "sha512-c1wmXavsHZIC4g1OLhOsafK6jZSAeMo0Ap3yivj59PUcCkpacy5YgWdgIp/dB4vp1JZrfBSsPCR0YuADB+ENLQ==", "license": "MIT", "engines": { "node": ">=20" @@ -28224,8 +22373,6 @@ }, "node_modules/p-try": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "license": "MIT", "engines": { "node": ">=6" @@ -28233,22 +22380,22 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "license": "MIT" + }, "node_modules/pako": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "dev": true, "license": "(MIT AND Zlib)" }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -28260,8 +22407,6 @@ }, "node_modules/parse-entities": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", @@ -28279,14 +22424,10 @@ }, "node_modules/parse-entities/node_modules/@types/unist": { "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, "node_modules/parse-json": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "license": "MIT", "dependencies": { @@ -28304,14 +22445,10 @@ }, "node_modules/parse-numeric-range": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", - "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", "license": "ISC" }, "node_modules/parse-png": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/parse-png/-/parse-png-2.1.0.tgz", - "integrity": "sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==", "license": "MIT", "dependencies": { "pngjs": "^3.3.0" @@ -28322,8 +22459,6 @@ }, "node_modules/parse-png/node_modules/pngjs": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", - "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", "license": "MIT", "engines": { "node": ">=4.0.0" @@ -28331,8 +22466,6 @@ }, "node_modules/parse5": { "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -28343,8 +22476,6 @@ }, "node_modules/parse5/node_modules/entities": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -28355,8 +22486,6 @@ }, "node_modules/parseurl": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -28364,8 +22493,6 @@ }, "node_modules/password-prompt": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/password-prompt/-/password-prompt-1.1.3.tgz", - "integrity": "sha512-HkrjG2aJlvF0t2BMH0e2LB/EHf3Lcq3fNMzy4GYHcQblAvOl+QQji1Lx7WRBMqpVK8p+KR7bCg7oqAMXtdgqyw==", "dev": true, "license": "0BSD", "dependencies": { @@ -28375,8 +22502,6 @@ }, "node_modules/patch-package": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", - "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", "dev": true, "license": "MIT", "dependencies": { @@ -28403,10 +22528,14 @@ "npm": ">5" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "license": "MIT", "engines": { "node": ">=8" @@ -28414,8 +22543,6 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -28423,8 +22550,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { "node": ">=8" @@ -28432,14 +22557,10 @@ }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -28455,21 +22576,15 @@ }, "node_modules/path-scurry/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, "node_modules/path-to-regexp": { "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, "license": "MIT", "engines": { @@ -28478,14 +22593,10 @@ }, "node_modules/pathe": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, "node_modules/pe-library": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", - "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", "dev": true, "license": "MIT", "engines": { @@ -28499,21 +22610,15 @@ }, "node_modules/pend": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { "node": ">=12" @@ -28524,8 +22629,6 @@ }, "node_modules/pify": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, "license": "MIT", "engines": { @@ -28534,8 +22637,6 @@ }, "node_modules/pino": { "version": "10.3.1", - "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", - "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", "license": "MIT", "dependencies": { "@pinojs/redact": "^0.4.0", @@ -28556,8 +22657,6 @@ }, "node_modules/pino-abstract-transport": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", - "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", "license": "MIT", "dependencies": { "split2": "^4.0.0" @@ -28565,8 +22664,6 @@ }, "node_modules/pino-pretty": { "version": "13.1.3", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", - "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", "license": "MIT", "dependencies": { "colorette": "^2.0.7", @@ -28589,14 +22686,10 @@ }, "node_modules/pino-std-serializers": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", - "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "license": "MIT" }, "node_modules/pirates": { "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "license": "MIT", "engines": { "node": ">= 6" @@ -28604,8 +22697,6 @@ }, "node_modules/pkce-challenge": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "license": "MIT", "engines": { "node": ">=16.20.0" @@ -28613,8 +22704,6 @@ }, "node_modules/pkg-dir": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "license": "MIT", "dependencies": { @@ -28626,8 +22715,6 @@ }, "node_modules/pkg-dir/node_modules/find-up": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", "dependencies": { @@ -28640,8 +22727,6 @@ }, "node_modules/pkg-dir/node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", "dependencies": { @@ -28653,8 +22738,6 @@ }, "node_modules/pkg-dir/node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", "dependencies": { @@ -28669,8 +22752,6 @@ }, "node_modules/pkg-dir/node_modules/p-locate": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "license": "MIT", "dependencies": { @@ -28682,8 +22763,6 @@ }, "node_modules/playwright": { "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -28701,8 +22780,6 @@ }, "node_modules/playwright-core": { "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -28714,10 +22791,7 @@ }, "node_modules/playwright/node_modules/fsevents": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -28729,8 +22803,6 @@ }, "node_modules/plist": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.8.8", @@ -28743,8 +22815,6 @@ }, "node_modules/plist/node_modules/@xmldom/xmldom": { "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -28752,8 +22822,6 @@ }, "node_modules/plist/node_modules/xmlbuilder": { "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "license": "MIT", "engines": { "node": ">=8.0" @@ -28761,18 +22829,30 @@ }, "node_modules/pngjs": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", - "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", "dev": true, "license": "MIT", "engines": { "node": ">=14.19.0" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -28780,8 +22860,6 @@ }, "node_modules/postcss": { "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -28808,14 +22886,10 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -28824,8 +22898,6 @@ }, "node_modules/prettier": { "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -28839,8 +22911,6 @@ }, "node_modules/prettier-linter-helpers": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", - "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", "dev": true, "license": "MIT", "dependencies": { @@ -28852,8 +22922,6 @@ }, "node_modules/pretty-bytes": { "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "license": "MIT", "engines": { "node": ">=6" @@ -28864,8 +22932,6 @@ }, "node_modules/pretty-format": { "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -28878,8 +22944,6 @@ }, "node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "license": "MIT", "engines": { "node": ">=10" @@ -28890,14 +22954,10 @@ }, "node_modules/pretty-format/node_modules/react-is": { "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, "node_modules/proc-log": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", - "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -28905,15 +22965,11 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true, "license": "MIT" }, "node_modules/process-warning": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", "funding": [ { "type": "github", @@ -28928,8 +22984,6 @@ }, "node_modules/progress": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "license": "MIT", "engines": { "node": ">=0.4.0" @@ -28937,8 +22991,6 @@ }, "node_modules/promise": { "version": "8.3.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", - "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", "license": "MIT", "dependencies": { "asap": "~2.0.6" @@ -28946,15 +22998,11 @@ }, "node_modules/promise-limit": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz", - "integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==", "dev": true, "license": "ISC" }, "node_modules/promise-retry": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "dev": true, "license": "MIT", "dependencies": { @@ -28967,8 +23015,6 @@ }, "node_modules/prompts": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "license": "MIT", "dependencies": { "kleur": "^3.0.3", @@ -28980,8 +23026,6 @@ }, "node_modules/prop-types": { "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -28991,14 +23035,10 @@ }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, "node_modules/proper-lockfile": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", "dev": true, "license": "MIT", "dependencies": { @@ -29009,15 +23049,11 @@ }, "node_modules/proper-lockfile/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, "license": "ISC" }, "node_modules/property-information": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "license": "MIT", "funding": { "type": "github", @@ -29026,8 +23062,6 @@ }, "node_modules/proxy-addr": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -29039,15 +23073,11 @@ }, "node_modules/proxy-from-env": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true, "license": "MIT" }, "node_modules/psl": { "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", "dev": true, "license": "MIT", "dependencies": { @@ -29059,8 +23089,6 @@ }, "node_modules/pump": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -29069,8 +23097,6 @@ }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "license": "MIT", "engines": { "node": ">=6" @@ -29078,8 +23104,6 @@ }, "node_modules/qrcode": { "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", "license": "MIT", "dependencies": { "dijkstrajs": "^1.0.1", @@ -29095,8 +23119,6 @@ }, "node_modules/qrcode-terminal": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", - "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", "dev": true, "bin": { "qrcode-terminal": "bin/qrcode-terminal.js" @@ -29104,8 +23126,6 @@ }, "node_modules/qrcode/node_modules/camelcase": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "license": "MIT", "engines": { "node": ">=6" @@ -29113,8 +23133,6 @@ }, "node_modules/qrcode/node_modules/cliui": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -29124,8 +23142,6 @@ }, "node_modules/qrcode/node_modules/decamelize": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -29133,8 +23149,6 @@ }, "node_modules/qrcode/node_modules/find-up": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -29146,8 +23160,6 @@ }, "node_modules/qrcode/node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -29158,8 +23170,6 @@ }, "node_modules/qrcode/node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -29173,8 +23183,6 @@ }, "node_modules/qrcode/node_modules/p-locate": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -29185,8 +23193,6 @@ }, "node_modules/qrcode/node_modules/pngjs": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", "license": "MIT", "engines": { "node": ">=10.13.0" @@ -29194,8 +23200,6 @@ }, "node_modules/qrcode/node_modules/wrap-ansi": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -29208,14 +23212,10 @@ }, "node_modules/qrcode/node_modules/y18n": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "license": "ISC" }, "node_modules/qrcode/node_modules/yargs": { "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "license": "MIT", "dependencies": { "cliui": "^6.0.0", @@ -29236,8 +23236,6 @@ }, "node_modules/qrcode/node_modules/yargs-parser": { "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "license": "ISC", "dependencies": { "camelcase": "^5.0.0", @@ -29249,8 +23247,6 @@ }, "node_modules/qs": { "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -29264,8 +23260,6 @@ }, "node_modules/query-string": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", "license": "MIT", "dependencies": { "decode-uri-component": "^0.2.2", @@ -29282,15 +23276,11 @@ }, "node_modules/querystringify": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true, "license": "MIT" }, "node_modules/queue": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", "license": "MIT", "dependencies": { "inherits": "~2.0.3" @@ -29298,8 +23288,6 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -29319,14 +23307,10 @@ }, "node_modules/quick-format-unescaped": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, "node_modules/quick-lru": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true, "license": "MIT", "engines": { @@ -29338,8 +23322,6 @@ }, "node_modules/range-parser": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -29347,8 +23329,6 @@ }, "node_modules/raw-body": { "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -29362,8 +23342,6 @@ }, "node_modules/raw-body/node_modules/iconv-lite": { "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -29374,8 +23352,6 @@ }, "node_modules/rc": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -29389,8 +23365,6 @@ }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -29398,8 +23372,6 @@ }, "node_modules/react": { "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -29407,8 +23379,6 @@ }, "node_modules/react-devtools-core": { "version": "6.1.5", - "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz", - "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", "license": "MIT", "dependencies": { "shell-quote": "^1.6.1", @@ -29417,8 +23387,6 @@ }, "node_modules/react-devtools-core/node_modules/ws": { "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "license": "MIT", "engines": { "node": ">=8.3.0" @@ -29438,8 +23406,6 @@ }, "node_modules/react-dom": { "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", "dependencies": { "scheduler": "^0.26.0" @@ -29450,14 +23416,10 @@ }, "node_modules/react-fast-compare": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", "license": "MIT" }, "node_modules/react-freeze": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", - "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==", "license": "MIT", "engines": { "node": ">=10" @@ -29468,8 +23430,6 @@ }, "node_modules/react-i18next": { "version": "17.0.8", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", - "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", @@ -29495,30 +23455,22 @@ }, "node_modules/react-is": { "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", - "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", "license": "MIT" }, "node_modules/react-is-18": { "name": "react-is", "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, "license": "MIT" }, "node_modules/react-is-19": { "name": "react-is", "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", - "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", "dev": true, "license": "MIT" }, "node_modules/react-markdown": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", - "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -29544,8 +23496,6 @@ }, "node_modules/react-native": { "version": "0.81.5", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", - "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", "license": "MIT", "dependencies": { "@jest/create-cache-key-function": "^29.7.0", @@ -29601,8 +23551,6 @@ }, "node_modules/react-native-draggable-flatlist": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/react-native-draggable-flatlist/-/react-native-draggable-flatlist-4.0.3.tgz", - "integrity": "sha512-2F4x5BFieWdGq9SetD2nSAR7s7oQCSgNllYgERRXXtNfSOuAGAVbDb/3H3lP0y5f7rEyNwabKorZAD/SyyNbDw==", "license": "MIT", "dependencies": { "@babel/preset-typescript": "^7.17.12" @@ -29615,8 +23563,6 @@ }, "node_modules/react-native-edge-to-edge": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.8.1.tgz", - "integrity": "sha512-bhvsKqeX9PGkY9wBUk9vni/tJNJdKtLPbs/j3e/3CdV4JmUWfTXYYoL+4Hc8Wmej+5eJxkc8KOFa454ruFWBCA==", "license": "MIT", "peerDependencies": { "react": "*", @@ -29625,8 +23571,6 @@ }, "node_modules/react-native-fit-image": { "version": "1.5.5", - "resolved": "https://registry.npmjs.org/react-native-fit-image/-/react-native-fit-image-1.5.5.tgz", - "integrity": "sha512-Wl3Vq2DQzxgsWKuW4USfck9zS7YzhvLNPpkwUUCF90bL32e1a0zOVQ3WsJILJOwzmPdHfzZmWasiiAUNBkhNkg==", "license": "Beerware", "dependencies": { "prop-types": "^15.5.10" @@ -29634,8 +23578,6 @@ }, "node_modules/react-native-gesture-handler": { "version": "2.28.0", - "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz", - "integrity": "sha512-0msfJ1vRxXKVgTgvL+1ZOoYw3/0z1R+Ked0+udoJhyplC2jbVKIJ8Z1bzWdpQRCV3QcQ87Op0zJVE5DhKK2A0A==", "license": "MIT", "dependencies": { "@egjs/hammerjs": "^2.0.17", @@ -29649,8 +23591,6 @@ }, "node_modules/react-native-is-edge-to-edge": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz", - "integrity": "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==", "license": "MIT", "peerDependencies": { "react": "*", @@ -29659,8 +23599,6 @@ }, "node_modules/react-native-markdown-display": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/react-native-markdown-display/-/react-native-markdown-display-7.0.2.tgz", - "integrity": "sha512-Mn4wotMvMfLAwbX/huMLt202W5DsdpMO/kblk+6eUs55S57VVNni1gzZCh5qpznYLjIQELNh50VIozEfY6fvaQ==", "license": "MIT", "dependencies": { "css-to-react-native": "^3.0.0", @@ -29675,8 +23613,6 @@ }, "node_modules/react-native-nitro-modules": { "version": "0.35.5", - "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.35.5.tgz", - "integrity": "sha512-aa03UzC5dLg5qFfyBkVK+JGSwHTjmK7jUZzyRz11r1Yk9C/nJTFe59EeHPxxNNTagkiwQTM6p3sySgD/TDRC7Q==", "license": "MIT", "peerDependencies": { "react": "*", @@ -29685,8 +23621,6 @@ }, "node_modules/react-native-reanimated": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.3.1.tgz", - "integrity": "sha512-KhGsS0YkCA+gusgyzlf9hnqzVPIR398KTpqXyqq/+yYJJPAvyEEPKcxlB0xtOOXSMrR2A9uRKVARVQhZwrOh+Q==", "license": "MIT", "dependencies": { "react-native-is-edge-to-edge": "^1.3.1", @@ -29700,8 +23634,6 @@ }, "node_modules/react-native-reanimated/node_modules/semver": { "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -29712,8 +23644,6 @@ }, "node_modules/react-native-safe-area-context": { "version": "5.6.2", - "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", - "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", "license": "MIT", "peerDependencies": { "react": "*", @@ -29722,8 +23652,6 @@ }, "node_modules/react-native-screens": { "version": "4.16.0", - "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", - "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", "license": "MIT", "dependencies": { "react-freeze": "^1.0.0", @@ -29737,8 +23665,6 @@ }, "node_modules/react-native-svg": { "version": "15.15.3", - "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.3.tgz", - "integrity": "sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA==", "license": "MIT", "dependencies": { "css-select": "^5.1.0", @@ -29752,8 +23678,7 @@ }, "node_modules/react-native-uitextview": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/react-native-uitextview/-/react-native-uitextview-2.2.0.tgz", - "integrity": "sha512-Vbv3cTAuyfkYrfsR2YKFsOd9OYgfys+IX5yvvYY7Wd6TrOd6FSGrX93xtQHjeLXV1ds6fDJcFJsuLi3S4Ypr8A==", + "license": "MIT", "workspaces": [ "example" ], @@ -29764,8 +23689,6 @@ }, "node_modules/react-native-unistyles": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/react-native-unistyles/-/react-native-unistyles-3.2.4.tgz", - "integrity": "sha512-S0aXJKZ6zYUViKMol+3on707AWD71fnAM7kmcHjk3FALy5QJicCCzxuluR0IptFQYNUP2dRqnnhgoS1X/U6Jpw==", "license": "MIT", "dependencies": { "@babel/types": "7.29.0" @@ -29792,8 +23715,6 @@ }, "node_modules/react-native-web": { "version": "0.21.2", - "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz", - "integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.6", @@ -29812,20 +23733,14 @@ }, "node_modules/react-native-web/node_modules/@react-native/normalize-colors": { "version": "0.74.89", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz", - "integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==", "license": "MIT" }, "node_modules/react-native-web/node_modules/memoize-one": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", "license": "MIT" }, "node_modules/react-native-webview": { "version": "13.16.1", - "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.1.tgz", - "integrity": "sha512-If0eHhoEdOYDcHsX+xBFwHMbWBGK1BvGDQDQdVkwtSIXiq1uiqjkpWVP2uQ1as94J0CzvFE9PUNDuhiX0Z6ubw==", "license": "MIT", "dependencies": { "escape-string-regexp": "^4.0.0", @@ -29838,8 +23753,6 @@ }, "node_modules/react-native-worklets": { "version": "0.8.3", - "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.8.3.tgz", - "integrity": "sha512-oCBJROyLU7yG/1R8s0INMflygTH71bx+5XcYkH0CM938TlhSoVbiunE1WVW5FZa51vwYqfLie/IXMX2s1Kh3eg==", "license": "MIT", "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.27.1", @@ -29863,8 +23776,6 @@ }, "node_modules/react-native-worklets/node_modules/semver": { "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -29875,14 +23786,10 @@ }, "node_modules/react-native/node_modules/@react-native/normalize-colors": { "version": "0.81.5", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz", - "integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==", "license": "MIT" }, "node_modules/react-native/node_modules/brace-expansion": { "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -29891,8 +23798,6 @@ }, "node_modules/react-native/node_modules/commander": { "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "license": "MIT", "engines": { "node": ">=18" @@ -29900,9 +23805,6 @@ }, "node_modules/react-native/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -29921,8 +23823,6 @@ }, "node_modules/react-native/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -29933,8 +23833,6 @@ }, "node_modules/react-native/node_modules/react-refresh": { "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -29942,8 +23840,6 @@ }, "node_modules/react-native/node_modules/ws": { "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", - "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", "license": "MIT", "dependencies": { "async-limiter": "~1.0.0" @@ -29951,8 +23847,6 @@ }, "node_modules/react-refresh": { "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -29960,8 +23854,6 @@ }, "node_modules/react-remove-scroll": { "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", @@ -29985,8 +23877,6 @@ }, "node_modules/react-remove-scroll-bar": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", "license": "MIT", "dependencies": { "react-style-singleton": "^2.2.2", @@ -30007,8 +23897,6 @@ }, "node_modules/react-style-singleton": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", @@ -30029,8 +23917,6 @@ }, "node_modules/read-binary-file-arch": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", - "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", "dev": true, "license": "MIT", "dependencies": { @@ -30042,8 +23928,6 @@ }, "node_modules/read-pkg": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz", - "integrity": "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==", "dev": true, "license": "MIT", "dependencies": { @@ -30061,8 +23945,6 @@ }, "node_modules/read-pkg-up": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-8.0.0.tgz", - "integrity": "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -30079,8 +23961,6 @@ }, "node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", "dependencies": { @@ -30095,22 +23975,16 @@ }, "node_modules/readable-stream/node_modules/isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true, "license": "MIT" }, "node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, "license": "MIT" }, "node_modules/readdirp": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "license": "MIT", "optional": true, @@ -30123,8 +23997,6 @@ }, "node_modules/readdirp/node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "optional": true, @@ -30137,8 +24009,6 @@ }, "node_modules/real-require": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", "license": "MIT", "engines": { "node": ">= 12.13.0" @@ -30146,8 +24016,6 @@ }, "node_modules/redent": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", - "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", "dev": true, "license": "MIT", "dependencies": { @@ -30163,8 +24031,6 @@ }, "node_modules/redeyed": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", - "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -30173,8 +24039,6 @@ }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, "license": "MIT", "dependencies": { @@ -30196,14 +24060,10 @@ }, "node_modules/regenerate": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "license": "MIT" }, "node_modules/regenerate-unicode-properties": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", - "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -30214,14 +24074,10 @@ }, "node_modules/regenerator-runtime": { "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "license": "MIT" }, "node_modules/regex": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", - "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", "license": "MIT", "dependencies": { "regex-utilities": "^2.3.0" @@ -30229,8 +24085,6 @@ }, "node_modules/regex-recursion": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", - "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", "license": "MIT", "dependencies": { "regex-utilities": "^2.3.0" @@ -30238,14 +24092,10 @@ }, "node_modules/regex-utilities": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", - "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "license": "MIT" }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "license": "MIT", "dependencies": { @@ -30265,8 +24115,6 @@ }, "node_modules/regexpp": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true, "license": "MIT", "engines": { @@ -30278,8 +24126,6 @@ }, "node_modules/regexpu-core": { "version": "6.4.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", - "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2", @@ -30295,14 +24141,10 @@ }, "node_modules/regjsgen": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", "license": "MIT" }, "node_modules/regjsparser": { "version": "0.13.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", - "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.1.0" @@ -30313,8 +24155,6 @@ }, "node_modules/rehype-autolink-headings": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz", - "integrity": "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -30331,8 +24171,6 @@ }, "node_modules/rehype-parse": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", - "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -30346,8 +24184,6 @@ }, "node_modules/rehype-pretty-code": { "version": "0.14.3", - "resolved": "https://registry.npmjs.org/rehype-pretty-code/-/rehype-pretty-code-0.14.3.tgz", - "integrity": "sha512-Cz692FeYusTjT5cfFWLc4r7JhgC3/JlJptgUh4iffBxWxUnWW1oqbWFi7jGCeq00DYUm8yzoTnvpocaYa5x82g==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.4", @@ -30366,8 +24202,6 @@ }, "node_modules/rehype-slug": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", - "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -30383,8 +24217,6 @@ }, "node_modules/remark-directive": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-4.0.0.tgz", - "integrity": "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -30399,8 +24231,6 @@ }, "node_modules/remark-gfm": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", - "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -30417,8 +24247,6 @@ }, "node_modules/remark-parse": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -30433,8 +24261,6 @@ }, "node_modules/remark-rehype": { "version": "11.1.2", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", - "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -30450,8 +24276,6 @@ }, "node_modules/remark-stringify": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -30465,15 +24289,11 @@ }, "node_modules/remove-trailing-slash": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz", - "integrity": "sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA==", "dev": true, "license": "MIT" }, "node_modules/rename-keys": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/rename-keys/-/rename-keys-1.2.0.tgz", - "integrity": "sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==", "dev": true, "license": "MIT", "engines": { @@ -30482,8 +24302,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -30491,8 +24309,6 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -30500,14 +24316,10 @@ }, "node_modules/require-main-filename": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "license": "ISC" }, "node_modules/requireg": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/requireg/-/requireg-0.2.2.tgz", - "integrity": "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==", "dependencies": { "nested-error-stacks": "~2.0.1", "rc": "~1.2.7", @@ -30519,8 +24331,6 @@ }, "node_modules/requireg/node_modules/resolve": { "version": "1.7.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", - "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", "license": "MIT", "dependencies": { "path-parse": "^1.0.5" @@ -30528,15 +24338,11 @@ }, "node_modules/requires-port": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true, "license": "MIT" }, "node_modules/resedit": { "version": "1.7.2", - "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", - "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", "dev": true, "license": "MIT", "dependencies": { @@ -30553,8 +24359,6 @@ }, "node_modules/resolve": { "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -30574,15 +24378,11 @@ }, "node_modules/resolve-alpn": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true, "license": "MIT" }, "node_modules/resolve-from": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "license": "MIT", "engines": { "node": ">=8" @@ -30590,8 +24390,6 @@ }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "devOptional": true, "license": "MIT", "funding": { @@ -30600,14 +24398,10 @@ }, "node_modules/resolve-workspace-root": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/resolve-workspace-root/-/resolve-workspace-root-2.0.1.tgz", - "integrity": "sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w==", "license": "MIT" }, "node_modules/resolve.exports": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "license": "MIT", "engines": { "node": ">=10" @@ -30615,8 +24409,6 @@ }, "node_modules/responselike": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "dev": true, "license": "MIT", "dependencies": { @@ -30628,8 +24420,6 @@ }, "node_modules/restore-cursor": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, "license": "MIT", "dependencies": { @@ -30642,15 +24432,11 @@ }, "node_modules/restore-cursor/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, "license": "ISC" }, "node_modules/retry": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true, "license": "MIT", "engines": { @@ -30659,8 +24445,6 @@ }, "node_modules/reusify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -30670,9 +24454,6 @@ }, "node_modules/rimraf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -30686,8 +24467,6 @@ }, "node_modules/rimraf/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -30696,9 +24475,6 @@ }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -30717,8 +24493,6 @@ }, "node_modules/rimraf/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -30729,8 +24503,6 @@ }, "node_modules/roarr": { "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", "dev": true, "license": "BSD-3-Clause", "optional": true, @@ -30748,16 +24520,18 @@ }, "node_modules/roarr/node_modules/sprintf-js": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true, "license": "BSD-3-Clause", "optional": true }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -30800,8 +24574,6 @@ }, "node_modules/rotating-file-stream": { "version": "3.2.9", - "resolved": "https://registry.npmjs.org/rotating-file-stream/-/rotating-file-stream-3.2.9.tgz", - "integrity": "sha512-i9i0KkHh12ryl4xtELg+0gyoFre2PJ9RcQQLzquWsiqygyYsrZLckrqqYrthhnJZGZb4g+KUHtcoWYVq34gaug==", "license": "MIT", "engines": { "node": ">=14.0" @@ -30812,14 +24584,22 @@ }, "node_modules/rou3": { "version": "0.8.1", - "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.8.1.tgz", - "integrity": "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==", "license": "MIT" }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/router": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -30834,8 +24614,6 @@ }, "node_modules/router/node_modules/path-to-regexp": { "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -30844,14 +24622,10 @@ }, "node_modules/rtl-detect": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.1.2.tgz", - "integrity": "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==", "license": "BSD-3-Clause" }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -30872,10 +24646,14 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/rxjs": { "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -30884,8 +24662,6 @@ }, "node_modules/safe-array-concat": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -30904,8 +24680,6 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -30924,16 +24698,12 @@ }, "node_modules/safe-json-stringify": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", - "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", "dev": true, "license": "MIT", "optional": true }, "node_modules/safe-push-apply": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", "dev": true, "license": "MIT", "dependencies": { @@ -30949,8 +24719,6 @@ }, "node_modules/safe-regex-test": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -30966,8 +24734,6 @@ }, "node_modules/safe-stable-stringify": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", "license": "MIT", "engines": { "node": ">=10" @@ -30975,14 +24741,10 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, "node_modules/sanitize-filename": { "version": "1.6.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", "dev": true, "license": "WTFPL OR ISC", "dependencies": { @@ -30991,8 +24753,6 @@ }, "node_modules/sax": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", - "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -31000,8 +24760,6 @@ }, "node_modules/saxes": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, "license": "ISC", "dependencies": { @@ -31013,14 +24771,10 @@ }, "node_modules/scheduler": { "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, "node_modules/secure-json-parse": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", - "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", "funding": [ { "type": "github", @@ -31035,8 +24789,6 @@ }, "node_modules/semver": { "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -31047,16 +24799,12 @@ }, "node_modules/semver-compare": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "dev": true, "license": "MIT", "optional": true }, "node_modules/send": { "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -31079,8 +24827,6 @@ }, "node_modules/send/node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -31088,14 +24834,10 @@ }, "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, "node_modules/send/node_modules/mime": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "license": "MIT", "bin": { "mime": "cli.js" @@ -31106,8 +24848,6 @@ }, "node_modules/serialize-error": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", - "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -31115,8 +24855,6 @@ }, "node_modules/seroval": { "version": "1.5.4", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.4.tgz", - "integrity": "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==", "license": "MIT", "engines": { "node": ">=10" @@ -31124,8 +24862,6 @@ }, "node_modules/seroval-plugins": { "version": "1.5.4", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.4.tgz", - "integrity": "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==", "license": "MIT", "engines": { "node": ">=10" @@ -31136,8 +24872,6 @@ }, "node_modules/serve-sim": { "version": "0.1.40", - "resolved": "https://registry.npmjs.org/serve-sim/-/serve-sim-0.1.40.tgz", - "integrity": "sha512-32JSUriN/EnNR3zx6fmiOKjuFrbA8VHWR2J3dX8T51Z/xkSmrSLOwaz/3W9lPiCIaF2O3LT4qpAhLkp0f1kVaw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -31152,8 +24886,6 @@ }, "node_modules/serve-static": { "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", @@ -31167,20 +24899,14 @@ }, "node_modules/server-only": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", - "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", "license": "MIT" }, "node_modules/set-blocking": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, "node_modules/set-function-length": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -31196,8 +24922,6 @@ }, "node_modules/set-function-name": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "license": "MIT", "dependencies": { @@ -31212,8 +24936,6 @@ }, "node_modules/set-interval-async": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/set-interval-async/-/set-interval-async-3.0.3.tgz", - "integrity": "sha512-o4DyBv6mko+A9cH3QKek4SAAT5UyJRkfdTi6JHii6ZCKUYFun8SwgBmQrOXd158JOwBQzA+BnO8BvT64xuCaSw==", "dev": true, "license": "MIT", "engines": { @@ -31222,8 +24944,6 @@ }, "node_modules/set-proto": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, "license": "MIT", "dependencies": { @@ -31237,20 +24957,14 @@ }, "node_modules/setimmediate": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, "node_modules/setprototypeof": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, "node_modules/sf-symbols-typescript": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz", - "integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==", "license": "MIT", "engines": { "node": ">=10" @@ -31258,14 +24972,10 @@ }, "node_modules/shallowequal": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", "license": "MIT" }, "node_modules/sharp": { "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -31308,8 +25018,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -31320,8 +25028,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { "node": ">=8" @@ -31329,8 +25035,6 @@ }, "node_modules/shell-quote": { "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -31341,8 +25045,6 @@ }, "node_modules/sherpa-onnx-darwin-arm64": { "version": "1.12.28", - "resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-arm64/-/sherpa-onnx-darwin-arm64-1.12.28.tgz", - "integrity": "sha512-QzKrJwZR9aOmz/X6i37e0p0XDCEAGS9AuLXD5SejQxyrwOc+8db+Qi7Y+IOKk7DUebpe1mTS4gAFEXk2JnSAAg==", "cpu": [ "arm64" ], @@ -31352,49 +25054,8 @@ "darwin" ] }, - "node_modules/sherpa-onnx-darwin-x64": { - "version": "1.12.28", - "resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-x64/-/sherpa-onnx-darwin-x64-1.12.28.tgz", - "integrity": "sha512-YG5WNS/19zNS25TbG5HttsBaHWo97Mdoz4OLspyQ3EkM/XEZILz6M+Kt1/2NjZEuDKMlpgdjIyfANnBY2GYRZA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/sherpa-onnx-linux-arm64": { - "version": "1.12.28", - "resolved": "https://registry.npmjs.org/sherpa-onnx-linux-arm64/-/sherpa-onnx-linux-arm64-1.12.28.tgz", - "integrity": "sha512-NeDVuNcSUmqye/fUlqogIDX/O8NVkD9423KTQt4E/EftwoiReHLQP4CbtGoL9wHhRAy+opCzw+ssMb/coB+TqA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/sherpa-onnx-linux-x64": { - "version": "1.12.28", - "resolved": "https://registry.npmjs.org/sherpa-onnx-linux-x64/-/sherpa-onnx-linux-x64-1.12.28.tgz", - "integrity": "sha512-+7ZwpAqQYz5fjNQ13F9+sR11zKo6+HhOa1OvT+F4g4PjmoCS1o3YYQqT4JZuLeNRWAY6kKn4IXGWLx93K+OV7A==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/sherpa-onnx-node": { "version": "1.12.28", - "resolved": "https://registry.npmjs.org/sherpa-onnx-node/-/sherpa-onnx-node-1.12.28.tgz", - "integrity": "sha512-EHSB3EG6hKyXaTNh6GU/bwh6i3dncCH6ZCU2mScNzkxRbVStZ7QmNj0Oo4E9XrGGo9jX9pKg9MPHEPyjdK+ApA==", "license": "Apache-2.0", "optionalDependencies": { "sherpa-onnx-darwin-arm64": "^1.12.28", @@ -31405,36 +25066,8 @@ "sherpa-onnx-win-x64": "^1.12.28" } }, - "node_modules/sherpa-onnx-win-ia32": { - "version": "1.12.28", - "resolved": "https://registry.npmjs.org/sherpa-onnx-win-ia32/-/sherpa-onnx-win-ia32-1.12.28.tgz", - "integrity": "sha512-C9bvyg3SuVGVVRCjPH3sKk9C1rYiihTO7C9EF1T9n1V7ABdr6VE2O/7HzzMeTx9B4kUaAIDQNUq52XFE5x8oWQ==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/sherpa-onnx-win-x64": { - "version": "1.12.28", - "resolved": "https://registry.npmjs.org/sherpa-onnx-win-x64/-/sherpa-onnx-win-x64-1.12.28.tgz", - "integrity": "sha512-p8vpdVUtPRuI9ksm2EnuabK9+r285m5ARYzZkCfvg9ia/TQAq8pVoOCOuUTbMteBSQRPOl9lamfOcg3Sy085yQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/shiki": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz", - "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==", "license": "MIT", "dependencies": { "@shikijs/core": "4.0.2", @@ -31452,8 +25085,6 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -31471,8 +25102,6 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -31487,8 +25116,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -31505,8 +25132,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -31524,15 +25149,11 @@ }, "node_modules/siginfo": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, "node_modules/signal-exit": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "license": "ISC", "engines": { @@ -31544,8 +25165,6 @@ }, "node_modules/simple-plist": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", - "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", "license": "MIT", "dependencies": { "bplist-creator": "0.1.0", @@ -31555,8 +25174,6 @@ }, "node_modules/simple-plist/node_modules/bplist-parser": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", - "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", "license": "MIT", "dependencies": { "big-integer": "1.6.x" @@ -31567,8 +25184,6 @@ }, "node_modules/simple-swizzle": { "version": "0.2.4", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", - "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", "license": "MIT", "dependencies": { "is-arrayish": "^0.3.1" @@ -31576,14 +25191,10 @@ }, "node_modules/simple-swizzle/node_modules/is-arrayish": { "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", "license": "MIT" }, "node_modules/simple-update-notifier": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dev": true, "license": "MIT", "dependencies": { @@ -31595,8 +25206,6 @@ }, "node_modules/sirv": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, "license": "MIT", "dependencies": { @@ -31610,14 +25219,10 @@ }, "node_modules/sisteransi": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" }, "node_modules/slash": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", "dev": true, "license": "MIT", "engines": { @@ -31626,8 +25231,6 @@ }, "node_modules/slice-ansi": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "license": "MIT", "dependencies": { @@ -31644,8 +25247,6 @@ }, "node_modules/slugify": { "version": "1.6.6", - "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", - "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", "license": "MIT", "engines": { "node": ">=8.0.0" @@ -31653,8 +25254,6 @@ }, "node_modules/smart-buffer": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, "license": "MIT", "engines": { @@ -31664,8 +25263,6 @@ }, "node_modules/smol-toml": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", - "integrity": "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -31677,8 +25274,6 @@ }, "node_modules/socks": { "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "dev": true, "license": "MIT", "dependencies": { @@ -31692,8 +25287,6 @@ }, "node_modules/socks-proxy-agent": { "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, "license": "MIT", "dependencies": { @@ -31707,8 +25300,6 @@ }, "node_modules/socks-proxy-agent/node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -31717,8 +25308,6 @@ }, "node_modules/sonic-boom": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", - "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", "license": "MIT", "dependencies": { "atomic-sleep": "^1.0.0" @@ -31726,8 +25315,6 @@ }, "node_modules/source-map": { "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "license": "BSD-3-Clause", "engines": { "node": ">= 12" @@ -31735,8 +25322,6 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -31744,8 +25329,6 @@ }, "node_modules/source-map-support": { "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -31754,8 +25337,6 @@ }, "node_modules/source-map-support/node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -31763,8 +25344,6 @@ }, "node_modules/space-separated-tokens": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", "license": "MIT", "funding": { "type": "github", @@ -31773,8 +25352,6 @@ }, "node_modules/spdx-correct": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -31784,15 +25361,11 @@ }, "node_modules/spdx-exceptions": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true, "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -31802,15 +25375,11 @@ }, "node_modules/spdx-license-ids": { "version": "3.0.23", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", - "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "dev": true, "license": "CC0-1.0" }, "node_modules/split-on-first": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", "license": "MIT", "engines": { "node": ">=6" @@ -31818,8 +25387,6 @@ }, "node_modules/split2": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "license": "ISC", "engines": { "node": ">= 10.x" @@ -31827,14 +25394,10 @@ }, "node_modules/sprintf-js": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, "node_modules/srvx": { "version": "0.11.15", - "resolved": "https://registry.npmjs.org/srvx/-/srvx-0.11.15.tgz", - "integrity": "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg==", "license": "MIT", "bin": { "srvx": "bin/srvx.mjs" @@ -31845,8 +25408,6 @@ }, "node_modules/ssri": { "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", "dev": true, "license": "ISC", "dependencies": { @@ -31858,15 +25419,11 @@ }, "node_modules/stable-hash": { "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", - "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", "dev": true, "license": "MIT" }, "node_modules/stack-generator": { "version": "2.0.10", - "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", - "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -31875,8 +25432,6 @@ }, "node_modules/stack-utils": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" @@ -31887,8 +25442,6 @@ }, "node_modules/stack-utils/node_modules/escape-string-regexp": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "license": "MIT", "engines": { "node": ">=8" @@ -31896,21 +25449,15 @@ }, "node_modules/stackback": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, "node_modules/stackframe": { "version": "1.3.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", "license": "MIT" }, "node_modules/stacktrace-gps": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", - "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", "dev": true, "license": "MIT", "dependencies": { @@ -31920,8 +25467,6 @@ }, "node_modules/stacktrace-gps/node_modules/source-map": { "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -31930,8 +25475,6 @@ }, "node_modules/stacktrace-js": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", - "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", "dev": true, "license": "MIT", "dependencies": { @@ -31942,8 +25485,6 @@ }, "node_modules/stacktrace-parser": { "version": "0.1.11", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", - "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", "license": "MIT", "dependencies": { "type-fest": "^0.7.1" @@ -31954,8 +25495,6 @@ }, "node_modules/stacktrace-parser/node_modules/type-fest": { "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=8" @@ -31963,8 +25502,6 @@ }, "node_modules/standardwebhooks": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", - "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", "license": "MIT", "dependencies": { "@stablelib/base64": "^1.0.0", @@ -31973,8 +25510,6 @@ }, "node_modules/stat-mode": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", "dev": true, "license": "MIT", "engines": { @@ -31983,8 +25518,6 @@ }, "node_modules/statuses": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -31992,15 +25525,11 @@ }, "node_modules/std-env": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, "license": "MIT", "dependencies": { @@ -32013,8 +25542,6 @@ }, "node_modules/stream-buffers": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", - "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", "license": "Unlicense", "engines": { "node": ">= 0.10.0" @@ -32022,8 +25549,6 @@ }, "node_modules/streamx": { "version": "2.23.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", - "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", "dev": true, "license": "MIT", "dependencies": { @@ -32034,8 +25559,6 @@ }, "node_modules/strict-uri-encode": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", "license": "MIT", "engines": { "node": ">=4" @@ -32043,8 +25566,6 @@ }, "node_modules/string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", "dependencies": { @@ -32053,15 +25574,11 @@ }, "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, "license": "MIT" }, "node_modules/string-length": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", - "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", "dev": true, "license": "MIT", "dependencies": { @@ -32077,8 +25594,6 @@ }, "node_modules/string-length/node_modules/ansi-regex": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -32090,8 +25605,6 @@ }, "node_modules/string-length/node_modules/strip-ansi": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { @@ -32106,8 +25619,6 @@ }, "node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -32121,8 +25632,6 @@ "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -32136,8 +25645,6 @@ }, "node_modules/string.prototype.matchall": { "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, "license": "MIT", "dependencies": { @@ -32164,8 +25671,6 @@ }, "node_modules/string.prototype.repeat": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, "license": "MIT", "dependencies": { @@ -32175,8 +25680,6 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "license": "MIT", "dependencies": { @@ -32197,8 +25700,6 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -32216,8 +25717,6 @@ }, "node_modules/string.prototype.trimstart": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "license": "MIT", "dependencies": { @@ -32234,8 +25733,6 @@ }, "node_modules/stringify-entities": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", "license": "MIT", "dependencies": { "character-entities-html4": "^2.0.0", @@ -32248,8 +25745,6 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -32261,8 +25756,6 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -32274,8 +25767,6 @@ }, "node_modules/strip-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, "license": "MIT", "engines": { @@ -32284,8 +25775,6 @@ }, "node_modules/strip-indent": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", - "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", "dev": true, "license": "MIT", "engines": { @@ -32297,8 +25786,6 @@ }, "node_modules/strip-json-comments": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", - "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", "license": "MIT", "engines": { "node": ">=14.16" @@ -32309,20 +25796,14 @@ }, "node_modules/structured-headers": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.4.1.tgz", - "integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==", "license": "MIT" }, "node_modules/style-mod": { "version": "4.1.3", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", - "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", "license": "MIT" }, "node_modules/style-to-js": { "version": "1.1.21", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", - "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", "license": "MIT", "dependencies": { "style-to-object": "1.0.14" @@ -32330,8 +25811,6 @@ }, "node_modules/style-to-object": { "version": "1.0.14", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", - "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", "license": "MIT", "dependencies": { "inline-style-parser": "0.2.7" @@ -32339,14 +25818,16 @@ }, "node_modules/styleq": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz", - "integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==", + "license": "MIT" + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", "license": "MIT" }, "node_modules/sucrase": { "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "dev": true, "license": "MIT", "dependencies": { @@ -32368,8 +25849,6 @@ }, "node_modules/sucrase/node_modules/commander": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true, "license": "MIT", "engines": { @@ -32378,8 +25857,6 @@ }, "node_modules/sumchecker": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -32391,8 +25868,6 @@ }, "node_modules/supports-color": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -32406,8 +25881,6 @@ }, "node_modules/supports-hyperlinks": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", "license": "MIT", "dependencies": { "has-flag": "^4.0.0", @@ -32419,8 +25892,6 @@ }, "node_modules/supports-hyperlinks/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -32431,8 +25902,6 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -32443,8 +25912,6 @@ }, "node_modules/svgson": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/svgson/-/svgson-5.3.1.tgz", - "integrity": "sha512-qdPgvUNWb40gWktBJnbJRelWcPzkLed/ShhnRsjbayXz8OtdPOzbil9jtiZdrYvSDumAz/VNQr6JaNfPx/gvPA==", "dev": true, "license": "MIT", "dependencies": { @@ -32454,15 +25921,11 @@ }, "node_modules/symbol-tree": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, "license": "MIT" }, "node_modules/synckit": { "version": "0.11.12", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", - "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -32477,15 +25940,11 @@ }, "node_modules/tailwindcss": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", - "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", "dev": true, "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "license": "MIT", "engines": { @@ -32498,9 +25957,6 @@ }, "node_modules/tar": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -32517,8 +25973,6 @@ }, "node_modules/tar-stream": { "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "dev": true, "license": "MIT", "dependencies": { @@ -32529,8 +25983,6 @@ }, "node_modules/tar/node_modules/minipass": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "dev": true, "license": "ISC", "engines": { @@ -32539,8 +25991,6 @@ }, "node_modules/tar/node_modules/minizlib": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "dev": true, "license": "MIT", "dependencies": { @@ -32553,8 +26003,6 @@ }, "node_modules/tar/node_modules/minizlib/node_modules/minipass": { "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "license": "ISC", "dependencies": { @@ -32566,8 +26014,6 @@ }, "node_modules/temp-dir": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", "dev": true, "license": "MIT", "engines": { @@ -32576,8 +26022,6 @@ }, "node_modules/temp-file": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", "dev": true, "license": "MIT", "dependencies": { @@ -32587,8 +26031,6 @@ }, "node_modules/terminal-link": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", "license": "MIT", "dependencies": { "ansi-escapes": "^4.2.1", @@ -32603,8 +26045,6 @@ }, "node_modules/terser": { "version": "5.46.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", - "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -32621,14 +26061,10 @@ }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, "node_modules/test-exclude": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", @@ -32641,8 +26077,6 @@ }, "node_modules/test-exclude/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -32651,9 +26085,6 @@ }, "node_modules/test-exclude/node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -32672,8 +26103,6 @@ }, "node_modules/test-exclude/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -32684,8 +26113,6 @@ }, "node_modules/text-decoder": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -32694,8 +26121,6 @@ }, "node_modules/thenify": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -32703,8 +26128,6 @@ }, "node_modules/thenify-all": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -32715,8 +26138,6 @@ }, "node_modules/this-file": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/this-file/-/this-file-2.0.3.tgz", - "integrity": "sha512-IdMH1bUkVJdJjM7o8v83Mv4QvVPdkAofur20STl2Bbw9uMuuS/bT/PZURkEdZsy9XC/1ZXWgZ1wIL9nvouGaEg==", "dev": true, "license": "MIT", "engines": { @@ -32725,8 +26146,6 @@ }, "node_modules/thread-stream": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", - "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", "license": "MIT", "dependencies": { "real-require": "^0.2.0" @@ -32737,14 +26156,10 @@ }, "node_modules/throat": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", "license": "MIT" }, "node_modules/tiny-async-pool": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", - "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", "dev": true, "license": "MIT", "dependencies": { @@ -32753,8 +26168,6 @@ }, "node_modules/tiny-async-pool/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -32763,34 +26176,23 @@ }, "node_modules/tiny-inflate": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", - "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", "license": "MIT" }, "node_modules/tiny-invariant": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, "node_modules/tiny-typed-emitter": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", - "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", "license": "MIT" }, "node_modules/tinybench": { "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", - "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -32798,8 +26200,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -32814,8 +26214,6 @@ }, "node_modules/tinyrainbow": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -32824,8 +26222,6 @@ }, "node_modules/tmp": { "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "dev": true, "license": "MIT", "engines": { @@ -32834,8 +26230,6 @@ }, "node_modules/tmp-promise": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -32844,14 +26238,10 @@ }, "node_modules/tmpl": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "license": "BSD-3-Clause" }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -32862,8 +26252,6 @@ }, "node_modules/toidentifier": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", "engines": { "node": ">=0.6" @@ -32871,8 +26259,6 @@ }, "node_modules/totalist": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, "license": "MIT", "engines": { @@ -32881,8 +26267,6 @@ }, "node_modules/tough-cookie": { "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -32897,8 +26281,6 @@ }, "node_modules/tough-cookie/node_modules/universalify": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, "license": "MIT", "engines": { @@ -32907,14 +26289,10 @@ }, "node_modules/tr46": { "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, "node_modules/tree-kill": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "license": "MIT", "bin": { "tree-kill": "cli.js" @@ -32922,8 +26300,6 @@ }, "node_modules/trim-lines": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", "license": "MIT", "funding": { "type": "github", @@ -32932,8 +26308,6 @@ }, "node_modules/trim-newlines": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", - "integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==", "dev": true, "license": "MIT", "engines": { @@ -32945,8 +26319,6 @@ }, "node_modules/trough": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "license": "MIT", "funding": { "type": "github", @@ -32955,8 +26327,6 @@ }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", "dev": true, "license": "WTFPL", "dependencies": { @@ -32965,14 +26335,10 @@ }, "node_modules/ts-algebra": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", - "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", "license": "MIT" }, "node_modules/ts-api-utils": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -32984,8 +26350,6 @@ }, "node_modules/ts-declaration-location": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/ts-declaration-location/-/ts-declaration-location-1.0.7.tgz", - "integrity": "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==", "dev": true, "funding": [ { @@ -33005,10 +26369,17 @@ "typescript": ">=4.0.0" } }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/ts-deepmerge": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-6.2.0.tgz", - "integrity": "sha512-2qxI/FZVDPbzh63GwWIZYE7daWKtwXZYuyc8YNq0iTmMUwn4mL0jRLsp6hfFlgbdRSR4x2ppe+E86FnvEpN7Nw==", "dev": true, "license": "ISC", "engines": { @@ -33017,14 +26388,10 @@ }, "node_modules/ts-interface-checker": { "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "license": "Apache-2.0" }, "node_modules/ts-jest": { "version": "29.0.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.5.tgz", - "integrity": "sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==", "dev": true, "license": "MIT", "dependencies": { @@ -33067,8 +26434,6 @@ }, "node_modules/ts-jest/node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { @@ -33077,8 +26442,6 @@ }, "node_modules/ts-node": { "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", "dependencies": { @@ -33121,15 +26484,11 @@ }, "node_modules/ts-node/node_modules/arg": { "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true, "license": "MIT" }, "node_modules/ts-node/node_modules/diff": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -33138,8 +26497,6 @@ }, "node_modules/tsconfck": { "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", - "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", "dev": true, "license": "MIT", "bin": { @@ -33159,8 +26516,6 @@ }, "node_modules/tsconfig-paths": { "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, "license": "MIT", "dependencies": { @@ -33172,8 +26527,6 @@ }, "node_modules/tsconfig-paths/node_modules/json5": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "license": "MIT", "dependencies": { @@ -33185,14 +26538,10 @@ }, "node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tsx": { "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -33211,8 +26560,6 @@ }, "node_modules/tunnel-agent": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -33224,8 +26571,6 @@ }, "node_modules/turndown": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.1.2.tgz", - "integrity": "sha512-ntI9R7fcUKjqBP6QU8rBK2Ehyt8LAzt3UBT9JR9tgo6GtuKvyUzpayWmeMKJw1DPdXzktvtIT8m2mVXz+bL/Qg==", "dev": true, "license": "MIT", "dependencies": { @@ -33234,20 +26579,14 @@ }, "node_modules/turndown-plugin-gfm": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.2.tgz", - "integrity": "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==", "license": "MIT" }, "node_modules/tweetnacl": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", "license": "Unlicense" }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -33259,8 +26598,6 @@ }, "node_modules/type-detect": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "license": "MIT", "engines": { "node": ">=4" @@ -33268,8 +26605,6 @@ }, "node_modules/type-fest": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -33281,8 +26616,6 @@ }, "node_modules/type-is": { "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", "dependencies": { "media-typer": "0.3.0", @@ -33294,8 +26627,6 @@ }, "node_modules/typed-array-buffer": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "license": "MIT", "dependencies": { @@ -33309,8 +26640,6 @@ }, "node_modules/typed-array-byte-length": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "license": "MIT", "dependencies": { @@ -33329,8 +26658,6 @@ }, "node_modules/typed-array-byte-offset": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "license": "MIT", "dependencies": { @@ -33351,8 +26678,6 @@ }, "node_modules/typed-array-length": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, "license": "MIT", "dependencies": { @@ -33372,8 +26697,6 @@ }, "node_modules/typescript": { "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -33386,8 +26709,6 @@ }, "node_modules/ua-parser-js": { "version": "1.0.41", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", - "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", "funding": [ { "type": "opencollective", @@ -33412,20 +26733,14 @@ }, "node_modules/uc.micro": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "license": "MIT" }, "node_modules/ufo": { "version": "1.6.4", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", - "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", "license": "MIT" }, "node_modules/unbash": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unbash/-/unbash-2.2.0.tgz", - "integrity": "sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==", "dev": true, "license": "ISC", "engines": { @@ -33434,8 +26749,6 @@ }, "node_modules/unbox-primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "license": "MIT", "dependencies": { @@ -33453,8 +26766,6 @@ }, "node_modules/undici": { "version": "7.24.8", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz", - "integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -33462,14 +26773,10 @@ }, "node_modules/undici-types": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/unenv": { "version": "2.0.0-rc.24", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", - "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "license": "MIT", "dependencies": { "pathe": "^2.0.3" @@ -33477,8 +26784,6 @@ }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "license": "MIT", "engines": { "node": ">=4" @@ -33486,8 +26791,6 @@ }, "node_modules/unicode-match-property-ecmascript": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", @@ -33499,8 +26802,6 @@ }, "node_modules/unicode-match-property-value-ecmascript": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", - "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "license": "MIT", "engines": { "node": ">=4" @@ -33508,8 +26809,6 @@ }, "node_modules/unicode-property-aliases-ecmascript": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", - "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "license": "MIT", "engines": { "node": ">=4" @@ -33517,8 +26816,6 @@ }, "node_modules/unified": { "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -33536,8 +26833,6 @@ }, "node_modules/unified/node_modules/is-plain-obj": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "license": "MIT", "engines": { "node": ">=12" @@ -33548,8 +26843,6 @@ }, "node_modules/unique-filename": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", "dev": true, "license": "ISC", "dependencies": { @@ -33561,8 +26854,6 @@ }, "node_modules/unique-slug": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", - "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", "dev": true, "license": "ISC", "dependencies": { @@ -33574,8 +26865,6 @@ }, "node_modules/unique-string": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", "dev": true, "license": "MIT", "dependencies": { @@ -33587,8 +26876,6 @@ }, "node_modules/unist-util-is": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -33600,8 +26887,6 @@ }, "node_modules/unist-util-position": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -33613,8 +26898,6 @@ }, "node_modules/unist-util-stringify-position": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -33626,8 +26909,6 @@ }, "node_modules/unist-util-visit": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -33641,8 +26922,6 @@ }, "node_modules/unist-util-visit-parents": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -33655,8 +26934,6 @@ }, "node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "license": "MIT", "engines": { "node": ">= 10.0.0" @@ -33664,8 +26941,6 @@ }, "node_modules/unpipe": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -33673,8 +26948,6 @@ }, "node_modules/unplugin": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", - "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", @@ -33687,8 +26960,6 @@ }, "node_modules/unrs-resolver": { "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -33722,8 +26993,6 @@ }, "node_modules/untildify": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "dev": true, "license": "MIT", "engines": { @@ -33732,8 +27001,6 @@ }, "node_modules/unzip-crx-3": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/unzip-crx-3/-/unzip-crx-3-0.2.0.tgz", - "integrity": "sha512-0+JiUq/z7faJ6oifVB5nSwt589v1KCduqIJupNVDoWSXZtWDmjDGO3RAEOvwJ07w90aoXoP4enKsR7ecMrJtWQ==", "dev": true, "license": "MIT", "dependencies": { @@ -33744,8 +27011,6 @@ }, "node_modules/unzip-crx-3/node_modules/mkdirp": { "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", "dependencies": { @@ -33757,8 +27022,6 @@ }, "node_modules/update-browserslist-db": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -33787,8 +27050,6 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -33796,8 +27057,6 @@ }, "node_modules/url-parse": { "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -33807,8 +27066,6 @@ }, "node_modules/use-callback-ref": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -33828,8 +27085,6 @@ }, "node_modules/use-latest-callback": { "version": "0.2.6", - "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz", - "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==", "license": "MIT", "peerDependencies": { "react": ">=16.8" @@ -33837,8 +27092,6 @@ }, "node_modules/use-sidecar": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", @@ -33859,8 +27112,6 @@ }, "node_modules/use-sync-external-store": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -33868,15 +27119,11 @@ }, "node_modules/utf8-byte-length": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", "dev": true, "license": "(WTFPL OR MIT)" }, "node_modules/util": { "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -33888,15 +27135,11 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "license": "MIT", "engines": { "node": ">= 0.4.0" @@ -33904,8 +27147,6 @@ }, "node_modules/uuid": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -33917,15 +27158,11 @@ }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true, "license": "MIT" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -33935,8 +27172,6 @@ }, "node_modules/validate-npm-package-name": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", - "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -33944,8 +27179,6 @@ }, "node_modules/vary": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -33953,8 +27186,6 @@ }, "node_modules/vaul": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", - "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", "license": "MIT", "dependencies": { "@radix-ui/react-dialog": "^1.1.1" @@ -33966,8 +27197,6 @@ }, "node_modules/verror": { "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", "dev": true, "license": "MIT", "optional": true, @@ -33982,8 +27211,6 @@ }, "node_modules/vfile": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -33996,8 +27223,6 @@ }, "node_modules/vfile-location": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", - "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -34010,8 +27235,6 @@ }, "node_modules/vfile-message": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -34024,8 +27247,6 @@ }, "node_modules/vite": { "version": "7.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", - "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", "license": "MIT", "dependencies": { "esbuild": "^0.27.0", @@ -34098,8 +27319,6 @@ }, "node_modules/vite-tsconfig-paths": { "version": "5.1.4", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", - "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", "dev": true, "license": "MIT", "dependencies": { @@ -34118,8 +27337,6 @@ }, "node_modules/vite/node_modules/postcss": { "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "funding": [ { "type": "opencollective", @@ -34146,8 +27363,6 @@ }, "node_modules/vitefu": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", - "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", "license": "MIT", "workspaces": [ "tests/deps/*", @@ -34165,8 +27380,6 @@ }, "node_modules/vitest": { "version": "4.1.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", - "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", "dev": true, "license": "MIT", "dependencies": { @@ -34255,14 +27468,10 @@ }, "node_modules/vlq": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", - "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", "license": "MIT" }, "node_modules/void-elements": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -34270,14 +27479,10 @@ }, "node_modules/w3c-keyname": { "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", "dev": true, "license": "MIT", "dependencies": { @@ -34289,8 +27494,6 @@ }, "node_modules/wait-on": { "version": "8.0.5", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.5.tgz", - "integrity": "sha512-J3WlS0txVHkhLRb2FsmRg3dkMTCV1+M6Xra3Ho7HzZDHpE7DCOnoSoCJsZotrmW3uRMhvIJGSKUKrh/MeF4iag==", "dev": true, "license": "MIT", "dependencies": { @@ -34309,15 +27512,11 @@ }, "node_modules/wait-on/node_modules/@hapi/hoek": { "version": "11.0.7", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", - "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/wait-on/node_modules/@hapi/topo": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", - "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -34326,8 +27525,6 @@ }, "node_modules/wait-on/node_modules/joi": { "version": "18.0.2", - "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", - "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -34345,8 +27542,6 @@ }, "node_modules/walk-up-path": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", - "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", "dev": true, "license": "ISC", "engines": { @@ -34355,8 +27550,6 @@ }, "node_modules/walker": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "license": "Apache-2.0", "dependencies": { "makeerror": "1.0.12" @@ -34364,14 +27557,10 @@ }, "node_modules/warn-once": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz", - "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", "license": "MIT" }, "node_modules/wcwidth": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", "license": "MIT", "dependencies": { "defaults": "^1.0.3" @@ -34379,8 +27568,6 @@ }, "node_modules/web-namespaces": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", - "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", "license": "MIT", "funding": { "type": "github", @@ -34389,26 +27576,18 @@ }, "node_modules/webidl-conversions": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", - "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" }, "node_modules/whatwg-fetch": { "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "license": "MIT" }, "node_modules/whatwg-url": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "license": "MIT", "dependencies": { "tr46": "~0.0.3", @@ -34417,8 +27596,6 @@ }, "node_modules/whatwg-url-without-unicode": { "version": "8.0.0-3", - "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", - "integrity": "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==", "license": "MIT", "dependencies": { "buffer": "^5.4.3", @@ -34431,8 +27608,6 @@ }, "node_modules/whatwg-url-without-unicode/node_modules/buffer": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -34455,8 +27630,6 @@ }, "node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", "license": "BSD-2-Clause", "engines": { "node": ">=8" @@ -34464,8 +27637,6 @@ }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -34479,8 +27650,6 @@ }, "node_modules/which-boxed-primitive": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, "license": "MIT", "dependencies": { @@ -34499,8 +27668,6 @@ }, "node_modules/which-builtin-type": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -34527,8 +27694,6 @@ }, "node_modules/which-collection": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "license": "MIT", "dependencies": { @@ -34546,14 +27711,10 @@ }, "node_modules/which-module": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, "node_modules/which-typed-array": { "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -34573,8 +27734,6 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -34590,8 +27749,6 @@ }, "node_modules/widest-line": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", "dev": true, "license": "MIT", "dependencies": { @@ -34603,14 +27760,10 @@ }, "node_modules/wonka": { "version": "6.3.5", - "resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz", - "integrity": "sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw==", "license": "MIT" }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -34619,15 +27772,11 @@ }, "node_modules/wordwrap": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "dev": true, "license": "MIT" }, "node_modules/workerd": { "version": "1.20260507.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260507.1.tgz", - "integrity": "sha512-z7JhsFSe6+X1b5fUHaVpo15VM1IRMJiLofEkq8iKdCo+Veqc+FUg5lIsuz8NwePxuSKrXtO4ZQpGkQLbPVXFhg==", "hasInstallScript": true, "license": "Apache-2.0", "bin": { @@ -34646,8 +27795,6 @@ }, "node_modules/wrangler": { "version": "4.90.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.90.0.tgz", - "integrity": "sha512-bmNIykl59TfCUn5xQgU7IWylSsPx3LQaPLMSAq2VQHt89CBrcj9qXQ0eYfjBCWA5XTBVgten391evt7xxtXwcA==", "license": "MIT OR Apache-2.0", "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", @@ -34680,14 +27827,10 @@ }, "node_modules/wrangler/node_modules/path-to-regexp": { "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "license": "MIT" }, "node_modules/wrap-ansi": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -34704,8 +27847,6 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -34722,14 +27863,10 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, "node_modules/write-file-atomic": { "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", "dev": true, "license": "ISC", "dependencies": { @@ -34740,15 +27877,11 @@ }, "node_modules/write-file-atomic/node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, "license": "ISC" }, "node_modules/ws": { "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -34768,8 +27901,6 @@ }, "node_modules/xcode": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", - "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", "license": "Apache-2.0", "dependencies": { "simple-plist": "^1.1.0", @@ -34781,8 +27912,6 @@ }, "node_modules/xcode/node_modules/uuid": { "version": "7.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", - "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -34790,8 +27919,6 @@ }, "node_modules/xml-lexer": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/xml-lexer/-/xml-lexer-0.2.2.tgz", - "integrity": "sha512-G0i98epIwiUEiKmMcavmVdhtymW+pCAohMRgybyIME9ygfVu8QheIi+YoQh3ngiThsT0SQzJT4R0sKDEv8Ou0w==", "dev": true, "license": "MIT", "dependencies": { @@ -34800,8 +27927,6 @@ }, "node_modules/xml-name-validator": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -34810,8 +27935,6 @@ }, "node_modules/xml-reader": { "version": "2.4.3", - "resolved": "https://registry.npmjs.org/xml-reader/-/xml-reader-2.4.3.tgz", - "integrity": "sha512-xWldrIxjeAMAu6+HSf9t50ot1uL5M+BtOidRCWHXIeewvSeIpscWCsp4Zxjk8kHHhdqFBrfK8U0EJeCcnyQ/gA==", "dev": true, "license": "MIT", "dependencies": { @@ -34821,8 +27944,6 @@ }, "node_modules/xml2js": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz", - "integrity": "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==", "license": "MIT", "dependencies": { "sax": ">=0.6.0", @@ -34834,8 +27955,6 @@ }, "node_modules/xml2js/node_modules/xmlbuilder": { "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "license": "MIT", "engines": { "node": ">=4.0" @@ -34843,8 +27962,6 @@ }, "node_modules/xmlbuilder": { "version": "14.0.0", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-14.0.0.tgz", - "integrity": "sha512-ts+B2rSe4fIckR6iquDjsKbQFK2NlUk6iG5nf14mDEyldgoc2nEKZ3jZWMPTxGQwVgToSjt6VGIho1H8/fNFTg==", "dev": true, "license": "MIT", "engines": { @@ -34853,8 +27970,6 @@ }, "node_modules/xmlbuilder2": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", - "integrity": "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==", "license": "MIT", "dependencies": { "@oozcitak/dom": "^2.0.2", @@ -34868,15 +27983,11 @@ }, "node_modules/xmlchars": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, "license": "MIT" }, "node_modules/y18n": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "license": "ISC", "engines": { "node": ">=10" @@ -34884,21 +27995,15 @@ }, "node_modules/yaku": { "version": "0.16.7", - "resolved": "https://registry.npmjs.org/yaku/-/yaku-0.16.7.tgz", - "integrity": "sha512-Syu3IB3rZvKvYk7yTiyl1bo/jiEFaaStrgv1V2TIJTqYPStSMQVO8EQjg/z+DRzLq/4LIIharNT3iH1hylEIRw==", "dev": true, "license": "MIT" }, "node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, "node_modules/yaml": { "version": "2.8.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", - "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -34912,8 +28017,6 @@ }, "node_modules/yargs": { "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -34930,8 +28033,6 @@ }, "node_modules/yargs-parser": { "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, "license": "ISC", "engines": { @@ -34940,8 +28041,6 @@ }, "node_modules/yargs/node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "license": "ISC", "engines": { "node": ">=12" @@ -34949,8 +28048,6 @@ }, "node_modules/yauzl": { "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "license": "MIT", "dependencies": { @@ -34960,8 +28057,6 @@ }, "node_modules/yn": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, "license": "MIT", "engines": { @@ -34970,8 +28065,6 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "license": "MIT", "engines": { "node": ">=10" @@ -34982,8 +28075,6 @@ }, "node_modules/youch": { "version": "4.1.0-beta.10", - "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", - "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", "license": "MIT", "dependencies": { "@poppinss/colors": "^4.1.5", @@ -34995,8 +28086,6 @@ }, "node_modules/youch-core": { "version": "0.3.3", - "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", - "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", "license": "MIT", "dependencies": { "@poppinss/exception": "^1.2.2", @@ -35005,8 +28094,6 @@ }, "node_modules/youch/node_modules/cookie": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { "node": ">=18" @@ -35018,8 +28105,6 @@ }, "node_modules/zod": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -35027,8 +28112,6 @@ }, "node_modules/zod-aot": { "version": "0.20.4", - "resolved": "https://registry.npmjs.org/zod-aot/-/zod-aot-0.20.4.tgz", - "integrity": "sha512-vVuKSG4MpJw3bRQu7UEd6ziJ8+AJY3JB2rWqshnSIRJmhxWFb3FiZ9QYnYcldKYZRzO7edgid8BxJkGbZg43OA==", "dev": true, "license": "MIT", "dependencies": { @@ -35047,8 +28130,6 @@ }, "node_modules/zod-to-json-schema": { "version": "3.25.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", "license": "ISC", "peerDependencies": { "zod": "^3.25 || ^4" @@ -35056,8 +28137,6 @@ }, "node_modules/zustand": { "version": "5.0.11", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", - "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", "license": "MIT", "engines": { "node": ">=12.20.0" @@ -35085,8 +28164,6 @@ }, "node_modules/zwitch": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", "license": "MIT", "funding": { "type": "github", @@ -35095,8 +28172,6 @@ }, "node_modules/zx": { "version": "8.8.5", - "resolved": "https://registry.npmjs.org/zx/-/zx-8.8.5.tgz", - "integrity": "sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -35108,7 +28183,7 @@ }, "packages/app": { "name": "@getpaseo/app", - "version": "0.3.1", + "version": "0.4.0", "dependencies": { "@codemirror/commands": "6.10.4", "@codemirror/language": "6.12.4", @@ -35125,6 +28200,7 @@ "@getpaseo/protocol": "*", "@gorhom/bottom-sheet": "^5.2.14", "@gorhom/portal": "^1.0.14", + "@mattermost/react-native-paste-input": "2.0.1", "@react-native-async-storage/async-storage": "2.2.0", "@react-native-masked-view/masked-view": "^0.3.2", "@react-native/normalize-colors": "^0.81.5", @@ -35170,6 +28246,7 @@ "i18next": "^26.3.0", "lucide-react-native": "^0.546.0", "markdown-it": "^10.0.0", + "mermaid": "^11.16.0", "mnemonic-id": "^3.2.7", "qrcode": "^1.5.4", "react": "19.1.0", @@ -35195,6 +28272,7 @@ "turndown": "^7.2.4", "turndown-plugin-gfm": "^1.0.2", "use-sync-external-store": "^1.6.0", + "yaml": "^2.8.4", "zod": "^4.4.3", "zustand": "^5.0.9" }, @@ -35212,6 +28290,7 @@ "@xterm/headless": "^6.1.0-beta.213", "dotenv": "^17.2.3", "eas-cli": "^16.24.1", + "esbuild": "0.28.1", "eslint": "^9.25.0", "eslint-config-expo": "~10.0.0", "expo-gradle-jvmargs": "^1.1.2", @@ -35225,27 +28304,8 @@ "ws": "^8.20.0" } }, - "packages/app/node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260625.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260625.1.tgz", - "integrity": "sha512-naCfBv0WnnTQIQPTniqMoUlklOIFjrAcSn1X+IAOhY8aFLF/xGYtFjs1eEE8sFib3ZuChGGpU23FFORVczqr0A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, "packages/app/node_modules/@cloudflare/workerd-darwin-arm64": { "version": "1.20260625.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260625.1.tgz", - "integrity": "sha512-jmH6zjp6Wrux46+qtFwDwrj+vd7s5bdwEqeGvdnwE0a4IEeAhKs0L42HQOyID+g5lkrHq9m55+AbhtmRAm63Pw==", "cpu": [ "arm64" ], @@ -35259,138 +28319,15 @@ "node": ">=16" } }, - "packages/app/node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260625.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260625.1.tgz", - "integrity": "sha512-MiQkpA/dX8d83Zp64pzHUKfd6ca4cvwxnNobSP6CnXvfESvnNI9pfa+nfwnParla36sPmnYntNkjR7NjRuDeKQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "packages/app/node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260625.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260625.1.tgz", - "integrity": "sha512-LxxW7Qv60Xvv37+w6gUSDpYZziyqMy+cZWd9IvSA5ehVgKAxmzEaYPMiSZlxk32nbIWL9u/tfjXYCOKJ4Lo+XQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "packages/app/node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260625.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260625.1.tgz", - "integrity": "sha512-LH6iIX1HHaTwVKV5VokDxxUErXJzQoNZFRwVm7Vx/3fB/ApcTcRCUaMqcxI4as94jEUqg+pmX5czOndiveohow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, "packages/app/node_modules/@cloudflare/workers-types": { "version": "4.20260627.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260627.1.tgz", - "integrity": "sha512-QhVvier1eW9Ydw96N/Ez79i5CSGy7h39JucrUejQmAWQw9qBdlB1aOTjaD93EGYrV0t9U81YWetI3LcaxxILNw==", "dev": true, "license": "MIT OR Apache-2.0", "optional": true, "peer": true }, - "packages/app/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, "packages/app/node_modules/@esbuild/darwin-arm64": { "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -35404,367 +28341,8 @@ "node": ">=18" } }, - "packages/app/node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "packages/app/node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "packages/app/node_modules/@xterm/addon-clipboard": { "version": "0.3.0-beta.213", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.213.tgz", - "integrity": "sha512-gFW1jSpKFhMN29ArgBXIyYzxrDcGeWVP943/BxcWelHthAjhWrt048LxkNtt7/FgF60ZfysIyvMXLeVetXfduA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" @@ -35775,8 +28353,6 @@ }, "packages/app/node_modules/@xterm/addon-fit": { "version": "0.12.0-beta.213", - "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.12.0-beta.213.tgz", - "integrity": "sha512-hFupt6CX7MNO0XKjugPpm2T3/PUGT2Y7o92Y9UyQAH0a7Da/H5nzkDbJLH1LZHfyUFaJOOvkJOQl8oreWMr/SQ==", "license": "MIT", "peerDependencies": { "@xterm/xterm": "^6.1.0-beta.213" @@ -35784,8 +28360,6 @@ }, "packages/app/node_modules/@xterm/addon-image": { "version": "0.10.0-beta.213", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.213.tgz", - "integrity": "sha512-L3QxLkj8g8TioVea9p1oVze52Jr75rRZPPLm1tgZl0X2EGJGFHnQzE3QRsgmYWI7R5j3kwwi3MRAkOAgvOK8uA==", "license": "MIT", "peerDependencies": { "@xterm/xterm": "^6.1.0-beta.213" @@ -35793,8 +28367,6 @@ }, "packages/app/node_modules/@xterm/addon-ligatures": { "version": "0.11.0-beta.213", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.213.tgz", - "integrity": "sha512-4YLFsl+K/RfTrIxHuMLXvQ3iJmBtxHlLaC4sutPZT1Nkh01QxOmMAn+JZI2jm5u1RPO2F6WRfwu1jtqhunGCpQ==", "license": "MIT", "dependencies": { "lru-cache": "^6.0.0", @@ -35809,8 +28381,6 @@ }, "packages/app/node_modules/@xterm/addon-search": { "version": "0.17.0-beta.213", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.213.tgz", - "integrity": "sha512-SlXOagJg8ZH78T6tfoUqauJPAWo1N8yb8FamrHNsGP7HAFpOq3m1U9hH8q8iHeOQzs7VcVzD7J2/X3gAMJRn+g==", "license": "MIT", "peerDependencies": { "@xterm/xterm": "^6.1.0-beta.213" @@ -35818,8 +28388,6 @@ }, "packages/app/node_modules/@xterm/addon-unicode11": { "version": "0.10.0-beta.213", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.213.tgz", - "integrity": "sha512-Tk2u+4HrkOwlagO2w/Knc5RF2LLA9D2i6yxUMmnxFKaQ88qXOnyqEn4b+OhPCaWHJJnWsV5grjMMQq6utRGxcQ==", "license": "MIT", "peerDependencies": { "@xterm/xterm": "^6.1.0-beta.213" @@ -35827,8 +28395,6 @@ }, "packages/app/node_modules/@xterm/addon-web-links": { "version": "0.13.0-beta.213", - "resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.13.0-beta.213.tgz", - "integrity": "sha512-SNMLWgcVxchXTSxIzqZkQ7xennu0YVpmtDMVZi3K03JyFXw8NDu57B0O01JR8d7Rlk+EWk+B/aMxIehnCSgX6Q==", "license": "MIT", "peerDependencies": { "@xterm/xterm": "^6.1.0-beta.213" @@ -35836,8 +28402,6 @@ }, "packages/app/node_modules/@xterm/addon-webgl": { "version": "0.20.0-beta.212", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.212.tgz", - "integrity": "sha512-RzaTYeukTXTJth+Sl7FzCmb8AqiyDrNWNnqG+/300qJbfHYfCMmBlBVZ0yCTSZIWMdgFbgkhmnmBN7Yzs6xoBA==", "license": "MIT", "peerDependencies": { "@xterm/xterm": "^6.1.0-beta.213" @@ -35845,8 +28409,6 @@ }, "packages/app/node_modules/@xterm/headless": { "version": "6.1.0-beta.213", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.213.tgz", - "integrity": "sha512-D08E4Bl0HRhYZRL2o5PxD4FNQN9qXpI8QSw3CkKrsym5dtTRAqsynVYE8V8echQrxfb+EpR0CB5reM7tEGZ/fA==", "dev": true, "license": "MIT", "workspaces": [ @@ -35855,8 +28417,6 @@ }, "packages/app/node_modules/@xterm/xterm": { "version": "6.1.0-beta.213", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.213.tgz", - "integrity": "sha512-bq5nJkgoZDSg6Ma4zgiQ7jlooGNo7U+HDa6vnhx43yXsVEK2laIF7R08bMYAuU0B+C9xX0gwSrcsW18GxjUoJQ==", "license": "MIT", "workspaces": [ "addons/*" @@ -35864,8 +28424,6 @@ }, "packages/app/node_modules/dom-serializer": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz", - "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==", "license": "MIT", "dependencies": { "domelementtype": "^3.0.0", @@ -35882,8 +28440,6 @@ }, "packages/app/node_modules/domelementtype": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz", - "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==", "funding": [ { "type": "github", @@ -35897,8 +28453,6 @@ }, "packages/app/node_modules/domhandler": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz", - "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^3.0.0" @@ -35913,8 +28467,6 @@ }, "packages/app/node_modules/domutils": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", - "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^3.0.0", @@ -35931,8 +28483,6 @@ }, "packages/app/node_modules/entities": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "license": "BSD-2-Clause", "engines": { "node": ">=20.19.0" @@ -35943,8 +28493,6 @@ }, "packages/app/node_modules/esbuild": { "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -35985,8 +28533,6 @@ }, "packages/app/node_modules/expo-clipboard": { "version": "8.0.7", - "resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-8.0.7.tgz", - "integrity": "sha512-zvlfFV+wB2QQrQnHWlo0EKHAkdi2tycLtE+EXFUWTPZYkgu1XcH+aiKfd4ul7Z0SDF+1IuwoiW9AA9eO35aj3Q==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -35996,8 +28542,6 @@ }, "packages/app/node_modules/htmlparser2": { "version": "12.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-12.0.0.tgz", - "integrity": "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -36018,8 +28562,6 @@ }, "packages/app/node_modules/miniflare": { "version": "4.20260625.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260625.0.tgz", - "integrity": "sha512-3kKXwRUObJsnBYPBgR0NiNZYKF/yv8GFyha1cx2EeAEraxNODgRVcyeRo+F1ok1tg5Mg7iUpOWSkknQTHuFhwA==", "dev": true, "license": "MIT", "dependencies": { @@ -36039,15 +28581,11 @@ }, "packages/app/node_modules/path-to-regexp": { "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "dev": true, "license": "MIT" }, "packages/app/node_modules/react-native-keyboard-controller": { "version": "1.21.12", - "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.21.12.tgz", - "integrity": "sha512-pZJNXS//AhXDD9RvkNBZF5MsHv9qbFpNeHMIZ9ESSUqubNsACU7GyTE4GekIcJ1MBu+r9/TNZQuEfpT9v01HZg==", "license": "MIT", "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" @@ -36060,8 +28598,6 @@ }, "packages/app/node_modules/turndown": { "version": "7.2.4", - "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.4.tgz", - "integrity": "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==", "license": "MIT", "dependencies": { "@mixmark-io/domino": "^2.2.0" @@ -36073,8 +28609,6 @@ }, "packages/app/node_modules/undici": { "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "dev": true, "license": "MIT", "engines": { @@ -36083,8 +28617,6 @@ }, "packages/app/node_modules/workerd": { "version": "1.20260625.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260625.1.tgz", - "integrity": "sha512-GApQvFX52SDM6L4u0+RRnUDB1wJOnEwoXjinkmOPtIyofWBxrlZckdegJSYc1leg++lLZ3+DQ4zMVmBqYVtzfA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -36104,8 +28636,6 @@ }, "packages/app/node_modules/wrangler": { "version": "4.105.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.105.0.tgz", - "integrity": "sha512-7dXFH6OLj1Fv0y6ZeRPUxFTkp+duWD7/xxVi/1c0vfOeEYwIFKWB7cdqnY05DvY1Ta3BnqAwRkXfLs8PDj538g==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { @@ -36140,8 +28670,6 @@ }, "packages/app/node_modules/ws": { "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "license": "MIT", "engines": { @@ -36162,12 +28690,12 @@ }, "packages/cli": { "name": "@getpaseo/cli", - "version": "0.3.1", + "version": "0.4.0", "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/client": "0.3.1", - "@getpaseo/protocol": "0.3.1", - "@getpaseo/server": "0.3.1", + "@getpaseo/client": "0.4.0", + "@getpaseo/protocol": "0.4.0", + "@getpaseo/server": "0.4.0", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", @@ -36190,8 +28718,6 @@ }, "packages/cli/node_modules/@vitest/expect": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", - "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", "dev": true, "license": "MIT", "dependencies": { @@ -36208,8 +28734,6 @@ }, "packages/cli/node_modules/@vitest/pretty-format": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", - "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", "dev": true, "license": "MIT", "dependencies": { @@ -36221,8 +28745,6 @@ }, "packages/cli/node_modules/@vitest/runner": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", - "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -36235,8 +28757,6 @@ }, "packages/cli/node_modules/@vitest/snapshot": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", - "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", "dev": true, "license": "MIT", "dependencies": { @@ -36251,8 +28771,6 @@ }, "packages/cli/node_modules/@vitest/spy": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", - "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", "dev": true, "license": "MIT", "funding": { @@ -36261,8 +28779,6 @@ }, "packages/cli/node_modules/@vitest/utils": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", - "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -36276,8 +28792,6 @@ }, "packages/cli/node_modules/chalk": { "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -36288,8 +28802,6 @@ }, "packages/cli/node_modules/commander": { "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "license": "MIT", "engines": { "node": ">=18" @@ -36297,8 +28809,6 @@ }, "packages/cli/node_modules/vitest": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", - "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -36387,8 +28897,6 @@ }, "packages/cli/node_modules/vitest/node_modules/@vitest/mocker": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", - "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -36414,10 +28922,10 @@ }, "packages/client": { "name": "@getpaseo/client", - "version": "0.3.1", + "version": "0.4.0", "dependencies": { - "@getpaseo/protocol": "0.3.1", - "@getpaseo/relay": "0.3.1", + "@getpaseo/protocol": "0.4.0", + "@getpaseo/relay": "0.4.0", "zod": "^4.4.3" }, "devDependencies": { @@ -36428,7 +28936,7 @@ }, "packages/desktop": { "name": "@getpaseo/desktop", - "version": "0.3.1", + "version": "0.4.0", "license": "AGPL-3.0-or-later", "dependencies": { "@getpaseo/cli": "*", @@ -36451,8 +28959,6 @@ }, "packages/desktop/node_modules/@types/node": { "version": "24.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.0.tgz", - "integrity": "sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA==", "dev": true, "license": "MIT", "dependencies": { @@ -36461,8 +28967,6 @@ }, "packages/desktop/node_modules/@vitest/expect": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", - "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", "dev": true, "license": "MIT", "dependencies": { @@ -36479,8 +28983,6 @@ }, "packages/desktop/node_modules/@vitest/pretty-format": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", - "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", "dev": true, "license": "MIT", "dependencies": { @@ -36492,8 +28994,6 @@ }, "packages/desktop/node_modules/@vitest/runner": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", - "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -36506,8 +29006,6 @@ }, "packages/desktop/node_modules/@vitest/snapshot": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", - "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", "dev": true, "license": "MIT", "dependencies": { @@ -36522,8 +29020,6 @@ }, "packages/desktop/node_modules/@vitest/spy": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", - "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", "dev": true, "license": "MIT", "funding": { @@ -36532,8 +29028,6 @@ }, "packages/desktop/node_modules/@vitest/utils": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", - "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -36547,15 +29041,11 @@ }, "packages/desktop/node_modules/undici-types": { "version": "7.13.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", - "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==", "dev": true, "license": "MIT" }, "packages/desktop/node_modules/vitest": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", - "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -36644,8 +29134,6 @@ }, "packages/desktop/node_modules/vitest/node_modules/@vitest/mocker": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", - "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -36671,7 +29159,7 @@ }, "packages/expo-two-way-audio": { "name": "@getpaseo/expo-two-way-audio", - "version": "0.3.1", + "version": "0.4.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.14", @@ -36687,8 +29175,6 @@ }, "packages/expo-two-way-audio/node_modules/@expo/config": { "version": "12.0.13", - "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.13.tgz", - "integrity": "sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ==", "dev": true, "license": "MIT", "dependencies": { @@ -36709,8 +29195,6 @@ }, "packages/expo-two-way-audio/node_modules/@expo/config-plugins": { "version": "54.0.4", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.4.tgz", - "integrity": "sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -36732,8 +29216,6 @@ }, "packages/expo-two-way-audio/node_modules/@expo/config-plugins/node_modules/@babel/code-frame": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "license": "MIT", "dependencies": { @@ -36742,8 +29224,6 @@ }, "packages/expo-two-way-audio/node_modules/@expo/config-plugins/node_modules/@expo/json-file": { "version": "10.0.16", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.16.tgz", - "integrity": "sha512-fcVkWEj+hLuP2yt5W0aw6LmDRqSPWDLUSxOMcmFeV+algmIF59sQVKCwB9btjQLd4V6x9N0pISkQEkBubUHrCw==", "dev": true, "license": "MIT", "dependencies": { @@ -36753,15 +29233,11 @@ }, "packages/expo-two-way-audio/node_modules/@expo/config-types": { "version": "54.0.10", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-54.0.10.tgz", - "integrity": "sha512-/J16SC2an1LdtCZ67xhSkGXpALYUVUNyZws7v+PVsFZxClYehDSoKLqyRaGkpHlYrCc08bS0RF5E0JV6g50psA==", "dev": true, "license": "MIT" }, "packages/expo-two-way-audio/node_modules/@expo/config/node_modules/@babel/code-frame": { "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "license": "MIT", "dependencies": { @@ -36770,8 +29246,6 @@ }, "packages/expo-two-way-audio/node_modules/@expo/json-file": { "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.2.0.tgz", - "integrity": "sha512-S6XzKe3R9GQeHiUPXc3xJjOv2VJhOEwFYf7xdC2z2cUqt3kZJ9mSO877sNQloVdnW/SUCtPY3bexlM7nwq+CAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -36781,8 +29255,6 @@ }, "packages/expo-two-way-audio/node_modules/@expo/plist": { "version": "0.4.9", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.9.tgz", - "integrity": "sha512-MPVpmKGfnQEnrCzgxuXcmPP/y/t6AVm+DcSb2Myp21LKWv1N3l8uFxMggesfF4ixAxkRlGmMMx9GyDC9M+XklQ==", "dev": true, "license": "MIT", "dependencies": { @@ -36793,8 +29265,6 @@ }, "packages/expo-two-way-audio/node_modules/@jest/schemas": { "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", "dev": true, "license": "MIT", "dependencies": { @@ -36806,15 +29276,11 @@ }, "packages/expo-two-way-audio/node_modules/@sinclair/typebox": { "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", "dev": true, "license": "MIT" }, "packages/expo-two-way-audio/node_modules/@types/react": { "version": "18.3.28", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", - "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "dev": true, "license": "MIT", "dependencies": { @@ -36824,8 +29290,6 @@ }, "packages/expo-two-way-audio/node_modules/@typescript-eslint/scope-manager": { "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", - "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", "dev": true, "license": "MIT", "dependencies": { @@ -36842,8 +29306,6 @@ }, "packages/expo-two-way-audio/node_modules/@typescript-eslint/types": { "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", - "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", "dev": true, "license": "MIT", "engines": { @@ -36856,8 +29318,6 @@ }, "packages/expo-two-way-audio/node_modules/@typescript-eslint/visitor-keys": { "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", - "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", "dev": true, "license": "MIT", "dependencies": { @@ -36874,8 +29334,6 @@ }, "packages/expo-two-way-audio/node_modules/@xmldom/xmldom": { "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", - "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", "dev": true, "license": "MIT", "engines": { @@ -36884,8 +29342,6 @@ }, "packages/expo-two-way-audio/node_modules/ansi-styles": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { @@ -36897,8 +29353,6 @@ }, "packages/expo-two-way-audio/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -36907,8 +29361,6 @@ }, "packages/expo-two-way-audio/node_modules/brace-expansion": { "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -36920,8 +29372,6 @@ }, "packages/expo-two-way-audio/node_modules/commander": { "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, "license": "MIT", "engines": { @@ -36930,8 +29380,6 @@ }, "packages/expo-two-way-audio/node_modules/eslint-config-prettier": { "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", - "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", "bin": { @@ -36943,8 +29391,6 @@ }, "packages/expo-two-way-audio/node_modules/eslint-config-universe": { "version": "15.2.0", - "resolved": "https://registry.npmjs.org/eslint-config-universe/-/eslint-config-universe-15.2.0.tgz", - "integrity": "sha512-n2662q/mM+2pTFVz7ELosqhN+/nbR75Ut/4vLme40kKSHHe0oPbPMxgPqyYrASlANuSDP4aAJ71rRviDMCZTxg==", "dev": true, "license": "MIT", "dependencies": { @@ -36971,8 +29417,6 @@ }, "packages/expo-two-way-audio/node_modules/eslint-config-universe/node_modules/@typescript-eslint/eslint-plugin": { "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", - "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -37000,8 +29444,6 @@ }, "packages/expo-two-way-audio/node_modules/eslint-config-universe/node_modules/@typescript-eslint/parser": { "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", - "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -37025,8 +29467,6 @@ }, "packages/expo-two-way-audio/node_modules/eslint-config-universe/node_modules/@typescript-eslint/project-service": { "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", - "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", "dev": true, "license": "MIT", "dependencies": { @@ -37047,8 +29487,6 @@ }, "packages/expo-two-way-audio/node_modules/eslint-config-universe/node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", - "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", "dev": true, "license": "MIT", "engines": { @@ -37064,8 +29502,6 @@ }, "packages/expo-two-way-audio/node_modules/eslint-config-universe/node_modules/@typescript-eslint/type-utils": { "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", - "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", "dev": true, "license": "MIT", "dependencies": { @@ -37089,8 +29525,6 @@ }, "packages/expo-two-way-audio/node_modules/eslint-config-universe/node_modules/@typescript-eslint/typescript-estree": { "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", - "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", "dev": true, "license": "MIT", "dependencies": { @@ -37117,8 +29551,6 @@ }, "packages/expo-two-way-audio/node_modules/eslint-config-universe/node_modules/@typescript-eslint/utils": { "version": "8.61.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", - "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", "dev": true, "license": "MIT", "dependencies": { @@ -37141,8 +29573,6 @@ }, "packages/expo-two-way-audio/node_modules/eslint-config-universe/node_modules/ts-api-utils": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -37154,8 +29584,6 @@ }, "packages/expo-two-way-audio/node_modules/eslint-plugin-react-hooks": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", - "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", "dependencies": { @@ -37174,8 +29602,6 @@ }, "packages/expo-two-way-audio/node_modules/eslint-visitor-keys": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -37187,8 +29613,6 @@ }, "packages/expo-two-way-audio/node_modules/expo-module-scripts": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/expo-module-scripts/-/expo-module-scripts-5.0.8.tgz", - "integrity": "sha512-3YkAic3E6QGQuNEIchq6/0GGSBwR2C70AhDUF7VI7/N3THwUpeqZrX20FxHKN95/wclHs1ljrGvRX+2YTgXxiw==", "dev": true, "license": "MIT", "dependencies": { @@ -37218,8 +29642,6 @@ }, "packages/expo-two-way-audio/node_modules/expo-module-scripts/node_modules/@testing-library/react-native": { "version": "13.3.3", - "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-13.3.3.tgz", - "integrity": "sha512-k6Mjsd9dbZgvY4Bl7P1NIpePQNi+dfYtlJ5voi9KQlynxSyQkfOgJmYGCYmw/aSgH/rUcFvG8u5gd4npzgRDyg==", "dev": true, "license": "MIT", "dependencies": { @@ -37245,8 +29667,6 @@ }, "packages/expo-two-way-audio/node_modules/expo-module-scripts/node_modules/jest-expo": { "version": "54.0.17", - "resolved": "https://registry.npmjs.org/jest-expo/-/jest-expo-54.0.17.tgz", - "integrity": "sha512-LyIhrsP4xvHEEcR1R024u/LBj3uPpAgB+UljgV+YXWkEHjprnr0KpE4tROsMNYCVTM1pPlAnPuoBmn5gnAN9KA==", "dev": true, "license": "MIT", "dependencies": { @@ -37281,8 +29701,6 @@ }, "packages/expo-two-way-audio/node_modules/expo-modules-core": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-2.5.0.tgz", - "integrity": "sha512-aIbQxZE2vdCKsolQUl6Q9Farlf8tjh/ROR4hfN1qT7QBGPl1XrJGnaOKkcgYaGrlzCPg/7IBe0Np67GzKMZKKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -37291,8 +29709,6 @@ }, "packages/expo-two-way-audio/node_modules/getenv": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", - "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", "dev": true, "license": "MIT", "engines": { @@ -37301,8 +29717,6 @@ }, "packages/expo-two-way-audio/node_modules/glob": { "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -37319,8 +29733,6 @@ }, "packages/expo-two-way-audio/node_modules/globals": { "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "dev": true, "license": "MIT", "engines": { @@ -37332,15 +29744,11 @@ }, "packages/expo-two-way-audio/node_modules/hermes-estree": { "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", "dev": true, "license": "MIT" }, "packages/expo-two-way-audio/node_modules/hermes-parser": { "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", "dev": true, "license": "MIT", "dependencies": { @@ -37349,8 +29757,6 @@ }, "packages/expo-two-way-audio/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -37359,8 +29765,6 @@ }, "packages/expo-two-way-audio/node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "license": "MIT", "engines": { @@ -37369,8 +29773,6 @@ }, "packages/expo-two-way-audio/node_modules/jest-diff": { "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", - "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", "dev": true, "license": "MIT", "dependencies": { @@ -37385,8 +29787,6 @@ }, "packages/expo-two-way-audio/node_modules/jest-matcher-utils": { "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", - "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", "dev": true, "license": "MIT", "dependencies": { @@ -37401,8 +29801,6 @@ }, "packages/expo-two-way-audio/node_modules/lru-cache": { "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -37411,8 +29809,6 @@ }, "packages/expo-two-way-audio/node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -37427,8 +29823,6 @@ }, "packages/expo-two-way-audio/node_modules/path-scurry": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -37444,8 +29838,6 @@ }, "packages/expo-two-way-audio/node_modules/pretty-format": { "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", "dependencies": { @@ -37460,8 +29852,6 @@ }, "packages/expo-two-way-audio/node_modules/react-test-renderer": { "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-19.1.0.tgz", - "integrity": "sha512-jXkSl3CpvPYEF+p/eGDLB4sPoDX8pKkYvRl9+rR8HxLY0X04vW7hCm1/0zHoUSjPZ3bDa+wXWNTDVIw/R8aDVw==", "dev": true, "license": "MIT", "dependencies": { @@ -37474,8 +29864,6 @@ }, "packages/expo-two-way-audio/node_modules/redent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dev": true, "license": "MIT", "dependencies": { @@ -37488,8 +29876,6 @@ }, "packages/expo-two-way-audio/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, "license": "MIT", "engines": { @@ -37498,8 +29884,6 @@ }, "packages/expo-two-way-audio/node_modules/strip-indent": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -37511,8 +29895,6 @@ }, "packages/expo-two-way-audio/node_modules/sucrase": { "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", "dev": true, "license": "MIT", "dependencies": { @@ -37534,8 +29916,6 @@ }, "packages/expo-two-way-audio/node_modules/sucrase/node_modules/commander": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true, "license": "MIT", "engines": { @@ -37544,8 +29924,6 @@ }, "packages/expo-two-way-audio/node_modules/xmlbuilder": { "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "dev": true, "license": "MIT", "engines": { @@ -37554,8 +29932,6 @@ }, "packages/expo-two-way-audio/node_modules/zod-validation-error": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", "dev": true, "license": "MIT", "engines": { @@ -37567,7 +29943,7 @@ }, "packages/highlight": { "name": "@getpaseo/highlight", - "version": "0.3.1", + "version": "0.4.0", "dependencies": { "@codemirror/language": "6.12.4", "@codemirror/legacy-modes": "^6.5.3", @@ -37596,8 +29972,6 @@ }, "packages/highlight/node_modules/@vitest/expect": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", - "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", "dev": true, "license": "MIT", "dependencies": { @@ -37614,8 +29988,6 @@ }, "packages/highlight/node_modules/@vitest/pretty-format": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", - "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", "dev": true, "license": "MIT", "dependencies": { @@ -37627,8 +29999,6 @@ }, "packages/highlight/node_modules/@vitest/runner": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", - "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -37641,8 +30011,6 @@ }, "packages/highlight/node_modules/@vitest/snapshot": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", - "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", "dev": true, "license": "MIT", "dependencies": { @@ -37657,8 +30025,6 @@ }, "packages/highlight/node_modules/@vitest/spy": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", - "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", "dev": true, "license": "MIT", "funding": { @@ -37667,8 +30033,6 @@ }, "packages/highlight/node_modules/@vitest/utils": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", - "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -37682,8 +30046,6 @@ }, "packages/highlight/node_modules/vitest": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", - "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -37772,8 +30134,6 @@ }, "packages/highlight/node_modules/vitest/node_modules/@vitest/mocker": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", - "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -37799,7 +30159,7 @@ }, "packages/protocol": { "name": "@getpaseo/protocol", - "version": "0.3.1", + "version": "0.4.0", "dependencies": { "zod": "^4.4.3" }, @@ -37812,7 +30172,7 @@ }, "packages/relay": { "name": "@getpaseo/relay", - "version": "0.3.1", + "version": "0.4.0", "dependencies": { "base64-js": "^1.5.1", "tweetnacl": "^1.0.3", @@ -37827,8 +30187,6 @@ }, "packages/relay/node_modules/@vitest/expect": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", - "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", "dev": true, "license": "MIT", "dependencies": { @@ -37845,8 +30203,6 @@ }, "packages/relay/node_modules/@vitest/pretty-format": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", - "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", "dev": true, "license": "MIT", "dependencies": { @@ -37858,8 +30214,6 @@ }, "packages/relay/node_modules/@vitest/runner": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", - "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -37872,8 +30226,6 @@ }, "packages/relay/node_modules/@vitest/snapshot": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", - "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", "dev": true, "license": "MIT", "dependencies": { @@ -37888,8 +30240,6 @@ }, "packages/relay/node_modules/@vitest/spy": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", - "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", "dev": true, "license": "MIT", "funding": { @@ -37898,8 +30248,6 @@ }, "packages/relay/node_modules/@vitest/utils": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", - "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -37913,8 +30261,6 @@ }, "packages/relay/node_modules/vitest": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", - "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -38003,8 +30349,6 @@ }, "packages/relay/node_modules/vitest/node_modules/@vitest/mocker": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", - "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -38030,17 +30374,17 @@ }, "packages/server": { "name": "@getpaseo/server", - "version": "0.3.1", + "version": "0.4.0", "dependencies": { "@agentclientprotocol/sdk": "^0.17.1", "@anthropic-ai/claude-agent-sdk": "^0.3.220", "@anthropic-ai/sdk": "^0.104.2", - "@getpaseo/client": "0.3.1", - "@getpaseo/highlight": "0.3.1", - "@getpaseo/protocol": "0.3.1", - "@getpaseo/relay": "0.3.1", - "@getpaseo/workspace-helper": "0.3.1", - "@getpaseo/workspace-runtime-contract": "0.3.1", + "@getpaseo/client": "0.4.0", + "@getpaseo/highlight": "0.4.0", + "@getpaseo/protocol": "0.4.0", + "@getpaseo/relay": "0.4.0", + "@getpaseo/workspace-helper": "0.4.0", + "@getpaseo/workspace-runtime-contract": "0.4.0", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "^1.20.1", "@opencode-ai/sdk": "1.14.46", @@ -38086,8 +30430,6 @@ }, "packages/server/node_modules/@anthropic-ai/claude-agent-sdk": { "version": "0.3.220", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.220.tgz", - "integrity": "sha512-glc7SdwPkOkLw8oxwLo9PKTdLJGqW/PIR4urWXFoRtX9YllwozsEVc5Tc1+EvLSkfrsxPJqQWqOgpjUOQXf1oA==", "license": "SEE LICENSE IN README.md", "engines": { "node": ">=18.0.0" @@ -38110,8 +30452,6 @@ }, "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": { "version": "0.3.220", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.220.tgz", - "integrity": "sha512-7VxlbEosK7DODiOnsjoVd0DSJzbnaPrM2jelMHI0y8zx1UnLS3WC6EFUXbvy74F2sXqEznh2tzn7EKWInaRN6Q==", "cpu": [ "arm64" ], @@ -38121,101 +30461,8 @@ "darwin" ] }, - "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": { - "version": "0.3.220", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.220.tgz", - "integrity": "sha512-X9RwDsSmbF6ultKZroaip+DL8WRgC64gHbrAwrRlAFSPNZV7zmJyP2ur8rW7KrxqmtuehdMMkw8+SAC/6hD2PA==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "darwin" - ] - }, - "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": { - "version": "0.3.220", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.220.tgz", - "integrity": "sha512-WkROPwWskqhKR9XgnmseHQ6rLi9zM9qt57IWoToIjL/eXOqDWipp7JXZ1L5ud+LrA42dunHPZfBwD/vXZ+A7LA==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ] - }, - "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": { - "version": "0.3.220", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.220.tgz", - "integrity": "sha512-OHoZOZ8Cf2TBr6oXIXPwyvUxj9jrq2w8E4poA8dMpacXszcPSPiCQCMuuOh4aWJzfeJE1+TtWxhKMVb2csXyZQ==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ] - }, - "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": { - "version": "0.3.220", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.220.tgz", - "integrity": "sha512-tkTJFnpR9VifvWX2fmkCAPkT6+8Wk/gVu8B5jsVekKZPiZoWRHmMXO30BnZn+f0TZhgYP+82PSX3S8crH1kn+w==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ] - }, - "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": { - "version": "0.3.220", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.220.tgz", - "integrity": "sha512-K+FWj+LcGhC1Z7wqeWoLxm1iemcba5xKpLLFVwYm4V6HyMx3ruYd/2r2TiQtjT+JWeNFWIys0ScHiItR6vWAiA==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ] - }, - "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": { - "version": "0.3.220", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.220.tgz", - "integrity": "sha512-rIwgq0UwQExWl6KrHUyC4w5KwpL9l6nd95aUTx6RitexaAuEw//xtfTVLnuE4hDDQZFkzEwpdKc3nxDWoGcUbA==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "win32" - ] - }, - "packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": { - "version": "0.3.220", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.220.tgz", - "integrity": "sha512-MuOuXhbr66HlGaWXD2f3w0k2PsvmnbkwcUZ0dAe2poFLdl72GC2dapwwOBefxm9QmoNqk9+jmv/dSKGOVWyvLw==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "win32" - ] - }, "packages/server/node_modules/@isaacs/ttlcache": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-2.1.4.tgz", - "integrity": "sha512-7kMz0BJpMvgAMkyglums7B2vtrn5g0a0am77JY0GjkZZNetOBCFn7AG7gKCwT0QPiXyxW7YIQSgtARknUEOcxQ==", "license": "BlueOak-1.0.0", "engines": { "node": ">=12" @@ -38223,8 +30470,6 @@ }, "packages/server/node_modules/@opencode-ai/sdk": { "version": "1.14.46", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.14.46.tgz", - "integrity": "sha512-7KOMuoCkNI+bLOw3GCg0nWZ5m7A/MzNsyLfTbZYmE/DIaUqkV2LNRULtrW6PHL1WtYVmJEFPws4dbw/4dVxjzA==", "license": "MIT", "dependencies": { "cross-spawn": "7.0.6" @@ -38232,8 +30477,6 @@ }, "packages/server/node_modules/@vitest/expect": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", - "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", "dev": true, "license": "MIT", "dependencies": { @@ -38250,8 +30493,6 @@ }, "packages/server/node_modules/@vitest/pretty-format": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", - "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", "dev": true, "license": "MIT", "dependencies": { @@ -38263,8 +30504,6 @@ }, "packages/server/node_modules/@vitest/runner": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", - "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -38277,8 +30516,6 @@ }, "packages/server/node_modules/@vitest/snapshot": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", - "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", "dev": true, "license": "MIT", "dependencies": { @@ -38293,8 +30530,6 @@ }, "packages/server/node_modules/@vitest/spy": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", - "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", "dev": true, "license": "MIT", "funding": { @@ -38303,8 +30538,6 @@ }, "packages/server/node_modules/@vitest/ui": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.6.tgz", - "integrity": "sha512-wiu5em68DfGv/2HFvI1Njr7JI2CHcBlQvereSzVG8my53PRxjTNOCsD9VOkRKrsJBDHmyuXvosxWZw7T91a2mw==", "dev": true, "license": "MIT", "dependencies": { @@ -38325,8 +30558,6 @@ }, "packages/server/node_modules/@vitest/utils": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", - "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -38340,8 +30571,6 @@ }, "packages/server/node_modules/ajv": { "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -38356,8 +30585,6 @@ }, "packages/server/node_modules/ansi-regex": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -38368,8 +30595,6 @@ }, "packages/server/node_modules/isexe": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -38377,8 +30602,6 @@ }, "packages/server/node_modules/lru-cache": { "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -38386,8 +30609,6 @@ }, "packages/server/node_modules/openai": { "version": "6.44.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.44.0.tgz", - "integrity": "sha512-09/gH+8jH0RgUwsgWHAaxsKGRT5zVZ95IaJUnqAWj6XejIBmnFRwq2WUIF37VtDEsmGrtPmvCs5+yBSeZGWvkA==", "license": "Apache-2.0", "peerDependencies": { "ws": "^8.18.0", @@ -38404,8 +30625,6 @@ }, "packages/server/node_modules/p-limit": { "version": "7.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", - "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", "license": "MIT", "dependencies": { "yocto-queue": "^1.2.1" @@ -38419,8 +30638,6 @@ }, "packages/server/node_modules/strip-ansi": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -38434,8 +30651,6 @@ }, "packages/server/node_modules/vitest": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", - "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -38524,8 +30739,6 @@ }, "packages/server/node_modules/vitest/node_modules/@vitest/mocker": { "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", - "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -38551,8 +30764,6 @@ }, "packages/server/node_modules/which": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -38566,8 +30777,6 @@ }, "packages/server/node_modules/yocto-queue": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", - "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", "license": "MIT", "engines": { "node": ">=12.20" @@ -38578,7 +30787,7 @@ }, "packages/website": { "name": "@getpaseo/website", - "version": "0.3.1", + "version": "0.4.0", "dependencies": { "@cloudflare/vite-plugin": "^1.29.1", "@cloudflare/workers-types": "^4.20260317.1", @@ -38612,8 +30821,6 @@ }, "packages/website/node_modules/@babel/code-frame": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -38624,26 +30831,8 @@ "node": ">=6.9.0" } }, - "packages/website/node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260625.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260625.1.tgz", - "integrity": "sha512-naCfBv0WnnTQIQPTniqMoUlklOIFjrAcSn1X+IAOhY8aFLF/xGYtFjs1eEE8sFib3ZuChGGpU23FFORVczqr0A==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, "packages/website/node_modules/@cloudflare/workerd-darwin-arm64": { "version": "1.20260625.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260625.1.tgz", - "integrity": "sha512-jmH6zjp6Wrux46+qtFwDwrj+vd7s5bdwEqeGvdnwE0a4IEeAhKs0L42HQOyID+g5lkrHq9m55+AbhtmRAm63Pw==", "cpu": [ "arm64" ], @@ -38656,128 +30845,12 @@ "node": ">=16" } }, - "packages/website/node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260625.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260625.1.tgz", - "integrity": "sha512-MiQkpA/dX8d83Zp64pzHUKfd6ca4cvwxnNobSP6CnXvfESvnNI9pfa+nfwnParla36sPmnYntNkjR7NjRuDeKQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "packages/website/node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260625.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260625.1.tgz", - "integrity": "sha512-LxxW7Qv60Xvv37+w6gUSDpYZziyqMy+cZWd9IvSA5ehVgKAxmzEaYPMiSZlxk32nbIWL9u/tfjXYCOKJ4Lo+XQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "packages/website/node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260625.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260625.1.tgz", - "integrity": "sha512-LH6iIX1HHaTwVKV5VokDxxUErXJzQoNZFRwVm7Vx/3fB/ApcTcRCUaMqcxI4as94jEUqg+pmX5czOndiveohow==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, "packages/website/node_modules/@cloudflare/workers-types": { "version": "4.20260627.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260627.1.tgz", - "integrity": "sha512-QhVvier1eW9Ydw96N/Ez79i5CSGy7h39JucrUejQmAWQw9qBdlB1aOTjaD93EGYrV0t9U81YWetI3LcaxxILNw==", "license": "MIT OR Apache-2.0" }, - "packages/website/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, "packages/website/node_modules/@esbuild/darwin-arm64": { "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -38790,346 +30863,8 @@ "node": ">=18" } }, - "packages/website/node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "packages/website/node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "packages/website/node_modules/@tanstack/history": { "version": "1.162.0", - "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.162.0.tgz", - "integrity": "sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==", "license": "MIT", "engines": { "node": ">=20.19" @@ -39141,8 +30876,6 @@ }, "packages/website/node_modules/@tanstack/react-router": { "version": "1.170.16", - "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.170.16.tgz", - "integrity": "sha512-w6eq1IJklujs1tESazaK/FxH0+H2l8vm/QPuu1cD3oRW/ubgKneQpd7b64ti/8gUyEimzimJQZDmJr6YHfP5+g==", "license": "MIT", "dependencies": { "@tanstack/history": "1.162.0", @@ -39164,8 +30897,6 @@ }, "packages/website/node_modules/@tanstack/react-start": { "version": "1.168.26", - "resolved": "https://registry.npmjs.org/@tanstack/react-start/-/react-start-1.168.26.tgz", - "integrity": "sha512-ZzNecqKWC0p2643/kIcFUdsMrXZ8A4dXm4Yfe9zV/Y0u14MtSkh/Sk76RQYIU5S84VyDyKhHo0Ffh8HQbLdvFw==", "license": "MIT", "dependencies": { "@tanstack/react-router": "1.170.16", @@ -39205,8 +30936,6 @@ }, "packages/website/node_modules/@tanstack/react-start-client": { "version": "1.168.14", - "resolved": "https://registry.npmjs.org/@tanstack/react-start-client/-/react-start-client-1.168.14.tgz", - "integrity": "sha512-oaz43fdOhBWfOPsdLkp3IejwYEXRyeaZ4CYexOPZx3eVXzm4LLozvNkkkBE9aje1sM5MVC2Yo6+cyv2HdVzkHQ==", "license": "MIT", "dependencies": { "@tanstack/react-router": "1.170.16", @@ -39227,8 +30956,6 @@ }, "packages/website/node_modules/@tanstack/react-start-server": { "version": "1.167.20", - "resolved": "https://registry.npmjs.org/@tanstack/react-start-server/-/react-start-server-1.167.20.tgz", - "integrity": "sha512-hg9xVI6yNDu+6NCalKz1h3Ea18HZEUu35i/ZMJz1WadwqcArLMp41nM1GNhOAtGIdpycrp9tT7ccqc9zuRMRpQ==", "license": "MIT", "dependencies": { "@tanstack/react-router": "1.170.16", @@ -39249,8 +30976,6 @@ }, "packages/website/node_modules/@tanstack/react-start/node_modules/@tanstack/react-start-rsc": { "version": "0.1.25", - "resolved": "https://registry.npmjs.org/@tanstack/react-start-rsc/-/react-start-rsc-0.1.25.tgz", - "integrity": "sha512-Rwm6cjcS148y2XAebr8jyrwU0SqsRSkW4/CuXesoHg+G3IOnVRHV0HOylJfnznUTVuH1nVhfQRPI5uWPJaw2TA==", "license": "MIT", "dependencies": { "@tanstack/react-router": "1.170.16", @@ -39291,8 +31016,6 @@ }, "packages/website/node_modules/@tanstack/react-start/node_modules/@tanstack/start-plugin-core": { "version": "1.171.18", - "resolved": "https://registry.npmjs.org/@tanstack/start-plugin-core/-/start-plugin-core-1.171.18.tgz", - "integrity": "sha512-weuOOjRD03BiKcF9NYKL5QADEQOp3yGHb0qIsOX42ENz5XUE4in2fXcVYHjSwwpzs+XGhWIFLp5J385pOVPuZQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "7.27.1", @@ -39338,8 +31061,6 @@ }, "packages/website/node_modules/@tanstack/react-start/node_modules/@tanstack/start-plugin-core/node_modules/@tanstack/router-plugin": { "version": "1.168.18", - "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.168.18.tgz", - "integrity": "sha512-MofS28/axfnfnhOD2RSgJEaU882aX5RsAzhGz5Vc4XhAmvCjy919u9JrNs4QsTWFbTD1P7IJ8WFlFVsrg0pStg==", "license": "MIT", "dependencies": { "@babel/core": "^7.28.5", @@ -39386,8 +31107,6 @@ }, "packages/website/node_modules/@tanstack/router-core": { "version": "1.171.13", - "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.171.13.tgz", - "integrity": "sha512-+NOwEj1kO/6IGmpHRIZHasYxYWpyBQGNIZAST9aNrk9Q3YlU9SgqVnl1pbLa9qAKfeNdXQIRve0RQb/0kyDeDA==", "license": "MIT", "dependencies": { "@tanstack/history": "1.162.0", @@ -39405,8 +31124,6 @@ }, "packages/website/node_modules/@tanstack/router-generator": { "version": "1.167.17", - "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.167.17.tgz", - "integrity": "sha512-xtB9tB2Ws0tWR6Pi7nc3Qk9IYgoh1mQCKWjHqIl9tf6BNUpKoqniJoPAQ4+LGrK8FeZYU0o0p/qlZEyj9FAulA==", "license": "MIT", "dependencies": { "@babel/types": "^7.28.5", @@ -39428,8 +31145,6 @@ }, "packages/website/node_modules/@tanstack/router-utils": { "version": "1.162.2", - "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.162.2.tgz", - "integrity": "sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ==", "license": "MIT", "dependencies": { "@babel/generator": "^7.28.5", @@ -39451,8 +31166,6 @@ }, "packages/website/node_modules/@tanstack/start-client-core": { "version": "1.170.12", - "resolved": "https://registry.npmjs.org/@tanstack/start-client-core/-/start-client-core-1.170.12.tgz", - "integrity": "sha512-gwtZRMPUIAxmDV2AIQUhC0kSW262SV7BkHXEgy5B1woHQdrdsELuGOdJwdweLxrjyefORxk+9MYGqDY0Cxn0bw==", "license": "MIT", "dependencies": { "@tanstack/router-core": "1.171.13", @@ -39470,8 +31183,6 @@ }, "packages/website/node_modules/@tanstack/start-fn-stubs": { "version": "1.162.0", - "resolved": "https://registry.npmjs.org/@tanstack/start-fn-stubs/-/start-fn-stubs-1.162.0.tgz", - "integrity": "sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ==", "license": "MIT", "engines": { "node": ">=22.12.0" @@ -39483,8 +31194,6 @@ }, "packages/website/node_modules/@tanstack/start-server-core": { "version": "1.169.15", - "resolved": "https://registry.npmjs.org/@tanstack/start-server-core/-/start-server-core-1.169.15.tgz", - "integrity": "sha512-V8ie2G2Ecb3Nklk8/QuKzulxb+tDUqgz6rjJe8Isdp4iKVXZu/TscItkUP/4Q5Bu7W4gUy3P25O6soC1oW1SXQ==", "license": "MIT", "dependencies": { "@tanstack/history": "1.162.0", @@ -39505,8 +31214,6 @@ }, "packages/website/node_modules/@tanstack/start-storage-context": { "version": "1.167.15", - "resolved": "https://registry.npmjs.org/@tanstack/start-storage-context/-/start-storage-context-1.167.15.tgz", - "integrity": "sha512-Jy0q4vdG6pv76N92+X+ag3fuOV2zINQagYyMN1/es7tPI1vzpKECIU8AqHqzI6ahkwaph7XDvmfUkiLJ3i4LOA==", "license": "MIT", "dependencies": { "@tanstack/router-core": "1.171.13" @@ -39521,8 +31228,6 @@ }, "packages/website/node_modules/@tanstack/virtual-file-routes": { "version": "1.162.0", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.162.0.tgz", - "integrity": "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==", "license": "MIT", "engines": { "node": ">=20.19" @@ -39534,8 +31239,6 @@ }, "packages/website/node_modules/@types/node": { "version": "22.19.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.6.tgz", - "integrity": "sha512-qm+G8HuG6hOHQigsi7VGuLjUVu6TtBo/F05zvX04Mw2uCg9Dv0Qxy3Qw7j41SidlTcl5D/5yg0SEZqOB+EqZnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -39544,8 +31247,6 @@ }, "packages/website/node_modules/@vitest/browser": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.10.tgz", - "integrity": "sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng==", "dev": true, "license": "MIT", "optional": true, @@ -39569,8 +31270,6 @@ }, "packages/website/node_modules/@vitest/browser-playwright": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.10.tgz", - "integrity": "sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==", "dev": true, "license": "MIT", "optional": true, @@ -39595,8 +31294,6 @@ }, "packages/website/node_modules/@vitest/browser-playwright/node_modules/@vitest/mocker": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "optional": true, @@ -39624,8 +31321,6 @@ }, "packages/website/node_modules/@vitest/browser/node_modules/@vitest/mocker": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "optional": true, @@ -39653,8 +31348,6 @@ }, "packages/website/node_modules/@vitest/expect": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { @@ -39671,8 +31364,6 @@ }, "packages/website/node_modules/@vitest/pretty-format": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -39684,8 +31375,6 @@ }, "packages/website/node_modules/@vitest/runner": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", "dependencies": { @@ -39698,8 +31387,6 @@ }, "packages/website/node_modules/@vitest/snapshot": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { @@ -39714,8 +31401,6 @@ }, "packages/website/node_modules/@vitest/spy": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", "funding": { @@ -39724,8 +31409,6 @@ }, "packages/website/node_modules/@vitest/utils": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", "dependencies": { @@ -39739,8 +31422,6 @@ }, "packages/website/node_modules/chokidar": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "license": "MIT", "dependencies": { "readdirp": "^5.0.0" @@ -39754,8 +31435,6 @@ }, "packages/website/node_modules/esbuild": { "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -39795,8 +31474,6 @@ }, "packages/website/node_modules/lightningcss": { "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -39824,8 +31501,6 @@ }, "packages/website/node_modules/lightningcss-darwin-arm64": { "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -39842,190 +31517,8 @@ "url": "https://opencollective.com/parcel" } }, - "packages/website/node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "packages/website/node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "packages/website/node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "packages/website/node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "packages/website/node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "packages/website/node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "packages/website/node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "packages/website/node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "packages/website/node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "packages/website/node_modules/miniflare": { "version": "4.20260625.0", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260625.0.tgz", - "integrity": "sha512-3kKXwRUObJsnBYPBgR0NiNZYKF/yv8GFyha1cx2EeAEraxNODgRVcyeRo+F1ok1tg5Mg7iUpOWSkknQTHuFhwA==", "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", @@ -40044,14 +31537,10 @@ }, "packages/website/node_modules/path-to-regexp": { "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "license": "MIT" }, "packages/website/node_modules/react": { "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", - "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -40059,8 +31548,6 @@ }, "packages/website/node_modules/react-dom": { "version": "19.2.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", - "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" @@ -40071,8 +31558,6 @@ }, "packages/website/node_modules/readdirp": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", "license": "MIT", "engines": { "node": ">= 20.19.0" @@ -40084,14 +31569,10 @@ }, "packages/website/node_modules/scheduler": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, "packages/website/node_modules/undici": { "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -40099,8 +31580,6 @@ }, "packages/website/node_modules/vitest": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", "dev": true, "license": "MIT", "dependencies": { @@ -40189,8 +31668,6 @@ }, "packages/website/node_modules/vitest/node_modules/@vitest/mocker": { "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, "license": "MIT", "dependencies": { @@ -40216,8 +31693,6 @@ }, "packages/website/node_modules/workerd": { "version": "1.20260625.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260625.1.tgz", - "integrity": "sha512-GApQvFX52SDM6L4u0+RRnUDB1wJOnEwoXjinkmOPtIyofWBxrlZckdegJSYc1leg++lLZ3+DQ4zMVmBqYVtzfA==", "hasInstallScript": true, "license": "Apache-2.0", "bin": { @@ -40236,8 +31711,6 @@ }, "packages/website/node_modules/wrangler": { "version": "4.105.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.105.0.tgz", - "integrity": "sha512-7dXFH6OLj1Fv0y6ZeRPUxFTkp+duWD7/xxVi/1c0vfOeEYwIFKWB7cdqnY05DvY1Ta3BnqAwRkXfLs8PDj538g==", "license": "MIT OR Apache-2.0", "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", @@ -40271,8 +31744,6 @@ }, "packages/website/node_modules/ws": { "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -40292,7 +31763,7 @@ }, "packages/workspace-helper": { "name": "@getpaseo/workspace-helper", - "version": "0.3.1", + "version": "0.4.0", "dependencies": { "zod": "^4.4.3" }, @@ -40307,7 +31778,7 @@ }, "packages/workspace-runtime-contract": { "name": "@getpaseo/workspace-runtime-contract", - "version": "0.3.1", + "version": "0.4.0", "dependencies": { "zod": "^4.4.3" }, @@ -40317,10 +31788,10 @@ }, "runtimes/fixture": { "name": "@getpaseo/fixture-workspace-runtime", - "version": "0.3.1", + "version": "0.4.0", "dependencies": { - "@getpaseo/workspace-helper": "0.3.1", - "@getpaseo/workspace-runtime-contract": "0.3.1", + "@getpaseo/workspace-helper": "0.4.0", + "@getpaseo/workspace-runtime-contract": "0.4.0", "node-pty": "1.2.0-beta.15" }, "bin": { diff --git a/package.json b/package.json index 645bbdabf6..ff7e882506 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paseo", - "version": "0.3.1", + "version": "0.4.0", "private": true, "description": "Paseo: voice-controlled development environment for local AI coding agents", "keywords": [ diff --git a/packages/app/app.config.js b/packages/app/app.config.js index b2f2a53d86..1e13997956 100644 --- a/packages/app/app.config.js +++ b/packages/app/app.config.js @@ -1,8 +1,10 @@ const fs = require("node:fs"); const path = require("node:path"); const pkg = require("./package.json"); +const withAndroidAsyncStorageSize = require("./plugins/with-android-async-storage-size"); const withAndroidProfileable = require("./plugins/with-android-profileable"); const withFdroidAutolinking = require("./plugins/with-fdroid-autolinking"); +const withPasteInput = require("./plugins/with-paste-input"); const { getNativeReleaseVersion } = require("./native-release-version"); const appVariant = process.env.APP_VARIANT ?? "production"; const isFdroidBuild = process.env.PASEO_FDROID_BUILD === "1"; @@ -138,6 +140,8 @@ export default { }, plugins: [ "expo-router", + withPasteInput, + [withAndroidAsyncStorageSize, 64], ...buildProfile.cameraPlugins, [ "expo-splash-screen", diff --git a/packages/app/e2e/browser/agent-profile-modeless-preferences.spec.ts b/packages/app/e2e/browser/agent-profile-modeless-preferences.spec.ts new file mode 100644 index 0000000000..980eaea5d0 --- /dev/null +++ b/packages/app/e2e/browser/agent-profile-modeless-preferences.spec.ts @@ -0,0 +1,225 @@ +import { expect, test, type Page } from "../support/fixtures"; +import path from "node:path"; +import type { FormPreferences } from "@/create-agent-preferences/preferences"; +import { + applyProfileFromPicker, + drillIntoProvider, + openModelPicker, + seedAgentProfiles, + seedModelProvider, +} from "../support/helpers/agent-profiles"; +import { gotoAppShell } from "../support/helpers/app"; +import { daemonWsRoutePattern } from "../support/helpers/daemon-port"; +import { + openGlobalNewWorkspaceComposer, + selectNewWorkspaceProject, + submitNewWorkspacePrompt, +} from "../support/helpers/new-workspace"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; + +const CREATE_AGENT_PREFERENCES_KEY = "@paseo:create-agent-preferences"; +const MODELESS_PROVIDER = "modeless-profile-e2e"; +const MODELESS_MODEL = "pi-profile-model"; + +type WebSocketMessage = string | Buffer; + +interface CreateAgentRequestMessage { + type: "create_agent_request"; + config?: { + provider?: unknown; + modeId?: unknown; + featureValues?: unknown; + }; +} + +function getSessionMessage(message: WebSocketMessage): Record | null { + const rawMessage = typeof message === "string" ? message : message.toString("utf8"); + try { + const envelope = JSON.parse(rawMessage) as { type?: unknown; message?: unknown }; + return envelope.type === "session" && envelope.message && typeof envelope.message === "object" + ? (envelope.message as Record) + : null; + } catch { + return null; + } +} + +async function recordAndBlockCreateAgentRequest(page: Page): Promise<{ + waitForRequest(): Promise; +}> { + let resolveRequest: ((message: CreateAgentRequestMessage) => void) | null = null; + const request = new Promise((resolve) => { + resolveRequest = resolve; + }); + + await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { + const server = ws.connectToServer(); + ws.onMessage((message) => { + const sessionMessage = getSessionMessage(message); + if (sessionMessage?.type === "create_agent_request") { + resolveRequest?.(sessionMessage as unknown as CreateAgentRequestMessage); + return; + } + server.send(message); + }); + server.onMessage((message) => ws.send(message)); + }); + + return { waitForRequest: () => request }; +} + +async function seedPoisonedModelessPreference(page: Page): Promise { + await page.addInitScript( + ({ preferencesKey, provider, model }) => { + localStorage.setItem( + preferencesKey, + JSON.stringify({ + provider, + providerPreferences: { + [provider]: { model, mode: "full-access" }, + mock: { model: "ten-second-stream", mode: "load-test" }, + }, + } satisfies FormPreferences), + ); + }, + { + preferencesKey: CREATE_AGENT_PREFERENCES_KEY, + provider: MODELESS_PROVIDER, + model: MODELESS_MODEL, + }, + ); +} + +async function readProviderModePreference(page: Page, provider: string): Promise { + return page.evaluate( + ({ preferencesKey, providerId }) => { + const raw = localStorage.getItem(preferencesKey); + if (!raw) return null; + const preferences = JSON.parse(raw) as FormPreferences; + return preferences.providerPreferences?.[providerId]?.mode ?? null; + }, + { preferencesKey: CREATE_AGENT_PREFERENCES_KEY, providerId: provider }, + ); +} + +test.describe("Agent profiles repair modeless provider preferences", () => { + test.describe.configure({ timeout: 240_000 }); + + test("applying a profile heals a stale mode before returning to Pi", async ({ page }) => { + const workspace = await seedWorkspace({ repoPrefix: "profile-modeless-preferences-" }); + const profiles = await seedAgentProfiles([ + { + id: "agent_profile_modeless_repair", + name: "Approval work", + provider: "mock", + model: "one-minute-stream", + modeId: "approval-test", + }, + ]); + const provider = await seedModelProvider({ + id: MODELESS_PROVIDER, + label: "Pi profile test", + extends: "pi", + command: [process.execPath, path.resolve("e2e/fixtures/fake-pi-rpc.mjs")], + models: [ + { + id: MODELESS_MODEL, + label: "Pi profile model", + description: "Modeless profile regression model", + }, + ], + }); + await seedPoisonedModelessPreference(page); + const createAgentRecorder = await recordAndBlockCreateAgentRequest(page); + + try { + await gotoAppShell(page); + await waitForSidebarHydration(page); + await openGlobalNewWorkspaceComposer(page); + await selectNewWorkspaceProject(page, { + projectKey: workspace.projectKey, + projectDisplayName: workspace.projectDisplayName, + }); + + await openModelPicker(page); + await applyProfileFromPicker(page, "Approval work"); + + await expect + .poll(() => readProviderModePreference(page, MODELESS_PROVIDER), { timeout: 10_000 }) + .toBeNull(); + await expect + .poll(() => readProviderModePreference(page, "mock"), { timeout: 10_000 }) + .toBe("approval-test"); + + await openModelPicker(page); + await drillIntoProvider(page, MODELESS_PROVIDER); + await page.getByRole("button", { name: /Pi profile model/ }).click(); + + await submitNewWorkspacePrompt(page, "Create a Pi agent after repairing preferences."); + const createAgentRequest = await createAgentRecorder.waitForRequest(); + expect(createAgentRequest).toMatchObject({ + config: { provider: MODELESS_PROVIDER }, + }); + expect(createAgentRequest.config).not.toHaveProperty("modeId"); + } finally { + await workspace.cleanup(); + await provider.restore(); + await profiles.restore(); + } + }); + + test("cross-provider profile features survive immediate workspace creation", async ({ page }) => { + const workspace = await seedWorkspace({ repoPrefix: "profile-feature-transition-" }); + const featureProviderId = "profile-feature-e2e"; + const featureModelId = "claude-opus-5"; + const profiles = await seedAgentProfiles([ + { + id: "agent_profile_feature_transition", + name: "Fast profile", + provider: featureProviderId, + model: featureModelId, + modeId: "bypassPermissions", + featureValues: { fast_mode: true }, + }, + ]); + const provider = await seedModelProvider({ + id: featureProviderId, + label: "Profile feature test", + models: [ + { + id: featureModelId, + label: "Profile feature model", + description: "Cross-provider feature regression model", + }, + ], + }); + const createAgentRecorder = await recordAndBlockCreateAgentRequest(page); + + try { + await gotoAppShell(page); + await waitForSidebarHydration(page); + await openGlobalNewWorkspaceComposer(page); + await selectNewWorkspaceProject(page, { + projectKey: workspace.projectKey, + projectDisplayName: workspace.projectDisplayName, + }); + + await openModelPicker(page); + await applyProfileFromPicker(page, "Fast profile"); + await submitNewWorkspacePrompt(page, "Create an agent with profile features."); + + const createAgentRequest = await createAgentRecorder.waitForRequest(); + expect(createAgentRequest).toMatchObject({ + config: { + provider: featureProviderId, + featureValues: { fast_mode: true }, + }, + }); + } finally { + await workspace.cleanup(); + await provider.restore(); + await profiles.restore(); + } + }); +}); diff --git a/packages/app/e2e/browser/agent-profiles-picker.spec.ts b/packages/app/e2e/browser/agent-profiles-picker.spec.ts new file mode 100644 index 0000000000..d67de4da36 --- /dev/null +++ b/packages/app/e2e/browser/agent-profiles-picker.spec.ts @@ -0,0 +1,118 @@ +import { expect, test } from "../support/fixtures"; +import { + applyProfileFromPicker, + closeModelPicker, + expectComposerDoesNotName, + expectAgentProfilesEmptyPrompt, + expectProfileEditTooltip, + expectComposerMode, + expectComposerModel, + expectModelRowSelected, + expectProfileEditIsPencilOnly, + expectProfileVisibleForProvider, + openModelPicker, + openAgentProfilesFromEmptyPrompt, + seedAgentProfiles, +} from "../support/helpers/agent-profiles"; +import { expectWorkspaceAgentConfiguration } from "../support/helpers/command-center-agent-controls"; +import { expectComposerVisible } from "../support/helpers/composer"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; + +const PROFILE = { + id: "agent_profile_e2e_ui_work", + name: "UI work", + icon: "🎨", + provider: "mock", + model: "one-minute-stream", + modeId: "approval-test", + notes: "Use for UI work.", +}; + +const PROFILE_SUMMARY = "Mock Load Test · One minute stream · Approval test"; + +test.describe("Agent profiles in the model picker", () => { + test("an empty host still exposes agent profile settings from the picker", async ({ page }) => { + const seed = await seedAgentProfiles([]); + const workspace = await seedMockAgentWorkspace({ + repoPrefix: "agent-profiles-empty-", + title: "Agent profiles empty", + }); + + try { + await openAgentRoute(page, workspace); + await expectComposerVisible(page); + await openModelPicker(page); + await expectAgentProfilesEmptyPrompt(page); + await openAgentProfilesFromEmptyPrompt(page); + } finally { + await workspace.cleanup(); + await seed.restore(); + } + }); + + test("applying a pinned profile materializes it into the composer and is then forgotten", async ({ + page, + }) => { + const seed = await seedAgentProfiles([PROFILE]); + // A live agent is one provider's process, so the profile has to name that + // same provider or the picker will not offer it at all. + const workspace = await seedMockAgentWorkspace({ + repoPrefix: "agent-profiles-picker-", + title: "Agent profiles picker", + model: "ten-second-stream", + modeId: "load-test", + }); + + try { + await test.step("the agent starts on its seeded model and mode", async () => { + await openAgentRoute(page, workspace); + await expectComposerVisible(page); + await expectComposerModel(page, "Ten second stream"); + await expectComposerMode(page, "Load test"); + }); + + await test.step("the sole provider opens directly", async () => { + await openModelPicker(page); + await expect(page.getByTestId("model-search-input").first()).toBeVisible(); + await expect(page.getByTestId("sheet-header-back")).toHaveCount(0); + await expect(page.locator('[data-testid^="model-provider-"]')).toHaveCount(0); + await expectProfileVisibleForProvider(page, { + name: PROFILE.name, + summary: PROFILE_SUMMARY, + }); + await expectProfileEditIsPencilOnly(page); + await expectProfileEditTooltip(page); + }); + + await test.step("applying it writes its model and mode into the composer", async () => { + await applyProfileFromPicker(page, PROFILE.name); + await expectComposerModel(page, "One minute stream"); + await expectComposerMode(page, "Approval test"); + await expectWorkspaceAgentConfiguration(workspace, { + id: workspace.agentId, + provider: "mock", + model: "one-minute-stream", + modeId: "approval-test", + }); + }); + + await test.step("the composer names the model, never the profile", async () => { + await expectComposerDoesNotName(page, PROFILE.name); + }); + + await test.step("reopening returns directly to the provider models", async () => { + await openModelPicker(page); + await expect(page.getByTestId("model-search-input").first()).toBeVisible(); + await expectProfileVisibleForProvider(page, { + name: PROFILE.name, + summary: PROFILE_SUMMARY, + }); + await expectModelRowSelected(page, { provider: "mock", modelId: "one-minute-stream" }); + await closeModelPicker(page); + }); + } finally { + await workspace.cleanup(); + await seed.restore(); + } + }); +}); diff --git a/packages/app/e2e/browser/agent-profiles-settings.spec.ts b/packages/app/e2e/browser/agent-profiles-settings.spec.ts new file mode 100644 index 0000000000..8aa2f3864d --- /dev/null +++ b/packages/app/e2e/browser/agent-profiles-settings.spec.ts @@ -0,0 +1,128 @@ +import { test } from "../support/fixtures"; +import { + createAgentProfile, + createAgentProfileFromEmptyState, + editAgentProfile, + expectAgentProfile, + expectAgentProfileForm, + expectAgentProfileOrder, + expectAgentProfileTagsGone, + expectHostAgentProfiles, + expectNoAgentProfiles, + moveAgentProfileUp, + openAgentProfileSettings, + removeAgentProfile, + seedAgentProfiles, + stageLegacyFavoritesForHostMigration, +} from "../support/helpers/agent-profiles"; + +const MOCK_PROVIDER_LABEL = "Mock Load Test"; + +test.describe("Agent profiles settings", () => { + test("legacy model favourites migrate into provider-and-model-only host profiles", async ({ + page, + }) => { + const seed = await seedAgentProfiles([]); + const migratedProfile = { + id: "legacy_favorite:mock:one-minute-stream", + name: "One minute stream", + provider: "mock", + model: "one-minute-stream", + }; + + try { + await stageLegacyFavoritesForHostMigration(page, [ + { provider: "mock", modelId: "one-minute-stream" }, + ]); + + await expectHostAgentProfiles([migratedProfile]); + await openAgentProfileSettings(page); + await expectAgentProfile(page, { + name: migratedProfile.name, + tags: [MOCK_PROVIDER_LABEL, "One minute stream"], + }); + } finally { + await seed.restore(); + } + }); + + test("host owner creates, edits, reorders and removes agent profiles", async ({ page }) => { + // The journey owns the list, so it starts from an empty one whatever the + // worker ran before, and hands it back untouched. + const seed = await seedAgentProfiles([]); + + try { + await test.step("the section starts empty", async () => { + await openAgentProfileSettings(page); + await expectNoAgentProfiles(page); + }); + + await test.step("create a profile from the modal", async () => { + await createAgentProfileFromEmptyState(page, { + name: "UI work", + provider: MOCK_PROVIDER_LABEL, + model: "Ten second stream", + thinking: "High", + mode: "Approval test", + notes: "Use for UI work — components, layout and design tokens.", + }); + await expectAgentProfile(page, { + name: "UI work", + tags: [MOCK_PROVIDER_LABEL, "Ten second stream", "Approval test", "High"], + notes: "Use for UI work — components, layout and design tokens.", + }); + }); + + await test.step("editing to a model without thinking levels drops the level", async () => { + await editAgentProfile(page, "UI work", { + model: "One minute stream", + notes: "Now for quick checks.", + }); + await expectAgentProfile(page, { + name: "UI work", + tags: [MOCK_PROVIDER_LABEL, "One minute stream", "Approval test"], + notes: "Now for quick checks.", + }); + await expectAgentProfileTagsGone(page, { + name: "UI work", + tags: ["Ten second stream", "High"], + }); + }); + + await test.step("the edit form reopens on what was stored", async () => { + await expectAgentProfileForm(page, "UI work", { + provider: MOCK_PROVIDER_LABEL, + model: "One minute stream", + mode: "Approval test", + notes: "Now for quick checks.", + }); + }); + + await test.step("a second profile lands below the first", async () => { + await createAgentProfile(page, { + name: "Deep review", + provider: MOCK_PROVIDER_LABEL, + model: "Five minute stream", + }); + await expectAgentProfileOrder(page, ["UI work", "Deep review"]); + }); + + await test.step("reorder puts the second profile on top", async () => { + await moveAgentProfileUp(page, "Deep review"); + await expectAgentProfileOrder(page, ["Deep review", "UI work"]); + }); + + await test.step("removing a profile confirms by name", async () => { + await removeAgentProfile(page, "UI work"); + await expectAgentProfileOrder(page, ["Deep review"]); + }); + + await test.step("removing the last profile returns the empty state", async () => { + await removeAgentProfile(page, "Deep review"); + await expectNoAgentProfiles(page); + }); + } finally { + await seed.restore(); + } + }); +}); diff --git a/packages/app/e2e/browser/agent-timeline-pagination-old-daemon.spec.ts b/packages/app/e2e/browser/agent-timeline-pagination-old-daemon.spec.ts index 16e2c7eaba..6ec68a2b1e 100644 --- a/packages/app/e2e/browser/agent-timeline-pagination-old-daemon.spec.ts +++ b/packages/app/e2e/browser/agent-timeline-pagination-old-daemon.spec.ts @@ -66,7 +66,7 @@ test("does not repeat an assistant block when the current app paginates a publis localStorage.setItem("@paseo:daemon-registry", JSON.stringify([seededHost])); localStorage.setItem("@paseo:create-agent-preferences", JSON.stringify(preferences)); }, - { seededHost: host, preferences: buildCreateAgentPreferences(serverId) }, + { seededHost: host, preferences: buildCreateAgentPreferences() }, ); const history = await holdOlderHistoryPages(page, agent, daemon.port); diff --git a/packages/app/e2e/browser/appearance-theme-picker.spec.ts b/packages/app/e2e/browser/appearance-theme-picker.spec.ts index 818cc52baa..0d654f3194 100644 --- a/packages/app/e2e/browser/appearance-theme-picker.spec.ts +++ b/packages/app/e2e/browser/appearance-theme-picker.spec.ts @@ -1,4 +1,7 @@ import { expect, test } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { seedWorkspace } from "../support/helpers/seed-client"; +import { getServerId } from "../support/helpers/server-id"; import { openSettingsSection } from "../support/helpers/settings"; test("shows Pure black in the appearance picker", async ({ page }, testInfo) => { @@ -15,6 +18,33 @@ test("shows Pure black in the appearance picker", async ({ page }, testInfo) => }); }); +test("keeps the selected workspace visible in Pure black", async ({ page }, testInfo) => { + const workspace = await seedWorkspace({ + repoPrefix: "pure-black-selected-workspace-", + title: "Selected workspace", + }); + + try { + await page.addInitScript(() => { + localStorage.setItem("@paseo:app-settings", JSON.stringify({ theme: "pureBlack" })); + }); + await gotoAppShell(page); + + const row = page.getByTestId(`sidebar-workspace-row-${getServerId()}:${workspace.workspaceId}`); + await expect(row).toBeVisible({ timeout: 30_000 }); + await row.click(); + + await expect(row).toHaveAttribute("aria-selected", "true"); + await expect(row).toHaveCSS("background-color", "rgb(22, 22, 22)"); + await page.screenshot({ + path: testInfo.outputPath("pure-black-selected-workspace.png"), + fullPage: true, + }); + } finally { + await workspace.cleanup(); + } +}); + test("applies the interface font size to settings text", async ({ page }) => { await page.addInitScript(() => { localStorage.setItem("@paseo:app-settings", JSON.stringify({ uiFontSize: 24 })); diff --git a/packages/app/e2e/browser/changes-pane.spec.ts b/packages/app/e2e/browser/changes-pane.spec.ts index 174cea357e..87f41af01a 100644 --- a/packages/app/e2e/browser/changes-pane.spec.ts +++ b/packages/app/e2e/browser/changes-pane.spec.ts @@ -1,8 +1,10 @@ -import { unlink, writeFile } from "node:fs/promises"; +import { execFileSync } from "node:child_process"; +import { readFile, unlink, writeFile } from "node:fs/promises"; import path from "node:path"; import { type Page } from "@playwright/test"; import { buildHostWorkspaceRoute, buildSettingsSectionRoute } from "../../src/utils/host-routes"; import { test, expect } from "../support/fixtures"; +import { daemonWsRoutePattern } from "../support/helpers/daemon-port"; import { getServerId } from "../support/helpers/server-id"; import { connectSeedClient } from "../support/helpers/seed-client"; import { createTempGitRepo } from "../support/helpers/workspace"; @@ -15,6 +17,9 @@ interface DirtyWorkspace { interface WorkspaceFixtureOptions { includeDeletedFile?: boolean; + includeNestedFolders?: boolean; + includeRenamedFile?: boolean; + includeUntrackedFile?: boolean; } interface CleanupTask { @@ -23,6 +28,39 @@ interface CleanupTask { const cleanupTasks: CleanupTask[] = []; const APP_SETTINGS_KEY = "@paseo:app-settings"; + +async function failNextDiscardRequest(page: Page): Promise { + await page.routeWebSocket(daemonWsRoutePattern(), (browserSocket) => { + const serverSocket = browserSocket.connectToServer(); + browserSocket.onMessage((message) => { + if (typeof message === "string") { + const envelope = JSON.parse(message) as { + message?: { type?: string; cwd?: string; requestId?: string }; + }; + if (envelope.message?.type === "checkout.discard_changes.request") { + browserSocket.send( + JSON.stringify({ + type: "session", + message: { + type: "checkout.discard_changes.response", + payload: { + cwd: envelope.message.cwd, + success: false, + error: { code: "UNKNOWN", message: "Injected revert failure" }, + requestId: envelope.message.requestId, + }, + }, + }), + ); + return; + } + } + serverSocket.send(message); + }); + serverSocket.onMessage((message) => browserSocket.send(message)); + }); +} + const CHANGES_PREFERENCES_KEY = "@paseo:changes-preferences"; const BEFORE = `import { useLayoutEffect, useMemo, useRef, useState } from "react"; @@ -209,9 +247,15 @@ test("changes file actions open below the right-click without a reserved kebab", await openWorkspaceChanges(page, workspace); await expect(page.getByTestId("diff-file-1")).toContainText("zz-deleted.ts"); + const deletedFileName = page.getByText("zz-deleted.ts", { exact: true }); + await expect(deletedFileName).toHaveCSS("user-select", "none"); + await deletedFileName.dblclick(); + expect(await page.evaluate(() => window.getSelection()?.toString() ?? "")).toBe(""); await expect(page.getByTestId(/diff-file-\d+-actions/)).toHaveCount(0); await page.getByTestId("diff-file-1-toggle").click({ button: "right" }); await expect(page.getByText("Copy path")).toBeVisible(); + await page.getByText("Copy path", { exact: true }).click({ button: "right" }); + await expect(page.getByText("Copy path")).toBeVisible(); await expect(page.getByTestId("diff-file-1-open-file")).toHaveCount(0); await page.keyboard.press("Escape"); @@ -230,6 +274,183 @@ test("changes file actions open below the right-click without a reserved kebab", await expect(page.getByTestId("workspace-tab-file_src/use-mounted-tab-set.ts")).toBeVisible(); }); +test("changes context menus duplicate files and folders", async ({ page }) => { + const workspace = await createWorkspaceWithMountedTabDiff(); + await useUnwrappedDiffLines(page); + await openWorkspaceChanges(page, workspace); + + await page.getByTestId("diff-file-0-toggle").click({ button: "right" }); + await page.getByTestId("diff-file-0-duplicate").click(); + await expect + .poll(() => readFile(path.join(workspace.repoPath, "src/use-mounted-tab-set copy.ts"), "utf8")) + .toBe(AFTER); + + await page.getByTestId("changes-toggle-view-mode").click(); + await page.getByTestId("diff-folder-src-toggle").click({ button: "right" }); + await page.getByTestId("diff-folder-src-duplicate").click(); + await expect + .poll(() => readFile(path.join(workspace.repoPath, "src copy/use-mounted-tab-set.ts"), "utf8")) + .toBe(AFTER); +}); + +test("changes context menu recursively collapses descendant folders", async ({ page }) => { + const workspace = await createWorkspaceWithMountedTabDiff({ includeNestedFolders: true }); + await useUnwrappedDiffLines(page); + await openWorkspaceChanges(page, workspace); + + await page.getByTestId("changes-toggle-view-mode").click(); + await expect(page.getByTestId("diff-folder-src/zz-folder")).toBeVisible(); + await expect(page.getByTestId("diff-folder-src/zz-folder/nested")).toBeVisible(); + + await page.getByTestId("diff-folder-src-toggle").click({ button: "right" }); + await page.getByTestId("diff-folder-src-collapse-folder").click(); + await expect(page.getByTestId("diff-folder-src/zz-folder")).toHaveCount(0); + + await page.getByTestId("diff-folder-src-toggle").click(); + await expect(page.getByTestId("diff-folder-src/zz-folder")).toBeVisible(); + await expect(page.getByText("root.ts", { exact: true })).toHaveCount(0); + + await page.getByTestId("diff-folder-src/zz-folder-toggle").click(); + await expect(page.getByText("root.ts", { exact: true })).toBeVisible(); + await expect(page.getByTestId("diff-folder-src/zz-folder/nested")).toBeVisible(); + await expect(page.getByText("changed.ts", { exact: true })).toHaveCount(0); + + await page.getByTestId("diff-folder-src/zz-folder/nested-toggle").click(); + await expect(page.getByText("changed.ts", { exact: true })).toBeVisible(); +}); + +test("changes context menus expose folder revert and restore a file after confirmation", async ({ + page, +}) => { + const workspace = await createWorkspaceWithMountedTabDiff(); + await useUnwrappedDiffLines(page); + await openWorkspaceChanges(page, workspace); + + await page.getByTestId("changes-toggle-view-mode").click(); + await page.getByTestId("diff-folder-src-toggle").click({ button: "right" }); + const folderRevert = page.getByTestId("diff-folder-src-revert"); + await expect(folderRevert).toBeVisible(); + const revertLabelColor = await folderRevert + .getByText("Discard changes", { exact: true }) + .evaluate((element) => getComputedStyle(element).color); + await expect(folderRevert.locator("svg")).toHaveCSS("stroke", revertLabelColor); + await page.keyboard.press("Escape"); + + await page.getByTestId("diff-file-0-toggle").click({ button: "right" }); + const cancelledConfirmation = new Promise((resolve) => { + page.once("dialog", async (dialog) => { + const message = dialog.message(); + await dialog.dismiss(); + resolve(message); + }); + }); + await page.getByTestId("diff-file-0-revert").click(); + expect(await cancelledConfirmation).toContain("src/use-mounted-tab-set.ts"); + await expect(page.getByTestId("diff-file-0")).toBeVisible(); + await expect + .poll(() => readFile(path.join(workspace.repoPath, "src/use-mounted-tab-set.ts"), "utf8")) + .toBe(AFTER); + + await page.getByTestId("diff-file-0-toggle").click({ button: "right" }); + const confirmation = new Promise((resolve) => { + page.once("dialog", async (dialog) => { + const message = dialog.message(); + await dialog.accept(); + resolve(message); + }); + }); + await page.getByTestId("diff-file-0-revert").click(); + expect(await confirmation).toContain("src/use-mounted-tab-set.ts"); + + await expect(page.getByTestId("diff-file-0")).toHaveCount(0, { timeout: 30_000 }); + await expect + .poll(() => readFile(path.join(workspace.repoPath, "src/use-mounted-tab-set.ts"), "utf8")) + .toBe(BEFORE); +}); + +test("discarding a staged rename restores its source path", async ({ page }) => { + const workspace = await createWorkspaceWithMountedTabDiff({ includeRenamedFile: true }); + await useUnwrappedDiffLines(page); + await openWorkspaceChanges(page, workspace); + + const renamedToggle = page + .getByTestId(/^diff-file-\d+-toggle$/) + .filter({ hasText: "zz-renamed.ts" }); + const toggleTestId = await renamedToggle.getAttribute("data-testid"); + expect(toggleTestId).not.toBeNull(); + const rowTestId = toggleTestId!.slice(0, -"-toggle".length); + await renamedToggle.click({ button: "right" }); + const confirmation = new Promise((resolve) => { + page.once("dialog", async (dialog) => { + await dialog.accept(); + resolve(); + }); + }); + await page.getByTestId(`${rowTestId}-revert`).click(); + await confirmation; + + await expect(page.getByText("zz-renamed.ts", { exact: true })).toHaveCount(0, { + timeout: 30_000, + }); + await expect + .poll(() => readFile(path.join(workspace.repoPath, "src/rename-source.ts"), "utf8")) + .toBe("export const renamed = true;\n"); +}); + +test("discarding an untracked file removes it from the working tree", async ({ page }) => { + const workspace = await createWorkspaceWithMountedTabDiff({ includeUntrackedFile: true }); + await useUnwrappedDiffLines(page); + await openWorkspaceChanges(page, workspace); + + const untrackedToggle = page + .getByTestId(/^diff-file-\d+-toggle$/) + .filter({ hasText: "zz-untracked.txt" }); + const toggleTestId = await untrackedToggle.getAttribute("data-testid"); + expect(toggleTestId).not.toBeNull(); + const rowTestId = toggleTestId!.slice(0, -"-toggle".length); + await untrackedToggle.click({ button: "right" }); + const confirmation = new Promise((resolve) => { + page.once("dialog", async (dialog) => { + await dialog.accept(); + resolve(); + }); + }); + await page.getByTestId(`${rowTestId}-revert`).click(); + await confirmation; + + await expect(page.getByText("zz-untracked.txt", { exact: true })).toHaveCount(0, { + timeout: 30_000, + }); + await expect( + readFile(path.join(workspace.repoPath, "zz-untracked.txt"), "utf8"), + ).rejects.toThrow(); +}); + +test("shows a revert error returned by the daemon", async ({ page }) => { + const workspace = await createWorkspaceWithMountedTabDiff(); + await failNextDiscardRequest(page); + await useUnwrappedDiffLines(page); + await openWorkspaceChanges(page, workspace); + + await page.getByTestId("diff-file-0-toggle").click({ button: "right" }); + const confirmation = new Promise((resolve) => { + page.once("dialog", async (dialog) => { + await dialog.accept(); + resolve(); + }); + }); + await page.getByTestId("diff-file-0-revert").click(); + await confirmation; + + await expect(page.getByText("Injected revert failure", { exact: true })).toBeVisible({ + timeout: 30_000, + }); + await expect(page.getByTestId("diff-file-0")).toBeVisible(); + await expect + .poll(() => readFile(path.join(workspace.repoPath, "src/use-mounted-tab-set.ts"), "utf8")) + .toBe(AFTER); +}); + test("Changes switches between inline and full-tab navigation", async ({ page }) => { const workspace = await createWorkspaceWithMountedTabDiff({ includeDeletedFile: true }); await useUnwrappedDiffLines(page); @@ -327,8 +548,48 @@ test("changes diff switches between flat and tree file lists", async ({ page }) await scrollToLowerUnwrappedDiffRows(page); await page.getByTestId("changes-toggle-view-mode").click(); await expect(page.getByTestId("diff-folder-src")).toBeVisible(); + await expect(page.getByTestId("diff-folder-src").getByText("src", { exact: true })).toHaveCSS( + "user-select", + "none", + ); await expect(page.getByTestId("diff-file-0")).toBeVisible(); - await page.getByTestId("diff-file-0-toggle").click({ button: "right" }); + await expect(page.getByTestId("diff-folder-src-toggle").locator("svg")).toHaveCount(1); + await expect(page.getByTestId("diff-file-0-toggle").locator("svg")).toHaveCount(1); + const folderToggleBounds = await page.getByTestId("diff-folder-src-toggle").boundingBox(); + const folderChevronBounds = await page + .getByTestId("diff-folder-src-toggle") + .locator("svg") + .boundingBox(); + expect(folderToggleBounds).not.toBeNull(); + expect(folderChevronBounds).not.toBeNull(); + expect(folderChevronBounds!.y + folderChevronBounds!.height / 2).toBeCloseTo( + folderToggleBounds!.y + folderToggleBounds!.height / 2, + 0, + ); + const folderLabelBounds = await page + .getByTestId("diff-folder-src") + .getByText("src", { exact: true }) + .boundingBox(); + const fileLabelBounds = await page + .getByTestId("diff-file-0") + .getByText("use-mounted-tab-set.ts", { exact: true }) + .boundingBox(); + expect(folderLabelBounds).not.toBeNull(); + expect(fileLabelBounds).not.toBeNull(); + expect(fileLabelBounds!.x - folderLabelBounds!.x).toBeCloseTo(16, 0); + + const folderToggle = page.getByTestId("diff-folder-src-toggle"); + await folderToggle.click(); + await expect(folderToggle).toHaveAttribute("aria-selected", "true"); + await expect(page.getByTestId("diff-file-0")).toHaveCount(0); + await folderToggle.click(); + await expect(folderToggle).toHaveAttribute("aria-selected", "true"); + await expect(page.getByTestId("diff-file-0")).toBeVisible(); + + const fileToggle = page.getByTestId("diff-file-0-toggle"); + await fileToggle.click({ button: "right" }); + await expect(fileToggle).toHaveAttribute("aria-selected", "true"); + await expect(folderToggle).toHaveAttribute("aria-selected", "false"); await expect(page.getByTestId("diff-file-0-context-menu")).toBeVisible(); await page.keyboard.press("Escape"); @@ -488,6 +749,15 @@ async function createWorkspaceWithMountedTabDiff( if (options.includeDeletedFile) { files.push({ path: "src/zz-deleted.ts", content: "export const deleted = true;\n" }); } + if (options.includeRenamedFile) { + files.push({ path: "src/rename-source.ts", content: "export const renamed = true;\n" }); + } + if (options.includeNestedFolders) { + files.push( + { path: "src/zz-folder/root.ts", content: "export const root = 1;\n" }, + { path: "src/zz-folder/nested/changed.ts", content: "export const nested = 1;\n" }, + ); + } const repo = await createTempGitRepo("changes-pane-", { files }); const client = await connectSeedClient(); cleanupTasks.push({ @@ -498,9 +768,24 @@ async function createWorkspaceWithMountedTabDiff( }); await writeFile(path.join(repo.path, "src/use-mounted-tab-set.ts"), AFTER); + if (options.includeUntrackedFile) { + await writeFile(path.join(repo.path, "zz-untracked.txt"), "remove me\n"); + } if (options.includeDeletedFile) { await unlink(path.join(repo.path, "src/zz-deleted.ts")); } + if (options.includeRenamedFile) { + execFileSync("git", ["mv", "src/rename-source.ts", "src/zz-renamed.ts"], { + cwd: repo.path, + }); + } + if (options.includeNestedFolders) { + await writeFile(path.join(repo.path, "src/zz-folder/root.ts"), "export const root = 2;\n"); + await writeFile( + path.join(repo.path, "src/zz-folder/nested/changed.ts"), + "export const nested = 2;\n", + ); + } const createdWorkspace = await client.createWorkspace({ source: { kind: "directory", path: repo.path }, }); diff --git a/packages/app/e2e/browser/command-center-agent-controls.spec.ts b/packages/app/e2e/browser/command-center-agent-controls.spec.ts index 3021a242ed..08597285fc 100644 --- a/packages/app/e2e/browser/command-center-agent-controls.spec.ts +++ b/packages/app/e2e/browser/command-center-agent-controls.spec.ts @@ -1,4 +1,5 @@ import { test } from "../support/fixtures"; +import type { FormPreferences } from "@/create-agent-preferences/preferences"; import { closeCommandCenter, openCommandCenter } from "../support/helpers/command-center"; import { applyCommandCenterAgentControls, @@ -12,17 +13,15 @@ import { import { clickNewChat, gotoWorkspace } from "../support/helpers/launcher"; import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; import { seedWorkspace } from "../support/helpers/seed-client"; -import { getServerId } from "../support/helpers/server-id"; const CREATE_AGENT_PREFERENCES_KEY = "@paseo:create-agent-preferences"; async function seedMockDraftPreferences(page: import("@playwright/test").Page): Promise { await page.addInitScript( - ({ preferencesKey, serverId }) => { + ({ preferencesKey }) => { localStorage.setItem( preferencesKey, JSON.stringify({ - serverId, provider: "mock", providerPreferences: { mock: { @@ -30,10 +29,10 @@ async function seedMockDraftPreferences(page: import("@playwright/test").Page): mode: "load-test", }, }, - }), + } satisfies FormPreferences), ); }, - { preferencesKey: CREATE_AGENT_PREFERENCES_KEY, serverId: getServerId() }, + { preferencesKey: CREATE_AGENT_PREFERENCES_KEY }, ); } diff --git a/packages/app/e2e/browser/command-center-file-search.spec.ts b/packages/app/e2e/browser/command-center-file-search.spec.ts new file mode 100644 index 0000000000..5964dcc978 --- /dev/null +++ b/packages/app/e2e/browser/command-center-file-search.spec.ts @@ -0,0 +1,139 @@ +import { expect, test } from "../support/fixtures"; +import { openCommandCenter } from "../support/helpers/command-center"; +import { + delayDirectorySuggestionResponses, + expectOneLineTruncatedFileResult, + expectStableCommandCenterLayout, + failDirectorySuggestionRequests, + startCommandCenterLayoutObservation, +} from "../support/helpers/command-center-file-search"; +import { expectFileTabOpen } from "../support/helpers/file-explorer"; +import { gotoWorkspace } from "../support/helpers/launcher"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; + +const FILE_PATH = + "packages/app/src/command-center/features/workspace-file-search/layout-stability/needle-layout-stability-command-center.tsx"; +const FILE_NAME = "needle-layout-stability-command-center.tsx"; +const FILE_DIRECTORY = + "packages/app/src/command-center/features/workspace-file-search/layout-stability"; + +function paseoSizedFiles(): Array<{ path: string; content: string }> { + const files = Array.from({ length: 120 }, (_, index) => ({ + path: `packages/app/src/features/feature-${String(index).padStart(3, "0")}/index.ts`, + content: `export const feature${index} = ${index};\n`, + })); + files.push({ path: FILE_PATH, content: "export const layoutNeedle = true;\n" }); + return files; +} + +async function seedMatchingWorkspaces(count: number): Promise { + return Promise.all( + Array.from({ length: count }, (_, index) => + seedWorkspace({ + repoPrefix: `command-center-file-search-error-${String(index).padStart(2, "0")}-`, + title: `Home failure ${String(index).padStart(2, "0")}`, + }), + ), + ); +} + +test.use({ + userAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/145.0 Safari/537.36", +}); + +test("workspace file search stays geometrically stable through delayed loading and results", async ({ + page, +}) => { + test.setTimeout(120_000); + const seeded = await seedWorkspace({ + repoPrefix: "command-center-file-search-", + title: "Paseo-shaped file search", + repo: { files: paseoSizedFiles() }, + }); + + try { + await delayDirectorySuggestionResponses(page, 800); + await gotoWorkspace(page, seeded.workspaceId); + await page.keyboard.press("Meta+P"); + + const panel = page.getByTestId("command-center-panel"); + await expect(panel).toBeVisible({ timeout: 30_000 }); + await expect(panel.getByRole("button", { name: "Files" })).toBeVisible(); + await startCommandCenterLayoutObservation(page); + + await panel.getByTestId("command-center-input").fill(FILE_NAME); + const row = panel.getByRole("button", { + name: `${FILE_NAME} ${FILE_DIRECTORY}`, + }); + await expect(row).toBeVisible({ timeout: 30_000 }); + await expectOneLineTruncatedFileResult(row, { + filename: FILE_NAME, + path: FILE_DIRECTORY, + }); + + await panel.getByTestId("command-center-input").fill("needle-layout"); + await page.waitForTimeout(150); + expect(await row.isVisible()).toBe(true); + await expect(panel.getByTestId("command-center-file-search-loading")).toBeVisible(); + await expect(panel.getByText("Searching files...", { exact: true })).toHaveCount(0); + await expect(panel.getByTestId("command-center-file-search-loading")).toBeHidden(); + await expectStableCommandCenterLayout(page); + + await row.click(); + await expectFileTabOpen(page, FILE_PATH); + } finally { + await seeded.cleanup(); + } +}); + +test("dropping the files scope leaves the search row the same height", async ({ page }) => { + const seeded = await seedWorkspace({ + repoPrefix: "command-center-file-search-scope-", + title: "Scope chip height", + }); + + try { + await gotoWorkspace(page, seeded.workspaceId); + await page.keyboard.press("Meta+P"); + + const panel = page.getByTestId("command-center-panel"); + await expect(panel).toBeVisible({ timeout: 30_000 }); + const header = panel.getByTestId("command-center-header"); + const scoped = await header.boundingBox(); + + await page.getByTestId("command-center-files-scope").click(); + await expect(page.getByTestId("command-center-files-scope")).toHaveCount(0); + const unscoped = await header.boundingBox(); + + expect(scoped?.height).toBe(unscoped?.height); + } finally { + await seeded.cleanup(); + } +}); + +test("unscoped command search keeps commands visible and reports file-search failures", async ({ + page, +}) => { + test.setTimeout(120_000); + const seeded = await seedMatchingWorkspaces(9); + + try { + await failDirectorySuggestionRequests(page); + await gotoWorkspace(page, seeded[0].workspaceId); + const panel = await openCommandCenter(page); + await panel.getByTestId("command-center-input").fill("home"); + + await expect(panel.getByRole("button", { name: "Home", exact: true })).toBeVisible(); + await expect(panel.getByRole("button", { name: /Home failure 08/ })).toBeVisible(); + await expect(panel.getByTestId("command-center-file-search-error")).toHaveText( + "Error: Test file search transport failure.", + ); + await expect(panel.getByTestId("command-center-file-search-error")).toBeInViewport({ + ratio: 1, + }); + await expect(panel.getByText("No matches", { exact: true })).toHaveCount(0); + } finally { + await Promise.allSettled(seeded.map((workspace) => workspace.cleanup())); + } +}); diff --git a/packages/app/e2e/browser/command-center-grouping.spec.ts b/packages/app/e2e/browser/command-center-grouping.spec.ts new file mode 100644 index 0000000000..8bf78177e2 --- /dev/null +++ b/packages/app/e2e/browser/command-center-grouping.spec.ts @@ -0,0 +1,58 @@ +import { expect, test, type Page } from "../support/fixtures"; +import { gotoAppShell } from "../support/helpers/app"; +import { openCommandCenter } from "../support/helpers/command-center"; +import { seedWorkspace } from "../support/helpers/seed-client"; + +const GROUP_BY_STATUS = "Group by status"; +const GROUP_BY_PROJECT = "Group by project"; + +// Result rows carry no testID of their own, so the entries are addressed by their visible label. +async function runGroupingEntry(page: Page, label: string, absent: string): Promise { + const panel = await openCommandCenter(page); + await panel.getByTestId("command-center-input").fill("group"); + + const entry = panel.getByText(label, { exact: true }); + await expect(entry).toBeVisible({ timeout: 30_000 }); + // The entry always names the mode you are not in, so its opposite must never be listed. + await expect(panel.getByText(absent, { exact: true })).toHaveCount(0); + + await entry.click(); + await expect(page.getByTestId("command-center-panel")).not.toBeVisible({ timeout: 30_000 }); +} + +test.describe("Command center sidebar grouping", () => { + test.describe.configure({ timeout: 120_000 }); + + test("flips sidebar grouping and persists the choice across a reload", async ({ page }) => { + const seeded = await seedWorkspace({ repoPrefix: "command-center-grouping-" }); + + try { + await gotoAppShell(page); + const projectList = page.getByTestId("sidebar-project-workspace-list-scroll"); + const statusList = page.getByTestId("sidebar-status-list-scroll"); + await expect(projectList).toBeVisible({ timeout: 30_000 }); + + // Query-only: the grouping entry stays out of the default empty-query list. + const panel = await openCommandCenter(page); + await expect(panel.getByText(GROUP_BY_STATUS, { exact: true })).toHaveCount(0); + await expect(panel.getByText(GROUP_BY_PROJECT, { exact: true })).toHaveCount(0); + await page.keyboard.press("Escape"); + await expect(panel).not.toBeVisible({ timeout: 30_000 }); + + await runGroupingEntry(page, GROUP_BY_STATUS, GROUP_BY_PROJECT); + await expect(statusList).toBeVisible({ timeout: 30_000 }); + await expect(projectList).toHaveCount(0); + + // The only assertion that catches a flip which never reached persisted storage. + await page.reload(); + await expect(statusList).toBeVisible({ timeout: 30_000 }); + + // Grouping persists, so the run must leave the sidebar back in project mode. + await runGroupingEntry(page, GROUP_BY_PROJECT, GROUP_BY_STATUS); + await expect(projectList).toBeVisible({ timeout: 30_000 }); + await expect(statusList).toHaveCount(0); + } finally { + await seeded.cleanup().catch(() => undefined); + } + }); +}); diff --git a/packages/app/e2e/browser/composer-attachments.spec.ts b/packages/app/e2e/browser/composer-attachments.spec.ts index 26a16f5179..acf1941c23 100644 --- a/packages/app/e2e/browser/composer-attachments.spec.ts +++ b/packages/app/e2e/browser/composer-attachments.spec.ts @@ -37,6 +37,7 @@ import { import { seedWorkspace } from "../support/helpers/seed-client"; import { hasGithubAuth, createTempGithubRepo } from "../support/helpers/github-fixtures"; import { getServerId } from "../support/helpers/server-id"; +import { openFileExplorer } from "../support/helpers/file-explorer"; const MINIMAL_PNG = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", @@ -294,6 +295,28 @@ test.describe("Composer attachments", () => { } }); + test("Escape cancels file creation without interrupting the running agent", async ({ page }) => { + test.setTimeout(120_000); + const agent = await startRunningMockAgent(page, { + prefix: "file-create-escape-", + model: "one-minute-stream", + prompt: "Stay running while a file draft is cancelled.", + }); + try { + await openFileExplorer(page); + await page.getByTestId("files-new-file").click(); + const nameInput = page.getByTestId("file-explorer-name-input"); + await expect(nameInput).toBeVisible(); + + await nameInput.press("Escape"); + + await expect(nameInput).toBeHidden(); + await expect(page.getByRole("button", { name: /stop|cancel/i }).first()).toBeVisible(); + } finally { + await agent.cleanup(); + } + }); + test("composer is locked while new workspace agent is being created", async ({ page }) => { test.setTimeout(120_000); const serverId = getServerId(); diff --git a/packages/app/e2e/browser/file-explorer-context-actions.spec.ts b/packages/app/e2e/browser/file-explorer-context-actions.spec.ts new file mode 100644 index 0000000000..d8622a6165 --- /dev/null +++ b/packages/app/e2e/browser/file-explorer-context-actions.spec.ts @@ -0,0 +1,445 @@ +import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { expect, type Page } from "@playwright/test"; +import { test } from "../support/fixtures"; +import { openFileExplorer } from "../support/helpers/file-explorer"; +import { gotoWorkspace } from "../support/helpers/launcher"; +import { daemonWsRoutePattern } from "../support/helpers/daemon-port"; +import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; + +let workspace: SeededWorkspace; + +async function failNextDeleteRequest(page: Page): Promise { + await page.routeWebSocket(daemonWsRoutePattern(), (browserSocket) => { + const serverSocket = browserSocket.connectToServer(); + browserSocket.onMessage((message) => { + if (typeof message === "string") { + const envelope = JSON.parse(message) as { + message?: { type?: string; cwd?: string; path?: string; requestId?: string }; + }; + if (envelope.message?.type === "fs.entry.delete.request") { + browserSocket.send( + JSON.stringify({ + type: "session", + message: { + type: "fs.entry.delete.response", + payload: { + cwd: envelope.message.cwd, + path: envelope.message.path, + success: false, + error: "Injected delete failure", + requestId: envelope.message.requestId, + }, + }, + }), + ); + return; + } + } + serverSocket.send(message); + }); + serverSocket.onMessage((message) => browserSocket.send(message)); + }); +} + +async function hideContextActionCapabilities(page: Page): Promise { + await page.routeWebSocket(daemonWsRoutePattern(), (browserSocket) => { + const serverSocket = browserSocket.connectToServer(); + browserSocket.onMessage((message) => serverSocket.send(message)); + serverSocket.onMessage((message) => { + if (typeof message !== "string") { + browserSocket.send(message); + return; + } + const envelope = JSON.parse(message) as { + message?: { + type?: string; + payload?: { status?: string; features?: Record }; + }; + }; + if ( + envelope.message?.type === "status" && + envelope.message.payload?.status === "server_info" + ) { + envelope.message.payload.features = { + ...envelope.message.payload.features, + fsEntryOps: false, + fsEntryDuplicate: false, + checkoutDiscardChanges: false, + }; + } + browserSocket.send(JSON.stringify(envelope)); + }); + }); +} + +async function disconnectNextContextAction(page: Page): Promise<{ wait: () => Promise }> { + let resolveRequest: (() => void) | undefined; + const requestSeen = new Promise((resolve) => { + resolveRequest = resolve; + }); + + await page.routeWebSocket(daemonWsRoutePattern(), (browserSocket) => { + const serverSocket = browserSocket.connectToServer(); + browserSocket.onMessage((message) => { + if (typeof message === "string") { + const envelope = JSON.parse(message) as { message?: { type?: string } }; + const type = envelope.message?.type; + if (type?.startsWith("fs.entry.") || type === "checkout.discard_changes.request") { + resolveRequest?.(); + void browserSocket.close({ + code: 1008, + reason: "Context action connection failed", + }); + return; + } + } + serverSocket.send(message); + }); + serverSocket.onMessage((message) => browserSocket.send(message)); + }); + + return { wait: () => requestSeen }; +} + +test.beforeEach(async () => { + workspace = await seedWorkspace({ repoPrefix: "file-explorer-context-actions-" }); +}); + +test.afterEach(async () => { + await workspace?.cleanup(); +}); + +test("creates, renames, copies, and deletes entries through the file explorer", async ({ + context, + page, +}) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + await gotoWorkspace(page, workspace.workspaceId); + await openFileExplorer(page); + + const tree = page.getByTestId("file-explorer-tree-scroll"); + const entry = (name: string) => tree.getByText(name, { exact: true }).first(); + const nameInput = page.getByTestId("file-explorer-name-input"); + + await page.getByTestId("files-new-folder").click(); + await expect(nameInput).toBeVisible(); + const draftRow = nameInput.locator("xpath=.."); + const placeholderColor = await nameInput.evaluate( + (input) => getComputedStyle(input, "::placeholder").color, + ); + const extraMutedChevronColor = await draftRow + .locator("svg") + .evaluate((icon) => getComputedStyle(icon).stroke); + expect(placeholderColor).toBe(extraMutedChevronColor); + await nameInput.press("Tab"); + await expect(nameInput).toBeHidden(); + + await page.getByTestId("files-new-file").click(); + await nameInput.fill("README.md"); + await nameInput.press("Enter"); + await expect(page.getByText('"README.md" already exists', { exact: true })).toBeVisible(); + + const emptyArea = page.getByTestId("files-empty-area"); + await expect(emptyArea).toBeVisible(); + const emptyAreaBounds = await emptyArea.boundingBox(); + expect(emptyAreaBounds).not.toBeNull(); + await emptyArea.click({ + button: "right", + position: { x: emptyAreaBounds!.width / 2, y: emptyAreaBounds!.height - 24 }, + }); + await expect(page.getByTestId("files-empty-area-new-file")).toBeVisible(); + await page.getByTestId("files-empty-area-new-folder").click(); + await nameInput.fill("folder"); + await nameInput.press("Enter"); + await expect(entry("folder")).toBeVisible(); + + await entry("folder").click({ button: "right" }); + await page.getByText("New file", { exact: true }).click(); + await nameInput.fill("child.txt"); + await nameInput.press("Enter"); + await expect(entry("child.txt")).toBeVisible(); + + await entry("folder").click({ button: "right" }); + await page.getByText("New folder", { exact: true }).click(); + await nameInput.fill("nested"); + await nameInput.press("Enter"); + await entry("nested").click({ button: "right" }); + await page.getByText("New folder", { exact: true }).click(); + await nameInput.fill("deep"); + await nameInput.press("Enter"); + await expect(entry("deep")).toBeVisible(); + + await entry("folder").click({ button: "right" }); + await page.getByText("Collapse folder", { exact: true }).click(); + await expect(entry("child.txt")).toBeHidden(); + await expect(entry("nested")).toBeHidden(); + await expect(entry("deep")).toBeHidden(); + + await entry("folder").click(); + await expect(entry("child.txt")).toBeVisible(); + await expect(entry("nested")).toBeVisible(); + await expect(entry("deep")).toBeHidden(); + await entry("nested").click(); + await expect(entry("deep")).toBeVisible(); + + await page.getByTestId("files-new-file").click(); + await nameInput.fill("created.txt"); + await nameInput.press("Enter"); + await expect(entry("created.txt")).toBeVisible(); + await expect(entry("created.txt")).toHaveCSS("user-select", "none"); + await entry("created.txt").dblclick(); + expect(await page.evaluate(() => window.getSelection()?.toString() ?? "")).toBe(""); + + const folderRow = entry("folder").locator( + "xpath=ancestor::*[starts-with(@data-testid, 'file-explorer-row-')][1]", + ); + const fileRow = entry("created.txt").locator( + "xpath=ancestor::*[starts-with(@data-testid, 'file-explorer-row-')][1]", + ); + await expect(folderRow.locator("svg")).toHaveCount(1); + await expect(fileRow.locator("svg")).toHaveCount(1); + const gitRow = entry(".git").locator( + "xpath=ancestor::*[starts-with(@data-testid, 'file-explorer-row-')][1]", + ); + const collapsedChevronBounds = await gitRow.locator("svg").first().boundingBox(); + const expandedChevronBounds = await folderRow.locator("svg").first().boundingBox(); + const collapsedChevronSlotBounds = await gitRow + .locator("svg") + .first() + .locator("xpath=../..") + .boundingBox(); + const expandedChevronSlotBounds = await folderRow + .locator("svg") + .first() + .locator("xpath=../..") + .boundingBox(); + expect(collapsedChevronBounds).not.toBeNull(); + expect(expandedChevronBounds).not.toBeNull(); + expect(collapsedChevronSlotBounds).not.toBeNull(); + expect(expandedChevronSlotBounds).not.toBeNull(); + const collapsedChevronCenter = collapsedChevronBounds!.x + collapsedChevronBounds!.width / 2; + const expandedChevronCenter = expandedChevronBounds!.x + expandedChevronBounds!.width / 2; + expect(collapsedChevronCenter).toBeCloseTo(expandedChevronCenter, 0); + expect(collapsedChevronCenter).toBeCloseTo( + collapsedChevronSlotBounds!.x + collapsedChevronSlotBounds!.width / 2, + 0, + ); + expect(expandedChevronCenter).toBeCloseTo( + expandedChevronSlotBounds!.x + expandedChevronSlotBounds!.width / 2, + 0, + ); + const folderLabelBounds = await entry("folder").boundingBox(); + const fileLabelBounds = await entry("created.txt").boundingBox(); + expect(folderLabelBounds).not.toBeNull(); + expect(fileLabelBounds).not.toBeNull(); + expect(fileLabelBounds!.x).toBeCloseTo(folderLabelBounds!.x, 0); + + await folderRow.click(); + await expect(folderRow).toHaveAttribute("aria-selected", "true"); + await expect(entry("child.txt")).toBeHidden(); + await folderRow.click(); + await expect(folderRow).toHaveAttribute("aria-selected", "true"); + await expect(entry("child.txt")).toBeVisible(); + + await entry("folder").click({ button: "right" }); + await page.getByText("Rename", { exact: true }).click(); + await nameInput.fill("renamed-folder"); + await nameInput.press("Tab"); + await expect(entry("renamed-folder")).toBeVisible(); + await expect(entry("folder")).toBeHidden(); + await expect(entry("child.txt")).toBeVisible(); + const renamedFolderRow = entry("renamed-folder").locator( + "xpath=ancestor::*[starts-with(@data-testid, 'file-explorer-row-')][1]", + ); + await expect(renamedFolderRow).toHaveAttribute("aria-selected", "true"); + + await entry("created.txt").click({ button: "right" }); + await expect(fileRow).toHaveAttribute("aria-selected", "true"); + await expect(renamedFolderRow).toHaveAttribute("aria-selected", "false"); + const menu = page.getByTestId(/file-explorer-row-\d+-context-menu$/); + await expect(menu).toBeVisible(); + + await page.getByText("Copy path", { exact: true }).click({ button: "right" }); + await expect(menu).toBeVisible(); + + const backdrop = page.getByTestId(/file-explorer-row-\d+-context-menu-backdrop$/); + await backdrop.click({ button: "right" }); + await expect(menu).toBeHidden(); + + await entry("created.txt").click({ button: "right" }); + await page.getByText("Rename", { exact: true }).click(); + await expect(nameInput).toHaveValue("created.txt"); + await nameInput.press("Escape"); + await expect(entry("created.txt")).toBeVisible(); + + await entry("created.txt").click({ button: "right" }); + await page.getByText("Rename", { exact: true }).click(); + await nameInput.fill("README.md"); + await nameInput.press("Enter"); + await expect(page.getByText('"README.md" already exists', { exact: true })).toBeVisible(); + await expect(entry("created.txt")).toBeVisible(); + + await entry("created.txt").click({ button: "right" }); + await page.getByText("Rename", { exact: true }).click(); + await nameInput.fill("renamed.txt"); + await nameInput.press("Tab"); + await expect(entry("renamed.txt")).toBeVisible(); + await expect(entry("created.txt")).toBeHidden(); + + await entry("renamed.txt").click({ button: "right" }); + await page.getByText("Copy relative path", { exact: true }).click(); + await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe("renamed.txt"); + + await entry("renamed.txt").click({ button: "right" }); + const deleteAction = page.getByTestId(/file-explorer-row-\d+-delete$/); + const deleteLabelColor = await deleteAction + .getByText("Delete", { exact: true }) + .evaluate((element) => getComputedStyle(element).color); + await expect(deleteAction.locator("svg")).toHaveCSS("stroke", deleteLabelColor); + const cancelledConfirmation = new Promise((resolve) => { + page.once("dialog", async (dialog) => { + const message = dialog.message(); + await dialog.dismiss(); + resolve(message); + }); + }); + await page.getByText("Delete", { exact: true }).click(); + expect(await cancelledConfirmation).toContain("renamed.txt"); + await expect(entry("renamed.txt")).toBeVisible(); + + await entry("renamed.txt").click({ button: "right" }); + const confirmation = new Promise((resolve) => { + page.once("dialog", async (dialog) => { + const message = dialog.message(); + await dialog.accept(); + resolve(message); + }); + }); + await page.getByText("Delete", { exact: true }).click(); + expect(await confirmation).toContain("renamed.txt"); + await expect(entry("renamed.txt")).toBeHidden(); + + await entry("renamed-folder").click({ button: "right" }); + const folderConfirmation = new Promise((resolve) => { + page.once("dialog", async (dialog) => { + const message = dialog.message(); + await dialog.accept(); + resolve(message); + }); + }); + await page.getByText("Delete", { exact: true }).click(); + expect(await folderConfirmation).toContain("renamed-folder"); + await expect(entry("renamed-folder")).toBeHidden(); + await expect(entry("child.txt")).toBeHidden(); +}); + +test("duplicates files and folders with collision-free names", async ({ page }) => { + const sourceFilePath = path.join(workspace.repoPath, "duplicate-source.txt"); + const sourceFolderPath = path.join(workspace.repoPath, "duplicate-folder"); + await writeFile(sourceFilePath, "duplicate contents", "utf8"); + await mkdir(sourceFolderPath); + await writeFile(path.join(sourceFolderPath, "nested.txt"), "nested contents", "utf8"); + + try { + await gotoWorkspace(page, workspace.workspaceId); + await openFileExplorer(page); + + const tree = page.getByTestId("file-explorer-tree-scroll"); + const entry = (name: string) => tree.getByText(name, { exact: true }).first(); + + await entry("duplicate-source.txt").click({ button: "right" }); + await page.getByTestId(/file-explorer-row-\d+-duplicate$/).click(); + await expect(entry("duplicate-source copy.txt")).toBeVisible(); + await expect + .poll(() => readFile(path.join(workspace.repoPath, "duplicate-source copy.txt"), "utf8")) + .toBe("duplicate contents"); + + await entry("duplicate-source.txt").click({ button: "right" }); + await page.getByTestId(/file-explorer-row-\d+-duplicate$/).click(); + await expect(entry("duplicate-source copy 2.txt")).toBeVisible(); + + await entry("duplicate-folder").click({ button: "right" }); + await page.getByTestId(/file-explorer-row-\d+-duplicate$/).click(); + await expect(entry("duplicate-folder copy")).toBeVisible(); + await expect + .poll(() => + readFile(path.join(workspace.repoPath, "duplicate-folder copy", "nested.txt"), "utf8"), + ) + .toBe("nested contents"); + } finally { + await Promise.all([ + rm(sourceFilePath, { force: true }), + rm(path.join(workspace.repoPath, "duplicate-source copy.txt"), { force: true }), + rm(path.join(workspace.repoPath, "duplicate-source copy 2.txt"), { force: true }), + rm(sourceFolderPath, { recursive: true, force: true }), + rm(path.join(workspace.repoPath, "duplicate-folder copy"), { recursive: true, force: true }), + ]); + } +}); + +test("shows an actionable error when the connection drops during creation", async ({ page }) => { + const disconnect = await disconnectNextContextAction(page); + await gotoWorkspace(page, workspace.workspaceId); + await openFileExplorer(page); + + await page.getByTestId("files-new-file").click(); + const nameInput = page.getByTestId("file-explorer-name-input"); + await nameInput.fill("disconnected.txt"); + await nameInput.press("Enter"); + await disconnect.wait(); + + await expect(page.getByText(/Context action connection failed/i)).toBeVisible({ + timeout: 30_000, + }); + await expect( + page.getByTestId("file-explorer-tree-scroll").getByText("disconnected.txt", { exact: true }), + ).toHaveCount(0); +}); + +test("keeps an entry visible when deletion fails", async ({ page }) => { + await failNextDeleteRequest(page); + await gotoWorkspace(page, workspace.workspaceId); + await openFileExplorer(page); + + const readme = page + .getByTestId("file-explorer-tree-scroll") + .getByText("README.md", { exact: true }); + await readme.click({ button: "right" }); + const confirmation = new Promise((resolve) => { + page.once("dialog", async (dialog) => { + await dialog.accept(); + resolve(); + }); + }); + await page.getByText("Delete", { exact: true }).click(); + await confirmation; + + await expect(page.getByText("Injected delete failure", { exact: true })).toBeVisible(); + await expect(readme).toBeVisible(); +}); + +test("hides unsupported file operations and revert actions", async ({ page }) => { + await hideContextActionCapabilities(page); + await writeFile(path.join(workspace.repoPath, "README.md"), "# Changed while unsupported\n"); + await gotoWorkspace(page, workspace.workspaceId); + await openFileExplorer(page); + + await expect(page.getByTestId("files-new-file")).toHaveCount(0); + await expect(page.getByTestId("files-new-folder")).toHaveCount(0); + await page + .getByTestId("file-explorer-tree-scroll") + .getByText("README.md", { exact: true }) + .click({ button: "right" }); + const filesMenu = page.getByTestId(/file-explorer-row-\d+-context-menu$/); + await expect(filesMenu.getByText("Rename", { exact: true })).toHaveCount(0); + await expect(filesMenu.getByText("Duplicate", { exact: true })).toHaveCount(0); + await expect(filesMenu.getByText("Delete", { exact: true })).toHaveCount(0); + await page.keyboard.press("Escape"); + + await page.getByTestId("explorer-tab-changes").click(); + await expect(page.getByTestId("diff-file-0")).toBeVisible({ timeout: 30_000 }); + await page.getByTestId("diff-file-0-toggle").click({ button: "right" }); + await expect(page.getByTestId("diff-file-0-duplicate")).toHaveCount(0); + await expect(page.getByTestId("diff-file-0-revert")).toHaveCount(0); +}); diff --git a/packages/app/e2e/browser/mermaid-streaming.spec.ts b/packages/app/e2e/browser/mermaid-streaming.spec.ts new file mode 100644 index 0000000000..a9f87862c6 --- /dev/null +++ b/packages/app/e2e/browser/mermaid-streaming.spec.ts @@ -0,0 +1,68 @@ +import { test } from "../support/fixtures"; +import { + expectCompletedDiagram, + expectDiagramRemainsRenderedWhileStreaming, + expectDiagramWithLabels, + reloadConversation, + requestDiagram, + waitForDiagramTurnToComplete, +} from "../support/helpers/diagram"; +import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; + +const STREAMED_MERMAID = [ + "```mermaid", + "flowchart LR", + " Start --> Middle", + ' Middle --> Done["Done"]', + " Middle --> Review", + " Review --> Verify", + " Verify --> Ship", + " Ship --> Package", + " Package --> Sign", + " Sign --> Upload", + " Upload --> Publish", + " Publish --> Deploy", + " Deploy --> Observe", + " Observe --> Validate", + " Validate --> Announce", + " Announce --> Document", + " Document --> Archive", + " Archive --> Release", + "```", +].join("\n"); + +test("keeps a Mermaid diagram rendered while its message streams, completes, and reloads", async ({ + page, +}) => { + const agent = await seedMockAgentWorkspace({ + repoPrefix: "mermaid-streaming-", + title: "Streaming Mermaid", + featureValues: { + mockStreamingAssistantResponse: STREAMED_MERMAID, + mockStreamingAssistantIntervalMs: 150, + }, + }); + try { + await test.step("Open the conversation and request a diagram", async () => { + await openAgentRoute(page, agent); + await requestDiagram(agent); + }); + + await test.step("The diagram renders and stays rendered while tokens arrive", async () => { + await expectDiagramWithLabels(page, ["Start", "Middle"]); + await expectDiagramRemainsRenderedWhileStreaming(page); + }); + + await test.step("The completed diagram shows the final streamed content", async () => { + await waitForDiagramTurnToComplete(agent); + await expectCompletedDiagram(page, ["Start", "Done", "Release"]); + }); + + await test.step("The completed diagram remains rendered after reload", async () => { + await reloadConversation(page); + await expectCompletedDiagram(page, ["Start", "Done", "Release"]); + }); + } finally { + await agent.cleanup(); + } +}); diff --git a/packages/app/e2e/browser/metadata-generation-settings.spec.ts b/packages/app/e2e/browser/metadata-generation-settings.spec.ts new file mode 100644 index 0000000000..5cdc507e8d --- /dev/null +++ b/packages/app/e2e/browser/metadata-generation-settings.spec.ts @@ -0,0 +1,110 @@ +import { expect, test, type Page } from "../support/fixtures"; +import { gotoAppShell, openSettings } from "../support/helpers/app"; +import { getServerId } from "../support/helpers/server-id"; +import { connectNewWorkspaceDaemonClient } from "../support/helpers/new-workspace"; +import { + expectSettingsHeader, + openHostSection, + openSettingsHost, +} from "../support/helpers/settings"; + +async function openMetadataGenerationSettings(page: Page) { + const serverId = getServerId(); + await gotoAppShell(page); + await openSettings(page); + await openSettingsHost(page, serverId); + await openHostSection(page, serverId, "metadata"); + await expectSettingsHeader(page, "Metadata"); +} + +async function openManualMetadataModelPicker(page: Page) { + await page.getByRole("button", { name: "Manual", exact: true }).click(); + await page.getByRole("button", { name: /Select model/ }).click(); +} + +test("chooses a metadata model and can return to automatic selection", async ({ + page, +}, testInfo) => { + await openMetadataGenerationSettings(page); + + await expect(page.getByText("Workspace titles", { exact: false })).toBeVisible(); + await expect(page.getByRole("button", { name: "Automatic", exact: true })).toHaveAttribute( + "aria-selected", + "true", + ); + await page.screenshot({ + path: testInfo.outputPath("metadata-automatic.png"), + fullPage: true, + }); + + await openManualMetadataModelPicker(page); + await page.getByText("Mock Load Test", { exact: true }).click(); + await expect(page.getByText("Ten second stream", { exact: true })).toBeVisible(); + await page.screenshot({ + path: testInfo.outputPath("metadata-model-picker.png"), + fullPage: true, + }); + await page.getByText("Ten second stream", { exact: true }).click(); + + await expect(page.getByRole("button", { name: /Ten second stream/ })).toBeVisible(); + await page.reload(); + await expect(page.getByRole("button", { name: "Manual", exact: true })).toHaveAttribute( + "aria-selected", + "true", + ); + await expect(page.getByRole("button", { name: /Ten second stream/ })).toBeVisible(); + await page.screenshot({ + path: testInfo.outputPath("metadata-manual-persisted.png"), + fullPage: true, + }); + + await page.getByRole("button", { name: "Automatic", exact: true }).click(); + await page.reload(); + + await expect(page.getByRole("button", { name: "Automatic", exact: true })).toHaveAttribute( + "aria-selected", + "true", + ); + await expect(page.getByRole("button", { name: /Ten second stream/ })).toHaveCount(0); +}); + +test("replaces only the first configured metadata model", async ({ page }) => { + const client = await connectNewWorkspaceDaemonClient({ ownProjects: false }); + const previousConfig = await client.getDaemonConfig(); + + try { + await client.patchDaemonConfig({ + metadataGeneration: { + providers: [ + { provider: "mock", model: "five-minute-stream" }, + { provider: "mock", model: "thirty-minute-stream" }, + ], + }, + }); + await openMetadataGenerationSettings(page); + + await expect(page.getByRole("button", { name: "Manual", exact: true })).toHaveAttribute( + "aria-selected", + "true", + ); + await openManualMetadataModelPicker(page); + await page.getByText("Ten second stream", { exact: true }).click(); + + await expect + .poll(async () => (await client.getDaemonConfig()).config.metadataGeneration.providers) + .toEqual([ + { provider: "mock", model: "ten-second-stream" }, + { provider: "mock", model: "thirty-minute-stream" }, + ]); + } finally { + try { + await client.patchDaemonConfig({ + metadataGeneration: { + providers: previousConfig.config.metadataGeneration.providers, + }, + }); + } finally { + await client.close().catch(() => undefined); + } + } +}); diff --git a/packages/app/e2e/browser/model-search-across-providers.spec.ts b/packages/app/e2e/browser/model-search-across-providers.spec.ts new file mode 100644 index 0000000000..4a4ab31ea2 --- /dev/null +++ b/packages/app/e2e/browser/model-search-across-providers.spec.ts @@ -0,0 +1,160 @@ +import { test } from "../support/fixtures"; +import { + closeModelPicker, + expectModelSearchEmptyState, + expectModelPickerHeight, + expectModelPickerWidth, + expectModelSearchResult, + expectPinnedProfilesHidden, + openModelPicker, + readModelPickerHeight, + readModelPickerWidth, + searchAllModels, + seedAgentProfiles, + seedModelProvider, + expectSearchResultsVirtualized, +} from "../support/helpers/agent-profiles"; +import { expectComposerVisible } from "../support/helpers/composer"; +import { clickNewChat, gotoWorkspace } from "../support/helpers/launcher"; +import { seedWorkspace } from "../support/helpers/seed-client"; + +const MOCK_PROVIDER_LABEL = "Mock Load Test"; + +// Its fast model deliberately carries the same label as the mock provider's, so +// the only thing telling the two result rows apart is the provider name. +const STUDIO = { + id: "mock-studio", + label: "Mock Studio", + models: [ + { id: "studio-fast", label: "Ten second stream", description: "Studio quick pass" }, + { id: "studio-deep", label: "Studio deep think", description: "Studio long pass" }, + ], +}; + +// The cross-provider search lives on the picker's root view, and a profile is +// what keeps the picker opening there. +const ROOT_VIEW_PROFILE = { + id: "agent_profile_e2e_search_root", + name: "Search anchor", + provider: "mock", +}; + +const LARGE_CATALOG_SIZE = 200; +const LARGE_CATALOG = { + id: "mock-bulk", + label: "Mock Bulk", + models: Array.from({ length: LARGE_CATALOG_SIZE }, (_, index) => ({ + id: `bulk-${index}`, + label: `Bulk model ${index}`, + description: `Bulk search result ${index}`, + })), +}; + +test.describe("Cross-provider model search", () => { + test("one query over the picker root reaches every provider and names each result's provider", async ({ + page, + }) => { + const provider = await seedModelProvider(STUDIO); + const profile = await seedAgentProfiles([ROOT_VIEW_PROFILE]); + const workspace = await seedWorkspace({ repoPrefix: "model-search-providers-" }); + + try { + await test.step("open a draft composer that can reach every provider", async () => { + await gotoWorkspace(page, workspace.workspaceId); + await clickNewChat(page); + await expectComposerVisible(page); + await openModelPicker(page); + }); + const restingWidth = await readModelPickerWidth(page); + + await test.step("one query returns the same model label from two providers", async () => { + await searchAllModels(page, "ten second stream"); + await expectModelSearchResult(page, { + provider: "mock", + modelId: "ten-second-stream", + modelLabel: "Ten second stream", + providerLabel: MOCK_PROVIDER_LABEL, + }); + await expectModelSearchResult(page, { + provider: STUDIO.id, + modelId: "studio-fast", + modelLabel: "Ten second stream", + providerLabel: STUDIO.label, + }); + await expectModelPickerWidth(page, restingWidth); + }); + + await test.step("searching by provider name reaches that provider's own models", async () => { + await searchAllModels(page, "studio deep"); + await expectModelSearchResult(page, { + provider: STUDIO.id, + modelId: "studio-deep", + modelLabel: "Studio deep think", + providerLabel: STUDIO.label, + }); + }); + + await test.step("results replace the pinned profiles", async () => { + await expectPinnedProfilesHidden(page); + }); + + // Search falls back to subsequence matching, so "no matches" needs letters + // that cannot be picked out of a model label or description in order. + await test.step("a query with no matches repeats the query back", async () => { + await searchAllModels(page, "zzz"); + await expectModelSearchEmptyState(page, "zzz"); + await closeModelPicker(page); + }); + } finally { + await workspace.cleanup(); + await profile.restore(); + await provider.restore(); + } + }); + + test("desktop search keeps its frame stable and virtualizes a host-wide catalog", async ({ + page, + }) => { + const provider = await seedModelProvider(LARGE_CATALOG); + const profile = await seedAgentProfiles([ROOT_VIEW_PROFILE]); + const workspace = await seedWorkspace({ repoPrefix: "model-search-large-catalog-" }); + + try { + await test.step("open the host-wide model picker", async () => { + await gotoWorkspace(page, workspace.workspaceId); + await clickNewChat(page); + await expectComposerVisible(page); + await openModelPicker(page); + }); + + const restingHeight = await readModelPickerHeight(page); + const restingWidth = await readModelPickerWidth(page); + + await test.step("a broad query renders only the visible result window", async () => { + await searchAllModels(page, "bulk model"); + await expectSearchResultsVirtualized(page, { + provider: LARGE_CATALOG.id, + total: LARGE_CATALOG_SIZE, + }); + await expectModelPickerHeight(page, restingHeight); + await expectModelPickerWidth(page, restingWidth); + }); + + await test.step("narrowing to one result does not resize the picker", async () => { + await searchAllModels(page, "bulk model 199"); + await expectModelSearchResult(page, { + provider: LARGE_CATALOG.id, + modelId: "bulk-199", + modelLabel: "Bulk model 199", + providerLabel: LARGE_CATALOG.label, + }); + await expectModelPickerHeight(page, restingHeight); + await expectModelPickerWidth(page, restingWidth); + }); + } finally { + await workspace.cleanup(); + await profile.restore(); + await provider.restore(); + } + }); +}); diff --git a/packages/app/e2e/browser/new-workspace-codex-mode-preferences.spec.ts b/packages/app/e2e/browser/new-workspace-codex-mode-preferences.spec.ts index 05f636007b..ddd415ed97 100644 --- a/packages/app/e2e/browser/new-workspace-codex-mode-preferences.spec.ts +++ b/packages/app/e2e/browser/new-workspace-codex-mode-preferences.spec.ts @@ -1,4 +1,5 @@ import { expect, test, type Page } from "../support/fixtures"; +import type { FormPreferences } from "@/create-agent-preferences/preferences"; import { gotoAppShell } from "../support/helpers/app"; import { daemonWsRoutePattern } from "../support/helpers/daemon-port"; import { openAgentRoute } from "../support/helpers/mock-agent"; @@ -10,7 +11,6 @@ import { import { expectNoTruncation } from "../support/helpers/no-truncation"; import { escapeRegex } from "../support/helpers/regex"; import { seedWorkspace } from "../support/helpers/seed-client"; -import { getServerId } from "../support/helpers/server-id"; import { waitForSidebarHydration } from "../support/helpers/workspace-ui"; const CREATE_AGENT_PREFERENCES_KEY = "@paseo:create-agent-preferences"; @@ -49,13 +49,12 @@ function getSessionMessage(message: WebSocketMessage): Record | return maybeEnvelope.message as Record; } -async function seedCodexDefaultPermissionPreferences(page: Page, serverId: string): Promise { +async function seedCodexDefaultPermissionPreferences(page: Page): Promise { await page.addInitScript( - ({ preferencesKey, serverId: seededServerId }) => { + ({ preferencesKey }) => { localStorage.setItem( preferencesKey, JSON.stringify({ - serverId: seededServerId, provider: "codex", providerPreferences: { codex: { @@ -69,10 +68,10 @@ async function seedCodexDefaultPermissionPreferences(page: Page, serverId: strin model: "ten-second-stream", }, }, - }), + } satisfies FormPreferences), ); }, - { preferencesKey: CREATE_AGENT_PREFERENCES_KEY, serverId }, + { preferencesKey: CREATE_AGENT_PREFERENCES_KEY }, ); } @@ -155,10 +154,9 @@ test.describe("New workspace Codex mode preferences", () => { test("keeps Full Access as the global Codex mode after the workspace draft auto-submit handoff", async ({ page, }) => { - const serverId = getServerId(); const seeded = await seedWorkspace({ repoPrefix: "codex-mode-preferences-" }); const createAgentRecorder = await recordAndBlockCreateAgentRequests(page); - await seedCodexDefaultPermissionPreferences(page, serverId); + await seedCodexDefaultPermissionPreferences(page); try { await gotoAppShell(page); @@ -194,9 +192,8 @@ test.describe("New workspace Codex mode preferences", () => { }); test("uses the live Codex agent mode as the next New Workspace default", async ({ page }) => { - const serverId = getServerId(); const seeded = await seedWorkspace({ repoPrefix: "codex-live-mode-preferences-" }); - await seedCodexDefaultPermissionPreferences(page, serverId); + await seedCodexDefaultPermissionPreferences(page); try { const agent = await seeded.client.createAgent({ diff --git a/packages/app/e2e/browser/new-workspace-mode-cycle-safety.spec.ts b/packages/app/e2e/browser/new-workspace-mode-cycle-safety.spec.ts index 61a58fee88..2b6ab5d53d 100644 --- a/packages/app/e2e/browser/new-workspace-mode-cycle-safety.spec.ts +++ b/packages/app/e2e/browser/new-workspace-mode-cycle-safety.spec.ts @@ -1,4 +1,5 @@ import { expect, test, type Page } from "../support/fixtures"; +import type { FormPreferences } from "@/create-agent-preferences/preferences"; import { daemonWsRoutePattern } from "../support/helpers/daemon-port"; import { openAgentRoute } from "../support/helpers/mock-agent"; import { @@ -6,7 +7,6 @@ import { selectNewWorkspaceProject, } from "../support/helpers/new-workspace"; import { seedWorkspace } from "../support/helpers/seed-client"; -import { getServerId } from "../support/helpers/server-id"; const CREATE_AGENT_PREFERENCES_KEY = "@paseo:create-agent-preferences"; @@ -62,13 +62,12 @@ async function recordSetAgentModeRequests(page: Page): Promise<{ }; } -async function seedCodexDefaultPreferences(page: Page, serverId: string): Promise { +async function seedCodexDefaultPreferences(page: Page): Promise { await page.addInitScript( - ({ preferencesKey, serverId: seededServerId }) => { + ({ preferencesKey }) => { localStorage.setItem( preferencesKey, JSON.stringify({ - serverId: seededServerId, provider: "codex", providerPreferences: { codex: { @@ -78,10 +77,10 @@ async function seedCodexDefaultPreferences(page: Page, serverId: string): Promis }, mock: { model: "ten-second-stream" }, }, - }), + } satisfies FormPreferences), ); }, - { preferencesKey: CREATE_AGENT_PREFERENCES_KEY, serverId }, + { preferencesKey: CREATE_AGENT_PREFERENCES_KEY }, ); } @@ -104,9 +103,8 @@ test.describe("New Workspace mode cycle safety", () => { // control and silently change that (possibly running) agent's mode — e.g. into a // permissive/bypass mode. See use-keyboard-action-handler.ts. test("Shift+Tab in New Workspace never changes a backgrounded agent's mode", async ({ page }) => { - const serverId = getServerId(); const seeded = await seedWorkspace({ repoPrefix: "mode-cycle-safety-" }); - await seedCodexDefaultPreferences(page, serverId); + await seedCodexDefaultPreferences(page); const modeRequests = await recordSetAgentModeRequests(page); try { diff --git a/packages/app/e2e/browser/projects-settings.spec.ts b/packages/app/e2e/browser/projects-settings.spec.ts index a6a68a3ba7..52954a3290 100644 --- a/packages/app/e2e/browser/projects-settings.spec.ts +++ b/packages/app/e2e/browser/projects-settings.spec.ts @@ -9,12 +9,14 @@ import { clickReloadProjectSettings, clickRetryProjectSettingsSave, clickSaveProjectSettings, + commitPaseoConfig, corruptPaseoConfig, editWorktreeSetup, expectEmptyScriptList, expectProjectHostContextHidden, expectNoEditableTarget, expectNoProjectSettingsError, + expectNoUncommittedSetupWarning, expectProjectEditFailed, expectProjectEditName, expectProjectEditSaved, @@ -27,6 +29,7 @@ import { expectSaveButtonDisabled, expectScriptRowCount, expectWriteFailedCalloutActions, + expectUncommittedSetupWarning, fillProjectIconUrl, fillProjectName, installDaemonConnectionGate, @@ -216,9 +219,16 @@ test.describe("Projects settings", () => { test("user edits worktree setup from the projects page", async ({ page, editableProject }) => { await openProjects(page); await openProjectSettings(page, editableProject.name); + await expectNoUncommittedSetupWarning(page); await editWorktreeSetup(page, updatedSetup); await clickSaveProjectSettings(page); await expectProjectConfigSaved(editableProject); + await expectUncommittedSetupWarning(page); + + commitPaseoConfig(editableProject.path); + await returnToProjectsList(page); + await openProjectSettings(page, editableProject.name); + await expectNoUncommittedSetupWarning(page); }); test("project navigation stays inside the selected host", async ({ page, editableProject }) => { @@ -439,5 +449,7 @@ test.describe("Projects settings — error UX", () => { await expectScriptRowCount(page, 0); await expectEmptyScriptList(page); + await clickSaveProjectSettings(page); + await expectNoUncommittedSetupWarning(page); }); }); diff --git a/packages/app/e2e/browser/provider-task-list.real.spec.ts b/packages/app/e2e/browser/provider-task-list.real.spec.ts new file mode 100644 index 0000000000..b388b7d2c7 --- /dev/null +++ b/packages/app/e2e/browser/provider-task-list.real.spec.ts @@ -0,0 +1,116 @@ +import { mkdtempSync, realpathSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { expect, test } from "../support/fixtures"; +import { + cleanupRewindFlow, + launchAgent, + sendMessage, + type AgentHandle, + type RewindFlowProvider, +} from "../support/helpers/rewind-flow"; +import { + expectCompletedTaskActivity, + expectTaskListEntries, + openCompletedTaskList, +} from "../support/helpers/task-list"; +import { submitMessage } from "../support/helpers/composer"; + +interface TaskScenario { + provider: RewindFlowProvider; + entries: [string, string]; + prompt: string; + providerConfig?: Parameters[0]["providerConfig"]; +} + +const scenarios: TaskScenario[] = [ + { + provider: "claude", + entries: ["Claude inventory alpha", "Claude inventory beta"], + providerConfig: { model: "opus" }, + prompt: + "Use TaskCreate to create exactly two tasks named Claude inventory alpha and Claude inventory beta. Use TaskUpdate to mark each in_progress and then completed. Do not use subagents, shell commands, or edit files. Finish with TASKS_COMPLETE.", + }, + { + provider: "codex", + entries: ["Codex inventory alpha", "Codex inventory beta"], + prompt: + "Use update_plan to track exactly two steps named Codex inventory alpha and Codex inventory beta. Move each through in_progress and completed. Do not use subagents, shell commands, or edit files. Finish with TASKS_COMPLETE.", + }, + { + provider: "opencode", + entries: ["OpenCode inventory alpha", "OpenCode inventory beta"], + prompt: + "Use TodoWrite to track exactly two tasks named OpenCode inventory alpha and OpenCode inventory beta. Move each through in_progress and completed. Do not use subagents, shell commands, or edit files. Finish with TASKS_COMPLETE.", + }, +]; + +test.describe("real provider composer task lists", () => { + test.setTimeout(600_000); + + for (const scenario of scenarios) { + test(`${scenario.provider} shows native task progress above the composer`, async ({ + page, + }, testInfo) => { + const cwd = realpathSync( + mkdtempSync(path.join(tmpdir(), `paseo-task-list-${scenario.provider}-`)), + ); + let handle: AgentHandle | undefined; + + try { + handle = await launchAgent({ + page, + provider: scenario.provider, + cwd, + mode: "full-access", + providerConfig: scenario.providerConfig, + }); + await sendMessage(handle, scenario.prompt); + await openCompletedTaskList(page, scenario.entries.length); + await expectTaskListEntries(page, scenario.entries); + await expectCompletedTaskActivity(page, scenario.entries); + await expect(page.getByText("TaskUpdate", { exact: true })).toHaveCount(0); + await expect(page.getByTestId("timeline-plan-card")).toHaveCount(0); + + const screenshot = testInfo.outputPath(`${scenario.provider}-composer-tasks.png`); + await page.screenshot({ path: screenshot }); + await testInfo.attach(`${scenario.provider} composer tasks`, { + path: screenshot, + contentType: "image/png", + }); + } finally { + await cleanupRewindFlow({ handle, cwd }); + } + }); + } + + test("Codex genuine Plan mode keeps the existing plan card", async ({ page }, testInfo) => { + const cwd = realpathSync(mkdtempSync(path.join(tmpdir(), "paseo-genuine-codex-plan-"))); + let handle: AgentHandle | undefined; + + try { + handle = await launchAgent({ + page, + provider: "codex", + cwd, + mode: "full-access", + providerConfig: { featureValues: { plan_mode: true } }, + }); + await submitMessage( + page, + "Produce a concise implementation plan with exactly these steps: Inspect genuine plan boundary; Implement genuine plan boundary; Verify genuine plan boundary. Do not implement it.", + ); + + const planCard = page.getByTestId("permission-plan-card"); + await expect(planCard).toBeVisible({ timeout: 120_000 }); + await expect(planCard).toContainText("Inspect genuine plan boundary"); + await expect(page.getByTestId("timeline-plan-card")).toHaveCount(0); + + const screenshot = testInfo.outputPath("codex-genuine-plan.png"); + await page.screenshot({ path: screenshot }); + await testInfo.attach("Codex genuine plan", { path: screenshot, contentType: "image/png" }); + } finally { + await cleanupRewindFlow({ handle, cwd }); + } + }); +}); diff --git a/packages/app/e2e/browser/replica-cache-performance.spec.ts b/packages/app/e2e/browser/replica-cache-performance.spec.ts new file mode 100644 index 0000000000..11ac76bd73 --- /dev/null +++ b/packages/app/e2e/browser/replica-cache-performance.spec.ts @@ -0,0 +1,65 @@ +import { expect, test as base } from "../support/fixtures"; +import { expectAgentIdle } from "../support/helpers/agent-stream"; +import { expectComposerVisible, submitMessage } from "../support/helpers/composer"; +import { + openAgentRoute, + seedMockAgentWorkspace, + type MockAgentWorkspace, +} from "../support/helpers/mock-agent"; +import { + beginReplicaCacheMeasurement, + observeReplicaCacheWrites, + readReplicaCacheMeasurement, +} from "../support/helpers/replica-cache-perf"; + +const RUN_REPLICA_CACHE_PERF = process.env.PASEO_REPLICA_CACHE_PERF_E2E === "1"; +const MAX_STREAM_WRITES = 12; +const MAX_SERIALIZED_CHARS = 190_000; + +const test = base.extend<{ replicaCacheAgent: MockAgentWorkspace }>({ + replicaCacheAgent: async ({ page: _page }, provide) => { + const agent = await seedMockAgentWorkspace({ + repoPrefix: "replica-cache-perf-", + title: "Replica cache performance", + model: "ten-second-stream", + }); + try { + await provide(agent); + } finally { + await agent.cleanup(); + } + }, +}); +const replicaCachePerfDescribe = RUN_REPLICA_CACHE_PERF ? test.describe : test.describe.skip; + +replicaCachePerfDescribe("Replica cache performance", () => { + test("streaming writes only persisted replica changes", async ({ + page, + replicaCacheAgent: agent, + }, testInfo) => { + test.setTimeout(60_000); + await observeReplicaCacheWrites(page); + await openAgentRoute(page, agent); + await expectComposerVisible(page); + await beginReplicaCacheMeasurement(page); + + await submitMessage(page, "Measure replica persistence during a sustained stream."); + await agent.client.waitForFinish(agent.agentId, 20_000); + await expectAgentIdle(page); + await page.waitForTimeout(1_000); + + const report = await readReplicaCacheMeasurement(page); + await testInfo.attach("replica-cache-performance", { + body: JSON.stringify(report, null, 2), + contentType: "application/json", + }); + console.log(`[perf] Replica cache: ${JSON.stringify(report)}`); + + expect( + report.writes, + "streaming should not continuously persist transient state", + ).toBeLessThanOrEqual(MAX_STREAM_WRITES); + expect(report.redundantWrites).toBe(0); + expect(report.serializedChars).toBeLessThanOrEqual(MAX_SERIALIZED_CHARS); + }); +}); diff --git a/packages/app/e2e/browser/rewind-menu.ui-contract.spec.ts b/packages/app/e2e/browser/rewind-menu.ui-contract.spec.ts index e620b4d0fb..ab1ce4e199 100644 --- a/packages/app/e2e/browser/rewind-menu.ui-contract.spec.ts +++ b/packages/app/e2e/browser/rewind-menu.ui-contract.spec.ts @@ -10,6 +10,7 @@ import { fillComposerDraft, submitMessage, } from "../support/helpers/composer"; +import { readReplicaCache, writeReplicaCache } from "../support/helpers/replica-cache-storage"; // UI plumbing contract against the dev mock provider. Real-provider behavior is tested in `daemon-e2e/*-rewind.real.e2e.test.ts`. @@ -27,39 +28,28 @@ async function expectUserMessageVisible(page: Page, text: string): Promise async function rewriteCachedMessageAsLegacyRow(page: Page, prompt: string): Promise { await expect - .poll(() => - page.evaluate((messageText) => { - const raw = localStorage.getItem("@paseo:replica-cache"); - if (!raw) return false; - const cache = JSON.parse(raw) as { - hosts?: Array<{ timeline?: { items?: Array> } | null }>; - }; - for (const host of cache.hosts ?? []) { - for (const item of host.timeline?.items ?? []) { - if (item.kind === "user_message" && item.text === messageText && item.messageId) { - return true; - } + .poll(async () => { + const cache = await readReplicaCache(page); + if (!cache) return false; + for (const host of cache.hosts ?? []) { + for (const item of host.timeline?.items ?? []) { + if (item.kind === "user_message" && item.text === prompt && item.messageId) { + return true; } } - return false; - }, prompt), - ) + } + return false; + }) .toBe(true); - await page.evaluate((messageText) => { - const key = "@paseo:replica-cache"; - const raw = localStorage.getItem(key); - if (!raw) throw new Error("Replica cache was not persisted"); - const cache = JSON.parse(raw) as { - hosts?: Array<{ timeline?: { items?: Array> } | null }>; - }; - const cachedMessage = cache.hosts - ?.flatMap((host) => host.timeline?.items ?? []) - .find((item) => item.kind === "user_message" && item.text === messageText); - if (!cachedMessage) throw new Error("Cached user message was not found"); - delete cachedMessage.messageId; - localStorage.setItem(key, JSON.stringify(cache)); - }, prompt); + const cache = await readReplicaCache(page); + if (!cache) throw new Error("Replica cache was not persisted"); + const cachedMessage = cache.hosts + ?.flatMap((host) => host.timeline?.items ?? []) + .find((item) => item.kind === "user_message" && item.text === prompt); + if (!cachedMessage) throw new Error("Cached user message was not found"); + delete cachedMessage.messageId; + await writeReplicaCache(page, cache); } async function waitForCurrentSubmissionExcludedFromCache( @@ -67,46 +57,34 @@ async function waitForCurrentSubmissionExcludedFromCache( prompt: string, ): Promise { await expect - .poll(() => - page.evaluate((messageText) => { - const raw = localStorage.getItem("@paseo:replica-cache"); - if (!raw) return false; - const cache = JSON.parse(raw) as { - hosts?: Array<{ timeline?: { items?: Array> } | null }>; - }; - return !cache.hosts - ?.flatMap((host) => host.timeline?.items ?? []) - .some( - (item) => - item.kind === "user_message" && - item.text === messageText && - typeof item.clientMessageId === "string" && - item.messageId === undefined, - ); - }, prompt), - ) + .poll(async () => { + const cache = await readReplicaCache(page); + if (!cache) return false; + return !cache.hosts + ?.flatMap((host) => host.timeline?.items ?? []) + .some( + (item) => + item.kind === "user_message" && + item.text === prompt && + typeof item.clientMessageId === "string" && + item.messageId === undefined, + ); + }) .toBe(true); } async function waitForCachedMessageWithoutProviderId(page: Page, prompt: string): Promise { await expect - .poll(() => - page.evaluate((messageText) => { - const raw = localStorage.getItem("@paseo:replica-cache"); - if (!raw) return false; - const cache = JSON.parse(raw) as { - hosts?: Array<{ timeline?: { items?: Array> } | null }>; - }; - return cache.hosts - ?.flatMap((host) => host.timeline?.items ?? []) - .some( - (item) => - item.kind === "user_message" && - item.text === messageText && - item.messageId === undefined, - ); - }, prompt), - ) + .poll(async () => { + const cache = await readReplicaCache(page); + if (!cache) return false; + return cache.hosts + ?.flatMap((host) => host.timeline?.items ?? []) + .some( + (item) => + item.kind === "user_message" && item.text === prompt && item.messageId === undefined, + ); + }) .toBe(true); } @@ -148,16 +126,13 @@ test.describe("Rewind sheet", () => { title: "Rewind cache upgrade e2e", initialPrompt: prompt, }); - let heldTimelineRequest = false; try { await openAgentRoute(page, session); await expectUserMessageVisible(page, prompt); + await gate.drop(); await rewriteCachedMessageAsLegacyRow(page, prompt); - gate.holdNextClientRequest("fetch_agent_timeline_request"); await page.reload(); - await gate.waitForHeldClientRequest(); - heldTimelineRequest = true; const restoredMessage = userMessage(page, prompt); await expect(restoredMessage).toBeVisible(); @@ -165,7 +140,6 @@ test.describe("Rewind sheet", () => { await expect(restoredMessage.getByTestId("rewind-menu-trigger")).toHaveCount(0); await waitForCachedMessageWithoutProviderId(page, prompt); } finally { - if (heldTimelineRequest) gate.releaseHeldClientRequest(); gate.restore(); await session.cleanup(); } diff --git a/packages/app/e2e/browser/sessions-empty.spec.ts b/packages/app/e2e/browser/sessions-empty.spec.ts index 05884b45a6..32769996e4 100644 --- a/packages/app/e2e/browser/sessions-empty.spec.ts +++ b/packages/app/e2e/browser/sessions-empty.spec.ts @@ -31,7 +31,7 @@ test("Sessions shows an empty placeholder when the host has no history", async ( localStorage.setItem("@paseo:daemon-registry", JSON.stringify([seededHost])); localStorage.setItem("@paseo:create-agent-preferences", JSON.stringify(preferences)); }, - { seededHost: host, preferences: buildCreateAgentPreferences(serverId) }, + { seededHost: host, preferences: buildCreateAgentPreferences() }, ); await gotoAppShell(page); diff --git a/packages/app/e2e/browser/sidebar-reorder.spec.ts b/packages/app/e2e/browser/sidebar-reorder.spec.ts index c0d199148e..6129d37a63 100644 --- a/packages/app/e2e/browser/sidebar-reorder.spec.ts +++ b/packages/app/e2e/browser/sidebar-reorder.spec.ts @@ -18,7 +18,25 @@ async function visibleBoundingBox(row: Locator) { return box; } -async function quickDragFirstRowAfterSecond(rows: Locator) { +async function pressProjectRow(rows: Locator) { + await rows.page().mouse.down(); +} + +async function pressWorkspaceRow(rows: Locator) { + const solidScrimStop = rows + .nth(0) + .getByTestId("sidebar-workspace-trailing-scrim") + .locator("stop") + .nth(1); + const hoverScrimColor = await solidScrimStop.getAttribute("stop-color"); + await rows.page().mouse.down(); + await expect.poll(() => solidScrimStop.getAttribute("stop-color")).not.toBe(hoverScrimColor); +} + +async function quickDragFirstRowAfterSecond( + rows: Locator, + pressRow: (rows: Locator) => Promise, +) { await expect(rows).toHaveCount(2); const before = await rowTestIds(rows); const sourceBox = await visibleBoundingBox(rows.nth(0)); @@ -29,15 +47,19 @@ async function quickDragFirstRowAfterSecond(rows: Locator) { const target = { x: targetBox.x + targetBox.width / 2, y: targetBox.y + targetBox.height / 2 }; await page.mouse.move(source.x, source.y); - await page.mouse.down(); + const trailingScrim = rows.nth(0).getByTestId("sidebar-workspace-trailing-scrim"); + await pressRow(rows); await page.mouse.move(source.x, source.y + 7); + await expect(trailingScrim).toHaveCount(0); await page.mouse.move(target.x, target.y, { steps: 4 }); await page.mouse.up(); await expect.poll(() => rowTestIds(rows)).toEqual([before[1], before[0]]); } -test("projects and workspaces reorder with an immediate mouse drag", async ({ page }) => { +test("projects, workspaces, and pinned chats reorder with an immediate mouse drag", async ({ + page, +}) => { const firstProject = await seedWorkspace({ repoPrefix: "sidebar-reorder-first-" }); const secondProject = await seedWorkspace({ repoPrefix: "sidebar-reorder-second-" }); @@ -61,6 +83,7 @@ test("projects and workspaces reorder with an immediate mouse drag", async ({ pa const secondProjectTestId = `sidebar-project-row-${projectEquivalenceViewKey(secondProject.projectKey)}`; await quickDragFirstRowAfterSecond( page.locator(`[data-testid="${firstProjectTestId}"], [data-testid="${secondProjectTestId}"]`), + pressProjectRow, ); const firstWorkspaceTestId = `sidebar-workspace-row-${getServerId()}:${firstProject.workspaceId}`; const secondWorkspaceTestId = `sidebar-workspace-row-${getServerId()}:${secondWorkspace.workspace.id}`; @@ -68,6 +91,17 @@ test("projects and workspaces reorder with an immediate mouse drag", async ({ pa page.locator( `[data-testid="${firstWorkspaceTestId}"], [data-testid="${secondWorkspaceTestId}"]`, ), + pressWorkspaceRow, + ); + + await firstProject.client.setWorkspacePinned(firstProject.workspaceId, true); + await secondProject.client.setWorkspacePinned(secondProject.workspaceId, true); + const secondProjectWorkspaceTestId = `sidebar-workspace-row-${getServerId()}:${secondProject.workspaceId}`; + await quickDragFirstRowAfterSecond( + page.locator( + `[data-testid="${firstWorkspaceTestId}"], [data-testid="${secondProjectWorkspaceTestId}"]`, + ), + pressWorkspaceRow, ); } finally { await firstProject.cleanup(); diff --git a/packages/app/e2e/browser/workspace-model-restart.spec.ts b/packages/app/e2e/browser/workspace-model-restart.spec.ts index c77b0bb1ec..54cbb0c0e8 100644 --- a/packages/app/e2e/browser/workspace-model-restart.spec.ts +++ b/packages/app/e2e/browser/workspace-model-restart.spec.ts @@ -7,6 +7,7 @@ import path from "node:path"; import net from "node:net"; import { expect, type Page } from "@playwright/test"; import { buildHostWorkspaceRoute, decodeWorkspaceIdFromPathSegment } from "@/utils/host-routes"; +import type { FormPreferences } from "@/create-agent-preferences/preferences"; import { metroTest as test } from "../support/fixtures"; import { buildSeededHost } from "../support/helpers/daemon-registry"; import { loadDaemonClientConstructor } from "../support/helpers/daemon-client-loader"; @@ -348,12 +349,14 @@ async function seedBrowserForDaemon(page: Page, input: { serverId: string; port: { daemon: host, preferences: { - serverId: input.serverId, provider: "codex", providerPreferences: { - codex: { model: "gpt-5.4-mini", thinkingOptionId: "low" }, + codex: { + model: "gpt-5.4-mini", + thinkingByModel: { "gpt-5.4-mini": "low" }, + }, }, - }, + } satisfies FormPreferences, }, ); } diff --git a/packages/app/e2e/browser/worktree-restore-after-restart.spec.ts b/packages/app/e2e/browser/worktree-restore-after-restart.spec.ts index 4cf294d69d..c4d2ccfffe 100644 --- a/packages/app/e2e/browser/worktree-restore-after-restart.spec.ts +++ b/packages/app/e2e/browser/worktree-restore-after-restart.spec.ts @@ -75,7 +75,7 @@ test.describe("Worktree restore after daemon restart", () => { label: "restart daemon", nowIso, }), - preferences: buildCreateAgentPreferences(serverId), + preferences: buildCreateAgentPreferences(), }, ); } diff --git a/packages/app/e2e/fixtures/fake-pi-rpc.mjs b/packages/app/e2e/fixtures/fake-pi-rpc.mjs new file mode 100644 index 0000000000..b6d156ddfc --- /dev/null +++ b/packages/app/e2e/fixtures/fake-pi-rpc.mjs @@ -0,0 +1,38 @@ +import readline from "node:readline"; + +const lines = readline.createInterface({ input: process.stdin }); + +for await (const line of lines) { + const command = JSON.parse(line); + let data = {}; + if (command.type === "get_available_models") { + data = { + models: [ + { + provider: "e2e", + id: "pi-profile-model", + name: "Pi profile model", + }, + ], + }; + } else if (command.type === "get_state") { + data = { + sessionId: "e2e-pi-session", + thinkingLevel: "medium", + isStreaming: false, + isCompacting: false, + messageCount: 0, + pendingMessageCount: 0, + }; + } + + process.stdout.write( + `${JSON.stringify({ + id: command.id, + type: "response", + command: command.type, + success: true, + data, + })}\n`, + ); +} diff --git a/packages/app/e2e/support/fixtures.ts b/packages/app/e2e/support/fixtures.ts index e8352a32e2..723f048081 100644 --- a/packages/app/e2e/support/fixtures.ts +++ b/packages/app/e2e/support/fixtures.ts @@ -132,7 +132,7 @@ const test = metroTest.extend< endpoint: `127.0.0.1:${daemonPort}`, nowIso, }); - const createAgentPreferences = buildCreateAgentPreferences(testDaemon.serverId); + const createAgentPreferences = buildCreateAgentPreferences(); await page.addInitScript( ({ daemon, preferences, seedNonce: nonce, extraHostsKey }) => { diff --git a/packages/app/e2e/support/helpers/agent-profiles.ts b/packages/app/e2e/support/helpers/agent-profiles.ts new file mode 100644 index 0000000000..59deb88638 --- /dev/null +++ b/packages/app/e2e/support/helpers/agent-profiles.ts @@ -0,0 +1,567 @@ +import { expect, type Locator, type Page } from "@playwright/test"; +import type { AgentProfile } from "@getpaseo/protocol/messages"; +import { buildSettingsHostSectionRoute } from "@/utils/host-routes"; +import { gotoAppShell, openSettings } from "./app"; +import { connectDaemonClient } from "./daemon-client-loader"; +import { getServerId } from "./server-id"; +import { expectAppRoute } from "./route-assertions"; +import { openSettingsHost } from "./settings"; + +// ─── Daemon-side seeding ─────────────────────────────────────────────────── + +interface AgentProfilesDaemonClient { + connect(): Promise; + close(): Promise; + getDaemonConfig(): Promise<{ config: { agentProfiles?: AgentProfile[] } }>; + patchDaemonConfig(config: { + agentProfiles?: AgentProfile[]; + providers?: Record>; + removeProviders?: string[]; + }): Promise; +} + +export interface HostSeed { + /** Puts the host's config back the way the test found it and drops the client. */ + restore(): Promise; +} + +async function connectAgentProfilesClient(): Promise { + return connectDaemonClient({ clientIdPrefix: "agent-profiles-e2e" }); +} + +/** + * Writes profiles into host config directly. Journeys that are *about* the + * settings UI create theirs through it; every other journey starts from a host + * that already has them. + */ +export async function seedAgentProfiles(profiles: AgentProfile[]): Promise { + const client = await connectAgentProfilesClient(); + const previous = (await client.getDaemonConfig()).config.agentProfiles ?? []; + await client.patchDaemonConfig({ agentProfiles: profiles }); + return { + async restore() { + await client.patchDaemonConfig({ agentProfiles: previous }).catch(() => undefined); + await client.close().catch(() => undefined); + }, + }; +} + +export async function stageLegacyFavoritesForHostMigration( + page: Page, + favorites: Array<{ provider: string; modelId: string }>, +): Promise { + const serverId = getServerId(); + await gotoAppShell(page); + await page.evaluate( + ({ favorites: storedFavorites, serverId: hostId }) => { + const preferencesKey = "@paseo:create-agent-preferences"; + const raw = localStorage.getItem(preferencesKey); + const preferences = raw ? JSON.parse(raw) : {}; + localStorage.setItem( + preferencesKey, + JSON.stringify({ ...preferences, favoriteModels: storedFavorites }), + ); + localStorage.removeItem(`@paseo:legacy-favorites-to-agent-profiles:v1:${hostId}`); + + // Preserve this upgrade-shaped storage across the fixture's next-page + // default seed; a reload is the startup boundary under test. + const nonce = localStorage.getItem("@paseo:e2e-seed-nonce"); + if (nonce) { + localStorage.setItem("@paseo:e2e-disable-default-seed-once", nonce); + } + }, + { favorites, serverId }, + ); + await page.reload(); +} + +export async function expectHostAgentProfiles(expected: AgentProfile[]): Promise { + const client = await connectAgentProfilesClient(); + try { + await expect + .poll(async () => (await client.getDaemonConfig()).config.agentProfiles ?? [], { + timeout: 30_000, + }) + .toEqual(expected); + } finally { + await client.close().catch(() => undefined); + } +} + +export interface SeededProviderModel { + id: string; + label: string; + description: string; +} + +/** + * Registers a second provider that reports a fixed catalog. The E2E daemon has + * exactly one provider with models (`mock`), so nothing cross-provider can be + * proven without one more. + * + * `extends: "mock"` is rejected — the config validator only accepts the six + * shipped providers plus `acp`. Extending `claude` with a replacement `models` + * list is what avoids running anything: replacement models skip catalog + * discovery, and Claude's static modes mean the provider never spawns. The + * command only has to resolve for the availability probe, hence `node`. + * Providers with dynamic catalogs can pass a small RPC fixture as `command`. + */ +export async function seedModelProvider(input: { + id: string; + label: string; + models: SeededProviderModel[]; + extends?: "claude" | "pi"; + command?: string[]; +}): Promise { + const client = await connectAgentProfilesClient(); + await client.patchDaemonConfig({ + providers: { + [input.id]: { + extends: input.extends ?? "claude", + label: input.label, + description: `${input.label} test provider`, + enabled: true, + command: input.command ?? ["node"], + models: input.models, + }, + }, + }); + return { + async restore() { + await client.patchDaemonConfig({ removeProviders: [input.id] }).catch(() => undefined); + await client.close().catch(() => undefined); + }, + }; +} + +// ─── Settings: navigation ────────────────────────────────────────────────── + +/** Reach agent profiles through the same visible Settings path a person uses. */ +export async function openAgentProfileSettings(page: Page): Promise { + const serverId = getServerId(); + await gotoAppShell(page); + await openSettings(page); + await openSettingsHost(page, serverId); + await page.getByRole("button", { name: "Agents", exact: true }).click(); + await expectAppRoute(page, buildSettingsHostSectionRoute(serverId, "agents")); + await expect(page.getByTestId("agent-profiles-card")).toBeVisible({ timeout: 30_000 }); +} + +export async function expectNoAgentProfiles(page: Page): Promise { + const card = page.getByTestId("agent-profiles-card"); + await expect(card.getByTestId("agent-profiles-empty")).toBeVisible(); + await expect(card.getByRole("button", { name: "New profile", exact: true })).toBeVisible(); +} + +// ─── Settings: the profile list ──────────────────────────────────────────── + +export function agentProfileRow(page: Page, name: string): Locator { + return page.locator('[data-testid^="agent-profile-row-"]').filter({ hasText: name }); +} + +export async function expectAgentProfile( + page: Page, + expected: { name: string; tags: string[]; notes?: string }, +): Promise { + const row = agentProfileRow(page, expected.name); + await expect(row).toBeVisible({ timeout: 30_000 }); + await expect(row.getByText(expected.tags.join(" · "), { exact: true })).toBeVisible(); + if (expected.notes !== undefined) { + await expect(row.getByText(expected.notes, { exact: true })).toBeVisible(); + } +} + +export async function expectAgentProfileTagsGone( + page: Page, + input: { name: string; tags: string[] }, +): Promise { + const row = agentProfileRow(page, input.name); + for (const tag of input.tags) { + await expect(row.getByText(tag, { exact: true })).toHaveCount(0); + } +} + +/** Order is what the user sees, so it is measured on screen rather than in the DOM. */ +export async function expectAgentProfileOrder(page: Page, names: string[]): Promise { + await expect + .poll( + async () => { + const measured = await Promise.all( + names.map(async (name) => ({ name, top: await boxTop(agentProfileRow(page, name)) })), + ); + return measured.sort((a, b) => a.top - b.top).map((entry) => entry.name); + }, + { timeout: 15_000 }, + ) + .toEqual(names); +} + +export async function moveAgentProfileUp(page: Page, name: string): Promise { + await agentProfileRow(page, name).getByRole("button", { name: "Move up", exact: true }).click(); +} + +export async function moveAgentProfileDown(page: Page, name: string): Promise { + await agentProfileRow(page, name).getByRole("button", { name: "Move down", exact: true }).click(); +} + +/** + * The remove confirmation is `window.confirm` on browser web, so the dialog is + * consumed here — and its text is asserted, since naming the profile is the + * whole point of confirming. + */ +export async function removeAgentProfile(page: Page, name: string): Promise { + const dialogMessage = page.waitForEvent("dialog").then(async (dialog) => { + const message = dialog.message(); + await dialog.accept(); + return message; + }); + await agentProfileRow(page, name).getByRole("button", { name: "Remove", exact: true }).click(); + expect(await dialogMessage).toContain(`Remove "${name}"?`); + await expect(agentProfileRow(page, name)).toHaveCount(0, { timeout: 30_000 }); +} + +// ─── Settings: the edit modal ────────────────────────────────────────────── + +export interface AgentProfileDraft { + name?: string; + provider?: string; + model?: string; + thinking?: string; + mode?: string; + notes?: string; +} + +function editModal(page: Page): Locator { + return page.getByTestId("agent-profile-edit-modal"); +} + +async function chooseSelectFieldOption( + page: Page, + input: { field: "provider" | "model" | "mode" | "thinking"; option: string }, +): Promise { + await editModal(page).getByTestId(`agent-profile-${input.field}-trigger`).click(); + const option = page + .getByTestId("combobox-desktop-container") + .getByRole("button", { name: input.option, exact: true }); + await expect(option).toBeVisible({ timeout: 30_000 }); + await option.click(); + await expect( + page.getByRole("button", { name: `${capitalize(input.field)} (${input.option})` }), + ).toBeVisible({ timeout: 30_000 }); +} + +function capitalize(value: string): string { + return value.charAt(0).toUpperCase() + value.slice(1); +} + +async function fillAgentProfileForm(page: Page, draft: AgentProfileDraft): Promise { + const modal = editModal(page); + if (draft.name !== undefined) { + await modal.getByRole("textbox", { name: "Name", exact: true }).fill(draft.name); + } + // Provider first: it resets every provider-scoped field below it. + if (draft.provider !== undefined) { + await chooseSelectFieldOption(page, { field: "provider", option: draft.provider }); + } + // Thinking after model: thinking levels hang off the model, and switching the + // model drops a level the new one does not offer. + if (draft.model !== undefined) { + await chooseSelectFieldOption(page, { field: "model", option: draft.model }); + } + if (draft.thinking !== undefined) { + await chooseSelectFieldOption(page, { field: "thinking", option: draft.thinking }); + } + if (draft.mode !== undefined) { + await chooseSelectFieldOption(page, { field: "mode", option: draft.mode }); + } + if (draft.notes !== undefined) { + await modal.getByRole("textbox", { name: "Notes for agents", exact: true }).fill(draft.notes); + } +} + +async function saveAgentProfile(page: Page): Promise { + await editModal(page).getByRole("button", { name: "Save", exact: true }).click(); + await expect(editModal(page)).toHaveCount(0, { timeout: 30_000 }); +} + +export async function createAgentProfile(page: Page, draft: AgentProfileDraft): Promise { + await page.getByRole("button", { name: "Add agent profile", exact: true }).click(); + await expect(editModal(page)).toBeVisible({ timeout: 30_000 }); + await fillAgentProfileForm(page, draft); + await saveAgentProfile(page); +} + +export async function createAgentProfileFromEmptyState( + page: Page, + draft: AgentProfileDraft, +): Promise { + await page + .getByTestId("agent-profiles-card") + .getByRole("button", { name: "New profile", exact: true }) + .click(); + await expect(editModal(page)).toBeVisible({ timeout: 30_000 }); + await fillAgentProfileForm(page, draft); + await saveAgentProfile(page); +} + +export async function editAgentProfile( + page: Page, + name: string, + changes: AgentProfileDraft, +): Promise { + await agentProfileRow(page, name) + .getByRole("button", { name: "Edit profile", exact: true }) + .click(); + await expect(editModal(page)).toBeVisible({ timeout: 30_000 }); + await fillAgentProfileForm(page, changes); + await saveAgentProfile(page); +} + +/** Opens the modal for a profile purely to read back what it stored. */ +export async function expectAgentProfileForm( + page: Page, + name: string, + expected: { provider?: string; model?: string; mode?: string; notes?: string }, +): Promise { + await agentProfileRow(page, name) + .getByRole("button", { name: "Edit profile", exact: true }) + .click(); + const modal = editModal(page); + await expect(modal).toBeVisible({ timeout: 30_000 }); + await expect(modal.getByRole("textbox", { name: "Name", exact: true })).toHaveValue(name); + for (const [field, value] of Object.entries(expected)) { + if (field === "notes") { + await expect( + modal.getByRole("textbox", { name: "Notes for agents", exact: true }), + ).toHaveValue(value); + continue; + } + await expect( + modal.getByRole("button", { name: `${capitalize(field)} (${value})` }), + ).toBeVisible({ timeout: 30_000 }); + } + await modal.getByRole("button", { name: "Cancel", exact: true }).click(); + await expect(editModal(page)).toHaveCount(0, { timeout: 30_000 }); +} + +// ─── The model picker ────────────────────────────────────────────────────── + +/** Desktop web renders the model browser inside the combobox popover. */ +function pickerViewport(page: Page): Locator { + return page.getByTestId("combobox-desktop-container"); +} + +export async function openModelPicker(page: Page): Promise { + await page.getByTestId("combined-model-selector").filter({ visible: true }).first().click(); + await expect(pickerViewport(page)).toBeVisible({ timeout: 30_000 }); +} + +export async function closeModelPicker(page: Page): Promise { + await page.keyboard.press("Escape"); + await expect(pickerViewport(page)).toHaveCount(0, { timeout: 30_000 }); +} + +export function profilePickerRow(page: Page, name: string): Locator { + return pickerViewport(page) + .locator('[data-testid^="model-profile-row-"]') + .filter({ hasText: name }); +} + +/** + * Profiles are pinned above the provider list, so "pinned" is an ordering claim: + * the profile row's top edge sits above every provider row's. + */ +export async function expectProfilePinnedAboveProviders( + page: Page, + input: { name: string; summary: string }, +): Promise { + const viewport = pickerViewport(page); + await expect(viewport.getByText("Profiles", { exact: true })).toBeVisible({ timeout: 30_000 }); + const row = profilePickerRow(page, input.name); + await expect(row).toBeVisible({ timeout: 30_000 }); + await expect(row.getByText(input.summary, { exact: true })).toBeVisible(); + + const profileTop = await boxTop(row); + const providerRows = viewport.locator('[data-testid^="model-provider-"]'); + await expect(providerRows.first()).toBeVisible(); + const providerCount = await providerRows.count(); + for (let index = 0; index < providerCount; index += 1) { + expect(profileTop).toBeLessThan(await boxTop(providerRows.nth(index))); + } +} + +export async function expectProfileVisibleForProvider( + page: Page, + input: { name: string; summary: string }, +): Promise { + const viewport = pickerViewport(page); + await expect(viewport.getByText("Profiles", { exact: true })).toBeVisible({ timeout: 30_000 }); + const row = profilePickerRow(page, input.name); + await expect(row).toBeVisible({ timeout: 30_000 }); + await expect(row.getByText(input.summary, { exact: true })).toBeVisible(); +} + +export async function expectProfileEditIsPencilOnly(page: Page): Promise { + const viewport = pickerViewport(page); + await expect( + viewport.getByRole("button", { name: "Edit agent profiles", exact: true }), + ).toBeVisible(); + await expect(viewport.getByText("Edit", { exact: true })).toHaveCount(0); +} + +export async function expectAgentProfilesEmptyPrompt(page: Page): Promise { + const viewport = pickerViewport(page); + await expect(viewport.getByText("Profiles", { exact: true })).toHaveCount(0); + await expect( + viewport.getByRole("button", { name: "Edit agent profiles", exact: true }), + ).toHaveCount(0); + await expect(viewport.getByRole("button", { name: "Create profile", exact: true })).toBeVisible({ + timeout: 30_000, + }); +} + +export async function expectProfileEditTooltip(page: Page): Promise { + const edit = pickerViewport(page).getByRole("button", { + name: "Edit agent profiles", + exact: true, + }); + await edit.hover(); + await expect(page.getByText("Edit agent profiles", { exact: true })).toBeVisible(); +} + +export async function openAgentProfilesFromEmptyPrompt(page: Page): Promise { + await pickerViewport(page).getByRole("button", { name: "Create profile", exact: true }).click(); + await expect(page.getByTestId("agent-profiles-card")).toBeVisible({ timeout: 30_000 }); +} + +async function boxTop(locator: Locator): Promise { + const box = await locator.boundingBox(); + if (!box) { + throw new Error("Expected a laid-out element to measure"); + } + return box.y; +} + +export async function applyProfileFromPicker(page: Page, name: string): Promise { + await profilePickerRow(page, name).click(); + await expect(pickerViewport(page)).toHaveCount(0, { timeout: 30_000 }); +} + +/** + * Applying a profile materializes it and forgets it: nothing in the root view + * claims selection. Model rows do carry `aria-selected`, so a count of zero here + * is the absence of a checkmark, not the absence of the attribute everywhere. + */ +export async function expectNothingSelectedInPickerRoot(page: Page): Promise { + await expect(pickerViewport(page).locator("[aria-selected]")).toHaveCount(0); +} + +export async function expectAgentProfilesEditShortcut(page: Page): Promise { + await expect( + pickerViewport(page).getByRole("button", { name: "Edit agent profiles", exact: true }), + ).toBeVisible({ timeout: 30_000 }); +} + +export async function drillIntoProvider(page: Page, providerId: string): Promise { + await pickerViewport(page).getByTestId(`model-provider-${providerId}`).click(); +} + +export async function expectModelRowSelected( + page: Page, + input: { provider: string; modelId: string }, +): Promise { + await expect( + pickerViewport(page).getByTestId(`model-row-${input.provider}-${input.modelId}`), + ).toHaveAttribute("aria-selected", "true", { timeout: 30_000 }); +} + +// ─── Cross-provider model search ─────────────────────────────────────────── + +export async function searchAllModels(page: Page, query: string): Promise { + const input = page.getByTestId("model-search-all-input"); + await expect(input).toBeVisible({ timeout: 30_000 }); + await input.fill(query); +} + +export async function expectModelSearchResult( + page: Page, + expected: { provider: string; modelId: string; providerLabel: string; modelLabel: string }, +): Promise { + const row = pickerViewport(page).getByTestId( + `model-row-${expected.provider}-${expected.modelId}`, + ); + await expect(row).toBeVisible({ timeout: 30_000 }); + await expect(row.getByText(expected.modelLabel, { exact: true })).toBeVisible(); + await expect( + row.getByText(new RegExp(`^${escapeForRegex(expected.providerLabel)}\\b`)), + ).toBeVisible(); +} + +function escapeForRegex(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +/** Search results replace the whole root view, pinned profiles included. */ +export async function expectPinnedProfilesHidden(page: Page): Promise { + await expect(pickerViewport(page).locator('[data-testid^="model-profile-row-"]')).toHaveCount(0); +} + +export async function expectModelSearchEmptyState(page: Page, query: string): Promise { + const empty = page.getByTestId("model-search-empty"); + await expect(empty).toBeVisible({ timeout: 30_000 }); + await expect(empty.getByText(`No models match "${query}"`, { exact: true })).toBeVisible(); +} + +export async function readModelPickerHeight(page: Page): Promise { + const box = await pickerViewport(page).boundingBox(); + if (!box) { + throw new Error("Expected the model picker to be laid out"); + } + return box.height; +} + +export async function readModelPickerWidth(page: Page): Promise { + const box = await pickerViewport(page).boundingBox(); + if (!box) { + throw new Error("Expected the model picker to be laid out"); + } + return box.width; +} + +export async function expectModelPickerHeight(page: Page, expectedHeight: number): Promise { + await expect.poll(() => readModelPickerHeight(page)).toBe(expectedHeight); +} + +export async function expectModelPickerWidth(page: Page, expectedWidth: number): Promise { + await expect.poll(() => readModelPickerWidth(page)).toBe(expectedWidth); +} + +export async function expectSearchResultsVirtualized( + page: Page, + input: { provider: string; total: number }, +): Promise { + const mountedRows = pickerViewport(page).locator(`[data-testid^="model-row-${input.provider}-"]`); + await expect.poll(() => mountedRows.count()).toBeGreaterThan(0); + expect(await mountedRows.count()).toBeLessThan(input.total); +} + +// ─── Composer agent controls ─────────────────────────────────────────────── + +export async function expectComposerModel(page: Page, modelLabel: string): Promise { + await expect( + page.getByRole("button", { name: `Select model (${modelLabel})`, exact: true }), + ).toBeVisible({ timeout: 30_000 }); +} + +export async function expectComposerMode(page: Page, modeLabel: string): Promise { + await expect( + page.getByRole("button", { name: `Select agent mode (${modeLabel})`, exact: true }).first(), + ).toBeVisible({ timeout: 30_000 }); +} + +/** The composer never shows the profile's name — a profile is not a selection. */ +export async function expectComposerDoesNotName(page: Page, profileName: string): Promise { + await expect( + page.locator('[data-testid="message-input-root"]:visible').getByText(profileName), + ).toHaveCount(0); +} diff --git a/packages/app/e2e/support/helpers/archive-tab.ts b/packages/app/e2e/support/helpers/archive-tab.ts index 201bb189ab..c33414412a 100644 --- a/packages/app/e2e/support/helpers/archive-tab.ts +++ b/packages/app/e2e/support/helpers/archive-tab.ts @@ -26,7 +26,7 @@ function buildSeededStoragePayload() { endpoint: `127.0.0.1:${getE2EDaemonPort()}`, nowIso, }), - preferences: buildCreateAgentPreferences(getServerId()), + preferences: buildCreateAgentPreferences(), }; } diff --git a/packages/app/e2e/support/helpers/command-center-file-search.ts b/packages/app/e2e/support/helpers/command-center-file-search.ts new file mode 100644 index 0000000000..781470b582 --- /dev/null +++ b/packages/app/e2e/support/helpers/command-center-file-search.ts @@ -0,0 +1,211 @@ +import { expect, type Locator, type Page } from "@playwright/test"; +import { daemonWsRoutePattern } from "./daemon-port"; + +type WebSocketMessage = string | Buffer; + +interface SurfaceRect { + height: number; + width: number; + x: number; + y: number; +} + +interface LayoutObservation { + active: boolean; + layoutShift: number; + samples: Record; +} + +type LayoutOracleWindow = Window & { + __commandCenterLayoutObservation?: LayoutObservation; +}; + +const SURFACE_SELECTORS = { + panel: '[data-testid="command-center-panel"]', + header: '[data-testid="command-center-header"]', + input: '[data-testid="command-center-input"]', + fileSearchStatus: '[data-testid="command-center-file-search-status"]', + results: '[data-testid="command-center-results"]', + scope: '[data-testid="command-center-files-scope"]', + fileRow: '[data-testid^="command-center-file-row-"]', + fileIcon: '[data-testid="command-center-file-icon"]', + fileLine: '[data-testid="command-center-file-line"]', +} as const; + +function parseSessionMessage(message: WebSocketMessage): Record | null { + const raw = typeof message === "string" ? message : message.toString("utf8"); + try { + const envelope = JSON.parse(raw) as { type?: unknown; message?: unknown }; + return envelope.type === "session" && envelope.message && typeof envelope.message === "object" + ? (envelope.message as Record) + : null; + } catch { + return null; + } +} + +export async function delayDirectorySuggestionResponses(page: Page, delayMs = 400): Promise { + await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { + const server = ws.connectToServer(); + ws.onMessage((message) => server.send(message)); + server.onMessage((message) => { + const sessionMessage = parseSessionMessage(message); + if (sessionMessage?.type === "directory_suggestions_response") { + setTimeout(() => ws.send(message), delayMs); + return; + } + ws.send(message); + }); + }); +} + +export async function failDirectorySuggestionRequests(page: Page): Promise { + await page.routeWebSocket(daemonWsRoutePattern(), (ws) => { + const server = ws.connectToServer(); + ws.onMessage((message) => { + const sessionMessage = parseSessionMessage(message); + if (sessionMessage?.type === "directory_suggestions_request") { + const requestId = sessionMessage.requestId; + if (typeof requestId === "string") { + ws.send( + JSON.stringify({ + type: "session", + message: { + type: "rpc_error", + payload: { + requestId, + requestType: "directory_suggestions_request", + error: "Test file search transport failure.", + code: "transport", + }, + }, + }), + ); + } + return; + } + server.send(message); + }); + server.onMessage((message) => ws.send(message)); + }); +} + +export async function startCommandCenterLayoutObservation(page: Page): Promise { + await page.evaluate((selectors) => { + const oracleWindow = window as LayoutOracleWindow; + const observation: LayoutObservation = { active: true, layoutShift: 0, samples: {} }; + oracleWindow.__commandCenterLayoutObservation = observation; + + const observedElements = new WeakSet(); + const resizeObserver = new ResizeObserver(record); + const mutationObserver = new MutationObserver(discover); + + function record(): void { + if (!observation.active) return; + for (const [name, selector] of Object.entries(selectors)) { + const element = document.querySelector(selector); + if (!element) continue; + const rect = element.getBoundingClientRect(); + (observation.samples[name] ??= []).push({ + height: rect.height, + width: rect.width, + x: rect.x, + y: rect.y, + }); + } + } + + function discover(): void { + for (const selector of Object.values(selectors)) { + const element = document.querySelector(selector); + if (!element || observedElements.has(element)) continue; + observedElements.add(element); + resizeObserver.observe(element); + } + record(); + } + + mutationObserver.observe(document.body, { attributes: true, childList: true, subtree: true }); + discover(); + + const sampleFrame = () => { + if (!observation.active) { + mutationObserver.disconnect(); + resizeObserver.disconnect(); + return; + } + record(); + requestAnimationFrame(sampleFrame); + }; + requestAnimationFrame(sampleFrame); + + if (typeof PerformanceObserver !== "undefined") { + try { + const performanceObserver = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + observation.layoutShift += (entry as PerformanceEntry & { value?: number }).value ?? 0; + } + }); + performanceObserver.observe({ type: "layout-shift", buffered: false }); + } catch { + // LayoutShift is diagnostic-only on engines that do not expose it. + } + } + }, SURFACE_SELECTORS); +} + +export async function expectStableCommandCenterLayout(page: Page): Promise { + await page.waitForTimeout(500); + const observation = await page.evaluate(() => { + const state = (window as LayoutOracleWindow).__commandCenterLayoutObservation; + if (!state) throw new Error("Command center layout observer was not installed"); + state.active = false; + return state; + }); + + for (const surface of Object.keys(SURFACE_SELECTORS)) { + const samples = observation.samples[surface] ?? []; + expect(samples.length, `${surface}: ${JSON.stringify(observation)}`).toBeGreaterThan(0); + for (const dimension of ["height", "width", "x", "y"] as const) { + const values = samples.map((sample) => sample[dimension]); + expect( + Math.max(...values) - Math.min(...values), + `${surface}.${dimension}: ${JSON.stringify(samples)}`, + ).toBeLessThanOrEqual(1); + } + } + expect(observation.layoutShift, JSON.stringify(observation)).toBeLessThanOrEqual(0.001); +} + +export async function expectOneLineTruncatedFileResult( + row: Locator, + input: { filename: string; path: string }, +): Promise { + await expect(row).toContainText(`${input.filename} ${input.path}`); + await expect(row.getByTestId("command-center-file-icon")).toBeVisible(); + const line = row.getByTestId("command-center-file-line"); + const filename = row.getByTestId("command-center-file-name"); + const filePath = row.getByTestId("command-center-file-path"); + const geometry = await Promise.all([ + row.boundingBox(), + line.boundingBox(), + filename.boundingBox(), + filePath.boundingBox(), + line.evaluate((element) => ({ + clientWidth: element.clientWidth, + overflow: getComputedStyle(element).overflow, + scrollWidth: element.scrollWidth, + whiteSpace: getComputedStyle(element).whiteSpace, + })), + ]); + const [rowBox, lineBox, filenameBox, pathBox, overflow] = geometry; + expect(rowBox).not.toBeNull(); + expect(lineBox).not.toBeNull(); + expect(filenameBox).not.toBeNull(); + expect(pathBox).not.toBeNull(); + expect(rowBox?.height).toBe(36); + expect(Math.abs((filenameBox?.y ?? 0) - (pathBox?.y ?? 0))).toBeLessThanOrEqual(1); + expect(overflow.scrollWidth).toBeGreaterThan(overflow.clientWidth); + expect(overflow.overflow).toBe("hidden"); + expect(overflow.whiteSpace).toBe("nowrap"); +} diff --git a/packages/app/e2e/support/helpers/daemon-registry.ts b/packages/app/e2e/support/helpers/daemon-registry.ts index 6ef72ddc26..f3b63a7518 100644 --- a/packages/app/e2e/support/helpers/daemon-registry.ts +++ b/packages/app/e2e/support/helpers/daemon-registry.ts @@ -1,14 +1,23 @@ +import type { + FormPreferences, + ProviderPreferences, +} from "../../../src/create-agent-preferences/preferences"; + export const TEST_HOST_LABEL = "localhost"; export const TEST_PROVIDER_PREFERENCES = { claude: { model: "haiku" }, - codex: { model: "gpt-5.4-mini", thinkingOptionId: "low" }, -} as const; + codex: { model: "gpt-5.4-mini", thinkingByModel: { "gpt-5.4-mini": "low" } }, +} satisfies Record; -export function buildDirectTcpConnection(endpoint: string) { +export function buildDirectTcpConnection(endpoint: string): { + id: string; + type: "directTcp"; + endpoint: string; +} { return { id: `direct:${endpoint}`, - type: "directTcp" as const, + type: "directTcp", endpoint, }; } @@ -33,12 +42,11 @@ export function buildSeededHost(input: { export const TEST_MOCK_PROVIDER_PREFERENCES = { ...TEST_PROVIDER_PREFERENCES, mock: { model: "ten-second-stream" }, -} as const; +} satisfies Record; -export function buildCreateAgentPreferences(serverId: string) { +export function buildCreateAgentPreferences() { return { - serverId, - provider: "mock" as const, + provider: "mock", providerPreferences: TEST_MOCK_PROVIDER_PREFERENCES, - }; + } satisfies FormPreferences; } diff --git a/packages/app/e2e/support/helpers/diagram.ts b/packages/app/e2e/support/helpers/diagram.ts new file mode 100644 index 0000000000..f3d2742c12 --- /dev/null +++ b/packages/app/e2e/support/helpers/diagram.ts @@ -0,0 +1,48 @@ +import { expect, type Page } from "@playwright/test"; +import type { MockAgentWorkspace } from "./mock-agent"; + +const DIAGRAM_NAME = "Diagram"; + +function renderedDiagram(page: Page) { + const diagram = page.getByRole("img", { name: DIAGRAM_NAME }).last(); + const svg = diagram.locator("iframe").contentFrame().locator("#diagram svg"); + return { diagram, svg }; +} + +export async function requestDiagram(agent: MockAgentWorkspace): Promise { + await agent.client.sendAgentMessage(agent.agentId, "Render the requested Mermaid diagram."); +} + +export async function expectDiagramWithLabels( + page: Page, + labels: readonly string[], +): Promise { + const { diagram, svg } = renderedDiagram(page); + await expect(diagram).toBeVisible({ timeout: 30_000 }); + await expect(svg).toBeVisible({ timeout: 30_000 }); + for (const label of labels) { + await expect(svg).toContainText(label); + } +} + +export async function expectDiagramRemainsRenderedWhileStreaming(page: Page): Promise { + const { diagram, svg } = renderedDiagram(page); + const samples = 80; + for (let sample = 0; sample < samples; sample += 1) { + await expect(diagram).toBeVisible({ timeout: 100 }); + await expect(svg).toBeVisible({ timeout: 100 }); + await page.waitForTimeout(25); + } +} + +export async function waitForDiagramTurnToComplete(agent: MockAgentWorkspace): Promise { + await agent.client.waitForFinish(agent.agentId, 30_000); +} + +export async function expectCompletedDiagram(page: Page, labels: readonly string[]): Promise { + await expectDiagramWithLabels(page, labels); +} + +export async function reloadConversation(page: Page): Promise { + await page.reload(); +} diff --git a/packages/app/e2e/support/helpers/project-settings.ts b/packages/app/e2e/support/helpers/project-settings.ts index 703a0fa83b..8b3b23faaf 100644 --- a/packages/app/e2e/support/helpers/project-settings.ts +++ b/packages/app/e2e/support/helpers/project-settings.ts @@ -1,4 +1,5 @@ import { chmod, readFile, writeFile } from "node:fs/promises"; +import { execFileSync } from "node:child_process"; import path from "node:path"; import { expect, type Page } from "@playwright/test"; import type { WebSocketRoute } from "@playwright/test"; @@ -177,6 +178,19 @@ export async function expectSaveButtonDisabled(page: Page): Promise { await expect(page.getByTestId("save-button")).toBeDisabled(); } +export async function expectUncommittedSetupWarning(page: Page): Promise { + const warning = page.getByRole("alert").filter({ hasText: "Commit paseo.json changes" }); + await expect(warning).toContainText("Commit paseo.json changes"); + await expect(warning).toContainText( + "New worktrees use the setup script from the base branch you select.", + ); +} + +export async function expectNoUncommittedSetupWarning(page: Page): Promise { + const warning = page.getByRole("alert").filter({ hasText: "Commit paseo.json changes" }); + await expect(warning).toHaveCount(0); +} + // --- Form-state assertions --- export async function expectProjectSettingsFormVisible(page: Page): Promise { @@ -249,6 +263,11 @@ export async function restorePaseoConfig( await writeFile(path.join(repoPath, "paseo.json"), JSON.stringify(config, null, 2) + "\n"); } +export function commitPaseoConfig(repoPath: string): void { + execFileSync("git", ["add", "paseo.json"], { cwd: repoPath }); + execFileSync("git", ["commit", "-m", "Update project config"], { cwd: repoPath }); +} + // The daemon writes atomically via a temp file + rename, so blocking writes requires // removing write permission from the *directory*, not just the file. export async function blockPaseoConfigWrites(repoPath: string): Promise { diff --git a/packages/app/e2e/support/helpers/relay-deployment.ts b/packages/app/e2e/support/helpers/relay-deployment.ts index 19e480d486..3d727e0804 100644 --- a/packages/app/e2e/support/helpers/relay-deployment.ts +++ b/packages/app/e2e/support/helpers/relay-deployment.ts @@ -60,7 +60,7 @@ export async function connectDaemonWebAppOnlyThroughRelay( localStorage.setItem("@paseo:daemon-registry", JSON.stringify([storedHost])); localStorage.setItem("@paseo:create-agent-preferences", JSON.stringify(preferences)); }, - { storedHost: host, preferences: buildCreateAgentPreferences(offer.serverId) }, + { storedHost: host, preferences: buildCreateAgentPreferences() }, ); const relaySocketOpened = new Promise((resolve) => { diff --git a/packages/app/e2e/support/helpers/replica-cache-perf.ts b/packages/app/e2e/support/helpers/replica-cache-perf.ts new file mode 100644 index 0000000000..5b48b02b9c --- /dev/null +++ b/packages/app/e2e/support/helpers/replica-cache-perf.ts @@ -0,0 +1,48 @@ +import type { Page } from "@playwright/test"; +import { + observeReplicaCacheStorageWrites, + readReplicaCacheStorageWriteObserver, + resetReplicaCacheStorageWriteObserver, +} from "./replica-cache-storage"; + +export interface ReplicaCachePerfReport { + elapsedMs: number; + redundantWrites: number; + serializedChars: number; + storageDurationMs: number; + writes: number; +} + +interface ReplicaCachePerfState { + resetAt: number; +} + +declare global { + interface Window { + __replicaCachePerf?: ReplicaCachePerfState; + } +} + +export async function observeReplicaCacheWrites(page: Page): Promise { + await observeReplicaCacheStorageWrites(page); + await page.addInitScript(() => { + window.__replicaCachePerf = { resetAt: performance.now() }; + }); +} + +export async function beginReplicaCacheMeasurement(page: Page): Promise { + await resetReplicaCacheStorageWriteObserver(page); + await page.evaluate(() => { + window.__replicaCachePerf = { resetAt: performance.now() }; + }); +} + +export async function readReplicaCacheMeasurement(page: Page): Promise { + const storage = await readReplicaCacheStorageWriteObserver(page); + const elapsedMs = await page.evaluate(() => { + const state = window.__replicaCachePerf; + if (!state) throw new Error("Replica cache performance observer is not installed"); + return performance.now() - state.resetAt; + }); + return { elapsedMs, ...storage }; +} diff --git a/packages/app/e2e/support/helpers/replica-cache-storage.ts b/packages/app/e2e/support/helpers/replica-cache-storage.ts new file mode 100644 index 0000000000..8d42be348d --- /dev/null +++ b/packages/app/e2e/support/helpers/replica-cache-storage.ts @@ -0,0 +1,150 @@ +import type { Page } from "@playwright/test"; + +const DATABASE_NAME = "paseo-replica-cache"; +const STORE_NAME = "key-value"; +const STORAGE_KEY = "@paseo:replica-cache"; + +export interface ReplicaCacheRecord { + hosts?: Array<{ + timeline?: { + agentId?: string; + items?: Array>; + } | null; + }>; +} + +interface ReplicaCacheWriteObserverState { + lastValue: string | null; + redundantWrites: number; + serializedChars: number; + storageDurationMs: number; + writes: number; +} + +declare global { + interface Window { + __replicaCacheWriteObserver?: ReplicaCacheWriteObserverState; + } +} + +async function readStoredValue(input: { + databaseName: string; + storeName: string; + storageKey: string; +}): Promise { + const database = await new Promise((resolve, reject) => { + const request = indexedDB.open(input.databaseName, 1); + request.addEventListener("success", () => resolve(request.result)); + request.addEventListener("error", () => reject(request.error)); + }); + return new Promise((resolve, reject) => { + const request = database + .transaction(input.storeName, "readonly") + .objectStore(input.storeName) + .get(input.storageKey); + request.addEventListener("success", () => + resolve(typeof request.result === "string" ? request.result : null), + ); + request.addEventListener("error", () => reject(request.error)); + }); +} + +export async function readReplicaCache(page: Page): Promise { + const raw = await page.evaluate(readStoredValue, { + databaseName: DATABASE_NAME, + storeName: STORE_NAME, + storageKey: STORAGE_KEY, + }); + return raw ? (JSON.parse(raw) as ReplicaCacheRecord) : null; +} + +export async function writeReplicaCache(page: Page, value: ReplicaCacheRecord): Promise { + await page.evaluate( + async ({ databaseName, storeName, storageKey, raw }) => { + const database = await new Promise((resolve, reject) => { + const request = indexedDB.open(databaseName, 1); + request.addEventListener("success", () => resolve(request.result)); + request.addEventListener("error", () => reject(request.error)); + }); + await new Promise((resolve, reject) => { + const transaction = database.transaction(storeName, "readwrite"); + transaction.objectStore(storeName).put(raw, storageKey); + transaction.addEventListener("complete", () => resolve()); + transaction.addEventListener("error", () => reject(transaction.error)); + transaction.addEventListener("abort", () => reject(transaction.error)); + }); + }, + { + databaseName: DATABASE_NAME, + storeName: STORE_NAME, + storageKey: STORAGE_KEY, + raw: JSON.stringify(value), + }, + ); +} + +export async function observeReplicaCacheStorageWrites(page: Page): Promise { + await page.addInitScript( + ({ databaseName, storeName, storageKey }) => { + window.__replicaCacheWriteObserver = { + lastValue: null, + redundantWrites: 0, + serializedChars: 0, + storageDurationMs: 0, + writes: 0, + }; + const originalPut = IDBObjectStore.prototype.put; + IDBObjectStore.prototype.put = function measuredPut(value: unknown, key?: IDBValidKey) { + const request = + key === undefined ? originalPut.call(this, value) : originalPut.call(this, value, key); + if ( + this.transaction.db.name !== databaseName || + this.name !== storeName || + key !== storageKey || + typeof value !== "string" + ) { + return request; + } + const startedAt = performance.now(); + request.addEventListener("success", () => { + const state = window.__replicaCacheWriteObserver; + if (!state) return; + if (state.lastValue === value) state.redundantWrites += 1; + state.lastValue = value; + state.writes += 1; + state.serializedChars += value.length; + state.storageDurationMs += performance.now() - startedAt; + }); + return request; + }; + }, + { databaseName: DATABASE_NAME, storeName: STORE_NAME, storageKey: STORAGE_KEY }, + ); +} + +export async function resetReplicaCacheStorageWriteObserver(page: Page): Promise { + const lastValue = await page.evaluate(readStoredValue, { + databaseName: DATABASE_NAME, + storeName: STORE_NAME, + storageKey: STORAGE_KEY, + }); + await page.evaluate((value) => { + window.__replicaCacheWriteObserver = { + lastValue: value, + redundantWrites: 0, + serializedChars: 0, + storageDurationMs: 0, + writes: 0, + }; + }, lastValue); +} + +export async function readReplicaCacheStorageWriteObserver( + page: Page, +): Promise { + return page.evaluate(() => { + const state = window.__replicaCacheWriteObserver; + if (!state) throw new Error("Replica cache write observer is not installed"); + return state; + }); +} diff --git a/packages/app/e2e/support/helpers/seed-client.ts b/packages/app/e2e/support/helpers/seed-client.ts index 00e660c4a3..723a0ae4cb 100644 --- a/packages/app/e2e/support/helpers/seed-client.ts +++ b/packages/app/e2e/support/helpers/seed-client.ts @@ -42,6 +42,7 @@ export interface SeedDaemonClient { fetchWorkspaces(options?: { filter?: { projectId?: string } }): Promise<{ entries: SeedWorkspaceDescriptor[]; }>; + setWorkspacePinned(workspaceId: string, pinned: boolean): Promise<{ pinnedAt: string | null }>; listProjects(): Promise<{ projects: SeedProjectDescriptor[] }>; createWorkspace(input: { source: diff --git a/packages/app/e2e/support/helpers/settings.ts b/packages/app/e2e/support/helpers/settings.ts index 9a3a125124..2af795375b 100644 --- a/packages/app/e2e/support/helpers/settings.ts +++ b/packages/app/e2e/support/helpers/settings.ts @@ -36,9 +36,11 @@ type HostSection = | "connections" | "pair-device" | "agents" + | "metadata" | "workspaces" | "providers" | "usage" + | "terminals" | "host"; export async function openSettingsSection(page: Page, section: SettingsSection): Promise { @@ -130,7 +132,7 @@ export async function seedSavedSettingsHosts( if (!firstHost) { throw new Error("Expected at least one settings host fixture."); } - const preferences = buildCreateAgentPreferences(firstHost.serverId); + const preferences = buildCreateAgentPreferences(); await page.evaluate( ({ keys, storedRegistry, storedPreferences }) => { diff --git a/packages/app/e2e/support/helpers/startup-dsl.ts b/packages/app/e2e/support/helpers/startup-dsl.ts index 96ad96f3c6..fe78109fe5 100644 --- a/packages/app/e2e/support/helpers/startup-dsl.ts +++ b/packages/app/e2e/support/helpers/startup-dsl.ts @@ -106,7 +106,7 @@ class StartupScenario { if (!firstHost) { throw new Error("Expected at least one startup test host."); } - const createAgentPreferences = buildStoredCreateAgentPreferences(firstHost.serverId); + const createAgentPreferences = buildStoredCreateAgentPreferences(); await this.page.evaluate( ({ keys, registry: storedRegistry, createAgentPreferences: storedPreferences }) => { @@ -245,6 +245,6 @@ function buildStoredHost(input: { return buildSeededHost(input); } -function buildStoredCreateAgentPreferences(serverId: string) { - return buildCreateAgentPreferences(serverId); +function buildStoredCreateAgentPreferences() { + return buildCreateAgentPreferences(); } diff --git a/packages/app/e2e/support/helpers/task-list.ts b/packages/app/e2e/support/helpers/task-list.ts new file mode 100644 index 0000000000..368e279fcd --- /dev/null +++ b/packages/app/e2e/support/helpers/task-list.ts @@ -0,0 +1,24 @@ +import { expect, type Page } from "@playwright/test"; + +export async function openCompletedTaskList(page: Page, count: number): Promise { + const taskButton = page.getByRole("button", { name: `${count}/${count} tasks` }); + await expect(taskButton).toBeVisible({ timeout: 60_000 }); + await taskButton.click(); +} + +export async function expectTaskListEntries(page: Page, entries: readonly string[]): Promise { + for (const entry of entries) { + await expect(page.getByLabel(entry, { exact: true })).toBeVisible(); + } +} + +export async function expectCompletedTaskActivity( + page: Page, + entries: readonly string[], +): Promise { + for (const entry of entries) { + await expect( + page.getByRole("button", { name: `Completed ${entry}`, exact: true }), + ).toBeVisible(); + } +} diff --git a/packages/app/e2e/support/helpers/timeline-pagination.ts b/packages/app/e2e/support/helpers/timeline-pagination.ts index 8acdcac3b6..f7c9722955 100644 --- a/packages/app/e2e/support/helpers/timeline-pagination.ts +++ b/packages/app/e2e/support/helpers/timeline-pagination.ts @@ -1,5 +1,6 @@ import { expect, type Page } from "@playwright/test"; import { buildAgentRoute, seedMockAgentWorkspace, type MockAgentWorkspace } from "./mock-agent"; +import { readReplicaCache } from "./replica-cache-storage"; import { delayAgentBootstrapTailResponse, delayAgentOlderTimelineResponse, @@ -284,22 +285,13 @@ export async function reloadAgentTimelineFromPersistedReplica( agent: LongTimelineAgent, ): Promise { await expect - .poll(() => - page.evaluate((agentId) => { - const raw = localStorage.getItem("@paseo:replica-cache"); - if (!raw) return false; - const cache = JSON.parse(raw) as { - hosts?: Array<{ - timeline?: { - agentId?: string; - items?: unknown[]; - } | null; - }>; - }; - const timeline = cache.hosts?.find((host) => host.timeline?.agentId === agentId)?.timeline; - return timeline?.items?.length === 50; - }, agent.agentId), - ) + .poll(async () => { + const cache = await readReplicaCache(page); + const timeline = cache?.hosts?.find( + (host) => host.timeline?.agentId === agent.agentId, + )?.timeline; + return timeline?.items?.length === 50; + }) .toBe(true); await page.reload(); diff --git a/packages/app/package.json b/packages/app/package.json index 6b1901f7cd..89e183cf6a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/app", - "version": "0.3.1", + "version": "0.4.0", "private": true, "main": "index.ts", "scripts": { @@ -29,7 +29,8 @@ "build:web": "npm --prefix ../.. run build:app-deps && expo export --platform web", "profile:workspace-tabs": "node ./scripts/profile-workspace-tabs.mjs", "deploy:web": "npm run build:web && wrangler pages deploy dist --project-name paseo-app --branch main", - "build:terminal-webview": "node ./scripts/build-terminal-webview-html.mjs" + "build:terminal-webview": "node ./scripts/build-terminal-webview-html.mjs", + "build:mermaid-runtime": "node ./src/components/markdown/fence/mermaid/build-runtime.mjs" }, "dependencies": { "@codemirror/commands": "6.10.4", @@ -47,6 +48,7 @@ "@getpaseo/protocol": "*", "@gorhom/bottom-sheet": "^5.2.14", "@gorhom/portal": "^1.0.14", + "@mattermost/react-native-paste-input": "2.0.1", "@react-native-async-storage/async-storage": "2.2.0", "@react-native-masked-view/masked-view": "^0.3.2", "@react-native/normalize-colors": "^0.81.5", @@ -92,6 +94,7 @@ "i18next": "^26.3.0", "lucide-react-native": "^0.546.0", "markdown-it": "^10.0.0", + "mermaid": "^11.16.0", "mnemonic-id": "^3.2.7", "qrcode": "^1.5.4", "react": "19.1.0", @@ -117,6 +120,7 @@ "turndown": "^7.2.4", "turndown-plugin-gfm": "^1.0.2", "use-sync-external-store": "^1.6.0", + "yaml": "^2.8.4", "zod": "^4.4.3", "zustand": "^5.0.9" }, @@ -134,6 +138,7 @@ "@xterm/headless": "^6.1.0-beta.213", "dotenv": "^17.2.3", "eas-cli": "^16.24.1", + "esbuild": "0.28.1", "eslint": "^9.25.0", "eslint-config-expo": "~10.0.0", "expo-gradle-jvmargs": "^1.1.2", diff --git a/packages/app/plugins/with-android-async-storage-size.js b/packages/app/plugins/with-android-async-storage-size.js new file mode 100644 index 0000000000..5890583563 --- /dev/null +++ b/packages/app/plugins/with-android-async-storage-size.js @@ -0,0 +1,24 @@ +const { withGradleProperties } = require("expo/config-plugins"); + +const ASYNC_STORAGE_SIZE_PROPERTY = "AsyncStorage_db_size_in_MB"; + +function withAndroidAsyncStorageSize(config, sizeInMegabytes) { + return withGradleProperties(config, (modConfig) => { + const existingProperty = modConfig.modResults.find( + (property) => property.type === "property" && property.key === ASYNC_STORAGE_SIZE_PROPERTY, + ); + if (existingProperty?.type === "property") { + existingProperty.value = String(sizeInMegabytes); + return modConfig; + } + + modConfig.modResults.push({ + type: "property", + key: ASYNC_STORAGE_SIZE_PROPERTY, + value: String(sizeInMegabytes), + }); + return modConfig; + }); +} + +module.exports = withAndroidAsyncStorageSize; diff --git a/packages/app/plugins/with-paste-input.js b/packages/app/plugins/with-paste-input.js new file mode 100644 index 0000000000..76be6a8fcd --- /dev/null +++ b/packages/app/plugins/with-paste-input.js @@ -0,0 +1,82 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { + IOSConfig, + withAppDelegate, + withDangerousMod, + withPodfile, +} = require("expo/config-plugins"); + +const MODULE_SETUP = " PasteInputModule.setup(factory.rootViewFactory)"; +const REACT_NATIVE_START_END = " launchOptions: launchOptions)\n#endif"; +const BRIDGING_HEADER_IMPORT = "#import "; +const PODFILE_NEW_ARCH_DISABLED = + "ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false'"; +const PODFILE_NEW_ARCH_ENABLED = + "ENV['RCT_NEW_ARCH_ENABLED'] ||= '1' if podfile_properties['newArchEnabled'] == 'true'"; + +function configurePasteInputAppDelegate(contents) { + const configured = contents.replace(`\n${MODULE_SETUP}`, ""); + if (!configured.includes(REACT_NATIVE_START_END)) { + throw new Error("Could not register paste input after React Native starts"); + } + return configured.replace( + REACT_NATIVE_START_END, + ` launchOptions: launchOptions)\n${MODULE_SETUP}\n#endif`, + ); +} + +function configurePasteInputBridgingHeader(contents) { + if (contents.includes(BRIDGING_HEADER_IMPORT)) { + return contents; + } + return `${contents.trimEnd()}\n${BRIDGING_HEADER_IMPORT}\n`; +} + +function configurePasteInputPodfile(contents) { + if (contents.includes(PODFILE_NEW_ARCH_ENABLED)) { + return contents; + } + if (!contents.includes(PODFILE_NEW_ARCH_DISABLED)) { + throw new Error("Could not configure the new architecture for paste input"); + } + return contents.replace( + PODFILE_NEW_ARCH_DISABLED, + `${PODFILE_NEW_ARCH_DISABLED}\n${PODFILE_NEW_ARCH_ENABLED}`, + ); +} + +function withPasteInput(config) { + const withDelegate = withAppDelegate(config, (modConfig) => { + if (modConfig.modResults.language !== "swift") { + throw new Error("Paste input setup requires a Swift AppDelegate"); + } + modConfig.modResults.contents = configurePasteInputAppDelegate(modConfig.modResults.contents); + return modConfig; + }); + const withPods = withPodfile(withDelegate, (modConfig) => { + modConfig.modResults.contents = configurePasteInputPodfile(modConfig.modResults.contents); + return modConfig; + }); + return withPasteInputBridgingHeader(withPods); +} + +function withPasteInputBridgingHeader(config) { + return withDangerousMod(config, [ + "ios", + async (modConfig) => { + const projectRoot = modConfig.modRequest.projectRoot; + const sourceRoot = IOSConfig.Paths.getSourceRoot(projectRoot); + const projectName = IOSConfig.XcodeUtils.getProjectName(projectRoot); + const headerPath = path.join(sourceRoot, `${projectName}-Bridging-Header.h`); + const contents = fs.readFileSync(headerPath, "utf8"); + fs.writeFileSync(headerPath, configurePasteInputBridgingHeader(contents)); + return modConfig; + }, + ]); +} + +module.exports = withPasteInput; +module.exports.configurePasteInputAppDelegate = configurePasteInputAppDelegate; +module.exports.configurePasteInputBridgingHeader = configurePasteInputBridgingHeader; +module.exports.configurePasteInputPodfile = configurePasteInputPodfile; diff --git a/packages/app/plugins/with-paste-input.test.ts b/packages/app/plugins/with-paste-input.test.ts new file mode 100644 index 0000000000..fdc271d7ab --- /dev/null +++ b/packages/app/plugins/with-paste-input.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; + +const { + configurePasteInputAppDelegate, + configurePasteInputBridgingHeader, + configurePasteInputPodfile, +} = require("./with-paste-input"); + +describe("withPasteInput", () => { + it("registers the module after React Native starts", () => { + const source = [ + "import ReactAppDependencyProvider", + "", + " bindReactNativeFactory(factory)", + " factory.startReactNative(", + ' withModuleName: "main",', + " in: window,", + " launchOptions: launchOptions)", + "#endif", + ].join("\n"); + const configured = configurePasteInputAppDelegate(source); + + expect(configured).toContain( + " launchOptions: launchOptions)\n PasteInputModule.setup(factory.rootViewFactory)\n#endif", + ); + expect(configurePasteInputAppDelegate(configured)).toBe(configured); + }); + + it("exposes the Objective-C module to Swift through the bridging header", () => { + const source = "// Swift bridging header\n"; + const configured = configurePasteInputBridgingHeader(source); + + expect(configured).toContain("#import "); + expect(configurePasteInputBridgingHeader(configured)).toBe(configured); + }); + + it("sets the new architecture environment before CocoaPods evaluates dependencies", () => { + const source = + "ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false'"; + const configured = configurePasteInputPodfile(source); + + expect(configured).toContain( + "ENV['RCT_NEW_ARCH_ENABLED'] ||= '1' if podfile_properties['newArchEnabled'] == 'true'", + ); + expect(configurePasteInputPodfile(configured)).toBe(configured); + }); +}); diff --git a/packages/app/src/agent-profiles/index.ts b/packages/app/src/agent-profiles/index.ts new file mode 100644 index 0000000000..d2ffebe78b --- /dev/null +++ b/packages/app/src/agent-profiles/index.ts @@ -0,0 +1,27 @@ +/** + * Agent profiles: named bundles of composer settings (provider, model, mode, + * thinking option, feature values, notes) stored host-wide in daemon config. + * + * Four capabilities leave this module — managing the list in settings, reading + * it, pinning it to the model picker, and drawing a profile's glyph. Everything + * else (the form model, the catalog and feature probes, the materialization + * rules, the icon registry, the row and modal chrome) is internal; import from + * `@/agent-profiles`, never a path inside it. + * + * `useAgentProfilePicker` deliberately hands the picker a flat row view model + * and one `applyProfile(id)` callback rather than the profiles themselves: what + * a profile contains, and how it reaches a live agent versus a draft, stays in + * here. + */ +export type { AgentProfile } from "@getpaseo/protocol/messages"; +export type { MaterializedAgentProfile } from "./internal/materialize-profile"; +export { useAgentProfiles } from "./internal/use-agent-profiles"; +export { + useAgentProfilePicker, + type AgentProfileApplyTarget, + type AgentProfilePicker, + type AgentProfilePickerRow, + type DraftAgentProfileControls, +} from "./internal/use-agent-profile-picker"; +export { AgentProfileGlyph } from "./internal/agent-profile-glyph"; +export { AgentProfilesSection } from "./settings/agent-profiles-section"; diff --git a/packages/app/src/agent-profiles/internal/agent-profile-glyph.tsx b/packages/app/src/agent-profiles/internal/agent-profile-glyph.tsx new file mode 100644 index 0000000000..e9795614e9 --- /dev/null +++ b/packages/app/src/agent-profiles/internal/agent-profile-glyph.tsx @@ -0,0 +1,128 @@ +import { withUnistyles } from "react-native-unistyles"; +import { + BookOpen, + Bot, + Boxes, + Brain, + Bug, + Cloud, + Code, + Compass, + Cpu, + Database, + Eye, + Feather, + FileText, + FlaskConical, + GitBranch, + Globe, + Hammer, + Layers, + Microscope, + Package, + Palette, + Pencil, + Rocket, + Search, + Server, + Shield, + Sparkles, + Terminal, + TestTube, + Wrench, +} from "lucide-react-native"; +import { identityForeground } from "@/styles/identity-colors"; +import { ICON_SIZE, type Theme } from "@/styles/theme"; +import { + AGENT_PROFILE_COLORS, + resolveAgentProfileColor, + resolveAgentProfileIconKey, + type AgentProfileColor, + type AgentProfileIconKey, +} from "./profile-appearance"; + +/** Drawn when a profile names no icon, and as the "default" cell in the picker grid. */ +const ThemedDefaultIcon = withUnistyles(Bot); + +/** + * `withUnistyles` has to wrap each icon once at module scope, so the registry + * stores the themed component rather than the raw lucide one. + */ +const THEMED_ICONS: Record = { + code: withUnistyles(Code), + terminal: withUnistyles(Terminal), + bug: withUnistyles(Bug), + wrench: withUnistyles(Wrench), + hammer: withUnistyles(Hammer), + + flask: withUnistyles(FlaskConical), + testTube: withUnistyles(TestTube), + microscope: withUnistyles(Microscope), + search: withUnistyles(Search), + eye: withUnistyles(Eye), + + palette: withUnistyles(Palette), + feather: withUnistyles(Feather), + pencil: withUnistyles(Pencil), + fileText: withUnistyles(FileText), + book: withUnistyles(BookOpen), + + rocket: withUnistyles(Rocket), + package: withUnistyles(Package), + boxes: withUnistyles(Boxes), + server: withUnistyles(Server), + database: withUnistyles(Database), + + cpu: withUnistyles(Cpu), + cloud: withUnistyles(Cloud), + globe: withUnistyles(Globe), + gitBranch: withUnistyles(GitBranch), + layers: withUnistyles(Layers), + + compass: withUnistyles(Compass), + brain: withUnistyles(Brain), + sparkles: withUnistyles(Sparkles), + shield: withUnistyles(Shield), +}; + +const mutedMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); + +/** + * One `uniProps` mapping per colour, built once. Building these inline would + * hand `withUnistyles` a new function every render and defeat its tracking. + */ +const COLOR_MAPPINGS: Record { color: string }> = (() => { + const byColor = {} as Record { color: string }>; + for (const color of AGENT_PROFILE_COLORS) { + byColor[color] = + color === "none" + ? mutedMapping + : (theme: Theme) => ({ color: identityForeground(color, theme.colorScheme) }); + } + return byColor; +})(); + +export function agentProfileColorMapping( + color: AgentProfileColor, +): (theme: Theme) => { color: string } { + return COLOR_MAPPINGS[color]; +} + +/** + * How a profile is drawn everywhere it appears — settings list, model picker, + * and the picker grid's own cells. One component so the three can't drift. + */ +export function AgentProfileGlyph({ + icon, + color, + size = ICON_SIZE.md, +}: { + icon?: string | undefined; + color?: string | undefined; + size?: number; +}) { + const iconKey = resolveAgentProfileIconKey(icon); + const mapping = COLOR_MAPPINGS[resolveAgentProfileColor(color)]; + const Icon = iconKey ? THEMED_ICONS[iconKey] : ThemedDefaultIcon; + return ; +} diff --git a/packages/app/src/agent-profiles/internal/capabilities.test.ts b/packages/app/src/agent-profiles/internal/capabilities.test.ts new file mode 100644 index 0000000000..7708e965a9 --- /dev/null +++ b/packages/app/src/agent-profiles/internal/capabilities.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from "vitest"; +import { supportsAgentProfiles } from "./capabilities"; + +describe("agent profile capabilities", () => { + it("requires storage and live config application support", () => { + expect(supportsAgentProfiles(undefined)).toBe(false); + expect(supportsAgentProfiles({ agentProfiles: true })).toBe(false); + expect(supportsAgentProfiles({ agentConfigApply: true })).toBe(false); + expect(supportsAgentProfiles({ agentProfiles: true, agentConfigApply: true })).toBe(true); + }); +}); diff --git a/packages/app/src/agent-profiles/internal/capabilities.ts b/packages/app/src/agent-profiles/internal/capabilities.ts new file mode 100644 index 0000000000..caf5fb2229 --- /dev/null +++ b/packages/app/src/agent-profiles/internal/capabilities.ts @@ -0,0 +1,8 @@ +interface AgentProfileCapabilities { + agentProfiles?: boolean; + agentConfigApply?: boolean; +} + +export function supportsAgentProfiles(features: AgentProfileCapabilities | undefined): boolean { + return features?.agentProfiles === true && features.agentConfigApply === true; +} diff --git a/packages/app/src/agent-profiles/internal/materialize-profile.test.ts b/packages/app/src/agent-profiles/internal/materialize-profile.test.ts new file mode 100644 index 0000000000..dd60b6680f --- /dev/null +++ b/packages/app/src/agent-profiles/internal/materialize-profile.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from "vitest"; +import type { AgentProfile } from "@getpaseo/protocol/messages"; +import { + materializeAgentProfile, + reconcileMaterializedProfileMode, + toAgentConfigApply, +} from "./materialize-profile"; + +function profile(overrides: Partial = {}): AgentProfile { + return { + id: "ui-work", + name: "UI work", + provider: "claude", + ...overrides, + }; +} + +describe("materializeAgentProfile", () => { + it("resolves every field a full profile names", () => { + expect( + materializeAgentProfile( + profile({ + model: "claude-opus-5", + modeId: "plan", + thinkingOptionId: "think-hard", + featureValues: { web_search: true }, + }), + ), + ).toEqual({ + provider: "claude", + modelId: "claude-opus-5", + modeId: "plan", + thinkingOptionId: "think-hard", + featureValues: { web_search: true }, + }); + }); + + it("treats omitted and blank fields the same", () => { + expect(materializeAgentProfile(profile({ model: " ", modeId: "" }))).toEqual({ + provider: "claude", + modelId: "", + modeId: "", + thinkingOptionId: "", + featureValues: {}, + }); + }); + + it("trims stored ids", () => { + expect(materializeAgentProfile(profile({ model: " claude-opus-5 " })).modelId).toBe( + "claude-opus-5", + ); + }); +}); + +describe("toAgentConfigApply", () => { + it("sends every value the profile names", () => { + expect( + toAgentConfigApply( + materializeAgentProfile( + profile({ + model: "claude-opus-5", + modeId: "plan", + thinkingOptionId: "think-hard", + featureValues: { web_search: true }, + }), + ), + ), + ).toEqual({ + modelId: "claude-opus-5", + modeId: "plan", + thinkingOptionId: "think-hard", + featureValues: { web_search: true }, + }); + }); + + it("omits what the profile leaves alone rather than clearing it", () => { + expect(toAgentConfigApply(materializeAgentProfile(profile({ modeId: "plan" })))).toEqual({ + modeId: "plan", + }); + }); + + it("omits an empty feature map so the agent keeps its own feature values", () => { + expect( + toAgentConfigApply(materializeAgentProfile(profile({ featureValues: {} }))), + ).not.toHaveProperty("featureValues"); + }); + + it("never carries a provider, which a running agent cannot change", () => { + expect( + toAgentConfigApply(materializeAgentProfile(profile({ model: "claude-opus-5" }))), + ).not.toHaveProperty("provider"); + }); +}); + +describe("reconcileMaterializedProfileMode", () => { + it("waits when the current provider mode catalog is unknown", () => { + expect( + reconcileMaterializedProfileMode(materializeAgentProfile(profile({ modeId: "plan" })), null), + ).toBeNull(); + }); + + it("drops a profile mode removed from the current provider catalog", () => { + expect( + reconcileMaterializedProfileMode( + materializeAgentProfile(profile({ modeId: "removed-mode" })), + ["plan", "default"], + ), + ).toMatchObject({ modeId: "" }); + }); + + it("preserves a profile mode still offered by the provider", () => { + expect( + reconcileMaterializedProfileMode(materializeAgentProfile(profile({ modeId: "plan" })), [ + "plan", + "default", + ]), + ).toMatchObject({ modeId: "plan" }); + }); +}); diff --git a/packages/app/src/agent-profiles/internal/materialize-profile.ts b/packages/app/src/agent-profiles/internal/materialize-profile.ts new file mode 100644 index 0000000000..fc393f5ec2 --- /dev/null +++ b/packages/app/src/agent-profiles/internal/materialize-profile.ts @@ -0,0 +1,59 @@ +import type { AgentConfigApply, AgentProfile } from "@getpaseo/protocol/messages"; + +/** + * A profile with its blank fields resolved away. Storage keeps every field + * optional and lets the user clear one to an empty string; applying has to know + * the difference between "set this" and "leave whatever is there alone", and + * every consumer would otherwise re-derive the same trim-and-drop rules. + */ +export interface MaterializedAgentProfile { + provider: string; + /** Empty when the profile names no model, meaning "leave the model alone". */ + modelId: string; + modeId: string; + thinkingOptionId: string; + featureValues: Record; +} + +function trimmed(value: string | undefined): string { + return value?.trim() ?? ""; +} + +export function materializeAgentProfile(profile: AgentProfile): MaterializedAgentProfile { + return { + provider: trimmed(profile.provider), + modelId: trimmed(profile.model), + modeId: trimmed(profile.modeId), + thinkingOptionId: trimmed(profile.thinkingOptionId), + featureValues: profile.featureValues ?? {}, + }; +} + +export function reconcileMaterializedProfileMode( + profile: MaterializedAgentProfile, + availableModeIds: readonly string[] | null, +): MaterializedAgentProfile | null { + if (availableModeIds === null) { + return null; + } + if (!profile.modeId || availableModeIds.includes(profile.modeId)) { + return profile; + } + return { ...profile, modeId: "" }; +} + +/** + * The payload for a running agent. Omitted fields are left alone by the daemon, + * so a profile that names no mode does not reset the agent's mode. Provider is + * absent because a running agent cannot change the process it is. + */ +export function toAgentConfigApply(profile: MaterializedAgentProfile): AgentConfigApply { + return { + ...(profile.modelId ? { modelId: profile.modelId } : {}), + ...(profile.modeId ? { modeId: profile.modeId } : {}), + ...(profile.thinkingOptionId ? { thinkingOptionId: profile.thinkingOptionId } : {}), + ...(Object.keys(profile.featureValues).length > 0 + ? { featureValues: profile.featureValues } + : {}), + }; +} diff --git a/packages/app/src/agent-profiles/internal/profile-appearance.ts b/packages/app/src/agent-profiles/internal/profile-appearance.ts new file mode 100644 index 0000000000..3602f7e511 --- /dev/null +++ b/packages/app/src/agent-profiles/internal/profile-appearance.ts @@ -0,0 +1,71 @@ +import { IDENTITY_COLOR_NAMES, type IdentityColorName } from "@/styles/identity-colors"; + +/** + * A profile's icon is a key into a fixed registry, not a glyph. The value is + * stored in daemon config and drawn by every client, so it has to survive being + * rendered by an app version that ships a different icon set — an unknown key + * falls back to the default glyph rather than showing the raw string. + * + * Order is what the grid draws, five to a row: related icons sit together so + * scanning the grid is scanning categories, not thirty unrelated shapes. + */ +export const AGENT_PROFILE_ICON_KEYS = [ + "code", + "terminal", + "bug", + "wrench", + "hammer", + + "flask", + "testTube", + "microscope", + "search", + "eye", + + "palette", + "feather", + "pencil", + "fileText", + "book", + + "rocket", + "package", + "boxes", + "server", + "database", + + "cpu", + "cloud", + "globe", + "gitBranch", + "layers", + + "compass", + "brain", + "sparkles", + "shield", +] as const; + +export type AgentProfileIconKey = (typeof AGENT_PROFILE_ICON_KEYS)[number]; + +/** Reuses the host identity palette so a profile and a host name read as one family. */ +export type AgentProfileColor = "none" | IdentityColorName; + +export const AGENT_PROFILE_COLORS: readonly AgentProfileColor[] = ["none", ...IDENTITY_COLOR_NAMES]; + +export function isAgentProfileIconKey(value: string | undefined): value is AgentProfileIconKey { + return AGENT_PROFILE_ICON_KEYS.some((key) => key === value); +} + +export function isAgentProfileColor(value: string | undefined): value is AgentProfileColor { + return AGENT_PROFILE_COLORS.some((color) => color === value); +} + +/** Anything unrecognised means "not chosen": the glyph and colour both default. */ +export function resolveAgentProfileIconKey(value: string | undefined): AgentProfileIconKey | null { + return isAgentProfileIconKey(value) ? value : null; +} + +export function resolveAgentProfileColor(value: string | undefined): AgentProfileColor { + return isAgentProfileColor(value) ? value : "none"; +} diff --git a/packages/app/src/agent-profiles/internal/profile-form-model.test.ts b/packages/app/src/agent-profiles/internal/profile-form-model.test.ts new file mode 100644 index 0000000000..0b5fe6253a --- /dev/null +++ b/packages/app/src/agent-profiles/internal/profile-form-model.test.ts @@ -0,0 +1,431 @@ +import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types"; +import { describe, expect, it } from "vitest"; +import { + buildFeatureRequestKey, + openAgentProfileForm, + type AgentProfileFormModel, +} from "./profile-form-model"; + +const CLAUDE: ProviderSnapshotEntry = { + provider: "claude", + status: "ready", + enabled: true, + label: "Claude Code", + defaultModeId: "plan", + modes: [ + { id: "plan", label: "Plan" }, + { id: "accept-edits", label: "Accept edits" }, + ], + models: [ + { + provider: "claude", + id: "claude-opus-5", + label: "Opus 5", + isDefault: true, + thinkingOptions: [ + { id: "think", label: "Think" }, + { id: "think-hard", label: "Think hard" }, + ], + }, + { + provider: "claude", + id: "claude-haiku-4-5", + label: "Haiku 4.5", + thinkingOptions: [{ id: "think", label: "Think" }], + }, + ], +}; + +const CODEX: ProviderSnapshotEntry = { + provider: "codex", + status: "ready", + enabled: true, + label: "Codex", + modes: [{ id: "read-only", label: "Read only" }], + models: [{ provider: "codex", id: "gpt-5.2-codex", label: "GPT-5.2 Codex", isDefault: true }], +}; + +const DISABLED: ProviderSnapshotEntry = { + provider: "pi", + status: "ready", + enabled: false, + label: "Pi", +}; + +const ENTRIES = [CLAUDE, CODEX, DISABLED]; + +function openWithCatalog( + snapshot: Parameters[0], +): AgentProfileFormModel { + const model = openAgentProfileForm(snapshot); + model.applyProviderCatalog(ENTRIES); + return model; +} + +function selectClaude(model: AgentProfileFormModel): void { + model.setProvider("claude", { label: "Claude Code" }); +} + +function optionValues(options: readonly { value: string }[]): string[] { + return options.map((option) => option.value); +} + +describe("openAgentProfileForm", () => { + it("starts a create form empty and cannot submit", () => { + const model = openWithCatalog({ mode: "create" }); + const state = model.getState(); + + expect(state.name).toBe(""); + expect(state.provider).toBe(""); + expect(state.canSubmit).toBe(false); + expect(state.submitValue).toBeNull(); + expect(state.disclosure.showModelField).toBe(false); + }); + + it("lists only enabled providers", () => { + const model = openWithCatalog({ mode: "create" }); + + expect(optionValues(model.getState().providerOptions)).toEqual(["claude", "codex"]); + }); + + it("requires a name and a provider before it can submit", () => { + const model = openWithCatalog({ mode: "create" }); + + model.setName("UI work"); + expect(model.getState().canSubmit).toBe(false); + + selectClaude(model); + expect(model.getState().canSubmit).toBe(true); + expect(model.getState().submitValue).toMatchObject({ name: "UI work", provider: "claude" }); + }); + + it("omits blank text fields from the submitted value but never a selection", () => { + const model = openWithCatalog({ mode: "create" }); + model.setName(" Cheap grunt "); + model.setProvider("codex", { label: "Codex" }); + model.setNotes(" "); + + // Codex declares no thinking options, so that key is genuinely absent — + // model and mode are seeded, never left for the host to decide. + expect(model.getState().submitValue).toEqual({ + name: "Cheap grunt", + provider: "codex", + model: "gpt-5.2-codex", + modeId: "read-only", + }); + }); + + it("seeds edit mode from the stored profile before the catalog lands", () => { + const model = openAgentProfileForm({ + mode: "edit", + profile: { + id: "p1", + name: "UI work", + icon: "palette", + provider: "claude", + model: "claude-opus-5", + modeId: "plan", + thinkingOptionId: "think-hard", + featureValues: { webSearch: true }, + notes: "Visual work only.", + }, + }); + const state = model.getState(); + + expect(state.name).toBe("UI work"); + expect(state.icon).toBe("palette"); + // Ids double as labels until the catalog arrives. + expect(state.modelDisplay).toEqual({ label: "claude-opus-5" }); + expect(state.thinkingDisplay).toEqual({ label: "think-hard" }); + expect(state.catalogResolution).toBe("idle"); + }); + + it("upgrades seeded displays to catalog labels without touching selections", () => { + const model = openAgentProfileForm({ + mode: "edit", + profile: { + id: "p1", + name: "UI work", + provider: "claude", + model: "claude-opus-5", + modeId: "plan", + thinkingOptionId: "think-hard", + }, + }); + model.applyProviderCatalog(ENTRIES); + const state = model.getState(); + + expect(state.providerDisplay).toEqual({ label: "Claude Code" }); + expect(state.modelDisplay).toEqual({ label: "Opus 5" }); + expect(state.modeDisplay).toEqual({ label: "Plan" }); + expect(state.thinkingDisplay).toEqual({ label: "Think hard" }); + expect(state.modelId).toBe("claude-opus-5"); + expect(state.catalogResolution).toBe("complete"); + }); + + it("keeps a stored value the catalog does not know", () => { + const model = openAgentProfileForm({ + mode: "edit", + profile: { id: "p1", name: "Legacy", provider: "claude", model: "claude-retired-3" }, + }); + model.applyProviderCatalog(ENTRIES); + + expect(model.getState().modelId).toBe("claude-retired-3"); + expect(model.getState().modelDisplay).toEqual({ label: "claude-retired-3" }); + expect(model.getState().disclosure.showModelField).toBe(true); + }); + + describe("provider cascade", () => { + it("reseeds every provider-scoped selection when the provider changes", () => { + const model = openWithCatalog({ mode: "create" }); + model.setName("Test writer"); + selectClaude(model); + model.setModel("claude-opus-5", { label: "Opus 5" }); + model.setThinking("think-hard", { label: "Think hard" }); + model.applyFeatures(model.getState().featureRequestKey ?? "", [ + { type: "toggle", id: "webSearch", label: "Web search", value: false }, + ]); + model.setFeatureValue("webSearch", true); + + model.setProvider("codex", { label: "Codex" }); + const state = model.getState(); + + // Claude's ids mean nothing under Codex, so each field lands on Codex's own default. + expect(state.modelId).toBe("gpt-5.2-codex"); + expect(state.modelDisplay).toEqual({ label: "GPT-5.2 Codex" }); + expect(state.thinkingOptionId).toBe(""); + expect(state.thinkingDisplay).toBeNull(); + expect(state.featureValues).toEqual({}); + expect(state.features).toEqual([]); + expect(state.name).toBe("Test writer"); + }); + + it("seeds each provider's default mode", () => { + const model = openWithCatalog({ mode: "create" }); + selectClaude(model); + + expect(model.getState().modeId).toBe("plan"); + + // Codex declares no defaultModeId, so the first mode it offers stands in. + model.setProvider("codex", { label: "Codex" }); + expect(model.getState().modeId).toBe("read-only"); + }); + + it("re-populates model, mode and thinking options from the new provider", () => { + const model = openWithCatalog({ mode: "create" }); + selectClaude(model); + + expect(optionValues(model.getState().modelOptions)).toEqual([ + "claude-opus-5", + "claude-haiku-4-5", + ]); + expect(optionValues(model.getState().thinkingOptions)).toEqual(["think", "think-hard"]); + + model.setProvider("codex", { label: "Codex" }); + + expect(optionValues(model.getState().modelOptions)).toEqual(["gpt-5.2-codex"]); + expect(optionValues(model.getState().modeOptions)).toEqual(["read-only"]); + // GPT-5.2 Codex declares no thinking options, so the field disappears. + expect(model.getState().thinkingOptions).toEqual([]); + expect(model.getState().disclosure.showThinkingField).toBe(false); + }); + + it("ignores a no-op provider selection", () => { + const model = openWithCatalog({ mode: "create" }); + selectClaude(model); + model.setModel("claude-opus-5", { label: "Opus 5" }); + + selectClaude(model); + + expect(model.getState().modelId).toBe("claude-opus-5"); + }); + }); + + describe("model cascade", () => { + it("reseeds a thinking level the new model does not offer", () => { + const model = openWithCatalog({ mode: "create" }); + selectClaude(model); + model.setModel("claude-opus-5", { label: "Opus 5" }); + model.setThinking("think-hard", { label: "Think hard" }); + + // Haiku offers only "think", so "think-hard" cannot survive the switch. + model.setModel("claude-haiku-4-5", { label: "Haiku 4.5" }); + + expect(model.getState().thinkingOptionId).toBe("think"); + expect(model.getState().thinkingDisplay).toEqual({ label: "Think" }); + }); + + it("keeps a thinking level the new model still offers", () => { + const model = openWithCatalog({ mode: "create" }); + selectClaude(model); + model.setModel("claude-opus-5", { label: "Opus 5" }); + model.setThinking("think", { label: "Think" }); + + model.setModel("claude-haiku-4-5", { label: "Haiku 4.5" }); + + expect(model.getState().thinkingOptionId).toBe("think"); + }); + + it("seeds the provider's default model and reads thinking options from it", () => { + const model = openWithCatalog({ mode: "create" }); + selectClaude(model); + + expect(model.getState().modelId).toBe("claude-opus-5"); + expect(optionValues(model.getState().thinkingOptions)).toEqual(["think", "think-hard"]); + expect(model.getState().thinkingOptionId).toBe("think"); + }); + + it("offers no way to unset a selection", () => { + const model = openWithCatalog({ mode: "create" }); + selectClaude(model); + const state = model.getState(); + + const everyOption = [...state.modelOptions, ...state.modeOptions, ...state.thinkingOptions]; + + expect(everyOption.length).toBeGreaterThan(0); + expect(optionValues(everyOption)).not.toContain(""); + }); + }); + + describe("features", () => { + it("asks for features per provider/model/mode/thinking combination", () => { + const model = openWithCatalog({ mode: "create" }); + selectClaude(model); + + expect(model.getState().featureRequest).toEqual({ + provider: "claude", + model: "claude-opus-5", + modeId: "plan", + thinkingOptionId: "think", + }); + const firstKey = model.getState().featureRequestKey; + + model.setModel("claude-haiku-4-5", { label: "Haiku 4.5" }); + + expect(model.getState().featureRequestKey).not.toBe(firstKey); + expect(model.getState().featureRequest).toEqual({ + provider: "claude", + model: "claude-haiku-4-5", + modeId: "plan", + thinkingOptionId: "think", + }); + }); + + it("moves from pending to complete only for the current request", () => { + const model = openWithCatalog({ mode: "create" }); + selectClaude(model); + const key = model.getState().featureRequestKey ?? ""; + expect(model.getState().featureResolution).toBe("pending"); + + model.applyFeatures("stale-key", [ + { type: "toggle", id: "webSearch", label: "Web search", value: false }, + ]); + expect(model.getState().featureResolution).toBe("pending"); + expect(model.getState().features).toEqual([]); + + model.applyFeatures(key, [ + { type: "toggle", id: "webSearch", label: "Web search", value: false }, + ]); + expect(model.getState().featureResolution).toBe("complete"); + expect(model.getState().disclosure.showFeaturesField).toBe(true); + }); + + it("reflects a set feature value back into the rendered feature", () => { + const model = openWithCatalog({ mode: "create" }); + selectClaude(model); + model.applyFeatures(model.getState().featureRequestKey ?? "", [ + { type: "toggle", id: "webSearch", label: "Web search", value: false }, + ]); + + model.setFeatureValue("webSearch", true); + + expect(model.getState().features[0]).toMatchObject({ id: "webSearch", value: true }); + expect(model.getState().featureValues).toEqual({ webSearch: true }); + }); + + it("keeps stored feature values while the feature list is still pending", () => { + const model = openAgentProfileForm({ + mode: "edit", + profile: { + id: "p1", + name: "UI work", + provider: "claude", + featureValues: { webSearch: true }, + }, + }); + model.applyProviderCatalog(ENTRIES); + + expect(model.getState().featureResolution).toBe("pending"); + expect(model.getState().submitValue?.featureValues).toEqual({ webSearch: true }); + }); + + it("prunes stored values the provider no longer reports", () => { + const model = openAgentProfileForm({ + mode: "edit", + profile: { + id: "p1", + name: "UI work", + provider: "claude", + featureValues: { webSearch: true, retiredFlag: true }, + }, + }); + model.applyProviderCatalog(ENTRIES); + model.applyFeatures(model.getState().featureRequestKey ?? "", [ + { type: "toggle", id: "webSearch", label: "Web search", value: false }, + ]); + + expect(model.getState().featureValues).toEqual({ webSearch: true }); + }); + + it("resolves an unavailable feature listing without showing the field", () => { + const model = openWithCatalog({ mode: "create" }); + selectClaude(model); + + model.applyFeaturesUnavailable(model.getState().featureRequestKey ?? ""); + + expect(model.getState().featureResolution).toBe("complete"); + expect(model.getState().disclosure.showFeaturesField).toBe(false); + }); + }); + + it("blocks submission while a save is in flight", () => { + const model = openWithCatalog({ mode: "create" }); + model.setName("UI work"); + selectClaude(model); + + model.setSubmitting(true); + expect(model.getState().canSubmit).toBe(false); + + model.setSubmitting(false); + expect(model.getState().canSubmit).toBe(true); + }); + + it("stops publishing after close", () => { + const model = openWithCatalog({ mode: "create" }); + let notifications = 0; + model.subscribe(() => { + notifications += 1; + }); + + model.setName("a"); + expect(notifications).toBe(1); + + model.close(); + model.setName("b"); + expect(notifications).toBe(1); + expect(model.getState().name).toBe("a"); + }); +}); + +describe("buildFeatureRequestKey", () => { + it("is null without a provider", () => { + expect(buildFeatureRequestKey(null)).toBeNull(); + }); + + it("separates the fields so a value cannot bleed across positions", () => { + expect(buildFeatureRequestKey({ provider: "claude", model: "a|b" })).toBe("claude|a|b||"); + expect(buildFeatureRequestKey({ provider: "claude", modeId: "a" })).not.toBe( + buildFeatureRequestKey({ provider: "claude", model: "a" }), + ); + }); +}); diff --git a/packages/app/src/agent-profiles/internal/profile-form-model.ts b/packages/app/src/agent-profiles/internal/profile-form-model.ts new file mode 100644 index 0000000000..ba3a9e5229 --- /dev/null +++ b/packages/app/src/agent-profiles/internal/profile-form-model.ts @@ -0,0 +1,594 @@ +import type { + AgentFeature, + AgentMode, + AgentModelDefinition, + AgentSelectOption, + ProviderSnapshotEntry, +} from "@getpaseo/protocol/agent-types"; +import type { AgentProfile } from "@getpaseo/protocol/messages"; +import { formatAgentModeLabel, formatThinkingOptionLabel } from "@/agent-controls/labels"; +import { applyFeatureValues, pruneFeatureValues } from "@/hooks/feature-preferences"; +import { filterSelectableModels } from "@/provider-selection/model-catalog"; + +/** + * The persisted profile minus the id; the list owns identity. + * + * `Omit` does not work here. `AgentProfileSchema` is `.passthrough()`, so + * `AgentProfile` carries a `[key: string]: unknown` index signature, and + * `Exclude` stays `string | number` — the named keys + * come back as `unknown` and `name`/`provider` stop being required. Mapping + * `keyof` with an `as` filter drops the index signature and the id together. + */ +export type AgentProfileValue = { + [K in keyof AgentProfile as string extends K + ? never + : K extends "id" + ? never + : K]: AgentProfile[K]; +}; + +export interface AgentProfileFormDisplay { + label: string; + description?: string; +} + +export interface AgentProfileFormOption { + id: string; + value: string; + label: string; + description?: string; + testID: string; +} + +export interface AgentProfileFormSnapshot { + mode: "create" | "edit"; + profile?: AgentProfile; +} + +/** + * The inputs a feature listing needs. Features are provider-scoped and change + * with model/mode/thinking, so every one of those is part of the request. + */ +export interface AgentProfileFeatureRequest { + provider: string; + model?: string; + modeId?: string; + thinkingOptionId?: string; +} + +export type AgentProfileResolutionStatus = "idle" | "pending" | "complete"; + +export interface AgentProfileFormDisclosure { + showModelField: boolean; + showModeField: boolean; + showThinkingField: boolean; + showFeaturesField: boolean; +} + +export interface AgentProfileFormState { + mode: "create" | "edit"; + name: string; + icon: string; + color: string; + notes: string; + provider: string; + modelId: string; + modeId: string; + thinkingOptionId: string; + featureValues: Record; + + providerOptions: AgentProfileFormOption[]; + modelOptions: AgentProfileFormOption[]; + modeOptions: AgentProfileFormOption[]; + thinkingOptions: AgentProfileFormOption[]; + features: AgentFeature[]; + + providerDisplay: AgentProfileFormDisplay | null; + modelDisplay: AgentProfileFormDisplay | null; + modeDisplay: AgentProfileFormDisplay | null; + thinkingDisplay: AgentProfileFormDisplay | null; + + catalogResolution: AgentProfileResolutionStatus; + featureResolution: AgentProfileResolutionStatus; + featureRequest: AgentProfileFeatureRequest | null; + featureRequestKey: string | null; + + disclosure: AgentProfileFormDisclosure; + isSubmitting: boolean; + submitError: string | null; + canSubmit: boolean; + submitValue: AgentProfileValue | null; +} + +export interface AgentProfileFormModel { + getState: () => AgentProfileFormState; + subscribe: (listener: () => void) => () => void; + close: () => void; + /** Late input: the host-scoped provider catalog. Never touches selections. */ + applyProviderCatalog: (entries: readonly ProviderSnapshotEntry[]) => void; + /** Late input: the feature list for one request. Stale keys are ignored. */ + applyFeatures: (requestKey: string, features: readonly AgentFeature[]) => void; + /** Resolve a request that produced no usable features (provider error). */ + applyFeaturesUnavailable: (requestKey: string) => void; + setName: (value: string) => void; + setAppearance: (value: { icon: string; color: string }) => void; + setNotes: (value: string) => void; + setProvider: (providerId: string, display: AgentProfileFormDisplay) => void; + setModel: (modelId: string, display: AgentProfileFormDisplay | null) => void; + setMode: (modeId: string, display: AgentProfileFormDisplay | null) => void; + setThinking: (thinkingOptionId: string, display: AgentProfileFormDisplay | null) => void; + setFeatureValue: (featureId: string, value: unknown) => void; + setSubmitting: (value: boolean) => void; + setSubmitError: (value: string | null) => void; +} + +function findEntry( + entries: readonly ProviderSnapshotEntry[], + provider: string, +): ProviderSnapshotEntry | null { + if (!provider) { + return null; + } + return entries.find((entry) => entry.provider === provider) ?? null; +} + +function resolveModels( + entries: readonly ProviderSnapshotEntry[], + provider: string, +): AgentModelDefinition[] { + return filterSelectableModels(findEntry(entries, provider)?.models ?? null) ?? []; +} + +function resolveModes(entries: readonly ProviderSnapshotEntry[], provider: string): AgentMode[] { + return findEntry(entries, provider)?.modes ?? []; +} + +/** + * Thinking options hang off a model, so an unset model still has to resolve to + * the model the daemon would pick — otherwise the field disappears whenever the + * user leaves the model on "provider default". + */ +function resolveEffectiveModel( + models: readonly AgentModelDefinition[], + modelId: string, +): AgentModelDefinition | null { + const trimmed = modelId.trim(); + if (trimmed) { + return models.find((model) => model.id === trimmed) ?? null; + } + return models.find((model) => model.isDefault) ?? models[0] ?? null; +} + +function resolveThinkingOptions( + entries: readonly ProviderSnapshotEntry[], + provider: string, + modelId: string, +): AgentSelectOption[] { + return resolveEffectiveModel(resolveModels(entries, provider), modelId)?.thinkingOptions ?? []; +} + +/** Every real option is selected by its own id; only the unset row differs. */ +function formOption(input: { + id: string; + label: string; + description: string | undefined; + testID: string; +}): AgentProfileFormOption { + return { + id: input.id, + value: input.id, + label: input.label, + ...(input.description ? { description: input.description } : {}), + testID: input.testID, + }; +} + +function buildProviderOptions(entries: readonly ProviderSnapshotEntry[]): AgentProfileFormOption[] { + return entries + .filter((entry) => entry.enabled) + .map((entry) => + formOption({ + id: entry.provider, + label: entry.label ?? entry.provider, + description: entry.description, + testID: `agent-profile-provider-option-${entry.provider}`, + }), + ); +} + +function buildModelOptions(models: readonly AgentModelDefinition[]): AgentProfileFormOption[] { + return models.map((model) => + formOption({ + id: model.id, + label: model.label, + // Models are labelled by family, so the id is the only thing that + // distinguishes two entries with the same label. + description: model.description ?? model.id, + testID: `agent-profile-model-option-${model.id}`, + }), + ); +} + +function buildModeOptions(modes: readonly AgentMode[]): AgentProfileFormOption[] { + return modes.map((mode) => + formOption({ + id: mode.id, + label: formatAgentModeLabel(mode), + description: mode.description, + testID: `agent-profile-mode-option-${mode.id}`, + }), + ); +} + +function buildThinkingOptions(options: readonly AgentSelectOption[]): AgentProfileFormOption[] { + return options.map((option) => + formOption({ + id: option.id, + label: formatThinkingOptionLabel(option), + description: option.description, + testID: `agent-profile-thinking-option-${option.id}`, + }), + ); +} + +/** + * Every selection resolves to a concrete id. + * + * Each list used to carry a synthetic "Provider default" row. It read as a value + * you could choose but stored an absent one, so a profile that looked fully + * specified would apply whatever the host preferred at the time — and two hosts + * would materialize the same profile differently. The schedule form never + * offered that row; this form now matches it and seeds the catalog's own default + * instead, so what the form shows is what the profile stores. + */ +function defaultModelId(models: readonly AgentModelDefinition[]): string { + return models.find((model) => model.isDefault)?.id ?? models[0]?.id ?? ""; +} + +function defaultModeId(entry: ProviderSnapshotEntry | null, modes: readonly AgentMode[]): string { + return entry?.defaultModeId ?? modes[0]?.id ?? ""; +} + +function defaultThinkingOptionId(model: AgentModelDefinition | null): string { + const options = model?.thinkingOptions ?? []; + return ( + model?.defaultThinkingOptionId ?? + options.find((option) => option.isDefault)?.id ?? + options[0]?.id ?? + "" + ); +} + +function seedSelections( + next: AgentProfileFormState, + input: { + entry: ProviderSnapshotEntry | null; + models: readonly AgentModelDefinition[]; + modes: readonly AgentMode[]; + }, +): AgentProfileFormState { + const modelId = next.modelId || defaultModelId(input.models); + const modeId = next.modeId || defaultModeId(input.entry, input.modes); + const thinkingOptionId = + next.thinkingOptionId || defaultThinkingOptionId(resolveEffectiveModel(input.models, modelId)); + if ( + modelId === next.modelId && + modeId === next.modeId && + thinkingOptionId === next.thinkingOptionId + ) { + return next; + } + return { ...next, modelId, modeId, thinkingOptionId }; +} + +/** + * Selected labels upgrade from the stored id to the catalog label once the + * catalog lands, and fall back to the id when the catalog does not know the + * value. They never blank out — a profile pointing at a retired model still + * reads as that model. + */ +function refineDisplay( + current: AgentProfileFormDisplay | null, + selectedId: string, + resolvedLabel: string | null, +): AgentProfileFormDisplay | null { + if (!selectedId) { + return null; + } + if (resolvedLabel) { + return { label: resolvedLabel }; + } + return current ?? { label: selectedId }; +} + +function buildFeatureRequest(state: AgentProfileFormState): AgentProfileFeatureRequest | null { + if (!state.provider) { + return null; + } + return { + provider: state.provider, + ...(state.modelId ? { model: state.modelId } : {}), + ...(state.modeId ? { modeId: state.modeId } : {}), + ...(state.thinkingOptionId ? { thinkingOptionId: state.thinkingOptionId } : {}), + }; +} + +export function buildFeatureRequestKey(request: AgentProfileFeatureRequest | null): string | null { + if (!request) { + return null; + } + return [ + request.provider, + request.model ?? "", + request.modeId ?? "", + request.thinkingOptionId ?? "", + ].join("|"); +} + +function buildSubmitValue(state: AgentProfileFormState): AgentProfileValue | null { + const name = state.name.trim(); + const notes = state.notes.trim(); + if (!name || !state.provider) { + return null; + } + return { + name, + ...(state.icon ? { icon: state.icon } : {}), + ...(state.color ? { color: state.color } : {}), + provider: state.provider, + ...(state.modelId ? { model: state.modelId } : {}), + ...(state.modeId ? { modeId: state.modeId } : {}), + ...(state.thinkingOptionId ? { thinkingOptionId: state.thinkingOptionId } : {}), + ...(Object.keys(state.featureValues).length > 0 ? { featureValues: state.featureValues } : {}), + ...(notes ? { notes } : {}), + }; +} + +function resolveFeatureStatus( + featureRequestKey: string | null, + featuresAreCurrent: boolean, +): AgentProfileResolutionStatus { + if (featureRequestKey === null) { + return "idle"; + } + return featuresAreCurrent ? "complete" : "pending"; +} + +const BLANK_PROFILE: AgentProfileValue = { name: "", provider: "" }; + +/** A stored id doubles as its own label until the catalog can upgrade it. */ +function seedDisplay(value: string | undefined): AgentProfileFormDisplay | null { + return value ? { label: value } : null; +} + +/** + * Edit mode seeds every value AND display from the stored profile alone: the + * catalog is not loaded yet, so `applyProviderCatalog` does the upgrading later. + */ +function buildInitialState(snapshot: AgentProfileFormSnapshot): AgentProfileFormState { + const profile = snapshot.profile ?? BLANK_PROFILE; + return { + mode: snapshot.mode, + name: profile.name, + icon: profile.icon ?? "", + color: profile.color ?? "", + notes: profile.notes ?? "", + provider: profile.provider, + modelId: profile.model ?? "", + modeId: profile.modeId ?? "", + thinkingOptionId: profile.thinkingOptionId ?? "", + featureValues: { ...profile.featureValues }, + providerOptions: [], + modelOptions: [], + modeOptions: [], + thinkingOptions: [], + features: [], + providerDisplay: seedDisplay(profile.provider), + modelDisplay: seedDisplay(profile.model), + modeDisplay: seedDisplay(profile.modeId), + thinkingDisplay: seedDisplay(profile.thinkingOptionId), + catalogResolution: "idle", + featureResolution: "idle", + featureRequest: null, + featureRequestKey: null, + disclosure: { + showModelField: false, + showModeField: false, + showThinkingField: false, + showFeaturesField: false, + }, + isSubmitting: false, + submitError: null, + canSubmit: false, + submitValue: null, + }; +} + +export function openAgentProfileForm(snapshot: AgentProfileFormSnapshot): AgentProfileFormModel { + let entries: readonly ProviderSnapshotEntry[] = []; + let catalogResolution: AgentProfileResolutionStatus = "idle"; + let resolvedFeatureKey: string | null = null; + let resolvedFeatures: AgentFeature[] = []; + let listeners = new Set<() => void>(); + let closed = false; + + function derive(incoming: AgentProfileFormState): AgentProfileFormState { + const models = resolveModels(entries, incoming.provider); + const modes = resolveModes(entries, incoming.provider); + const next = seedSelections(incoming, { + entry: findEntry(entries, incoming.provider), + models, + modes, + }); + const thinking = resolveThinkingOptions(entries, next.provider, next.modelId); + const featureRequest = buildFeatureRequest(next); + const featureRequestKey = buildFeatureRequestKey(featureRequest); + const featuresAreCurrent = + featureRequestKey !== null && featureRequestKey === resolvedFeatureKey; + const features = featuresAreCurrent + ? applyFeatureValues(resolvedFeatures, next.featureValues) + : []; + const featureResolution = resolveFeatureStatus(featureRequestKey, featuresAreCurrent); + + const withOptions: AgentProfileFormState = { + ...next, + providerOptions: buildProviderOptions(entries), + modelOptions: buildModelOptions(models), + modeOptions: buildModeOptions(modes), + thinkingOptions: buildThinkingOptions(thinking), + features, + providerDisplay: refineDisplay( + next.providerDisplay, + next.provider, + findEntry(entries, next.provider)?.label ?? null, + ), + modelDisplay: refineDisplay( + next.modelDisplay, + next.modelId, + models.find((model) => model.id === next.modelId)?.label ?? null, + ), + modeDisplay: refineDisplay( + next.modeDisplay, + next.modeId, + (() => { + const mode = modes.find((entry) => entry.id === next.modeId); + return mode ? formatAgentModeLabel(mode) : null; + })(), + ), + thinkingDisplay: refineDisplay( + next.thinkingDisplay, + next.thinkingOptionId, + (() => { + const option = thinking.find((entry) => entry.id === next.thinkingOptionId); + return option ? formatThinkingOptionLabel(option) : null; + })(), + ), + catalogResolution, + featureResolution, + featureRequest, + featureRequestKey, + }; + + // A field appears once the catalog can populate it, and stays visible while + // a stored value needs a home even if this host cannot resolve it. + const hasProvider = Boolean(withOptions.provider); + const disclosure: AgentProfileFormDisclosure = { + showModelField: hasProvider && (models.length > 0 || Boolean(withOptions.modelId)), + showModeField: hasProvider && (modes.length > 0 || Boolean(withOptions.modeId)), + showThinkingField: + hasProvider && (thinking.length > 0 || Boolean(withOptions.thinkingOptionId)), + showFeaturesField: hasProvider && features.length > 0, + }; + const canSubmit = + withOptions.name.trim().length > 0 && + withOptions.provider.length > 0 && + !withOptions.isSubmitting; + const resolved: AgentProfileFormState = { ...withOptions, disclosure, canSubmit }; + return { ...resolved, submitValue: canSubmit ? buildSubmitValue(resolved) : null }; + } + + let state: AgentProfileFormState = derive(buildInitialState(snapshot)); + + function publish(mutate: (current: AgentProfileFormState) => AgentProfileFormState): void { + if (closed) { + return; + } + state = derive(mutate(state)); + for (const listener of listeners) { + listener(); + } + } + + return { + getState: () => state, + subscribe: (listener) => { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, + close: () => { + closed = true; + listeners = new Set(); + }, + applyProviderCatalog: (nextEntries) => { + entries = [...nextEntries]; + catalogResolution = "complete"; + publish((current) => current); + }, + applyFeatures: (requestKey, features) => { + if (requestKey !== state.featureRequestKey) { + return; + } + resolvedFeatureKey = requestKey; + resolvedFeatures = [...features]; + publish((current) => ({ + ...current, + featureValues: pruneFeatureValues(current.featureValues, resolvedFeatures), + })); + }, + applyFeaturesUnavailable: (requestKey) => { + if (requestKey !== state.featureRequestKey) { + return; + } + resolvedFeatureKey = requestKey; + resolvedFeatures = []; + publish((current) => current); + }, + setName: (value) => publish((current) => ({ ...current, name: value })), + setAppearance: (value) => + publish((current) => ({ ...current, icon: value.icon, color: value.color })), + setNotes: (value) => publish((current) => ({ ...current, notes: value })), + setProvider: (providerId, display) => + publish((current) => { + if (current.provider === providerId) { + return current; + } + // Everything below the provider is provider-scoped: a model id, mode id, + // thinking id or feature id from the old provider means nothing here. + // Clearing them is enough — `derive` seeds the new provider's defaults. + return { + ...current, + provider: providerId, + providerDisplay: display, + modelId: "", + modelDisplay: null, + modeId: "", + modeDisplay: null, + thinkingOptionId: "", + thinkingDisplay: null, + featureValues: {}, + }; + }), + setModel: (modelId, display) => + publish((current) => { + if (current.modelId === modelId) { + return current; + } + // Thinking options are a property of the model, so a level the new model + // does not offer has to go. + const nextThinking = resolveThinkingOptions(entries, current.provider, modelId); + const keepsThinking = + current.thinkingOptionId.length > 0 && + nextThinking.some((option) => option.id === current.thinkingOptionId); + return { + ...current, + modelId, + modelDisplay: display, + thinkingOptionId: keepsThinking ? current.thinkingOptionId : "", + thinkingDisplay: keepsThinking ? current.thinkingDisplay : null, + }; + }), + setMode: (modeId, display) => + publish((current) => ({ ...current, modeId, modeDisplay: display })), + setThinking: (thinkingOptionId, display) => + publish((current) => ({ ...current, thinkingOptionId, thinkingDisplay: display })), + setFeatureValue: (featureId, value) => + publish((current) => ({ + ...current, + featureValues: { ...current.featureValues, [featureId]: value }, + })), + setSubmitting: (value) => publish((current) => ({ ...current, isSubmitting: value })), + setSubmitError: (value) => publish((current) => ({ ...current, submitError: value })), + }; +} diff --git a/packages/app/src/agent-profiles/internal/profile-summary.ts b/packages/app/src/agent-profiles/internal/profile-summary.ts new file mode 100644 index 0000000000..a0507e6b94 --- /dev/null +++ b/packages/app/src/agent-profiles/internal/profile-summary.ts @@ -0,0 +1,56 @@ +import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types"; +import type { AgentProfile } from "@getpaseo/protocol/messages"; +import { formatAgentModeLabel, formatThinkingOptionLabel } from "@/agent-controls/labels"; + +export interface AgentProfileTag { + id: string; + label: string; +} + +function findEntry( + entries: readonly ProviderSnapshotEntry[], + provider: string, +): ProviderSnapshotEntry | null { + return entries.find((entry) => entry.provider === provider) ?? null; +} + +/** + * The one-line résumé of a profile in the settings list. Every label falls back + * to the stored id: a row must stay readable on a host whose provider catalog + * is offline, or that no longer ships the model the profile names. + */ +export function buildAgentProfileTags(input: { + profile: AgentProfile; + entries: readonly ProviderSnapshotEntry[] | undefined; + formatFeatureCount: (count: number) => string; +}): AgentProfileTag[] { + const entries = input.entries ?? []; + const entry = findEntry(entries, input.profile.provider); + const tags: AgentProfileTag[] = [ + { id: "provider", label: entry?.label ?? input.profile.provider }, + ]; + + const modelId = input.profile.model?.trim(); + if (modelId) { + const model = entry?.models?.find((candidate) => candidate.id === modelId); + tags.push({ id: "model", label: model?.label ?? modelId }); + } + + const modeId = input.profile.modeId?.trim(); + if (modeId) { + const mode = entry?.modes?.find((candidate) => candidate.id === modeId); + tags.push({ id: "mode", label: mode ? formatAgentModeLabel(mode) : modeId }); + } + + const thinkingOptionId = input.profile.thinkingOptionId?.trim(); + if (thinkingOptionId) { + tags.push({ id: "thinking", label: formatThinkingOptionLabel({ id: thinkingOptionId }) }); + } + + const featureCount = Object.keys(input.profile.featureValues ?? {}).length; + if (featureCount > 0) { + tags.push({ id: "features", label: input.formatFeatureCount(featureCount) }); + } + + return tags; +} diff --git a/packages/app/src/agent-profiles/internal/use-agent-profile-picker.ts b/packages/app/src/agent-profiles/internal/use-agent-profile-picker.ts new file mode 100644 index 0000000000..e90b8e8bb4 --- /dev/null +++ b/packages/app/src/agent-profiles/internal/use-agent-profile-picker.ts @@ -0,0 +1,164 @@ +import { useCallback, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { mergeCreateAgentSelectionPreferences } from "@/create-agent-preferences/preferences"; +import { useFormPreferences } from "@/hooks/use-form-preferences"; +import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; +import { useSessionStore } from "@/stores/session-store"; +import { useToast } from "@/contexts/toast-context"; +import { toErrorMessage } from "@/utils/error-messages"; +import { showProviderNoticeToast } from "@/utils/provider-notice-toast"; +import { + materializeAgentProfile, + reconcileMaterializedProfileMode, + toAgentConfigApply, + type MaterializedAgentProfile, +} from "./materialize-profile"; +import { buildAgentProfileTags } from "./profile-summary"; +import { useAgentProfiles } from "./use-agent-profiles"; + +/** The draft composer owns profile application as one state transition. */ +export interface DraftAgentProfileControls { + applyProfile: (profile: MaterializedAgentProfile) => void; +} + +export type AgentProfileApplyTarget = + | { kind: "agent"; agentId: string; availableModeIds: readonly string[] | null } + | { kind: "draft"; controls: DraftAgentProfileControls }; + +/** Everything the model picker renders for one profile. It never sees the profile itself. */ +export interface AgentProfilePickerRow { + id: string; + provider: string; + /** Icon registry key and identity colour; either may be empty for the default glyph. */ + icon: string; + color: string; + name: string; + /** "Claude Code · Opus 5 · Plan · Think hard" */ + summary: string; +} + +export interface AgentProfilePicker { + rows: AgentProfilePickerRow[]; + applyProfile: (profileId: string) => void; +} + +export interface UseAgentProfilePickerInput { + serverId: string | null; + /** + * Providers this composer can actually run; pass a stable reference. A profile + * naming anything else is hidden rather than shown as a row that cannot do + * what it says — a live agent is one provider's process and cannot switch, and + * the draft form ignores a provider the host does not offer. + */ + availableProviders: readonly string[]; + target: AgentProfileApplyTarget; +} + +/** + * The agent-profiles section of the model picker: the rows to draw, and what + * pressing one does. A supported host returns an empty row list when there is + * nothing applicable so the picker can retain its settings shortcut without + * pinning an empty section. Unsupported or still-loading hosts return `null`. + */ +export function useAgentProfilePicker( + input: UseAgentProfilePickerInput, +): AgentProfilePicker | null { + const { serverId, availableProviders, target } = input; + const { t } = useTranslation(); + const { profiles, isSupported } = useAgentProfiles(serverId); + // Profiles are host config, so their labels read from the host-wide catalog + // rather than a workspace's. That is also the key the settings section uses, + // so every composer on a host shares one query instead of adding its own. + const { entries } = useProvidersSnapshot(serverId, { cwd: null }); + const { updatePreferences } = useFormPreferences(); + const client = useSessionStore((state) => state.sessions[serverId ?? ""]?.client ?? null); + const toast = useToast(); + + const applicableProfiles = useMemo(() => { + if (!isSupported || !profiles) { + return []; + } + const available = new Set(availableProviders); + return profiles.filter((profile) => available.has(profile.provider)); + }, [availableProviders, isSupported, profiles]); + + const formatFeatureCount = useCallback( + (count: number) => + count === 1 + ? t("settings.host.agentProfiles.featureCountOne", { count }) + : t("settings.host.agentProfiles.featureCount", { count }), + [t], + ); + + const rows = useMemo( + () => + applicableProfiles.map((profile) => ({ + id: profile.id, + provider: profile.provider, + icon: profile.icon ?? "", + color: profile.color ?? "", + name: profile.name, + summary: buildAgentProfileTags({ profile, entries, formatFeatureCount }) + .map((tag) => tag.label) + .join(" · "), + })), + [applicableProfiles, entries, formatFeatureCount], + ); + + const persistSelection = useCallback( + (resolved: MaterializedAgentProfile) => { + void updatePreferences((current) => + mergeCreateAgentSelectionPreferences({ + preferences: current, + provider: resolved.provider, + modelId: resolved.modelId, + modeId: resolved.modeId, + thinkingOptionId: resolved.thinkingOptionId, + ...(Object.keys(resolved.featureValues).length > 0 + ? { featureValues: resolved.featureValues } + : {}), + }), + ).catch((error) => { + console.warn("[useAgentProfilePicker] persist profile selection failed", error); + }); + }, + [updatePreferences], + ); + + const applyProfile = useCallback( + (profileId: string) => { + const profile = applicableProfiles.find((entry) => entry.id === profileId); + if (!profile) { + return; + } + const resolved = materializeAgentProfile(profile); + + if (target.kind === "draft") { + target.controls.applyProfile(resolved); + return; + } + + const reconciled = reconcileMaterializedProfileMode(resolved, target.availableModeIds); + if (!reconciled) { + return; + } + persistSelection(reconciled); + if (!client) { + return; + } + void client + .applyAgentConfig(target.agentId, toAgentConfigApply(reconciled)) + .then((notice) => showProviderNoticeToast(toast, notice)) + .catch((error) => { + console.warn("[useAgentProfilePicker] applyAgentConfig failed", error); + toast.error(toErrorMessage(error)); + }); + }, + [applicableProfiles, client, persistSelection, target, toast], + ); + + return useMemo( + () => (isSupported && profiles !== null ? { rows, applyProfile } : null), + [applyProfile, isSupported, profiles, rows], + ); +} diff --git a/packages/app/src/agent-profiles/internal/use-agent-profiles.ts b/packages/app/src/agent-profiles/internal/use-agent-profiles.ts new file mode 100644 index 0000000000..fe5105b18c --- /dev/null +++ b/packages/app/src/agent-profiles/internal/use-agent-profiles.ts @@ -0,0 +1,34 @@ +import { useCallback } from "react"; +import type { AgentProfile } from "@getpaseo/protocol/messages"; +import { useDaemonConfig } from "@/hooks/use-daemon-config"; +import { useSessionStore } from "@/stores/session-store"; +import { supportsAgentProfiles } from "./capabilities"; + +export interface UseAgentProfilesResult { + /** `null` until the daemon config has arrived. */ + profiles: AgentProfile[] | null; + /** False on daemons that predate agent profiles, or while disconnected. */ + isSupported: boolean; + /** Writes the whole list; there is no per-profile RPC. */ + saveProfiles: (next: AgentProfile[]) => Promise; +} + +export function useAgentProfiles(serverId: string | null): UseAgentProfilesResult { + const { config, patchConfig } = useDaemonConfig(serverId); + const isSupported = useSessionStore((state) => { + return supportsAgentProfiles(state.sessions[serverId ?? ""]?.serverInfo?.features); + }); + + const saveProfiles = useCallback( + async (next: AgentProfile[]) => { + await patchConfig({ agentProfiles: next }); + }, + [patchConfig], + ); + + return { + profiles: config ? (config.agentProfiles ?? []) : null, + isSupported, + saveProfiles, + }; +} diff --git a/packages/app/src/agent-profiles/internal/use-profile-form-inputs.ts b/packages/app/src/agent-profiles/internal/use-profile-form-inputs.ts new file mode 100644 index 0000000000..fb78320ca1 --- /dev/null +++ b/packages/app/src/agent-profiles/internal/use-profile-form-inputs.ts @@ -0,0 +1,78 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useFetchQuery } from "@/data/query"; +import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; +import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; +import type { AgentProfileFormModel, AgentProfileFormState } from "./profile-form-model"; + +/** + * Agent profiles are host-wide, so the catalog is probed in the daemon's home + * directory (`cwd: null` for the snapshot, `~` for the feature listing) rather + * than in a workspace. A profile that resolved against one project's directory + * would name models the next project cannot launch. + */ +const HOME_SCOPE_CWD = "~"; + +export function useAgentProfileFormCatalog(input: { + serverId: string; + model: AgentProfileFormModel; +}): void { + const { entries } = useProvidersSnapshot(input.serverId, { cwd: null }); + + useEffect(() => { + if (!entries) { + return; + } + input.model.applyProviderCatalog(entries); + }, [entries, input.model]); +} + +export function useAgentProfileFormFeatures(input: { + serverId: string; + model: AgentProfileFormModel; + state: AgentProfileFormState; +}): void { + const { t } = useTranslation(); + const client = useHostRuntimeClient(input.serverId); + const isConnected = useHostRuntimeIsConnected(input.serverId); + const request = input.state.featureRequest; + const requestKey = input.state.featureRequestKey; + + // `dataShape: "value"` and not `"list"`: a list shape keeps the previous + // page's data while the next request is in flight, which would hand the + // features of one provider/model combination to the key of another. + const featuresQuery = useFetchQuery({ + queryKey: ["agentProfileFeatures", input.serverId, requestKey], + dataShape: "value", + staleTimeMs: 5 * 60 * 1000, + enabled: Boolean(client && isConnected && request), + retry: false, + queryFn: async () => { + if (!client || !request) { + throw new Error(t("workspace.terminal.hostDisconnected")); + } + const payload = await client.listProviderFeatures({ ...request, cwd: HOME_SCOPE_CWD }); + if (payload.error) { + throw new Error(payload.error); + } + return payload.features ?? []; + }, + }); + + const { data, isError } = featuresQuery; + const { model } = input; + + useEffect(() => { + if (!requestKey || !data) { + return; + } + model.applyFeatures(requestKey, data); + }, [data, model, requestKey]); + + useEffect(() => { + if (!requestKey || !isError) { + return; + } + model.applyFeaturesUnavailable(requestKey); + }, [isError, model, requestKey]); +} diff --git a/packages/app/src/agent-profiles/internal/use-profile-form-model.ts b/packages/app/src/agent-profiles/internal/use-profile-form-model.ts new file mode 100644 index 0000000000..f800252be4 --- /dev/null +++ b/packages/app/src/agent-profiles/internal/use-profile-form-model.ts @@ -0,0 +1,29 @@ +import { useEffect, useState, useSyncExternalStore } from "react"; +import { + openAgentProfileForm, + type AgentProfileFormModel, + type AgentProfileFormSnapshot, + type AgentProfileFormState, +} from "./profile-form-model"; + +/** + * One model per mount. The modal keys its body on mode + profile id, so create + * and edit never share an instance and reopening always starts from the record. + */ +export function useAgentProfileFormModel( + snapshot: AgentProfileFormSnapshot, +): AgentProfileFormModel { + const [model] = useState(() => openAgentProfileForm(snapshot)); + + useEffect(() => { + return () => { + model.close(); + }; + }, [model]); + + return model; +} + +export function useAgentProfileFormState(model: AgentProfileFormModel): AgentProfileFormState { + return useSyncExternalStore(model.subscribe, model.getState, model.getState); +} diff --git a/packages/app/src/agent-profiles/migration/index.test.ts b/packages/app/src/agent-profiles/migration/index.test.ts new file mode 100644 index 0000000000..a2034b5d11 --- /dev/null +++ b/packages/app/src/agent-profiles/migration/index.test.ts @@ -0,0 +1,203 @@ +import { describe, expect, it } from "vitest"; +import type { AgentProfile } from "@getpaseo/protocol/messages"; +import { LegacyFavoriteProfileMigration } from "."; + +class MemoryStorage { + private readonly values = new Map(); + + constructor(initial: Record = {}) { + for (const [key, value] of Object.entries(initial)) { + this.values.set(key, JSON.stringify(value)); + } + } + + async getItem(key: string): Promise { + return this.values.get(key) ?? null; + } + + async setItem(key: string, value: string): Promise { + this.values.set(key, value); + } + + async removeItem(key: string): Promise { + this.values.delete(key); + } +} + +class FakeProfileHost { + readonly patches: AgentProfile[][] = []; + config: { agentProfiles?: AgentProfile[] }; + shouldFailPatch = false; + shouldFailCatalog = false; + catalogLoadingReads = 0; + + constructor( + profiles: AgentProfile[] = [], + readonly supported = true, + ) { + this.config = { agentProfiles: profiles }; + } + + getLastServerInfoMessage() { + return { + features: this.supported + ? { agentProfiles: true, agentConfigApply: true } + : { agentProfiles: false, agentConfigApply: false }, + }; + } + + async getDaemonConfig() { + return { requestId: "config", config: this.config }; + } + + async patchDaemonConfig(patch: { agentProfiles?: AgentProfile[] }) { + if (this.shouldFailPatch) { + throw new Error("host write failed"); + } + const profiles = patch.agentProfiles ?? []; + this.patches.push(profiles); + this.config = { ...this.config, agentProfiles: profiles }; + return { requestId: "patch", config: this.config }; + } + + async getProvidersSnapshot() { + if (this.shouldFailCatalog) { + throw new Error("catalog unavailable"); + } + if (this.catalogLoadingReads > 0) { + this.catalogLoadingReads -= 1; + return { + entries: [ + { + provider: "claude" as const, + status: "loading" as const, + enabled: true, + }, + ], + generatedAt: new Date(0).toISOString(), + requestId: "providers-loading", + }; + } + return { + entries: [ + { + provider: "claude" as const, + status: "ready" as const, + enabled: true, + label: "Claude Code", + models: [ + { provider: "claude" as const, id: "opus", label: "Opus" }, + { provider: "claude" as const, id: "sonnet", label: "Sonnet" }, + ], + }, + ], + generatedAt: new Date(0).toISOString(), + requestId: "providers", + }; + } +} + +const PREFERENCES_KEY = "@paseo:create-agent-preferences"; + +function migrationStorage() { + return new MemoryStorage({ + [PREFERENCES_KEY]: { + favoriteModels: [ + { provider: "claude", modelId: "opus" }, + { provider: "claude", modelId: "sonnet" }, + { provider: "claude", modelId: "opus" }, + ], + }, + }); +} + +describe("legacy favourite profile migration", () => { + it("appends provider-and-model-only profiles with useful catalog names", async () => { + const existing: AgentProfile = { + id: "hand-written", + name: "My Opus", + provider: "claude", + model: "opus", + modeId: "plan", + }; + const host = new FakeProfileHost([existing]); + const migration = new LegacyFavoriteProfileMigration(migrationStorage()); + + await migration.migrateHost("host-a", host); + + expect(host.patches).toEqual([ + [ + existing, + { + id: "legacy_favorite:claude:sonnet", + name: "Sonnet", + provider: "claude", + model: "sonnet", + }, + ], + ]); + }); + + it("runs once per host, even if the imported profiles are later deleted", async () => { + const storage = migrationStorage(); + const migration = new LegacyFavoriteProfileMigration(storage); + const firstHost = new FakeProfileHost(); + + await migration.migrateHost("host-a", firstHost); + firstHost.config = { agentProfiles: [] }; + await migration.migrateHost("host-a", firstHost); + + const secondHost = new FakeProfileHost(); + await migration.migrateHost("host-b", secondHost); + + expect(firstHost.patches).toHaveLength(1); + expect(secondHost.patches).toHaveLength(1); + }); + + it("waits for profile capabilities without consuming the host migration", async () => { + const storage = migrationStorage(); + const migration = new LegacyFavoriteProfileMigration(storage); + const oldHost = new FakeProfileHost([], false); + + await migration.migrateHost("host-a", oldHost); + + const upgradedHost = new FakeProfileHost(); + await migration.migrateHost("host-a", upgradedHost); + expect(oldHost.patches).toHaveLength(0); + expect(upgradedHost.patches).toHaveLength(1); + }); + + it("retries after a failed host write", async () => { + const host = new FakeProfileHost(); + const migration = new LegacyFavoriteProfileMigration(migrationStorage()); + host.shouldFailPatch = true; + + await expect(migration.migrateHost("host-a", host)).rejects.toThrow("host write failed"); + host.shouldFailPatch = false; + await migration.migrateHost("host-a", host); + + expect(host.patches).toHaveLength(1); + }); + + it("retries catalog resolution instead of permanently storing raw ids", async () => { + const host = new FakeProfileHost(); + const migration = new LegacyFavoriteProfileMigration(migrationStorage()); + host.shouldFailCatalog = true; + + await expect(migration.migrateHost("host-a", host)).rejects.toThrow("catalog unavailable"); + host.shouldFailCatalog = false; + await migration.migrateHost("host-a", host); + + expect(host.patches[0]?.map((profile) => profile.name)).toEqual(["Opus", "Sonnet"]); + }); + + it("waits for relevant provider models to finish loading", async () => { + const host = new FakeProfileHost(); + const migration = new LegacyFavoriteProfileMigration(migrationStorage()); + host.catalogLoadingReads = 1; + + await migration.migrateHost("host-a", host); + + expect(host.patches[0]?.map((profile) => profile.name)).toEqual(["Opus", "Sonnet"]); + }); +}); diff --git a/packages/app/src/agent-profiles/migration/index.ts b/packages/app/src/agent-profiles/migration/index.ts new file mode 100644 index 0000000000..f160420756 --- /dev/null +++ b/packages/app/src/agent-profiles/migration/index.ts @@ -0,0 +1,214 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { z } from "zod"; +import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types"; +import type { AgentProfile } from "@getpaseo/protocol/messages"; +import { FormPreferencesSchema } from "@/create-agent-preferences/preferences"; +import { readValidatedJson, readValidatedString } from "@/storage/validated-storage"; + +const PREFERENCES_KEY = "@paseo:create-agent-preferences"; +const COMPLETION_KEY_PREFIX = "@paseo:legacy-favorites-to-agent-profiles:v1:"; +const CATALOG_LOADING_RETRY_DELAYS_MS = [100, 250, 500, 1_000, 2_000, 4_000] as const; + +type LegacyFavorite = NonNullable["favoriteModels"]>[number]; + +export interface LegacyFavoriteMigrationStorage { + getItem(key: string): Promise; + setItem(key: string, value: string): Promise; + removeItem(key: string): Promise; +} + +export interface LegacyFavoriteMigrationHost { + getLastServerInfoMessage(): { + features?: { agentProfiles?: boolean; agentConfigApply?: boolean }; + } | null; + getDaemonConfig(): Promise<{ config: { agentProfiles?: AgentProfile[] } }>; + patchDaemonConfig(patch: { + agentProfiles: AgentProfile[]; + }): Promise<{ config: { agentProfiles?: AgentProfile[] } }>; + getProvidersSnapshot(): Promise<{ entries: ProviderSnapshotEntry[] }>; +} + +function supportsMigration(host: LegacyFavoriteMigrationHost): boolean { + const features = host.getLastServerInfoMessage()?.features; + return features?.agentProfiles === true && features.agentConfigApply === true; +} + +function completionKey(serverId: string): string { + return `${COMPLETION_KEY_PREFIX}${serverId}`; +} + +function favoriteKey(favorite: Pick): string { + return `${favorite.provider}\u0000${favorite.modelId}`; +} + +function migratedProfileId(favorite: LegacyFavorite): string { + return `legacy_favorite:${encodeURIComponent(favorite.provider)}:${encodeURIComponent(favorite.modelId)}`; +} + +function deduplicateFavorites(favorites: readonly LegacyFavorite[]): LegacyFavorite[] { + const seen = new Set(); + return favorites.filter((favorite) => { + const key = favoriteKey(favorite); + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); +} + +function findCatalogLabels( + favorite: LegacyFavorite, + entries: readonly ProviderSnapshotEntry[], +): { model: string; provider: string } { + const provider = entries.find((entry) => entry.provider === favorite.provider); + const model = provider?.models?.find((entry) => entry.id === favorite.modelId); + return { + model: model?.label?.trim() || favorite.modelId, + provider: provider?.label?.trim() || favorite.provider, + }; +} + +function uniqueProfileName(input: { + favorite: LegacyFavorite; + entries: readonly ProviderSnapshotEntry[]; + usedNames: Set; +}): string { + const labels = findCatalogLabels(input.favorite, input.entries); + const candidates = [ + labels.model, + `${labels.provider} · ${labels.model}`, + `${labels.provider} · ${labels.model} (${input.favorite.modelId})`, + ]; + for (const candidate of candidates) { + if (!input.usedNames.has(candidate)) { + input.usedNames.add(candidate); + return candidate; + } + } + let suffix = 2; + while (input.usedNames.has(`${candidates[2]} ${suffix}`)) { + suffix += 1; + } + const name = `${candidates[2]} ${suffix}`; + input.usedNames.add(name); + return name; +} + +function buildMigratedProfiles(input: { + favorites: readonly LegacyFavorite[]; + existing: readonly AgentProfile[]; + entries: readonly ProviderSnapshotEntry[]; +}): AgentProfile[] { + const existingSelections = new Set( + input.existing.map((profile) => + favoriteKey({ provider: profile.provider, modelId: profile.model?.trim() ?? "" }), + ), + ); + const existingIds = new Set(input.existing.map((profile) => profile.id)); + const usedNames = new Set(input.existing.map((profile) => profile.name)); + + return deduplicateFavorites(input.favorites).flatMap((favorite) => { + const id = migratedProfileId(favorite); + if (existingSelections.has(favoriteKey(favorite)) || existingIds.has(id)) { + return []; + } + return [ + { + id, + name: uniqueProfileName({ favorite, entries: input.entries, usedNames }), + provider: favorite.provider, + model: favorite.modelId, + }, + ]; + }); +} + +function catalogIsLoading( + entries: readonly ProviderSnapshotEntry[], + favorites: readonly LegacyFavorite[], +): boolean { + const providers = new Set(favorites.map((favorite) => favorite.provider)); + return entries.some((entry) => providers.has(entry.provider) && entry.status === "loading"); +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function readProviderCatalog( + host: LegacyFavoriteMigrationHost, + favorites: readonly LegacyFavorite[], +): Promise { + for (const retryDelay of CATALOG_LOADING_RETRY_DELAYS_MS) { + const entries = (await host.getProvidersSnapshot()).entries; + if (!catalogIsLoading(entries, favorites)) { + return entries; + } + await delay(retryDelay); + } + const entries = (await host.getProvidersSnapshot()).entries; + if (catalogIsLoading(entries, favorites)) { + throw new Error("Provider catalog remained loading during legacy favourite migration"); + } + return entries; +} + +/** + * One-time bridge from device-local model favourites to host-wide profiles. + * The module owns every migration invariant; callers only announce a connected + * host and may safely call again after reconnects or React remounts. + */ +export class LegacyFavoriteProfileMigration { + private readonly inFlight = new Map>(); + + constructor(private readonly storage: LegacyFavoriteMigrationStorage) {} + + migrateHost(serverId: string, host: LegacyFavoriteMigrationHost): Promise { + const active = this.inFlight.get(serverId); + if (active) { + return active; + } + const migration = this.run(serverId, host).finally(() => { + if (this.inFlight.get(serverId) === migration) { + this.inFlight.delete(serverId); + } + }); + this.inFlight.set(serverId, migration); + return migration; + } + + private async run(serverId: string, host: LegacyFavoriteMigrationHost): Promise { + if (!supportsMigration(host)) { + return; + } + const markerKey = completionKey(serverId); + if ((await readValidatedString(this.storage, markerKey, z.literal("1"))) === "1") { + return; + } + + const preferences = await readValidatedJson( + this.storage, + PREFERENCES_KEY, + FormPreferencesSchema, + ); + const favorites = preferences?.favoriteModels ?? []; + if (favorites.length === 0) { + await this.storage.setItem(markerKey, "1"); + return; + } + + const [{ config }, entries] = await Promise.all([ + host.getDaemonConfig(), + readProviderCatalog(host, favorites), + ]); + const existing = config.agentProfiles ?? []; + const migrated = buildMigratedProfiles({ favorites, existing, entries }); + if (migrated.length > 0) { + await host.patchDaemonConfig({ agentProfiles: [...existing, ...migrated] }); + } + await this.storage.setItem(markerKey, "1"); + } +} + +export const legacyFavoriteProfileMigration = new LegacyFavoriteProfileMigration(AsyncStorage); diff --git a/packages/app/src/agent-profiles/settings/agent-profile-appearance-field.tsx b/packages/app/src/agent-profiles/settings/agent-profile-appearance-field.tsx new file mode 100644 index 0000000000..9d958daffe --- /dev/null +++ b/packages/app/src/agent-profiles/settings/agent-profile-appearance-field.tsx @@ -0,0 +1,384 @@ +import { useCallback, useMemo, useRef, useState, type ReactElement } from "react"; +import { Pressable, View, type PressableStateCallbackType } from "react-native"; +import { useTranslation } from "react-i18next"; +import type { TFunction } from "i18next"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { Check, ChevronDown } from "lucide-react-native"; +import { Combobox, type ComboboxOption } from "@/components/ui/combobox"; +import { + createControlGeometry, + resolveControlInteractionStyles, + type FieldControlSize, +} from "@/components/ui/control-geometry"; +import { Field } from "@/components/ui/form-field"; +import { identityForeground } from "@/styles/identity-colors"; +import { ICON_SIZE, type Theme } from "@/styles/theme"; +import { AgentProfileGlyph } from "../internal/agent-profile-glyph"; +import { + AGENT_PROFILE_COLORS, + AGENT_PROFILE_ICON_KEYS, + resolveAgentProfileColor, + resolveAgentProfileIconKey, + type AgentProfileColor, + type AgentProfileIconKey, +} from "../internal/profile-appearance"; + +const CELL_SIZE = 40; +const CELL_GAP = 4; +const GRID_COLUMNS = 5; +const CELL_ROW_WIDTH = GRID_COLUMNS * CELL_SIZE + (GRID_COLUMNS - 1) * CELL_GAP; +/** Matches `styles.body.padding`; both derive from `theme.spacing[3]`. */ +const CELL_GRID_PADDING = 12; +/** Swatches are dots, not targets the eye has to land on precisely, so they pack tighter. */ +const COLOR_CELL_SIZE = 32; +const SWATCH_SIZE = 20; + +const EMPTY_COMBOBOX_OPTIONS: ComboboxOption[] = []; +const ThemedChevronDown = withUnistyles(ChevronDown); +const ThemedCheck = withUnistyles(Check); + +const foregroundMutedMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); +const onIdentityMapping = (theme: Theme) => ({ color: theme.colors.background }); + +/** + * Locked, not a floor: an unlocked popover stretches to its 400px maximum and + * the wrapping grid follows it to eight columns. Two spare pixels so a frame + * border can never push the fifth cell onto its own row. + */ +const DESKTOP_WIDTH = CELL_ROW_WIDTH + CELL_GRID_PADDING * 2 + 2; + +/** Colours are named in host appearance; profiles reuse both the palette and its labels. */ +function colorLabel(t: TFunction, color: AgentProfileColor): string { + return t(`settings.host.appearance.color.options.${color}`); +} + +function IconCell({ + iconKey, + selected, + color, + onSelect, +}: { + iconKey: AgentProfileIconKey | null; + selected: boolean; + color: AgentProfileColor; + onSelect: (iconKey: AgentProfileIconKey | null) => void; +}) { + const handlePress = useCallback(() => onSelect(iconKey), [iconKey, onSelect]); + const accessibilityState = useMemo(() => ({ selected }), [selected]); + const cellStyle = useCallback( + ({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [ + styles.cell, + Boolean(hovered) && styles.cellHovered, + pressed && styles.cellPressed, + selected && styles.cellSelected, + ], + [selected], + ); + return ( + + + + ); +} + +function ColorCell({ + color, + selected, + onSelect, +}: { + color: AgentProfileColor; + selected: boolean; + onSelect: (color: AgentProfileColor) => void; +}) { + const { t } = useTranslation(); + const handlePress = useCallback(() => onSelect(color), [color, onSelect]); + const accessibilityState = useMemo(() => ({ selected }), [selected]); + const swatchStyle = useMemo( + () => [styles.swatch, color === "none" ? styles.swatchNone : identitySwatch(color)], + [color], + ); + const cellStyle = useCallback( + ({ pressed }: PressableStateCallbackType) => [styles.colorCell, pressed && styles.cellPressed], + [], + ); + return ( + + + {selected ? ( + + ) : null} + + + ); +} + +/** + * Icon and colour in one popover rather than two fields: the pair is a single + * visual decision, and picking them apart means never seeing the result until + * both are closed. The grid redraws in the chosen colour as you switch colours, + * so the swatch row sits above it. + */ +export function AgentProfileAppearanceField({ + label, + icon, + color, + onChange, + size = "md", + disabled = false, + testID, + triggerTestID, +}: { + label: string; + icon: string; + color: string; + onChange: (next: { icon: string; color: string }) => void; + size?: FieldControlSize; + disabled?: boolean; + testID?: string; + triggerTestID?: string; +}): ReactElement { + const { t } = useTranslation(); + const anchorRef = useRef(null); + const [open, setOpen] = useState(false); + const selectedIcon = resolveAgentProfileIconKey(icon); + const selectedColor = resolveAgentProfileColor(color); + + const handleTriggerPress = useCallback(() => { + if (disabled) { + return; + } + setOpen((current) => !current); + }, [disabled]); + + // Colour keeps the popover open — you are still choosing. An icon is the last + // decision, so it commits and closes. + const handleColorSelect = useCallback( + (next: AgentProfileColor) => onChange({ icon, color: next === "none" ? "" : next }), + [icon, onChange], + ); + const handleIconSelect = useCallback( + (next: AgentProfileIconKey | null) => { + onChange({ icon: next ?? "", color }); + setOpen(false); + }, + [color, onChange], + ); + + const triggerStyle = useCallback( + ({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [ + styles.trigger, + size === "sm" ? styles.triggerSm : styles.triggerMd, + styles.triggerIconOnly, + resolveControlInteractionStyles( + { + controlRest: styles.controlRest, + controlHover: styles.controlHover, + controlActive: styles.controlActive, + controlDisabled: styles.controlDisabled, + }, + { hovered: Boolean(hovered), active: pressed || open, disabled }, + ), + ], + [disabled, open, size], + ); + + const header = useMemo(() => ({ title: label }), [label]); + + return ( + + + + + + + + + + + {AGENT_PROFILE_COLORS.map((option) => ( + + ))} + + + + {AGENT_PROFILE_ICON_KEYS.map((key) => ( + + ))} + + + + + ); +} + +function noop() {} + +/** + * One named rule per hue. Unistyles cannot resolve a colour computed at render, + * so the palette has to be spelled out as static styles. + */ +function identitySwatch(color: Exclude) { + return styles[IDENTITY_SWATCH_STYLE_KEYS[color]]; +} + +const styles = StyleSheet.create((theme) => { + const geometry = createControlGeometry(theme); + + return { + trigger: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + gap: theme.spacing[1], + backgroundColor: theme.colors.surface2, + }, + triggerSm: { ...geometry.fieldControlSm }, + triggerMd: { ...geometry.fieldControlMd }, + // The field geometry pads for a line of text. This trigger holds a glyph and + // a chevron, so it keeps the shared height and drops the text-sized inset. + triggerIconOnly: { + paddingHorizontal: theme.spacing[2], + }, + controlRest: { ...geometry.controlRest }, + controlHover: { ...geometry.controlHover }, + controlActive: { ...geometry.controlActive }, + controlDisabled: { ...geometry.controlDisabled }, + + body: { + padding: CELL_GRID_PADDING, + gap: theme.spacing[3], + }, + colorRow: { + flexDirection: "row", + flexWrap: "wrap", + gap: CELL_GAP, + paddingBottom: theme.spacing[3], + borderBottomWidth: theme.borderWidth[1], + borderBottomColor: theme.colors.border, + }, + colorCell: { + width: COLOR_CELL_SIZE, + height: COLOR_CELL_SIZE, + alignItems: "center", + justifyContent: "center", + borderRadius: theme.borderRadius.md, + }, + grid: { + flexDirection: "row", + flexWrap: "wrap", + gap: CELL_GAP, + }, + cell: { + width: CELL_SIZE, + height: CELL_SIZE, + alignItems: "center", + justifyContent: "center", + borderRadius: theme.borderRadius.md, + borderWidth: theme.borderWidth[1], + borderColor: "transparent", + }, + cellHovered: { + backgroundColor: theme.colors.surface2, + }, + cellPressed: { + opacity: 0.7, + }, + cellSelected: { + backgroundColor: theme.colors.surface2, + borderColor: theme.colors.border, + }, + swatch: { + width: SWATCH_SIZE, + height: SWATCH_SIZE, + borderRadius: SWATCH_SIZE / 2, + alignItems: "center", + justifyContent: "center", + borderWidth: theme.borderWidth[1], + borderColor: theme.colors.border, + }, + swatchNone: { + backgroundColor: theme.colors.surface2, + }, + swatchViolet: { backgroundColor: identityForeground("violet", theme.colorScheme) }, + swatchSky: { backgroundColor: identityForeground("sky", theme.colorScheme) }, + swatchEmerald: { backgroundColor: identityForeground("emerald", theme.colorScheme) }, + swatchOrange: { backgroundColor: identityForeground("orange", theme.colorScheme) }, + swatchPink: { backgroundColor: identityForeground("pink", theme.colorScheme) }, + swatchIndigo: { backgroundColor: identityForeground("indigo", theme.colorScheme) }, + swatchTeal: { backgroundColor: identityForeground("teal", theme.colorScheme) }, + swatchRed: { backgroundColor: identityForeground("red", theme.colorScheme) }, + swatchAmber: { backgroundColor: identityForeground("amber", theme.colorScheme) }, + swatchBlue: { backgroundColor: identityForeground("blue", theme.colorScheme) }, + }; +}); + +const IDENTITY_SWATCH_STYLE_KEYS = { + violet: "swatchViolet", + sky: "swatchSky", + emerald: "swatchEmerald", + orange: "swatchOrange", + pink: "swatchPink", + indigo: "swatchIndigo", + teal: "swatchTeal", + red: "swatchRed", + amber: "swatchAmber", + blue: "swatchBlue", +} as const; diff --git a/packages/app/src/agent-profiles/settings/agent-profile-edit-modal.tsx b/packages/app/src/agent-profiles/settings/agent-profile-edit-modal.tsx new file mode 100644 index 0000000000..c4e63a847c --- /dev/null +++ b/packages/app/src/agent-profiles/settings/agent-profile-edit-modal.tsx @@ -0,0 +1,549 @@ +import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from "react"; +import { Text, View } from "react-native"; +import { useTranslation } from "react-i18next"; +import { StyleSheet } from "react-native-unistyles"; +import type { AgentFeature } from "@getpaseo/protocol/agent-types"; +import type { AgentProfile } from "@getpaseo/protocol/messages"; +import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet"; +import { Button } from "@/components/ui/button"; +import { type FieldControlSize } from "@/components/ui/control-geometry"; +import { Field, FormTextInput } from "@/components/ui/form-field"; +import { + SelectField, + type SelectFieldDisplay, + type SelectFieldOption, +} from "@/components/ui/select-field"; +import { Switch } from "@/components/ui/switch"; +import { useIsCompactFormFactor } from "@/constants/layout"; +import { toErrorMessage } from "@/utils/error-messages"; +import { AgentProfileAppearanceField } from "./agent-profile-appearance-field"; +import type { + AgentProfileFormModel, + AgentProfileFormOption, + AgentProfileValue, +} from "../internal/profile-form-model"; +import { + useAgentProfileFormCatalog, + useAgentProfileFormFeatures, +} from "../internal/use-profile-form-inputs"; +import { + useAgentProfileFormModel, + useAgentProfileFormState, +} from "../internal/use-profile-form-model"; + +export interface AgentProfileEditModalProps { + serverId: string; + visible: boolean; + mode: "create" | "edit"; + profile?: AgentProfile; + onClose: () => void; + onSave: (value: AgentProfileValue) => Promise; +} + +/** + * Only reachable before the catalog lands. Every select is seeded to a concrete + * id as soon as one exists, and none of them offer an "unset" row. + */ +const UNSET_VALUE = ""; + +function openKey(props: AgentProfileEditModalProps): string { + return props.mode === "edit" ? `edit:${props.profile?.id ?? ""}` : "create"; +} + +/** + * Create and edit never share a mounted instance: the body is keyed on mode plus + * profile id, so every open constructs a form model seeded from that record. + */ +export function AgentProfileEditModal(props: AgentProfileEditModalProps): ReactElement | null { + const [renderedProps, setRenderedProps] = useState(() => + props.visible ? props : null, + ); + const [sheetVisible, setSheetVisible] = useState(props.visible); + const livePropsRef = useRef(props); + const closeRequestedRef = useRef(false); + livePropsRef.current = props; + + useEffect(() => { + if (props.visible) { + if (closeRequestedRef.current) { + return; + } + setRenderedProps(props); + setSheetVisible(true); + return; + } + if (renderedProps) { + setSheetVisible(false); + } + }, [props, renderedProps]); + + const requestClose = useCallback(() => { + closeRequestedRef.current = true; + setSheetVisible(false); + }, []); + + const handleDismiss = useCallback(() => { + const dismissedProps = livePropsRef.current; + closeRequestedRef.current = false; + setRenderedProps(null); + setSheetVisible(false); + if (dismissedProps.visible) { + dismissedProps.onClose(); + } + }, []); + + if (!renderedProps) { + return null; + } + + return ( + + ); +} + +function toSelectOptions(options: AgentProfileFormOption[]): SelectFieldOption[] { + return options.map((option) => ({ + id: option.id, + value: option.value, + label: option.label, + ...(option.description ? { description: option.description } : {}), + testID: option.testID, + })); +} + +function OpenAgentProfileEditModal({ + serverId, + visible, + mode, + profile, + onClose, + onDismiss, + onSave, +}: AgentProfileEditModalProps & { onDismiss: () => void }): ReactElement { + const { t } = useTranslation(); + const controlSize: FieldControlSize = useIsCompactFormFactor() ? "md" : "sm"; + const snapshot = useMemo(() => ({ mode, ...(profile ? { profile } : {}) }), [mode, profile]); + const model = useAgentProfileFormModel(snapshot); + const state = useAgentProfileFormState(model); + useAgentProfileFormCatalog({ serverId, model }); + useAgentProfileFormFeatures({ serverId, model, state }); + + const sheetHeader = useMemo( + () => ({ + title: + mode === "edit" + ? t("settings.host.agentProfiles.editProfileTitle") + : t("settings.host.agentProfiles.addProfileTitle"), + }), + [mode, t], + ); + + const providerOptions = useMemo( + () => toSelectOptions(state.providerOptions), + [state.providerOptions], + ); + const modelOptions = useMemo(() => toSelectOptions(state.modelOptions), [state.modelOptions]); + const modeOptions = useMemo(() => toSelectOptions(state.modeOptions), [state.modeOptions]); + const thinkingOptions = useMemo( + () => toSelectOptions(state.thinkingOptions), + [state.thinkingOptions], + ); + + const handleAppearanceChange = useCallback( + (value: { icon: string; color: string }) => model.setAppearance(value), + [model], + ); + const handleProviderChange = useCallback( + (value: string, display: SelectFieldDisplay) => model.setProvider(value, display), + [model], + ); + const handleModelChange = useCallback( + (value: string, display: SelectFieldDisplay) => model.setModel(value, value ? display : null), + [model], + ); + const handleModeChange = useCallback( + (value: string, display: SelectFieldDisplay) => model.setMode(value, value ? display : null), + [model], + ); + const handleThinkingChange = useCallback( + (value: string, display: SelectFieldDisplay) => + model.setThinking(value, value ? display : null), + [model], + ); + + const handleSave = useCallback(async () => { + const value = model.getState().submitValue; + if (!value) { + return; + } + model.setSubmitError(null); + model.setSubmitting(true); + try { + await onSave(value); + onClose(); + } catch (error) { + model.setSubmitError(toErrorMessage(error)); + } finally { + model.setSubmitting(false); + } + }, [model, onClose, onSave]); + + const handleSavePress = useCallback(() => { + void handleSave(); + }, [handleSave]); + + const handleCancel = useCallback(() => { + if (state.isSubmitting) { + return; + } + onClose(); + }, [onClose, state.isSubmitting]); + + return ( + + + + + + + + + + + + + + 6} + title={t("settings.host.agentProfiles.providerLabel")} + size={controlSize} + testID="agent-profile-provider-field" + triggerTestID="agent-profile-provider-trigger" + /> + + {state.disclosure.showModelField ? ( + 6} + title={t("settings.host.agentProfiles.modelLabel")} + size={controlSize} + testID="agent-profile-model-field" + triggerTestID="agent-profile-model-trigger" + /> + ) : null} + + {state.disclosure.showModeField ? ( + 6} + title={t("settings.host.agentProfiles.modeLabel")} + size={controlSize} + testID="agent-profile-mode-field" + triggerTestID="agent-profile-mode-trigger" + /> + ) : null} + + {state.disclosure.showThinkingField ? ( + 6} + title={t("settings.host.agentProfiles.thinkingLabel")} + size={controlSize} + testID="agent-profile-thinking-field" + triggerTestID="agent-profile-thinking-trigger" + /> + ) : null} + + {state.disclosure.showFeaturesField ? ( + + + {state.features.map((feature, index) => ( + + ))} + + + ) : null} + + + + + + {state.submitError ? ( + + {state.submitError} + + ) : null} + + + + + + + + ); +} + +function AgentProfileFeatureRow({ + feature, + isFirst, + disabled, + size, + model, +}: { + feature: AgentFeature; + isFirst: boolean; + disabled: boolean; + size: FieldControlSize; + model: AgentProfileFormModel; +}): ReactElement { + const rowStyle = useMemo( + () => [styles.featureRow, isFirst ? null : styles.featureRowBorder], + [isFirst], + ); + const handleToggle = useCallback( + (value: boolean) => model.setFeatureValue(feature.id, value), + [feature.id, model], + ); + const handleSelect = useCallback( + (value: string) => model.setFeatureValue(feature.id, value), + [feature.id, model], + ); + const selectOptions = useMemo[]>( + () => + feature.type === "select" + ? feature.options.map((option) => ({ + id: option.id, + value: option.id, + label: option.label, + ...(option.description ? { description: option.description } : {}), + testID: `agent-profile-feature-option-${feature.id}-${option.id}`, + })) + : [], + [feature], + ); + const selectedDisplay = useMemo(() => { + if (feature.type !== "select" || !feature.value) { + return null; + } + const option = feature.options.find((candidate) => candidate.id === feature.value); + return { label: option?.label ?? feature.value }; + }, [feature]); + + if (feature.type === "select") { + return ( + + + + ); + } + + return ( + + + + {feature.label} + + {feature.description ? ( + + {feature.description} + + ) : null} + + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + body: { + gap: theme.spacing[4], + paddingBottom: theme.spacing[2], + }, + nameRow: { + flexDirection: "row", + alignItems: "flex-start", + gap: theme.spacing[2], + }, + // No width: the trigger is a glyph and a chevron, so it sizes to them. + iconField: { + flexShrink: 0, + }, + nameField: { + flex: 1, + }, + featureCard: { + backgroundColor: theme.colors.surface2, + borderRadius: theme.borderRadius.md, + borderWidth: 1, + borderColor: theme.colors.border, + paddingHorizontal: theme.spacing[3], + }, + featureRow: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + gap: theme.spacing[3], + paddingVertical: theme.spacing[3], + }, + featureRowBorder: { + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + featureMeta: { + flex: 1, + gap: theme.spacing[1], + }, + featureLabel: { + color: theme.colors.foreground, + fontSize: theme.fontSize.sm, + }, + featureDescription: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.xs, + }, + notesInput: { + minHeight: 88, + textAlignVertical: "top", + }, + submitError: { + color: theme.colors.statusDanger, + fontSize: theme.fontSize.sm, + }, + actions: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + marginTop: theme.spacing[2], + }, + actionButton: { + flex: 1, + }, +})); diff --git a/packages/app/src/agent-profiles/settings/agent-profile-row.tsx b/packages/app/src/agent-profiles/settings/agent-profile-row.tsx new file mode 100644 index 0000000000..e8da192191 --- /dev/null +++ b/packages/app/src/agent-profiles/settings/agent-profile-row.tsx @@ -0,0 +1,188 @@ +import { useCallback, useMemo, type ReactElement } from "react"; +import { Text, View } from "react-native"; +import { useTranslation } from "react-i18next"; +import { ArrowDown, ArrowUp, FileText, Pencil, Trash2 } from "lucide-react-native"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types"; +import type { AgentProfile } from "@getpaseo/protocol/messages"; +import { Button } from "@/components/ui/button"; +import { settingsStyles } from "@/styles/settings"; +import { ICON_SIZE, type Theme } from "@/styles/theme"; +import { AgentProfileGlyph } from "../internal/agent-profile-glyph"; +import { buildAgentProfileTags } from "../internal/profile-summary"; + +const ThemedArrowUp = withUnistyles(ArrowUp); +const ThemedArrowDown = withUnistyles(ArrowDown); +const ThemedPencil = withUnistyles(Pencil); +const ThemedTrash2 = withUnistyles(Trash2); +const ThemedFileText = withUnistyles(FileText); + +const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); +const destructiveColorMapping = (theme: Theme) => ({ color: theme.colors.destructive }); + +const moveUpIcon = ; +const moveDownIcon = ; +const editIcon = ; +const removeIcon = ; + +export interface AgentProfileRowProps { + profile: AgentProfile; + entries: readonly ProviderSnapshotEntry[] | undefined; + isFirst: boolean; + isLast: boolean; + onEdit: (id: string) => void; + onRemove: (id: string) => void; + onMoveUp: (id: string) => void; + onMoveDown: (id: string) => void; +} + +export function AgentProfileRow({ + profile, + entries, + isFirst, + isLast, + onEdit, + onRemove, + onMoveUp, + onMoveDown, +}: AgentProfileRowProps): ReactElement { + const { t } = useTranslation(); + + const handleEdit = useCallback(() => onEdit(profile.id), [onEdit, profile.id]); + const handleRemove = useCallback(() => onRemove(profile.id), [onRemove, profile.id]); + const handleMoveUp = useCallback(() => onMoveUp(profile.id), [onMoveUp, profile.id]); + const handleMoveDown = useCallback(() => onMoveDown(profile.id), [onMoveDown, profile.id]); + + const formatFeatureCount = useCallback( + (count: number) => + count === 1 + ? t("settings.host.agentProfiles.featureCountOne", { count }) + : t("settings.host.agentProfiles.featureCount", { count }), + [t], + ); + const tags = useMemo( + () => buildAgentProfileTags({ profile, entries, formatFeatureCount }), + [entries, formatFeatureCount, profile], + ); + const summary = useMemo(() => tags.map((tag) => tag.label).join(" · "), [tags]); + + const rowStyle = useMemo( + () => [settingsStyles.row, isFirst ? null : settingsStyles.rowBorder, styles.row], + [isFirst], + ); + + return ( + + + + + + + + + {profile.name} + + + {summary} + + + {profile.notes ? ( + + + + {profile.notes} + + + ) : null} + + + + + + )} + + + + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + emptyCard: { + paddingVertical: theme.spacing[6], + paddingHorizontal: theme.spacing[4], + alignItems: "center", + gap: theme.spacing[3], + }, + emptyText: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.sm, + textAlign: "center", + }, +})); diff --git a/packages/app/src/agent-stream/layout.test.ts b/packages/app/src/agent-stream/layout.test.ts index 2fce5c4379..1cc645f167 100644 --- a/packages/app/src/agent-stream/layout.test.ts +++ b/packages/app/src/agent-stream/layout.test.ts @@ -143,6 +143,30 @@ function findLayoutItem(layout: StreamLayout, id: string): StreamLayoutItem { } describe("layoutStream", () => { + it.each(["web", "android"] as const)( + "marks only the active live-head assistant block as streaming on %s", + (platform) => { + const completed = assistantMessage("turn:block:0", 2, { groupId: "turn", index: 0 }); + const live = assistantMessage("turn:block:1", 3, { groupId: "turn", index: 1 }); + const active = layoutFor({ + platform, + isTurnActive: true, + tail: [userMessage("u1", 1), completed], + head: [live], + }); + const complete = layoutFor({ + platform, + isTurnActive: false, + tail: [userMessage("u1", 1), completed], + head: [live], + }); + + expect(findLayoutItem(active, completed.id).phase).toBe("complete"); + expect(findLayoutItem(active, live.id).phase).toBe("streaming"); + expect(findLayoutItem(complete, live.id).phase).toBe("complete"); + }, + ); + it.each(["web", "android"] as const)( "keeps split assistant block spacing identical to unsplit history on %s", (platform) => { diff --git a/packages/app/src/agent-stream/layout.ts b/packages/app/src/agent-stream/layout.ts index 9249f81592..990b1f8071 100644 --- a/packages/app/src/agent-stream/layout.ts +++ b/packages/app/src/agent-stream/layout.ts @@ -26,6 +26,7 @@ export interface StreamLayoutItem { isLastInUserGroup: boolean; isLastInToolSequence: boolean; frameOrder: StreamFrameChildOrder; + phase: "streaming" | "complete"; } export interface StreamLayout { @@ -53,6 +54,7 @@ interface LayoutSegmentInput { boundaryBelowItem: StreamItem | null; boundaryAboveItems: StreamItem[] | null; boundaryAboveIndex: number | null; + phase: "streaming" | "complete"; } interface AssistantFooterSource { @@ -273,6 +275,7 @@ function layoutSegment(input: LayoutSegmentInput): StreamLayoutItem[] { isLastInUserGroup: item.kind === "user_message" && belowItem?.kind !== "user_message", isLastInToolSequence: isToolSequenceItem(item) && !isToolSequenceItem(belowItem), frameOrder: input.frameOrder, + phase: input.phase, }; }); } @@ -324,6 +327,7 @@ export function layoutStream(input: StreamLayoutInput): StreamLayout { boundaryBelowItem: liveHeadBoundaryItem, boundaryAboveItems: null, boundaryAboveIndex: null, + phase: "complete", }); byKey.set(historyCacheKey, history); } @@ -342,6 +346,7 @@ export function layoutStream(input: StreamLayoutInput): StreamLayout { boundaryBelowItem: null, boundaryAboveItems: input.history, boundaryAboveIndex: historyBoundaryIndex, + phase: input.isTurnActive ? "streaming" : "complete", }); return { diff --git a/packages/app/src/agent-stream/strategy-web.test.tsx b/packages/app/src/agent-stream/strategy-web.test.tsx index 61777cb21a..98be7dd916 100644 --- a/packages/app/src/agent-stream/strategy-web.test.tsx +++ b/packages/app/src/agent-stream/strategy-web.test.tsx @@ -206,6 +206,78 @@ describe("createWebStreamStrategy", () => { expect(renderLiveHeadRow).toHaveBeenCalledTimes(2); }); + it("keeps a row mounted when it moves from the live head into mounted history", () => { + const strategy = createWebStreamStrategy({ isMobileBreakpoint: false }); + const viewportRef = React.createRef(); + const item = userMessage(1); + const mounted = vi.fn(); + const unmounted = vi.fn(); + function StatefulRow() { + React.useEffect(() => { + mounted(); + return unmounted; + }, []); + return
{item.id}
; + } + const renderers: StreamSegmentRenderers = { + ...createRenderers(vi.fn()), + renderHistoryMountedRow: () => , + renderLiveHeadRow: () => , + }; + const renderInput: Omit = { + agentId: "agent", + renderers, + listEmptyComponent: null, + viewportRef, + routeBottomAnchorRequest: null, + isAuthoritativeHistoryReady: true, + onNearBottomChange: vi.fn(), + onNearHistoryStart: vi.fn().mockReturnValue(true), + isLoadingOlderHistory: false, + hasOlderHistory: false, + olderHistoryProgressKey: null, + scrollEnabled: true, + listStyle: null, + baseListContentContainerStyle: null, + forwardListContentContainerStyle: null, + }; + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + + act(() => { + root?.render( + strategy.render({ + ...renderInput, + segments: { historyVirtualized: [], historyMounted: [], liveHead: [item] }, + boundary: { + hasVirtualizedHistory: false, + hasMountedHistory: false, + hasLiveHead: true, + }, + }), + ); + }); + expect(mounted).toHaveBeenCalledTimes(1); + + act(() => { + root?.render( + strategy.render({ + ...renderInput, + segments: { historyVirtualized: [], historyMounted: [item], liveHead: [] }, + boundary: { + hasVirtualizedHistory: false, + hasMountedHistory: true, + hasLiveHead: false, + }, + }), + ); + }); + + expect(mounted).toHaveBeenCalledTimes(1); + expect(unmounted).not.toHaveBeenCalled(); + }); + it("reports the live-head row as the reading position", () => { const strategy = createWebStreamStrategy({ isMobileBreakpoint: false }); const viewportRef = React.createRef(); diff --git a/packages/app/src/agent-stream/strategy-web.tsx b/packages/app/src/agent-stream/strategy-web.tsx index 08781f80b0..db244719b6 100644 --- a/packages/app/src/agent-stream/strategy-web.tsx +++ b/packages/app/src/agent-stream/strategy-web.tsx @@ -1068,6 +1068,10 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool )); }, [liveHeadRowRevision, renderLiveHeadRow, segments.liveHead]); + const mountedRows = useMemo( + () => [...mountedHistoryRows, ...liveHeadRows], + [liveHeadRows, mountedHistoryRows], + ); const liveAuxiliary = useMemo(() => { return renderLiveAuxiliary(); }, [renderLiveAuxiliary]); @@ -1119,8 +1123,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool })} ) : null} - {mountedHistoryRows} - {liveHeadRows} + {mountedRows} {liveAuxiliary} {shouldRenderEmpty ? listEmptyComponent : null} diff --git a/packages/app/src/agent-stream/view.tsx b/packages/app/src/agent-stream/view.tsx index 35a8afef5b..58174eae19 100644 --- a/packages/app/src/agent-stream/view.tsx +++ b/packages/app/src/agent-stream/view.tsx @@ -653,6 +653,7 @@ const AgentStreamViewComponent = forwardRef ); @@ -798,7 +799,7 @@ const AgentStreamViewComponent = forwardRef; + return ; case "compaction": return ( diff --git a/packages/app/src/android-async-storage-config.test.ts b/packages/app/src/android-async-storage-config.test.ts new file mode 100644 index 0000000000..fa49bbf83d --- /dev/null +++ b/packages/app/src/android-async-storage-config.test.ts @@ -0,0 +1,27 @@ +import { execFileSync } from "node:child_process"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +describe("Android AsyncStorage configuration", () => { + it("writes a 64 MiB database limit into Gradle properties", () => { + const appDirectory = path.resolve(import.meta.dirname, ".."); + const output = execFileSync( + process.execPath, + [ + "-e", + ` + const plugin = require("./plugins/with-android-async-storage-size"); + const config = plugin({ name: "Paseo", slug: "paseo" }, 64); + const mod = config.mods.android.gradleProperties; + mod({ ...config, modResults: [], modRequest: {}, modRawConfig: config }) + .then((result) => process.stdout.write(JSON.stringify(result.modResults))); + `, + ], + { cwd: appDirectory, encoding: "utf8" }, + ); + + expect(JSON.parse(output)).toEqual([ + { type: "property", key: "AsyncStorage_db_size_in_MB", value: "64" }, + ]); + }); +}); diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index a9a49e326e..431d1cc66d 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -72,6 +72,7 @@ import { registerWorkspaceRouteNavigationRef } from "@/navigation/workspace-rout import { ThemedStack } from "@/navigation/themed-stack"; import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon"; import { AgentNavigationListener } from "@/desktop/agent-navigation"; +import { legacyFavoriteProfileMigration } from "@/agent-profiles/migration"; import { listenToDesktopEvent } from "@/desktop/electron/events"; import { updateDesktopWindowControls } from "@/desktop/electron/window"; import { getDesktopHost } from "@/desktop/host"; @@ -101,13 +102,15 @@ import { useHostRegistryLoaded, useHostMutations, useHostRuntimeClient, + useHostRuntimeIsConnected, useHosts, } from "@/runtime/host-runtime"; import { getDaemonStartService } from "@/runtime/daemon-start-service"; import { applyAppearance } from "@/screens/settings/appearance/apply-appearance"; import { selectIsAgentListOpen, usePanelStore } from "@/stores/panel-store"; import { flushDraftPersistStorage } from "@/stores/draft-store"; -import { THEME_TO_UNISTYLES, type ThemeName } from "@/styles/theme"; +import { getNextThemePreference, THEME_TO_UNISTYLES } from "@/styles/theme"; +import { useSessionStore } from "@/stores/session-store"; import { installWebScrollbarStyles } from "@/styles/install-web-scrollbar-styles"; import type { HostProfile } from "@/types/host-connection"; import { toggleDesktopSidebarsWithCheckoutIntent } from "@/utils/desktop-sidebar-toggle"; @@ -266,11 +269,33 @@ function ManagedDaemonSession({ daemon }: { daemon: HostProfile }) { return ( - {null} + ); } +function LegacyFavoriteProfileMigrationBootstrap({ + serverId, + client, +}: { + serverId: string; + client: NonNullable>; +}) { + const serverInfo = useSessionStore((state) => state.sessions[serverId]?.serverInfo ?? null); + const isConnected = useHostRuntimeIsConnected(serverId); + + useEffect(() => { + if (!serverInfo || !isConnected) { + return; + } + void legacyFavoriteProfileMigration.migrateHost(serverId, client).catch((error) => { + console.warn("[AgentProfiles] Failed to migrate legacy favourites", error); + }); + }, [client, isConnected, serverId, serverInfo]); + + return null; +} + function HostSessionManager() { const hosts = useHosts(); @@ -424,7 +449,6 @@ interface AppContainerProps { chromeEnabled?: boolean; } -const THEME_CYCLE_ORDER: ThemeName[] = ["dark", "zinc", "midnight", "claude", "ghostty", "light"]; const WINDOW_SIDEBAR_TOGGLE_HORIZONTAL_PADDING = 12; function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppContainerProps) { @@ -444,9 +468,7 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon const { width: viewportWidth } = useWindowDimensions(); const cycleTheme = useCallback(() => { - const currentIndex = THEME_CYCLE_ORDER.indexOf(settings.theme as ThemeName); - const nextIndex = (currentIndex + 1) % THEME_CYCLE_ORDER.length; - void updateSettings({ theme: THEME_CYCLE_ORDER[nextIndex] }); + void updateSettings({ theme: getNextThemePreference(settings.theme) }); }, [settings.theme, updateSettings]); const isCompactLayout = useIsCompactFormFactor(); diff --git a/packages/app/src/assistant-file-links/link.tsx b/packages/app/src/assistant-file-links/link.tsx index d99188cff4..9b2838c3d2 100644 --- a/packages/app/src/assistant-file-links/link.tsx +++ b/packages/app/src/assistant-file-links/link.tsx @@ -1,16 +1,9 @@ -import { useMemo, useState, type CSSProperties, type MouseEvent, type ReactNode } from "react"; -import { - Platform, - Pressable, - Text, - View, - type StyleProp, - type TextStyle, - type ViewStyle, -} from "react-native"; +import { useMemo, type CSSProperties, type MouseEvent, type ReactNode } from "react"; +import { Platform, Text, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native"; import { StyleSheet } from "react-native-unistyles"; import { isNative, isWeb } from "@/constants/platform"; import { MarkdownTextSpan } from "@/components/markdown-text"; +import { MarkdownLinkText } from "@/components/markdown/link-text"; import { AssistantLinkPressProvider, type AssistantLinkPress } from "./link-press-context"; import { Shortcut } from "@/components/ui/shortcut"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; @@ -39,7 +32,6 @@ export function AssistantMarkdownLink({ monoSurface, children, }: AssistantMarkdownLinkProps) { - const [hovered, setHovered] = useState(false); const { target, onHoverIn, onPress, onAuxPress } = useFileLink(source); const { configRef } = useAssistantFileLinkResolverContext(); const workspaceRoot = configRef.current.workspaceRoot; @@ -55,15 +47,6 @@ export function AssistantMarkdownLink({ event.stopPropagation(); onAuxPress(); }); - const handleHoverIn = useStableEvent(() => { - setHovered(true); - onHoverIn(); - }); - const handleHoverOut = useStableEvent(() => setHovered(false)); - const hoveredTextStyle = useMemo>( - () => [style, hovered && { textDecorationLine: "underline" as const }], - [style, hovered], - ); const linkPress = useMemo( () => ({ onPress, accessibilityRole: "link" }), [onPress], @@ -114,19 +97,14 @@ export function AssistantMarkdownLink({ onAuxClickCapture={preventAnchorNavigation} style={LINK_ANCHOR_STYLE} > - - - {children} - - + {children} + ); diff --git a/packages/app/src/command-center/command-center.tsx b/packages/app/src/command-center/command-center.tsx index 051655bc50..0daac4cb2e 100644 --- a/packages/app/src/command-center/command-center.tsx +++ b/packages/app/src/command-center/command-center.tsx @@ -13,7 +13,7 @@ import { } from "react-native"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Check, ChevronRight, Folder } from "lucide-react-native"; +import { Check, ChevronRight, Folder, X } from "lucide-react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { BottomSheetBackdrop, @@ -22,6 +22,8 @@ import { type BottomSheetFlatListMethods, } from "@gorhom/bottom-sheet"; import { AgentStatusDot } from "@/components/agent-status-dot"; +import { MaterialFileIcon } from "@/components/material-file-icon"; +import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { Shortcut } from "@/components/ui/shortcut"; import { IsolatedBottomSheetModal, @@ -37,7 +39,10 @@ import { useWebOverlayRegistration, } from "@/lib/overlay-root"; import { useHosts } from "@/runtime/host-runtime"; -import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; +import { + useKeyboardShortcutsStore, + type CommandCenterScope, +} from "@/stores/keyboard-shortcuts-store"; import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store"; import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher"; import { @@ -56,11 +61,13 @@ import { preserveActiveResultId, projectCommandCenterRows, type CommandCenterAgentResult, + type CommandCenterFileResult, type CommandCenterListRow, type CommandCenterResult, type CommandCenterResultSection, type CommandCenterWorkspaceResult, } from "./results"; +import { useWorkspaceFileSearch } from "./workspace-file-search"; const ThemedBottomSheetTextInput = withUnistyles(BottomSheetTextInput, (theme) => ({ placeholderTextColor: theme.colors.foregroundMuted, @@ -73,6 +80,10 @@ const ThemedCheck = withUnistyles(Check, (theme) => ({ color: theme.colors.foreg const ThemedChevronRight = withUnistyles(ChevronRight, (theme) => ({ color: theme.colors.foregroundMuted, })); +const ThemedX = withUnistyles(X, (theme) => ({ color: theme.colors.foregroundMuted })); +const ThemedLoadingSpinner = withUnistyles(LoadingSpinner, (theme) => ({ + color: theme.colors.foregroundMuted, +})); const COMMAND_CENTER_SNAP_POINTS = ["60%", "90%"]; const KEYBOARD_SHOULD_PERSIST_TAPS = "always" as const; const DEFAULT_CATEGORY_RESULT_LIMIT = 5; @@ -191,6 +202,8 @@ function useBuiltInSections(open: boolean, query: string): CommandCenterResultSe interface CommandCenterState { open: boolean; + scope: CommandCenterScope; + clearScope(): void; query: string; setQuery(query: string): void; activeId: string | null; @@ -199,27 +212,58 @@ interface CommandCenterState { rowIndexByResultId: ReadonlyMap; offsets: readonly number[]; inputRef: React.RefObject; + fileSearchLoading: boolean; + fileSearchError: string | null; close(): void; select(result: CommandCenterResult): void; key(key: string): boolean; } function useCommandCenterState(): CommandCenterState { + const { t } = useTranslation(); const open = useKeyboardShortcutsStore((state) => state.commandCenterOpen); + const scope = useKeyboardShortcutsStore((state) => state.commandCenterScope); const setOpen = useKeyboardShortcutsStore((state) => state.setCommandCenterOpen); + const setScope = useKeyboardShortcutsStore((state) => state.setCommandCenterScope); const snapshot = useCommandCenterContributions(); const inputRef = useRef(null); const previousOpenRef = useRef(open); const [query, setQuery] = useState(""); const [activeId, setActiveId] = useState(null); const builtInSections = useBuiltInSections(open, query); + const { + entries: fileSearchEntries, + loading: fileSearchLoading, + error: fileSearchError, + openFile, + } = useWorkspaceFileSearch({ + enabled: open && (scope === "files" || Boolean(query.trim())), + query, + }); + const fileSections = useMemo(() => { + const results = fileSearchEntries.map((entry) => ({ + kind: "file", + id: `file:${entry.path}`, + filePath: entry.path, + title: entry.name, + subtitle: entry.directory, + searchText: entry.path.toLowerCase(), + run: () => openFile(entry.path), + })); + return [{ id: "files", rank: 4, title: t("shell.commandCenter.files"), results }]; + }, [fileSearchEntries, openFile, t]); const contributionSections = useMemo( () => buildContributionSections(snapshot.contributions, query), [query, snapshot.contributions], ); const projection = useMemo( - () => projectCommandCenterRows([...contributionSections, ...builtInSections]), - [builtInSections, contributionSections], + () => + projectCommandCenterRows( + scope === "files" + ? fileSections + : [...contributionSections, ...fileSections, ...builtInSections], + ), + [builtInSections, contributionSections, fileSections, scope], ); const resolvedActiveId = preserveActiveResultId(activeId, projection.selectableResults); @@ -239,6 +283,10 @@ function useCommandCenterState(): CommandCenterState { close(); return true; } + if (pressed === "Backspace" && !query.trim() && scope) { + setScope(null); + return true; + } if (pressed === "Enter") { const selected = results.find((result) => result.id === resolvedActiveId); if (!selected) return false; @@ -251,7 +299,7 @@ function useCommandCenterState(): CommandCenterState { setActiveId(moveActiveResultId(resolvedActiveId, results, direction)); return true; }, - [close, open, projection.selectableResults, resolvedActiveId, select], + [close, open, projection.selectableResults, query, resolvedActiveId, scope, select, setScope], ); useEffect(() => { @@ -277,6 +325,8 @@ function useCommandCenterState(): CommandCenterState { return { open, + scope, + clearScope: () => setScope(null), query, setQuery, activeId: resolvedActiveId, @@ -285,6 +335,8 @@ function useCommandCenterState(): CommandCenterState { rowIndexByResultId: projection.rowIndexByResultId, offsets: projection.offsets, inputRef, + fileSearchLoading, + fileSearchError, close, select, key, @@ -303,7 +355,10 @@ const ResultRow = memo(function ResultRow({ result, active, onSelect }: ResultRo result.kind === "contribution" && result.contribution.presentation.kind === "choice" ? result.contribution.presentation : null; - const accessibilityLabel = choice?.path.join(" › "); + const accessibilityLabel = + result.kind === "file" + ? [result.title, result.subtitle].filter(Boolean).join(" ") + : choice?.path.join(" › "); const accessibilityState = useMemo( () => (isNative && choice ? { selected: choice.selected } : undefined), [choice], @@ -329,7 +384,9 @@ const ResultRow = memo(function ResultRow({ result, active, onSelect }: ResultRo accessibilityLabel={accessibilityLabel} accessibilityState={accessibilityState} aria-pressed={isWeb ? choice?.selected : undefined} - testID={choice?.testId} + testID={ + result.kind === "file" ? `command-center-file-row-${result.filePath}` : choice?.testId + } > @@ -337,6 +394,28 @@ const ResultRow = memo(function ResultRow({ result, active, onSelect }: ResultRo }); function ResultContent({ result }: { result: CommandCenterResult }) { + if (result.kind === "file") { + return ( + + + + + + + + {result.title} + + {result.subtitle ? ( + + {" "} + {result.subtitle} + + ) : null} + + + + ); + } if (result.kind === "agent") { const agent = result.agent; return ( @@ -545,8 +624,24 @@ export function CommandCenter() { ); const keyExtractor = useCallback((row: CommandCenterListRow) => row.key, []); const empty = useMemo( - () => {t("shell.commandCenter.noMatches")}, - [t], + () => + state.fileSearchError || state.fileSearchLoading ? null : ( + {t("shell.commandCenter.noMatches")} + ), + [state.fileSearchError, state.fileSearchLoading, t], + ); + const fileSearchError = useMemo( + () => + state.fileSearchError ? ( + + {t("common.errors.error")}: {state.fileSearchError} + + ) : null, + [state.fileSearchError, t], ); const commonListProps = { data: state.rows, @@ -554,6 +649,8 @@ export function CommandCenter() { keyExtractor, getItemLayout, ListEmptyComponent: empty, + style: styles.results, + testID: "command-center-results", keyboardShouldPersistTaps: KEYBOARD_SHOULD_PERSIST_TAPS, showsVerticalScrollIndicator: false, initialNumToRender: 12, @@ -606,7 +703,10 @@ export function CommandCenter() { keyboardBlurBehavior="restore" accessible={false} > - + + {state.scope === "files" ? ( + + ) : null} + + {fileSearchError} ); @@ -632,20 +741,32 @@ export function CommandCenter() { - + + {state.scope === "files" ? ( + + ) : null} + - + {fileSearchError} + @@ -653,6 +774,41 @@ export function CommandCenter() { ); } +function FileSearchLoadingIndicator({ loading, label }: { loading: boolean; label: string }) { + return ( + + {loading ? ( + + + + ) : null} + + ); +} + +// The chip sits at the input's own height and type size so it reads as the scope of the field +// rather than a badge dropped into the query. Its 28px matches the input's line box, so showing or +// dropping the scope cannot resize the header. +function ScopeChip({ label, onRemove }: { label: string; onRemove(): void }) { + return ( + + + {label} + + + ); +} + const styles = StyleSheet.create((theme) => ({ overlay: { flex: 1, @@ -663,6 +819,7 @@ const styles = StyleSheet.create((theme) => ({ backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: "rgba(0, 0, 0, 0.5)" }, panel: { width: 640, + height: 560, maxWidth: "92%", maxHeight: "80%", borderWidth: 1, @@ -690,7 +847,29 @@ const styles = StyleSheet.create((theme) => ({ color: theme.colors.foreground, outlineWidth: 0, }, - results: { flexGrow: 0 }, + growingInput: { flex: 1, minWidth: 0 }, + searchRow: { flexDirection: "row", alignItems: "center", gap: theme.spacing[2] }, + fileSearchStatus: { width: 14, height: 14, alignItems: "center", justifyContent: "center" }, + scopeChip: { + // Stretching to the row rather than setting a height keeps the chip exactly as tall as the + // input's line box, so showing or dropping the scope can never resize the header. + alignSelf: "stretch", + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[1.5], + marginRight: theme.spacing[1], + paddingHorizontal: theme.spacing[2], + borderRadius: theme.borderRadius.md, + borderWidth: 1, + borderColor: theme.colors.borderAccent, + backgroundColor: theme.colors.surface2, + }, + scopeChipLabel: { + color: theme.colors.foreground, + fontSize: theme.fontSize.sm, + fontWeight: "500", + }, + results: { flex: 1 }, sectionLabel: { paddingHorizontal: theme.spacing[4], paddingBottom: theme.spacing[2], @@ -726,6 +905,9 @@ const styles = StyleSheet.create((theme) => ({ lineHeight: 18, flexShrink: 1, }, + fileLine: { flex: 1, minWidth: 0, fontSize: theme.fontSize.sm, lineHeight: 20 }, + fileName: { color: theme.colors.foreground }, + filePath: { color: theme.colors.foregroundMuted }, subtitle: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, lineHeight: 16 }, iconSlot: { width: 16, height: 20, alignItems: "center", justifyContent: "center" }, rowShortcut: { flexShrink: 0 }, @@ -755,6 +937,14 @@ const styles = StyleSheet.create((theme) => ({ color: theme.colors.foregroundMuted, fontSize: theme.fontSize.sm, }, + errorText: { + paddingHorizontal: theme.spacing[4], + paddingVertical: theme.spacing[3], + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + color: theme.colors.statusDanger, + fontSize: theme.fontSize.sm, + }, sheetBackground: { backgroundColor: theme.colors.surface0 }, sheetHandle: { backgroundColor: theme.colors.palette.zinc[600] }, titledSection: { height: 32, justifyContent: "flex-end" }, diff --git a/packages/app/src/command-center/results.ts b/packages/app/src/command-center/results.ts index da3ea5c250..e0c6107958 100644 --- a/packages/app/src/command-center/results.ts +++ b/packages/app/src/command-center/results.ts @@ -20,6 +20,16 @@ export interface CommandCenterAgentResult { run(): void; } +export interface CommandCenterFileResult { + kind: "file"; + id: string; + filePath: string; + title: string; + subtitle: string; + searchText: string; + run(): void; +} + export interface CommandCenterContributionResult { kind: "contribution"; id: string; @@ -31,6 +41,7 @@ export interface CommandCenterContributionResult { export type CommandCenterResult = | CommandCenterWorkspaceResult | CommandCenterAgentResult + | CommandCenterFileResult | CommandCenterContributionResult; export interface CommandCenterResultSection { @@ -78,6 +89,7 @@ function contributionSearchText(contribution: CommandCenterContribution): string function resultHeight(result: CommandCenterResult): number { if (result.kind === "workspace" || result.kind === "agent") return 56; + if (result.kind === "file") return 36; if (result.contribution.presentation.kind === "action") { return result.contribution.presentation.subtitle ? 56 : 36; } diff --git a/packages/app/src/command-center/root-contributions.test.ts b/packages/app/src/command-center/root-contributions.test.ts new file mode 100644 index 0000000000..9ac7626889 --- /dev/null +++ b/packages/app/src/command-center/root-contributions.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "vitest"; +import type { SidebarGroupMode } from "@/stores/sidebar-view-store"; +import type { CommandCenterIconProps } from "./contributions"; +import { buildGroupingContribution, type GroupingCommandCenterSource } from "./root-contributions"; + +function ProjectIcon(_props: CommandCenterIconProps) { + return null; +} + +function StatusIcon(_props: CommandCenterIconProps) { + return null; +} + +function source(groupMode: SidebarGroupMode): { + value: GroupingCommandCenterSource; + applied: SidebarGroupMode[]; +} { + const applied: SidebarGroupMode[] = []; + return { + value: { + groupMode, + labels: { + section: "Actions", + groupByProject: "Group by project", + groupByStatus: "Group by status", + }, + icons: { project: ProjectIcon, status: StatusIcon }, + setGroupMode: (mode) => applied.push(mode), + }, + applied, + }; +} + +describe("grouping command center contribution", () => { + it("offers status while grouped by project", () => { + const fixture = source("project"); + const contribution = buildGroupingContribution(fixture.value); + + expect(contribution.presentation).toMatchObject({ + title: "Group by status", + icon: StatusIcon, + }); + + contribution.run(); + expect(fixture.applied).toEqual(["status"]); + }); + + it("offers project while grouped by status", () => { + const fixture = source("status"); + const contribution = buildGroupingContribution(fixture.value); + + expect(contribution.presentation).toMatchObject({ + title: "Group by project", + icon: ProjectIcon, + }); + + contribution.run(); + expect(fixture.applied).toEqual(["project"]); + }); + + it("keeps a stable id across both modes so the registry tiebreak never moves", () => { + expect(buildGroupingContribution(source("project").value).id).toBe( + buildGroupingContribution(source("status").value).id, + ); + }); + + it("stays out of the default empty-query list", () => { + for (const mode of ["project", "status"] as const) { + const contribution = buildGroupingContribution(source(mode).value); + expect(contribution.visibility).toBe("query"); + expect(contribution.group).toBe("actions"); + // 6 is keyboard-shortcuts and 7 belongs to the workspace actions in #3013. + expect(contribution.rank).toBe(8); + } + }); +}); diff --git a/packages/app/src/command-center/root-contributions.ts b/packages/app/src/command-center/root-contributions.ts new file mode 100644 index 0000000000..f9b6a6d180 --- /dev/null +++ b/packages/app/src/command-center/root-contributions.ts @@ -0,0 +1,39 @@ +import type { SidebarGroupMode } from "@/stores/sidebar-view-store"; +import type { CommandCenterContribution, CommandCenterIcon } from "./contributions"; + +export interface GroupingCommandCenterSource { + groupMode: SidebarGroupMode; + labels: { + section: string; + groupByProject: string; + groupByStatus: string; + }; + icons: { + project?: CommandCenterIcon; + status?: CommandCenterIcon; + }; + setGroupMode(mode: SidebarGroupMode): void; +} + +// One entry that always names the mode you are not in, so it can never read as a no-op. +export function buildGroupingContribution( + source: GroupingCommandCenterSource, +): CommandCenterContribution { + // Collapse into nextGroupMode() from sidebar-view-store once #2504 lands. + const target: SidebarGroupMode = source.groupMode === "project" ? "status" : "project"; + return { + id: "sidebar-grouping", + group: "actions", + groupRank: 0, + rank: 8, + keywords: ["group", "grouping", "sort", "sidebar", "project", "status"], + visibility: "query", + run: () => source.setGroupMode(target), + presentation: { + kind: "action", + title: target === "status" ? source.labels.groupByStatus : source.labels.groupByProject, + sectionTitle: source.labels.section, + icon: target === "status" ? source.icons.status : source.icons.project, + }, + }; +} diff --git a/packages/app/src/command-center/root-registration.tsx b/packages/app/src/command-center/root-registration.tsx index 25d88f20c4..faa89e0fc6 100644 --- a/packages/app/src/command-center/root-registration.tsx +++ b/packages/app/src/command-center/root-registration.tsx @@ -3,6 +3,8 @@ import { router, type Href } from "expo-router"; import { useTranslation } from "react-i18next"; import { CalendarClock, + CircleDashed, + Folder, FolderPlus, History, Home, @@ -18,6 +20,7 @@ import { keyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher" import { useKeyboardShortcutsAvailable } from "@/keyboard/availability"; import { resolveShortcutKeysForAction } from "@/keyboard/keyboard-shortcuts"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; +import { useSidebarViewStore } from "@/stores/sidebar-view-store"; import { clearCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore"; import { buildOpenProjectRoute, @@ -28,6 +31,7 @@ import { import { getShortcutOs } from "@/utils/shortcut-platform"; import type { CommandCenterContribution, CommandCenterIconProps } from "./contributions"; import { useCommandCenterActions } from "./provider"; +import { buildGroupingContribution } from "./root-contributions"; const ThemedPlus = withUnistyles(Plus, (theme) => ({ color: theme.colors.foregroundMuted })); const ThemedFolderPlus = withUnistyles(FolderPlus, (theme) => ({ @@ -46,6 +50,10 @@ const ThemedSettings = withUnistyles(Settings, (theme) => ({ color: theme.colors.foregroundMuted, })); const ThemedHome = withUnistyles(Home, (theme) => ({ color: theme.colors.foregroundMuted })); +const ThemedFolder = withUnistyles(Folder, (theme) => ({ color: theme.colors.foregroundMuted })); +const ThemedCircleDashed = withUnistyles(CircleDashed, (theme) => ({ + color: theme.colors.foregroundMuted, +})); function PlusIcon({ size }: CommandCenterIconProps) { return ; @@ -75,6 +83,14 @@ function HomeIcon({ size }: CommandCenterIconProps) { return ; } +function FolderIcon({ size }: CommandCenterIconProps) { + return ; +} + +function CircleDashedIcon({ size }: CommandCenterIconProps) { + return ; +} + export function CommandCenterRootActions() { const { t } = useTranslation(); const { overrides } = useKeyboardShortcutOverrides(); @@ -85,6 +101,10 @@ export function CommandCenterRootActions() { const sessionsRoute = useMemo(() => buildSessionsRoute(), []); const schedulesRoute = useMemo(() => buildSchedulesRoute(), []); const setShortcutsDialogOpen = useKeyboardShortcutsStore((state) => state.setShortcutsDialogOpen); + // Narrow selector on purpose: a whole-store subscription would re-register every root action + // each time host filters are reconciled. + const groupMode = useSidebarViewStore((state) => state.groupMode); + const setGroupMode = useSidebarViewStore((state) => state.setGroupMode); const shortcutPlatform = useMemo( () => ({ isMac: getShortcutOs() === "mac", isDesktop: getIsElectronRuntime() }), [], @@ -229,13 +249,28 @@ export function CommandCenterRootActions() { }); } + availableActions.push( + buildGroupingContribution({ + groupMode, + labels: { + section: t("shell.commandCenter.actions"), + groupByProject: t("shell.commandCenter.groupByProject"), + groupByStatus: t("shell.commandCenter.groupByStatus"), + }, + icons: { project: FolderIcon, status: CircleDashedIcon }, + setGroupMode, + }), + ); + return availableActions; }, [ + groupMode, homeRoute, openAddProject, overrides, schedulesRoute, sessionsRoute, + setGroupMode, setShortcutsDialogOpen, settingsRoute, shortcutPlatform, diff --git a/packages/app/src/command-center/workspace-file-search-model.ts b/packages/app/src/command-center/workspace-file-search-model.ts new file mode 100644 index 0000000000..4f43feaf78 --- /dev/null +++ b/packages/app/src/command-center/workspace-file-search-model.ts @@ -0,0 +1,15 @@ +export interface WorkspaceFileSearchEntry { + path: string; + name: string; + directory: string; +} + +export function describeWorkspaceFilePath(path: string): WorkspaceFileSearchEntry { + const normalized = path.replace(/\\/g, "/"); + const separator = normalized.lastIndexOf("/"); + return { + path: normalized, + name: separator >= 0 ? normalized.slice(separator + 1) : normalized, + directory: separator >= 0 ? normalized.slice(0, separator) : "", + }; +} diff --git a/packages/app/src/command-center/workspace-file-search.test.ts b/packages/app/src/command-center/workspace-file-search.test.ts new file mode 100644 index 0000000000..6070ce1576 --- /dev/null +++ b/packages/app/src/command-center/workspace-file-search.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { describeWorkspaceFilePath } from "./workspace-file-search-model"; + +describe("describeWorkspaceFilePath", () => { + it("separates a workspace-relative file into its row labels", () => { + expect(describeWorkspaceFilePath("src/components/message.tsx")).toEqual({ + path: "src/components/message.tsx", + name: "message.tsx", + directory: "src/components", + }); + }); + + it("normalizes Windows separators before opening or presenting a file", () => { + expect(describeWorkspaceFilePath("src\\components\\message.tsx")).toEqual({ + path: "src/components/message.tsx", + name: "message.tsx", + directory: "src/components", + }); + }); + + it("keeps root files free of a redundant directory label", () => { + expect(describeWorkspaceFilePath("package.json")).toEqual({ + path: "package.json", + name: "package.json", + directory: "", + }); + }); +}); diff --git a/packages/app/src/command-center/workspace-file-search.ts b/packages/app/src/command-center/workspace-file-search.ts new file mode 100644 index 0000000000..7a536159b6 --- /dev/null +++ b/packages/app/src/command-center/workspace-file-search.ts @@ -0,0 +1,148 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { openWorkspaceFileFromExplorer } from "@/screens/workspace/workspace-file-open-command"; +import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store"; +import { usePanelStore } from "@/stores/panel-store"; +import { useSessionStore } from "@/stores/session-store"; +import { useWorkspaceDirectory } from "@/stores/session-store-hooks"; +import { useWorkspaceLayoutStore } from "@/stores/workspace-layout-store"; +import { clearCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore"; +import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model"; +import { + describeWorkspaceFilePath, + type WorkspaceFileSearchEntry, +} from "./workspace-file-search-model"; + +interface DirectorySuggestionEntry { + path: string; +} + +const FILE_SEARCH_DEBOUNCE_MS = 100; +const FILE_SEARCH_LIMIT = 100; + +interface WorkspaceFileSearchState { + sourceKey: string | null; + requestKey: string | null; + entries: readonly WorkspaceFileSearchEntry[]; + loading: boolean; + error: string | null; +} + +const EMPTY_STATE: WorkspaceFileSearchState = { + sourceKey: null, + requestKey: null, + entries: [], + loading: false, + error: null, +}; + +function errorMessage(error: unknown): string { + if (!(error instanceof Error)) return String(error); + if (error.name !== "DaemonRpcError") return error.message; + return error.message.replace(/ requestType=\S+(?: code=\S+)?$/, ""); +} + +function describeFileEntries( + entries: readonly DirectorySuggestionEntry[], +): WorkspaceFileSearchEntry[] { + return entries.map(({ path }) => describeWorkspaceFilePath(path)); +} + +export function useWorkspaceFileSearch(input: { enabled: boolean; query: string }): { + entries: readonly WorkspaceFileSearchEntry[]; + loading: boolean; + error: string | null; + openFile(path: string): void; +} { + const selection = useActiveWorkspaceSelection(); + const serverId = selection?.serverId ?? null; + const workspaceId = selection?.workspaceId ?? null; + const cwd = useWorkspaceDirectory(serverId, workspaceId); + const client = useSessionStore((state) => + serverId ? (state.sessions[serverId]?.client ?? null) : null, + ); + const [state, setState] = useState(EMPTY_STATE); + const sourceKey = useMemo( + () => (serverId && workspaceId && cwd && client ? `${serverId}\0${workspaceId}\0${cwd}` : null), + [client, cwd, serverId, workspaceId], + ); + const requestKey = useMemo( + () => (input.enabled && sourceKey ? `${sourceKey}\0${input.query}` : null), + [input.enabled, input.query, sourceKey], + ); + + useEffect(() => { + if (!requestKey || !client || !cwd) { + setState(EMPTY_STATE); + return; + } + const activeClient = client; + const activeCwd = cwd; + + let cancelled = false; + setState((previous) => ({ + sourceKey, + requestKey, + entries: previous.sourceKey === sourceKey ? previous.entries : [], + loading: true, + error: null, + })); + async function search(): Promise { + try { + const payload = await activeClient.getDirectorySuggestions({ + cwd: activeCwd, + query: input.query, + includeFiles: true, + includeDirectories: false, + limit: FILE_SEARCH_LIMIT, + }); + if (cancelled) return; + setState({ + sourceKey, + requestKey, + entries: payload.error ? [] : describeFileEntries(payload.entries), + loading: false, + error: payload.error ?? null, + }); + } catch (error) { + if (!cancelled) { + setState({ + sourceKey, + requestKey, + entries: [], + loading: false, + error: errorMessage(error), + }); + } + } + } + + const timer = setTimeout(() => void search(), input.query.trim() ? FILE_SEARCH_DEBOUNCE_MS : 0); + + return () => { + cancelled = true; + clearTimeout(timer); + }; + }, [client, cwd, input.query, requestKey, sourceKey]); + + const openFile = useCallback( + (path: string) => { + if (!serverId || !workspaceId) return; + clearCommandCenterFocusRestoreElement(); + openWorkspaceFileFromExplorer({ + filePath: path, + persistenceKey: buildWorkspaceTabPersistenceKey({ serverId, workspaceId }), + showMobileAgent: usePanelStore.getState().showMobileAgent, + openWorkspaceTabFocused: useWorkspaceLayoutStore.getState().openTabFocused, + focusWorkspaceTab: useWorkspaceLayoutStore.getState().focusTab, + }); + }, + [serverId, workspaceId], + ); + + return { + entries: requestKey && state.sourceKey === sourceKey ? state.entries : [], + loading: Boolean(requestKey) && (state.requestKey !== requestKey || state.loading), + error: state.requestKey === requestKey ? state.error : null, + openFile, + }; +} diff --git a/packages/app/src/components/adaptive-modal-sheet.tsx b/packages/app/src/components/adaptive-modal-sheet.tsx index b689bdc6a4..94191f4eb9 100644 --- a/packages/app/src/components/adaptive-modal-sheet.tsx +++ b/packages/app/src/components/adaptive-modal-sheet.tsx @@ -128,9 +128,13 @@ const styles = StyleSheet.create((theme) => ({ // Horizontal padding matches the model picker's row indent: the picker uses // children mode (desktopChildrenScrollContent, no scroll padding), so the // row content starts at item.paddingHorizontal = spacing[3]. + // The search row below owns the gap under the title: the input already + // carries its own vertical padding, so a paddingBottom here would stack on + // top of two more and push the title far off the field. inlineHeaderRow: { paddingHorizontal: theme.spacing[3], - paddingVertical: theme.spacing[2], + paddingTop: theme.spacing[2], + paddingBottom: 0, flexDirection: "row", alignItems: "center", gap: theme.spacing[2], diff --git a/packages/app/src/components/combined-model-selector.tsx b/packages/app/src/components/combined-model-selector.tsx index 179e531324..5053e138d7 100644 --- a/packages/app/src/components/combined-model-selector.tsx +++ b/packages/app/src/components/combined-model-selector.tsx @@ -3,6 +3,7 @@ import { Pressable, Text, View, type PressableStateCallbackType } from "react-na import { useTranslation } from "react-i18next"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import type { AgentProvider } from "@getpaseo/protocol/agent-types"; +import type { AgentProfilePicker } from "@/agent-profiles"; import { ComboboxTrigger } from "@/components/ui/combobox-trigger"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { Combobox, type ComboboxOption, type ComboboxProps } from "@/components/ui/combobox"; @@ -12,7 +13,6 @@ import type { ProviderSelectorProvider } from "@/provider-selection/provider-sel import { ICON_SIZE, type Theme } from "@/styles/theme"; const EMPTY_COMBOBOX_OPTIONS: ComboboxOption[] = []; -const EMPTY_FAVORITE_KEYS = new Set(); const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); const foregroundMutedMapping = (theme: Theme) => ({ @@ -27,8 +27,9 @@ interface CombinedModelSelectorProps { selectedModel: string; onSelect: (provider: AgentProvider, modelId: string) => void; isLoading: boolean; - favoriteKeys?: Set; - onToggleFavorite?: (provider: string, modelId: string) => void; + profiles?: AgentProfilePicker | null; + onApplyProfile?: (profileId: string) => void; + onEditProfiles?: () => void; renderTrigger?: (input: { selectedModelLabel: string; onPress: () => void; @@ -66,8 +67,9 @@ export function CombinedModelSelector({ selectedModel, onSelect, isLoading, - favoriteKeys = EMPTY_FAVORITE_KEYS, - onToggleFavorite, + profiles = null, + onApplyProfile, + onEditProfiles, renderTrigger, onOpen, onClose, @@ -89,7 +91,7 @@ export function CombinedModelSelector({ selectedProvider, selectedModel, isLoading, - favoriteKeys, + profiles, serverId, }); const { prepareToOpen, reset } = browser; @@ -153,13 +155,28 @@ export function CombinedModelSelector({ [disabled, isOpen, renderTrigger, triggerFill], ); + const handleApplyProfile = useCallback( + (profileId: string) => { + onApplyProfile?.(profileId); + handleOpenChange(false); + }, + [handleOpenChange, onApplyProfile], + ); + + const handleEditProfiles = useCallback(() => { + handleOpenChange(false); + onEditProfiles?.(); + }, [handleOpenChange, onEditProfiles]); + const selectorBody = isContentReady ? ( ) : ( @@ -230,7 +247,9 @@ export function CombinedModelSelector({ anchorRef={anchorRef} desktopPlacement={desktopPlacement} desktopMinWidth={desktopMinWidth} + desktopLockWidth desktopFixedHeight={browser.desktopFixedHeight} + desktopChildrenScrollEnabled={false} header={browser.header} mobileChildrenScrollEnabled={!browser.isProviderView || !isNative} mobileChildrenContentContainerStyle={styles.mobileBrowserContent} diff --git a/packages/app/src/components/draggable-list.web.test.tsx b/packages/app/src/components/draggable-list.web.test.tsx new file mode 100644 index 0000000000..4ccca38297 --- /dev/null +++ b/packages/app/src/components/draggable-list.web.test.tsx @@ -0,0 +1,90 @@ +/** + * @vitest-environment jsdom + */ +import React, { memo } from "react"; +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { DraggableListDragHandleProps } from "./draggable-list.types"; +import { DraggableList, type DraggableRenderItemInfo } from "./draggable-list.web"; + +interface Item { + id: string; +} + +const items: Item[] = [{ id: "one" }]; + +function keyExtractor(item: Item): string { + return item.id; +} + +function onDragEnd() {} + +let root: Root | null = null; +let container: HTMLElement | null = null; +let rowRenderCount = 0; + +const Row = memo(function Row({ + dragHandleProps: _dragHandleProps, +}: { + dragHandleProps?: DraggableListDragHandleProps; +}) { + rowRenderCount += 1; + return
row
; +}); + +function renderRow(info: DraggableRenderItemInfo) { + return ; +} + +function renderReplacementRow(info: DraggableRenderItemInfo) { + return ; +} + +beforeEach(() => { + vi.stubGlobal("React", React); + vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + rowRenderCount = 0; +}); + +afterEach(() => { + act(() => root?.unmount()); + container?.remove(); + root = null; + container = null; + vi.unstubAllGlobals(); +}); + +describe("DraggableList web row stability", () => { + it("keeps drag handle props stable when the render callback changes", () => { + act(() => { + root?.render( + , + ); + }); + expect(rowRenderCount).toBe(1); + + act(() => { + root?.render( + , + ); + }); + + expect(rowRenderCount).toBe(1); + }); +}); diff --git a/packages/app/src/components/draggable-list.web.tsx b/packages/app/src/components/draggable-list.web.tsx index 7c1e683e50..92a4d2287e 100644 --- a/packages/app/src/components/draggable-list.web.tsx +++ b/packages/app/src/components/draggable-list.web.tsx @@ -34,6 +34,61 @@ const DRAG_ACTIVATION_CONFIG = { touchHoldTolerance: 8, }; +function areRecordsEqual( + left: Record | undefined, + right: Record | undefined, +): boolean { + if (left === right) return true; + if (!left || !right) return false; + const leftKeys = Object.keys(left); + const rightKeys = Object.keys(right); + if (leftKeys.length !== rightKeys.length) return false; + return leftKeys.every((key) => Object.is(left[key], right[key])); +} + +function useShallowStableRecord( + value: Record | undefined, +): Record | undefined { + const stableRef = useRef(value); + if (!areRecordsEqual(stableRef.current, value)) { + stableRef.current = value; + } + return stableRef.current; +} + +function useStableListenerRecord( + listeners: Record | undefined, +): Record | undefined { + const latestListenersRef = useRef(listeners); + latestListenersRef.current = listeners; + const listenerKeys = Object.keys(listeners ?? {}).sort(); + const listenerKeySignature = listenerKeys.join("\u0000"); + const stableListenersRef = useRef<{ + keySignature: string; + listeners: Record | undefined; + }>(undefined); + if (stableListenersRef.current?.keySignature !== listenerKeySignature) { + stableListenersRef.current = { + keySignature: listenerKeySignature, + listeners: + listenerKeys.length === 0 + ? undefined + : Object.fromEntries( + listenerKeys.map((key) => [ + key, + (...args: unknown[]) => { + const listener = latestListenersRef.current?.[key]; + if (typeof listener === "function") { + return Reflect.apply(listener, undefined, args); + } + }, + ]), + ), + }; + } + return stableListenersRef.current.listeners; +} + interface SortableItemProps { id: string; item: T; @@ -92,19 +147,28 @@ function SortableItemInner({ }), [combinedTransform, transition, isDragging], ); + const stableAttributes = useShallowStableRecord(attributes as unknown as Record); + const stableListeners = useStableListenerRecord( + listeners as unknown as Record | undefined, + ); + const dragHandleProps = useMemo( + () => + useDragHandle + ? { + attributes: stableAttributes, + listeners: stableListeners, + setActivatorNodeRef: setActivatorNodeRef as unknown as (node: unknown) => void, + } + : undefined, + [setActivatorNodeRef, stableAttributes, stableListeners, useDragHandle], + ); const info: DraggableRenderItemInfo = { item, index, drag, isActive: activeId === id, - dragHandleProps: useDragHandle - ? { - attributes: attributes as unknown as Record, - listeners: listeners as unknown as Record, - setActivatorNodeRef: setActivatorNodeRef as unknown as (node: unknown) => void, - } - : undefined, + dragHandleProps, }; const wrapperProps = useDragHandle diff --git a/packages/app/src/components/file-actions-menu.tsx b/packages/app/src/components/file-actions-menu.tsx index 185a04fde3..63de27475e 100644 --- a/packages/app/src/components/file-actions-menu.tsx +++ b/packages/app/src/components/file-actions-menu.tsx @@ -1,6 +1,20 @@ -import { useMemo, type ReactElement, type ReactNode } from "react"; +import { Fragment, useMemo, type ReactElement, type ReactNode } from "react"; import { withUnistyles } from "react-native-unistyles"; -import { Copy, Download, FileText, MessageSquarePlus, type LucideIcon } from "lucide-react-native"; +import { + Copy, + CopyPlus, + Download, + FilePlus, + FileText, + FolderMinus, + FolderOpen, + FolderPlus, + MessageSquarePlus, + Pencil, + Trash2, + Undo2, + type LucideIcon, +} from "lucide-react-native"; import { useTranslation } from "react-i18next"; import { ICON_SIZE, type Theme } from "@/styles/theme"; import { @@ -10,11 +24,14 @@ import { } from "@/components/ui/context-menu"; const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); +const destructiveColorMapping = (theme: Theme) => ({ color: theme.colors.destructive }); interface FileAction { key: string; label: string; icon: LucideIcon; onSelect: () => void; + destructive?: boolean; + separatorBefore?: boolean; testID?: string; } @@ -23,8 +40,18 @@ interface FileActionsContextMenuContentProps { fileExists?: boolean; onOpenFile?: () => void; onCopyPath?: () => void; + onCopyRelativePath?: () => void; + onReveal?: () => void; + revealTargetName?: string; onDownload?: () => void; onAddToChat?: () => void; + onNewFile?: () => void; + onNewFolder?: () => void; + onCollapseFolder?: () => void; + onRename?: () => void; + onDuplicate?: () => void; + onRevert?: () => void; + onDelete?: () => void; /** Optional metadata block rendered above the actions (e.g. size/modified). */ header?: ReactNode; testIDPrefix?: string; @@ -39,51 +66,161 @@ export function FileActionsContextMenuContent({ fileExists = true, onOpenFile, onCopyPath, + onCopyRelativePath, + onReveal, + revealTargetName, onDownload, onAddToChat, + onNewFile, + onNewFolder, + onCollapseFolder, + onRename, + onDuplicate, + onRevert, + onDelete, header, testIDPrefix, }: FileActionsContextMenuContentProps): ReactElement | null { const { t } = useTranslation(); const actions = useMemo(() => { const availableFile = fileKind === "file" && fileExists; - const next: FileAction[] = []; - if (availableFile && onOpenFile) { - next.push({ - key: "open-file", - label: t("workspace.fileActions.openFile"), - icon: FileText, - onSelect: onOpenFile, - testID: testIDPrefix ? `${testIDPrefix}-open-file` : undefined, - }); - } - if (onCopyPath) { - next.push({ - key: "copy-path", - label: t("workspace.fileActions.copyPath"), - icon: Copy, - onSelect: onCopyPath, - }); - } - if (availableFile && onDownload) { - next.push({ - key: "download", - label: t("workspace.fileActions.download"), - icon: Download, - onSelect: onDownload, - }); - } - if (availableFile && onAddToChat) { - next.push({ - key: "add-to-chat", - label: t("workspace.fileActions.addToChat"), - icon: MessageSquarePlus, - onSelect: onAddToChat, - testID: testIDPrefix ? `${testIDPrefix}-add-to-chat` : undefined, - }); - } - return next; - }, [fileExists, fileKind, onAddToChat, onCopyPath, onDownload, onOpenFile, t, testIDPrefix]); + const specs: Array = [ + onNewFile + ? { + key: "new-file", + label: t("workspace.fileActions.newFile"), + icon: FilePlus, + onSelect: onNewFile, + } + : null, + onNewFolder + ? { + key: "new-folder", + label: t("workspace.fileActions.newFolder"), + icon: FolderPlus, + onSelect: onNewFolder, + } + : null, + onCollapseFolder + ? { + key: "collapse-folder", + label: t("workspace.fileActions.collapseFolder"), + icon: FolderMinus, + onSelect: onCollapseFolder, + } + : null, + availableFile && onOpenFile + ? { + key: "open-file", + label: t("workspace.fileActions.openFile"), + icon: FileText, + onSelect: onOpenFile, + } + : null, + onRename + ? { + key: "rename", + label: t("workspace.fileActions.rename"), + icon: Pencil, + onSelect: onRename, + } + : null, + onDuplicate + ? { + key: "duplicate", + label: t("workspace.fileActions.duplicate"), + icon: CopyPlus, + onSelect: onDuplicate, + } + : null, + onCopyPath + ? { + key: "copy-path", + label: t("workspace.fileActions.copyPath"), + icon: Copy, + onSelect: onCopyPath, + } + : null, + onCopyRelativePath + ? { + key: "copy-relative-path", + label: t("workspace.fileActions.copyRelativePath"), + icon: Copy, + onSelect: onCopyRelativePath, + } + : null, + onReveal && revealTargetName + ? { + key: "reveal", + label: t("workspace.fileActions.revealIn", { target: revealTargetName }), + icon: FolderOpen, + onSelect: onReveal, + } + : null, + availableFile && onDownload + ? { + key: "download", + label: t("workspace.fileActions.download"), + icon: Download, + onSelect: onDownload, + } + : null, + availableFile && onAddToChat + ? { + key: "add-to-chat", + label: t("workspace.fileActions.addToChat"), + icon: MessageSquarePlus, + onSelect: onAddToChat, + } + : null, + onRevert + ? { + key: "revert", + label: t("workspace.fileActions.revert"), + icon: Undo2, + onSelect: onRevert, + destructive: true, + } + : null, + onDelete + ? { + key: "delete", + label: t("workspace.fileActions.delete"), + icon: Trash2, + onSelect: onDelete, + destructive: true, + } + : null, + ]; + const availableActions = specs.filter((action): action is FileAction => action !== null); + return availableActions.map((action, index) => + Object.assign(action, { + separatorBefore: Boolean( + action.destructive && index > 0 && !availableActions[index - 1].destructive, + ), + testID: testIDPrefix ? `${testIDPrefix}-${action.key}` : undefined, + }), + ); + }, [ + fileExists, + fileKind, + onAddToChat, + onCollapseFolder, + onCopyPath, + onCopyRelativePath, + onDelete, + onDownload, + onDuplicate, + onNewFile, + onNewFolder, + onOpenFile, + onRename, + onReveal, + onRevert, + revealTargetName, + t, + testIDPrefix, + ]); if (actions.length === 0) { return null; @@ -101,21 +238,32 @@ export function FileActionsContextMenuContent({ ) : null} {actions.map((action) => ( - + + {action.separatorBefore ? : null} + + ))} ); } function FileActionMenuItem({ action }: { action: FileAction }): ReactElement { - const Icon = action.icon; - const ThemedIcon = useMemo(() => withUnistyles(Icon), [Icon]); - const leading = useMemo( - () => , - [ThemedIcon], - ); + const leading = useMemo(() => { + const ThemedIcon = withUnistyles(action.icon); + return ( + + ); + }, [action.destructive, action.icon]); return ( - + {action.label} ); diff --git a/packages/app/src/components/file-explorer-pane.tsx b/packages/app/src/components/file-explorer-pane.tsx index 798ad36b9c..c24268a6d6 100644 --- a/packages/app/src/components/file-explorer-pane.tsx +++ b/packages/app/src/components/file-explorer-pane.tsx @@ -1,4 +1,13 @@ -import { useCallback, useEffect, useMemo, useRef, type ReactElement, type RefObject } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type ReactElement, + type ReactNode, + type RefObject, +} from "react"; import { useQuery } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; import { @@ -6,15 +15,19 @@ import { ListRenderItemInfo, Pressable, Text, + TextInput, View, + type NativeSyntheticEvent, type PressableStateCallbackType, type StyleProp, + type TextInputKeyPressEventData, type ViewStyle, } from "react-native"; import { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles"; import { useIsCompactFormFactor, WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout"; +import { isWeb } from "@/constants/platform"; import * as Clipboard from "expo-clipboard"; -import { ChevronDown, Eye, EyeOff, RotateCw } from "lucide-react-native"; +import { ChevronDown, Eye, EyeOff, FilePlus, FolderPlus, RotateCw } from "lucide-react-native"; import { MaterialFileIcon } from "@/components/material-file-icon"; import { TreeChevron, @@ -39,8 +52,9 @@ import type { } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store"; import { FileActionsContextMenuContent } from "@/components/file-actions-menu"; -import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu"; +import { ContextMenu, ContextMenuTrigger, useContextMenu } from "@/components/ui/context-menu"; import { useFileDownload } from "@/hooks/use-file-download"; +import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon"; import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions"; import { buildWorkspaceExplorerStateKey } from "@/hooks/use-file-explorer-actions"; import { usePanelStore, type ExpandedPathsUpdate, type SortOption } from "@/stores/panel-store"; @@ -56,6 +70,9 @@ import { type ExplorerTreeRow, } from "@/file-explorer/tree"; import { useWorkspaceFileDragSource } from "@/attachments/use-workspace-file-drag-source"; +import { confirmDialog } from "@/utils/confirm-dialog"; +import { useToast } from "@/contexts/toast-context"; +import { openDesktopTarget, useDesktopOpenTargets } from "@/workspace/desktop-open-targets"; const SORT_OPTIONS: { value: SortOption }[] = [ { value: "name" }, @@ -68,6 +85,18 @@ const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted, }); +function DirectoryChevronIcon({ loading, expanded }: { loading: boolean; expanded: boolean }) { + if (loading) { + return ( + + ); + } + return ; +} + function formatFileSize({ size }: { size: number }): string { if (size < 1024) { return `${size} B`; @@ -87,9 +116,18 @@ interface TreeRowItemProps { isSelected: boolean; loading: boolean; onEntryPress: (entry: ExplorerEntry) => void; + onSelectEntry: (entry: ExplorerEntry) => void; onCopyPath: (path: string) => void; + onCopyRelativePath: (path: string) => void; + onRevealEntry?: (entry: ExplorerEntry) => void; + revealTargetName?: string; onDownloadEntry: (entry: ExplorerEntry) => void; onAddToChat?: (path: string) => void; + onNewEntry?: (parentPath: string, kind: "file" | "directory") => void; + onCollapseDirectory?: (path: string) => void; + onRenameEntry?: (entry: ExplorerEntry) => void; + onDuplicateEntry?: (entry: ExplorerEntry) => void; + onDeleteEntry?: (entry: ExplorerEntry) => void; testID?: string; } @@ -104,8 +142,97 @@ function iconButtonStyle({ hovered, pressed }: PressableStateCallbackType & { ho return [styles.iconButton, (Boolean(hovered) || pressed) && styles.iconButtonHovered]; } -function treeRowKeyExtractor(row: ExplorerTreeRow) { - return row.entry.path; +type ExplorerListRow = + | { type: "entry"; row: ExplorerTreeRow } + | { type: "draft"; parentPath: string; kind: "file" | "directory"; depth: number } + | { type: "rename"; entry: ExplorerEntry; depth: number }; + +type ExplorerPendingEdit = + | { type: "create"; parentPath: string; kind: "file" | "directory" } + | { type: "rename"; entry: ExplorerEntry }; + +function listRowKeyExtractor(row: ExplorerListRow) { + if (row.type === "entry") { + return row.row.entry.path; + } + return row.type === "rename" ? `rename:${row.entry.path}` : `draft:${row.parentPath}:${row.kind}`; +} + +interface EntryNameInputRowProps { + depth: number; + kind: "file" | "directory"; + initialName?: string; + onCommit: (name: string) => void; + onCancel: () => void; +} + +function EntryNameInputRow({ + depth, + kind, + initialName = "", + onCommit, + onCancel, +}: EntryNameInputRowProps) { + const { t } = useTranslation(); + const [name, setName] = useState(initialName); + const settledRef = useRef(false); + + const commit = useCallback(() => { + if (settledRef.current) { + return; + } + settledRef.current = true; + const trimmed = name.trim(); + if (!trimmed) { + onCancel(); + return; + } + onCommit(trimmed); + }, [name, onCancel, onCommit]); + + const handleKeyPress = useCallback( + (event: NativeSyntheticEvent) => { + if (event.nativeEvent.key === "Escape") { + settledRef.current = true; + onCancel(); + } + }, + [onCancel], + ); + + return ( + + + + + {kind === "directory" ? ( + + ) : ( + + )} + + + + + ); } function TreeRowItem({ @@ -117,9 +244,18 @@ function TreeRowItem({ isSelected, loading, onEntryPress, + onSelectEntry, onCopyPath, + onCopyRelativePath, + onRevealEntry, + revealTargetName, onDownloadEntry, onAddToChat, + onNewEntry, + onCollapseDirectory, + onRenameEntry, + onDuplicateEntry, + onDeleteEntry, testID, }: TreeRowItemProps) { const { t } = useTranslation(); @@ -132,9 +268,18 @@ function TreeRowItem({ }); const handlePress = useCallback(() => { + const selection = isWeb ? window.getSelection() : null; + if (selection && !selection.isCollapsed && selection.toString().length > 0) { + return; + } onEntryPress(entry); }, [onEntryPress, entry]); + const handleSelect = useCallback(() => { + onSelectEntry(entry); + }, [entry, onSelectEntry]); + const accessibilityState = useMemo(() => ({ selected: isSelected }), [isSelected]); + const pressableStyle = useCallback( ({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [ styles.entryRow, @@ -148,6 +293,14 @@ function TreeRowItem({ onCopyPath(entry.path); }, [onCopyPath, entry.path]); + const handleCopyRelativePath = useCallback(() => { + onCopyRelativePath(entry.path); + }, [onCopyRelativePath, entry.path]); + + const handleReveal = useCallback(() => { + onRevealEntry?.(entry); + }, [onRevealEntry, entry]); + const handleDownload = useCallback(() => { onDownloadEntry(entry); }, [onDownloadEntry, entry]); @@ -156,6 +309,30 @@ function TreeRowItem({ onAddToChat?.(entry.path); }, [onAddToChat, entry.path]); + const handleNewFile = useCallback(() => { + onNewEntry?.(entry.path, "file"); + }, [onNewEntry, entry.path]); + + const handleNewFolder = useCallback(() => { + onNewEntry?.(entry.path, "directory"); + }, [onNewEntry, entry.path]); + + const handleCollapseDirectory = useCallback(() => { + onCollapseDirectory?.(entry.path); + }, [entry.path, onCollapseDirectory]); + + const handleRename = useCallback(() => { + onRenameEntry?.(entry); + }, [onRenameEntry, entry]); + + const handleDuplicate = useCallback(() => { + onDuplicateEntry?.(entry); + }, [entry, onDuplicateEntry]); + + const handleDelete = useCallback(() => { + onDeleteEntry?.(entry); + }, [onDeleteEntry, entry]); + const metaHeader = useMemo( () => ( @@ -182,24 +359,23 @@ function TreeRowItem({ return ( - + - {(() => { - if (!isDirectory) { - return ; - } - if (loading) { - return ( - - ); - } - return ; - })()} + {isDirectory ? ( + + ) : ( + + )} {entry.name} @@ -209,8 +385,17 @@ function TreeRowItem({ @@ -252,11 +437,33 @@ export function FileExplorerPane({ : undefined, ); - const { requestDirectoryListing, selectExplorerEntry } = useFileExplorerActions({ + const { + requestDirectoryListing, + createEntry, + renameEntry, + duplicateEntry, + deleteEntry, + selectExplorerEntry, + } = useFileExplorerActions({ serverId, workspaceId, workspaceRoot: normalizedWorkspaceRoot, }); + const toast = useToast(); + const isLocalDaemon = useIsLocalDaemon(serverId); + const { targets: desktopOpenTargets } = useDesktopOpenTargets({ + isLocalExecution: isLocalDaemon, + }); + const fileManagerTarget = desktopOpenTargets.find((target) => target.kind === "file-manager"); + // COMPAT(fsEntryOps): added in v0.3.0, remove gate after 2027-02-08. + const fsEntryOpsEnabled = useSessionStore( + (state) => state.sessions[serverId]?.serverInfo?.features?.fsEntryOps === true, + ); + // COMPAT(fsEntryDuplicate): added in v0.3.0, remove gate after 2027-02-09. + const fsEntryDuplicateEnabled = useSessionStore( + (state) => state.sessions[serverId]?.serverInfo?.features?.fsEntryDuplicate === true, + ); + const [pendingEdit, setPendingEdit] = useState(null); const downloadFile = useFileDownload({ serverId, workspaceId, @@ -286,7 +493,7 @@ export function FileExplorerPane({ [isExplorerLoading, pendingRequest], ); - const treeListRef = useRef>(null); + const treeListRef = useRef>(null); const scrollbar = useOverlayFlatListScrollbar(treeListRef, { enabled: !isCompact }); const hasInitializedRef = useRef(false); @@ -333,26 +540,49 @@ export function FileExplorerPane({ ], ); + // Selection is intentionally separate from opening/expansion so future keyboard actions + // (for example, pressing R to rename) have one stable file-or-folder target. + const handleSelectEntry = useCallback( + (entry: ExplorerEntry) => { + if (hasWorkspaceScope) { + selectExplorerEntry(entry.path); + } + }, + [hasWorkspaceScope, selectExplorerEntry], + ); + const handleOpenFile = useCallback( (entry: ExplorerEntry) => { if (!hasWorkspaceScope) { return; } - selectExplorerEntry(entry.path); onOpenFile?.(entry.path); }, - [hasWorkspaceScope, onOpenFile, selectExplorerEntry], + [hasWorkspaceScope, onOpenFile], ); const handleEntryPress = useCallback( (entry: ExplorerEntry) => { + handleSelectEntry(entry); if (entry.kind === "directory") { handleToggleDirectory(entry); return; } handleOpenFile(entry); }, - [handleOpenFile, handleToggleDirectory], + [handleOpenFile, handleSelectEntry, handleToggleDirectory], + ); + + const handleCollapseDirectory = useCallback( + (path: string) => { + if (!workspaceStateKey) { + return; + } + setExpandedPathsForWorkspace(workspaceStateKey, (currentPaths) => + currentPaths.filter((expandedPath) => !isExplorerPathWithin(expandedPath, path)), + ); + }, + [setExpandedPathsForWorkspace, workspaceStateKey], ); const handleCopyPath = useCallback( @@ -367,6 +597,33 @@ export function FileExplorerPane({ [normalizedWorkspaceRoot], ); + const handleCopyRelativePath = useCallback(async (path: string) => { + await Clipboard.setStringAsync(path); + }, []); + + const handleRevealEntry = useCallback( + async (entry: ExplorerEntry) => { + if (!fileManagerTarget) { + return; + } + try { + await openDesktopTarget({ + editorId: fileManagerTarget.id, + workspacePath: normalizedWorkspaceRoot, + filePath: buildAbsoluteExplorerPath({ + workspaceRoot: normalizedWorkspaceRoot, + entryPath: entry.path, + }), + }); + } catch (cause) { + toast.error( + cause instanceof Error ? cause.message : t("workspace.fileExplorer.errors.revealFailed"), + ); + } + }, + [fileManagerTarget, normalizedWorkspaceRoot, t, toast], + ); + const handleDownloadEntry = useCallback( (entry: ExplorerEntry) => { if (entry.kind !== "file") { @@ -377,6 +634,197 @@ export function FileExplorerPane({ [downloadFile], ); + const handleNewEntry = useCallback( + (parentPath: string, kind: "file" | "directory") => { + if (!workspaceStateKey) { + return; + } + if (parentPath !== ".") { + setExpandedPathsForWorkspace(workspaceStateKey, (currentPaths) => + setExpandedDirectoryPath({ + currentExpandedPaths: currentPaths, + directoryPath: parentPath, + expanded: true, + }), + ); + if (!directories.has(parentPath)) { + void requestDirectoryListing(parentPath, { + recordHistory: false, + setCurrentPath: false, + }); + } + } + setPendingEdit({ type: "create", parentPath, kind }); + }, + [directories, requestDirectoryListing, setExpandedPathsForWorkspace, workspaceStateKey], + ); + + const handleEditCancel = useCallback(() => { + setPendingEdit(null); + }, []); + + const handleRenameEntry = useCallback((entry: ExplorerEntry) => { + setPendingEdit({ type: "rename", entry }); + }, []); + + const handleDraftCommit = useCallback( + async (name: string) => { + const edit = pendingEdit; + setPendingEdit(null); + if (edit?.type !== "create") { + return; + } + try { + const payload = await createEntry({ + parentPath: edit.parentPath, + name, + kind: edit.kind, + }); + if (!payload) { + return; + } + if (!payload.success) { + toast.error(payload.error ?? t("workspace.fileExplorer.errors.createFailed")); + return; + } + if (edit.kind === "file" && payload.path) { + selectExplorerEntry(payload.path); + onOpenFile?.(payload.path); + } + } catch (cause) { + toast.error(cause instanceof Error ? cause.message : String(cause)); + } + }, + [createEntry, onOpenFile, pendingEdit, selectExplorerEntry, t, toast], + ); + + const handleRenameCommit = useCallback( + async (name: string) => { + const edit = pendingEdit; + setPendingEdit(null); + if (edit?.type !== "rename" || name === edit.entry.name) { + return; + } + const { entry } = edit; + try { + const payload = await renameEntry({ + path: entry.path, + name, + }); + if (!payload) { + return; + } + if (!payload.success || !payload.renamedPath) { + toast.error(payload.error ?? t("workspace.fileExplorer.errors.renameFailed")); + return; + } + + const renamedPath = payload.renamedPath; + if (workspaceStateKey && entry.kind === "directory") { + const expandedRenamedPaths = Array.from(expandedPaths) + .filter((expandedPath) => isExplorerPathWithin(expandedPath, entry.path)) + .map((expandedPath) => + replaceExplorerPathPrefix(expandedPath, entry.path, renamedPath), + ); + setExpandedPathsForWorkspace(workspaceStateKey, (currentPaths) => + currentPaths.map((currentPath) => + isExplorerPathWithin(currentPath, entry.path) + ? replaceExplorerPathPrefix(currentPath, entry.path, renamedPath) + : currentPath, + ), + ); + await Promise.all( + expandedRenamedPaths.map((expandedPath) => + requestDirectoryListing(expandedPath, { + recordHistory: false, + setCurrentPath: false, + }), + ), + ); + } + if (selectedEntryPath && isExplorerPathWithin(selectedEntryPath, entry.path)) { + const renamedSelection = replaceExplorerPathPrefix( + selectedEntryPath, + entry.path, + renamedPath, + ); + selectExplorerEntry(renamedSelection); + if (entry.kind === "file") { + onOpenFile?.(renamedSelection); + } + } + } catch (cause) { + toast.error(cause instanceof Error ? cause.message : String(cause)); + } + }, + [ + expandedPaths, + onOpenFile, + pendingEdit, + requestDirectoryListing, + renameEntry, + selectExplorerEntry, + selectedEntryPath, + setExpandedPathsForWorkspace, + t, + toast, + workspaceStateKey, + ], + ); + + const handleDuplicateEntry = useCallback( + async (entry: ExplorerEntry) => { + try { + const payload = await duplicateEntry(entry.path); + if (!payload) { + return; + } + if (!payload.success || !payload.duplicatedPath) { + toast.error(payload.error ?? t("workspace.fileExplorer.errors.duplicateFailed")); + return; + } + selectExplorerEntry(payload.duplicatedPath); + } catch (cause) { + toast.error(cause instanceof Error ? cause.message : String(cause)); + } + }, + [duplicateEntry, selectExplorerEntry, t, toast], + ); + + const handleDeleteEntry = useCallback( + async (entry: ExplorerEntry) => { + const confirmed = await confirmDialog({ + title: + entry.kind === "directory" + ? t("workspace.fileActions.confirmDelete.folderTitle") + : t("workspace.fileActions.confirmDelete.fileTitle"), + message: t("workspace.fileActions.confirmDelete.message", { name: entry.name }), + confirmLabel: t("workspace.fileActions.confirmDelete.confirm"), + cancelLabel: t("workspace.fileActions.confirmDelete.cancel"), + destructive: true, + }); + if (!confirmed) { + return; + } + try { + const payload = await deleteEntry(entry.path); + if (!payload) { + return; + } + if (!payload.success) { + toast.error(payload.error ?? t("workspace.fileExplorer.errors.deleteFailed")); + return; + } + if (selectedEntryPath === entry.path) { + selectExplorerEntry(null); + } + } catch (cause) { + toast.error(cause instanceof Error ? cause.message : String(cause)); + } + }, + [deleteEntry, selectExplorerEntry, selectedEntryPath, t, toast], + ); + const handleSortCycle = useCallback(() => { const currentIndex = SORT_OPTIONS.findIndex((opt) => opt.value === sortOption); const nextIndex = (currentIndex + 1) % SORT_OPTIONS.length; @@ -456,6 +904,33 @@ export function FileExplorerPane({ [directories, expandedPaths, showHiddenFiles, sortOption], ); + const listRows = useMemo(() => { + const rows: ExplorerListRow[] = treeRows.map((row) => + pendingEdit?.type === "rename" && pendingEdit.entry.path === row.entry.path + ? { type: "rename", entry: row.entry, depth: row.depth } + : { type: "entry", row }, + ); + if (pendingEdit?.type !== "create") { + return rows; + } + let insertionIndex = 0; + let depth = 0; + if (pendingEdit.parentPath !== ".") { + const parentIndex = treeRows.findIndex((row) => row.entry.path === pendingEdit.parentPath); + if (parentIndex >= 0) { + insertionIndex = parentIndex + 1; + depth = treeRows[parentIndex].depth + 1; + } + } + rows.splice(insertionIndex, 0, { + type: "draft", + parentPath: pendingEdit.parentPath, + kind: pendingEdit.kind, + depth, + }); + return rows; + }, [pendingEdit, treeRows]); + const showInitialLoading = resolveShowInitialLoading({ directories, isExplorerLoading, @@ -465,26 +940,73 @@ export function FileExplorerPane({ const errorRecoveryPath = useMemo(() => getErrorRecoveryPath(explorerState), [explorerState]); const renderTreeRow = useCallback( - (info: ListRenderItemInfo) => ( - - ), + (info: ListRenderItemInfo) => { + if (info.item.type === "draft") { + return ( + + ); + } + if (info.item.type === "rename") { + return ( + + ); + } + return ( + + ); + }, [ expandedPaths, - handleEntryPress, + fsEntryDuplicateEnabled, + fsEntryOpsEnabled, + handleCollapseDirectory, handleCopyPath, + handleCopyRelativePath, + handleDeleteEntry, handleDownloadEntry, + handleDraftCommit, + handleDuplicateEntry, + handleEditCancel, + handleEntryPress, + handleNewEntry, + handleRenameCommit, + handleRenameEntry, + handleRevealEntry, + handleSelectEntry, isDirectoryLoading, + fileManagerTarget, selectedEntryPath, onAddToChat, serverId, @@ -519,12 +1041,18 @@ export function FileExplorerPane({ } return ( - + void }) => event.preventDefault?.(), + }} + style={styles.container} + > { + if (!enabled || isFileExplorerRowTarget(event.target)) { + return; + } + event.preventDefault(); + event.stopPropagation(); + contextMenu.setAnchorRect({ + x: event.nativeEvent.pageX, + y: event.nativeEvent.pageY, + width: 0, + height: 0, + }); + contextMenu.setOpen(true); + }, + [contextMenu, enabled], + ); + + return ( + + {children} + + ); +} + interface FileExplorerPaneContentProps { error: string | null; showInitialLoading: boolean; showBackFromError: boolean; - treeRows: ExplorerTreeRow[]; + listRows: ExplorerListRow[]; + onNewEntryAtRoot?: (parentPath: string, kind: "file" | "directory") => void; currentSortLabel: string; isRefreshFetching: boolean; - treeListRef: RefObject | null>; + treeListRef: RefObject | null>; scrollbar: OverlayFlatListScrollbar; - renderTreeRow: (info: ListRenderItemInfo) => ReactElement; + renderTreeRow: (info: ListRenderItemInfo) => ReactElement; handleSortCycle: () => void; handleToggleHiddenFiles: () => void; handleRefresh: () => void; @@ -568,7 +1148,8 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) { error, showInitialLoading, showBackFromError, - treeRows, + listRows, + onNewEntryAtRoot, currentSortLabel, isRefreshFetching, treeListRef, @@ -585,6 +1166,13 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) { const showHiddenFiles = usePanelStore((state) => state.explorerShowHiddenFiles); + const handleNewFileAtRoot = useCallback(() => { + onNewEntryAtRoot?.(".", "file"); + }, [onNewEntryAtRoot]); + const handleNewFolderAtRoot = useCallback(() => { + onNewEntryAtRoot?.(".", "directory"); + }, [onNewEntryAtRoot]); + const hiddenFilesToggleAccessibilityLabel = showHiddenFiles ? t("workspace.fileExplorer.actions.hideHiddenFiles") : t("workspace.fileExplorer.actions.showHiddenFiles"); @@ -644,6 +1232,30 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
+ {onNewEntryAtRoot ? ( + <> + + + + + + + + ) : null}
- {treeRows.length === 0 ? ( - - {emptyLabel} - - ) : ( - - )} - {treeRows.length > 0 ? scrollbar.overlay : null} + + + {listRows.length === 0 ? ( + + {emptyLabel} + + ) : ( + + )} + {listRows.length > 0 ? scrollbar.overlay : null} + + {onNewEntryAtRoot ? ( + + ) : null} +
); } @@ -799,28 +1423,48 @@ function toggleDirectory({ function TreeRowDispatcher({ serverId, workspaceId, - info, + row, + index, expandedPaths, selectedEntryPath, isDirectoryLoading, onEntryPress, + onSelectEntry, onCopyPath, + onCopyRelativePath, + onRevealEntry, + revealTargetName, onDownloadEntry, onAddToChat, + onNewEntry, + onCollapseDirectory, + onRenameEntry, + onDuplicateEntry, + onDeleteEntry, }: { serverId: string; workspaceId?: string | null; - info: ListRenderItemInfo; + row: ExplorerTreeRow; + index: number; expandedPaths: Set; selectedEntryPath: string | null; isDirectoryLoading: (path: string) => boolean; onEntryPress: (entry: ExplorerEntry) => void; + onSelectEntry: (entry: ExplorerEntry) => void; onCopyPath: (path: string) => void | Promise; + onCopyRelativePath: (path: string) => void | Promise; + onRevealEntry?: (entry: ExplorerEntry) => void; + revealTargetName?: string; onDownloadEntry: (entry: ExplorerEntry) => void; onAddToChat?: (path: string) => void; + onNewEntry?: (parentPath: string, kind: "file" | "directory") => void; + onCollapseDirectory?: (path: string) => void; + onRenameEntry?: (entry: ExplorerEntry) => void; + onDuplicateEntry?: (entry: ExplorerEntry) => void; + onDeleteEntry?: (entry: ExplorerEntry) => void; }) { - const entry = info.item.entry; - const depth = info.item.depth; + const entry = row.entry; + const depth = row.depth; const isDirectory = entry.kind === "directory"; const isExpanded = isDirectory && expandedPaths.has(entry.path); const isSelected = selectedEntryPath === entry.path; @@ -836,14 +1480,35 @@ function TreeRowDispatcher({ isSelected={isSelected} loading={loading} onEntryPress={onEntryPress} + onSelectEntry={onSelectEntry} onCopyPath={onCopyPath} + onCopyRelativePath={onCopyRelativePath} + onRevealEntry={onRevealEntry} + revealTargetName={revealTargetName} onDownloadEntry={onDownloadEntry} onAddToChat={onAddToChat} - testID={`file-explorer-row-${info.index}`} + onNewEntry={onNewEntry} + onCollapseDirectory={onCollapseDirectory} + onRenameEntry={onRenameEntry} + onDuplicateEntry={onDuplicateEntry} + onDeleteEntry={onDeleteEntry} + testID={`file-explorer-row-${index}`} /> ); } +function isExplorerPathWithin(candidatePath: string, parentPath: string): boolean { + return candidatePath === parentPath || candidatePath.startsWith(`${parentPath}/`); +} + +function replaceExplorerPathPrefix( + candidatePath: string, + previousPrefix: string, + nextPrefix: string, +): string { + return `${nextPrefix}${candidatePath.slice(previousPrefix.length)}`; +} + async function initializeExplorer({ hasWorkspaceScope, hasInitializedRef, @@ -1023,9 +1688,14 @@ const styles = StyleSheet.create((theme) => ({ minHeight: 0, }, entriesContent: { + flexGrow: 1, paddingTop: theme.spacing[2], paddingBottom: theme.spacing[4], }, + rootContextTarget: { + flex: 1, + minHeight: 0, + }, centerState: { flex: 1, alignItems: "center", @@ -1096,6 +1766,17 @@ const styles = StyleSheet.create((theme) => ({ flex: 1, color: theme.colors.foreground, fontSize: theme.fontSize.sm, + userSelect: "none", + }, + draftInput: { + flex: 1, + color: theme.colors.foreground, + fontSize: theme.fontSize.sm, + paddingVertical: 0, + paddingHorizontal: 0, + }, + draftPlaceholder: { + color: theme.colors.foregroundExtraMuted, }, contextMetaBlock: { paddingVertical: theme.spacing[1], diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index bfb75a08ee..9c0b2dd182 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -2,6 +2,7 @@ import { router, usePathname } from "expo-router"; import { CalendarClock, FolderPlus, + GitBranch, History, Home, Plus, @@ -77,6 +78,8 @@ import { SidebarWorkspaceList } from "./sidebar-workspace-list"; type SidebarTheme = ReturnType["theme"]; +const DEV_BUILD_LABEL = process.env.EXPO_PUBLIC_PASEO_DEV_BUILD_LABEL?.trim() || null; + interface SidebarSharedProps { theme: SidebarTheme; statusGroups: StatusGroup[]; @@ -853,9 +856,22 @@ function DesktopSidebar({ > - {ownsTopLeft ? ( + {ownsTopLeft || DEV_BUILD_LABEL ? ( + {DEV_BUILD_LABEL ? ( + + + + {DEV_BUILD_LABEL} + + + ) : null} ) : ( @@ -1080,9 +1096,28 @@ const styles = StyleSheet.create((theme) => ({ height: HEADER_INNER_HEIGHT, flexDirection: "row", alignItems: "center", + justifyContent: "flex-end", + paddingHorizontal: theme.spacing[3], borderBottomWidth: theme.borderWidth[1], borderBottomColor: "transparent", }, + devBuildBadge: { + maxWidth: "60%", + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[1], + paddingHorizontal: theme.spacing[2], + paddingVertical: 2, + borderRadius: theme.borderRadius.full, + backgroundColor: theme.colors.accent, + }, + devBuildBadgeText: { + minWidth: 0, + flexShrink: 1, + color: theme.colors.accentForeground, + fontSize: theme.fontSize.xs, + fontWeight: theme.fontWeight.medium, + }, sidebarFooter: { flexDirection: "row", alignItems: "center", diff --git a/packages/app/src/components/markdown/fence/index.tsx b/packages/app/src/components/markdown/fence/index.tsx new file mode 100644 index 0000000000..846aa91355 --- /dev/null +++ b/packages/app/src/components/markdown/fence/index.tsx @@ -0,0 +1,42 @@ +import type { ComponentType } from "react"; +import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; +import { getMarkdownFenceLanguage } from "./language"; +import { MermaidFence } from "./mermaid"; +import type { MarkdownFenceRendererProps } from "./types"; + +export interface MarkdownFenceBlockProps extends MarkdownFenceRendererProps { + info: string | null | undefined; +} + +const diagramFences: Partial>> = { + mermaid: MermaidFence, +}; + +export function MarkdownFenceBlock({ + code, + info, + phase, + inheritedStyles, + textStyle, +}: MarkdownFenceBlockProps) { + const language = getMarkdownFenceLanguage(info); + const DiagramFence = language ? diagramFences[language] : undefined; + if (DiagramFence) { + return ( + + ); + } + return ( + + ); +} diff --git a/packages/app/src/components/markdown/fence/language.test.ts b/packages/app/src/components/markdown/fence/language.test.ts new file mode 100644 index 0000000000..172f72c310 --- /dev/null +++ b/packages/app/src/components/markdown/fence/language.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { getMarkdownFenceLanguage } from "./language"; + +describe("getMarkdownFenceLanguage", () => { + it("normalizes the first info-string token", () => { + expect(getMarkdownFenceLanguage(" Sirena theme=dark ")).toBe("sirena"); + expect(getMarkdownFenceLanguage("TypeScript title=example")).toBe("typescript"); + }); + + it("returns null when no language is declared", () => { + expect(getMarkdownFenceLanguage("")).toBeNull(); + expect(getMarkdownFenceLanguage(null)).toBeNull(); + expect(getMarkdownFenceLanguage(undefined)).toBeNull(); + }); +}); diff --git a/packages/app/src/components/markdown/fence/language.ts b/packages/app/src/components/markdown/fence/language.ts new file mode 100644 index 0000000000..fd414a5ac2 --- /dev/null +++ b/packages/app/src/components/markdown/fence/language.ts @@ -0,0 +1,3 @@ +export function getMarkdownFenceLanguage(info: string | null | undefined): string | null { + return info?.trim().split(/\s+/)[0]?.toLowerCase() || null; +} diff --git a/packages/app/src/components/markdown/fence/mermaid/build-runtime.mjs b/packages/app/src/components/markdown/fence/mermaid/build-runtime.mjs new file mode 100644 index 0000000000..e4f61b8899 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/build-runtime.mjs @@ -0,0 +1,76 @@ +import esbuild from "esbuild"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const appRoot = path.resolve(__dirname, "../../../../.."); +const repoRoot = path.resolve(appRoot, "../.."); +const entry = path.join(__dirname, "runtime/entry.ts"); +const output = path.join(__dirname, "runtime/html.gen.ts"); + +const result = await esbuild.build({ + entryPoints: [entry], + bundle: true, + write: false, + format: "iife", + platform: "browser", + target: ["ios15", "chrome100"], + minify: true, + // The app's deployment floor is iOS 15.1 but structuredClone (used by + // mermaid) only exists from iOS 15.4; esbuild targets transform syntax, + // not runtime APIs. Mermaid only clones plain config objects, so a + // JSON-based fallback is sufficient. + banner: { + js: 'if(typeof globalThis.structuredClone!=="function"){globalThis.structuredClone=function(v){return v===undefined?v:JSON.parse(JSON.stringify(v));};}', + }, + logLevel: "info", +}); + +const rawJs = result.outputFiles[0]?.text; +if (!rawJs) { + throw new Error("Mermaid runtime bundle produced no JavaScript"); +} +// JSON.stringify below escapes the TypeScript representation, not the runtime +// HTML — an embedded closing-script sequence would still terminate the inline +// + +`; + +const contents = `// Generated by packages/app/src/components/markdown/fence/mermaid/build-runtime.mjs. +// Do not edit by hand. + +export const mermaidRuntimeHtml = ${JSON.stringify(html)}; +`; + +await fs.writeFile(output, contents); +console.log(`Wrote ${path.relative(repoRoot, output)} (${html.length} bytes)`); diff --git a/packages/app/src/components/markdown/fence/mermaid/host.native.tsx b/packages/app/src/components/markdown/fence/mermaid/host.native.tsx new file mode 100644 index 0000000000..044341f3aa --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/host.native.tsx @@ -0,0 +1,379 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + Modal, + Pressable, + StyleSheet as RNStyleSheet, + ScrollView, + View, + type TextStyle, + type ViewStyle, +} from "react-native"; +import { WebView, type WebViewMessageEvent } from "react-native-webview"; +import { Code, Workflow, X } from "lucide-react-native"; +import { useTranslation } from "react-i18next"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; +import type { Theme } from "@/styles/theme"; +import type { MarkdownFenceRendererProps } from "../types"; +import type { MermaidRenderRequest } from "./render-model"; +import { mermaidRuntimeHtml } from "./runtime/html.gen"; +import { + parseMermaidRuntimeMessage, + serializeMermaidRuntimeRenderMessage, + type MermaidRuntimeRenderMessage, +} from "./runtime/messages"; +import { MermaidRuntimeRequestDriver } from "./runtime/request-driver"; +import { useMermaidRenderModel } from "./use-render-model"; +import { getDiagramBoxStyle } from "./presentation"; + +interface MermaidFenceHostImplProps extends MarkdownFenceRendererProps { + colorScheme?: "light" | "dark"; +} + +const WEBVIEW_SOURCE = { html: mermaidRuntimeHtml }; +const WEBVIEW_ORIGIN_WHITELIST = ["*"]; +const MAX_PREVIEW_HEIGHT = 480; + +interface MermaidWebViewProps { + request: MermaidRenderRequest | null; + interactive: boolean; + onRendered: (message: { + revision: number; + source: string; + colorScheme: "light" | "dark"; + height: number; + width: number; + }) => void; + onRenderFailed: (revision: number) => void; + style?: ViewStyle; +} + +function MermaidWebView({ + request, + interactive, + onRendered, + onRenderFailed, + style, +}: MermaidWebViewProps) { + const webViewRef = useRef(null); + const driverRef = useRef(null); + driverRef.current ??= new MermaidRuntimeRequestDriver(); + + const sendRequest = useCallback( + (current: MermaidRenderRequest | null) => { + if (!current || !webViewRef.current) { + return; + } + const message: MermaidRuntimeRenderMessage = { + type: "render", + revision: current.revision, + source: current.source, + colorScheme: current.colorScheme, + interactive, + }; + const payload = serializeMermaidRuntimeRenderMessage(message); + webViewRef.current.injectJavaScript( + `window.__PASEO_MERMAID_RUNTIME_RECEIVE__ && window.__PASEO_MERMAID_RUNTIME_RECEIVE__(${payload}); true;`, + ); + }, + [interactive], + ); + + useEffect(() => { + sendRequest(driverRef.current?.update(request) ?? null); + }, [request, sendRequest]); + + const handleMessage = useCallback( + (event: WebViewMessageEvent) => { + let value: unknown; + try { + value = JSON.parse(event.nativeEvent.data); + } catch { + return; + } + const message = parseMermaidRuntimeMessage(value); + if (!message) { + return; + } + if (message.type === "bridgeReady") { + sendRequest(driverRef.current?.ready() ?? null); + return; + } + if (message.type === "renderError") { + onRenderFailed(message.revision); + sendRequest(driverRef.current?.settled(message.revision, false) ?? null); + return; + } + onRendered(message); + sendRequest(driverRef.current?.settled(message.revision, true) ?? null); + }, + [onRenderFailed, onRendered, sendRequest], + ); + const handleShouldStartLoad = useCallback( + (load: { url: string }) => load.url === "about:blank" || load.url.startsWith("data:"), + [], + ); + + return ( + + ); +} + +const webViewStyles = RNStyleSheet.create({ + webView: { backgroundColor: "transparent" }, +}); + +interface MermaidDiagramViewerProps { + code: string; + colorScheme: "light" | "dark"; + onClose: () => void; + inheritedStyles: TextStyle; + textStyle: TextStyle; +} + +function MermaidDiagramViewer({ + code, + colorScheme, + onClose, + inheritedStyles, + textStyle, +}: MermaidDiagramViewerProps) { + const { t } = useTranslation(); + const [showSource, setShowSource] = useState(false); + const toggleSource = useCallback(() => setShowSource((current) => !current), []); + const { request, rendered, renderFailed } = useMermaidRenderModel({ + source: code, + phase: "complete", + colorScheme, + }); + const handleRendered = useCallback( + (message: { + revision: number; + source: string; + colorScheme: "light" | "dark"; + height: number; + width: number; + }) => { + rendered({ + revision: message.revision, + source: message.source, + colorScheme: message.colorScheme, + dimensions: { height: message.height, width: message.width }, + }); + }, + [rendered], + ); + + return ( + + + + + {showSource ? ( + + + + ) : null} + + + + {showSource ? ( + + ) : ( + + )} + + + + + + + + ); +} + +const ThemedCloseIcon = withUnistyles(X); +const ThemedSourceIcon = withUnistyles(Code); +const ThemedDiagramIcon = withUnistyles(Workflow); +const closeIconColor = (theme: Theme) => ({ color: theme.colors.foreground }); + +const viewerStyles = StyleSheet.create((theme, runtime) => ({ + backdrop: { flex: 1, backgroundColor: theme.colors.surface0 }, + webView: { + flex: 1, + marginTop: runtime.insets.top, + marginBottom: runtime.insets.bottom, + }, + runtime: { flex: 1 }, + sourceOverlay: { + position: "absolute", + inset: 0, + backgroundColor: theme.colors.surface0, + }, + source: { + paddingTop: theme.spacing[12], + paddingHorizontal: theme.spacing[4], + paddingBottom: theme.spacing[4], + }, + actions: { + position: "absolute", + top: runtime.insets.top + theme.spacing[3], + right: theme.spacing[4], + flexDirection: "row", + gap: theme.spacing[2], + }, + actionButton: { + padding: theme.spacing[2], + borderRadius: theme.borderRadius.lg, + backgroundColor: theme.colors.surface2, + }, +})); + +function MermaidFenceHostImpl({ + code, + phase, + inheritedStyles, + textStyle, + colorScheme = "dark", +}: MermaidFenceHostImplProps) { + const { t } = useTranslation(); + const { state, request, rendered, renderFailed } = useMermaidRenderModel({ + source: code, + phase, + colorScheme, + }); + const [hasRuntimeContent, setHasRuntimeContent] = useState(false); + const handleRendered = useCallback( + (message: { + revision: number; + source: string; + colorScheme: "light" | "dark"; + height: number; + width: number; + }) => { + setHasRuntimeContent(true); + rendered({ + revision: message.revision, + source: message.source, + colorScheme: message.colorScheme, + dimensions: { height: message.height, width: message.width }, + }); + }, + [rendered], + ); + const [viewerOpen, setViewerOpen] = useState(false); + const openViewer = useCallback(() => setViewerOpen(true), []); + const closeViewer = useCallback(() => setViewerOpen(false), []); + const visible = state.visible; + const canShowDiagram = visible !== null && hasRuntimeContent; + const previewInnerStyle = useMemo( + () => + canShowDiagram && visible + ? { height: Math.min(visible.height, MAX_PREVIEW_HEIGHT) } + : previewStyles.measuringInner, + [canShowDiagram, visible], + ); + const diagramBoxStyle = getDiagramBoxStyle(textStyle); + + return ( + <> + {!canShowDiagram ? ( + + ) : null} + + + + + + {viewerOpen && canShowDiagram && visible ? ( + + ) : null} + + ); +} + +const previewStyles = RNStyleSheet.create({ + measuring: { + position: "absolute", + left: 0, + right: 0, + opacity: 0, + pointerEvents: "none", + }, + measuringInner: { height: 240 }, + preview: { overflow: "hidden" }, +}); + +const mapColorScheme = (theme: Theme) => ({ colorScheme: theme.colorScheme }); +const ThemedMermaidFenceHost = withUnistyles(MermaidFenceHostImpl); + +export function MermaidFenceHost(props: MarkdownFenceRendererProps) { + return ; +} diff --git a/packages/app/src/components/markdown/fence/mermaid/host.tsx b/packages/app/src/components/markdown/fence/mermaid/host.tsx new file mode 100644 index 0000000000..50e9596740 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/host.tsx @@ -0,0 +1,13 @@ +import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; +import type { MarkdownFenceRendererProps } from "../types"; + +export function MermaidFenceHost({ code, inheritedStyles, textStyle }: MarkdownFenceRendererProps) { + return ( + + ); +} diff --git a/packages/app/src/components/markdown/fence/mermaid/host.web.tsx b/packages/app/src/components/markdown/fence/mermaid/host.web.tsx new file mode 100644 index 0000000000..c51ab75428 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/host.web.tsx @@ -0,0 +1,490 @@ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type ComponentType, +} from "react"; +import { Pressable, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native"; +import { useTranslation } from "react-i18next"; +import { Code, Scan, Workflow, ZoomIn, ZoomOut } from "lucide-react-native"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; +import { useIsCompactFormFactor } from "@/constants/layout"; +import type { Theme } from "@/styles/theme"; +import type { MarkdownFenceRendererProps } from "../types"; +import type { MermaidRenderRequest } from "./render-model"; +import { mermaidRuntimeHtml } from "./runtime/html.gen"; +import { parseMermaidRuntimeMessage, type MermaidRuntimeRenderMessage } from "./runtime/messages"; +import { MermaidRuntimeRequestDriver } from "./runtime/request-driver"; +import { useMermaidRenderModel } from "./use-render-model"; +import { getDiagramBoxStyle } from "./presentation"; + +interface MermaidFenceHostImplProps extends MarkdownFenceRendererProps { + colorScheme?: "light" | "dark"; +} + +interface MermaidIframeRuntimeProps { + request: MermaidRenderRequest | null; + height: number; + onRendered: (message: { + revision: number; + source: string; + colorScheme: "light" | "dark"; + height: number; + width: number; + }) => void; + onRenderFailed: (revision: number) => void; +} + +function MermaidIframeRuntime({ + request, + height, + onRendered, + onRenderFailed, +}: MermaidIframeRuntimeProps) { + const iframeRef = useRef(null); + const driverRef = useRef(null); + driverRef.current ??= new MermaidRuntimeRequestDriver(); + const iframeStyle = useMemo( + () => ({ + display: "block", + width: "100%", + height, + border: 0, + pointerEvents: "none", + background: "transparent", + }), + [height], + ); + + const sendRequest = useCallback((current: MermaidRenderRequest | null) => { + const target = iframeRef.current?.contentWindow; + if (!current || !target) { + return; + } + const message: MermaidRuntimeRenderMessage = { + type: "render", + revision: current.revision, + source: current.source, + colorScheme: current.colorScheme, + interactive: false, + }; + target.postMessage(message, "*"); + }, []); + + useEffect(() => { + sendRequest(driverRef.current?.update(request) ?? null); + }, [request, sendRequest]); + + useEffect(() => { + function receiveMessage(event: MessageEvent): void { + if (event.source !== iframeRef.current?.contentWindow) { + return; + } + const message = parseMermaidRuntimeMessage(event.data); + if (!message) { + return; + } + if (message.type === "bridgeReady") { + sendRequest(driverRef.current?.ready() ?? null); + return; + } + if (message.type === "renderError") { + onRenderFailed(message.revision); + sendRequest(driverRef.current?.settled(message.revision, false) ?? null); + return; + } + onRendered(message); + sendRequest(driverRef.current?.settled(message.revision, true) ?? null); + } + window.addEventListener("message", receiveMessage); + return () => window.removeEventListener("message", receiveMessage); + }, [onRenderFailed, onRendered, sendRequest]); + + return ( + `},\"putIntoIFrame\"),n$e=v((t,e,r,n,i)=>{let a=t.append(\"div\");a.attr(\"id\",r),n&&a.attr(\"style\",n);let s=a.append(\"svg\").attr(\"id\",e).attr(\"width\",\"100%\").attr(\"xmlns\",gNt);return i&&s.attr(\"xmlns:xlink\",i),s.append(\"g\"),t},\"appendDivSvgG\");function VK(t,e){return t.append(\"iframe\").attr(\"id\",e).attr(\"style\",\"width: 100%; height: 100%;\").attr(\"sandbox\",\"\")}v(VK,\"sandboxedIframe\");var INt=v((t,e,r,n)=>{t.getElementById(e)?.remove(),t.getElementById(r)?.remove(),t.getElementById(n)?.remove()},\"removeExistingElements\"),MNt=v(async function(t,e,r){lM();let n=qK(e);e=n.code;let i=It();ne.debug(i),e.length>(i?.maxTextSize??hNt)&&(e=dNt);let a=`#${t}`,s=\"i\"+t,o=\"#\"+s,l=\"d\"+t,u=\"#\"+l,h=v(()=>{let _=tt(f?o:u).node();_&&\"remove\"in _&&_.remove()},\"removeTempElements\"),d=tt(document.body),f=i.securityLevel===fNt,p=i.securityLevel===pNt,g=i.fontFamily;if(r!==void 0){if(r&&(r.innerHTML=\"\"),f){let R=VK(tt(r),s);d=tt(R.nodes()[0].contentDocument.body),d.node().style.margin=\"0\"}else d=tt(r);n$e(d,t,l,`font-family: ${g}`,mNt)}else{if(INt(document,t,l,s),f){let R=VK(tt(document.body),s);d=tt(R.nodes()[0].contentDocument.body),d.node().style.margin=\"0\"}else d=tt(\"body\");n$e(d,t,l)}let m,y;try{m=await GK.fromText(e,{title:n.title})}catch(R){if(i.suppressErrorRendering)throw h(),R;m=await GK.fromText(\"error\"),y=R}let x=d.select(u).node(),b=m.type,T=x.firstChild,k=T.firstChild,C=m.renderer.getClasses?.(e,m),w=_Nt(i,b,C,a),S=document.createElement(\"style\");S.innerHTML=w,T.insertBefore(S,k);try{await m.renderer.draw(e,t,\"11.16.0\",m)}catch(R){throw i.suppressErrorRendering?h():$Mt.draw(e,t,\"11.16.0\"),R}let A=d.select(`${u} svg`),L=m.db.getAccTitle?.(),M=m.db.getAccDescription?.();K$e(b,A,L,M),d.select(`[id=\"${t}\"]`).selectAll(\"foreignobject > *\").attr(\"xmlns\",yNt);let P=d.select(u).node().innerHTML;if(ne.debug(\"config.arrowMarkerAbsolute\",i.arrowMarkerAbsolute),P=RNt(P,f,yl(i.arrowMarkerAbsolute)),f){let R=d.select(u+\" svg\").node();P=LNt(P,R)}else p||(P=to.sanitize(P,{ADD_TAGS:kNt,ADD_ATTR:SNt,HTML_INTEGRATION_POINTS:{foreignobject:!0}}));if(sNt(),y)throw y;return h(),{diagramType:b,svg:P,bindFunctions:m.db.bindFunctions}},\"render\");function X$e(t={}){let e=Jn({},t);e?.fontFamily&&!e.themeVariables?.fontFamily&&(e.themeVariables||(e.themeVariables={}),e.themeVariables.fontFamily=e.fontFamily),NJ(e),e?.theme&&e.theme in qc?e.themeVariables=qc[e.theme].getThemeVariables(e.themeVariables):e&&(e.themeVariables=qc.default.getThemeVariables(e.themeVariables));let r=typeof e==\"object\"?DJ(e):AD();rC(r.logLevel),lM()}v(X$e,\"initialize\");var j$e=v((t,e={})=>{let{code:r}=WK(t);return GK.fromText(r,e)},\"getDiagramFromText\");function K$e(t,e,r,n){W$e(e,t),q$e(e,r,n,e.attr(\"id\"))}v(K$e,\"addA11yInfo\");var Im=Object.freeze({render:MNt,parse:Y$e,getDiagramFromText:j$e,initialize:X$e,getConfig:It,setConfig:_D,getSiteConfig:AD,updateSiteConfig:OJ,reset:v(()=>{CC()},\"reset\"),globalReset:v(()=>{CC(Gp)},\"globalReset\"),defaultConfig:Gp});rC(It().logLevel);CC(It());var DNt=v((t,e,r)=>{ne.warn(t),fA(t)?(r&&r(t.str,t.hash),e.push({...t,message:t.str,error:t})):(r&&r(t),t instanceof Error&&e.push({str:t.message,message:t.message,hash:t.name,error:t}))},\"handleError\"),Z$e=v(async function(t={querySelector:\".mermaid\"}){try{await NNt(t)}catch(e){if(fA(e)&&ne.error(e.str),wd.parseError&&wd.parseError(e),!t.suppressErrors)throw ne.error(\"Use the suppressErrors option to suppress these errors\"),e}},\"run\"),NNt=v(async function({postRenderCallback:t,querySelector:e,nodes:r}={querySelector:\".mermaid\"}){let n=Im.getConfig();ne.debug(`${t?\"\":\"No \"}Callback function found`);let i;if(r)i=r;else if(e)i=document.querySelectorAll(e);else throw new Error(\"Nodes and querySelector are both undefined\");ne.debug(`Found ${i.length} diagrams`),n?.startOnLoad!==void 0&&(ne.debug(\"Start On Load: \"+n?.startOnLoad),Im.updateSiteConfig({startOnLoad:n?.startOnLoad}));let a=new Zt.InitIDGenerator(n.deterministicIds,n.deterministicIDSeed),s,o=[];for(let l of Array.from(i)){if(ne.info(\"Rendering diagram: \"+l.id),l.getAttribute(\"data-processed\"))continue;l.setAttribute(\"data-processed\",\"true\");let u=`mermaid-${a.next()}`;s=l.innerHTML,s=CA(Zt.entityDecode(s)).trim().replace(//gi,\"
\");let h=Zt.detectInit(s);h&&ne.debug(\"Detected early reinit: \",h);try{let{svg:d,bindFunctions:f}=await tFe(u,s,l);l.innerHTML=d,t&&await t(u),f&&f(l)}catch(d){DNt(d,o,wd.parseError)}}if(o.length>0)throw o[0]},\"runThrowsErrors\"),Q$e=v(function(t){Im.initialize(t)},\"initialize\"),ONt=v(async function(t,e,r){ne.warn(\"mermaid.init is deprecated. Please use run instead.\"),t&&Q$e(t);let n={postRenderCallback:r,querySelector:\".mermaid\"};typeof e==\"string\"?n.querySelector=e:e&&(e instanceof HTMLElement?n.nodes=[e]:n.nodes=e),await Z$e(n)},\"init\"),PNt=v(async(t,{lazyLoad:e=!0}={})=>{lM(),c5(...t),e===!1&&await iNt()},\"registerExternalDiagrams\"),J$e=v(function(){if(wd.startOnLoad){let{startOnLoad:t}=Im.getConfig();t&&wd.run().catch(e=>ne.error(\"Mermaid failed to initialize\",e))}},\"contentLoaded\");typeof document<\"u\"&&window.addEventListener(\"load\",J$e,!1);var BNt=v(function(t){wd.parseError=t},\"setParseErrorHandler\"),oM=[],zK=!1,eFe=v(async()=>{if(!zK){for(zK=!0;oM.length>0;){let t=oM.shift();if(t)try{await t()}catch(e){ne.error(\"Error executing queue\",e)}}zK=!1}},\"executeQueue\"),$Nt=v(async(t,e)=>new Promise((r,n)=>{let i=v(()=>new Promise((a,s)=>{Im.parse(t,e).then(o=>{a(o),r(o)},o=>{ne.error(\"Error parsing\",o),wd.parseError?.(o),s(o),n(o)})}),\"performCall\");oM.push(i),eFe().catch(n)}),\"parse\"),tFe=v((t,e,r)=>new Promise((n,i)=>{let a=v(()=>new Promise((s,o)=>{Im.render(t,e,r).then(l=>{s(l),n(l)},l=>{ne.error(\"Error parsing\",l),wd.parseError?.(l),o(l),i(l)})}),\"performCall\");oM.push(a),eFe().catch(i)}),\"render\"),FNt=v(()=>Object.keys(Bd).map(t=>({id:t})),\"getRegisteredDiagramsMetadata\"),wd={startOnLoad:!0,mermaidAPI:Im,parse:$Nt,render:tFe,init:ONt,run:Z$e,registerExternalDiagrams:PNt,registerLayoutLoaders:_G,initialize:Q$e,parseError:void 0,contentLoaded:J$e,setParseErrorHandler:BNt,detectType:wC,registerIconPacks:qy,getRegisteredDiagramsMetadata:FNt},UK=wd;function zNt(t){return typeof t==\"object\"&&t!==null}function GNt(t){return t===\"light\"||t===\"dark\"}function rFe(t){return!zNt(t)||t.type!==\"render\"||typeof t.revision!=\"number\"||!Number.isInteger(t.revision)||typeof t.source!=\"string\"||!GNt(t.colorScheme)||typeof t.interactive!=\"boolean\"?null:{type:\"render\",revision:t.revision,source:t.source,colorScheme:t.colorScheme,interactive:t.interactive}}function YK(t){window.ReactNativeWebView?.postMessage?.(JSON.stringify(t)),window.parent!==window&&window.parent.postMessage(t,\"*\")}function VNt(t){UK.initialize({startOnLoad:!1,securityLevel:\"strict\",suppressErrorRendering:!0,theme:t===\"dark\"?\"dark\":\"default\",secure:[\"secure\",\"securityLevel\",\"startOnLoad\",\"maxTextSize\",\"suppressErrorRendering\",\"maxEdges\",\"theme\",\"themeVariables\",\"themeCSS\"],flowchart:{htmlLabels:!1},class:{htmlLabels:!1}})}function WNt(t){document.querySelector('meta[name=\"viewport\"]')?.setAttribute(\"content\",t?\"width=device-width, initial-scale=1, maximum-scale=8\":\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\")}var XK=0,cM=null,uM=!1,HK=!1;async function qNt(t){try{VNt(t.colorScheme);let{svg:e}=await UK.render(`paseo-mermaid-${t.revision}`,t.source);if(t.revision!==XK)return;WNt(t.interactive);let r=document.getElementById(\"diagram\");if(!r)return;r.innerHTML=e;let n=r.querySelector(\"svg\")?.getBoundingClientRect();YK({type:\"rendered\",revision:t.revision,source:t.source,colorScheme:t.colorScheme,height:Math.ceil(n?.height??r.scrollHeight),width:Math.ceil(n?.width??r.scrollWidth)})}catch{t.revision===XK&&YK({type:\"renderError\",revision:t.revision})}}async function UNt(){if(!uM){uM=!0;try{for(;cM;){let t=cM;cM=null,await qNt(t)}}finally{uM=!1}}}function nFe(t){let e=rFe(t);e&&(XK=e.revision,cM=e,!(uM||HK)&&(HK=!0,window.setTimeout(()=>{HK=!1,UNt()},0)))}window.__PASEO_MERMAID_RUNTIME_RECEIVE__=nFe;window.addEventListener(\"message\",t=>{t.source===window.parent&&nFe(t.data)});YK({type:\"bridgeReady\"});})();\n/*! Bundled license information:\n\ndompurify/dist/purify.es.mjs:\n (*! @license DOMPurify 3.4.12 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.12/LICENSE *)\n\nmermaid/dist/chunks/mermaid.core/chunk-ZIRB5QZD.mjs:\n (*! Bundled license information:\n \n js-yaml/dist/js-yaml.mjs:\n (*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT *)\n *)\n\nlodash-es/lodash.js:\n (**\n * @license\n * Lodash (Custom Build) \n * Build: `lodash modularize exports=\"es\" --repo lodash/lodash#4.18.1 -o ./`\n * Copyright OpenJS Foundation and other contributors \n * Released under MIT license \n * Based on Underscore.js 1.8.3 \n * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n *)\n\ncytoscape/dist/cytoscape.esm.mjs:\n (*!\n Embeddable Minimum Strictly-Compliant Promises/A+ 1.1.1 Thenable\n Copyright (c) 2013-2014 Ralf S. Engelschall (http://engelschall.com)\n Licensed under The MIT License (http://opensource.org/licenses/MIT)\n *)\n (*!\n Event object based on jQuery events, MIT license\n \n https://jquery.org/license/\n https://tldrlegal.com/license/mit-license\n https://github.com/jquery/jquery/blob/master/src/event.js\n *)\n (*! Bezier curve function generator. Copyright Gaetan Renaudeau. MIT License: http://en.wikipedia.org/wiki/MIT_License *)\n (*! Runge-Kutta spring physics function generator. Adapted from Framer.js, copyright Koen Bok. MIT License: http://en.wikipedia.org/wiki/MIT_License *)\n\n@mermaid-js/parser/dist/chunks/mermaid-parser.core/chunk-KEIR6QF5.mjs:\n (*! Bundled license information:\n \n lodash-es/lodash.js:\n (**\n * @license\n * Lodash (Custom Build) \n * Build: `lodash modularize exports=\"es\" -o ./`\n * Copyright OpenJS Foundation and other contributors \n * Released under MIT license \n * Based on Underscore.js 1.8.3 \n * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n *)\n *)\n\nmermaid/dist/mermaid.core.mjs:\n (*! Check if previously processed *)\n (*!\n * Wait for document loaded before starting the execution\n *)\n*/\n\n \n"; diff --git a/packages/app/src/components/markdown/fence/mermaid/runtime/messages.ts b/packages/app/src/components/markdown/fence/mermaid/runtime/messages.ts new file mode 100644 index 0000000000..99b23f9d6d --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/runtime/messages.ts @@ -0,0 +1,91 @@ +import type { DiagramColorScheme, DiagramDimensions } from "../render-model"; + +export interface MermaidRuntimeRenderMessage { + type: "render"; + revision: number; + source: string; + colorScheme: DiagramColorScheme; + interactive: boolean; +} + +export type MermaidRuntimeMessage = + | { type: "bridgeReady" } + | ({ + type: "rendered"; + revision: number; + source: string; + colorScheme: DiagramColorScheme; + } & DiagramDimensions) + | { type: "renderError"; revision: number }; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function isColorScheme(value: unknown): value is DiagramColorScheme { + return value === "light" || value === "dark"; +} + +export function parseMermaidRuntimeRenderMessage( + value: unknown, +): MermaidRuntimeRenderMessage | null { + if ( + !isRecord(value) || + value.type !== "render" || + typeof value.revision !== "number" || + !Number.isInteger(value.revision) || + typeof value.source !== "string" || + !isColorScheme(value.colorScheme) || + typeof value.interactive !== "boolean" + ) { + return null; + } + return { + type: "render", + revision: value.revision, + source: value.source, + colorScheme: value.colorScheme, + interactive: value.interactive, + }; +} + +export function parseMermaidRuntimeMessage(value: unknown): MermaidRuntimeMessage | null { + if (!isRecord(value) || typeof value.type !== "string") { + return null; + } + if (value.type === "bridgeReady") { + return { type: "bridgeReady" }; + } + if ( + value.type === "renderError" && + typeof value.revision === "number" && + Number.isInteger(value.revision) + ) { + return { type: "renderError", revision: value.revision }; + } + if ( + value.type === "rendered" && + typeof value.revision === "number" && + Number.isInteger(value.revision) && + typeof value.source === "string" && + isColorScheme(value.colorScheme) && + typeof value.height === "number" && + Number.isFinite(value.height) && + typeof value.width === "number" && + Number.isFinite(value.width) + ) { + return { + type: "rendered", + revision: value.revision, + source: value.source, + colorScheme: value.colorScheme, + height: value.height, + width: value.width, + }; + } + return null; +} + +export function serializeMermaidRuntimeRenderMessage(message: MermaidRuntimeRenderMessage): string { + return JSON.stringify(message).replace(/<\/script/gi, "<\\/script"); +} diff --git a/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.test.ts b/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.test.ts new file mode 100644 index 0000000000..f2e93e2437 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import type { MermaidRenderRequest } from "../render-model"; +import { MermaidRuntimeRequestDriver } from "./request-driver"; + +function request(revision: number, source: string): MermaidRenderRequest { + return { revision, source, colorScheme: "dark" }; +} + +describe("MermaidRuntimeRequestDriver", () => { + it("tries queued streaming prefixes in order until the runtime renders one", () => { + const driver = new MermaidRuntimeRequestDriver(); + const first = request(1, "flow"); + const valid = request(2, "flowchart TD\nA --> B\n"); + const transient = request(3, "flowchart TD\nA --> B\nB --"); + + expect(driver.update(first)).toBeNull(); + expect(driver.update(valid)).toBeNull(); + expect(driver.update(transient)).toBeNull(); + expect(driver.ready()).toBe(valid); + expect(driver.settled(valid.revision, true)).toBe(transient); + }); + + it("keeps completed lines but collapses incomplete token revisions before readiness", () => { + const driver = new MermaidRuntimeRequestDriver(); + const partial = request(1, "flowchart TD\nA --"); + const completed = request(2, "flowchart TD\nA --> B\n"); + const nextPartial = request(3, "flowchart TD\nA --> B\nB --"); + const latestPartial = request(4, "flowchart TD\nA --> B\nB -->"); + + driver.update(partial); + driver.update(completed); + driver.update(nextPartial); + driver.update(latestPartial); + + expect(driver.ready()).toBe(completed); + expect(driver.settled(completed.revision, true)).toBe(latestPartial); + }); + + it("collapses later updates to the newest request after the first render", () => { + const driver = new MermaidRuntimeRequestDriver(); + const first = request(1, "flowchart TD\nA --> B"); + const second = request(2, "flowchart TD\nA --> B\nB --> C"); + const latest = request(3, "flowchart TD\nA --> B\nB --> D"); + + driver.update(first); + expect(driver.ready()).toBe(first); + expect(driver.settled(first.revision, true)).toBeNull(); + expect(driver.update(second)).toBe(second); + expect(driver.update(latest)).toBeNull(); + expect(driver.settled(second.revision, false)).toBe(latest); + }); +}); diff --git a/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.ts b/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.ts new file mode 100644 index 0000000000..8e30587017 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/runtime/request-driver.ts @@ -0,0 +1,63 @@ +import type { MermaidRenderRequest } from "../render-model"; + +function isCompletedLineCandidate(source: string): boolean { + return source.endsWith("\n") && source.trim().includes("\n"); +} + +export class MermaidRuntimeRequestDriver { + private readyState = false; + private renderedOnce = false; + private inFlight: MermaidRenderRequest | null = null; + private queued: MermaidRenderRequest[] = []; + + update(request: MermaidRenderRequest | null): MermaidRenderRequest | null { + if (!request || this.hasRevision(request.revision)) { + return null; + } + if (this.renderedOnce) { + this.queued = [request]; + } else { + const lastIndex = this.queued.length - 1; + const last = this.queued[lastIndex]; + if (last && !isCompletedLineCandidate(last.source)) { + this.queued[lastIndex] = request; + } else { + this.queued.push(request); + } + } + return this.takeNext(); + } + + ready(): MermaidRenderRequest | null { + this.readyState = true; + return this.takeNext(); + } + + settled(revision: number, rendered: boolean): MermaidRenderRequest | null { + if (this.inFlight?.revision !== revision) { + return null; + } + this.inFlight = null; + if (rendered) { + this.renderedOnce = true; + const latest = this.queued.at(-1); + this.queued = latest ? [latest] : []; + } + return this.takeNext(); + } + + private hasRevision(revision: number): boolean { + return ( + this.inFlight?.revision === revision || + this.queued.some((request) => request.revision === revision) + ); + } + + private takeNext(): MermaidRenderRequest | null { + if (!this.readyState || this.inFlight || this.queued.length === 0) { + return null; + } + this.inFlight = this.queued.shift() ?? null; + return this.inFlight; + } +} diff --git a/packages/app/src/components/markdown/fence/mermaid/runtime/runtime-html.test.ts b/packages/app/src/components/markdown/fence/mermaid/runtime/runtime-html.test.ts new file mode 100644 index 0000000000..a11ffd5092 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/runtime/runtime-html.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; +import { mermaidRuntimeHtml } from "./html.gen"; + +describe("Mermaid runtime document", () => { + it("ships a closed network policy", () => { + expect(mermaidRuntimeHtml).toContain("default-src 'none'"); + expect(mermaidRuntimeHtml).toContain("connect-src 'none'"); + expect(mermaidRuntimeHtml).toContain("img-src data: blob:"); + expect(mermaidRuntimeHtml).toContain("font-src 'none'"); + expect(mermaidRuntimeHtml).toContain("frame-src 'none'"); + }); +}); diff --git a/packages/app/src/components/markdown/fence/mermaid/runtime/runtime.browser.test.ts b/packages/app/src/components/markdown/fence/mermaid/runtime/runtime.browser.test.ts new file mode 100644 index 0000000000..1dda832eb3 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/runtime/runtime.browser.test.ts @@ -0,0 +1,119 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { mermaidRuntimeHtml } from "./html.gen"; +import { parseMermaidRuntimeMessage, type MermaidRuntimeMessage } from "./messages"; + +const mountedFrames: HTMLIFrameElement[] = []; + +function waitForRuntimeMessage( + frame: HTMLIFrameElement, + predicate: (message: MermaidRuntimeMessage) => boolean, +): Promise { + return new Promise((resolve, reject) => { + const timeout = window.setTimeout(() => { + window.removeEventListener("message", receive); + reject(new Error("Timed out waiting for Mermaid runtime")); + }, 10_000); + function receive(event: MessageEvent): void { + if (event.source !== frame.contentWindow) { + return; + } + const message = parseMermaidRuntimeMessage(event.data); + if (!message || !predicate(message)) { + return; + } + window.clearTimeout(timeout); + window.removeEventListener("message", receive); + resolve(message); + } + window.addEventListener("message", receive); + }); +} + +async function mountRuntime(): Promise { + const frame = document.createElement("iframe"); + frame.sandbox.add("allow-scripts"); + const ready = waitForRuntimeMessage(frame, (message) => message.type === "bridgeReady"); + frame.srcdoc = mermaidRuntimeHtml; + document.body.append(frame); + mountedFrames.push(frame); + await ready; + return frame; +} + +function render( + frame: HTMLIFrameElement, + input: { revision: number; source: string }, +): Promise { + const response = waitForRuntimeMessage( + frame, + (message) => + message.type !== "bridgeReady" && + "revision" in message && + message.revision === input.revision, + ); + frame.contentWindow?.postMessage( + { + type: "render", + revision: input.revision, + source: input.source, + colorScheme: "dark", + interactive: false, + }, + "*", + ); + return response; +} + +afterEach(() => { + for (const frame of mountedFrames.splice(0)) { + frame.remove(); + } +}); + +describe("Mermaid sandbox runtime", () => { + it("renders successive valid streaming prefixes and reports an invalid prefix", async () => { + const frame = await mountRuntime(); + const firstSource = "flowchart TD\nA --> B"; + const secondSource = `${firstSource}\nB --> C`; + + const first = await render(frame, { revision: 1, source: firstSource }); + const invalid = await render(frame, { revision: 2, source: "not a mermaid diagram" }); + const second = await render(frame, { revision: 3, source: secondSource }); + + expect(first).toMatchObject({ type: "rendered", revision: 1, source: firstSource }); + expect(invalid).toEqual({ type: "renderError", revision: 2 }); + expect(second).toMatchObject({ type: "rendered", revision: 3, source: secondSource }); + }); + + it("coalesces queued input and never reports an obsolete result", async () => { + const frame = await mountRuntime(); + const obsoleteSource = `flowchart TD\n${Array.from({ length: 250 }, (_, index) => `A${index} --> A${index + 1}`).join("\n")}`; + const currentSource = "flowchart TD\nCurrent --> Result"; + const obsoleteResponses: MermaidRuntimeMessage[] = []; + function collect(event: MessageEvent): void { + if (event.source !== frame.contentWindow) { + return; + } + const message = parseMermaidRuntimeMessage(event.data); + if (message && message.type !== "bridgeReady" && message.revision === 10) { + obsoleteResponses.push(message); + } + } + window.addEventListener("message", collect); + frame.contentWindow?.postMessage( + { + type: "render", + revision: 10, + source: obsoleteSource, + colorScheme: "dark", + interactive: false, + }, + "*", + ); + const current = await render(frame, { revision: 11, source: currentSource }); + window.removeEventListener("message", collect); + + expect(current).toMatchObject({ type: "rendered", revision: 11, source: currentSource }); + expect(obsoleteResponses).toEqual([]); + }); +}); diff --git a/packages/app/src/components/markdown/fence/mermaid/source-policy.test.ts b/packages/app/src/components/markdown/fence/mermaid/source-policy.test.ts new file mode 100644 index 0000000000..48b844e1e3 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/source-policy.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from "vitest"; +import { containsUnsafeMermaidSource } from "./source-policy"; + +describe("containsUnsafeMermaidSource", () => { + it("rejects resource-bearing constructs", () => { + expect(containsUnsafeMermaidSource('flowchart TD\n A@{ img: "https://x/y.png" }')).toBe(true); + expect(containsUnsafeMermaidSource("flowchart TD\n A@{ icon: 'pack:name' }")).toBe(true); + expect( + containsUnsafeMermaidSource('%%{init: {"themeCSS": "a { color: red }"}}%%\ngraph TD'), + ).toBe(true); + expect(containsUnsafeMermaidSource("graph TD\n A[url(http://x)]")).toBe(true); + expect(containsUnsafeMermaidSource("graph TD\n A[@import 'x']")).toBe(true); + expect(containsUnsafeMermaidSource('graph TD\n A[""]')).toBe(true); + expect(containsUnsafeMermaidSource('graph TD\n A["styled"]')).toBe(true); + expect(containsUnsafeMermaidSource('graph TD\n A["<img src=x>"]')).toBe(true); + expect(containsUnsafeMermaidSource('graph TD\n A[""]')).toBe(true); + }); + + it("rejects all shape-data constructs, including yaml key evasions", () => { + expect(containsUnsafeMermaidSource('flowchart TD\n A@{ "img": "https://x/y.png" }')).toBe( + true, + ); + expect(containsUnsafeMermaidSource("flowchart TD\n A@{ 'img': 'https://x/y.png' }")).toBe( + true, + ); + expect( + containsUnsafeMermaidSource('flowchart TD\n A@{ "\\u0069mg": "https://x/y.png" }'), + ).toBe(true); + expect( + containsUnsafeMermaidSource('flowchart TD\n A@{ "\\u{69}mg": "https://x/y.png" }'), + ).toBe(true); + expect(containsUnsafeMermaidSource('flowchart TD\n A@{ "\\x69mg": "https://x/y.png" }')).toBe( + true, + ); + expect( + containsUnsafeMermaidSource('flowchart TD\n A@{ "\\U00000069mg": "https://attacker/x" }'), + ).toBe(true); + expect(containsUnsafeMermaidSource('flowchart TD\n A@{ "icon": "pack:name" }')).toBe(true); + expect( + containsUnsafeMermaidSource( + 'flowchart TD\n A@{ ? img # comment\n : "https://attacker/x" }', + ), + ).toBe(true); + expect( + containsUnsafeMermaidSource( + 'flowchart TD\n A@{ dummy: &k img\n ? *k # comment\n : "https://attacker/x" }', + ), + ).toBe(true); + }); + + it("fails closed for malformed or out-of-range escapes without throwing", () => { + expect(() => + containsUnsafeMermaidSource('graph TD\n A["\\u{110000} disguised"]'), + ).not.toThrow(); + expect(containsUnsafeMermaidSource('graph TD\n A["\\u{110000} disguised"]')).toBe(true); + expect(() => + containsUnsafeMermaidSource('graph TD\n A["\\u{FFFFFF} disguised"]'), + ).not.toThrow(); + expect(containsUnsafeMermaidSource('graph TD\n A["\\u{FFFFFF} disguised"]')).toBe(true); + }); + + it("allows ordinary diagrams including formatting-only labels", () => { + expect(containsUnsafeMermaidSource("flowchart TD\n A[Start] --> B{Choice}")).toBe(false); + expect(containsUnsafeMermaidSource('graph TD\n A["line one
line two"]')).toBe(false); + expect(containsUnsafeMermaidSource('graph TD\n A["line one
line two"]')).toBe(false); + expect(containsUnsafeMermaidSource('graph TD\n A["formatted"]')).toBe(false); + expect(containsUnsafeMermaidSource("sequenceDiagram\n Alice->>Bob: a < b and x > y")).toBe( + false, + ); + expect(containsUnsafeMermaidSource("flowchart TD\n A@{ shape: rect }")).toBe(true); + }); +}); diff --git a/packages/app/src/components/markdown/fence/mermaid/source-policy.ts b/packages/app/src/components/markdown/fence/mermaid/source-policy.ts new file mode 100644 index 0000000000..df6ad0312b --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/source-policy.ts @@ -0,0 +1,52 @@ +// Mermaid can fetch external resources while *rendering* — image shapes +// (`A@{ img: "url" }`) construct an Image and await decode before any output +// sanitization runs, and CSS can pull url()/@import — so a prompt-injected +// diagram could exfiltrate data in a request URL. securityLevel "strict" does +// not prevent this (mermaid-js/mermaid#7645). Mermaid 11.16 parses the shape +// metadata object with yaml.JSON_SCHEMA semantics, including aliases and +// explicit keys; matching those keys safely would require a real parser. We +// therefore reject any `@{ ... }` shape-data construct up front and fall back +// to the source code block. Formatting-only `
` and `` tags are common +// in generated labels and carry no resource-bearing attributes; all other +// tags (and entity-encoded text that could smuggle one) are rejected. +const UNSAFE_MERMAID_SOURCE = + /@\s*\{|url\s*\(|@import\b|themeCSS|&#|<(?!\/?(?:br|i)\s*\/?>)[a-z!/]/i; + +// Mermaid labels can contain escaped text. Decode the escape forms we care +// about before running the denylist so disguised HTML still gets caught. +// Invalid escape sequences fail closed and fall back to the source block. +function normalizeMermaidSource(code: string): string | null { + try { + return code + .replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (_, hex: string) => + decodeCodePointEscape(Number.parseInt(hex, 16)), + ) + .replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) => + String.fromCharCode(Number.parseInt(hex, 16)), + ) + .replace(/\\x([0-9a-fA-F]{2})/g, (_, hex: string) => + String.fromCharCode(Number.parseInt(hex, 16)), + ) + .replace(/["'`\\]/g, ""); + } catch { + return null; + } +} + +function decodeCodePointEscape(value: number): string { + if (!Number.isInteger(value) || value < 0 || value > 0x10ffff) { + throw new RangeError("Invalid Unicode code point"); + } + return String.fromCodePoint(value); +} + +export function containsUnsafeMermaidSource(code: string): boolean { + if (UNSAFE_MERMAID_SOURCE.test(code)) { + return true; + } + const normalized = normalizeMermaidSource(code); + if (normalized === null) { + return true; + } + return UNSAFE_MERMAID_SOURCE.test(normalized); +} diff --git a/packages/app/src/components/markdown/fence/mermaid/use-render-model.ts b/packages/app/src/components/markdown/fence/mermaid/use-render-model.ts new file mode 100644 index 0000000000..cb35549b10 --- /dev/null +++ b/packages/app/src/components/markdown/fence/mermaid/use-render-model.ts @@ -0,0 +1,96 @@ +import { useCallback, useEffect, useMemo, useReducer } from "react"; +import { + createMermaidRenderModel, + getMermaidRenderRequest, + reduceMermaidRenderModel, + type DiagramColorScheme, + type DiagramDimensions, + type RenderedDiagram, +} from "./render-model"; +import { containsUnsafeMermaidSource } from "./source-policy"; +import type { MarkdownPhase } from "../types"; + +const renderCache = new Map(); +const RENDER_CACHE_LIMIT = 50; + +function cacheKey(source: string, colorScheme: DiagramColorScheme): string { + return `${colorScheme}\u0000${source}`; +} + +function readCachedRender(source: string, colorScheme: DiagramColorScheme): RenderedDiagram | null { + return renderCache.get(cacheKey(source, colorScheme)) ?? null; +} + +function cacheRender(rendered: RenderedDiagram): void { + if (renderCache.size >= RENDER_CACHE_LIMIT) { + const oldest = renderCache.keys().next().value; + if (oldest !== undefined) { + renderCache.delete(oldest); + } + } + renderCache.set(cacheKey(rendered.source, rendered.colorScheme), rendered); +} + +export function useMermaidRenderModel({ + source, + phase, + colorScheme, +}: { + source: string; + phase: MarkdownPhase; + colorScheme: DiagramColorScheme; +}) { + const renderInput = useMemo( + () => ({ + source, + phase, + colorScheme, + rejected: containsUnsafeMermaidSource(source), + cached: readCachedRender(source, colorScheme), + }), + [colorScheme, phase, source], + ); + const [state, dispatch] = useReducer( + reduceMermaidRenderModel, + renderInput, + createMermaidRenderModel, + ); + + useEffect(() => { + dispatch({ type: "inputChanged", input: renderInput }); + }, [renderInput]); + + const rendered = useCallback( + (response: { + revision: number; + source: string; + colorScheme: DiagramColorScheme; + dimensions: DiagramDimensions; + }) => { + const cached: RenderedDiagram = { + source: response.source, + colorScheme: response.colorScheme, + ...response.dimensions, + }; + cacheRender(cached); + dispatch({ + type: "rendered", + revision: response.revision, + source: response.source, + colorScheme: response.colorScheme, + dimensions: response.dimensions, + }); + }, + [], + ); + const renderFailed = useCallback((revision: number) => { + dispatch({ type: "renderFailed", revision }); + }, []); + + return { + state, + request: getMermaidRenderRequest(state), + rendered, + renderFailed, + }; +} diff --git a/packages/app/src/components/markdown/fence/types.ts b/packages/app/src/components/markdown/fence/types.ts new file mode 100644 index 0000000000..b499bc893b --- /dev/null +++ b/packages/app/src/components/markdown/fence/types.ts @@ -0,0 +1,10 @@ +import type { TextStyle } from "react-native"; + +export type MarkdownPhase = "streaming" | "complete"; + +export interface MarkdownFenceRendererProps { + code: string; + phase: MarkdownPhase; + inheritedStyles: TextStyle; + textStyle: TextStyle; +} diff --git a/packages/app/src/components/markdown/link-children.ts b/packages/app/src/components/markdown/link-children.ts new file mode 100644 index 0000000000..9a81042c86 --- /dev/null +++ b/packages/app/src/components/markdown/link-children.ts @@ -0,0 +1,21 @@ +import { Children, cloneElement, isValidElement, type ReactNode } from "react"; +import type { StyleProp, TextStyle } from "react-native"; + +export function colorMarkdownLinkChildren( + children: ReactNode, + color: TextStyle["color"], +): ReactNode { + return Children.map(children, (child) => { + if (!isValidElement<{ style?: StyleProp }>(child)) return child; + return cloneElement(child, { style: [child.props.style, { color }] }); + }); +} + +const MARKDOWN_LINK_HOVER_STYLE: TextStyle = { textDecorationLine: "underline" }; + +export function markdownLinkTextStyle( + style: StyleProp, + hovered: boolean, +): StyleProp { + return [style, hovered && MARKDOWN_LINK_HOVER_STYLE]; +} diff --git a/packages/app/src/components/markdown/link-text.tsx b/packages/app/src/components/markdown/link-text.tsx new file mode 100644 index 0000000000..18b1b6c93b --- /dev/null +++ b/packages/app/src/components/markdown/link-text.tsx @@ -0,0 +1,41 @@ +import { useMemo, useState, type ReactNode } from "react"; +import { Pressable, Text, type StyleProp, type TextStyle } from "react-native"; +import { useStableEvent } from "@/hooks/use-stable-event"; +import { markdownLinkTextStyle } from "./link-children"; + +interface MarkdownLinkTextProps { + style: StyleProp; + dataSet?: Record; + onPress(): void; + onHoverIn?(): void; + children?: ReactNode; +} + +export function MarkdownLinkText({ + style, + dataSet, + onPress, + onHoverIn, + children, +}: MarkdownLinkTextProps) { + const [hovered, setHovered] = useState(false); + const handleHoverIn = useStableEvent(() => { + setHovered(true); + onHoverIn?.(); + }); + const handleHoverOut = useStableEvent(() => setHovered(false)); + const textStyle = useMemo(() => markdownLinkTextStyle(style, hovered), [hovered, style]); + + return ( + + + {children} + + + ); +} diff --git a/packages/app/src/components/markdown/renderer.test.ts b/packages/app/src/components/markdown/renderer.test.ts index 9412d46332..1dc279a2d5 100644 --- a/packages/app/src/components/markdown/renderer.test.ts +++ b/packages/app/src/components/markdown/renderer.test.ts @@ -1,5 +1,48 @@ -import { describe, expect, it } from "vitest"; +/** + * @vitest-environment jsdom + */ +import * as React from "react"; +import { createElement, type ReactNode } from "react"; +import { fireEvent, render } from "@testing-library/react"; +import { Text, type StyleProp, type TextStyle } from "react-native"; +import { describe, expect, it, vi } from "vitest"; import { resolveInlineImageSize } from "./inline-image-size"; +import { colorMarkdownLinkChildren } from "./link-children"; +import { MarkdownLinkText } from "./link-text"; + +vi.stubGlobal("React", React); + +vi.mock("react-native", () => ({ + Pressable: ({ + accessibilityRole, + children, + onHoverIn, + onHoverOut, + onPress, + }: { + accessibilityRole?: string; + children?: ReactNode; + onHoverIn?(): void; + onHoverOut?(): void; + onPress?(): void; + }) => + createElement( + "div", + { + role: accessibilityRole, + onClick: onPress, + onMouseEnter: onHoverIn, + onMouseLeave: onHoverOut, + }, + children, + ), + Text: ({ children, style }: { children?: ReactNode; style?: StyleProp }) => + createElement("span", { style: flattenStyle(style) }, children), +})); + +function flattenStyle(style: StyleProp): TextStyle { + return Object.assign({}, ...(Array.isArray(style) ? style.filter(Boolean) : [style])); +} describe("resolveInlineImageSize", () => { it("respects a one-sided explicit width using natural aspect ratio", () => { @@ -27,3 +70,30 @@ describe("resolveInlineImageSize", () => { }); }); }); + +describe("shared Markdown links", () => { + it("renders accent text and underlines it while hovered", () => { + const onPress = vi.fn(); + const children = colorMarkdownLinkChildren( + createElement(Text, { style: { color: "white" } }, "Paseo"), + "rgb(0, 122, 255)", + ); + const view = render( + createElement(MarkdownLinkText, { style: { color: "rgb(0, 122, 255)" }, onPress }, children), + ); + const link = view.getByRole("link"); + const linkText = link.firstElementChild as HTMLElement; + + expect((view.getByText("Paseo") as HTMLElement).style.color).toBe("rgb(0, 122, 255)"); + expect(linkText.style.textDecorationLine).toBe(""); + + fireEvent.mouseEnter(link); + expect(linkText.style.textDecorationLine).toBe("underline"); + + fireEvent.mouseLeave(link); + expect(linkText.style.textDecorationLine).toBe(""); + + fireEvent.click(link); + expect(onPress).toHaveBeenCalledOnce(); + }); +}); diff --git a/packages/app/src/components/markdown/renderer.tsx b/packages/app/src/components/markdown/renderer.tsx index 292ea6f76e..d9b0cc2be1 100644 --- a/packages/app/src/components/markdown/renderer.tsx +++ b/packages/app/src/components/markdown/renderer.tsx @@ -23,6 +23,7 @@ import Markdown, { } from "react-native-markdown-display"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; +import { MarkdownFenceBlock } from "@/components/markdown/fence"; import { MarkdownParagraphView, MarkdownTextSpan } from "@/components/markdown-text"; import { MarkdownTableCellText } from "@/components/markdown-text-selection"; import { getMarkdownListMarker, getMarkdownListSpacing } from "@/utils/markdown-list"; @@ -30,6 +31,7 @@ import { markdownNodeContainsType } from "@/utils/markdown-ast"; import { createCompactMarkdownStyles, createMarkdownStyles } from "@/styles/markdown-styles"; import type { Theme } from "@/styles/theme"; import { openExternalUrl } from "@/utils/open-external-url"; +import { isNative } from "@/constants/platform"; import { splitHtmlishMarkdown, type MarkdownDisplayPart, @@ -37,6 +39,8 @@ import { } from "./html-ish"; import { resolveInlineImageSize, type InlineImageDimensions } from "./inline-image-size"; import { groupMarkdownParts, type MarkdownPartGroup } from "./part-groups"; +import { colorMarkdownLinkChildren } from "./link-children"; +import { MarkdownLinkText } from "./link-text"; export type MarkdownStyles = Record; @@ -477,6 +481,15 @@ function SharedMarkdownLink({ if (onLinkPress?.(href) === false) return; void openExternalUrl(href); }, [href, onLinkPress]); + const style = useMemo(() => [inheritedStyles, linkStyle], [inheritedStyles, linkStyle]); + + if (!isNative) { + return ( + + {children} + + ); + } return ( ( - @@ -713,7 +727,7 @@ export function createSharedMarkdownRules(): RenderRules { linkStyle={styles.link} onLinkPress={onLinkPress} > - {children} + {colorMarkdownLinkChildren(children, styles.link.color)} ), }; diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index e2e54fa778..8058320f99 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -1,4 +1,5 @@ import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { TaskListRow } from "@/components/task-list-row"; import { View, Text, @@ -23,9 +24,6 @@ import { useCallback, createContext, useContext, - isValidElement, - Children, - cloneElement, } from "react"; import type { ComponentType, ReactNode } from "react"; import type MarkdownIt from "markdown-it"; @@ -41,7 +39,10 @@ import { ChevronDown, Check, CheckSquare, + CircleDot, Copy, + Plus, + RotateCcw, TriangleAlertIcon, Scissors, MicVocal, @@ -62,7 +63,7 @@ import Svg, { Defs, LinearGradient as SvgLinearGradient, Rect, Stop } from "reac import { CODE_SURFACE_DATASET } from "@/styles/code-surface"; import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style"; import { MarkdownRenderer, type MarkdownStyles } from "@/components/markdown/renderer"; -import type { TodoEntry, UserMessageImageAttachment } from "@/types/stream"; +import type { TaskActivity, TodoEntry, UserMessageImageAttachment } from "@/types/stream"; import type { AgentAttachment } from "@getpaseo/protocol/messages"; import type { ToolCallDetail } from "@getpaseo/protocol/agent-types"; import { buildToolCallPresentation } from "@/tool-calls/presentation"; @@ -71,7 +72,10 @@ import { getMarkdownListMarker, getMarkdownListSpacing } from "@/utils/markdown- import { markdownNodeContainsType } from "@/utils/markdown-ast"; import { useStableEvent } from "@/hooks/use-stable-event"; import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; +import { MarkdownFenceBlock } from "@/components/markdown/fence"; +import type { MarkdownPhase } from "@/components/markdown/fence/types"; import { splitMarkdownBlocks } from "@/utils/split-markdown-blocks"; +import { colorMarkdownLinkChildren } from "@/components/markdown/link-children"; import { createAssistantMarkdownParser } from "@/utils/assistant-markdown-parser"; import { formatDuration, formatMessageTimestamp } from "@/utils/time"; import { writeMarkdownToRichClipboard } from "@/utils/rich-clipboard"; @@ -163,7 +167,6 @@ const MARKDOWN_ALLOWED_IMAGE_HANDLERS = [ const MARKDOWN_TOP_LEVEL_MAX_EXCEEDED_ITEM = ...; const ThemedMicVocal = withUnistyles(MicVocal); -const ThemedTodoCheckIcon = withUnistyles(Check); const ThemedFileSymlinkIcon = withUnistyles(FileSymlink); const ThemedTriangleAlertIcon = withUnistyles(TriangleAlertIcon); const ThemedChevronRightIcon = withUnistyles(ChevronRight); @@ -176,9 +179,6 @@ const foregroundMutedColorMapping = (theme: Theme) => ({ const mutedForegroundColorMapping = (theme: Theme) => ({ color: theme.colors.mutedForeground, }); -const primaryForegroundColorMapping = (theme: Theme) => ({ - color: theme.colors.primaryForeground, -}); const destructiveColorMapping = (theme: Theme) => ({ color: theme.colors.destructive }); const WEB_TOOLCALL_SHIMMER_KEYFRAME_CSS = ` @keyframes ${WEB_TOOLCALL_SHIMMER_ANIMATION_NAME} { @@ -739,6 +739,7 @@ interface AssistantMessageProps { serverId?: string; client?: DaemonClient | null; spacing?: "default" | "compactTop" | "compactBottom" | "compactBoth"; + phase: MarkdownPhase; } export const assistantMessageStylesheet = StyleSheet.create((theme) => ({ @@ -1450,6 +1451,7 @@ export const AssistantMessage = memo(function AssistantMessage({ serverId, client, spacing = "default", + phase, }: AssistantMessageProps) { const markdownParser = useMemo(createAssistantMarkdownParser, []); @@ -1695,10 +1697,11 @@ export const AssistantMessage = memo(function AssistantMessage({ styles: MarkdownStyles, inheritedStyles: TextStyle = {}, ) => ( - @@ -1855,13 +1858,7 @@ export const AssistantMessage = memo(function AssistantMessage({ source={getMarkdownLinkSource(node)} style={styles.link} > - {Children.map(children, (child) => { - if (!isValidElement(child)) return child; - const childProps = child.props as { style?: StyleProp }; - return cloneElement(child, { - style: [childProps.style, { color: styles.link.color }], - } as Partial<{ style: StyleProp }>); - })} + {colorMarkdownLinkChildren(children, styles.link.color)} ), image: ( @@ -1893,11 +1890,11 @@ export const AssistantMessage = memo(function AssistantMessage({ ); }, }; - }, [client, fileLinkActions, markdownParser, occurrenceKey, serverId, workspaceRoot]); + }, [client, fileLinkActions, markdownParser, occurrenceKey, phase, serverId, workspaceRoot]); const blocks = useMemo(() => splitMarkdownBlocks(message), [message]); const keyedBlocks = useMemo( - () => blocks.map((block, index) => ({ key: `${index}:${block.slice(0, 32)}`, block })), + () => blocks.map((block, index) => ({ key: `block:${index}`, block })), [blocks], ); @@ -2235,38 +2232,23 @@ export const CompactionMarker = memo(function CompactionMarker({ interface TodoListCardProps { items: TodoEntry[]; + activity: TaskActivity; disableOuterSpacing?: boolean; } -interface TodoListItemRowProps { - text: string; - completed: boolean; -} - -function TodoListItemRow({ text, completed }: TodoListItemRowProps) { - const badgeStyle = useMemo( - () => [ - todoListCardStylesheet.radioBadge, - completed - ? todoListCardStylesheet.radioBadgeComplete - : todoListCardStylesheet.radioBadgeIncomplete, - ], - [completed], - ); - const textStyle = useMemo( - () => [todoListCardStylesheet.itemText, completed && todoListCardStylesheet.itemTextCompleted], - [completed], - ); - return ( - - - {completed ? ( - - ) : null} - - {text} - - ); +function taskActivityIcon(activity: TaskActivity) { + switch (activity.type) { + case "added": + return Plus; + case "started": + return CircleDot; + case "completed": + return Check; + case "reopened": + return RotateCcw; + default: + return CheckSquare; + } } const todoListCardStylesheet = StyleSheet.create((theme) => ({ @@ -2276,34 +2258,6 @@ const todoListCardStylesheet = StyleSheet.create((theme) => ({ list: { gap: theme.spacing[1], }, - itemRow: { - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[2], - }, - radioBadge: { - width: 16, - height: 16, - borderRadius: 8, - backgroundColor: theme.colors.foregroundMuted, - alignItems: "center", - justifyContent: "center", - }, - radioBadgeIncomplete: { - opacity: 0.55, - }, - radioBadgeComplete: { - opacity: 0.95, - }, - itemText: { - flex: 1, - color: theme.colors.foreground, - fontSize: theme.fontSize.base, - }, - itemTextCompleted: { - color: theme.colors.foregroundMuted, - textDecorationLine: "line-through", - }, emptyText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.base, @@ -2312,12 +2266,23 @@ const todoListCardStylesheet = StyleSheet.create((theme) => ({ export const TodoListCard = memo(function TodoListCard({ items, + activity, disableOuterSpacing, }: TodoListCardProps) { const { t } = useTranslation(); const [isExpanded, setIsExpanded] = useState(false); - - const nextTask = useMemo(() => items.find((item) => !item.completed)?.text, [items]); + const activityDisplay = useMemo(() => { + if (activity.type === "created") { + return { + label: t("message.todo.activity.created", { count: activity.count }), + secondaryLabel: undefined, + }; + } + return { + label: t(`message.todo.activity.${activity.type}`), + secondaryLabel: activity.task, + }; + }, [activity, t]); const handleToggle = useCallback(() => { setIsExpanded((prev) => !prev); @@ -2330,9 +2295,7 @@ export const TodoListCard = memo(function TodoListCard({ {items.length === 0 ? ( {t("message.todo.empty")} ) : ( - items.map((item) => ( - - )) + items.map((item) => ) )}
@@ -2341,9 +2304,9 @@ export const TodoListCard = memo(function TodoListCard({ return ( { providers: [pi], selectedProvider: "", selectedModel: "", - favoriteKeys: new Set(), + hasProfiles: false, }), ).toEqual({ kind: "provider", providerId: "pi", providerLabel: "Pi" }); }); - it("opens the selected provider when its model is not a favorite", () => { + it("opens the selected provider when there is one", () => { expect( resolveInitialModelBrowserView({ providers: [codex, pi], selectedProvider: "pi", selectedModel: "pi-pro", - favoriteKeys: new Set(), + hasProfiles: false, }), ).toEqual({ kind: "provider", providerId: "pi", providerLabel: "Pi" }); }); - it("opens the provider overview when the selected model is a favorite", () => { + it("opens the root so pinned profiles are reachable", () => { expect( resolveInitialModelBrowserView({ providers: [codex, pi], selectedProvider: "pi", selectedModel: "pi-pro", - favoriteKeys: new Set(["pi:pi-pro"]), + hasProfiles: true, + }), + ).toEqual({ kind: "all" }); + }); + + it("opens a sole provider directly regardless of root content", () => { + expect( + resolveInitialModelBrowserView({ + providers: [pi], + selectedProvider: "pi", + selectedModel: "pi-pro", + hasProfiles: true, + }), + ).toEqual({ kind: "provider", providerId: "pi", providerLabel: "Pi" }); + }); + + it("falls back to the root when the selected provider is gone", () => { + expect( + resolveInitialModelBrowserView({ + providers: [codex, pi], + selectedProvider: "gemini", + selectedModel: "gemini-3", + hasProfiles: false, }), ).toEqual({ kind: "all" }); }); }); + +describe("model browser all view", () => { + const claude = provider("claude", "Claude Code", [ + modelRow("claude", "Claude Code", "opus-5", "Opus 5"), + modelRow("claude", "Claude Code", "sonnet-4.6", "Sonnet 4.6"), + ]); + const copilot = provider("copilot", "Copilot", [ + modelRow("copilot", "Copilot", "claude-opus-5", "Opus 5"), + ]); + const codex = provider("codex", "Codex", [modelRow("codex", "Codex", "gpt-5.4", "GPT-5.4")]); + const providers = [claude, copilot, codex]; + + it("browses providers while the query is empty", () => { + expect(resolveModelBrowserAllView({ providers, normalizedQuery: "" })).toEqual({ + kind: "browse", + }); + }); + + it("ranks the same model label across every provider that offers it", () => { + const view = resolveModelBrowserAllView({ providers, normalizedQuery: "opus" }); + + expect(view.kind).toBe("searchResults"); + expect(view.kind === "searchResults" ? view.rows.map((row) => row.favoriteKey) : []).toEqual([ + "claude:opus-5", + "copilot:claude-opus-5", + ]); + }); + + it("matches models by their provider label", () => { + const view = resolveModelBrowserAllView({ providers, normalizedQuery: "codex" }); + + expect(view.kind === "searchResults" ? view.rows.map((row) => row.modelId) : []).toEqual([ + "gpt-5.4", + ]); + }); + + it("reports no matches instead of falling back to the provider list", () => { + expect(resolveModelBrowserAllView({ providers, normalizedQuery: "zzzz" })).toEqual({ + kind: "noSearchMatches", + }); + }); + + it("ignores providers that are still loading or errored", () => { + const loading: ProviderSelectorProvider = { + id: "opencode", + label: "OpenCode", + modelSelection: { kind: "loading" }, + }; + const failed: ProviderSelectorProvider = { + id: "pi", + label: "Pi", + modelSelection: { kind: "error", message: "unavailable" }, + }; + + expect( + resolveModelBrowserAllView({ providers: [loading, failed], normalizedQuery: "opus" }), + ).toEqual({ kind: "noSearchMatches" }); + }); +}); diff --git a/packages/app/src/components/model-browser-view.ts b/packages/app/src/components/model-browser-view.ts index 9452b956d6..75b1c75411 100644 --- a/packages/app/src/components/model-browser-view.ts +++ b/packages/app/src/components/model-browser-view.ts @@ -1,19 +1,48 @@ -import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection"; +import { + filterAndRankModelRows, + getAllProviderModelRows, + type ProviderSelectionModelRow, + type ProviderSelectorProvider, +} from "@/provider-selection/provider-selection"; export type ModelBrowserView = | { kind: "all" } | { kind: "provider"; providerId: string; providerLabel: string }; +/** What the root view shows: the provider drill-down, or ranked cross-provider results. */ +export type ModelBrowserAllView = + | { kind: "browse" } + | { kind: "searchResults"; rows: ProviderSelectionModelRow[] } + | { kind: "noSearchMatches" }; + +export function resolveModelBrowserAllView({ + providers, + normalizedQuery, +}: { + providers: ProviderSelectorProvider[]; + normalizedQuery: string; +}): ModelBrowserAllView { + if (!normalizedQuery) { + return { kind: "browse" }; + } + const rows = filterAndRankModelRows(getAllProviderModelRows(providers), normalizedQuery); + if (rows.length === 0) { + return { kind: "noSearchMatches" }; + } + return { kind: "searchResults", rows }; +} + +/** Where the picker lands when it opens. A sole provider skips the redundant root view. */ export function resolveInitialModelBrowserView({ providers, selectedProvider, selectedModel, - favoriteKeys, + hasProfiles, }: { providers: ProviderSelectorProvider[]; selectedProvider: string; selectedModel: string; - favoriteKeys: Set; + hasProfiles: boolean; }): ModelBrowserView { const singleProvider = providers.length === 1 ? providers[0] : undefined; if (singleProvider) { @@ -24,12 +53,11 @@ export function resolveInitialModelBrowserView({ }; } - const selectedFavoriteKey = `${selectedProvider}:${selectedModel}`; - const shouldOpenSelectedProvider = - selectedProvider.length > 0 && - selectedModel.length > 0 && - !favoriteKeys.has(selectedFavoriteKey); - if (shouldOpenSelectedProvider) { + if (hasProfiles) { + return { kind: "all" }; + } + + if (selectedProvider.length > 0 && selectedModel.length > 0) { const provider = providers.find((entry) => entry.id === selectedProvider); if (provider) { return { kind: "provider", providerId: provider.id, providerLabel: provider.label }; diff --git a/packages/app/src/components/model-browser.tsx b/packages/app/src/components/model-browser.tsx index e9c34b0ec8..49a37bfe09 100644 --- a/packages/app/src/components/model-browser.tsx +++ b/packages/app/src/components/model-browser.tsx @@ -16,15 +16,30 @@ import { import { BottomSheetFlatList } from "@gorhom/bottom-sheet"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; -import { AlertTriangle, Check, ChevronRight, Search, Settings, Star } from "lucide-react-native"; +import { + AlertTriangle, + Check, + ChevronRight, + Pencil, + Plus, + Search, + Settings, +} from "lucide-react-native"; import type { AgentProvider } from "@getpaseo/protocol/agent-types"; +import { + AgentProfileGlyph, + type AgentProfilePicker, + type AgentProfilePickerRow as AgentProfilePickerRowModel, +} from "@/agent-profiles"; import type { SheetHeader } from "@/components/adaptive-modal-sheet"; import { Button } from "@/components/ui/button"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { getProviderIcon } from "@/components/provider-icons"; import { useIsCompactFormFactor } from "@/constants/layout"; import { isNative, isWeb } from "@/constants/platform"; import { + buildProviderQualifiedDescription, buildSelectedTriggerLabel, filterAndRankModelRows, getAllProviderModelRows, @@ -38,6 +53,7 @@ import { useCurrentOverlayLayer } from "@/lib/overlay-root"; import { ICON_SIZE, type Theme } from "@/styles/theme"; import { resolveInitialModelBrowserView, + resolveModelBrowserAllView, type ModelBrowserView, } from "@/components/model-browser-view"; @@ -50,9 +66,10 @@ const ThemedAlertTriangle = withUnistyles(AlertTriangle); const ThemedCheck = withUnistyles(Check); const ThemedChevronRight = withUnistyles(ChevronRight); const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); +const ThemedPencil = withUnistyles(Pencil); +const ThemedPlus = withUnistyles(Plus); const ThemedSearch = withUnistyles(Search); const ThemedSettings = withUnistyles(Settings); -const ThemedStar = withUnistyles(Star); function ProviderSettingsAction({ accessibilityLabel, @@ -84,6 +101,29 @@ function ProviderSettingsAction({ ); } +function AgentProfilesEditAction({ onPress }: { onPress: () => void }) { + const { t } = useTranslation(); + return ( + + + + + + + + {t("modelSelector.editProfilesLabel")} + + + ); +} + const IndependentScrollGestureContext = createContext | null>( null, ); @@ -92,29 +132,21 @@ const foregroundMutedMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted, }); +const foregroundExtraMutedMapping = (theme: Theme) => ({ + color: theme.colors.foregroundExtraMuted, +}); + const headerSettingsMapping = (disabled: boolean) => (theme: Theme) => ({ color: disabled ? theme.colors.border : theme.colors.foregroundMuted, }); -const favoriteStarMapping = - (isFavorite: boolean, hovered: boolean) => - (theme: Theme): { color: string; fill: string } => { - const favoriteColor = theme.colors.palette.amber[500]; - if (isFavorite) { - return { color: favoriteColor, fill: favoriteColor }; - } - return { - color: hovered ? theme.colors.foregroundMuted : theme.colors.border, - fill: "transparent", - }; - }; - interface ModelBrowserInput { providers: ProviderSelectorProvider[]; selectedProvider: string; selectedModel: string; isLoading: boolean; - favoriteKeys: Set; + /** Pinned above the provider list on the root view. `null` hides the section. */ + profiles?: AgentProfilePicker | null; serverId?: string | null; } @@ -122,7 +154,7 @@ export interface ModelBrowserState { providers: ProviderSelectorProvider[]; selectedProvider: string; selectedModel: string; - favoriteKeys: Set; + profiles: AgentProfilePicker | null; view: ModelBrowserView; searchQuery: string; header: SheetHeader; @@ -138,7 +170,9 @@ export interface ModelBrowserState { interface ModelBrowserProps { state: ModelBrowserState; onSelect: (provider: string, modelId: string) => void; - onToggleFavorite?: (provider: string, modelId: string) => void; + /** Applying a profile resolves the pick and dismisses, exactly like a model row. */ + onApplyProfile?: (profileId: string) => void; + onEditProfiles?: () => void; onRetryProvider?: (provider: AgentProvider) => void; isRetryingProvider?: boolean; scrolling?: "sheet" | "independent"; @@ -150,7 +184,7 @@ interface ModelBrowserContentProps extends Omit; + profiles: AgentProfilePicker | null; onDrillDown: (providerId: string, providerLabel: string) => void; scrolling: "sheet" | "independent"; } @@ -177,22 +211,6 @@ function HeaderSettingsIcon({ disabled }: { disabled: boolean }) { return ; } -function FavoriteStar({ isFavorite, hovered }: { isFavorite: boolean; hovered: boolean }) { - const uniProps = useMemo(() => favoriteStarMapping(isFavorite, hovered), [hovered, isFavorite]); - return ; -} - -function favoriteButtonStyle({ - hovered, - pressed, -}: PressableStateCallbackType & { hovered?: boolean }) { - return [ - styles.favoriteButton, - Boolean(hovered) && styles.favoriteButtonHovered, - pressed && styles.favoriteButtonPressed, - ]; -} - function iconButtonStyle({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) { return [ styles.rowIconButton, @@ -201,18 +219,19 @@ function iconButtonStyle({ hovered, pressed }: PressableStateCallbackType & { ho ]; } +function countModelsInView(view: ModelBrowserView, providers: ProviderSelectorProvider[]): number { + if (view.kind === "all") { + return getAllProviderModelRows(providers).length; + } + const provider = providers.find((entry) => entry.id === view.providerId); + return provider?.modelSelection.kind === "models" ? getProviderModelRows(provider).length : 0; +} + function resolveDesktopFixedHeight( view: ModelBrowserView, providers: ProviderSelectorProvider[], -): number | undefined { - if (view.kind !== "provider") { - return undefined; - } - const provider = providers.find((entry) => entry.id === view.providerId); - if (!provider || provider.modelSelection.kind !== "models") { - return DESKTOP_PROVIDER_VIEW_MIN_HEIGHT; - } - const modelCount = getProviderModelRows(provider).length; +): number { + const modelCount = countModelsInView(view, providers); return Math.min( Math.max( DESKTOP_PROVIDER_VIEW_MIN_HEIGHT, @@ -227,13 +246,14 @@ export function useModelBrowser({ selectedProvider, selectedModel, isLoading, - favoriteKeys, + profiles = null, serverId = null, }: ModelBrowserInput): ModelBrowserState { const { t } = useTranslation(); const [view, setView] = useState({ kind: "all" }); const [searchQuery, setSearchQuery] = useState(""); const [searchResetKey, bumpSearchResetKey] = useReducer((key: number) => key + 1, 0); + const hasProfiles = (profiles?.rows.length ?? 0) > 0; const initialView = useMemo( () => @@ -241,9 +261,9 @@ export function useModelBrowser({ providers, selectedProvider, selectedModel, - favoriteKeys, + hasProfiles, }), - [favoriteKeys, providers, selectedModel, selectedProvider], + [hasProfiles, providers, selectedModel, selectedProvider], ); const prepareToOpen = useCallback(() => { @@ -260,9 +280,13 @@ export function useModelBrowser({ reset(); }, [reset]); - const drillDown = useCallback((providerId: string, providerLabel: string) => { - setView({ kind: "provider", providerId, providerLabel }); - }, []); + const drillDown = useCallback( + (providerId: string, providerLabel: string) => { + setView({ kind: "provider", providerId, providerLabel }); + reset(); + }, + [reset], + ); const handleSearchQueryChange = useCallback((value: string) => { setSearchQuery(value); @@ -271,7 +295,16 @@ export function useModelBrowser({ const singleProviderView = providers.length === 1; const header = useMemo(() => { if (view.kind === "all") { - return { title: t("modelSelector.title") }; + return { + title: t("modelSelector.title"), + search: { + onChange: handleSearchQueryChange, + resetKey: `all:${searchResetKey}`, + placeholder: t("modelSelector.searchAllPlaceholder"), + autoFocus: isWeb, + testID: "model-search-all-input", + }, + }; } return { title: view.providerLabel, @@ -280,13 +313,15 @@ export function useModelBrowser({ ), back: singleProviderView ? undefined : { onPress: handleBackToAll }, actions: ( - + + + ), search: { onChange: handleSearchQueryChange, @@ -333,7 +368,7 @@ export function useModelBrowser({ providers, selectedProvider, selectedModel, - favoriteKeys, + profiles, view, searchQuery, header, @@ -351,22 +386,6 @@ function normalizeSearchQuery(value: string): string { return value.trim().toLowerCase(); } -function sortFavoritesFirst( - rows: ProviderSelectionModelRow[], - favoriteKeys: Set, -): ProviderSelectionModelRow[] { - const favorites: ProviderSelectionModelRow[] = []; - const rest: ProviderSelectionModelRow[] = []; - for (const row of rows) { - if (favoriteKeys.has(row.favoriteKey)) { - favorites.push(row); - } else { - rest.push(row); - } - } - return [...favorites, ...rest]; -} - interface ModelBrowserPressableProps { children: React.ReactNode | ((state: PressableStateCallbackType) => React.ReactNode); style?: @@ -375,6 +394,8 @@ interface ModelBrowserPressableProps { onPress: () => void; hitSlop?: number; accessibilityLabel?: string; + /** Only rows that can express selection pass this; the rest stay unannotated. */ + accessibilitySelected?: boolean; testID?: string; } @@ -384,6 +405,7 @@ function ModelBrowserPressable({ onPress, hitSlop, accessibilityLabel, + accessibilitySelected, testID, }: ModelBrowserPressableProps) { const independentScrollGesture = useContext(IndependentScrollGestureContext); @@ -420,6 +442,10 @@ function ModelBrowserPressable({ }, [onPress], ); + const accessibilityState = useMemo( + () => (accessibilitySelected === undefined ? undefined : { selected: accessibilitySelected }), + [accessibilitySelected], + ); if (!independentScrollGesture) { return ( @@ -429,6 +455,8 @@ function ModelBrowserPressable({ style={style} accessibilityRole="button" accessibilityLabel={accessibilityLabel} + accessibilityState={accessibilityState} + aria-selected={accessibilitySelected} testID={testID} > {children} @@ -445,6 +473,8 @@ function ModelBrowserPressable({ accessible accessibilityRole="button" accessibilityLabel={accessibilityLabel} + accessibilityState={accessibilityState} + aria-selected={accessibilitySelected} accessibilityActions={[{ name: "activate" }]} onAccessibilityAction={handleAccessibilityAction} style={resolvedStyle} @@ -466,6 +496,7 @@ function ModelBrowserRow({ selected = false, selectionIndicator = false, tone = "default", + labelMuted = false, spacing = "model", onPress, testID, @@ -477,6 +508,8 @@ function ModelBrowserRow({ selected?: boolean; selectionIndicator?: boolean; tone?: ModelBrowserRowTone; + /** For rows that offer an action rather than name a thing you can pick. */ + labelMuted?: boolean; spacing?: "model" | "provider"; onPress: () => void; testID?: string; @@ -498,11 +531,21 @@ function ModelBrowserRow({ const hasTrailing = selected || trailingSlot; return ( - + {leadingSlot} - + {label} {description ? ( @@ -531,55 +574,30 @@ function ModelBrowserRow({ function ModelRow({ row, isSelected, - isFavorite, - elevated = false, + showProviderLabel = false, onPress, - onToggleFavorite, }: { row: ProviderSelectionModelRow; isSelected: boolean; - isFavorite: boolean; - elevated?: boolean; + showProviderLabel?: boolean; onPress: () => void; - onToggleFavorite?: (provider: string, modelId: string) => void; }) { - const { t } = useTranslation(); - const handleToggleFavorite = useCallback( - () => onToggleFavorite?.(row.provider, row.modelId), - [onToggleFavorite, row.modelId, row.provider], - ); const leadingSlot = useMemo( () => , [row.provider], ); - const trailingSlot = useMemo( - () => - onToggleFavorite ? ( - - {({ hovered }) => } - - ) : null, - [handleToggleFavorite, isFavorite, onToggleFavorite, row.modelId, row.provider, t], - ); + + const description = showProviderLabel ? buildProviderQualifiedDescription(row) : row.description; return ( ); } @@ -587,17 +605,13 @@ function ModelRow({ function SelectableModelRow({ row, isSelected, - isFavorite, - elevated, + showProviderLabel, onSelect, - onToggleFavorite, }: { row: ProviderSelectionModelRow; isSelected: boolean; - isFavorite: boolean; - elevated?: boolean; + showProviderLabel?: boolean; onSelect: (provider: string, modelId: string) => void; - onToggleFavorite?: (provider: string, modelId: string) => void; }) { const handlePress = useCallback(() => { onSelect(row.provider, row.modelId); @@ -606,51 +620,114 @@ function SelectableModelRow({ ); } -function FavoritesSection({ - favoriteRows, - selectedProvider, - selectedModel, - favoriteKeys, - onSelect, - onToggleFavorite, +function AgentProfilePickerRowView({ + row, + onApply, }: { - favoriteRows: ProviderSelectionModelRow[]; - selectedProvider: string; - selectedModel: string; - favoriteKeys: Set; - onSelect: (provider: string, modelId: string) => void; - onToggleFavorite?: (provider: string, modelId: string) => void; + row: AgentProfilePickerRowModel; + onApply: (profileId: string) => void; +}) { + const handlePress = useCallback(() => onApply(row.id), [onApply, row.id]); + const leadingSlot = useMemo( + () => , + [row.color, row.icon], + ); + return ( + + ); +} + +/** + * Pinned above the provider list. Rows are actions, not selections: applying a + * profile writes its values into the composer and nothing stays bound to it, so + * there is no checkmark and no active row to show. + */ +function AgentProfilesPickerSection({ + rows, + onApplyProfile, + onEditProfiles, +}: { + rows: AgentProfilePickerRowModel[]; + onApplyProfile?: (profileId: string) => void; + onEditProfiles?: () => void; }) { const { t } = useTranslation(); - if (favoriteRows.length === 0) return null; + const handleApply = useCallback( + (profileId: string) => onApplyProfile?.(profileId), + [onApplyProfile], + ); return ( - + - {t("modelSelector.favorites")} + {t("modelSelector.profiles")} + {onEditProfiles ? ( + + + + ) : null} - {favoriteRows.map((row) => ( - + {rows.map((row) => ( + ))} ); } +function CreateAgentProfileRow({ onPress }: { onPress: () => void }) { + const { t } = useTranslation(); + const leadingSlot = useMemo( + () => ( + + + + ), + [], + ); + return ( + + ); +} + +function AgentProfilesPickerContent({ + rows, + onApplyProfile, + onEditProfiles, +}: { + rows: AgentProfilePickerRowModel[]; + onApplyProfile?: (profileId: string) => void; + onEditProfiles?: () => void; +}) { + if (rows.length === 0) { + return onEditProfiles ? : null; + } + return ( + + ); +} + function GroupProviderButton({ provider, onDrillDown, @@ -764,15 +841,18 @@ function IndependentScrollBoundary({ children }: { children: React.ReactElement function IndependentModelList({ rows, renderItem, + header, }: { rows: ProviderSelectionModelRow[]; renderItem: ({ item }: { item: ProviderSelectionModelRow }) => React.ReactElement; + header?: React.ReactElement; }) { return ( ; onSelect: (provider: string, modelId: string) => void; - onToggleFavorite?: (provider: string, modelId: string) => void; - normalizedQuery: string; + showProviderLabel?: boolean; + header?: React.ReactElement; scrolling: "sheet" | "independent"; }) { const isCompact = useIsCompactFormFactor(); - const displayRows = useMemo( - () => (normalizedQuery ? rows : sortFavoritesFirst(rows, favoriteKeys)), - [favoriteKeys, normalizedQuery, rows], - ); const renderItem = useCallback( ({ item }: { item: ProviderSelectionModelRow }) => ( ), - [favoriteKeys, onSelect, onToggleFavorite, selectedModel, selectedProvider], + [onSelect, selectedModel, selectedProvider, showProviderLabel], ); const keyExtractor = useCallback((row: ProviderSelectionModelRow) => row.favoriteKey, []); if (scrolling === "independent") { - return ; + return ; } if (isCompact && isNative) { return ( - {displayRows.map((row) => ( + {header} + {rows.map((row) => ( {renderItem({ item: row })} ))} @@ -902,15 +977,111 @@ function ProviderErrorEmptyState({ ); } +function ModelSearchEmptyState() { + const { t } = useTranslation(); + return ( + + + {t("modelSelector.noMatches")} + + ); +} + +function ProviderModelBrowserContent({ + view, + provider, + profiles, + selectedProvider, + selectedModel, + normalizedQuery, + onSelect, + onApplyProfile, + onEditProfiles, + onRetryProvider, + isRetryingProvider, + scrolling, +}: { + view: Extract; + provider: ProviderSelectorProvider | null; + profiles: AgentProfilePicker | null; + selectedProvider: string; + selectedModel: string; + normalizedQuery: string; + onSelect: (provider: string, modelId: string) => void; + onApplyProfile?: (profileId: string) => void; + onEditProfiles?: () => void; + onRetryProvider?: (provider: AgentProvider) => void; + isRetryingProvider: boolean; + scrolling: "sheet" | "independent"; +}) { + const { t } = useTranslation(); + const visibleRows = useMemo( + () => (provider ? filterAndRankModelRows(getProviderModelRows(provider), normalizedQuery) : []), + [normalizedQuery, provider], + ); + const providerProfileRows = useMemo( + () => profiles?.rows.filter((row) => row.provider === view.providerId) ?? [], + [profiles, view.providerId], + ); + const profileHeader = useMemo( + () => + normalizedQuery.length === 0 && profiles ? ( + + ) : undefined, + [normalizedQuery, onApplyProfile, onEditProfiles, profiles, providerProfileRows], + ); + + if (!provider) return ; + const selection = provider.modelSelection; + if (selection.kind === "loading") { + return ( + + + + + {t("modelSelector.loadingShort")} + + ); + } + if (selection.kind === "error") { + return ( + + ); + } + if (visibleRows.length === 0) { + return profileHeader ?? ; + } + return ( + + ); +} + function ModelBrowserContent({ view, providers, selectedProvider, selectedModel, searchQuery, - favoriteKeys, + profiles, onSelect, - onToggleFavorite, + onApplyProfile, + onEditProfiles, onDrillDown, onRetryProvider, isRetryingProvider = false, @@ -921,62 +1092,54 @@ function ModelBrowserContent({ const selectedViewProvider = useMemo( () => view.kind === "provider" - ? providers.find((provider) => provider.id === view.providerId) + ? (providers.find((provider) => provider.id === view.providerId) ?? null) : null, [providers, view], ); - const visibleRows = useMemo( - () => - selectedViewProvider - ? filterAndRankModelRows(getProviderModelRows(selectedViewProvider), normalizedQuery) - : [], - [normalizedQuery, selectedViewProvider], - ); - const favoriteRows = useMemo( - () => getAllProviderModelRows(providers).filter((row) => favoriteKeys.has(row.favoriteKey)), - [favoriteKeys, providers], - ); - const hasResults = favoriteRows.length > 0 || providers.length > 0; - const emptyState = ( - - - {t("modelSelector.noMatches")} - + const allView = useMemo( + () => resolveModelBrowserAllView({ providers, normalizedQuery }), + [normalizedQuery, providers], ); + const hasResults = profiles !== null || providers.length > 0; if (view.kind === "provider") { - if (!selectedViewProvider) return emptyState; - const selection = selectedViewProvider.modelSelection; - if (selection.kind === "loading") { - return ( - - - - - {t("modelSelector.loadingShort")} - - ); - } - if (selection.kind === "error") { - return ( - - ); - } - if (visibleRows.length === 0) return emptyState; return ( - + ); + } + + if (allView.kind === "noSearchMatches") { + return ( + + + + {t("modelSelector.noMatchesForQuery", { query: searchQuery.trim() })} + + + ); + } + + if (allView.kind === "searchResults") { + return ( + ); @@ -984,18 +1147,24 @@ function ModelBrowserContent({ const allProvidersContent = ( - - {providers.length > 0 ? ( - + {profiles ? ( + ) : null} - {!hasResults ? emptyState : null} + {providers.length > 0 ? ( + + {profiles ? ( + + {t("modelSelector.providers")} + + ) : null} + + + ) : null} + {!hasResults ? : null} ); @@ -1009,7 +1178,8 @@ function ModelBrowserContent({ export function ModelBrowser({ state, onSelect, - onToggleFavorite, + onApplyProfile, + onEditProfiles, onRetryProvider, isRetryingProvider = false, scrolling = "sheet", @@ -1021,9 +1191,10 @@ export function ModelBrowser({ selectedProvider={state.selectedProvider} selectedModel={state.selectedModel} searchQuery={state.searchQuery} - favoriteKeys={state.favoriteKeys} + profiles={state.profiles} onSelect={onSelect} - onToggleFavorite={onToggleFavorite} + onApplyProfile={onApplyProfile} + onEditProfiles={onEditProfiles} onDrillDown={state.drillDown} onRetryProvider={onRetryProvider} isRetryingProvider={isRetryingProvider} @@ -1033,7 +1204,12 @@ export function ModelBrowser({ } const styles = StyleSheet.create((theme) => ({ - favoritesContainer: { + headerActionRow: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[1], + }, + profilesContainer: { backgroundColor: theme.colors.surface1, borderBottomWidth: 1, borderBottomColor: theme.colors.border, @@ -1043,6 +1219,7 @@ const styles = StyleSheet.create((theme) => ({ backgroundColor: theme.colors.border, }, sectionHeading: { + position: "relative", flexDirection: "row", alignItems: "center", gap: theme.spacing[2], @@ -1050,7 +1227,13 @@ const styles = StyleSheet.create((theme) => ({ paddingTop: theme.spacing[2], paddingBottom: theme.spacing[1], }, + sectionHeadingAction: { + position: "absolute", + top: theme.spacing[1], + right: isWeb ? theme.spacing[3] : theme.spacing[6], + }, sectionHeadingText: { + flex: 1, fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.normal, color: theme.colors.foregroundMuted, @@ -1099,6 +1282,11 @@ const styles = StyleSheet.create((theme) => ({ color: theme.colors.foreground, flexShrink: 0, }, + browserRowLabelMuted: { + fontSize: theme.fontSize.sm, + color: theme.colors.foregroundMuted, + flexShrink: 0, + }, browserRowDescription: { fontSize: theme.fontSize.xs, color: theme.colors.foregroundMuted, @@ -1156,6 +1344,10 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted, }, + tooltipText: { + fontSize: theme.fontSize.xs, + color: theme.colors.foreground, + }, virtualizedModelList: { flex: 1, }, @@ -1166,19 +1358,6 @@ const styles = StyleSheet.create((theme) => ({ virtualizedProviderListContent: { paddingTop: 0, }, - favoriteButton: { - width: 24, - height: 24, - borderRadius: theme.borderRadius.full, - alignItems: "center", - justifyContent: "center", - }, - favoriteButtonHovered: { - backgroundColor: theme.colors.surface2, - }, - favoriteButtonPressed: { - backgroundColor: theme.colors.surface1, - }, providerIconMuted: { color: theme.colors.foregroundMuted, }, diff --git a/packages/app/src/components/project-edit-sheet.tsx b/packages/app/src/components/project-edit-sheet.tsx index 65801dee2f..a2790cb432 100644 --- a/packages/app/src/components/project-edit-sheet.tsx +++ b/packages/app/src/components/project-edit-sheet.tsx @@ -40,7 +40,6 @@ export interface ProjectEditSheetProps { export function ProjectEditSheet({ visible, onClose, - serverId, projectId, projectViewKey, client, @@ -60,7 +59,6 @@ export function ProjectEditSheet({ submitProjectEdit({ client, projectId, submission }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["projects"] }); - queryClient.invalidateQueries({ queryKey: ["projectIcon", serverId] }); toast.show(t("settings.project.edit.savedToast"), { variant: "success" }); onClose(); }, diff --git a/packages/app/src/components/schedules/schedule-row.tsx b/packages/app/src/components/schedules/schedule-row.tsx index faf491a35d..8c2e50ba81 100644 --- a/packages/app/src/components/schedules/schedule-row.tsx +++ b/packages/app/src/components/schedules/schedule-row.tsx @@ -384,12 +384,12 @@ const styles = StyleSheet.create((theme) => ({ flex: 1, minWidth: 0, flexDirection: "row", - alignItems: "center", + alignItems: "flex-start", gap: theme.spacing[3], }, leading: { width: PROVIDER_ICON_SIZE, - height: PROVIDER_ICON_SIZE, + height: 20, alignItems: "center", justifyContent: "center", }, diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index a6f0904191..5988106bba 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -33,6 +33,7 @@ import { } from "@/stores/navigation-active-workspace-store"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import type { Theme } from "@/styles/theme"; +import type { SidebarSurfaceBackdrop } from "@/styles/surface-backdrop"; import { getSidebarRowBackdrop } from "@/components/sidebar/sidebar-row-backdrop"; import { type GestureType } from "react-native-gesture-handler"; import * as Clipboard from "expo-clipboard"; @@ -616,6 +617,7 @@ function ProjectMenuItems({ function WorkspaceRowRightGroup({ workspace, + backdrop, isHovered, isTouchPlatform, isCreating, @@ -634,6 +636,7 @@ function WorkspaceRowRightGroup({ onTogglePin, }: { workspace: SidebarWorkspaceEntry; + backdrop: SidebarSurfaceBackdrop; isHovered: boolean; isTouchPlatform: boolean; isCreating: boolean; @@ -681,7 +684,10 @@ function WorkspaceRowRightGroup({ - + {onArchive ? ( state.getPinnedWorkspaceOrder); + const setPinnedWorkspaceOrder = useSidebarOrderStore((state) => state.setPinnedWorkspaceOrder); + const handlePinnedWorkspaceReorder = useCallback( + (reorderedWorkspaces: SidebarWorkspacePlacement[]) => { + const reorderedWorkspaceKeys = reorderedWorkspaces.map((workspace) => workspace.workspaceKey); + const currentOrder = getPinnedWorkspaceOrder(); + if ( + !hasVisibleOrderChanged({ + currentOrder, + reorderedVisibleKeys: reorderedWorkspaceKeys, + }) + ) { + return; + } + + setPinnedWorkspaceOrder( + mergeWithRemainder({ + currentOrder, + reorderedVisibleKeys: reorderedWorkspaceKeys, + }), + ); + }, + [getPinnedWorkspaceOrder, setPinnedWorkspaceOrder], + ); // Status mode drops the project grouping, so its rows carry their own project // icon. Project mode fetches the same icons inside ProjectModeList for its // project headers, so only the active mode requests them. @@ -1964,7 +1996,10 @@ export function SidebarWorkspaceList({ hostBadgeByServerId={hostBadgeByServerId} supportsPinningByServerId={supportsPinningByServerId} onToggleWorkspacePin={onToggleWorkspacePin} + onPinnedWorkspaceReorder={handlePinnedWorkspaceReorder} listHeaderComponent={listHeaderComponent} + parentGestureRef={parentGestureRef} + dragGestureHostPresented={dragGestureHostPresented} /> ) : ( ); @@ -2001,7 +2037,10 @@ function SidebarStatusModeWrapper({ hostBadgeByServerId, supportsPinningByServerId, onToggleWorkspacePin, + onPinnedWorkspaceReorder, listHeaderComponent, + parentGestureRef, + dragGestureHostPresented, }: { statusGroups: StatusGroup[]; pinnedGroups: PinnedSidebarGroups; @@ -2012,17 +2051,25 @@ function SidebarStatusModeWrapper({ hostBadgeByServerId: ReadonlyMap; supportsPinningByServerId: ReadonlyMap; onToggleWorkspacePin: ToggleSidebarWorkspacePin; + onPinnedWorkspaceReorder: (workspaces: SidebarWorkspacePlacement[]) => void; listHeaderComponent?: ReactElement | null; + parentGestureRef?: MutableRefObject; + dragGestureHostPresented?: boolean; }) { const showShortcutBadges = useShowShortcutBadges(); + const pinnedWorkspaces = useMemo( + () => + pinnedGroups.pinnedChats.flatMap((workspace) => { + const entry = workspaceEntriesByKey.get(workspace.workspaceKey); + return entry ? [entry] : []; + }), + [pinnedGroups.pinnedChats, workspaceEntriesByKey], + ); return ( { - const entry = workspaceEntriesByKey.get(workspace.workspaceKey); - return entry ? [entry] : []; - })} + pinnedWorkspaces={pinnedWorkspaces} projectIconByProjectViewKey={projectIconByProjectViewKey} shortcutIndexByWorkspaceKey={_projectShortcutIndex} showShortcutBadges={showShortcutBadges} @@ -2030,7 +2077,10 @@ function SidebarStatusModeWrapper({ hostBadgeByServerId={hostBadgeByServerId} supportsPinningByServerId={supportsPinningByServerId} onToggleWorkspacePin={onToggleWorkspacePin} + onPinnedWorkspaceReorder={onPinnedWorkspaceReorder} listHeaderComponent={listHeaderComponent} + parentGestureRef={parentGestureRef} + dragGestureHostPresented={dragGestureHostPresented} /> ); } @@ -2053,12 +2103,14 @@ function ProjectModeList({ supportsMultiplicityByServerId, supportsPinningByServerId, onToggleWorkspacePin, + onPinnedWorkspaceReorder, }: Omit & { pathname: string; hostBadgeByServerId: ReadonlyMap; supportsMultiplicityByServerId: ReadonlyMap; supportsPinningByServerId: ReadonlyMap; onToggleWorkspacePin: ToggleSidebarWorkspacePin; + onPinnedWorkspaceReorder: (workspaces: SidebarWorkspacePlacement[]) => void; }) { const hasActiveHostFilter = useSidebarViewStore((state) => state.hostFilters.length > 0); const { t } = useTranslation(); @@ -2296,10 +2348,14 @@ function ProjectModeList({ ); const renderPinnedChat = useCallback( - (workspace: SidebarWorkspacePlacement) => { + ({ + item: workspace, + drag, + isActive, + dragHandleProps, + }: DraggableRenderItemInfo) => { return ( ); }, @@ -2341,7 +2400,20 @@ function ProjectModeList({ {pinnedCollapsed ? null : ( <> - {visiblePinnedChats.map(renderPinnedChat)} + {canTogglePinnedChats ? ( state.collapsedStatusGroupKeys, ); const pinnedCollapsed = useSidebarCollapsedSectionsStore((state) => state.collapsedPinned); + const pinnedWorkspaceOrder = useSidebarOrderStore((state) => state.pinnedWorkspaceOrder); const toggleProjectCollapsed = useSidebarCollapsedSectionsStore( (state) => state.toggleProjectCollapsed, ); @@ -58,6 +60,7 @@ export function SidebarModelProvider({ buildSidebarProjection({ projects: list.projects, pinnedKeys, + pinnedWorkspaceOrder, workspaceEntriesByKey: projectionWorkspaceEntriesByKey, projectNamesByViewKey: list.projectNamesByViewKey, groupMode, @@ -73,6 +76,7 @@ export function SidebarModelProvider({ list.projects, pinnedCollapsed, pinnedKeys, + pinnedWorkspaceOrder, projectionWorkspaceEntriesByKey, ], ); diff --git a/packages/app/src/components/sidebar/sidebar-projection.test.ts b/packages/app/src/components/sidebar/sidebar-projection.test.ts index b5d15d2451..5eb82a9a83 100644 --- a/packages/app/src/components/sidebar/sidebar-projection.test.ts +++ b/packages/app/src/components/sidebar/sidebar-projection.test.ts @@ -66,6 +66,7 @@ function projectionInput(options?: { pinnedWorkspaceKeys: [pinned.placement.workspaceKey], pinnedAtByKey: { [pinned.placement.workspaceKey]: "2026-07-12T12:00:00.000Z" }, }, + pinnedWorkspaceOrder: [], workspaceEntriesByKey: new Map([ [pinned.entry.workspaceKey, pinned.entry], [unpinned.entry.workspaceKey, unpinned.entry], diff --git a/packages/app/src/components/sidebar/sidebar-projection.ts b/packages/app/src/components/sidebar/sidebar-projection.ts index be96f55979..d6c0c0e3a5 100644 --- a/packages/app/src/components/sidebar/sidebar-projection.ts +++ b/packages/app/src/components/sidebar/sidebar-projection.ts @@ -24,6 +24,7 @@ export interface SidebarProjection { export function buildSidebarProjection(input: { projects: SidebarProjectEntry[]; pinnedKeys: PinnedSidebarKeys; + pinnedWorkspaceOrder: string[]; workspaceEntriesByKey: ReadonlyMap; projectNamesByViewKey: Map; groupMode: SidebarGroupMode; @@ -34,6 +35,7 @@ export function buildSidebarProjection(input: { const pinnedGroups = splitPinnedSidebarGroups({ projects: input.projects, keys: input.pinnedKeys, + pinnedWorkspaceOrder: input.pinnedWorkspaceOrder, }); const pinnedWorkspaceKeys = new Set(input.pinnedKeys.pinnedWorkspaceKeys); const statusGroups = diff --git a/packages/app/src/components/sidebar/sidebar-status-list.tsx b/packages/app/src/components/sidebar/sidebar-status-list.tsx index c1152ddc65..3d463ba058 100644 --- a/packages/app/src/components/sidebar/sidebar-status-list.tsx +++ b/packages/app/src/components/sidebar/sidebar-status-list.tsx @@ -1,7 +1,23 @@ -import { memo, useCallback, useMemo, useState, type ReactNode } from "react"; +import { + memo, + useCallback, + useMemo, + useState, + type MutableRefObject, + type ReactNode, + type Ref, +} from "react"; import { useTranslation } from "react-i18next"; -import { View, Text, Pressable, ScrollView, type PressableStateCallbackType } from "react-native"; +import { + View, + Text, + Pressable, + ScrollView, + type GestureResponderEvent, + type PressableStateCallbackType, +} from "react-native"; import { NestableScrollContainer } from "react-native-draggable-flatlist"; +import type { GestureType } from "react-native-gesture-handler"; import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store"; import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store"; import { type SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list"; @@ -10,6 +26,7 @@ import type { HostBadgeModel } from "@/hosts/appearance"; import { isWeb as platformIsWeb, isNative as platformIsNative } from "@/constants/platform"; import { StyleSheet } from "react-native-unistyles"; import type { Theme } from "@/styles/theme"; +import type { SidebarSurfaceBackdrop } from "@/styles/surface-backdrop"; import { withUnistyles } from "react-native-unistyles"; import { ChevronDown, @@ -59,6 +76,9 @@ import { PinnedSectionHeader } from "@/components/sidebar/pinned-section-header" import { SidebarGroupToggleRow } from "@/components/sidebar/sidebar-group-toggle-row"; import { useLimitedSidebarGroup } from "@/components/sidebar/use-limited-sidebar-group"; import type { ToggleSidebarWorkspacePin } from "@/hooks/use-sidebar-workspace-pin"; +import { DraggableList, type DraggableRenderItemInfo } from "@/components/draggable-list"; +import type { DraggableListDragHandleProps } from "@/components/draggable-list.types"; +import { useLongPressDragInteraction } from "@/components/sidebar/use-long-press-drag-interaction"; // Themed icon wrappers const foregroundMutedColorMapping = (theme: Theme) => ({ @@ -85,6 +105,12 @@ const ThemedCircleAlert = withUnistyles(CircleAlert); const ThemedCircleCheck = withUnistyles(CircleCheck); const ThemedCircleDot = withUnistyles(CircleDot); const ThemedCircleX = withUnistyles(CircleX); +const EMPTY_SHORTCUT_INDEX = new Map(); + +function statusWorkspaceKeyExtractor(workspace: SidebarWorkspaceEntry): string { + return workspace.workspaceKey; +} + interface StatusWorkspaceListProps { groups: StatusGroup[]; pinnedWorkspaces: SidebarWorkspaceEntry[]; @@ -95,7 +121,10 @@ interface StatusWorkspaceListProps { hostBadgeByServerId: ReadonlyMap; supportsPinningByServerId: ReadonlyMap; onToggleWorkspacePin: ToggleSidebarWorkspacePin; + onPinnedWorkspaceReorder: (workspaces: SidebarWorkspaceEntry[]) => void; listHeaderComponent?: ReactNode; + parentGestureRef?: MutableRefObject; + dragGestureHostPresented?: boolean; } export function SidebarStatusWorkspaceList({ @@ -108,7 +137,10 @@ export function SidebarStatusWorkspaceList({ hostBadgeByServerId, supportsPinningByServerId, onToggleWorkspacePin, + onPinnedWorkspaceReorder, listHeaderComponent, + parentGestureRef, + dragGestureHostPresented, }: StatusWorkspaceListProps) { const collapsedStatusGroupKeys = useSidebarCollapsedSectionsStore( (state) => state.collapsedStatusGroupKeys, @@ -124,7 +156,44 @@ export function SidebarStatusWorkspaceList({ toggleExpanded: togglePinnedWorkspacesExpanded, } = useLimitedSidebarGroup(pinnedWorkspaces); - const statusShortcutIndex = showShortcutBadges ? shortcutIndexByWorkspaceKey : new Map(); + const statusShortcutIndex = showShortcutBadges + ? shortcutIndexByWorkspaceKey + : EMPTY_SHORTCUT_INDEX; + const renderPinnedWorkspace = useCallback( + ({ + item: workspace, + drag, + isActive, + dragHandleProps, + }: DraggableRenderItemInfo) => ( + + ), + [ + hostBadgeByServerId, + onToggleWorkspacePin, + onWorkspacePress, + projectIconByProjectViewKey, + showShortcutBadges, + statusShortcutIndex, + supportsPinningByServerId, + ], + ); const content = ( <> {pinnedWorkspaces.length > 0 ? ( @@ -132,23 +201,18 @@ export function SidebarStatusWorkspaceList({ {pinnedCollapsed ? null : ( <> - {visiblePinnedWorkspaces.map((workspace) => ( - - ))} + {canTogglePinnedWorkspaces ? ( void; + drag?: () => void; + isDragging?: boolean; + dragHandleProps?: DraggableListDragHandleProps; }) { const activeWorkspaceSelection = useActiveWorkspaceSelection(); const selected = @@ -465,6 +535,9 @@ const StatusWorkspaceRow = memo(function StatusWorkspaceRow({ reserveIdleStatusIndicatorSpace={reserveIdleStatusIndicatorSpace} inStatusGroup={inStatusGroup} onPress={handlePress} + drag={drag} + isDragging={isDragging} + dragHandleProps={dragHandleProps} /> ); }); @@ -482,6 +555,9 @@ function StatusWorkspaceRowWithMenu({ reserveIdleStatusIndicatorSpace = true, inStatusGroup = true, onPress, + drag, + isDragging = false, + dragHandleProps, }: { workspace: SidebarWorkspaceEntry; hostBadge: HostBadgeModel | null; @@ -499,6 +575,9 @@ function StatusWorkspaceRowWithMenu({ */ inStatusGroup?: boolean; onPress: () => void; + drag?: () => void; + isDragging?: boolean; + dragHandleProps?: DraggableListDragHandleProps; }) { const { t } = useTranslation(); const toast = useToast(); @@ -620,6 +699,9 @@ function StatusWorkspaceRowWithMenu({ onTogglePin={onTogglePin} reserveIdleStatusIndicatorSpace={reserveIdleStatusIndicatorSpace} inStatusGroup={inStatusGroup} + drag={drag} + isDragging={isDragging} + dragHandleProps={dragHandleProps} /> void; + isArchiving: boolean; + archiveLabel?: string; + archiveStatus?: "idle" | "pending" | "success"; + archivePendingLabel?: string; + onArchive?: () => void; + onCopyBranchName?: () => void; + onCopyPath?: () => void; + onRename?: () => void; + onMarkAsRead?: () => void; + archiveShortcutKeys?: ShortcutKey[][] | null; + isPinned?: boolean; + onTogglePin?: () => void; + reserveIdleStatusIndicatorSpace?: boolean; + /** Pinned rows are flat under their own header; status-group rows indent from theirs. */ + inStatusGroup?: boolean; + drag?: () => void; + isDragging?: boolean; + dragHandleProps?: DraggableListDragHandleProps; +} + +function StatusWorkspaceRowInner(props: StatusWorkspaceRowInnerProps) { + if (props.drag) { + return ; + } + return ; +} + +function DraggableStatusWorkspaceRowInner( + props: StatusWorkspaceRowInnerProps & { drag: () => void }, +) { + const dragInteraction = useLongPressDragInteraction({ + drag: props.drag, + menuController: null, + }); + return ; +} + +function StatusWorkspaceRowInnerContent({ workspace, hostBadge, projectName, @@ -658,47 +786,51 @@ function StatusWorkspaceRowInner({ onTogglePin, reserveIdleStatusIndicatorSpace = true, inStatusGroup = true, -}: { - workspace: SidebarWorkspaceEntry; - hostBadge: HostBadgeModel | null; - projectName: string; - projectIconDataUri: string | null; - selected: boolean; - shortcutNumber: number | null; - showShortcutBadge: boolean; - onPress: () => void; - isArchiving: boolean; - archiveLabel?: string; - archiveStatus?: "idle" | "pending" | "success"; - archivePendingLabel?: string; - onArchive?: () => void; - onCopyBranchName?: () => void; - onCopyPath?: () => void; - onRename?: () => void; - onMarkAsRead?: () => void; - archiveShortcutKeys?: ShortcutKey[][] | null; - isPinned?: boolean; - onTogglePin?: () => void; - reserveIdleStatusIndicatorSpace?: boolean; - /** - * Whether the row sits under a status header, which is what it indents from. Pinned rows - * are a flat list under their own header and sit flush. - */ - inStatusGroup?: boolean; + isDragging = false, + dragHandleProps, + dragInteraction, +}: StatusWorkspaceRowInnerProps & { + dragInteraction?: ReturnType; }) { const isTouchPlatform = platformIsNative; const [isPressed, setIsPressed] = useState(false); const trailing = useSidebarWorkspaceTrailing(); + const { + role: _dragRole, + tabIndex: _dragTabIndex, + "aria-roledescription": _dragRoleDescription, + ...dragAttributes + } = dragHandleProps?.attributes ?? {}; const isDesktop = !isTouchPlatform; const serviceSummary = isDesktop ? selectWorkspaceServiceSummary(workspace.scripts) : null; const accessibilityState = useMemo(() => ({ selected }), [selected]); - const handlePressIn = useCallback(() => setIsPressed(true), []); - const handlePressOut = useCallback(() => setIsPressed(false), []); + const didLongPressRef = dragInteraction?.didLongPressRef; + const startDragPress = dragInteraction?.handlePressIn; + const moveDragPress = dragInteraction?.handleTouchMove; + const endDragPress = dragInteraction?.handlePressOut; + const handlePress = useCallback(() => { + if (didLongPressRef?.current) { + didLongPressRef.current = false; + return; + } + onPress(); + }, [didLongPressRef, onPress]); + const handlePressIn = useCallback( + (event: GestureResponderEvent) => { + setIsPressed(true); + startDragPress?.(event); + }, + [startDragPress], + ); + const handlePressOut = useCallback(() => { + setIsPressed(false); + endDragPress?.(); + }, [endDragPress]); return ( - + {({ isHovered, contextMenuOpen, onContextMenuOpenChange, hoverHandlers }) => { const showShortcut = showShortcutBadge && shortcutNumber !== null; const { @@ -720,9 +852,17 @@ function StatusWorkspaceRowInner({ selected, isHovered, inStatusGroup, + isDragging, }); + const backdrop = getSidebarRowBackdrop({ isDragging, isPressed, selected, isHovered }); return ( - + } + style={styles.workspaceRowContainer} + {...hoverHandlers} + > - + {kebab.showKebab && onArchive ? ( ({ workspaceRowPressed: { backgroundColor: theme.colors.surface2, }, + workspaceRowDragging: { + backgroundColor: theme.colors.surface2, + borderWidth: 1, + borderColor: theme.colors.border, + transform: [{ scale: 1.02 }], + zIndex: 3, + ...theme.shadow.md, + }, sidebarRowSelected: { backgroundColor: theme.colors.surfaceSidebarHover, }, diff --git a/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx b/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx index 40ef6e0a48..2e6b5ac5e5 100644 --- a/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx +++ b/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx @@ -70,7 +70,11 @@ function TrailingActionScrimSvg({ gradientId, color }: { gradientId: string; col const ThemedTrailingActionScrimSvg = withUnistyles(TrailingActionScrimSvg); -const scrimColorMapping = (theme: Theme) => ({ color: theme.colors.surfaceSidebarHover }); +const scrimColorMappings: Record { color: string }> = { + surfaceSidebar: (theme) => ({ color: theme.colors.surfaceSidebar }), + surfaceSidebarHover: (theme) => ({ color: theme.colors.surfaceSidebarHover }), + surface2: (theme) => ({ color: theme.colors.surface2 }), +}; export function SidebarWorkspaceRowFrame({ workspace, @@ -109,7 +113,7 @@ export function SidebarWorkspaceRowFrame({ disabled={contextMenuOpen} > {children({ - isHovered: isHovered && !contextMenuOpen, + isHovered: isHovered && !contextMenuOpen && !isDragging, contextMenuOpen, onContextMenuOpenChange: handleContextMenuOpenChange, hoverHandlers, @@ -458,18 +462,18 @@ export function SidebarWorkspaceTrailingActionBase({ export function SidebarWorkspaceTrailingActionOverlay({ visible, - scrim = false, + scrimBackdrop, children, }: { visible: boolean; /** Fade the row into the kebab when something (the diff stat) is still rendered behind it. */ - scrim?: boolean; + scrimBackdrop?: SidebarSurfaceBackdrop; children: ReactNode; }) { if (!visible || !children) return null; return ( <> - {scrim ? : null} + {scrimBackdrop ? : null} {children} ); @@ -484,14 +488,21 @@ export function SidebarWorkspaceTrailingActionOverlay({ * Anchored to the trailing slot, which is position:relative. Wider than the slot on purpose: * the fade has to start before the diff stat does or the diff's left edge cuts off hard. */ -function TrailingActionScrim() { +function TrailingActionScrim({ backdrop }: { backdrop: SidebarSurfaceBackdrop }) { // useId's output contains characters that are not legal inside url(#...) — React 19 wraps // ids in guillemets, React 18 in colons — and an unresolvable fill paints nothing at all. // Keep the per-instance uniqueness, drop everything a fragment reference can't carry. const gradientId = `sidebar-scrim-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`; return ( - - + + ); } diff --git a/packages/app/src/components/sidebar/sidebar-workspace-row.tsx b/packages/app/src/components/sidebar/sidebar-workspace-row.tsx index 44b49a8608..40f88dd24f 100644 --- a/packages/app/src/components/sidebar/sidebar-workspace-row.tsx +++ b/packages/app/src/components/sidebar/sidebar-workspace-row.tsx @@ -6,6 +6,7 @@ import { useMutation } from "@tanstack/react-query"; import * as Clipboard from "expo-clipboard"; import type { HostBadgeModel } from "@/hosts/appearance"; import type { SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list"; +import type { SidebarSurfaceBackdrop } from "@/styles/surface-backdrop"; import type { DraggableListDragHandleProps } from "@/components/draggable-list.types"; import type { ShortcutKey } from "@/utils/format-shortcut"; import { AdaptiveRenameModal } from "@/components/rename-modal"; @@ -310,6 +311,7 @@ function WorkspaceRowBody({ selected, isHovered, }); + const backdrop = getSidebarRowBackdrop({ isDragging, isPressed, selected, isHovered }); return ( - + {onArchive ? ( void; menuController: ReturnType | null; }) { + const { drag, menuController } = input; const didLongPressRef = useRef(false); const dragArmedRef = useRef(false); const dragActivatedRef = useRef(false); @@ -32,20 +33,20 @@ export function useLongPressDragInteraction(input: { }, []); const openContextMenuAtStartPoint = useCallback(() => { - if (!input.menuController || !touchStartRef.current) { + if (!menuController || !touchStartRef.current) { return; } const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0; - input.menuController.setAnchorRect({ + menuController.setAnchorRect({ x: touchStartRef.current.x, y: touchStartRef.current.y + statusBarHeight, width: 0, height: 0, }); - input.menuController.setOpen(true); + menuController.setOpen(true); menuOpenedRef.current = true; didLongPressRef.current = true; - }, [input.menuController]); + }, [menuController]); const handleLongPress = useCallback(() => { // Manual timers own long-press behavior on mobile. @@ -87,10 +88,10 @@ export function useLongPressDragInteraction(input: { dragActivatedRef.current = true; didLongPressRef.current = true; void Haptics.selectionAsync().catch(() => {}); - input.drag(); + drag(); }, DRAG_ARM_DELAY_MS); - if (!input.menuController) { + if (!menuController) { return; } @@ -112,7 +113,7 @@ export function useLongPressDragInteraction(input: { void Haptics.selectionAsync().catch(() => {}); openContextMenuAtStartPoint(); }, CONTEXT_MENU_DELAY_MS); - }, [clearTimers, input, openContextMenuAtStartPoint]); + }, [clearTimers, drag, menuController, openContextMenuAtStartPoint]); const handleDragIntent = useCallback( (_details: { dx: number; dy: number; distance: number }) => { diff --git a/packages/app/src/components/task-list-row.test.tsx b/packages/app/src/components/task-list-row.test.tsx new file mode 100644 index 0000000000..94ed47bc7d --- /dev/null +++ b/packages/app/src/components/task-list-row.test.tsx @@ -0,0 +1,115 @@ +/** + * @vitest-environment jsdom + */ +import React, { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { theme } = vi.hoisted(() => ({ + theme: { + spacing: { 2: 8 }, + fontSize: { sm: 13 }, + colors: { + foreground: "#foreground", + foregroundMuted: "#muted", + foregroundExtraMuted: "#extra-muted", + statusDotRunning: "#running", + }, + }, +})); + +vi.mock("react-native", () => ({ + View: ({ children, ...props }: React.PropsWithChildren>) => + React.createElement("div", props, children), + Text: ({ children, style }: React.PropsWithChildren<{ style?: Record[] }>) => { + const resolvedStyle = Object.assign({}, ...(style ?? []).filter(Boolean)); + return React.createElement( + "span", + { + "data-color": resolvedStyle.color, + "data-decoration": resolvedStyle.textDecorationLine, + }, + children, + ); + }, +})); + +vi.mock("react-native-unistyles", () => ({ + StyleSheet: { + create: (factory: (value: typeof theme) => unknown) => factory(theme), + }, + withUnistyles: + (Component: React.ComponentType>) => + ({ uniProps, ...props }: Record) => { + const themedProps = (uniProps as (value: typeof theme) => Record)(theme); + return React.createElement(Component, { ...props, ...themedProps }); + }, +})); + +vi.mock("lucide-react-native", () => { + const createIcon = (name: string) => (props: Record) => + React.createElement("span", { ...props, "data-icon": name }); + return { + Circle: createIcon("Circle"), + CircleCheck: createIcon("CircleCheck"), + CircleDot: createIcon("CircleDot"), + }; +}); + +vi.stubGlobal("React", React); +vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); + +import { TaskListRow } from "./task-list-row"; + +const pendingTask = { text: "Queued", completed: false } as const; +const runningTask = { + text: "Run checks", + activeForm: "Running checks", + completed: false, + status: "in_progress", +} as const; +const completedTask = { text: "Finished", completed: true, status: "completed" } as const; + +describe("TaskListRow", () => { + let container: HTMLElement; + let root: Root; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(() => { + act(() => root.unmount()); + container.remove(); + }); + + it("keeps pending tasks muted", () => { + act(() => root.render()); + + expect(container.querySelector('[data-icon="Circle"]')?.getAttribute("color")).toBe("#muted"); + expect(container.querySelector("[data-color]")?.getAttribute("data-color")).toBe("#muted"); + }); + + it("foregrounds only the running task with the running blue circle dot", () => { + act(() => root.render()); + + expect(container.textContent).toBe("Running checks"); + expect(container.querySelector('[data-icon="CircleDot"]')?.getAttribute("color")).toBe( + "#running", + ); + expect(container.querySelector("[data-color]")?.getAttribute("data-color")).toBe("#foreground"); + }); + + it("extra-mutes completed tasks with a circle check", () => { + act(() => root.render()); + + expect(container.querySelector('[data-icon="CircleCheck"]')?.getAttribute("color")).toBe( + "#extra-muted", + ); + const text = container.querySelector("[data-color]"); + expect(text?.getAttribute("data-color")).toBe("#extra-muted"); + expect(text?.getAttribute("data-decoration")).toBe("line-through"); + }); +}); diff --git a/packages/app/src/components/task-list-row.tsx b/packages/app/src/components/task-list-row.tsx new file mode 100644 index 0000000000..5dc834b617 --- /dev/null +++ b/packages/app/src/components/task-list-row.tsx @@ -0,0 +1,62 @@ +import { Circle, CircleCheck, CircleDot } from "lucide-react-native"; +import { memo } from "react"; +import { Text, View } from "react-native"; +import { StyleSheet, withUnistyles } from "react-native-unistyles"; +import type { Theme } from "@/styles/theme"; +import type { TodoEntry } from "@/types/stream"; + +const ThemedCircle = withUnistyles(Circle); +const ThemedCircleCheck = withUnistyles(CircleCheck); +const ThemedCircleDot = withUnistyles(CircleDot); + +const extraMutedIcon = (theme: Theme) => ({ color: theme.colors.foregroundExtraMuted }); +const mutedIcon = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); +const runningIcon = (theme: Theme) => ({ color: theme.colors.statusDotRunning }); + +function TaskStatusIcon({ isCompleted, isRunning }: { isCompleted: boolean; isRunning: boolean }) { + if (isCompleted) { + return ; + } + if (isRunning) { + return ; + } + return ; +} + +export const TaskListRow = memo(function TaskListRow({ task }: { task: TodoEntry }) { + const isCompleted = task.completed || task.status === "completed"; + const isRunning = !isCompleted && task.status === "in_progress"; + const text = isRunning && task.activeForm ? task.activeForm : task.text; + + return ( + + + + {text} + + + ); +}); + +const styles = StyleSheet.create((theme) => ({ + row: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + }, + text: { + flex: 1, + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.sm, + }, + runningText: { + color: theme.colors.foreground, + }, + completedText: { + color: theme.colors.foregroundExtraMuted, + textDecorationLine: "line-through", + }, +})); diff --git a/packages/app/src/components/tree-primitives.tsx b/packages/app/src/components/tree-primitives.tsx index 01c26d5fa6..9d3c129d4b 100644 --- a/packages/app/src/components/tree-primitives.tsx +++ b/packages/app/src/components/tree-primitives.tsx @@ -63,13 +63,7 @@ export function TreeIndentGuides({ depth }: { depth: number }) { /** Rotating disclosure chevron for a directory row (points right; rotates down when expanded). */ export function TreeChevron({ expanded }: { expanded: boolean }) { return ( - + ({ justifyContent: "center", flexShrink: 0, }, - // Lucide leaves space around the chevron path. Align its painted right edge inside the fixed - // slot; the rotated glyph is wider, so it needs half the optical offset. - chevronCollapsed: { - left: 4, - }, chevronExpanded: { - left: 2, transform: [{ rotate: "90deg" }], }, })); diff --git a/packages/app/src/components/ui/combobox-frame-style.test.ts b/packages/app/src/components/ui/combobox-frame-style.test.ts index 84f884c4e7..69026b141a 100644 --- a/packages/app/src/components/ui/combobox-frame-style.test.ts +++ b/packages/app/src/components/ui/combobox-frame-style.test.ts @@ -9,6 +9,7 @@ function buildWidthStyle(input: { }): Pick { const [frameStyle] = buildDesktopFrameStyle({ desktopMinWidth: input.desktopMinWidth, + desktopLockWidth: false, referenceWidth: input.referenceWidth, desktopFixedHeight: undefined, desktopPositionStyle: { left: 0, top: 0 }, @@ -55,4 +56,18 @@ describe("buildDesktopFrameStyle", () => { maxWidth: 400, }); }); + + it("locks the frame to its opening floor when content must not resize it", () => { + const [frameStyle] = buildDesktopFrameStyle({ + desktopMinWidth: 360, + desktopLockWidth: true, + referenceWidth: 120, + desktopFixedHeight: undefined, + desktopPositionStyle: { left: 0, top: 0 }, + shouldHideDesktopContent: false, + availableHeight: undefined, + }) as ViewStyle[]; + + expect(frameStyle).toMatchObject({ width: 360, minWidth: 360, maxWidth: 360 }); + }); }); diff --git a/packages/app/src/components/ui/combobox-frame-style.ts b/packages/app/src/components/ui/combobox-frame-style.ts index 0e825abaf6..8c1a495c61 100644 --- a/packages/app/src/components/ui/combobox-frame-style.ts +++ b/packages/app/src/components/ui/combobox-frame-style.ts @@ -2,6 +2,7 @@ import type { StyleProp, ViewStyle } from "react-native"; export interface DesktopFrameStyleInput { desktopMinWidth: number | undefined; + desktopLockWidth: boolean; referenceWidth: number | null; desktopFixedHeight: number | undefined; desktopPositionStyle: StyleProp; @@ -12,6 +13,7 @@ export interface DesktopFrameStyleInput { export function buildDesktopFrameStyle(input: DesktopFrameStyleInput): StyleProp { const { desktopMinWidth, + desktopLockWidth, referenceWidth, desktopFixedHeight, desktopPositionStyle, @@ -28,11 +30,13 @@ export function buildDesktopFrameStyle(input: DesktopFrameStyleInput): StyleProp ? { maxHeight: Math.min(availableHeight, desktopFixedHeight ?? 400) } : null; const floor = Math.max(desktopMinWidth ?? 0, referenceWidth ?? 200); + const widthStyle = desktopLockWidth + ? { width: floor, minWidth: floor, maxWidth: floor } + : { minWidth: floor, maxWidth: Math.max(400, floor) }; return [ { position: "absolute" as const, - minWidth: floor, - maxWidth: Math.max(400, floor), + ...widthStyle, }, fixedHeightStyle, desktopPositionStyle, diff --git a/packages/app/src/components/ui/combobox.tsx b/packages/app/src/components/ui/combobox.tsx index b756ecc9c2..da9dc2e192 100644 --- a/packages/app/src/components/ui/combobox.tsx +++ b/packages/app/src/components/ui/combobox.tsx @@ -108,6 +108,8 @@ export interface ComboboxProps { */ header?: SheetHeader; mobileChildrenScrollEnabled?: boolean; + /** Let custom desktop children own scrolling, for example a virtualized list. */ + desktopChildrenScrollEnabled?: boolean; /** Overrides the mobile scroll container spacing for custom child content. */ mobileChildrenContentContainerStyle?: StyleProp; presentation?: "push" | "replace"; @@ -122,6 +124,8 @@ export interface ComboboxProps { desktopPreventInitialFlash?: boolean; /** Minimum width for the desktop popover (overrides trigger-based width). */ desktopMinWidth?: number; + /** Keep the desktop popover at its opening width as its content changes. */ + desktopLockWidth?: boolean; /** Fixed height for the desktop popover (overrides default 400px max). */ desktopFixedHeight?: number; /** Content rendered above the scroll area on desktop (sticky header). */ @@ -1080,17 +1084,28 @@ interface DesktopBodyProps { handleSelect: (id: string) => void; renderOption: RenderOptionFn | undefined; hasChildren: boolean; + childrenScrollEnabled: boolean; children: ReactNode; } function DesktopComboboxChildrenBody(props: { header: SheetHeader | undefined; stickyHeader: ReactNode; + scrollEnabled: boolean; children: ReactNode; }): ReactElement { + const header = props.header ? : props.stickyHeader; + if (!props.scrollEnabled) { + return ( + <> + {header} + {props.children} + + ); + } return ( <> - {props.header ? : props.stickyHeader} + {header} {props.hasChildren ? ( - + {props.children} ) : ( @@ -1284,6 +1303,7 @@ export function Combobox({ title, header, mobileChildrenScrollEnabled = true, + desktopChildrenScrollEnabled = true, mobileChildrenContentContainerStyle, presentation, open, @@ -1291,6 +1311,7 @@ export function Combobox({ desktopPlacement = "bottom-start", desktopPreventInitialFlash = true, desktopMinWidth, + desktopLockWidth, desktopFixedHeight, stickyHeader, footer, @@ -1538,6 +1559,7 @@ export function Combobox({ () => buildDesktopFrameStyle({ desktopMinWidth, + desktopLockWidth: Boolean(desktopLockWidth), referenceWidth, desktopFixedHeight, desktopPositionStyle, @@ -1546,6 +1568,7 @@ export function Combobox({ }), [ desktopMinWidth, + desktopLockWidth, referenceWidth, desktopFixedHeight, desktopPositionStyle, @@ -1630,6 +1653,7 @@ export function Combobox({ handleSelect={handleSelect} renderOption={renderOption} hasChildren={hasChildren} + childrenScrollEnabled={desktopChildrenScrollEnabled} > {children} @@ -1786,6 +1810,10 @@ const styles = StyleSheet.create((theme) => ({ desktopChildrenScrollContent: { // No padding — custom children (e.g. model selector) control their own spacing }, + desktopChildrenBody: { + flex: 1, + minHeight: 0, + }, desktopScrollContentAboveSearch: { flexGrow: 1, justifyContent: "flex-end", diff --git a/packages/app/src/components/ui/context-menu.tsx b/packages/app/src/components/ui/context-menu.tsx index 17a2aba754..8b117c8012 100644 --- a/packages/app/src/components/ui/context-menu.tsx +++ b/packages/app/src/components/ui/context-menu.tsx @@ -105,6 +105,7 @@ export function ContextMenuTrigger({ enabledOnMobile = true, enabledOnWeb = true, longPressDelayMs, + onContextMenu, triggerRef, ...props }: PropsWithChildren< @@ -115,6 +116,7 @@ export function ContextMenuTrigger({ enabledOnMobile?: boolean; enabledOnWeb?: boolean; longPressDelayMs?: number; + onContextMenu?: (event: unknown) => void; triggerRef?: Ref; } >): ReactElement { @@ -170,9 +172,10 @@ export function ContextMenuTrigger({ if (isCallable(preventDefault)) preventDefault.call(event); if (isCallable(stopPropagation)) stopPropagation.call(event); } + onContextMenu?.(event); openAtEvent(event); }, - [openAtEvent], + [onContextMenu, openAtEvent], ); const pressableStyle = useCallback( diff --git a/packages/app/src/components/ui/menu/menu-overlay.tsx b/packages/app/src/components/ui/menu/menu-overlay.tsx index ba5f3cf019..fb75fa6bfe 100644 --- a/packages/app/src/components/ui/menu/menu-overlay.tsx +++ b/packages/app/src/components/ui/menu/menu-overlay.tsx @@ -333,6 +333,7 @@ export function AnchoredSurface({ <> {backdrop ? ( void }) => event.preventDefault?.(), + }} ref={setWebOverlayScope} collapsable={false} style={[ diff --git a/packages/app/src/components/ui/menu/menu-root.test.tsx b/packages/app/src/components/ui/menu/menu-root.test.tsx new file mode 100644 index 0000000000..2b24fa122d --- /dev/null +++ b/packages/app/src/components/ui/menu/menu-root.test.tsx @@ -0,0 +1,26 @@ +/** + * @vitest-environment jsdom + */ +import React, { createRef } from "react"; +import { render } from "@testing-library/react"; +import { Text, type View } from "react-native"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { MenuRoot, MenuTrigger } from "./menu-root"; + +beforeEach(() => vi.stubGlobal("React", React)); + +describe("MenuTrigger", () => { + it("forwards its rendered trigger to callers", () => { + const triggerRef = createRef(); + + render( + + + Open + + , + ); + + expect(triggerRef.current).not.toBeNull(); + }); +}); diff --git a/packages/app/src/components/ui/menu/menu-root.tsx b/packages/app/src/components/ui/menu/menu-root.tsx index ca801c8caa..08c4b2407b 100644 --- a/packages/app/src/components/ui/menu/menu-root.tsx +++ b/packages/app/src/components/ui/menu/menu-root.tsx @@ -1,6 +1,14 @@ -import { useCallback, type PropsWithChildren, type ReactElement, type ReactNode } from "react"; +import { + forwardRef, + useCallback, + type PropsWithChildren, + type ReactElement, + type ReactNode, + type Ref, +} from "react"; import { Pressable, + type View, type PressableProps, type PressableStateCallbackType, type StyleProp, @@ -49,14 +57,29 @@ export interface MenuTriggerProps extends Omit ReactNode); } -export function MenuTrigger({ - children, - disabled, - style, - ...props -}: MenuTriggerProps): ReactElement { +function assignRef(ref: Ref | undefined, value: T | null): void { + if (!ref) return; + if (typeof ref === "function") { + ref(value); + return; + } + Object.assign(ref, { current: value }); +} + +export const MenuTrigger = forwardRef(function MenuTrigger( + { children, disabled, style, ...props }, + forwardedRef, +): ReactElement { const ctx = useMenuContext("MenuTrigger"); + const handleTriggerRef = useCallback( + (node: View | null) => { + assignRef(ctx.triggerRef, node); + assignRef(forwardedRef, node); + }, + [ctx.triggerRef, forwardedRef], + ); + const handlePress = useCallback(() => { if (disabled) return; ctx.setOpen(!ctx.open); @@ -83,7 +106,7 @@ export function MenuTrigger({ return ( ); -} +}); diff --git a/packages/app/src/components/workspace-setup-dialog.tsx b/packages/app/src/components/workspace-setup-dialog.tsx index feea2da63d..adb49f0870 100644 --- a/packages/app/src/components/workspace-setup-dialog.tsx +++ b/packages/app/src/components/workspace-setup-dialog.tsx @@ -440,7 +440,8 @@ export function WorkspaceSetupDialog() { isSubmitLoading={pendingAction === "chat"} blurOnSubmit={true} value={chatDraft.text} - onChangeText={chatDraft.setText} + onChangeText={chatDraft.editText} + textReplacementKey={chatDraft.textReplacementKey} attachments={chatDraft.attachments} onChangeAttachments={chatDraft.setAttachments} cwd={sourceDirectory} diff --git a/packages/app/src/composer/agent-controls/control.test.tsx b/packages/app/src/composer/agent-controls/control.test.tsx new file mode 100644 index 0000000000..70eb679a23 --- /dev/null +++ b/packages/app/src/composer/agent-controls/control.test.tsx @@ -0,0 +1,39 @@ +/** + * @vitest-environment jsdom + */ +import React from "react"; +import { fireEvent, render } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { AgentControlTrigger } from "./control"; + +beforeEach(() => vi.stubGlobal("React", React)); + +function TestIcon() { + return null; +} + +describe("AgentControlTrigger", () => { + it("forwards interaction handlers to the rendered trigger", () => { + const onPointerEnter = vi.fn(); + const onFocus = vi.fn(); + const onPress = vi.fn(); + const view = render( + , + ); + const trigger = view.getByRole("button", { name: "Select mode" }); + + fireEvent.pointerEnter(trigger); + fireEvent.focus(trigger); + + expect(onPointerEnter).toHaveBeenCalledTimes(1); + expect(onFocus).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/app/src/composer/agent-controls/control.tsx b/packages/app/src/composer/agent-controls/control.tsx index 2077c86ab0..bef4bc891d 100644 --- a/packages/app/src/composer/agent-controls/control.tsx +++ b/packages/app/src/composer/agent-controls/control.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useCallback } from "react"; +import { forwardRef, useCallback, type ComponentProps } from "react"; import { Text, View, type PressableStateCallbackType } from "react-native"; import { StyleSheet } from "react-native-unistyles"; import { ComboboxTrigger } from "@/components/ui/combobox-trigger"; @@ -6,7 +6,10 @@ import { useComposerControlLayout } from "@/composer/agent-controls/layout-conte import { ComposerToolbarGlyph } from "@/composer/agent-controls/glyph"; import type { AgentControlIcon } from "@/agent-controls/icons"; -interface AgentControlTriggerProps { +type AgentControlTriggerProps = Omit< + ComponentProps, + "accessibilityLabel" | "block" | "children" | "chevron" | "onPress" | "style" +> & { icon: AgentControlIcon; iconColor?: string; surface: "toolbar" | "sheet"; @@ -15,11 +18,9 @@ interface AgentControlTriggerProps { showToolbarLabel?: boolean; showCaret?: boolean; open?: boolean; - disabled?: boolean; onPress: () => void; accessibilityLabel: string; - testID?: string; -} +}; export const AgentControlTrigger = forwardRef( function AgentControlTrigger( @@ -36,6 +37,7 @@ export const AgentControlTrigger = forwardRef( onPress, accessibilityLabel, testID, + ...triggerProps }, ref, ) { @@ -57,6 +59,7 @@ export const AgentControlTrigger = forwardRef( return ( ; - onToggleFavoriteModel?: (provider: string, modelId: string) => void; + agentProfiles?: AgentProfilePicker | null; + onApplyAgentProfile?: (profileId: string) => void; + onEditAgentProfiles?: () => void; features?: AgentFeature[]; onSetFeature?: (featureId: string, value: unknown) => void; onDropdownClose?: () => void; @@ -122,7 +126,6 @@ interface ControlledAgentControlsProps { export interface DraftAgentControlsProps { providerDefinitions: AgentProviderDefinition[]; selectedProvider: AgentProvider | null; - onSelectProvider: (provider: AgentProvider) => void; modeOptions: AgentMode[]; selectedMode: string; onSelectMode: (modeId: string) => void; @@ -136,6 +139,7 @@ export interface DraftAgentControlsProps { thinkingOptions: NonNullable; selectedThinkingOptionId: string; onSelectThinkingOption: (thinkingOptionId: string) => void; + onApplyAgentProfile: DraftAgentProfileControls["applyProfile"]; features?: AgentFeature[]; onSetFeature?: (featureId: string, value: unknown) => void; onDropdownClose?: () => void; @@ -241,6 +245,21 @@ function toThinkingControlOptions(options: AgentControlOption[] | undefined): Ag })); } +/** + * The picker's edit shortcut. Agent profiles are host config, so it lands on the + * host settings section that owns the list. + */ +function useEditAgentProfilesNavigation( + serverId: string | null, + isSupported: boolean, +): (() => void) | undefined { + const handleEdit = useCallback(() => { + if (!serverId) return; + router.push(buildSettingsHostSectionRoute(serverId, "agents")); + }, [serverId]); + return serverId && isSupported ? handleEdit : undefined; +} + function buildFallbackModelSelectorProviders( provider: string, modelOptions: AgentControlOption[] | undefined, @@ -255,7 +274,7 @@ function buildFallbackModelSelectorProviders( modelSelection: { kind: "models", rows: modelOptions.map((option) => ({ - favoriteKey: buildFavoriteModelKey({ provider, modelId: option.id }), + favoriteKey: `${provider}:${option.id}`, provider, providerLabel: provider, modelId: option.id, @@ -359,6 +378,16 @@ function selectAgentControlsSlice( }; } +function providerSnapshotTarget(agent: AgentControlsSlice): { + cwd: string | null | undefined; + workspaceId: string | undefined; +} { + return { + cwd: agent?.cwd, + workspaceId: agent?.workspaceId, + }; +} + function resolveSnapshotSelectedEntry( snapshotEntries: ReturnType["entries"], agentProvider: string | undefined, @@ -369,6 +398,15 @@ function resolveSnapshotSelectedEntry( return snapshotEntries.find((e) => e.provider === agentProvider) ?? null; } +function resolveSnapshotModeIds( + entry: ReturnType, +): string[] | null { + if (entry?.status !== "ready" || !entry.modes) { + return null; + } + return entry.modes.map((mode) => mode.id); +} + function buildAgentProviderDefinitions( agentProvider: string | undefined, snapshotEntries: ReturnType["entries"], @@ -418,8 +456,9 @@ function ControlledAgentControls({ disabled = false, isModelLoading = false, modelSelectorProviders, - favoriteKeys = new Set(), - onToggleFavoriteModel, + agentProfiles = null, + onApplyAgentProfile, + onEditAgentProfiles, features, onSetFeature, onDropdownClose, @@ -537,7 +576,7 @@ function ControlledAgentControls({ } }, [updateDensityForWidth]); - const modelDisabled = disabled || isModelLoading; + const modelDisabled = [disabled, isModelLoading].some(Boolean); const comboboxProviderOptions = useMemo( () => toComboboxOptions(providerOptions), @@ -672,12 +711,13 @@ function ControlledAgentControls({ selectedThinkingOptionId={selectedThinkingOptionId} features={features} onSetFeature={onSetFeature} - onToggleFavoriteModel={onToggleFavoriteModel} + onApplyAgentProfile={onApplyAgentProfile} + onEditAgentProfiles={onEditAgentProfiles} onDropdownClose={onDropdownClose} onModelSelectorOpen={onModelSelectorOpen} onRetryModelProvider={onRetryModelProvider} isRetryingModelProvider={isRetryingModelProvider} - favoriteKeys={favoriteKeys} + agentProfiles={agentProfiles} disabled={disabled} isModelLoading={isModelLoading} canSelectProvider={canSelectProvider} @@ -718,12 +758,13 @@ function ControlledAgentControls({ selectedThinkingOptionId={selectedThinkingOptionId} features={features} onSetFeature={onSetFeature} - onToggleFavoriteModel={onToggleFavoriteModel} + onApplyAgentProfile={onApplyAgentProfile} + onEditAgentProfiles={onEditAgentProfiles} onDropdownClose={onDropdownClose} onModelSelectorOpen={onModelSelectorOpen} onRetryModelProvider={onRetryModelProvider} isRetryingModelProvider={isRetryingModelProvider} - favoriteKeys={favoriteKeys} + agentProfiles={agentProfiles} disabled={disabled} isModelLoading={isModelLoading} canSelectModel={canSelectModel} @@ -760,12 +801,13 @@ interface DesktopAgentControlsContentProps { selectedThinkingOptionId?: string; features?: AgentFeature[]; onSetFeature?: (featureId: string, value: unknown) => void; - onToggleFavoriteModel?: (provider: string, modelId: string) => void; + onApplyAgentProfile?: (profileId: string) => void; + onEditAgentProfiles?: () => void; onDropdownClose?: () => void; onModelSelectorOpen?: () => void; onRetryModelProvider?: (provider: AgentProvider) => void; isRetryingModelProvider: boolean; - favoriteKeys: Set; + agentProfiles: AgentProfilePicker | null; disabled: boolean; isModelLoading: boolean; canSelectProvider: boolean; @@ -819,12 +861,13 @@ function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) { selectedThinkingOptionId, features, onSetFeature, - onToggleFavoriteModel, + onApplyAgentProfile, + onEditAgentProfiles, onDropdownClose, onModelSelectorOpen, onRetryModelProvider, isRetryingModelProvider, - favoriteKeys, + agentProfiles, disabled, isModelLoading, canSelectProvider, @@ -905,8 +948,9 @@ function DesktopAgentControlsContent(props: DesktopAgentControlsContentProps) { selectedProvider={provider} selectedModel={selectedModelId ?? ""} onSelect={handleDesktopModelSelect} - favoriteKeys={favoriteKeys} - onToggleFavorite={onToggleFavoriteModel} + profiles={agentProfiles} + onApplyProfile={onApplyAgentProfile} + onEditProfiles={onEditAgentProfiles} isLoading={isModelLoading} disabled={modelDisabled} onOpen={onModelSelectorOpen} @@ -1023,12 +1067,13 @@ interface SheetAgentControlsContentProps { selectedThinkingOptionId?: string; features?: AgentFeature[]; onSetFeature?: (featureId: string, value: unknown) => void; - onToggleFavoriteModel?: (provider: string, modelId: string) => void; + onApplyAgentProfile?: (profileId: string) => void; + onEditAgentProfiles?: () => void; onDropdownClose?: () => void; onModelSelectorOpen?: () => void; onRetryModelProvider?: (provider: AgentProvider) => void; isRetryingModelProvider: boolean; - favoriteKeys: Set; + agentProfiles: AgentProfilePicker | null; disabled: boolean; isModelLoading: boolean; canSelectModel: boolean; @@ -1063,12 +1108,13 @@ function SheetAgentControlsContent(props: SheetAgentControlsContentProps) { selectedThinkingOptionId, features, onSetFeature, - onToggleFavoriteModel, + onApplyAgentProfile, + onEditAgentProfiles, onDropdownClose, onModelSelectorOpen, onRetryModelProvider, isRetryingModelProvider, - favoriteKeys, + agentProfiles, disabled, isModelLoading, canSelectModel, @@ -1160,8 +1206,9 @@ function SheetAgentControlsContent(props: SheetAgentControlsContentProps) { selectedProvider={provider} selectedModel={selectedModelId ?? ""} onSelect={handleSheetModelSelect} - favoriteKeys={favoriteKeys} - onToggleFavorite={onToggleFavoriteModel} + profiles={agentProfiles} + onApplyProfile={onApplyAgentProfile} + onEditProfiles={onEditAgentProfiles} isLoading={isModelLoading} disabled={modelDisabled} onOpen={onModelSelectorOpen} @@ -1430,7 +1477,7 @@ export const AgentControls = memo(function AgentControls({ onDropdownClose, isCompactLayout, }: AgentControlsProps) { - const { preferences, updatePreferences } = useFormPreferences(); + const { updatePreferences } = useFormPreferences(); const agent = useSessionStore( useShallow((state) => selectAgentControlsSlice(state, serverId, agentId)), ); @@ -1446,10 +1493,7 @@ export const AgentControls = memo(function AgentControls({ isRefreshing: snapshotIsRefreshing, refresh: refreshSnapshot, refetchIfStale: refetchSnapshotIfStale, - } = useProvidersSnapshot(serverId, { - cwd: agent?.cwd, - workspaceId: agent?.workspaceId, - }); + } = useProvidersSnapshot(serverId, providerSnapshotTarget(agent)); const snapshotSelectedEntry = useMemo( () => resolveSnapshotSelectedEntry(snapshotEntries, agent?.provider), @@ -1488,13 +1532,6 @@ export const AgentControls = memo(function AgentControls({ const modelOptions = useMemo(() => { return (models ?? []).map((model) => ({ id: model.id, label: model.label })); }, [models]); - const favoriteKeys = useMemo( - () => - new Set( - (preferences.favoriteModels ?? []).map((favorite) => buildFavoriteModelKey(favorite)), - ), - [preferences.favoriteModels], - ); const thinkingOptions = useMemo(() => { return (modelSelection.thinkingOptions ?? []).map((option) => ({ @@ -1532,16 +1569,23 @@ export const AgentControls = memo(function AgentControls({ [handleSelectModel], ); - const handleToggleFavoriteModel = useCallback( - (provider: string, modelId: string) => { - void updatePreferences((current) => - toggleFavoriteModel({ preferences: current, provider, modelId }), - ).catch((error) => { - console.warn("[AgentControls] toggle favorite model failed", error); - }); - }, - [updatePreferences], + // A running agent is one provider's process, so only that provider's profiles + // can apply to it. + const profileProviders = useMemo(() => (agentProvider ? [agentProvider] : []), [agentProvider]); + const profileModeIds = useMemo( + () => resolveSnapshotModeIds(snapshotSelectedEntry), + [snapshotSelectedEntry], ); + const profileTarget = useMemo( + () => ({ kind: "agent", agentId, availableModeIds: profileModeIds }), + [agentId, profileModeIds], + ); + const agentProfiles = useAgentProfilePicker({ + serverId, + availableProviders: profileProviders, + target: profileTarget, + }); + const handleEditAgentProfiles = useEditAgentProfilesNavigation(serverId, agentProfiles !== null); const handleSelectThinkingOption = useCallback( (thinkingOptionId: string) => { @@ -1650,8 +1694,9 @@ export const AgentControls = memo(function AgentControls({ modelOptions={modelOptions} selectedModelId={modelSelection.activeModelId ?? undefined} onSelectModel={handleSelectModel} - favoriteKeys={favoriteKeys} - onToggleFavoriteModel={handleToggleFavoriteModel} + agentProfiles={agentProfiles} + onApplyAgentProfile={agentProfiles?.applyProfile} + onEditAgentProfiles={handleEditAgentProfiles} thinkingOptions={thinkingOptions.length > 1 ? thinkingOptions : undefined} selectedThinkingOptionId={modelSelection.selectedThinkingId ?? undefined} onSelectThinkingOption={handleSelectThinkingOption} @@ -1673,7 +1718,6 @@ export const AgentControls = memo(function AgentControls({ export function DraftAgentControls({ providerDefinitions, selectedProvider, - onSelectProvider: _onSelectProvider, modeOptions, selectedMode, onSelectMode, @@ -1687,6 +1731,7 @@ export function DraftAgentControls({ thinkingOptions, selectedThinkingOptionId, onSelectThinkingOption, + onApplyAgentProfile, features, onSetFeature, onDropdownClose, @@ -1697,17 +1742,9 @@ export function DraftAgentControls({ modelSelectorServerId = null, isCompactLayout, }: DraftAgentControlsProps) { - const { preferences, updatePreferences } = useFormPreferences(); const mappedThinkingOptions = useMemo(() => { return toThinkingControlOptions(thinkingOptions); }, [thinkingOptions]); - const favoriteKeys = useMemo( - () => - new Set( - (preferences.favoriteModels ?? []).map((favorite) => buildFavoriteModelKey(favorite)), - ), - [preferences.favoriteModels], - ); const effectiveSelectedThinkingOption = selectedThinkingOptionId || mappedThinkingOptions[0]?.id || undefined; @@ -1721,15 +1758,29 @@ export function DraftAgentControls({ [models], ); - const handleToggleFavorite = useCallback( - (provider: string, modelId: string) => { - void updatePreferences((current) => - toggleFavoriteModel({ preferences: current, provider, modelId }), - ).catch((error) => { - console.warn("[DraftAgentControls] toggle favorite model failed", error); - }); - }, - [updatePreferences], + // The draft form is the one surface that can switch provider, so every profile + // the host can actually run is offered here. + const profileProviders = useMemo( + () => modelSelectorProviders.map((entry) => entry.id), + [modelSelectorProviders], + ); + const profileTarget = useMemo( + () => ({ + kind: "draft", + controls: { + applyProfile: onApplyAgentProfile, + }, + }), + [onApplyAgentProfile], + ); + const agentProfiles = useAgentProfilePicker({ + serverId: modelSelectorServerId, + availableProviders: profileProviders, + target: profileTarget, + }); + const handleEditAgentProfiles = useEditAgentProfilesNavigation( + modelSelectorServerId, + agentProfiles !== null, ); const modeControl = useMemo( @@ -1756,8 +1807,9 @@ export function DraftAgentControls({ onSelectModel={onSelectModel} onSelectProviderAndModel={onSelectProviderAndModel} isModelLoading={isAllModelsLoading} - favoriteKeys={favoriteKeys} - onToggleFavoriteModel={handleToggleFavorite} + agentProfiles={agentProfiles} + onApplyAgentProfile={agentProfiles?.applyProfile} + onEditAgentProfiles={handleEditAgentProfiles} thinkingOptions={mappedThinkingOptions.length > 0 ? mappedThinkingOptions : undefined} selectedThinkingOptionId={effectiveSelectedThinkingOption} onSelectThinkingOption={onSelectThinkingOption} diff --git a/packages/app/src/composer/agent-controls/mode-control.tsx b/packages/app/src/composer/agent-controls/mode-control.tsx index a277faa6b6..0155d9d820 100644 --- a/packages/app/src/composer/agent-controls/mode-control.tsx +++ b/packages/app/src/composer/agent-controls/mode-control.tsx @@ -321,6 +321,7 @@ const styles = StyleSheet.create((theme) => ({ }, tooltipText: { color: theme.colors.foreground, - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.sm, + lineHeight: theme.fontSize.sm * 1.4, }, })); diff --git a/packages/app/src/composer/agent-controls/mode.test.ts b/packages/app/src/composer/agent-controls/mode.test.ts index 89c22787b9..0905e3cd4c 100644 --- a/packages/app/src/composer/agent-controls/mode.test.ts +++ b/packages/app/src/composer/agent-controls/mode.test.ts @@ -20,7 +20,6 @@ describe("resolveAgentControlsMode", () => { resolveAgentControlsMode({ providerDefinitions: [], selectedProvider: "codex", - onSelectProvider: () => undefined, modeOptions: [], selectedMode: "", onSelectMode: () => undefined, @@ -34,6 +33,7 @@ describe("resolveAgentControlsMode", () => { thinkingOptions: [], selectedThinkingOptionId: "", onSelectThinkingOption: () => undefined, + onApplyAgentProfile: () => undefined, }), ).toBe("draft"); }); diff --git a/packages/app/src/composer/agent-controls/model-sheet.tsx b/packages/app/src/composer/agent-controls/model-sheet.tsx index a3ea5e022c..b49813dd42 100644 --- a/packages/app/src/composer/agent-controls/model-sheet.tsx +++ b/packages/app/src/composer/agent-controls/model-sheet.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { Keyboard, ScrollView, Text, View, type PressableStateCallbackType } from "react-native"; import { StyleSheet } from "react-native-unistyles"; import type { AgentProvider } from "@getpaseo/protocol/agent-types"; +import type { AgentProfilePicker } from "@/agent-profiles"; import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet"; import { ComboboxTrigger } from "@/components/ui/combobox-trigger"; import { getProviderIcon } from "@/components/provider-icons"; @@ -24,8 +25,9 @@ interface CompactModelSheetProps { selectedModel: string; onSelect: (provider: string, modelId: string) => void; isLoading: boolean; - favoriteKeys: Set; - onToggleFavorite?: (provider: string, modelId: string) => void; + profiles?: AgentProfilePicker | null; + onApplyProfile?: (profileId: string) => void; + onEditProfiles?: () => void; onOpen?: () => void; onClose?: () => void; onRetryProvider?: (provider: AgentProvider) => void; @@ -47,8 +49,9 @@ export function CompactModelSheet({ selectedModel, onSelect, isLoading, - favoriteKeys, - onToggleFavorite, + profiles = null, + onApplyProfile, + onEditProfiles, onOpen, onClose, onRetryProvider, @@ -66,7 +69,7 @@ export function CompactModelSheet({ selectedProvider, selectedModel, isLoading, - favoriteKeys, + profiles, serverId, }); const { prepareToOpen, reset } = browser; @@ -104,6 +107,19 @@ export function CompactModelSheet({ [close, onSelect], ); + const handleApplyProfile = useCallback( + (profileId: string) => { + onApplyProfile?.(profileId); + close(); + }, + [close, onApplyProfile], + ); + + const handleEditProfiles = useCallback(() => { + close(); + onEditProfiles?.(); + }, [close, onEditProfiles]); + const toggle = useCallback(() => { if (isOpen) { close(); @@ -168,7 +184,8 @@ export function CompactModelSheet({ { thinkingOptionId: "high", }); + const hydratedReplacementKey = getLatest().textReplacementKey; + await act(async () => { - getLatest().setText("hello world"); + getLatest().editText("hello world"); getLatest().setAttachments([{ kind: "image", metadata: image }]); }); + expect(getLatest().textReplacementKey).toBe(hydratedReplacementKey); + + await act(async () => { + getLatest().replaceText("replacement text"); + }); + + expect(getLatest().textReplacementKey).not.toBe(hydratedReplacementKey); + + await act(async () => { + getLatest().editText("hello world"); + }); + await act(async () => { root!.unmount(); }); @@ -418,7 +432,7 @@ describe("useAgentInputDraft live contract", () => { }); await act(async () => { - getLatest().setText("with attachment"); + getLatest().editText("with attachment"); getLatest().setAttachments([{ kind: "image", metadata: image }]); }); @@ -500,7 +514,7 @@ describe("useAgentInputDraft live contract", () => { }); await act(async () => { - getLatest().setText("queued message"); + getLatest().editText("queued message"); getLatest().setAttachments([{ kind: "image", metadata: image }]); }); @@ -554,7 +568,7 @@ describe("useAgentInputDraft live contract", () => { }); await act(async () => { - getLatest().setText("queued message"); + getLatest().editText("queued message"); getLatest().setAttachments([{ kind: "image", metadata: sentImage }]); }); @@ -570,7 +584,7 @@ describe("useAgentInputDraft live contract", () => { }); await act(async () => { - getLatest().setText("draft again"); + getLatest().editText("draft again"); }); await act(async () => { diff --git a/packages/app/src/composer/draft/input-draft.ts b/packages/app/src/composer/draft/input-draft.ts index b47d6ae060..9ebb834328 100644 --- a/packages/app/src/composer/draft/input-draft.ts +++ b/packages/app/src/composer/draft/input-draft.ts @@ -56,7 +56,9 @@ type DraftComposerState = UseAgentFormStateResult & { export interface AgentInputDraft { text: string; - setText: (text: string) => void; + editText: (text: string) => void; + replaceText: (text: string) => void; + textReplacementKey: string; attachments: UserComposerAttachment[]; setAttachments: (updater: AttachmentUpdater) => void; clear: (lifecycle: "sent" | "abandoned") => void; @@ -97,6 +99,7 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr (state) => state.attachmentFocusRequestByDraftKey[draftKey] ?? 0, ); const [hydratedDraftKey, setHydratedDraftKey] = useState(null); + const [textReplacementRevision, setTextReplacementRevision] = useState(0); const text = draft?.text ?? ""; const attachments = draft?.attachments ?? []; const isHydrated = hydratedDraftKey === draftKey; @@ -120,13 +123,21 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr [draftKey], ); - const setText = useCallback( + const editText = useCallback( (nextText: string) => { saveDraft((current) => ({ ...current, text: nextText })); }, [saveDraft], ); + const replaceText = useCallback( + (nextText: string) => { + saveDraft((current) => ({ ...current, text: nextText })); + setTextReplacementRevision((revision) => revision + 1); + }, + [saveDraft], + ); + const setAttachments = useCallback( (updater: AttachmentUpdater) => { saveDraft((current) => ({ @@ -149,6 +160,7 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr void (async () => { await useDraftStore.getState().hydrateDraftInput({ draftKey }); if (!cancelled) { + setTextReplacementRevision((revision) => revision + 1); setHydratedDraftKey(draftKey); } })(); @@ -203,6 +215,7 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr features: draftFeatures, featureValues: draftFeatureValues, setFeatureValue: setDraftFeatureValue, + applyProfileFeatureValues, } = useDraftAgentFeatures({ serverId: formState.selectedServerId, provider: formState.selectedProvider, @@ -213,6 +226,14 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr initialFeatureValues: composerOptions?.initialFeatureValues, }); + const applyDraftAgentProfile = useCallback( + (profile: Parameters[0]) => { + formState.applyProfileFromUser(profile); + applyProfileFeatureValues(profile.featureValues); + }, + [applyProfileFeatureValues, formState], + ); + const commandDraftConfig = useMemo( () => composerOptions @@ -249,6 +270,7 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr formState, features: draftFeatures, onSetFeature: setDraftFeatureValue, + onApplyAgentProfile: applyDraftAgentProfile, }), commandDraftConfig, }; @@ -259,6 +281,7 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr effectiveThinkingOptionId, draftFeatures, draftFeatureValues, + applyDraftAgentProfile, formState, setDraftFeatureValue, workingDir, @@ -266,7 +289,9 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr return { text, - setText, + editText, + replaceText, + textReplacementKey: `${draftKey}:${textReplacementRevision}`, attachments, setAttachments, clear, diff --git a/packages/app/src/composer/draft/workspace-tab.tsx b/packages/app/src/composer/draft/workspace-tab.tsx index 12867a7c01..277d851154 100644 --- a/packages/app/src/composer/draft/workspace-tab.tsx +++ b/packages/app/src/composer/draft/workspace-tab.tsx @@ -391,7 +391,7 @@ export function WorkspaceDraftAgentTab({ const draftOnSetFeature = composerState.agentControls.onSetFeature; const clearDraftInput = draftInput.clear; - const setDraftText = draftInput.setText; + const replaceDraftText = draftInput.replaceText; const setDraftAttachments = draftInput.setAttachments; const pendingAutoSubmit = useWorkspaceDraftSubmissionStore((state) => { const pending = state.pendingByDraftId[draftId] ?? null; @@ -588,7 +588,7 @@ export function WorkspaceDraftAgentTab({ return; } autoSubmitKeyRef.current = submitKey; - setDraftText(""); + replaceDraftText(""); setDraftAttachments([]); const preparedAttempt = initialCreateAttempt?.clientMessageId === submission.clientMessageId @@ -605,7 +605,7 @@ export function WorkspaceDraftAgentTab({ cwd: submission.cwd, }); void createPromise.catch(() => { - setDraftText(submission.text); + replaceDraftText(submission.text); setDraftAttachments(composerWorkspaceAttachment.userAttachmentsOnly(submission.attachments)); autoSubmitKeyRef.current = null; }); @@ -618,7 +618,7 @@ export function WorkspaceDraftAgentTab({ isReadyForPendingAutoSubmit, serverId, setDraftAttachments, - setDraftText, + replaceDraftText, workspaceId, ]); @@ -700,7 +700,8 @@ export function WorkspaceDraftAgentTab({ isSubmitLoading={isSubmitting} blurOnSubmit={true} value={draftInput.text} - onChangeText={draftInput.setText} + onChangeText={draftInput.editText} + textReplacementKey={draftInput.textReplacementKey} attachments={draftInput.attachments} attachmentScopeKeys={attachmentScopeKeys} onOpenWorkspaceAttachment={handleOpenWorkspaceAttachment} diff --git a/packages/app/src/composer/index.tsx b/packages/app/src/composer/index.tsx index ea706565b4..3f6e47db4c 100644 --- a/packages/app/src/composer/index.tsx +++ b/packages/app/src/composer/index.tsx @@ -126,6 +126,7 @@ import { getForgePresentation } from "@/git/forge"; import { ForgeBrandIcon } from "@/git/forge-icon"; import { useComposerGithubAutoAttach } from "./github/auto-attach"; import { readClipboardImage } from "./clipboard-image"; +import { normalizeNativePastedImages, type NativePastedFile } from "./native-pasted-image"; import { resolveClientSlashCommand, type ClientSlashCommand } from "@/client-slash-commands"; import { appendWorkspaceFileAttachment, @@ -847,6 +848,7 @@ interface ComposerProps { blurOnSubmit?: boolean; value: string; onChangeText: (text: string) => void; + textReplacementKey: string; attachments: UserComposerAttachment[]; attachmentScopeKeys?: readonly string[]; onOpenWorkspaceAttachment?: (attachment: WorkspaceComposerAttachment) => void; @@ -1055,6 +1057,7 @@ export function Composer({ blurOnSubmit = false, value, onChangeText, + textReplacementKey, attachments, attachmentScopeKeys = EMPTY_ATTACHMENT_SCOPE_KEYS, onOpenWorkspaceAttachment, @@ -1152,6 +1155,7 @@ export function Composer({ const [cursorIndex, setCursorIndex] = useState(0); const [isProcessing, setIsProcessing] = useState(false); const [isUploadingFile, setIsUploadingFile] = useState(false); + const [pendingNativeImagePastes, setPendingNativeImagePastes] = useState(0); const [sendError, setSendError] = useState(null); const [isMessageInputFocused, setIsMessageInputFocused] = useState(false); const [isGithubPickerOpen, setIsGithubPickerOpen] = useState(false); @@ -1164,6 +1168,17 @@ export function Composer({ `message-input:${serverId}:${agentId}:${Math.random().toString(36).slice(2)}`, ); + const replaceUserInput = useCallback( + (text: string, selection?: { start: number; end: number }) => { + if (messageInputRef.current) { + messageInputRef.current.replaceText(text, selection); + return; + } + onChangeText(text); + }, + [onChangeText], + ); + const runClientSlashCommand = useCallback( (command: ClientSlashCommand): boolean => { if (command.execution !== "immediate" || !onClientSlashCommand) { @@ -1174,7 +1189,7 @@ export function Composer({ messageInputRef.current?.blur(); } clearDraft("sent"); - setUserInput(""); + replaceUserInput(""); setSelectedAttachments([]); resetSuppression(); setSendError(null); @@ -1195,14 +1210,14 @@ export function Composer({ onClientSlashCommand, resetSuppression, setSelectedAttachments, - setUserInput, + replaceUserInput, ], ); const autocomplete = useAgentAutocomplete({ userInput, cursorIndex, - setUserInput, + setUserInput: replaceUserInput, serverId, agentId, draftConfig: commandDraftConfig, @@ -1357,7 +1372,7 @@ export function Composer({ }); if (!result.queued) return; - setUserInput(""); + replaceUserInput(""); setSelectedAttachments([]); resetSuppression(); clearSentAttachments(queuedAttachments); @@ -1368,7 +1383,7 @@ export function Composer({ queueWriter, resetSuppression, setSelectedAttachments, - setUserInput, + replaceUserInput, ], ); @@ -1399,7 +1414,7 @@ export function Composer({ await submitMessage(submitText, submitAttachments); }, clearDraft, - setUserInput, + setUserInput: replaceUserInput, setAttachments: (nextAttachments) => { setSelectedAttachments(composerWorkspaceAttachment.userAttachmentsOnly(nextAttachments)); }, @@ -1424,7 +1439,7 @@ export function Composer({ isAgentRunning, queueMessage, setSelectedAttachments, - setUserInput, + replaceUserInput, submitBehavior, submitMessage, t, @@ -1485,6 +1500,30 @@ export function Composer({ } }, [addImages, t]); + const handleNativePasteImages = useCallback( + (files: readonly NativePastedFile[]) => { + setPendingNativeImagePastes((pending) => pending + 1); + void pickAndPersistImages({ + pickImages: async () => normalizeNativePastedImages(files), + persister: composerImageAttachmentPersister, + }) + .then((newImages) => { + if (newImages.length > 0) { + addImages(newImages); + } + return undefined; + }) + .catch((error) => { + console.error("[Composer] Failed to persist pasted image:", error); + toastErrorRef.current(t("composer.errors.pasteImageFailed")); + }) + .finally(() => { + setPendingNativeImagePastes((pending) => Math.max(0, pending - 1)); + }); + }, + [addImages, t], + ); + const uploadPickedFiles = useCallback( async (files: PickedFile[]) => { if (files.length === 0) return; @@ -1687,10 +1726,10 @@ export function Composer({ queue: queueWriter, }); if (!result) return; - setUserInput(result.text); + replaceUserInput(result.text); setSelectedAttachments(result.attachments); }, - [agentId, queueWriter, setSelectedAttachments, setUserInput], + [agentId, queueWriter, replaceUserInput, setSelectedAttachments], ); const handleSendQueuedNow = useCallback( @@ -2072,7 +2111,8 @@ export function Composer({ const messageInputContainerRef = useRef(null); - const isSubmitLoadingVisible = isProcessing || isSubmitLoading || isUploadingFile; + const isSubmitLoadingVisible = + isProcessing || isSubmitLoading || isUploadingFile || pendingNativeImagePastes > 0; const isSubmitDisabled = isSubmitLoadingVisible || (waitForGithubAutoAttachOnSubmit && githubAutoAttach.isResolving); @@ -2141,6 +2181,7 @@ export function Composer({ attachmentMenuItems={attachmentMenuItems} onAttachButtonRef={handleAttachButtonRef} onAddImages={addImages} + onPasteImages={handleNativePasteImages} client={client} isReadyForDictation={isDictationReady} placeholder={messagePlaceholder} @@ -2166,6 +2207,7 @@ export function Composer({ attachmentSlot={attachmentTray} inputMode={inputMode} readOnly={readOnly} + textReplacementKey={textReplacementKey} submitLabel={submitLabel} /> void; onAddImages?: (images: ImageAttachment[]) => void; + onPasteImages?: (files: readonly NativePastedFile[]) => void; client: DaemonClient | null; /** Dictation start gate from host runtime (socket connected + directory ready). */ isReadyForDictation?: boolean; @@ -147,6 +151,8 @@ export interface MessageInputProps { inputMode?: ComposerInputMode; /** Renders `value` as static text on the same surface, for content there is nothing to type into. */ readOnly?: boolean; + /** Changes only when application state must replace native-owned text. */ + textReplacementKey: string; /** Replaces the submit icon with this label, still inside the composer's own toolbar row. */ submitLabel?: string; } @@ -154,6 +160,8 @@ export interface MessageInputProps { export interface MessageInputRef { focus: () => void; blur: () => void; + getText: () => string; + replaceText: (text: string, selection?: { start: number; end: number }) => void; runKeyboardAction: (action: MessageInputKeyboardActionKind) => boolean; /** * Web-only: return the underlying DOM element for focus assertions/retries. @@ -397,12 +405,9 @@ function handleDesktopKeyPressImpl( ctx.handleDefaultSendAction(); } -function getTextInputNativeElement( - current: TextInput | (TextInput & { getNativeRef?: () => unknown }) | null, -): HTMLElement | null { +function getTextInputNativeElement(current: ComposerTextInputHandle | null): HTMLElement | null { if (!current) return null; - const handle = current as TextInput & { getNativeRef?: () => unknown }; - const native = typeof handle.getNativeRef === "function" ? handle.getNativeRef() : current; + const native = typeof current.getNativeRef === "function" ? current.getNativeRef() : current; return native instanceof HTMLElement ? native : null; } @@ -478,9 +483,7 @@ function usePasteImagesEffect(args: PasteImagesEffectArgs): void { } function useAutoFocusOnWebEffect( - textInputRef: React.MutableRefObject< - TextInput | (TextInput & { getNativeRef?: () => unknown }) | null - >, + textInputRef: React.MutableRefObject, autoFocus: boolean, autoFocusKey: string | undefined, ): void { @@ -588,8 +591,8 @@ function FocusHint({ interface ComposerTextSurfaceProps { readOnly: boolean; value: string; - textInputRef: React.Ref; - textInputStyle: React.ComponentProps["style"]; + textInputRef: React.Ref; + textInputStyle: React.ComponentProps["style"]; readOnlyTextStyle: React.ComponentProps["style"]; placeholder: string; accessibilityLabel: string; @@ -602,6 +605,8 @@ interface ComposerTextSurfaceProps { onContentSizeChange: (event: NativeSyntheticEvent) => void; onKeyPress: ((event: WebTextInputKeyPressEvent) => void) | undefined; onSelectionChange: (event: NativeSyntheticEvent) => void; + onPasteImages: ((files: readonly NativePastedFile[]) => void) | undefined; + onPasteError: (message: string) => void; focusHintVisible: boolean; focusInputKeys: ShortcutChord | null | undefined; focusHintLabel: string; @@ -624,13 +629,12 @@ function ComposerTextSurface(props: ComposerTextSurfaceProps): React.ReactElemen } return ( - void) | undefined; - onChangeText: (text: string) => void; + replaceText: (text: string) => void; onMinimizeHeight: () => void; } @@ -913,7 +919,7 @@ function queueMessageImpl(ctx: QueueMessageContext): void { const trimmed = ctx.value.trim(); if (!trimmed && ctx.attachments.length === 0) return; ctx.onQueue({ text: trimmed, attachments: ctx.attachments, cwd: ctx.cwd }); - ctx.onChangeText(""); + ctx.replaceText(""); ctx.onMinimizeHeight(); } @@ -965,9 +971,7 @@ function isTextAreaLike(v: unknown): v is TextAreaHandle { return typeof v === "object" && v !== null && "scrollHeight" in v; } -function getWebTextAreaImpl( - current: TextInput | (TextInput & { getNativeRef?: () => unknown }) | null, -): TextAreaHandle | null { +function getWebTextAreaImpl(current: ComposerTextInputHandle | null): TextAreaHandle | null { if (!current) return null; const candidate = current as { getNativeRef?: () => unknown }; if (typeof candidate.getNativeRef === "function") { @@ -1019,6 +1023,7 @@ interface ResolvedMessageInputProps { attachmentMenuItems: AttachmentMenuItem[]; onAttachButtonRef: ((node: View | null) => void) | undefined; onAddImages: ((images: ImageAttachment[]) => void) | undefined; + onPasteImages: ((files: readonly NativePastedFile[]) => void) | undefined; client: DaemonClient | null; isReadyForDictation: boolean | undefined; placeholder: string | undefined; @@ -1044,6 +1049,7 @@ interface ResolvedMessageInputProps { attachmentSlot: React.ReactNode; inputMode: ComposerInputMode; readOnly: boolean; + textReplacementKey: string; submitLabel: string | undefined; } @@ -1065,6 +1071,7 @@ function resolveMessageInputProps(props: MessageInputProps): ResolvedMessageInpu attachmentMenuItems: props.attachmentMenuItems, onAttachButtonRef: props.onAttachButtonRef, onAddImages: props.onAddImages, + onPasteImages: props.onPasteImages, client: props.client, isReadyForDictation: props.isReadyForDictation, placeholder: props.placeholder, @@ -1090,6 +1097,7 @@ function resolveMessageInputProps(props: MessageInputProps): ResolvedMessageInpu attachmentSlot: props.attachmentSlot, inputMode: props.inputMode ?? "chat", readOnly: props.readOnly ?? false, + textReplacementKey: props.textReplacementKey, submitLabel: props.submitLabel, }; } @@ -1119,6 +1127,7 @@ export const MessageInput = forwardRef( attachmentMenuItems, onAttachButtonRef, onAddImages, + onPasteImages, client, isReadyForDictation, placeholder, @@ -1144,6 +1153,7 @@ export const MessageInput = forwardRef( attachmentSlot, inputMode, readOnly, + textReplacementKey, submitLabel, } = resolveMessageInputProps(props); const mode = resolveComposerInputMode(inputMode); @@ -1161,18 +1171,29 @@ export const MessageInput = forwardRef( const [isInputFocused, setIsInputFocused] = useState(false); const rootRef = useRef(null); const inputWrapperRef = useRef(null); - const textInputRef = useRef unknown }) | null>( - null, - ); + const textInputRef = useRef(null); const isInputFocusedRef = useRef(false); + const valueRef = useRef(value); + const appliedTextReplacementKeyRef = useRef(textReplacementKey); + + const replaceText = useCallback( + (nextText: string, selection?: { start: number; end: number }) => { + valueRef.current = nextText; + textInputRef.current?.replaceText(nextText, selection); + onChangeText(nextText); + }, + [onChangeText], + ); useImperativeHandle(ref, () => ({ focus: () => { textInputRef.current?.focus(); }, blur: () => { - textInputRef.current?.blur?.(); + textInputRef.current?.blur(); }, + getText: () => textInputRef.current?.getText() ?? valueRef.current, + replaceText, runKeyboardAction: (action) => runMessageInputKeyboardAction(action, { focusInput: () => textInputRef.current?.focus(), @@ -1191,7 +1212,6 @@ export const MessageInput = forwardRef( })); const inputHeightRef = useRef(MIN_INPUT_HEIGHT); const sendAfterTranscriptRef = useRef(false); - const valueRef = useRef(value); const serverInfo = useSessionStore( useCallback( (state) => { @@ -1205,8 +1225,11 @@ export const MessageInput = forwardRef( ); useEffect(() => { + if (appliedTextReplacementKeyRef.current === textReplacementKey) return; + appliedTextReplacementKeyRef.current = textReplacementKey; valueRef.current = value; - }, [value]); + textInputRef.current?.replaceText(value); + }, [textReplacementKey, value]); useEffect(() => { return () => { @@ -1226,13 +1249,13 @@ export const MessageInput = forwardRef( isAgentRunning, onQueue, onSubmit, - onChangeText, + replaceText, attachments, cwd, autoSend, }); }, - [onChangeText, onSubmit, onQueue, attachments, cwd, isAgentRunning, defaultSendBehavior], + [replaceText, onSubmit, onQueue, attachments, cwd, isAgentRunning, defaultSendBehavior], ); const handleDictationError = useCallback( @@ -1413,7 +1436,7 @@ export const MessageInput = forwardRef( const handleSendMessage = useCallback( () => sendMessageImpl({ - value: valueRef.current, + value: textInputRef.current?.getText() ?? valueRef.current, attachments, hasExternalContent, allowEmptySubmit, @@ -1438,14 +1461,14 @@ export const MessageInput = forwardRef( const handleQueueMessage = useCallback( () => queueMessageImpl({ - value: valueRef.current, + value: textInputRef.current?.getText() ?? valueRef.current, attachments, cwd, onQueue, - onChangeText, + replaceText, onMinimizeHeight: minimizeInputHeight, }), - [attachments, cwd, onQueue, onChangeText, minimizeInputHeight], + [attachments, cwd, onQueue, replaceText, minimizeInputHeight], ); const handleDefaultSendAction = useCallback(() => { @@ -1618,6 +1641,14 @@ export const MessageInput = forwardRef( onFocusChange?.(false); }, [onFocusChange]); + const handlePasteError = useCallback( + (message: string) => { + console.error("[MessageInput] Native paste failed:", message); + toast.error(t("composer.errors.pasteImageFailed")); + }, + [t, toast], + ); + const attachButtonStyle = useCallback( ({ hovered }: { hovered?: boolean }) => [ styles.attachButton, @@ -1732,6 +1763,8 @@ export const MessageInput = forwardRef( onContentSizeChange={handleContentSizeChange} onKeyPress={shouldHandleWebKeyPress ? handleDesktopKeyPress : undefined} onSelectionChange={handleSelectionChange} + onPasteImages={onPasteImages} + onPasteError={handlePasteError} focusHintVisible={isWeb && isPaneFocused && !isInputFocused && !value} focusInputKeys={focusInputKeys} focusHintLabel={t("composer.input.focusHint", { @@ -1982,11 +2015,7 @@ const ThemedMicOff = withUnistyles(MicOff); const ThemedArrowUp = withUnistyles(ArrowUp); const ThemedCornerDownLeft = withUnistyles(CornerDownLeft); const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); -const ThemedTextInput = withUnistyles(TextInput); const iconForegroundMapping = (theme: Theme) => ({ color: theme.colors.foreground }); const iconForegroundMutedMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); const iconAccentForegroundMapping = (theme: Theme) => ({ color: theme.colors.accentForeground }); -const textInputPlaceholderColorMapping = (theme: Theme) => ({ - placeholderTextColor: theme.colors.surface4, -}); diff --git a/packages/app/src/composer/input/state.test.ts b/packages/app/src/composer/input/state.test.ts index d6265b063d..2b0c216a5c 100644 --- a/packages/app/src/composer/input/state.test.ts +++ b/packages/app/src/composer/input/state.test.ts @@ -173,7 +173,7 @@ describe("dictation transcript behavior", () => { defaultSendBehavior: "interrupt", isAgentRunning: false, onQueue: undefined, - onChangeText: (text) => actions.push(`change:${text}`), + replaceText: (text) => actions.push(`replace:${text}`), onSubmit: (payload) => actions.push(`submit:${payload.text}`), attachments: [], cwd: "/repo", @@ -181,7 +181,7 @@ describe("dictation transcript behavior", () => { }); expect(actions).toEqual([ - "change:typed context spoken prompt", + "replace:typed context spoken prompt", "submit:typed context spoken prompt", ]); }); diff --git a/packages/app/src/composer/input/state.ts b/packages/app/src/composer/input/state.ts index 733a30cc4c..15ea7199ee 100644 --- a/packages/app/src/composer/input/state.ts +++ b/packages/app/src/composer/input/state.ts @@ -52,7 +52,7 @@ interface DictationTranscriptContext { isAgentRunning: boolean; onQueue: ((payload: MessagePayload) => void) | undefined; onSubmit: (payload: MessagePayload) => void; - onChangeText: (text: string) => void; + replaceText: (text: string) => void; attachments: MessagePayload["attachments"]; cwd: string; autoSend: boolean; @@ -64,15 +64,15 @@ export function applyDictationTranscript(text: string, ctx: DictationTranscriptC const nextValue = `${ctx.value}${shouldPad ? " " : ""}${text}`; if (!ctx.autoSend) { - ctx.onChangeText(nextValue); + ctx.replaceText(nextValue); return; } - ctx.onChangeText(nextValue); + ctx.replaceText(nextValue); if (ctx.defaultSendBehavior === "queue" && ctx.isAgentRunning && ctx.onQueue) { ctx.onQueue({ text: nextValue, attachments: ctx.attachments, cwd: ctx.cwd }); - ctx.onChangeText(""); + ctx.replaceText(""); return; } diff --git a/packages/app/src/composer/input/text-input-types.ts b/packages/app/src/composer/input/text-input-types.ts new file mode 100644 index 0000000000..5963057bc0 --- /dev/null +++ b/packages/app/src/composer/input/text-input-types.ts @@ -0,0 +1,20 @@ +import type { TextInputProps } from "react-native"; +import type { NativePastedFile } from "@/composer/native-pasted-image"; + +export interface ComposerTextInputHandle { + focus: () => void; + blur: () => void; + getText: () => string; + replaceText: (text: string, selection?: { start: number; end: number }) => void; + getNativeRef?: () => unknown; +} + +export interface ComposerTextInputProps extends Omit< + TextInputProps, + "defaultValue" | "onChangeText" | "value" +> { + text: string; + onChangeText: (text: string) => void; + onPasteImages?: (files: readonly NativePastedFile[]) => void; + onPasteError?: (message: string) => void; +} diff --git a/packages/app/src/composer/input/text-input.d.ts b/packages/app/src/composer/input/text-input.d.ts new file mode 100644 index 0000000000..5043ffbeb6 --- /dev/null +++ b/packages/app/src/composer/input/text-input.d.ts @@ -0,0 +1 @@ +export * from "./text-input.native"; diff --git a/packages/app/src/composer/input/text-input.native.tsx b/packages/app/src/composer/input/text-input.native.tsx new file mode 100644 index 0000000000..875c983d34 --- /dev/null +++ b/packages/app/src/composer/input/text-input.native.tsx @@ -0,0 +1,72 @@ +import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"; +import PasteInput, { + type PastedFile, + type PasteTextInputInstance, +} from "@mattermost/react-native-paste-input"; +import { withUnistyles } from "react-native-unistyles"; +import type { ComposerTextInputHandle, ComposerTextInputProps } from "./text-input-types"; + +type NativeInputInstance = PasteTextInputInstance & { + blur: () => void; + focus: () => void; + setNativeProps: (props: { text: string }) => void; +}; + +const ThemedPasteInput = withUnistyles(PasteInput, (theme) => ({ + placeholderTextColor: theme.colors.surface4, +})); + +export const ComposerTextInput = forwardRef( + function ComposerTextInputNative( + { text, onChangeText, onPasteImages, onPasteError, ...props }, + ref, + ) { + const inputRef = useRef(null); + const textRef = useRef(text); + + useImperativeHandle(ref, () => ({ + focus: () => inputRef.current?.focus(), + blur: () => inputRef.current?.blur(), + getText: () => textRef.current, + replaceText: (nextText, selection) => { + textRef.current = nextText; + inputRef.current?.setNativeProps({ text: nextText }); + if (selection) { + inputRef.current?.setSelection(selection.start, selection.end); + } + }, + getNativeRef: () => inputRef.current?.getNativeRef?.() ?? inputRef.current, + })); + + const handleChangeText = useCallback( + (nextText: string) => { + textRef.current = nextText; + onChangeText(nextText); + }, + [onChangeText], + ); + + const handlePaste = useCallback( + (error: string | null | undefined, files: PastedFile[]) => { + if (error) { + onPasteError?.(error); + return; + } + if (files.length > 0) { + onPasteImages?.(files); + } + }, + [onPasteError, onPasteImages], + ); + + return ( + + ); + }, +); diff --git a/packages/app/src/composer/input/text-input.web.browser.test.tsx b/packages/app/src/composer/input/text-input.web.browser.test.tsx new file mode 100644 index 0000000000..10fc2696a4 --- /dev/null +++ b/packages/app/src/composer/input/text-input.web.browser.test.tsx @@ -0,0 +1,148 @@ +import React, { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { ComposerTextInput } from "./text-input.web"; + +interface MountedInput { + root: Root; + container: HTMLDivElement; + textarea: HTMLTextAreaElement; +} + +interface TextRecorder { + changes: string[]; + onChangeText: (text: string) => void; +} + +const mountedInputs: MountedInput[] = []; + +function mountInput(onChangeText: (text: string) => void): MountedInput { + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + + act(() => { + root.render( + , + ); + }); + + const textarea = container.querySelector("textarea"); + if (!textarea) { + throw new Error("Composer text input did not render a textarea"); + } + + const mounted = { root, container, textarea }; + mountedInputs.push(mounted); + return mounted; +} + +function dispatchComposition( + textarea: HTMLTextAreaElement, + type: "compositionstart" | "compositionend", +) { + textarea.dispatchEvent(new CompositionEvent(type, { bubbles: true })); +} + +function typeFromIme(textarea: HTMLTextAreaElement, text: string): void { + const valueSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set; + if (!valueSetter) { + throw new Error("HTML textarea value setter is unavailable"); + } + valueSetter.call(textarea, text); + textarea.dispatchEvent(new InputEvent("input", { bubbles: true, data: text })); +} + +function ignoreTextChange(): void {} + +function createTextRecorder(): TextRecorder { + const changes: string[] = []; + return { + changes, + onChangeText: (text) => changes.push(text), + }; +} + +afterEach(() => { + for (const mounted of mountedInputs.splice(0)) { + act(() => mounted.root.unmount()); + mounted.container.remove(); + } + vi.useRealTimers(); +}); + +describe("ComposerTextInput web IME composition", () => { + it("keeps the DOM-owned candidate during composition and reports the committed text", () => { + vi.useFakeTimers(); + const recorder = createTextRecorder(); + const mounted = mountInput(recorder.onChangeText); + + act(() => { + dispatchComposition(mounted.textarea, "compositionstart"); + mounted.textarea.value = "你好"; + mounted.root.render( + , + ); + }); + + expect(mounted.textarea.value).toBe("你好"); + + act(() => { + dispatchComposition(mounted.textarea, "compositionend"); + vi.runAllTimers(); + }); + + expect(recorder.changes).toEqual(["你好"]); + }); + + it("does not let a previous composition-end timer take ownership from a new composition", () => { + vi.useFakeTimers(); + const mounted = mountInput(ignoreTextChange); + + act(() => { + dispatchComposition(mounted.textarea, "compositionstart"); + mounted.textarea.value = "你"; + dispatchComposition(mounted.textarea, "compositionend"); + dispatchComposition(mounted.textarea, "compositionstart"); + mounted.textarea.value = "你好"; + vi.runAllTimers(); + mounted.root.render( + , + ); + }); + + expect(mounted.textarea.value).toBe("你好"); + }); + + it("does not report committed text twice when the input event already reported it", () => { + vi.useFakeTimers(); + const changes: string[] = []; + const mounted = mountInput((text) => changes.push(text)); + + act(() => { + dispatchComposition(mounted.textarea, "compositionstart"); + typeFromIme(mounted.textarea, "你好"); + dispatchComposition(mounted.textarea, "compositionend"); + vi.runAllTimers(); + }); + + expect(changes).toEqual(["你好"]); + }); +}); diff --git a/packages/app/src/composer/input/text-input.web.tsx b/packages/app/src/composer/input/text-input.web.tsx new file mode 100644 index 0000000000..5b46172ee2 --- /dev/null +++ b/packages/app/src/composer/input/text-input.web.tsx @@ -0,0 +1,107 @@ +import React, { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useRef, + useState, +} from "react"; +import { TextInput } from "react-native"; +import { withUnistyles } from "react-native-unistyles"; +import type { ComposerTextInputHandle, ComposerTextInputProps } from "./text-input-types"; + +interface WebTextInputElement extends TextInput { + value?: string; + setSelectionRange?: (start: number, end: number) => void; + addEventListener: (type: "compositionstart" | "compositionend", listener: EventListener) => void; + removeEventListener: ( + type: "compositionstart" | "compositionend", + listener: EventListener, + ) => void; +} + +const ThemedTextInput = withUnistyles(TextInput, (theme) => ({ + placeholderTextColor: theme.colors.surface4, +})); + +export const ComposerTextInput = forwardRef( + function ComposerTextInputWeb( + { text, onChangeText, onPasteImages: _, onPasteError: __, ...props }, + ref, + ) { + const inputRef = useRef(null); + const textRef = useRef(text); + const onChangeTextRef = useRef(onChangeText); + const [isComposing, setIsComposing] = useState(false); + if (!isComposing) textRef.current = text; + onChangeTextRef.current = onChangeText; + + useEffect(() => { + const input = inputRef.current as WebTextInputElement | null; + if (!input) return; + + let compositionEndTimer: ReturnType | null = null; + const startComposition = () => { + if (compositionEndTimer !== null) { + clearTimeout(compositionEndTimer); + compositionEndTimer = null; + } + setIsComposing(true); + }; + const endComposition = () => { + const nextText = input.value ?? ""; + if (nextText !== textRef.current) { + textRef.current = nextText; + onChangeTextRef.current(nextText); + } + compositionEndTimer = setTimeout(() => { + compositionEndTimer = null; + setIsComposing(false); + }, 0); + }; + + input.addEventListener("compositionstart", startComposition); + input.addEventListener("compositionend", endComposition); + return () => { + if (compositionEndTimer !== null) clearTimeout(compositionEndTimer); + input.removeEventListener("compositionstart", startComposition); + input.removeEventListener("compositionend", endComposition); + }; + }, []); + + const handleChangeText = useCallback( + (nextText: string) => { + if (nextText === textRef.current) return; + textRef.current = nextText; + onChangeText(nextText); + }, + [onChangeText], + ); + + useImperativeHandle(ref, () => ({ + focus: () => inputRef.current?.focus(), + blur: () => inputRef.current?.blur(), + getText: () => textRef.current, + replaceText: (nextText, selection) => { + textRef.current = nextText; + const input = inputRef.current as WebTextInputElement | null; + if (input && "value" in input) { + input.value = nextText; + } + if (selection && typeof input?.setSelectionRange === "function") { + input.setSelectionRange(selection.start, selection.end); + } + }, + getNativeRef: () => inputRef.current, + })); + + return ( + + ); + }, +); diff --git a/packages/app/src/composer/native-pasted-image.test.ts b/packages/app/src/composer/native-pasted-image.test.ts new file mode 100644 index 0000000000..59d2954a1b --- /dev/null +++ b/packages/app/src/composer/native-pasted-image.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import { normalizeNativePastedImages, UnsupportedPastedImageError } from "./native-pasted-image"; + +describe("normalizeNativePastedImages", () => { + it("turns pasted native image files into the existing picked-image input", () => { + expect( + normalizeNativePastedImages([ + { + fileName: "clipboard.jpg", + fileSize: 128, + type: "image/jpg", + uri: "file:///cache/clipboard.jpg", + }, + { + fileName: "animation.gif", + fileSize: 256, + type: "image/gif", + uri: "file:///cache/animation.gif", + }, + ]), + ).toEqual([ + { + source: { kind: "file_uri", uri: "file:///cache/clipboard.jpg" }, + mimeType: "image/jpeg", + fileName: "clipboard.jpg", + }, + { + source: { kind: "file_uri", uri: "file:///cache/animation.gif" }, + mimeType: "image/gif", + fileName: "animation.gif", + }, + ]); + }); + + it("rejects non-image content instead of swallowing it as an attachment", () => { + expect(() => + normalizeNativePastedImages([ + { + fileName: "notes.txt", + fileSize: 12, + type: "text/plain", + uri: "file:///cache/notes.txt", + }, + ]), + ).toThrow(UnsupportedPastedImageError); + }); +}); diff --git a/packages/app/src/composer/native-pasted-image.ts b/packages/app/src/composer/native-pasted-image.ts new file mode 100644 index 0000000000..34f3d822b3 --- /dev/null +++ b/packages/app/src/composer/native-pasted-image.ts @@ -0,0 +1,35 @@ +import { resolveRasterImageMimeType } from "@/attachments/file-types"; +import type { PickedImageAttachmentInput } from "@/hooks/image-attachment-picker"; + +export interface NativePastedFile { + fileName: string; + fileSize: number; + type: string; + uri: string; +} + +export class UnsupportedPastedImageError extends Error { + constructor(fileName: string) { + super(`Unsupported pasted image '${fileName}'.`); + this.name = "UnsupportedPastedImageError"; + } +} + +export function normalizeNativePastedImages( + files: readonly NativePastedFile[], +): PickedImageAttachmentInput[] { + return files.map((file) => { + const mimeType = resolveRasterImageMimeType({ + mimeType: file.type, + path: file.fileName, + }); + if (!mimeType) { + throw new UnsupportedPastedImageError(file.fileName); + } + return { + source: { kind: "file_uri", uri: file.uri }, + mimeType, + fileName: file.fileName, + }; + }); +} diff --git a/packages/app/src/composer/task-list/index.tsx b/packages/app/src/composer/task-list/index.tsx new file mode 100644 index 0000000000..4a991abdc8 --- /dev/null +++ b/packages/app/src/composer/task-list/index.tsx @@ -0,0 +1,116 @@ +import { memo, useCallback, useMemo, useState } from "react"; +import { View } from "react-native"; +import { ChevronDown, ChevronRight } from "lucide-react-native"; +import { useTranslation } from "react-i18next"; +import { StyleSheet } from "react-native-unistyles"; +import { Button } from "@/components/ui/button"; +import { TaskListRow } from "@/components/task-list-row"; +import { MAX_CONTENT_WIDTH } from "@/constants/layout"; +import { useSessionStore } from "@/stores/session-store"; +import type { TodoEntry } from "@/types/stream"; + +export const AgentTaskList = memo(function AgentTaskList({ + serverId, + agentId, +}: { + serverId: string; + agentId: string; +}) { + const tasks = useSessionStore((state) => state.sessions[serverId]?.agentTasks.get(agentId)); + if (!tasks?.length) return null; + return ; +}); + +const TaskListCard = memo(function TaskListCard({ tasks }: { tasks: TodoEntry[] }) { + const { t } = useTranslation(); + const [expanded, setExpanded] = useState(false); + const completed = useMemo( + () => tasks.filter((task) => task.completed || task.status === "completed").length, + [tasks], + ); + const currentTask = useMemo( + () => + tasks.find((task) => task.status === "in_progress") ?? + tasks.find((task) => !task.completed && task.status !== "completed"), + [tasks], + ); + const currentTaskText = + currentTask?.status === "in_progress" && currentTask.activeForm + ? currentTask.activeForm + : currentTask?.text; + const label = currentTaskText + ? t("message.todo.tasksProgressCurrent", { + completed, + total: tasks.length, + task: currentTaskText, + }) + : t("message.todo.tasksProgress", { completed, total: tasks.length }); + const toggle = useCallback(() => setExpanded((current) => !current), []); + const accessibilityState = useMemo(() => ({ expanded }), [expanded]); + + return ( + + + + + {expanded ? ( + + {tasks.map((task, index) => ( + + ))} + + ) : null} + + + + ); +}); + +const styles = StyleSheet.create((theme) => ({ + container: { + alignItems: "center", + paddingHorizontal: theme.spacing[4], + }, + track: { + width: "100%", + maxWidth: MAX_CONTENT_WIDTH, + marginBottom: -theme.spacing[4], + }, + card: { + backgroundColor: theme.colors.surface1, + borderWidth: theme.borderWidth[1], + borderColor: theme.colors.border, + borderBottomWidth: 0, + borderTopLeftRadius: theme.borderRadius["2xl"], + borderTopRightRadius: theme.borderRadius["2xl"], + overflow: "hidden", + paddingBottom: theme.spacing[4], + }, + header: { + width: "100%", + justifyContent: "flex-start", + borderRadius: 0, + paddingHorizontal: theme.spacing[3], + }, + headerText: { + color: theme.colors.foregroundMuted, + }, + list: { + borderTopWidth: theme.borderWidth[1], + borderTopColor: theme.colors.border, + paddingHorizontal: theme.spacing[3], + paddingTop: theme.spacing[2], + gap: theme.spacing[1], + }, +})); diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index 8a93e9626f..2de5bc5236 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -5,12 +5,12 @@ import { useQueryClient } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; import { useClientActivity } from "@/hooks/use-client-activity"; import { useAppVisible } from "@/hooks/use-app-visible"; -import { usePushTokenRegistration } from "@/hooks/use-push-token-registration"; +import { startPushNotifications } from "@/push-notifications"; import { createSetAgentInitializing, refreshAgentInitializationTimeout, } from "@/hooks/use-agent-initialization"; -import { generateMessageId, type StreamItem } from "@/types/stream"; +import type { StreamItem } from "@/types/stream"; import { createSessionAgentStreamReducerQueue, deriveAgentStreamTurnLiveness, @@ -43,7 +43,6 @@ import type { AudioPlaybackSource } from "@/voice/audio-engine-types"; import { selectAgentTimelineState, useSessionStore, - type MessageEntry, type SessionState, } from "@/stores/session-store"; import { useWorkspaceSetupStore } from "@/stores/workspace-setup-store"; @@ -71,7 +70,6 @@ import { revalidateSessionAfterResume } from "@/contexts/session-resume-revalida // Re-export types from session-store and draft-store for backward compatibility export type { DraftInput } from "@/stores/draft-store"; export type { - MessageEntry, Agent, ExplorerEntry, ExplorerFile, @@ -275,30 +273,6 @@ function finalizeTimelineApplication(input: { } } -function applyToolResultToMessages( - toolCallId: string, - result: unknown, -): (prev: MessageEntry[]) => MessageEntry[] { - return (prev) => - prev.map((msg) => - msg.type === "tool_call" && msg.id === toolCallId - ? { ...msg, result, status: "completed" as const } - : msg, - ); -} - -function applyToolErrorToMessages( - toolCallId: string, - error: unknown, -): (prev: MessageEntry[]) => MessageEntry[] { - return (prev) => - prev.map((msg) => - msg.type === "tool_call" && msg.id === toolCallId - ? { ...msg, error, status: "failed" as const } - : msg, - ); -} - function notifyVoiceAbortFailure( data: Extract["payload"], notifyError: (message: string) => void, @@ -342,8 +316,6 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider // Zustand store actions const setIsPlayingAudio = useSessionStore((state) => state.setIsPlayingAudio); - const setMessages = useSessionStore((state) => state.setMessages); - const setCurrentAssistantMessage = useSessionStore((state) => state.setCurrentAssistantMessage); const setAgentStreamTail = useSessionStore((state) => state.setAgentStreamTail); const setAgentStreamHead = useSessionStore((state) => state.setAgentStreamHead); const setAgentStreamState = useSessionStore((state) => state.setAgentStreamState); @@ -418,7 +390,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider // Client activity tracking (heartbeat, push token registration) useClientActivity({ client, focusedAgentId, focusedTerminalId, onAppResumed: handleAppResumed }); - usePushTokenRegistration({ client, serverId }); + useEffect(() => startPushNotifications({ client, serverId }), [client, serverId]); const notifyAgentAttention = useCallback( (params: { @@ -935,98 +907,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider const unsubActivity = client.on("activity_log", (message) => { if (message.type !== "activity_log") return; - const data = message.payload; - if (data.type === "system" && data.content.includes("Transcribing")) { - return; - } - - if (data.type === "tool_call" && data.metadata) { - const toolCallId = - typeof data.metadata.toolCallId === "string" ? data.metadata.toolCallId : ""; - const toolName = typeof data.metadata.toolName === "string" ? data.metadata.toolName : ""; - const args = data.metadata.arguments; - - setMessages(serverId, (prev) => [ - ...prev, - { - type: "tool_call", - id: toolCallId, - timestamp: Date.now(), - toolName, - args, - status: "executing", - }, - ]); - return; - } - - if (data.type === "tool_result" && data.metadata) { - const toolCallId = - typeof data.metadata.toolCallId === "string" ? data.metadata.toolCallId : ""; - const result = data.metadata.result; - - const applyToolResult = applyToolResultToMessages(toolCallId, result); - setMessages(serverId, applyToolResult); - return; - } - - if (data.type === "error" && data.metadata && "toolCallId" in data.metadata) { - const toolCallId = - typeof data.metadata.toolCallId === "string" ? data.metadata.toolCallId : ""; - const error = data.metadata.error; - - const applyToolError = applyToolErrorToMessages(toolCallId, error); - setMessages(serverId, applyToolError); - } - - notifyVoiceAbortFailure(data, toast.error); - - let activityType: "system" | "info" | "success" | "error" = "info"; - if (data.type === "error") activityType = "error"; - - if (data.type === "transcript") { - setMessages(serverId, (prev) => [ - ...prev, - { - type: "user", - id: generateMessageId(), - timestamp: Date.now(), - message: data.content, - }, - ]); - return; - } - - if (data.type === "assistant") { - setMessages(serverId, (prev) => [ - ...prev, - { - type: "assistant", - id: generateMessageId(), - timestamp: Date.now(), - message: data.content, - }, - ]); - setCurrentAssistantMessage(serverId, ""); - return; - } - - setMessages(serverId, (prev) => [ - ...prev, - { - type: "activity", - id: generateMessageId(), - timestamp: Date.now(), - activityType, - message: data.content, - metadata: data.metadata, - }, - ]); - }); - - const unsubChunk = client.on("assistant_chunk", (message) => { - if (message.type !== "assistant_chunk") return; - setCurrentAssistantMessage(serverId, (prev) => prev + message.payload.chunk); + notifyVoiceAbortFailure(message.payload, toast.error); }); const unsubTranscription = client.on("transcription_result", (message) => { @@ -1034,11 +915,6 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider const transcriptText = message.payload.text.trim(); voiceRuntime?.onTranscriptionResult(serverId, transcriptText); - if (!transcriptText) { - return; - } - - setCurrentAssistantMessage(serverId, ""); }); const unsubVoiceInputState = client.on("voice_input_state", (message) => { @@ -1081,7 +957,6 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider unsubPermissionResolved(); unsubAudioOutput(); unsubActivity(); - unsubChunk(); unsubTranscription(); unsubVoiceInputState(); unsubTerminalAttention(); @@ -1092,8 +967,6 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider queryClient, serverId, setIsPlayingAudio, - setMessages, - setCurrentAssistantMessage, setAgentStreamTail, setAgentStreamHead, setAgentStreamState, diff --git a/packages/app/src/contexts/sidebar-callout-context.tsx b/packages/app/src/contexts/sidebar-callout-context.tsx index 2e337aadd8..548a32edc5 100644 --- a/packages/app/src/contexts/sidebar-callout-context.tsx +++ b/packages/app/src/contexts/sidebar-callout-context.tsx @@ -13,9 +13,9 @@ import { useStableEvent } from "@/hooks/use-stable-event"; import { clearSidebarCallouts, createSidebarCalloutState, + DismissedCalloutKeysSchema, dismissSidebarCallout, loadDismissedCalloutKeys, - parseDismissedCalloutKeys, selectActiveSidebarCallout, serializeDismissedCalloutKeys, showSidebarCallout, @@ -24,6 +24,7 @@ import { type SidebarCalloutState, unregisterSidebarCallout, } from "./sidebar-callout-state"; +import { readValidatedJson } from "@/storage/validated-storage"; export type { SidebarCalloutOptions } from "./sidebar-callout-state"; @@ -62,8 +63,12 @@ export function SidebarCalloutProvider({ children }: { children: ReactNode }) { async function loadDismissedKeys(): Promise { let dismissedKeys: ReadonlySet; try { - const value = await AsyncStorage.getItem(DISMISSED_CALLOUTS_STORAGE_KEY); - dismissedKeys = parseDismissedCalloutKeys(value); + const value = await readValidatedJson( + AsyncStorage, + DISMISSED_CALLOUTS_STORAGE_KEY, + DismissedCalloutKeysSchema, + ); + dismissedKeys = new Set(value ?? []); } catch (error) { console.error("[SidebarCallouts] Failed to load dismissed callouts", error); dismissedKeys = stateRef.current.dismissedKeys; diff --git a/packages/app/src/contexts/sidebar-callout-state.test.ts b/packages/app/src/contexts/sidebar-callout-state.test.ts index 9b7d1a13aa..f73468f0b3 100644 --- a/packages/app/src/contexts/sidebar-callout-state.test.ts +++ b/packages/app/src/contexts/sidebar-callout-state.test.ts @@ -4,7 +4,6 @@ import { createSidebarCalloutState, dismissSidebarCallout, loadDismissedCalloutKeys, - parseDismissedCalloutKeys, selectActiveSidebarCallout, serializeDismissedCalloutKeys, showSidebarCallout, @@ -118,12 +117,6 @@ describe("sidebar callout state", () => { expect(selectActiveSidebarCallout(state)?.title).toBe("Update available"); }); - it("parses stored dismissal keys defensively", () => { - expect(parseDismissedCalloutKeys(JSON.stringify(["a", 4, "b"]))).toEqual(new Set(["a", "b"])); - expect(parseDismissedCalloutKeys("{")).toEqual(new Set()); - expect(parseDismissedCalloutKeys(JSON.stringify({ key: "a" }))).toEqual(new Set()); - }); - it("clears visible callouts without dropping dismissal state", () => { let state = loadDismissedCalloutKeys(createSidebarCalloutState(), new Set(["dismissed"])); state = showSidebarCallout(state, { id: "visible", title: "Visible" }).state; diff --git a/packages/app/src/contexts/sidebar-callout-state.ts b/packages/app/src/contexts/sidebar-callout-state.ts index fa1da011ab..0cd83c53fd 100644 --- a/packages/app/src/contexts/sidebar-callout-state.ts +++ b/packages/app/src/contexts/sidebar-callout-state.ts @@ -1,4 +1,5 @@ import type { ReactNode } from "react"; +import { z } from "zod"; import type { SidebarCalloutAction, SidebarCalloutVariant } from "@/components/sidebar-callout"; export interface SidebarCalloutOptions { @@ -44,21 +45,7 @@ export function normalizeDismissalKey(key: string | null | undefined): string | return trimmed ? trimmed : null; } -export function parseDismissedCalloutKeys(value: string | null): Set { - if (!value) { - return new Set(); - } - - try { - const parsed = JSON.parse(value) as unknown; - if (!Array.isArray(parsed)) { - return new Set(); - } - return new Set(parsed.filter((entry): entry is string => typeof entry === "string")); - } catch { - return new Set(); - } -} +export const DismissedCalloutKeysSchema = z.array(z.string()); export function serializeDismissedCalloutKeys(keys: ReadonlySet): string { return JSON.stringify([...keys]); diff --git a/packages/app/src/create-agent-preferences/preferences.test.ts b/packages/app/src/create-agent-preferences/preferences.test.ts index 64ace339e5..33bdd1dbc5 100644 --- a/packages/app/src/create-agent-preferences/preferences.test.ts +++ b/packages/app/src/create-agent-preferences/preferences.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { CreateAgentPreferencesService } from "./service"; import { + applyAgentProfilePreferences, mergeCreateAgentSelectionPreferences, mergeProviderPreferences, parseFormPreferences, @@ -133,10 +134,84 @@ describe("create agent preferences", () => { }); }); + it("erases a saved mode when a complete selection explicitly has no mode", () => { + expect( + mergeCreateAgentSelectionPreferences({ + preferences: { + provider: "pi", + providerPreferences: { pi: { model: "anthropic/sonnet", mode: "full-access" } }, + }, + provider: "pi", + modelId: "anthropic/sonnet", + modeId: null, + }), + ).toEqual({ + provider: "pi", + providerPreferences: { pi: { model: "anthropic/sonnet" } }, + }); + }); + + it("repairs the previous provider while applying a profile", () => { + expect( + applyAgentProfilePreferences({ + preferences: { + provider: "pi", + providerPreferences: { + pi: { model: "anthropic/sonnet", mode: "full-access" }, + mock: { model: "ten-second-stream", mode: "load-test" }, + }, + }, + previousProvider: "pi", + previousProviderModeIds: [], + provider: "mock", + modelId: "one-minute-stream", + modeId: "approval-test", + thinkingOptionId: "", + featureValues: {}, + }), + ).toEqual({ + provider: "mock", + providerPreferences: { + pi: { model: "anthropic/sonnet" }, + mock: { model: "one-minute-stream", mode: "approval-test", featureValues: {} }, + }, + }); + }); + it("loads invalid stored preferences as empty preferences", () => { expect(parseFormPreferences({ providerPreferences: { codex: { mode: 42 } } })).toEqual({}); }); + it("strips the explicitly supported legacy location fields", () => { + expect( + parseFormPreferences({ + workingDir: "/old/workspace", + provider: "codex", + providerPreferences: { + codex: { + model: "gpt-5.4-mini", + mode: "full-access", + thinkingOptionId: "high", + }, + }, + serverId: "old-host", + }), + ).toEqual({ + provider: "codex", + providerPreferences: { + codex: { + model: "gpt-5.4-mini", + mode: "full-access", + thinkingByModel: { "gpt-5.4-mini": "high" }, + }, + }, + }); + }); + + it("rejects unknown persisted fields outside the explicit legacy shape", () => { + expect(parseFormPreferences({ provider: "codex", surprise: true })).toEqual({}); + }); + it("persists and reloads the workspace runtime choice", async () => { const storage = new FakeCreateAgentPreferenceStorage(); const preferences = new CreateAgentPreferencesService(storage); @@ -165,6 +240,23 @@ describe("create agent preferences", () => { ).toEqual({ provider: "codex", runtimeId: "fixture" }); }); + it("preserves legacy favourites across preference writes until host migration", async () => { + const favoriteModels = [{ provider: "claude", modelId: "opus" }]; + const storage = new FakeCreateAgentPreferenceStorage({ stored: { favoriteModels } }); + const preferences = new CreateAgentPreferencesService(storage); + + const save = preferences.update({ runtimeId: "worktree" }); + await storage.nextWrite(); + storage.finishOldestWrite(); + await save; + + expect(storage.savedPreferences()).toEqual({ favoriteModels, runtimeId: "worktree" }); + }); + + it("treats stored preferences without a runtime choice as undefined", () => { + expect(parseFormPreferences({ provider: "codex" }).runtimeId).toBeUndefined(); + }); + it("rejects an unknown isolation value as invalid stored preferences", () => { expect(parseFormPreferences({ provider: "codex", isolation: "sandbox" })).toEqual({}); }); diff --git a/packages/app/src/create-agent-preferences/preferences.ts b/packages/app/src/create-agent-preferences/preferences.ts index 48979458b6..699740d497 100644 --- a/packages/app/src/create-agent-preferences/preferences.ts +++ b/packages/app/src/create-agent-preferences/preferences.ts @@ -1,38 +1,46 @@ import { z } from "zod"; import type { AgentProvider } from "@getpaseo/protocol/agent-types"; -export interface FavoriteModelPreference { - provider: string; - modelId: string; +const featureValuesSchema = z.record(z.string(), z.union([z.boolean(), z.string(), z.null()])); + +export interface ProviderPreferences { + model?: string; + mode?: string; + thinkingByModel?: Record; + featureValues?: Record; } -export interface FavoriteModelRow { - favoriteKey: string; - provider: string; - providerLabel: string; - modelId: string; - modelLabel: string; - description?: string; +export type LaunchTarget = { kind: "chat" } | { kind: "terminal"; profileId: string }; + +export interface FormPreferences { + provider?: string; + providerPreferences?: Record; + favoriteModels?: Array<{ provider: string; modelId: string }>; + runtimeId?: string; + launchTarget?: LaunchTarget; } -const providerPreferencesSchema = z.object({ +const providerPreferencesSchema: z.ZodType = z.strictObject({ model: z.string().optional(), mode: z.string().optional(), thinkingByModel: z.record(z.string(), z.string()).optional(), - featureValues: z.record(z.string(), z.unknown()).optional(), + featureValues: featureValuesSchema.optional(), }); -const launchTargetSchema = z.discriminatedUnion("kind", [ - z.object({ kind: z.literal("chat") }), - z.object({ kind: z.literal("terminal"), profileId: z.string() }), +const launchTargetSchema: z.ZodType = z.discriminatedUnion("kind", [ + z.strictObject({ kind: z.literal("chat") }), + z.strictObject({ kind: z.literal("terminal"), profileId: z.string() }), ]); -const formPreferencesSchema = z.object({ +export const FormPreferencesSchema = z.strictObject({ provider: z.string().optional(), providerPreferences: z.record(z.string(), providerPreferencesSchema).optional(), + // COMPAT(agentProfileFavoriteMigration): favourites were removed in v0.3.2. + // Keep the legacy payload alive until every capable host has had a chance to + // import it; ordinary preference writes must not erase it first. favoriteModels: z .array( - z.object({ + z.strictObject({ provider: z.string(), modelId: z.string(), }), @@ -42,25 +50,58 @@ const formPreferencesSchema = z.object({ // What the New workspace composer submits to: the chat agent (default) or a // terminal profile. See `@/new-workspace-launch` for resolution/fallback. launchTarget: launchTargetSchema.optional(), +}) satisfies z.ZodType; + +const LegacyProviderPreferencesSchema = z.strictObject({ + model: z.string().optional(), + mode: z.string().optional(), + thinkingOptionId: z.string().optional(), }); -const storedFormPreferencesSchema = formPreferencesSchema.extend({ +const CurrentStoredFormPreferencesSchema = FormPreferencesSchema.extend({ isolation: z.enum(["local", "worktree"]).optional(), }); +const LegacyFormPreferencesSchema = z + .strictObject({ + workingDir: z.string().optional(), + provider: z.string().optional(), + serverId: z.string().optional(), + providerPreferences: z.record(z.string(), LegacyProviderPreferencesSchema).optional(), + }) + .transform(({ provider, providerPreferences }): FormPreferences => { + const migratedProviderPreferences: Record = {}; + for (const [providerId, legacy] of Object.entries(providerPreferences ?? {})) { + const model = legacy.model; + migratedProviderPreferences[providerId] = { + ...(model !== undefined ? { model } : {}), + ...(legacy.mode !== undefined ? { mode: legacy.mode } : {}), + ...(model !== undefined && legacy.thinkingOptionId !== undefined + ? { thinkingByModel: { [model]: legacy.thinkingOptionId } } + : {}), + }; + } + return { + ...(provider !== undefined ? { provider } : {}), + ...(providerPreferences !== undefined + ? { providerPreferences: migratedProviderPreferences } + : {}), + }; + }); -export type ProviderPreferences = z.infer; -export type FormPreferences = z.infer; -export type LaunchTarget = z.infer; +export const StoredFormPreferencesSchema: z.ZodType = z.union([ + CurrentStoredFormPreferencesSchema.transform(({ isolation, ...preferences }) => + preferences.runtimeId !== undefined || isolation === undefined + ? preferences + : { ...preferences, runtimeId: isolation }, + ), + LegacyFormPreferencesSchema, +]); export const DEFAULT_FORM_PREFERENCES: FormPreferences = {}; export function parseFormPreferences(value: unknown): FormPreferences { - const result = storedFormPreferencesSchema.safeParse(value); - if (!result.success) return DEFAULT_FORM_PREFERENCES; - - const { isolation, ...preferences } = result.data; - if (preferences.runtimeId !== undefined || isolation === undefined) return preferences; - return { ...preferences, runtimeId: isolation }; + const result = StoredFormPreferencesSchema.safeParse(value); + return result.success ? result.data : DEFAULT_FORM_PREFERENCES; } function mergeDefinedRecord( @@ -78,7 +119,7 @@ function mergeDefinedRecord( function applyProviderPreferenceUpdates( existing: ProviderPreferences, - updates: Partial, + updates: Omit, "mode"> & { mode?: string | null }, ): ProviderPreferences { const next: ProviderPreferences = { ...existing }; const nextThinkingByModel = mergeDefinedRecord(existing.thinkingByModel, updates.thinkingByModel); @@ -87,7 +128,9 @@ function applyProviderPreferenceUpdates( if (updates.model !== undefined) { next.model = updates.model; } - if (updates.mode !== undefined) { + if (updates.mode === null) { + delete next.mode; + } else if (updates.mode !== undefined) { next.mode = updates.mode; } if (nextThinkingByModel !== undefined) { @@ -103,7 +146,7 @@ function applyProviderPreferenceUpdates( export function mergeProviderPreferences(args: { preferences: FormPreferences; provider: AgentProvider; - updates: Partial; + updates: Omit, "mode"> & { mode?: string | null }; }): FormPreferences { const { preferences, provider, updates } = args; const existingProviderPreferences = preferences.providerPreferences ?? {}; @@ -134,50 +177,52 @@ export function mergeCreateAgentSelectionPreferences(args: { const modelId = args.modelId?.trim() ?? ""; const modeId = args.modeId?.trim() ?? ""; const thinkingOptionId = args.thinkingOptionId?.trim() ?? ""; + const featureValues = featureValuesSchema.safeParse(args.featureValues); return mergeProviderPreferences({ preferences: args.preferences, provider: args.provider, updates: { model: modelId || undefined, - mode: modeId || undefined, + mode: args.modeId === undefined ? undefined : modeId || null, ...(modelId && thinkingOptionId ? { thinkingByModel: { [modelId]: thinkingOptionId } } : {}), - ...(args.featureValues ? { featureValues: args.featureValues } : {}), + ...(featureValues.success ? { featureValues: featureValues.data } : {}), }, }); } -export function buildFavoriteModelKey(input: FavoriteModelPreference): string { - return `${input.provider}:${input.modelId}`; -} - -export function isFavoriteModel(args: { +export function applyAgentProfilePreferences(args: { preferences: FormPreferences; - provider: string; - modelId: string; -}): boolean { - const favoriteKey = buildFavoriteModelKey({ provider: args.provider, modelId: args.modelId }); - return (args.preferences.favoriteModels ?? []).some( - (favorite) => buildFavoriteModelKey(favorite) === favoriteKey, - ); -} - -export function toggleFavoriteModel(args: { - preferences: FormPreferences; - provider: string; + previousProvider: AgentProvider | null; + previousProviderModeIds: readonly string[]; + provider: AgentProvider; modelId: string; + modeId: string; + thinkingOptionId: string; + featureValues: Record; }): FormPreferences { - const favorite = { provider: args.provider, modelId: args.modelId }; - const favoriteKey = buildFavoriteModelKey(favorite); - const existingFavorites = args.preferences.favoriteModels ?? []; - const hasFavorite = existingFavorites.some( - (entry) => buildFavoriteModelKey(entry) === favoriteKey, - ); + let next = args.preferences; + if (args.previousProvider) { + const previousMode = next.providerPreferences?.[args.previousProvider]?.mode; + if (previousMode && !args.previousProviderModeIds.includes(previousMode)) { + next = mergeProviderPreferences({ + preferences: next, + provider: args.previousProvider, + updates: { mode: null }, + }); + } + } - return { - ...args.preferences, - favoriteModels: hasFavorite - ? existingFavorites.filter((entry) => buildFavoriteModelKey(entry) !== favoriteKey) - : [...existingFavorites, favorite], - }; + return mergeProviderPreferences({ + preferences: next, + provider: args.provider, + updates: { + model: args.modelId || undefined, + mode: args.modeId || null, + ...(args.modelId && args.thinkingOptionId + ? { thinkingByModel: { [args.modelId]: args.thinkingOptionId } } + : {}), + featureValues: args.featureValues, + }, + }); } diff --git a/packages/app/src/create-agent-preferences/storage.ts b/packages/app/src/create-agent-preferences/storage.ts index 2dd2311423..951c9efcf0 100644 --- a/packages/app/src/create-agent-preferences/storage.ts +++ b/packages/app/src/create-agent-preferences/storage.ts @@ -1,5 +1,10 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; -import type { FormPreferences } from "./preferences"; +import { readValidatedJson } from "@/storage/validated-storage"; +import { + FormPreferencesSchema, + StoredFormPreferencesSchema, + type FormPreferences, +} from "./preferences"; export const CREATE_AGENT_PREFERENCES_STORAGE_KEY = "@paseo:create-agent-preferences"; @@ -10,19 +15,19 @@ export interface CreateAgentPreferenceStorage { export class AsyncStorageCreateAgentPreferenceStorage implements CreateAgentPreferenceStorage { async read(): Promise { - const stored = await AsyncStorage.getItem(CREATE_AGENT_PREFERENCES_STORAGE_KEY); - if (!stored) { - return null; - } - - try { - return JSON.parse(stored); - } catch { - return null; - } + return readValidatedJson( + AsyncStorage, + CREATE_AGENT_PREFERENCES_STORAGE_KEY, + StoredFormPreferencesSchema, + ); } async write(preferences: FormPreferences): Promise { - await AsyncStorage.setItem(CREATE_AGENT_PREFERENCES_STORAGE_KEY, JSON.stringify(preferences)); + const result = FormPreferencesSchema.safeParse(preferences); + if (!result.success) { + await AsyncStorage.removeItem(CREATE_AGENT_PREFERENCES_STORAGE_KEY); + return; + } + await AsyncStorage.setItem(CREATE_AGENT_PREFERENCES_STORAGE_KEY, JSON.stringify(result.data)); } } diff --git a/packages/app/src/create-agent-preferences/test-utils/fake-preference-storage.ts b/packages/app/src/create-agent-preferences/test-utils/fake-preference-storage.ts index 6b0fcf245a..84d7cc53df 100644 --- a/packages/app/src/create-agent-preferences/test-utils/fake-preference-storage.ts +++ b/packages/app/src/create-agent-preferences/test-utils/fake-preference-storage.ts @@ -73,5 +73,5 @@ export class FakeCreateAgentPreferenceStorage implements CreateAgentPreferenceSt } function clone(value: T): T { - return JSON.parse(JSON.stringify(value)) as T; + return structuredClone(value); } diff --git a/packages/app/src/data/provider-snapshot-cache.test.ts b/packages/app/src/data/provider-snapshot-cache.test.ts index 8136735f32..ea608e035f 100644 --- a/packages/app/src/data/provider-snapshot-cache.test.ts +++ b/packages/app/src/data/provider-snapshot-cache.test.ts @@ -1,24 +1,153 @@ import { describe, expect, it } from "vitest"; +import { Buffer } from "buffer"; import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types"; import { compactProviderSnapshot } from "@getpaseo/protocol/provider-snapshot-codec"; -import { createProviderSnapshotCache } from "./provider-snapshot-cache"; +import { z } from "zod"; +import { createProviderSnapshotCache, type ProviderSnapshotCache } from "./provider-snapshot-cache"; +import type { ProviderSnapshotCacheScope } from "./provider-snapshot-cache"; -function createStorage() { +const SNAPSHOT_KEY_PREFIX = "@paseo/provider-snapshot/v1:"; +const SNAPSHOT_INDEX_KEY = "@paseo/provider-snapshot-index/v1"; +const SnapshotIndexSchema = z.object({ + version: z.literal(1), + entries: z.array(z.object({ key: z.string(), bytes: z.number(), writtenAt: z.string() })), +}); + +function legacyScope(cwd: string | null): ProviderSnapshotCacheScope { + return { type: "legacy-cwd", cwd }; +} + +function snapshotKey(serverId: string, cwd: string | null): string { + return `${SNAPSHOT_KEY_PREFIX}${JSON.stringify([serverId, legacyScope(cwd)])}`; +} + +function createStorage(maxSnapshotBytes = Number.POSITIVE_INFINITY) { const values = new Map(); + const stats = { getAllKeysCalls: 0 }; + type Operation = "setItem" | "removeItem" | "multiRemove"; + let failure: { operation: Operation; timing: "before" | "after"; key?: string } | null = null; + + function takeFailure(operation: Operation, timing: "before" | "after", key?: string): boolean { + if ( + failure?.operation !== operation || + failure.timing !== timing || + (failure.key !== undefined && failure.key !== key) + ) { + return false; + } + failure = null; + return true; + } + return { values, + stats, + failNext(operation: Operation, timing: "before" | "after", key?: string) { + failure = { operation, timing, key }; + }, async getItem(key: string) { return values.get(key) ?? null; }, async setItem(key: string, value: string) { + if (takeFailure("setItem", "before", key)) throw new Error("Injected setItem failure"); + if (key.startsWith(SNAPSHOT_KEY_PREFIX)) { + const bytesWithoutReplacement = snapshotBytes( + new Map([...values].filter(([storedKey]) => storedKey !== key)), + ); + if ( + bytesWithoutReplacement + + Buffer.byteLength(key, "utf8") + + Buffer.byteLength(value, "utf8") > + maxSnapshotBytes + ) { + throw new Error("Injected storage capacity failure"); + } + } values.set(key, value); + if (takeFailure("setItem", "after", key)) throw new Error("Injected setItem failure"); }, async removeItem(key: string) { + if (takeFailure("removeItem", "before", key)) throw new Error("Injected removeItem failure"); values.delete(key); + if (takeFailure("removeItem", "after", key)) throw new Error("Injected removeItem failure"); + }, + async getAllKeys() { + stats.getAllKeysCalls += 1; + return [...values.keys()]; + }, + async multiGet(keys: readonly string[]) { + return keys.map((key) => [key, values.get(key) ?? null] as const); + }, + async multiRemove(keys: readonly string[]) { + if (takeFailure("multiRemove", "before")) throw new Error("Injected multiRemove failure"); + for (const [index, key] of keys.entries()) { + values.delete(key); + if (index === 0 && takeFailure("multiRemove", "after")) { + throw new Error("Injected partial multiRemove failure"); + } + } }, }; } +function snapshotEntries(label: string): ProviderSnapshotEntry[] { + return [ + { + provider: "pi", + status: "ready", + enabled: true, + models: [ + { + provider: "pi", + id: label, + label: label.repeat(20), + thinkingOptions: [], + }, + ], + }, + ]; +} + +function snapshotBytes(values: Map): number { + return [...values] + .filter(([key]) => key.startsWith(SNAPSHOT_KEY_PREFIX)) + .reduce( + (total, [key, value]) => + total + Buffer.byteLength(key, "utf8") + Buffer.byteLength(value, "utf8"), + 0, + ); +} + +function expectIndexMatchesSnapshots(values: Map): void { + const storedIndex = values.get(SNAPSHOT_INDEX_KEY); + if (!storedIndex) throw new Error("Expected provider snapshot index"); + const index = SnapshotIndexSchema.parse(JSON.parse(storedIndex)); + const storedSnapshots = [...values] + .filter(([key]) => key.startsWith(SNAPSHOT_KEY_PREFIX)) + .map(([key, value]) => ({ + key, + bytes: Buffer.byteLength(key, "utf8") + Buffer.byteLength(value, "utf8"), + })) + .toSorted((left, right) => left.key.localeCompare(right.key)); + const indexedSnapshots = index.entries + .map(({ key, bytes }) => ({ key, bytes })) + .toSorted((left, right) => left.key.localeCompare(right.key)); + expect(indexedSnapshots).toEqual(storedSnapshots); +} + +function writeSnapshot( + cache: ProviderSnapshotCache, + input: { cwd: string; label: string; generatedAt: string }, +): Promise { + return cache.write({ + serverId: "server-1", + scope: legacyScope(input.cwd), + hash: input.label, + generatedAt: input.generatedAt, + compactSnapshot: compactProviderSnapshot(snapshotEntries(input.label)), + }); +} + describe("provider snapshot cache", () => { it("round-trips compact snapshots with shared thinking option references", async () => { const thinkingOptions = [{ id: "high", label: "High", isDefault: true }]; @@ -63,9 +192,359 @@ describe("provider snapshot cache", () => { ); await expect(cache.read("server-1", { type: "legacy-cwd", cwd: "/repo" })).resolves.toBeNull(); - expect(storage.values.size).toBe(0); + expect([...storage.values.keys()].some((key) => key.startsWith(SNAPSHOT_KEY_PREFIX))).toBe( + false, + ); }); + it("evicts the oldest snapshots to keep the cache within its byte budget", async () => { + const storage = createStorage(); + const maxBytes = 1_000; + const cache = createProviderSnapshotCache(storage, { maxBytes }); + + await Promise.all([ + cache.write({ + serverId: "server-1", + scope: legacyScope("/oldest"), + hash: "oldest", + generatedAt: "2026-08-01T00:00:00.000Z", + compactSnapshot: compactProviderSnapshot(snapshotEntries("oldest")), + }), + cache.write({ + serverId: "server-1", + scope: legacyScope("/middle"), + hash: "middle", + generatedAt: "2026-08-02T00:00:00.000Z", + compactSnapshot: compactProviderSnapshot(snapshotEntries("middle")), + }), + cache.write({ + serverId: "server-1", + scope: legacyScope("/newest"), + hash: "newest", + generatedAt: "2026-08-03T00:00:00.000Z", + compactSnapshot: compactProviderSnapshot(snapshotEntries("newest")), + }), + ]); + + await expect(cache.read("server-1", legacyScope("/oldest"))).resolves.toBeNull(); + await expect(cache.read("server-1", legacyScope("/newest"))).resolves.not.toBeNull(); + expect(snapshotBytes(storage.values)).toBeLessThanOrEqual(maxBytes); + expectIndexMatchesSnapshots(storage.values); + expect(storage.stats.getAllKeysCalls).toBe(1); + }); + + it("clears unindexed snapshots once when creating the cache index", async () => { + const storage = createStorage(); + storage.values.set('@paseo/provider-snapshot/v1:["server-1","/legacy"]', "legacy"); + const cache = createProviderSnapshotCache(storage); + + await cache.write({ + serverId: "server-1", + scope: legacyScope("/current"), + hash: "current", + generatedAt: "2026-08-03T00:00:00.000Z", + compactSnapshot: compactProviderSnapshot(snapshotEntries("current")), + }); + + expect(storage.values.has('@paseo/provider-snapshot/v1:["server-1","/legacy"]')).toBe(false); + await expect(cache.read("server-1", legacyScope("/current"))).resolves.not.toBeNull(); + expectIndexMatchesSnapshots(storage.values); + }); + + it("clears unindexed snapshots on the first read", async () => { + const storage = createStorage(); + storage.values.set('@paseo/provider-snapshot/v1:["server-1","/legacy"]', "legacy"); + const cache = createProviderSnapshotCache(storage); + + await expect(cache.read("server-1", legacyScope("/legacy"))).resolves.toBeNull(); + + expect(storage.values.has('@paseo/provider-snapshot/v1:["server-1","/legacy"]')).toBe(false); + expectIndexMatchesSnapshots(storage.values); + expect(storage.stats.getAllKeysCalls).toBe(1); + }); + + it("initializes the cache index once when the first read and write race", async () => { + const storage = createStorage(); + storage.values.set('@paseo/provider-snapshot/v1:["server-1","/legacy"]', "legacy"); + const cache = createProviderSnapshotCache(storage); + + await Promise.all([ + cache.read("server-1", legacyScope("/legacy")), + writeSnapshot(cache, { + cwd: "/current", + label: "current", + generatedAt: "2026-08-02T00:00:00.000Z", + }), + ]); + + await expect(cache.read("server-1", legacyScope("/current"))).resolves.not.toBeNull(); + expectIndexMatchesSnapshots(storage.values); + expect(storage.stats.getAllKeysCalls).toBe(1); + }); + + it("replaces an indexed snapshot without counting both versions", async () => { + const storage = createStorage(); + const maxBytes = 1_000; + const cache = createProviderSnapshotCache(storage, { maxBytes }); + + await writeSnapshot(cache, { + cwd: "/repo", + label: "first", + generatedAt: "2026-08-01T00:00:00.000Z", + }); + await writeSnapshot(cache, { + cwd: "/repo", + label: "replacement-is-larger", + generatedAt: "2026-08-02T00:00:00.000Z", + }); + + await expect(cache.read("server-1", legacyScope("/repo"))).resolves.toMatchObject({ + hash: "replacement-is-larger", + }); + expect(snapshotBytes(storage.values)).toBeLessThanOrEqual(maxBytes); + expect( + [...storage.values.keys()].filter((key) => key.startsWith(SNAPSHOT_KEY_PREFIX)), + ).toHaveLength(1); + expectIndexMatchesSnapshots(storage.values); + }); + + it("removes the previous value when a replacement exceeds the entire budget", async () => { + const storage = createStorage(); + const cache = createProviderSnapshotCache(storage, { maxBytes: 1_000 }); + await writeSnapshot(cache, { + cwd: "/repo", + label: "cached", + generatedAt: "2026-08-01T00:00:00.000Z", + }); + + const tinyCache = createProviderSnapshotCache(storage, { maxBytes: 1 }); + await writeSnapshot(tinyCache, { + cwd: "/repo", + label: "too-large", + generatedAt: "2026-08-02T00:00:00.000Z", + }); + + await expect(tinyCache.read("server-1", legacyScope("/repo"))).resolves.toBeNull(); + expect(snapshotBytes(storage.values)).toBe(0); + expectIndexMatchesSnapshots(storage.values); + }); + + it("resets corrupted index state before accepting another snapshot", async () => { + const storage = createStorage(); + storage.values.set(SNAPSHOT_INDEX_KEY, "corrupt"); + storage.values.set(`${SNAPSHOT_KEY_PREFIX}legacy`, "legacy"); + const cache = createProviderSnapshotCache(storage); + + await writeSnapshot(cache, { + cwd: "/current", + label: "current", + generatedAt: "2026-08-02T00:00:00.000Z", + }); + + expect(storage.values.has(`${SNAPSHOT_KEY_PREFIX}legacy`)).toBe(false); + await expect(cache.read("server-1", legacyScope("/current"))).resolves.not.toBeNull(); + expectIndexMatchesSnapshots(storage.values); + expect(storage.stats.getAllKeysCalls).toBe(1); + }); + + it("measures UTF-8 bytes rather than JavaScript string length", async () => { + const stagingStorage = createStorage(); + const stagingCache = createProviderSnapshotCache(stagingStorage); + await writeSnapshot(stagingCache, { + cwd: "/emoji", + label: "🧨".repeat(100), + generatedAt: "2026-08-02T00:00:00.000Z", + }); + const storedSnapshot = [...stagingStorage.values].find(([key]) => + key.startsWith(SNAPSHOT_KEY_PREFIX), + ); + if (!storedSnapshot) throw new Error("Expected staged provider snapshot"); + const [key, value] = storedSnapshot; + const codeUnitBytes = key.length + value.length; + const utf8Bytes = Buffer.byteLength(key, "utf8") + Buffer.byteLength(value, "utf8"); + expect(utf8Bytes).toBeGreaterThan(codeUnitBytes); + + const storage = createStorage(); + const cache = createProviderSnapshotCache(storage, { maxBytes: codeUnitBytes }); + await writeSnapshot(cache, { + cwd: "/emoji", + label: "🧨".repeat(100), + generatedAt: "2026-08-02T00:00:00.000Z", + }); + + await expect(cache.read("server-1", legacyScope("/emoji"))).resolves.toBeNull(); + expectIndexMatchesSnapshots(storage.values); + }); + + it("keeps hundreds of concurrent writes bounded without rescanning snapshots", async () => { + const storage = createStorage(); + const maxBytes = 8_000; + const cache = createProviderSnapshotCache(storage, { maxBytes }); + const writes = Array.from({ length: 250 }, (_, index) => + writeSnapshot(cache, { + cwd: `/repo-${index}`, + label: `snapshot-${index}-${"x".repeat(index % 40)}`, + generatedAt: new Date(Date.UTC(2026, 0, 1, 0, 0, index)).toISOString(), + }), + ); + + await Promise.all(writes); + + expect(snapshotBytes(storage.values)).toBeLessThanOrEqual(maxBytes); + await expect(cache.read("server-1", legacyScope("/repo-249"))).resolves.not.toBeNull(); + expectIndexMatchesSnapshots(storage.values); + expect(storage.stats.getAllKeysCalls).toBe(1); + }); + + it.each(["before", "after"] as const)( + "recovers when a snapshot write fails %s its side effect", + async (timing) => { + const storage = createStorage(); + const cache = createProviderSnapshotCache(storage, { maxBytes: 2_000 }); + await writeSnapshot(cache, { + cwd: "/stable", + label: "stable", + generatedAt: "2026-08-01T00:00:00.000Z", + }); + const failedKey = snapshotKey("server-1", "/failed"); + storage.failNext("setItem", timing, failedKey); + + await writeSnapshot(cache, { + cwd: "/failed", + label: "failed", + generatedAt: "2026-08-02T00:00:00.000Z", + }); + + const restarted = createProviderSnapshotCache(storage, { maxBytes: 2_000 }); + await writeSnapshot(restarted, { + cwd: "/next", + label: "next", + generatedAt: "2026-08-03T00:00:00.000Z", + }); + await expect(restarted.read("server-1", legacyScope("/stable"))).resolves.not.toBeNull(); + await expect(restarted.read("server-1", legacyScope("/next"))).resolves.not.toBeNull(); + expect(snapshotBytes(storage.values)).toBeLessThanOrEqual(2_000); + expectIndexMatchesSnapshots(storage.values); + }, + ); + + it.each(["before", "after"] as const)( + "reconciles snapshot records when the derived index write fails %s its side effect", + async (timing) => { + const storage = createStorage(); + const cache = createProviderSnapshotCache(storage, { maxBytes: 2_000 }); + await writeSnapshot(cache, { + cwd: "/stable", + label: "stable", + generatedAt: "2026-08-01T00:00:00.000Z", + }); + storage.failNext("setItem", timing, SNAPSHOT_INDEX_KEY); + + await writeSnapshot(cache, { + cwd: "/orphan-after-index-failure", + label: "orphan", + generatedAt: "2026-08-02T00:00:00.000Z", + }); + + const restarted = createProviderSnapshotCache(storage, { maxBytes: 2_000 }); + await expect( + restarted.read("server-1", legacyScope("/orphan-after-index-failure")), + ).resolves.not.toBeNull(); + expectIndexMatchesSnapshots(storage.values); + expect(storage.stats.getAllKeysCalls).toBe(2); + }, + ); + + it("repairs partial eviction, stale index metadata, and preserves unrelated storage", async () => { + const storage = createStorage(); + const maxBytes = 1_000; + const cache = createProviderSnapshotCache(storage, { maxBytes }); + storage.values.set("@paseo/unrelated", "keep-me"); + await writeSnapshot(cache, { + cwd: "/oldest", + label: "oldest", + generatedAt: "2026-08-01T00:00:00.000Z", + }); + await writeSnapshot(cache, { + cwd: "/middle", + label: "middle", + generatedAt: "2026-08-02T00:00:00.000Z", + }); + storage.failNext("multiRemove", "after"); + await writeSnapshot(cache, { + cwd: "/large-newest", + label: "newest", + generatedAt: "2026-08-03T00:00:00.000Z", + }); + storage.values.set( + SNAPSHOT_INDEX_KEY, + JSON.stringify({ + version: 1, + entries: [{ key: `${SNAPSHOT_KEY_PREFIX}phantom`, bytes: 999_999, writtenAt: "bad" }], + }), + ); + + const restarted = createProviderSnapshotCache(storage, { maxBytes }); + await writeSnapshot(restarted, { + cwd: "/final", + label: "final", + generatedAt: "2026-08-04T00:00:00.000Z", + }); + + expect(storage.values.get("@paseo/unrelated")).toBe("keep-me"); + expect(snapshotBytes(storage.values)).toBeLessThanOrEqual(maxBytes); + expectIndexMatchesSnapshots(storage.values); + await expect(restarted.read("server-1", legacyScope("/final"))).resolves.not.toBeNull(); + }); + + it("evicts before writing when storage has no temporary headroom", async () => { + const maxBytes = 1_000; + const storage = createStorage(maxBytes); + const cache = createProviderSnapshotCache(storage, { maxBytes }); + await writeSnapshot(cache, { + cwd: "/oldest", + label: "oldest", + generatedAt: "2026-08-01T00:00:00.000Z", + }); + await writeSnapshot(cache, { + cwd: "/middle", + label: "middle", + generatedAt: "2026-08-02T00:00:00.000Z", + }); + + await writeSnapshot(cache, { + cwd: "/newest", + label: "newest", + generatedAt: "2026-08-03T00:00:00.000Z", + }); + + await expect(cache.read("server-1", legacyScope("/newest"))).resolves.not.toBeNull(); + expect(snapshotBytes(storage.values)).toBeLessThanOrEqual(maxBytes); + expectIndexMatchesSnapshots(storage.values); + }); + + it.each(["before", "after"] as const)( + "recovers when invalid-record cleanup fails %s deletion", + async (timing) => { + const storage = createStorage(); + const key = snapshotKey("server-1", "/invalid"); + storage.values.set(key, "invalid"); + storage.failNext("multiRemove", timing); + const cache = createProviderSnapshotCache(storage); + + await expect(cache.read("server-1", legacyScope("/invalid"))).resolves.toBeNull(); + await writeSnapshot(cache, { + cwd: "/recovered", + label: "recovered", + generatedAt: "2026-08-03T00:00:00.000Z", + }); + + expect(storage.values.has(key)).toBe(false); + await expect(cache.read("server-1", legacyScope("/recovered"))).resolves.not.toBeNull(); + expectIndexMatchesSnapshots(storage.values); + }, + ); + it("keeps selected workspaces with the same cwd structurally isolated", async () => { const storage = createStorage(); const cache = createProviderSnapshotCache(storage); diff --git a/packages/app/src/data/provider-snapshot-cache.ts b/packages/app/src/data/provider-snapshot-cache.ts index b6fd29835b..a441ce19dc 100644 --- a/packages/app/src/data/provider-snapshot-cache.ts +++ b/packages/app/src/data/provider-snapshot-cache.ts @@ -1,18 +1,30 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; +import { Buffer } from "buffer"; import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types"; import { expandProviderSnapshot, type CompactProviderSnapshot, } from "@getpaseo/protocol/provider-snapshot-codec"; import { CompactProviderSnapshotSchema } from "@getpaseo/protocol/messages"; +import { z } from "zod"; const CACHE_VERSION = 1; const CACHE_KEY_PREFIX = "@paseo/provider-snapshot/v1"; +const CACHE_INDEX_KEY = "@paseo/provider-snapshot-index/v1"; +const CACHE_INDEX_VERSION = 1; +const DEFAULT_MAX_CACHE_BYTES = 4 * 1024 * 1024; interface ProviderSnapshotStorage { getItem(key: string): Promise; setItem(key: string, value: string): Promise; removeItem(key: string): Promise; + getAllKeys(): Promise; + multiGet(keys: readonly string[]): Promise>; + multiRemove(keys: readonly string[]): Promise; +} + +interface ProviderSnapshotCacheOptions { + maxBytes?: number; } interface StoredProviderSnapshot { @@ -22,6 +34,24 @@ interface StoredProviderSnapshot { compactSnapshot: CompactProviderSnapshot; } +interface ProviderSnapshotIndexEntry { + key: string; + bytes: number; + writtenAt: string; +} + +interface ProviderSnapshotIndex { + version: typeof CACHE_INDEX_VERSION; + entries: ProviderSnapshotIndexEntry[]; +} + +const StoredProviderSnapshotSchema: z.ZodType = z.strictObject({ + version: z.literal(CACHE_VERSION), + hash: z.string(), + generatedAt: z.string().datetime({ offset: true }), + compactSnapshot: CompactProviderSnapshotSchema, +}); + export interface CachedProviderSnapshot extends StoredProviderSnapshot { entries: ProviderSnapshotEntry[]; } @@ -52,77 +82,160 @@ function cacheKey(serverId: string, scope: ProviderSnapshotCacheScope): string { return `${CACHE_KEY_PREFIX}:${JSON.stringify([serverId, scope])}`; } -function parseStoredProviderSnapshot(value: string): StoredProviderSnapshot | null { - const parsed: unknown = JSON.parse(value); - if (typeof parsed !== "object" || parsed === null) return null; - const version = Reflect.get(parsed, "version"); - const hash = Reflect.get(parsed, "hash"); - const generatedAt = Reflect.get(parsed, "generatedAt"); - const compactSnapshot = CompactProviderSnapshotSchema.safeParse( - Reflect.get(parsed, "compactSnapshot"), - ); - if ( - version !== CACHE_VERSION || - typeof hash !== "string" || - typeof generatedAt !== "string" || - !compactSnapshot.success - ) { - return null; - } - return { - version, - hash, - generatedAt, - compactSnapshot: compactSnapshot.data, - }; +function storedBytes(key: string, value: string): number { + return Buffer.byteLength(key, "utf8") + Buffer.byteLength(value, "utf8"); } -async function discardCachedSnapshot(storage: ProviderSnapshotStorage, key: string): Promise { - try { - await storage.removeItem(key); - } catch (error) { - if (!(error instanceof Error)) throw error; - } +function oldestFirst(left: ProviderSnapshotIndexEntry, right: ProviderSnapshotIndexEntry): number { + const writtenAtOrder = left.writtenAt.localeCompare(right.writtenAt); + return writtenAtOrder !== 0 ? writtenAtOrder : left.key.localeCompare(right.key); +} + +function parseStoredProviderSnapshot(value: string): StoredProviderSnapshot | null { + const parsed: unknown = JSON.parse(value); + const result = StoredProviderSnapshotSchema.safeParse(parsed); + return result.success ? result.data : null; } export function createProviderSnapshotCache( storage: ProviderSnapshotStorage, + options: ProviderSnapshotCacheOptions = {}, ): ProviderSnapshotCache { + const maxBytes = options.maxBytes ?? DEFAULT_MAX_CACHE_BYTES; + let index: ProviderSnapshotIndex | null = null; + let operationQueue = Promise.resolve(); + + async function persistIndex(entries: ProviderSnapshotIndexEntry[]): Promise { + const nextIndex: ProviderSnapshotIndex = { version: CACHE_INDEX_VERSION, entries }; + await storage.setItem(CACHE_INDEX_KEY, JSON.stringify(nextIndex)); + index = nextIndex; + } + + async function reconcileCache(): Promise { + const keys = (await storage.getAllKeys()).filter((key) => + key.startsWith(`${CACHE_KEY_PREFIX}:`), + ); + const values = await storage.multiGet(keys); + const keysToRemove: string[] = []; + const entries: ProviderSnapshotIndexEntry[] = []; + for (const [key, value] of values) { + if (value === null) continue; + try { + const stored = parseStoredProviderSnapshot(value); + if (!stored) { + keysToRemove.push(key); + continue; + } + entries.push({ key, bytes: storedBytes(key, value), writtenAt: stored.generatedAt }); + } catch (error) { + if (!(error instanceof SyntaxError) && !(error instanceof RangeError)) throw error; + keysToRemove.push(key); + } + } + + let totalBytes = entries.reduce((total, entry) => total + entry.bytes, 0); + const retainedEntries = [...entries].sort(oldestFirst); + while (totalBytes > maxBytes) { + const evicted = retainedEntries.shift(); + if (!evicted) break; + totalBytes -= evicted.bytes; + keysToRemove.push(evicted.key); + } + if (keysToRemove.length > 0) await storage.multiRemove(keysToRemove); + await persistIndex(retainedEntries); + } + + async function ensureCacheIndex(): Promise { + if (index === null) await reconcileCache(); + if (index === null) throw new Error("Provider snapshot cache reconciliation failed"); + return index; + } + + async function runSerialized(operation: () => Promise): Promise { + const result = operationQueue.then(operation); + operationQueue = result.then( + () => undefined, + () => { + index = null; + }, + ); + return result; + } + + async function writeSnapshot(input: { + serverId: string; + scope: ProviderSnapshotCacheScope; + hash: string; + generatedAt: string; + compactSnapshot: CompactProviderSnapshot; + }): Promise { + const currentIndex = await ensureCacheIndex(); + const key = cacheKey(input.serverId, input.scope); + const stored = StoredProviderSnapshotSchema.safeParse({ + version: CACHE_VERSION, + hash: input.hash, + generatedAt: input.generatedAt, + compactSnapshot: input.compactSnapshot, + }); + if (!stored.success) { + await storage.removeItem(key); + await persistIndex(currentIndex.entries.filter((entry) => entry.key !== key)); + return; + } + const value = JSON.stringify(stored.data); + const incomingBytes = storedBytes(key, value); + const canStoreIncoming = incomingBytes <= maxBytes; + const retainedEntries = currentIndex.entries.filter((entry) => entry.key !== key); + let projectedBytes = retainedEntries.reduce((total, entry) => total + entry.bytes, 0); + if (canStoreIncoming) projectedBytes += incomingBytes; + + const keysToRemove: string[] = []; + for (const candidate of [...retainedEntries].sort(oldestFirst)) { + if (projectedBytes <= maxBytes) break; + projectedBytes -= candidate.bytes; + keysToRemove.push(candidate.key); + } + const removedKeys = new Set(keysToRemove); + const nextEntries = retainedEntries.filter((entry) => !removedKeys.has(entry.key)); + + if (!canStoreIncoming && currentIndex.entries.some((entry) => entry.key === key)) { + keysToRemove.push(key); + } + if (keysToRemove.length > 0) await storage.multiRemove(keysToRemove); + if (canStoreIncoming) { + await storage.setItem(key, value); + nextEntries.push({ key, bytes: incomingBytes, writtenAt: input.generatedAt }); + } + await persistIndex(nextEntries); + } + return { async read(serverId, scope) { - const key = cacheKey(serverId, scope); - let value: string | null; try { - value = await storage.getItem(key); + return await runSerialized(async () => { + const currentIndex = await ensureCacheIndex(); + const key = cacheKey(serverId, scope); + const value = await storage.getItem(key); + if (value === null) return null; + try { + const stored = parseStoredProviderSnapshot(value); + if (!stored) throw new SyntaxError("Invalid provider snapshot"); + return { ...stored, entries: expandProviderSnapshot(stored.compactSnapshot) }; + } catch (error) { + if (!(error instanceof SyntaxError) && !(error instanceof RangeError)) throw error; + await storage.removeItem(key); + await persistIndex(currentIndex.entries.filter((entry) => entry.key !== key)); + return null; + } + }); } catch (error) { if (error instanceof Error) return null; throw error; } - if (value === null) return null; - try { - const stored = parseStoredProviderSnapshot(value); - if (!stored) { - await discardCachedSnapshot(storage, key); - return null; - } - return { ...stored, entries: expandProviderSnapshot(stored.compactSnapshot) }; - } catch (error) { - if (!(error instanceof SyntaxError) && !(error instanceof RangeError)) { - throw error; - } - await discardCachedSnapshot(storage, key); - return null; - } }, async write(input) { - const stored: StoredProviderSnapshot = { - version: CACHE_VERSION, - hash: input.hash, - generatedAt: input.generatedAt, - compactSnapshot: input.compactSnapshot, - }; try { - await storage.setItem(cacheKey(input.serverId, input.scope), JSON.stringify(stored)); + await runSerialized(() => writeSnapshot(input)); } catch (error) { if (!(error instanceof Error)) throw error; } diff --git a/packages/app/src/desktop/browser/pane/element-selector.electron.ts b/packages/app/src/desktop/browser/pane/element-selector.electron.ts new file mode 100644 index 0000000000..3161168284 --- /dev/null +++ b/packages/app/src/desktop/browser/pane/element-selector.electron.ts @@ -0,0 +1,470 @@ +import type { BrowserElementAttachment } from "@/attachments/types"; + +export type BrowserElementSelection = Omit & { + attributes?: Record; +}; + +export type ElementSelectorMode = "annotate" | "screenshot"; + +export type ElementSelectorOutcome = + | { type: "selected"; mode: ElementSelectorMode; selection: BrowserElementSelection } + | { type: "cancelled" } + | { type: "failed"; reason: "loading" | "timeout" | "unavailable" }; + +export type ElementSelectorStartResult = "started" | "loading" | "unavailable"; + +interface ElementSelectorWebview extends HTMLElement { + isLoading?: () => boolean; + executeJavaScript?: (code: string) => Promise; +} + +interface ElementSelectorSession { + mode: ElementSelectorMode; + token: string; + webview: ElementSelectorWebview; + onFinish: (outcome: ElementSelectorOutcome) => void; + stopPolling?: () => void; + timeoutId?: number; +} + +export interface ElementSelectorController { + start(input: { + webview: ElementSelectorWebview; + mode: ElementSelectorMode; + onFinish: (outcome: ElementSelectorOutcome) => void; + }): ElementSelectorStartResult; + cancel(): void; + stopForWebview(webview: ElementSelectorWebview): void; +} + +const SELECTOR_TIMEOUT_MS = 30_000; + +function executeWebviewJavaScript(webview: ElementSelectorWebview, code: string): Promise { + if (!webview.isConnected) { + return Promise.resolve(null); + } + try { + return webview.executeJavaScript?.(code) ?? Promise.resolve(null); + } catch (error) { + return Promise.reject(error); + } +} + +function ignoreWebviewJavaScriptError() {} + +function destroyWebviewSelector(webview: ElementSelectorWebview, sessionToken: string): void { + const token = JSON.stringify(sessionToken); + void executeWebviewJavaScript( + webview, + `if (window.__paseoSelector?.sessionToken === ${token}) window.__paseoSelector.destroy();`, + ).catch(ignoreWebviewJavaScriptError); +} + +function clearWebviewSelector(webview: ElementSelectorWebview, sessionToken: string): void { + const token = JSON.stringify(sessionToken); + void executeWebviewJavaScript( + webview, + `if (window.__paseoSelector?.sessionToken === ${token}) window.__paseoSelector.destroy(); if (window.__paseoSelectorResult?.__paseoSessionToken === ${token}) window.__paseoSelectorResult = null;`, + ).catch(ignoreWebviewJavaScriptError); +} + +function readSelectorInstallation( + value: unknown, + sessionToken: string, +): "installed" | "loading" | "unavailable" { + if (value === null || typeof value !== "object") { + return "unavailable"; + } + if (Reflect.get(value, "sessionToken") !== sessionToken) { + return "unavailable"; + } + if (Reflect.get(value, "installed") === true) { + return "installed"; + } + return Reflect.get(value, "reason") === "document-loading" ? "loading" : "unavailable"; +} + +function stopSessionTimers(session: ElementSelectorSession): void { + session.stopPolling?.(); + session.stopPolling = undefined; + if (session.timeoutId !== undefined) { + window.clearTimeout(session.timeoutId); + session.timeoutId = undefined; + } +} + +function startSelectorResultPolling(input: { + webview: ElementSelectorWebview; + sessionToken: string; + onResult: (selection: BrowserElementSelection | null) => void; +}): () => void { + const { webview, sessionToken, onResult } = input; + const token = JSON.stringify(sessionToken); + let stopped = false; + let timerId: number | undefined; + + const schedule = () => { + if (!stopped) { + timerId = window.setTimeout(poll, 200); + } + }; + const poll = () => { + void (async () => { + try { + const raw = await executeWebviewJavaScript( + webview, + `JSON.stringify(window.__paseoSelectorResult?.__paseoSessionToken === ${token} ? window.__paseoSelectorResult : null)`, + ); + const result = typeof raw === "string" ? JSON.parse(raw) : null; + if (!result) { + schedule(); + return; + } + stopped = true; + await executeWebviewJavaScript( + webview, + `if (window.__paseoSelectorResult?.__paseoSessionToken === ${token}) window.__paseoSelectorResult = null;`, + ).catch(ignoreWebviewJavaScriptError); + const cancelled = result.__cancelled === true; + delete result.__cancelled; + delete result.__paseoSessionToken; + onResult(cancelled ? null : (result as BrowserElementSelection)); + } catch { + schedule(); + } + })(); + }; + + schedule(); + return () => { + stopped = true; + if (timerId !== undefined) { + window.clearTimeout(timerId); + } + }; +} + +function buildElementSelectorScript(sessionToken: string): string { + const token = JSON.stringify(sessionToken); + return ` + (function() { + var sessionToken = ${token}; + if (document.readyState === 'loading' || !document.head || !document.documentElement) { + return { installed: false, reason: 'document-loading', sessionToken: sessionToken }; + } + if (window.__paseoSelector) { window.__paseoSelector.destroy(); } + window.__paseoSelectorResult = null; + var style = document.createElement('style'); + style.textContent = [ + '.__paseo-hover { outline: 2px solid #3b82f6 !important; outline-offset: 2px !important; cursor: crosshair !important; }', + '.__paseo-select-mode, .__paseo-select-mode * { cursor: crosshair !important; pointer-events: auto !important; user-select: none !important; }', + '.__paseo-select-mode *, .__paseo-select-mode *::before, .__paseo-select-mode *::after { animation: none !important; transition: none !important; }', + '.__paseo-select-mode a, .__paseo-select-mode button, .__paseo-select-mode input, .__paseo-select-mode select, .__paseo-select-mode textarea, .__paseo-select-mode [role="button"], .__paseo-select-mode [onclick] { pointer-events: none !important; }', + '.__paseo-select-mode iframe, .__paseo-select-mode video, .__paseo-select-mode audio { pointer-events: none !important; }', + '.__paseo-hover-label { position: fixed; z-index: 2147483647; pointer-events: none; max-width: 360px; padding: 4px 8px; border-radius: 6px; background: rgba(24,24,27,0.96); color: #fff; font: 500 11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; box-shadow: 0 2px 10px rgba(0,0,0,0.35); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }', + '.__paseo-hover-label .__paseo-tag { color: #93c5fd; }', + '.__paseo-hover-label .__paseo-id { color: #fca5a5; }', + '.__paseo-hover-label .__paseo-cls { color: #fcd34d; }', + '.__paseo-hover-label .__paseo-dim { color: #a1a1aa; margin-left: 6px; }', + '.__paseo-hover-label .__paseo-comp { color: #86efac; margin-left: 6px; }', + ].join('\\n'); + document.head.appendChild(style); + document.documentElement.classList.add('__paseo-select-mode'); + var hoverLabel = document.createElement('div'); + hoverLabel.className = '__paseo-hover-label'; + hoverLabel.style.display = 'none'; + document.documentElement.appendChild(hoverLabel); + var last = null; + function escapeHtml(value) { + return String(value).replace(/[&<>"]/g, function(ch) { + return ch === '&' ? '&' : ch === '<' ? '<' : ch === '>' ? '>' : '"'; + }); + } + function describeElement(el) { + var tag = el.tagName ? el.tagName.toLowerCase() : 'node'; + var parts = ['' + escapeHtml(tag) + '']; + if (el.id) { + parts.push('#' + escapeHtml(el.id) + ''); + } + if (el.classList && el.classList.length) { + var cls = Array.prototype.slice.call(el.classList, 0, 2) + .filter(function(c) { return c.indexOf('__paseo') !== 0; }) + .map(function(c) { return '.' + escapeHtml(c); }) + .join(''); + if (cls) parts.push('' + cls + ''); + } + var comp = getReactSource(el); + if (comp && comp.componentName) { + parts.push('<' + escapeHtml(comp.componentName) + '>'); + } + var rect = el.getBoundingClientRect(); + parts.push('' + Math.round(rect.width) + '×' + Math.round(rect.height) + ''); + return { html: parts.join(''), rect: rect }; + } + function positionLabel(rect, e) { + var lw = hoverLabel.offsetWidth || 0; + var lh = hoverLabel.offsetHeight || 0; + var top = rect.top - lh - 6; + if (top < 4) top = rect.bottom + 6; + if (top + lh > window.innerHeight - 4) top = Math.max(4, e.clientY - lh - 6); + var left = rect.left; + if (left + lw > window.innerWidth - 4) left = Math.max(4, window.innerWidth - lw - 4); + if (left < 4) left = 4; + hoverLabel.style.top = Math.round(top) + 'px'; + hoverLabel.style.left = Math.round(left) + 'px'; + } + function onMove(e) { + e.preventDefault(); + e.stopPropagation(); + if (last) last.classList.remove('__paseo-hover'); + var el = e.target; + el.classList.add('__paseo-hover'); + last = el; + try { + var info = describeElement(el); + hoverLabel.innerHTML = info.html; + hoverLabel.style.display = 'block'; + positionLabel(info.rect, e); + } catch (err) { + hoverLabel.style.display = 'none'; + } + } + function buildSelector(el) { + if (el.id) return '#' + el.id; + var path = []; + while (el && el.nodeType === 1) { + var seg = el.tagName.toLowerCase(); + if (el.id) { path.unshift('#' + el.id); break; } + var sib = el, nth = 1; + while (sib = sib.previousElementSibling) { if (sib.tagName === el.tagName) nth++; } + if (nth > 1) seg += ':nth-of-type(' + nth + ')'; + path.unshift(seg); + el = el.parentElement; + } + return path.join(' > '); + } + function getReactSource(el) { + var keys = Object.keys(el); + for (var i = 0; i < keys.length; i++) { + if (keys[i].startsWith('__reactFiber$') || keys[i].startsWith('__reactInternalInstance$')) { + var fiber = el[keys[i]]; + while (fiber) { + if (fiber._debugSource) { + return { + fileName: fiber._debugSource.fileName || null, + lineNumber: fiber._debugSource.lineNumber || null, + columnNumber: fiber._debugSource.columnNumber || null, + componentName: (fiber.type && (typeof fiber.type === 'string' ? fiber.type : fiber.type.displayName || fiber.type.name)) || null + }; + } + if (fiber._debugOwner) { fiber = fiber._debugOwner; } + else if (fiber.return) { fiber = fiber.return; } + else break; + } + } + } + return null; + } + function getParentChain(el, depth) { + var chain = []; + var cur = el.parentElement; + for (var i = 0; i < (depth || 5) && cur; i++) { + var desc = cur.tagName.toLowerCase(); + if (cur.id) desc += '#' + cur.id; + if (cur.className && typeof cur.className === 'string') { var cls = cur.className.trim().replace(/ +/g, ' ').split(' ').slice(0,2).join('.'); if (cls) desc += '.' + cls; } + chain.push(desc); + cur = cur.parentElement; + } + return chain; + } + function getChildSummary(el, max) { + var kids = []; + for (var i = 0; i < Math.min(el.children.length, max || 8); i++) { + var c = el.children[i]; + var desc = c.tagName.toLowerCase(); + if (c.id) desc += '#' + c.id; + kids.push(desc); + } + if (el.children.length > (max || 8)) kids.push('...(' + el.children.length + ' total)'); + return kids; + } + function getRelevantStyles(el) { + var cs = window.getComputedStyle(el); + var pick = ['display','position','width','height','color','background-color','font-size','font-family','padding','margin','border','flex','grid-template-columns','gap','overflow','opacity','z-index']; + var out = {}; + pick.forEach(function(p) { + var v = cs.getPropertyValue(p); + if (v && v !== 'none' && v !== 'normal' && v !== 'auto' && v !== '0px' && v !== 'rgba(0, 0, 0, 0)') out[p] = v; + }); + return out; + } + function onClick(e) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + var el = e.target; + if (last) last.classList.remove('__paseo-hover'); + hoverLabel.style.display = 'none'; + var attrs = {}; + for (var i = 0; i < el.attributes.length; i++) { + attrs[el.attributes[i].name] = el.attributes[i].value; + } + var rect = el.getBoundingClientRect(); + var result = { + tag: el.tagName.toLowerCase(), + text: (el.innerText || '').substring(0, 500), + selector: buildSelector(el), + attributes: attrs, + url: location.href, + outerHTML: el.outerHTML.substring(0, 2000), + computedStyles: getRelevantStyles(el), + __paseoSessionToken: sessionToken, + boundingRect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) }, + reactSource: getReactSource(el), + parentChain: getParentChain(el, 5), + children: getChildSummary(el, 8) + }; + destroy(); + window.__paseoSelectorResult = result; + } + function onKey(e) { + if (e.key === 'Escape') { + destroy(); + window.__paseoSelectorResult = { __cancelled: true, __paseoSessionToken: sessionToken }; + } + } + function blockEvent(e) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + } + function destroy() { + document.removeEventListener('mousemove', onMove, true); + document.removeEventListener('click', onClick, true); + document.removeEventListener('keydown', onKey, true); + document.removeEventListener('mousedown', blockEvent, true); + document.removeEventListener('mouseup', blockEvent, true); + document.removeEventListener('pointerdown', blockEvent, true); + document.removeEventListener('pointerup', blockEvent, true); + document.removeEventListener('touchstart', blockEvent, true); + document.removeEventListener('touchend', blockEvent, true); + document.removeEventListener('focus', blockEvent, true); + document.removeEventListener('submit', blockEvent, true); + document.documentElement.classList.remove('__paseo-select-mode'); + if (last) last.classList.remove('__paseo-hover'); + if (hoverLabel.parentNode) hoverLabel.parentNode.removeChild(hoverLabel); + style.remove(); + window.__paseoSelector = null; + } + document.addEventListener('mousemove', onMove, true); + document.addEventListener('click', onClick, true); + document.addEventListener('keydown', onKey, true); + document.addEventListener('mousedown', blockEvent, true); + document.addEventListener('mouseup', blockEvent, true); + document.addEventListener('pointerdown', blockEvent, true); + document.addEventListener('pointerup', blockEvent, true); + document.addEventListener('touchstart', blockEvent, true); + document.addEventListener('touchend', blockEvent, true); + document.addEventListener('focus', blockEvent, true); + document.addEventListener('submit', blockEvent, true); + window.__paseoSelector = { destroy: destroy, sessionToken: sessionToken }; + return { installed: true, sessionToken: sessionToken }; + })() + `; +} + +export function createElementSelectorController(): ElementSelectorController { + let currentSession: ElementSelectorSession | null = null; + let sessionCounter = 0; + + const finish = ( + session: ElementSelectorSession, + outcome: ElementSelectorOutcome, + guestCleanup: "clear" | "destroy" | null, + ): boolean => { + stopSessionTimers(session); + if (currentSession !== session) { + return false; + } + currentSession = null; + if (guestCleanup === "clear") { + clearWebviewSelector(session.webview, session.token); + } else if (guestCleanup === "destroy") { + destroyWebviewSelector(session.webview, session.token); + } + session.onFinish(outcome); + return true; + }; + + return { + start({ webview, mode, onFinish }) { + if (!webview.isConnected) { + return "unavailable"; + } + if (webview.isLoading?.()) { + return "loading"; + } + + if (currentSession) { + finish(currentSession, { type: "cancelled" }, "clear"); + } + + sessionCounter += 1; + const session: ElementSelectorSession = { + mode, + token: `${sessionCounter}:${crypto.randomUUID()}`, + webview, + onFinish, + }; + currentSession = session; + session.timeoutId = window.setTimeout(() => { + finish(session, { type: "failed", reason: "timeout" }, "destroy"); + }, SELECTOR_TIMEOUT_MS); + + void executeWebviewJavaScript(webview, buildElementSelectorScript(session.token)) + .then((installation) => { + const installationState = readSelectorInstallation(installation, session.token); + if (currentSession !== session) { + if (installationState === "installed") { + destroyWebviewSelector(webview, session.token); + } + return undefined; + } + if (installationState !== "installed") { + finish(session, { type: "failed", reason: installationState }, null); + return undefined; + } + session.stopPolling = startSelectorResultPolling({ + webview, + sessionToken: session.token, + onResult: (selection) => { + finish( + session, + selection + ? { type: "selected", mode: session.mode, selection } + : { type: "cancelled" }, + null, + ); + }, + }); + return undefined; + }) + .catch(() => { + finish(session, { type: "failed", reason: "unavailable" }, "destroy"); + return undefined; + }); + + return "started"; + }, + cancel() { + if (currentSession) { + finish(currentSession, { type: "cancelled" }, "clear"); + } + }, + stopForWebview(webview) { + if (currentSession?.webview === webview) { + finish(currentSession, { type: "cancelled" }, "destroy"); + } + }, + }; +} diff --git a/packages/app/src/desktop/browser/pane/index.electron.tsx b/packages/app/src/desktop/browser/pane/index.electron.tsx index a97e9ee92e..4edc016eed 100644 --- a/packages/app/src/desktop/browser/pane/index.electron.tsx +++ b/packages/app/src/desktop/browser/pane/index.electron.tsx @@ -59,7 +59,6 @@ import { } from "@/desktop/browser/store"; import { applyInactiveBrowserWebviewViewport, - isResidentBrowserWebviewReady, prepareBrowserWebview, presentBrowserWebview, rememberBrowserWebviewSize, @@ -67,6 +66,11 @@ import { removeResidentBrowserWebview, takeResidentBrowserWebview, } from "../resident-webviews"; +import { + createElementSelectorController, + type BrowserElementSelection, + type ElementSelectorOutcome, +} from "./element-selector.electron"; type ElectronWebview = HTMLElement & { canGoBack?: () => boolean; @@ -77,6 +81,7 @@ type ElectronWebview = HTMLElement & { stop?: () => void; loadURL?: (url: string) => Promise; getURL?: () => string; + isLoading?: () => boolean; executeJavaScript?: (code: string) => Promise; focus?: () => void; addEventListener: (type: string, listener: EventListenerOrEventListenerObject) => void; @@ -87,10 +92,6 @@ type WebTextInput = TextInput & { getNativeRef?: () => unknown; }; -type BrowserElementSelection = Omit & { - attributes?: Record; -}; - interface BrowserElementAnnotation { comment: string; } @@ -315,20 +316,6 @@ function executeWebviewJavaScript(webview: ElectronWebview, code: string): Promi function ignoreWebviewJavaScriptError() {} -function destroyWebviewSelector(webview: ElectronWebview): void { - void executeWebviewJavaScript( - webview, - "if(window.__paseoSelector) window.__paseoSelector.destroy();", - ).catch(ignoreWebviewJavaScriptError); -} - -function clearWebviewSelector(webview: ElectronWebview): void { - void executeWebviewJavaScript( - webview, - "if(window.__paseoSelector) window.__paseoSelector.destroy(); window.__paseoSelectorResult = null;", - ).catch(ignoreWebviewJavaScriptError); -} - interface BrowserAnnotationMarker { index: number; selector: string; @@ -437,38 +424,6 @@ function isDesktopBrowserShortcutEvent(payload: unknown): payload is DesktopBrow return event.action === "focus-url"; } -function startSelectorResultPolling(input: { - webview: ElectronWebview; - onSelection: (selection: BrowserElementSelection) => void; - onDone: () => void; -}): number { - const { webview, onSelection, onDone } = input; - const poll = window.setInterval(() => { - void (async () => { - try { - const raw = await executeWebviewJavaScript( - webview, - "JSON.stringify(window.__paseoSelectorResult || null)", - ); - const result = typeof raw === "string" ? JSON.parse(raw) : null; - if (!result) { - return; - } - window.clearInterval(poll); - onDone(); - await executeWebviewJavaScript(webview, "window.__paseoSelectorResult = null;"); - if (!result.__cancelled) { - onSelection(result as BrowserElementSelection); - } - } catch { - // Keep polling; cross-origin/webview timing can make this transient. - } - })(); - }, 200); - - return poll; -} - function ToolbarButton({ label, children, @@ -655,13 +610,15 @@ export function BrowserPane({ const browserRef = useRef(browser); browserRef.current = browser; const pendingNavigationUrlRef = useRef(null); - const domReadyRef = useRef(false); const annotationMarkersRef = useRef([]); const [selectorMode, setSelectorMode] = useState<"annotate" | "screenshot" | null>(null); + const selectorControllerRef = useRef | null>( + null, + ); + if (!selectorControllerRef.current) { + selectorControllerRef.current = createElementSelectorController(); + } const selectorActive = selectorMode !== null; - // Which action the active selector performs on click: open the annotation card - // ("annotate") or copy a screenshot of the element to the clipboard ("screenshot"). - const selectorModeRef = useRef<"annotate" | "screenshot">("annotate"); const toast = useToast(); const toastRef = useRef(toast); toastRef.current = toast; @@ -737,7 +694,7 @@ export function BrowserPane({ const syncNavigationState = useCallback((input?: { syncUrl?: boolean }) => { const webview = webviewRef.current; - if (!webview || !domReadyRef.current) { + if (!webview) { return; } @@ -776,7 +733,6 @@ export function BrowserPane({ const residentWebview = takeResidentBrowserWebview(browserId) as ElectronWebview | null; const webview = residentWebview ?? (document.createElement("webview") as ElectronWebview); webviewRef.current = webview; - domReadyRef.current = isResidentBrowserWebviewReady(webview); if (!residentWebview) { prepareBrowserWebview(webview, { browserId, @@ -808,7 +764,7 @@ export function BrowserPane({ }); const handleStartLoading = () => { - domReadyRef.current = false; + selectorControllerRef.current?.stopForWebview(webview); updateBrowser(browserId, { isLoading: true, lastError: null }); syncNavigationState({ syncUrl: false }); }; @@ -875,7 +831,6 @@ export function BrowserPane({ }); }; const handleDomReady = () => { - domReadyRef.current = true; syncNavigationState(); // The previous page's overlay is gone after a load; re-apply markers for // the freshly loaded document. @@ -940,10 +895,10 @@ export function BrowserPane({ } else { removeResidentBrowserWebview(browserIdRef.current); } + selectorControllerRef.current?.stopForWebview(webview); if (webviewRef.current === webview) { webviewRef.current = null; } - domReadyRef.current = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [browserId, onFocusPane]); @@ -1196,8 +1151,8 @@ export function BrowserPane({ ); const handleSelectorResult = useCallback( - (selection: BrowserElementSelection) => { - if (selectorModeRef.current === "screenshot") { + (selection: BrowserElementSelection, mode: "annotate" | "screenshot") => { + if (mode === "screenshot") { void screenshotElementToClipboard(selection); return; } @@ -1230,271 +1185,62 @@ export function BrowserPane({ setPendingSelection(null); }, []); + const showSelectorFailure = useCallback( + (reason: "loading" | "timeout" | "unavailable") => { + const message = + reason === "loading" + ? t("workspace.browser.controls.selectorLoading") + : t("workspace.browser.controls.selectorFailed"); + toastRef.current?.error(message); + }, + [t], + ); + + const handleElementSelectorOutcome = useCallback( + (outcome: ElementSelectorOutcome) => { + setSelectorMode(null); + if (outcome.type === "selected") { + handleSelectorResult(outcome.selection, outcome.mode); + } else if (outcome.type === "failed") { + showSelectorFailure(outcome.reason); + } + }, + [handleSelectorResult, showSelectorFailure], + ); + const startElementSelector = useCallback( (mode: "annotate" | "screenshot") => { + if (mode === "annotate" && !workspaceAttachmentScopeKey) { + showSelectorFailure("unavailable"); + return; + } const webview = webviewRef.current; - if (!webview || !domReadyRef.current) return; - // Annotate needs a workspace scope to attach to; screenshot only copies. - if (mode === "annotate" && !workspaceAttachmentScopeKey) return; - selectorModeRef.current = mode; + const controller = selectorControllerRef.current; + if (!webview || !controller) { + showSelectorFailure("unavailable"); + return; + } + + const startResult = controller.start({ + webview, + mode, + onFinish: handleElementSelectorOutcome, + }); + if (startResult !== "started") { + showSelectorFailure(startResult); + return; + } + pendingScreenshotRef.current = undefined; setPendingSelection(null); setSelectorMode(mode); - - const js = ` - (function() { - if (window.__paseoSelector) { window.__paseoSelector.destroy(); } - var overlay = null; - var style = document.createElement('style'); - style.textContent = [ - '.__paseo-hover { outline: 2px solid #3b82f6 !important; outline-offset: 2px !important; cursor: crosshair !important; }', - '.__paseo-select-mode, .__paseo-select-mode * { cursor: crosshair !important; pointer-events: auto !important; user-select: none !important; }', - '.__paseo-select-mode *, .__paseo-select-mode *::before, .__paseo-select-mode *::after { animation: none !important; transition: none !important; }', - '.__paseo-select-mode a, .__paseo-select-mode button, .__paseo-select-mode input, .__paseo-select-mode select, .__paseo-select-mode textarea, .__paseo-select-mode [role="button"], .__paseo-select-mode [onclick] { pointer-events: none !important; }', - '.__paseo-select-mode iframe, .__paseo-select-mode video, .__paseo-select-mode audio { pointer-events: none !important; }', - '.__paseo-hover-label { position: fixed; z-index: 2147483647; pointer-events: none; max-width: 360px; padding: 4px 8px; border-radius: 6px; background: rgba(24,24,27,0.96); color: #fff; font: 500 11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; box-shadow: 0 2px 10px rgba(0,0,0,0.35); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }', - '.__paseo-hover-label .__paseo-tag { color: #93c5fd; }', - '.__paseo-hover-label .__paseo-id { color: #fca5a5; }', - '.__paseo-hover-label .__paseo-cls { color: #fcd34d; }', - '.__paseo-hover-label .__paseo-dim { color: #a1a1aa; margin-left: 6px; }', - '.__paseo-hover-label .__paseo-comp { color: #86efac; margin-left: 6px; }', - ].join('\\n'); - document.head.appendChild(style); - document.documentElement.classList.add('__paseo-select-mode'); - var hoverLabel = document.createElement('div'); - hoverLabel.className = '__paseo-hover-label'; - hoverLabel.style.display = 'none'; - document.documentElement.appendChild(hoverLabel); - var last = null; - function escapeHtml(value) { - return String(value).replace(/[&<>"]/g, function(ch) { - return ch === '&' ? '&' : ch === '<' ? '<' : ch === '>' ? '>' : '"'; - }); - } - function describeElement(el) { - var tag = el.tagName ? el.tagName.toLowerCase() : 'node'; - var parts = ['' + escapeHtml(tag) + '']; - if (el.id) { - parts.push('#' + escapeHtml(el.id) + ''); - } - if (el.classList && el.classList.length) { - var cls = Array.prototype.slice.call(el.classList, 0, 2) - .filter(function(c) { return c.indexOf('__paseo') !== 0; }) - .map(function(c) { return '.' + escapeHtml(c); }) - .join(''); - if (cls) parts.push('' + cls + ''); - } - var comp = getReactSource(el); - if (comp && comp.componentName) { - parts.push('<' + escapeHtml(comp.componentName) + '>'); - } - var rect = el.getBoundingClientRect(); - parts.push('' + Math.round(rect.width) + '×' + Math.round(rect.height) + ''); - return { html: parts.join(''), rect: rect }; - } - function positionLabel(rect, e) { - var pad = 12; - var lw = hoverLabel.offsetWidth || 0; - var lh = hoverLabel.offsetHeight || 0; - var top = rect.top - lh - 6; - if (top < 4) top = rect.bottom + 6; - if (top + lh > window.innerHeight - 4) top = Math.max(4, e.clientY - lh - 6); - var left = rect.left; - if (left + lw > window.innerWidth - 4) left = Math.max(4, window.innerWidth - lw - 4); - if (left < 4) left = 4; - hoverLabel.style.top = Math.round(top) + 'px'; - hoverLabel.style.left = Math.round(left) + 'px'; - } - function onMove(e) { - e.preventDefault(); - e.stopPropagation(); - if (last) last.classList.remove('__paseo-hover'); - var el = e.target; - el.classList.add('__paseo-hover'); - last = el; - try { - var info = describeElement(el); - hoverLabel.innerHTML = info.html; - hoverLabel.style.display = 'block'; - positionLabel(info.rect, e); - } catch (err) { - hoverLabel.style.display = 'none'; - } - } - function buildSelector(el) { - if (el.id) return '#' + el.id; - var path = []; - while (el && el.nodeType === 1) { - var seg = el.tagName.toLowerCase(); - if (el.id) { path.unshift('#' + el.id); break; } - var sib = el, nth = 1; - while (sib = sib.previousElementSibling) { if (sib.tagName === el.tagName) nth++; } - if (nth > 1) seg += ':nth-of-type(' + nth + ')'; - path.unshift(seg); - el = el.parentElement; - } - return path.join(' > '); - } - function getReactSource(el) { - var keys = Object.keys(el); - for (var i = 0; i < keys.length; i++) { - if (keys[i].startsWith('__reactFiber$') || keys[i].startsWith('__reactInternalInstance$')) { - var fiber = el[keys[i]]; - while (fiber) { - if (fiber._debugSource) { - return { - fileName: fiber._debugSource.fileName || null, - lineNumber: fiber._debugSource.lineNumber || null, - columnNumber: fiber._debugSource.columnNumber || null, - componentName: (fiber.type && (typeof fiber.type === 'string' ? fiber.type : fiber.type.displayName || fiber.type.name)) || null - }; - } - if (fiber._debugOwner) { fiber = fiber._debugOwner; } - else if (fiber.return) { fiber = fiber.return; } - else break; - } - } - } - return null; - } - function getParentChain(el, depth) { - var chain = []; - var cur = el.parentElement; - for (var i = 0; i < (depth || 5) && cur; i++) { - var desc = cur.tagName.toLowerCase(); - if (cur.id) desc += '#' + cur.id; - if (cur.className && typeof cur.className === 'string') { var cls = cur.className.trim().replace(/ +/g, ' ').split(' ').slice(0,2).join('.'); if (cls) desc += '.' + cls; } - chain.push(desc); - cur = cur.parentElement; - } - return chain; - } - function getChildSummary(el, max) { - var kids = []; - for (var i = 0; i < Math.min(el.children.length, max || 8); i++) { - var c = el.children[i]; - var desc = c.tagName.toLowerCase(); - if (c.id) desc += '#' + c.id; - kids.push(desc); - } - if (el.children.length > (max || 8)) kids.push('...(' + el.children.length + ' total)'); - return kids; - } - function getRelevantStyles(el) { - var cs = window.getComputedStyle(el); - var pick = ['display','position','width','height','color','background-color','font-size','font-family','padding','margin','border','flex','grid-template-columns','gap','overflow','opacity','z-index']; - var out = {}; - pick.forEach(function(p) { - var v = cs.getPropertyValue(p); - if (v && v !== 'none' && v !== 'normal' && v !== 'auto' && v !== '0px' && v !== 'rgba(0, 0, 0, 0)') out[p] = v; - }); - return out; - } - function onClick(e) { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - var el = e.target; - if (last) last.classList.remove('__paseo-hover'); - hoverLabel.style.display = 'none'; - var attrs = {}; - for (var i = 0; i < el.attributes.length; i++) { - attrs[el.attributes[i].name] = el.attributes[i].value; - } - var rect = el.getBoundingClientRect(); - var result = { - tag: el.tagName.toLowerCase(), - text: (el.innerText || '').substring(0, 500), - selector: buildSelector(el), - attributes: attrs, - url: location.href, - outerHTML: el.outerHTML.substring(0, 2000), - computedStyles: getRelevantStyles(el), - boundingRect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) }, - reactSource: getReactSource(el), - parentChain: getParentChain(el, 5), - children: getChildSummary(el, 8) - }; - destroy(); - window.__paseoSelectorResult = result; - } - function onKey(e) { - if (e.key === 'Escape') { destroy(); window.__paseoSelectorResult = { __cancelled: true }; } - } - function blockEvent(e) { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - } - function destroy() { - document.removeEventListener('mousemove', onMove, true); - document.removeEventListener('click', onClick, true); - document.removeEventListener('keydown', onKey, true); - document.removeEventListener('mousedown', blockEvent, true); - document.removeEventListener('mouseup', blockEvent, true); - document.removeEventListener('pointerdown', blockEvent, true); - document.removeEventListener('pointerup', blockEvent, true); - document.removeEventListener('touchstart', blockEvent, true); - document.removeEventListener('touchend', blockEvent, true); - document.removeEventListener('focus', blockEvent, true); - document.removeEventListener('submit', blockEvent, true); - document.documentElement.classList.remove('__paseo-select-mode'); - if (last) last.classList.remove('__paseo-hover'); - if (hoverLabel.parentNode) hoverLabel.parentNode.removeChild(hoverLabel); - style.remove(); - window.__paseoSelector = null; - } - document.addEventListener('mousemove', onMove, true); - document.addEventListener('click', onClick, true); - document.addEventListener('keydown', onKey, true); - document.addEventListener('mousedown', blockEvent, true); - document.addEventListener('mouseup', blockEvent, true); - document.addEventListener('pointerdown', blockEvent, true); - document.addEventListener('pointerup', blockEvent, true); - document.addEventListener('touchstart', blockEvent, true); - document.addEventListener('touchend', blockEvent, true); - document.addEventListener('focus', blockEvent, true); - document.addEventListener('submit', blockEvent, true); - window.__paseoSelector = { destroy: destroy }; - })() - `; - - try { - void executeWebviewJavaScript(webview, js) - .then(() => { - const poll = startSelectorResultPolling({ - webview, - onSelection: handleSelectorResult, - onDone: () => setSelectorMode(null), - }); - window.setTimeout(() => { - window.clearInterval(poll); - setSelectorMode(null); - if (webviewRef.current !== webview || !domReadyRef.current) { - return; - } - destroyWebviewSelector(webview); - }, 30000); - return undefined; - }) - .catch(() => { - setSelectorMode(null); - }); - } catch { - setSelectorMode(null); - } }, - [handleSelectorResult, workspaceAttachmentScopeKey], + [handleElementSelectorOutcome, showSelectorFailure, workspaceAttachmentScopeKey], ); const cancelElementSelector = useCallback(() => { - const webview = webviewRef.current; + selectorControllerRef.current?.cancel(); setSelectorMode(null); - if (webview && domReadyRef.current) { - try { - clearWebviewSelector(webview); - } catch {} - } }, []); const currentPageUrl = browser?.url ?? null; @@ -1526,7 +1272,7 @@ export function BrowserPane({ return; } const webview = webviewRef.current; - if (!webview || !domReadyRef.current) { + if (!webview) { return; } if (annotationMarkers.length === 0) { diff --git a/packages/app/src/desktop/browser/resident-webviews.browser.test.ts b/packages/app/src/desktop/browser/resident-webviews.browser.test.ts index 868005817f..679081df0e 100644 --- a/packages/app/src/desktop/browser/resident-webviews.browser.test.ts +++ b/packages/app/src/desktop/browser/resident-webviews.browser.test.ts @@ -5,7 +5,6 @@ import { clearResidentBrowserWebviewsForTests, ensureResidentBrowserWebview, getResidentBrowserWebview, - isResidentBrowserWebviewReady, prepareBrowserWebview, presentBrowserWebview, rememberBrowserWebviewSize, @@ -205,28 +204,6 @@ describe("resident browser webviews", () => { expect(webview.parentElement.style.height).toBe("600px"); }); - it("retains document readiness with the resident webview", () => { - const webview = ensureTestBrowser({ - browserId: "browser-ready", - workspaceId: "workspace-ready", - url: "https://example.com", - }); - if (!webview) { - throw new Error("Expected resident browser webview"); - } - - expect(isResidentBrowserWebviewReady(webview)).toBe(false); - webview.dispatchEvent(new Event("dom-ready")); - releaseResidentBrowserWebview("browser-ready", webview); - - const reusedWebview = takeResidentBrowserWebview("browser-ready"); - expect(reusedWebview).toBe(webview); - expect(isResidentBrowserWebviewReady(reusedWebview as HTMLElement)).toBe(true); - - webview.dispatchEvent(new Event("did-start-loading")); - expect(isResidentBrowserWebviewReady(webview)).toBe(false); - }); - it("creates a resident webview for an agent-created unfocused tab", () => { const webview = ensureTestBrowser({ browserId: "browser-agent", diff --git a/packages/app/src/desktop/browser/resident-webviews.ts b/packages/app/src/desktop/browser/resident-webviews.ts index 0b53525849..dfce3551e0 100644 --- a/packages/app/src/desktop/browser/resident-webviews.ts +++ b/packages/app/src/desktop/browser/resident-webviews.ts @@ -15,7 +15,6 @@ const RESIDENT_VIEWPORT_HEIGHT = 800; const residentWebviewsByBrowserId = new Map(); const residentSurfacesByBrowserId = new Map(); const residentWebviewSizesByBrowserId = new Map(); -const readyResidentWebviews = new WeakSet(); interface BrowserWebviewElement extends HTMLElement { src: string; @@ -75,15 +74,6 @@ function registerBrowserWhenAttached( }); } -function registerBrowserReadiness(webview: HTMLElement): void { - webview.addEventListener("did-start-loading", () => { - readyResidentWebviews.delete(webview); - }); - webview.addEventListener("dom-ready", () => { - readyResidentWebviews.add(webview); - }); -} - function trimNonEmpty(value: string | null | undefined): string | null { if (typeof value !== "string") { return null; @@ -322,11 +312,6 @@ export function prepareBrowserWebview( (webview as BrowserWebviewElement).src = input.initialUrl; } registerBrowserWhenAttached(webview as BrowserWebviewElement, input, browser); - registerBrowserReadiness(webview); -} - -export function isResidentBrowserWebviewReady(webview: HTMLElement): boolean { - return readyResidentWebviews.has(webview); } export function ensureResidentBrowserWebview(input: { diff --git a/packages/app/src/desktop/browser/store/index.ts b/packages/app/src/desktop/browser/store/index.ts index 7ff21c733a..16558eaa0d 100644 --- a/packages/app/src/desktop/browser/store/index.ts +++ b/packages/app/src/desktop/browser/store/index.ts @@ -1,9 +1,11 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { BrowserAutomationBrowserIdSchema } from "@getpaseo/protocol/browser-automation/rpc-schemas"; import { create } from "zustand"; -import { createJSONStorage, persist } from "zustand/middleware"; +import { persist } from "zustand/middleware"; +import { createValidatedPersistStorage } from "@/storage/validated-persist-storage"; import { applyBrowserPatch, + BrowserIndexStateSchema, type BrowserIndexState, type BrowserRecord, type BrowserRecordPatch, @@ -74,7 +76,7 @@ export const useBrowserStore = create()( }), { name: "workspace-browser-store", - storage: createJSONStorage(() => AsyncStorage), + storage: createValidatedPersistStorage(AsyncStorage, BrowserIndexStateSchema), partialize: (state) => sanitizeBrowsersForPersist(state), merge: (persistedState, currentState) => ({ ...currentState, diff --git a/packages/app/src/desktop/browser/store/state.ts b/packages/app/src/desktop/browser/store/state.ts index 3909a7aa31..47d0de2455 100644 --- a/packages/app/src/desktop/browser/store/state.ts +++ b/packages/app/src/desktop/browser/store/state.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + export type BrowserViewport = | { mode: "responsive" } | { mode: "fixed"; width: number; height: number }; @@ -23,6 +25,32 @@ export interface BrowserIndexState { browsersById: Record; } +const BrowserViewportSchema = z.discriminatedUnion("mode", [ + z.strictObject({ mode: z.literal("responsive") }), + z.strictObject({ + mode: z.literal("fixed"), + width: z.number().positive(), + height: z.number().positive(), + }), +]); + +const BrowserRecordSchema = z.strictObject({ + browserId: z.string(), + url: z.string(), + title: z.string(), + isLoading: z.boolean(), + canGoBack: z.boolean(), + canGoForward: z.boolean(), + faviconUrl: z.string().nullable(), + lastError: z.string().nullable(), + viewport: BrowserViewportSchema.optional().default(RESPONSIVE_BROWSER_VIEWPORT), + createdAt: z.number(), +}); + +export const BrowserIndexStateSchema: z.ZodType = z.strictObject({ + browsersById: z.record(z.string(), BrowserRecordSchema), +}); + export function createFixedBrowserViewport(width: number, height: number): BrowserViewport { return { mode: "fixed", @@ -32,19 +60,10 @@ export function createFixedBrowserViewport(width: number, height: number): Brows } export function normalizeBrowserViewport(value: unknown): BrowserViewport { - if ( - value && - typeof value === "object" && - (value as { mode?: unknown }).mode === "fixed" && - typeof (value as { width?: unknown }).width === "number" && - typeof (value as { height?: unknown }).height === "number" - ) { - return createFixedBrowserViewport( - (value as { width: number }).width, - (value as { height: number }).height, - ); - } - return RESPONSIVE_BROWSER_VIEWPORT; + const result = BrowserViewportSchema.safeParse(value); + return result.success && result.data.mode === "fixed" + ? createFixedBrowserViewport(result.data.width, result.data.height) + : RESPONSIVE_BROWSER_VIEWPORT; } function browserViewportsEqual(left: BrowserViewport, right: BrowserViewport): boolean { @@ -56,27 +75,8 @@ function browserViewportsEqual(left: BrowserViewport, right: BrowserViewport): b } export function normalizeBrowserIndexState(value: unknown): BrowserIndexState { - if (!value || typeof value !== "object") { - return { browsersById: {} }; - } - const browsersById = (value as { browsersById?: unknown }).browsersById; - if (!browsersById || typeof browsersById !== "object") { - return { browsersById: {} }; - } - return { - browsersById: Object.fromEntries( - Object.entries(browsersById).map(([browserId, record]) => { - const browser = record as BrowserRecord & { viewport?: unknown }; - return [ - browserId, - { - ...browser, - viewport: normalizeBrowserViewport(browser.viewport), - } satisfies BrowserRecord, - ]; - }), - ), - }; + const result = BrowserIndexStateSchema.safeParse(value); + return result.success ? result.data : { browsersById: {} }; } export function trimNonEmpty(value: string | null | undefined): string | null { diff --git a/packages/app/src/dictation/dictation-stream-sender.test.ts b/packages/app/src/dictation/dictation-stream-sender.test.ts index 0684dd2b12..1af06997a5 100644 --- a/packages/app/src/dictation/dictation-stream-sender.test.ts +++ b/packages/app/src/dictation/dictation-stream-sender.test.ts @@ -1,6 +1,5 @@ -import { describe, expect, it } from "vitest"; -import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; - +import { describe, expect, it, vi } from "vitest"; +import type { SessionOutboundMessage } from "@getpaseo/protocol/messages"; import { DictationStreamSender } from "@/dictation/dictation-stream-sender"; interface FakeFinish { @@ -18,19 +17,66 @@ interface FakeChunk { format: string; } +interface FakeAck { + type: "dictation_stream_ack"; + payload: { + dictationId: string; + ackSeq: number; + }; +} + +type FakeRawMessageListener = (message: SessionOutboundMessage) => void; + class FakeDaemonClient { isConnected = true; + autoAck = true; + throwOnNextChunk = false; + disconnectOnChunkError = false; starts: FakeStart[] = []; chunks: FakeChunk[] = []; finishes: FakeFinish[] = []; cancels: string[] = []; + private readonly rawMessageListeners = new Set(); + + get rawMessageListenerCount(): number { + return this.rawMessageListeners.size; + } async startDictationStream(dictationId: string, format: string): Promise { this.starts.push({ dictationId, format }); } sendDictationStreamChunk(dictationId: string, seq: number, audio: string, format: string): void { + if (this.throwOnNextChunk) { + this.throwOnNextChunk = false; + if (this.disconnectOnChunkError) { + this.isConnected = false; + } + throw new Error("WebSocket is closing"); + } this.chunks.push({ dictationId, seq, audio, format }); + if (this.autoAck) { + this.emitAck(dictationId, seq); + } + } + + subscribeRawMessages(handler: FakeRawMessageListener): () => void { + this.rawMessageListeners.add(handler); + return () => this.rawMessageListeners.delete(handler); + } + + setConnected(isConnected: boolean): void { + this.isConnected = isConnected; + } + + emitAck(dictationId: string, ackSeq: number): void { + const message: FakeAck = { + type: "dictation_stream_ack", + payload: { dictationId, ackSeq }, + }; + for (const listener of this.rawMessageListeners) { + listener(message); + } } async finishDictationStream( @@ -51,12 +97,22 @@ const tick = async () => { await Promise.resolve(); }; +const promiseOutcome = async (promise: Promise, timeoutMs: number) => { + return Promise.race([ + promise.then( + () => "resolved" as const, + () => "rejected" as const, + ), + new Promise<"timed-out">((resolve) => setTimeout(() => resolve("timed-out"), timeoutMs)), + ]); +}; + describe("DictationStreamSender", () => { it("enqueues segments and sends them after stream start", async () => { const client = new FakeDaemonClient(); const ids = ["d1"]; const sender = new DictationStreamSender({ - client: client as unknown as DaemonClient, + client, format: "audio/pcm;rate=16000;bits=16", createDictationId: () => ids.shift() ?? "dX", }); @@ -77,7 +133,7 @@ describe("DictationStreamSender", () => { const client = new FakeDaemonClient(); const ids = ["d1", "d2"]; const sender = new DictationStreamSender({ - client: client as unknown as DaemonClient, + client, format: "audio/pcm;rate=16000;bits=16", createDictationId: () => ids.shift() ?? "dX", }); @@ -100,7 +156,7 @@ describe("DictationStreamSender", () => { const client = new FakeDaemonClient(); const ids = ["d1"]; const sender = new DictationStreamSender({ - client: client as unknown as DaemonClient, + client, format: "audio/pcm;rate=16000;bits=16", createDictationId: () => ids.shift() ?? "dX", }); @@ -121,7 +177,7 @@ describe("DictationStreamSender", () => { client.isConnected = false; const ids = ["d1"]; const sender = new DictationStreamSender({ - client: client as unknown as DaemonClient, + client, format: "audio/pcm;rate=16000;bits=16", createDictationId: () => ids.shift() ?? "dX", }); @@ -138,11 +194,131 @@ describe("DictationStreamSender", () => { expect(client.chunks.map((c) => c.seq)).toEqual([0, 1]); }); + it("keeps capture safe and buffers later segments when a chunk send throws", async () => { + const client = new FakeDaemonClient(); + const sender = new DictationStreamSender({ + client, + format: "audio/pcm;rate=16000;bits=16", + createDictationId: () => "d1", + }); + + sender.enqueueSegment("before-outage"); + await tick(); + + client.throwOnNextChunk = true; + client.disconnectOnChunkError = true; + + expect(() => sender.enqueueSegment("during-outage")).not.toThrow(); + expect(() => sender.enqueueSegment("after-outage")).not.toThrow(); + expect(sender.getSegmentCount()).toBe(3); + }); + + it("removes its raw-message listener when disposed", () => { + const client = new FakeDaemonClient(); + const sender = new DictationStreamSender({ + client, + format: "audio/pcm;rate=16000;bits=16", + }); + + expect(client.rawMessageListenerCount).toBe(1); + sender.dispose(); + expect(client.rawMessageListenerCount).toBe(0); + }); + + it("replays segments captured before, during, and after an outage from seq 0", async () => { + const client = new FakeDaemonClient(); + const ids = ["d1", "d2"]; + const sender = new DictationStreamSender({ + client, + format: "audio/pcm;rate=16000;bits=16", + createDictationId: () => ids.shift() ?? "dX", + }); + + sender.enqueueSegment("before-outage"); + await tick(); + + client.throwOnNextChunk = true; + client.disconnectOnChunkError = true; + expect(() => sender.enqueueSegment("during-outage")).not.toThrow(); + sender.enqueueSegment("after-outage"); + + client.setConnected(true); + await sender.restartStream("reconnect"); + + const replay = client.chunks + .filter((chunk) => chunk.dictationId === "d2") + .map((chunk) => [chunk.seq, chunk.audio]); + expect(replay).toEqual([ + [0, "before-outage"], + [1, "during-outage"], + [2, "after-outage"], + ]); + }); + + it("fails finish after a transport transition without losing buffered audio", async () => { + vi.useFakeTimers(); + try { + const client = new FakeDaemonClient(); + client.setConnected(false); + const ids = ["d1", "d2"]; + const sender = new DictationStreamSender({ + client, + format: "audio/pcm;rate=16000;bits=16", + createDictationId: () => ids.shift() ?? "dX", + }); + + for (let seq = 0; seq < 129; seq += 1) { + sender.enqueueSegment(`segment-${seq}`); + } + + client.setConnected(true); + const finish = sender.finish(sender.getFinalSeq()); + const outcome = promiseOutcome(finish, 1); + + await tick(); + client.setConnected(false); + await vi.advanceTimersByTimeAsync(1); + + await expect(outcome).resolves.toBe("rejected"); + expect(sender.getSegmentCount()).toBe(129); + expect(sender.getFinalSeq()).toBe(128); + + client.setConnected(true); + sender.resetStreamForReplay(); + await sender.restartStream("retry"); + await expect(sender.finish(128)).resolves.toEqual({ dictationId: "d2", text: "ok" }); + expect(client.chunks.filter((chunk) => chunk.dictationId === "d2")).toHaveLength(129); + } finally { + vi.useRealTimers(); + } + }); + + it("waits for server delivery acknowledgement before finishing", async () => { + const client = new FakeDaemonClient(); + client.autoAck = false; + const sender = new DictationStreamSender({ + client, + format: "audio/pcm;rate=16000;bits=16", + createDictationId: () => "d1", + }); + + sender.enqueueSegment("seg0"); + sender.enqueueSegment("seg1"); + await tick(); + + const finish = sender.finish(1); + await tick(); + expect(client.finishes).toEqual([]); + + client.emitAck("d1", 1); + await expect(finish).resolves.toEqual({ dictationId: "d1", text: "ok" }); + }); + it("does not replay long buffered native dictation in one synchronous burst", async () => { const client = new FakeDaemonClient(); client.isConnected = false; const sender = new DictationStreamSender({ - client: client as unknown as DaemonClient, + client, format: "audio/pcm;rate=16000;bits=16", createDictationId: () => "d1", }); diff --git a/packages/app/src/dictation/dictation-stream-sender.ts b/packages/app/src/dictation/dictation-stream-sender.ts index e4562e0b98..2388458843 100644 --- a/packages/app/src/dictation/dictation-stream-sender.ts +++ b/packages/app/src/dictation/dictation-stream-sender.ts @@ -1,13 +1,32 @@ import { generateMessageId } from "@/types/stream"; -import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; +import type { SessionOutboundMessage } from "@getpaseo/protocol/messages"; import { i18n } from "@/i18n/i18next"; const MAX_CHUNKS_PER_FLUSH_TURN = 128; +const DICTATION_DRAIN_IDLE_TIMEOUT_MS = 30_000; const waitForNextFlushTurn = (): Promise => new Promise((resolve) => setTimeout(resolve, 0)); +type DictationStreamAckMessage = Extract; + +interface DrainWaitResult { + kind: "signaled" | "timed-out"; +} + +export interface DictationStreamClient { + readonly isConnected: boolean; + startDictationStream(dictationId: string, format: string): Promise; + sendDictationStreamChunk(dictationId: string, seq: number, audio: string, format: string): void; + finishDictationStream( + dictationId: string, + finalSeq: number, + ): Promise<{ dictationId: string; text: string }>; + cancelDictationStream(dictationId: string): void; + subscribeRawMessages(handler: (message: SessionOutboundMessage) => void): () => void; +} + export interface DictationStreamSenderParams { - client: DaemonClient | null; + client: DictationStreamClient | null; format: string; createDictationId?: () => string; } @@ -30,28 +49,50 @@ interface DictationFinishResult { * so enqueues can't "miss" a flush due to in-flight await/coalescing bugs. */ export class DictationStreamSender { - private client: DaemonClient | null; + private client: DictationStreamClient | null = null; private readonly format: string; private readonly createDictationId: () => string; private dictationId: string | null = null; private sendSeq = 0; + private acknowledgedSeq = -1; private segments: string[] = []; private streamReady = false; private flushTimer: ReturnType | null = null; private drainWaiters: Array<() => void> = []; + private clientCleanup: (() => void) | null = null; private startGeneration = 0; private startPromise: Promise | null = null; constructor(params: DictationStreamSenderParams) { - this.client = params.client; this.format = params.format; this.createDictationId = params.createDictationId ?? generateMessageId; + this.setClient(params.client); } - setClient(client: DaemonClient | null): void { + setClient(client: DictationStreamClient | null): void { + if (this.client === client) { + return; + } + + if (this.clientCleanup) { + this.clientCleanup(); + this.clientCleanup = null; + } + this.resetStreamForReplay(); this.client = client; + + if (!client) { + return; + } + + this.clientCleanup = client.subscribeRawMessages((message) => { + if (message.type !== "dictation_stream_ack") { + return; + } + this.handleAck(message); + }); } getDictationId(): string | null { @@ -62,6 +103,10 @@ export class DictationStreamSender { return this.segments.length; } + dispose(): void { + this.setClient(null); + } + getFinalSeq(): number { return this.segments.length - 1; } @@ -71,22 +116,19 @@ export class DictationStreamSender { } clearAll(): void { - this.clearScheduledFlush(); - this.dictationId = null; - this.sendSeq = 0; + this.resetStreamForReplay(); this.segments = []; - this.streamReady = false; - this.startPromise = null; - this.startGeneration += 1; } resetStreamForReplay(): void { this.clearScheduledFlush(); this.dictationId = null; this.sendSeq = 0; + this.acknowledgedSeq = -1; this.streamReady = false; this.startPromise = null; this.startGeneration += 1; + this.resolveDrainWaiters(); } enqueueSegment(base64Pcm: string): void { @@ -113,6 +155,9 @@ export class DictationStreamSender { const client = this.client; const dictationId = this.dictationId; if (!client?.isConnected || !dictationId || !this.streamReady) { + if (!client?.isConnected && this.hasPendingSegments()) { + this.invalidateStream(); + } return 0; } @@ -120,10 +165,16 @@ export class DictationStreamSender { while (this.sendSeq < this.segments.length && sent < MAX_CHUNKS_PER_FLUSH_TURN) { const seq = this.sendSeq; const audio = this.segments[seq]; - client.sendDictationStreamChunk(dictationId, seq, audio, this.format); + try { + client.sendDictationStreamChunk(dictationId, seq, audio, this.format); + } catch { + this.invalidateStream(); + return sent; + } this.sendSeq = seq + 1; sent += 1; } + if (this.hasPendingSegments()) { this.scheduleFlush(); } else { @@ -144,6 +195,7 @@ export class DictationStreamSender { const dictationId = this.createDictationId(); this.dictationId = dictationId; this.sendSeq = 0; + this.acknowledgedSeq = -1; this.streamReady = false; const start = (async () => { @@ -161,7 +213,10 @@ export class DictationStreamSender { // If starting failed, keep the segments for retry but clear the stream so finish can error cleanly. if (this.startGeneration === generation && this.dictationId === dictationId) { this.dictationId = null; + this.sendSeq = 0; + this.acknowledgedSeq = -1; this.streamReady = false; + this.resolveDrainWaiters(); } throw error; }) @@ -198,7 +253,7 @@ export class DictationStreamSender { } this.flush(); - await this.waitForFlushDrain(); + await this.waitForFlushDrain(finalSeq); return client.finishDictationStream(dictationId, finalSeq); } @@ -215,6 +270,25 @@ export class DictationStreamSender { return this.sendSeq < this.segments.length; } + private hasPendingDelivery(finalSeq: number): boolean { + return this.hasPendingSegments() || this.acknowledgedSeq < finalSeq; + } + + private handleAck(message: DictationStreamAckMessage): void { + if (message.payload.dictationId !== this.dictationId) { + return; + } + if (message.payload.ackSeq <= this.acknowledgedSeq) { + return; + } + this.acknowledgedSeq = message.payload.ackSeq; + this.resolveDrainWaiters(); + } + + private invalidateStream(): void { + this.resetStreamForReplay(); + } + private scheduleFlush(): void { if (this.flushTimer) { return; @@ -233,19 +307,44 @@ export class DictationStreamSender { this.flushTimer = null; } - private async waitForFlushDrain(): Promise { - while (this.hasPendingSegments()) { + private async waitForFlushDrain(finalSeq: number): Promise { + while (this.hasPendingDelivery(finalSeq)) { const client = this.client; if (!client?.isConnected || !this.dictationId || !this.streamReady) { throw new Error("Failed to flush dictation stream"); } - await new Promise((resolve) => { - this.drainWaiters.push(resolve); - }); + const result = await this.waitForDrainSignal(); + if (result.kind === "timed-out") { + this.invalidateStream(); + throw new Error("Timed out waiting for dictation stream delivery"); + } await waitForNextFlushTurn(); } } + private waitForDrainSignal(): Promise { + let timeoutHandle: ReturnType | null = null; + let waiter: (() => void) | null = null; + const signal = new Promise<"signaled">((resolve) => { + waiter = () => resolve("signaled"); + this.drainWaiters.push(waiter); + }); + const timeout = new Promise<"timed-out">((resolve) => { + timeoutHandle = setTimeout(() => resolve("timed-out"), DICTATION_DRAIN_IDLE_TIMEOUT_MS); + }); + + return Promise.race([signal, timeout]) + .finally(() => { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + if (waiter) { + this.drainWaiters = this.drainWaiters.filter((candidate) => candidate !== waiter); + } + }) + .then((kind) => ({ kind })); + } + private resolveDrainWaiters(): void { const waiters = this.drainWaiters; this.drainWaiters = []; diff --git a/packages/app/src/file-pane/markdown-preview/document.test.ts b/packages/app/src/file-pane/markdown-preview/document.test.ts new file mode 100644 index 0000000000..020d7246d6 --- /dev/null +++ b/packages/app/src/file-pane/markdown-preview/document.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from "vitest"; +import { parseMarkdownPreviewDocument } from "./document"; + +describe("parseMarkdownPreviewDocument", () => { + it("separates YAML front matter from the Markdown body", () => { + expect( + parseMarkdownPreviewDocument(`--- +name: release-beta +description: Cut a beta release +allowed-tools: + - Bash + - Read +--- +# Release beta + +Ship it. +`), + ).toEqual({ + frontMatter: [ + { key: "name", value: "release-beta" }, + { key: "description", value: "Cut a beta release" }, + { key: "allowed-tools", value: "Bash, Read" }, + ], + body: "# Release beta\n\nShip it.\n", + }); + }); + + it("formats nested metadata without flattening its meaning", () => { + expect( + parseMarkdownPreviewDocument(`--- +metadata: + audience: maintainers + stable: true +--- +Body +`), + ).toEqual({ + frontMatter: [{ key: "metadata", value: "audience: maintainers\nstable: true" }], + body: "Body\n", + }); + }); + + it("leaves the document intact when front matter is invalid", () => { + const source = `--- +name: [broken +--- +Body +`; + + expect(parseMarkdownPreviewDocument(source)).toEqual({ + frontMatter: [], + body: source, + }); + }); + + it("leaves the document intact when YAML conversion rejects excessive aliases", () => { + const aliases = Array.from({ length: 101 }, () => "*item").join(", "); + const source = `--- +item: &item value +items: [${aliases}] +--- +Body +`; + + expect(parseMarkdownPreviewDocument(source)).toEqual({ + frontMatter: [], + body: source, + }); + }); + + it("does not treat a thematic break inside the document as front matter", () => { + const source = `Introduction + +--- + +Body +`; + + expect(parseMarkdownPreviewDocument(source)).toEqual({ + frontMatter: [], + body: source, + }); + }); +}); diff --git a/packages/app/src/file-pane/markdown-preview/document.ts b/packages/app/src/file-pane/markdown-preview/document.ts new file mode 100644 index 0000000000..7e2fff5206 --- /dev/null +++ b/packages/app/src/file-pane/markdown-preview/document.ts @@ -0,0 +1,70 @@ +import { parseDocument, stringify } from "yaml"; + +export interface MarkdownFrontMatterRow { + key: string; + value: string; +} + +export interface MarkdownPreviewDocument { + frontMatter: MarkdownFrontMatterRow[]; + body: string; +} + +const FRONT_MATTER_PATTERN = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/; + +export function parseMarkdownPreviewDocument(source: string): MarkdownPreviewDocument { + const match = FRONT_MATTER_PATTERN.exec(source); + if (!match) { + return { frontMatter: [], body: source }; + } + + try { + return parseFrontMatter(match, source); + } catch { + return { frontMatter: [], body: source }; + } +} + +function parseFrontMatter(match: RegExpExecArray, source: string): MarkdownPreviewDocument { + const document = parseDocument(match[1], { schema: "core" }); + if (document.errors.length > 0) { + return { frontMatter: [], body: source }; + } + + const metadata = document.toJS({ maxAliasCount: 100 }); + if (!isMetadataRecord(metadata)) { + return { frontMatter: [], body: source }; + } + + const frontMatter = Object.entries(metadata).map(([key, value]) => ({ + key, + value: formatFrontMatterValue(value), + })); + + return { + frontMatter, + body: source.slice(match[0].length), + }; +} + +function isMetadataRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function formatFrontMatterValue(value: unknown): string { + if (Array.isArray(value) && value.every(isScalar)) { + return value.map(formatScalar).join(", "); + } + if (isScalar(value)) { + return formatScalar(value); + } + return stringify(value).trim(); +} + +function isScalar(value: unknown): value is string | number | boolean | null { + return value === null || ["string", "number", "boolean"].includes(typeof value); +} + +function formatScalar(value: string | number | boolean | null): string { + return value === null ? "null" : String(value); +} diff --git a/packages/app/src/file-pane/markdown-preview/index.tsx b/packages/app/src/file-pane/markdown-preview/index.tsx new file mode 100644 index 0000000000..1e8a3bfbd6 --- /dev/null +++ b/packages/app/src/file-pane/markdown-preview/index.tsx @@ -0,0 +1,95 @@ +import { useMemo } from "react"; +import { Text, View } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; +import { MarkdownRenderer } from "@/components/markdown/renderer"; +import { MAX_CONTENT_WIDTH } from "@/constants/layout"; +import { parseMarkdownPreviewDocument } from "./document"; + +export function FileMarkdownPreview({ source }: { source: string }) { + const document = useMemo(() => parseMarkdownPreviewDocument(source), [source]); + + return ( + + + {document.frontMatter.length > 0 ? ( + + {document.frontMatter.map((row, index) => ( + 0 && styles.frontMatterRowBorder]} + > + + + {row.key} + + + + + {row.value} + + + + ))} + + ) : null} + + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + outerGutter: { + width: "100%", + paddingHorizontal: { + xs: theme.spacing[3], + md: theme.spacing[4], + }, + paddingVertical: theme.spacing[4], + }, + readingFrame: { + width: "100%", + maxWidth: MAX_CONTENT_WIDTH, + alignSelf: "center", + paddingHorizontal: theme.spacing[2], + }, + frontMatterTable: { + borderWidth: theme.borderWidth[1], + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.lg, + overflow: "hidden", + marginBottom: theme.spacing[6], + }, + frontMatterRow: { + flexDirection: "row", + alignItems: "stretch", + }, + frontMatterRowBorder: { + borderTopWidth: theme.borderWidth[1], + borderTopColor: theme.colors.border, + }, + frontMatterKeyCell: { + flexBasis: "30%", + maxWidth: 180, + paddingHorizontal: theme.spacing[3], + paddingVertical: theme.spacing[2], + backgroundColor: theme.colors.surface2, + }, + frontMatterValueCell: { + flex: 1, + minWidth: 0, + paddingHorizontal: theme.spacing[3], + paddingVertical: theme.spacing[2], + }, + frontMatterKey: { + color: theme.colors.foregroundMuted, + fontFamily: theme.fontFamily.mono, + fontSize: theme.fontSize.sm, + lineHeight: 20, + }, + frontMatterValue: { + color: theme.colors.foreground, + fontSize: theme.fontSize.sm, + lineHeight: 20, + }, +})); diff --git a/packages/app/src/file-pane/pane.tsx b/packages/app/src/file-pane/pane.tsx index a04c969b63..28c1f1478e 100644 --- a/packages/app/src/file-pane/pane.tsx +++ b/packages/app/src/file-pane/pane.tsx @@ -12,7 +12,6 @@ import type { DaemonClient, FileReadResult } from "@getpaseo/client/internal/dae import { Image as RNImage, ScrollView as RNScrollView, Text, View } from "react-native"; import { StyleSheet, UnistylesRuntime, withUnistyles } from "react-native-unistyles"; import { useTranslation } from "react-i18next"; -import { MarkdownRenderer } from "@/components/markdown/renderer"; import { useIsCompactFormFactor } from "@/constants/layout"; import { useSessionStore, type ExplorerFile } from "@/stores/session-store"; import { highlightCode, type HighlightToken } from "@getpaseo/highlight"; @@ -36,6 +35,7 @@ import { useAppSettings } from "@/hooks/use-settings"; import { useLiveFile } from "./live-file/hook"; import { FilePanelBar } from "./bar"; import { FileHtmlPreview } from "./html-preview"; +import { FileMarkdownPreview } from "./markdown-preview"; import { FileEditorModel, getFileConflictCallout, type FileConflictCallout } from "./editor/model"; import { createFileObservationSource } from "./editor/observation-source"; import { FileEditorView } from "./editor/view"; @@ -305,10 +305,9 @@ function FilePreviewBody({ - + ); @@ -890,9 +889,6 @@ const styles = StyleSheet.create((theme) => ({ previewCodeScrollContent: { padding: theme.spacing[4], }, - previewMarkdownScrollContent: { - padding: theme.spacing[4], - }, previewImageScrollContent: { flexGrow: 1, padding: theme.spacing[4], diff --git a/packages/app/src/git/actions-store.test.ts b/packages/app/src/git/actions-store.test.ts index dd25ddf56b..4d7892450d 100644 --- a/packages/app/src/git/actions-store.test.ts +++ b/packages/app/src/git/actions-store.test.ts @@ -66,6 +66,24 @@ describe("checkout-git-actions-store", () => { expect(order).toEqual(["pull", "push"]); }); + it("does not push when pull fails", async () => { + const push = vi.fn(); + const target = capability({ + pull: vi.fn(async () => ({ error: { message: "pull conflict" } })), + push, + }); + + await expect( + useCheckoutGitActionsStore.getState().pullAndPush({ serverId, target }), + ).rejects.toThrow("pull conflict"); + expect(push).not.toHaveBeenCalled(); + expect( + useCheckoutGitActionsStore + .getState() + .getStatus({ serverId, target, actionId: "pull-and-push" }), + ).toBe("idle"); + }); + it("returns to idle when a bound mutation reports an error", async () => { const target = capability({ refresh: vi.fn(async () => ({ error: { message: "not git" } })), @@ -78,6 +96,22 @@ describe("checkout-git-actions-store", () => { ).toBe("idle"); }); + it("discards selected paths through the bound workspace capability", async () => { + const discardChanges = vi.fn(async () => ({ success: true, error: null })); + const target = capability({ discardChanges }); + + await useCheckoutGitActionsStore + .getState() + .discardChanges({ serverId, target, paths: ["renamed.ts", "original.ts"] }); + + expect(discardChanges).toHaveBeenCalledWith({ paths: ["renamed.ts", "original.ts"] }); + expect( + useCheckoutGitActionsStore + .getState() + .getStatus({ serverId, target, actionId: "discard-changes" }), + ).toBe("success"); + }); + it("fails auto-merge before invoking Git when the daemon lacks the feature", async () => { const setForgeAutoMerge = vi.fn(); const target = capability({ setForgeAutoMerge }); diff --git a/packages/app/src/git/actions-store.ts b/packages/app/src/git/actions-store.ts index b8f2fa7704..f2307098f5 100644 --- a/packages/app/src/git/actions-store.ts +++ b/packages/app/src/git/actions-store.ts @@ -26,7 +26,8 @@ export type CheckoutGitAsyncActionId = | "enable-pr-auto-merge-rebase" | "disable-pr-auto-merge" | "merge-branch" - | "merge-from-base"; + | "merge-from-base" + | "discard-changes"; type CheckoutKey = string; type StatusMap = Partial>; @@ -120,6 +121,11 @@ interface CheckoutGitActionsStoreState { target: WorkspaceGitTarget; baseRef: string; }) => Promise; + discardChanges: (params: { + serverId: string; + target: WorkspaceGitTarget; + paths: string[]; + }) => Promise; } async function runCheckoutAction({ @@ -356,6 +362,20 @@ export const useCheckoutGitActionsStore = create() }, }); }, + + discardChanges: async ({ serverId, target, paths }) => { + await runCheckoutAction({ + serverId, + target, + actionId: "discard-changes", + run: async () => { + const payload = await target.discardChanges({ paths }); + if (!payload.success) { + throw new Error(payload.error?.message ?? "Failed to discard changes"); + } + }, + }); + }, })); export function __resetCheckoutGitActionsStoreForTests() { diff --git a/packages/app/src/git/diff-folder-row.tsx b/packages/app/src/git/diff-folder-row.tsx index 992165a07a..be5b76fc7b 100644 --- a/packages/app/src/git/diff-folder-row.tsx +++ b/packages/app/src/git/diff-folder-row.tsx @@ -1,11 +1,5 @@ import { useCallback, useMemo } from "react"; -import { - View, - Text, - Pressable, - type LayoutChangeEvent, - type PressableStateCallbackType, -} from "react-native"; +import { View, Text, type LayoutChangeEvent, type PressableStateCallbackType } from "react-native"; import { StyleSheet } from "react-native-unistyles"; import { DiffStat } from "@/components/diff-stat"; import { @@ -15,9 +9,13 @@ import { WORKSPACE_FILE_ROW_TRAILING_PADDING, WORKSPACE_FILE_ROW_VERTICAL_PADDING, WORKSPACE_TREE_ICON_LABEL_GAP, + WORKSPACE_TREE_ICON_SIZE, } from "@/components/tree-primitives"; import { type Theme } from "@/styles/theme"; import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style"; +import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu"; +import { FileActionsContextMenuContent } from "@/components/file-actions-menu"; +import { isWeb } from "@/constants/platform"; interface DiffFolderRowProps { /** full uncompressed directory path — the collapse identity */ @@ -25,20 +23,27 @@ interface DiffFolderRowProps { displayName: string; depth: number; collapsed: boolean; + isSelected: boolean; additions: number; deletions: number; onToggle: (dirPath: string) => void; + onCollapse: (dirPath: string) => void; + onSelect: (dirPath: string) => void; onHeightChange?: (height: number) => void; + onCopyPath?: (path: string) => void; + onCopyRelativePath?: (path: string) => void; + onReveal?: (path: string) => void; + revealTargetName?: string; + onDuplicate?: (path: string) => void; + onRevert?: (path: string) => void; testID?: string; } -function folderRowPressableStyle({ - hovered, - pressed, -}: PressableStateCallbackType & { hovered?: boolean }) { - // Subtle background highlight on hover/press, matching the Files explorer rows - // (entryRowActive) — no opacity darken. - return [styles.folderRow, (Boolean(hovered) || pressed) && styles.folderRowActive]; +function folderRowPressableStyle( + { hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }, + isSelected: boolean, +) { + return [styles.folderRow, (Boolean(hovered) || pressed || isSelected) && styles.folderRowActive]; } export function DiffFolderRow({ @@ -46,15 +51,39 @@ export function DiffFolderRow({ displayName, depth, collapsed, + isSelected, additions, deletions, onToggle, + onCollapse, + onSelect, onHeightChange, + onCopyPath, + onCopyRelativePath, + onReveal, + revealTargetName, + onDuplicate, + onRevert, testID, }: DiffFolderRowProps) { + const handleSelect = useCallback(() => { + onSelect(dirPath); + }, [dirPath, onSelect]); + const handlePress = useCallback(() => { + const selection = isWeb ? window.getSelection() : null; + if (selection && !selection.isCollapsed && selection.toString().length > 0) { + return; + } + handleSelect(); onToggle(dirPath); - }, [dirPath, onToggle]); + }, [dirPath, handleSelect, onToggle]); + + const pressableStyle = useCallback( + (state: PressableStateCallbackType & { hovered?: boolean }) => + folderRowPressableStyle(state, isSelected), + [isSelected], + ); const handleLayout = useCallback( (event: LayoutChangeEvent) => { @@ -63,39 +92,82 @@ export function DiffFolderRow({ [onHeightChange], ); + const handleCollapse = useCallback(() => { + onCollapse(dirPath); + }, [dirPath, onCollapse]); + + const handleCopyPath = useCallback(() => { + onCopyPath?.(dirPath); + }, [dirPath, onCopyPath]); + + const handleCopyRelativePath = useCallback(() => { + onCopyRelativePath?.(dirPath); + }, [dirPath, onCopyRelativePath]); + + const handleReveal = useCallback(() => { + onReveal?.(dirPath); + }, [dirPath, onReveal]); + + const handleDuplicate = useCallback(() => { + onDuplicate?.(dirPath); + }, [dirPath, onDuplicate]); + + const handleRevert = useCallback(() => { + onRevert?.(dirPath); + }, [dirPath, onRevert]); + const leftStyle = useMemo( () => [styles.left, inlineUnistylesStyle({ paddingLeft: treeRowPaddingLeft(depth) })], [depth], ); - const accessibilityState = useMemo(() => ({ expanded: !collapsed }), [collapsed]); + const accessibilityState = useMemo( + () => ({ expanded: !collapsed, selected: isSelected }), + [collapsed, isSelected], + ); return ( - - - - + + + + + + + + {displayName} + - - {displayName} - - - - - - + + + + + + ); } @@ -122,9 +194,11 @@ const styles = StyleSheet.create((theme: Theme) => ({ flex: 1, minWidth: 0, }, - chevronOpticalOffset: { - // The Changes directory chevron reads high beside the folder label. - transform: [{ translateY: 2 }], + chevronSlot: { + width: WORKSPACE_TREE_ICON_SIZE, + height: WORKSPACE_TREE_ICON_SIZE, + alignItems: "center", + justifyContent: "center", }, right: { flexDirection: "row", @@ -138,5 +212,6 @@ const styles = StyleSheet.create((theme: Theme) => ({ color: theme.colors.foreground, flexShrink: 1, minWidth: 0, + userSelect: "none", }, })); diff --git a/packages/app/src/git/diff-pane.tsx b/packages/app/src/git/diff-pane.tsx index 59ca827fb2..58d157bdc3 100644 --- a/packages/app/src/git/diff-pane.tsx +++ b/packages/app/src/git/diff-pane.tsx @@ -81,6 +81,7 @@ import * as Clipboard from "expo-clipboard"; import { FileActionsContextMenuContent } from "@/components/file-actions-menu"; import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu"; import { useFileDownload } from "@/hooks/use-file-download"; +import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon"; import { buildAbsoluteExplorerPath } from "@/utils/explorer-paths"; import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import { lineNumberGutterWidth } from "@/components/code-insets"; @@ -95,6 +96,7 @@ import { useCheckoutGitActionsStore } from "@/git/actions-store"; import { useRequiredWorkspaceGit } from "@/git/workspace-git"; import { useToast } from "@/contexts/toast-context"; import { useSessionStore } from "@/stores/session-store"; +import { confirmDialog } from "@/utils/confirm-dialog"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; import { useOverlayFlatListScrollbar } from "@/components/ui/overlay-scrollbar/use-overlay-flat-list-scrollbar"; import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style"; @@ -120,6 +122,7 @@ import { } from "@/review"; import { usePublishWorkingDiffAttachment, useWorkingDiff } from "@/git/use-working-diff"; import { DiffTooLargeState } from "@/git/diff-too-large-state"; +import { openDesktopTarget, useDesktopOpenTargets } from "@/workspace/desktop-open-targets"; export type { GitActionId, GitAction, GitActions } from "@/git/policy"; @@ -130,11 +133,14 @@ export function resolveDiffLayout( return canUseSplitLayout ? layout : "unified"; } -function fileHeaderPressableStyle({ - hovered, - pressed, -}: PressableStateCallbackType & { hovered?: boolean }) { - return [styles.fileHeader, (Boolean(hovered) || pressed) && styles.fileHeaderActive]; +function fileHeaderPressableStyle( + { hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }, + isSelected: boolean, +) { + return [ + styles.fileHeader, + (Boolean(hovered) || pressed || isSelected) && styles.fileHeaderActive, + ]; } interface HighlightedTextProps { @@ -209,16 +215,23 @@ interface DiffFileSectionProps { file: ParsedDiffFile; workspaceFileDragScope?: { serverId: string; workspaceId: string }; isExpanded: boolean; + isSelected?: boolean; /** Tree indentation level (0 on the flat/mobile path). */ depth?: number; /** Show the muted directory suffix (flat list); false inside the folder tree. */ showDir?: boolean; interactive?: boolean; onToggle?: (path: string) => void; + onSelect?: (path: string) => void; onOpenFile?: (path: string) => void; onAddToChat?: (path: string) => void; onCopyPath?: (path: string) => void; + onCopyRelativePath?: (path: string) => void; + onReveal?: (path: string) => void; + revealTargetName?: string; onDownload?: (path: string) => void; + onDuplicate?: (path: string) => void; + onRevert?: (path: string, oldPath?: string) => void; onHeaderHeightChange?: (path: string, height: number) => void; testID?: string; } @@ -227,6 +240,56 @@ const EMPTY_COMMENTS: readonly ReviewDraftComment[] = []; function noopStartComment(): void {} +function useDiscardChangesAction({ + serverId, + diffMode, +}: { + serverId: string; + diffMode: "uncommitted" | "base"; +}): ((path: string, oldPath?: string) => void) | undefined { + const { t } = useTranslation(); + const toast = useToast(); + const workspaceGit = useRequiredWorkspaceGit(); + const discardChanges = useCheckoutGitActionsStore((state) => state.discardChanges); + // COMPAT(checkoutDiscardChanges): added in v0.3.0, remove gate after 2027-02-08. + const discardSupported = useSessionStore( + (s) => s.sessions[serverId]?.serverInfo?.features?.checkoutDiscardChanges === true, + ); + const discardPath = useCallback( + async (path: string, oldPath?: string) => { + const confirmed = await confirmDialog({ + title: t("workspace.fileActions.confirmRevert.title"), + message: t("workspace.fileActions.confirmRevert.message", { name: path }), + confirmLabel: t("workspace.fileActions.confirmRevert.confirm"), + cancelLabel: t("workspace.fileActions.confirmRevert.cancel"), + destructive: true, + }); + if (!confirmed) { + return; + } + try { + await discardChanges({ + serverId, + target: workspaceGit, + paths: oldPath ? [path, oldPath] : [path], + }); + } catch (cause) { + toast.error( + cause instanceof Error ? cause.message : t("workspace.fileActions.confirmRevert.failed"), + ); + } + }, + [discardChanges, serverId, t, toast, workspaceGit], + ); + const handleDiscardPath = useCallback( + (path: string, oldPath?: string) => { + void discardPath(path, oldPath); + }, + [discardPath], + ); + return discardSupported && diffMode === "uncommitted" ? handleDiscardPath : undefined; +} + const DIFF_LINE_HOVER_STYLE = isWeb ? ({ cursor: "auto" } as const) : null; function LongPressableLine({ @@ -248,6 +311,10 @@ function LongPressableLine({ }) { const onStartComment = reviewActions?.onStartComment; const handlePress = useCallback(() => { + const selection = isWeb ? window.getSelection() : null; + if (selection && !selection.isCollapsed && selection.toString().length > 0) { + return; + } if (reviewTarget && onStartComment) { onStartComment(reviewTarget); } @@ -913,18 +980,84 @@ function SplitDiffColumn({ ); } +function DiffFileActionsContextMenuContent({ + file, + onOpenFile, + onAddToChat, + onCopyPath, + onCopyRelativePath, + onReveal, + revealTargetName, + onDownload, + onDuplicate, + onRevert, + testID, +}: Pick< + DiffFileSectionProps, + | "file" + | "onOpenFile" + | "onAddToChat" + | "onCopyPath" + | "onCopyRelativePath" + | "onReveal" + | "revealTargetName" + | "onDownload" + | "onDuplicate" + | "onRevert" + | "testID" +>) { + const handleOpenFile = useCallback(() => onOpenFile?.(file.path), [file.path, onOpenFile]); + const handleAddToChat = useCallback(() => onAddToChat?.(file.path), [file.path, onAddToChat]); + const handleCopyPath = useCallback(() => onCopyPath?.(file.path), [file.path, onCopyPath]); + const handleCopyRelativePath = useCallback( + () => onCopyRelativePath?.(file.path), + [file.path, onCopyRelativePath], + ); + const handleReveal = useCallback(() => onReveal?.(file.path), [file.path, onReveal]); + const handleDownload = useCallback(() => onDownload?.(file.path), [file.path, onDownload]); + const handleDuplicate = useCallback(() => onDuplicate?.(file.path), [file.path, onDuplicate]); + const handleRevert = useCallback( + () => onRevert?.(file.path, file.oldPath), + [file.oldPath, file.path, onRevert], + ); + + return ( + + ); +} + const DiffFileHeader = memo(function DiffFileHeader({ file, workspaceFileDragScope, isExpanded, + isSelected = false, depth = 0, showDir = true, interactive = true, onToggle, + onSelect, onOpenFile, onAddToChat, onCopyPath, + onCopyRelativePath, + onReveal, + revealTargetName, onDownload, + onDuplicate, + onRevert, onHeaderHeightChange, testID, }: DiffFileSectionProps) { @@ -939,29 +1072,24 @@ const DiffFileHeader = memo(function DiffFileHeader({ const pressHandledRef = useRef(false); const pressInRef = useRef<{ ts: number; pageX: number; pageY: number } | null>(null); + const handleSelect = useCallback(() => { + if (interactive) { + onSelect?.(file.path); + } + }, [file.path, interactive, onSelect]); + const toggleExpanded = useCallback(() => { if (!interactive) { return; } + const selection = isWeb ? window.getSelection() : null; + if (selection && !selection.isCollapsed && selection.toString().length > 0) { + return; + } pressHandledRef.current = true; + handleSelect(); onToggle?.(file.path); - }, [file.path, interactive, onToggle]); - - const handleOpenFile = useCallback(() => { - onOpenFile?.(file.path); - }, [file.path, onOpenFile]); - - const handleAddToChat = useCallback(() => { - onAddToChat?.(file.path); - }, [file.path, onAddToChat]); - - const handleCopyPath = useCallback(() => { - onCopyPath?.(file.path); - }, [file.path, onCopyPath]); - - const handleDownload = useCallback(() => { - onDownload?.(file.path); - }, [file.path, onDownload]); + }, [file.path, handleSelect, interactive, onToggle]); const handleLayout = useCallback( (event: LayoutChangeEvent) => { @@ -982,7 +1110,8 @@ const DiffFileHeader = memo(function DiffFileHeader({ const handleLongPress = useCallback(() => { pressHandledRef.current = true; - }, []); + handleSelect(); + }, [handleSelect]); const handlePressOut = useCallback( (event: { nativeEvent: { pageX: number; pageY: number } }) => { @@ -1009,16 +1138,20 @@ const DiffFileHeader = memo(function DiffFileHeader({ () => [styles.fileSectionHeaderContainer, isExpanded && styles.fileSectionHeaderExpanded], [isExpanded], ); + const accessibilityState = useMemo( + () => ({ expanded: isExpanded, selected: isSelected }), + [isExpanded, isSelected], + ); const headerPressableStyle = useCallback( (state: PressableStateCallbackType) => depth > 0 ? [ - fileHeaderPressableStyle(state), + fileHeaderPressableStyle(state, isSelected), inlineUnistylesStyle({ paddingLeft: treeRowPaddingLeft(depth) }), ] - : fileHeaderPressableStyle(state), - [depth], + : fileHeaderPressableStyle(state, isSelected), + [depth, isSelected], ); const fileName = file.path.split("/").pop() ?? file.path; @@ -1030,7 +1163,7 @@ const DiffFileHeader = memo(function DiffFileHeader({ > {showDir ? null : ( - + )} @@ -1061,7 +1194,14 @@ const DiffFileHeader = memo(function DiffFileHeader({ let trigger: ReactElement; if (!interactive) { trigger = ( - {headerContent} + void }) => event.preventDefault?.(), + }} + style={headerPressableStyle({ hovered: false, pressed: false })} + > + {headerContent} + ); } else { trigger = ( @@ -1073,7 +1213,10 @@ const DiffFileHeader = memo(function DiffFileHeader({ onPressIn={handlePressIn} onPressOut={handlePressOut} onLongPress={handleLongPress} + onContextMenu={handleSelect} onPress={toggleExpanded} + accessibilityState={accessibilityState} + aria-selected={isSelected} > {headerContent} @@ -1091,14 +1234,18 @@ const DiffFileHeader = memo(function DiffFileHeader({ {interactive ? ( - ) : null} @@ -1828,7 +1975,12 @@ interface SharedDiffViewProps { onOpenFile?: (path: string) => void; onAddToChat?: (path: string) => void; onCopyPath?: (path: string) => void; + onCopyRelativePath?: (path: string) => void; + onReveal?: (path: string) => void; + revealTargetName?: string; onDownload?: (path: string) => void; + onDuplicate?: (path: string) => void; + onRevert?: (path: string, oldPath?: string) => void; onExpandedPathsChange: (paths: string[]) => void; onCollapsedFoldersChange: (paths: string[]) => void; } @@ -1883,10 +2035,28 @@ export function SharedDiffView({ files, displayPreferences, mode }: SharedDiffVi const workspaceFileDragScope = mode.kind === "working_tree" ? mode.workspaceFileDragScope : undefined; const onCopyPath = mode.kind === "working_tree" ? mode.onCopyPath : undefined; + const onCopyRelativePath = mode.kind === "working_tree" ? mode.onCopyRelativePath : undefined; + const onReveal = mode.kind === "working_tree" ? mode.onReveal : undefined; + const revealTargetName = mode.kind === "working_tree" ? mode.revealTargetName : undefined; const onDownload = mode.kind === "working_tree" ? mode.onDownload : undefined; + const onDuplicate = mode.kind === "working_tree" ? mode.onDuplicate : undefined; + const onRevert = mode.kind === "working_tree" ? mode.onRevert : undefined; + // Keep selection independent from expansion so future keyboard actions (such as R to rename) + // can target the current VCS file or folder without changing its open state. + const [selectedPath, setSelectedPath] = useState(null); + const handleSelectPath = useCallback((path: string) => setSelectedPath(path), []); const compressedTree = useMemo(() => compressSingleChildChains(buildDiffTree(files)), [files]); const allFolderPaths = useMemo(() => collectDirPaths(compressedTree), [compressedTree]); const allFolderPathSet = useMemo(() => new Set(allFolderPaths), [allFolderPaths]); + useEffect(() => { + if ( + selectedPath && + !allFolderPathSet.has(selectedPath) && + !files.some((file) => file.path === selectedPath) + ) { + setSelectedPath(null); + } + }, [allFolderPathSet, files, selectedPath]); const effectiveCollapsedFolders = useMemo( () => new Set(Array.from(collapsedFolders).filter((path) => allFolderPathSet.has(path))), [allFolderPathSet, collapsedFolders], @@ -2182,6 +2352,40 @@ export function SharedDiffView({ files, displayPreferences, mode }: SharedDiffVi [computeItemOffset, effectiveCollapsedFolders, mode], ); + const handleCollapseFolder = useCallback( + (dirPath: string) => { + if (mode.kind !== "working_tree") { + return; + } + const targetOffset = computeItemOffset( + (item) => item.type === "folder" && item.dirPath === dirPath, + ); + const folderHeight = folderRowHeightRef.current || defaultHeaderHeightRef.current; + if ( + targetOffset !== null && + shouldAnchorHeaderBeforeCollapse({ + headerOffset: targetOffset, + headerHeight: folderHeight, + viewportOffset: diffListScrollOffsetRef.current, + viewportHeight: diffListViewportHeightRef.current, + }) + ) { + diffListRef.current?.scrollToOffset({ offset: targetOffset, animated: false }); + } + + const pathPrefix = `${dirPath}/`; + mode.onCollapsedFoldersChange([ + ...new Set([ + ...effectiveCollapsedFolders, + ...allFolderPaths.filter( + (folderPath) => folderPath === dirPath || folderPath.startsWith(pathPrefix), + ), + ]), + ]); + }, + [allFolderPaths, computeItemOffset, effectiveCollapsedFolders, mode], + ); + const renderFlatItem = useCallback( ({ item }: { item: DiffFlatItem }) => { if (item.type === "folder") { @@ -2191,10 +2395,19 @@ export function SharedDiffView({ files, displayPreferences, mode }: SharedDiffVi displayName={item.displayName} depth={item.depth} collapsed={item.collapsed} + isSelected={selectedPath === item.dirPath} additions={item.additions} deletions={item.deletions} onToggle={handleToggleFolder} + onCollapse={handleCollapseFolder} + onSelect={handleSelectPath} onHeightChange={handleFolderRowHeightChange} + onCopyPath={onCopyPath} + onCopyRelativePath={onCopyRelativePath} + onReveal={onReveal} + revealTargetName={revealTargetName} + onDuplicate={onDuplicate} + onRevert={onRevert} testID={`diff-folder-${item.dirPath}`} /> ); @@ -2205,14 +2418,21 @@ export function SharedDiffView({ files, displayPreferences, mode }: SharedDiffVi file={item.file} workspaceFileDragScope={workspaceFileDragScope} isExpanded={item.isExpanded} + isSelected={selectedPath === item.file.path} depth={item.depth} showDir={viewMode === "flat"} interactive={interactive} onToggle={interactive ? (onFilePress ?? handleToggleExpanded) : undefined} + onSelect={handleSelectPath} onOpenFile={onOpenFile} onAddToChat={onAddToChat} onCopyPath={onCopyPath} + onCopyRelativePath={onCopyRelativePath} + onReveal={onReveal} + revealTargetName={revealTargetName} onDownload={onDownload} + onDuplicate={onDuplicate} + onRevert={onRevert} onHeaderHeightChange={handleHeaderHeightChange} testID={`diff-file-${item.fileIndex}`} /> @@ -2236,6 +2456,8 @@ export function SharedDiffView({ files, displayPreferences, mode }: SharedDiffVi handleBodyHeightChange, handleFolderRowHeightChange, handleHeaderHeightChange, + handleCollapseFolder, + handleSelectPath, handleToggleExpanded, handleToggleFolder, layout, @@ -2249,7 +2471,13 @@ export function SharedDiffView({ files, displayPreferences, mode }: SharedDiffVi onOpenFile, onAddToChat, onCopyPath, + onCopyRelativePath, + onReveal, + revealTargetName, onDownload, + onDuplicate, + onRevert, + selectedPath, ], ); @@ -2620,7 +2848,7 @@ export function GitDiffPane({ onOpenFile, onAddToChat, }: GitDiffPaneProps) { - const target = useRequiredWorkspaceGit(); + const workspaceGit = useRequiredWorkspaceGit(); const { settings: appSettings } = useAppSettings(); const { t } = useTranslation(); const isMobile = useIsCompactFormFactor(); @@ -2661,6 +2889,11 @@ export function GitDiffPane({ const overflowToggleStyle = useMemo(() => buildOverflowButtonStyle(), []); const toast = useToast(); + const isLocalDaemon = useIsLocalDaemon(serverId); + const { targets: desktopOpenTargets } = useDesktopOpenTargets({ + isLocalExecution: isLocalDaemon, + }); + const fileManagerTarget = desktopOpenTargets.find((target) => target.kind === "file-manager"); const { changesTabOpen, toggleChanges: handleToggleChangesTab, @@ -2670,12 +2903,17 @@ export function GitDiffPane({ const refreshSupported = useSessionStore( (s) => s.sessions[serverId]?.serverInfo?.features?.checkoutRefresh === true, ); + const client = useSessionStore((state) => state.sessions[serverId]?.client); + // COMPAT(fsEntryDuplicate): added in v0.3.0, remove gate after 2027-02-09. + const fsEntryDuplicateEnabled = useSessionStore( + (state) => state.sessions[serverId]?.serverInfo?.features?.fsEntryDuplicate === true, + ); const runRefresh = useCheckoutGitActionsStore((s) => s.refresh); const isRefreshing = useCheckoutGitActionsStore((s) => s.getStatus({ serverId, - target, + target: workspaceGit, actionId: "refresh", }), ) === "pending"; @@ -2684,10 +2922,10 @@ export function GitDiffPane({ if (isRefreshing) { return; } - void runRefresh({ serverId, target }).catch((error) => { + void runRefresh({ serverId, target: workspaceGit }).catch((error) => { toast.error(error instanceof Error ? error.message : t("workspace.git.diff.failedRefresh")); }); - }, [isRefreshing, runRefresh, serverId, t, target, toast]); + }, [isRefreshing, runRefresh, serverId, t, toast, workspaceGit]); const { status, @@ -2708,7 +2946,7 @@ export function GitDiffPane({ reviewAttachment, } = useWorkingDiff({ serverId, - workspaceId: target.workspaceId, + workspaceId: workspaceGit.workspaceId, cwd, ignoreWhitespace: changesPreferences.hideWhitespace, enabled: enabled !== false, @@ -2754,7 +2992,7 @@ export function GitDiffPane({ [updateChangesPreferences], ); const changesTree = useChangesTreeState({ - workspaceId: target.workspaceId, + workspaceId: workspaceGit.workspaceId, cwd, files, viewMode, @@ -2779,12 +3017,51 @@ export function GitDiffPane({ }, [cwd], ); + const handleCopyRelativePath = useCallback((path: string) => { + void Clipboard.setStringAsync(path); + }, []); + const handleRevealPath = useCallback( + async (path: string) => { + if (!fileManagerTarget) { + return; + } + try { + await openDesktopTarget({ + editorId: fileManagerTarget.id, + workspacePath: cwd, + filePath: buildAbsoluteExplorerPath({ workspaceRoot: cwd, entryPath: path }), + }); + } catch (cause) { + toast.error( + cause instanceof Error ? cause.message : t("workspace.fileExplorer.errors.revealFailed"), + ); + } + }, + [cwd, fileManagerTarget, t, toast], + ); const handleDownloadPath = useCallback( (path: string) => { downloadFile({ fileName: path.split("/").pop() ?? path, path }); }, [downloadFile], ); + const handleDuplicatePath = useCallback( + async (path: string) => { + if (!client) { + return; + } + try { + const payload = await client.duplicateFileEntry({ cwd, path }); + if (!payload.success) { + toast.error(payload.error ?? t("workspace.fileExplorer.errors.duplicateFailed")); + } + } catch (cause) { + toast.error(cause instanceof Error ? cause.message : String(cause)); + } + }, + [client, cwd, t, toast], + ); + const onRevertPath = useDiscardChangesAction({ serverId, diffMode }); const workingTreeMode = useMemo( () => ({ kind: "working_tree" as const, @@ -2797,7 +3074,12 @@ export function GitDiffPane({ onOpenFile, onAddToChat, onCopyPath: handleCopyPath, + onCopyRelativePath: handleCopyRelativePath, + onReveal: fileManagerTarget ? handleRevealPath : undefined, + revealTargetName: fileManagerTarget?.label, onDownload: handleDownloadPath, + onDuplicate: fsEntryDuplicateEnabled ? handleDuplicatePath : undefined, + onRevert: onRevertPath, onExpandedPathsChange: changesTree.updateExpandedPaths, onCollapsedFoldersChange: changesTree.updateCollapsedFolders, }), @@ -2812,7 +3094,13 @@ export function GitDiffPane({ onOpenFile, onAddToChat, handleCopyPath, + handleCopyRelativePath, handleDownloadPath, + handleDuplicatePath, + handleRevealPath, + fileManagerTarget, + fsEntryDuplicateEnabled, + onRevertPath, changesTree.updateExpandedPaths, changesTree.updateCollapsedFolders, ], @@ -2867,7 +3155,12 @@ export function GitDiffPane({ ); return ( - + void }) => event.preventDefault?.(), + }} + style={styles.container} + > {isGit && (currentBranchName || isMobile) ? ( ({ color: theme.colors.foreground, flexShrink: 1, minWidth: 0, + userSelect: "none", }, fileDir: { fontSize: theme.fontSize.sm, @@ -3194,6 +3488,7 @@ const styles = StyleSheet.create((theme) => ({ color: theme.colors.foregroundMuted, flex: 1, minWidth: 0, + userSelect: "none", }, fileDirSpacer: { flex: 1, diff --git a/packages/app/src/git/use-actions.tsx b/packages/app/src/git/use-actions.tsx index 4bd4186f82..4cecaa05b9 100644 --- a/packages/app/src/git/use-actions.tsx +++ b/packages/app/src/git/use-actions.tsx @@ -2,6 +2,7 @@ import { useState, useCallback, useEffect, useMemo, type ReactElement } from "re import { Info } from "lucide-react-native"; import { withUnistyles } from "react-native-unistyles"; import AsyncStorage from "@react-native-async-storage/async-storage"; +import { z } from "zod"; import { useTranslation } from "react-i18next"; import type { Theme } from "@/styles/theme"; import { getForgePresentation, type Forge } from "@/git/forge"; @@ -30,6 +31,7 @@ import { type WorktreeArchiveWarningLabels } from "@/git/worktree-archive-warnin import { useWorkspaceArchive } from "@/workspace/use-workspace-archive"; import { resolveWorkspaceMapKeyByIdentity } from "@/utils/workspace-identity"; import { useRequiredWorkspaceGit } from "@/git/workspace-git"; +import { readValidatedString } from "@/storage/validated-storage"; export type { GitActionId, GitAction, GitActions } from "@/git/policy"; @@ -362,10 +364,10 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use } let isActive = true; setShipDefault("pr"); - AsyncStorage.getItem(shipDefaultStorageKey) + readValidatedString(AsyncStorage, shipDefaultStorageKey, z.enum(["pr", "merge"])) .then((value) => { if (!isActive) return; - if (value === "pr" || value === "merge") { + if (value) { setShipDefault(value); return; } diff --git a/packages/app/src/hooks/use-agent-form-state.ts b/packages/app/src/hooks/use-agent-form-state.ts index 037d68b2ef..4266fec8f1 100644 --- a/packages/app/src/hooks/use-agent-form-state.ts +++ b/packages/app/src/hooks/use-agent-form-state.ts @@ -14,6 +14,7 @@ import { } from "@/provider-selection/provider-selection"; import { filterSelectableModels } from "@/provider-selection/model-catalog"; import { OptimisticFormPreferences } from "@/create-agent-preferences/optimistic-preferences"; +import { applyAgentProfilePreferences } from "@/create-agent-preferences/preferences"; import { useProvidersSnapshot } from "./use-providers-snapshot"; import { useFormPreferences, @@ -37,6 +38,7 @@ import { type FormState, type ProviderModelsByProvider, } from "@/provider-selection/resolve-agent-form"; +import type { MaterializedAgentProfile } from "@/agent-profiles"; export type { FormInitialValues } from "@/provider-selection/resolve-agent-form"; @@ -56,7 +58,6 @@ export interface UseAgentFormStateResult { setSelectedServerId: (value: string | null) => void; setSelectedServerIdFromUser: (value: string | null) => void; selectedProvider: AgentProvider | null; - setProviderFromUser: (provider: AgentProvider) => void; selectedMode: string; setModeFromUser: (modeId: string) => void; selectedModel: string; @@ -82,6 +83,7 @@ export interface UseAgentFormStateResult { refreshProviderModels: (provider?: AgentProvider) => void; refetchProviderModelsIfStale: () => void; setProviderAndModelFromUser: (provider: AgentProvider, modelId: string) => void; + applyProfileFromUser: (profile: MaterializedAgentProfile) => void; clearProviderSelectionFromUser: () => void; workingDirIsEmpty: boolean; persistFormPreferences: () => Promise; @@ -256,11 +258,6 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg }), ); - const reducerStateRef = useRef({ form: formState, userModified }); - useEffect(() => { - reducerStateRef.current = { form: formState, userModified }; - }, [formState, userModified]); - useEffect(() => { if (!isVisible) { dispatch({ type: "RESET" }); @@ -432,27 +429,6 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg dispatch({ type: "SET_SERVER_ID_FROM_USER", value }); }, []); - const setProviderFromUser = useCallback( - (provider: AgentProvider) => { - if (!selectableProviderDefinitionMap.has(provider)) { - return; - } - const providerModels = allProviderModels.get(provider) ?? null; - const providerDef = selectableProviderDefinitionMap.get(provider); - const providerPrefs = preferenceOverlayRef.current.current().providerPreferences?.[provider]; - - dispatch({ - type: "SET_PROVIDER_FROM_USER", - provider, - providerModels, - providerDef, - providerPrefs, - }); - void updateCurrentPreferences({ provider }); - }, - [allProviderModels, selectableProviderDefinitionMap, updateCurrentPreferences], - ); - const setProviderAndModelFromUser = useCallback( (provider: AgentProvider, modelId: string) => { if (!selectableProviderDefinitionMap.has(provider)) { @@ -489,10 +465,64 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg dispatch({ type: "CLEAR_PROVIDER_SELECTION_FROM_USER" }); }, []); + const applyProfileFromUser = useCallback( + (profile: MaterializedAgentProfile) => { + const provider = profile.provider as AgentProvider; + if (!selectableProviderDefinitionMap.has(provider)) { + return; + } + + const previousProvider = formState.provider; + const providerDef = selectableProviderDefinitionMap.get(provider); + const providerModels = allProviderModels.get(provider) ?? null; + const providerPrefs = preferenceOverlayRef.current.current().providerPreferences?.[provider]; + const action = { + type: "APPLY_PROFILE_FROM_USER" as const, + provider, + modelId: profile.modelId, + modeId: profile.modeId, + thinkingOptionId: profile.thinkingOptionId, + providerDef, + providerModels, + providerPrefs, + }; + const nextState = resolveAgentForm({ form: formState, userModified, resolution }, action); + const previousProviderModeIds = previousProvider + ? (providerDefinitionMap.get(previousProvider)?.modes.map((mode) => mode.id) ?? []) + : []; + + dispatch(action); + void updateCurrentPreferences((current) => { + const { model, modeId, thinkingOptionId } = nextState.form; + return applyAgentProfilePreferences({ + preferences: current, + previousProvider, + previousProviderModeIds, + provider, + modelId: model, + modeId, + thinkingOptionId, + featureValues: profile.featureValues, + }); + }).catch((error) => { + console.warn("[useAgentFormState] persist profile preference failed", error); + }); + }, + [ + allProviderModels, + formState, + providerDefinitionMap, + resolution, + selectableProviderDefinitionMap, + updateCurrentPreferences, + userModified, + ], + ); + const setModeFromUser = useCallback( (modeId: string) => { dispatch({ type: "SET_MODE_FROM_USER", modeId }); - const provider = reducerStateRef.current.form.provider; + const provider = formState.provider; if (provider) { void updateCurrentPreferences((current) => mergeSelectedComposerPreferences({ @@ -505,12 +535,12 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg ); } }, - [updateCurrentPreferences], + [formState.provider, updateCurrentPreferences], ); const setModelFromUser = useCallback( (modelId: string) => { - const provider = reducerStateRef.current.form.provider; + const provider = formState.provider; const providerPrefs = provider ? preferenceOverlayRef.current.current().providerPreferences?.[provider] : undefined; @@ -534,13 +564,13 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg ); } }, - [availableModels, updateCurrentPreferences], + [availableModels, formState.provider, updateCurrentPreferences], ); const setThinkingOptionFromUser = useCallback( (thinkingOptionId: string) => { dispatch({ type: "SET_THINKING_OPTION_FROM_USER", thinkingOptionId }); - const { provider, model: modelId } = reducerStateRef.current.form; + const { provider, model: modelId } = formState; if (provider && modelId) { void updateCurrentPreferences((current) => mergeSelectedComposerPreferences({ @@ -555,7 +585,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg ); } }, - [updateCurrentPreferences], + [formState, updateCurrentPreferences], ); const setWorkingDir = useCallback((value: string) => { @@ -578,8 +608,8 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg ); const refetchProviderModelsIfStale = useCallback(() => { - refetchSnapshotIfStale(reducerStateRef.current.form.provider); - }, [refetchSnapshotIfStale]); + refetchSnapshotIfStale(formState.provider); + }, [formState.provider, refetchSnapshotIfStale]); const persistFormPreferences = useCallback(async () => { if (!formState.provider) { @@ -613,7 +643,6 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg setSelectedServerId, setSelectedServerIdFromUser, selectedProvider: formState.provider, - setProviderFromUser, selectedMode: formState.modeId, setModeFromUser, selectedModel: formState.model, @@ -639,6 +668,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg refreshProviderModels, refetchProviderModelsIfStale, setProviderAndModelFromUser, + applyProfileFromUser, clearProviderSelectionFromUser, workingDirIsEmpty, persistFormPreferences, @@ -652,7 +682,6 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg formState.workingDir, setSelectedServerId, setSelectedServerIdFromUser, - setProviderFromUser, setModeFromUser, setModelFromUser, setThinkingOptionFromUser, @@ -674,6 +703,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg refreshProviderModels, refetchProviderModelsIfStale, setProviderAndModelFromUser, + applyProfileFromUser, clearProviderSelectionFromUser, workingDirIsEmpty, persistFormPreferences, diff --git a/packages/app/src/hooks/use-changes-preferences/fakes.ts b/packages/app/src/hooks/use-changes-preferences/fakes.ts index 96e7a84907..62f194a5d1 100644 --- a/packages/app/src/hooks/use-changes-preferences/fakes.ts +++ b/packages/app/src/hooks/use-changes-preferences/fakes.ts @@ -16,5 +16,8 @@ export function createInMemoryKeyValueStorage( async setItem(key, value) { entries.set(key, value); }, + async removeItem(key) { + entries.delete(key); + }, }; } diff --git a/packages/app/src/hooks/use-changes-preferences/storage.ts b/packages/app/src/hooks/use-changes-preferences/storage.ts index 3ceaadeadd..05a0038dcd 100644 --- a/packages/app/src/hooks/use-changes-preferences/storage.ts +++ b/packages/app/src/hooks/use-changes-preferences/storage.ts @@ -1,11 +1,12 @@ import type { QueryClient } from "@tanstack/react-query"; import { z } from "zod"; +import { readValidatedJson, readValidatedString } from "@/storage/validated-storage"; export const CHANGES_PREFERENCES_STORAGE_KEY = "@paseo:changes-preferences"; export const LEGACY_WRAP_LINES_STORAGE_KEY = "diff-wrap-lines"; export const CHANGES_PREFERENCES_QUERY_KEY = ["changes-preferences"]; -const changesPreferencesSchema = z.object({ +const changesPreferencesSchema = z.strictObject({ layout: z.enum(["unified", "split"]).optional(), viewMode: z.enum(["flat", "tree"]).optional(), wrapLines: z.boolean().optional(), @@ -32,28 +33,28 @@ export const DEFAULT_CHANGES_PREFERENCES: ChangesPreferences = { export interface KeyValueStorage { getItem(key: string): Promise; setItem(key: string, value: string): Promise; + removeItem(key: string): Promise; } async function loadLegacyWrapLinesPreference(storage: KeyValueStorage): Promise { - const legacyValue = await storage.getItem(LEGACY_WRAP_LINES_STORAGE_KEY); - if (legacyValue === "true") { - return true; - } - if (legacyValue === "false") { - return false; - } - return null; + const legacyValue = await readValidatedString( + storage, + LEGACY_WRAP_LINES_STORAGE_KEY, + z.enum(["true", "false"]), + ); + return legacyValue === null ? null : legacyValue === "true"; } export async function loadChangesPreferencesFromStorage( storage: KeyValueStorage, ): Promise { - const stored = await storage.getItem(CHANGES_PREFERENCES_STORAGE_KEY); + const stored = await readValidatedJson( + storage, + CHANGES_PREFERENCES_STORAGE_KEY, + changesPreferencesSchema, + ); if (stored) { - const parsed = changesPreferencesSchema.safeParse(JSON.parse(stored)); - if (parsed.success) { - return { ...DEFAULT_CHANGES_PREFERENCES, ...parsed.data }; - } + return { ...DEFAULT_CHANGES_PREFERENCES, ...stored }; } const legacyWrapLines = await loadLegacyWrapLinesPreference(storage); diff --git a/packages/app/src/hooks/use-dictation.ts b/packages/app/src/hooks/use-dictation.ts index 09442b4c77..dde72964bc 100644 --- a/packages/app/src/hooks/use-dictation.ts +++ b/packages/app/src/hooks/use-dictation.ts @@ -449,6 +449,7 @@ export function useDictation(options: UseDictationOptions): UseDictationResult { attemptGuard.cancel(); stopDurationTracking(); void audioStop.current().catch(() => undefined); + senderRef.current?.dispose(); }; }, [stopDurationTracking]); diff --git a/packages/app/src/hooks/use-draft-agent-features.ts b/packages/app/src/hooks/use-draft-agent-features.ts index 207ac72e80..9ab23ad6d0 100644 --- a/packages/app/src/hooks/use-draft-agent-features.ts +++ b/packages/app/src/hooks/use-draft-agent-features.ts @@ -145,10 +145,15 @@ export function useDraftAgentFeatures(input: { [provider, updatePreferences], ); + const applyProfileFeatureValues = useCallback((values: Record) => { + setLocalFeatureValues(values); + }, []); + return { features, featureValues: effectiveFeatureValues, isLoading: featuresQuery.isLoading, setFeatureValue, + applyProfileFeatureValues, }; } diff --git a/packages/app/src/hooks/use-file-explorer-actions.ts b/packages/app/src/hooks/use-file-explorer-actions.ts index b908996728..e39d5e56a8 100644 --- a/packages/app/src/hooks/use-file-explorer-actions.ts +++ b/packages/app/src/hooks/use-file-explorer-actions.ts @@ -6,6 +6,7 @@ import { type ExplorerDirectory, } from "@/stores/session-store"; import { explorerFileFromReadResult } from "@/file-explorer/read-result"; +import { parentExplorerPath } from "@/utils/explorer-paths"; function createExplorerState(): AgentFileExplorerState { return { @@ -267,6 +268,80 @@ export function useFileExplorerActions(params: { serverId: string } & FileExplor [client, normalizedWorkspaceRoot, t, workspaceId], ); + const createEntry = useCallback( + async (input: { parentPath: string; name: string; kind: "file" | "directory" }) => { + if (!client || !normalizedWorkspaceRoot) { + return null; + } + const payload = await client.createFileEntry({ + cwd: normalizedWorkspaceRoot, + ...input, + }); + if (payload.success) { + await requestDirectoryListing(input.parentPath, { + recordHistory: false, + setCurrentPath: false, + }); + } + return payload; + }, + [client, normalizedWorkspaceRoot, requestDirectoryListing], + ); + + const renameEntry = useCallback( + async (input: { path: string; name: string }) => { + if (!client || !normalizedWorkspaceRoot) { + return null; + } + const payload = await client.renameFileEntry({ + cwd: normalizedWorkspaceRoot, + ...input, + }); + if (payload.success) { + await requestDirectoryListing(parentExplorerPath(input.path), { + recordHistory: false, + setCurrentPath: false, + }); + } + return payload; + }, + [client, normalizedWorkspaceRoot, requestDirectoryListing], + ); + + const duplicateEntry = useCallback( + async (path: string) => { + if (!client || !normalizedWorkspaceRoot) { + return null; + } + const payload = await client.duplicateFileEntry({ cwd: normalizedWorkspaceRoot, path }); + if (payload.success) { + await requestDirectoryListing(parentExplorerPath(path), { + recordHistory: false, + setCurrentPath: false, + }); + } + return payload; + }, + [client, normalizedWorkspaceRoot, requestDirectoryListing], + ); + + const deleteEntry = useCallback( + async (path: string) => { + if (!client || !normalizedWorkspaceRoot) { + return null; + } + const payload = await client.deleteFileEntry({ cwd: normalizedWorkspaceRoot, path }); + if (payload.success) { + await requestDirectoryListing(parentExplorerPath(path), { + recordHistory: false, + setCurrentPath: false, + }); + } + return payload; + }, + [client, normalizedWorkspaceRoot, requestDirectoryListing], + ); + const selectExplorerEntry = useCallback( (path: string | null) => { updateExplorerState((state) => ({ @@ -282,6 +357,10 @@ export function useFileExplorerActions(params: { serverId: string } & FileExplor requestDirectoryListing, requestFilePreview, requestFileDownloadToken, + createEntry, + renameEntry, + duplicateEntry, + deleteEntry, selectExplorerEntry, }; } diff --git a/packages/app/src/hooks/use-form-preferences.test.ts b/packages/app/src/hooks/use-form-preferences.test.ts index 4dd2774203..be0653fbc1 100644 --- a/packages/app/src/hooks/use-form-preferences.test.ts +++ b/packages/app/src/hooks/use-form-preferences.test.ts @@ -1,11 +1,6 @@ import { describe, expect, it } from "vitest"; -import { - buildFavoriteModelKey, - isFavoriteModel, - mergeProviderPreferences, - toggleFavoriteModel, -} from "./use-form-preferences"; +import { mergeProviderPreferences } from "./use-form-preferences"; describe("mergeProviderPreferences", () => { it("stores the selected model for a provider", () => { @@ -95,92 +90,3 @@ describe("mergeProviderPreferences", () => { }); }); }); - -describe("favorite model preferences", () => { - it("builds a stable favorite key from provider and model", () => { - expect(buildFavoriteModelKey({ provider: "claude", modelId: "sonnet-4.6" })).toBe( - "claude:sonnet-4.6", - ); - }); - - it("adds a model to favorites without dropping other preferences", () => { - expect( - toggleFavoriteModel({ - preferences: { - provider: "claude", - providerPreferences: { - claude: { - model: "claude-sonnet-4-6", - }, - }, - }, - provider: "codex", - modelId: "gpt-5.4", - }), - ).toEqual({ - provider: "claude", - providerPreferences: { - claude: { - model: "claude-sonnet-4-6", - }, - }, - favoriteModels: [ - { - provider: "codex", - modelId: "gpt-5.4", - }, - ], - }); - }); - - it("removes a model from favorites when toggled again", () => { - expect( - toggleFavoriteModel({ - preferences: { - favoriteModels: [ - { - provider: "codex", - modelId: "gpt-5.4", - }, - ], - }, - provider: "codex", - modelId: "gpt-5.4", - }), - ).toEqual({ - favoriteModels: [], - }); - }); - - it("reports whether a model is favorited", () => { - expect( - isFavoriteModel({ - preferences: { - favoriteModels: [ - { - provider: "codex", - modelId: "gpt-5.4", - }, - ], - }, - provider: "codex", - modelId: "gpt-5.4", - }), - ).toBe(true); - - expect( - isFavoriteModel({ - preferences: { - favoriteModels: [ - { - provider: "codex", - modelId: "gpt-5.4", - }, - ], - }, - provider: "claude", - modelId: "sonnet-4.6", - }), - ).toBe(false); - }); -}); diff --git a/packages/app/src/hooks/use-form-preferences.ts b/packages/app/src/hooks/use-form-preferences.ts index ee69498b6b..72006faa51 100644 --- a/packages/app/src/hooks/use-form-preferences.ts +++ b/packages/app/src/hooks/use-form-preferences.ts @@ -1,13 +1,8 @@ import { useCallback } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { - buildFavoriteModelKey, DEFAULT_FORM_PREFERENCES, - isFavoriteModel, mergeProviderPreferences, - toggleFavoriteModel, - type FavoriteModelPreference, - type FavoriteModelRow, type FormPreferences, type ProviderPreferences, } from "@/create-agent-preferences/preferences"; @@ -18,9 +13,9 @@ import { const FORM_PREFERENCES_QUERY_KEY = ["form-preferences"]; -export type { FavoriteModelPreference, FavoriteModelRow, FormPreferences, ProviderPreferences }; +export type { FormPreferences, ProviderPreferences }; -export { buildFavoriteModelKey, isFavoriteModel, mergeProviderPreferences, toggleFavoriteModel }; +export { mergeProviderPreferences }; async function loadFormPreferences(): Promise { return createAgentPreferencesService.load(); diff --git a/packages/app/src/hooks/use-keyboard-shortcut-overrides.ts b/packages/app/src/hooks/use-keyboard-shortcut-overrides.ts index 874b28122c..620e76efd0 100644 --- a/packages/app/src/hooks/use-keyboard-shortcut-overrides.ts +++ b/packages/app/src/hooks/use-keyboard-shortcut-overrides.ts @@ -1,15 +1,18 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { type QueryClient, useQuery, useQueryClient } from "@tanstack/react-query"; +import { z } from "zod"; import type { ShortcutOverrides } from "@/keyboard/keyboard-shortcuts"; import { createShortcutOverrideStore, type ShortcutOverrideStore, } from "@/keyboard/shortcut-override-store"; +import { readValidatedJson } from "@/storage/validated-storage"; const STORAGE_KEY = "@paseo:keyboard-shortcut-overrides"; const QUERY_KEY = ["keyboard-shortcut-overrides"]; const EMPTY_OVERRIDES: ShortcutOverrides = {}; +const ShortcutOverridesSchema = z.record(z.string(), z.string().nullable()); export interface UseKeyboardShortcutOverridesReturn { overrides: ShortcutOverrides; @@ -76,10 +79,10 @@ function getStore(queryClient: QueryClient): ShortcutOverrideStore { async function loadOverridesFromStorage(): Promise { try { - const stored = await AsyncStorage.getItem(STORAGE_KEY); - if (stored) { - return JSON.parse(stored) as ShortcutOverrides; - } + return ( + (await readValidatedJson(AsyncStorage, STORAGE_KEY, ShortcutOverridesSchema)) ?? + EMPTY_OVERRIDES + ); } catch (err) { console.error("[KeyboardShortcutOverrides] Failed to load overrides:", err); } diff --git a/packages/app/src/hooks/use-keyboard-shortcuts.ts b/packages/app/src/hooks/use-keyboard-shortcuts.ts index 6c5785b483..d7abeb1db2 100644 --- a/packages/app/src/hooks/use-keyboard-shortcuts.ts +++ b/packages/app/src/hooks/use-keyboard-shortcuts.ts @@ -201,7 +201,9 @@ export function useKeyboardShortcuts({ setCommandCenterFocusRestoreElement(browserFocusRestoreElement); } } - useKeyboardShortcutsStore.getState().setCommandCenterOpen(action.nextOpen); + useKeyboardShortcutsStore + .getState() + .setCommandCenterOpen(action.nextOpen, action.scope ?? null); return true; } case "shortcuts-dialog-toggle": diff --git a/packages/app/src/hooks/use-preferred-editor.ts b/packages/app/src/hooks/use-preferred-editor.ts index 480fd3d562..b216185182 100644 --- a/packages/app/src/hooks/use-preferred-editor.ts +++ b/packages/app/src/hooks/use-preferred-editor.ts @@ -1,6 +1,8 @@ import { useCallback } from "react"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { z } from "zod"; +import { readValidatedString } from "@/storage/validated-storage"; type EditorTargetId = string; @@ -8,11 +10,7 @@ const PREFERRED_EDITOR_STORAGE_KEY = "@paseo:preferred-editor"; const PREFERRED_EDITOR_QUERY_KEY = ["preferred-editor"]; async function loadPreferredEditor(): Promise { - const stored = await AsyncStorage.getItem(PREFERRED_EDITOR_STORAGE_KEY); - if (!stored) { - return null; - } - return stored.trim() || null; + return readValidatedString(AsyncStorage, PREFERRED_EDITOR_STORAGE_KEY, z.string().trim().min(1)); } export function resolvePreferredEditorId( diff --git a/packages/app/src/hooks/use-push-token-registration.ts b/packages/app/src/hooks/use-push-token-registration.ts deleted file mode 100644 index 4dd1209f9d..0000000000 --- a/packages/app/src/hooks/use-push-token-registration.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { useCallback, useEffect, useRef } from "react"; -import { Platform } from "react-native"; -import AsyncStorage from "@react-native-async-storage/async-storage"; -import * as Notifications from "expo-notifications"; -import Constants from "expo-constants"; -import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; -import { isWeb } from "@/constants/platform"; - -const STORAGE_PREFIX = "@paseo:expo-push-token:"; - -function getExpoProjectId(): string | null { - const constants = Constants as unknown as { - easConfig?: { projectId?: unknown }; - expoConfig?: { extra?: { eas?: { projectId?: unknown } } }; - }; - const fromEas = constants?.easConfig?.projectId; - if (typeof fromEas === "string" && fromEas.trim()) return fromEas.trim(); - - const fromExtra = constants?.expoConfig?.extra?.eas?.projectId; - if (typeof fromExtra === "string" && fromExtra.trim()) return fromExtra.trim(); - - return null; -} - -async function ensurePushPermission(): Promise { - const existing = await Notifications.getPermissionsAsync(); - if (existing.status === Notifications.PermissionStatus.GRANTED) return true; - if (!existing.canAskAgain) return false; - const requested = await Notifications.requestPermissionsAsync(); - return requested.status === Notifications.PermissionStatus.GRANTED; -} - -export function usePushTokenRegistration(params: { client: DaemonClient; serverId: string }): void { - const { client, serverId } = params; - const tokenRef = useRef(null); - const lastSentTokenRef = useRef(null); - - const registerIfPossible = useCallback(async () => { - if (isWeb) return; - if (!client.isConnected) return; - const token = tokenRef.current; - if (!token) return; - if (lastSentTokenRef.current === token) return; - lastSentTokenRef.current = token; - client.registerPushToken(token); - }, [client]); - - useEffect(() => { - if (isWeb) return; - - const storageKey = `${STORAGE_PREFIX}${serverId}`; - let cancelled = false; - - const run = async () => { - const cached = await AsyncStorage.getItem(storageKey); - if (cancelled) return; - if (cached && typeof cached === "string") { - tokenRef.current = cached; - } - - const granted = await ensurePushPermission(); - if (!granted || cancelled) return; - - if (Platform.OS === "android") { - await Notifications.setNotificationChannelAsync("default", { - name: "default", - importance: Notifications.AndroidImportance.DEFAULT, - }); - } - - const projectId = getExpoProjectId(); - if (!projectId) { - console.warn("[PushToken] Missing EAS projectId; cannot fetch Expo push token"); - return; - } - - const result = await Notifications.getExpoPushTokenAsync({ projectId }); - if (cancelled) return; - - const token = result.data; - if (typeof token !== "string" || !token.trim()) return; - - tokenRef.current = token; - await AsyncStorage.setItem(storageKey, token); - await registerIfPossible(); - }; - - void run().catch((error) => { - console.warn("[PushToken] Failed to register push token", error); - }); - - return () => { - cancelled = true; - }; - }, [registerIfPossible, serverId]); - - useEffect(() => { - const unsubscribe = client.subscribeConnectionStatus((state) => { - if (state.status === "connected") { - void registerIfPossible(); - } else { - // Re-register on the next successful connect. - lastSentTokenRef.current = null; - } - }); - if (client.isConnected) { - void registerIfPossible(); - } - return unsubscribe; - }, [client, registerIfPossible]); -} diff --git a/packages/app/src/hooks/use-settings/fakes.ts b/packages/app/src/hooks/use-settings/fakes.ts index 0e7ce025d8..691c3e9c18 100644 --- a/packages/app/src/hooks/use-settings/fakes.ts +++ b/packages/app/src/hooks/use-settings/fakes.ts @@ -17,6 +17,9 @@ export function createInMemoryKeyValueStorage( async setItem(key, value) { entries.set(key, value); }, + async removeItem(key) { + entries.delete(key); + }, }; } diff --git a/packages/app/src/hooks/use-settings/storage.test.ts b/packages/app/src/hooks/use-settings/storage.test.ts index b421c27cb8..87fdd300f1 100644 --- a/packages/app/src/hooks/use-settings/storage.test.ts +++ b/packages/app/src/hooks/use-settings/storage.test.ts @@ -15,6 +15,7 @@ import { type SettingsDeps, } from "./storage"; import { createFakeDesktopBridge, createInMemoryKeyValueStorage } from "./fakes"; +import { THEME_OPTIONS } from "@/styles/theme"; const LEGACY_SETTINGS_KEY = "@paseo:settings"; @@ -42,6 +43,18 @@ describe("loadAppSettingsFromStorage", () => { expect(result.theme).toBe("auto"); }); + it.each(THEME_OPTIONS)("loads the persisted $name theme", async ({ name }) => { + const deps = makeDeps({ + storage: createInMemoryKeyValueStorage({ + [APP_SETTINGS_KEY]: JSON.stringify({ theme: name }), + }), + }); + + const result = await loadAppSettingsFromStorage(deps); + + expect(result.theme).toBe(name); + }); + it("seeds storage with the client defaults when nothing is persisted", async () => { const deps = makeDeps(); @@ -382,14 +395,14 @@ describe("appearance settings", () => { expect((await loadAppSettingsFromStorage(deps)).toolCallDetailLevel).toBe("overview"); }); - it("maps an unrecognized tool call detail level to overview", async () => { + it("clears settings with an unrecognized tool call detail level", async () => { const deps = makeDeps({ storage: createInMemoryKeyValueStorage({ [APP_SETTINGS_KEY]: JSON.stringify({ toolCallDetailLevel: "unknown" }), }), }); - expect((await loadAppSettingsFromStorage(deps)).toolCallDetailLevel).toBe("overview"); + expect((await loadAppSettingsFromStorage(deps)).toolCallDetailLevel).toBe("detailed"); }); it("migrates a switched-off checks row item to the hidden checks display", async () => { diff --git a/packages/app/src/hooks/use-settings/storage.ts b/packages/app/src/hooks/use-settings/storage.ts index dbe180cc79..0b7a733de0 100644 --- a/packages/app/src/hooks/use-settings/storage.ts +++ b/packages/app/src/hooks/use-settings/storage.ts @@ -13,7 +13,9 @@ import { parseSidebarRowItems, type SidebarRowItems, } from "@/components/sidebar/display-preferences/row-items"; -import { THEME_TO_UNISTYLES, type ThemeName } from "@/styles/theme"; +import { THEME_OPTIONS, type ThemePreference } from "@/styles/theme"; +import { z } from "zod"; +import { readValidatedJson } from "@/storage/validated-storage"; export const APP_SETTINGS_KEY = "@paseo:app-settings"; export const APP_SETTINGS_QUERY_KEY = ["app-settings"]; @@ -27,7 +29,8 @@ export type WorkspaceTitleSource = "title" | "branch"; export type SidebarWorkspaceTrailing = "diff" | "timestamp" | "none"; export type ToolCallDetailLevel = "overview" | "detailed"; -const VALID_THEMES = new Set([...Object.keys(THEME_TO_UNISTYLES), "auto"]); +const VALID_THEMES = new Set(THEME_OPTIONS.map((option) => option.name)); +const ThemePreferenceSchema = z.enum(THEME_OPTIONS.map((option) => option.name)); const VALID_SERVICE_URL_BEHAVIORS = new Set(["ask", "in-app", "external"]); const VALID_WORKSPACE_TITLE_SOURCES = new Set(["title", "branch"]); const VALID_SIDEBAR_WORKSPACE_TRAILINGS = new Set([ @@ -48,7 +51,7 @@ export const MAX_CODE_FONT_SIZE = 22; // line-height 1.5×22=33 stays safe export const MAX_FONT_FAMILY_LENGTH = 200; export interface AppSettings { - theme: ThemeName | "auto"; + theme: ThemePreference; language: AppLanguage; sendBehavior: SendBehavior; serviceUrlBehavior: ServiceUrlBehavior; @@ -74,14 +77,45 @@ export interface Settings extends AppSettings { releaseChannel: ReleaseChannel; } -/** - * `sidebarRowItems` is widened back to `unknown` because it is still read for a value the - * current shape no longer has — see `isChecksHiddenByLegacyRowItem`. - */ -type StoredAppSettings = Partial> & { - compactToolCalls?: unknown; - sidebarRowItems?: unknown; -}; +const SidebarRowItemsSchema = z.strictObject({ + host: z.boolean().optional(), + changeRequest: z.boolean().optional(), + services: z.boolean().optional(), + checks: z.boolean().optional(), + scripts: z.boolean().optional(), +}); + +const StoredAppSettingsSchema = z.strictObject({ + theme: ThemePreferenceSchema.optional(), + language: z + .enum(["system", "ar", "en", "es", "fr", "ja", "ko", "pt-BR", "ru", "zh-CN"]) + .optional(), + sendBehavior: z.enum(["interrupt", "queue"]).optional(), + serviceUrlBehavior: z.enum(["ask", "in-app", "external"]).optional(), + terminalScrollbackLines: z.union([z.number(), z.string()]).optional(), + useLegacyTerminalRenderer: z.boolean().optional(), + uiFontFamily: z.string().optional(), + monoFontFamily: z.string().optional(), + uiFontSize: z.union([z.number(), z.string()]).optional(), + codeFontSize: z.union([z.number(), z.string()]).optional(), + syntaxTheme: z.string().refine(isSyntaxThemeId).optional(), + workspaceTitleSource: z.enum(["title", "branch"]).optional(), + sidebarWorkspaceTrailing: z.enum(["diff", "timestamp", "none"]).optional(), + sidebarRowItems: SidebarRowItemsSchema.optional(), + sidebarChecksDisplay: z.enum(["iconAndText", "icon", "none"]).optional(), + autoExpandReasoning: z.boolean().optional(), + toolCallDetailLevel: z.enum(["overview", "detailed"]).optional(), + compactToolCalls: z.boolean().optional(), + chatOutlineEnabled: z.boolean().optional(), + vimKeybindings: z.boolean().optional(), + // COMPAT(rendererDesktopSettings): these fields used to share this renderer-owned key. + manageBuiltInDaemon: z.boolean().optional(), + releaseChannel: z.enum(["stable", "beta"]).optional(), +}); + +const LegacyRendererSettingsSchema = StoredAppSettingsSchema; + +type StoredAppSettings = z.infer; export const DEFAULT_CLIENT_SETTINGS: AppSettings = { theme: "auto", @@ -114,6 +148,7 @@ export const DEFAULT_APP_SETTINGS: Settings = { export interface KeyValueStorage { getItem(key: string): Promise; setItem(key: string, value: string): Promise; + removeItem(key: string): Promise; } export interface DesktopSettingsBridge { @@ -146,17 +181,20 @@ export async function saveAppSettings(input: { export async function loadAppSettingsFromStorage(deps: SettingsDeps): Promise { try { - const stored = await deps.storage.getItem(APP_SETTINGS_KEY); + const stored = await readValidatedJson(deps.storage, APP_SETTINGS_KEY, StoredAppSettingsSchema); if (stored) { - return normalizeAppSettings(JSON.parse(stored)); + return normalizeAppSettings(stored); } - const legacyStored = await deps.storage.getItem(LEGACY_SETTINGS_KEY); + const legacyStored = await readValidatedJson( + deps.storage, + LEGACY_SETTINGS_KEY, + LegacyRendererSettingsSchema, + ); if (legacyStored) { - const legacyParsed = JSON.parse(legacyStored) as Record; const next = { ...DEFAULT_CLIENT_SETTINGS, - ...pickAppSettingsFromLegacy(legacyParsed), + ...pickAppSettingsFromLegacy(legacyStored), } satisfies AppSettings; await deps.storage.setItem(APP_SETTINGS_KEY, JSON.stringify(next)); return next; @@ -197,11 +235,11 @@ export async function loadSettingsFromStorage(deps: SettingsDeps): Promise { return result; } -function pickAppSettingsFromLegacy(legacy: Record): Partial { +function pickAppSettingsFromLegacy( + legacy: z.infer, +): Partial { const result: Partial = {}; if (legacy.theme === "dark" || legacy.theme === "light" || legacy.theme === "auto") { result.theme = legacy.theme; @@ -420,15 +460,11 @@ async function loadLegacyDesktopSettingsFromStorage(storage: KeyValueStorage): P async function loadRendererSettingsPayload( storage: KeyValueStorage, -): Promise | null> { - const current = await storage.getItem(APP_SETTINGS_KEY); +): Promise | null> { + const current = await readValidatedJson(storage, APP_SETTINGS_KEY, LegacyRendererSettingsSchema); if (current) { - return JSON.parse(current) as Record; + return current; } - const legacy = await storage.getItem(LEGACY_SETTINGS_KEY); - if (!legacy) { - return null; - } - return JSON.parse(legacy) as Record; + return readValidatedJson(storage, LEGACY_SETTINGS_KEY, LegacyRendererSettingsSchema); } diff --git a/packages/app/src/hooks/use-sidebar-pins.test.ts b/packages/app/src/hooks/use-sidebar-pins.test.ts index 80af801e07..f418027fca 100644 --- a/packages/app/src/hooks/use-sidebar-pins.test.ts +++ b/packages/app/src/hooks/use-sidebar-pins.test.ts @@ -39,6 +39,7 @@ describe("splitPinnedSidebarGroups", () => { pinnedWorkspaceKeys: ["w1"], pinnedAtByKey: { w1: "2026-01-01T00:00:00Z" }, }, + pinnedWorkspaceOrder: [], }); expect(result.pinnedChats).toHaveLength(1); expect(result.unpinnedProjects).toHaveLength(0); @@ -49,6 +50,7 @@ describe("splitPinnedSidebarGroups", () => { const result = splitPinnedSidebarGroups({ projects, keys: { pinnedWorkspaceKeys: [], pinnedAtByKey: {} }, + pinnedWorkspaceOrder: [], }); expect(result.unpinnedProjects).toHaveLength(1); }); @@ -61,6 +63,7 @@ describe("splitPinnedSidebarGroups", () => { pinnedWorkspaceKeys: ["w1"], pinnedAtByKey: { w1: "2026-01-01T00:00:00Z" }, }, + pinnedWorkspaceOrder: [], }); expect(result.pinnedChats.map((w) => w.workspaceKey)).toEqual(["w1"]); expect(result.unpinnedProjects[0]?.workspaces.map((w) => w.workspaceKey)).toEqual(["w2"]); @@ -77,6 +80,7 @@ describe("splitPinnedSidebarGroups", () => { newer: "2026-02-01T00:00:00Z", }, }, + pinnedWorkspaceOrder: [], }); expect(result.pinnedChats.map((workspace) => workspace.workspaceKey)).toEqual([ @@ -84,4 +88,26 @@ describe("splitPinnedSidebarGroups", () => { "older", ]); }); + + it("applies the saved order while keeping a newly pinned chat first", () => { + const projects = [project("p1", [placement("older"), placement("newer"), placement("new")])]; + const result = splitPinnedSidebarGroups({ + projects, + keys: { + pinnedWorkspaceKeys: ["older", "newer", "new"], + pinnedAtByKey: { + older: "2026-01-01T00:00:00Z", + newer: "2026-02-01T00:00:00Z", + new: "2026-03-01T00:00:00Z", + }, + }, + pinnedWorkspaceOrder: ["older", "newer"], + }); + + expect(result.pinnedChats.map((workspace) => workspace.workspaceKey)).toEqual([ + "new", + "older", + "newer", + ]); + }); }); diff --git a/packages/app/src/hooks/use-sidebar-pins.ts b/packages/app/src/hooks/use-sidebar-pins.ts index 37524b2461..fade0507d4 100644 --- a/packages/app/src/hooks/use-sidebar-pins.ts +++ b/packages/app/src/hooks/use-sidebar-pins.ts @@ -5,6 +5,7 @@ import type { SidebarProjectEntry, SidebarWorkspacePlacement, } from "@/hooks/use-sidebar-workspaces-list"; +import { applyStoredOrdering } from "@/hooks/sidebar-workspaces-view-model"; import { useSessionStore } from "@/stores/session-store"; export interface PinnedSidebarKeys { @@ -101,8 +102,9 @@ export function usePinnedSidebarKeys(projects: SidebarProjectEntry[]): PinnedSid export function splitPinnedSidebarGroups(input: { projects: SidebarProjectEntry[]; keys: PinnedSidebarKeys; + pinnedWorkspaceOrder: string[]; }): PinnedSidebarGroups { - const { projects, keys } = input; + const { projects, keys, pinnedWorkspaceOrder } = input; if (keys.pinnedWorkspaceKeys.length === 0) { return { pinnedChats: [], unpinnedProjects: projects }; } @@ -138,5 +140,12 @@ export function splitPinnedSidebarGroups(input: { ), ); - return { pinnedChats, unpinnedProjects }; + return { + pinnedChats: applyStoredOrdering({ + items: pinnedChats, + storedOrder: pinnedWorkspaceOrder, + getKey: (workspace) => workspace.workspaceKey, + }), + unpinnedProjects, + }; } diff --git a/packages/app/src/hooks/use-sidebar-workspaces-list.ts b/packages/app/src/hooks/use-sidebar-workspaces-list.ts index 38e9d84ffb..279d838eab 100644 --- a/packages/app/src/hooks/use-sidebar-workspaces-list.ts +++ b/packages/app/src/hooks/use-sidebar-workspaces-list.ts @@ -2,7 +2,10 @@ import { useCallback, useEffect, useMemo } from "react"; import { useStoreWithEqualityFn } from "zustand/traditional"; import { useCreateFlowStore } from "@/stores/create-flow-store"; import { useSessionStore } from "@/stores/session-store"; -import { useHydratedWorkspaceServerIds } from "@/stores/session-store-hooks"; +import { + useHydratedWorkspaceServerIds, + useWorkspaceDirectoryServerIds, +} from "@/stores/session-store-hooks"; import { workspaceEqualityFns } from "@/stores/session-store-hooks/selectors"; import { useHostProjects } from "@/projects/host-projects"; import { getHostRuntimeStore, useHostRegistryLoaded, useHosts } from "@/runtime/host-runtime"; @@ -132,8 +135,9 @@ export function useSidebarWorkspacesList(options?: { const persistedProjectOrder = useSidebarOrderStore((state) => state.projectOrder ?? EMPTY_ORDER); const hydratedServerIds = useHydratedWorkspaceServerIds(serverIds); + const directoryServerIds = useWorkspaceDirectoryServerIds(serverIds); - const hostProjects = useHostProjects(hydratedServerIds); + const hostProjects = useHostProjects(directoryServerIds); const sidebarModel = useMemo( () => diff --git a/packages/app/src/hosts/appearance.ts b/packages/app/src/hosts/appearance.ts index 8f4fc80a2a..e51f4410dc 100644 --- a/packages/app/src/hosts/appearance.ts +++ b/packages/app/src/hosts/appearance.ts @@ -1,5 +1,6 @@ import { IDENTITY_COLOR_NAMES, type IdentityColorName } from "@/styles/identity-colors"; import type { HostProfile } from "@/types/host-connection"; +import { z } from "zod"; export type HostColor = "none" | IdentityColorName; @@ -19,27 +20,18 @@ export interface HostAppearance { badgeDisplay: HostBadgeDisplay | null; } +export const HostAppearanceSchema: z.ZodType = z.strictObject({ + color: z.enum(["none", ...IDENTITY_COLOR_NAMES]), + badgeDisplay: z.enum(["name", "icon", "hidden"]).nullable(), +}); + export function defaultHostAppearance(): HostAppearance { return { color: "none", badgeDisplay: null }; } -function isHostColor(value: unknown): value is HostColor { - return HOST_COLORS.some((color) => color === value); -} - -function isHostBadgeDisplay(value: unknown): value is HostBadgeDisplay { - return HOST_BADGE_DISPLAYS.some((display) => display === value); -} - export function normalizeStoredHostAppearance(value: unknown): HostAppearance { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return defaultHostAppearance(); - } - const record = value as Record; - return { - color: isHostColor(record.color) ? record.color : "none", - badgeDisplay: isHostBadgeDisplay(record.badgeDisplay) ? record.badgeDisplay : null, - }; + const result = HostAppearanceSchema.safeParse(value); + return result.success ? result.data : defaultHostAppearance(); } export function resolveHostBadgeDisplay(input: { diff --git a/packages/app/src/i18n/resources.test.ts b/packages/app/src/i18n/resources.test.ts index 1c0ff7b8ca..20de9d0469 100644 --- a/packages/app/src/i18n/resources.test.ts +++ b/packages/app/src/i18n/resources.test.ts @@ -190,7 +190,11 @@ describe("translation resources", () => { expect(en.shell.menu.toggleSidebar).toBe("Toggle sidebar"); expect(en.shell.menu.open).toBe("Open menu"); expect(en.shell.menu.close).toBe("Close menu"); - expect(en.shell.commandCenter.placeholder).toBe("Search commands, workspaces, and agents..."); + expect(en.shell.commandCenter.placeholder).toBe( + "Search commands, files, workspaces, and agents...", + ); + expect(en.shell.commandCenter.filePlaceholder).toBe("Search files..."); + expect(en.shell.commandCenter.files).toBe("Files"); expect(en.shell.commandCenter.noMatches).toBe("No matches"); expect(en.shell.commandCenter.actions).toBe("Actions"); expect(en.shell.commandCenter.workspaces).toBe("Workspaces"); @@ -314,7 +318,7 @@ describe("translation resources", () => { it("includes provider selector and pairing keys for the Batch 4D migration", () => { expect(en.modelSelector.title).toBe("Select provider"); - expect(en.modelSelector.favorites).toBe("Favorites"); + expect(en.modelSelector.profiles).toBe("Profiles"); expect(en.providerCatalog.title).toBe("Add provider"); expect(en.providerCatalog.actions.installInstructions).toBe("Install instructions"); expect(en.pairing.link.title).toBe("Paste pairing link"); @@ -389,6 +393,14 @@ describe("translation resources", () => { expect(en.message.question.otherPlaceholder).toBe("Other..."); expect(en.message.todo.title).toBe("Tasks"); expect(en.message.todo.empty).toBe("No tasks yet."); + expect(en.message.todo.tasksProgressCurrent).toBe("{{completed}}/{{total}} tasks · {{task}}"); + expect(en.message.todo.activity).toEqual({ + created: "Created {{count}} tasks", + added: "Added", + started: "Started", + completed: "Completed", + reopened: "Reopened", + }); }); it("includes workspace tab toast keys for the Batch 4J migration", () => { diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index e1e1472818..b57546459e 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -54,14 +54,19 @@ export const ar: TranslationResources = { close: "إغلاق القائمة", }, commandCenter: { - placeholder: "ابحث في الأوامر ومساحات العمل والوكلاء...", + placeholder: "ابحث في الأوامر والملفات ومساحات العمل والوكلاء...", + filePlaceholder: "البحث في الملفات...", + searchingFiles: "جارٍ البحث في الملفات...", noMatches: "لا توجد مباريات", actions: "الإجراءات", + files: "الملفات", workspaces: "مساحات العمل", agents: "الوكلاء", newAgent: "وكيل جديد", addProject: "إضافة مشروع", home: "بيت", + groupByProject: "تجميع حسب المشروع", + groupByStatus: "تجميع حسب الحالة", modelGroupLabel: "النموذج", modelSearchKeywords: "تبديل النموذج تغيير النموذج تعيين النموذج اختيار النموذج", thinkingGroupLabel: "التفكير", @@ -269,6 +274,14 @@ export const ar: TranslationResources = { }, }, message: { + diagram: { + diagram: "مخطط", + zoomIn: "تكبير", + zoomOut: "تصغير", + resetZoom: "إعادة تعيين العرض", + viewSource: "عرض المصدر", + viewDiagram: "عرض المخطط", + }, actions: { copyCode: "نسخ الرمز", copyTurn: "نسخ بدوره", @@ -320,6 +333,15 @@ export const ar: TranslationResources = { todo: { title: "المهام", empty: "لا توجد مهام حتى الآن.", + tasksProgress: "{{completed}}/{{total}} مهام", + tasksProgressCurrent: "{{completed}}/{{total}} مهام · {{task}}", + activity: { + created: "تم إنشاء {{count}} مهام", + added: "أُضيفت", + started: "بدأت", + completed: "اكتملت", + reopened: "أُعيد فتحها", + }, }, compaction: { loading: "الضغط...", @@ -390,9 +412,32 @@ export const ar: TranslationResources = { fileActions: { openFile: "افتح الملف", copyPath: "نسخ المسار", + copyRelativePath: "نسخ المسار النسبي", + revealIn: "إظهار في {{target}}", download: "تحميل", addToChat: "إضافة إلى الدردشة", moreActions: "المزيد من الإجراءات", + newFile: "ملف جديد", + newFolder: "مجلد جديد", + collapseFolder: "طي المجلد", + rename: "إعادة تسمية", + duplicate: "تكرار", + revert: "تجاهل التغييرات", + delete: "حذف", + confirmDelete: { + fileTitle: "حذف الملف؟", + folderTitle: "حذف المجلد؟", + message: 'سيتم حذف "{{name}}" نهائيًا.', + confirm: "حذف", + cancel: "إلغاء", + }, + confirmRevert: { + title: "تجاهل التغييرات؟", + message: 'سيتم تجاهل التغييرات في "{{name}}" نهائيًا.', + confirm: "تجاهل", + cancel: "إلغاء", + failed: "فشل تجاهل التغييرات", + }, }, fileExplorer: { sort: { @@ -422,6 +467,15 @@ export const ar: TranslationResources = { }, errors: { failedToListDirectory: "فشل في سرد ​​الدليل", + createFailed: "فشل إنشاء العنصر", + renameFailed: "فشل إعادة تسمية العنصر", + duplicateFailed: "فشل تكرار العنصر", + revealFailed: "فشل إظهار العنصر", + deleteFailed: "فشل حذف العنصر", + }, + draft: { + filePlaceholder: "اسم الملف", + folderPlaceholder: "اسم المجلد", }, }, setup: { @@ -469,6 +523,8 @@ export const ar: TranslationResources = { screenshotCopied: "تم نسخ لقطة الشاشة إلى الحافظة", elementCopied: "تم نسخ العنصر إلى الحافظة", screenshotFailed: "تعذّر نسخ لقطة الشاشة", + selectorLoading: "انتظر حتى ينتهي تحميل الصفحة", + selectorFailed: "تعذّر بدء محدد العنصر", }, annotate: { title: "التعليق على العنصر", @@ -560,6 +616,7 @@ export const ar: TranslationResources = { reloadingAgent: "وكيل إعادة التحميل...", reloadedAgent: "وكيل إعادة تحميل", failedToReloadAgent: "فشل في إعادة تحميل الوكيل", + failedToCloseAgent: "فشل في إغلاق الوكيل", }, confirmations: { unsavedTitle: "تغييرات غير محفوظة", @@ -1354,14 +1411,18 @@ export const ar: TranslationResources = { loadingSelector: "جارٍ تحميل محدد النموذج...", error: "خطأ", defaultModel: "تقصير", - favorites: "المفضلة", - favoriteModel: "النموذج المفضل", - unfavoriteModel: "نموذج غير مفضل", + profiles: "الملفات الشخصية", + providers: "المزودون", + editProfiles: "تحرير", + editProfilesLabel: "تحرير ملفات الوكيل", + createProfile: "إنشاء ملف شخصي", modelCount: "{{count}} نموذج", modelCountPlural: "{{count}} نماذج", retry: "أعد المحاولة", retrying: "جارٍ إعادة المحاولة...", noMatches: "لا توجد نماذج تطابق بحثك", + noMatchesForQuery: 'لا توجد نماذج تطابق "{{query}}"', + searchAllPlaceholder: "ابحث في كل النماذج...", searchPlaceholder: "نماذج البحث...", openProviderSettings: "افتح إعدادات{{provider}}", }, @@ -1722,12 +1783,27 @@ export const ar: TranslationResources = { projects: "المشاريع", connections: "اتصالات", agents: "Agents", + metadata: "البيانات الوصفية", workspaces: "Workspaces", providers: "مقدمي الخدمات", usage: "الاستخدام", terminals: "Terminals", host: "نظرة عامة", }, + metadataGeneration: { + title: "إنشاء البيانات الوصفية", + description: + "اختر النموذج الذي يستخدمه Paseo لعناوين مساحات العمل وأسماء الفروع ورسائل الالتزام ومسودات طلبات السحب", + selection: "اختيار النموذج", + automatic: "تلقائي", + preferred: "يدوي", + automaticHint: "يختار Paseo نموذجًا سريعًا متاحًا", + preferredHint: "اختر النموذج الذي يستخدمه Paseo", + model: "النموذج", + fallbackHint: "إذا لم يكن متاحًا، يستخدم Paseo نموذجًا آخر متاحًا", + docs: "الوثائق", + saveError: "تعذر تحديث إنشاء البيانات الوصفية", + }, general: { title: "عام", browserData: { @@ -1915,10 +1991,10 @@ export const ar: TranslationResources = { resetAll: "إعادة ضبط الكل", }, sections: { - navigation: "ملاحة", + general: "عام", + workspaces: "المشاريع ومساحات العمل", tabsPanes: "علامات التبويب والأجزاء", - projects: "المشاريع", - panels: "لوحات", + layout: "التخطيط", agentInput: "إدخال Agent", }, help: { @@ -1946,6 +2022,7 @@ export const ar: TranslationResources = { moveTabDown: "حرك علامة التبويب لأسفل", closePane: "إغلاق الجزء", newTerminal: "محطة جديدة", + searchFiles: "البحث في الملفات", toggleCommandCenter: "تبديل مركز القيادة", showKeyboardShortcuts: "إظهار اختصارات لوحة المفاتيح", toggleLeftSidebar: "تبديل الشريط الجانبي الأيسر", @@ -2132,6 +2209,42 @@ export const ar: TranslationResources = { save: "Save", emptyState: "No profiles yet. Add one to launch terminals with a specific command.", }, + agentProfiles: { + sectionTitle: "ملفات تعريف الوكيل", + unavailable: "اتصل بهذا المضيف لإدارة ملفات تعريف الوكيل", + unsupported: "يشغّل هذا المضيف نسخة Daemon لا تدعم ملفات تعريف الوكيل بعد", + emptyState: "لا توجد ملفات تعريف بعد. أضف واحدًا لبدء الوكلاء من مزود ونموذج محفوظين.", + addProfileTitle: "إضافة ملف تعريف وكيل", + newProfile: "ملف تعريف جديد", + editProfile: "تعديل الملف", + editProfileTitle: "تعديل ملف تعريف الوكيل", + nameLabel: "الاسم", + namePlaceholder: "عمل الواجهة", + iconLabel: "الأيقونة", + noIcon: "بلا", + providerLabel: "المزود", + providerPlaceholder: "اختر مزودًا", + noProviders: "لا يوجد مزودون متاحون على هذا المضيف", + modelLabel: "النموذج", + noModels: "لا توجد نماذج يمكن الاختيار من بينها لهذا المزود", + modeLabel: "الوضع", + noModes: "لا توجد أوضاع يمكن الاختيار من بينها لهذا المزود", + thinkingLabel: "التفكير", + noThinkingOptions: "لا توجد مستويات تفكير لهذا النموذج", + featuresLabel: "السمات", + featureCount: "{{count}} سمة", + featureCountOne: "{{count}} سمة", + notesLabel: "ملاحظات للوكلاء", + notesPlaceholder: "استخدمه لعمل الواجهة — المكونات والتخطيط ورموز التصميم.", + notesHint: 'تُعاد بواسطة أداة MCP "list_profiles". اكتبها كتعليمة لوكيل آخر.', + save: "حفظ", + saving: "جارٍ الحفظ...", + remove: "إزالة", + removeConfirmTitle: "إزالة الملف؟", + removeConfirmMessage: 'إزالة "{{name}}"؟', + moveUp: "نقل لأعلى", + moveDown: "نقل لأسفل", + }, daemon: { rename: { editLabel: "تحرير التسمية", @@ -2297,6 +2410,9 @@ export const ar: TranslationResources = { docsTooltip: "راجع المستندات لمزيد من التفاصيل ومتغيرات البيئة المتاحة لهذه الأوامر", setup: "يثبت", setupAccessibility: "أوامر إعداد شجرة العمل", + uncommittedTitle: "ثبّت تغييرات paseo.json", + uncommittedDescription: + "تستخدم أشجار العمل الجديدة نص الإعداد البرمجي من الفرع الأساسي الذي تحدده.", teardown: "هدم", teardownAccessibility: "أوامر هدم شجرة العمل", }, diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index d95b8975b5..f93e57b33b 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -52,14 +52,19 @@ export const en = { close: "Close menu", }, commandCenter: { - placeholder: "Search commands, workspaces, and agents...", + placeholder: "Search commands, files, workspaces, and agents...", + filePlaceholder: "Search files...", + searchingFiles: "Searching files...", noMatches: "No matches", actions: "Actions", + files: "Files", workspaces: "Workspaces", agents: "Agents", newAgent: "New agent", addProject: "Add project", home: "Home", + groupByProject: "Group by project", + groupByStatus: "Group by status", modelGroupLabel: "Model", modelSearchKeywords: "switch model change model set model select model", thinkingGroupLabel: "Thinking", @@ -268,6 +273,14 @@ export const en = { }, }, message: { + diagram: { + diagram: "Diagram", + zoomIn: "Zoom in", + zoomOut: "Zoom out", + resetZoom: "Reset view", + viewSource: "View source", + viewDiagram: "View diagram", + }, actions: { copyCode: "Copy code", copyTurn: "Copy turn", @@ -319,6 +332,15 @@ export const en = { todo: { title: "Tasks", empty: "No tasks yet.", + tasksProgress: "{{completed}}/{{total}} tasks", + tasksProgressCurrent: "{{completed}}/{{total}} tasks · {{task}}", + activity: { + created: "Created {{count}} tasks", + added: "Added", + started: "Started", + completed: "Completed", + reopened: "Reopened", + }, }, compaction: { loading: "Compacting...", @@ -389,9 +411,32 @@ export const en = { fileActions: { openFile: "Open file", copyPath: "Copy path", + copyRelativePath: "Copy relative path", + revealIn: "Reveal in {{target}}", download: "Download", addToChat: "Add to chat", moreActions: "More actions", + newFile: "New file", + newFolder: "New folder", + collapseFolder: "Collapse folder", + rename: "Rename", + duplicate: "Duplicate", + revert: "Discard changes", + delete: "Delete", + confirmDelete: { + fileTitle: "Delete file?", + folderTitle: "Delete folder?", + message: '"{{name}}" will be permanently deleted.', + confirm: "Delete", + cancel: "Cancel", + }, + confirmRevert: { + title: "Discard changes?", + message: 'Changes to "{{name}}" will be permanently discarded.', + confirm: "Discard", + cancel: "Cancel", + failed: "Failed to discard changes", + }, }, fileExplorer: { sort: { @@ -421,6 +466,15 @@ export const en = { }, errors: { failedToListDirectory: "Failed to list directory", + createFailed: "Failed to create entry", + renameFailed: "Failed to rename entry", + duplicateFailed: "Failed to duplicate entry", + revealFailed: "Failed to reveal entry", + deleteFailed: "Failed to delete entry", + }, + draft: { + filePlaceholder: "File name", + folderPlaceholder: "Folder name", }, }, setup: { @@ -468,6 +522,8 @@ export const en = { screenshotCopied: "Copied screenshot to clipboard", elementCopied: "Copied element to clipboard", screenshotFailed: "Couldn't copy screenshot", + selectorLoading: "Wait for the page to finish loading", + selectorFailed: "Couldn't start element selector", }, annotate: { title: "Annotate element", @@ -559,6 +615,7 @@ export const en = { reloadingAgent: "Reloading agent...", reloadedAgent: "Reloaded agent", failedToReloadAgent: "Failed to reload agent", + failedToCloseAgent: "Failed to close agent", }, confirmations: { close: "Close", @@ -1365,14 +1422,18 @@ export const en = { loadingSelector: "Loading model selector...", error: "Error", defaultModel: "Default", - favorites: "Favorites", - favoriteModel: "Favorite model", - unfavoriteModel: "Unfavorite model", + profiles: "Profiles", + providers: "Providers", + editProfiles: "Edit", + editProfilesLabel: "Edit agent profiles", + createProfile: "Create profile", modelCount: "{{count}} model", modelCountPlural: "{{count}} models", retry: "Retry", retrying: "Retrying...", noMatches: "No models match your search", + noMatchesForQuery: 'No models match "{{query}}"', + searchAllPlaceholder: "Search all models...", searchPlaceholder: "Search models...", openProviderSettings: "Open {{provider}} settings", }, @@ -1733,12 +1794,27 @@ export const en = { projects: "Projects", connections: "Connections", agents: "Agents", + metadata: "Metadata", workspaces: "Workspaces", providers: "Providers", usage: "Usage", terminals: "Terminals", host: "Overview", }, + metadataGeneration: { + title: "Metadata generation", + description: + "Choose the model Paseo uses for workspace titles, branch names, commit messages, and pull request drafts", + selection: "Model selection", + automatic: "Automatic", + preferred: "Manual", + automaticHint: "Paseo picks a fast available model", + preferredHint: "Choose the model Paseo uses", + model: "Model", + fallbackHint: "If it is unavailable, Paseo falls back to another available model", + docs: "Docs", + saveError: "Unable to update metadata generation", + }, general: { title: "General", browserData: { @@ -1925,10 +2001,10 @@ export const en = { resetAll: "Reset all", }, sections: { - navigation: "Navigation", + general: "General", + workspaces: "Projects & Workspaces", tabsPanes: "Tabs & Panes", - projects: "Projects", - panels: "Panels", + layout: "Layout", agentInput: "Agent Input", }, help: { @@ -1956,6 +2032,7 @@ export const en = { moveTabDown: "Move tab down", closePane: "Close pane", newTerminal: "New terminal", + searchFiles: "Search files", toggleCommandCenter: "Toggle command center", showKeyboardShortcuts: "Show keyboard shortcuts", toggleLeftSidebar: "Toggle left sidebar", @@ -2143,6 +2220,43 @@ export const en = { save: "Save", emptyState: "No profiles yet. Add one to launch terminals with a specific command.", }, + agentProfiles: { + sectionTitle: "Agent profiles", + unavailable: "Connect to this host to manage agent profiles", + unsupported: "This host runs a daemon that does not support agent profiles yet", + emptyState: "No profiles yet. Add one to start agents from a saved provider and model.", + addProfileTitle: "Add agent profile", + newProfile: "New profile", + editProfile: "Edit profile", + editProfileTitle: "Edit agent profile", + nameLabel: "Name", + namePlaceholder: "UI work", + iconLabel: "Icon", + noIcon: "None", + providerLabel: "Provider", + providerPlaceholder: "Select a provider", + noProviders: "No providers are available on this host", + modelLabel: "Model", + noModels: "This provider has no models to choose from", + modeLabel: "Mode", + noModes: "This provider has no modes to choose from", + thinkingLabel: "Thinking", + noThinkingOptions: "This model has no thinking levels", + featuresLabel: "Features", + featureCount: "{{count}} features", + featureCountOne: "{{count}} feature", + notesLabel: "Notes for agents", + notesPlaceholder: "Use for UI work — components, layout and design tokens.", + notesHint: + "Returned by the list_profiles MCP tool. Write it as an instruction to another agent.", + save: "Save", + saving: "Saving...", + remove: "Remove", + removeConfirmTitle: "Remove profile?", + removeConfirmMessage: 'Remove "{{name}}"?', + moveUp: "Move up", + moveDown: "Move down", + }, daemon: { rename: { editLabel: "Edit label", @@ -2310,6 +2424,9 @@ export const en = { "See docs for more details and the environment variables available to these commands", setup: "Setup", setupAccessibility: "Worktree setup commands", + uncommittedTitle: "Commit paseo.json changes", + uncommittedDescription: + "New worktrees use the setup script from the base branch you select.", teardown: "Teardown", teardownAccessibility: "Worktree teardown commands", }, diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index 13abeb0e27..8720e61bd6 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -54,14 +54,19 @@ export const es: TranslationResources = { close: "Cerrar menú", }, commandCenter: { - placeholder: "Buscar comandos, espacios de trabajo y agentes...", + placeholder: "Buscar comandos, archivos, espacios de trabajo y agentes...", + filePlaceholder: "Buscar archivos...", + searchingFiles: "Buscando archivos...", noMatches: "No hay coincidencias", actions: "Comportamiento", + files: "Archivos", workspaces: "Espacios de trabajo", agents: "Agentes", newAgent: "Nuevo agente", addProject: "Agregar proyecto", home: "Hogar", + groupByProject: "Agrupar por proyecto", + groupByStatus: "Agrupar por estado", modelGroupLabel: "Modelo", modelSearchKeywords: "cambiar modelo modificar modelo establecer modelo seleccionar modelo", thinkingGroupLabel: "Razonamiento", @@ -272,6 +277,14 @@ export const es: TranslationResources = { }, }, message: { + diagram: { + diagram: "Diagrama", + zoomIn: "Acercar", + zoomOut: "Alejar", + resetZoom: "Restablecer vista", + viewSource: "Ver fuente", + viewDiagram: "Ver diagrama", + }, actions: { copyCode: "Copiar código", copyTurn: "Copiar turno", @@ -323,6 +336,15 @@ export const es: TranslationResources = { todo: { title: "Tareas", empty: "Aún no hay tareas.", + tasksProgress: "{{completed}}/{{total}} tareas", + tasksProgressCurrent: "{{completed}}/{{total}} tareas · {{task}}", + activity: { + created: "Se crearon {{count}} tareas", + added: "Añadida", + started: "Iniciada", + completed: "Completada", + reopened: "Reabierta", + }, }, compaction: { loading: "Compactando...", @@ -394,9 +416,32 @@ export const es: TranslationResources = { fileActions: { openFile: "Abrir archivo", copyPath: "Copiar ruta", + copyRelativePath: "Copiar ruta relativa", + revealIn: "Mostrar en {{target}}", download: "Descargar", addToChat: "Añadir al chat", moreActions: "Más acciones", + newFile: "Nuevo archivo", + newFolder: "Nueva carpeta", + collapseFolder: "Contraer carpeta", + rename: "Renombrar", + duplicate: "Duplicar", + revert: "Descartar cambios", + delete: "Eliminar", + confirmDelete: { + fileTitle: "¿Eliminar archivo?", + folderTitle: "¿Eliminar carpeta?", + message: '"{{name}}" se eliminará permanentemente.', + confirm: "Eliminar", + cancel: "Cancelar", + }, + confirmRevert: { + title: "¿Descartar cambios?", + message: 'Los cambios en "{{name}}" se descartarán permanentemente.', + confirm: "Descartar", + cancel: "Cancelar", + failed: "No se pudieron descartar los cambios", + }, }, fileExplorer: { sort: { @@ -426,6 +471,15 @@ export const es: TranslationResources = { }, errors: { failedToListDirectory: "No se pudo listar el directorio", + createFailed: "No se pudo crear la entrada", + renameFailed: "No se pudo renombrar la entrada", + duplicateFailed: "No se pudo duplicar la entrada", + revealFailed: "No se pudo mostrar la entrada", + deleteFailed: "No se pudo eliminar la entrada", + }, + draft: { + filePlaceholder: "Nombre del archivo", + folderPlaceholder: "Nombre de la carpeta", }, }, setup: { @@ -473,6 +527,8 @@ export const es: TranslationResources = { screenshotCopied: "Captura copiada al portapapeles", elementCopied: "Elemento copiado al portapapeles", screenshotFailed: "No se pudo copiar la captura", + selectorLoading: "Espera a que la página termine de cargar", + selectorFailed: "No se pudo iniciar el selector de elementos", }, annotate: { title: "Anotar elemento", @@ -565,6 +621,7 @@ export const es: TranslationResources = { reloadingAgent: "Agente de recarga...", reloadedAgent: "Agente recargado", failedToReloadAgent: "No se pudo recargar el agente", + failedToCloseAgent: "No se pudo cerrar el agente", }, confirmations: { unsavedTitle: "Cambios sin guardar", @@ -1395,14 +1452,18 @@ export const es: TranslationResources = { loadingSelector: "Cargando selector de modelo...", error: "Error", defaultModel: "Por defecto", - favorites: "Favoritos", - favoriteModel: "modelo favorito", - unfavoriteModel: "Modelo no favorito", + profiles: "Perfiles", + providers: "Proveedores", + editProfiles: "Editar", + editProfilesLabel: "Editar perfiles de agente", + createProfile: "Crear perfil", modelCount: "{{count}} modelo", modelCountPlural: "{{count}} modelos", retry: "Rever", retrying: "Reintentando...", noMatches: "Ningún modelo coincide con tu búsqueda", + noMatchesForQuery: "Ningún modelo coincide con «{{query}}»", + searchAllPlaceholder: "Buscar en todos los modelos...", searchPlaceholder: "Buscar modelos...", openProviderSettings: "Abrir configuración de{{provider}}", }, @@ -1765,12 +1826,27 @@ export const es: TranslationResources = { projects: "Proyectos", connections: "Conexiones", agents: "Agents", + metadata: "Metadatos", workspaces: "Workspaces", providers: "Proveedores", usage: "Uso", terminals: "Terminals", host: "Resumen", }, + metadataGeneration: { + title: "Generación de metadatos", + description: + "Elige el modelo que Paseo usa para títulos de espacios de trabajo, nombres de ramas, mensajes de commit y borradores de pull requests", + selection: "Selección de modelo", + automatic: "Automática", + preferred: "Manual", + automaticHint: "Paseo elige un modelo rápido disponible", + preferredHint: "Elige el modelo que usa Paseo", + model: "Modelo", + fallbackHint: "Si no está disponible, Paseo usa otro modelo disponible", + docs: "Documentación", + saveError: "No se pudo actualizar la generación de metadatos", + }, general: { title: "General", browserData: { @@ -1963,10 +2039,10 @@ export const es: TranslationResources = { resetAll: "Restablecer todo", }, sections: { - navigation: "Navegación", + general: "General", + workspaces: "Proyectos y espacios de trabajo", tabsPanes: "Pestañas y paneles", - projects: "Proyectos", - panels: "Paneles", + layout: "Diseño", agentInput: "EntradaAgent", }, help: { @@ -1994,6 +2070,7 @@ export const es: TranslationResources = { moveTabDown: "Mover pestaña hacia abajo", closePane: "Cerrar panel", newTerminal: "Nueva terminal", + searchFiles: "Buscar archivos", toggleCommandCenter: "Alternar centro de comando", showKeyboardShortcuts: "Mostrar atajos de teclado", toggleLeftSidebar: "Alternar barra lateral izquierda", @@ -2182,6 +2259,44 @@ export const es: TranslationResources = { save: "Save", emptyState: "No profiles yet. Add one to launch terminals with a specific command.", }, + agentProfiles: { + sectionTitle: "Perfiles de agente", + unavailable: "Conéctese a este host para administrar perfiles de agente", + unsupported: "Este host ejecuta un daemon que aún no admite perfiles de agente", + emptyState: + "Aún no hay perfiles. Agregue uno para iniciar agentes con un proveedor y un modelo guardados.", + addProfileTitle: "Agregar perfil de agente", + newProfile: "Nuevo perfil", + editProfile: "Editar perfil", + editProfileTitle: "Editar perfil de agente", + nameLabel: "Nombre", + namePlaceholder: "Trabajo de UI", + iconLabel: "Icono", + noIcon: "Ninguno", + providerLabel: "Proveedor", + providerPlaceholder: "Seleccione un proveedor", + noProviders: "No hay proveedores disponibles en este host", + modelLabel: "Modelo", + noModels: "Este proveedor no tiene modelos para elegir", + modeLabel: "Modo", + noModes: "Este proveedor no tiene modos para elegir", + thinkingLabel: "Pensamiento", + noThinkingOptions: "Este modelo no tiene niveles de pensamiento", + featuresLabel: "Características", + featureCount: "{{count}} características", + featureCountOne: "{{count}} característica", + notesLabel: "Notas para los agentes", + notesPlaceholder: "Úselo para trabajo de UI: componentes, diseño y tokens de diseño.", + notesHint: + 'Devuelto por la herramienta MCP "list_profiles". Escríbalo como una instrucción para otro agente.', + save: "Guardar", + saving: "Guardando...", + remove: "Eliminar", + removeConfirmTitle: "¿Eliminar perfil?", + removeConfirmMessage: '¿Eliminar "{{name}}"?', + moveUp: "Mover hacia arriba", + moveDown: "Mover hacia abajo", + }, daemon: { rename: { editLabel: "Editar etiqueta", @@ -2350,6 +2465,9 @@ export const es: TranslationResources = { "Consulte los documentos para obtener más detalles y las variables de entorno disponibles para estos comandos.", setup: "Configuración", setupAccessibility: "Comandos de configuración del árbol de trabajo", + uncommittedTitle: "Confirma los cambios de paseo.json", + uncommittedDescription: + "Los árboles de trabajo nuevos usan el script de configuración de la rama base que selecciones.", teardown: "Demoler", teardownAccessibility: "Comandos de desmontaje del árbol de trabajo", }, diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index d8f119a447..c904e8b659 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -55,14 +55,19 @@ export const fr: TranslationResources = { close: "Fermer le menu", }, commandCenter: { - placeholder: "Rechercher des commandes, espaces de travail et agents...", + placeholder: "Rechercher des commandes, fichiers, espaces de travail et agents...", + filePlaceholder: "Rechercher des fichiers...", + searchingFiles: "Recherche de fichiers...", noMatches: "Aucune correspondance", actions: "Actes", + files: "Fichiers", workspaces: "Espaces de travail", agents: "Agents", newAgent: "Nouvel agent", addProject: "Ajouter un projet", home: "Maison", + groupByProject: "Grouper par projet", + groupByStatus: "Grouper par statut", modelGroupLabel: "Modèle", modelSearchKeywords: "changer de modèle modifier le modèle définir le modèle sélectionner le modèle", @@ -273,6 +278,14 @@ export const fr: TranslationResources = { }, }, message: { + diagram: { + diagram: "Diagramme", + zoomIn: "Zoomer", + zoomOut: "Dézoomer", + resetZoom: "Réinitialiser la vue", + viewSource: "Afficher la source", + viewDiagram: "Afficher le diagramme", + }, actions: { copyCode: "Copier le code", copyTurn: "Copier le tour", @@ -324,6 +337,15 @@ export const fr: TranslationResources = { todo: { title: "Tâches", empty: "Aucune tâche pour l'instant.", + tasksProgress: "{{completed}}/{{total}} tâches", + tasksProgressCurrent: "{{completed}}/{{total}} tâches · {{task}}", + activity: { + created: "{{count}} tâches créées", + added: "Ajoutée", + started: "Commencée", + completed: "Terminée", + reopened: "Rouverte", + }, }, compaction: { loading: "Compactage...", @@ -394,9 +416,32 @@ export const fr: TranslationResources = { fileActions: { openFile: "Ouvrir le fichier", copyPath: "Copier le chemin", + copyRelativePath: "Copier le chemin relatif", + revealIn: "Afficher dans {{target}}", download: "Télécharger", addToChat: "Ajouter au chat", moreActions: "Plus de propositions", + newFile: "Nouveau fichier", + newFolder: "Nouveau dossier", + collapseFolder: "Replier le dossier", + rename: "Renommer", + duplicate: "Dupliquer", + revert: "Abandonner les modifications", + delete: "Supprimer", + confirmDelete: { + fileTitle: "Supprimer le fichier ?", + folderTitle: "Supprimer le dossier ?", + message: "« {{name}} » sera définitivement supprimé.", + confirm: "Supprimer", + cancel: "Annuler", + }, + confirmRevert: { + title: "Abandonner les modifications ?", + message: "Les modifications de « {{name}} » seront définitivement abandonnées.", + confirm: "Abandonner", + cancel: "Annuler", + failed: "Échec de l'abandon des modifications", + }, }, fileExplorer: { sort: { @@ -426,6 +471,15 @@ export const fr: TranslationResources = { }, errors: { failedToListDirectory: "Échec de la liste du répertoire", + createFailed: "Échec de la création de l'entrée", + renameFailed: "Échec du renommage de l'entrée", + duplicateFailed: "Échec de la duplication de l'entrée", + revealFailed: "Échec de l'affichage de l'entrée", + deleteFailed: "Échec de la suppression de l'entrée", + }, + draft: { + filePlaceholder: "Nom du fichier", + folderPlaceholder: "Nom du dossier", }, }, setup: { @@ -473,6 +527,8 @@ export const fr: TranslationResources = { screenshotCopied: "Capture d'écran copiée dans le presse-papiers", elementCopied: "Élément copié dans le presse-papiers", screenshotFailed: "Impossible de copier la capture", + selectorLoading: "Attendez la fin du chargement de la page", + selectorFailed: "Impossible de démarrer le sélecteur d’élément", }, annotate: { title: "Annoter l'élément", @@ -565,6 +621,7 @@ export const fr: TranslationResources = { reloadingAgent: "Agent de rechargement...", reloadedAgent: "Agent rechargé", failedToReloadAgent: "Échec du rechargement de l'agent", + failedToCloseAgent: "Échec de la fermeture de l'agent", }, confirmations: { unsavedTitle: "Modifications non enregistrées", @@ -1399,14 +1456,18 @@ export const fr: TranslationResources = { loadingSelector: "Chargement du sélecteur de modèle...", error: "Erreur", defaultModel: "Défaut", - favorites: "Favoris", - favoriteModel: "Modèle préféré", - unfavoriteModel: "Modèle défavorisé", + profiles: "Profils", + providers: "Fournisseurs", + editProfiles: "Modifier", + editProfilesLabel: "Modifier les profils d'agent", + createProfile: "Créer un profil", modelCount: "{{count}} modèle", modelCountPlural: "{{count}} modèles", retry: "Réessayer", retrying: "Nouvelle tentative...", noMatches: "Aucun modèle ne correspond à votre recherche", + noMatchesForQuery: "Aucun modèle ne correspond à « {{query}} »", + searchAllPlaceholder: "Rechercher dans tous les modèles...", searchPlaceholder: "Rechercher des modèles...", openProviderSettings: "Ouvrir les paramètres{{provider}}", }, @@ -1769,12 +1830,27 @@ export const fr: TranslationResources = { projects: "Projets", connections: "Relations", agents: "Agents", + metadata: "Métadonnées", workspaces: "Workspaces", providers: "Fournisseurs", usage: "Utilisation", terminals: "Terminals", host: "Aperçu", }, + metadataGeneration: { + title: "Génération de métadonnées", + description: + "Choisissez le modèle utilisé par Paseo pour les titres d’espaces de travail, les noms de branches, les messages de commit et les brouillons de pull request", + selection: "Sélection du modèle", + automatic: "Automatique", + preferred: "Manuel", + automaticHint: "Paseo choisit un modèle rapide disponible", + preferredHint: "Choisissez le modèle utilisé par Paseo", + model: "Modèle", + fallbackHint: "S’il est indisponible, Paseo utilise un autre modèle disponible", + docs: "Documentation", + saveError: "Impossible de mettre à jour la génération de métadonnées", + }, general: { title: "Général", browserData: { @@ -1966,10 +2042,10 @@ export const fr: TranslationResources = { resetAll: "Tout réinitialiser", }, sections: { - navigation: "Navigation", + general: "Général", + workspaces: "Projets et espaces de travail", tabsPanes: "Onglets et volets", - projects: "Projets", - panels: "Panneaux", + layout: "Disposition", agentInput: "EntréeAgent", }, help: { @@ -1997,6 +2073,7 @@ export const fr: TranslationResources = { moveTabDown: "Déplacer l'onglet vers le bas", closePane: "Fermer le volet", newTerminal: "Nouvelle borne", + searchFiles: "Rechercher des fichiers", toggleCommandCenter: "Basculer le centre de commande", showKeyboardShortcuts: "Afficher les raccourcis clavier", toggleLeftSidebar: "Basculer la barre latérale gauche", @@ -2186,6 +2263,46 @@ export const fr: TranslationResources = { save: "Save", emptyState: "No profiles yet. Add one to launch terminals with a specific command.", }, + agentProfiles: { + sectionTitle: "Profils d'agent", + unavailable: "Connectez-vous à cet hôte pour gérer les profils d'agent", + unsupported: + "Cet hôte exécute un daemon qui ne prend pas encore en charge les profils d'agent", + emptyState: + "Aucun profil pour le moment. Ajoutez-en un pour démarrer des agents à partir d'un fournisseur et d'un modèle enregistrés.", + addProfileTitle: "Ajouter un profil d'agent", + newProfile: "Nouveau profil", + editProfile: "Modifier le profil", + editProfileTitle: "Modifier le profil d'agent", + nameLabel: "Nom", + namePlaceholder: "Travail sur l'UI", + iconLabel: "Icône", + noIcon: "Aucune", + providerLabel: "Fournisseur", + providerPlaceholder: "Sélectionner un fournisseur", + noProviders: "Aucun fournisseur n'est disponible sur cet hôte", + modelLabel: "Modèle", + noModels: "Ce fournisseur n'a aucun modèle à choisir", + modeLabel: "Mode", + noModes: "Ce fournisseur n'a aucun mode à choisir", + thinkingLabel: "Pensée", + noThinkingOptions: "Ce modèle n'a aucun niveau de pensée", + featuresLabel: "Caractéristiques", + featureCount: "{{count}} caractéristiques", + featureCountOne: "{{count}} caractéristique", + notesLabel: "Notes pour les agents", + notesPlaceholder: + "À utiliser pour le travail sur l'UI — composants, mise en page et tokens de design.", + notesHint: + "Renvoyé par l'outil MCP « list_profiles ». Rédigez-le comme une instruction destinée à un autre agent.", + save: "Enregistrer", + saving: "Enregistrement...", + remove: "Supprimer", + removeConfirmTitle: "Supprimer le profil ?", + removeConfirmMessage: "Supprimer « {{name}} » ?", + moveUp: "Déplacer vers le haut", + moveDown: "Déplacer vers le bas", + }, daemon: { rename: { editLabel: "Modifier l'étiquette", @@ -2354,6 +2471,9 @@ export const fr: TranslationResources = { "Voir la documentation pour plus de détails et les variables d'environnement disponibles pour ces commandes", setup: "Installation", setupAccessibility: "Commandes de configuration de Worktree", + uncommittedTitle: "Validez les modifications de paseo.json", + uncommittedDescription: + "Les nouveaux worktrees utilisent le script de configuration de la branche de base sélectionnée.", teardown: "Démolir", teardownAccessibility: "Commandes de démontage de Worktree", }, diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index dba15fa621..e49caf5219 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -54,14 +54,19 @@ export const ja: TranslationResources = { close: "メニューを閉じる", }, commandCenter: { - placeholder: "コマンド、ワークスペース、エージェントを検索...", + placeholder: "コマンド、ファイル、ワークスペース、エージェントを検索...", + filePlaceholder: "ファイルを検索...", + searchingFiles: "ファイルを検索中...", noMatches: "一致なし", actions: "アクション", + files: "ファイル", workspaces: "ワークスペース", agents: "エージェント", newAgent: "新しいエージェント", addProject: "プロジェクトを追加", home: "ホーム", + groupByProject: "プロジェクトでグループ化", + groupByStatus: "ステータスでグループ化", modelGroupLabel: "モデル", modelSearchKeywords: "モデルを切り替え モデルを変更 モデルを設定 モデルを選択", thinkingGroupLabel: "思考", @@ -272,6 +277,14 @@ export const ja: TranslationResources = { }, }, message: { + diagram: { + diagram: "図", + zoomIn: "拡大", + zoomOut: "縮小", + resetZoom: "表示をリセット", + viewSource: "ソースを表示", + viewDiagram: "図を表示", + }, actions: { copyCode: "コードをコピー", copyTurn: "ターンをコピー", @@ -323,6 +336,15 @@ export const ja: TranslationResources = { todo: { title: "タスク", empty: "タスクがまだありません。", + tasksProgress: "{{completed}}/{{total}}タスク", + tasksProgressCurrent: "{{completed}}/{{total}}タスク · {{task}}", + activity: { + created: "{{count}}件のタスクを作成", + added: "追加", + started: "開始", + completed: "完了", + reopened: "再開", + }, }, compaction: { loading: "コンテキストを圧縮中...", @@ -394,9 +416,32 @@ export const ja: TranslationResources = { fileActions: { openFile: "ファイルを開く", copyPath: "パスをコピー", + copyRelativePath: "相対パスをコピー", + revealIn: "{{target}}で表示", download: "ダウンロード", addToChat: "チャットに追加", moreActions: "その他のアクション", + newFile: "新規ファイル", + newFolder: "新規フォルダ", + collapseFolder: "フォルダを折りたたむ", + rename: "名前を変更", + duplicate: "複製", + revert: "変更を破棄", + delete: "削除", + confirmDelete: { + fileTitle: "ファイルを削除しますか?", + folderTitle: "フォルダを削除しますか?", + message: "「{{name}}」は完全に削除されます。", + confirm: "削除", + cancel: "キャンセル", + }, + confirmRevert: { + title: "変更を破棄しますか?", + message: "「{{name}}」への変更は完全に破棄されます。", + confirm: "破棄", + cancel: "キャンセル", + failed: "変更の破棄に失敗しました", + }, }, fileExplorer: { sort: { @@ -426,6 +471,15 @@ export const ja: TranslationResources = { }, errors: { failedToListDirectory: "ディレクトリの一覧取得に失敗しました", + createFailed: "エントリの作成に失敗しました", + renameFailed: "エントリの名前変更に失敗しました", + duplicateFailed: "エントリの複製に失敗しました", + revealFailed: "エントリの表示に失敗しました", + deleteFailed: "エントリの削除に失敗しました", + }, + draft: { + filePlaceholder: "ファイル名", + folderPlaceholder: "フォルダ名", }, }, setup: { @@ -473,6 +527,8 @@ export const ja: TranslationResources = { screenshotCopied: "スクリーンショットをクリップボードにコピーしました", elementCopied: "要素をクリップボードにコピーしました", screenshotFailed: "スクリーンショットをコピーできませんでした", + selectorLoading: "ページの読み込みが完了するまでお待ちください", + selectorFailed: "要素セレクターを開始できませんでした", }, annotate: { title: "要素に注釈を付ける", @@ -565,6 +621,7 @@ export const ja: TranslationResources = { reloadingAgent: "エージェントを再読み込み中...", reloadedAgent: "エージェントを再読み込みしました", failedToReloadAgent: "エージェントの再読み込みに失敗しました", + failedToCloseAgent: "エージェントを閉じられませんでした", }, confirmations: { unsavedTitle: "未保存の変更", @@ -1368,14 +1425,18 @@ export const ja: TranslationResources = { loadingSelector: "モデルセレクターを読み込み中...", error: "エラー", defaultModel: "デフォルト", - favorites: "お気に入り", - favoriteModel: "お気に入りモデル", - unfavoriteModel: "お気に入りを解除", + profiles: "プロファイル", + providers: "プロバイダー", + editProfiles: "編集", + editProfilesLabel: "エージェントプロファイルを編集", + createProfile: "プロファイルを作成", modelCount: "{{count}}つのモデル", modelCountPlural: "{{count}}つのモデル", retry: "再試行", retrying: "再試行中...", noMatches: "検索に一致するモデルがありません", + noMatchesForQuery: "「{{query}}」に一致するモデルがありません", + searchAllPlaceholder: "すべてのモデルを検索...", searchPlaceholder: "モデルを検索...", openProviderSettings: "{{provider}}の設定を開く", }, @@ -1738,12 +1799,27 @@ export const ja: TranslationResources = { projects: "プロジェクト", connections: "接続", agents: "エージェント", + metadata: "メタデータ", workspaces: "ワークスペース", providers: "プロバイダー", usage: "使用状況", terminals: "ターミナル", host: "概要", }, + metadataGeneration: { + title: "メタデータ生成", + description: + "ワークスペースのタイトル、ブランチ名、コミットメッセージ、プルリクエストの下書きに使用するモデルを選択します", + selection: "モデル選択", + automatic: "自動", + preferred: "手動", + automaticHint: "Paseo が利用可能な高速モデルを選択します", + preferredHint: "Paseo が使用するモデルを選択します", + model: "モデル", + fallbackHint: "利用できない場合、Paseo は別の利用可能なモデルを使用します", + docs: "ドキュメント", + saveError: "メタデータ生成を更新できません", + }, general: { title: "一般", browserData: { @@ -1931,10 +2007,10 @@ export const ja: TranslationResources = { resetAll: "すべてリセット", }, sections: { - navigation: "ナビゲーション", + general: "一般", + workspaces: "プロジェクト&ワークスペース", tabsPanes: "タブ&ペイン", - projects: "プロジェクト", - panels: "パネル", + layout: "レイアウト", agentInput: "エージェント入力", }, help: { @@ -1962,6 +2038,7 @@ export const ja: TranslationResources = { moveTabDown: "タブを下に移動", closePane: "ペインを閉じる", newTerminal: "新しいターミナル", + searchFiles: "ファイルを検索", toggleCommandCenter: "コマンドセンターを切り替え", showKeyboardShortcuts: "キーボードショートカットを表示", toggleLeftSidebar: "左サイドバーを切り替え", @@ -2152,6 +2229,44 @@ export const ja: TranslationResources = { emptyState: "プロファイルがまだありません。特定のコマンドでターミナルを起動するために追加してください。", }, + agentProfiles: { + sectionTitle: "エージェントプロファイル", + unavailable: "エージェントプロファイルを管理するにはこのホストに接続してください", + unsupported: "このホストで動作するデーモンはまだエージェントプロファイルに対応していません", + emptyState: + "プロファイルがまだありません。保存したプロバイダーとモデルでエージェントを起動するために追加してください。", + addProfileTitle: "エージェントプロファイルを追加", + newProfile: "新しいプロファイル", + editProfile: "プロファイルを編集", + editProfileTitle: "エージェントプロファイルを編集", + nameLabel: "名前", + namePlaceholder: "UI作業", + iconLabel: "アイコン", + noIcon: "なし", + providerLabel: "プロバイダー", + providerPlaceholder: "プロバイダーを選択", + noProviders: "このホストで利用できるプロバイダーがありません", + modelLabel: "モデル", + noModels: "このプロバイダーには選択できるモデルがありません", + modeLabel: "モード", + noModes: "このプロバイダーには選択できるモードがありません", + thinkingLabel: "思考", + noThinkingOptions: "このモデルには思考レベルがありません", + featuresLabel: "機能", + featureCount: "{{count}}個の機能", + featureCountOne: "{{count}}個の機能", + notesLabel: "エージェントへのメモ", + notesPlaceholder: "UI作業に使用 — コンポーネント、レイアウト、デザイントークンなど。", + notesHint: + "list_profiles MCPツールによって返されます。他のエージェントへの指示として記述してください。", + save: "保存", + saving: "保存中...", + remove: "削除", + removeConfirmTitle: "プロファイルを削除しますか?", + removeConfirmMessage: '"{{name}}"を削除しますか?', + moveUp: "上に移動", + moveDown: "下に移動", + }, daemon: { rename: { editLabel: "ラベルを編集", @@ -2320,6 +2435,9 @@ export const ja: TranslationResources = { "これらのコマンドで使用可能な詳細と環境変数についてはドキュメントを参照してください", setup: "セットアップ", setupAccessibility: "ワークツリーセットアップコマンド", + uncommittedTitle: "paseo.json の変更をコミットしてください", + uncommittedDescription: + "新しいワークツリーでは、選択したベースブランチのセットアップスクリプトが使われます。", teardown: "削除時", teardownAccessibility: "ワークツリー削除時のコマンド", }, diff --git a/packages/app/src/i18n/resources/ko.ts b/packages/app/src/i18n/resources/ko.ts index e57b0f2547..2b35a0ed1c 100644 --- a/packages/app/src/i18n/resources/ko.ts +++ b/packages/app/src/i18n/resources/ko.ts @@ -54,14 +54,19 @@ export const ko: TranslationResources = { close: "메뉴 닫기", }, commandCenter: { - placeholder: "명령, 워크스페이스 및 에이전트 검색...", + placeholder: "명령, 파일, 워크스페이스 및 에이전트 검색...", + filePlaceholder: "파일 검색...", + searchingFiles: "파일 검색 중...", noMatches: "일치 항목 없음", actions: "작업", + files: "파일", workspaces: "워크스페이스", agents: "에이전트", newAgent: "새 에이전트", addProject: "프로젝트 추가", home: "홈", + groupByProject: "프로젝트별 그룹화", + groupByStatus: "상태별 그룹화", modelGroupLabel: "모델", modelSearchKeywords: "모델 전환 모델 변경 모델 설정 모델 선택", thinkingGroupLabel: "추론", @@ -270,6 +275,14 @@ export const ko: TranslationResources = { }, }, message: { + diagram: { + diagram: "다이어그램", + zoomIn: "확대", + zoomOut: "축소", + resetZoom: "보기 초기화", + viewSource: "소스 보기", + viewDiagram: "다이어그램 보기", + }, actions: { copyCode: "코드 복사", copyTurn: "턴 복사", @@ -321,6 +334,15 @@ export const ko: TranslationResources = { todo: { title: "작업", empty: "아직 작업이 없습니다.", + tasksProgress: "작업 {{completed}}/{{total}}개", + tasksProgressCurrent: "작업 {{completed}}/{{total}}개 · {{task}}", + activity: { + created: "작업 {{count}}개 생성", + added: "추가됨", + started: "시작됨", + completed: "완료됨", + reopened: "다시 열림", + }, }, compaction: { loading: "압축하는 중...", @@ -391,9 +413,32 @@ export const ko: TranslationResources = { fileActions: { openFile: "파일 열기", copyPath: "경로 복사", + copyRelativePath: "상대 경로 복사", + revealIn: "{{target}}에서 보기", download: "다운로드", addToChat: "채팅에 추가", moreActions: "추가 작업", + newFile: "새 파일", + newFolder: "새 폴더", + collapseFolder: "폴더 접기", + rename: "이름 바꾸기", + duplicate: "복제", + revert: "변경 사항 버리기", + delete: "삭제", + confirmDelete: { + fileTitle: "파일을 삭제할까요?", + folderTitle: "폴더를 삭제할까요?", + message: '"{{name}}"이(가) 영구적으로 삭제됩니다.', + confirm: "삭제", + cancel: "취소", + }, + confirmRevert: { + title: "변경 사항을 버릴까요?", + message: '"{{name}}"의 변경 사항이 영구적으로 삭제됩니다.', + confirm: "버리기", + cancel: "취소", + failed: "변경 사항을 버리지 못했습니다", + }, }, fileExplorer: { sort: { @@ -423,6 +468,15 @@ export const ko: TranslationResources = { }, errors: { failedToListDirectory: "디렉터리 목록을 불러오지 못했습니다", + createFailed: "항목을 만들지 못했습니다", + renameFailed: "항목 이름을 바꾸지 못했습니다", + duplicateFailed: "항목을 복제하지 못했습니다", + revealFailed: "항목을 표시하지 못했습니다", + deleteFailed: "항목을 삭제하지 못했습니다", + }, + draft: { + filePlaceholder: "파일 이름", + folderPlaceholder: "폴더 이름", }, }, setup: { @@ -470,6 +524,8 @@ export const ko: TranslationResources = { screenshotCopied: "스크린샷을 클립보드에 복사했습니다.", elementCopied: "요소를 클립보드에 복사했습니다.", screenshotFailed: "스크린샷을 복사할 수 없습니다.", + selectorLoading: "페이지 로딩이 끝날 때까지 기다려 주세요.", + selectorFailed: "요소 선택기를 시작할 수 없습니다.", }, annotate: { title: "요소에 주석 달기", @@ -562,6 +618,7 @@ export const ko: TranslationResources = { reloadingAgent: "에이전트 다시 로드 중...", reloadedAgent: "에이전트를 다시 로드했습니다", failedToReloadAgent: "에이전트를 다시 로드하지 못했습니다", + failedToCloseAgent: "에이전트를 닫지 못했습니다", }, confirmations: { close: "닫기", @@ -1364,14 +1421,18 @@ export const ko: TranslationResources = { loadingSelector: "모델 선택기 불러오는 중...", error: "오류", defaultModel: "기본값", - favorites: "즐겨찾기", - favoriteModel: "모델 즐겨찾기", - unfavoriteModel: "모델 즐겨찾기 해제", + profiles: "프로필", + providers: "제공자", + editProfiles: "편집", + editProfilesLabel: "에이전트 프로필 편집", + createProfile: "프로필 만들기", modelCount: "모델 {{count}}개", modelCountPlural: "모델 {{count}}개", retry: "다시 시도", retrying: "다시 시도 중...", noMatches: "검색과 일치하는 모델이 없습니다", + noMatchesForQuery: '"{{query}}"과(와) 일치하는 모델이 없습니다', + searchAllPlaceholder: "모든 모델 검색...", searchPlaceholder: "모델 검색...", openProviderSettings: "{{provider}} 설정 열기", }, @@ -1732,12 +1793,27 @@ export const ko: TranslationResources = { projects: "프로젝트", connections: "연결", agents: "에이전트", + metadata: "메타데이터", workspaces: "워크스페이스", providers: "프로바이더", usage: "사용량", terminals: "터미널", host: "개요", }, + metadataGeneration: { + title: "메타데이터 생성", + description: + "워크스페이스 제목, 브랜치 이름, 커밋 메시지 및 풀 리퀘스트 초안에 사용할 모델을 선택하세요", + selection: "모델 선택", + automatic: "자동", + preferred: "수동", + automaticHint: "Paseo가 사용 가능한 빠른 모델을 선택합니다", + preferredHint: "Paseo에서 사용할 모델을 선택하세요", + model: "모델", + fallbackHint: "사용할 수 없으면 Paseo가 다른 사용 가능한 모델을 사용합니다", + docs: "문서", + saveError: "메타데이터 생성을 업데이트할 수 없습니다", + }, general: { title: "일반", browserData: { @@ -1926,10 +2002,10 @@ export const ko: TranslationResources = { resetAll: "모두 재설정", }, sections: { - navigation: "탐색", + general: "일반", + workspaces: "프로젝트 및 워크스페이스", tabsPanes: "탭 및 창", - projects: "프로젝트", - panels: "패널", + layout: "레이아웃", agentInput: "에이전트 입력", }, help: { @@ -1957,6 +2033,7 @@ export const ko: TranslationResources = { moveTabDown: "탭을 아래로 이동", closePane: "창 닫기", newTerminal: "새 터미널", + searchFiles: "파일 검색", toggleCommandCenter: "명령 센터 토글", showKeyboardShortcuts: "키보드 단축키 표시", toggleLeftSidebar: "왼쪽 사이드바 토글", @@ -2144,6 +2221,44 @@ export const ko: TranslationResources = { save: "저장", emptyState: "아직 프로필이 없습니다. 특정 명령으로 터미널을 실행하려면 하나 추가하세요.", }, + agentProfiles: { + sectionTitle: "에이전트 프로필", + unavailable: "에이전트 프로필을 관리하려면 이 호스트에 연결하세요", + unsupported: "이 호스트에서 실행 중인 데몬은 아직 에이전트 프로필을 지원하지 않습니다", + emptyState: + "아직 프로필이 없습니다. 저장된 프로바이더와 모델로 에이전트를 시작하려면 하나 추가하세요.", + addProfileTitle: "에이전트 프로필 추가", + newProfile: "새 프로필", + editProfile: "프로필 편집", + editProfileTitle: "에이전트 프로필 편집", + nameLabel: "이름", + namePlaceholder: "UI 작업", + iconLabel: "아이콘", + noIcon: "없음", + providerLabel: "프로바이더", + providerPlaceholder: "프로바이더 선택", + noProviders: "이 호스트에서 사용 가능한 프로바이더가 없습니다", + modelLabel: "모델", + noModels: "이 프로바이더에는 선택할 수 있는 모델이 없습니다", + modeLabel: "모드", + noModes: "이 프로바이더에는 선택할 수 있는 모드가 없습니다", + thinkingLabel: "사고", + noThinkingOptions: "이 모델에는 사고 수준이 없습니다", + featuresLabel: "기능", + featureCount: "기능 {{count}}개", + featureCountOne: "기능 {{count}}개", + notesLabel: "에이전트를 위한 메모", + notesPlaceholder: "UI 작업에 사용 — 컴포넌트, 레이아웃, 디자인 토큰.", + notesHint: + "list_profiles MCP 도구가 반환합니다. 다른 에이전트에게 전달할 지침으로 작성하세요.", + save: "저장", + saving: "저장하는 중...", + remove: "제거", + removeConfirmTitle: "프로필을 제거할까요?", + removeConfirmMessage: '"{{name}}"을(를) 제거할까요?', + moveUp: "위로 이동", + moveDown: "아래로 이동", + }, daemon: { rename: { editLabel: "레이블 편집", @@ -2312,6 +2427,8 @@ export const ko: TranslationResources = { docsTooltip: "자세한 내용과 이 명령에 사용할 수 있는 환경 변수는 문서를 참조하세요", setup: "설정", setupAccessibility: "워크트리 설정 명령", + uncommittedTitle: "paseo.json 변경 사항을 커밋하세요", + uncommittedDescription: "새 워크트리는 선택한 기본 브랜치의 설정 스크립트를 사용합니다.", teardown: "정리", teardownAccessibility: "워크트리 정리 명령", }, diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index 70e61dabd8..711f417b8f 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -54,14 +54,19 @@ export const ptBR: TranslationResources = { close: "Fechar menu", }, commandCenter: { - placeholder: "Buscar comandos, espaços de trabalho e agentes...", + placeholder: "Buscar comandos, arquivos, espaços de trabalho e agentes...", + filePlaceholder: "Pesquisar arquivos...", + searchingFiles: "Pesquisando arquivos...", noMatches: "Nenhuma correspondência", actions: "Ações", + files: "Arquivos", workspaces: "Espaços de trabalho", agents: "Agentes", newAgent: "Novo agente", addProject: "Adicionar projeto", home: "Início", + groupByProject: "Agrupar por projeto", + groupByStatus: "Agrupar por status", modelGroupLabel: "Modelo", modelSearchKeywords: "trocar modelo mudar modelo definir modelo selecionar modelo", thinkingGroupLabel: "Raciocínio", @@ -272,6 +277,14 @@ export const ptBR: TranslationResources = { }, }, message: { + diagram: { + diagram: "Diagrama", + zoomIn: "Aproximar", + zoomOut: "Afastar", + resetZoom: "Redefinir visualização", + viewSource: "Ver fonte", + viewDiagram: "Ver diagrama", + }, actions: { copyCode: "Copiar código", copyTurn: "Copiar turno", @@ -323,6 +336,15 @@ export const ptBR: TranslationResources = { todo: { title: "Tarefas", empty: "Nenhuma tarefa ainda.", + tasksProgress: "{{completed}}/{{total}} tarefas", + tasksProgressCurrent: "{{completed}}/{{total}} tarefas · {{task}}", + activity: { + created: "{{count}} tarefas criadas", + added: "Adicionada", + started: "Iniciada", + completed: "Concluída", + reopened: "Reaberta", + }, }, compaction: { loading: "Compactando...", @@ -394,9 +416,32 @@ export const ptBR: TranslationResources = { fileActions: { openFile: "Abrir arquivo", copyPath: "Copiar caminho", + copyRelativePath: "Copiar caminho relativo", + revealIn: "Mostrar no {{target}}", download: "Baixar", addToChat: "Adicionar ao chat", moreActions: "Mais ações", + newFile: "Novo arquivo", + newFolder: "Nova pasta", + collapseFolder: "Recolher pasta", + rename: "Renomear", + duplicate: "Duplicar", + revert: "Descartar alterações", + delete: "Excluir", + confirmDelete: { + fileTitle: "Excluir arquivo?", + folderTitle: "Excluir pasta?", + message: '"{{name}}" será excluído permanentemente.', + confirm: "Excluir", + cancel: "Cancelar", + }, + confirmRevert: { + title: "Descartar alterações?", + message: 'As alterações em "{{name}}" serão descartadas permanentemente.', + confirm: "Descartar", + cancel: "Cancelar", + failed: "Falha ao descartar alterações", + }, }, fileExplorer: { sort: { @@ -426,6 +471,15 @@ export const ptBR: TranslationResources = { }, errors: { failedToListDirectory: "Falha ao listar diretório", + createFailed: "Falha ao criar entrada", + renameFailed: "Falha ao renomear entrada", + duplicateFailed: "Falha ao duplicar entrada", + revealFailed: "Falha ao mostrar entrada", + deleteFailed: "Falha ao excluir entrada", + }, + draft: { + filePlaceholder: "Nome do arquivo", + folderPlaceholder: "Nome da pasta", }, }, setup: { @@ -473,6 +527,8 @@ export const ptBR: TranslationResources = { screenshotCopied: "Captura copiada para a área de transferência", elementCopied: "Elemento copiado para a área de transferência", screenshotFailed: "Não foi possível copiar a captura", + selectorLoading: "Aguarde o carregamento da página terminar", + selectorFailed: "Não foi possível iniciar o seletor de elemento", }, annotate: { title: "Anotar elemento", @@ -564,6 +620,7 @@ export const ptBR: TranslationResources = { reloadingAgent: "Recarregando agente...", reloadedAgent: "Agente recarregado", failedToReloadAgent: "Falha ao recarregar agente", + failedToCloseAgent: "Falha ao fechar agente", }, confirmations: { unsavedTitle: "Alterações não salvas", @@ -1381,14 +1438,18 @@ export const ptBR: TranslationResources = { loadingSelector: "Carregando seletor de modelos...", error: "Erro", defaultModel: "Padrão", - favorites: "Favoritos", - favoriteModel: "Favoritar modelo", - unfavoriteModel: "Remover modelo dos favoritos", + profiles: "Perfis", + providers: "Provedores", + editProfiles: "Editar", + editProfilesLabel: "Editar perfis de agente", + createProfile: "Criar perfil", modelCount: "{{count}} modelo", modelCountPlural: "{{count}} modelos", retry: "Tentar novamente", retrying: "Tentando novamente...", noMatches: "Nenhum modelo corresponde à sua busca", + noMatchesForQuery: "Nenhum modelo corresponde a «{{query}}»", + searchAllPlaceholder: "Buscar em todos os modelos...", searchPlaceholder: "Buscar modelos...", openProviderSettings: "Abrir configurações de {{provider}}", }, @@ -1751,12 +1812,27 @@ export const ptBR: TranslationResources = { projects: "Projetos", connections: "Conexões", agents: "Agentes", + metadata: "Metadados", workspaces: "Workspaces", providers: "Provedores", usage: "Uso", terminals: "Terminais", host: "Visão geral", }, + metadataGeneration: { + title: "Geração de metadados", + description: + "Escolha o modelo usado pelo Paseo para títulos de espaços de trabalho, nomes de branches, mensagens de commit e rascunhos de pull request", + selection: "Seleção de modelo", + automatic: "Automática", + preferred: "Manual", + automaticHint: "O Paseo escolhe um modelo rápido disponível", + preferredHint: "Escolha o modelo que o Paseo usa", + model: "Modelo", + fallbackHint: "Se ele não estiver disponível, o Paseo usa outro modelo disponível", + docs: "Documentação", + saveError: "Não foi possível atualizar a geração de metadados", + }, general: { title: "Geral", browserData: { @@ -1946,10 +2022,10 @@ export const ptBR: TranslationResources = { resetAll: "Redefinir tudo", }, sections: { - navigation: "Navegação", + general: "Geral", + workspaces: "Projetos e workspaces", tabsPanes: "Abas e painéis", - projects: "Projetos", - panels: "Painéis", + layout: "Layout", agentInput: "Entrada do agente", }, help: { @@ -1977,6 +2053,7 @@ export const ptBR: TranslationResources = { moveTabDown: "Mover aba para baixo", closePane: "Fechar painel", newTerminal: "Novo terminal", + searchFiles: "Pesquisar arquivos", toggleCommandCenter: "Alternar central de comandos", showKeyboardShortcuts: "Mostrar atalhos de teclado", toggleLeftSidebar: "Alternar barra lateral esquerda", @@ -2167,6 +2244,44 @@ export const ptBR: TranslationResources = { emptyState: "Nenhum perfil ainda. Adicione um para abrir terminais com um comando específico.", }, + agentProfiles: { + sectionTitle: "Perfis de agente", + unavailable: "Conecte-se a este host para gerenciar perfis de agente", + unsupported: "Este host executa um daemon que ainda não é compatível com perfis de agente", + emptyState: + "Nenhum perfil ainda. Adicione um para iniciar agentes a partir de um provedor e modelo salvos.", + addProfileTitle: "Adicionar perfil de agente", + newProfile: "Novo perfil", + editProfile: "Editar perfil", + editProfileTitle: "Editar perfil de agente", + nameLabel: "Nome", + namePlaceholder: "Trabalho de UI", + iconLabel: "Ícone", + noIcon: "Nenhum", + providerLabel: "Provedor", + providerPlaceholder: "Selecione um provedor", + noProviders: "Nenhum provedor está disponível neste host", + modelLabel: "Modelo", + noModels: "Este provedor não tem modelos para escolher", + modeLabel: "Modo", + noModes: "Este provedor não tem modos para escolher", + thinkingLabel: "Raciocínio", + noThinkingOptions: "Este modelo não tem níveis de raciocínio", + featuresLabel: "Recursos", + featureCount: "{{count}} recursos", + featureCountOne: "{{count}} recurso", + notesLabel: "Notas para os agentes", + notesPlaceholder: "Use para trabalho de UI — componentes, layout e tokens de design.", + notesHint: + 'Retornado pela ferramenta MCP "list_profiles". Escreva como uma instrução para outro agente.', + save: "Salvar", + saving: "Salvando...", + remove: "Remover", + removeConfirmTitle: "Remover perfil?", + removeConfirmMessage: 'Remover "{{name}}"?', + moveUp: "Mover para cima", + moveDown: "Mover para baixo", + }, daemon: { rename: { editLabel: "Editar rótulo", @@ -2335,6 +2450,9 @@ export const ptBR: TranslationResources = { "Veja a documentação para mais detalhes e as variáveis de ambiente disponíveis para estes comandos", setup: "Configuração", setupAccessibility: "Comandos de configuração do worktree", + uncommittedTitle: "Faça commit das alterações no paseo.json", + uncommittedDescription: + "Novos worktrees usam o script de configuração do branch base selecionado.", teardown: "Desmontagem", teardownAccessibility: "Comandos de desmontagem do worktree", }, diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 0846281578..771ef1c5ae 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -54,14 +54,19 @@ export const ru: TranslationResources = { close: "Закрыть меню", }, commandCenter: { - placeholder: "Поиск команд, рабочих пространств и агентов...", + placeholder: "Поиск команд, файлов, рабочих пространств и агентов...", + filePlaceholder: "Поиск файлов...", + searchingFiles: "Поиск файлов...", noMatches: "Нет совпадений", actions: "Действия", + files: "Файлы", workspaces: "Рабочие пространства", agents: "Агенты", newAgent: "Новый агент", addProject: "Добавить проект", home: "Дом", + groupByProject: "Группировать по проекту", + groupByStatus: "Группировать по статусу", modelGroupLabel: "Модель", modelSearchKeywords: "сменить модель изменить модель выбрать модель установить модель", thinkingGroupLabel: "Мышление", @@ -271,6 +276,14 @@ export const ru: TranslationResources = { }, }, message: { + diagram: { + diagram: "Диаграмма", + zoomIn: "Приблизить", + zoomOut: "Отдалить", + resetZoom: "Сбросить вид", + viewSource: "Показать код", + viewDiagram: "Показать диаграмму", + }, actions: { copyCode: "Скопировать код", copyTurn: "Копировать ход", @@ -322,6 +335,15 @@ export const ru: TranslationResources = { todo: { title: "Задачи", empty: "Заданий пока нет.", + tasksProgress: "{{completed}}/{{total}} задач", + tasksProgressCurrent: "{{completed}}/{{total}} задач · {{task}}", + activity: { + created: "Создано задач: {{count}}", + added: "Добавлена", + started: "Начата", + completed: "Завершена", + reopened: "Возобновлена", + }, }, compaction: { loading: "Уплотнение...", @@ -393,9 +415,32 @@ export const ru: TranslationResources = { fileActions: { openFile: "Открыть файл", copyPath: "Копировать путь", + copyRelativePath: "Копировать относительный путь", + revealIn: "Показать в {{target}}", download: "Скачать", addToChat: "Добавить в чат", moreActions: "Дополнительные действия", + newFile: "Новый файл", + newFolder: "Новая папка", + collapseFolder: "Свернуть папку", + rename: "Переименовать", + duplicate: "Дублировать", + revert: "Отменить изменения", + delete: "Удалить", + confirmDelete: { + fileTitle: "Удалить файл?", + folderTitle: "Удалить папку?", + message: "«{{name}}» будет удалён безвозвратно.", + confirm: "Удалить", + cancel: "Отмена", + }, + confirmRevert: { + title: "Отменить изменения?", + message: "Изменения в «{{name}}» будут безвозвратно отменены.", + confirm: "Отменить", + cancel: "Отмена", + failed: "Не удалось отменить изменения", + }, }, fileExplorer: { sort: { @@ -425,6 +470,15 @@ export const ru: TranslationResources = { }, errors: { failedToListDirectory: "Не удалось указать каталог", + createFailed: "Не удалось создать элемент", + renameFailed: "Не удалось переименовать элемент", + duplicateFailed: "Не удалось дублировать элемент", + revealFailed: "Не удалось показать элемент", + deleteFailed: "Не удалось удалить элемент", + }, + draft: { + filePlaceholder: "Имя файла", + folderPlaceholder: "Имя папки", }, }, setup: { @@ -473,6 +527,8 @@ export const ru: TranslationResources = { screenshotCopied: "Снимок скопирован в буфер обмена", elementCopied: "Элемент скопирован в буфер обмена", screenshotFailed: "Не удалось скопировать снимок", + selectorLoading: "Дождитесь окончания загрузки страницы", + selectorFailed: "Не удалось запустить выбор элемента", }, annotate: { title: "Аннотировать элемент", @@ -564,6 +620,7 @@ export const ru: TranslationResources = { reloadingAgent: "Перезагрузка агента...", reloadedAgent: "Перезагруженный агент", failedToReloadAgent: "Не удалось перезагрузить агент", + failedToCloseAgent: "Не удалось закрыть агент", }, confirmations: { unsavedTitle: "Несохранённые изменения", @@ -1386,14 +1443,18 @@ export const ru: TranslationResources = { loadingSelector: "Загрузка выбора модели...", error: "Ошибка", defaultModel: "По умолчанию", - favorites: "Избранное", - favoriteModel: "Любимая модель", - unfavoriteModel: "Нелюбимая модель", + profiles: "Профили", + providers: "Провайдеры", + editProfiles: "Изменить", + editProfilesLabel: "Изменить профили агентов", + createProfile: "Создать профиль", modelCount: "{{count}} модель", modelCountPlural: "{{count}} моделей", retry: "Повторить попытку", retrying: "Повторная попытка...", noMatches: "Ни одна модель не соответствует вашему запросу", + noMatchesForQuery: "Нет моделей, соответствующих «{{query}}»", + searchAllPlaceholder: "Поиск по всем моделям...", searchPlaceholder: "Поиск моделей...", openProviderSettings: "Открыть настройки{{provider}}", }, @@ -1756,12 +1817,27 @@ export const ru: TranslationResources = { projects: "Проекты", connections: "Соединения", agents: "Agents", + metadata: "Метаданные", workspaces: "Workspaces", providers: "Провайдеры", usage: "Использование", terminals: "Terminals", host: "Обзор", }, + metadataGeneration: { + title: "Создание метаданных", + description: + "Выберите модель, которую Paseo использует для названий рабочих пространств, веток, сообщений коммитов и черновиков pull request", + selection: "Выбор модели", + automatic: "Автоматически", + preferred: "Вручную", + automaticHint: "Paseo выбирает быструю доступную модель", + preferredHint: "Выберите модель, которую использует Paseo", + model: "Модель", + fallbackHint: "Если она недоступна, Paseo использует другую доступную модель", + docs: "Документация", + saveError: "Не удалось обновить создание метаданных", + }, general: { title: "Общий", browserData: { @@ -1953,10 +2029,10 @@ export const ru: TranslationResources = { resetAll: "Сбросить все", }, sections: { - navigation: "Навигация", + general: "Общие", + workspaces: "Проекты и рабочие пространства", tabsPanes: "Вкладки и панели", - projects: "Проекты", - panels: "Панели", + layout: "Макет", agentInput: "Вход Agent", }, help: { @@ -1984,6 +2060,7 @@ export const ru: TranslationResources = { moveTabDown: "Переместить вкладку вниз", closePane: "Закрыть панель", newTerminal: "Новый терминал", + searchFiles: "Поиск файлов", toggleCommandCenter: "Переключить командный центр", showKeyboardShortcuts: "Показать сочетания клавиш", toggleLeftSidebar: "Переключить левую боковую панель", @@ -2171,6 +2248,44 @@ export const ru: TranslationResources = { save: "Save", emptyState: "No profiles yet. Add one to launch terminals with a specific command.", }, + agentProfiles: { + sectionTitle: "Профили агента", + unavailable: "Подключитесь к этому хосту, чтобы управлять профилями агента", + unsupported: "На этом хосте работает daemon, который пока не поддерживает профили агента", + emptyState: + "Пока нет профилей. Добавьте один, чтобы запускать агентов с сохранённым провайдером и моделью.", + addProfileTitle: "Добавить профиль агента", + newProfile: "Новый профиль", + editProfile: "Изменить профиль", + editProfileTitle: "Изменить профиль агента", + nameLabel: "Имя", + namePlaceholder: "Работа над UI", + iconLabel: "Значок", + noIcon: "Нет", + providerLabel: "Провайдер", + providerPlaceholder: "Выберите провайдера", + noProviders: "На этом хосте нет доступных провайдеров", + modelLabel: "Модель", + noModels: "У этого провайдера нет моделей для выбора", + modeLabel: "Режим", + noModes: "У этого провайдера нет режимов для выбора", + thinkingLabel: "Мышление", + noThinkingOptions: "У этой модели нет уровней мышления", + featuresLabel: "Функции", + featureCount: "{{count}} функций", + featureCountOne: "{{count}} функция", + notesLabel: "Заметки для агентов", + notesPlaceholder: "Используйте для работы над UI — компоненты, макет и токены дизайна.", + notesHint: + "Возвращается инструментом MCP «list_profiles». Напишите это как инструкцию для другого агента.", + save: "Сохранить", + saving: "Сохранение...", + remove: "Удалить", + removeConfirmTitle: "Удалить профиль?", + removeConfirmMessage: "Удалить «{{name}}»?", + moveUp: "Переместить вверх", + moveDown: "Переместить вниз", + }, daemon: { rename: { editLabel: "Изменить ярлык", @@ -2338,6 +2453,9 @@ export const ru: TranslationResources = { "Дополнительную информацию и переменные среды, доступные для этих команд, см. в документации.", setup: "Настраивать", setupAccessibility: "Команды настройки рабочего дерева", + uncommittedTitle: "Закоммитьте изменения paseo.json", + uncommittedDescription: + "Новые рабочие деревья используют сценарий настройки из выбранной базовой ветки.", teardown: "Срывать", teardownAccessibility: "Команды разрушения рабочего дерева", }, diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 571e7dae89..b824abc8ba 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -54,14 +54,19 @@ export const zhCN: TranslationResources = { close: "关闭菜单", }, commandCenter: { - placeholder: "搜索命令、工作区和 Agent...", + placeholder: "搜索命令、文件、工作区和 Agent...", + filePlaceholder: "搜索文件...", + searchingFiles: "正在搜索文件...", noMatches: "没有匹配项", actions: "操作", + files: "文件", workspaces: "工作区", agents: "Agents", newAgent: "新建 Agent", addProject: "添加 project", home: "首页", + groupByProject: "按项目分组", + groupByStatus: "按状态分组", modelGroupLabel: "模型", modelSearchKeywords: "切换模型 更改模型 设置模型 选择模型", thinkingGroupLabel: "思考", @@ -269,6 +274,14 @@ export const zhCN: TranslationResources = { }, }, message: { + diagram: { + diagram: "图表", + zoomIn: "放大", + zoomOut: "缩小", + resetZoom: "重置视图", + viewSource: "查看源码", + viewDiagram: "查看图表", + }, actions: { copyCode: "复制代码", copyTurn: "复制回合", @@ -320,6 +333,15 @@ export const zhCN: TranslationResources = { todo: { title: "任务", empty: "还没有任务。", + tasksProgress: "{{completed}}/{{total}} 项任务", + tasksProgressCurrent: "{{completed}}/{{total}} 项任务 · {{task}}", + activity: { + created: "已创建 {{count}} 项任务", + added: "已添加", + started: "已开始", + completed: "已完成", + reopened: "已重新打开", + }, }, compaction: { loading: "正在压缩...", @@ -390,9 +412,32 @@ export const zhCN: TranslationResources = { fileActions: { openFile: "打开文件", copyPath: "复制路径", + copyRelativePath: "复制相对路径", + revealIn: "在 {{target}} 中显示", download: "下载", addToChat: "添加到聊天", moreActions: "更多操作", + newFile: "新建文件", + newFolder: "新建文件夹", + collapseFolder: "折叠文件夹", + rename: "重命名", + duplicate: "复制", + revert: "放弃更改", + delete: "删除", + confirmDelete: { + fileTitle: "删除文件?", + folderTitle: "删除文件夹?", + message: "“{{name}}”将被永久删除。", + confirm: "删除", + cancel: "取消", + }, + confirmRevert: { + title: "放弃更改?", + message: "对“{{name}}”的更改将被永久放弃。", + confirm: "放弃", + cancel: "取消", + failed: "放弃更改失败", + }, }, fileExplorer: { sort: { @@ -422,6 +467,15 @@ export const zhCN: TranslationResources = { }, errors: { failedToListDirectory: "列出目录失败", + createFailed: "创建条目失败", + renameFailed: "重命名条目失败", + duplicateFailed: "复制条目失败", + revealFailed: "显示条目失败", + deleteFailed: "删除条目失败", + }, + draft: { + filePlaceholder: "文件名", + folderPlaceholder: "文件夹名称", }, }, setup: { @@ -469,6 +523,8 @@ export const zhCN: TranslationResources = { screenshotCopied: "已将截图复制到剪贴板", elementCopied: "已将元素复制到剪贴板", screenshotFailed: "无法复制截图", + selectorLoading: "请等待页面加载完成", + selectorFailed: "无法启动元素选择器", }, annotate: { title: "标注元素", @@ -560,6 +616,7 @@ export const zhCN: TranslationResources = { reloadingAgent: "正在重新加载 Agent...", reloadedAgent: "已重新加载 Agent", failedToReloadAgent: "重新加载 Agent 失败", + failedToCloseAgent: "关闭 Agent 失败", }, confirmations: { unsavedTitle: "未保存的更改", @@ -1337,14 +1394,18 @@ export const zhCN: TranslationResources = { loadingSelector: "正在加载模型选择器...", error: "错误", defaultModel: "默认", - favorites: "收藏", - favoriteModel: "收藏模型", - unfavoriteModel: "取消收藏模型", + profiles: "配置文件", + providers: "提供方", + editProfiles: "编辑", + editProfilesLabel: "编辑智能体配置文件", + createProfile: "创建配置文件", modelCount: "{{count}} 个模型", modelCountPlural: "{{count}} 个模型", retry: "重试", retrying: "正在重试...", noMatches: "没有匹配的模型", + noMatchesForQuery: "没有与“{{query}}”匹配的模型", + searchAllPlaceholder: "搜索所有模型...", searchPlaceholder: "搜索模型...", openProviderSettings: "打开 {{provider}} 设置", }, @@ -1702,12 +1763,26 @@ export const zhCN: TranslationResources = { projects: "项目", connections: "连接", agents: "Agents", + metadata: "元数据", workspaces: "Workspaces", providers: "Providers", usage: "使用情况", terminals: "Terminals", host: "概览", }, + metadataGeneration: { + title: "元数据生成", + description: "选择 Paseo 用于工作区标题、分支名称、提交消息和拉取请求草稿的模型", + selection: "模型选择", + automatic: "自动", + preferred: "手动", + automaticHint: "Paseo 会选择一个可用的快速模型", + preferredHint: "选择 Paseo 使用的模型", + model: "模型", + fallbackHint: "如果不可用,Paseo 会改用其他可用模型", + docs: "文档", + saveError: "无法更新元数据生成设置", + }, general: { title: "通用", browserData: { @@ -1893,10 +1968,10 @@ export const zhCN: TranslationResources = { resetAll: "全部重置", }, sections: { - navigation: "导航", + general: "通用", + workspaces: "项目和工作区", tabsPanes: "标签和窗格", - projects: "项目", - panels: "面板", + layout: "布局", agentInput: "Agent 输入", }, help: { @@ -1924,6 +1999,7 @@ export const zhCN: TranslationResources = { moveTabDown: "向下移动标签", closePane: "关闭窗格", newTerminal: "新建终端", + searchFiles: "搜索文件", toggleCommandCenter: "切换命令中心", showKeyboardShortcuts: "显示键盘快捷键", toggleLeftSidebar: "切换左侧边栏", @@ -2110,6 +2186,42 @@ export const zhCN: TranslationResources = { save: "Save", emptyState: "No profiles yet. Add one to launch terminals with a specific command.", }, + agentProfiles: { + sectionTitle: "Agent 配置", + unavailable: "连接到这个 Host 以管理 Agent 配置", + unsupported: "此 Host 运行的 Daemon 尚不支持 Agent 配置", + emptyState: "还没有配置。添加一个以便使用保存的 Provider 和 Model 启动 Agent。", + addProfileTitle: "添加 Agent 配置", + newProfile: "新建配置", + editProfile: "编辑配置", + editProfileTitle: "编辑 Agent 配置", + nameLabel: "名称", + namePlaceholder: "UI 工作", + iconLabel: "图标", + noIcon: "无", + providerLabel: "Provider", + providerPlaceholder: "选择一个 Provider", + noProviders: "此 Host 上没有可用的 Provider", + modelLabel: "Model", + noModels: "此 Provider 没有可选择的 Model", + modeLabel: "Mode", + noModes: "此 Provider 没有可选择的 Mode", + thinkingLabel: "Thinking", + noThinkingOptions: "此 Model 没有 Thinking 级别", + featuresLabel: "Features", + featureCount: "{{count}} 个 Feature", + featureCountOne: "{{count}} 个 Feature", + notesLabel: "给 Agent 的说明", + notesPlaceholder: "用于 UI 工作 — 组件、布局和设计 token。", + notesHint: "由 list_profiles MCP 工具返回。请将其写成给另一个 Agent 的指令。", + save: "保存", + saving: "保存中...", + remove: "移除", + removeConfirmTitle: "移除配置?", + removeConfirmMessage: "移除「{{name}}」?", + moveUp: "上移", + moveDown: "下移", + }, daemon: { rename: { editLabel: "编辑标签", @@ -2270,6 +2382,8 @@ export const zhCN: TranslationResources = { docsTooltip: "查看命令可用的环境变量和更多细节", setup: "Setup", setupAccessibility: "Worktree setup 命令", + uncommittedTitle: "提交 paseo.json 更改", + uncommittedDescription: "新工作树使用所选基础分支中的设置脚本。", teardown: "Teardown", teardownAccessibility: "Worktree teardown 命令", }, diff --git a/packages/app/src/keyboard/actions.ts b/packages/app/src/keyboard/actions.ts index 8130852578..5a0d86f842 100644 --- a/packages/app/src/keyboard/actions.ts +++ b/packages/app/src/keyboard/actions.ts @@ -41,6 +41,7 @@ export type KeyboardActionId = | "sidebar.toggle.both" | "settings.toggle" | "command-center.toggle" + | "command-center.files" | "shortcuts.dialog.toggle" | "workspace.terminal.new" | "workspace.new" diff --git a/packages/app/src/keyboard/keyboard-shortcuts.test.ts b/packages/app/src/keyboard/keyboard-shortcuts.test.ts index cf12f2c5d9..9a88975d24 100644 --- a/packages/app/src/keyboard/keyboard-shortcuts.test.ts +++ b/packages/app/src/keyboard/keyboard-shortcuts.test.ts @@ -9,6 +9,7 @@ import { parseBindingChord, resolveKeyboardShortcut, resolveShortcutKeysForAction, + SHORTCUT_HELP_ROW_ORDER, UNASSIGNED_COMBO, type ChordState, type KeyboardShortcutContext, @@ -149,16 +150,16 @@ describe("keyboard-shortcuts", () => { action: "workspace.new", }, { - name: "matches Cmd+P to switch project on mac", + name: "matches Cmd+P to search workspace files on mac", event: { key: "p", code: "KeyP", metaKey: true }, context: { isMac: true, commandCenterOpen: false }, - action: "workspace.project.pick", + action: "command-center.files", }, { - name: "matches Ctrl+P to switch project on non-mac", + name: "matches Ctrl+P to search workspace files on non-mac", event: { key: "p", code: "KeyP", ctrlKey: true }, context: { isMac: false, commandCenterOpen: false, focusScope: "other" }, - action: "workspace.project.pick", + action: "command-center.files", }, { name: "matches question-mark shortcut to toggle the shortcuts dialog", @@ -403,6 +404,13 @@ describe("keyboard-shortcuts", () => { }, ); + it("leaves Escape to an editable field", () => { + expectNoShortcutResolution({ + event: { key: "Escape", code: "Escape" }, + context: { focusScope: "editable" }, + }); + }); + const nonMatchingCases: NonMatchingShortcutCase[] = [ { name: "does not keep old Mod+Alt+N binding", @@ -781,20 +789,63 @@ describe("keyboard-shortcut help sections", () => { it("returns stable i18n keys for section titles and help rows", () => { const sections = buildKeyboardShortcutHelpSections({ isMac: true, isDesktop: true }); - const projects = sections.find((section) => section.id === "projects"); - const panels = sections.find((section) => section.id === "panels"); + const workspaces = sections.find((section) => section.id === "workspaces"); + const layout = sections.find((section) => section.id === "layout"); const openProject = findRow(sections, "new-agent"); const cycleAgentMode = findRow(sections, "cycle-agent-mode"); const showShortcuts = findRow(sections, "show-shortcuts"); - expect(projects?.titleKey).toBe("settings.shortcuts.sections.projects"); - expect(panels?.titleKey).toBe("settings.shortcuts.sections.panels"); + expect(workspaces?.titleKey).toBe("settings.shortcuts.sections.workspaces"); + expect(layout?.titleKey).toBe("settings.shortcuts.sections.layout"); expect(openProject?.labelKey).toBe("settings.shortcuts.help.openProject"); expect(openProject?.label).toBe("Open project"); expect(cycleAgentMode?.labelKey).toBe("settings.shortcuts.help.cycleAgentMode"); expect(showShortcuts?.noteKey).toBe("settings.shortcuts.helpNotes.showKeyboardShortcuts"); }); + it("gives every help row an explicit place in its section's order", () => { + const platforms = [ + { isMac: true, isDesktop: true }, + { isMac: false, isDesktop: true }, + { isMac: true, isDesktop: false }, + { isMac: false, isDesktop: false }, + ]; + const unplaced: string[] = []; + for (const platform of platforms) { + for (const section of buildKeyboardShortcutHelpSections(platform)) { + for (const row of section.rows) { + if (SHORTCUT_HELP_ROW_ORDER[section.id].includes(row.id)) continue; + unplaced.push(`${section.id}:${row.id}`); + } + } + } + + expect(unplaced).toEqual([]); + }); + + it("leads the general section with the command center and file search", () => { + const sections = buildKeyboardShortcutHelpSections({ isMac: true, isDesktop: true }); + + expect(sections[0]?.id).toBe("general"); + expect(sections[0]?.rows.slice(0, 2).map((row) => row.id)).toEqual([ + "toggle-command-center", + "search-files", + ]); + }); + + it("reuses the project-picker binding ids for rebindable file search", () => { + expect(getBindingIdForAction("search-files", { isMac: true, isDesktop: true })).toBe( + "workspace-project-pick-cmd-p-mac", + ); + expect(getBindingIdForAction("search-files", { isMac: false, isDesktop: true })).toBe( + "workspace-project-pick-ctrl-p-non-mac", + ); + expect( + findRow(buildKeyboardShortcutHelpSections({ isMac: true, isDesktop: true }), "search-files") + ?.chord, + ).not.toBeNull(); + }); + it("does not expose Enter send behavior as rebindable shortcut rows", () => { const sections = buildKeyboardShortcutHelpSections({ isMac: true, isDesktop: true }); diff --git a/packages/app/src/keyboard/keyboard-shortcuts.ts b/packages/app/src/keyboard/keyboard-shortcuts.ts index 6ca660c378..6653e412d8 100644 --- a/packages/app/src/keyboard/keyboard-shortcuts.ts +++ b/packages/app/src/keyboard/keyboard-shortcuts.ts @@ -49,7 +49,7 @@ export interface KeyboardShortcutHelpRow { noteKey?: string; } -export type ShortcutSectionId = "navigation" | "tabs-panes" | "projects" | "panels" | "agent-input"; +export type ShortcutSectionId = "general" | "workspaces" | "tabs-panes" | "layout" | "agent-input"; export interface KeyboardShortcutHelpSection { id: ShortcutSectionId; @@ -76,8 +76,8 @@ interface ShortcutWhen { terminal?: false; /** false = disabled when command center is open */ commandCenter?: false; - /** Exact focus scope match */ - focusScope?: KeyboardFocusScope; + /** Allowed focus scope or scopes */ + focusScope?: KeyboardFocusScope | readonly KeyboardFocusScope[]; } type ShortcutPayloadDef = @@ -124,22 +124,81 @@ export interface ChordState { // --- Constants --- +// Sections are listed in this order, most-reached-for first. +const SHORTCUT_HELP_SECTION_ORDER: readonly ShortcutSectionId[] = [ + "general", + "workspaces", + "tabs-panes", + "layout", + "agent-input", +]; + const SHORTCUT_HELP_SECTION_TITLES: Record = { - navigation: "Navigation", + general: "General", + workspaces: "Projects & Workspaces", "tabs-panes": "Tabs & Panes", - projects: "Projects", - panels: "Panels", + layout: "Layout", "agent-input": "Agent Input", }; const SHORTCUT_HELP_SECTION_LABEL_KEYS: Record = { - navigation: "settings.shortcuts.sections.navigation", + general: "settings.shortcuts.sections.general", + workspaces: "settings.shortcuts.sections.workspaces", "tabs-panes": "settings.shortcuts.sections.tabsPanes", - projects: "settings.shortcuts.sections.projects", - panels: "settings.shortcuts.sections.panels", + layout: "settings.shortcuts.sections.layout", "agent-input": "settings.shortcuts.sections.agentInput", }; +// Rows render in this order rather than in binding-definition order, so the shortcut someone opens +// this sheet to find sits at the top of its section. Every help id must appear here; the unit test +// fails when a new shortcut is added without a place, instead of silently sinking to the bottom. +export const SHORTCUT_HELP_ROW_ORDER: Record = { + general: [ + "toggle-command-center", + "search-files", + "show-shortcuts", + "toggle-settings", + "cycle-theme", + ], + workspaces: [ + "new-agent", + "new-workspace", + "workspace-jump-index", + "workspace-prev", + "workspace-next", + "pin-workspace", + "archive-workspace", + ], + "tabs-panes": [ + "workspace-tab-new", + "workspace-terminal-new", + "workspace-tab-close-current", + "workspace-tab-jump-index", + "workspace-tab-prev", + "workspace-tab-next", + "workspace-pane-split-right", + "workspace-pane-split-down", + "workspace-pane-focus-left", + "workspace-pane-focus-right", + "workspace-pane-focus-up", + "workspace-pane-focus-down", + "workspace-pane-move-tab-left", + "workspace-pane-move-tab-right", + "workspace-pane-move-tab-up", + "workspace-pane-move-tab-down", + "workspace-pane-close", + ], + layout: ["toggle-left-sidebar", "toggle-right-sidebar", "toggle-both-sidebars", "toggle-focus"], + "agent-input": [ + "focus-message-input", + "cycle-agent-mode", + "voice-toggle", + "dictation-toggle", + "agent-interrupt", + "voice-mute-toggle", + ], +}; + const SHORTCUT_HELP_LABEL_KEYS: Record = { "new-agent": "settings.shortcuts.help.openProject", "new-workspace": "settings.shortcuts.help.newWorkspace", @@ -165,6 +224,7 @@ const SHORTCUT_HELP_LABEL_KEYS: Record = { "workspace-pane-move-tab-down": "settings.shortcuts.help.moveTabDown", "workspace-pane-close": "settings.shortcuts.help.closePane", "workspace-terminal-new": "settings.shortcuts.help.newTerminal", + "search-files": "settings.shortcuts.help.searchFiles", "toggle-command-center": "settings.shortcuts.help.toggleCommandCenter", "show-shortcuts": "settings.shortcuts.help.showKeyboardShortcuts", "toggle-left-sidebar": "settings.shortcuts.help.toggleLeftSidebar", @@ -200,7 +260,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: true }, help: { id: "new-agent", - section: "projects", + section: "workspaces", label: "Open project", }, }, @@ -211,7 +271,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: false, terminal: false }, help: { id: "new-agent", - section: "projects", + section: "workspaces", label: "Open project", }, }, @@ -224,7 +284,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: true, commandCenter: false }, help: { id: "new-workspace", - section: "projects", + section: "workspaces", label: "New workspace", }, }, @@ -235,32 +295,32 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: false, commandCenter: false, terminal: false }, help: { id: "new-workspace", - section: "projects", + section: "workspaces", label: "New workspace", }, }, - // --- Switch project (New Workspace screen) --- + // --- Search files (switch project on the New Workspace screen) --- { id: "workspace-project-pick-cmd-p-mac", - action: "workspace.project.pick", + action: "command-center.files", combo: "Cmd+P", when: { mac: true, commandCenter: false }, help: { - id: "switch-project", - section: "projects", - label: "Switch project", + id: "search-files", + section: "general", + label: "Search files", }, }, { id: "workspace-project-pick-ctrl-p-non-mac", - action: "workspace.project.pick", + action: "command-center.files", combo: "Ctrl+P", when: { mac: false, commandCenter: false, terminal: false }, help: { - id: "switch-project", - section: "projects", - label: "Switch project", + id: "search-files", + section: "general", + label: "Search files", }, }, @@ -274,7 +334,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: true, commandCenter: false }, help: { id: "archive-workspace", - section: "projects", + section: "workspaces", label: "Archive workspace", }, }, @@ -287,7 +347,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: false, commandCenter: false, terminal: false }, help: { id: "archive-workspace", - section: "projects", + section: "workspaces", label: "Archive workspace", }, }, @@ -300,7 +360,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: true, commandCenter: false }, help: { id: "pin-workspace", - section: "projects", + section: "workspaces", label: "Pin chat", }, }, @@ -311,7 +371,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: false, commandCenter: false, terminal: false }, help: { id: "pin-workspace", - section: "projects", + section: "workspaces", label: "Pin chat", }, }, @@ -382,7 +442,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ payload: { type: "index" }, help: { id: "workspace-jump-index", - section: "navigation", + section: "workspaces", label: "Jump to workspace", defaultDisplayKeys: ["mod", "1-9"], }, @@ -395,7 +455,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ payload: { type: "index" }, help: { id: "workspace-jump-index", - section: "navigation", + section: "workspaces", label: "Jump to workspace", defaultDisplayKeys: ["mod", "1-9"], }, @@ -408,7 +468,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ payload: { type: "index" }, help: { id: "workspace-jump-index", - section: "navigation", + section: "workspaces", label: "Jump to workspace", defaultDisplayKeys: ["alt", "1-9"], }, @@ -423,7 +483,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ payload: { type: "index" }, help: { id: "workspace-tab-jump-index", - section: "navigation", + section: "tabs-panes", label: "Jump to tab", defaultDisplayKeys: ["mod", "alt", "1-9"], }, @@ -436,7 +496,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ payload: { type: "index" }, help: { id: "workspace-tab-jump-index", - section: "navigation", + section: "tabs-panes", label: "Jump to tab", defaultDisplayKeys: ["alt", "1-9"], }, @@ -449,7 +509,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ payload: { type: "index" }, help: { id: "workspace-tab-jump-index", - section: "navigation", + section: "tabs-panes", label: "Jump to tab", defaultDisplayKeys: ["alt", "shift", "1-9"], }, @@ -464,7 +524,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ payload: { type: "delta", delta: -1 }, help: { id: "workspace-prev", - section: "navigation", + section: "workspaces", label: "Previous workspace", }, }, @@ -476,7 +536,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ payload: { type: "delta", delta: -1 }, help: { id: "workspace-prev", - section: "navigation", + section: "workspaces", label: "Previous workspace", }, }, @@ -488,7 +548,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ payload: { type: "delta", delta: 1 }, help: { id: "workspace-next", - section: "navigation", + section: "workspaces", label: "Next workspace", }, }, @@ -500,7 +560,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ payload: { type: "delta", delta: 1 }, help: { id: "workspace-next", - section: "navigation", + section: "workspaces", label: "Next workspace", }, }, @@ -512,7 +572,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ payload: { type: "delta", delta: -1 }, help: { id: "workspace-prev", - section: "navigation", + section: "workspaces", label: "Previous workspace", }, }, @@ -524,7 +584,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ payload: { type: "delta", delta: 1 }, help: { id: "workspace-next", - section: "navigation", + section: "workspaces", label: "Next workspace", }, }, @@ -538,7 +598,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ payload: { type: "delta", delta: -1 }, help: { id: "workspace-tab-prev", - section: "navigation", + section: "tabs-panes", label: "Previous tab", }, }, @@ -550,7 +610,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ payload: { type: "delta", delta: 1 }, help: { id: "workspace-tab-next", - section: "navigation", + section: "tabs-panes", label: "Next tab", }, }, @@ -686,7 +746,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: true, commandCenter: false }, help: { id: "workspace-terminal-new", - section: "panels", + section: "tabs-panes", label: "New terminal", }, }, @@ -697,7 +757,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: false, commandCenter: false, terminal: false }, help: { id: "workspace-terminal-new", - section: "panels", + section: "tabs-panes", label: "New terminal", }, }, @@ -710,7 +770,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: true }, help: { id: "toggle-command-center", - section: "panels", + section: "general", label: "Toggle command center", }, }, @@ -721,7 +781,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: false, terminal: false }, help: { id: "toggle-command-center", - section: "panels", + section: "general", label: "Toggle command center", }, }, @@ -735,7 +795,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { focusScope: "other" }, help: { id: "show-shortcuts", - section: "panels", + section: "general", label: "Show keyboard shortcuts", defaultDisplayKeys: ["?"], note: "Available when focus is not in a text field or terminal.", @@ -750,7 +810,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: true }, help: { id: "toggle-left-sidebar", - section: "panels", + section: "layout", label: "Toggle left sidebar", }, }, @@ -761,7 +821,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: false, commandCenter: false, terminal: false }, help: { id: "toggle-left-sidebar", - section: "panels", + section: "layout", label: "Toggle left sidebar", }, }, @@ -772,7 +832,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: true, commandCenter: false }, help: { id: "toggle-right-sidebar", - section: "panels", + section: "layout", label: "Toggle right sidebar", }, }, @@ -783,7 +843,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: false, commandCenter: false, terminal: false }, help: { id: "toggle-right-sidebar", - section: "panels", + section: "layout", label: "Toggle right sidebar", }, }, @@ -802,7 +862,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: true, commandCenter: false }, help: { id: "toggle-both-sidebars", - section: "panels", + section: "layout", label: "Toggle both sidebars", }, }, @@ -813,7 +873,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: false, commandCenter: false, terminal: false }, help: { id: "toggle-both-sidebars", - section: "panels", + section: "layout", label: "Toggle both sidebars", }, }, @@ -826,7 +886,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: true, commandCenter: false }, help: { id: "toggle-settings", - section: "panels", + section: "general", label: "Toggle settings", }, }, @@ -837,7 +897,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: false, commandCenter: false, terminal: false }, help: { id: "toggle-settings", - section: "panels", + section: "general", label: "Toggle settings", }, }, @@ -850,7 +910,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: true, commandCenter: false }, help: { id: "toggle-focus", - section: "panels", + section: "layout", label: "Toggle focus mode", }, }, @@ -861,7 +921,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: false, commandCenter: false, terminal: false }, help: { id: "toggle-focus", - section: "panels", + section: "layout", label: "Toggle focus mode", }, }, @@ -874,7 +934,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: true, commandCenter: false }, help: { id: "cycle-theme", - section: "panels", + section: "general", label: "Cycle theme", }, }, @@ -885,7 +945,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ when: { mac: false, commandCenter: false, terminal: false }, help: { id: "cycle-theme", - section: "panels", + section: "general", label: "Cycle theme", }, }, @@ -982,7 +1042,7 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ id: "agent-interrupt", action: "agent.interrupt", combo: "Escape", - when: { commandCenter: false, terminal: false }, + when: { commandCenter: false, focusScope: ["message-input", "other"] }, preventDefault: false, stopPropagation: false, help: { @@ -1155,7 +1215,14 @@ export function matchesKeyboardShortcutContext( } if (when.terminal === false && context.focusScope === "terminal") return false; if (when.commandCenter === false && context.commandCenterOpen) return false; - if (when.focusScope !== undefined && context.focusScope !== when.focusScope) return false; + if ( + when.focusScope !== undefined && + !(typeof when.focusScope === "string" + ? context.focusScope === when.focusScope + : when.focusScope.includes(context.focusScope)) + ) { + return false; + } return true; } @@ -1504,13 +1571,9 @@ export function buildKeyboardShortcutHelpSections( bindings: readonly ParsedShortcutBinding[] = DEFAULT_BINDINGS, ): KeyboardShortcutHelpSection[] { const seenRows = new Set(); - const rowsBySection = new Map([ - ["navigation", []], - ["tabs-panes", []], - ["projects", []], - ["panels", []], - ["agent-input", []], - ]); + const rowsBySection = new Map( + SHORTCUT_HELP_SECTION_ORDER.map((sectionId) => [sectionId, []]), + ); for (const binding of bindings) { const help = binding.help; @@ -1540,19 +1603,17 @@ export function buildKeyboardShortcutHelpSections( }); } - const sectionOrder: ShortcutSectionId[] = [ - "navigation", - "tabs-panes", - "projects", - "panels", - "agent-input", - ]; - - return sectionOrder.flatMap((sectionId) => { + return SHORTCUT_HELP_SECTION_ORDER.flatMap((sectionId) => { const rows = rowsBySection.get(sectionId) ?? []; if (rows.length === 0) { return []; } + const order = SHORTCUT_HELP_ROW_ORDER[sectionId]; + const rank = (row: KeyboardShortcutHelpRow) => { + const index = order.indexOf(row.id); + return index === -1 ? order.length : index; + }; + rows.sort((left, right) => rank(left) - rank(right)); return [ { id: sectionId, diff --git a/packages/app/src/keyboard/route-shortcut.test.ts b/packages/app/src/keyboard/route-shortcut.test.ts index 5ace809b91..494c2a1883 100644 --- a/packages/app/src/keyboard/route-shortcut.test.ts +++ b/packages/app/src/keyboard/route-shortcut.test.ts @@ -351,6 +351,24 @@ describe("routeKeyboardShortcut — callbacks and pickers", () => { }); describe("routeKeyboardShortcut — toggle dialogs", () => { + it("opens the command center scoped to files from a workspace", () => { + expect( + routeKeyboardShortcut({ action: "command-center.files", payload: null }, makeCtx()), + ).toEqual({ kind: "command-center-toggle", nextOpen: true, scope: "files" }); + }); + + it("leaves the file-search shortcut to the project-picker host outside a workspace", () => { + expect( + routeKeyboardShortcut( + { action: "command-center.files", payload: null }, + makeCtx({ pathname: "/settings" }), + ), + ).toEqual({ + kind: "dispatch", + action: { id: "workspace.project.pick", scope: "workspace" }, + }); + }); + it("opens the command center when closed", () => { expect( routeKeyboardShortcut( diff --git a/packages/app/src/keyboard/route-shortcut.ts b/packages/app/src/keyboard/route-shortcut.ts index 71b4395cca..0fd622d30e 100644 --- a/packages/app/src/keyboard/route-shortcut.ts +++ b/packages/app/src/keyboard/route-shortcut.ts @@ -32,7 +32,7 @@ export type ShortcutAction = | { kind: "router-push"; route: string } | { kind: "open-project-picker" } | { kind: "callback"; name: ShortcutCallbackName } - | { kind: "command-center-toggle"; nextOpen: boolean } + | { kind: "command-center-toggle"; nextOpen: boolean; scope?: "files" } | { kind: "shortcuts-dialog-toggle"; nextOpen: boolean }; const NONE: ShortcutAction = { kind: "none" }; @@ -198,6 +198,11 @@ export function routeKeyboardShortcut( return routeSettingsToggle(ctx); case "command-center.toggle": return { kind: "command-center-toggle", nextOpen: !ctx.commandCenterOpen }; + case "command-center.files": + if (parseHostWorkspaceRouteFromPathname(ctx.pathname)) { + return { kind: "command-center-toggle", nextOpen: true, scope: "files" }; + } + return dispatch({ id: "workspace.project.pick", scope: "workspace" }); case "shortcuts.dialog.toggle": return { kind: "shortcuts-dialog-toggle", nextOpen: !ctx.shortcutsDialogOpen }; default: diff --git a/packages/app/src/keyboard/shortcut-help-search.test.ts b/packages/app/src/keyboard/shortcut-help-search.test.ts index c365a09b03..201d4865f0 100644 --- a/packages/app/src/keyboard/shortcut-help-search.test.ts +++ b/packages/app/src/keyboard/shortcut-help-search.test.ts @@ -99,12 +99,12 @@ describe("filterShortcutHelpSections", () => { }); it("keeps every row of a section whose own title matches", () => { - const projects = filter("projects").find((section) => section.id === "projects"); - const allProjects = buildKeyboardShortcutHelpSections(MAC_DESKTOP).find( - (section) => section.id === "projects", + const matched = filter("layout").find((section) => section.id === "layout"); + const all = buildKeyboardShortcutHelpSections(MAC_DESKTOP).find( + (section) => section.id === "layout", ); - expect(projects?.rows).toHaveLength(allProjects?.rows.length ?? 0); + expect(matched?.rows).toHaveLength(all?.rows.length ?? 0); }); it("drops a section whose rows all miss", () => { diff --git a/packages/app/src/panels/agent-panel.tsx b/packages/app/src/panels/agent-panel.tsx index ae58e89237..59eb41112b 100644 --- a/packages/app/src/panels/agent-panel.tsx +++ b/packages/app/src/panels/agent-panel.tsx @@ -74,6 +74,7 @@ import { deriveRouteBottomAnchorRequest, } from "@/screens/agent/agent-ready-screen-bottom-anchor"; import { WorkspaceDraftAgentTab } from "@/composer/draft/workspace-tab"; +import { AgentTaskList } from "@/composer/task-list"; import { useCreateFlowStore } from "@/stores/create-flow-store"; import { buildDraftStoreKey, generateDraftId } from "@/stores/draft-keys"; import { usePanelStore } from "@/stores/panel-store"; @@ -1227,7 +1228,9 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({ // when only toast state changes (which does not affect any draft field). const { text, - setText, + editText, + replaceText, + textReplacementKey, attachments, setAttachments, clear, @@ -1238,7 +1241,9 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({ const agentInputDraft = useMemo( (): AgentInputDraft => ({ text, - setText, + editText, + replaceText, + textReplacementKey, attachments, setAttachments, clear, @@ -1248,7 +1253,9 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({ }), [ text, - setText, + editText, + replaceText, + textReplacementKey, attachments, setAttachments, clear, @@ -1295,7 +1302,10 @@ const ChatAgentReadyContent = memo(function ChatAgentReadyContent({ const contentContainer = {streamContent}; return ( - + {contentContainer} @@ -1612,6 +1622,7 @@ function ActiveAgentComposer({ return ( + project.projectKey === input.candidate.projectKey, ); if (equivalents.length === 0) return null; - return equivalents.toSorted((left, right) => + return equivalents.sort((left, right) => compareHostProjectsForServer(left, right, input.serverId), )[0]!; } diff --git a/packages/app/src/projects/host-projects.test.ts b/packages/app/src/projects/host-projects.test.ts index 798d08b08e..2806f5b90f 100644 --- a/packages/app/src/projects/host-projects.test.ts +++ b/packages/app/src/projects/host-projects.test.ts @@ -7,6 +7,7 @@ import { getWorktreeSupportForHostProject, hostProjectFromRoute, hostProjectFromWorkspace, + resolveEquivalentHostProjectCandidate, } from "./host-project-model"; import { normalizeWorkspaceDescriptor } from "@/stores/session-store"; @@ -36,6 +37,31 @@ function project(): HostProjectListItem { } describe("host project lookups", () => { + test("resolves equivalent projects without Array.prototype.toSorted", () => { + const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, "toSorted"); + Reflect.deleteProperty(Array.prototype, "toSorted"); + const first = project(); + first.viewKey = "view:first"; + first.projectName = "First"; + const second = project(); + second.viewKey = "view:second"; + second.projectName = "Second"; + const projects = [second, first]; + + try { + expect( + resolveEquivalentHostProjectCandidate({ + candidate: first, + projects, + serverId: "host-a", + }), + ).toBe(first); + expect(projects).toEqual([second, first]); + } finally { + if (descriptor) Reflect.defineProperty(Array.prototype, "toSorted", descriptor); + } + }); + test("returns host-local ids and roots without falling back to the grouping key", () => { expect(getHostProjectId(project(), "host-b")).toBe("prj_b"); expect(getHostProjectSourceDirectory(project(), "host-b")).toBe("/repo/b"); diff --git a/packages/app/src/projects/icon-cache.test.ts b/packages/app/src/projects/icon-cache.test.ts new file mode 100644 index 0000000000..92df4c4099 --- /dev/null +++ b/packages/app/src/projects/icon-cache.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from "vitest"; +import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; +import { ProjectIconCache, type ProjectIconCacheStorage } from "./icon-cache"; + +class MemoryStorage implements ProjectIconCacheStorage { + readonly values = new Map(); + async getItem(key: string): Promise { + return this.values.get(key) ?? null; + } + async setItem(key: string, value: string): Promise { + this.values.set(key, value); + } +} + +const target = { + serverId: "host-a", + projectId: "project-a", + iconWorkingDir: "/project-a", + iconRevision: "revision-a", +}; +const icon = { data: "aWNvbg==", mimeType: "image/png" }; + +function clientWith(iconResult: typeof icon | null) { + return { + getProjectIcon: async () => ({ requestId: "icon", icon: iconResult }), + } as unknown as DaemonClient; +} + +describe("ProjectIconCache", () => { + it("resolves, persists, and hydrates an icon before a host connects", async () => { + const storage = new MemoryStorage(); + const writer = new ProjectIconCache(storage); + writer.setHosts([target.serverId]); + await writer.query(target, true, () => clientWith(icon), true).queryFn(); + await writer.flush(); + + const reader = new ProjectIconCache(storage); + reader.setHosts([target.serverId]); + await reader.restore(); + + expect(reader.query(target, true, () => null, false).initialData).toEqual(icon); + }); + + it("persists an explicit no-icon result", async () => { + const storage = new MemoryStorage(); + const writer = new ProjectIconCache(storage); + writer.setHosts([target.serverId]); + await writer.query(target, true, () => clientWith(null), true).queryFn(); + await writer.flush(); + + const reader = new ProjectIconCache(storage); + reader.setHosts([target.serverId]); + await reader.restore(); + expect(reader.query(target, true, () => null, false)).toHaveProperty("initialData", null); + }); + + it("misses only the project whose revision changed", async () => { + const cache = new ProjectIconCache(new MemoryStorage()); + const other = { ...target, projectId: "project-b" }; + await cache.query(target, true, () => clientWith(icon), true).queryFn(); + await cache.query(other, true, () => clientWith(null), true).queryFn(); + + expect( + cache.query({ ...target, iconRevision: "revision-b" }, true, () => null, false), + ).not.toHaveProperty("initialData"); + expect(cache.query(other, true, () => null, false)).toHaveProperty("initialData", null); + }); +}); diff --git a/packages/app/src/projects/icon-cache.ts b/packages/app/src/projects/icon-cache.ts new file mode 100644 index 0000000000..93761708cf --- /dev/null +++ b/packages/app/src/projects/icon-cache.ts @@ -0,0 +1,217 @@ +import type { ProjectIcon } from "@getpaseo/protocol/messages"; +import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; +import { replicaCacheStorage } from "@/runtime/replica-cache/storage"; + +const STORAGE_KEY = "@paseo:project-icon-cache"; +const CACHE_VERSION = 1; +const PERSIST_DELAY_MS = 250; +const MAX_ENTRIES = 512; + +export interface ProjectIconCacheStorage { + getItem(key: string): Promise; + setItem(key: string, value: string): Promise; +} + +interface ProjectIconIdentity { + serverId: string; + projectId: string; + revision: string; +} + +type ProjectIconCacheRead = { hit: true; icon: ProjectIcon | null } | { hit: false }; + +interface ProjectIconTarget { + serverId: string; + projectId: string; + iconWorkingDir: string; + customIconRevision?: string | null; + iconRevision?: string; +} + +interface StoredIcon extends ProjectIconIdentity { + icon: ProjectIcon | null; +} + +function keyOf(identity: ProjectIconIdentity): string { + return JSON.stringify([identity.serverId, identity.projectId, identity.revision]); +} + +function parseStoredIcon(value: unknown): StoredIcon | null { + if (!value || typeof value !== "object") return null; + const entry = value as Record; + if ( + typeof entry.serverId !== "string" || + typeof entry.projectId !== "string" || + typeof entry.revision !== "string" + ) { + return null; + } + if (entry.icon === null) { + return { + serverId: entry.serverId, + projectId: entry.projectId, + revision: entry.revision, + icon: null, + }; + } + if (!entry.icon || typeof entry.icon !== "object") return null; + const icon = entry.icon as Record; + if (typeof icon.data !== "string" || typeof icon.mimeType !== "string") return null; + return { + serverId: entry.serverId, + projectId: entry.projectId, + revision: entry.revision, + icon: { data: icon.data, mimeType: icon.mimeType }, + }; +} + +/** Owns project-icon persistence, revision matching, and negative results. */ +export class ProjectIconCache { + private readonly entries = new Map(); + private activeServerIds = new Set(); + private persistTimer: ReturnType | null = null; + private writeQueue: Promise = Promise.resolve(); + + constructor(private readonly storage: ProjectIconCacheStorage) {} + + setHosts(serverIds: Iterable): void { + this.activeServerIds = new Set(serverIds); + let changed = false; + for (const [key, entry] of this.entries) { + if (!this.activeServerIds.has(entry.serverId)) { + this.entries.delete(key); + changed = true; + } + } + if (changed) this.schedulePersist(); + } + + async restore(): Promise { + let raw: string | null; + try { + raw = await this.storage.getItem(STORAGE_KEY); + } catch { + return; + } + if (!raw) return; + try { + const parsed = JSON.parse(raw) as { version?: unknown; entries?: unknown }; + if (parsed.version !== CACHE_VERSION || !Array.isArray(parsed.entries)) return; + for (const value of parsed.entries) { + const entry = parseStoredIcon(value); + if (entry && this.activeServerIds.has(entry.serverId)) { + this.entries.set(keyOf(entry), entry); + } + } + } catch { + // A corrupt cache is disposable. + } + } + + private read(identity: ProjectIconIdentity): ProjectIconCacheRead { + const entry = this.entries.get(keyOf(identity)); + return entry ? { hit: true, icon: entry.icon } : { hit: false }; + } + + private write(identity: ProjectIconIdentity, icon: ProjectIcon | null): void { + const key = keyOf(identity); + this.entries.delete(key); + this.entries.set(key, { ...identity, icon }); + while (this.entries.size > MAX_ENTRIES) { + const oldest = this.entries.keys().next().value; + if (oldest === undefined) break; + this.entries.delete(oldest); + } + this.schedulePersist(); + } + + query( + target: ProjectIconTarget, + supportsCustomIcons: boolean | null, + getClient: () => DaemonClient | null, + connected: boolean, + ) { + // COMPAT(projectIconCache): daemons before v0.2.7 do not expose an effective revision, so + // their icon results remain memory-only. + const revision = target.iconRevision ?? target.customIconRevision ?? "automatic"; + const identity: ProjectIconIdentity | null = target.iconRevision + ? { serverId: target.serverId, projectId: target.projectId, revision: target.iconRevision } + : null; + const cached = identity ? this.read(identity) : { hit: false as const }; + const lookup = resolveLookup(target, supportsCustomIcons); + let queryKey: readonly string[]; + if (!lookup) { + queryKey = ["projectIcon", target.serverId, "pending", target.projectId]; + } else if (lookup.kind === "project") { + queryKey = ["projectIcon", target.serverId, target.projectId, revision]; + } else { + queryKey = ["projectIcon", target.serverId, "legacy", lookup.cwd]; + } + return { + queryKey, + queryFn: async () => { + if (!lookup) return null; + const client = getClient(); + if (!client) return null; + const result = + lookup.kind === "project" + ? await client.getProjectIcon(lookup.projectId) + : await client.requestProjectIcon(lookup.cwd); + if (result.error) throw new Error(result.error); + if (identity) this.write(identity, result.icon); + return result.icon; + }, + enabled: Boolean(lookup && getClient() && connected), + staleTime: Infinity, + gcTime: 1000 * 60 * 60, + refetchOnMount: false as const, + refetchOnWindowFocus: false as const, + refetchOnReconnect: false as const, + ...(cached.hit ? { initialData: cached.icon } : {}), + }; + } + + reconcileServerId(oldServerId: string, newServerId: string): void { + for (const [key, entry] of this.entries) { + if (entry.serverId !== oldServerId) continue; + this.entries.delete(key); + const reconciled = { ...entry, serverId: newServerId }; + this.entries.set(keyOf(reconciled), reconciled); + } + if (this.activeServerIds.delete(oldServerId)) this.activeServerIds.add(newServerId); + this.schedulePersist(); + } + + async flush(): Promise { + if (this.persistTimer) { + clearTimeout(this.persistTimer); + this.persistTimer = null; + } + const payload = JSON.stringify({ version: CACHE_VERSION, entries: [...this.entries.values()] }); + const write = this.writeQueue + .catch(() => undefined) + .then(() => this.storage.setItem(STORAGE_KEY, payload)); + this.writeQueue = write; + await write.catch(() => undefined); + } + + private schedulePersist(): void { + if (this.persistTimer) return; + this.persistTimer = setTimeout(() => { + this.persistTimer = null; + void this.flush(); + }, PERSIST_DELAY_MS); + } +} + +export const projectIconCache = new ProjectIconCache(replicaCacheStorage); + +function resolveLookup( + target: Pick, + supportsCustomIcons: boolean | null, +): { kind: "project"; projectId: string } | { kind: "legacy"; cwd: string } | null { + if (supportsCustomIcons === null) return null; + return supportsCustomIcons + ? { kind: "project", projectId: target.projectId } + : { kind: "legacy", cwd: target.iconWorkingDir }; +} diff --git a/packages/app/src/projects/icons.test.ts b/packages/app/src/projects/icons.test.ts index 48b99df4a4..e13b468cb5 100644 --- a/packages/app/src/projects/icons.test.ts +++ b/packages/app/src/projects/icons.test.ts @@ -20,4 +20,8 @@ describe("project icon lookup", () => { cwd: "/projects/paseo", }); }); + + it("waits while custom-icon capability is unknown", () => { + expect(resolveProjectIconLookup(target, null)).toBeNull(); + }); }); diff --git a/packages/app/src/projects/icons.ts b/packages/app/src/projects/icons.ts index f94fdc0f41..92bef909a6 100644 --- a/packages/app/src/projects/icons.ts +++ b/packages/app/src/projects/icons.ts @@ -2,7 +2,8 @@ import { useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; import { useQueries, useQuery } from "@tanstack/react-query"; import type { ProjectIcon } from "@getpaseo/protocol/messages"; -import { useHostFeatureMap } from "@/runtime/host-features"; +import { useHostFeatureAvailabilityMap } from "@/runtime/host-features"; +import { projectIconCache } from "@/projects/icon-cache"; import { getHostRuntimeStore, isHostRuntimeConnected, @@ -16,6 +17,7 @@ interface ProjectIconTarget { projectId: string; iconWorkingDir: string; customIconRevision?: string | null; + iconRevision?: string; } /** @@ -24,8 +26,9 @@ interface ProjectIconTarget { */ export function resolveProjectIconLookup( target: Pick, - supportsCustomIcons: boolean, -): { kind: "project"; projectId: string } | { kind: "legacy"; cwd: string } { + supportsCustomIcons: boolean | null, +): { kind: "project"; projectId: string } | { kind: "legacy"; cwd: string } | null { + if (supportsCustomIcons === null) return null; return supportsCustomIcons ? { kind: "project", projectId: target.projectId } : { kind: "legacy", cwd: target.iconWorkingDir }; @@ -35,10 +38,6 @@ function legacyIconQueryKey(serverId: string, cwd: string) { return ["projectIcon", serverId, "legacy", cwd] as const; } -function iconQueryKey(serverId: string, projectId: string, revision: string) { - return ["projectIcon", serverId, projectId, revision] as const; -} - function iconDataUri(icon: ProjectIcon | null): string | null { if (!icon) return null; return `data:${icon.mimeType};base64,${icon.data}`; @@ -88,7 +87,7 @@ export function useProjectIcons(input: { () => [...new Set(input.projects.map((project) => project.serverId))], [input.projects], ); - const supportsCustomIcons = useHostFeatureMap(serverIds, "projectCustomIcon"); + const supportsCustomIcons = useHostFeatureAvailabilityMap(serverIds, "projectCustomIcon"); const requests = useMemo(() => { const unique = new Map(); for (const project of input.projects) { @@ -100,35 +99,14 @@ export function useProjectIcons(input: { const queries = useQueries({ queries: requests.map((request) => { - const revision = request.customIconRevision ?? "automatic"; - const lookup = resolveProjectIconLookup( - request, - supportsCustomIcons.get(request.serverId) === true, - ); return { - queryKey: - lookup.kind === "project" - ? iconQueryKey(request.serverId, lookup.projectId, revision) - : legacyIconQueryKey(request.serverId, lookup.cwd), - queryFn: async () => { - const client = getHostRuntimeStore().getClient(request.serverId); - if (!client) return null; - const result = - lookup.kind === "project" - ? await client.getProjectIcon(lookup.projectId) - : await client.requestProjectIcon(lookup.cwd); - return result.icon; - }, - select: iconDataUri, - enabled: Boolean( - getHostRuntimeStore().getClient(request.serverId) && + ...projectIconCache.query( + request, + supportsCustomIcons.get(request.serverId) ?? null, + () => getHostRuntimeStore().getClient(request.serverId), isHostRuntimeConnected(getHostRuntimeStore().getSnapshot(request.serverId)), ), - staleTime: Infinity, - gcTime: 1000 * 60 * 60, - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, + select: iconDataUri, }; }), }); diff --git a/packages/app/src/projects/workspace-structure.ts b/packages/app/src/projects/workspace-structure.ts index 6a70f21fb5..939e1381d2 100644 --- a/packages/app/src/projects/workspace-structure.ts +++ b/packages/app/src/projects/workspace-structure.ts @@ -7,6 +7,7 @@ export interface WorkspaceStructureHostPlacement { iconWorkingDir: string; worktreeSupport: "supported" | "unsupported" | "unknown"; customIconRevision?: string | null; + iconRevision?: string; } export interface WorkspaceStructureProject { @@ -160,6 +161,7 @@ function addProjectToView(input: { iconWorkingDir: project.projectRootPath, worktreeSupport: project.projectKind === "git" ? "supported" : "unsupported", customIconRevision: project.projectCustomIconRevision, + iconRevision: project.projectIconRevision, }; const draft = byProject.get(viewKey); if (!draft) { diff --git a/packages/app/src/provider-selection/provider-selection.test.ts b/packages/app/src/provider-selection/provider-selection.test.ts index 4468e2739e..55fef44854 100644 --- a/packages/app/src/provider-selection/provider-selection.test.ts +++ b/packages/app/src/provider-selection/provider-selection.test.ts @@ -3,6 +3,7 @@ import type { AgentModelDefinition, ProviderSnapshotEntry } from "@getpaseo/prot import type { AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest"; import { i18n } from "@/i18n/i18next"; import { + buildProviderQualifiedDescription, buildProviderSelectorProviders, buildSelectableProviderSelectorProviders, buildSelectedTriggerLabel, @@ -246,6 +247,20 @@ describe("combined model selector data", () => { expect(buildSelectedTriggerLabel("GPT-5.4")).toBe("GPT-5.4"); }); + it("names the provider first when a model row is shown outside its provider", () => { + const row = { + favoriteKey: "copilot:claude-opus-5", + provider: "copilot", + providerLabel: "Copilot", + modelId: "claude-opus-5", + modelLabel: "Opus 5", + description: "claude-opus-5", + }; + + expect(buildProviderQualifiedDescription(row)).toBe("Copilot · claude-opus-5"); + expect(buildProviderQualifiedDescription({ ...row, description: undefined })).toBe("Copilot"); + }); + it("resolves selected labels from explicit provider model-selection state", () => { const providers = buildSelectableProviderSelectorProviders([ snapshotEntry({ diff --git a/packages/app/src/provider-selection/provider-selection.ts b/packages/app/src/provider-selection/provider-selection.ts index 8087e0f3d7..2f06860a1d 100644 --- a/packages/app/src/provider-selection/provider-selection.ts +++ b/packages/app/src/provider-selection/provider-selection.ts @@ -6,12 +6,28 @@ import type { } from "@getpaseo/protocol/agent-types"; import type { AgentProviderDefinition } from "@getpaseo/protocol/provider-manifest"; import type { DraftCommandConfig } from "@/hooks/use-agent-commands-query"; -import { buildFavoriteModelKey, type FavoriteModelRow } from "@/hooks/use-form-preferences"; import { i18n } from "@/i18n/i18next"; import { compareMatchScores, scoreTextFields } from "@getpaseo/protocol/search/text-match"; import { filterSelectableModels } from "./model-catalog"; -export type ProviderSelectionModelRow = FavoriteModelRow & { isDefault?: boolean }; +export interface ProviderSelectionModelRow { + /** + * Row identity — `provider:modelId`. Used as the React key and as the row's + * stable handle across list rebuilds. The name is historical; it has nothing + * to do with the removed favourites feature. + */ + favoriteKey: string; + provider: string; + providerLabel: string; + modelId: string; + modelLabel: string; + description?: string; + isDefault?: boolean; +} + +function buildModelRowKey(provider: string, modelId: string): string { + return `${provider}:${modelId}`; +} export type ProviderModelSelection = | { kind: "models"; rows: ProviderSelectionModelRow[] } @@ -44,7 +60,7 @@ function buildModelRows( models: AgentModelDefinition[], ): ProviderSelectionModelRow[] { return models.map((model) => ({ - favoriteKey: buildFavoriteModelKey({ provider, modelId: model.id }), + favoriteKey: buildModelRowKey(provider, model.id), provider, providerLabel, modelId: model.id, @@ -59,7 +75,7 @@ function buildSyntheticDefaultRow( providerLabel: string, ): ProviderSelectionModelRow { return { - favoriteKey: buildFavoriteModelKey({ provider, modelId: "" }), + favoriteKey: buildModelRowKey(provider, ""), provider, providerLabel, modelId: "", @@ -195,6 +211,14 @@ export function buildSelectedTriggerLabel(modelLabel: string): string { return modelLabel; } +/** + * Cross-provider result rows need the provider named up front: the same model + * label ships on several providers at once. + */ +export function buildProviderQualifiedDescription(row: ProviderSelectionModelRow): string { + return row.description ? `${row.providerLabel} · ${row.description}` : row.providerLabel; +} + export function matchesModelSearch( row: ProviderSelectionModelRow, normalizedQuery: string, diff --git a/packages/app/src/provider-selection/resolve-agent-form.test.ts b/packages/app/src/provider-selection/resolve-agent-form.test.ts index 82d0290464..5b6cc2a509 100644 --- a/packages/app/src/provider-selection/resolve-agent-form.test.ts +++ b/packages/app/src/provider-selection/resolve-agent-form.test.ts @@ -47,6 +47,14 @@ const TEST_CLAUDE_DEFINITION: AgentProviderDefinition = { ], }; +const TEST_PI_DEFINITION: AgentProviderDefinition = { + id: "pi", + label: "Pi", + description: "Pi test provider", + defaultModeId: null, + modes: [], +}; + const CODEX_MODELS: AgentModelDefinition[] = [ { provider: "codex", @@ -310,7 +318,6 @@ describe("mergeSelectedComposerPreferences", () => { }, claude: { model: "claude-sonnet-4-6" }, }, - favoriteModels: [{ provider: "codex", modelId: "gpt-5.4-mini" }], }, provider: "codex", updates: { model: "gpt-5.4" }, @@ -326,7 +333,6 @@ describe("mergeSelectedComposerPreferences", () => { }, claude: { model: "claude-sonnet-4-6" }, }, - favoriteModels: [{ provider: "codex", modelId: "gpt-5.4-mini" }], }); }); @@ -972,55 +978,6 @@ describe("resolveAgentForm", () => { }); }); - describe("SET_PROVIDER_FROM_USER", () => { - it("switches provider, picks preferred model and mode, marks provider modified", () => { - const state = makeState(); - const next = resolveAgentForm(state, { - type: "SET_PROVIDER_FROM_USER", - provider: "codex", - providerModels: CODEX_MODELS, - providerDef: TEST_CODEX_DEFINITION, - providerPrefs: { model: "gpt-5.3-codex", mode: "full-access" }, - }); - - expect(next.form.provider).toBe("codex"); - expect(next.form.model).toBe("gpt-5.3-codex"); - expect(next.form.modeId).toBe("full-access"); - expect(next.userModified.provider).toBe(true); - expect(next.userModified.model).toBe(false); - }); - - it("falls back to provider defaults when no prefs", () => { - const state = makeState(); - const next = resolveAgentForm(state, { - type: "SET_PROVIDER_FROM_USER", - provider: "codex", - providerModels: CODEX_MODELS, - providerDef: TEST_CODEX_DEFINITION, - providerPrefs: undefined, - }); - - expect(next.form.modeId).toBe("auto"); - expect(next.form.model).toBe("gpt-5.3-codex"); - }); - - it("canonicalizes an aliased preferred model and restores its thinking option", () => { - const next = resolveAgentForm(makeState(), { - type: "SET_PROVIDER_FROM_USER", - provider: "codex", - providerModels: ALIASED_CODEX_MODELS, - providerDef: TEST_CODEX_DEFINITION, - providerPrefs: { - model: "gpt-5.3-codex-legacy", - thinkingByModel: { "gpt-5.3-codex-legacy": "low" }, - }, - }); - - expect(next.form.model).toBe("gpt-5.3-codex"); - expect(next.form.thinkingOptionId).toBe("low"); - }); - }); - describe("SET_PROVIDER_AND_MODEL_FROM_USER", () => { it("sets provider, model, and default mode; marks both modified", () => { const state = makeState(); @@ -1092,6 +1049,42 @@ describe("resolveAgentForm", () => { }); }); + describe("APPLY_PROFILE_FROM_USER", () => { + it("drops a stale saved mode for a modeless profile provider", () => { + const next = resolveAgentForm(makeState({ provider: "codex", modeId: "full-access" }), { + type: "APPLY_PROFILE_FROM_USER", + provider: "pi", + modelId: "anthropic/sonnet", + modeId: "", + thinkingOptionId: "", + providerDef: TEST_PI_DEFINITION, + providerModels: [{ provider: "pi", id: "anthropic/sonnet", label: "Sonnet" }], + providerPrefs: { mode: "full-access" }, + }); + + expect(next.form).toMatchObject({ + provider: "pi", + model: "anthropic/sonnet", + modeId: "", + }); + }); + + it("restores thinking for the selected model when the profile omits it", () => { + const next = resolveAgentForm(makeState(), { + type: "APPLY_PROFILE_FROM_USER", + provider: "codex", + modelId: "gpt-5.3-codex", + modeId: "full-access", + thinkingOptionId: "", + providerDef: TEST_CODEX_DEFINITION, + providerModels: CODEX_MODELS, + providerPrefs: { thinkingByModel: { "gpt-5.3-codex": "low" } }, + }); + + expect(next.form.thinkingOptionId).toBe("low"); + }); + }); + describe("SET_MODEL_FROM_USER", () => { it("updates model and resets thinking to model default when thinking is not user-modified", () => { const state = makeState({ provider: "codex", model: "", thinkingOptionId: "" }); diff --git a/packages/app/src/provider-selection/resolve-agent-form.ts b/packages/app/src/provider-selection/resolve-agent-form.ts index 1e2b56bab4..9fa187200e 100644 --- a/packages/app/src/provider-selection/resolve-agent-form.ts +++ b/packages/app/src/provider-selection/resolve-agent-form.ts @@ -79,13 +79,6 @@ export type AgentFormAction = } | { type: "SET_SERVER_ID"; value: string | null } | { type: "SET_SERVER_ID_FROM_USER"; value: string | null } - | { - type: "SET_PROVIDER_FROM_USER"; - provider: AgentProvider; - providerModels: AgentModelDefinition[] | null; - providerDef: AgentProviderDefinition | undefined; - providerPrefs: ProviderPrefs | undefined; - } | { type: "SET_PROVIDER_AND_MODEL_FROM_USER"; provider: AgentProvider; @@ -94,6 +87,16 @@ export type AgentFormAction = providerModels: AgentModelDefinition[] | null; providerPrefs?: ProviderPrefs | undefined; } + | { + type: "APPLY_PROFILE_FROM_USER"; + provider: AgentProvider; + modelId: string; + modeId: string; + thinkingOptionId: string; + providerDef: AgentProviderDefinition | undefined; + providerModels: AgentModelDefinition[] | null; + providerPrefs?: ProviderPrefs | undefined; + } | { type: "SET_MODE_FROM_USER"; modeId: string } | { type: "SET_MODEL_FROM_USER"; @@ -109,6 +112,7 @@ export type AgentFormAction = | { type: "RESET" }; type CompleteResolutionAction = Extract; +type ApplyProfileAction = Extract; export function normalizeSelectedModelId(modelId: string | null | undefined): string { return typeof modelId === "string" ? modelId.trim() : ""; @@ -484,20 +488,6 @@ export function resolveFormStateFromProviderModels( ); } -function pickNextModelForProvider(input: { - providerModels: AgentModelDefinition[] | null; - providerPrefs: ProviderPrefs | undefined; -}): string { - const { providerModels, providerPrefs } = input; - const preferredModel = normalizeSelectedModelId(providerPrefs?.model); - const defaultModelId = resolveDefaultModelId(providerModels); - if (preferredModel) { - if (!providerModels) return preferredModel; - return resolveCanonicalModelId(providerModels, preferredModel) || defaultModelId; - } - return defaultModelId; -} - function pickNextModeForProvider(input: { providerDef: AgentProviderDefinition | undefined; providerPrefs: ProviderPrefs | undefined; @@ -586,6 +576,45 @@ function completeResolution( return { ...nextState, form: resolved }; } +function applyProfile(state: AgentFormReducerState, action: ApplyProfileAction) { + const preferredModelId = action.modelId || action.providerPrefs?.model || ""; + const normalizedModelId = resolveCanonicalModelId(action.providerModels, preferredModelId); + const nextModelId = normalizedModelId || resolveDefaultModelId(action.providerModels); + const availableModeIds = new Set(action.providerDef?.modes.map((mode) => mode.id) ?? []); + const preferredModeId = action.modeId || action.providerPrefs?.mode || ""; + const defaultModeId = action.providerDef?.defaultModeId ?? ""; + let nextModeId = ""; + if (availableModeIds.has(preferredModeId)) { + nextModeId = preferredModeId; + } else if (availableModeIds.has(defaultModeId)) { + nextModeId = defaultModeId; + } + const nextThinkingOptionId = + action.thinkingOptionId || + pickNextThinkingOptionForProvider({ + providerModels: action.providerModels, + providerPrefs: action.providerPrefs, + modelId: nextModelId, + }); + return { + ...state, + form: { + ...state.form, + provider: action.provider, + model: nextModelId, + modeId: nextModeId, + thinkingOptionId: nextThinkingOptionId, + }, + userModified: { + ...state.userModified, + provider: true, + model: true, + modeId: true, + thinkingOptionId: true, + }, + }; +} + export function resolveAgentForm( state: AgentFormReducerState, action: AgentFormAction, @@ -611,33 +640,6 @@ export function resolveAgentForm( userModified: { ...state.userModified, serverId: true }, }; - case "SET_PROVIDER_FROM_USER": { - const nextModelId = pickNextModelForProvider({ - providerModels: action.providerModels, - providerPrefs: action.providerPrefs, - }); - const nextModeId = pickNextModeForProvider({ - providerDef: action.providerDef, - providerPrefs: action.providerPrefs, - }); - const nextThinkingOptionId = pickNextThinkingOptionForProvider({ - providerModels: action.providerModels, - providerPrefs: action.providerPrefs, - modelId: nextModelId, - }); - return { - ...state, - form: { - ...state.form, - provider: action.provider, - modeId: nextModeId, - model: nextModelId, - thinkingOptionId: nextThinkingOptionId, - }, - userModified: { ...state.userModified, provider: true }, - }; - } - case "SET_PROVIDER_AND_MODEL_FROM_USER": { const normalizedModelId = resolveCanonicalModelId(action.providerModels, action.modelId); const nextModelId = normalizedModelId || resolveDefaultModelId(action.providerModels); @@ -669,6 +671,10 @@ export function resolveAgentForm( }; } + case "APPLY_PROFILE_FROM_USER": { + return applyProfile(state, action); + } + case "SET_MODE_FROM_USER": return { ...state, diff --git a/packages/app/src/push-notifications/index.native.ts b/packages/app/src/push-notifications/index.native.ts new file mode 100644 index 0000000000..579c9a0738 --- /dev/null +++ b/packages/app/src/push-notifications/index.native.ts @@ -0,0 +1,12 @@ +import { revokeSubscription, startSubscription } from "./internal/subscriptions"; +import type { RevokePushNotificationsInput, StartPushNotificationsInput } from "./internal/types"; + +export function startPushNotifications(input: StartPushNotificationsInput): () => void { + return startSubscription(input); +} + +export function revokePushNotifications(input: RevokePushNotificationsInput): Promise { + return revokeSubscription(input).catch((error) => { + console.warn("[PushNotifications] Failed to remove local push subscription", error); + }); +} diff --git a/packages/app/src/push-notifications/index.ts b/packages/app/src/push-notifications/index.ts new file mode 100644 index 0000000000..9aebb187d6 --- /dev/null +++ b/packages/app/src/push-notifications/index.ts @@ -0,0 +1,9 @@ +import type { RevokePushNotificationsInput, StartPushNotificationsInput } from "./internal/types"; + +export function startPushNotifications(_input: StartPushNotificationsInput): () => void { + return () => undefined; +} + +export async function revokePushNotifications(_input: RevokePushNotificationsInput): Promise { + // Push notifications are native-only. +} diff --git a/packages/app/src/push-notifications/index.web.ts b/packages/app/src/push-notifications/index.web.ts new file mode 100644 index 0000000000..9aebb187d6 --- /dev/null +++ b/packages/app/src/push-notifications/index.web.ts @@ -0,0 +1,9 @@ +import type { RevokePushNotificationsInput, StartPushNotificationsInput } from "./internal/types"; + +export function startPushNotifications(_input: StartPushNotificationsInput): () => void { + return () => undefined; +} + +export async function revokePushNotifications(_input: RevokePushNotificationsInput): Promise { + // Push notifications are native-only. +} diff --git a/packages/app/src/push-notifications/internal/subscriptions.ts b/packages/app/src/push-notifications/internal/subscriptions.ts new file mode 100644 index 0000000000..26df575ebc --- /dev/null +++ b/packages/app/src/push-notifications/internal/subscriptions.ts @@ -0,0 +1,102 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import Constants from "expo-constants"; +import * as Notifications from "expo-notifications"; +import { Platform } from "react-native"; +import { z } from "zod"; +import { readValidatedString } from "@/storage/validated-storage"; +import type { RevokePushNotificationsInput, StartPushNotificationsInput } from "./types"; + +const STORAGE_PREFIX = "@paseo:expo-push-token:"; +const ExpoPushTokenSchema = z.string().trim().min(1); + +function storageKey(serverId: string): string { + return `${STORAGE_PREFIX}${serverId}`; +} + +function getExpoProjectId(): string | null { + const fromEas = ExpoPushTokenSchema.safeParse(Constants.easConfig?.projectId); + if (fromEas.success) return fromEas.data; + const fromExtra = ExpoPushTokenSchema.safeParse(Constants.expoConfig?.extra?.eas?.projectId); + return fromExtra.success ? fromExtra.data : null; +} + +async function ensurePushPermission(): Promise { + const existing = await Notifications.getPermissionsAsync(); + if (existing.status === Notifications.PermissionStatus.GRANTED) return true; + if (!existing.canAskAgain) return false; + const requested = await Notifications.requestPermissionsAsync(); + return requested.status === Notifications.PermissionStatus.GRANTED; +} + +async function resolveToken(serverId: string): Promise { + const key = storageKey(serverId); + const cached = await readValidatedString(AsyncStorage, key, ExpoPushTokenSchema); + if (!(await ensurePushPermission())) { + await AsyncStorage.removeItem(key); + return null; + } + + if (Platform.OS === "android") { + await Notifications.setNotificationChannelAsync("default", { + name: "default", + importance: Notifications.AndroidImportance.DEFAULT, + }); + } + + const projectId = getExpoProjectId(); + if (!projectId) { + console.warn("[PushNotifications] Missing EAS projectId; cannot fetch Expo push token"); + return cached; + } + + const result = await Notifications.getExpoPushTokenAsync({ projectId }); + const token = result.data.trim(); + if (!token) return cached; + await AsyncStorage.setItem(key, token); + return token; +} + +export function startSubscription(input: StartPushNotificationsInput): () => void { + let stopped = false; + let token: string | null = null; + const register = () => { + if (!stopped && token && input.client.isConnected) { + input.client.registerPushToken(token); + } + }; + + void resolveToken(input.serverId) + .then((resolved) => { + if (stopped) return undefined; + token = resolved; + register(); + return undefined; + }) + .catch((error) => console.warn("[PushNotifications] Failed to register push token", error)); + + const unsubscribe = input.client.subscribeConnectionStatus((state) => { + if (state.status === "connected") register(); + }); + + return () => { + stopped = true; + unsubscribe(); + }; +} + +export async function revokeSubscription(input: RevokePushNotificationsInput): Promise { + const key = storageKey(input.serverId); + const token = await readValidatedString(AsyncStorage, key, ExpoPushTokenSchema); + if ( + token && + input.client?.isConnected && + input.client.getLastServerInfoMessage()?.features?.pushTokenRevocation === true + ) { + try { + await input.client.unregisterPushToken(token); + } catch (error) { + console.warn("[PushNotifications] Failed to revoke push token", error); + } + } + await AsyncStorage.removeItem(key); +} diff --git a/packages/app/src/push-notifications/internal/types.ts b/packages/app/src/push-notifications/internal/types.ts new file mode 100644 index 0000000000..6256c298aa --- /dev/null +++ b/packages/app/src/push-notifications/internal/types.ts @@ -0,0 +1,11 @@ +import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; + +export interface StartPushNotificationsInput { + client: DaemonClient; + serverId: string; +} + +export interface RevokePushNotificationsInput { + client: DaemonClient | null; + serverId: string; +} diff --git a/packages/app/src/review/state.ts b/packages/app/src/review/state.ts index dff34ff038..1b5057a648 100644 --- a/packages/app/src/review/state.ts +++ b/packages/app/src/review/state.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + export type ReviewDraftMode = "uncommitted" | "base"; export type ReviewDraftSide = "old" | "new"; @@ -30,8 +32,27 @@ export interface ReviewDraftStoreState { // Only drafts are persisted; diffModeOverrides is intentionally excluded. export interface SerializedReviewDraftState { drafts: Record; + activeModesByScope?: Record; } +const IsoDateTimeSchema = z.string().datetime({ offset: true }); +export const ReviewDraftCommentSchema: z.ZodType = z.strictObject({ + id: z.string(), + filePath: z.string(), + side: z.enum(["old", "new"]), + lineNumber: z.number().int().positive(), + body: z.string(), + createdAt: IsoDateTimeSchema, + updatedAt: IsoDateTimeSchema, +}); + +export const SerializedReviewDraftStateSchema: z.ZodType = + z.strictObject({ + drafts: z.record(z.string(), z.array(ReviewDraftCommentSchema)), + // COMPAT(reviewDraftModes): v1 persisted this field; v2 discards it during migration. + activeModesByScope: z.record(z.string(), z.enum(["uncommitted", "base"])).optional(), + }); + export function setDiffModeOverrideInState( state: ReviewDraftStoreState, input: { scopeKey: string; override: DiffModeOverride }, @@ -160,45 +181,11 @@ export function serializeReviewDraftState( } export function normalizePersistedState(state: unknown): ReviewDraftStoreState { - if (!state || typeof state !== "object") { - return { drafts: {}, diffModeOverrides: {} }; - } - // activeModesByScope may be present in old persisted JSON — tolerate and ignore it. - const persisted = state as { drafts?: unknown }; - const drafts = persisted.drafts; - if (!drafts || typeof drafts !== "object" || Array.isArray(drafts)) { - return { drafts: {}, diffModeOverrides: {} }; - } - - const normalized: Record = {}; - for (const [key, value] of Object.entries(drafts)) { - if (!Array.isArray(value)) { - continue; - } - normalized[key] = value.filter((comment): comment is ReviewDraftComment => - isReviewDraftComment(comment), - ); - } - - return { drafts: normalized, diffModeOverrides: {} }; -} - -export function isReviewDraftComment(value: unknown): value is ReviewDraftComment { - if (!value || typeof value !== "object") { - return false; - } - const record = value as Record; - return ( - typeof record.id === "string" && - typeof record.filePath === "string" && - (record.side === "old" || record.side === "new") && - typeof record.lineNumber === "number" && - Number.isInteger(record.lineNumber) && - record.lineNumber > 0 && - typeof record.body === "string" && - typeof record.createdAt === "string" && - typeof record.updatedAt === "string" - ); + const result = SerializedReviewDraftStateSchema.safeParse(state); + return { + drafts: result.success ? result.data.drafts : {}, + diffModeOverrides: {}, + }; } function applyCommentUpdates( diff --git a/packages/app/src/review/store.test.ts b/packages/app/src/review/store.test.ts index 62e68f0152..8c8dea9463 100644 --- a/packages/app/src/review/store.test.ts +++ b/packages/app/src/review/store.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from "vitest"; +import type { StateStorage } from "zustand/middleware"; import type { ParsedDiffFile } from "@/git/use-diff-query"; +import { createValidatedPersistStorage } from "@/storage/validated-persist-storage"; import { buildReviewAttachmentSnapshot, buildReviewDraftKey, @@ -16,6 +18,7 @@ import { type ReviewDraftComment, type ReviewDraftStoreState, serializeReviewDraftState, + SerializedReviewDraftStateSchema, setDiffModeOverrideInState, updateCommentInState, } from "./state"; @@ -69,6 +72,20 @@ function makeFile(): ParsedDiffFile { }; } +function createMemoryStorage(): StateStorage & { values: Map } { + const values = new Map(); + return { + values, + getItem: async (key) => values.get(key) ?? null, + setItem: async (key, value) => { + values.set(key, value); + }, + removeItem: async (key) => { + values.delete(key); + }, + }; +} + describe("buildReviewDraftKey", () => { it("scopes by server, workspace-or-cwd, diff mode, base ref, and whitespace mode", () => { const base = buildReviewDraftKey({ @@ -122,7 +139,27 @@ describe("buildReviewDraftKey", () => { }); describe("normalizePersistedState", () => { - it("filters invalid draft comments and ignores activeModesByScope from old persisted JSON", () => { + it("keeps v1 review comments for migration while dropping the legacy mode field", async () => { + const backing = createMemoryStorage(); + const legacyState = { + drafts: { "review:key": [makeComment()] }, + activeModesByScope: { "review:scope": "base" }, + }; + backing.values.set( + "@paseo:review-draft-store", + JSON.stringify({ state: legacyState, version: 1 }), + ); + const storage = createValidatedPersistStorage(backing, SerializedReviewDraftStateSchema); + + const stored = await storage.getItem("@paseo:review-draft-store"); + const normalized = normalizePersistedState(stored?.state); + + expect(normalized.drafts["review:key"]).toEqual([makeComment()]); + expect(normalized.diffModeOverrides).toEqual({}); + expect(backing.values.has("@paseo:review-draft-store")).toBe(true); + }); + + it("rejects the complete payload when any draft comment or field is invalid", () => { const normalized = normalizePersistedState({ drafts: { "review:key": [ @@ -146,17 +183,7 @@ describe("normalizePersistedState", () => { }); expect(normalized.diffModeOverrides).toEqual({}); - expect(normalized.drafts["review:key"]).toEqual([ - { - id: "comment-1", - filePath: "src/example.ts", - side: "new", - lineNumber: 41, - body: "Keep me.", - createdAt: "2026-04-21T00:00:00.000Z", - updatedAt: "2026-04-21T00:00:00.000Z", - }, - ]); + expect(normalized.drafts).toEqual({}); }); it("returns empty state for null, non-object, or malformed inputs", () => { diff --git a/packages/app/src/review/store.ts b/packages/app/src/review/store.ts index d1cc5e16b3..3f9d7c55ea 100644 --- a/packages/app/src/review/store.ts +++ b/packages/app/src/review/store.ts @@ -1,7 +1,7 @@ import { useMemo } from "react"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { create } from "zustand"; -import { createJSONStorage, persist } from "zustand/middleware"; +import { persist } from "zustand/middleware"; import type { ComposerAttachment } from "@/attachments/types"; import type { ParsedDiffFile } from "@/git/use-diff-query"; import { @@ -17,12 +17,14 @@ import { type ReviewDraftSide, type ReviewDraftStoreState, serializeReviewDraftState, + SerializedReviewDraftStateSchema, setDiffModeOverrideInState, updateCommentInState, } from "@/review/state"; import { generateMessageId } from "@/types/stream"; import { buildNumberedDiffHunks, type NumberedDiffLine } from "@/utils/diff-layout"; import type { AgentAttachment } from "@getpaseo/protocol/messages"; +import { createValidatedPersistStorage } from "@/storage/validated-persist-storage"; export type { DiffModeOverride, @@ -168,7 +170,7 @@ export const useReviewDraftStore = create()( { name: "@paseo:review-draft-store", version: STORE_VERSION, - storage: createJSONStorage(() => AsyncStorage), + storage: createValidatedPersistStorage(AsyncStorage, SerializedReviewDraftStateSchema), partialize: (state) => serializeReviewDraftState(state), migrate: async (state) => normalizePersistedState(state), }, diff --git a/packages/app/src/runtime/directory-sync/agent-replica.test.ts b/packages/app/src/runtime/directory-sync/agent-replica.test.ts index 4456ecc0a8..d798282aa8 100644 --- a/packages/app/src/runtime/directory-sync/agent-replica.test.ts +++ b/packages/app/src/runtime/directory-sync/agent-replica.test.ts @@ -51,6 +51,31 @@ function entry(agent: AgentSnapshotPayload): FetchAgentsEntry { } describe("AgentDirectoryReplica", () => { + it("preserves an unchanged running agent's turn identity during catch-up", () => { + const serverId = "agent-replica-catch-up"; + const store = useSessionStore.getState(); + store.initializeSession(serverId, null as unknown as DaemonClient); + const replica = new AgentDirectoryReplica(serverId, () => undefined); + replica.commitSnapshot( + [ + entry({ + ...payload("running"), + status: "running", + activeTurn: { turnId: "turn-1", startedAt: "2026-07-17T00:01:00.000Z" }, + }), + ], + [], + ); + + replica.commitChanges([], [], []); + + expect(useSessionStore.getState().sessions[serverId]?.agents.get("agent")?.activeTurn).toEqual({ + turnId: "turn-1", + startedAt: new Date("2026-07-17T00:01:00.000Z"), + }); + store.clearSession(serverId); + }); + it("keeps membership authoritative across remove, stale timeline, and re-add", () => { const serverId = "agent-replica"; const store = useSessionStore.getState(); diff --git a/packages/app/src/runtime/directory-sync/agent-replica.ts b/packages/app/src/runtime/directory-sync/agent-replica.ts index 62de843cdc..7f90998e5b 100644 --- a/packages/app/src/runtime/directory-sync/agent-replica.ts +++ b/packages/app/src/runtime/directory-sync/agent-replica.ts @@ -3,7 +3,7 @@ import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages"; import { clearArchiveAgentPending } from "@/hooks/use-archive-agent"; import { queryClient } from "@/data/query-client"; import { useSessionStore, type Agent } from "@/stores/session-store"; -import { normalizeAgentSnapshot } from "@/utils/agent-snapshots"; +import { normalizeAgentSnapshot, projectAgentSnapshot } from "@/utils/agent-snapshots"; import { applyAgentDirectoryDelta, type AgentDirectoryDelta, @@ -15,6 +15,12 @@ import { import { reconcileAgentDirectory } from "@/utils/agent-directory-reconciliation"; import { applyLegacyDaemonWorkspaceOwnership } from "@/workspace/legacy-daemon-workspaces"; +function projectAgentDirectoryEntry(agent: Agent): FetchAgentsEntry | null { + return agent.projectPlacement + ? { agent: projectAgentSnapshot(agent), project: agent.projectPlacement } + : null; +} + export interface AgentLifecycleToken { readonly agentId: string; readonly version: number; @@ -97,6 +103,25 @@ export class AgentDirectoryReplica { return agents; } + commitChanges( + entries: FetchAgentsEntry[], + removals: readonly { id: string }[], + deltas: readonly AgentDirectoryDelta[], + ): Map { + const previous = useSessionStore.getState().sessions[this.serverId]?.agents ?? new Map(); + const merged = new Map(); + for (const agent of previous.values()) { + const entry = projectAgentDirectoryEntry(agent); + if (entry) merged.set(agent.id, entry); + } + for (const entry of entries) merged.set(entry.agent.id, entry); + const removalsAsDeltas: AgentDirectoryDelta[] = removals.map(({ id }) => ({ + kind: "remove", + agentId: id, + })); + return this.commitSnapshot(Array.from(merged.values()), [...removalsAsDeltas, ...deltas]); + } + archive(agentId: string, archivedAt: string): void { this.advance(agentId); useSessionStore.getState().setAgents(this.serverId, (current) => { diff --git a/packages/app/src/runtime/directory-sync/index.test.ts b/packages/app/src/runtime/directory-sync/index.test.ts index fc9368fae0..9725170d9f 100644 --- a/packages/app/src/runtime/directory-sync/index.test.ts +++ b/packages/app/src/runtime/directory-sync/index.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it } from "vitest"; import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import type { SessionOutboundMessage } from "@getpaseo/protocol/messages"; -import { useSessionStore } from "@/stores/session-store"; +import { normalizeProjectDescriptor, useSessionStore } from "@/stores/session-store"; import { DirectoryRefreshSupersededError, DirectorySync } from "./index"; type WorkspaceFetchResult = Awaited>; @@ -9,8 +9,11 @@ type ProjectListResult = Awaited>; class FakeDirectoryClient { fetchAgentsCalls = 0; + lastAgentOptions: unknown; fetchWorkspacesCalls = 0; listProjectsCalls = 0; + lastProjectOptions: unknown; + projectResult: ProjectListResult | null = null; private pendingWorkspaceFetch: Promise | null = null; private readonly handlers = new Map< SessionOutboundMessage["type"], @@ -42,8 +45,9 @@ class FakeDirectoryClient { return complete; } - async fetchAgents(): Promise>> { + async fetchAgents(options?: unknown): Promise>> { this.fetchAgentsCalls += 1; + this.lastAgentOptions = options; return { requestId: "agents", entries: [], @@ -66,8 +70,10 @@ class FakeDirectoryClient { }; } - async listProjects(): Promise { + async listProjects(options?: unknown): Promise { this.listProjectsCalls += 1; + this.lastProjectOptions = options; + if (this.projectResult) return this.projectResult; return { requestId: "projects", projects: [ @@ -111,6 +117,52 @@ afterEach(() => { }); describe("DirectorySync session readiness", () => { + it("uses the saved agent sequence while bounding the bootstrap page", async () => { + const serverId = "agent-list-sequence-page"; + serverIds.add(serverId); + const client = new FakeDirectoryClient(); + const directory = new DirectorySync( + serverId, + { + onAgentStoppedRunning: () => undefined, + markAgentLoading: () => undefined, + markAgentReady: () => undefined, + markAgentError: () => undefined, + }, + { + readDirectoryCheckpoint: () => ({ agents: { generation: "generation", afterSeq: 12 } }), + writeDirectoryCheckpoint: () => undefined, + }, + ); + directory.connectionChanged({ + client: client as unknown as DaemonClient, + status: "online", + source: { clientGeneration: 1, connectionEpoch: 1 }, + }); + const store = useSessionStore.getState(); + store.initializeSession(serverId, client as unknown as DaemonClient, 1); + store.updateSessionServerInfo(serverId, { + serverId, + hostname: null, + version: "test", + features: { directorySync: true, workspaceMultiplicity: true }, + }); + + await directory.refreshAgents({ + subscribe: { subscriptionId: `app:${serverId}` }, + page: { limit: 200 }, + }); + + expect(client.lastAgentOptions).toEqual({ + scope: "active", + sort: [{ key: "updated_at", direction: "desc" }], + subscribe: { subscriptionId: `app:${serverId}` }, + page: { limit: 200 }, + sync: { generation: "generation", afterSeq: 12 }, + }); + directory.dispose(); + }); + it("waits for workspace capability metadata before choosing the workspace protocol", async () => { const serverId = "workspace-metadata"; const { client, directory } = createDirectory(serverId); @@ -159,6 +211,82 @@ describe("DirectorySync session readiness", () => { directory.dispose(); }); + it("merges project changes from the existing list RPC and advances its cursor", async () => { + const serverId = "project-list-sequence"; + serverIds.add(serverId); + const client = new FakeDirectoryClient(); + const writes: unknown[] = []; + const directory = new DirectorySync( + serverId, + { + onAgentStoppedRunning: () => undefined, + markAgentLoading: () => undefined, + markAgentReady: () => undefined, + markAgentError: () => undefined, + }, + { + readDirectoryCheckpoint: () => ({ projects: { generation: "generation", afterSeq: 4 } }), + writeDirectoryCheckpoint: (_serverId, checkpoint) => writes.push(checkpoint), + }, + ); + directory.connectionChanged({ + client: client as unknown as DaemonClient, + status: "online", + source: { clientGeneration: 1, connectionEpoch: 1 }, + }); + const store = useSessionStore.getState(); + store.initializeSession(serverId, client as unknown as DaemonClient, 1); + store.setProjects(serverId, [ + normalizeProjectDescriptor({ + projectId: "project-1", + projectDisplayName: "Old name", + projectRootPath: "/repo/one", + projectKind: "git", + }), + normalizeProjectDescriptor({ + projectId: "project-2", + projectDisplayName: "Removed", + projectRootPath: "/repo/two", + projectKind: "git", + }), + ]); + store.updateSessionServerInfo(serverId, { + serverId, + hostname: null, + version: "test", + features: { workspaceMultiplicity: true, projectList: true, directorySync: true }, + }); + client.projectResult = { + requestId: "projects", + projects: [ + { + projectId: "project-1", + projectDisplayName: "New name", + projectRootPath: "/repo/one", + projectKind: "git", + syncSeq: 5, + }, + ], + sync: { + generation: "generation", + headSeq: 6, + mode: "changes", + removals: [{ id: "project-2", seq: 6 }], + }, + }; + + await directory.refreshWorkspaces(); + + expect(client.lastProjectOptions).toEqual({ + sync: { generation: "generation", afterSeq: 4 }, + }); + const projects = useSessionStore.getState().sessions[serverId]?.projects; + expect(Array.from(projects?.keys() ?? [])).toEqual(["project-1"]); + expect(projects?.get("project-1")?.projectDisplayName).toBe("New name"); + expect(writes).toContainEqual({ projects: { generation: "generation", afterSeq: 6 } }); + directory.dispose(); + }); + it("rejects a session wait on disconnect so the reconnect can refresh", async () => { const serverId = "session-wait-reconnect"; const { client, directory } = createDirectory(serverId); diff --git a/packages/app/src/runtime/directory-sync/index.ts b/packages/app/src/runtime/directory-sync/index.ts index bfb2f93a6b..ac80a81bd9 100644 --- a/packages/app/src/runtime/directory-sync/index.ts +++ b/packages/app/src/runtime/directory-sync/index.ts @@ -36,10 +36,41 @@ const AGENT_SORT: NonNullable = [ { key: "updated_at", direction: "desc" }, ]; +function resolveAgentNextPage(pageInfo: AgentPageInfo): { + hasMore: boolean; + nextCursor: string | null; +} { + return { + hasMore: pageInfo.hasMore ?? pageInfo.hasMoreAfter ?? false, + nextCursor: pageInfo.nextCursor ?? pageInfo.afterCursor ?? null, + }; +} + interface AgentSnapshot { entries: FetchAgentsEntry[]; subscriptionId: string | null; legacy: boolean; + syncMode?: "snapshot" | "changes"; + syncCursor?: DirectoryCursor; + syncRemovals: Array<{ id: string; seq: number }>; +} + +interface AgentPageInfo { + hasMore?: boolean; + hasMoreAfter?: boolean; + nextCursor?: string | null; + afterCursor?: string | null; +} + +interface DirectoryCursor { + generation: string; + afterSeq: number; +} + +interface DirectoryCursors { + projects?: DirectoryCursor; + workspaces?: DirectoryCursor; + agents?: DirectoryCursor; } export interface DirectoryConnection { @@ -48,6 +79,11 @@ export interface DirectoryConnection { source: DirectorySourceToken; } +export interface DirectoryCheckpointStorage { + readDirectoryCheckpoint(serverId: string): unknown; + writeDirectoryCheckpoint(serverId: string, checkpoint: unknown): void; +} + export interface RefreshAgentDirectoryInput { filter?: FetchAgentsOptions["filter"]; subscribe?: FetchAgentsOptions["subscribe"]; @@ -86,6 +122,7 @@ export class DirectorySync { markAgentReady: () => void; markAgentError: (error: string) => void; }, + private readonly checkpoints?: DirectoryCheckpointStorage, ) { this.agents = new AgentDirectoryReplica(serverId, callbacks.onAgentStoppedRunning); this.workspaces = new WorkspaceDirectoryReplica(serverId); @@ -116,19 +153,26 @@ export class DirectorySync { const subscriptions = [ client.on("agent_update", (message) => { if (message.type !== "agent_update" || !this.isCurrent(client, source)) return; - if (!this.agentTransactions.record(source, message.payload)) + const recorded = this.agentTransactions.record(source, message.payload); + if (!recorded) { this.agents.applyDelta(message.payload); + this.noteLiveCursor("agents", message.payload); + } }), client.on("workspace_update", (message) => { if (message.type !== "workspace_update" || !this.isCurrent(client, source)) return; - if (!this.workspaceTransactions.record(source, message.payload)) { + const recorded = this.workspaceTransactions.record(source, message.payload); + if (!recorded) { this.workspaces.applyDelta(message.payload); + this.noteLiveCursor("workspaces", message.payload); } }), client.on("project.update", (message) => { if (message.type !== "project.update" || !this.isCurrent(client, source)) return; - if (!this.workspaceTransactions.record(source, message.payload)) { + const recorded = this.workspaceTransactions.record(source, message.payload); + if (!recorded) { this.workspaces.applyDelta(message.payload); + this.noteLiveCursor("projects", message.payload); } }), client.on("agent_deleted", (message) => { @@ -174,6 +218,7 @@ export class DirectorySync { entries: [], subscriptionId: null, legacy: false, + syncRemovals: [], })); this.callbacks.markAgentLoading(); try { @@ -222,7 +267,8 @@ export class DirectorySync { : delta, ) : completion.deltas; - const agents = this.agents.commitSnapshot(completion.snapshot.entries, deltas); + const agents = this.commitAgentSnapshot(completion.snapshot, deltas); + this.persistAgentCursors(completion.snapshot, completion.deltas); this.callbacks.markAgentReady(); return { agents, subscriptionId: completion.snapshot.subscriptionId }; } catch (error) { @@ -238,8 +284,9 @@ export class DirectorySync { async refreshWorkspaces(input?: { subscribe?: boolean }): Promise { const { client, source } = this.requireOnline(); const transaction = this.workspaceTransactions.begin(source, () => ({ - workspaces: new Map(), - projects: new Map(), + workspaces: new Map(useSessionStore.getState().sessions[this.serverId]?.workspaces), + projects: new Map(useSessionStore.getState().sessions[this.serverId]?.projects), + syncCursors: {}, })); try { await this.waitForSessionMetadata(client, source); @@ -250,24 +297,19 @@ export class DirectorySync { return; } const supportsProjectList = serverInfo.features?.projectList === true; + const supportsDirectorySync = serverInfo.features?.directorySync === true; if (supportsProjectList) { - const payload = await client.listProjects(); - this.assertWorkspaceTransactionCurrent(client, source, transaction); - for (const entry of payload.projects) { - const project = normalizeProjectDescriptor(entry); - transaction.snapshot.projects.set(project.projectId, project); - } + await this.fetchProjectSnapshot(client, source, transaction, supportsDirectorySync); } - await this.fetchWorkspaceSnapshot(client, source, transaction, input?.subscribe === true); + await this.fetchWorkspaceSnapshot( + client, + source, + transaction, + input?.subscribe === true, + supportsDirectorySync, + ); if (!supportsProjectList) this.buildLegacyProjectSnapshot(transaction.snapshot); - if (!this.isCurrent(client, source) || !this.hasMatchingSession(client, source)) { - throw new DirectoryRefreshSupersededError("workspace completion no longer current"); - } - const completion = this.workspaceTransactions.complete(transaction); - if (completion.kind === "stale") { - throw new DirectoryRefreshSupersededError("workspace completion was superseded"); - } - this.workspaces.commitSnapshot(completion.snapshot, completion.deltas); + this.completeWorkspaceRefresh(client, source, transaction); } catch (error) { const deltas = this.workspaceTransactions.fail(transaction); if (deltas) for (const delta of deltas) this.workspaces.applyDelta(delta); @@ -280,6 +322,7 @@ export class DirectorySync { source: DirectorySourceToken, transaction: DirectoryTransaction, initialSubscribe: boolean, + supportsDirectorySync: boolean, ): Promise { let cursor: string | null = null; let subscribe = initialSubscribe; @@ -288,12 +331,23 @@ export class DirectorySync { sort: [{ key: "activity_at", direction: "desc" }], ...(subscribe ? { subscribe: {} } : {}), page: cursor ? { limit: PAGE_LIMIT, cursor } : { limit: PAGE_LIMIT }, + ...(supportsDirectorySync ? { sync: this.readCursors().workspaces ?? {} } : {}), }); this.assertWorkspaceTransactionCurrent(client, source, transaction); + if (payload.sync?.mode === "snapshot") transaction.snapshot.workspaces.clear(); for (const entry of payload.entries) { const workspace = normalizeWorkspaceDescriptor(entry); transaction.snapshot.workspaces.set(workspace.id, workspace); } + for (const removal of payload.sync?.removals ?? []) { + transaction.snapshot.workspaces.delete(removal.id); + } + if (payload.sync) { + (transaction.snapshot.syncCursors ??= {}).workspaces = { + generation: payload.sync.generation, + afterSeq: payload.sync.headSeq, + }; + } for (const entry of payload.emptyProjects ?? []) { const project = normalizeProjectDescriptor(entry); transaction.snapshot.projects.set(project.projectId, project); @@ -332,18 +386,15 @@ export class DirectorySync { sort: AGENT_SORT, ...(subscribe ? { subscribe } : {}), page: cursor ? { limit, cursor } : { limit }, + ...(!input.filter && cursor === null && this.supportsDirectorySync() + ? { sync: this.readCursors().agents ?? {} } + : {}), }); this.assertAgentTransactionCurrent(client, source, transaction); transaction.snapshot.entries.push(...payload.entries); transaction.snapshot.subscriptionId ??= payload.subscriptionId ?? null; - const pageInfo = payload.pageInfo as { - hasMore?: boolean; - hasMoreAfter?: boolean; - nextCursor?: string | null; - afterCursor?: string | null; - }; - const hasMore = pageInfo.hasMore ?? pageInfo.hasMoreAfter ?? false; - const nextCursor = pageInfo.nextCursor ?? pageInfo.afterCursor ?? null; + this.recordAgentSync(transaction.snapshot, payload.sync); + const { hasMore, nextCursor } = resolveAgentNextPage(payload.pageInfo as AgentPageInfo); if (!hasMore || !nextCursor) break; cursor = nextCursor; subscribe = undefined; @@ -377,6 +428,122 @@ export class DirectorySync { return { client: this.connection.client, source: this.connection.source }; } + private supportsDirectorySync(): boolean { + return ( + useSessionStore.getState().sessions[this.serverId]?.serverInfo?.features?.directorySync === + true + ); + } + + private commitAgentSnapshot( + snapshot: AgentSnapshot, + deltas: readonly AgentDirectoryDelta[], + ): Map { + return snapshot.syncMode === "changes" + ? this.agents.commitChanges(snapshot.entries, snapshot.syncRemovals, deltas) + : this.agents.commitSnapshot(snapshot.entries, deltas); + } + + private persistAgentCursors( + snapshot: AgentSnapshot, + deltas: readonly AgentDirectoryDelta[], + ): void { + if (snapshot.syncCursor) this.writeCursor("agents", snapshot.syncCursor); + for (const delta of deltas) this.noteLiveCursor("agents", delta); + } + + private async fetchProjectSnapshot( + client: DaemonClient, + source: DirectorySourceToken, + transaction: DirectoryTransaction, + supportsDirectorySync: boolean, + ): Promise { + const payload = await client.listProjects( + supportsDirectorySync ? { sync: this.readCursors().projects ?? {} } : undefined, + ); + this.assertWorkspaceTransactionCurrent(client, source, transaction); + if (payload.sync?.mode === "snapshot") transaction.snapshot.projects.clear(); + for (const entry of payload.projects) { + const project = normalizeProjectDescriptor(entry); + transaction.snapshot.projects.set(project.projectId, project); + } + for (const removal of payload.sync?.removals ?? []) { + transaction.snapshot.projects.delete(removal.id); + } + if (payload.sync) { + (transaction.snapshot.syncCursors ??= {}).projects = { + generation: payload.sync.generation, + afterSeq: payload.sync.headSeq, + }; + } + } + + private completeWorkspaceRefresh( + client: DaemonClient, + source: DirectorySourceToken, + transaction: DirectoryTransaction, + ): void { + if (!this.isCurrent(client, source) || !this.hasMatchingSession(client, source)) { + throw new DirectoryRefreshSupersededError("workspace completion no longer current"); + } + const completion = this.workspaceTransactions.complete(transaction); + if (completion.kind === "stale") { + throw new DirectoryRefreshSupersededError("workspace completion was superseded"); + } + this.workspaces.commitSnapshot(completion.snapshot, completion.deltas); + for (const [entity, cursor] of Object.entries(completion.snapshot.syncCursors ?? {})) { + if (cursor) this.writeCursor(entity as "projects" | "workspaces", cursor); + } + for (const delta of completion.deltas) { + const entity = "projectId" in delta || "project" in delta ? "projects" : "workspaces"; + this.noteLiveCursor(entity, delta); + } + } + + private recordAgentSync( + snapshot: AgentSnapshot, + sync: Awaited>["sync"], + ): void { + if (!sync) return; + snapshot.syncMode = sync.mode; + snapshot.syncRemovals.push(...sync.removals); + snapshot.syncCursor = { generation: sync.generation, afterSeq: sync.headSeq }; + } + + private noteLiveCursor( + entity: keyof DirectoryCursors, + payload: { generation?: string; seq?: number }, + ): void { + if (!payload.generation || payload.seq === undefined) return; + this.writeCursor(entity, { + generation: payload.generation, + afterSeq: payload.seq, + }); + } + + private readCursors(): DirectoryCursors { + const value = this.checkpoints?.readDirectoryCheckpoint(this.serverId); + if (!value || typeof value !== "object" || Array.isArray(value)) return {}; + const cursors: DirectoryCursors = {}; + for (const entity of ["projects", "workspaces", "agents"] as const) { + const cursor = (value as Record)[entity]; + if (!cursor || typeof cursor !== "object" || Array.isArray(cursor)) continue; + const { generation, afterSeq } = cursor as Record; + if (typeof generation === "string" && typeof afterSeq === "number" && afterSeq >= 0) { + cursors[entity] = { generation, afterSeq }; + } + } + return cursors; + } + + private writeCursor(entity: keyof DirectoryCursors, cursor: DirectoryCursor): void { + if (!this.checkpoints) return; + const current = this.readCursors(); + const previous = current[entity]; + if (previous?.generation === cursor.generation && previous.afterSeq >= cursor.afterSeq) return; + this.checkpoints.writeDirectoryCheckpoint(this.serverId, { ...current, [entity]: cursor }); + } + private isCurrent(client: DaemonClient, source: DirectorySourceToken): boolean { return ( this.connection.client === client && diff --git a/packages/app/src/runtime/directory-sync/workspace-replica.ts b/packages/app/src/runtime/directory-sync/workspace-replica.ts index 0d940724bb..b64b5b14c1 100644 --- a/packages/app/src/runtime/directory-sync/workspace-replica.ts +++ b/packages/app/src/runtime/directory-sync/workspace-replica.ts @@ -21,6 +21,9 @@ type ProjectDirectoryDelta = Extract; projects: Map; + syncCursors?: Partial< + Record<"projects" | "workspaces", { generation: string; afterSeq: number }> + >; } function applyProjectDelta( diff --git a/packages/app/src/runtime/host-features.ts b/packages/app/src/runtime/host-features.ts index e513c33647..37453fecb2 100644 --- a/packages/app/src/runtime/host-features.ts +++ b/packages/app/src/runtime/host-features.ts @@ -51,3 +51,22 @@ export function useHostFeatureMap( [flags, serverIds], ); } + +export function useHostFeatureAvailabilityMap( + serverIds: readonly string[], + feature: HostFeatureName, +): ReadonlyMap { + const flags = useSessionStore( + useShallow((state) => + serverIds.map((serverId) => { + const serverInfo = state.sessions[serverId]?.serverInfo; + return serverInfo ? hostSupportsFeature(serverInfo, feature) : null; + }), + ), + ); + + return useMemo( + () => new Map(serverIds.map((serverId, index) => [serverId, flags[index]] as const)), + [flags, serverIds], + ); +} diff --git a/packages/app/src/runtime/host-runtime.test.ts b/packages/app/src/runtime/host-runtime.test.ts index 697872f095..9fc711d683 100644 --- a/packages/app/src/runtime/host-runtime.test.ts +++ b/packages/app/src/runtime/host-runtime.test.ts @@ -445,6 +445,9 @@ function createMemoryHostRuntimeStorage(entries: Record = {}): H setItem: async (key, value) => { values.set(key, value); }, + removeItem: async (key) => { + values.delete(key); + }, }; } @@ -1384,6 +1387,54 @@ describe("HostRuntimeController", () => { }); describe("HostRuntimeStore", () => { + it("revokes push notifications before removing a host", async () => { + const host = makeHost({ connections: [makeHost().connections[0]!] }); + const revocation = createDeferred(); + const storage = createMemoryHostRuntimeStorage({ + "@paseo:daemon-registry": JSON.stringify([host]), + "@paseo:e2e": "1", + }); + const store = new HostRuntimeStore({ + storage, + deps: makeDeps({}, []), + revokePushNotifications: async ({ serverId }) => { + expect(serverId).toBe(host.serverId); + expect(store.getHosts().map((candidate) => candidate.serverId)).toEqual([host.serverId]); + await revocation.promise; + }, + }); + await store.boot(); + + const removal = store.removeHost(host.serverId); + expect(store.getHosts().map((candidate) => candidate.serverId)).toEqual([host.serverId]); + revocation.resolve(); + await removal; + + expect(store.getHosts()).toEqual([]); + }); + + it("revokes push notifications when removing a host's final connection", async () => { + const host = makeHost({ connections: [makeHost().connections[0]!] }); + const revokedServerIds: string[] = []; + const storage = createMemoryHostRuntimeStorage({ + "@paseo:daemon-registry": JSON.stringify([host]), + "@paseo:e2e": "1", + }); + const store = new HostRuntimeStore({ + storage, + deps: makeDeps({}, []), + revokePushNotifications: async ({ serverId }) => { + revokedServerIds.push(serverId); + }, + }); + await store.boot(); + + await store.removeConnection(host.serverId, host.connections[0]!.id); + + expect(revokedServerIds).toEqual([host.serverId]); + expect(store.getHosts()).toEqual([]); + }); + it("restores the display replica before declaring the host registry loaded", async () => { const host = makeHost(); const storage = createMemoryHostRuntimeStorage(); diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts index 982d411e46..86ee71c73f 100644 --- a/packages/app/src/runtime/host-runtime.ts +++ b/packages/app/src/runtime/host-runtime.ts @@ -3,6 +3,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import equal from "fast-deep-equal/es6"; import { DaemonClient, + type DaemonClientConfig, type ConnectionState, type FetchAgentsOptions, } from "@getpaseo/client/internal/daemon-client"; @@ -11,6 +12,7 @@ import { normalizeStoredHostProfile, upsertHostConnectionInProfiles, registryHasConnection, + StoredHostRegistrySchema, type HostConnection, type HostProfile, } from "@/types/host-connection"; @@ -28,6 +30,8 @@ import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon"; import { isWeb } from "@/constants/platform"; import { connectToDaemon } from "@/utils/test-daemon-connection"; import { getOrCreateClientId } from "@/utils/client-id"; +import { z } from "zod"; +import { readValidatedJson, readValidatedString } from "@/storage/validated-storage"; import { selectBestConnection, type ConnectionCandidate, @@ -56,7 +60,10 @@ import { resolveComposerAttachmentSubmitFormat } from "@/composer/attachments/su import { encodeImages } from "@/utils/encode-images"; import { DirectorySync, type RefreshAgentDirectoryResult } from "@/runtime/directory-sync"; import { ReplicaCache } from "@/runtime/replica-cache"; +import { replicaCacheStorage } from "@/runtime/replica-cache/storage"; +import { projectIconCache } from "@/projects/icon-cache"; import { nativePerformanceTrace } from "@/performance/native-trace"; +import { revokePushNotifications } from "@/push-notifications"; import { createAppWebSocketFactory } from "./websocket-factory"; export type HostRuntimeConnectionStatus = "idle" | "connecting" | "online" | "offline" | "error"; @@ -162,6 +169,7 @@ export interface HostRuntimeControllerDeps { export interface HostRuntimeStorage { getItem: (key: string) => Promise; setItem: (key: string, value: string) => Promise; + removeItem: (key: string) => Promise; } export interface HostRuntimeStartOptions { @@ -482,12 +490,12 @@ function createDefaultDeps(): HostRuntimeControllerDeps { const base = { suppressSendErrors: true, clientId, - clientType: "mobile" as const, + clientType: "mobile", appVersion: resolveAppVersion() ?? undefined, runtimeGeneration, capabilities: appCapabilities, trace: nativePerformanceTrace, - }; + } satisfies Omit; if (connection.type === "directSocket" || connection.type === "directPipe") { return new DaemonClient({ ...base, @@ -1287,9 +1295,10 @@ export interface InitialDaemonConnectionHint { useTls?: boolean; } -function isInitialConnectionHintRecord(value: unknown): value is Record { - return Boolean(value) && typeof value === "object" && !Array.isArray(value); -} +const InitialDaemonConnectionHintSchema: z.ZodType = z.object({ + listen: z.string().trim().min(1), + useTls: z.boolean().optional().default(false), +}); export function readInitialDaemonConnectionHint(input?: { isWebRuntime?: boolean; @@ -1298,18 +1307,10 @@ export function readInitialDaemonConnectionHint(input?: { if (!isWebRuntime || typeof globalThis === "undefined") { return null; } - const value = (globalThis as Record)[INITIAL_DAEMON_CONNECTION_HINT_GLOBAL_KEY]; - if (!isInitialConnectionHintRecord(value)) { - return null; - } - const listen = typeof value.listen === "string" ? value.listen.trim() : ""; - if (!listen) { - return null; - } - return { - listen, - useTls: value.useTls === true, - }; + const result = InitialDaemonConnectionHintSchema.safeParse( + Reflect.get(globalThis, INITIAL_DAEMON_CONNECTION_HINT_GLOBAL_KEY), + ); + return result.success ? result.data : null; } function readConfiguredLocalDaemonOverride(): string | null { @@ -1366,11 +1367,17 @@ export class HostRuntimeStore { private bootPromise: Promise | null = null; private storage: HostRuntimeStorage; private replicaCache: ReplicaCache; + private readonly revokePushNotifications: typeof revokePushNotifications; - constructor(input?: { deps?: HostRuntimeControllerDeps; storage?: HostRuntimeStorage }) { + constructor(input?: { + deps?: HostRuntimeControllerDeps; + storage?: HostRuntimeStorage; + revokePushNotifications?: typeof revokePushNotifications; + }) { this.deps = input?.deps ?? createDefaultDeps(); this.storage = input?.storage ?? AsyncStorage; - this.replicaCache = new ReplicaCache(this.storage); + this.replicaCache = new ReplicaCache(input?.storage ?? replicaCacheStorage); + this.revokePushNotifications = input?.revokePushNotifications ?? revokePushNotifications; } // --- Host registry --- @@ -1412,7 +1419,7 @@ export class HostRuntimeStore { let isE2E: string | null = null; try { - isE2E = await this.storage.getItem(E2E_STORAGE_KEY); + isE2E = await readValidatedString(this.storage, E2E_STORAGE_KEY, z.string().min(1)); } catch { return; } @@ -1445,22 +1452,31 @@ export class HostRuntimeStore { let shouldPersistHosts = false; let profiles: HostProfile[] = []; try { - const stored = await this.storage.getItem(REGISTRY_STORAGE_KEY); + const stored = await readValidatedJson( + this.storage, + REGISTRY_STORAGE_KEY, + StoredHostRegistrySchema, + ); if (stored) { - const parsed = JSON.parse(stored) as unknown; - if (Array.isArray(parsed)) { - const normalizedProfiles = parsed - .map((entry) => normalizeStoredHostProfile(entry)) - .filter((entry): entry is HostProfile => entry !== null); - profiles = normalizedProfiles.filter((entry) => !isPlaceholderServerId(entry.serverId)); - if (profiles.length !== normalizedProfiles.length) { - shouldPersistHosts = true; + const normalizedProfiles: HostProfile[] = []; + for (const entry of stored) { + const profile = normalizeStoredHostProfile(entry); + if (!profile) { + await this.storage.removeItem(REGISTRY_STORAGE_KEY); + normalizedProfiles.length = 0; + break; } + normalizedProfiles.push(profile); + } + profiles = normalizedProfiles.filter((entry) => !isPlaceholderServerId(entry.serverId)); + if (profiles.length !== normalizedProfiles.length) { + shouldPersistHosts = true; } } this.hosts = profiles; this.replicaCache.setHosts(profiles.map((profile) => profile.serverId)); - await this.replicaCache.restore(); + projectIconCache.setHosts(profiles.map((profile) => profile.serverId)); + await Promise.all([this.replicaCache.restore(), projectIconCache.restore()]); this.syncHosts(profiles); } catch (error) { console.error("[HostRuntime] Failed to load host registry from storage", error); @@ -1597,14 +1613,19 @@ export class HostRuntimeStore { rekeyMap(this.connectionStatusStartedAtByServer, oldServerId, newServerId); rekeyMap(this.directoryBootstrapInFlight, oldServerId, newServerId); this.replicaCache.reconcileServerId(oldServerId, newServerId); + projectIconCache.reconcileServerId(oldServerId, newServerId); this.directorySyncByServer.get(oldServerId)?.dispose(); this.directorySyncByServer.delete(oldServerId); - const directory = new DirectorySync(newServerId, { - onAgentStoppedRunning: (agentId) => this.drainQueuedAgentMessage(newServerId, agentId), - markAgentLoading: () => controller.markAgentDirectorySyncLoading(), - markAgentReady: () => controller.markAgentDirectorySyncReady(), - markAgentError: (error) => controller.markAgentDirectorySyncError(error), - }); + const directory = new DirectorySync( + newServerId, + { + onAgentStoppedRunning: (agentId) => this.drainQueuedAgentMessage(newServerId, agentId), + markAgentLoading: () => controller.markAgentDirectorySyncLoading(), + markAgentReady: () => controller.markAgentDirectorySyncReady(), + markAgentError: (error) => controller.markAgentDirectorySyncError(error), + }, + this.replicaCache, + ); this.directorySyncByServer.set(newServerId, directory); controller.adoptReconciledServerId(newServerId); const snapshot = controller.getSnapshot(); @@ -1849,12 +1870,18 @@ export class HostRuntimeStore { } async removeHost(serverId: string): Promise { + await this.revokePushNotifications({ client: this.getClient(serverId), serverId }); const remaining = this.hosts.filter((daemon) => daemon.serverId !== serverId); this.setHostsAndSync(remaining); await this.persistHosts(); } async removeConnection(serverId: string, connectionId: string): Promise { + const host = this.hosts.find((candidate) => candidate.serverId === serverId); + if (host?.connections.length === 1 && host.connections[0]?.id === connectionId) { + await this.removeHost(serverId); + return; + } const now = new Date().toISOString(); const next = this.hosts .map((daemon) => { @@ -1909,7 +1936,11 @@ export class HostRuntimeStore { void this.persistHosts().catch((error) => console.error("[HostRuntime] Failed to persist host registry", error), ); - return next.find((daemon) => daemon.serverId === input.serverId) as HostProfile; + const profile = next.find((daemon) => daemon.serverId === input.serverId); + if (!profile) { + throw new Error(`Host ${input.serverId} was not inserted`); + } + return profile; } private setHostsAndSync( @@ -1947,6 +1978,7 @@ export class HostRuntimeStore { }, ): void { this.replicaCache.setHosts(hosts.map((host) => host.serverId)); + projectIconCache.setHosts(hosts.map((host) => host.serverId)); const nextIds = new Set(hosts.map((host) => host.serverId)); for (const [serverId, controller] of this.controllers) { if (nextIds.has(serverId)) { @@ -1983,12 +2015,17 @@ export class HostRuntimeStore { this.controllers.set(host.serverId, controller); this.directorySyncByServer.set( host.serverId, - new DirectorySync(host.serverId, { - onAgentStoppedRunning: (agentId) => this.drainQueuedAgentMessage(host.serverId, agentId), - markAgentLoading: () => controller.markAgentDirectorySyncLoading(), - markAgentReady: () => controller.markAgentDirectorySyncReady(), - markAgentError: (error) => controller.markAgentDirectorySyncError(error), - }), + new DirectorySync( + host.serverId, + { + onAgentStoppedRunning: (agentId) => + this.drainQueuedAgentMessage(host.serverId, agentId), + markAgentLoading: () => controller.markAgentDirectorySyncLoading(), + markAgentReady: () => controller.markAgentDirectorySyncReady(), + markAgentError: (error) => controller.markAgentDirectorySyncError(error), + }, + this.replicaCache, + ), ); const initialSnapshot = controller.getSnapshot(); this.lastConnectionStatusByServer.set(host.serverId, initialSnapshot.connectionStatus); @@ -2271,22 +2308,6 @@ export class HostRuntimeStore { } } - markAgentDirectorySyncLoading(serverId: string): void { - this.controllers.get(serverId)?.markAgentDirectorySyncLoading(); - } - - markAgentDirectorySyncReady(serverId: string): void { - this.controllers.get(serverId)?.markAgentDirectorySyncReady(); - } - - markAgentDirectorySyncError(serverId: string, error: string): void { - this.controllers.get(serverId)?.markAgentDirectorySyncError(error); - } - - markAgentDirectorySyncIdle(serverId: string): void { - this.controllers.get(serverId)?.markAgentDirectorySyncIdle(); - } - private emit(serverId: string): void { this.version += 1; const listeners = this.serverListeners.get(serverId); @@ -2308,25 +2329,19 @@ export class HostRuntimeStore { let singletonHostRuntimeStore: HostRuntimeStore | null = null; const HOST_RUNTIME_STORE_GLOBAL_KEY = "__paseoHostRuntimeStore"; -type HostRuntimeGlobal = typeof globalThis & { - [HOST_RUNTIME_STORE_GLOBAL_KEY]?: HostRuntimeStore; -}; - export function getHostRuntimeStore(): HostRuntimeStore { if (singletonHostRuntimeStore) { return singletonHostRuntimeStore; } - const runtimeGlobal = globalThis as HostRuntimeGlobal; - if (runtimeGlobal[HOST_RUNTIME_STORE_GLOBAL_KEY]) { - singletonHostRuntimeStore = runtimeGlobal[HOST_RUNTIME_STORE_GLOBAL_KEY] ?? null; - if (singletonHostRuntimeStore) { - return singletonHostRuntimeStore; - } + const existing = Reflect.get(globalThis, HOST_RUNTIME_STORE_GLOBAL_KEY); + if (existing instanceof HostRuntimeStore) { + singletonHostRuntimeStore = existing; + return existing; } singletonHostRuntimeStore = new HostRuntimeStore(); - runtimeGlobal[HOST_RUNTIME_STORE_GLOBAL_KEY] = singletonHostRuntimeStore; + Reflect.set(globalThis, HOST_RUNTIME_STORE_GLOBAL_KEY, singletonHostRuntimeStore); return singletonHostRuntimeStore; } @@ -2383,12 +2398,11 @@ export function useHostRuntimeConnectionStatuses( return useMemo(() => { // The aggregate version is the reactivity trigger; re-read snapshots on every host tick. void version; - return new Map( - serverIds.map( - (serverId) => - [serverId, store.getSnapshot(serverId)?.connectionStatus ?? "connecting"] as const, - ), - ); + const entries: Array<[string, HostRuntimeConnectionStatus]> = serverIds.map((serverId) => [ + serverId, + store.getSnapshot(serverId)?.connectionStatus ?? "connecting", + ]); + return new Map(entries); }, [serverIds, store, version]); } diff --git a/packages/app/src/runtime/replica-cache/index.test.ts b/packages/app/src/runtime/replica-cache/index.test.ts index c634ff250c..ee008f2ab0 100644 --- a/packages/app/src/runtime/replica-cache/index.test.ts +++ b/packages/app/src/runtime/replica-cache/index.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import type { WorkspaceDescriptorPayload } from "@getpaseo/protocol/messages"; import { normalizeAgentSnapshot } from "@/utils/agent-snapshots"; import { @@ -15,14 +15,20 @@ const LRU_SERVER_IDS = ["host-a", "host-b", "host-c"] as const; class MemoryStorage implements ReplicaCacheStorage { readonly values = new Map(); + writes = 0; async getItem(key: string): Promise { return this.values.get(key) ?? null; } async setItem(key: string, value: string): Promise { + this.writes += 1; this.values.set(key, value); } + + async removeItem(key: string): Promise { + this.values.delete(key); + } } function workspace( @@ -49,34 +55,50 @@ function workspace( } function agent(id: string, workspaceId = "workspace-1", cwd = "/repo/paseo") { - return normalizeAgentSnapshot( - { - id, - provider: "codex", - cwd, - workspaceId, - model: null, - createdAt: "2026-07-18T08:00:00.000Z", - updatedAt: "2026-07-18T08:01:00.000Z", - lastUserMessageAt: "2026-07-18T08:01:00.000Z", - status: "idle", - capabilities: { - supportsStreaming: true, - supportsSessionPersistence: true, - supportsDynamicModes: true, - supportsMcpServers: true, - supportsReasoningStream: true, - supportsToolInvocations: true, + return { + ...normalizeAgentSnapshot( + { + id, + provider: "codex", + cwd, + workspaceId, + model: null, + createdAt: "2026-07-18T08:00:00.000Z", + updatedAt: "2026-07-18T08:01:00.000Z", + lastUserMessageAt: "2026-07-18T08:01:00.000Z", + status: "idle", + capabilities: { + supportsStreaming: true, + supportsSessionPersistence: true, + supportsDynamicModes: true, + supportsMcpServers: true, + supportsReasoningStream: true, + supportsToolInvocations: true, + }, + currentModeId: null, + availableModes: [], + pendingPermissions: [], + persistence: null, + title: `Agent ${id}`, + labels: {}, + }, + SERVER_ID, + ), + projectPlacement: { + projectKey: cwd, + projectName: cwd.split("/").at(-1) ?? cwd, + workspaceName: workspaceId, + checkout: { + cwd, + isGit: false as const, + currentBranch: null, + remoteUrl: null, + worktreeRoot: null, + isPaseoOwnedWorktree: false as const, + mainRepoRoot: null, }, - currentModeId: null, - availableModes: [], - pendingPermissions: [], - persistence: null, - title: `Agent ${id}`, - labels: {}, }, - SERVER_ID, - ); + }; } function message(id: string, text: string): StreamItem { @@ -154,12 +176,39 @@ function seedTimeline(serverId: string, text: string): void { } afterEach(() => { + vi.useRealTimers(); const store = useSessionStore.getState(); store.clearSession(SERVER_ID); for (const serverId of LRU_SERVER_IDS) store.clearSession(serverId); }); describe("ReplicaCache", () => { + it("persists focused replica changes without writing transient stream head updates", async () => { + vi.useFakeTimers(); + const storage = new MemoryStorage(); + const cache = new ReplicaCache(storage); + cache.setHosts([SERVER_ID]); + seedSession(); + await cache.flush(); + cache.start(); + const writesBeforeStream = storage.writes; + + useSessionStore + .getState() + .setAgentStreamHead(SERVER_ID, new Map([["agent-1", [message("live", "Streaming")]]])); + await vi.advanceTimersByTimeAsync(1_000); + + expect(storage.writes).toBe(writesBeforeStream); + + useSessionStore + .getState() + .setAgentStreamTail(SERVER_ID, new Map([["agent-1", [message("saved", "Committed")]]])); + await vi.advanceTimersByTimeAsync(1_000); + + expect(storage.writes).toBe(writesBeforeStream + 1); + cache.setHosts([]); + }); + it("restores a displayable stale replica without claiming remote hydration", async () => { const storage = new MemoryStorage(); const writer = new ReplicaCache(storage); @@ -179,10 +228,12 @@ describe("ReplicaCache", () => { expect(session.client).toBeNull(); expect(session.hasHydratedAgents).toBe(false); expect(session.hasHydratedWorkspaces).toBe(false); + expect(session.hasWorkspaceDirectorySnapshot).toBe(true); expect(Array.from(session.agents.keys())).toEqual(["agent-1"]); expect(Array.from(session.workspaces.keys())).toEqual(["workspace-1"]); - expect(Array.from(session.projects.keys())).toEqual(["project-1"]); + expect(Array.from(session.projects.keys())).toEqual(["project-1", "empty-project"]); expect(session.agents.get("agent-1")?.updatedAt).toBeInstanceOf(Date); + expect(session.agents.get("agent-1")?.projectPlacement?.checkout.cwd).toBe("/repo/paseo"); expect(session.workspaces.get("workspace-1")?.statusEnteredAt).toBeInstanceOf(Date); expect(session.workspaces.get("workspace-1")?.worktreeSlug).toBe("owned-worktree"); expect(session.agentStreamTail.get("agent-1")).toEqual([message("message-1", "Cached")]); @@ -197,7 +248,7 @@ describe("ReplicaCache", () => { }); }); - it("persists only the focused agent view with a short timeline tail", async () => { + it("persists the complete directory with only the focused timeline tail", async () => { const storage = new MemoryStorage(); const cache = new ReplicaCache(storage); cache.setHosts([SERVER_ID]); @@ -233,9 +284,13 @@ describe("ReplicaCache", () => { const session = useSessionStore.getState().sessions[SERVER_ID]; const timelines = session?.agentStreamTail; - expect(Array.from(session?.agents.keys() ?? [])).toEqual(["agent-2"]); - expect(Array.from(session?.workspaces.keys() ?? [])).toEqual(["workspace-2"]); - expect(Array.from(session?.projects.keys() ?? [])).toEqual(["project-2"]); + expect(Array.from(session?.agents.keys() ?? [])).toEqual(["agent-1", "agent-2"]); + expect(Array.from(session?.workspaces.keys() ?? [])).toEqual(["workspace-1", "workspace-2"]); + expect(Array.from(session?.projects.keys() ?? [])).toEqual([ + "project-1", + "project-2", + "empty-project", + ]); expect(Array.from(timelines?.keys() ?? [])).toEqual(["agent-2"]); expect(timelines?.get("agent-2")).toEqual(secondTimeline.slice(-50)); @@ -243,7 +298,7 @@ describe("ReplicaCache", () => { version: number; hosts: Array<{ timeline: Record | null }>; }; - expect(persisted.version).toBe(3); + expect(persisted.version).toBe(5); expect(Object.keys(persisted.hosts[0]?.timeline ?? {}).sort()).toEqual(["agentId", "items"]); }); @@ -277,6 +332,53 @@ describe("ReplicaCache", () => { ]); }); + it("persists monotonic directory cursors with the complete host replica", async () => { + const storage = new MemoryStorage(); + const cache = new ReplicaCache(storage); + cache.setHosts([SERVER_ID]); + seedSession(); + await cache.flush(); + + cache.writeDirectoryCheckpoint(SERVER_ID, { + agents: { generation: "daemon-generation", afterSeq: 7 }, + }); + useSessionStore.getState().setAgents(SERVER_ID, (agents) => { + const current = agents.get("agent-1"); + if (!current) throw new Error("Expected seeded agent"); + return new Map(agents).set("agent-1", { ...current, title: "Updated agent" }); + }); + await cache.flush(); + + const reader = new ReplicaCache(storage); + reader.setHosts([SERVER_ID]); + await reader.restore(); + expect(reader.readDirectoryCheckpoint(SERVER_ID)).toEqual({ + agents: { generation: "daemon-generation", afterSeq: 7 }, + }); + }); + + it("restores every registered host directory before any host reconnects", async () => { + const storage = new MemoryStorage(); + const writer = new ReplicaCache(storage); + writer.setHosts(LRU_SERVER_IDS); + for (const serverId of LRU_SERVER_IDS) seedTimeline(serverId, `cached-${serverId}`); + await writer.flush(); + for (const serverId of LRU_SERVER_IDS) useSessionStore.getState().clearSession(serverId); + + const reader = new ReplicaCache(storage); + reader.setHosts(LRU_SERVER_IDS); + await reader.restore(); + + for (const serverId of LRU_SERVER_IDS) { + const session = useSessionStore.getState().sessions[serverId]; + expect(Array.from(session?.agents.keys() ?? [])).toEqual([`agent-${serverId}`]); + expect(Array.from(session?.workspaces.keys() ?? [])).toEqual([`workspace-${serverId}`]); + expect(session?.hasHydratedAgents).toBe(false); + expect(session?.hasHydratedWorkspaces).toBe(false); + expect(session?.hasWorkspaceDirectorySnapshot).toBe(true); + } + }); + it("evicts the least recently written host when the cache exceeds its byte budget", async () => { const storage = new MemoryStorage(); const cache = new ReplicaCache(storage, { maxBytes: 7_000 }); @@ -302,12 +404,12 @@ describe("ReplicaCache", () => { expect(Object.keys(useSessionStore.getState().sessions).sort()).toEqual(["host-a", "host-c"]); }); - it("rejects version 1 cache data and overwrites it on flush", async () => { + it("rejects and clears version 4 cache data before overwriting it on flush", async () => { const storage = new MemoryStorage(); storage.values.set( "@paseo:replica-cache", JSON.stringify({ - version: 1, + version: 4, hosts: [ { serverId: SERVER_ID, @@ -328,11 +430,12 @@ describe("ReplicaCache", () => { cache.setHosts([SERVER_ID]); await cache.restore(); + expect(storage.values.has("@paseo:replica-cache")).toBe(false); await cache.flush(); expect(useSessionStore.getState().sessions[SERVER_ID]).toBeUndefined(); expect(JSON.parse(storage.values.get("@paseo:replica-cache") ?? "null")).toEqual({ - version: 3, + version: 5, hosts: [], }); }); diff --git a/packages/app/src/runtime/replica-cache/index.ts b/packages/app/src/runtime/replica-cache/index.ts index 67c45a5474..35ddf09fe5 100644 --- a/packages/app/src/runtime/replica-cache/index.ts +++ b/packages/app/src/runtime/replica-cache/index.ts @@ -1,13 +1,7 @@ import { Buffer } from "buffer"; import { z } from "zod"; -import { - AgentSnapshotPayloadSchema, - ProjectPlacementPayloadSchema, - WorkspaceDescriptorPayloadSchema, - WorkspaceProjectDescriptorPayloadSchema, - type AgentSnapshotPayload, - type WorkspaceDescriptorPayload, -} from "@getpaseo/protocol/messages"; +import { AgentStatusSchema } from "@getpaseo/protocol/messages"; +import { AgentProviderSchema } from "@getpaseo/protocol/provider-manifest"; import { normalizeProjectDescriptor, normalizeWorkspaceDescriptor, @@ -23,141 +17,444 @@ import { isUnreconciledLocalUserMessage, type StreamItem } from "@/types/stream" import { normalizeAgentSnapshot } from "@/utils/agent-snapshots"; const STORAGE_KEY = "@paseo:replica-cache"; -const CACHE_VERSION = 3; +const CACHE_VERSION = 5; const PERSIST_DELAY_MS = 750; const MAX_TIMELINE_ITEMS = 50; -const MAX_CACHE_BYTES = 1024 * 1024; -const DATE_TAG = "__paseoDate"; - -const StoredAgentSchema = z.object({ - snapshot: AgentSnapshotPayloadSchema, - projectPlacement: ProjectPlacementPayloadSchema.nullable(), - lastActivityAt: z.string(), +const MAX_CACHE_BYTES = 32 * 1024 * 1024; +const IsoDateSchema = z.iso.datetime(); +const TimelinePositionSchema = z.strictObject({ + epoch: z.string(), + seq: z.number().int().nonnegative(), }); -const StoredTimelineSchema = z.object({ +const TimelineItemBaseShape = { + id: z.string(), + timelineCursor: TimelinePositionSchema.optional(), + timestamp: IsoDateSchema, +}; + +const TodoEntrySchema = z.strictObject({ + text: z.string(), + completed: z.boolean(), + id: z.string().optional(), + status: z.enum(["pending", "in_progress", "completed"]).optional(), + activeForm: z.string().optional(), +}); + +const TaskActivitySchema = z.discriminatedUnion("type", [ + z.strictObject({ type: z.literal("created"), count: z.number().int().nonnegative() }), + z.strictObject({ + type: z.enum(["added", "started", "completed", "reopened"]), + task: z.string(), + }), +]); + +const StoredTimelineItemSchema = z.discriminatedUnion("kind", [ + z.strictObject({ + ...TimelineItemBaseShape, + kind: z.literal("user_message"), + clientMessageId: z.string().optional(), + messageId: z.string().optional(), + text: z.string(), + }), + z.strictObject({ + ...TimelineItemBaseShape, + kind: z.literal("assistant_message"), + messageId: z.string().optional(), + text: z.string(), + blockGroupId: z.string().optional(), + blockIndex: z.number().int().nonnegative().optional(), + }), + z.strictObject({ + ...TimelineItemBaseShape, + kind: z.literal("thought"), + text: z.string(), + status: z.enum(["loading", "ready"]), + }), + z.strictObject({ + ...TimelineItemBaseShape, + kind: z.literal("todo_list"), + provider: AgentProviderSchema, + items: z.array(TodoEntrySchema), + activity: TaskActivitySchema, + }), + z.strictObject({ + ...TimelineItemBaseShape, + kind: z.literal("activity_log"), + activityType: z.enum(["system", "info", "success", "error"]), + message: z.string(), + }), + z.strictObject({ + ...TimelineItemBaseShape, + kind: z.literal("compaction"), + status: z.enum(["loading", "completed"]), + trigger: z.enum(["auto", "manual"]).optional(), + preTokens: z.number().nonnegative().optional(), + }), +]); + +const AgentCapabilitiesSchema = z.strictObject({ + supportsStreaming: z.boolean(), + supportsSessionPersistence: z.boolean(), + supportsSessionListing: z.boolean().optional(), + supportsDynamicModes: z.boolean(), + supportsMcpServers: z.boolean(), + supportsReasoningStream: z.boolean(), + supportsToolInvocations: z.boolean(), + supportsRewindConversation: z.boolean().optional(), + supportsRewindFiles: z.boolean().optional(), + supportsRewindBoth: z.boolean().optional(), +}); + +const StoredProjectCheckoutSchema = z.union([ + z.strictObject({ + cwd: z.string(), + isGit: z.literal(false), + currentBranch: z.null(), + remoteUrl: z.null(), + worktreeRoot: z.null(), + isPaseoOwnedWorktree: z.literal(false), + mainRepoRoot: z.null(), + }), + z.strictObject({ + cwd: z.string(), + isGit: z.literal(true), + currentBranch: z.string().nullable(), + remoteUrl: z.string().nullable(), + worktreeRoot: z.string(), + isPaseoOwnedWorktree: z.literal(false), + mainRepoRoot: z.string().nullable(), + }), + z.strictObject({ + cwd: z.string(), + isGit: z.literal(true), + currentBranch: z.string().nullable(), + remoteUrl: z.string().nullable(), + worktreeRoot: z.string(), + isPaseoOwnedWorktree: z.literal(true), + mainRepoRoot: z.string(), + }), +]); + +const StoredProjectPlacementSchema = z.strictObject({ + projectKey: z.string(), + projectName: z.string(), + workspaceName: z.string().nullable().optional(), + checkout: StoredProjectCheckoutSchema, +}); + +const StoredAgentSnapshotSchema = z.strictObject({ + id: z.string(), + provider: AgentProviderSchema, + cwd: z.string(), + workspaceId: z.string().optional(), + model: z.string().nullable(), + thinkingOptionId: z.string().nullable().optional(), + createdAt: IsoDateSchema, + updatedAt: IsoDateSchema, + lastUserMessageAt: IsoDateSchema.nullable(), + status: AgentStatusSchema, + activeTurn: z + .strictObject({ + turnId: z.string(), + startedAt: IsoDateSchema.nullable(), + }) + .nullable() + .optional(), + capabilities: AgentCapabilitiesSchema, + currentModeId: z.string().nullable(), + availableModes: z.array(z.never()).max(0), + pendingPermissions: z.array(z.never()).max(0), + persistence: z.null(), + lastError: z.string().optional(), + title: z.string().nullable(), + labels: z.record(z.string(), z.string()), + requiresAttention: z.boolean().optional(), + attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(), + attentionTimestamp: IsoDateSchema.nullable().optional(), + archivedAt: IsoDateSchema.nullable().optional(), +}); + +const StoredAgentSchema = z.strictObject({ + snapshot: StoredAgentSnapshotSchema, + projectPlacement: StoredProjectPlacementSchema.nullable(), + lastActivityAt: IsoDateSchema, +}); + +const WorkspaceScriptSchema = z.strictObject({ + scriptName: z.string(), + type: z.enum(["script", "service"]), + hostname: z.string(), + port: z.number().int().positive().nullable(), + localProxyUrl: z.string().nullable().optional(), + publicProxyUrl: z.string().nullable().optional(), + proxyUrl: z.string().nullable(), + lifecycle: z.enum(["running", "stopped"]), + health: z.enum(["healthy", "unhealthy"]).nullable(), + exitCode: z.number().nullable(), + terminalId: z.string().nullable(), +}); + +const WorkspaceGitRuntimeSchema = z + .strictObject({ + currentBranch: z.string().nullable().optional(), + remoteUrl: z.string().nullable().optional(), + isPaseoOwnedWorktree: z.boolean().optional(), + isDirty: z.boolean().nullable().optional(), + aheadBehind: z.strictObject({ ahead: z.number(), behind: z.number() }).nullable().optional(), + aheadOfOrigin: z.number().nullable().optional(), + behindOfOrigin: z.number().nullable().optional(), + }) + .nullable() + .optional(); + +const StoredWorkspaceSchema = z.strictObject({ + id: z.string(), + projectId: z.string(), + projectDisplayName: z.string(), + projectCustomName: z.string().nullable(), + projectCustomIconRevision: z.string().nullable(), + projectRootPath: z.string(), + workspaceDirectory: z.string(), + worktreeSlug: z.string().optional(), + projectKind: z.enum(["git", "non_git", "directory"]), + workspaceKind: z.enum(["directory", "local_checkout", "checkout", "worktree"]), + name: z.string(), + title: z.string().nullable(), + pinnedAt: z.string().nullable(), + status: z.enum(["needs_input", "failed", "running", "attention", "done"]), + statusEnteredAt: IsoDateSchema.nullable(), + activityAt: z.null(), + archivingAt: z.string().nullable(), + diffStat: z.strictObject({ additions: z.number(), deletions: z.number() }).nullable(), + scripts: z.array(WorkspaceScriptSchema), + gitRuntime: WorkspaceGitRuntimeSchema, + forge: z.string().optional(), +}); + +const StoredProjectSchema = z.strictObject({ + projectId: z.string(), + projectKey: z.string().optional(), + projectDisplayName: z.string(), + projectCustomName: z.string().nullable(), + projectCustomIconRevision: z.string().nullable(), + projectIconRevision: z.string().optional(), + projectRootPath: z.string(), + projectKind: z.enum(["git", "non_git", "directory"]), +}); + +const StoredTimelineSchema = z.strictObject({ agentId: z.string(), - items: z.unknown(), + items: z.array(StoredTimelineItemSchema), }); -const StoredHostSchema = z.object({ +const StoredHostSchema = z.strictObject({ serverId: z.string(), agents: z.array(StoredAgentSchema), - workspaces: z.array(WorkspaceDescriptorPayloadSchema), - projects: z.array(WorkspaceProjectDescriptorPayloadSchema).optional().default([]), - emptyProjects: z.array(WorkspaceProjectDescriptorPayloadSchema), + workspaces: z.array(StoredWorkspaceSchema), + projects: z.array(StoredProjectSchema), + emptyProjects: z.array(StoredProjectSchema), timeline: StoredTimelineSchema.nullable(), + directorySync: z.unknown().optional(), }); -const StoredCacheSchema = z.object({ +const StoredCacheSchema = z.strictObject({ version: z.literal(CACHE_VERSION), hosts: z.array(StoredHostSchema), }); type StoredAgent = z.infer; type StoredHost = z.infer; +type StoredTimelineItem = z.infer; +type StoredWorkspace = z.infer; +type StoredProject = z.infer; + +interface ReplicaInput { + agents: ReadonlyMap; + workspaces: ReadonlyMap; + projects: ReadonlyMap; + focusedAgent: Agent | undefined; + timelineItems: StreamItem[] | undefined; +} export interface ReplicaCacheStorage { getItem: (key: string) => Promise; setItem: (key: string, value: string) => Promise; + removeItem: (key: string) => Promise; } interface ReplicaCacheOptions { maxBytes?: number; } -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function hasString(value: Record, key: string): boolean { - return typeof value[key] === "string"; -} - -function isStreamItem(value: unknown): value is StreamItem { - if (!isRecord(value) || !hasString(value, "id") || !(value.timestamp instanceof Date)) { - return false; - } - switch (value.kind) { - case "user_message": - case "assistant_message": - return hasString(value, "text"); - case "thought": - return hasString(value, "text") && (value.status === "loading" || value.status === "ready"); - case "tool_call": - return isRecord(value.payload) && isRecord(value.payload.data); - case "todo_list": - return hasString(value, "provider") && Array.isArray(value.items); - case "activity_log": - return hasString(value, "message") && hasString(value, "activityType"); - case "compaction": - return value.status === "loading" || value.status === "completed"; - default: - return false; - } -} - -function encodeDates(value: unknown): unknown { - if (value instanceof Date) { - return { [DATE_TAG]: value.toISOString() }; - } - if (Array.isArray(value)) { - return value.map(encodeDates); - } - if (!isRecord(value)) { - return value; - } - return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, encodeDates(entry)])); -} - -function decodeDates(value: unknown): unknown { - if (Array.isArray(value)) { - return value.map(decodeDates); - } - if (!isRecord(value)) { - return value; - } - if (Object.keys(value).length === 1 && typeof value[DATE_TAG] === "string") { - const date = new Date(value[DATE_TAG]); - return Number.isNaN(date.getTime()) ? value : date; - } - return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, decodeDates(entry)])); -} - function deserializeTimeline(stored: StoredHost["timeline"]): SessionReplica["timeline"] { if (!stored) { return null; } - const decoded = decodeDates(stored.items); - if (!Array.isArray(decoded) || !decoded.every(isStreamItem)) { - return null; - } return { agentId: stored.agentId, - items: decoded, + items: stored.items.map(deserializeTimelineItem), }; } +function timelineBase(item: StreamItem) { + return { + id: item.id, + ...(item.timelineCursor ? { timelineCursor: item.timelineCursor } : {}), + timestamp: item.timestamp.toISOString(), + }; +} + +function serializeTimelineItem(item: StreamItem): StoredTimelineItem | null { + const base = timelineBase(item); + switch (item.kind) { + case "user_message": + return { + ...base, + kind: item.kind, + ...(item.clientMessageId ? { clientMessageId: item.clientMessageId } : {}), + ...(item.messageId ? { messageId: item.messageId } : {}), + text: item.text, + }; + case "assistant_message": + return { + ...base, + kind: item.kind, + ...(item.messageId ? { messageId: item.messageId } : {}), + text: item.text, + ...(item.blockGroupId ? { blockGroupId: item.blockGroupId } : {}), + ...(item.blockIndex !== undefined ? { blockIndex: item.blockIndex } : {}), + }; + case "thought": + return { ...base, kind: item.kind, text: item.text, status: item.status }; + case "todo_list": + return { + ...base, + kind: item.kind, + provider: item.provider, + items: item.items, + activity: item.activity, + }; + case "activity_log": + return { + ...base, + kind: item.kind, + activityType: item.activityType, + message: item.message, + }; + case "compaction": + return { + ...base, + kind: item.kind, + status: item.status, + ...(item.trigger ? { trigger: item.trigger } : {}), + ...(item.preTokens !== undefined ? { preTokens: item.preTokens } : {}), + }; + case "tool_call": + return null; + } +} + +function deserializeTimelineItem(item: StoredTimelineItem): StreamItem { + const base = { + id: item.id, + ...(item.timelineCursor ? { timelineCursor: item.timelineCursor } : {}), + timestamp: new Date(item.timestamp), + }; + switch (item.kind) { + case "user_message": + return { + ...base, + kind: item.kind, + ...(item.clientMessageId ? { clientMessageId: item.clientMessageId } : {}), + ...(item.messageId ? { messageId: item.messageId } : {}), + text: item.text, + }; + case "assistant_message": + return { + ...base, + kind: item.kind, + ...(item.messageId ? { messageId: item.messageId } : {}), + text: item.text, + ...(item.blockGroupId ? { blockGroupId: item.blockGroupId } : {}), + ...(item.blockIndex !== undefined ? { blockIndex: item.blockIndex } : {}), + }; + case "thought": + return { ...base, kind: item.kind, text: item.text, status: item.status }; + case "todo_list": + return { + ...base, + kind: item.kind, + provider: item.provider, + items: item.items, + activity: item.activity, + }; + case "activity_log": + return { + ...base, + kind: item.kind, + activityType: item.activityType, + message: item.message, + }; + case "compaction": + return { + ...base, + kind: item.kind, + status: item.status, + ...(item.trigger ? { trigger: item.trigger } : {}), + ...(item.preTokens !== undefined ? { preTokens: item.preTokens } : {}), + }; + } +} + +function serializeProjectPlacement(agent: Agent): StoredAgent["projectPlacement"] { + return agent.projectPlacement ?? null; +} + function serializeAgent(agent: Agent): StoredAgent { - const snapshot: AgentSnapshotPayload = { + const snapshot = { id: agent.id, provider: agent.provider, cwd: agent.cwd, ...(agent.workspaceId ? { workspaceId: agent.workspaceId } : {}), model: agent.model, - ...(agent.features ? { features: agent.features } : {}), thinkingOptionId: agent.thinkingOptionId ?? null, createdAt: agent.createdAt.toISOString(), updatedAt: agent.updatedAt.toISOString(), lastUserMessageAt: agent.lastUserMessageAt?.toISOString() ?? null, status: agent.status, - capabilities: agent.capabilities, + ...(agent.activeTurn?.turnId + ? { + activeTurn: { + turnId: agent.activeTurn.turnId, + startedAt: agent.activeTurn.startedAt?.toISOString() ?? null, + }, + } + : {}), + capabilities: { + supportsStreaming: agent.capabilities.supportsStreaming, + supportsSessionPersistence: agent.capabilities.supportsSessionPersistence, + ...(agent.capabilities.supportsSessionListing !== undefined + ? { supportsSessionListing: agent.capabilities.supportsSessionListing } + : {}), + supportsDynamicModes: agent.capabilities.supportsDynamicModes, + supportsMcpServers: agent.capabilities.supportsMcpServers, + supportsReasoningStream: agent.capabilities.supportsReasoningStream, + supportsToolInvocations: agent.capabilities.supportsToolInvocations, + ...(agent.capabilities.supportsRewindConversation !== undefined + ? { supportsRewindConversation: agent.capabilities.supportsRewindConversation } + : {}), + ...(agent.capabilities.supportsRewindFiles !== undefined + ? { supportsRewindFiles: agent.capabilities.supportsRewindFiles } + : {}), + ...(agent.capabilities.supportsRewindBoth !== undefined + ? { supportsRewindBoth: agent.capabilities.supportsRewindBoth } + : {}), + }, currentModeId: agent.currentModeId, - availableModes: agent.availableModes, + availableModes: [], pendingPermissions: [], - persistence: agent.persistence, - ...(agent.runtimeInfo ? { runtimeInfo: agent.runtimeInfo } : {}), - ...(agent.lastUsage ? { lastUsage: agent.lastUsage } : {}), + persistence: null, ...(agent.lastError ? { lastError: agent.lastError } : {}), title: agent.title, labels: agent.labels, @@ -168,7 +465,7 @@ function serializeAgent(agent: Agent): StoredAgent { }; return { snapshot, - projectPlacement: agent.projectPlacement ?? null, + projectPlacement: serializeProjectPlacement(agent), lastActivityAt: agent.lastActivityAt.toISOString(), }; } @@ -181,7 +478,7 @@ function deserializeAgent(serverId: string, stored: StoredAgent): Agent { }; } -function serializeWorkspace(workspace: WorkspaceDescriptor): WorkspaceDescriptorPayload { +function serializeWorkspace(workspace: WorkspaceDescriptor): StoredWorkspace { return { id: workspace.id, projectId: workspace.projectId, @@ -201,25 +498,83 @@ function serializeWorkspace(workspace: WorkspaceDescriptor): WorkspaceDescriptor activityAt: null, archivingAt: workspace.archivingAt, diffStat: workspace.diffStat, - scripts: workspace.scripts, + scripts: workspace.scripts.map((script) => ({ + scriptName: script.scriptName, + type: script.type, + hostname: script.hostname, + port: script.port, + ...(script.localProxyUrl !== undefined ? { localProxyUrl: script.localProxyUrl } : {}), + ...(script.publicProxyUrl !== undefined ? { publicProxyUrl: script.publicProxyUrl } : {}), + proxyUrl: script.proxyUrl, + lifecycle: script.lifecycle, + health: script.health, + exitCode: script.exitCode, + terminalId: script.terminalId, + })), gitRuntime: workspace.gitRuntime, - githubRuntime: workspace.githubRuntime, forge: workspace.forge, - project: workspace.project, }; } -function serializeProject(project: ProjectDescriptor) { +function serializeProject(project: ProjectDescriptor): StoredProject { return { projectId: project.projectId, ...(project.projectKey ? { projectKey: project.projectKey } : {}), projectDisplayName: project.projectDisplayName, projectCustomName: project.projectCustomName, + projectCustomIconRevision: project.projectCustomIconRevision ?? null, + projectIconRevision: project.projectIconRevision, projectRootPath: project.projectRootPath, projectKind: project.projectKind, }; } +function replicaInputsEqual(left: ReplicaInput, right: ReplicaInput): boolean { + return ( + left.agents === right.agents && + left.workspaces === right.workspaces && + left.projects === right.projects && + left.focusedAgent === right.focusedAgent && + left.timelineItems === right.timelineItems + ); +} + +function selectReplicaInput(session: SessionState, agentId: string | null): ReplicaInput { + const agent = agentId ? session.agents.get(agentId) : undefined; + const timeline = agentId + ? selectAgentTimelineState(session, agentId) + : { status: "cold" as const }; + return { + agents: session.agents, + workspaces: session.workspaces, + projects: session.projects, + focusedAgent: agent, + timelineItems: timeline.status === "cold" ? undefined : timeline.items, + }; +} + +function serializeHost(serverId: string, input: ReplicaInput, directorySync?: unknown): StoredHost { + const items = input.timelineItems + ?.filter((item) => item.kind !== "user_message" || !isUnreconciledLocalUserMessage(item)) + .map(serializeTimelineItem) + .filter((item) => item !== null); + return { + serverId, + agents: Array.from(input.agents.values(), serializeAgent), + workspaces: Array.from(input.workspaces.values(), serializeWorkspace), + projects: Array.from(input.projects.values(), serializeProject), + emptyProjects: [], + timeline: + input.focusedAgent && items + ? { + agentId: input.focusedAgent.id, + items: items.slice(-MAX_TIMELINE_ITEMS), + } + : null, + ...(directorySync ? { directorySync } : {}), + }; +} + function deserializeHost(stored: StoredHost): SessionReplica { const agents = stored.agents.map((entry) => deserializeAgent(stored.serverId, entry)); const workspaces = stored.workspaces.map(normalizeWorkspaceDescriptor); @@ -254,7 +609,7 @@ export class ReplicaCache { private readonly activeServerIds = new Set(); private readonly storedHosts = new Map(); private readonly lastFocusedAgentIds = new Map(); - private readonly capturedSessions = new Map(); + private readonly capturedInputs = new Map(); private readonly maxBytes: number; private needsPersist = false; private unsubscribe: (() => void) | null = null; @@ -286,8 +641,8 @@ export class ReplicaCache { for (const serverId of this.lastFocusedAgentIds.keys()) { if (!next.has(serverId)) this.lastFocusedAgentIds.delete(serverId); } - for (const serverId of this.capturedSessions.keys()) { - if (!next.has(serverId)) this.capturedSessions.delete(serverId); + for (const serverId of this.capturedInputs.keys()) { + if (!next.has(serverId)) this.capturedInputs.delete(serverId); } if (removedStoredHost) this.needsPersist = true; if (this.unsubscribe && this.needsPersist) this.schedulePersist(); @@ -305,10 +660,14 @@ export class ReplicaCache { try { parsed = JSON.parse(raw); } catch { + await this.clearInvalidCache(); return; } const cache = StoredCacheSchema.safeParse(parsed); - if (!cache.success) return; + if (!cache.success) { + await this.clearInvalidCache(); + return; + } for (const host of cache.data.hosts) { if (!this.activeServerIds.has(host.serverId)) { this.needsPersist = true; @@ -317,25 +676,38 @@ export class ReplicaCache { this.storedHosts.set(host.serverId, host); if (host.timeline) this.lastFocusedAgentIds.set(host.serverId, host.timeline.agentId); } - if (this.buildBoundedPayload().evicted) this.needsPersist = true; + const bounded = this.buildBoundedPayload(); + if (!bounded) { + await this.clearInvalidCache(); + this.storedHosts.clear(); + return; + } + if (bounded.evicted) this.needsPersist = true; for (const host of this.storedHosts.values()) { useSessionStore.getState().restoreSessionReplica(host.serverId, deserializeHost(host)); const session = useSessionStore.getState().sessions[host.serverId]; - if (session) this.capturedSessions.set(host.serverId, session); + if (session) { + this.capturedInputs.set( + host.serverId, + selectReplicaInput(session, this.lastFocusedAgentIds.get(host.serverId) ?? null), + ); + } } } start(): void { if (this.unsubscribe) return; + const changedBeforeSubscription = this.captureSessions(); this.unsubscribe = useSessionStore.subscribe((state) => { if (this.activeServerIds.size === 0) return; + let changed = false; for (const serverId of this.activeServerIds) { - const focusedAgentId = state.sessions[serverId]?.focusedAgentId; - if (focusedAgentId) this.lastFocusedAgentIds.set(serverId, focusedAgentId); + const session = state.sessions[serverId]; + if (session && this.captureHost(serverId, session)) changed = true; } - this.schedulePersist(); + if (changed) this.schedulePersist(); }); - if (this.needsPersist) this.schedulePersist(); + if (changedBeforeSubscription || this.needsPersist) this.schedulePersist(); } reconcileServerId(oldServerId: string, newServerId: string): void { @@ -349,102 +721,108 @@ export class ReplicaCache { this.lastFocusedAgentIds.delete(oldServerId); this.lastFocusedAgentIds.set(newServerId, focusedAgentId); } - const capturedSession = this.capturedSessions.get(oldServerId); - if (capturedSession) { - this.capturedSessions.delete(oldServerId); - this.capturedSessions.set(newServerId, capturedSession); + const capturedInput = this.capturedInputs.get(oldServerId); + if (capturedInput) { + this.capturedInputs.delete(oldServerId); + this.capturedInputs.set(newServerId, capturedInput); } if (this.activeServerIds.delete(oldServerId)) this.activeServerIds.add(newServerId); this.needsPersist = true; this.schedulePersist(); } + readDirectoryCheckpoint(serverId: string): unknown { + return this.storedHosts.get(serverId)?.directorySync; + } + + writeDirectoryCheckpoint(serverId: string, checkpoint: unknown): void { + let stored = this.storedHosts.get(serverId); + if (!stored) { + const session = useSessionStore.getState().sessions[serverId]; + if (!session) return; + const input = selectReplicaInput(session, this.lastFocusedAgentIds.get(serverId) ?? null); + this.capturedInputs.set(serverId, input); + stored = serializeHost(serverId, input); + } + this.storedHosts.delete(serverId); + this.storedHosts.set(serverId, { ...stored, directorySync: checkpoint }); + this.schedulePersist(); + } + async flush(): Promise { if (this.persistTimer) { clearTimeout(this.persistTimer); this.persistTimer = null; } this.captureSessions(); - const { payload } = this.buildBoundedPayload(); + const bounded = this.buildBoundedPayload(); this.needsPersist = false; + if (!bounded) { + await this.clearInvalidCache(); + return; + } const write = this.writeQueue .catch(() => undefined) - .then(() => this.storage.setItem(STORAGE_KEY, payload)); + .then(() => this.storage.setItem(STORAGE_KEY, bounded.payload)); this.writeQueue = write; await write.catch(() => undefined); } - private captureSessions(): void { + private captureSessions(): boolean { const sessions = useSessionStore.getState().sessions; + let changed = false; for (const serverId of this.activeServerIds) { const session = sessions[serverId]; if (!session) continue; - if (this.capturedSessions.get(serverId) === session) continue; - this.capturedSessions.set(serverId, session); - if (session.focusedAgentId) { - this.lastFocusedAgentIds.set(serverId, session.focusedAgentId); - } - const focusedAgentId = this.lastFocusedAgentIds.get(serverId) ?? null; - const focusedAgent = focusedAgentId ? session.agents.get(focusedAgentId) : undefined; - const focusedWorkspace = focusedAgent - ? ((focusedAgent.workspaceId - ? session.workspaces.get(focusedAgent.workspaceId) - : undefined) ?? - Array.from(session.workspaces.values()).find( - (workspace) => workspace.workspaceDirectory === focusedAgent.cwd, - )) - : undefined; - const timelineState = focusedAgentId - ? selectAgentTimelineState(session, focusedAgentId) - : { status: "cold" as const }; - const items = - timelineState.status === "cold" - ? undefined - : timelineState.items.filter( - (item) => item.kind !== "user_message" || !isUnreconciledLocalUserMessage(item), - ); - const timeline = - focusedAgent && items - ? { - agentId: focusedAgent.id, - items: encodeDates(items.slice(-MAX_TIMELINE_ITEMS)), - } - : null; - const stored: StoredHost = { - serverId, - agents: focusedAgent ? [serializeAgent(focusedAgent)] : [], - workspaces: focusedWorkspace ? [serializeWorkspace(focusedWorkspace)] : [], - projects: focusedWorkspace - ? [session.projects.get(focusedWorkspace.projectId)].flatMap((project) => - project ? [serializeProject(project)] : [], - ) - : [], - emptyProjects: [], - timeline, - }; - this.storedHosts.delete(serverId); - this.storedHosts.set(serverId, stored); + if (this.captureHost(serverId, session)) changed = true; } + return changed; } - private buildBoundedPayload(): { payload: string; evicted: boolean } { + private captureHost(serverId: string, session: SessionState): boolean { + if (session.focusedAgentId) { + this.lastFocusedAgentIds.set(serverId, session.focusedAgentId); + } + const input = selectReplicaInput(session, this.lastFocusedAgentIds.get(serverId) ?? null); + const previous = this.capturedInputs.get(serverId); + if (previous && replicaInputsEqual(previous, input)) return false; + + this.capturedInputs.set(serverId, input); + const directorySync = this.storedHosts.get(serverId)?.directorySync; + this.storedHosts.delete(serverId); + this.storedHosts.set(serverId, serializeHost(serverId, input, directorySync)); + return true; + } + + private buildBoundedPayload(): { payload: string; evicted: boolean } | null { let evicted = false; let payload = this.serialize(); + if (payload === null) return null; while (Buffer.byteLength(payload, "utf8") > this.maxBytes && this.storedHosts.size > 0) { const oldestServerId = this.storedHosts.keys().next().value; if (oldestServerId === undefined) break; this.storedHosts.delete(oldestServerId); evicted = true; payload = this.serialize(); + if (payload === null) return null; } return { payload, evicted }; } - private serialize(): string { - return JSON.stringify({ + private serialize(): string | null { + const cache = StoredCacheSchema.safeParse({ version: CACHE_VERSION, hosts: Array.from(this.storedHosts.values()), }); + return cache.success ? JSON.stringify(cache.data) : null; + } + + private async clearInvalidCache(): Promise { + try { + await this.storage.removeItem(STORAGE_KEY); + } catch { + // A storage failure must not turn invalid cached data into application state. + } } private schedulePersist(): void { diff --git a/packages/app/src/runtime/replica-cache/storage.ts b/packages/app/src/runtime/replica-cache/storage.ts new file mode 100644 index 0000000000..e4bc220e24 --- /dev/null +++ b/packages/app/src/runtime/replica-cache/storage.ts @@ -0,0 +1,4 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import type { ReplicaCacheStorage } from "./index"; + +export const replicaCacheStorage: ReplicaCacheStorage = AsyncStorage; diff --git a/packages/app/src/runtime/replica-cache/storage.web.ts b/packages/app/src/runtime/replica-cache/storage.web.ts new file mode 100644 index 0000000000..ef2ef68960 --- /dev/null +++ b/packages/app/src/runtime/replica-cache/storage.web.ts @@ -0,0 +1,70 @@ +import type { ReplicaCacheStorage } from "./index"; + +const DATABASE_NAME = "paseo-replica-cache"; +const STORE_NAME = "key-value"; + +let databasePromise: Promise | null = null; + +function openDatabase(): Promise { + databasePromise ??= new Promise((resolve, reject) => { + const request = indexedDB.open(DATABASE_NAME, 1); + request.addEventListener("upgradeneeded", () => { + if (!request.result.objectStoreNames.contains(STORE_NAME)) { + request.result.createObjectStore(STORE_NAME); + } + }); + request.addEventListener("success", () => resolve(request.result)); + request.addEventListener("error", () => + reject(request.error ?? new Error("Failed to open replica cache")), + ); + request.addEventListener("blocked", () => + reject(new Error("Replica cache database upgrade was blocked")), + ); + }); + return databasePromise; +} + +async function getItem(key: string): Promise { + const database = await openDatabase(); + return new Promise((resolve, reject) => { + const request = database.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(key); + request.addEventListener("success", () => + resolve(typeof request.result === "string" ? request.result : null), + ); + request.addEventListener("error", () => + reject(request.error ?? new Error("Failed to read replica cache")), + ); + }); +} + +async function setItem(key: string, value: string): Promise { + const database = await openDatabase(); + return new Promise((resolve, reject) => { + const transaction = database.transaction(STORE_NAME, "readwrite"); + transaction.objectStore(STORE_NAME).put(value, key); + transaction.addEventListener("complete", () => resolve()); + transaction.addEventListener("error", () => + reject(transaction.error ?? new Error("Failed to write replica cache")), + ); + transaction.addEventListener("abort", () => + reject(transaction.error ?? new Error("Replica cache write was aborted")), + ); + }); +} + +async function removeItem(key: string): Promise { + const database = await openDatabase(); + return new Promise((resolve, reject) => { + const transaction = database.transaction(STORE_NAME, "readwrite"); + transaction.objectStore(STORE_NAME).delete(key); + transaction.addEventListener("complete", () => resolve()); + transaction.addEventListener("error", () => + reject(transaction.error ?? new Error("Failed to remove replica cache")), + ); + transaction.addEventListener("abort", () => + reject(transaction.error ?? new Error("Replica cache removal was aborted")), + ); + }); +} + +export const replicaCacheStorage: ReplicaCacheStorage = { getItem, setItem, removeItem }; diff --git a/packages/app/src/screens/new-workspace-screen.tsx b/packages/app/src/screens/new-workspace-screen.tsx index a1ff505398..d9e9e4b245 100644 --- a/packages/app/src/screens/new-workspace-screen.tsx +++ b/packages/app/src/screens/new-workspace-screen.tsx @@ -2492,6 +2492,7 @@ export function NewWorkspaceScreen({ blurOnSubmit={true} value={terminalComposerValue} onChangeText={setTerminalPromptText} + textReplacementKey={launchFocusKey} attachments={NO_TERMINAL_ATTACHMENTS} onChangeAttachments={noopChangeAttachments} cwd={selectedSourceDirectory ?? ""} @@ -2515,7 +2516,8 @@ export function NewWorkspaceScreen({ submitBehavior="preserve-and-lock" blurOnSubmit={true} value={chatDraft.text} - onChangeText={chatDraft.setText} + onChangeText={chatDraft.editText} + textReplacementKey={chatDraft.textReplacementKey} attachments={chatDraft.attachments} attachmentScopeKeys={visibleDraftContextScopeKeys} onChangeAttachments={chatDraft.setAttachments} diff --git a/packages/app/src/screens/project-settings-screen.tsx b/packages/app/src/screens/project-settings-screen.tsx index fd9ff32403..5d9c4fba21 100644 --- a/packages/app/src/screens/project-settings-screen.tsx +++ b/packages/app/src/screens/project-settings-screen.tsx @@ -3,6 +3,7 @@ import type { TFunction } from "i18next"; import { useTranslation } from "react-i18next"; import { Pressable, Text, TextInput, View } from "react-native"; import { router } from "expo-router"; +import { useFocusEffect } from "@react-navigation/native"; import { StyleSheet } from "react-native-unistyles"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ArrowLeft, MoreVertical, Pencil, Plus } from "lucide-react-native"; @@ -200,6 +201,12 @@ function ProjectSettingsBody({ queryFn: () => client.readProjectConfig(selectedHost.repoRoot), retry: false, }); + const refetchProjectConfig = readQuery.refetch; + useFocusEffect( + useCallback(() => { + void refetchProjectConfig(); + }, [refetchProjectConfig]), + ); const data = readQuery.data; const supportsCustomIcon = useHostFeature(selectedHost.serverId, "projectCustomIcon"); @@ -240,6 +247,8 @@ function ProjectSettingsBody({ ); const loadedConfig: PaseoConfigRaw | null = data?.ok ? (data.config ?? {}) : null; const loadedRevision: PaseoConfigRevision | null = data?.ok ? data.revision : null; + const hasUncommittedWorktreeSetupChanges = + data?.ok === true && data.hasUncommittedWorktreeSetupChanges === true; const readError: ProjectConfigRpcError | null = data && !data.ok ? data.error : null; const handleReload = useCallback(() => { @@ -290,6 +299,7 @@ function ProjectSettingsBody({ readQuery, loadedConfig, loadedRevision, + hasUncommittedWorktreeSetupChanges, readError, selectedHost, queryKey, @@ -305,6 +315,7 @@ interface RenderContentInput { readQuery: ReturnType>; loadedConfig: PaseoConfigRaw | null; loadedRevision: PaseoConfigRevision | null; + hasUncommittedWorktreeSetupChanges: boolean; readError: ProjectConfigRpcError | null; selectedHost: ProjectHostEntry; queryKey: readonly [string, string, string]; @@ -317,6 +328,7 @@ function renderContent({ readQuery, loadedConfig, loadedRevision, + hasUncommittedWorktreeSetupChanges, readError, selectedHost, queryKey, @@ -358,6 +370,7 @@ function renderContent({ key={formKey} baseConfig={loadedConfig} revision={loadedRevision} + hasUncommittedWorktreeSetupChanges={hasUncommittedWorktreeSetupChanges} repoRoot={selectedHost.repoRoot} queryKey={queryKey} client={client} @@ -438,6 +451,7 @@ function errorToDetail(error: unknown): string | null { interface ProjectConfigFormProps { baseConfig: PaseoConfigRaw; revision: PaseoConfigRevision | null; + hasUncommittedWorktreeSetupChanges: boolean; repoRoot: string; queryKey: readonly [string, string, string]; client: DaemonClient; @@ -447,6 +461,7 @@ interface ProjectConfigFormProps { function ProjectConfigForm({ baseConfig, revision, + hasUncommittedWorktreeSetupChanges, repoRoot, queryKey, client, @@ -479,6 +494,11 @@ function ProjectConfigForm({ revision: result.revision, requestId: "local-cache", repoRoot, + ...(result.hasUncommittedWorktreeSetupChanges === undefined + ? {} + : { + hasUncommittedWorktreeSetupChanges: result.hasUncommittedWorktreeSetupChanges, + }), }); setWriteError(null); queryClient.invalidateQueries({ queryKey: ["projects"] }); @@ -660,6 +680,13 @@ function ProjectConfigForm({ testID="worktree-setup-section" trailing={setupDocsLink} > + {hasUncommittedWorktreeSetupChanges ? ( + + ) : null} ; case "agents": return ; + case "metadata": + return ; case "workspaces": return ; case "providers": diff --git a/packages/app/src/screens/settings/appearance/appearance-section.tsx b/packages/app/src/screens/settings/appearance/appearance-section.tsx index b6516bf901..8dfda23538 100644 --- a/packages/app/src/screens/settings/appearance/appearance-section.tsx +++ b/packages/app/src/screens/settings/appearance/appearance-section.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { Fragment, useCallback, useEffect, useMemo, useState } from "react"; import type { TFunction } from "i18next"; import { useTranslation } from "react-i18next"; import { Text, TextInput, View, type PressableStateCallbackType } from "react-native"; @@ -32,6 +32,7 @@ import { DEFAULT_MONO_FONT_STACK, DEFAULT_UI_FONT_STACK, ICON_SIZE, + THEME_OPTIONS, THEME_SWATCHES, type Theme, } from "@/styles/theme"; @@ -53,28 +54,9 @@ const ThemedChevronDown = withUnistyles(ChevronDown); const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); function getThemeLabel(t: TFunction, value: AppSettings["theme"]): string { - const labelKeys: Record = { - light: "settings.appearance.theme.options.light", - dark: "settings.appearance.theme.options.dark", - zinc: "settings.appearance.theme.options.zinc", - midnight: "settings.appearance.theme.options.midnight", - claude: "settings.appearance.theme.options.claude", - ghostty: "settings.appearance.theme.options.ghostty", - pureBlack: "settings.appearance.theme.options.pureBlack", - auto: "settings.appearance.theme.options.auto", - }; - return t(labelKeys[value]); + return t(`settings.appearance.theme.options.${value}`); } -const PRIMARY_THEMES: readonly AppSettings["theme"][] = ["light", "dark", "auto"]; -const DARK_VARIANT_THEMES: readonly AppSettings["theme"][] = [ - "zinc", - "midnight", - "claude", - "ghostty", - "pureBlack", -]; - // Platform default stacks can be the bare native tokens ("normal"/"monospace"); // those read as a bug, so show a human label in the placeholder instead. const BARE_DEFAULT_STACKS: ReadonlySet = new Set(["normal", "monospace"]); @@ -169,23 +151,21 @@ function ThemeRow({ value, onChange }: ThemeRowProps) { - {PRIMARY_THEMES.map((themeValue) => ( - - ))} - - {DARK_VARIANT_THEMES.map((themeValue) => ( - - ))} + {THEME_OPTIONS.map((option, index) => { + const previousOption = THEME_OPTIONS[index - 1]; + return ( + + {previousOption && previousOption.group !== option.group ? ( + + ) : null} + + + ); + })} diff --git a/packages/app/src/screens/settings/appearance/apply-appearance.test.ts b/packages/app/src/screens/settings/appearance/apply-appearance.test.ts index 3b6afddbd9..3ce041fbd5 100644 --- a/packages/app/src/screens/settings/appearance/apply-appearance.test.ts +++ b/packages/app/src/screens/settings/appearance/apply-appearance.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { darkHighlightColors, resolveSyntaxColors } from "@getpaseo/highlight"; -import { DEFAULT_UI_FONT_STACK } from "@/styles/theme"; +import { DEFAULT_UI_FONT_STACK, REGISTERED_THEMES } from "@/styles/theme"; import { applyAppearance, type AppearanceInput } from "./apply-appearance"; // Override the global react-native-unistyles mock (vitest.setup.ts) so that @@ -14,16 +14,7 @@ const { runtime, updateTheme } = vi.hoisted(() => { }); vi.mock("react-native-unistyles", () => ({ UnistylesRuntime: runtime })); -// The registered Unistyles theme keys, in the order applyAppearance patches them. -const ALL_THEME_KEYS = [ - "light", - "dark", - "darkZinc", - "darkMidnight", - "darkClaude", - "darkGhostty", - "darkPureBlack", -] as const; +const ALL_THEME_KEYS = Object.keys(REGISTERED_THEMES); // The signature of the updater passed to UnistylesRuntime.updateTheme. type ThemeUpdater = (theme: FakeTheme) => FakeTheme; diff --git a/packages/app/src/screens/settings/appearance/apply-appearance.ts b/packages/app/src/screens/settings/appearance/apply-appearance.ts index 90fe782fdb..9f469d5c8a 100644 --- a/packages/app/src/screens/settings/appearance/apply-appearance.ts +++ b/packages/app/src/screens/settings/appearance/apply-appearance.ts @@ -4,22 +4,12 @@ import { DEFAULT_UI_FONT_STACK, DEFAULT_MONO_FONT_STACK, FONT_SIZE, + REGISTERED_THEMES, type Theme, } from "@/styles/theme"; import { applyRootUiFont } from "./apply-root-font"; -// All registered Unistyles keys — pinned literal (greppable, type-checked). -// The `as const` element types stay assignable to -// `UnistylesRuntime.updateTheme`'s first argument as themes are added. -const ALL_THEME_KEYS = [ - "light", - "dark", - "darkZinc", - "darkMidnight", - "darkClaude", - "darkGhostty", - "darkPureBlack", -] as const; +const ALL_THEME_KEYS = Object.keys(REGISTERED_THEMES) as (keyof typeof REGISTERED_THEMES)[]; // The UI font size at which the FONT_SIZE ramp is authored (1.0 scale factor). const BASE_UI_REFERENCE = FONT_SIZE.base; // 16 diff --git a/packages/app/src/screens/settings/host-page.tsx b/packages/app/src/screens/settings/host-page.tsx index 9a2a413345..85c0ac25df 100644 --- a/packages/app/src/screens/settings/host-page.tsx +++ b/packages/app/src/screens/settings/host-page.tsx @@ -21,6 +21,7 @@ import { getTerminalProfileIcon, DEFAULT_TERMINAL_PROFILES, } from "@getpaseo/protocol/terminal-profiles"; +import { AgentProfilesSection } from "@/agent-profiles"; import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet"; import { SettingsTextAreaCard } from "@/components/settings-textarea"; import { Alert as InlineAlert } from "@/components/ui/alert"; @@ -288,6 +289,7 @@ export function HostAgentsPage({ serverId }: { serverId: string }) { {t("settings.host.agents.unavailable")} )} + ); } diff --git a/packages/app/src/screens/settings/metadata-generation-page.tsx b/packages/app/src/screens/settings/metadata-generation-page.tsx new file mode 100644 index 0000000000..3576c23839 --- /dev/null +++ b/packages/app/src/screens/settings/metadata-generation-page.tsx @@ -0,0 +1,181 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Alert, Text, View } from "react-native"; +import { useTranslation } from "react-i18next"; +import { StyleSheet } from "react-native-unistyles"; +import type { AgentProvider } from "@getpaseo/protocol/agent-types"; +import { CombinedModelSelector } from "@/components/combined-model-selector"; +import { ExternalLink } from "@/components/ui/external-link"; +import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { SegmentedControl } from "@/components/ui/segmented-control"; +import { useDaemonConfig } from "@/hooks/use-daemon-config"; +import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot"; +import { buildSelectableProviderSelectorProviders } from "@/provider-selection/provider-selection"; +import { SettingsSection } from "@/screens/settings/settings-section"; +import { settingsStyles } from "@/styles/settings"; + +const METADATA_GENERATION_DOCS_URL = "https://paseo.sh/docs/metadata-generation"; +type SelectionMode = "automatic" | "preferred"; + +export function MetadataGenerationPage({ serverId }: { serverId: string }) { + const { t } = useTranslation(); + const { config, isLoading: isConfigLoading, patchConfig } = useDaemonConfig(serverId); + const snapshot = useProvidersSnapshot(serverId); + const providers = useMemo( + () => buildSelectableProviderSelectorProviders(snapshot.entries), + [snapshot.entries], + ); + const configuredProviders = config?.metadataGeneration.providers; + const configuredProvider = configuredProviders?.[0] ?? null; + const savedMode: SelectionMode = configuredProvider ? "preferred" : "automatic"; + const [draftMode, setDraftMode] = useState(null); + const [isSaving, setIsSaving] = useState(false); + const mode = draftMode ?? savedMode; + + useEffect(() => { + setDraftMode(null); + }, [configuredProvider?.model, configuredProvider?.provider]); + + const modeOptions = useMemo( + () => [ + { value: "automatic" as const, label: t("settings.metadataGeneration.automatic") }, + { value: "preferred" as const, label: t("settings.metadataGeneration.preferred") }, + ], + [t], + ); + + const saveProviders = useCallback( + async (providersPatch: { provider: string; model?: string }[]) => { + setIsSaving(true); + try { + await patchConfig({ metadataGeneration: { providers: providersPatch } }); + } catch (error) { + setDraftMode(null); + Alert.alert( + t("settings.metadataGeneration.saveError"), + error instanceof Error ? error.message : String(error), + ); + } finally { + setIsSaving(false); + } + }, + [patchConfig, t], + ); + + const handleModeChange = useCallback( + (next: SelectionMode) => { + setDraftMode(next); + if (next === "automatic") { + void saveProviders([]); + } + }, + [saveProviders], + ); + + const handleModelSelect = useCallback( + (provider: AgentProvider, model: string) => { + setDraftMode("preferred"); + void saveProviders([ + { provider, ...(model ? { model } : {}) }, + ...(configuredProviders?.slice(1) ?? []), + ]); + }, + [configuredProviders, saveProviders], + ); + + const handleSelectorOpen = useCallback(() => { + snapshot.refetchIfStale(configuredProvider?.provider); + }, [configuredProvider?.provider, snapshot]); + const handleRetryProvider = useCallback( + (provider: AgentProvider) => snapshot.refresh([provider]), + [snapshot], + ); + const docsLink = useMemo( + () => ( + + ), + [t], + ); + + if (isConfigLoading || !config) { + return ( + + + + ); + } + + return ( + + {t("settings.metadataGeneration.description")} + + + + + {t("settings.metadataGeneration.selection")} + + + {mode === "automatic" + ? t("settings.metadataGeneration.automaticHint") + : t("settings.metadataGeneration.preferredHint")} + + + + + {mode === "preferred" ? ( + + + {t("settings.metadataGeneration.model")} + + {t("settings.metadataGeneration.fallbackHint")} + + + + + ) : null} + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + description: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.xs, + lineHeight: theme.fontSize.xs * 1.45, + marginHorizontal: theme.spacing[1], + }, + loading: { + alignItems: "center", + justifyContent: "center", + minHeight: 180, + }, + spinnerColor: { + color: theme.colors.foregroundMuted, + }, +})); diff --git a/packages/app/src/screens/workspace/workspace-bulk-close.test.ts b/packages/app/src/screens/workspace/workspace-bulk-close.test.ts index efa3b333b2..4dfe308c14 100644 --- a/packages/app/src/screens/workspace/workspace-bulk-close.test.ts +++ b/packages/app/src/screens/workspace/workspace-bulk-close.test.ts @@ -42,7 +42,8 @@ describe("workspace bulk close helpers", () => { ]); expect(groups).toEqual({ - agentTabs: [{ tabId: "agent_a1", agentId: "a1" }], + archiveAgentTabs: [{ tabId: "agent_a1", agentId: "a1" }], + layoutOnlyAgentTabs: [], terminalTabs: [{ tabId: "terminal_t1", terminalId: "t1" }], otherTabs: [ { @@ -53,6 +54,21 @@ describe("workspace bulk close helpers", () => { }); }); + it("separates subagent tabs from agents that archive on close", () => { + const groups = classifyBulkClosableTabs( + [makeAgentTab("root"), makeAgentTab("child")], + (agentId) => (agentId === "child" ? "layout-only" : "archive"), + ); + + expect(groups).toMatchObject({ + archiveAgentTabs: [{ tabId: "agent_root", agentId: "root" }], + layoutOnlyAgentTabs: [{ tabId: "agent_child", agentId: "child" }], + }); + expect(buildBulkCloseConfirmationMessage(groups)).toBe( + "This will archive 1 agent(s) and close 1 tab(s).", + ); + }); + it("describes mixed destructive bulk close operations in the confirmation copy", () => { const message = buildBulkCloseConfirmationMessage( classifyBulkClosableTabs([ @@ -106,6 +122,7 @@ describe("workspace bulk close helpers", () => { closeWorkspaceTabWithCleanup: (input) => { cleanupCalls.push(input); }, + closeLayoutOnlyAgent: async () => {}, logLabel: "all tabs", }); @@ -152,6 +169,7 @@ describe("workspace bulk close helpers", () => { closeWorkspaceTabWithCleanup: (input) => { cleanupCalls.push(input); }, + closeLayoutOnlyAgent: async () => {}, warn, logLabel: "others", }); @@ -166,4 +184,31 @@ describe("workspace bulk close helpers", () => { { tabId: "file_/repo/README.md", target: { kind: "file", path: "/repo/README.md" } }, ]); }); + + it("closes subagent tabs without sending them to closeItems", async () => { + const groups = classifyBulkClosableTabs( + [makeAgentTab("root"), makeAgentTab("child")], + (agentId) => (agentId === "child" ? "layout-only" : "archive"), + ); + const closeItems = vi.fn(async () => ({ agents: [], terminals: [], requestId: "req-1" })); + const preparedSubagents: string[] = []; + const cleanedTabs: string[] = []; + + await closeBulkWorkspaceTabs({ + groups, + client: { closeItems }, + closeTab: async (_tabId, action) => action(), + closeLayoutOnlyAgent: async (agentId) => { + preparedSubagents.push(agentId); + }, + closeWorkspaceTabWithCleanup: ({ tabId }) => { + cleanedTabs.push(tabId); + }, + logLabel: "others", + }); + + expect(closeItems).toHaveBeenCalledWith({ agentIds: ["root"], terminalIds: [] }); + expect(preparedSubagents).toEqual(["child"]); + expect(cleanedTabs).toEqual(["agent_child", "agent_root"]); + }); }); diff --git a/packages/app/src/screens/workspace/workspace-bulk-close.ts b/packages/app/src/screens/workspace/workspace-bulk-close.ts index fc986de41c..8e9cf9dad1 100644 --- a/packages/app/src/screens/workspace/workspace-bulk-close.ts +++ b/packages/app/src/screens/workspace/workspace-bulk-close.ts @@ -3,7 +3,8 @@ import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs- import { i18n } from "@/i18n/i18next"; export interface BulkClosableTabGroups { - agentTabs: Array<{ tabId: string; agentId: string }>; + archiveAgentTabs: Array<{ tabId: string; agentId: string }>; + layoutOnlyAgentTabs: Array<{ tabId: string; agentId: string }>; terminalTabs: Array<{ tabId: string; terminalId: string }>; otherTabs: Array<{ tabId: string; target: WorkspaceTabDescriptor["target"] }>; } @@ -43,20 +44,30 @@ interface CloseBulkWorkspaceTabsInput { groups: BulkClosableTabGroups; closeTab: (tabId: string, action: () => Promise) => Promise; closeWorkspaceTabWithCleanup: (input: CloseWorkspaceTabWithCleanupInput) => void; + closeLayoutOnlyAgent: (agentId: string) => Promise; logLabel: string; warn?: (message: string, payload: object) => void; } -export function classifyBulkClosableTabs(tabs: WorkspaceTabDescriptor[]): BulkClosableTabGroups { +export function classifyBulkClosableTabs( + tabs: WorkspaceTabDescriptor[], + resolveAgentCloseKind: (agentId: string) => "archive" | "layout-only" = () => "archive", +): BulkClosableTabGroups { const groups: BulkClosableTabGroups = { - agentTabs: [], + archiveAgentTabs: [], + layoutOnlyAgentTabs: [], terminalTabs: [], otherTabs: [], }; for (const tab of tabs) { if (tab.target.kind === "agent") { - groups.agentTabs.push({ tabId: tab.tabId, agentId: tab.target.agentId }); + const agentTab = { tabId: tab.tabId, agentId: tab.target.agentId }; + if (resolveAgentCloseKind(tab.target.agentId) === "layout-only") { + groups.layoutOnlyAgentTabs.push(agentTab); + } else { + groups.archiveAgentTabs.push(agentTab); + } continue; } if (tab.target.kind === "terminal") { @@ -73,49 +84,73 @@ export function buildBulkCloseConfirmationMessage( input: BulkClosableTabGroups, labels: BulkCloseConfirmationLabels = DEFAULT_BULK_CLOSE_CONFIRMATION_LABELS, ): string { - const { agentTabs, terminalTabs, otherTabs } = input; - if (agentTabs.length > 0 && terminalTabs.length > 0 && otherTabs.length > 0) { + const { archiveAgentTabs, layoutOnlyAgentTabs, terminalTabs, otherTabs } = input; + const tabCount = layoutOnlyAgentTabs.length + otherTabs.length; + if (archiveAgentTabs.length > 0 && terminalTabs.length > 0 && tabCount > 0) { return labels.all({ - agents: agentTabs.length, + agents: archiveAgentTabs.length, terminals: terminalTabs.length, - tabs: otherTabs.length, + tabs: tabCount, }); } - if (agentTabs.length > 0 && terminalTabs.length > 0) { + if (archiveAgentTabs.length > 0 && terminalTabs.length > 0) { return labels.agentsAndTerminals({ - agents: agentTabs.length, + agents: archiveAgentTabs.length, terminals: terminalTabs.length, }); } - if (terminalTabs.length > 0 && otherTabs.length > 0) { + if (terminalTabs.length > 0 && tabCount > 0) { return labels.terminalsAndTabs({ terminals: terminalTabs.length, - tabs: otherTabs.length, + tabs: tabCount, }); } - if (agentTabs.length > 0 && otherTabs.length > 0) { + if (archiveAgentTabs.length > 0 && tabCount > 0) { return labels.agentsAndTabs({ - agents: agentTabs.length, - tabs: otherTabs.length, + agents: archiveAgentTabs.length, + tabs: tabCount, }); } if (terminalTabs.length > 0) { return labels.terminals({ terminals: terminalTabs.length }); } - if (otherTabs.length > 0) { - return labels.tabs({ tabs: otherTabs.length }); + if (tabCount > 0) { + return labels.tabs({ tabs: tabCount }); } - return labels.agents({ agents: agentTabs.length }); + return labels.agents({ agents: archiveAgentTabs.length }); } export async function closeBulkWorkspaceTabs(input: CloseBulkWorkspaceTabsInput): Promise { - const { client, groups, closeTab, closeWorkspaceTabWithCleanup, logLabel, warn } = input; - const hasDestructiveTabs = groups.agentTabs.length > 0 || groups.terminalTabs.length > 0; + const { + client, + groups, + closeTab, + closeWorkspaceTabWithCleanup, + closeLayoutOnlyAgent, + logLabel, + warn, + } = input; + const hasDestructiveTabs = groups.archiveAgentTabs.length > 0 || groups.terminalTabs.length > 0; + + for (const { tabId, agentId } of groups.layoutOnlyAgentTabs) { + await closeTab(tabId, async () => { + try { + await closeLayoutOnlyAgent(agentId); + } catch (error) { + warn?.(`[WorkspaceScreen] Failed to close subagent tab ${logLabel}`, { error, agentId }); + return; + } + closeWorkspaceTabWithCleanup({ + tabId, + target: { kind: "agent", agentId }, + }); + }); + } if (hasDestructiveTabs && client) { void client .closeItems({ - agentIds: groups.agentTabs.map((tab) => tab.agentId), + agentIds: groups.archiveAgentTabs.map((tab) => tab.agentId), terminalIds: groups.terminalTabs.map((tab) => tab.terminalId), }) .catch((error) => { @@ -127,27 +162,27 @@ export async function closeBulkWorkspaceTabs(input: CloseBulkWorkspaceTabsInput) }); } - for (const { tabId, agentId } of groups.agentTabs) { - void closeTab(tabId, async () => { - closeWorkspaceTabWithCleanup({ - tabId, - target: { kind: "agent", agentId }, - }); - }); - } - - for (const { tabId, terminalId } of groups.terminalTabs) { - void closeTab(tabId, async () => { - closeWorkspaceTabWithCleanup({ - tabId, - target: { kind: "terminal", terminalId }, - }); - }); - } - - for (const { tabId, target } of groups.otherTabs) { - void closeTab(tabId, async () => { - closeWorkspaceTabWithCleanup({ tabId, target }); - }); - } + await Promise.all([ + ...groups.archiveAgentTabs.map(({ tabId, agentId }) => + closeTab(tabId, async () => { + closeWorkspaceTabWithCleanup({ + tabId, + target: { kind: "agent", agentId }, + }); + }), + ), + ...groups.terminalTabs.map(({ tabId, terminalId }) => + closeTab(tabId, async () => { + closeWorkspaceTabWithCleanup({ + tabId, + target: { kind: "terminal", terminalId }, + }); + }), + ), + ...groups.otherTabs.map(({ tabId, target }) => + closeTab(tabId, async () => { + closeWorkspaceTabWithCleanup({ tabId, target }); + }), + ), + ]); } diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 88154f7d34..60192c5f15 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -1,4 +1,5 @@ import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { getOpenAgentTabLabel } from "@getpaseo/protocol/agent-labels"; import { memo, useCallback, @@ -67,6 +68,7 @@ import { WorkspaceOpenInEditorButton } from "@/screens/workspace/workspace-open- import { WorkspaceScriptsButton } from "@/screens/workspace/workspace-scripts-button"; import { ImportSessionSheet } from "@/components/import-session-sheet"; import { useToast } from "@/contexts/toast-context"; +import { getOrCreateClientId } from "@/utils/client-id"; import { selectIsFileExplorerOpen, usePanelStore } from "@/stores/panel-store"; import { type ExplorerCheckoutContext } from "@/stores/explorer-checkout-context"; import { traceInstant } from "@/performance/native-trace"; @@ -113,6 +115,7 @@ import { getDesktopHost } from "@/desktop/host"; import { buildProviderCommand } from "@/utils/provider-command-templates"; import { generateDraftId } from "@/stores/draft-keys"; import { resolveWorkspaceRouteId } from "@/utils/workspace-identity"; +import { useOpenAgentTabLabels } from "@/subagents/use-open-agent-tab-labels"; import { WorkspaceTabPresentationResolver, WorkspaceTabIcon, @@ -1987,6 +1990,12 @@ function WorkspaceScreenContent({ () => (workspaceLayout ? collectAllTabs(workspaceLayout.root) : EMPTY_UI_TABS), [workspaceLayout], ); + useOpenAgentTabLabels({ + client, + serverId: normalizedServerId, + tabs: uiTabs, + enabled: hasHydratedWorkspaceLayoutStore, + }); useSyncWorkspaceActiveBrowser({ workspaceLayout, isRouteFocused, @@ -2668,7 +2677,7 @@ function WorkspaceScreenContent({ const agent = useSessionStore.getState().sessions[normalizedServerId]?.agents?.get(agentId) ?? null; - const closePolicy = resolveCloseAgentTabPolicy(agent); + let closePolicy = resolveCloseAgentTabPolicy(agent); const isRunning = agent?.status === "running"; if (isRunning && closePolicy.kind === "archive-on-close") { @@ -2684,6 +2693,27 @@ function WorkspaceScreenContent({ } } + if (closePolicy.kind === "layout-only") { + const sessionClient = useSessionStore.getState().sessions[normalizedServerId]?.client; + if (!sessionClient) { + toast.error(t("common.errors.daemonClientUnavailable")); + return; + } + try { + const clientId = await getOrCreateClientId(); + await sessionClient.updateAgent(agentId, { + labels: { [getOpenAgentTabLabel(clientId)]: "false" }, + }); + const latestAgent = + useSessionStore.getState().sessions[normalizedServerId]?.agents?.get(agentId) ?? null; + closePolicy = resolveCloseAgentTabPolicy(latestAgent); + } catch (error) { + console.error("[WorkspaceScreen] Failed to close subagent tab", { error, agentId }); + toast.error(t("workspace.tabs.toasts.failedToCloseAgent")); + return; + } + } + setHoveredCloseTabKey((current) => (current === tabId ? null : current)); if (persistenceKey) { closeWorkspaceTabWithCleanup({ @@ -2700,7 +2730,15 @@ function WorkspaceScreenContent({ void archiveAgent({ serverId: normalizedServerId, agentId }).catch(() => {}); }); }, - [archiveAgent, closeTab, closeWorkspaceTabWithCleanup, normalizedServerId, persistenceKey, t], + [ + archiveAgent, + closeTab, + closeWorkspaceTabWithCleanup, + normalizedServerId, + persistenceKey, + t, + toast, + ], ); const handleClosePassiveTab = useCallback( @@ -2916,7 +2954,10 @@ function WorkspaceScreenContent({ return; } - const groups = classifyBulkClosableTabs(tabsToClose); + const groups = classifyBulkClosableTabs(tabsToClose, (agentId) => { + const agent = useSessionStore.getState().sessions[normalizedServerId]?.agents?.get(agentId); + return resolveCloseAgentTabPolicy(agent).kind === "layout-only" ? "layout-only" : "archive"; + }); const modifiedCount = tabsToClose.filter( (tab) => getPanelInstanceAttributes({ @@ -2944,6 +2985,20 @@ function WorkspaceScreenContent({ client, groups, closeTab, + closeLayoutOnlyAgent: async (agentId) => { + if (!client) { + throw new Error(t("common.errors.daemonClientUnavailable")); + } + const clientId = await getOrCreateClientId(); + await client.updateAgent(agentId, { + labels: { [getOpenAgentTabLabel(clientId)]: "false" }, + }); + const latestAgent = + useSessionStore.getState().sessions[normalizedServerId]?.agents?.get(agentId) ?? null; + if (resolveCloseAgentTabPolicy(latestAgent).kind === "archive-on-close") { + await archiveAgent({ serverId: normalizedServerId, agentId }); + } + }, closeWorkspaceTabWithCleanup: (cleanupInput) => { if (!persistenceKey) { return; @@ -2960,6 +3015,7 @@ function WorkspaceScreenContent({ setHoveredCloseTabKey((current) => (current && closedKeys.has(current) ? null : current)); }, [ + archiveAgent, bulkCloseConfirmationLabels, client, closeTab, diff --git a/packages/app/src/storage/validated-persist-storage.test.ts b/packages/app/src/storage/validated-persist-storage.test.ts new file mode 100644 index 0000000000..7255086e37 --- /dev/null +++ b/packages/app/src/storage/validated-persist-storage.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; +import type { StateStorage } from "zustand/middleware"; +import { createValidatedPersistStorage } from "./validated-persist-storage"; + +class MemoryStorage implements StateStorage { + readonly values = new Map(); + + getItem(name: string): string | null { + return this.values.get(name) ?? null; + } + + setItem(name: string, value: string): void { + this.values.set(name, value); + } + + removeItem(name: string): void { + this.values.delete(name); + } +} + +const StateSchema = z.strictObject({ enabled: z.boolean() }); + +describe("createValidatedPersistStorage", () => { + it.each([ + ["malformed JSON", "{"], + ["invalid state", JSON.stringify({ state: { enabled: "yes" }, version: 1 })], + ["unknown state fields", JSON.stringify({ state: { enabled: true, extra: true }, version: 1 })], + [ + "unknown envelope fields", + JSON.stringify({ state: { enabled: true }, version: 1, extra: true }), + ], + ])("clears %s", async (_label, stored) => { + const backing = new MemoryStorage(); + backing.values.set("settings", stored); + const storage = createValidatedPersistStorage(backing, StateSchema); + + await expect(storage.getItem("settings")).resolves.toBeNull(); + expect(backing.values.has("settings")).toBe(false); + }); + + it("returns schema-validated state", async () => { + const backing = new MemoryStorage(); + backing.values.set("settings", JSON.stringify({ state: { enabled: true }, version: 1 })); + const storage = createValidatedPersistStorage(backing, StateSchema); + + await expect(storage.getItem("settings")).resolves.toEqual({ + state: { enabled: true }, + version: 1, + }); + }); + + it("clears a value rejected by the schema before writing", async () => { + const backing = new MemoryStorage(); + backing.values.set("settings", JSON.stringify({ state: { count: 1 } })); + const storage = createValidatedPersistStorage( + backing, + z.strictObject({ count: z.number().finite() }), + ); + + await storage.setItem("settings", { state: { count: Number.NaN } }); + + expect(backing.values.has("settings")).toBe(false); + }); +}); diff --git a/packages/app/src/storage/validated-persist-storage.ts b/packages/app/src/storage/validated-persist-storage.ts new file mode 100644 index 0000000000..07459f8642 --- /dev/null +++ b/packages/app/src/storage/validated-persist-storage.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; +import type { PersistStorage, StateStorage } from "zustand/middleware"; + +export function createValidatedPersistStorage( + backingStorage: StateStorage, + stateSchema: z.ZodType, +): PersistStorage { + const envelopeSchema = z.strictObject({ + state: stateSchema, + version: z.number().int().nonnegative().optional(), + }); + + return { + getItem: async (name) => { + const raw = await backingStorage.getItem(name); + if (raw === null) return null; + + let decoded: unknown; + try { + decoded = JSON.parse(raw); + } catch { + await backingStorage.removeItem(name); + return null; + } + + const result = envelopeSchema.safeParse(decoded); + if (!result.success) { + await backingStorage.removeItem(name); + return null; + } + return result.data; + }, + setItem: async (name, value) => { + const result = envelopeSchema.safeParse(value); + if (!result.success) { + await backingStorage.removeItem(name); + return; + } + await backingStorage.setItem(name, JSON.stringify(result.data)); + }, + removeItem: (name) => backingStorage.removeItem(name), + }; +} diff --git a/packages/app/src/storage/validated-storage.test.ts b/packages/app/src/storage/validated-storage.test.ts new file mode 100644 index 0000000000..8ebd7339db --- /dev/null +++ b/packages/app/src/storage/validated-storage.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; +import { readValidatedJson, readValidatedString } from "./validated-storage"; + +class MemoryStorage { + readonly values = new Map(); + + async getItem(key: string): Promise { + return this.values.get(key) ?? null; + } + + async removeItem(key: string): Promise { + this.values.delete(key); + } +} + +describe("validated storage reads", () => { + it.each([ + ["malformed JSON", "{"], + ["wrong field type", JSON.stringify({ enabled: "yes" })], + ["unknown field", JSON.stringify({ enabled: true, surprise: true })], + ])("clears invalid JSON payloads: %s", async (_label, raw) => { + const storage = new MemoryStorage(); + storage.values.set("settings", raw); + + await expect( + readValidatedJson(storage, "settings", z.strictObject({ enabled: z.boolean() })), + ).resolves.toBeNull(); + expect(storage.values.has("settings")).toBe(false); + }); + + it("clears invalid scalar values", async () => { + const storage = new MemoryStorage(); + storage.values.set("channel", "nightly"); + + await expect( + readValidatedString(storage, "channel", z.enum(["stable", "beta"])), + ).resolves.toBeNull(); + expect(storage.values.has("channel")).toBe(false); + }); + + it("returns only schema-validated values", async () => { + const storage = new MemoryStorage(); + storage.values.set("settings", JSON.stringify({ enabled: true })); + + await expect( + readValidatedJson(storage, "settings", z.strictObject({ enabled: z.boolean() })), + ).resolves.toEqual({ enabled: true }); + expect(storage.values.has("settings")).toBe(true); + }); +}); diff --git a/packages/app/src/storage/validated-storage.ts b/packages/app/src/storage/validated-storage.ts new file mode 100644 index 0000000000..b47c92e493 --- /dev/null +++ b/packages/app/src/storage/validated-storage.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +export interface ValidatedStorage { + getItem(key: string): Promise; + removeItem(key: string): Promise; +} + +async function clearInvalidValue(storage: ValidatedStorage, key: string): Promise { + try { + await storage.removeItem(key); + } catch { + // Invalid persisted data must never cross the boundary, even when cleanup fails. + } +} + +export async function readValidatedString( + storage: ValidatedStorage, + key: string, + schema: z.ZodType, +): Promise { + const raw = await storage.getItem(key); + if (raw === null) return null; + const result = schema.safeParse(raw); + if (result.success) return result.data; + await clearInvalidValue(storage, key); + return null; +} + +export async function readValidatedJson( + storage: ValidatedStorage, + key: string, + schema: z.ZodType, +): Promise { + const raw = await storage.getItem(key); + if (raw === null) return null; + let decoded: unknown; + try { + decoded = JSON.parse(raw); + } catch { + await clearInvalidValue(storage, key); + return null; + } + const result = schema.safeParse(decoded); + if (result.success) return result.data; + await clearInvalidValue(storage, key); + return null; +} diff --git a/packages/app/src/stores/draft-store/index.ts b/packages/app/src/stores/draft-store/index.ts index 5285170415..4b89ee98c0 100644 --- a/packages/app/src/stores/draft-store/index.ts +++ b/packages/app/src/stores/draft-store/index.ts @@ -1,5 +1,5 @@ import { create } from "zustand"; -import { createJSONStorage, persist } from "zustand/middleware"; +import { persist } from "zustand/middleware"; import AsyncStorage from "@react-native-async-storage/async-storage"; import type { AttachmentMetadata, WorkspaceFileComposerAttachment } from "@/attachments/types"; import { appendWorkspaceFileAttachment } from "@/attachments/workspace-file"; @@ -27,8 +27,14 @@ import { type DraftRecord, type DraftStoreState, } from "./state"; -import { migrateDraftInput, migratePersistedState, type MigrateLegacyImages } from "./migration"; +import { + migrateDraftInput, + migratePersistedState, + type MigrateLegacyImages, + PersistedDraftStoreSchema, +} from "./migration"; import { createDraftPersistStorage } from "./persistence"; +import { createValidatedPersistStorage } from "@/storage/validated-persist-storage"; export type { DraftInput, DraftLifecycleState } from "./state"; @@ -58,7 +64,7 @@ type DraftStore = DraftStoreState & DraftStoreRuntimeState & DraftStoreActions; let gcScheduled = false; const draftPersistStorage = createDraftPersistStorage( - createJSONStorage(() => AsyncStorage), + createValidatedPersistStorage(AsyncStorage, PersistedDraftStoreSchema), ); export function flushDraftPersistStorage(): Promise { @@ -414,12 +420,11 @@ export const useDraftStore = create()( version: DRAFT_STORE_VERSION, storage: draftPersistStorage, partialize: ({ drafts, createModalDraft }) => ({ drafts, createModalDraft }), - migrate: (persistedState) => { - return migratePersistedState(persistedState, { + migrate: (state) => + migratePersistedState(state, { migrateLegacyImages, nowMs: Date.now(), - }); - }, + }), onRehydrateStorage: () => { return () => { void migrateAllLegacyDrafts(); diff --git a/packages/app/src/stores/draft-store/migration.test.ts b/packages/app/src/stores/draft-store/migration.test.ts index 6ef20097fa..218e74c4e0 100644 --- a/packages/app/src/stores/draft-store/migration.test.ts +++ b/packages/app/src/stores/draft-store/migration.test.ts @@ -1,11 +1,31 @@ import { describe, expect, it } from "vitest"; +import type { StateStorage } from "zustand/middleware"; import type { ComposerAttachment, UserComposerAttachment } from "@/attachments/types"; -import { migratePersistedState, type MigrateLegacyImages } from "./migration"; +import { createValidatedPersistStorage } from "@/storage/validated-persist-storage"; +import { + migratePersistedState, + type MigrateLegacyImages, + PersistedDraftStoreSchema, +} from "./migration"; import { isAttachmentMetadata, type DraftRecord } from "./state"; const passThroughMigrateLegacyImages: MigrateLegacyImages = async (images) => images.filter(isAttachmentMetadata); +function createMemoryStorage(): StateStorage & { values: Map } { + const values = new Map(); + return { + values, + getItem: async (key) => values.get(key) ?? null, + setItem: async (key, value) => { + values.set(key, value); + }, + removeItem: async (key) => { + values.delete(key); + }, + }; +} + function activeDraft( text: string, updatedAt: number, @@ -96,6 +116,30 @@ function workspaceReviewAttachment(): Extract { + it("keeps a supported legacy draft for migration", async () => { + const backing = createMemoryStorage(); + const legacyState = { + drafts: { + "draft:unsent": { + text: "do not lose this", + attachments: [], + }, + }, + createModalDraft: null, + }; + backing.values.set("paseo-drafts", JSON.stringify({ state: legacyState, version: 4 })); + const storage = createValidatedPersistStorage(backing, PersistedDraftStoreSchema); + + const stored = await storage.getItem("paseo-drafts"); + const migrated = await migratePersistedState(stored?.state, { + migrateLegacyImages: passThroughMigrateLegacyImages, + nowMs: 1, + }); + + expect(migrated.drafts["draft:unsent"]?.input.text).toBe("do not lose this"); + expect(backing.values.has("paseo-drafts")).toBe(true); + }); + it("promotes the newest legacy New Workspace draft into the singleton surface", async () => { const forkDraft = activeDraft("fork context", 1700000000003); const agentDraft = activeDraft("agent prompt", 1700000000004); @@ -242,25 +286,35 @@ describe("draft-store migration", () => { }); }); - it("rejects workspace review attachments from migrated draft attachments", async () => { - const migrated = await migratePersistedState( - { - drafts: { - "agent:server:agent": { - input: { - text: "hello", - attachments: [workspaceReviewAttachment()], - }, - lifecycle: "active", - updatedAt: 1700000000001, - version: 2, + it("drops a legacy workspace review attachment without deleting its draft", async () => { + const backing = createMemoryStorage(); + const persistedState = { + drafts: { + "agent:server:agent": { + input: { + text: "hello", + attachments: [workspaceReviewAttachment()], }, + lifecycle: "active", + updatedAt: 1700000000001, + version: 2, }, - createModalDraft: null, }, - { migrateLegacyImages: passThroughMigrateLegacyImages, nowMs: 1700000000002 }, - ); + createModalDraft: null, + }; + backing.values.set("paseo-drafts", JSON.stringify({ state: persistedState, version: 4 })); + const storage = createValidatedPersistStorage(backing, PersistedDraftStoreSchema); - expect(migrated.drafts["agent:server:agent"]?.input.attachments).toEqual([]); + const stored = await storage.getItem("paseo-drafts"); + const migrated = await migratePersistedState(stored?.state, { + migrateLegacyImages: passThroughMigrateLegacyImages, + nowMs: 1700000000002, + }); + + expect(migrated.drafts["agent:server:agent"]?.input).toEqual({ + text: "hello", + attachments: [], + }); + expect(backing.values.has("paseo-drafts")).toBe(true); }); }); diff --git a/packages/app/src/stores/draft-store/migration.ts b/packages/app/src/stores/draft-store/migration.ts index fc21087ca2..7d1756fc75 100644 --- a/packages/app/src/stores/draft-store/migration.ts +++ b/packages/app/src/stores/draft-store/migration.ts @@ -1,11 +1,12 @@ import type { AttachmentMetadata, UserComposerAttachment } from "@/attachments/types"; import { isLegacyNewWorkspaceDraftKey, NEW_WORKSPACE_DRAFT_KEY } from "@/stores/draft-keys"; +import { z } from "zod"; import { - isAttachmentMetadata, - isLegacyDraftImage, - isUserComposerAttachment, + AttachmentMetadataSchema, + LegacyDraftImageSchema, normalizeAttachmentMetadata, normalizeComposerAttachment, + UserComposerAttachmentSchema, type CanonicalDraftInput, type DraftLifecycleState, type DraftRecord, @@ -13,28 +14,108 @@ import { type PersistedDraftImage, } from "./state"; +const LegacyPullRequestItemSchema = z.strictObject({ + kind: z.literal("pr"), + forge: z.string().optional(), + number: z.number(), + title: z.string(), + url: z.string(), + state: z.string(), + body: z.string().nullable(), + labels: z.array(z.string()), + projectPath: z.string().optional(), + baseRefName: z.string().nullable().optional(), + headRefName: z.string().nullable().optional(), + updatedAt: z.string().optional(), +}); +const LegacyGithubPrAttachmentSchema = z.strictObject({ + kind: z.literal("github_pr"), + item: LegacyPullRequestItemSchema, + owner: z.literal("new-workspace-picker").optional(), +}); +const LegacyReviewAttachmentContextLineSchema = z.strictObject({ + oldLineNumber: z.number().int().positive().nullable(), + newLineNumber: z.number().int().positive().nullable(), + type: z.enum(["add", "remove", "context"]), + content: z.string(), +}); +const LegacyReviewAttachmentSchema = z.strictObject({ + kind: z.literal("review"), + reviewDraftKey: z.string(), + commentCount: z.number().int().nonnegative(), + attachment: z.strictObject({ + type: z.literal("review"), + mimeType: z.literal("application/paseo-review"), + cwd: z.string(), + mode: z.enum(["uncommitted", "base"]), + baseRef: z.string().nullable().optional(), + comments: z.array( + z.strictObject({ + filePath: z.string(), + side: z.enum(["old", "new"]), + lineNumber: z.number().int().positive(), + body: z.string(), + context: z.strictObject({ + hunkHeader: z.string(), + targetLine: LegacyReviewAttachmentContextLineSchema, + lines: z.array(LegacyReviewAttachmentContextLineSchema), + }), + }), + ), + }), +}); +const PersistedComposerAttachmentSchema = z.union([ + UserComposerAttachmentSchema, + LegacyGithubPrAttachmentSchema, + LegacyReviewAttachmentSchema, +]); +type PersistedComposerAttachment = z.infer; +const LegacyAttachmentMetadataSchema = AttachmentMetadataSchema.extend({ + previewUri: z.string(), +}); +type PersistedImage = PersistedDraftImage | z.infer; + +const RawDraftInputSchema = z.strictObject({ + text: z.string().optional(), + attachments: z.array(PersistedComposerAttachmentSchema).optional(), + images: z + .array( + z.union([AttachmentMetadataSchema, LegacyAttachmentMetadataSchema, LegacyDraftImageSchema]), + ) + .optional(), + cwd: z.string().optional(), +}); +const DraftLifecycleSchema = z.enum(["active", "abandoned", "sent"]); +const NestedDraftRecordSchema = z.strictObject({ + input: RawDraftInputSchema, + lifecycle: DraftLifecycleSchema.optional(), + updatedAt: z.number().optional(), + version: z.number().int().positive().optional(), +}); +const FlatDraftRecordSchema = RawDraftInputSchema.extend({ + lifecycle: DraftLifecycleSchema.optional(), + updatedAt: z.number().optional(), + version: z.number().int().positive().optional(), +}); +const PersistedDraftRecordSchema = z.union([NestedDraftRecordSchema, FlatDraftRecordSchema]); +export const PersistedDraftStoreSchema = z.strictObject({ + drafts: z.record(z.string(), PersistedDraftRecordSchema).optional(), + createModalDraft: PersistedDraftRecordSchema.nullable().optional(), +}); +type PersistedDraftRecord = z.infer; + export type MigrateLegacyImages = ( images: readonly PersistedDraftImage[], ) => Promise; -function normalizePersistedImage(value: unknown): PersistedDraftImage | null { - if (isAttachmentMetadata(value)) { +function normalizePersistedImage(value: PersistedImage): PersistedDraftImage { + if ("id" in value) { return normalizeAttachmentMetadata(value); } - if (isLegacyDraftImage(value)) { - return { - uri: value.uri, - ...(value.mimeType ? { mimeType: value.mimeType } : {}), - }; - } - return null; -} - -function normalizePersistedComposerAttachment(value: unknown): UserComposerAttachment | null { - if (!isUserComposerAttachment(value)) { - return null; - } - return normalizeComposerAttachment(value); + return { + uri: value.uri, + ...(value.mimeType ? { mimeType: value.mimeType } : {}), + }; } function legacyImagesToAttachments( @@ -46,24 +127,36 @@ function legacyImagesToAttachments( })); } +function normalizePersistedComposerAttachment( + attachment: PersistedComposerAttachment, +): UserComposerAttachment | null { + if (attachment.kind === "review") { + return null; + } + if (attachment.kind === "github_pr" && attachment.item.kind === "pr") { + return { + kind: "github_pr", + item: { ...attachment.item, kind: "change_request" }, + ...(attachment.owner ? { owner: attachment.owner } : {}), + }; + } + const result = UserComposerAttachmentSchema.safeParse(attachment); + if (!result.success) { + throw new Error("Persisted composer attachment failed validation after migration"); + } + return normalizeComposerAttachment(result.data); +} + export async function migrateDraftInput( input: { rawInput: unknown }, ports: { migrateLegacyImages: MigrateLegacyImages }, ): Promise { - const rawInput = - input.rawInput && typeof input.rawInput === "object" - ? (input.rawInput as Record) - : {}; - const attachments = Array.isArray(rawInput.attachments) - ? rawInput.attachments - .map((entry) => normalizePersistedComposerAttachment(entry)) - .filter((entry): entry is UserComposerAttachment => entry !== null) - : []; - const legacyImages = Array.isArray(rawInput.images) - ? rawInput.images - .map((entry) => normalizePersistedImage(entry)) - .filter((entry): entry is PersistedDraftImage => entry !== null) - : []; + const result = RawDraftInputSchema.safeParse(input.rawInput); + const rawInput = result.success ? result.data : {}; + const attachments = (rawInput.attachments ?? []) + .map(normalizePersistedComposerAttachment) + .filter((attachment): attachment is UserComposerAttachment => attachment !== null); + const legacyImages = (rawInput.images ?? []).map(normalizePersistedImage); const migratedImages = await ports.migrateLegacyImages(legacyImages); return { @@ -72,30 +165,29 @@ export async function migrateDraftInput( }; } -function resolvePersistedLifecycle(lifecycle: unknown): DraftLifecycleState { - if (lifecycle === "sent" || lifecycle === "abandoned") { - return lifecycle as DraftLifecycleState; - } - return "active"; +function resolvePersistedLifecycle( + lifecycle: DraftLifecycleState | undefined, +): DraftLifecycleState { + return lifecycle ?? "active"; } -function extractRawInput(record: Record): unknown { - if ("input" in record && record.input && typeof record.input === "object") { +function extractRawInput(record: PersistedDraftRecord): z.infer { + if ("input" in record) { return record.input; } return record; } async function buildMigratedDraftRecord( - parsed: Record, + parsed: PersistedDraftRecord, ports: { migrateLegacyImages: MigrateLegacyImages }, nowMs: number, ): Promise { return { input: await migrateDraftInput({ rawInput: extractRawInput(parsed) }, ports), lifecycle: resolvePersistedLifecycle(parsed.lifecycle), - updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : nowMs, - version: typeof parsed.version === "number" ? parsed.version : 1, + updatedAt: parsed.updatedAt ?? nowMs, + version: parsed.version ?? 1, }; } @@ -143,30 +235,17 @@ export async function migratePersistedState( state: unknown, ports: { migrateLegacyImages: MigrateLegacyImages; nowMs: number }, ): Promise { - const input = (state ?? {}) as { - drafts?: Record; - createModalDraft?: unknown; - }; + const result = PersistedDraftStoreSchema.safeParse(state); + const input = result.success ? result.data : {}; const nextDrafts: Record = {}; for (const [draftKey, rawRecord] of Object.entries(input.drafts ?? {})) { - if (!rawRecord || typeof rawRecord !== "object") { - continue; - } - nextDrafts[draftKey] = await buildMigratedDraftRecord( - rawRecord as Record, - ports, - ports.nowMs, - ); + nextDrafts[draftKey] = await buildMigratedDraftRecord(rawRecord, ports, ports.nowMs); } let createModalDraft: DraftRecord | null = null; - if (input.createModalDraft && typeof input.createModalDraft === "object") { - createModalDraft = await buildMigratedDraftRecord( - input.createModalDraft as Record, - ports, - ports.nowMs, - ); + if (input.createModalDraft) { + createModalDraft = await buildMigratedDraftRecord(input.createModalDraft, ports, ports.nowMs); } return { diff --git a/packages/app/src/stores/draft-store/persistence.ts b/packages/app/src/stores/draft-store/persistence.ts index ab7c374c5a..f56b671307 100644 --- a/packages/app/src/stores/draft-store/persistence.ts +++ b/packages/app/src/stores/draft-store/persistence.ts @@ -1,7 +1,6 @@ import type { PersistStorage } from "zustand/middleware"; export const DRAFT_PERSIST_INTERVAL_MS = 200; - export interface PersistenceScheduler { now: () => number; schedule: (callback: () => void, delayMs: number) => unknown; @@ -12,10 +11,28 @@ export interface DraftPersistStorage extends PersistStorage { flush: () => Promise; } +let nextSystemTimerId = 0; +const systemTimers = new Map>(); const systemScheduler: PersistenceScheduler = { now: Date.now, - schedule: (callback, delayMs) => setTimeout(callback, delayMs), - cancel: (handle) => clearTimeout(handle as ReturnType), + schedule: (callback, delayMs) => { + const id = nextSystemTimerId++; + systemTimers.set( + id, + setTimeout(() => { + systemTimers.delete(id); + callback(); + }, delayMs), + ); + return id; + }, + cancel: (handle) => { + if (typeof handle !== "number") return; + const timer = systemTimers.get(handle); + if (timer === undefined) return; + clearTimeout(timer); + systemTimers.delete(handle); + }, }; export function createDraftPersistStorage( diff --git a/packages/app/src/stores/draft-store/state.ts b/packages/app/src/stores/draft-store/state.ts index a2314ec3a1..ee53825ed4 100644 --- a/packages/app/src/stores/draft-store/state.ts +++ b/packages/app/src/stores/draft-store/state.ts @@ -3,12 +3,7 @@ import { type AttachmentMetadata, type UserComposerAttachment, } from "@/attachments/types"; -import { isWorkspaceFileComposerAttachment } from "@/attachments/workspace-file"; -import { - ForgeSearchItemSchema, - GitHubSearchItemSchema, - UploadedFileAttachmentSchema, -} from "@getpaseo/protocol/messages"; +import { z } from "zod"; export const DRAFT_STORE_VERSION = 5; export const FINALIZED_DRAFT_TTL_MS = 5 * 60 * 1000; @@ -41,26 +36,95 @@ export interface DraftStoreState { createModalDraft: DraftRecord | null; } +export const AttachmentMetadataSchema = z.strictObject({ + id: z.string(), + mimeType: z.string(), + storageType: z.enum(["web-indexeddb", "desktop-file", "native-file"]), + storageKey: z.string(), + fileName: z.string().nullable().optional(), + byteSize: z.number().nullable().optional(), + createdAt: z.number(), +}) satisfies z.ZodType; +export const LegacyDraftImageSchema: z.ZodType = z.strictObject({ + uri: z.string(), + mimeType: z.string().optional(), +}); +const UploadedFileSchema = z.strictObject({ + type: z.literal("uploaded_file"), + id: z.string(), + fileName: z.string(), + mimeType: z.string(), + size: z.number().int().nonnegative(), + path: z.string(), +}); +const ForgeItemFields = { + forge: z.string().optional(), + number: z.number(), + title: z.string(), + url: z.string(), + state: z.string(), + body: z.string().nullable(), + labels: z.array(z.string()), + projectPath: z.string().optional(), + baseRefName: z.string().nullable().optional(), + headRefName: z.string().nullable().optional(), + updatedAt: z.string().optional(), +}; +const IssueItemSchema = z.strictObject({ kind: z.literal("issue"), ...ForgeItemFields }); +const ChangeRequestItemSchema = z.strictObject({ + kind: z.literal("change_request"), + ...ForgeItemFields, +}); +export const UserComposerAttachmentSchema: z.ZodType = z.discriminatedUnion( + "kind", + [ + z.strictObject({ kind: z.literal("image"), metadata: AttachmentMetadataSchema }), + z.strictObject({ kind: z.literal("file"), attachment: UploadedFileSchema }), + z.strictObject({ + kind: z.literal("workspace_file"), + path: z.string(), + selection: z.discriminatedUnion("kind", [ + z.strictObject({ kind: z.literal("whole_file") }), + z.strictObject({ + kind: z.literal("line_range"), + startLine: z.number().int().positive(), + endLine: z.number().int().positive(), + }), + ]), + }), + z.strictObject({ kind: z.literal("forge_issue"), item: IssueItemSchema }), + z.strictObject({ kind: z.literal("forge_change_request"), item: ChangeRequestItemSchema }), + z.strictObject({ kind: z.literal("github_issue"), item: IssueItemSchema }), + z.strictObject({ + kind: z.literal("github_pr"), + item: ChangeRequestItemSchema, + owner: z.literal(NEW_WORKSPACE_PICKER_ATTACHMENT_OWNER).optional(), + }), + ], +); +export const CanonicalDraftInputSchema = z.strictObject({ + text: z.string(), + attachments: z.array(UserComposerAttachmentSchema), + // COMPAT(draft-cwd): accept legacy persisted drafts that include cwd. Stop accepting after 2026-11-09. + cwd: z.string().optional(), +}); +const DraftRecordSchema: z.ZodType = z.strictObject({ + input: CanonicalDraftInputSchema, + lifecycle: z.enum(["active", "abandoned", "sent"]), + updatedAt: z.number(), + version: z.number().int().positive(), +}); +export const DraftStoreStateSchema: z.ZodType = z.strictObject({ + drafts: z.record(z.string(), DraftRecordSchema), + createModalDraft: DraftRecordSchema.nullable(), +}); + export function isAttachmentMetadata(value: unknown): value is AttachmentMetadata { - if (!value || typeof value !== "object") { - return false; - } - const record = value as Record; - return ( - typeof record.id === "string" && - typeof record.mimeType === "string" && - typeof record.storageType === "string" && - typeof record.storageKey === "string" && - typeof record.createdAt === "number" - ); + return AttachmentMetadataSchema.safeParse(value).success; } export function isLegacyDraftImage(value: unknown): value is LegacyDraftImage { - if (!value || typeof value !== "object") { - return false; - } - const record = value as Record; - return typeof record.uri === "string"; + return LegacyDraftImageSchema.safeParse(value).success; } export function normalizeAttachmentMetadata(image: AttachmentMetadata): AttachmentMetadata { @@ -80,39 +144,7 @@ export function normalizeAttachmentMetadata(image: AttachmentMetadata): Attachme } export function isUserComposerAttachment(value: unknown): value is UserComposerAttachment { - if (!value || typeof value !== "object") { - return false; - } - const record = value as Record; - if (record.kind === "image") { - const metadata = record.metadata; - return isAttachmentMetadata(metadata); - } - if (record.kind === "workspace_file") { - return isWorkspaceFileComposerAttachment(value); - } - if (record.kind === "file") { - return UploadedFileAttachmentSchema.safeParse(record.attachment).success; - } - if ( - record.kind !== "forge_issue" && - record.kind !== "forge_change_request" && - record.kind !== "github_issue" && - record.kind !== "github_pr" - ) { - return false; - } - if ( - record.kind === "github_pr" && - record.owner !== undefined && - record.owner !== NEW_WORKSPACE_PICKER_ATTACHMENT_OWNER - ) { - return false; - } - return ( - ForgeSearchItemSchema.safeParse(record.item).success || - GitHubSearchItemSchema.safeParse(record.item).success - ); + return UserComposerAttachmentSchema.safeParse(value).success; } export function normalizeComposerAttachment( @@ -131,33 +163,11 @@ export function normalizeComposerAttachment( selection: attachment.selection, }; } - if (attachment.kind === "github_pr") { - const item = - (attachment.item as { kind: string }).kind === "pr" - ? { ...attachment.item, kind: "change_request" as const } - : attachment.item; - return { - kind: "github_pr", - item, - ...(attachment.owner === NEW_WORKSPACE_PICKER_ATTACHMENT_OWNER - ? { owner: attachment.owner } - : {}), - }; - } return attachment; } export function isCanonicalDraftInput(value: unknown): value is CanonicalDraftInput { - if (!value || typeof value !== "object") { - return false; - } - const input = value as Record; - // COMPAT(draft-cwd): accept legacy persisted drafts that include cwd. Stop accepting after 2026-11-09. - return ( - typeof input.text === "string" && - Array.isArray(input.attachments) && - input.attachments.every(isUserComposerAttachment) - ); + return CanonicalDraftInputSchema.safeParse(value).success; } export function toDraftInputIfReady( diff --git a/packages/app/src/stores/keyboard-shortcuts-store.test.ts b/packages/app/src/stores/keyboard-shortcuts-store.test.ts index 17a67d767f..f35b6ea18a 100644 --- a/packages/app/src/stores/keyboard-shortcuts-store.test.ts +++ b/packages/app/src/stores/keyboard-shortcuts-store.test.ts @@ -4,6 +4,7 @@ import { useKeyboardShortcutsStore } from "./keyboard-shortcuts-store"; beforeEach(() => { useKeyboardShortcutsStore.setState({ commandCenterOpen: false, + commandCenterScope: null, shortcutsDialogOpen: false, capturingShortcut: false, altDown: false, @@ -19,6 +20,20 @@ describe("keyboard-shortcuts-store", () => { expect(useKeyboardShortcutsStore.getState().commandCenterOpen).toBe(true); }); + it("opens the command center with a scope and clears it when closed", () => { + useKeyboardShortcutsStore.getState().setCommandCenterOpen(true, "files"); + expect(useKeyboardShortcutsStore.getState()).toMatchObject({ + commandCenterOpen: true, + commandCenterScope: "files", + }); + + useKeyboardShortcutsStore.getState().setCommandCenterOpen(false); + expect(useKeyboardShortcutsStore.getState()).toMatchObject({ + commandCenterOpen: false, + commandCenterScope: null, + }); + }); + it("toggles shortcut capture state", () => { expect(useKeyboardShortcutsStore.getState().capturingShortcut).toBe(false); useKeyboardShortcutsStore.getState().setCapturingShortcut(true); diff --git a/packages/app/src/stores/keyboard-shortcuts-store.ts b/packages/app/src/stores/keyboard-shortcuts-store.ts index f0ce7f960f..0b0e2a516c 100644 --- a/packages/app/src/stores/keyboard-shortcuts-store.ts +++ b/packages/app/src/stores/keyboard-shortcuts-store.ts @@ -3,8 +3,11 @@ import type { SidebarShortcutWorkspaceTarget } from "@/utils/sidebar-shortcuts"; const SHORTCUT_BADGE_DELAY_MS = 150; +export type CommandCenterScope = "files" | null; + interface KeyboardShortcutsState { commandCenterOpen: boolean; + commandCenterScope: CommandCenterScope; shortcutsDialogOpen: boolean; capturingShortcut: boolean; altDown: boolean; @@ -13,7 +16,8 @@ interface KeyboardShortcutsState { /** Sidebar-visible workspace targets (up to 9), in top-to-bottom visual order. */ sidebarShortcutWorkspaceTargets: SidebarShortcutWorkspaceTarget[]; - setCommandCenterOpen: (open: boolean) => void; + setCommandCenterOpen: (open: boolean, scope?: CommandCenterScope) => void; + setCommandCenterScope: (scope: CommandCenterScope) => void; setShortcutsDialogOpen: (open: boolean) => void; setCapturingShortcut: (capturing: boolean) => void; setAltDown: (down: boolean) => void; @@ -47,6 +51,7 @@ function updateBadgeTimer( export const useKeyboardShortcutsStore = create((set, get) => ({ commandCenterOpen: false, + commandCenterScope: null, shortcutsDialogOpen: false, capturingShortcut: false, altDown: false, @@ -54,7 +59,9 @@ export const useKeyboardShortcutsStore = create((set, ge showShortcutBadges: false, sidebarShortcutWorkspaceTargets: [], - setCommandCenterOpen: (open) => set({ commandCenterOpen: open }), + setCommandCenterOpen: (open, scope = null) => + set({ commandCenterOpen: open, commandCenterScope: open ? scope : null }), + setCommandCenterScope: (scope) => set({ commandCenterScope: scope }), setShortcutsDialogOpen: (open) => set({ shortcutsDialogOpen: open }), setCapturingShortcut: (capturing) => set({ capturingShortcut: capturing }), setAltDown: (down) => { diff --git a/packages/app/src/stores/last-workspace-selection.test.ts b/packages/app/src/stores/last-workspace-selection.test.ts index 06f4dceecf..e5fc979be3 100644 --- a/packages/app/src/stores/last-workspace-selection.test.ts +++ b/packages/app/src/stores/last-workspace-selection.test.ts @@ -20,12 +20,19 @@ class DelayedWorkspaceSelectionStorage implements LastWorkspaceSelectionStorage this.saved = value; } + async clear(): Promise { + this.saved = null; + } + finishHydrationWith(selection: ActiveWorkspaceSelection | null) { this.finishRead(selection ? JSON.stringify(selection) : null); } getSavedSelection(): ActiveWorkspaceSelection | null { - return this.saved ? (JSON.parse(this.saved) as ActiveWorkspaceSelection) : null; + if (!this.saved) return null; + const parsed: unknown = JSON.parse(this.saved); + if (!parsed || typeof parsed !== "object") return null; + return this.saved ? JSON.parse(this.saved) : null; } } diff --git a/packages/app/src/stores/last-workspace-selection.ts b/packages/app/src/stores/last-workspace-selection.ts index f7bd3bc2e3..6e689e0a48 100644 --- a/packages/app/src/stores/last-workspace-selection.ts +++ b/packages/app/src/stores/last-workspace-selection.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + export interface ActiveWorkspaceSelection { serverId: string; workspaceId: string; @@ -8,19 +10,17 @@ export const LAST_WORKSPACE_SELECTION_STORAGE_KEY = "paseo:last-workspace-route- export interface LastWorkspaceSelectionStorage { read(): Promise; write(value: string): Promise; + clear(): Promise; } +const ActiveWorkspaceSelectionSchema: z.ZodType = z.strictObject({ + serverId: z.string().trim().min(1), + workspaceId: z.string().trim().min(1), +}); + function normalizeWorkspaceSelection(input: unknown): ActiveWorkspaceSelection | null { - if (!input || typeof input !== "object" || Array.isArray(input)) { - return null; - } - const record = input as Record; - const serverId = typeof record.serverId === "string" ? record.serverId.trim() : ""; - const workspaceId = typeof record.workspaceId === "string" ? record.workspaceId.trim() : ""; - if (!serverId || !workspaceId) { - return null; - } - return { serverId, workspaceId }; + const result = ActiveWorkspaceSelectionSchema.safeParse(input); + return result.success ? result.data : null; } function parseStoredWorkspaceSelection(stored: string | null): ActiveWorkspaceSelection | null { @@ -75,6 +75,9 @@ export function createLastWorkspaceSelectionStore(storage: LastWorkspaceSelectio .then((stored) => { if (revision === hydrationRevision) { selection = parseStoredWorkspaceSelection(stored); + if (stored !== null && selection === null) { + void storage.clear().catch(() => {}); + } } return undefined; }) diff --git a/packages/app/src/stores/navigation-active-workspace-store/index.ts b/packages/app/src/stores/navigation-active-workspace-store/index.ts index 6cc8191936..9adcd6c6ba 100644 --- a/packages/app/src/stores/navigation-active-workspace-store/index.ts +++ b/packages/app/src/stores/navigation-active-workspace-store/index.ts @@ -25,6 +25,7 @@ export type { NavigateToWorkspaceInput } from "./navigation"; const lastWorkspaceSelectionStorage: LastWorkspaceSelectionStorage = { read: () => AsyncStorage.getItem(LAST_WORKSPACE_SELECTION_STORAGE_KEY), write: (value) => AsyncStorage.setItem(LAST_WORKSPACE_SELECTION_STORAGE_KEY, value), + clear: () => AsyncStorage.removeItem(LAST_WORKSPACE_SELECTION_STORAGE_KEY), }; const lastWorkspaceSelectionStore = createLastWorkspaceSelectionStore( diff --git a/packages/app/src/stores/panel-store/index.ts b/packages/app/src/stores/panel-store/index.ts index baac870f41..6695ee5f4d 100644 --- a/packages/app/src/stores/panel-store/index.ts +++ b/packages/app/src/stores/panel-store/index.ts @@ -1,5 +1,5 @@ import { create } from "zustand"; -import { persist, createJSONStorage } from "zustand/middleware"; +import { persist } from "zustand/middleware"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { buildExplorerCheckoutKey, @@ -24,6 +24,7 @@ import { MIN_EXPLORER_SIDEBAR_WIDTH, MIN_SIDEBAR_WIDTH, migratePanelState, + PanelPersistedStateSchema, selectIsAgentListOpen, selectIsFileExplorerOpen, setMobilePanelTarget, @@ -37,6 +38,7 @@ import { type SortOption, } from "./state"; import { isWeb } from "@/constants/platform"; +import { createValidatedPersistStorage } from "@/storage/validated-persist-storage"; export type { ExplorerTab } from "../explorer-tab-memory"; export type { ExplorerCheckoutContext } from "../explorer-checkout-context"; export type { @@ -311,9 +313,8 @@ export const usePanelStore = create()( { name: "panel-state", version: 12, - storage: createJSONStorage(() => AsyncStorage), - migrate: (persistedState, version) => - migratePanelState(persistedState, version, { isWeb }) as unknown as PanelState, + storage: createValidatedPersistStorage(AsyncStorage, PanelPersistedStateSchema), + migrate: (persistedState, version) => migratePanelState(persistedState, version, { isWeb }), partialize: (state) => ({ desktop: state.desktop, explorerTab: state.explorerTab, diff --git a/packages/app/src/stores/panel-store/state.ts b/packages/app/src/stores/panel-store/state.ts index a7e13288b7..611c133b5e 100644 --- a/packages/app/src/stores/panel-store/state.ts +++ b/packages/app/src/stores/panel-store/state.ts @@ -5,6 +5,7 @@ import { type ExplorerTab, } from "../explorer-tab-memory"; import { type ExplorerCheckoutContext } from "../explorer-checkout-context"; +import { z } from "zod"; export type MobilePanelView = "agent" | "agent-list" | "file-explorer"; @@ -161,7 +162,37 @@ export function buildToggleFileExplorerPatch( return { desktop: { ...state.desktop, fileExplorerOpen: false } }; } -type MigratablePanelState = Record; +const ExplorerTabSchema = z.enum(["changes", "files", "pr"]); +const DesktopSidebarStorageSchema = z.strictObject({ + agentListOpen: z.boolean().optional(), + fileExplorerOpen: z.boolean().optional(), + focusModeEnabled: z.boolean().optional(), + zoomed: z.boolean().optional(), + focused: z.boolean().optional(), +}); + +export const PanelPersistedStateSchema = z.strictObject({ + mobileView: z.enum(["agent", "agent-list", "file-explorer"]).optional(), + mobilePanel: z + .strictObject({ + target: z.enum(["agent", "agent-list", "file-explorer"]), + revision: z.number().int().nonnegative(), + }) + .optional(), + desktop: DesktopSidebarStorageSchema.optional(), + explorerTab: ExplorerTabSchema.optional(), + explorerTabByCheckout: z.record(z.string(), ExplorerTabSchema).optional(), + expandedPathsByWorkspace: z.record(z.string(), z.array(z.string())).optional(), + diffExpandedPathsByWorkspace: z.record(z.string(), z.array(z.string())).optional(), + diffCollapsedFoldersByWorkspace: z.record(z.string(), z.array(z.string())).optional(), + sidebarWidth: z.number().optional(), + explorerWidth: z.number().optional(), + explorerSortOption: z.enum(["name", "modified", "size"]).optional(), + explorerShowHiddenFiles: z.boolean().optional(), + explorerFilesSplitRatio: z.number().optional(), +}); + +type MigratablePanelState = z.infer; function migratePanelV2Explorer(state: MigratablePanelState, isWeb: boolean): void { if (isWeb && typeof state.explorerWidth === "number" && state.explorerWidth === 400) { @@ -193,7 +224,7 @@ function migratePanelExplorerTabByCheckout(state: MigratablePanelState, version: state.explorerTabByCheckout = {}; return; } - const entries = Object.entries(state.explorerTabByCheckout as Record); + const entries = Object.entries(state.explorerTabByCheckout); const next: Record = {}; for (const [key, value] of entries) { if (!isExplorerTab(value)) { @@ -205,7 +236,7 @@ function migratePanelExplorerTabByCheckout(state: MigratablePanelState, version: } function migratePanelDesktopFocusMode(state: MigratablePanelState): void { - const desktop = state.desktop as Record | undefined; + const desktop = state.desktop; if (!desktop) { return; } @@ -227,7 +258,8 @@ export function migratePanelState( version: number, options: { isWeb: boolean }, ): MigratablePanelState { - const state = (persistedState ?? {}) as MigratablePanelState; + const result = PanelPersistedStateSchema.safeParse(persistedState); + const state: MigratablePanelState = result.success ? result.data : {}; const { isWeb } = options; if (version < 2) { diff --git a/packages/app/src/stores/session-store-hooks/index.ts b/packages/app/src/stores/session-store-hooks/index.ts index f2021a4909..7d01fc9a99 100644 --- a/packages/app/src/stores/session-store-hooks/index.ts +++ b/packages/app/src/stores/session-store-hooks/index.ts @@ -5,6 +5,7 @@ import { composeWorkspaceStructure, selectHasHydratedWorkspaces, selectHydratedWorkspaceServerIds, + selectWorkspaceDirectoryServerIds, selectHasWorkspaces, selectProjectOrder, selectRecommendedProjectPaths, @@ -79,6 +80,14 @@ export function useHydratedWorkspaceServerIds(serverIds: string[]): string[] { ); } +export function useWorkspaceDirectoryServerIds(serverIds: string[]): string[] { + return useStoreWithEqualityFn( + useSessionStore, + (state) => selectWorkspaceDirectoryServerIds(state, serverIds), + workspaceEqualityFns.deep, + ); +} + export function useWorkspaceDirectory( serverId: string | null, workspaceId: string | null, diff --git a/packages/app/src/stores/session-store-hooks/selectors.test.ts b/packages/app/src/stores/session-store-hooks/selectors.test.ts index 96f21de53b..2c0ec9b46b 100644 --- a/packages/app/src/stores/session-store-hooks/selectors.test.ts +++ b/packages/app/src/stores/session-store-hooks/selectors.test.ts @@ -6,6 +6,7 @@ import { composeWorkspaceStructure, selectHasWorkspaces, selectHydratedWorkspaceServerIds, + selectWorkspaceDirectoryServerIds, selectProjectOrder, selectRecommendedProjectPaths, selectWorkspace, @@ -131,20 +132,32 @@ afterEach(() => { }); describe("workspace replica authority", () => { - it("keeps a cached workspace addressable without publishing it as an authoritative directory", () => { + it("publishes a restored directory while keeping remote hydration false", () => { const cachedWorkspace = createWorkspace({ id: "cached-workspace" }); - initializeWorkspaces([cachedWorkspace]); + useSessionStore.getState().restoreSessionReplica(SERVER_ID, { + agents: new Map(), + workspaces: new Map([[cachedWorkspace.id, cachedWorkspace]]), + projects: new Map([ + [cachedWorkspace.projectId, projectDescriptorFromTestWorkspace(cachedWorkspace)], + ]), + timeline: null, + }); + const cachedSession = useSessionStore.getState().sessions[SERVER_ID]; + if (!cachedSession) throw new Error("expected initialized session"); - const cachedServerIds = selectHydratedWorkspaceServerIds(useSessionStore.getState(), [ + const cachedServerIds = selectWorkspaceDirectoryServerIds(useSessionStore.getState(), [ SERVER_ID, ]); expect(selectWorkspace(useSessionStore.getState(), SERVER_ID, cachedWorkspace.id)).toBe( cachedWorkspace, ); - expect(selectWorkspaceStructureProjects(useSessionStore.getState(), cachedServerIds)).toEqual( - [], - ); + expect(cachedSession.hasHydratedWorkspaces).toBe(false); + expect( + selectWorkspaceStructureProjects(useSessionStore.getState(), cachedServerIds).map( + (project) => project.workspaceKeys, + ), + ).toEqual([[`${SERVER_ID}:${cachedWorkspace.id}`]]); const authoritativeWorkspace = createWorkspace({ id: "authoritative-workspace", diff --git a/packages/app/src/stores/session-store-hooks/selectors.ts b/packages/app/src/stores/session-store-hooks/selectors.ts index f911e30d41..409a660557 100644 --- a/packages/app/src/stores/session-store-hooks/selectors.ts +++ b/packages/app/src/stores/session-store-hooks/selectors.ts @@ -16,6 +16,7 @@ export interface SessionsSnapshot { string, { hasHydratedWorkspaces?: boolean; + hasWorkspaceDirectorySnapshot?: boolean; workspaces: Map; projects?: Map; } @@ -138,6 +139,18 @@ export function selectHydratedWorkspaceServerIds( return serverIds.filter((serverId) => state.sessions[serverId]?.hasHydratedWorkspaces === true); } +export function selectWorkspaceDirectoryServerIds( + state: SessionsSnapshot, + serverIds: readonly string[], +): string[] { + return serverIds.filter((serverId) => { + const session = state.sessions[serverId]; + return ( + session?.hasHydratedWorkspaces === true || session?.hasWorkspaceDirectorySnapshot === true + ); + }); +} + export function selectWorkspaceStructureProjects( state: SessionsSnapshot, serverIds: readonly string[], diff --git a/packages/app/src/stores/session-store.test.ts b/packages/app/src/stores/session-store.test.ts index a784c85431..201dddaf46 100644 --- a/packages/app/src/stores/session-store.test.ts +++ b/packages/app/src/stores/session-store.test.ts @@ -80,6 +80,62 @@ function permutations(values: readonly T[]): T[][] { ); } +function taskTexts(tasks: ReadonlyArray<{ text: string }>): string[] { + return tasks.map((task) => task.text); +} + +describe("agent task state", () => { + it("notifies the task subscriber only when its task snapshot changes", () => { + initializeTestSession(); + const snapshots: string[][] = []; + const unsubscribe = useSessionStore.subscribe( + (state) => state.sessions["test-server"]?.agentTasks.get("agent-1") ?? [], + (tasks) => snapshots.push(taskTexts(tasks)), + ); + + const tasks = [{ id: "1", text: "Inspect", status: "pending" as const, completed: false }]; + useSessionStore + .getState() + .setAgentStreamState("test-server", "agent-1", { taskSnapshot: tasks }); + useSessionStore.getState().setIsPlayingAudio("test-server", true); + useSessionStore + .getState() + .setAgentStreamState("test-server", "agent-1", { taskSnapshot: [...tasks] }); + useSessionStore.getState().setAgentStreamState("test-server", "agent-1", { + taskSnapshot: [{ ...tasks[0], status: "completed", completed: true }], + }); + unsubscribe(); + + expect(snapshots).toEqual([["Inspect"], ["Inspect"]]); + }); + + it("restores the latest task snapshot from an authoritative timeline", () => { + initializeTestSession(); + const todo: StreamItem = { + kind: "todo_list", + id: "todo-1", + provider: "codex", + timestamp: new Date("2026-08-11T10:00:00.000Z"), + activity: { type: "started", task: "Verify" }, + items: [{ text: "Verify", status: "in_progress", completed: false }], + }; + + useSessionStore.getState().applyAgentTimelineResponseState("test-server", "agent-1", { + items: [todo], + head: [], + range: null, + older: "none", + newer: false, + synchronized: true, + acknowledgedClientMessageIds: [], + }); + + expect(useSessionStore.getState().sessions["test-server"]?.agentTasks.get("agent-1")).toEqual( + todo.items, + ); + }); +}); + describe("agent timeline state", () => { it("commits canonical items, range, and older availability as one synced state", () => { initializeTestSession(); diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index 57a6f61d5c..9250809cc4 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -9,6 +9,7 @@ import { handoffCreatedAgentUserMessageToStream, removeSubmittedUserMessage, type StreamItem, + type TodoEntry, type UserMessageItem, } from "@/types/stream"; import { @@ -61,47 +62,6 @@ import { type TurnLivenessTransition, } from "@/timeline/turn-liveness"; -// Re-export types that were in session-context -export type MessageEntry = - | { - type: "user"; - id: string; - timestamp: number; - message: string; - } - | { - type: "assistant"; - id: string; - timestamp: number; - message: string; - } - | { - type: "activity"; - id: string; - timestamp: number; - activityType: "system" | "info" | "success" | "error"; - message: string; - metadata?: Record; - } - | { - type: "artifact"; - id: string; - timestamp: number; - artifactId: string; - artifactType: string; - title: string; - } - | { - type: "tool_call"; - id: string; - timestamp: number; - toolName: string; - args: unknown; - result?: unknown; - error?: unknown; - status: "executing" | "completed" | "failed"; - }; - export interface AgentRuntimeInfo { provider: AgentProvider; sessionId: string | null; @@ -214,6 +174,7 @@ export interface ProjectDescriptor { projectDisplayName: string; projectCustomName: string | null; projectCustomIconRevision?: string | null; + projectIconRevision?: string; projectRootPath: string; projectKind: WorkspaceDescriptorPayload["projectKind"]; } @@ -227,6 +188,7 @@ export function normalizeProjectDescriptor( projectDisplayName: payload.projectDisplayName, projectCustomName: payload.projectCustomName ?? null, projectCustomIconRevision: payload.projectCustomIconRevision ?? null, + projectIconRevision: payload.projectIconRevision, projectRootPath: payload.projectRootPath, projectKind: payload.projectKind, }; @@ -396,6 +358,26 @@ export function selectAgentTurnPresentation( ); } +function latestTasksFromStream(items: readonly StreamItem[]): TodoEntry[] { + for (let index = items.length - 1; index >= 0; index -= 1) { + const item = items[index]; + if (item?.kind === "todo_list") return item.items; + } + return []; +} + +function updateAgentTasks( + current: Map, + agentId: string, + taskSnapshot: TodoEntry[] | undefined, +): Map { + if (taskSnapshot === undefined || equal(current.get(agentId) ?? [], taskSnapshot)) return current; + const next = new Map(current); + if (taskSnapshot.length > 0) next.set(agentId, taskSnapshot); + else next.delete(agentId); + return next; +} + export type WorkspaceRestoreStatus = "restoring" | "failed" | "needs-host-upgrade"; // Per-session state @@ -413,6 +395,7 @@ export interface SessionState { // Hydration status hasHydratedAgents: boolean; hasHydratedWorkspaces: boolean; + hasWorkspaceDirectorySnapshot: boolean; // Audio state isPlayingAudio: boolean; @@ -421,13 +404,10 @@ export interface SessionState { focusedAgentId: string | null; focusedTerminalId: string | null; - // Messages - messages: MessageEntry[]; - currentAssistantMessage: string; - // Stream state (head/tail model) agentStreamTail: Map; agentStreamHead: Map; + agentTasks: Map; agentTurnLiveness: Map; messageSubmissions: Map; agentTimelineCursor: Map; @@ -495,16 +475,6 @@ interface SessionStoreActions { setFocusedAgentId: (serverId: string, agentId: string | null) => void; setFocusedTerminalId: (serverId: string, terminalId: string | null) => void; - // Messages - setMessages: ( - serverId: string, - messages: MessageEntry[] | ((prev: MessageEntry[]) => MessageEntry[]), - ) => void; - setCurrentAssistantMessage: ( - serverId: string, - message: string | ((prev: string) => string), - ) => void; - // Stream state (head/tail model) setAgentStreamTail: ( serverId: string, @@ -525,6 +495,7 @@ interface SessionStoreActions { tail?: StreamItem[]; head?: StreamItem[]; acknowledgedClientMessageIds?: readonly string[]; + taskSnapshot?: TodoEntry[]; }, ) => void; applyAgentTurnLiveness: ( @@ -687,13 +658,13 @@ function createInitialSessionState( serverInfo: null, hasHydratedAgents: false, hasHydratedWorkspaces: false, + hasWorkspaceDirectorySnapshot: false, isPlayingAudio: false, focusedAgentId: null, focusedTerminalId: null, - messages: [], - currentAssistantMessage: "", agentStreamTail: new Map(), agentStreamHead: new Map(), + agentTasks: new Map(), agentTurnLiveness: new Map(), messageSubmissions: new Map(), agentTimelineCursor: new Map(), @@ -814,8 +785,11 @@ export const useSessionStore = create()( const session = createInitialSessionState(serverId, null); const timeline = replica.timeline; const agentStreamTail = new Map(); + const agentTasks = new Map(); if (timeline) { agentStreamTail.set(timeline.agentId, timeline.items); + const tasks = latestTasksFromStream(timeline.items); + if (tasks.length > 0) agentTasks.set(timeline.agentId, tasks); } const agentLastActivity = new Map(prev.agentLastActivity); for (const agent of replica.agents.values()) { @@ -831,7 +805,9 @@ export const useSessionStore = create()( workspaceAgentActivity: buildWorkspaceAgentActivityIndex(replica.agents), workspaces: replica.workspaces, projects: replica.projects, + hasWorkspaceDirectorySnapshot: true, agentStreamTail, + agentTasks, }, }, agentLastActivity, @@ -1024,49 +1000,6 @@ export const useSessionStore = create()( }); }, - // Messages - setMessages: (serverId, messages) => { - set((prev) => { - const session = prev.sessions[serverId]; - if (!session) { - return prev; - } - const nextMessages = - typeof messages === "function" ? messages(session.messages) : messages; - if (session.messages === nextMessages) { - return prev; - } - return { - ...prev, - sessions: { - ...prev.sessions, - [serverId]: { ...session, messages: nextMessages }, - }, - }; - }); - }, - - setCurrentAssistantMessage: (serverId, message) => { - set((prev) => { - const session = prev.sessions[serverId]; - if (!session) { - return prev; - } - const nextMessage = - typeof message === "function" ? message(session.currentAssistantMessage) : message; - if (session.currentAssistantMessage === nextMessage) { - return prev; - } - return { - ...prev, - sessions: { - ...prev.sessions, - [serverId]: { ...session, currentAssistantMessage: nextMessage }, - }, - }; - }); - }, - // Stream state (head/tail model) setAgentStreamTail: (serverId, state) => { set((prev) => { @@ -1151,8 +1084,10 @@ export const useSessionStore = create()( state.acknowledgedClientMessageIds ?? [], ); const changedSubmissions = observedSubmissions !== currentSubmissions; + const agentTasks = updateAgentTasks(session.agentTasks, agentId, state.taskSnapshot); + const changedTasks = agentTasks !== session.agentTasks; - if (!changedTail && !changedHead && !changedSubmissions) { + if (!changedTail && !changedHead && !changedSubmissions && !changedTasks) { return prev; } @@ -1174,6 +1109,7 @@ export const useSessionStore = create()( ...session, agentStreamTail: nextTail, agentStreamHead: nextHead, + agentTasks, messageSubmissions, }, }, @@ -1603,6 +1539,10 @@ export const useSessionStore = create()( nextAuthoritative.set(agentId, true); nextSyncGeneration.set(agentId, session.historySyncGeneration); } + const tasks = latestTasksFromStream([...state.items, ...state.head]); + const agentTasks = new Map(session.agentTasks); + if (tasks.length > 0) agentTasks.set(agentId, tasks); + else agentTasks.delete(agentId); return { ...prev, @@ -1612,6 +1552,7 @@ export const useSessionStore = create()( ...session, agentStreamTail: nextTail, agentStreamHead: nextHead, + agentTasks, agentTimelineCursor: nextCursor, agentTimelineHasOlder: nextHasOlder, agentTimelineHasNewer: nextHasNewer, diff --git a/packages/app/src/stores/sidebar-collapsed-sections-store/index.ts b/packages/app/src/stores/sidebar-collapsed-sections-store/index.ts index abda2aac76..c4fb6239af 100644 --- a/packages/app/src/stores/sidebar-collapsed-sections-store/index.ts +++ b/packages/app/src/stores/sidebar-collapsed-sections-store/index.ts @@ -1,9 +1,12 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { create } from "zustand"; -import { createJSONStorage, persist } from "zustand/middleware"; +import { persist } from "zustand/middleware"; +import { createValidatedPersistStorage } from "@/storage/validated-persist-storage"; import { type CollapsedProjectsState, + type PersistedCollapsedProjects, mergePersistedCollapsedProjects, + PersistedCollapsedProjectsSchema, serializeCollapsedProjects, setProjectCollapsed, togglePinnedCollapsed, @@ -19,7 +22,7 @@ interface SidebarCollapsedSectionsState extends CollapsedProjectsState { } export const useSidebarCollapsedSectionsStore = create()( - persist( + persist( (set) => ({ collapsedProjectKeys: new Set(), collapsedStatusGroupKeys: new Set(), @@ -34,13 +37,10 @@ export const useSidebarCollapsedSectionsStore = create AsyncStorage), + storage: createValidatedPersistStorage(AsyncStorage, PersistedCollapsedProjectsSchema), partialize: (state) => serializeCollapsedProjects(state), merge: (persistedState, currentState) => - mergePersistedCollapsedProjects( - persistedState as { collapsedProjectKeys?: unknown } | undefined, - currentState, - ), + mergePersistedCollapsedProjects(persistedState, currentState), }, ), ); diff --git a/packages/app/src/stores/sidebar-collapsed-sections-store/state.test.ts b/packages/app/src/stores/sidebar-collapsed-sections-store/state.test.ts index 051d7cc7cc..f2d9258b6d 100644 --- a/packages/app/src/stores/sidebar-collapsed-sections-store/state.test.ts +++ b/packages/app/src/stores/sidebar-collapsed-sections-store/state.test.ts @@ -52,13 +52,13 @@ describe("sidebar collapsed projects transitions", () => { expect(restored.collapsedPinned).toBe(true); }); - it("restores collapsed project keys from persisted preferences", () => { + it("rejects the complete value when a persisted project key is invalid", () => { const restored = mergePersistedCollapsedProjects( { collapsedProjectKeys: ["project-a", "project-b", 42] }, emptyState(), ); - expect(Array.from(restored.collapsedProjectKeys)).toEqual(["project-a", "project-b"]); + expect(Array.from(restored.collapsedProjectKeys)).toEqual([]); expect(Array.from(restored.collapsedStatusGroupKeys)).toEqual([]); }); diff --git a/packages/app/src/stores/sidebar-collapsed-sections-store/state.ts b/packages/app/src/stores/sidebar-collapsed-sections-store/state.ts index 3a03ab0ac2..4d84205e1a 100644 --- a/packages/app/src/stores/sidebar-collapsed-sections-store/state.ts +++ b/packages/app/src/stores/sidebar-collapsed-sections-store/state.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + export interface CollapsedProjectsState { collapsedProjectKeys: Set; collapsedStatusGroupKeys: Set; @@ -5,11 +7,18 @@ export interface CollapsedProjectsState { } export interface PersistedCollapsedProjects { - collapsedProjectKeys?: unknown; - collapsedStatusGroupKeys?: unknown; - collapsedPinned?: unknown; + collapsedProjectKeys?: string[]; + collapsedStatusGroupKeys?: string[]; + collapsedPinned?: boolean; } +export const PersistedCollapsedProjectsSchema: z.ZodType = + z.strictObject({ + collapsedProjectKeys: z.array(z.string()).optional(), + collapsedStatusGroupKeys: z.array(z.string()).optional(), + collapsedPinned: z.boolean().optional(), + }); + export function togglePinnedCollapsed(state: CollapsedProjectsState): CollapsedProjectsState { return { ...state, collapsedPinned: !state.collapsedPinned }; } @@ -67,22 +76,21 @@ export function serializeCollapsedProjects(state: CollapsedProjectsState): { } export function mergePersistedCollapsedProjects( - persisted: PersistedCollapsedProjects | undefined, + persistedValue: unknown, current: S, ): S { - if ( - !persisted?.collapsedProjectKeys && - !persisted?.collapsedStatusGroupKeys && - persisted?.collapsedPinned === undefined - ) { + const result = PersistedCollapsedProjectsSchema.safeParse(persistedValue); + if (!result.success) { return current; } - const restoredProjects = deserializeCollapsedKeys(persisted.collapsedProjectKeys); - const restoredStatusGroups = deserializeCollapsedKeys(persisted.collapsedStatusGroupKeys); - const restoredPinned = - typeof persisted.collapsedPinned === "boolean" - ? persisted.collapsedPinned - : current.collapsedPinned; + const persisted = result.data; + const restoredProjects = deserializeCollapsedKeys( + persisted.collapsedProjectKeys ?? Array.from(current.collapsedProjectKeys), + ); + const restoredStatusGroups = deserializeCollapsedKeys( + persisted.collapsedStatusGroupKeys ?? Array.from(current.collapsedStatusGroupKeys), + ); + const restoredPinned = persisted.collapsedPinned ?? current.collapsedPinned; if ( areSetsEqual(current.collapsedProjectKeys, restoredProjects) && areSetsEqual(current.collapsedStatusGroupKeys, restoredStatusGroups) && @@ -98,11 +106,8 @@ export function mergePersistedCollapsedProjects { - if (!Array.isArray(value)) { - return new Set(); - } - return new Set(value.filter((key): key is string => typeof key === "string")); +function deserializeCollapsedKeys(value: string[]): Set { + return new Set(value); } function areSetsEqual(left: Set, right: Set): boolean { diff --git a/packages/app/src/stores/sidebar-order-store.test.ts b/packages/app/src/stores/sidebar-order-store.test.ts index d28e03f73a..c58931ca69 100644 --- a/packages/app/src/stores/sidebar-order-store.test.ts +++ b/packages/app/src/stores/sidebar-order-store.test.ts @@ -16,9 +16,18 @@ describe("migrateSidebarOrderState", () => { expect(migrated).toEqual({ projectOrder: ["project-a"], + pinnedWorkspaceOrder: [], workspaceOrderByProject: { "project-a": ["host-a:main", "host-a:feature", "host-b:main"], }, }); }); + + it("normalizes pinned workspace order", () => { + const migrated = migrateSidebarOrderState({ + pinnedWorkspaceOrder: [" host-a:one ", "host-a:one", "", "host-b:two"], + }); + + expect(migrated.pinnedWorkspaceOrder).toEqual(["host-a:one", "host-b:two"]); + }); }); diff --git a/packages/app/src/stores/sidebar-order-store.ts b/packages/app/src/stores/sidebar-order-store.ts index 307e432c0a..27e7a190c8 100644 --- a/packages/app/src/stores/sidebar-order-store.ts +++ b/packages/app/src/stores/sidebar-order-store.ts @@ -1,23 +1,38 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { create } from "zustand"; -import { createJSONStorage, persist } from "zustand/middleware"; +import { persist } from "zustand/middleware"; +import { z } from "zod"; +import { createValidatedPersistStorage } from "@/storage/validated-persist-storage"; interface SidebarOrderStoreState { projectOrder: string[]; + pinnedWorkspaceOrder: string[]; workspaceOrderByProject: Record; getProjectOrder: () => string[]; setProjectOrder: (keys: string[]) => void; + getPinnedWorkspaceOrder: () => string[]; + setPinnedWorkspaceOrder: (keys: string[]) => void; getWorkspaceOrder: (projectViewKey: string) => string[]; setWorkspaceOrder: (projectViewKey: string, keys: string[]) => void; } interface SidebarOrderPersistedState { projectOrder?: string[]; + pinnedWorkspaceOrder?: string[]; workspaceOrderByProject?: Record; projectOrderByServerId?: Record; workspaceOrderByServerAndProject?: Record; } +const StringArrayRecordSchema = z.record(z.string(), z.array(z.string())); +const SidebarOrderPersistedStateSchema = z.strictObject({ + projectOrder: z.array(z.string()).optional(), + pinnedWorkspaceOrder: z.array(z.string()).optional(), + workspaceOrderByProject: StringArrayRecordSchema.optional(), + projectOrderByServerId: StringArrayRecordSchema.optional(), + workspaceOrderByServerAndProject: StringArrayRecordSchema.optional(), +}); + interface SidebarWorkspaceOrderScope { serverId: string; projectViewKey: string; @@ -69,13 +84,14 @@ function normalizeLegacyWorkspaceKey(serverId: string, rawWorkspaceKey: string): export function migrateSidebarOrderState(persistedState: unknown): { projectOrder: string[]; + pinnedWorkspaceOrder: string[]; workspaceOrderByProject: Record; } { - const state = persistedState as SidebarOrderPersistedState | undefined; - - if (!state) { - return { projectOrder: [], workspaceOrderByProject: {} }; + const result = SidebarOrderPersistedStateSchema.safeParse(persistedState); + if (!result.success) { + return { projectOrder: [], pinnedWorkspaceOrder: [], workspaceOrderByProject: {} }; } + const state: SidebarOrderPersistedState = result.data; const projectOrder = normalizeKeys(state.projectOrder ?? []); const seenProjects = new Set(projectOrder); @@ -103,19 +119,29 @@ export function migrateSidebarOrderState(persistedState: unknown): { workspaceOrderByProject[scope.projectViewKey] = merged; } - return { projectOrder, workspaceOrderByProject }; + return { + projectOrder, + pinnedWorkspaceOrder: normalizeKeys(state.pinnedWorkspaceOrder ?? []), + workspaceOrderByProject, + }; } export const useSidebarOrderStore = create()( persist( (set, get) => ({ projectOrder: [], + pinnedWorkspaceOrder: [], workspaceOrderByProject: {}, getProjectOrder: () => get().projectOrder, setProjectOrder: (keys) => { const normalized = normalizeKeys(keys); set({ projectOrder: normalized }); }, + getPinnedWorkspaceOrder: () => get().pinnedWorkspaceOrder, + setPinnedWorkspaceOrder: (keys) => { + const normalized = normalizeKeys(keys); + set({ pinnedWorkspaceOrder: normalized }); + }, getWorkspaceOrder: (projectViewKey) => { const scope = projectViewKey.trim(); if (!scope) return []; @@ -135,9 +161,10 @@ export const useSidebarOrderStore = create()( }), { name: "sidebar-project-workspace-order", - storage: createJSONStorage(() => AsyncStorage), + storage: createValidatedPersistStorage(AsyncStorage, SidebarOrderPersistedStateSchema), partialize: (state) => ({ projectOrder: state.projectOrder, + pinnedWorkspaceOrder: state.pinnedWorkspaceOrder, workspaceOrderByProject: state.workspaceOrderByProject, }), version: 1, diff --git a/packages/app/src/stores/sidebar-view-store.ts b/packages/app/src/stores/sidebar-view-store.ts index 52d2198d98..e8d77b0a08 100644 --- a/packages/app/src/stores/sidebar-view-store.ts +++ b/packages/app/src/stores/sidebar-view-store.ts @@ -1,6 +1,8 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { create } from "zustand"; -import { createJSONStorage, persist, type StateStorage } from "zustand/middleware"; +import { persist, type StateStorage } from "zustand/middleware"; +import { z } from "zod"; +import { createValidatedPersistStorage } from "@/storage/validated-persist-storage"; export type SidebarGroupMode = "project" | "status"; @@ -23,51 +25,55 @@ interface SidebarViewPersistedState { hostFilters: string[]; } -function isSidebarGroupMode(value: unknown): value is SidebarGroupMode { - return value === "project" || value === "status"; -} +const SidebarGroupModeSchema = z.enum(["project", "status"]); +const SidebarViewPersistedStateSchema = z.strictObject({ + groupMode: SidebarGroupModeSchema.optional(), + hostFilters: z.array(z.string()).optional(), + hostFilter: z.string().nullable().optional(), + groupModeByServerId: z.record(z.string(), SidebarGroupModeSchema).optional(), +}); -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} +type SidebarViewStorageState = z.infer; -function readLegacyGroupMode(persistedState: Record): SidebarGroupMode | null { +function readLegacyGroupMode(persistedState: SidebarViewStorageState): SidebarGroupMode | null { const groupModeByServerId = persistedState.groupModeByServerId; - if (!isRecord(groupModeByServerId)) { + if (!groupModeByServerId) { return null; } - const modes = Object.values(groupModeByServerId).filter(isSidebarGroupMode); + const modes = Object.values(groupModeByServerId); if (modes.length === 0) return null; return modes.includes("status") ? "status" : "project"; } // Reads the host filter from any persisted shape: the current `hostFilters` array, or the // pre-v2 single `hostFilter` string (null/absent meant "all hosts"). -function readHostFilters(persistedState: Record): string[] { +function readHostFilters(persistedState: SidebarViewStorageState): string[] { const hostFilters = persistedState.hostFilters; - if (Array.isArray(hostFilters)) { - return hostFilters.filter((value): value is string => typeof value === "string"); + if (hostFilters) { + return hostFilters; } // COMPAT(sidebarHostFilters): added in v0.1.102, remove after 2026-12-30 once pre-v2 persisted // sidebar state (a single `hostFilter` string) has aged out. const legacyHostFilter = persistedState.hostFilter; - return typeof legacyHostFilter === "string" ? [legacyHostFilter] : []; + return legacyHostFilter ? [legacyHostFilter] : []; } export function migrateSidebarViewState(persistedState: unknown): SidebarViewPersistedState { - if (!isRecord(persistedState)) { + const result = SidebarViewPersistedStateSchema.safeParse(persistedState); + if (!result.success) { return { groupMode: "project", hostFilters: [] }; } + const state = result.data; - const legacyGroupMode = readLegacyGroupMode(persistedState); + const legacyGroupMode = readLegacyGroupMode(state); if (legacyGroupMode) { return { groupMode: legacyGroupMode, hostFilters: [] }; } return { - groupMode: isSidebarGroupMode(persistedState.groupMode) ? persistedState.groupMode : "project", - hostFilters: readHostFilters(persistedState), + groupMode: state.groupMode ?? "project", + hostFilters: readHostFilters(state), }; } @@ -116,7 +122,10 @@ export const useSidebarViewStore = create()( { name: SIDEBAR_VIEW_STORAGE_KEY, version: SIDEBAR_VIEW_STORE_VERSION, - storage: createJSONStorage(createSidebarViewStorage), + storage: createValidatedPersistStorage( + createSidebarViewStorage(), + SidebarViewPersistedStateSchema, + ), partialize: (state) => ({ groupMode: state.groupMode, hostFilters: state.hostFilters, diff --git a/packages/app/src/stores/workspace-layout-store.test.ts b/packages/app/src/stores/workspace-layout-store.test.ts index 24d162eae4..4951d28687 100644 --- a/packages/app/src/stores/workspace-layout-store.test.ts +++ b/packages/app/src/stores/workspace-layout-store.test.ts @@ -15,6 +15,7 @@ vi.mock("@react-native-async-storage/async-storage", () => { }; }); +import AsyncStorage from "@react-native-async-storage/async-storage"; import { buildWorkspaceTabPersistenceKey, type WorkspaceTab } from "@/workspace-tabs/model"; import { collectAllPanes, @@ -316,6 +317,24 @@ describe("workspace-layout-store actions", () => { }); }); + it("keeps layouts written before split sizes were persisted", async () => { + const legacyLayout = createDefaultLayout(); + await AsyncStorage.setItem( + "workspace-layout-state", + JSON.stringify({ + state: { layoutByWorkspace: { legacy: legacyLayout } }, + version: 1, + }), + ); + const restored = createWorkspaceLayoutStore(createDeterministicWorkspaceLayoutIds()); + + await restored.persist.rehydrate(); + + expect(restored.getState().layoutByWorkspace.legacy).toEqual(legacyLayout); + expect(restored.getState().splitSizesByWorkspace).toEqual({}); + await expect(AsyncStorage.getItem("workspace-layout-state")).resolves.not.toBeNull(); + }); + it("opens tabs into the focused pane and focuses duplicate opens instead of creating them", () => { useWorkspaceLayoutIds("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); const workspaceKey = createWorkspaceKey(); diff --git a/packages/app/src/stores/workspace-layout-store.ts b/packages/app/src/stores/workspace-layout-store.ts index dee6c58693..8631756a6c 100644 --- a/packages/app/src/stores/workspace-layout-store.ts +++ b/packages/app/src/stores/workspace-layout-store.ts @@ -1,7 +1,8 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { useEffect, useState } from "react"; import { create } from "zustand"; -import { createJSONStorage, persist } from "zustand/middleware"; +import { persist } from "zustand/middleware"; +import { z } from "zod"; import type { WorkspaceTab, WorkspaceTabTarget } from "@/workspace-tabs/model"; import { defaultWorkspaceLayoutIds, @@ -42,6 +43,7 @@ import { type WorkspaceLayout, } from "@/stores/workspace-layout-actions"; import { normalizeWorkspaceTabTarget } from "@/workspace-tabs/identity"; +import { createValidatedPersistStorage } from "@/storage/validated-persist-storage"; export { collectAllPanes, @@ -123,6 +125,83 @@ interface WorkspaceFocusRestorationState { const MAX_TREE_DEPTH = 4; +const WorkspaceDraftTabSetupStorageSchema = z.strictObject({ + provider: z.string(), + cwd: z.string(), + modeId: z.string().nullable(), + model: z.string().nullable(), + thinkingOptionId: z.string().nullable(), + featureValues: z.record(z.string(), z.union([z.boolean(), z.string(), z.null()])), +}); +const WorkspaceTabTargetStorageSchema = z.discriminatedUnion("kind", [ + z.strictObject({ + kind: z.literal("draft"), + draftId: z.string(), + setup: WorkspaceDraftTabSetupStorageSchema.optional(), + }), + z.strictObject({ kind: z.literal("agent"), agentId: z.string() }), + z.strictObject({ + kind: z.literal("provider_subagent"), + parentAgentId: z.string(), + subagentId: z.string(), + }), + z.strictObject({ kind: z.literal("terminal"), terminalId: z.string() }), + z.strictObject({ kind: z.literal("browser"), browserId: z.string() }), + z.strictObject({ + kind: z.literal("file"), + path: z.string(), + lineStart: z.number().int().positive().optional(), + lineEnd: z.number().int().positive().optional(), + }), + z.strictObject({ + kind: z.literal("working_diff"), + focusPath: z.string().optional(), + focusRequestId: z.number().optional(), + // COMPAT(workingDiffTarget): accepted from pre-canonical tab ids; normalization removes them. + mode: z.enum(["uncommitted", "base"]).optional(), + baseRef: z.string().nullable().optional(), + ignoreWhitespace: z.boolean().optional(), + }), + z.strictObject({ kind: z.literal("setup"), workspaceId: z.string() }), + z.strictObject({ kind: z.literal("commit_diff"), sha: z.string() }), +]); +const WorkspaceTabStorageSchema = z.strictObject({ + tabId: z.string(), + target: WorkspaceTabTargetStorageSchema, + createdAt: z.number(), +}); +const SplitNodeStorageSchema: z.ZodType = z.lazy(() => + z.discriminatedUnion("kind", [ + z.strictObject({ + kind: z.literal("pane"), + pane: z.strictObject({ + id: z.string(), + tabIds: z.array(z.string()), + focusedTabId: z.string().nullable(), + tabs: z.array(WorkspaceTabStorageSchema).optional(), + }), + }), + z.strictObject({ + kind: z.literal("group"), + group: z.strictObject({ + id: z.string(), + direction: z.enum(["horizontal", "vertical"]), + children: z.array(SplitNodeStorageSchema), + sizes: z.array(z.number()), + }), + }), + ]), +); +const WorkspaceLayoutStorageSchema: z.ZodType = z.strictObject({ + root: SplitNodeStorageSchema, + focusedPaneId: z.string().nullable(), + parentTabIdByTabId: z.record(z.string(), z.string()).optional(), +}); +const WorkspaceLayoutPersistedStateSchema = z.strictObject({ + layoutByWorkspace: z.record(z.string(), WorkspaceLayoutStorageSchema), + splitSizesByWorkspace: z.record(z.string(), z.record(z.string(), z.array(z.number()))).optional(), +}); + function trimNonEmpty(value: string | null | undefined): string | null { if (typeof value !== "string") { return null; @@ -942,7 +1021,7 @@ export function createWorkspaceLayoutStore( { name: "workspace-layout-state", version: 1, - storage: createJSONStorage(() => AsyncStorage), + storage: createValidatedPersistStorage(AsyncStorage, WorkspaceLayoutPersistedStateSchema), partialize: (state) => { const layoutByWorkspace: Record = {}; for (const key in state.layoutByWorkspace) { @@ -957,6 +1036,17 @@ export function createWorkspaceLayoutStore( splitSizesByWorkspace: state.splitSizesByWorkspace, }; }, + merge: (persistedState, currentState) => { + const result = WorkspaceLayoutPersistedStateSchema.safeParse(persistedState); + if (!result.success) { + return currentState; + } + return { + ...currentState, + layoutByWorkspace: result.data.layoutByWorkspace, + splitSizesByWorkspace: result.data.splitSizesByWorkspace ?? {}, + }; + }, }, ), ); diff --git a/packages/app/src/styles/theme.test.ts b/packages/app/src/styles/theme.test.ts index daa6004948..ea2164192c 100644 --- a/packages/app/src/styles/theme.test.ts +++ b/packages/app/src/styles/theme.test.ts @@ -1,5 +1,22 @@ import { describe, expect, it } from "vitest"; -import { darkPureBlackTheme } from "./theme"; +import { darkPureBlackTheme, getNextThemePreference, THEME_OPTIONS } from "./theme"; + +describe("Theme catalog", () => { + it("owns the picker and shortcut order", () => { + expect(THEME_OPTIONS.map((option) => option.name)).toEqual([ + "light", + "dark", + "auto", + "zinc", + "midnight", + "claude", + "ghostty", + "pureBlack", + ]); + expect(getNextThemePreference("dark")).toBe("auto"); + expect(getNextThemePreference("pureBlack")).toBe("light"); + }); +}); describe("Pure black theme", () => { it("uses a pure black application and terminal background", () => { @@ -13,6 +30,11 @@ describe("Pure black theme", () => { expect(darkPureBlackTheme.colors.accentBright).toBe("#7ccba0"); }); + it("keeps selected sidebar rows distinct from the black sidebar", () => { + expect(darkPureBlackTheme.colors.surfaceSidebar).toBe("#000000"); + expect(darkPureBlackTheme.colors.surfaceSidebarHover).toBe("#161616"); + }); + it("keeps ANSI black output readable on its zero-luminance terminal background", () => { expect(darkPureBlackTheme.colors.terminal.black).toBe("#595959"); expect(darkPureBlackTheme.colors.terminal.brightBlack).toBe("#8a8a8a"); diff --git a/packages/app/src/styles/theme.ts b/packages/app/src/styles/theme.ts index 709061a988..408ed87ae4 100644 --- a/packages/app/src/styles/theme.ts +++ b/packages/app/src/styles/theme.ts @@ -107,8 +107,6 @@ export const baseColors = { }, } as const; -export type ThemeName = "light" | "dark" | "zinc" | "midnight" | "claude" | "ghostty" | "pureBlack"; - // Diff colors — the +/- inside a diff view, where the color *is* the signal and has to // survive being scanned line by line, so it stays saturated. Light uses muted tones, dark // uses the brighter palette values. @@ -669,7 +667,8 @@ const pureBlackDarkColors = buildDarkSemanticColors({ surface4: "#2d2d2d", surfaceDiffEmpty: "#0c0c0c", surfaceSidebar: "#000000", - surfaceSidebarHover: "#0d0d0d", + // Selected sidebar rows share this surface with hover, so it must remain visible at rest. + surfaceSidebarHover: "#161616", foregroundMuted: "#a1a1aa", foregroundExtraMuted: "#71717a", scrollbarHandle: "#71717a", @@ -717,40 +716,94 @@ export const lightTheme = { // Keep compatibility with existing code export const theme = darkTheme; -export type Theme = - | typeof darkTheme - | typeof darkZincTheme - | typeof darkMidnightTheme - | typeof darkClaudeTheme - | typeof darkGhosttyTheme - | typeof darkPureBlackTheme - | typeof lightTheme; +export const THEME_OPTIONS = [ + { + name: "light", + group: "primary", + unistylesName: "light", + theme: lightTheme, + swatch: "#ffffff", + }, + { + name: "dark", + group: "primary", + unistylesName: "dark", + theme: darkTheme, + swatch: "#2D8B62", + }, + { name: "auto", group: "primary" }, + { + name: "zinc", + group: "variant", + unistylesName: "darkZinc", + theme: darkZincTheme, + swatch: "#808080", + }, + { + name: "midnight", + group: "variant", + unistylesName: "darkMidnight", + theme: darkMidnightTheme, + swatch: "#4A6BA8", + }, + { + name: "claude", + group: "variant", + unistylesName: "darkClaude", + theme: darkClaudeTheme, + swatch: "#D97757", + }, + { + name: "ghostty", + group: "variant", + unistylesName: "darkGhostty", + theme: darkGhosttyTheme, + swatch: "#8caaee", + }, + { + name: "pureBlack", + group: "variant", + unistylesName: "darkPureBlack", + theme: darkPureBlackTheme, + swatch: "#000000", + }, +] as const; -type UnistylesThemeKey = - | "light" - | "dark" - | "darkZinc" - | "darkMidnight" - | "darkClaude" - | "darkGhostty" - | "darkPureBlack"; +export type ThemePreference = (typeof THEME_OPTIONS)[number]["name"]; +export type ThemeName = Exclude; +type ConcreteThemeOption = Exclude<(typeof THEME_OPTIONS)[number], { name: "auto" }>; +export type Theme = ConcreteThemeOption["theme"]; -export const THEME_TO_UNISTYLES: Record = { - light: "light", - dark: "dark", - zinc: "darkZinc", - midnight: "darkMidnight", - claude: "darkClaude", - ghostty: "darkGhostty", - pureBlack: "darkPureBlack", +const CONCRETE_THEME_OPTIONS = THEME_OPTIONS.filter( + (option): option is ConcreteThemeOption => option.name !== "auto", +); + +type ThemeToUnistyles = { + [Name in ThemeName]: Extract["unistylesName"]; }; -export const THEME_SWATCHES: Record = { - light: "#ffffff", - dark: "#2D8B62", - zinc: "#808080", - midnight: "#4A6BA8", - claude: "#D97757", - ghostty: "#8caaee", - pureBlack: "#000000", +type ThemeSwatches = { + [Name in ThemeName]: Extract["swatch"]; }; + +type RegisteredThemes = { + [Option in ConcreteThemeOption as Option["unistylesName"]]: Option["theme"]; +}; + +export const THEME_TO_UNISTYLES = Object.fromEntries( + CONCRETE_THEME_OPTIONS.map((option) => [option.name, option.unistylesName]), +) as ThemeToUnistyles; + +export const THEME_SWATCHES = Object.fromEntries( + CONCRETE_THEME_OPTIONS.map((option) => [option.name, option.swatch]), +) as ThemeSwatches; + +export const REGISTERED_THEMES = Object.fromEntries( + CONCRETE_THEME_OPTIONS.map((option) => [option.unistylesName, option.theme]), +) as RegisteredThemes; + +export function getNextThemePreference(current: ThemePreference): ThemePreference { + const currentIndex = THEME_OPTIONS.findIndex((option) => option.name === current); + const nextIndex = (currentIndex + 1) % THEME_OPTIONS.length; + return THEME_OPTIONS[nextIndex]?.name ?? THEME_OPTIONS[0].name; +} diff --git a/packages/app/src/styles/unistyles.ts b/packages/app/src/styles/unistyles.ts index 46124d0530..b679d5e694 100644 --- a/packages/app/src/styles/unistyles.ts +++ b/packages/app/src/styles/unistyles.ts @@ -1,24 +1,8 @@ import { StyleSheet } from "react-native-unistyles"; -import { - lightTheme, - darkTheme, - darkZincTheme, - darkMidnightTheme, - darkClaudeTheme, - darkGhosttyTheme, - darkPureBlackTheme, -} from "./theme"; +import { REGISTERED_THEMES } from "./theme"; StyleSheet.configure({ - themes: { - light: lightTheme, - dark: darkTheme, - darkZinc: darkZincTheme, - darkMidnight: darkMidnightTheme, - darkClaude: darkClaudeTheme, - darkGhostty: darkGhosttyTheme, - darkPureBlack: darkPureBlackTheme, - }, + themes: REGISTERED_THEMES, breakpoints: { xs: 0, sm: 576, @@ -32,15 +16,7 @@ StyleSheet.configure({ }); // Type augmentation for TypeScript -interface AppThemes { - light: typeof lightTheme; - dark: typeof darkTheme; - darkZinc: typeof darkZincTheme; - darkMidnight: typeof darkMidnightTheme; - darkClaude: typeof darkClaudeTheme; - darkGhostty: typeof darkGhosttyTheme; - darkPureBlack: typeof darkPureBlackTheme; -} +type AppThemes = typeof REGISTERED_THEMES; interface AppBreakpoints { xs: number; diff --git a/packages/app/src/subagents/open-tab-labels.test.ts b/packages/app/src/subagents/open-tab-labels.test.ts new file mode 100644 index 0000000000..61874cd058 --- /dev/null +++ b/packages/app/src/subagents/open-tab-labels.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; +import type { Agent } from "@/stores/session-store"; +import type { WorkspaceTab } from "@/workspace-tabs/model"; +import { getAgentTabsNeedingOpenLabel } from "./open-tab-labels"; + +const label = "paseo.open-agent-tab.client-1"; + +function tab(agentId: string, createdAt: number): WorkspaceTab { + return { tabId: `agent_${agentId}`, target: { kind: "agent", agentId }, createdAt }; +} + +function agent(input: { id: string; parentAgentId?: string | null; open?: boolean }): Agent { + return { + id: input.id, + parentAgentId: input.parentAgentId ?? null, + labels: input.open ? { [label]: "true" } : {}, + } as Agent; +} + +describe("getAgentTabsNeedingOpenLabel", () => { + it("marks every present parented tab, regardless of tab order or focus", () => { + const agents = new Map([ + ["background-child", agent({ id: "background-child", parentAgentId: "parent" })], + ["focused-child", agent({ id: "focused-child", parentAgentId: "parent" })], + ["root", agent({ id: "root" })], + ]); + + expect( + getAgentTabsNeedingOpenLabel({ + tabs: [tab("background-child", 1), tab("root", 2), tab("focused-child", 3)], + getAgent: (agentId) => agents.get(agentId), + label, + pendingAgentIds: new Set(), + }), + ).toEqual(["background-child", "focused-child"]); + }); + + it("skips tabs already marked or currently being marked", () => { + const agents = new Map([ + ["marked", agent({ id: "marked", parentAgentId: "parent", open: true })], + ["pending", agent({ id: "pending", parentAgentId: "parent" })], + ]); + + expect( + getAgentTabsNeedingOpenLabel({ + tabs: [tab("marked", 1), tab("pending", 2)], + getAgent: (agentId) => agents.get(agentId), + label, + pendingAgentIds: new Set(["pending"]), + }), + ).toEqual([]); + }); +}); diff --git a/packages/app/src/subagents/open-tab-labels.ts b/packages/app/src/subagents/open-tab-labels.ts new file mode 100644 index 0000000000..ca7d8e63dc --- /dev/null +++ b/packages/app/src/subagents/open-tab-labels.ts @@ -0,0 +1,25 @@ +import type { Agent } from "@/stores/session-store"; +import type { WorkspaceTab } from "@/workspace-tabs/model"; + +export function getAgentTabsNeedingOpenLabel(input: { + tabs: WorkspaceTab[]; + getAgent: (agentId: string) => Agent | null | undefined; + label: string; + pendingAgentIds: ReadonlySet; +}): string[] { + const agentIds = new Set(); + for (const tab of input.tabs) { + if (tab.target.kind !== "agent") { + continue; + } + const agent = input.getAgent(tab.target.agentId); + if ( + agent?.parentAgentId && + agent.labels[input.label] !== "true" && + !input.pendingAgentIds.has(agent.id) + ) { + agentIds.add(agent.id); + } + } + return [...agentIds]; +} diff --git a/packages/app/src/subagents/track.tsx b/packages/app/src/subagents/track.tsx index f49ed12f61..8878851da1 100644 --- a/packages/app/src/subagents/track.tsx +++ b/packages/app/src/subagents/track.tsx @@ -1,9 +1,10 @@ import { useCallback, useMemo, useState, type ReactElement } from "react"; -import { Pressable, ScrollView, Text, View, type PressableStateCallbackType } from "react-native"; +import { Pressable, ScrollView, Text, View } from "react-native"; import { useTranslation } from "react-i18next"; import { Archive, ChevronDown, ChevronRight, Unlink } from "lucide-react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { getProviderIcon } from "@/components/provider-icons"; +import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useIsCompactFormFactor, MAX_CONTENT_WIDTH } from "@/constants/layout"; import { isNative } from "@/constants/platform"; @@ -20,8 +21,6 @@ import { } from "./track-presentation"; const ThemedArchive = withUnistyles(Archive); -const ThemedChevronDown = withUnistyles(ChevronDown); -const ThemedChevronRight = withUnistyles(ChevronRight); const ThemedUnlink = withUnistyles(Unlink); const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground }); @@ -70,17 +69,11 @@ export function SubagentsTrack({ [expanded], ); - const headerStyle = useCallback( - ({ hovered, pressed }: PressableStateCallbackType) => [ - styles.headerToggle, - (hovered || pressed) && styles.headerActive, - ], - [], - ); const headerContainerStyle = useMemo( () => [styles.header, expanded ? styles.headerDivider : styles.headerCollapsed], [expanded], ); + const headerAccessibilityState = useMemo(() => ({ expanded }), [expanded]); if (rows.length === 0) { return null; @@ -94,22 +87,19 @@ export function SubagentsTrack({ - - {expanded ? ( - - ) : ( - - )} - - {headerLabel} - - + {headerLabel} + {finishedCount > 0 && onArchiveFinished ? ( ({ headerToggle: { flex: 1, minWidth: 0, - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[2], + justifyContent: "flex-start", + borderRadius: 0, paddingLeft: theme.spacing[3], paddingRight: theme.spacing[1], paddingVertical: theme.spacing[2], @@ -359,9 +348,6 @@ const styles = StyleSheet.create((theme) => ({ headerCollapsed: { paddingBottom: theme.spacing[4], }, - headerActive: { - backgroundColor: theme.colors.surface2, - }, headerDivider: { borderBottomWidth: theme.borderWidth[1], borderBottomColor: theme.colors.border, diff --git a/packages/app/src/subagents/use-open-agent-tab-labels.ts b/packages/app/src/subagents/use-open-agent-tab-labels.ts new file mode 100644 index 0000000000..ea02fd1b16 --- /dev/null +++ b/packages/app/src/subagents/use-open-agent-tab-labels.ts @@ -0,0 +1,86 @@ +import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; +import { getOpenAgentTabLabel } from "@getpaseo/protocol/agent-labels"; +import { useEffect, useRef, useState } from "react"; +import { useSessionStore } from "@/stores/session-store"; +import { getOrCreateClientId } from "@/utils/client-id"; +import type { WorkspaceTab } from "@/workspace-tabs/model"; +import { getAgentTabsNeedingOpenLabel } from "./open-tab-labels"; + +function increment(value: number): number { + return value + 1; +} + +export function useOpenAgentTabLabels(input: { + client: DaemonClient | null; + serverId: string; + tabs: WorkspaceTab[]; + enabled: boolean; +}): void { + const agents = useSessionStore((state) => state.sessions[input.serverId]?.agents ?? null); + const agentDetails = useSessionStore( + (state) => state.sessions[input.serverId]?.agentDetails ?? null, + ); + const pendingAgentIdsRef = useRef(new Set()); + const retryTimerRef = useRef | null>(null); + const [retryVersion, setRetryVersion] = useState(0); + + useEffect( + () => () => { + if (retryTimerRef.current) { + clearTimeout(retryTimerRef.current); + } + }, + [], + ); + + useEffect(() => { + const client = input.client; + if (!client || !input.enabled) { + return; + } + + const openAgentIds = new Set( + input.tabs.flatMap((tab) => (tab.target.kind === "agent" ? [tab.target.agentId] : [])), + ); + for (const agentId of pendingAgentIdsRef.current) { + if (!openAgentIds.has(agentId)) { + pendingAgentIdsRef.current.delete(agentId); + } + } + if (openAgentIds.size === 0) { + return; + } + + void (async () => { + try { + const clientId = await getOrCreateClientId(); + const label = getOpenAgentTabLabel(clientId); + const agentIds = getAgentTabsNeedingOpenLabel({ + tabs: input.tabs, + getAgent: (agentId) => agents?.get(agentId) ?? agentDetails?.get(agentId), + label, + pendingAgentIds: pendingAgentIdsRef.current, + }); + for (const agentId of agentIds) { + pendingAgentIdsRef.current.add(agentId); + try { + await client.updateAgent(agentId, { labels: { [label]: "true" } }); + } catch (error) { + console.warn("[OpenAgentTabLabels] Failed to mark open subagent tab", { + error, + agentId, + }); + retryTimerRef.current ??= setTimeout(() => { + retryTimerRef.current = null; + setRetryVersion(increment); + }, 2_000); + } finally { + pendingAgentIdsRef.current.delete(agentId); + } + } + } catch (error) { + console.warn("[OpenAgentTabLabels] Failed to resolve client ID", { error }); + } + })(); + }, [agentDetails, agents, input.client, input.enabled, input.tabs, retryVersion]); +} diff --git a/packages/app/src/timeline/session-stream-reducers.test.ts b/packages/app/src/timeline/session-stream-reducers.test.ts index c89855105b..a0daa3db1d 100644 --- a/packages/app/src/timeline/session-stream-reducers.test.ts +++ b/packages/app/src/timeline/session-stream-reducers.test.ts @@ -3557,6 +3557,59 @@ describe("processAgentStreamEvent", () => { expect(result.sideEffects).toEqual([]); }); + it("publishes task snapshots only from accepted timeline events", () => { + const tasks = [ + { id: "inspect", text: "Inspect", status: "in_progress" as const, completed: false }, + ]; + const event: AgentStreamEventPayload = { + type: "timeline", + provider: "codex", + item: { + type: "todo", + items: tasks, + }, + }; + const currentCursor: TimelineCursor = { + epoch: "epoch-1", + startSeq: 1, + endSeq: 4, + }; + + const accepted = processAgentStreamEvent({ + ...baseStreamInput, + event, + seq: 5, + epoch: "epoch-1", + currentCursor, + }); + const stale = processAgentStreamEvent({ + ...baseStreamInput, + event, + seq: 4, + epoch: "epoch-1", + currentCursor, + }); + const wrongEpoch = processAgentStreamEvent({ + ...baseStreamInput, + event, + seq: 5, + epoch: "epoch-2", + currentCursor, + }); + const gapped = processAgentStreamEvent({ + ...baseStreamInput, + event, + seq: 8, + epoch: "epoch-1", + currentCursor, + }); + + expect(accepted.taskSnapshot).toEqual(tasks); + expect(stale.taskSnapshot).toBeUndefined(); + expect(wrongEpoch.taskSnapshot).toBeUndefined(); + expect(gapped.taskSnapshot).toBeUndefined(); + }); + it("detects gap and emits catch-up side effect", () => { const existingCursor: TimelineCursor = { epoch: "epoch-1", diff --git a/packages/app/src/timeline/session-stream-reducers.ts b/packages/app/src/timeline/session-stream-reducers.ts index 158a8ead85..4d6515ada2 100644 --- a/packages/app/src/timeline/session-stream-reducers.ts +++ b/packages/app/src/timeline/session-stream-reducers.ts @@ -1,6 +1,6 @@ import type { AgentStreamEventPayload } from "@getpaseo/protocol/messages"; import { selectAgentTimelineState, useSessionStore } from "@/stores/session-store"; -import type { AssistantMessageItem, StreamItem } from "@/types/stream"; +import type { AssistantMessageItem, StreamItem, TodoEntry } from "@/types/stream"; import type { TurnLivenessTransition } from "@/timeline/turn-liveness"; import { applyStreamEvent, @@ -1436,6 +1436,7 @@ export interface ProcessAgentStreamEventOutput { cursor: TimelineCursor | null; cursorChanged: boolean; acknowledgedClientMessageIds: string[]; + taskSnapshot?: TodoEntry[]; sideEffects: AgentStreamReducerSideEffect[]; } @@ -1642,6 +1643,9 @@ export function processAgentStreamEvent( cursor: sequencing.nextTimelineCursor, cursorChanged: sequencing.cursorChanged, acknowledgedClientMessageIds: streamResult.acknowledgedClientMessageIds ?? [], + ...(sequencing.shouldApplyStreamEvent && event.type === "timeline" && event.item.type === "todo" + ? { taskSnapshot: event.item.items } + : {}), sideEffects: sequencing.sideEffects, }; } @@ -1667,6 +1671,7 @@ export function processAgentStreamEvents( let changedTail = false; let changedHead = false; let cursorChanged = false; + let taskSnapshot: TodoEntry[] | undefined; const acknowledgedClientMessageIds = new Set(); const sideEffects: AgentStreamReducerSideEffect[] = []; @@ -1690,6 +1695,9 @@ export function processAgentStreamEvents( for (const clientMessageId of result.acknowledgedClientMessageIds) { acknowledgedClientMessageIds.add(clientMessageId); } + if (result.taskSnapshot !== undefined) { + taskSnapshot = result.taskSnapshot; + } if (result.cursorChanged) { cursor = result.cursor ?? undefined; @@ -1705,6 +1713,7 @@ export function processAgentStreamEvents( cursor: cursor ?? null, cursorChanged, acknowledgedClientMessageIds: [...acknowledgedClientMessageIds], + ...(taskSnapshot !== undefined ? { taskSnapshot } : {}), sideEffects, }; } @@ -1788,6 +1797,7 @@ interface StreamStatePatch { tail?: StreamItem[]; head?: StreamItem[]; acknowledgedClientMessageIds?: readonly string[]; + taskSnapshot?: TodoEntry[]; } export function deriveAgentStreamTurnLiveness( @@ -1850,7 +1860,8 @@ export function createSessionAgentStreamReducerQueue( if ( result.changedTail || result.changedHead || - result.acknowledgedClientMessageIds.length > 0 + result.acknowledgedClientMessageIds.length > 0 || + result.taskSnapshot !== undefined ) { setAgentStreamState(serverId, agentId, { ...(result.changedTail ? { tail: result.tail } : {}), @@ -1858,6 +1869,7 @@ export function createSessionAgentStreamReducerQueue( ...(result.acknowledgedClientMessageIds.length > 0 ? { acknowledgedClientMessageIds: result.acknowledgedClientMessageIds } : {}), + ...(result.taskSnapshot !== undefined ? { taskSnapshot: result.taskSnapshot } : {}), }); } if (result.cursorChanged && result.cursor) { diff --git a/packages/app/src/types/host-connection.ts b/packages/app/src/types/host-connection.ts index 421f5868c0..2105312e88 100644 --- a/packages/app/src/types/host-connection.ts +++ b/packages/app/src/types/host-connection.ts @@ -9,8 +9,9 @@ import { import { type HostAppearance, defaultHostAppearance, - normalizeStoredHostAppearance, + HostAppearanceSchema, } from "@/hosts/appearance"; +import { z } from "zod"; export { DirectTcpHostConnectionSchema, type DirectTcpHostConnection }; @@ -293,54 +294,74 @@ export function connectionFromListen(listen: string): HostConnection | null { } } -function isPlainRecord(value: unknown): value is Record { - return Boolean(value) && typeof value === "object" && !Array.isArray(value); -} +const StoredHostConnectionSchema = z.discriminatedUnion("type", [ + z.strictObject({ + id: z.string().optional(), + type: z.literal("directTcp"), + endpoint: z.string(), + useTls: z.boolean().optional(), + password: z.string().optional(), + }), + z.strictObject({ + id: z.string().optional(), + type: z.literal("directSocket"), + path: z.string(), + }), + z.strictObject({ + id: z.string().optional(), + type: z.literal("directPipe"), + path: z.string(), + }), + z.strictObject({ + id: z.string().optional(), + type: z.literal("relay"), + relayEndpoint: z.string(), + useTls: z.boolean().optional(), + daemonPublicKeyB64: z.string(), + }), +]); +const StoredHostProfileSchema = z.strictObject({ + serverId: z.string().trim().min(1), + label: z.string().optional(), + appearance: HostAppearanceSchema.optional(), + lifecycle: z.strictObject({}).optional(), + connections: z.array(StoredHostConnectionSchema).min(1), + preferredConnectionId: z.string().nullable().optional(), + createdAt: z.string().datetime({ offset: true }).optional(), + updatedAt: z.string().datetime({ offset: true }).optional(), +}); +export const StoredHostRegistrySchema = z.array(StoredHostProfileSchema); +type StoredHostConnection = z.infer; -function toObjectRecord(value: unknown): Record | undefined { - return isPlainRecord(value) ? value : undefined; -} - -function normalizeStoredConnection(connection: unknown): HostConnection | null { - const record = toObjectRecord(connection); - if (!record) { - return null; - } - const type = record.type; - if (type === "directTcp") { +function normalizeStoredConnection(connection: StoredHostConnection): HostConnection | null { + if (connection.type === "directTcp") { try { - const endpoint = normalizeLoopbackToLocalhost( - normalizeHostPort(typeof record.endpoint === "string" ? record.endpoint : ""), - ); + const endpoint = normalizeLoopbackToLocalhost(normalizeHostPort(connection.endpoint)); return DirectTcpHostConnectionSchema.parse({ id: `direct:${endpoint}`, type: "directTcp", endpoint, - useTls: record.useTls, - ...(typeof record.password === "string" ? { password: record.password } : {}), + useTls: connection.useTls, + ...(connection.password !== undefined ? { password: connection.password } : {}), }); } catch { return null; } } - if (type === "directSocket") { - const path = (typeof record.path === "string" ? record.path : "").trim(); + if (connection.type === "directSocket") { + const path = connection.path.trim(); return path ? { id: `socket:${path}`, type: "directSocket", path } : null; } - if (type === "directPipe") { - const path = (typeof record.path === "string" ? record.path : "").trim(); + if (connection.type === "directPipe") { + const path = connection.path.trim(); return path ? { id: `pipe:${path}`, type: "directPipe", path } : null; } - if (type === "relay") { + if (connection.type === "relay") { try { - const relayEndpoint = normalizeHostPort( - typeof record.relayEndpoint === "string" ? record.relayEndpoint : "", - ); - const daemonPublicKeyB64 = ( - typeof record.daemonPublicKeyB64 === "string" ? record.daemonPublicKeyB64 : "" - ).trim(); + const relayEndpoint = normalizeHostPort(connection.relayEndpoint); + const daemonPublicKeyB64 = connection.daemonPublicKeyB64.trim(); if (!daemonPublicKeyB64) return null; - const useTls = typeof record.useTls === "boolean" ? record.useTls : undefined; + const useTls = connection.useTls; return { id: useTls === true ? `relay:wss:${relayEndpoint}` : `relay:${relayEndpoint}`, type: "relay", @@ -357,17 +378,14 @@ function normalizeStoredConnection(connection: unknown): HostConnection | null { } export function normalizeStoredHostProfile(entry: unknown): HostProfile | null { - const record = toObjectRecord(entry); - if (!record) { - return null; - } - const serverId = typeof record.serverId === "string" ? record.serverId.trim() : ""; - if (!serverId) { + const result = StoredHostProfileSchema.safeParse(entry); + if (!result.success) { return null; } + const record = result.data; + const serverId = record.serverId; - const rawConnections = Array.isArray(record.connections) ? record.connections : []; - const connections = rawConnections + const connections = record.connections .map((connection) => normalizeStoredConnection(connection)) .filter((connection): connection is HostConnection => connection !== null); if (connections.length === 0) { @@ -375,12 +393,10 @@ export function normalizeStoredHostProfile(entry: unknown): HostProfile | null { } const now = new Date().toISOString(); - const label = normalizeHostLabel( - typeof record.label === "string" ? record.label : null, - serverId, - ); + const label = normalizeHostLabel(record.label, serverId); const preferredConnectionId = - typeof record.preferredConnectionId === "string" && + record.preferredConnectionId !== null && + record.preferredConnectionId !== undefined && connections.some((connection) => connection.id === record.preferredConnectionId) ? record.preferredConnectionId : (connections[0]?.id ?? null); @@ -388,12 +404,12 @@ export function normalizeStoredHostProfile(entry: unknown): HostProfile | null { return { serverId, label, - appearance: normalizeStoredHostAppearance(record.appearance), + appearance: record.appearance ?? defaultHostAppearance(), lifecycle: defaultLifecycle(), connections, preferredConnectionId, - createdAt: typeof record.createdAt === "string" ? record.createdAt : now, - updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : now, + createdAt: record.createdAt ?? now, + updatedAt: record.updatedAt ?? now, }; } diff --git a/packages/app/src/types/stream.test.ts b/packages/app/src/types/stream.test.ts index c12d452539..c3594b9d75 100644 --- a/packages/app/src/types/stream.test.ts +++ b/packages/app/src/types/stream.test.ts @@ -192,10 +192,19 @@ function canonicalToolTimeline(params: { }; } -function todoTimeline(items: { text: string; completed: boolean }[]): AgentStreamEventPayload { +function todoTimeline( + items: Array<{ + id?: string; + text: string; + completed: boolean; + status?: "pending" | "in_progress" | "completed"; + activeForm?: string; + }>, + provider: AgentProvider = "codex", +): AgentStreamEventPayload { return { type: "timeline", - provider: "codex", + provider, item: { type: "todo", items, @@ -970,9 +979,83 @@ describe("stream reducer canonical tool calls", () => { assert.ok(todos); assert.strictEqual(todos.items.length, 2); assert.strictEqual(todos.items[1]?.completed, true); + assert.deepStrictEqual(todos.activity, { type: "created", count: 2 }); }); - it("preserves compaction trigger when completed update replaces loading marker", () => { + it("turns task snapshots into semantic timeline activity", () => { + const state = hydrateStreamState([ + { + event: todoTimeline([ + { id: "a", text: "Inspect provider", completed: false, status: "pending" }, + { id: "b", text: "Ship fix", completed: false, status: "pending" }, + ]), + timestamp: new Date("2025-01-01T10:50:00Z"), + }, + { + event: todoTimeline([ + { id: "a", text: "Inspect provider", completed: false, status: "in_progress" }, + { id: "b", text: "Ship fix", completed: false, status: "pending" }, + ]), + timestamp: new Date("2025-01-01T10:50:01Z"), + }, + { + event: todoTimeline([ + { id: "a", text: "Inspect provider", completed: true, status: "completed" }, + { id: "b", text: "Ship fix", completed: false, status: "in_progress" }, + ]), + timestamp: new Date("2025-01-01T10:50:02Z"), + }, + { + event: todoTimeline([ + { id: "a", text: "Inspect provider", completed: true, status: "completed" }, + { id: "b", text: "Ship fix", completed: true, status: "completed" }, + ]), + timestamp: new Date("2025-01-01T10:50:03Z"), + }, + ]); + + expect(state.flatMap((item) => (item.kind === "todo_list" ? [item.activity] : []))).toEqual([ + { type: "created", count: 2 }, + { type: "started", task: "Inspect provider" }, + { type: "completed", task: "Inspect provider" }, + { type: "started", task: "Ship fix" }, + { type: "completed", task: "Ship fix" }, + ]); + }); + + it("groups consecutive initial Claude TaskCreate snapshots", () => { + const state = hydrateStreamState([ + { + event: todoTimeline( + [{ id: "a", text: "Inspect provider", completed: false, status: "pending" }], + "claude", + ), + timestamp: new Date("2025-01-01T10:50:00Z"), + }, + { + event: todoTimeline( + [ + { id: "a", text: "Inspect provider", completed: false, status: "pending" }, + { id: "b", text: "Ship fix", completed: false, status: "pending" }, + ], + "claude", + ), + timestamp: new Date("2025-01-01T10:50:01Z"), + }, + ]); + + expect(state.filter((item) => item.kind === "todo_list")).toEqual([ + expect.objectContaining({ + activity: { type: "created", count: 2 }, + items: expect.arrayContaining([ + expect.objectContaining({ text: "Inspect provider" }), + expect.objectContaining({ text: "Ship fix" }), + ]), + }), + ]); + }); + + it("terminalizes the loading compaction before a completed turn", () => { const state = hydrateStreamState([ { event: compactionTimeline("loading", "auto"), @@ -982,6 +1065,10 @@ describe("stream reducer canonical tool calls", () => { event: compactionTimeline("completed"), timestamp: new Date("2025-01-01T10:50:01Z"), }, + { + event: { type: "turn_completed", provider: "codex" }, + timestamp: new Date("2025-01-01T10:50:02Z"), + }, ]); const compactions = state.filter( @@ -991,6 +1078,10 @@ describe("stream reducer canonical tool calls", () => { assert.strictEqual(compactions.length, 1); assert.strictEqual(compactions[0].status, "completed"); assert.strictEqual(compactions[0].trigger, "auto"); + assert.strictEqual( + state.some((item) => item.kind === "compaction" && item.status === "loading"), + false, + ); }); it("renders Claude TodoWrite as todo_list and suppresses tool call badge", () => { @@ -1022,6 +1113,26 @@ describe("stream reducer canonical tool calls", () => { assert.strictEqual(todos.items[0]?.text, "Task 1"); }); + it.each(["TaskCreate", "TaskUpdate", "TaskList"])( + "suppresses Claude %s bookkeeping tool calls", + (name) => { + const state = hydrateStreamState([ + { + event: canonicalToolTimeline({ + provider: "claude", + callId: name, + name, + status: "completed", + input: { taskId: "1", status: "completed" }, + }), + timestamp: new Date("2025-01-01T11:00:00Z"), + }, + ]); + + expect(state.filter(isAgentToolCallItem)).toEqual([]); + }, + ); + it("preserves submitted user message images when authoritative user message arrives", () => { const messageId = "msg-user-images"; const submittedTimestamp = new Date("2025-01-01T11:10:00Z"); diff --git a/packages/app/src/types/stream.ts b/packages/app/src/types/stream.ts index 2a2b41f5a9..5572797677 100644 --- a/packages/app/src/types/stream.ts +++ b/packages/app/src/types/stream.ts @@ -766,8 +766,15 @@ export interface CompactionItem { export interface TodoEntry { text: string; completed: boolean; + id?: string; + status?: "pending" | "in_progress" | "completed"; + activeForm?: string; } +export type TaskActivity = + | { type: "created"; count: number } + | { type: "added" | "started" | "completed" | "reopened"; task: string }; + export interface TodoListItem { kind: "todo_list"; id: string; @@ -775,6 +782,7 @@ export interface TodoListItem { timestamp: Date; provider: AgentProvider; items: TodoEntry[]; + activity: TaskActivity; } export type StreamUpdateSource = "live" | "canonical"; @@ -1178,34 +1186,103 @@ function appendTodoList( const normalizedItems = items.map((item) => ({ text: item.text, completed: item.completed, + ...(item.id ? { id: item.id } : {}), + ...(item.status ? { status: item.status } : {}), + ...(item.activeForm ? { activeForm: item.activeForm } : {}), })); - const lastItem = state[state.length - 1]; - if (lastItem && lastItem.kind === "todo_list" && lastItem.provider === provider) { + const previousIndex = state.findLastIndex( + (item) => item.kind === "todo_list" && item.provider === provider, + ); + const previous = state[previousIndex]; + const previousItems = previous?.kind === "todo_list" ? previous.items : []; + const activities = deriveTaskActivities(previousItems, normalizedItems); + + if (activities.length === 0) { + if (!previous || previous.kind !== "todo_list") return state; const next = [...state]; - const updated: TodoListItem = { - ...lastItem, + next[previousIndex] = { + ...previous, ...(timelineCursor ? { timelineCursor } : {}), items: normalizedItems, timestamp, }; - next[next.length - 1] = updated; return next; } - const idSeed = `${provider}:${JSON.stringify(normalizedItems)}`; - const entryId = createUniqueTimelineId(state, "todo", idSeed, timestamp); + const lastItem = state[state.length - 1]; + if ( + activities.length === 1 && + activities[0]?.type === "added" && + lastItem?.kind === "todo_list" && + lastItem.provider === provider && + lastItem.activity.type === "created" && + normalizedItems.every((item) => taskStatus(item) === "pending") + ) { + const next = [...state]; + next[next.length - 1] = { + ...lastItem, + ...(timelineCursor ? { timelineCursor } : {}), + items: normalizedItems, + activity: { type: "created", count: normalizedItems.length }, + timestamp, + }; + return next; + } - const entry: TodoListItem = { - kind: "todo_list", - id: entryId, - ...(timelineCursor ? { timelineCursor } : {}), - timestamp, - provider, - items: normalizedItems, - }; + const next = [...state]; + for (const activity of activities) { + const idSeed = `${provider}:${JSON.stringify(activity)}:${JSON.stringify(normalizedItems)}`; + next.push({ + kind: "todo_list", + id: createUniqueTimelineId(next, "todo", idSeed, timestamp), + ...(timelineCursor ? { timelineCursor } : {}), + timestamp, + provider, + items: normalizedItems, + activity, + }); + } + return next; +} - return [...state, entry]; +function taskStatus(task: TodoEntry): NonNullable { + if (task.completed || task.status === "completed") return "completed"; + return task.status === "in_progress" ? "in_progress" : "pending"; +} + +function taskKey(task: TodoEntry, index: number): string { + return task.id ?? `${index}:${task.text}`; +} + +function deriveTaskActivities( + previous: readonly TodoEntry[], + current: readonly TodoEntry[], +): TaskActivity[] { + if (previous.length === 0) { + return current.length > 0 ? [{ type: "created", count: current.length }] : []; + } + + const previousByKey = new Map(previous.map((task, index) => [taskKey(task, index), task])); + const activities: TaskActivity[] = []; + for (const [index, task] of current.entries()) { + const prior = previousByKey.get(taskKey(task, index)); + if (!prior) { + activities.push({ type: "added", task: task.text }); + continue; + } + const before = taskStatus(prior); + const after = taskStatus(task); + if (before === after) continue; + if (after === "completed") { + activities.push({ type: "completed", task: task.text }); + } else if (before === "completed") { + activities.push({ type: "reopened", task: task.text }); + } else if (after === "in_progress") { + activities.push({ type: "started", task: task.text }); + } + } + return activities; } function reduceTimelineToolCall( @@ -1243,6 +1320,15 @@ function reduceTimelineToolCall( ); } + if ( + event.provider === "claude" && + (normalizedToolName === "taskcreate" || + normalizedToolName === "taskupdate" || + normalizedToolName === "tasklist") + ) { + return state; + } + const tasks = extractTaskEntriesFromToolCall(item.name, inputFromUnknownDetail(item.detail)); if (tasks) { return appendTodoList( @@ -1349,12 +1435,12 @@ function reduceTimelineEvent( reduceTimelineToolCall(state, event, item, timestamp, timelineCursor), ); case "todo": { - if (event.provider === "claude") { - return finalizeActiveThoughts(state); - } const items: TodoEntry[] = (item.items ?? []).map((todo) => ({ text: todo.text, completed: todo.completed, + id: todo.id, + status: todo.status, + activeForm: todo.activeForm, })); return finalizeActiveThoughts( appendTodoList(state, event.provider, items, timestamp, timelineCursor), diff --git a/packages/app/src/utils/agent-snapshots.test.ts b/packages/app/src/utils/agent-snapshots.test.ts index 966b17eb34..334e36d9f2 100644 --- a/packages/app/src/utils/agent-snapshots.test.ts +++ b/packages/app/src/utils/agent-snapshots.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages"; import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; -import { normalizeAgentSnapshot } from "./agent-snapshots"; +import { normalizeAgentSnapshot, projectAgentSnapshot } from "./agent-snapshots"; function createSnapshot( input: Partial> & { @@ -36,6 +36,18 @@ function createSnapshot( } describe("normalizeAgentSnapshot", () => { + it("round-trips identified active turns through the canonical snapshot boundary", () => { + const snapshot = createSnapshot({ + status: "running", + activeTurn: { turnId: "turn-1", startedAt: "2026-07-31T12:00:00.000Z" }, + }); + + expect(projectAgentSnapshot(normalizeAgentSnapshot(snapshot, "server-1"))).toMatchObject({ + status: "running", + activeTurn: snapshot.activeTurn, + }); + }); + it("normalizes identified and legacy active turns at the snapshot boundary", () => { const startedAt = "2026-07-31T12:00:00.000Z"; expect( diff --git a/packages/app/src/utils/agent-snapshots.ts b/packages/app/src/utils/agent-snapshots.ts index aeaef3ca44..a035a054a2 100644 --- a/packages/app/src/utils/agent-snapshots.ts +++ b/packages/app/src/utils/agent-snapshots.ts @@ -2,6 +2,7 @@ import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages"; import type { AgentPermissionRequest } from "@getpaseo/protocol/agent-types"; import { getParentAgentIdFromLabels } from "@getpaseo/protocol/agent-labels"; import type { ActiveTurnIdentity } from "@/timeline/turn-liveness"; +import type { Agent } from "@/stores/session-store"; function normalizeActiveTurn( snapshot: AgentSnapshotPayload, @@ -17,6 +18,17 @@ function normalizeActiveTurn( return snapshot.status === "running" ? { turnId: null, startedAt: lastUserMessageAt } : null; } +function projectActiveTurn(agent: Agent): Pick { + if (agent.activeTurn === null) return { activeTurn: null }; + if (agent.activeTurn.turnId === null) return {}; + return { + activeTurn: { + turnId: agent.activeTurn.turnId, + startedAt: agent.activeTurn.startedAt?.toISOString() ?? null, + }, + }; +} + export function derivePendingPermissionKey( agentId: string, request: AgentPermissionRequest, @@ -31,6 +43,37 @@ export function derivePendingPermissionKey( return `${agentId}:${fallbackId}`; } +export function projectAgentSnapshot(agent: Agent): AgentSnapshotPayload { + return { + id: agent.id, + provider: agent.provider, + cwd: agent.cwd, + ...(agent.workspaceId ? { workspaceId: agent.workspaceId } : {}), + model: agent.model, + ...(agent.features ? { features: agent.features } : {}), + thinkingOptionId: agent.thinkingOptionId ?? null, + createdAt: agent.createdAt.toISOString(), + updatedAt: agent.updatedAt.toISOString(), + lastUserMessageAt: agent.lastUserMessageAt?.toISOString() ?? null, + status: agent.status, + ...projectActiveTurn(agent), + capabilities: agent.capabilities, + currentModeId: agent.currentModeId, + availableModes: agent.availableModes, + pendingPermissions: agent.pendingPermissions, + persistence: agent.persistence, + ...(agent.runtimeInfo ? { runtimeInfo: agent.runtimeInfo } : {}), + ...(agent.lastUsage ? { lastUsage: agent.lastUsage } : {}), + ...(agent.lastError ? { lastError: agent.lastError } : {}), + title: agent.title, + labels: agent.labels, + requiresAttention: agent.requiresAttention ?? false, + attentionReason: agent.attentionReason ?? null, + attentionTimestamp: agent.attentionTimestamp?.toISOString() ?? null, + archivedAt: agent.archivedAt?.toISOString() ?? null, + }; +} + export function normalizeAgentSnapshot(snapshot: AgentSnapshotPayload, serverId: string) { const createdAt = new Date(snapshot.createdAt); const updatedAt = new Date(snapshot.updatedAt); diff --git a/packages/app/src/utils/client-id.test.ts b/packages/app/src/utils/client-id.test.ts index 647ea48b0e..7184ab5547 100644 --- a/packages/app/src/utils/client-id.test.ts +++ b/packages/app/src/utils/client-id.test.ts @@ -18,6 +18,9 @@ function inMemoryStorage(initial: Record = {}): InMemoryStorage storage.setCallCount += 1; items.set(key, value); }, + async removeItem(key) { + items.delete(key); + }, }; return storage; } diff --git a/packages/app/src/utils/client-id.ts b/packages/app/src/utils/client-id.ts index 52397e91bb..d721110980 100644 --- a/packages/app/src/utils/client-id.ts +++ b/packages/app/src/utils/client-id.ts @@ -1,23 +1,20 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; +import { z } from "zod"; +import { readValidatedString } from "@/storage/validated-storage"; const CLIENT_ID_STORAGE_KEY = "@paseo:client-id-v1"; export interface ClientIdStorage { getItem(key: string): Promise; setItem(key: string, value: string): Promise; + removeItem(key: string): Promise; } export interface ClientIdResolver { getOrCreate(): Promise; } -function normalizeStoredClientId(value: unknown): string | null { - if (typeof value !== "string") { - return null; - } - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} +const ClientIdSchema = z.string().trim().min(1); export function createClientIdResolver(deps: { storage: ClientIdStorage; @@ -38,8 +35,7 @@ export function createClientIdResolver(deps: { } inFlight = (async () => { - const stored = await deps.storage.getItem(storageKey); - const existing = normalizeStoredClientId(stored); + const existing = await readValidatedString(deps.storage, storageKey, ClientIdSchema); if (existing) { cached = existing; return existing; @@ -61,7 +57,7 @@ export function createClientIdResolver(deps: { } function generateUuidFromGlobalCrypto(): string { - const cryptoObj = globalThis.crypto as { randomUUID?: () => string } | undefined; + const cryptoObj = globalThis.crypto; if (cryptoObj && typeof cryptoObj.randomUUID === "function") { return cryptoObj.randomUUID().replace(/-/g, ""); } diff --git a/packages/app/src/utils/explorer-paths.ts b/packages/app/src/utils/explorer-paths.ts index 30f9a8f52c..5f09541a57 100644 --- a/packages/app/src/utils/explorer-paths.ts +++ b/packages/app/src/utils/explorer-paths.ts @@ -32,3 +32,8 @@ export function buildAbsoluteExplorerPath({ return `${normalizedWorkspaceRoot}${separator}${segments.join(separator)}`; } + +export function parentExplorerPath(entryPath: string): string { + const separatorIndex = entryPath.lastIndexOf("/"); + return separatorIndex > 0 ? entryPath.slice(0, separatorIndex) : "."; +} diff --git a/packages/app/src/utils/host-routes.test.ts b/packages/app/src/utils/host-routes.test.ts index 29ac067c90..644139e92d 100644 --- a/packages/app/src/utils/host-routes.test.ts +++ b/packages/app/src/utils/host-routes.test.ts @@ -248,6 +248,7 @@ describe("host settings section slugs", () => { expect(normalizeHostSectionSlug("connections")).toBe("connections"); expect(normalizeHostSectionSlug("pair-device")).toBe("pair-device"); expect(normalizeHostSectionSlug("agents")).toBe("agents"); + expect(normalizeHostSectionSlug("metadata")).toBe("metadata"); expect(normalizeHostSectionSlug("workspaces")).toBe("workspaces"); expect(normalizeHostSectionSlug("projects")).toBe("projects"); expect(normalizeHostSectionSlug("providers")).toBe("providers"); diff --git a/packages/app/src/utils/host-routes.ts b/packages/app/src/utils/host-routes.ts index 8c53ef0b45..87e1517884 100644 --- a/packages/app/src/utils/host-routes.ts +++ b/packages/app/src/utils/host-routes.ts @@ -512,6 +512,7 @@ export const HOST_SECTION_SLUGS = [ "connections", "pair-device", "agents", + "metadata", "workspaces", "providers", "usage", diff --git a/packages/app/src/utils/provider-command-templates.test.ts b/packages/app/src/utils/provider-command-templates.test.ts index c995368a90..8ed8e18567 100644 --- a/packages/app/src/utils/provider-command-templates.test.ts +++ b/packages/app/src/utils/provider-command-templates.test.ts @@ -3,6 +3,16 @@ import { describe, expect, test } from "vitest"; import { buildProviderCommand } from "@/utils/provider-command-templates"; describe("buildProviderCommand", () => { + test("builds Hermes resume commands from native session ids", () => { + expect( + buildProviderCommand({ + provider: "hermes", + id: "resume", + sessionId: "20260813_111500_abc123", + }), + ).toBe("hermes --resume 20260813_111500_abc123"); + }); + test("builds OpenCode resume commands from native session ids", () => { expect( buildProviderCommand({ diff --git a/packages/app/src/utils/provider-command-templates.ts b/packages/app/src/utils/provider-command-templates.ts index 0d55a731ee..d8e65f5788 100644 --- a/packages/app/src/utils/provider-command-templates.ts +++ b/packages/app/src/utils/provider-command-templates.ts @@ -17,6 +17,9 @@ export const PROVIDER_COMMAND_TEMPLATES: Record< claude: { resume: "claude --resume {sessionId}", }, + hermes: { + resume: "hermes --resume {sessionId}", + }, pi: { resume: "pi --session {sessionId}", }, diff --git a/packages/app/src/utils/sidebar-project-row-model.ts b/packages/app/src/utils/sidebar-project-row-model.ts index 76e42097ae..4a9bf0a8b6 100644 --- a/packages/app/src/utils/sidebar-project-row-model.ts +++ b/packages/app/src/utils/sidebar-project-row-model.ts @@ -5,6 +5,7 @@ export interface SidebarProjectHostTarget { projectId: string; iconWorkingDir: string; customIconRevision?: string | null; + iconRevision?: string; } export type SidebarProjectTrailingAction = @@ -25,6 +26,7 @@ function hostTarget(input: { projectId: string; iconWorkingDir: string; customIconRevision?: string | null; + iconRevision?: string; }): SidebarProjectHostTarget | null { const iconWorkingDir = input.iconWorkingDir.trim(); if (!input.serverId || !iconWorkingDir) { @@ -35,6 +37,7 @@ function hostTarget(input: { projectId: input.projectId, iconWorkingDir, customIconRevision: input.customIconRevision, + iconRevision: input.iconRevision, }; } diff --git a/packages/app/src/workspace-pins/store.ts b/packages/app/src/workspace-pins/store.ts index 8fd3178a91..45a49c4712 100644 --- a/packages/app/src/workspace-pins/store.ts +++ b/packages/app/src/workspace-pins/store.ts @@ -1,7 +1,9 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { create } from "zustand"; -import { createJSONStorage, persist } from "zustand/middleware"; +import { persist } from "zustand/middleware"; +import { z } from "zod"; import { isTargetPinned, togglePinnedTarget, type PinnedTabTarget } from "@/workspace-pins/target"; +import { createValidatedPersistStorage } from "@/storage/validated-persist-storage"; interface PinnedTargetsState { pinned: PinnedTabTarget[]; @@ -10,6 +12,15 @@ interface PinnedTargetsState { } const DEFAULT_PINNED_TARGETS: PinnedTabTarget[] = [{ kind: "terminal" }, { kind: "browser" }]; +const PinnedTabTargetSchema = z.discriminatedUnion("kind", [ + z.strictObject({ kind: z.literal("draft") }), + z.strictObject({ kind: z.literal("terminal") }), + z.strictObject({ kind: z.literal("browser") }), + z.strictObject({ kind: z.literal("profile"), profileId: z.string() }), +]); +const PinnedTargetsPersistedStateSchema = z.strictObject({ + pinned: z.array(PinnedTabTargetSchema), +}); function applyDefaultPinnedTargets(pinned: PinnedTabTarget[]): PinnedTabTarget[] { const next = [...DEFAULT_PINNED_TARGETS]; @@ -22,7 +33,7 @@ function applyDefaultPinnedTargets(pinned: PinnedTabTarget[]): PinnedTabTarget[] } export const usePinnedTargetsStore = create()( - persist( + persist>( (set, get) => ({ pinned: [], toggle: (target) => set((state) => ({ pinned: togglePinnedTarget(state.pinned, target) })), @@ -32,21 +43,21 @@ export const usePinnedTargetsStore = create()( name: "pinned-tab-targets", version: 1, merge: (persistedState, currentState) => { - const persisted = persistedState as Partial | null; + const result = PinnedTargetsPersistedStateSchema.safeParse(persistedState); return { ...currentState, - ...persisted, - pinned: persisted?.pinned ?? applyDefaultPinnedTargets([]), + pinned: result.success ? result.data.pinned : applyDefaultPinnedTargets([]), }; }, - storage: createJSONStorage(() => AsyncStorage), + storage: createValidatedPersistStorage(AsyncStorage, PinnedTargetsPersistedStateSchema), partialize: (state) => ({ pinned: state.pinned }), migrate: (persistedState, version) => { + const result = PinnedTargetsPersistedStateSchema.safeParse(persistedState); + const pinned = result.success ? result.data.pinned : []; if (version === 0) { - const pinned = (persistedState as { pinned?: PinnedTabTarget[] } | null)?.pinned ?? []; return { pinned: applyDefaultPinnedTargets(pinned) }; } - return persistedState as PinnedTargetsState; + return { pinned }; }, }, ), diff --git a/packages/app/src/workspace-service-routes/store.test.ts b/packages/app/src/workspace-service-routes/store.test.ts index ba2bab1f2c..719c4f9acc 100644 --- a/packages/app/src/workspace-service-routes/store.test.ts +++ b/packages/app/src/workspace-service-routes/store.test.ts @@ -32,7 +32,7 @@ describe("workspace service route preferences", () => { expect(restored.getState().byServerId).toEqual({ desktop: "direct", devbox: "public" }); }); - it("drops invalid route kinds from persisted storage", async () => { + it("clears the complete persisted value when one route kind is invalid", async () => { const storage = createMemoryStorage({ "workspace-service-route-preferences": JSON.stringify({ state: { byServerId: { desktop: "direct", broken: "unknown" } }, @@ -42,6 +42,7 @@ describe("workspace service route preferences", () => { const store = createWorkspaceServiceRoutePreferencesStore(storage); await store.persist.rehydrate(); - expect(store.getState().byServerId).toEqual({ desktop: "direct" }); + expect(store.getState().byServerId).toEqual({}); + expect(storage.values.has("workspace-service-route-preferences")).toBe(false); }); }); diff --git a/packages/app/src/workspace-service-routes/store.ts b/packages/app/src/workspace-service-routes/store.ts index 15479f08e9..bf061effc4 100644 --- a/packages/app/src/workspace-service-routes/store.ts +++ b/packages/app/src/workspace-service-routes/store.ts @@ -1,6 +1,8 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { create } from "zustand"; -import { createJSONStorage, persist, type StateStorage } from "zustand/middleware"; +import { persist, type StateStorage } from "zustand/middleware"; +import { z } from "zod"; +import { createValidatedPersistStorage } from "@/storage/validated-persist-storage"; import type { WorkspaceScriptLinkKind } from "@/utils/workspace-script-links"; interface WorkspaceServiceRoutePreferencesState { @@ -8,25 +10,19 @@ interface WorkspaceServiceRoutePreferencesState { setPreferredRoute: (serverId: string, kind: WorkspaceScriptLinkKind) => void; } -function isWorkspaceScriptLinkKind(value: unknown): value is WorkspaceScriptLinkKind { - return value === "public" || value === "paseo" || value === "direct"; -} - -function sanitizePreferences(value: unknown): Record { - if (!value || typeof value !== "object") return {}; - const byServerId = (value as { byServerId?: unknown }).byServerId; - if (!byServerId || typeof byServerId !== "object") return {}; - - const result: Record = {}; - for (const [serverId, kind] of Object.entries(byServerId)) { - if (isWorkspaceScriptLinkKind(kind)) result[serverId] = kind; - } - return result; -} +const WorkspaceScriptLinkKindSchema = z.enum(["public", "paseo", "direct"]); +const WorkspaceServiceRoutePreferencesSchema = z.strictObject({ + byServerId: z.record(z.string(), WorkspaceScriptLinkKindSchema), +}); export function createWorkspaceServiceRoutePreferencesStore(storage: StateStorage) { return create()( - persist( + persist< + WorkspaceServiceRoutePreferencesState, + [], + [], + z.infer + >( (set) => ({ byServerId: {}, setPreferredRoute: (serverId, kind) => @@ -35,12 +31,15 @@ export function createWorkspaceServiceRoutePreferencesStore(storage: StateStorag { name: "workspace-service-route-preferences", version: 1, - storage: createJSONStorage(() => storage), + storage: createValidatedPersistStorage(storage, WorkspaceServiceRoutePreferencesSchema), partialize: (state) => ({ byServerId: state.byServerId }), - merge: (persistedState, currentState) => ({ - ...currentState, - byServerId: sanitizePreferences(persistedState), - }), + merge: (persistedState, currentState) => { + const result = WorkspaceServiceRoutePreferencesSchema.safeParse(persistedState); + return { + ...currentState, + byServerId: result.success ? result.data.byServerId : {}, + }; + }, }, ), ); diff --git a/packages/cli/package.json b/packages/cli/package.json index 04be4e1b6c..3e4bea361a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/cli", - "version": "0.3.1", + "version": "0.4.0", "description": "Paseo CLI - control your AI coding agents from the command line", "bin": { "paseo": "bin/paseo" @@ -28,9 +28,9 @@ }, "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/client": "0.3.1", - "@getpaseo/protocol": "0.3.1", - "@getpaseo/server": "0.3.1", + "@getpaseo/client": "0.4.0", + "@getpaseo/protocol": "0.4.0", + "@getpaseo/server": "0.4.0", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 0455b81b2b..cb93b810a4 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,8 +1,6 @@ import { Command, Option } from "commander"; import { createAgentCommand } from "./commands/agent/index.js"; import { createDaemonCommand } from "./commands/daemon/index.js"; -import { createChatCommand } from "./commands/chat/index.js"; -import { createLoopCommand } from "./commands/loop/index.js"; import { createPermitCommand } from "./commands/permit/index.js"; import { createProviderCommand } from "./commands/provider/index.js"; import { createScheduleCommand } from "./commands/schedule/index.js"; @@ -168,7 +166,6 @@ export function createCli(): Command { program.addCommand(createHubCommand()); // Chat commands - program.addCommand(createChatCommand()); // Terminal commands program.addCommand(createTerminalCommand()); @@ -176,9 +173,6 @@ export function createCli(): Command { // Workspace script commands program.addCommand(createScriptCommand()); - // Loop commands - program.addCommand(createLoopCommand()); - // Schedule commands program.addCommand(createScheduleCommand()); program.addCommand(createHeartbeatCommand()); diff --git a/packages/cli/src/commands/chat/create.ts b/packages/cli/src/commands/chat/create.ts deleted file mode 100644 index 2747a6cf65..0000000000 --- a/packages/cli/src/commands/chat/create.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Command } from "commander"; -import type { SingleResult } from "../../output/index.js"; -import { connectChatClient, toChatCommandError, type ChatCommandOptions } from "./shared.js"; -import { chatRoomSchema, type ChatRoomRow, toChatRoomRow } from "./schema.js"; - -export interface ChatCreateOptions extends ChatCommandOptions { - purpose?: string; -} - -export async function runCreateCommand( - name: string, - options: ChatCreateOptions, - _command: Command, -): Promise> { - const { client } = await connectChatClient(options.host); - try { - const payload = await client.createChatRoom({ - name, - purpose: options.purpose, - }); - return { - type: "single", - data: toChatRoomRow(payload.room!), - schema: chatRoomSchema, - }; - } catch (err) { - throw toChatCommandError("CHAT_CREATE_FAILED", "create chat room", err); - } finally { - await client.close().catch(() => {}); - } -} diff --git a/packages/cli/src/commands/chat/delete.ts b/packages/cli/src/commands/chat/delete.ts deleted file mode 100644 index f803caf4e3..0000000000 --- a/packages/cli/src/commands/chat/delete.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Command } from "commander"; -import type { SingleResult } from "../../output/index.js"; -import { connectChatClient, toChatCommandError, type ChatCommandOptions } from "./shared.js"; -import { chatRoomSchema, type ChatRoomRow, toChatRoomRow } from "./schema.js"; - -export async function runDeleteCommand( - room: string, - options: ChatCommandOptions, - _command: Command, -): Promise> { - const { client } = await connectChatClient(options.host); - try { - const payload = await client.deleteChatRoom({ room }); - return { - type: "single", - data: toChatRoomRow(payload.room!), - schema: chatRoomSchema, - }; - } catch (err) { - throw toChatCommandError("CHAT_DELETE_FAILED", "delete chat room", err); - } finally { - await client.close().catch(() => {}); - } -} diff --git a/packages/cli/src/commands/chat/index.ts b/packages/cli/src/commands/chat/index.ts deleted file mode 100644 index 197b5844ef..0000000000 --- a/packages/cli/src/commands/chat/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Command } from "commander"; -import { withOutput } from "../../output/index.js"; -import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js"; -import { runCreateCommand } from "./create.js"; -import { runLsCommand } from "./ls.js"; -import { runInspectCommand } from "./inspect.js"; -import { runDeleteCommand } from "./delete.js"; -import { runPostCommand } from "./post.js"; -import { runReadCommand } from "./read.js"; -import { runWaitCommand } from "./wait.js"; - -export function createChatCommand(): Command { - const chat = new Command("chat").description("Manage chat rooms for agent coordination"); - - addJsonAndDaemonHostOptions( - chat - .command("create") - .description("Create a chat room") - .argument("", "Room name (must be unique)") - .option("--purpose ", "Room purpose/description"), - ).action(withOutput(runCreateCommand)); - - addJsonAndDaemonHostOptions(chat.command("ls").description("List chat rooms")).action( - withOutput(runLsCommand), - ); - - addJsonAndDaemonHostOptions( - chat - .command("inspect") - .description("Inspect a chat room") - .argument("", "Room name or ID"), - ).action(withOutput(runInspectCommand)); - - addJsonAndDaemonHostOptions( - chat - .command("delete") - .description("Delete a chat room") - .argument("", "Room name or ID"), - ).action(withOutput(runDeleteCommand)); - - addJsonAndDaemonHostOptions( - chat - .command("post") - .description("Post a chat message") - .argument("", "Room name or ID") - .argument("", "Message body") - .option("--reply-to ", "Reply to a specific message ID"), - ).action(withOutput(runPostCommand)); - - addJsonAndDaemonHostOptions( - chat - .command("read") - .description("Read chat messages") - .argument("", "Room name or ID") - .option("--limit ", "Maximum number of messages to return") - .option("--since ", "Filter by relative duration or ISO timestamp") - .option("--agent ", "Filter by author agent ID"), - ).action(withOutput(runReadCommand)); - - addJsonAndDaemonHostOptions( - chat - .command("wait") - .description("Wait for new chat messages") - .argument("", "Room name or ID") - .option("--timeout ", "Maximum wait time"), - ).action(withOutput(runWaitCommand)); - - return chat; -} diff --git a/packages/cli/src/commands/chat/inspect.ts b/packages/cli/src/commands/chat/inspect.ts deleted file mode 100644 index a07039331d..0000000000 --- a/packages/cli/src/commands/chat/inspect.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Command } from "commander"; -import type { SingleResult } from "../../output/index.js"; -import { connectChatClient, toChatCommandError, type ChatCommandOptions } from "./shared.js"; -import { chatRoomSchema, type ChatRoomRow, toChatRoomRow } from "./schema.js"; - -export async function runInspectCommand( - room: string, - options: ChatCommandOptions, - _command: Command, -): Promise> { - const { client } = await connectChatClient(options.host); - try { - const payload = await client.inspectChatRoom({ room }); - return { - type: "single", - data: toChatRoomRow(payload.room!), - schema: chatRoomSchema, - }; - } catch (err) { - throw toChatCommandError("CHAT_INSPECT_FAILED", "inspect chat room", err); - } finally { - await client.close().catch(() => {}); - } -} diff --git a/packages/cli/src/commands/chat/ls.ts b/packages/cli/src/commands/chat/ls.ts deleted file mode 100644 index 5e323cd7f4..0000000000 --- a/packages/cli/src/commands/chat/ls.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Command } from "commander"; -import type { ListResult } from "../../output/index.js"; -import { connectChatClient, toChatCommandError, type ChatCommandOptions } from "./shared.js"; -import { chatRoomSchema, type ChatRoomRow, toChatRoomRow } from "./schema.js"; - -export async function runLsCommand( - options: ChatCommandOptions, - _command: Command, -): Promise> { - const { client } = await connectChatClient(options.host); - try { - const payload = await client.listChatRooms(); - return { - type: "list", - data: payload.rooms.map(toChatRoomRow), - schema: chatRoomSchema, - }; - } catch (err) { - throw toChatCommandError("CHAT_LIST_FAILED", "list chat rooms", err); - } finally { - await client.close().catch(() => {}); - } -} diff --git a/packages/cli/src/commands/chat/post.ts b/packages/cli/src/commands/chat/post.ts deleted file mode 100644 index abf8bdbbbb..0000000000 --- a/packages/cli/src/commands/chat/post.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { Command } from "commander"; -import type { SingleResult } from "../../output/index.js"; -import { - attachAgentNamesToMessages, - connectChatClient, - resolveChatAuthorAgentId, - toChatCommandError, - type ChatCommandOptions, -} from "./shared.js"; -import { chatMessageSchema, type ChatMessageRow, toChatMessageRow } from "./schema.js"; - -export interface ChatPostOptions extends ChatCommandOptions { - replyTo?: string; -} - -export async function runPostCommand( - room: string, - body: string, - options: ChatPostOptions, - _command: Command, -): Promise> { - const { client } = await connectChatClient(options.host); - try { - const payload = await client.postChatMessage({ - room, - body, - authorAgentId: resolveChatAuthorAgentId(), - replyToMessageId: options.replyTo, - }); - const [message] = await attachAgentNamesToMessages(client, [ - toChatMessageRow(payload.message!), - ]); - return { - type: "single", - data: message, - schema: chatMessageSchema, - }; - } catch (err) { - throw toChatCommandError("CHAT_POST_FAILED", "post chat message", err); - } finally { - await client.close().catch(() => {}); - } -} diff --git a/packages/cli/src/commands/chat/read.ts b/packages/cli/src/commands/chat/read.ts deleted file mode 100644 index 7b224ff2b6..0000000000 --- a/packages/cli/src/commands/chat/read.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { Command } from "commander"; -import type { ListResult } from "../../output/index.js"; -import { - attachAgentNamesToMessages, - connectChatClient, - parseSinceValue, - toChatCommandError, - type ChatCommandOptions, -} from "./shared.js"; -import { chatMessageSchema, type ChatMessageRow, toChatMessageRow } from "./schema.js"; - -export interface ChatReadOptions extends ChatCommandOptions { - limit?: string; - since?: string; - agent?: string; -} - -function parseLimit(value?: string): number | undefined { - if (!value) { - return undefined; - } - const parsed = Number.parseInt(value, 10); - if (!Number.isInteger(parsed) || parsed < 0) { - throw { - code: "INVALID_LIMIT", - message: "Invalid --limit value", - details: "Use a non-negative integer.", - }; - } - return parsed; -} - -export async function runReadCommand( - room: string, - options: ChatReadOptions, - _command: Command, -): Promise> { - const { client } = await connectChatClient(options.host); - try { - const payload = await client.readChatMessages({ - room, - limit: parseLimit(options.limit), - since: parseSinceValue(options.since), - authorAgentId: options.agent, - }); - const messages = await attachAgentNamesToMessages( - client, - payload.messages.map(toChatMessageRow), - ); - return { - type: "list", - data: messages, - schema: chatMessageSchema, - }; - } catch (err) { - throw toChatCommandError("CHAT_READ_FAILED", "read chat messages", err); - } finally { - await client.close().catch(() => {}); - } -} diff --git a/packages/cli/src/commands/chat/schema.ts b/packages/cli/src/commands/chat/schema.ts deleted file mode 100644 index 269c8d9982..0000000000 --- a/packages/cli/src/commands/chat/schema.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { OutputSchema } from "../../output/index.js"; -import type { AnyCommandResult, OutputOptions } from "../../output/index.js"; - -export interface ChatRoomRow { - name: string; - id: string; - purpose: string; - messages: number; - lastMessageAt: string; -} - -export interface ChatMessageRow { - id: string; - author: string; - authorName: string | null; - createdAt: string; - replyTo: string; - mentionAgentIds: string[]; - mentionLabels: string[]; - body: string; -} - -export const chatRoomSchema: OutputSchema = { - idField: "id", - columns: [ - { header: "NAME", field: "name", width: 22 }, - { header: "ID", field: "id", width: 36 }, - { header: "PURPOSE", field: "purpose", width: 30 }, - { header: "MESSAGES", field: "messages", width: 10, align: "right" }, - { header: "LAST MESSAGE", field: "lastMessageAt", width: 24 }, - ], -}; - -export const chatMessageSchema: OutputSchema = { - idField: "id", - columns: [ - { header: "ID", field: "id", width: 36 }, - { header: "AUTHOR", field: "author", width: 16 }, - { header: "AUTHOR NAME", field: (row) => row.authorName ?? "-", width: 20 }, - { header: "CREATED", field: "createdAt", width: 24 }, - { header: "REPLY TO", field: "replyTo", width: 36 }, - { header: "MENTIONS", field: (row) => row.mentionLabels.join(", ") || "-", width: 24 }, - { header: "MESSAGE", field: "body", width: 60 }, - ], - renderHuman: renderChatTranscript, -}; - -function renderChatTranscript( - result: AnyCommandResult, - _options: OutputOptions, -): string { - const data = result.type === "list" ? result.data : [result.data]; - if (data.length === 0) { - return ""; - } - return data.map(renderChatMessageBlock).join("\n\n"); -} - -function renderChatMessageBlock(message: ChatMessageRow): string { - const authorLabel = message.authorName - ? `${message.authorName} (${message.author})` - : message.author; - const lines = [ - `┌─ ${authorLabel} ── ${formatTimestamp(message.createdAt)} ── [msg ${message.id}]`, - ]; - - if (message.replyTo !== "-") { - lines.push(`│ reply-to: msg ${message.replyTo}`); - } - if (message.mentionLabels.length > 0) { - lines.push(`│ mentions: ${message.mentionLabels.join(", ")}`); - } - - lines.push("│"); - - const bodyLines = message.body.split(/\r?\n/); - for (const line of bodyLines) { - lines.push(`│ ${line}`); - } - - lines.push("│"); - lines.push("└─"); - return lines.join("\n"); -} - -function formatTimestamp(input: string): string { - const date = new Date(input); - if (Number.isNaN(date.getTime())) { - return input; - } - return date.toISOString().replace("T", " ").replace(".000Z", "Z"); -} - -export function toChatRoomRow(room: { - id: string; - name: string; - purpose: string | null; - messageCount: number; - lastMessageAt: string | null; -}): ChatRoomRow { - return { - id: room.id, - name: room.name, - purpose: room.purpose ?? "-", - messages: room.messageCount, - lastMessageAt: room.lastMessageAt ?? "-", - }; -} - -export function toChatMessageRow(message: { - id: string; - authorAgentId: string; - createdAt: string; - replyToMessageId: string | null; - mentionAgentIds: string[]; - body: string; -}): ChatMessageRow { - return { - id: message.id, - author: message.authorAgentId, - authorName: null, - createdAt: message.createdAt, - replyTo: message.replyToMessageId ?? "-", - mentionAgentIds: message.mentionAgentIds, - mentionLabels: message.mentionAgentIds, - body: message.body, - }; -} diff --git a/packages/cli/src/commands/chat/shared.ts b/packages/cli/src/commands/chat/shared.ts deleted file mode 100644 index 52ea27096e..0000000000 --- a/packages/cli/src/commands/chat/shared.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { connectToDaemon, getDaemonHost } from "../../utils/client.js"; -import { parseDuration } from "../../utils/duration.js"; -import type { CommandError, CommandOptions } from "../../output/index.js"; -import type { ChatMessageRow } from "./schema.js"; - -export interface ChatCommandOptions extends CommandOptions { - host?: string; -} - -export async function connectChatClient(host?: string) { - const daemonHost = getDaemonHost({ host }); - try { - const client = await connectToDaemon({ host }); - return { client, daemonHost }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - const error: CommandError = { - code: "DAEMON_NOT_RUNNING", - message: `Cannot connect to daemon at ${daemonHost}: ${message}`, - details: "Start the daemon with: paseo daemon start", - }; - throw error; - } -} - -export async function attachAgentNamesToMessages( - client: Awaited>, - messages: ChatMessageRow[], - options: { timeout?: number; bestEffort?: boolean } = {}, -): Promise { - const agentIds = new Set(); - for (const message of messages) { - agentIds.add(message.author); - for (const mentionId of message.mentionAgentIds) { - agentIds.add(mentionId); - } - } - - if (agentIds.size === 0) { - return messages; - } - - let payload: Awaited>; - try { - payload = await client.fetchAgents({ - filter: { includeArchived: true }, - ...(typeof options.timeout === "number" ? { timeout: options.timeout } : {}), - }); - } catch (error) { - if (options.bestEffort) { - return messages; - } - throw error; - } - const agentNames = new Map(); - for (const entry of payload.entries) { - const title = entry.agent.title?.trim(); - if (title) { - agentNames.set(entry.agent.id, title); - } - } - - return messages.map((message) => ({ - ...message, - authorName: agentNames.get(message.author) ?? null, - mentionLabels: message.mentionAgentIds.map((agentId) => { - const name = agentNames.get(agentId); - return name ? `${name} (${agentId})` : agentId; - }), - })); -} - -export function toChatCommandError(code: string, action: string, err: unknown): CommandError { - if (err && typeof err === "object" && "code" in err && "message" in err) { - return err as CommandError; - } - - const message = err instanceof Error ? err.message : String(err); - const rpcCode = - typeof err === "object" && err !== null && "code" in err && typeof err.code === "string" - ? err.code - : undefined; - - return { - code: rpcCode ?? code, - message: `Failed to ${action}: ${message}`, - }; -} - -export function parseSinceValue(input?: string): string | undefined { - if (!input) { - return undefined; - } - - try { - const durationMs = parseDuration(input); - return new Date(Date.now() - durationMs).toISOString(); - } catch { - const timestamp = new Date(input); - if (Number.isNaN(timestamp.getTime())) { - const error: CommandError = { - code: "INVALID_SINCE", - message: "Invalid --since value", - details: "Use a duration like 10m or an ISO timestamp.", - }; - throw error; - } - return timestamp.toISOString(); - } -} - -export function parseTimeoutMs(input?: string): number | undefined { - if (!input) { - return undefined; - } - - try { - const timeoutMs = parseDuration(input); - if (timeoutMs <= 0) { - throw new Error("Timeout must be positive"); - } - return timeoutMs; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - const error: CommandError = { - code: "INVALID_TIMEOUT", - message: "Invalid timeout value", - details: message, - }; - throw error; - } -} - -export function resolveChatAuthorAgentId(): string { - const agentId = process.env.PASEO_AGENT_ID?.trim(); - return agentId && agentId.length > 0 ? agentId : "manual"; -} diff --git a/packages/cli/src/commands/chat/wait.ts b/packages/cli/src/commands/chat/wait.ts deleted file mode 100644 index 3db53c8970..0000000000 --- a/packages/cli/src/commands/chat/wait.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { Command } from "commander"; -import type { ListResult } from "../../output/index.js"; -import { - attachAgentNamesToMessages, - connectChatClient, - parseTimeoutMs, - toChatCommandError, - type ChatCommandOptions, -} from "./shared.js"; -import { chatMessageSchema, type ChatMessageRow, toChatMessageRow } from "./schema.js"; - -export interface ChatWaitOptions extends ChatCommandOptions { - timeout?: string; -} - -const CHAT_WAIT_PREFLIGHT_TIMEOUT_MS = 2000; - -export async function runWaitCommand( - room: string, - options: ChatWaitOptions, - _command: Command, -): Promise> { - const timeoutMs = parseTimeoutMs(options.timeout); - const { client } = await connectChatClient(options.host); - const deadline = typeof timeoutMs === "number" ? Date.now() + timeoutMs : null; - const hasExplicitTimeout = deadline !== null; - const remainingTimeoutMs = () => - deadline === null ? undefined : Math.max(1, deadline - Date.now()); - try { - const latest = await client.readChatMessages({ - room, - limit: 1, - ...(hasExplicitTimeout - ? { - timeout: Math.min(remainingTimeoutMs() ?? 1, CHAT_WAIT_PREFLIGHT_TIMEOUT_MS), - } - : {}), - }); - const afterMessageId = latest.messages[0]?.id; - const payload = await client.waitForChatMessages({ - room, - afterMessageId, - timeoutMs: remainingTimeoutMs() ?? timeoutMs, - }); - const messages = await attachAgentNamesToMessages( - client, - payload.messages.map(toChatMessageRow), - hasExplicitTimeout - ? { - timeout: remainingTimeoutMs(), - bestEffort: true, - } - : {}, - ); - return { - type: "list", - data: messages, - schema: chatMessageSchema, - }; - } catch (err) { - throw toChatCommandError("CHAT_WAIT_FAILED", "wait for chat messages", err); - } finally { - await client.close().catch(() => {}); - } -} diff --git a/packages/cli/src/commands/hub/client.test.ts b/packages/cli/src/commands/hub/client.test.ts index 7cb47ba82b..f4948b3206 100644 --- a/packages/cli/src/commands/hub/client.test.ts +++ b/packages/cli/src/commands/hub/client.test.ts @@ -91,6 +91,35 @@ describe("Hub HTTP client", () => { ); }); + it("reads configuration resources in the Hub's slug vocabulary", async () => { + const requests: Array<{ url: string | undefined; body: string }> = []; + const origin = await startServer( + () => ({ + status: 200, + body: { + daemons: [{ id: "a50e05af-4f20-4c8f-8dcc-58e5ea360663", slug: "macbook" }], + github: [ + { + slug: "getpaseo", + accountLogin: "getpaseo", + accountType: "Organization", + repositories: ["getpaseo/paseo"], + }, + ], + discord: [{ slug: "paseo", guildName: "Paseo" }], + slack: [{ slug: "paseo", teamName: "Paseo" }], + }, + }), + requests, + ); + + const resources = await new HubHttpClient().listConfigurationResources(origin, "secret"); + + assert.equal(resources.daemons[0]?.slug, "macbook"); + assert.equal(resources.discord[0]?.slug, "paseo"); + assert.equal(requests[0]?.url, "/api/v1/configuration-resources"); + }); + it("renders file-aware Hub validation issues without exposing credentials or response bodies", async () => { const requests: Array<{ url: string | undefined; body: string }> = []; const origin = await startServer( @@ -126,10 +155,14 @@ describe("Hub HTTP client", () => { (error: unknown) => { assert.ok(error instanceof HubCommandError); assert.equal(error.message.includes("operator-secret"), false); + assert.equal(error.message.includes("Correct the canonical"), false); assert.equal(error.details?.includes("operator-secret"), false); assert.equal(error.message.includes("sensitive bundle content"), false); assert.equal(error.details?.includes("sensitive bundle content"), false); - assert.match(error.details ?? "", /\.paseo\/workflows\/answer\.yml\.steps\.work\.agent/u); + assert.equal( + error.details, + ".paseo/workflows/answer.yml: steps.work.agent: unknown named agent [redacted]", + ); return true; }, ); diff --git a/packages/cli/src/commands/hub/commands.test.ts b/packages/cli/src/commands/hub/commands.test.ts index 2815982a24..0857bd1971 100644 --- a/packages/cli/src/commands/hub/commands.test.ts +++ b/packages/cli/src/commands/hub/commands.test.ts @@ -21,6 +21,7 @@ describe("Hub commands", () => { assert.deepEqual(names, [ "login", + "init", "connect", "status", "disconnect", @@ -344,7 +345,6 @@ describe("Hub commands", () => { daemon.beforeDisconnect = () => { assert.equal(credentials.active()?.credential, "human-secret"); }; - const result = await runHubLogout( { json: true, disconnectDaemon: true, force: true }, { @@ -359,9 +359,31 @@ describe("Hub commands", () => { assert.equal(daemon.disconnects, 1); assert.deepEqual(daemon.disconnectForces, [true]); assert.equal(result.data.daemonDisconnected, true); + assert.equal(result.data.warning, undefined); }); - it("disconnect reports the current Hub and preserves it in the final result", async () => { + it("logout reports a daemon disconnect warning", async () => { + const credentials = new MemoryCredentials(); + credentials.save({ origin: "https://hub.test", credential: "human-secret" }); + const daemon = new FakeDaemon("https://hub.test"); + daemon.disconnectWarning = "Hub could not be reached; cleanup may remain pending."; + + const result = await runHubLogout( + { json: true, disconnectDaemon: true }, + { + credentials, + daemon: new FakeDaemonConnection(daemon), + isInteractive: () => false, + confirmDisconnect: async () => false, + reporter: quietReporter, + }, + ); + + assert.equal(result.data.daemonDisconnected, true); + assert.equal(result.data.warning, "Hub could not be reached; cleanup may remain pending."); + }); + + it("disconnect reports clean local status without a ghost Hub origin", async () => { const daemon = new FakeDaemon("https://hub.test"); const progress: string[] = []; @@ -374,7 +396,7 @@ describe("Hub commands", () => { ); assert.deepEqual(progress, ["Disconnecting this daemon from https://hub.test"]); - assert.equal(result.data[0]?.hub, "https://hub.test"); + assert.equal(result.data[0]?.hub, null); }); it("preserves login when an accepted daemon disconnect fails", async () => { @@ -448,6 +470,7 @@ class FakeDaemon implements HubDaemonClient { disconnects = 0; readonly disconnectForces: boolean[] = []; disconnectError: Error | null = null; + disconnectWarning: string | null = null; beforeDisconnect: (() => void) | null = null; constructor(private readonly origin: string) {} @@ -466,7 +489,10 @@ class FakeDaemon implements HubDaemonClient { this.disconnectForces.push(force); this.beforeDisconnect?.(); if (this.disconnectError !== null) throw this.disconnectError; - return { status: hubStatus("not_connected", null) }; + return { + status: hubStatus("not_connected", null), + ...(this.disconnectWarning ? { warning: this.disconnectWarning } : {}), + }; } async close() {} diff --git a/packages/cli/src/commands/hub/deploy.ts b/packages/cli/src/commands/hub/deploy.ts index 7cf4cc5795..d64137879a 100644 --- a/packages/cli/src/commands/hub/deploy.ts +++ b/packages/cli/src/commands/hub/deploy.ts @@ -8,7 +8,7 @@ import { type HubValidationResult, } from "./hub-client/index.js"; import { PrivateHubCredentialStore, type HubCredentialStore } from "./credentials.js"; -import { discoverHubBundle } from "./deploy-bundle.js"; +import { discoverHubBundle, type HubDeployBundle } from "./deploy-bundle.js"; import { processHubReporter, reportHubProgress, type HubReporter } from "./reporter.js"; import { addHubResolutionHelp } from "./help.js"; @@ -20,7 +20,7 @@ export interface HubDeployOptions { json?: boolean; } -interface HubDeployEnvironment { +export interface HubDeployEnvironment { cwd: string; env: Readonly>; credentials?: HubCredentialStore; @@ -76,6 +76,18 @@ export async function runHubDeploy( credentials: new PrivateHubCredentialStore(), hub: new HubHttpClient(), }, +): Promise | SingleResult> { + const deployInput = await discoverHubBundle({ + cwd: environment.cwd, + ...(options.project === undefined ? {} : { project: options.project }), + }); + return runHubDeployBundle(options, deployInput, environment); +} + +export async function runHubDeployBundle( + options: HubDeployOptions, + deployInput: HubDeployBundle, + environment: HubDeployEnvironment, ): Promise | SingleResult> { const credentials = environment.credentials ?? new PrivateHubCredentialStore(environment.env); const resolution = { @@ -84,10 +96,6 @@ export async function runHubDeploy( credentials, }; const origin = resolveHubOrigin(resolution); - const deployInput = await discoverHubBundle({ - cwd: environment.cwd, - ...(options.project === undefined ? {} : { project: options.project }), - }); const action = options.dryRun === true ? "Validating" : "Deploying"; reportHubProgress( environment.reporter ?? processHubReporter, diff --git a/packages/cli/src/commands/hub/disconnect.ts b/packages/cli/src/commands/hub/disconnect.ts index 5cbe0e4291..867201376f 100644 --- a/packages/cli/src/commands/hub/disconnect.ts +++ b/packages/cli/src/commands/hub/disconnect.ts @@ -31,7 +31,7 @@ export function runHubDisconnect( ); } const response = await client.disconnectHub(options.force ?? false); - return hubStatusResult(response.status, response.warning, current.hubOrigin); + return hubStatusResult(response.status, response.warning); }); } @@ -42,7 +42,7 @@ export function addHubDisconnectCommand( addJsonAndDaemonHostOptions( parent .command("disconnect") - .option("--force", "Remove local authority even if the Hub is offline"), + .option("--force", "Remove local authority without notifying the Hub"), ).action( withOutput(async (...args) => { const options = args.at(-2) as HubDisconnectOptions; diff --git a/packages/cli/src/commands/hub/hub-client/index.ts b/packages/cli/src/commands/hub/hub-client/index.ts index 8bc50b314e..3086d648f2 100644 --- a/packages/cli/src/commands/hub/hub-client/index.ts +++ b/packages/cli/src/commands/hub/hub-client/index.ts @@ -3,6 +3,7 @@ import { HubCommandError } from "../error.js"; import { authorizationPollSchema, authorizationSchema, + configurationResourcesSchema, enrollmentTokenSchema, installResponseSchema, projectsResponseSchema, @@ -10,6 +11,7 @@ import { type CliAuthorization, type CliAuthorizationPoll, type HubInstallResult, + type HubConfigurationResources, type HubProject, type HubValidationResult, } from "./internal/contracts.js"; @@ -19,6 +21,7 @@ export type { CliAuthorization, CliAuthorizationPoll, HubInstallResult, + HubConfigurationResources, HubProject, HubValidationResult, } from "./internal/contracts.js"; @@ -80,6 +83,18 @@ export class HubHttpClient { return response.projects; } + listConfigurationResources(origin: string, apiKey: string): Promise { + return requestHub({ + origin, + path: "/api/v1/configuration-resources", + method: "GET", + apiKey, + successStatus: 200, + schema: configurationResourcesSchema, + failureMessage: "Hub configuration resource listing failed", + }); + } + installConfiguration(input: HubConfigurationInput): Promise { return requestHub({ origin: input.origin, diff --git a/packages/cli/src/commands/hub/hub-client/internal/contracts.ts b/packages/cli/src/commands/hub/hub-client/internal/contracts.ts index efbbf27d64..82b7e3ca58 100644 --- a/packages/cli/src/commands/hub/hub-client/internal/contracts.ts +++ b/packages/cli/src/commands/hub/hub-client/internal/contracts.ts @@ -36,6 +36,24 @@ const projectSchema = z export const projectsResponseSchema = z.object({ projects: z.array(projectSchema) }).strict(); +export const configurationResourcesSchema = z + .object({ + daemons: z.array(z.object({ id: z.string().uuid(), slug: z.string().min(1) }).strict()), + github: z.array( + z + .object({ + slug: z.string().min(1), + accountLogin: z.string().min(1), + accountType: z.string().min(1), + repositories: z.array(z.string().min(1)), + }) + .strict(), + ), + discord: z.array(z.object({ slug: z.string().min(1), guildName: z.string().min(1) }).strict()), + slack: z.array(z.object({ slug: z.string().min(1), teamName: z.string().min(1) }).strict()), + }) + .strict(); + export const installResponseSchema = z .object({ projectSlug: z.string().min(1), @@ -56,5 +74,6 @@ export const enrollmentTokenSchema = z export type CliAuthorization = z.infer; export type CliAuthorizationPoll = z.infer; export type HubProject = z.infer; +export type HubConfigurationResources = z.infer; export type HubInstallResult = z.infer; export type HubValidationResult = z.infer; diff --git a/packages/cli/src/commands/hub/hub-client/internal/problem.ts b/packages/cli/src/commands/hub/hub-client/internal/problem.ts index 8ed7bdf309..38482953dc 100644 --- a/packages/cli/src/commands/hub/hub-client/internal/problem.ts +++ b/packages/cli/src/commands/hub/hub-client/internal/problem.ts @@ -51,9 +51,14 @@ export async function hubRequestFailure( ); } const title = parsed.data.title ?? `${failureMessage} with HTTP ${response.status}`; - const message = parsed.data.detail === undefined ? title : `${title}: ${parsed.data.detail}`; const details = formatFieldIssues(parsed.data.errors, parsed.data.issues); - const code = response.status === 422 ? "HUB_VALIDATION_FAILED" : "HUB_REQUEST_FAILED"; + const message = + details !== undefined || parsed.data.detail === undefined + ? title + : `${title}: ${parsed.data.detail}`; + let code = "HUB_REQUEST_FAILED"; + if (response.status === 422) code = "HUB_VALIDATION_FAILED"; + if (response.status === 404) code = "HUB_NOT_FOUND"; return new HubCommandError( code, redactSecret(message, apiKey), @@ -82,12 +87,20 @@ function formatFieldIssues( function formatIssuePath(path: z.infer | undefined): string | undefined { if (path === undefined || typeof path === "string") return path; + const [file, ...fieldPath] = path; + if (typeof file === "string" && file.startsWith(".paseo/") && fieldPath.length > 0) { + return `${file}: ${formatPathSegments(fieldPath)}`; + } + return formatPathSegments(path) || undefined; +} + +function formatPathSegments(path: readonly (string | number)[]): string { let formatted = ""; for (const segment of path) { if (typeof segment === "number") formatted += `[${segment}]`; else formatted += formatted.length === 0 ? segment : `.${segment}`; } - return formatted || undefined; + return formatted; } function redactSecret(value: string, secret: string | undefined): string { diff --git a/packages/cli/src/commands/hub/index.ts b/packages/cli/src/commands/hub/index.ts index 1dac54fc7a..3edd48ef3f 100644 --- a/packages/cli/src/commands/hub/index.ts +++ b/packages/cli/src/commands/hub/index.ts @@ -18,6 +18,7 @@ import { addHubProjectsCommand } from "./projects.js"; import { processHubReporter, type HubReporter } from "./reporter.js"; import { hubStatusResult } from "./status-output.js"; import { addHubResolutionHelp } from "./help.js"; +import { addHubInitCommand } from "./init.js"; interface HubCommandEnvironment { env: Readonly>; @@ -56,6 +57,7 @@ export function createHubCommand(overrides: Partial = {}) flow: environment.login, reporter: environment.reporter, }); + addHubInitCommand(hub, environment); addHubConnectCommand(hub, { env: environment.env, credentials: environment.credentials, diff --git a/packages/cli/src/commands/hub/init-plan.test.ts b/packages/cli/src/commands/hub/init-plan.test.ts new file mode 100644 index 0000000000..afc91a690f --- /dev/null +++ b/packages/cli/src/commands/hub/init-plan.test.ts @@ -0,0 +1,159 @@ +import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import YAML from "yaml"; +import { afterEach, describe, expect, it } from "vitest"; +import { discoverHubBundle } from "./deploy-bundle.js"; +import { runHubDeploy } from "./deploy.js"; +import { + createHubInitScaffold, + planHubInitOpening, + resolveHubInitConnection, + resolveHubInitProjects, + type HubInitProvider, +} from "./init-plan.js"; +import { githubRepositoryFromRemote } from "./init.js"; + +const directories: string[] = []; +const project = (slug: string) => ({ + id: "7e950d84-eec0-4e18-b1f9-9c115bdb31e4", + slug, + name: slug.toUpperCase(), +}); + +afterEach(async () => { + await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true }))); +}); + +describe("Hub init planning", () => { + it("includes login only when there is no active login", () => { + expect(planHubInitOpening({ loggedIn: false, paseoDirectoryExists: false })).toEqual({ + replaceExisting: false, + steps: ["login", "connect", "project", "scaffold"], + }); + expect(planHubInitOpening({ loggedIn: true, paseoDirectoryExists: false })).toEqual({ + replaceExisting: false, + steps: ["connect", "project", "scaffold"], + }); + }); + + it("plans a confirmed replacement for an existing .paseo directory", () => { + expect(planHubInitOpening({ loggedIn: true, paseoDirectoryExists: true })).toEqual({ + replaceExisting: true, + steps: ["connect", "project", "scaffold"], + }); + }); + + it("stops, defaults, or asks according to the project count", () => { + expect(resolveHubInitProjects([])).toEqual({ kind: "none" }); + expect(resolveHubInitProjects([project("one")])).toEqual({ + kind: "selected", + project: project("one"), + }); + expect(resolveHubInitProjects([project("one"), project("two")])).toEqual({ + kind: "choose", + projects: [project("one"), project("two")], + }); + }); + + it("reuses a connected daemon, waits for reconnect, and rejects a different Hub", () => { + const status = { + state: "connected", + daemonId: "daemon-1", + hubOrigin: "https://hub.test", + scopes: ["hub.execution.*"], + connectedAt: null, + lastError: null, + }; + expect(resolveHubInitConnection(status, "https://hub.test")).toEqual({ + kind: "connected", + daemonId: "daemon-1", + }); + expect(resolveHubInitConnection(status, "https://other.test")).toEqual({ + kind: "conflict", + origin: "https://hub.test", + }); + expect( + resolveHubInitConnection({ ...status, state: "reconnecting" }, "https://hub.test"), + ).toEqual({ kind: "pending", state: "reconnecting" }); + expect( + resolveHubInitConnection( + { ...status, state: "not_connected", daemonId: null, hubOrigin: null }, + "https://hub.test", + ), + ).toEqual({ kind: "connect" }); + }); +}); + +describe("Hub init scaffold", () => { + it.each([ + ["github", { repo: "getpaseo/paseo", user: "boudra" }], + ["slack", { workspace: "paseo", user: "boudra" }], + ["discord", { guild: "paseo", user: "boudra" }], + ] satisfies readonly [HubInitProvider, Record][])( + "creates a closed %s trigger that the deploy discovery path accepts", + async (provider, providerFilters) => { + const cwd = await temporaryDirectory(); + const scaffold = createHubInitScaffold({ + cwd, + daemonSlug: "build-studio", + provider, + providerFilters, + }); + await mkdir(path.join(cwd, ".paseo", "workflows"), { recursive: true }); + await writeFile(path.join(cwd, ".paseo", "hub.yml"), scaffold.hub); + await writeFile(path.join(cwd, scaffold.workflowPath), scaffold.workflow); + + const bundle = await discoverHubBundle({ cwd, project: "paseo" }); + expect(bundle.workflowCount).toBe(1); + expect(bundle.files.map((file) => file.path)).toEqual([ + ".paseo/hub.yml", + scaffold.workflowPath, + ]); + const parsed = YAML.parse(scaffold.workflow) as { + filters: { from_users: string[]; channels?: string[] }; + }; + expect(parsed.filters.from_users).toEqual([providerFilters.user]); + expect(parsed.filters.channels).toBeUndefined(); + + let validatedPaths: readonly string[] = []; + const result = await runHubDeploy( + { project: "paseo", hub: "https://hub.test", dryRun: true }, + { + cwd, + env: { PASEO_HUB_API_KEY: "test-key" }, + reporter: { progress() {} }, + hub: { + async validateConfiguration(input) { + validatedPaths = input.files.map((file) => file.path); + for (const file of input.files) YAML.parse(file.content); + return { projectSlug: input.projectSlug, valid: true }; + }, + async installConfiguration() { + throw new Error("dry-run must not install"); + }, + }, + }, + ); + expect(result.data).toMatchObject({ projectSlug: "paseo", valid: true, workflows: 1 }); + expect(validatedPaths).toEqual([".paseo/hub.yml", scaffold.workflowPath]); + }, + ); +}); + +describe("GitHub origin detection", () => { + it.each([ + ["git@github.com:getpaseo/paseo.git", "getpaseo/paseo"], + ["ssh://git@github.com/getpaseo/paseo.git", "getpaseo/paseo"], + ["https://github.com/getpaseo/paseo.git", "getpaseo/paseo"], + ["https://gitlab.com/getpaseo/paseo.git", undefined], + ])("resolves %s", (remote, expected) => { + expect(githubRepositoryFromRemote(remote)).toBe(expected); + }); +}); + +async function temporaryDirectory(): Promise { + const directory = await mkdtemp(path.join(tmpdir(), "paseo-hub-init-")); + directories.push(directory); + return directory; +} diff --git a/packages/cli/src/commands/hub/init-plan.ts b/packages/cli/src/commands/hub/init-plan.ts new file mode 100644 index 0000000000..88e90de38f --- /dev/null +++ b/packages/cli/src/commands/hub/init-plan.ts @@ -0,0 +1,201 @@ +import path from "node:path"; +import YAML from "yaml"; +import type { HubDeployBundle } from "./deploy-bundle.js"; +import type { HubProject } from "./hub-client/index.js"; +import type { HubStatus } from "./daemon-client.js"; + +export type HubInitProvider = "github" | "slack" | "discord"; + +export type HubInitProjectResolution = + | { kind: "none" } + | { kind: "selected"; project: HubProject } + | { kind: "choose"; projects: readonly HubProject[] }; + +export type HubInitConnectionResolution = + | { kind: "connected"; daemonId: string } + | { kind: "pending"; state: "connecting" | "reconnecting" } + | { kind: "connect" } + | { kind: "conflict"; origin: string }; + +export interface HubInitOpeningPlan { + replaceExisting: boolean; + steps: readonly ("login" | "connect" | "project" | "scaffold")[]; +} + +export interface HubInitScaffoldInput { + cwd: string; + daemonSlug: string; + provider: HubInitProvider; + providerFilters: Readonly>; +} + +export interface HubInitScaffold { + hub: string; + workflowPath: string; + workflow: string; + testAction: string; +} + +export function planHubInitOpening(input: { + loggedIn: boolean; + paseoDirectoryExists: boolean; +}): HubInitOpeningPlan { + return { + replaceExisting: input.paseoDirectoryExists, + steps: [...(input.loggedIn ? [] : (["login"] as const)), "connect", "project", "scaffold"], + }; +} + +export function createHubInitBundle( + projectSlug: string, + scaffold: HubInitScaffold, +): HubDeployBundle { + return { + projectSlug, + workflowCount: 1, + files: [ + { path: ".paseo/hub.yml", content: scaffold.hub }, + { path: scaffold.workflowPath, content: scaffold.workflow }, + ], + }; +} + +export function resolveHubInitProjects(projects: readonly HubProject[]): HubInitProjectResolution { + if (projects.length === 0) return { kind: "none" }; + if (projects.length === 1) return { kind: "selected", project: projects[0]! }; + return { kind: "choose", projects }; +} + +export function resolveHubInitConnection( + status: HubStatus, + origin: string, +): HubInitConnectionResolution { + if (status.state === "connected" && status.hubOrigin === origin && status.daemonId !== null) { + return { kind: "connected", daemonId: status.daemonId }; + } + if ( + (status.state === "connecting" || status.state === "reconnecting") && + status.hubOrigin === origin + ) { + return { kind: "pending", state: status.state }; + } + if (status.hubOrigin !== null && status.state !== "not_connected" && status.state !== "revoked") { + return { kind: "conflict", origin: status.hubOrigin }; + } + return { kind: "connect" }; +} + +export function createHubInitScaffold(input: HubInitScaffoldInput): HubInitScaffold { + const environmentName = input.daemonSlug; + const hub = YAML.stringify( + { + environments: { + [environmentName]: { + kind: "daemon", + daemon: input.daemonSlug, + cwd: path.resolve(input.cwd), + }, + }, + agents: { + codex: { + provider: "codex", + mode: "full-access", + }, + }, + }, + { lineWidth: 0 }, + ); + const provider = providerScaffold(input.provider, input.providerFilters, environmentName); + return { + hub, + workflowPath: `.paseo/workflows/${input.provider}-help.yml`, + workflow: YAML.stringify(provider.workflow, { lineWidth: 0 }), + testAction: provider.testAction, + }; +} + +function providerScaffold( + provider: HubInitProvider, + filters: Readonly>, + environment: string, +): { workflow: object; testAction: string } { + if (provider === "github") { + const repo = requireFilter(filters, "repo"); + const user = requireFilter(filters, "user"); + return { + workflow: workflow({ + name: "github-help", + on: "github.issue_comment", + filters: { repo, contains: "@paseo", from_users: [user] }, + environment, + }), + testAction: `Comment \`@paseo have a look\` on ${repo}.`, + }; + } + + const user = requireFilter(filters, "user"); + if (provider === "slack") { + const workspace = requireFilter(filters, "workspace"); + return { + workflow: workflow({ + name: "slack-help", + on: "slack.mention", + filters: { workspace, from_users: [user] }, + environment, + reply: "slack.reply", + }), + testAction: "Mention `@Paseo have a look` in Slack.", + }; + } + + const guild = requireFilter(filters, "guild"); + return { + workflow: workflow({ + name: "discord-help", + on: "discord.mention", + filters: { guild, from_users: [user] }, + environment, + reply: "discord.reply", + }), + testAction: "Mention `@Paseo have a look` in Discord.", + }; +} + +function workflow(input: { + name: string; + on: string; + filters: object; + environment: string; + reply?: "slack.reply" | "discord.reply"; +}): object { + const replyInstruction = input.reply === undefined ? "" : "Answer with hub.reply, then "; + return { + name: input.name, + on: input.on, + max_runtime: "2h", + filters: input.filters, + steps: [ + { + id: "work", + environment: input.environment, + max_runtime: "90m", + idle_timeout: "10m", + agent: "codex", + prompt: [ + { + text: `${replyInstruction}complete this request and call hub.finish_execution when done.\n\n\n\${{ paseo.prompt }}\n\n`, + }, + ], + ...(input.reply === undefined + ? {} + : { allow_outputs: [{ type: input.reply, max: 1, required: true }] }), + }, + ], + }; +} + +function requireFilter(filters: Readonly>, name: string): string { + const value = filters[name]?.trim(); + if (!value) throw new Error(`${name} is required for this provider`); + return value; +} diff --git a/packages/cli/src/commands/hub/init.ts b/packages/cli/src/commands/hub/init.ts new file mode 100644 index 0000000000..c86f79b4ec --- /dev/null +++ b/packages/cli/src/commands/hub/init.ts @@ -0,0 +1,558 @@ +import { + cancel, + confirm, + intro, + isCancel, + log, + note, + outro, + select, + spinner, + text, +} from "@clack/prompts"; +import { execFile } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { lstat, mkdir, rename, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { promisify } from "node:util"; +import type { Command } from "commander"; +import { DEFAULT_HUB_ORIGIN, resolveHubCredential } from "./authority.js"; +import type { HubCredentialStore } from "./credentials.js"; +import type { HubDaemonConnection } from "./daemon-client.js"; +import { withHubDaemon } from "./daemon-client.js"; +import { HubCommandError } from "./error.js"; +import type { HubConfigurationResources, HubHttpClient, HubProject } from "./hub-client/index.js"; +import { + createHubInitBundle, + createHubInitScaffold, + planHubInitOpening, + resolveHubInitConnection, + resolveHubInitProjects, + type HubInitProvider, +} from "./init-plan.js"; +import type { CliLoginFlow } from "./login-flow.js"; +import { runHubLogin } from "./login.js"; +import { runHubConnect } from "./connect.js"; +import { runHubDeployBundle } from "./deploy.js"; +import { runHubProjects } from "./projects.js"; +import type { HubReporter } from "./reporter.js"; +import { normalizeHubOrigin } from "./origin.js"; + +const execFileAsync = promisify(execFile); +const DAEMON_READY_TIMEOUT_MS = 60_000; +const DAEMON_READY_POLL_MS = 250; + +interface HubInitEnvironment { + env: Readonly>; + credentials: HubCredentialStore; + hub: HubHttpClient; + login: Pick; + daemon: HubDaemonConnection; + reporter: HubReporter; + cwd(): string; +} + +class HubInitCancelledError extends Error {} + +function initErrorMessage(error: unknown): string { + if (error instanceof HubCommandError && error.details) { + return `${error.message}\n${error.details}`; + } + return error instanceof Error ? error.message : String(error); +} + +export function addHubInitCommand(parent: Command, environment: HubInitEnvironment): void { + parent + .command("init") + .description("Create and optionally deploy a safe starter Hub bundle") + .action(async () => { + try { + await runHubInit(environment); + } catch (error) { + if (error instanceof HubInitCancelledError) { + cancel(error.message); + return; + } + cancel(initErrorMessage(error)); + process.exitCode = 1; + } + }); +} + +export async function runHubInit(environment: HubInitEnvironment): Promise { + requireInteractiveTerminal(); + intro("Set up Paseo Hub"); + + const cwd = environment.cwd(); + const activeLogin = environment.credentials.active(); + const opening = planHubInitOpening({ + loggedIn: activeLogin !== null, + paseoDirectoryExists: await pathExists(path.join(cwd, ".paseo")), + }); + if ( + opening.replaceExisting && + !(await requiredConfirm("Replace the existing .paseo/ Hub bundle?", false)) + ) { + throw new HubInitCancelledError("Existing .paseo/ bundle left unchanged."); + } + + const origin = await ensureLogin(activeLogin?.origin, environment); + const daemonId = await ensureDaemonConnection(origin, environment); + const project = await chooseProject(origin, environment); + const resources = await loadConfigurationResources(origin, environment); + const daemon = resources.daemons.find(({ id }) => id === daemonId); + if (daemon === undefined) { + throw new HubCommandError( + "HUB_DAEMON_RESOURCE_MISSING", + "The connected daemon is not available in this Hub organization. Reconnect it and try again.", + ); + } + log.success(`Connected as ${daemon.slug}`); + const provider = await chooseProvider(); + const providerFilters = await collectProviderFilters(provider, resources, cwd); + const scaffold = createHubInitScaffold({ + cwd, + daemonSlug: daemon.slug, + provider, + providerFilters, + }); + + const bundle = createHubInitBundle(project.slug, scaffold); + + await withSpinner("Validating bundle", async () => { + await runHubDeployBundle({ project: project.slug, hub: origin, dryRun: true }, bundle, { + cwd, + env: environment.env, + credentials: environment.credentials, + hub: environment.hub, + reporter: { progress() {} }, + }); + }); + log.success("Dry run passed"); + await writeScaffold(cwd, scaffold, opening.replaceExisting); + log.success(`Created .paseo/hub.yml and ${scaffold.workflowPath}`); + + const deploy = await requiredConfirm("Deploy now?", true); + if (deploy) { + await withSpinner("Deploying bundle", async () => { + await runHubDeployBundle({ project: project.slug, hub: origin }, bundle, { + cwd, + env: environment.env, + credentials: environment.credentials, + hub: environment.hub, + reporter: { progress() {} }, + }); + }); + log.success("Deployed"); + } else { + log.message(`Skipped deployment. Run: paseo hub deploy -p ${project.slug}`); + } + + const activityUrl = new URL(`/projects/${project.slug}/activity`, origin).toString(); + note(`${scaffold.testAction}\nWatch it at ${activityUrl}`, "Test your workflow"); + outro(deploy ? "Hub is ready" : "Hub bundle is ready"); +} + +async function ensureLogin( + activeOrigin: string | undefined, + environment: HubInitEnvironment, +): Promise { + const endpoint = await requiredSelect({ + message: "Hub endpoint", + initialValue: + activeOrigin === undefined || activeOrigin === DEFAULT_HUB_ORIGIN ? "hosted" : "custom", + options: [ + { value: "hosted", label: "hub.paseo.sh" }, + { value: "custom", label: "Custom endpoint…" }, + ], + }); + const origin = + endpoint === "hosted" + ? DEFAULT_HUB_ORIGIN + : await requiredText({ + message: "Custom Hub URL", + initialValue: + activeOrigin === undefined || activeOrigin === DEFAULT_HUB_ORIGIN + ? environment.env.PASEO_HUB_URL + : activeOrigin, + validate(value) { + try { + normalizeHubOrigin(value ?? ""); + } catch { + return "Enter a valid Hub URL"; + } + return undefined; + }, + }); + const normalizedOrigin = normalizeHubOrigin(origin); + if (environment.credentials.get(normalizedOrigin) !== null) { + log.success(`Logged in to ${normalizedOrigin}`); + return normalizedOrigin; + } + await runHubLogin( + normalizedOrigin, + {}, + { + env: environment.env, + credentials: environment.credentials, + flow: environment.login, + reporter: environment.reporter, + }, + ); + log.success(`Logged in to ${normalizedOrigin}`); + return normalizedOrigin; +} + +async function ensureDaemonConnection( + origin: string, + environment: HubInitEnvironment, +): Promise { + const status = await withHubDaemon(environment.daemon, undefined, async (daemon) => + daemon.getHubStatus().then((response) => response.status), + ); + const connection = resolveHubInitConnection(status, origin); + if (connection.kind === "connected") { + return connection.daemonId; + } + if (connection.kind === "pending") { + return waitForDaemonReady(origin, environment.daemon); + } + if (connection.kind === "conflict") { + throw new HubCommandError( + "HUB_DAEMON_ALREADY_CONNECTED", + `This daemon is connected to ${connection.origin}. Disconnect it before running Hub init for ${origin}.`, + ); + } + if (!(await requiredConfirm(`Connect this daemon to ${origin}?`, true))) { + throw new HubInitCancelledError("A connected daemon is required to create the bundle."); + } + await runHubConnect( + origin, + {}, + { + env: environment.env, + credentials: environment.credentials, + hub: environment.hub, + daemon: environment.daemon, + reporter: environment.reporter, + }, + ); + return waitForDaemonReady(origin, environment.daemon); +} + +async function waitForDaemonReady( + origin: string, + connection: HubDaemonConnection, +): Promise { + return withSpinner("Waiting for the daemon to connect", async (reporter) => + withHubDaemon(connection, undefined, async (daemon) => { + const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS; + while (true) { + const status = (await daemon.getHubStatus()).status; + const resolution = resolveHubInitConnection(status, origin); + if (resolution.kind === "connected") return resolution.daemonId; + if (resolution.kind === "conflict") { + throw new HubCommandError( + "HUB_DAEMON_ALREADY_CONNECTED", + `This daemon connected to ${resolution.origin} while Hub init was waiting for ${origin}.`, + ); + } + if (resolution.kind === "connect") { + throw new HubCommandError( + "HUB_DAEMON_CONNECTION_LOST", + "The daemon lost its Hub relationship while Hub init was waiting for it.", + ); + } + if (Date.now() >= deadline) { + throw new HubCommandError( + "HUB_DAEMON_CONNECTION_TIMEOUT", + "The daemon did not connect within 60 seconds. Check `paseo hub status`, then run Hub init again.", + ); + } + reporter.progress(`Daemon is ${resolution.state}`); + await delay(DAEMON_READY_POLL_MS); + } + }), + ); +} + +async function chooseProject(origin: string, environment: HubInitEnvironment): Promise { + const result = await runHubProjects( + { hub: origin }, + { + env: environment.env, + credentials: environment.credentials, + hub: environment.hub, + reporter: { progress() {} }, + }, + ); + const resolution = resolveHubInitProjects(result.data.projects); + if (resolution.kind === "none") { + throw new HubInitCancelledError( + `No Hub projects exist yet. Create one at ${new URL("/projects/new", origin).toString()}, then run paseo hub init again.`, + ); + } + if (resolution.kind === "selected") { + return resolution.project; + } + const slug = await requiredSelect({ + message: "Project", + options: resolution.projects.map((project) => ({ + value: project.slug, + label: project.name, + hint: project.slug, + })), + }); + const project = resolution.projects.find((candidate) => candidate.slug === slug); + if (project === undefined) { + throw new HubCommandError( + "HUB_PROJECT_SELECTION_INVALID", + "The selected Hub project is unavailable.", + ); + } + return project; +} + +async function chooseProvider(): Promise { + return requiredSelect({ + message: "Trigger provider", + options: [ + { value: "github", label: "GitHub", hint: "issue or pull request comment" }, + { value: "slack", label: "Slack", hint: "channel mention" }, + { value: "discord", label: "Discord", hint: "channel mention" }, + ], + }); +} + +async function collectProviderFilters( + provider: HubInitProvider, + resources: HubConfigurationResources, + cwd: string, +): Promise>> { + if (provider === "github") { + const [login, originRemote] = await Promise.all([ + readGhValue(["api", "user", "--jq", ".login"]), + readCommandValue("git", ["remote", "get-url", "origin"], cwd), + ]); + const repo = originRemote === undefined ? undefined : githubRepositoryFromRemote(originRemote); + if (repo === undefined) { + throw new HubCommandError( + "HUB_GITHUB_REPOSITORY_UNDETECTED", + "Could not detect a GitHub repository from the origin remote.", + ); + } + if (!resources.github.some(({ repositories }) => repositories.includes(repo))) { + const connected = resources.github.flatMap(({ repositories }) => repositories); + throw new HubCommandError( + "HUB_GITHUB_REPOSITORY_NOT_CONNECTED", + `${repo} is not connected to this Hub organization. Connected repositories: ${connected.join(", ") || "none"}.`, + ); + } + return { + user: await requiredText({ + message: "Your GitHub username (only this user can trigger the bot)", + initialValue: login, + }), + repo, + }; + } + if (provider === "slack") { + return { + workspace: await chooseConnection( + "Slack workspace", + resources.slack.map(({ slug, teamName }) => ({ slug, label: teamName })), + ), + user: await requiredText({ + message: "Your Slack username (only this user can trigger the bot)", + }), + }; + } + return { + guild: await chooseConnection( + "Discord server", + resources.discord.map(({ slug, guildName }) => ({ slug, label: guildName })), + ), + user: await requiredText({ + message: "Your Discord username (only this user can trigger the bot)", + }), + }; +} + +async function loadConfigurationResources( + origin: string, + environment: HubInitEnvironment, +): Promise { + try { + return await withSpinner("Loading Hub connections", () => + environment.hub.listConfigurationResources( + origin, + resolveHubCredential({ + options: { origin }, + env: environment.env, + credentials: environment.credentials, + origin, + }), + ), + ); + } catch (error) { + if (error instanceof HubCommandError && error.code === "HUB_NOT_FOUND") { + throw new HubCommandError( + "HUB_UPDATE_REQUIRED", + "This Hub does not support guided setup. Update the Hub and try again.", + ); + } + throw error; + } +} + +async function chooseConnection( + message: string, + connections: readonly { slug: string; label: string }[], +): Promise { + if (connections.length === 0) { + throw new HubCommandError( + "HUB_PROVIDER_CONNECTION_REQUIRED", + `No ${message.toLowerCase()} is connected in this Hub organization. Connect one and try again.`, + ); + } + if (connections.length === 1) return connections[0]!.slug; + return requiredSelect({ + message, + options: connections.map(({ slug, label }) => ({ value: slug, label })), + }); +} + +async function writeScaffold( + cwd: string, + scaffold: ReturnType, + replaceExisting: boolean, +): Promise { + const root = path.join(cwd, ".paseo"); + const staging = path.join(cwd, `.paseo-init-${randomUUID()}`); + const backup = path.join(cwd, `.paseo-backup-${randomUUID()}`); + let movedExisting = false; + try { + await mkdir(path.join(staging, "workflows"), { recursive: true }); + await writeFile(path.join(staging, "hub.yml"), scaffold.hub, { flag: "wx" }); + await writeFile( + path.join(staging, "workflows", path.basename(scaffold.workflowPath)), + scaffold.workflow, + { + flag: "wx", + }, + ); + if (replaceExisting) { + await rename(root, backup); + movedExisting = true; + } + await rename(staging, root); + if (movedExisting) await rm(backup, { recursive: true, force: true }); + } catch (error) { + await rm(staging, { recursive: true, force: true }); + if (movedExisting) { + await rename(backup, root).catch(() => undefined); + } + throw error; + } +} + +async function pathExists(target: string): Promise { + try { + await lstat(target); + return true; + } catch (error) { + if (errorCode(error) === "ENOENT") return false; + throw error; + } +} + +function errorCode(error: unknown): string | undefined { + return error instanceof Error && "code" in error && typeof error.code === "string" + ? error.code + : undefined; +} + +function delay(milliseconds: number): Promise { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); +} + +async function readGhValue(args: readonly string[]): Promise { + return readCommandValue("gh", args); +} + +async function readCommandValue( + command: string, + args: readonly string[], + cwd?: string, +): Promise { + try { + const { stdout } = await execFileAsync(command, [...args], { encoding: "utf8", cwd }); + return stdout.trim() || undefined; + } catch { + return undefined; + } +} + +export function githubRepositoryFromRemote(remote: string): string | undefined { + const trimmed = remote.trim().replace(/\.git$/u, ""); + const ssh = /^(?:ssh:\/\/)?git@github\.com[:/]([^/]+\/[^/]+)$/u.exec(trimmed); + if (ssh?.[1] !== undefined) return ssh[1]; + try { + const url = new URL(trimmed); + if (url.hostname !== "github.com") return undefined; + const repository = url.pathname.replace(/^\//u, ""); + return /^[^/]+\/[^/]+$/u.test(repository) ? repository : undefined; + } catch { + return undefined; + } +} + +async function withSpinner( + message: string, + action: (reporter: HubReporter) => Promise, +): Promise { + const progress = spinner(); + progress.start(message); + try { + const result = await action({ progress: (nextMessage) => progress.message(nextMessage) }); + progress.stop(message); + return result; + } catch (error) { + progress.error(message); + throw error; + } +} + +async function requiredText(options: Parameters[0]): Promise { + const answer = await text({ + ...options, + validate(value) { + const input = value ?? ""; + const customError = options.validate?.(input); + if (customError !== undefined) return customError; + return input.trim().length === 0 ? "A value is required" : undefined; + }, + }); + if (isCancel(answer)) throw new HubInitCancelledError("Hub init cancelled."); + return answer.trim(); +} + +async function requiredConfirm(message: string, initialValue: boolean): Promise { + const answer = await confirm({ message, initialValue }); + if (isCancel(answer)) throw new HubInitCancelledError("Hub init cancelled."); + return answer; +} + +async function requiredSelect( + options: Parameters>[0], +): Promise { + const answer = await select(options); + if (isCancel(answer)) throw new HubInitCancelledError("Hub init cancelled."); + return answer; +} + +function requireInteractiveTerminal(): void { + if (!process.stdin.isTTY || !process.stdout.isTTY) { + throw new HubCommandError("HUB_INIT_INTERACTIVE_REQUIRED", "paseo hub init requires a TTY."); + } +} diff --git a/packages/cli/src/commands/hub/logout.ts b/packages/cli/src/commands/hub/logout.ts index e4ea721376..28f0837021 100644 --- a/packages/cli/src/commands/hub/logout.ts +++ b/packages/cli/src/commands/hub/logout.ts @@ -11,6 +11,7 @@ interface HubLogoutResult { origin: string | null; status: "logged_out" | "not_logged_in"; daemonDisconnected: boolean; + warning?: string; } const schema: OutputSchema = { @@ -19,6 +20,7 @@ const schema: OutputSchema = { { header: "HUB", field: "origin" }, { header: "STATUS", field: "status" }, { header: "DAEMON DISCONNECTED", field: "daemonDisconnected" }, + { header: "WARNING", field: "warning" }, ], }; @@ -70,11 +72,16 @@ export async function runHubLogout( options, `Disconnecting this daemon from ${active.origin}`, ); - await withHubDaemon(dependencies.daemon, options.host, async (daemon) => { - await daemon.disconnectHub(options.force ?? false); + const disconnect = await withHubDaemon(dependencies.daemon, options.host, async (daemon) => { + return daemon.disconnectHub(options.force ?? false); }); dependencies.credentials.logoutActive(); - return logoutResult({ origin: active.origin, status: "logged_out", daemonDisconnected: true }); + return logoutResult({ + origin: active.origin, + status: "logged_out", + daemonDisconnected: disconnect.status.state === "not_connected", + ...(disconnect.warning ? { warning: disconnect.warning } : {}), + }); } export function addHubLogoutCommand(parent: Command, dependencies: HubLogoutDependencies): void { @@ -83,7 +90,7 @@ export function addHubLogoutCommand(parent: Command, dependencies: HubLogoutDepe .command("logout") .description("Remove the active stored Hub CLI login") .option("--disconnect-daemon", "Also disconnect a daemon related to the same Hub") - .option("--force", "Remove daemon authority even if Hub is offline"), + .option("--force", "Remove daemon authority without notifying the Hub"), ).action( withOutput(async (...args) => { const options = args.at(-2) as HubLogoutOptions; diff --git a/packages/cli/src/commands/loop/index.ts b/packages/cli/src/commands/loop/index.ts deleted file mode 100644 index 95359ce59e..0000000000 --- a/packages/cli/src/commands/loop/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Command } from "commander"; -import { withOutput } from "../../output/index.js"; -import { addJsonAndDaemonHostOptions, addDaemonHostOption } from "../../utils/command-options.js"; -import { addLoopRunOptions, runLoopRunCommand } from "./run.js"; -import { addLoopLsOptions, runLoopLsCommand } from "./ls.js"; -import { addLoopInspectOptions, runLoopInspectCommand } from "./inspect.js"; -import { addLoopLogsOptions, runLoopLogsCommand } from "./logs.js"; -import { addLoopStopOptions, runLoopStopCommand } from "./stop.js"; - -export function createLoopCommand(): Command { - const loop = new Command("loop").description("Run iterative worker loops"); - - addJsonAndDaemonHostOptions(addLoopRunOptions(loop.command("run"))).action( - withOutput(runLoopRunCommand), - ); - - addJsonAndDaemonHostOptions(addLoopLsOptions(loop.command("ls"))).action( - withOutput(runLoopLsCommand), - ); - - addJsonAndDaemonHostOptions(addLoopInspectOptions(loop.command("inspect"))).action( - withOutput(runLoopInspectCommand), - ); - - addDaemonHostOption(addLoopLogsOptions(loop.command("logs"))).action(runLoopLogsCommand); - - addJsonAndDaemonHostOptions(addLoopStopOptions(loop.command("stop"))).action( - withOutput(runLoopStopCommand), - ); - - return loop; -} diff --git a/packages/cli/src/commands/loop/inspect.ts b/packages/cli/src/commands/loop/inspect.ts deleted file mode 100644 index 4f4cece487..0000000000 --- a/packages/cli/src/commands/loop/inspect.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Command } from "commander"; -import { connectToDaemon, getDaemonHost } from "../../utils/client.js"; -import type { CommandOptions, CommandError, OutputSchema, ListResult } from "../../output/index.js"; -import type { LoopDaemonClient, LoopRecord } from "./types.js"; - -interface InspectRow { - key: string; - value: string; -} - -export interface LoopInspectOptions extends CommandOptions {} - -export function addLoopInspectOptions(command: Command): Command { - return command.description("Show loop details and iteration history").argument("", "Loop ID"); -} - -function createInspectSchema(loop: LoopRecord): OutputSchema { - return { - idField: "key", - columns: [ - { header: "KEY", field: "key", width: 18 }, - { header: "VALUE", field: "value", width: 80 }, - ], - serialize: () => loop, - }; -} - -function toRows(loop: LoopRecord): InspectRow[] { - return [ - { key: "Id", value: loop.id }, - { key: "Name", value: loop.name ?? "null" }, - { key: "Status", value: loop.status }, - { key: "Cwd", value: loop.cwd }, - { key: "Provider", value: loop.provider }, - { key: "Model", value: loop.model ?? "null" }, - { key: "WorkerProvider", value: loop.workerProvider ?? "null" }, - { key: "WorkerModel", value: loop.workerModel ?? "null" }, - { key: "VerifierProvider", value: loop.verifierProvider ?? "null" }, - { key: "VerifierModel", value: loop.verifierModel ?? "null" }, - { key: "Prompt", value: loop.prompt }, - { key: "VerifyPrompt", value: loop.verifyPrompt ?? "null" }, - { - key: "VerifyChecks", - value: loop.verifyChecks.length > 0 ? loop.verifyChecks.join(" | ") : "[]", - }, - { key: "Archive", value: String(loop.archive) }, - { key: "SleepMs", value: String(loop.sleepMs) }, - { - key: "MaxIterations", - value: loop.maxIterations === null ? "null" : String(loop.maxIterations), - }, - { key: "MaxTimeMs", value: loop.maxTimeMs === null ? "null" : String(loop.maxTimeMs) }, - { key: "CreatedAt", value: loop.createdAt }, - { key: "UpdatedAt", value: loop.updatedAt }, - { key: "CompletedAt", value: loop.completedAt ?? "null" }, - { - key: "Iterations", - value: - loop.iterations.length === 0 - ? "[]" - : loop.iterations - .map((iteration) => { - const summary = [ - `#${iteration.index}`, - iteration.status, - iteration.workerAgentId ? `worker=${iteration.workerAgentId}` : null, - iteration.verifierAgentId ? `verifier=${iteration.verifierAgentId}` : null, - iteration.failureReason ? `reason=${iteration.failureReason}` : null, - ] - .filter(Boolean) - .join(" "); - return summary; - }) - .join(" | "), - }, - ]; -} - -export type LoopInspectResult = ListResult; - -export async function runLoopInspectCommand( - id: string, - options: LoopInspectOptions, - _command: Command, -): Promise { - const host = getDaemonHost({ host: options.host }); - let client; - try { - client = (await connectToDaemon({ - host: options.host, - })) as unknown as LoopDaemonClient; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw { - code: "DAEMON_NOT_RUNNING", - message: `Cannot connect to daemon at ${host}: ${message}`, - details: "Start the daemon with: paseo daemon start", - } satisfies CommandError; - } - - try { - const payload = await client.loopInspect(id); - await client.close(); - if (payload.error || !payload.loop) { - throw new Error(payload.error ?? `Loop not found: ${id}`); - } - return { - type: "list", - data: toRows(payload.loop), - schema: createInspectSchema(payload.loop), - }; - } catch (error) { - await client.close().catch(() => {}); - throw { - code: "LOOP_INSPECT_FAILED", - message: error instanceof Error ? error.message : String(error), - } satisfies CommandError; - } -} diff --git a/packages/cli/src/commands/loop/logs.ts b/packages/cli/src/commands/loop/logs.ts deleted file mode 100644 index 76a7726b74..0000000000 --- a/packages/cli/src/commands/loop/logs.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Command } from "commander"; -import { connectToDaemon, getDaemonHost } from "../../utils/client.js"; -import type { CommandError, CommandOptions } from "../../output/index.js"; -import type { LoopDaemonClient, LoopLogEntry } from "./types.js"; - -export interface LoopLogsOptions extends CommandOptions { - pollInterval?: string; -} - -export function addLoopLogsOptions(command: Command): Command { - return command - .description("Stream loop logs") - .argument("", "Loop ID") - .option("--poll-interval ", "Polling interval in milliseconds", "1000"); -} - -function parsePollInterval(value: string): number { - const parsed = Number.parseInt(value, 10); - if (!Number.isInteger(parsed) || parsed <= 0) { - throw { - code: "INVALID_POLL_INTERVAL", - message: "--poll-interval must be a positive integer", - } satisfies CommandError; - } - return parsed; -} - -function renderLogEntry(entry: LoopLogEntry): string { - const prefix = [ - entry.timestamp, - entry.source, - entry.iteration === null ? null : `iteration=${entry.iteration}`, - entry.level === "error" ? "ERROR" : null, - ] - .filter(Boolean) - .join(" "); - return `${prefix}\n${entry.text}`; -} - -export async function runLoopLogsCommand( - id: string, - options: LoopLogsOptions, - _command: Command, -): Promise { - const host = getDaemonHost({ host: options.host }); - const pollInterval = parsePollInterval(options.pollInterval ?? "1000"); - let client: LoopDaemonClient; - try { - client = (await connectToDaemon({ - host: options.host, - })) as unknown as LoopDaemonClient; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.error(`Error: Cannot connect to daemon at ${host}: ${message}`); - console.error("Start the daemon with: paseo daemon start"); - process.exit(1); - } - - async function streamLogs(cursor: number): Promise { - const payload = await client.loopLogs(id, cursor); - if (payload.error || !payload.loop) { - throw new Error(payload.error ?? `Loop not found: ${id}`); - } - for (const entry of payload.entries) { - console.log(renderLogEntry(entry)); - } - if (payload.loop.status !== "running") { - await client.close(); - return; - } - await new Promise((resolve) => setTimeout(resolve, pollInterval)); - return streamLogs(payload.nextCursor); - } - - try { - await streamLogs(0); - } catch (error) { - await client.close().catch(() => {}); - const message = error instanceof Error ? error.message : String(error); - console.error(`Error: Failed to stream loop logs: ${message}`); - process.exit(1); - } -} diff --git a/packages/cli/src/commands/loop/ls.ts b/packages/cli/src/commands/loop/ls.ts deleted file mode 100644 index 514fcdfdcc..0000000000 --- a/packages/cli/src/commands/loop/ls.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Command } from "commander"; -import { connectToDaemon, getDaemonHost } from "../../utils/client.js"; -import type { CommandOptions, CommandError, OutputSchema, ListResult } from "../../output/index.js"; -import type { LoopDaemonClient, LoopListItem } from "./types.js"; - -interface LoopListRow { - id: string; - name: string | null; - status: string; - cwd: string; - updated: string; - activeIteration: string; -} - -export interface LoopLsOptions extends CommandOptions {} - -export const loopLsSchema: OutputSchema = { - idField: "id", - columns: [ - { header: "LOOP ID", field: "id", width: 10 }, - { header: "NAME", field: "name", width: 20 }, - { header: "STATUS", field: "status", width: 10 }, - { header: "ITER", field: "activeIteration", width: 8 }, - { header: "CWD", field: "cwd", width: 40 }, - { header: "UPDATED", field: "updated", width: 24 }, - ], -}; - -export function addLoopLsOptions(command: Command): Command { - return command.description("List loops"); -} - -function toRow(loop: LoopListItem): LoopListRow { - return { - id: loop.id, - name: loop.name, - status: loop.status, - cwd: loop.cwd, - updated: loop.updatedAt, - activeIteration: loop.activeIteration === null ? "-" : String(loop.activeIteration), - }; -} - -export type LoopLsResult = ListResult; - -export async function runLoopLsCommand( - options: LoopLsOptions, - _command: Command, -): Promise { - const host = getDaemonHost({ host: options.host }); - let client; - try { - client = (await connectToDaemon({ - host: options.host, - })) as unknown as LoopDaemonClient; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw { - code: "DAEMON_NOT_RUNNING", - message: `Cannot connect to daemon at ${host}: ${message}`, - details: "Start the daemon with: paseo daemon start", - } satisfies CommandError; - } - - try { - const payload = await client.loopList(); - await client.close(); - if (payload.error) { - throw new Error(payload.error); - } - return { - type: "list", - data: payload.loops.map(toRow), - schema: loopLsSchema, - }; - } catch (error) { - await client.close().catch(() => {}); - throw { - code: "LOOP_LIST_FAILED", - message: error instanceof Error ? error.message : String(error), - } satisfies CommandError; - } -} diff --git a/packages/cli/src/commands/loop/run.test.ts b/packages/cli/src/commands/loop/run.test.ts deleted file mode 100644 index 5a2368924e..0000000000 --- a/packages/cli/src/commands/loop/run.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { buildLoopRunInput } from "./run.js"; -import type { LoopRunOptions } from "./types.js"; - -describe("buildLoopRunInput", () => { - it("should parse provider/model from provider string", () => { - const options: LoopRunOptions = { - provider: "opencode/hy3-preview-free", - model: undefined, - verifyProvider: undefined, - verifyModel: undefined, - verify: undefined, - verifyCheck: [], - archive: false, - name: undefined, - sleep: undefined, - maxIterations: undefined, - maxTime: undefined, - } as LoopRunOptions; - - const result = buildLoopRunInput("test prompt", options); - - expect(result.provider).toBe("opencode"); - expect(result.model).toBe("hy3-preview-free"); - }); - - it("should use provider only when no model in string", () => { - const options = { - provider: "opencode", - model: undefined, - verifyProvider: undefined, - verifyModel: undefined, - verify: undefined, - verifyCheck: [], - archive: false, - name: undefined, - sleep: undefined, - maxIterations: undefined, - maxTime: undefined, - } as LoopRunOptions; - - const result = buildLoopRunInput("test prompt", options); - - expect(result.provider).toBe("opencode"); - expect(result.model).toBeUndefined(); - }); - - it("should pass verifierProvider with model parsing", () => { - const options = { - provider: undefined, - model: undefined, - verifyProvider: "codex/gpt-5.4", - verifyModel: undefined, - verify: undefined, - verifyCheck: [], - archive: false, - name: undefined, - sleep: undefined, - maxIterations: undefined, - maxTime: undefined, - } as LoopRunOptions; - - const result = buildLoopRunInput("test prompt", options); - - expect(result.verifierProvider).toBe("codex"); - expect(result.verifierModel).toBe("gpt-5.4"); - }); - - it("should prefer explicit model over parsed model", () => { - const options = { - provider: "opencode/hy3-preview-free", - model: "other-model", - verifyProvider: undefined, - verifyModel: undefined, - verify: undefined, - verifyCheck: [], - archive: false, - name: undefined, - sleep: undefined, - maxIterations: undefined, - maxTime: undefined, - } as LoopRunOptions; - - const result = buildLoopRunInput("test prompt", options); - - expect(result.provider).toBe("opencode"); - expect(result.model).toBe("other-model"); - }); -}); diff --git a/packages/cli/src/commands/loop/run.ts b/packages/cli/src/commands/loop/run.ts deleted file mode 100644 index fc8c25c54b..0000000000 --- a/packages/cli/src/commands/loop/run.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { Command } from "commander"; -import { connectToDaemon, getDaemonHost } from "../../utils/client.js"; -import type { - CommandOptions, - CommandError, - OutputSchema, - SingleResult, -} from "../../output/index.js"; -import { collectMultiple } from "../../utils/command-options.js"; -import { parseDuration } from "../../utils/duration.js"; -import { resolveProviderAndModel } from "../../utils/provider-model.js"; -import type { LoopDaemonClient, LoopRecord, LoopRunInput } from "./types.js"; - -export interface LoopRunRow { - id: string; - status: string; - name: string | null; - cwd: string; -} - -export interface LoopRunOptions extends CommandOptions { - provider?: string; - model?: string; - mode?: string; - verifyProvider?: string; - verifyModel?: string; - verifyMode?: string; - verify?: string; - verifyCheck?: string[]; - archive?: boolean; - name?: string; - sleep?: string; - maxIterations?: string; - maxTime?: string; -} - -export const loopRunSchema: OutputSchema = { - idField: "id", - columns: [ - { header: "LOOP ID", field: "id", width: 10 }, - { header: "STATUS", field: "status", width: 10 }, - { header: "NAME", field: "name", width: 20 }, - { header: "CWD", field: "cwd", width: 40 }, - ], -}; - -export function addLoopRunOptions(command: Command): Command { - return command - .description("Start a loop") - .argument("", "Prompt for each fresh worker iteration") - .option("--provider ", "Default provider for worker and verifier agents") - .option("--model ", "Default model for worker and verifier agents") - .option( - "--mode ", - "Provider-specific mode for the worker agent (e.g. claude bypassPermissions, opencode build)", - ) - .option("--verify-provider ", "Provider for the verifier agent") - .option("--verify-model ", "Model for the verifier agent") - .option("--verify-mode ", "Provider-specific mode for the verifier agent") - .option("--verify ", "Verifier agent prompt") - .option( - "--verify-check ", - "Shell command that must exit 0 (repeatable)", - collectMultiple, - [], - ) - .option("--archive", "Archive worker and verifier agents after each iteration") - .option("--name ", "Optional loop name") - .option("--sleep ", "Delay between iterations (for example: 30s, 5m)") - .option("--max-iterations ", "Maximum number of iterations") - .option("--max-time ", "Maximum total runtime (for example: 1h, 30m)"); -} - -function toRow(loop: LoopRecord): LoopRunRow { - return { - id: loop.id, - status: loop.status, - name: loop.name, - cwd: loop.cwd, - }; -} - -function parseMaxIterations(value: string | undefined): number | undefined { - if (value === undefined) { - return undefined; - } - const parsed = Number.parseInt(value, 10); - if (!Number.isInteger(parsed) || parsed <= 0) { - throw { - code: "INVALID_MAX_ITERATIONS", - message: "--max-iterations must be a positive integer", - } satisfies CommandError; - } - return parsed; -} - -// oxlint-disable complexity -export function buildLoopRunInput(prompt: string, options: LoopRunOptions): LoopRunInput { - const verifyPrompt = options.verify?.trim(); - if (options.verify !== undefined && !verifyPrompt) { - throw { - code: "INVALID_VERIFY_PROMPT", - message: "--verify cannot be empty", - } satisfies CommandError; - } - - const result: LoopRunInput = { - prompt, - cwd: process.cwd(), - }; - - // Resolve provider/model - if (options.provider) { - const { provider, model } = resolveProviderAndModel({ provider: options.provider }); - if (provider) result.provider = provider; - // Explicit --model takes precedence over parsed model - if (options.model?.trim()) { - result.model = options.model.trim(); - } else if (model) { - result.model = model; - } - } else if (options.model?.trim()) { - result.model = options.model.trim(); - } - - if (options.mode?.trim()) { - result.modeId = options.mode.trim(); - } - - // Resolve verifier provider/model - if (options.verifyProvider) { - const { provider, model } = resolveProviderAndModel({ provider: options.verifyProvider }); - if (provider) result.verifierProvider = provider; - // Explicit --verify-model takes precedence over parsed model - if (options.verifyModel?.trim()) { - result.verifierModel = options.verifyModel.trim(); - } else if (model) { - result.verifierModel = model; - } - } else if (options.verifyModel?.trim()) { - result.verifierModel = options.verifyModel.trim(); - } - - if (options.verifyMode?.trim()) { - result.verifierModeId = options.verifyMode.trim(); - } - - if (verifyPrompt) result.verifyPrompt = verifyPrompt; - if (options.verifyCheck && options.verifyCheck.length > 0) { - result.verifyChecks = options.verifyCheck; - } - if (options.archive) result.archive = true; - if (options.name?.trim()) result.name = options.name.trim(); - if (options.sleep) result.sleepMs = parseDuration(options.sleep); - if (options.maxIterations) { - result.maxIterations = parseMaxIterations(options.maxIterations); - } - if (options.maxTime) result.maxTimeMs = parseDuration(options.maxTime); - - return result; -} -// oxlint-enable complexity - -export type LoopRunResult = SingleResult; - -export async function runLoopRunCommand( - prompt: string, - options: LoopRunOptions, - _command: Command, -): Promise { - const host = getDaemonHost({ host: options.host }); - const input = buildLoopRunInput(prompt, options); - let client; - try { - client = (await connectToDaemon({ - host: options.host, - })) as unknown as LoopDaemonClient; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw { - code: "DAEMON_NOT_RUNNING", - message: `Cannot connect to daemon at ${host}: ${message}`, - details: "Start the daemon with: paseo daemon start", - } satisfies CommandError; - } - - try { - const payload = await client.loopRun(input); - await client.close(); - if (payload.error || !payload.loop) { - throw new Error(payload.error ?? "Loop creation failed"); - } - return { - type: "single", - data: toRow(payload.loop), - schema: loopRunSchema, - }; - } catch (error) { - await client.close().catch(() => {}); - if (error && typeof error === "object" && "code" in error) { - throw error; - } - throw { - code: "LOOP_RUN_FAILED", - message: error instanceof Error ? error.message : String(error), - } satisfies CommandError; - } -} diff --git a/packages/cli/src/commands/loop/stop.ts b/packages/cli/src/commands/loop/stop.ts deleted file mode 100644 index 1207559ced..0000000000 --- a/packages/cli/src/commands/loop/stop.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Command } from "commander"; -import { connectToDaemon, getDaemonHost } from "../../utils/client.js"; -import type { - CommandOptions, - CommandError, - OutputSchema, - SingleResult, -} from "../../output/index.js"; -import type { LoopDaemonClient, LoopRecord } from "./types.js"; - -interface LoopStopRow { - id: string; - status: string; - activeIteration: string; -} - -export interface LoopStopOptions extends CommandOptions {} - -export const loopStopSchema: OutputSchema = { - idField: "id", - columns: [ - { header: "LOOP ID", field: "id", width: 10 }, - { header: "STATUS", field: "status", width: 10 }, - { header: "ITER", field: "activeIteration", width: 8 }, - ], -}; - -export function addLoopStopOptions(command: Command): Command { - return command.description("Stop a running loop").argument("", "Loop ID"); -} - -function toRow(loop: LoopRecord): LoopStopRow { - return { - id: loop.id, - status: loop.status, - activeIteration: loop.activeIteration === null ? "-" : String(loop.activeIteration), - }; -} - -export type LoopStopResult = SingleResult; - -export async function runLoopStopCommand( - id: string, - options: LoopStopOptions, - _command: Command, -): Promise { - const host = getDaemonHost({ host: options.host }); - let client; - try { - client = (await connectToDaemon({ - host: options.host, - })) as unknown as LoopDaemonClient; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - throw { - code: "DAEMON_NOT_RUNNING", - message: `Cannot connect to daemon at ${host}: ${message}`, - details: "Start the daemon with: paseo daemon start", - } satisfies CommandError; - } - - try { - const payload = await client.loopStop(id); - await client.close(); - if (payload.error || !payload.loop) { - throw new Error(payload.error ?? `Loop not found: ${id}`); - } - return { - type: "single", - data: toRow(payload.loop), - schema: loopStopSchema, - }; - } catch (error) { - await client.close().catch(() => {}); - throw { - code: "LOOP_STOP_FAILED", - message: error instanceof Error ? error.message : String(error), - } satisfies CommandError; - } -} diff --git a/packages/cli/src/commands/loop/types.ts b/packages/cli/src/commands/loop/types.ts deleted file mode 100644 index 66db601dcd..0000000000 --- a/packages/cli/src/commands/loop/types.ts +++ /dev/null @@ -1,151 +0,0 @@ -export type LoopStatus = "running" | "succeeded" | "failed" | "stopped"; - -export interface LoopLogEntry { - seq: number; - timestamp: string; - iteration: number | null; - source: "loop" | "worker" | "verifier" | "verify-check"; - level: "info" | "error"; - text: string; -} - -export interface LoopVerifyCheckResult { - command: string; - exitCode: number; - passed: boolean; - stdout: string; - stderr: string; - startedAt: string; - completedAt: string; -} - -export interface LoopVerifyPromptResult { - passed: boolean; - reason: string; - verifierAgentId: string | null; - startedAt: string; - completedAt: string; -} - -export interface LoopIterationRecord { - index: number; - workerAgentId: string | null; - workerStartedAt: string; - workerCompletedAt: string | null; - verifierAgentId: string | null; - status: "running" | "succeeded" | "failed" | "stopped"; - workerOutcome: "completed" | "failed" | "canceled" | null; - failureReason: string | null; - verifyChecks: LoopVerifyCheckResult[]; - verifyPrompt: LoopVerifyPromptResult | null; -} - -export interface LoopRecord { - id: string; - name: string | null; - prompt: string; - cwd: string; - provider: string; - model: string | null; - modeId: string | null; - workerProvider: string | null; - workerModel: string | null; - verifierProvider: string | null; - verifierModel: string | null; - verifierModeId: string | null; - verifyPrompt: string | null; - verifyChecks: string[]; - archive: boolean; - sleepMs: number; - maxIterations: number | null; - maxTimeMs: number | null; - status: LoopStatus; - createdAt: string; - updatedAt: string; - startedAt: string; - completedAt: string | null; - stopRequestedAt: string | null; - iterations: LoopIterationRecord[]; - logs: LoopLogEntry[]; - nextLogSeq: number; - activeIteration: number | null; - activeWorkerAgentId: string | null; - activeVerifierAgentId: string | null; -} - -export interface LoopListItem { - id: string; - name: string | null; - status: LoopStatus; - cwd: string; - createdAt: string; - updatedAt: string; - activeIteration: number | null; -} - -export interface LoopLogsResult { - loop: LoopRecord; - entries: LoopLogEntry[]; - nextCursor: number; -} - -export interface LoopRunPayload { - requestId: string; - loop: LoopRecord | null; - error: string | null; -} - -export interface LoopListPayload { - requestId: string; - loops: LoopListItem[]; - error: string | null; -} - -export interface LoopInspectPayload { - requestId: string; - loop: LoopRecord | null; - error: string | null; -} - -export interface LoopLogsPayload { - requestId: string; - loop: LoopRecord | null; - entries: LoopLogEntry[]; - nextCursor: number; - error: string | null; -} - -export interface LoopStopPayload { - requestId: string; - loop: LoopRecord | null; - error: string | null; -} - -export interface LoopRunInput { - prompt: string; - cwd: string; - provider?: string; - model?: string; - modeId?: string; - workerProvider?: string; - workerModel?: string; - verifierProvider?: string; - verifierModel?: string; - verifierModeId?: string; - verifyPrompt?: string; - verifyChecks?: string[]; - archive?: boolean; - name?: string; - sleepMs?: number; - maxIterations?: number; - maxTimeMs?: number; -} - -export interface LoopDaemonClient { - loopRun(input: LoopRunInput): Promise; - loopList(): Promise; - loopInspect(id: string): Promise; - loopLogs(id: string, afterSeq?: number): Promise; - loopStop(id: string): Promise; - close(): Promise; -} diff --git a/packages/cli/src/commands/provider/diagnostic.ts b/packages/cli/src/commands/provider/diagnostic.ts new file mode 100644 index 0000000000..0ff05556a1 --- /dev/null +++ b/packages/cli/src/commands/provider/diagnostic.ts @@ -0,0 +1,45 @@ +import type { Command } from "commander"; +import { connectToDaemon } from "../../utils/client.js"; +import type { CommandOptions, OutputSchema, SingleResult } from "../../output/index.js"; + +export interface ProviderDiagnosticResult { + provider: string; + diagnostic: string; +} + +export const providerDiagnosticSchema: OutputSchema = { + idField: "provider", + columns: [ + { header: "PROVIDER", field: "provider" }, + { header: "DIAGNOSTIC", field: "diagnostic" }, + ], + renderHuman(result) { + if (result.type === "single") return result.data.diagnostic; + return result.data.map((entry) => entry.diagnostic).join("\n\n"); + }, +}; + +export interface ProviderDiagnosticOptions extends CommandOptions { + host?: string; +} + +export async function runDiagnosticCommand( + provider: string, + options: ProviderDiagnosticOptions, + _command: Command, +): Promise> { + const client = await connectToDaemon({ host: options.host }); + try { + const result = await client.getProviderDiagnostic(provider.trim().toLowerCase()); + return { + type: "single", + data: { + provider: result.provider, + diagnostic: result.diagnostic, + }, + schema: providerDiagnosticSchema, + }; + } finally { + await client.close(); + } +} diff --git a/packages/cli/src/commands/provider/index.ts b/packages/cli/src/commands/provider/index.ts index acd513fa0b..1e934940ee 100644 --- a/packages/cli/src/commands/provider/index.ts +++ b/packages/cli/src/commands/provider/index.ts @@ -1,6 +1,7 @@ import { Command } from "commander"; import { runLsCommand } from "./ls.js"; import { runModelsCommand } from "./models.js"; +import { runDiagnosticCommand } from "./diagnostic.js"; import { withOutput } from "../../output/index.js"; import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js"; @@ -19,5 +20,12 @@ export function createProviderCommand(): Command { .option("--thinking", "Include thinking option IDs for each model"), ).action(withOutput(runModelsCommand)); + addJsonAndDaemonHostOptions( + provider + .command("diagnostic") + .description("Show provider installation, environment, and availability diagnostics") + .argument("", "Provider name"), + ).action(withOutput(runDiagnosticCommand)); + return provider; } diff --git a/packages/cli/src/commands/workspace/index.ts b/packages/cli/src/commands/workspace/index.ts index 27d7131dce..bf143fa137 100644 --- a/packages/cli/src/commands/workspace/index.ts +++ b/packages/cli/src/commands/workspace/index.ts @@ -4,6 +4,7 @@ import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js"; import { runArchiveCommand } from "./archive.js"; import { runCreateCommand } from "./create.js"; import { runLsCommand } from "./ls.js"; +import { runRenameCommand } from "./rename.js"; export function createWorkspaceCommand(): Command { const workspace = new Command("workspace").description("Manage workspaces"); @@ -32,6 +33,18 @@ export function createWorkspaceCommand(): Command { withOutput(runLsCommand), ); + addJsonAndDaemonHostOptions( + workspace + .command("rename") + .description("Set a workspace's user-visible title") + .argument("", "Workspace id") + .argument("[title]", "New workspace title") + .option("--reset", "Clear the title and revert to the branch or directory name") + // Commander 12 accepts excess positionals by default, which would rename to + // the first word of an unquoted multi-word title and silently drop the rest. + .allowExcessArguments(false), + ).action(withOutput(runRenameCommand)); + addJsonAndDaemonHostOptions( workspace .command("archive") diff --git a/packages/cli/src/commands/workspace/rename.test.ts b/packages/cli/src/commands/workspace/rename.test.ts new file mode 100644 index 0000000000..587a4810bd --- /dev/null +++ b/packages/cli/src/commands/workspace/rename.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { createWorkspaceCommand } from "./index.js"; +import { resolveWorkspaceTitle } from "./rename.js"; + +function catchError(run: () => unknown): unknown { + try { + run(); + return null; + } catch (error) { + return error; + } +} + +describe("workspace rename title", () => { + it("trims the requested title", () => { + expect(resolveWorkspaceTitle({ title: " Fix login " })).toBe("Fix login"); + }); + + it("clears the title when resetting to the derived name", () => { + expect(resolveWorkspaceTitle({ reset: true })).toBeNull(); + }); + + it("rejects a reset that also carries a title", () => { + expect( + catchError(() => resolveWorkspaceTitle({ title: "Fix login", reset: true })), + ).toMatchObject({ code: "INVALID_OPTIONS" }); + // An explicit empty title is still a title the caller passed, not a reset. + expect(catchError(() => resolveWorkspaceTitle({ title: "", reset: true }))).toMatchObject({ + code: "INVALID_OPTIONS", + }); + }); + + it("requires a non-empty title when not resetting", () => { + expect(catchError(() => resolveWorkspaceTitle({}))).toMatchObject({ code: "MISSING_TITLE" }); + expect(catchError(() => resolveWorkspaceTitle({ title: " " }))).toMatchObject({ + code: "MISSING_TITLE", + }); + }); +}); + +describe("workspace rename arguments", () => { + function parseRename(argv: string[]): unknown { + const workspace = createWorkspaceCommand() + .exitOverride() + .configureOutput({ writeErr: () => undefined }); + workspace.commands.find((command) => command.name() === "rename")?.exitOverride(); + return catchError(() => workspace.parse(argv, { from: "user" })); + } + + it("rejects an unquoted multi-word title instead of dropping words", () => { + expect(parseRename(["rename", "ws-1", "Auth", "rework"])).toMatchObject({ + code: "commander.excessArguments", + }); + }); +}); diff --git a/packages/cli/src/commands/workspace/rename.ts b/packages/cli/src/commands/workspace/rename.ts new file mode 100644 index 0000000000..fd5857682b --- /dev/null +++ b/packages/cli/src/commands/workspace/rename.ts @@ -0,0 +1,80 @@ +import type { Command } from "commander"; +import { connectToDaemon, getDaemonHost } from "../../utils/client.js"; +import type { CommandError, OutputSchema, SingleResult } from "../../output/index.js"; + +interface WorkspaceRenameResult { + workspaceId: string; + /** Raw title override; null once it has been reset to the derived name. */ + title: string | null; +} + +const workspaceRenameSchema: OutputSchema = { + idField: "workspaceId", + columns: [ + { header: "WORKSPACE ID", field: "workspaceId", width: 20 }, + { header: "TITLE", field: "title", width: 30 }, + ], +}; + +export interface WorkspaceRenameOptions { + host?: string; + reset?: boolean; +} + +/** + * Resolve the title to send to the daemon. Null clears the override and + * reverts the workspace to its branch- or directory-derived name. + */ +export function resolveWorkspaceTitle(input: { title?: string; reset?: boolean }): string | null { + const title = input.title?.trim() ?? ""; + if (input.reset) { + if (input.title !== undefined) { + throw { + code: "INVALID_OPTIONS", + message: "--reset cannot be combined with a title", + details: "Pass a title to rename the workspace, or --reset to revert to the derived name", + } satisfies CommandError; + } + return null; + } + if (title.length === 0) { + throw { + code: "MISSING_TITLE", + message: "Title cannot be empty", + details: "Usage: paseo workspace rename | --reset", + } satisfies CommandError; + } + return title; +} + +export async function runRenameCommand( + workspaceId: string, + titleArg: string | undefined, + options: WorkspaceRenameOptions, + _command: Command, +): Promise<SingleResult<WorkspaceRenameResult>> { + const title = resolveWorkspaceTitle({ title: titleArg, reset: options.reset }); + + const host = getDaemonHost({ host: options.host }); + const client = await connectToDaemon({ host: options.host }).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + throw { + code: "DAEMON_NOT_RUNNING", + message: `Cannot connect to daemon at ${host}: ${message}`, + details: "Start the daemon with: paseo daemon start", + } satisfies CommandError; + }); + try { + const applied = await client.setWorkspaceTitle(workspaceId, title); + return { + type: "single", + data: { workspaceId, title: applied.title }, + schema: workspaceRenameSchema, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw { code: "WORKSPACE_RENAME_FAILED", message } satisfies CommandError; + } finally { + await client.close().catch(() => undefined); + } +} diff --git a/packages/cli/tests/03-daemon.test.ts b/packages/cli/tests/03-daemon.test.ts index b5401fb5ba..dcafbb8e83 100644 --- a/packages/cli/tests/03-daemon.test.ts +++ b/packages/cli/tests/03-daemon.test.ts @@ -16,9 +16,13 @@ */ import assert from "node:assert"; -import { mkdtemp, readFile, rm, unlink, writeFile } from "fs/promises"; +import { spawn, type ChildProcess } from "node:child_process"; +import { once } from "node:events"; +import { existsSync, readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { mkdtemp, readFile, rm, writeFile } from "fs/promises"; import { tmpdir } from "os"; -import { join } from "path"; +import { dirname, join } from "path"; import { runLocalPaseo } from "./helpers/local-cli.ts"; console.log("=== Daemon Commands ===\n"); @@ -26,11 +30,82 @@ console.log("=== Daemon Commands ===\n"); // Keep restart off default 6767 to avoid collisions with any existing daemon. const port = 10000 + Math.floor(Math.random() * 50000); const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-")); +const require = createRequire(import.meta.url); function daemonCommand(args: string[]) { return runLocalPaseo(["daemon", ...args], { PASEO_HOME: paseoHome }); } +async function stopChildProcess(child: ChildProcess): Promise<void> { + if (child.exitCode !== null || child.signalCode !== null) return; + + const exited = once(child, "exit"); + child.kill("SIGTERM"); + const forceKill = setTimeout(() => child.kill("SIGKILL"), 5_000); + try { + await exited; + } finally { + clearTimeout(forceKill); + } +} + +function resolveDaemonWorkerEntry(): string { + let currentDir = dirname(require.resolve("@getpaseo/server")); + + while (true) { + const packageJsonPath = join(currentDir, "package.json"); + if (existsSync(packageJsonPath)) { + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); + if (packageJson.name === "@getpaseo/server") { + const candidates = [ + join(currentDir, "dist", "server", "server", "daemon-worker.js"), + join(currentDir, "src", "server", "daemon-worker.ts"), + ]; + const workerEntry = candidates.find(existsSync); + if (workerEntry) return workerEntry; + throw new Error(`Daemon worker not found under ${currentDir}`); + } + } + + const parentDir = dirname(currentDir); + if (parentDir === currentDir) break; + currentDir = parentDir; + } + + throw new Error("Unable to resolve @getpaseo/server package root"); +} + +async function tailDaemonLog(): Promise<string> { + try { + const log = await readFile(join(paseoHome, "daemon.log"), "utf-8"); + return log.split("\n").slice(-30).join("\n"); + } catch { + return "<daemon log unavailable>"; + } +} + +async function retryWhileWorkerRuns<T>( + worker: ChildProcess, + probe: () => Promise<T>, + accepts: (result: T) => boolean, + failureDetails: (result: T) => string | Promise<string>, +): Promise<T> { + const deadline = Date.now() + 120_000; + let result = await probe(); + + while (!accepts(result)) { + assert( + worker.exitCode === null && worker.signalCode === null, + "IPC daemon exited before the probe completed", + ); + assert(Date.now() < deadline, await failureDetails(result)); + await new Promise((resolve) => setTimeout(resolve, 100)); + result = await probe(); + } + + return result; +} + try { // Test 1: daemon --help shows subcommands { @@ -132,56 +207,110 @@ try { process.platform === "win32" ? `\\\\.\\pipe\\paseo-status-${process.pid}-${Date.now()}` : join(paseoHome, "status.sock"); - const start = await daemonCommand(["start", "--listen", listen, "--relay"]); - assert.strictEqual(start.exitCode, 0, `IPC daemon should start: ${start.stderr}`); - const configPath = join(paseoHome, "config.json"); const config = JSON.parse(await readFile(configPath, "utf-8")); - config.daemon = { ...config.daemon, listen }; + config.daemon = { + ...config.daemon, + listen, + relay: { ...config.daemon?.relay, enabled: false }, + }; await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8"); - const pidPath = join(paseoHome, "paseo.pid"); - const pidContents = await readFile(pidPath, "utf-8"); - await unlink(pidPath); - const status = await daemonCommand(["status", "--json"]); - const pairing = await daemonCommand(["pair", "--json"]); - await writeFile(pidPath, pidContents, "utf-8"); - assert.strictEqual(status.exitCode, 0, `IPC daemon status should succeed: ${status.stderr}`); - const payload = JSON.parse(status.stdout); - assert.strictEqual(payload.connectedDaemon, "reachable", "IPC daemon should be reachable"); - assert.strictEqual(payload.localDaemon, "stopped", "missing PID should report stopped locally"); - assert.notStrictEqual(payload.relay, "disabled", "status should use live relay state"); - assert.strictEqual(pairing.exitCode, 0, `IPC daemon pairing should succeed: ${pairing.stderr}`); - const pairingPayload = JSON.parse(pairing.stdout); - assert.strictEqual(pairingPayload.relayEnabled, true, "pairing should use live relay state"); - assert.match(pairingPayload.url, /#offer=/, "pairing should use the live daemon offer"); + // A supervised daemon owns and heartbeats paseo.pid. Launch the worker + // directly so this fixture naturally has a reachable daemon without a PID file. + const workerEntry = resolveDaemonWorkerEntry(); + const workerArgs = workerEntry.endsWith(".ts") + ? ["--import", "tsx", workerEntry, "--relay"] + : [workerEntry, "--relay"]; + const worker = spawn(process.execPath, workerArgs, { + cwd: join(import.meta.dirname, ".."), + env: { + ...process.env, + PASEO_HOME: paseoHome, + PASEO_LISTEN: listen, + PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: "0", + PASEO_DICTATION_ENABLED: "0", + PASEO_VOICE_MODE_ENABLED: "0", + CI: "true", + }, + stdio: "ignore", + }); - const foreignHome = await mkdtemp(join(tmpdir(), "paseo-test-foreign-home-")); try { - await writeFile( - join(foreignHome, "config.json"), - `${JSON.stringify({ daemon: { listen } }, null, 2)}\n`, - "utf-8", + const relayProbe = await retryWhileWorkerRuns( + worker, + async () => { + const statusResult = await daemonCommand(["status", "--json"]); + const statusPayload = + statusResult.exitCode === 0 ? JSON.parse(statusResult.stdout) : null; + const pairingResult = await daemonCommand(["pair", "--json"]); + return { statusResult, statusPayload, pairingResult }; + }, + (result) => + result.statusPayload?.connectedDaemon === "reachable" && + result.statusPayload.relay !== "disabled" && + result.pairingResult.exitCode === 0, + async (result) => + [ + "IPC daemon did not expose live relay state", + `status: ${result.statusResult.stdout || result.statusResult.stderr}`, + `pairing: ${result.pairingResult.stdout || result.pairingResult.stderr}`, + `daemon log:\n${await tailDaemonLog()}`, + ].join("\n"), ); - const foreignPairing = await runLocalPaseo( - ["daemon", "pair", "--home", foreignHome, "--json"], - { PASEO_HOME: foreignHome }, - ); - assert.notStrictEqual( - foreignPairing.exitCode, - 0, - "pairing should reject a daemon owned by another home", - ); - assert( - foreignPairing.stderr.includes("different Paseo home"), - "pairing should explain the daemon identity mismatch", - ); - assert(!foreignPairing.stdout.includes("#offer="), "pairing should not expose another offer"); - } finally { - await rm(foreignHome, { recursive: true, force: true }); - } + const { statusResult: status, statusPayload: payload, pairingResult: pairing } = relayProbe; - const cleanup = await daemonCommand(["stop", "--force"]); - assert.strictEqual(cleanup.exitCode, 0, "cleanup stop should succeed after IPC status"); + assert.strictEqual(status.exitCode, 0, `IPC daemon status should succeed: ${status.stderr}`); + assert.strictEqual(payload.connectedDaemon, "reachable", "IPC daemon should be reachable"); + assert.strictEqual( + payload.localDaemon, + "stopped", + "missing PID should report stopped locally", + ); + assert.notStrictEqual(payload.relay, "disabled", "status should use live relay state"); + assert.strictEqual( + pairing.exitCode, + 0, + `IPC daemon pairing should succeed: ${pairing.stderr}`, + ); + const pairingPayload = JSON.parse(pairing.stdout); + assert.strictEqual(pairingPayload.relayEnabled, true, "pairing should use live relay state"); + assert.match(pairingPayload.url, /#offer=/, "pairing should use the live daemon offer"); + + const foreignHome = await mkdtemp(join(tmpdir(), "paseo-test-foreign-home-")); + try { + await writeFile( + join(foreignHome, "config.json"), + `${JSON.stringify({ daemon: { listen } }, null, 2)}\n`, + "utf-8", + ); + const foreignPairing = await retryWhileWorkerRuns( + worker, + () => + runLocalPaseo(["daemon", "pair", "--home", foreignHome, "--json"], { + PASEO_HOME: foreignHome, + }), + (result) => result.stderr.includes("different Paseo home"), + (result) => `Pairing did not report the daemon identity mismatch: ${result.stderr}`, + ); + assert.notStrictEqual( + foreignPairing.exitCode, + 0, + "pairing should reject a daemon owned by another home", + ); + assert( + foreignPairing.stderr.includes("different Paseo home"), + "pairing should explain the daemon identity mismatch", + ); + assert( + !foreignPairing.stdout.includes("#offer="), + "pairing should not expose another offer", + ); + } finally { + await rm(foreignHome, { recursive: true, force: true }); + } + } finally { + await stopChildProcess(worker); + } console.log("✓ daemon status probes live relay state over local IPC\n"); } diff --git a/packages/cli/tests/15-provider.test.ts b/packages/cli/tests/15-provider.test.ts index 3dba994fe6..ee2eaaf298 100644 --- a/packages/cli/tests/15-provider.test.ts +++ b/packages/cli/tests/15-provider.test.ts @@ -17,6 +17,8 @@ * - provider models opencode lists opencode models * - provider models unknown fails with error * - provider models --json outputs valid JSON + * - provider diagnostic shows the daemon's provider diagnostic + * - provider diagnostic --json returns structured output */ import assert from "node:assert"; @@ -44,6 +46,11 @@ interface ProviderListRow { enabled: string; } +interface ProviderDiagnostic { + provider: string; + diagnostic: string; +} + const EXPECTED_CLAUDE_MODELS = [ { id: "claude-opus-5", @@ -190,6 +197,7 @@ try { assert.strictEqual(result.exitCode, 0, "provider --help should exit 0"); assert(result.stdout.includes("ls"), "help should mention ls"); assert(result.stdout.includes("models"), "help should mention models"); + assert(result.stdout.includes("diagnostic"), "help should mention diagnostic"); console.log("✓ provider --help shows subcommands\n"); } @@ -424,6 +432,39 @@ try { ); console.log("✓ provider models --quiet outputs model IDs only\n"); } + + // Test 12: provider diagnostic shows the daemon's provider diagnostic + { + console.log("Test 12: provider diagnostic shows the daemon's provider diagnostic"); + const result = await ctx.paseo([ + "provider", + "diagnostic", + " Claude ", + "--host", + `127.0.0.1:${ctx.port}`, + ]); + assert.strictEqual(result.exitCode, 0, "provider diagnostic should exit 0"); + assert(result.stdout.includes("Claude Code"), "diagnostic should identify the provider"); + assert(result.stdout.includes("Daemon PATH:"), "diagnostic should include the daemon PATH"); + assert(result.stdout.includes("Resolved path:"), "diagnostic should include binary resolution"); + assert(result.stdout.includes("Version:"), "diagnostic should include the provider version"); + assert(result.stdout.includes("Status:"), "diagnostic should include provider status"); + console.log("✓ provider diagnostic shows the daemon's provider diagnostic\n"); + } + + // Test 13: provider diagnostic --json returns structured output + { + console.log("Test 13: provider diagnostic --json returns structured output"); + const result = await ctx.paseo(["provider", "diagnostic", "claude", "--json"]); + assert.strictEqual(result.exitCode, 0, "provider diagnostic --json should exit 0"); + const data = JSON.parse(result.stdout.trim()) as ProviderDiagnostic; + assert.strictEqual(data.provider, "claude", "JSON should identify the provider"); + assert(data.diagnostic.includes("Daemon PATH:"), "JSON should include the daemon PATH"); + assert(data.diagnostic.includes("Resolved path:"), "JSON should include binary resolution"); + assert(data.diagnostic.includes("Version:"), "JSON should include the provider version"); + assert(data.diagnostic.includes("Status:"), "JSON should include provider status"); + console.log("✓ provider diagnostic --json returns structured output\n"); + } } finally { await ctx.stop(); } diff --git a/packages/cli/tests/30-chat.test.ts b/packages/cli/tests/30-chat.test.ts deleted file mode 100644 index efcf2a4fc2..0000000000 --- a/packages/cli/tests/30-chat.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env npx tsx - -import assert from "node:assert"; -import { rm } from "node:fs/promises"; -import { createE2ETestContext } from "./helpers/test-daemon.ts"; - -console.log("=== Chat Command Tests ===\n"); - -const ctx = await createE2ETestContext({ timeout: 30000 }); - -try { - { - console.log("Test 1: chat create/ls/inspect work"); - const created = await ctx.paseo(["chat", "create", "coord-room", "--purpose", "Coordination"]); - assert.strictEqual(created.exitCode, 0, created.stderr); - assert(created.stdout.includes("coord-room"), created.stdout); - - const listed = await ctx.paseo(["chat", "ls"]); - assert.strictEqual(listed.exitCode, 0, listed.stderr); - assert(listed.stdout.includes("coord-room"), listed.stdout); - - const inspected = await ctx.paseo(["chat", "inspect", "coord-room"]); - assert.strictEqual(inspected.exitCode, 0, inspected.stderr); - assert(inspected.stdout.includes("Coordination"), inspected.stdout); - console.log("chat create/ls/inspect work\n"); - } - - { - console.log("Test 2: chat post/read/wait work"); - const posted = await ctx.paseo(["chat", "post", "coord-room", "first message for @agent-1"], { - env: { PASEO_AGENT_ID: "00000000-0000-4000-8000-000000000111" }, - }); - assert.strictEqual(posted.exitCode, 0, posted.stderr); - assert(posted.stdout.includes("first message"), posted.stdout); - assert(posted.stdout.includes("00000000-0000-4000-8000-000000000111"), posted.stdout); - - const read = await ctx.paseo(["chat", "read", "coord-room", "--limit", "10"]); - assert.strictEqual(read.exitCode, 0, read.stderr); - assert(read.stdout.includes("first message"), read.stdout); - assert(read.stdout.includes("00000000-0000-4000-8000-000000000111"), read.stdout); - - const readJson = await ctx.paseo(["chat", "read", "coord-room", "--limit", "10", "--json"]); - assert.strictEqual(readJson.exitCode, 0, readJson.stderr); - const readPayload = JSON.parse(readJson.stdout); - assert.strictEqual(readPayload[0]?.author, "00000000-0000-4000-8000-000000000111"); - - // `chat wait` reads "latest message id" then subscribes for newer ones. - // Under CI load the subprocess can take >1s to bootstrap, so a single - // delayed post races against the read. Post repeatedly and race against - // wait — every post is newer than the snapshot wait took, so one of them - // always wakes it up. - const waitPromise = ctx.paseo(["chat", "wait", "coord-room", "--timeout", "30s"]); - const waitSentinel = waitPromise.then(() => "settled" as const); - let postedSecond = false; - for (let attempt = 0; attempt < 60; attempt++) { - const post = await ctx.paseo(["chat", "post", "coord-room", "second message"]); - assert.strictEqual(post.exitCode, 0, post.stderr); - const tick = new Promise<"tick">((resolve) => setTimeout(() => resolve("tick"), 250)); - const result = await Promise.race([waitSentinel, tick]); - if (result === "settled") { - postedSecond = true; - break; - } - } - assert(postedSecond, "chat wait did not return after repeated posts"); - - const waited = await waitPromise; - assert.strictEqual(waited.exitCode, 0, waited.stderr); - assert(waited.stdout.includes("second message"), waited.stdout); - console.log("chat post/read/wait work\n"); - } - - { - console.log("Test 3: duplicate room create fails"); - const duplicate = await ctx.paseo(["chat", "create", "coord-room"]); - assert.notStrictEqual(duplicate.exitCode, 0, "duplicate create should fail"); - const combined = `${duplicate.stdout}\n${duplicate.stderr}`; - assert(combined.toLowerCase().includes("already exists"), combined); - console.log("duplicate room create fails\n"); - } - - { - console.log("Test 4: chat delete works"); - const deleted = await ctx.paseo(["chat", "delete", "coord-room"]); - assert.strictEqual(deleted.exitCode, 0, deleted.stderr); - assert(deleted.stdout.includes("coord-room"), deleted.stdout); - console.log("chat delete works\n"); - } -} finally { - await ctx.stop(); - await rm(ctx.paseoHome, { recursive: true, force: true }); - await rm(ctx.workDir, { recursive: true, force: true }); -} - -console.log("=== Chat Command Tests Passed ==="); diff --git a/packages/cli/tests/31-loop-schedule.test.ts b/packages/cli/tests/31-schedule.test.ts similarity index 66% rename from packages/cli/tests/31-loop-schedule.test.ts rename to packages/cli/tests/31-schedule.test.ts index 0120ffea68..b3d3c5ada5 100644 --- a/packages/cli/tests/31-loop-schedule.test.ts +++ b/packages/cli/tests/31-schedule.test.ts @@ -4,31 +4,7 @@ import assert from "node:assert"; import { rm } from "node:fs/promises"; import { createE2ETestContext } from "./helpers/test-daemon.ts"; -function sleep(ms: number): Promise<void> { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -async function waitForLoopInList( - ctx: Awaited<ReturnType<typeof createE2ETestContext>>, - id: string, -) { - for (let attempt = 0; attempt < 20; attempt++) { - const listed = await ctx.paseo(["loop", "ls", "--json"]); - assert.strictEqual(listed.exitCode, 0, listed.stderr); - const listedJson = JSON.parse(listed.stdout); - assert(Array.isArray(listedJson), listed.stdout); - if (listedJson.some((item: { id: string }) => item.id === id)) { - return listedJson; - } - await sleep(250); - } - - const listed = await ctx.paseo(["loop", "ls", "--json"]); - assert.strictEqual(listed.exitCode, 0, listed.stderr); - return JSON.parse(listed.stdout); -} - -console.log("=== Loop And Schedule Command Tests ===\n"); +console.log("=== Schedule Command Tests ===\n"); const ctx = await createE2ETestContext({ timeout: 30000 }); @@ -171,62 +147,10 @@ try { assert.strictEqual(JSON.parse(deleted.stdout).id, createdJson.id); console.log("compatibility agent-target schedules remain deletable\n"); } - - { - console.log("Test 2: loop run/ls/inspect/logs/stop work"); - const run = await ctx.paseo( - [ - "loop", - "run", - "Return any response", - "--name", - "smoke-loop", - "--verify-check", - "true", - "--json", - ], - { timeout: 30000 }, - ); - assert.strictEqual(run.exitCode, 0, run.stderr); - const runJson = JSON.parse(run.stdout); - assert.strictEqual(runJson.name, "smoke-loop"); - - const listedJson = await waitForLoopInList(ctx, runJson.id); - assert( - listedJson.some((item: { id: string }) => item.id === runJson.id), - JSON.stringify(listedJson), - ); - - async function pollStatus(attempt: number): Promise<string> { - if (attempt >= 40) return "running"; - const inspect = await ctx.paseo(["loop", "inspect", runJson.id, "--json"]); - assert.strictEqual(inspect.exitCode, 0, inspect.stderr); - const inspectJson = JSON.parse(inspect.stdout); - const current = inspectJson.status; - if (current !== "running") { - assert.strictEqual(current, "succeeded", inspect.stdout); - return current; - } - await sleep(250); - return pollStatus(attempt + 1); - } - const status = await pollStatus(0); - assert.strictEqual(status, "succeeded"); - - const logs = await ctx.paseo(["loop", "logs", runJson.id], { timeout: 15000 }); - assert.strictEqual(logs.exitCode, 0, logs.stderr); - assert(logs.stdout.includes("verify-check"), logs.stdout); - - const stopped = await ctx.paseo(["loop", "stop", runJson.id, "--json"]); - assert.strictEqual(stopped.exitCode, 0, stopped.stderr); - const stoppedJson = JSON.parse(stopped.stdout); - assert(["succeeded", "stopped"].includes(stoppedJson.status), stopped.stdout); - console.log("loop commands work\n"); - } } finally { await ctx.stop(); await rm(ctx.paseoHome, { recursive: true, force: true }); await rm(ctx.workDir, { recursive: true, force: true }); } -console.log("=== Loop And Schedule Command Tests Passed ==="); +console.log("=== Schedule Command Tests Passed ==="); diff --git a/packages/cli/tests/38-workspace-rename.test.ts b/packages/cli/tests/38-workspace-rename.test.ts new file mode 100644 index 0000000000..50d3b4ce82 --- /dev/null +++ b/packages/cli/tests/38-workspace-rename.test.ts @@ -0,0 +1,58 @@ +#!/usr/bin/env npx tsx + +import assert from "node:assert"; +import { basename } from "node:path"; +import { createE2ETestContext } from "./helpers/test-daemon.ts"; + +console.log("=== Workspace Rename Command ===\n"); + +const ctx = await createE2ETestContext({ timeout: 30000 }); + +try { + const created = await ctx.paseo([ + "workspace", + "create", + "--isolation", + "local", + "--path", + ctx.workDir, + "--title", + "Original title", + "--json", + ]); + assert.strictEqual(created.exitCode, 0, created.stderr); + const workspaceId = JSON.parse(created.stdout).workspaceId as string; + + const renamed = await ctx.paseo(["workspace", "rename", workspaceId, "Auth rework", "--json"]); + assert.strictEqual(renamed.exitCode, 0, renamed.stderr); + assert.deepStrictEqual(JSON.parse(renamed.stdout), { + workspaceId, + title: "Auth rework", + }); + + const renamedList = await ctx.paseo(["workspace", "ls", "--json"]); + assert.strictEqual(renamedList.exitCode, 0, renamedList.stderr); + assert.strictEqual( + JSON.parse(renamedList.stdout).find( + (workspace: { workspaceId: string }) => workspace.workspaceId === workspaceId, + )?.name, + "Auth rework", + ); + + const reset = await ctx.paseo(["workspace", "rename", workspaceId, "--reset", "--json"]); + assert.strictEqual(reset.exitCode, 0, reset.stderr); + assert.deepStrictEqual(JSON.parse(reset.stdout), { workspaceId, title: null }); + + const resetList = await ctx.paseo(["workspace", "ls", "--json"]); + assert.strictEqual(resetList.exitCode, 0, resetList.stderr); + assert.strictEqual( + JSON.parse(resetList.stdout).find( + (workspace: { workspaceId: string }) => workspace.workspaceId === workspaceId, + )?.name, + basename(ctx.workDir), + ); +} finally { + await ctx.stop(); +} + +console.log("=== Workspace Rename Command Tests Passed ==="); diff --git a/packages/client/package.json b/packages/client/package.json index e0b3fabb7e..ac42d7ce07 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/client", - "version": "0.3.1", + "version": "0.4.0", "description": "Paseo client SDK package", "files": [ "dist", @@ -39,8 +39,8 @@ "test": "vitest run" }, "dependencies": { - "@getpaseo/protocol": "0.3.1", - "@getpaseo/relay": "0.3.1", + "@getpaseo/protocol": "0.4.0", + "@getpaseo/relay": "0.4.0", "zod": "^4.4.3" }, "devDependencies": { diff --git a/packages/client/src/daemon-client.test.ts b/packages/client/src/daemon-client.test.ts index 43ef54b3b2..ce7328a116 100644 --- a/packages/client/src/daemon-client.test.ts +++ b/packages/client/src/daemon-client.test.ts @@ -967,6 +967,59 @@ test("keeps the transport connected when a session RPC ping times out", async () expect(client.getConnectionState().status).toBe("connected"); }); +test("waits for the daemon to acknowledge push token revocation", async () => { + const mock = createMockTransport(); + const client = new DaemonClient({ + url: "ws://test", + clientId: "clsk_push_revocation", + reconnect: { enabled: false }, + transportFactory: () => mock.transport, + }); + clients.push(client); + const connectPromise = client.connect(); + mock.triggerOpen({ features: { pushTokenRevocation: true } }); + await connectPromise; + + const revocation = client.unregisterPushToken("ExponentPushToken[test-device]"); + const request = parseSentFrame(mock.sent.at(-1)); + expect(request).toMatchObject({ + type: "push.unregister.request", + token: "ExponentPushToken[test-device]", + }); + mock.triggerMessage( + wrapSessionMessage({ + type: "push.unregister.response", + payload: { requestId: request.requestId }, + }), + ); + + await revocation; +}); + +test("bounds the wait for push token revocation", async () => { + useHeartbeatClock(); + const mock = createMockTransport(); + const client = new DaemonClient({ + url: "ws://test", + clientId: "clsk_push_revocation_timeout", + reconnect: { enabled: false }, + transportFactory: () => mock.transport, + }); + clients.push(client); + const connectPromise = client.connect(); + mock.triggerOpen({ features: { pushTokenRevocation: true } }); + await connectPromise; + + const revocation = client.unregisterPushToken("ExponentPushToken[test-device]"); + const rejection = expect(revocation).rejects.toThrow("Timeout waiting for message (2000ms)"); + await vi.advanceTimersByTimeAsync(1_999); + expect(client.getConnectionState().status).toBe("connected"); + + await vi.advanceTimersByTimeAsync(1); + await rejection; + expect(client.getConnectionState().status).toBe("connected"); +}); + test("defaults session RPC waiters to sixty seconds", async () => { useHeartbeatClock(); const logger = createMockLogger(); @@ -1294,56 +1347,6 @@ test("honors explicit shutdownServer timeout below the session RPC default", asy await expect(responsePromise).rejects.toThrow("Timeout waiting for message (1500ms)"); }); -test("honors explicit readChatMessages timeout below the session RPC default", async () => { - useHeartbeatClock(); - const logger = createMockLogger(); - const mock = createMockTransport(); - - const client = new DaemonClient({ - url: "ws://test", - clientId: "clsk_unit_test", - logger, - reconnect: { enabled: false }, - transportFactory: () => mock.transport, - }); - clients.push(client); - - const connectPromise = client.connect(); - mock.triggerOpen(); - await connectPromise; - - const responsePromise = client.readChatMessages({ - requestId: "req-chat-read-1", - room: "room-1", - limit: 1, - timeout: 2_500, - }); - let settled = false; - void responsePromise.then( - () => { - settled = true; - return undefined; - }, - () => { - settled = true; - return undefined; - }, - ); - - expect(parseSentFrame(mock.sent[0])).toEqual({ - type: "chat/read", - requestId: "req-chat-read-1", - room: "room-1", - limit: 1, - }); - - await vi.advanceTimersByTimeAsync(2_499); - expect(settled).toBe(false); - - await vi.advanceTimersByTimeAsync(1); - await expect(responsePromise).rejects.toThrow("Timeout waiting for message (2500ms)"); -}); - test("honors explicit getDaemonStatus timeout below the session RPC default", async () => { useHeartbeatClock(); const logger = createMockLogger(); @@ -1727,6 +1730,181 @@ test("a candidate measurement that times out under a heartbeat tick does not cou expect(session.teardownCount()).toBe(0); }); +test("file context action RPCs correlate success and error responses", async () => { + const mock = createMockTransport(); + const client = new DaemonClient({ + url: "ws://test", + clientId: "clsk_context_actions", + logger: createMockLogger(), + reconnect: { enabled: false }, + transportFactory: () => mock.transport, + }); + clients.push(client); + + const connectPromise = client.connect(); + mock.triggerOpen(); + await connectPromise; + + const createPromise = client.createFileEntry({ + cwd: "/tmp/project", + parentPath: "src", + name: "new.ts", + kind: "file", + }); + const createRequest = parseSentFrame(mock.sent.at(-1)); + expect(createRequest).toEqual({ + type: "fs.entry.create.request", + cwd: "/tmp/project", + parentPath: "src", + name: "new.ts", + kind: "file", + requestId: expect.any(String), + }); + mock.triggerMessage( + wrapSessionMessage({ + type: "fs.entry.create.response", + payload: { + cwd: "/tmp/project", + parentPath: "src", + path: "src/new.ts", + success: true, + error: null, + requestId: createRequest.requestId, + }, + }), + ); + await expect(createPromise).resolves.toMatchObject({ + path: "src/new.ts", + success: true, + error: null, + }); + + const renamePromise = client.renameFileEntry({ + cwd: "/tmp/project", + path: "src/new.ts", + name: "existing.ts", + }); + const renameRequest = parseSentFrame(mock.sent.at(-1)); + expect(renameRequest).toMatchObject({ + type: "fs.entry.rename.request", + cwd: "/tmp/project", + path: "src/new.ts", + name: "existing.ts", + }); + mock.triggerMessage( + wrapSessionMessage({ + type: "fs.entry.rename.response", + payload: { + cwd: "/tmp/project", + path: "src/new.ts", + renamedPath: null, + success: false, + error: '"existing.ts" already exists', + requestId: renameRequest.requestId, + }, + }), + ); + await expect(renamePromise).resolves.toMatchObject({ + renamedPath: null, + success: false, + error: '"existing.ts" already exists', + }); + + const duplicatePromise = client.duplicateFileEntry({ + cwd: "/tmp/project", + path: "src/new.ts", + }); + const duplicateRequest = parseSentFrame(mock.sent.at(-1)); + expect(duplicateRequest).toMatchObject({ + type: "fs.entry.duplicate.request", + cwd: "/tmp/project", + path: "src/new.ts", + }); + mock.triggerMessage( + wrapSessionMessage({ + type: "fs.entry.duplicate.response", + payload: { + cwd: "/tmp/project", + path: "src/new.ts", + duplicatedPath: "src/new copy.ts", + success: true, + error: null, + requestId: duplicateRequest.requestId, + }, + }), + ); + await expect(duplicatePromise).resolves.toMatchObject({ + duplicatedPath: "src/new copy.ts", + success: true, + error: null, + }); + + const deletePromise = client.deleteFileEntry({ cwd: "/tmp/project", path: "src/new.ts" }); + const deleteRequest = parseSentFrame(mock.sent.at(-1)); + expect(deleteRequest).toMatchObject({ + type: "fs.entry.delete.request", + cwd: "/tmp/project", + path: "src/new.ts", + }); + mock.triggerMessage( + wrapSessionMessage({ + type: "fs.entry.delete.response", + payload: { + cwd: "/tmp/project", + path: "src/new.ts", + success: true, + error: null, + requestId: deleteRequest.requestId, + }, + }), + ); + await expect(deletePromise).resolves.toMatchObject({ success: true, error: null }); + + const discardPromise = client.checkoutDiscardChanges("/tmp/project", { paths: ["src"] }); + const discardRequest = parseSentFrame(mock.sent.at(-1)); + expect(discardRequest).toMatchObject({ + type: "checkout.discard_changes.request", + cwd: "/tmp/project", + paths: ["src"], + }); + mock.triggerMessage( + wrapSessionMessage({ + type: "checkout.discard_changes.response", + payload: { + cwd: "/tmp/project", + success: false, + error: { code: "NOT_GIT_REPO", message: "Not a git repository" }, + requestId: discardRequest.requestId, + }, + }), + ); + await expect(discardPromise).resolves.toMatchObject({ + success: false, + error: { code: "NOT_GIT_REPO", message: "Not a git repository" }, + }); +}); + +test("a connection loss rejects an in-flight file context action", async () => { + const mock = createMockTransport(); + const client = new DaemonClient({ + url: "ws://test", + clientId: "clsk_context_disconnect", + logger: createMockLogger(), + reconnect: { enabled: false }, + transportFactory: () => mock.transport, + }); + clients.push(client); + + const connectPromise = client.connect(); + mock.triggerOpen(); + await connectPromise; + + const pending = client.deleteFileEntry({ cwd: "/tmp/project", path: "src/file.ts" }); + mock.triggerClose({ code: 1006, reason: "network lost" }); + + await expect(pending).rejects.toThrow(/network lost|disconnected|closed/i); +}); + test("listDirectory sends a list file explorer request and returns directory entries", async () => { const logger = createMockLogger(); const mock = createMockTransport(); @@ -3843,11 +4021,32 @@ test("bound workspace Git carries identity for the normal mutation surface", asy ); await expect(pull).resolves.toMatchObject({ success: true }); + const discard = workspaceGit.discardChanges({ paths: ["changed.ts"] }); + const discardRequest = parseSentFrame(mock.sent[1]); + expect(discardRequest).toMatchObject({ + type: "checkout.discard_changes.request", + workspaceId: "workspace-a", + cwd: "/shared", + paths: ["changed.ts"], + }); + mock.triggerMessage( + wrapSessionMessage({ + type: "checkout.discard_changes.response", + payload: { + cwd: "/shared", + requestId: discardRequest.requestId, + success: true, + error: null, + }, + }), + ); + await expect(discard).resolves.toMatchObject({ success: true }); + const checkDetails = workspaceGit.getForgeCheckDetails( { repoOwner: "getpaseo", repoName: "paseo", checkRunId: 42 }, "bound-check-details", ); - expect(parseSentFrame(mock.sent[1])).toMatchObject({ + expect(parseSentFrame(mock.sent[2])).toMatchObject({ type: "checkout.forge.get_check_details.request", workspaceId: "workspace-a", cwd: "/shared", diff --git a/packages/client/src/daemon-client.ts b/packages/client/src/daemon-client.ts index 1dd3db5619..4ba76be16e 100644 --- a/packages/client/src/daemon-client.ts +++ b/packages/client/src/daemon-client.ts @@ -112,7 +112,11 @@ import type { AgentProvider, AgentSessionConfig, } from "@getpaseo/protocol/agent-types"; -import type { MutableDaemonConfig, MutableDaemonConfigPatch } from "@getpaseo/protocol/messages"; +import type { + AgentConfigApply, + MutableDaemonConfig, + MutableDaemonConfigPatch, +} from "@getpaseo/protocol/messages"; import { isRelayClientWebSocketUrl } from "@getpaseo/protocol/daemon-endpoints"; import { terminalSubscriptionKey } from "@getpaseo/protocol/terminal-subscription-key"; import { @@ -494,30 +498,6 @@ type SubscribeTerminalPayload = SubscribeTerminalResponse["payload"]; type CloseItemsPayload = CloseItemsResponse["payload"]; type KillTerminalPayload = KillTerminalResponse["payload"]; type CaptureTerminalPayload = CaptureTerminalResponse["payload"]; -type ChatCreatePayload = Extract< - SessionOutboundMessage, - { type: "chat/create/response" } ->["payload"]; -type ChatListPayload = Extract<SessionOutboundMessage, { type: "chat/list/response" }>["payload"]; -type ChatInspectPayload = Extract< - SessionOutboundMessage, - { type: "chat/inspect/response" } ->["payload"]; -type ChatDeletePayload = Extract< - SessionOutboundMessage, - { type: "chat/delete/response" } ->["payload"]; -type ChatPostPayload = Extract<SessionOutboundMessage, { type: "chat/post/response" }>["payload"]; -type ChatReadPayload = Extract<SessionOutboundMessage, { type: "chat/read/response" }>["payload"]; -type ChatWaitPayload = Extract<SessionOutboundMessage, { type: "chat/wait/response" }>["payload"]; -type LoopRunPayload = Extract<SessionOutboundMessage, { type: "loop/run/response" }>["payload"]; -type LoopListPayload = Extract<SessionOutboundMessage, { type: "loop/list/response" }>["payload"]; -type LoopInspectPayload = Extract< - SessionOutboundMessage, - { type: "loop/inspect/response" } ->["payload"]; -type LoopLogsPayload = Extract<SessionOutboundMessage, { type: "loop/logs/response" }>["payload"]; -type LoopStopPayload = Extract<SessionOutboundMessage, { type: "loop/stop/response" }>["payload"]; type ScheduleCreatePayload = Extract< SessionOutboundMessage, { type: "schedule/create/response" } @@ -699,70 +679,10 @@ export type ProjectListPayload = Extract< SessionOutboundMessage, { type: "project.list.response" } >["payload"]; -export interface CreateChatRoomOptions { - name: string; - purpose?: string | null; +type ProjectListRequest = Extract<SessionInboundMessage, { type: "project.list.request" }>; +export type ProjectListOptions = Omit<ProjectListRequest, "type" | "requestId"> & { requestId?: string; -} -export interface InspectChatRoomOptions { - room: string; - requestId?: string; -} -export interface DeleteChatRoomOptions { - room: string; - requestId?: string; -} -export interface PostChatMessageOptions { - room: string; - body: string; - authorAgentId?: string; - replyToMessageId?: string | null; - requestId?: string; -} -export interface ReadChatMessagesOptions { - room: string; - limit?: number; - since?: string; - authorAgentId?: string; - requestId?: string; - timeout?: number; -} -export interface WaitForChatMessagesOptions { - room: string; - afterMessageId?: string | null; - timeoutMs?: number; - requestId?: string; -} -export interface RunLoopOptions { - prompt: string; - cwd: string; - provider?: string; - model?: string; - modeId?: string; - verifierProvider?: string; - verifierModel?: string; - verifierModeId?: string; - verifyPrompt?: string | null; - verifyChecks?: string[]; - name?: string | null; - sleepMs?: number; - maxIterations?: number; - maxTimeMs?: number; - requestId?: string; -} -export interface InspectLoopOptions { - id: string; - requestId?: string; -} -export interface LoopLogsOptions { - id: string; - afterSeq?: number; - requestId?: string; -} -export interface StopLoopOptions { - id: string; - requestId?: string; -} +}; export interface CreateScheduleOptions { prompt: string; name?: string | null; @@ -966,6 +886,7 @@ function toTimeoutError(error: unknown, label: string, timeoutMs: number): Error const DEFAULT_RECONNECT_BASE_DELAY_MS = 1500; const DEFAULT_RECONNECT_MAX_DELAY_MS = 30000; const DEFAULT_SESSION_RPC_TIMEOUT_MS = 60_000; +const PUSH_TOKEN_REVOCATION_TIMEOUT_MS = 2_000; const DEFAULT_CONNECT_TIMEOUT_MS = 15_000; const DEFAULT_LIVENESS_TIMEOUT_MS = 5000; const LIVENESS_HEARTBEAT_INTERVAL_MS = 10_000; @@ -1931,6 +1852,16 @@ export class DaemonClient { }); } + async unregisterPushToken(token: string): Promise<void> { + const requestId = this.createRequestId(); + await this.sendCorrelatedSessionRequest({ + requestId, + message: { type: "push.unregister.request", token, requestId }, + responseType: "push.unregister.response", + timeout: PUSH_TOKEN_REVOCATION_TIMEOUT_MS, + }); + } + async ping(params?: { requestId?: string; timeoutMs?: number }): Promise<{ requestId: string; clientSentAt: number; @@ -2080,6 +2011,7 @@ export class DaemonClient { ...(options?.sort ? { sort: options.sort } : {}), ...(options?.page ? { page: options.page } : {}), ...(options?.subscribe ? { subscribe: options.subscribe } : {}), + ...(options?.sync ? { sync: options.sync } : {}), }); return this.sendRequest({ requestId: resolvedRequestId, @@ -2162,6 +2094,7 @@ export class DaemonClient { ...(options?.sort ? { sort: options.sort } : {}), ...(options?.page ? { page: options.page } : {}), ...(options?.subscribe ? { subscribe: options.subscribe } : {}), + ...(options?.sync ? { sync: options.sync } : {}), }); return this.sendRequest({ requestId: resolvedRequestId, @@ -2179,11 +2112,13 @@ export class DaemonClient { }); } - async listProjects(requestId?: string): Promise<ProjectListPayload> { + async listProjects(options?: string | ProjectListOptions): Promise<ProjectListPayload> { + const requestId = typeof options === "string" ? options : options?.requestId; const resolvedRequestId = this.createRequestId(requestId); const message = SessionInboundMessageSchema.parse({ type: "project.list.request", requestId: resolvedRequestId, + ...(typeof options === "object" && options.sync ? { sync: options.sync } : {}), }); return this.sendRequest({ requestId: resolvedRequestId, @@ -3233,6 +3168,44 @@ export class DaemonClient { return payload.notice ?? null; } + /** + * Applies a whole agent-config bundle in one request. Use this instead of + * chaining the single-field setters when the values belong together so client + * interruption and other mutations cannot interleave between steps. A + * provider rejection can still leave earlier steps applied. + * Gated on `server_info.features.agentConfigApply`. + */ + async applyAgentConfig( + agentId: string, + config: AgentConfigApply, + ): Promise<AgentProviderNotice | null> { + const requestId = this.createRequestId(); + const message = SessionInboundMessageSchema.parse({ + type: "agent.config.apply.request", + agentId, + config, + requestId, + }); + const payload = await this.sendRequest({ + requestId, + message, + options: { skipQueue: true }, + select: (msg) => { + if (msg.type !== "agent.config.apply.response") { + return null; + } + if (msg.payload.requestId !== requestId) { + return null; + } + return msg.payload; + }, + }); + if (!payload.accepted) { + throw new Error(payload.error ?? "applyAgentConfig rejected"); + } + return payload.notice ?? null; + } + async restartServer(reason?: string, requestId?: string): Promise<RestartRequestedStatusPayload> { const resolvedRequestId = this.createRequestId(requestId); const message = SessionInboundMessageSchema.parse({ @@ -3657,6 +3630,8 @@ export class DaemonClient { if (payload.error) throw new Error(payload.error.message); return { file: payload.file }; }, + discardChanges: (input: { paths: string[] }) => + this.checkoutDiscardChanges(cwd, { ...input, workspaceId }), createPr: (input: { title?: string; body?: string; baseRef?: string }, requestId?: string) => this.sendCorrelatedSessionRequest({ requestId, @@ -4648,6 +4623,59 @@ export class DaemonClient { return payload.result; } + async createFileEntry(input: { + cwd: string; + parentPath: string; + name: string; + kind: "file" | "directory"; + }): Promise<CorrelatedResponsePayload<"fs.entry.create.response">> { + return this.sendNamespacedCorrelatedSessionRequest<"fs.entry.create.response">({ + message: { type: "fs.entry.create.request", ...input }, + }); + } + + async renameFileEntry(input: { + cwd: string; + path: string; + name: string; + }): Promise<CorrelatedResponsePayload<"fs.entry.rename.response">> { + return this.sendNamespacedCorrelatedSessionRequest<"fs.entry.rename.response">({ + message: { type: "fs.entry.rename.request", ...input }, + }); + } + + async duplicateFileEntry(input: { + cwd: string; + path: string; + }): Promise<CorrelatedResponsePayload<"fs.entry.duplicate.response">> { + return this.sendNamespacedCorrelatedSessionRequest<"fs.entry.duplicate.response">({ + message: { type: "fs.entry.duplicate.request", ...input }, + }); + } + + async deleteFileEntry(input: { + cwd: string; + path: string; + }): Promise<CorrelatedResponsePayload<"fs.entry.delete.response">> { + return this.sendNamespacedCorrelatedSessionRequest<"fs.entry.delete.response">({ + message: { type: "fs.entry.delete.request", ...input }, + }); + } + + async checkoutDiscardChanges( + cwd: string, + input: { paths: string[]; workspaceId?: string }, + ): Promise<CorrelatedResponsePayload<"checkout.discard_changes.response">> { + return this.sendNamespacedCorrelatedSessionRequest<"checkout.discard_changes.response">({ + message: { + type: "checkout.discard_changes.request", + cwd, + workspaceId: input.workspaceId, + paths: input.paths, + }, + }); + } + async uploadFile(input: FileUploadInput): Promise<FileUploadResult> { const bytes = asUint8Array(input.bytes); if (!bytes) { @@ -5395,93 +5423,6 @@ export class DaemonClient { }); } - async createChatRoom(options: CreateChatRoomOptions): Promise<ChatCreatePayload> { - return this.sendCorrelatedSessionRequest({ - requestId: options.requestId, - message: { - type: "chat/create", - name: options.name, - ...(options.purpose ? { purpose: options.purpose } : {}), - }, - responseType: "chat/create/response", - }); - } - - async listChatRooms(requestId?: string): Promise<ChatListPayload> { - return this.sendCorrelatedSessionRequest({ - requestId, - message: { - type: "chat/list", - }, - responseType: "chat/list/response", - }); - } - - async inspectChatRoom(options: InspectChatRoomOptions): Promise<ChatInspectPayload> { - return this.sendCorrelatedSessionRequest({ - requestId: options.requestId, - message: { - type: "chat/inspect", - room: options.room, - }, - responseType: "chat/inspect/response", - }); - } - - async deleteChatRoom(options: DeleteChatRoomOptions): Promise<ChatDeletePayload> { - return this.sendCorrelatedSessionRequest({ - requestId: options.requestId, - message: { - type: "chat/delete", - room: options.room, - }, - responseType: "chat/delete/response", - }); - } - - async postChatMessage(options: PostChatMessageOptions): Promise<ChatPostPayload> { - return this.sendCorrelatedSessionRequest({ - requestId: options.requestId, - message: { - type: "chat/post", - room: options.room, - body: options.body, - ...(options.authorAgentId ? { authorAgentId: options.authorAgentId } : {}), - ...(options.replyToMessageId ? { replyToMessageId: options.replyToMessageId } : {}), - }, - responseType: "chat/post/response", - }); - } - - async readChatMessages(options: ReadChatMessagesOptions): Promise<ChatReadPayload> { - return this.sendCorrelatedSessionRequest({ - requestId: options.requestId, - message: { - type: "chat/read", - room: options.room, - ...(typeof options.limit === "number" ? { limit: options.limit } : {}), - ...(options.since ? { since: options.since } : {}), - ...(options.authorAgentId ? { authorAgentId: options.authorAgentId } : {}), - }, - responseType: "chat/read/response", - timeout: options.timeout, - }); - } - - async waitForChatMessages(options: WaitForChatMessagesOptions): Promise<ChatWaitPayload> { - return this.sendCorrelatedSessionRequest({ - requestId: options.requestId, - message: { - type: "chat/wait", - room: options.room, - ...(options.afterMessageId ? { afterMessageId: options.afterMessageId } : {}), - ...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}), - }, - responseType: "chat/wait/response", - timeout: (options.timeoutMs ?? 0) + 10000, - }); - } - async scheduleCreate(options: CreateScheduleOptions): Promise<ScheduleCreatePayload> { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, @@ -5592,81 +5533,6 @@ export class DaemonClient { }); } - async loopRun(options: RunLoopOptions): Promise<LoopRunPayload> { - return this.sendCorrelatedSessionRequest({ - requestId: options.requestId, - message: { - type: "loop/run", - prompt: options.prompt, - cwd: options.cwd, - ...(options.provider ? { provider: options.provider } : {}), - ...(options.model ? { model: options.model } : {}), - ...(options.modeId ? { modeId: options.modeId } : {}), - ...(options.verifierProvider ? { verifierProvider: options.verifierProvider } : {}), - ...(options.verifierModel ? { verifierModel: options.verifierModel } : {}), - ...(options.verifierModeId ? { verifierModeId: options.verifierModeId } : {}), - ...(options.verifyPrompt ? { verifyPrompt: options.verifyPrompt } : {}), - ...(options.verifyChecks && options.verifyChecks.length > 0 - ? { verifyChecks: options.verifyChecks } - : {}), - ...(options.name ? { name: options.name } : {}), - ...(typeof options.sleepMs === "number" ? { sleepMs: options.sleepMs } : {}), - ...(typeof options.maxIterations === "number" - ? { maxIterations: options.maxIterations } - : {}), - ...(typeof options.maxTimeMs === "number" ? { maxTimeMs: options.maxTimeMs } : {}), - }, - responseType: "loop/run/response", - }); - } - - async loopList(requestId?: string): Promise<LoopListPayload> { - return this.sendCorrelatedSessionRequest({ - requestId, - message: { - type: "loop/list", - }, - responseType: "loop/list/response", - }); - } - - async loopInspect(options: string | InspectLoopOptions): Promise<LoopInspectPayload> { - const normalized = typeof options === "string" ? { id: options } : options; - return this.sendCorrelatedSessionRequest({ - requestId: normalized.requestId, - message: { - type: "loop/inspect", - id: normalized.id, - }, - responseType: "loop/inspect/response", - }); - } - - async loopLogs(options: string | LoopLogsOptions, afterSeq?: number): Promise<LoopLogsPayload> { - const normalized = typeof options === "string" ? { id: options, afterSeq } : options; - return this.sendCorrelatedSessionRequest({ - requestId: normalized.requestId, - message: { - type: "loop/logs", - id: normalized.id, - ...(typeof normalized.afterSeq === "number" ? { afterSeq: normalized.afterSeq } : {}), - }, - responseType: "loop/logs/response", - }); - } - - async loopStop(options: string | StopLoopOptions): Promise<LoopStopPayload> { - const normalized = typeof options === "string" ? { id: options } : options; - return this.sendCorrelatedSessionRequest({ - requestId: normalized.requestId, - message: { - type: "loop/stop", - id: normalized.id, - }, - responseType: "loop/stop/response", - }); - } - onTerminalStreamEvent(handler: (event: TerminalStreamEvent) => void): () => void { return this.terminalStreams.onEvent(handler); } diff --git a/packages/desktop/e2e/browser-tabs.e2e.mjs b/packages/desktop/e2e/browser-tabs.e2e.mjs index cde56586d4..9522ccc270 100644 --- a/packages/desktop/e2e/browser-tabs.e2e.mjs +++ b/packages/desktop/e2e/browser-tabs.e2e.mjs @@ -301,6 +301,21 @@ async function callBrowserTool(client, name, args = {}) { return mcpPayload(await client.callTool({ name, args }), name); } +async function waitForGuestSelector(client, browserId) { + const deadline = Date.now() + 5_000; + while (Date.now() < deadline) { + const evaluated = await callBrowserTool(client, "browser_evaluate", { + browserId, + function: "() => Boolean(globalThis.__paseoSelector)", + }); + if (JSON.parse(evaluated.resultJson) === true) { + return true; + } + await delay(50); + } + return false; +} + async function createCallerAgent(daemonPort) { const transport = new StreamableHTTPClientTransport( new URL(`http://127.0.0.1:${daemonPort}/mcp/agents`), @@ -453,7 +468,7 @@ function recordViewportMismatch(failures, label, actual, expected) { ); } -async function runRegression({ page, client, serverId, targetUrl, callerAgentId }) { +async function runRegression({ page, client, serverId, targetUrl, callerAgentId, artifactDir }) { const failures = []; const originalWorkspaceId = workspaceIds[0]; const originalWorkspaceRow = page.getByTestId( @@ -742,16 +757,90 @@ async function runRegression({ page, client, serverId, targetUrl, callerAgentId responsiveViewport, ); - await originalDeck.getByRole("button", { name: "Annotate element" }).click(); - await page.waitForTimeout(250); - const selectorResult = await callBrowserTool(client, "browser_evaluate", { + const annotateButton = originalDeck.getByRole("button", { name: "Annotate element" }); + await page.evaluate((id) => { + const webview = document.querySelector(`[data-paseo-browser-id="${id}"]`); + if (!(webview instanceof HTMLElement)) throw new Error(`Browser webview ${id} was unavailable`); + globalThis.__paseoOriginalIsLoadingDescriptor = Object.getOwnPropertyDescriptor( + webview, + "isLoading", + ); + Object.defineProperty(webview, "isLoading", { + configurable: true, + value: () => true, + }); + }, browserId); + await annotateButton.click(); + await page.waitForTimeout(100); + assert( + !(await originalDeck.getByRole("button", { name: "Cancel element selector" }).isVisible()), + "Element selector started while the guest reported a genuine load", + ); + const selectorDuringLoad = await callBrowserTool(client, "browser_evaluate", { browserId, function: "() => Boolean(globalThis.__paseoSelector)", }); - if (JSON.parse(selectorResult.resultJson) !== true) { - failures.push("reused loaded browser remains ready for element annotation after remount"); + assert( + JSON.parse(selectorDuringLoad.resultJson) === false, + "Selector injected while the guest reported a genuine load", + ); + assert( + await page.getByText("Wait for the page to finish loading").isVisible(), + "Element selector loading failure was not visible", + ); + await page.evaluate((id) => { + const webview = document.querySelector(`[data-paseo-browser-id="${id}"]`); + if (!(webview instanceof HTMLElement)) throw new Error(`Browser webview ${id} was unavailable`); + const descriptor = globalThis.__paseoOriginalIsLoadingDescriptor; + if (descriptor) Object.defineProperty(webview, "isLoading", descriptor); + else delete webview.isLoading; + delete globalThis.__paseoOriginalIsLoadingDescriptor; + }, browserId); + + const readyStateResult = await callBrowserTool(client, "browser_evaluate", { + browserId, + function: "() => document.readyState", + }); + assert( + JSON.parse(readyStateResult.resultJson) === "complete", + "Local browser document did not finish loading", + ); + // Reproduce the report's mismatch: the guest is complete, but the pane's + // last loading signal says it is not ready. + await page.evaluate((id) => { + const webview = document.querySelector(`[data-paseo-browser-id="${id}"]`); + if (!(webview instanceof HTMLElement)) { + throw new Error(`Browser webview ${id} was unavailable`); + } + webview.dispatchEvent(new Event("did-start-loading")); + }, browserId); + + await annotateButton.click(); + const annotateSelectorActive = await waitForGuestSelector(client, browserId); + await page.screenshot({ path: path.join(artifactDir, "local-page-annotate-selector.png") }); + if (!annotateSelectorActive) { + failures.push("loaded local browser remains ready for element annotation"); } + await originalDeck.getByRole("button", { name: "Cancel element selector" }).click(); + await delay(10_000); + const screenshotButton = originalDeck.getByRole("button", { name: "Screenshot element" }); + await screenshotButton.click(); + const screenshotSelectorActive = await waitForGuestSelector(client, browserId); + if (!screenshotSelectorActive) { + failures.push("loaded local browser remains ready for element screenshots"); + } + await delay(20_500); + const selectorAfterPriorTimeout = await callBrowserTool(client, "browser_evaluate", { + browserId, + function: "() => Boolean(globalThis.__paseoSelector)", + }); + if (JSON.parse(selectorAfterPriorTimeout.resultJson) !== true) { + failures.push("a previous selector timeout does not destroy the current selector session"); + } + await page.screenshot({ path: path.join(artifactDir, "local-page-screenshot-selector.png") }); + await originalDeck.getByRole("button", { name: "Cancel element selector" }).click(); + if (failures.length > 0) { throw new Error(`Browser viewport regressions:\n- ${failures.join("\n- ")}`); } @@ -767,6 +856,7 @@ async function runRegression({ page, client, serverId, targetUrl, callerAgentId list: "passed", snapshot: "passed", click: "passed", + localPageSelectors: "passed", }; } @@ -858,10 +948,11 @@ async function main() { serverId: status.serverId, targetUrl: target.url, callerAgentId, + artifactDir, }); writeJson(path.join(artifactDir, "result.json"), { ...report, localProbeWorkspaceId }); console.log( - `Browser desktop browser E2E passed: WebContents ${report.originalWebContentsId} remained ${report.finalWebContentsId}; viewport, inactive capture, focus continuity, list, snapshot, click passed.`, + `Browser desktop browser E2E passed: WebContents ${report.originalWebContentsId} remained ${report.finalWebContentsId}; viewport, inactive capture, focus continuity, list, snapshot, click, local-page selectors passed.`, ); } catch (error) { console.error(`Browser desktop browser E2E failed. Artifacts: ${artifactDir}`); diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 0d0617280e..f592b21b4d 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/desktop", - "version": "0.3.1", + "version": "0.4.0", "private": true, "description": "Paseo desktop app (Electron wrapper)", "homepage": "https://paseo.sh", diff --git a/packages/desktop/scripts/dev-runner-config.mjs b/packages/desktop/scripts/dev-runner-config.mjs index 8efbd3a033..c855b3e5d1 100644 --- a/packages/desktop/scripts/dev-runner-config.mjs +++ b/packages/desktop/scripts/dev-runner-config.mjs @@ -15,3 +15,9 @@ export function createElectronSpawnOptions({ env, colorEnv, expoDevUrl }) { export function resolveChildKillTarget(pid, detached) { return detached ? -pid : pid; } + +export function registerDevRunnerShutdownSignals({ signalSource, stop }) { + for (const signal of ["SIGHUP", "SIGINT", "SIGTERM"]) { + signalSource.on(signal, () => stop("SIGTERM")); + } +} diff --git a/packages/desktop/scripts/dev-runner-config.test.mjs b/packages/desktop/scripts/dev-runner-config.test.mjs index 813bc6760c..ca34fe8c16 100644 --- a/packages/desktop/scripts/dev-runner-config.test.mjs +++ b/packages/desktop/scripts/dev-runner-config.test.mjs @@ -1,7 +1,23 @@ import { describe, expect, test } from "vitest"; -import { createElectronSpawnOptions, resolveChildKillTarget } from "./dev-runner-config.mjs"; +import { readFileSync } from "node:fs"; +import { + createElectronSpawnOptions, + registerDevRunnerShutdownSignals, + resolveChildKillTarget, +} from "./dev-runner-config.mjs"; describe("desktop dev process ownership", () => { + test("makes the signal-handling runner the workspace terminal process", () => { + const paseoConfig = JSON.parse( + readFileSync(new URL("../../../paseo.json", import.meta.url), "utf8"), + ); + const devScript = readFileSync(new URL("./dev.sh", import.meta.url), "utf8"); + + expect(paseoConfig.scripts.desktop.command).toContain("exec ./packages/desktop/scripts/dev.sh"); + expect(devScript).toContain('npm --prefix "$DESKTOP_DIR" run build:main'); + expect(devScript).toContain('exec node "$SCRIPT_DIR/dev-runner.mjs"'); + }); + test("keeps Electron in the runner process group", () => { const options = createElectronSpawnOptions({ env: { PATH: "/usr/bin" }, @@ -23,4 +39,24 @@ describe("desktop dev process ownership", () => { expect(resolveChildKillTarget(42, true)).toBe(-42); expect(resolveChildKillTarget(42, false)).toBe(42); }); + + test("stops children when the owning terminal hangs up", () => { + const listeners = new Map(); + const receivedSignals = []; + + registerDevRunnerShutdownSignals({ + signalSource: { + on(signal, listener) { + listeners.set(signal, listener); + }, + }, + stop(signal) { + receivedSignals.push(signal); + }, + }); + + expect(Array.from(listeners.keys())).toEqual(["SIGHUP", "SIGINT", "SIGTERM"]); + listeners.get("SIGHUP")(); + expect(receivedSignals).toEqual(["SIGTERM"]); + }); }); diff --git a/packages/desktop/scripts/dev-runner.mjs b/packages/desktop/scripts/dev-runner.mjs index ac96fee5ac..5e0344c0d1 100644 --- a/packages/desktop/scripts/dev-runner.mjs +++ b/packages/desktop/scripts/dev-runner.mjs @@ -1,10 +1,14 @@ #!/usr/bin/env node import net from "node:net"; import path from "node:path"; -import { spawn } from "node:child_process"; +import { execFileSync, spawn } from "node:child_process"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; -import { createElectronSpawnOptions, resolveChildKillTarget } from "./dev-runner-config.mjs"; +import { + createElectronSpawnOptions, + registerDevRunnerShutdownSignals, + resolveChildKillTarget, +} from "./dev-runner-config.mjs"; import { resolveDevElectronArgs } from "./dev-runner-args.mjs"; @@ -27,6 +31,10 @@ const colorEnv = { FORCE_COLOR: process.env.FORCE_COLOR || "1", npm_config_color: process.env.npm_config_color || "always", }; +const devBuildLabel = execFileSync("git", ["branch", "--show-current"], { + cwd: rootDir, + encoding: "utf8", +}).trim(); const children = new Map(); let stopping = false; @@ -155,8 +163,7 @@ function canConnect(port, host) { }); } -process.on("SIGINT", () => stopAll("SIGTERM")); -process.on("SIGTERM", () => stopAll("SIGTERM")); +registerDevRunnerShutdownSignals({ signalSource: process, stop: stopAll }); spawnChild("metro", "npx", ["expo", "start", "--port", String(expoPort)], { cwd: appDir, @@ -166,6 +173,7 @@ spawnChild("metro", "npx", ["expo", "start", "--port", String(expoPort)], { ...colorEnv, BROWSER: "none", APP_VARIANT: "development", + EXPO_PUBLIC_PASEO_DEV_BUILD_LABEL: devBuildLabel, PASEO_WEB_PLATFORM: "electron", }, }); diff --git a/packages/desktop/scripts/dev.ps1 b/packages/desktop/scripts/dev.ps1 index 81f8e4ab6e..edb532f869 100644 --- a/packages/desktop/scripts/dev.ps1 +++ b/packages/desktop/scripts/dev.ps1 @@ -32,6 +32,7 @@ try { # Set EXPO_DEV_URL in the environment so Electron inherits it $env:EXPO_DEV_URL = "http://localhost:$($env:EXPO_PORT)" +$env:EXPO_PUBLIC_PASEO_DEV_BUILD_LABEL = (git -C $RootDir branch --show-current).Trim() $env:PASEO_DEV_ROOT = $RootDir $env:PASEO_DEV_RUNTIME_FALLBACK_ROOT = $RootDir diff --git a/packages/desktop/scripts/dev.sh b/packages/desktop/scripts/dev.sh index f00625d873..90d5ffa095 100755 --- a/packages/desktop/scripts/dev.sh +++ b/packages/desktop/scripts/dev.sh @@ -31,7 +31,7 @@ export PASEO_DAEMON_ENDPOINT="$DAEMON_ENDPOINT" export PASEO_CORS_ORIGINS="${PASEO_CORS_ORIGINS:-*}" -npm run build:main +npm --prefix "$DESKTOP_DIR" run build:main echo "══════════════════════════════════════════════════════" echo " Paseo Desktop Dev" diff --git a/packages/desktop/src/daemon/quit-lifecycle.test.ts b/packages/desktop/src/daemon/quit-lifecycle.test.ts index 5dd1e66806..9985817d23 100644 --- a/packages/desktop/src/daemon/quit-lifecycle.test.ts +++ b/packages/desktop/src/daemon/quit-lifecycle.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { DEFAULT_DESKTOP_SETTINGS } from "../settings/desktop-settings"; import { createQuitLifecycle, + registerExternalQuitSignals, shouldStopDesktopManagedDaemonOnQuit, stopDesktopManagedDaemonOnQuitIfNeeded, } from "./quit-lifecycle"; @@ -29,6 +30,25 @@ function waitForQuitLifecycle(): Promise<void> { } describe("quit-lifecycle", () => { + it("turns external termination signals into one Electron quit", () => { + const listeners = new Map<NodeJS.Signals, () => void>(); + const quits: string[] = []; + + registerExternalQuitSignals({ + signals: { + on: (signal, listener) => { + listeners.set(signal, listener); + }, + }, + quit: () => quits.push("quit"), + }); + + expect(Array.from(listeners.keys())).toEqual(["SIGHUP", "SIGINT", "SIGTERM"]); + listeners.get("SIGTERM")?.(); + listeners.get("SIGHUP")?.(); + expect(quits).toEqual(["quit"]); + }); + it("stops by default and only keeps running when keepRunningAfterQuit is enabled", () => { expect(shouldStopDesktopManagedDaemonOnQuit(SETTINGS_STOP_ON_QUIT)).toBe(true); expect(shouldStopDesktopManagedDaemonOnQuit(SETTINGS_KEEP_RUNNING)).toBe(false); diff --git a/packages/desktop/src/daemon/quit-lifecycle.ts b/packages/desktop/src/daemon/quit-lifecycle.ts index 1998985f56..bd1847ce6b 100644 --- a/packages/desktop/src/daemon/quit-lifecycle.ts +++ b/packages/desktop/src/daemon/quit-lifecycle.ts @@ -14,6 +14,10 @@ interface BeforeQuitApp { exit(code: number): void; } +interface ExternalQuitSignalSource { + on(signal: NodeJS.Signals, listener: () => void): unknown; +} + interface QuitLifecycle { handleBeforeQuit(event: BeforeQuitEvent): void; handleBeforeQuitForUpdate(): void; @@ -31,6 +35,23 @@ export interface StopOnQuitDeps { showShutdownFeedback: () => void; } +export function registerExternalQuitSignals({ + signals, + quit, +}: { + signals: ExternalQuitSignalSource; + quit: () => void; +}): void { + let quitRequested = false; + for (const signal of ["SIGHUP", "SIGINT", "SIGTERM"] satisfies NodeJS.Signals[]) { + signals.on(signal, () => { + if (quitRequested) return; + quitRequested = true; + quit(); + }); + } +} + export function shouldStopDesktopManagedDaemonOnQuit(settings: QuitLifecycleSettings): boolean { return !settings.daemon.keepRunningAfterQuit; } diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index 36b66783ad..cb0ae0aa5a 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -83,6 +83,7 @@ import { } from "./daemon/daemon-manager.js"; import { createQuitLifecycle, + registerExternalQuitSignals, stopDesktopManagedDaemonOnQuitIfNeeded, } from "./daemon/quit-lifecycle.js"; import { runDesktopStartup } from "./desktop-startup.js"; @@ -1081,6 +1082,7 @@ const quitLifecycle = createQuitLifecycle({ // electron-updater forwards this event through Electron's built-in autoUpdater. electronAutoUpdater.on("before-quit-for-update", quitLifecycle.handleBeforeQuitForUpdate); app.on("before-quit", quitLifecycle.handleBeforeQuit); +registerExternalQuitSignals({ signals: process, quit: () => app.quit() }); app.on("window-all-closed", () => { if (process.platform !== "darwin") { diff --git a/packages/expo-two-way-audio/CONTRIBUTING.md b/packages/expo-two-way-audio/CONTRIBUTING.md index 2720c386e7..cf9701ccca 100644 --- a/packages/expo-two-way-audio/CONTRIBUTING.md +++ b/packages/expo-two-way-audio/CONTRIBUTING.md @@ -26,7 +26,7 @@ npm i ## How to Submit Changes -We try not to be too prescreptive about how people work, but we also believe in helping make things easier by following a couple of basic steps. If you follow these recommendations, it should make the review process simpler and quicker: +We try not to be too prescriptive about how people work, but we also believe in helping make things easier by following a couple of basic steps. If you follow these recommendations, it should make the review process simpler and quicker: 1. If your change is large, consider reaching out to us beforehand and discussing the issue. This could save you a lot of time if there are good reasons why we haven't done something. 2. Fork the repo and work on a branch on your fork. Try to give your branches short, descriptive names in the format {type}/{description} e.g. bugfix/missing-try-catch. diff --git a/packages/expo-two-way-audio/README.md b/packages/expo-two-way-audio/README.md index 5f298c6f3f..293a439f01 100644 --- a/packages/expo-two-way-audio/README.md +++ b/packages/expo-two-way-audio/README.md @@ -88,7 +88,7 @@ npx expo run:ios --device --configuration Release npx expo run:android --device --variant release ``` -For Android, the following permissions are needed: `RECORD_AUDIO`, `MODIFY_AUDIO_SETTINGS`. In Expo apps they can bee added in your `app.json` file: +For Android, the following permissions are needed: `RECORD_AUDIO`, `MODIFY_AUDIO_SETTINGS`. In Expo apps they can be added in your `app.json` file: ```javascript expo.android.permissions: ["RECORD_AUDIO", "MODIFY_AUDIO_SETTINGS"] diff --git a/packages/expo-two-way-audio/package.json b/packages/expo-two-way-audio/package.json index 922a2d20ae..0b880ad771 100644 --- a/packages/expo-two-way-audio/package.json +++ b/packages/expo-two-way-audio/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/expo-two-way-audio", - "version": "0.3.1", + "version": "0.4.0", "description": "Native module for two way audio streaming", "keywords": [ "ExpoTwoWayAudio", diff --git a/packages/highlight/package.json b/packages/highlight/package.json index c14a1af1cb..9a9b8a6ad0 100644 --- a/packages/highlight/package.json +++ b/packages/highlight/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/highlight", - "version": "0.3.1", + "version": "0.4.0", "files": [ "dist", "!dist/**/*.map" diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 46e2db0c98..9e9a9f14c5 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/protocol", - "version": "0.3.1", + "version": "0.4.0", "description": "Paseo shared protocol schemas and wire types", "files": [ "dist", diff --git a/packages/protocol/src/agent-labels.test.ts b/packages/protocol/src/agent-labels.test.ts index c57db3a09e..1f52a652fc 100644 --- a/packages/protocol/src/agent-labels.test.ts +++ b/packages/protocol/src/agent-labels.test.ts @@ -1,7 +1,10 @@ import { describe, expect, test } from "vitest"; import { getParentAgentIdFromLabels, + getOpenAgentTabLabel, + hasOpenAgentTab, isDelegatedAgent, + isOpenAgentTabLabel, PARENT_AGENT_ID_LABEL, } from "./agent-labels.js"; @@ -18,4 +21,19 @@ describe("agent label policy", () => { expect(isDelegatedAgent({ labels: { [PARENT_AGENT_ID_LABEL]: " " } })).toBe(false); expect(isDelegatedAgent({ labels: { [PARENT_AGENT_ID_LABEL]: 42 } })).toBe(false); }); + + test("treats any true client-scoped open-tab label as open", () => { + const desktopLabel = getOpenAgentTabLabel("desktop-client"); + const mobileLabel = getOpenAgentTabLabel("mobile-client"); + + expect(hasOpenAgentTab({ [desktopLabel]: "false", [mobileLabel]: "true" })).toBe(true); + expect(hasOpenAgentTab({ [desktopLabel]: "false", [mobileLabel]: "false" })).toBe(false); + expect(hasOpenAgentTab({})).toBe(false); + }); + + test("recognizes only client-scoped open-tab labels", () => { + expect(isOpenAgentTabLabel(getOpenAgentTabLabel("client-a"))).toBe(true); + expect(isOpenAgentTabLabel("paseo.open-agent-tab")).toBe(false); + expect(isOpenAgentTabLabel("custom.open-agent-tab.client-a")).toBe(false); + }); }); diff --git a/packages/protocol/src/agent-labels.ts b/packages/protocol/src/agent-labels.ts index 3ad40022ea..2bf05950dd 100644 --- a/packages/protocol/src/agent-labels.ts +++ b/packages/protocol/src/agent-labels.ts @@ -1,4 +1,13 @@ export const PARENT_AGENT_ID_LABEL = "paseo.parent-agent-id"; +const OPEN_AGENT_TAB_LABEL_PREFIX = "paseo.open-agent-tab."; + +export function getOpenAgentTabLabel(clientId: string): string { + return `${OPEN_AGENT_TAB_LABEL_PREFIX}${clientId}`; +} + +export function isOpenAgentTabLabel(label: string): boolean { + return label.startsWith(OPEN_AGENT_TAB_LABEL_PREFIX); +} export interface AgentLabelSource { labels?: Record<string, unknown> | null; @@ -14,3 +23,9 @@ export function getParentAgentIdFromLabels(labels: Record<string, unknown> | nul export function isDelegatedAgent(agent: AgentLabelSource): boolean { return getParentAgentIdFromLabels(agent.labels) !== null; } + +export function hasOpenAgentTab(labels: Record<string, unknown> | null | undefined): boolean { + return Object.entries(labels ?? {}).some( + ([label, value]) => isOpenAgentTabLabel(label) && value === "true", + ); +} diff --git a/packages/protocol/src/agent-types.ts b/packages/protocol/src/agent-types.ts index 9d2dffd2e3..06d7fa72a7 100644 --- a/packages/protocol/src/agent-types.ts +++ b/packages/protocol/src/agent-types.ts @@ -339,12 +339,20 @@ export interface CompactionTimelineItem { preTokens?: number; } +export interface AgentTaskItem { + text: string; + completed: boolean; + id?: string; + status?: "pending" | "in_progress" | "completed"; + activeForm?: string; +} + export type AgentTimelineItem = | { type: "user_message"; text: string; messageId?: string; clientMessageId?: string } | { type: "assistant_message"; text: string; messageId?: string } | { type: "reasoning"; text: string } | ToolCallTimelineItem - | { type: "todo"; items: { text: string; completed: boolean }[] } + | { type: "todo"; items: AgentTaskItem[] } | { type: "error"; message: string } | CompactionTimelineItem; diff --git a/packages/protocol/src/chat/rpc-schemas.ts b/packages/protocol/src/chat/rpc-schemas.ts index b3520f4c50..2a44d74410 100644 --- a/packages/protocol/src/chat/rpc-schemas.ts +++ b/packages/protocol/src/chat/rpc-schemas.ts @@ -1,6 +1,8 @@ import { z } from "zod"; import { ChatMessageSchema, ChatRoomDetailSchema } from "./types.js"; +// COMPAT(chatRooms): retained after the v0.3.0 feature removal; remove after 2027-02-09 when mixed-version peers no longer send legacy messages. + export const ChatCreateRequestSchema = z.object({ type: z.literal("chat/create"), requestId: z.string(), diff --git a/packages/protocol/src/chat/types.ts b/packages/protocol/src/chat/types.ts index d8ecd05ab3..30be366631 100644 --- a/packages/protocol/src/chat/types.ts +++ b/packages/protocol/src/chat/types.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +// COMPAT(chatRooms): retained after the v0.3.0 feature removal; remove after 2027-02-09 when mixed-version peers no longer send legacy messages. + export const ChatRoomSchema = z.object({ id: z.string(), name: z.string(), diff --git a/packages/protocol/src/loop/rpc-schemas.ts b/packages/protocol/src/loop/rpc-schemas.ts index 53159afcb2..29996a2a60 100644 --- a/packages/protocol/src/loop/rpc-schemas.ts +++ b/packages/protocol/src/loop/rpc-schemas.ts @@ -1,6 +1,8 @@ import { z } from "zod"; import { AgentProviderSchema } from "../provider-manifest.js"; +// COMPAT(agentLoops): retained after the v0.3.0 feature removal; remove after 2027-02-09 when mixed-version peers no longer send legacy messages. + export const LoopLogEntrySchema = z.object({ seq: z.number().int().positive(), timestamp: z.string(), diff --git a/packages/protocol/src/messages.file-context-actions.test.ts b/packages/protocol/src/messages.file-context-actions.test.ts new file mode 100644 index 0000000000..d0a1ee7c09 --- /dev/null +++ b/packages/protocol/src/messages.file-context-actions.test.ts @@ -0,0 +1,169 @@ +import { describe, expect, test } from "vitest"; +import { + CheckoutDiscardChangesRequestSchema, + CheckoutDiscardChangesResponseSchema, + FileEntryCreateRequestSchema, + FileEntryCreateResponseSchema, + FileEntryDeleteRequestSchema, + FileEntryDeleteResponseSchema, + FileEntryDuplicateRequestSchema, + FileEntryDuplicateResponseSchema, + FileEntryRenameRequestSchema, + FileEntryRenameResponseSchema, + ServerInfoStatusPayloadSchema, +} from "./messages.js"; + +describe("file context action messages", () => { + test("keeps context-action capabilities optional and preserves advertised support", () => { + const legacy = ServerInfoStatusPayloadSchema.parse({ + status: "server_info", + serverId: "server-1", + features: {}, + }); + expect(legacy.features?.fsEntryOps).toBeUndefined(); + expect(legacy.features?.fsEntryDuplicate).toBeUndefined(); + expect(legacy.features?.checkoutDiscardChanges).toBeUndefined(); + + const current = ServerInfoStatusPayloadSchema.parse({ + status: "server_info", + serverId: "server-1", + features: { + fsEntryOps: true, + fsEntryDuplicate: true, + checkoutDiscardChanges: true, + }, + }); + expect(current.features).toMatchObject({ + fsEntryOps: true, + fsEntryDuplicate: true, + checkoutDiscardChanges: true, + }); + }); + + test("rejects discard requests without a path", () => { + expect(() => + CheckoutDiscardChangesRequestSchema.parse({ + type: "checkout.discard_changes.request", + cwd: "/workspace", + paths: [], + requestId: "discard-empty", + }), + ).toThrow(); + }); + + test("round-trips file entry create requests and responses", () => { + const request = { + type: "fs.entry.create.request", + cwd: "/workspace", + parentPath: "src/components", + name: "button.tsx", + kind: "file", + requestId: "create-1", + }; + expect(FileEntryCreateRequestSchema.parse(request)).toEqual(request); + + const response = { + type: "fs.entry.create.response", + payload: { + cwd: "/workspace", + parentPath: "src/components", + path: "src/components/button.tsx", + success: true, + error: null, + requestId: "create-1", + }, + }; + expect(FileEntryCreateResponseSchema.parse(response)).toEqual(response); + }); + + test("round-trips file entry rename requests and responses", () => { + const request = { + type: "fs.entry.rename.request", + cwd: "/workspace", + path: "src/old.ts", + name: "new.ts", + requestId: "rename-1", + }; + expect(FileEntryRenameRequestSchema.parse(request)).toEqual(request); + + const response = { + type: "fs.entry.rename.response", + payload: { + cwd: "/workspace", + path: "src/old.ts", + renamedPath: "src/new.ts", + success: true, + error: null, + requestId: "rename-1", + }, + }; + expect(FileEntryRenameResponseSchema.parse(response)).toEqual(response); + }); + + test("round-trips file entry duplicate requests and responses", () => { + const request = { + type: "fs.entry.duplicate.request", + cwd: "/workspace", + path: "src/button.tsx", + requestId: "duplicate-1", + }; + expect(FileEntryDuplicateRequestSchema.parse(request)).toEqual(request); + + const response = { + type: "fs.entry.duplicate.response", + payload: { + cwd: "/workspace", + path: "src/button.tsx", + duplicatedPath: "src/button copy.tsx", + success: true, + error: null, + requestId: "duplicate-1", + }, + }; + expect(FileEntryDuplicateResponseSchema.parse(response)).toEqual(response); + }); + + test("round-trips file entry delete requests and responses", () => { + const request = { + type: "fs.entry.delete.request", + cwd: "/workspace", + path: "src/old.ts", + requestId: "delete-1", + }; + expect(FileEntryDeleteRequestSchema.parse(request)).toEqual(request); + + const response = { + type: "fs.entry.delete.response", + payload: { + cwd: "/workspace", + path: "src/old.ts", + success: false, + error: "File or folder no longer exists", + requestId: "delete-1", + }, + }; + expect(FileEntryDeleteResponseSchema.parse(response)).toEqual(response); + }); + + test("round-trips checkout discard requests and responses", () => { + const request = { + type: "checkout.discard_changes.request", + cwd: "/workspace", + workspaceId: "workspace-a", + paths: ["src", "README.md"], + requestId: "discard-1", + }; + expect(CheckoutDiscardChangesRequestSchema.parse(request)).toEqual(request); + + const response = { + type: "checkout.discard_changes.response", + payload: { + cwd: "/workspace", + success: false, + error: { code: "UNKNOWN", message: "discard failed" }, + requestId: "discard-1", + }, + }; + expect(CheckoutDiscardChangesResponseSchema.parse(response)).toEqual(response); + }); +}); diff --git a/packages/protocol/src/messages.test.ts b/packages/protocol/src/messages.test.ts index 9e6f628b1f..015164f703 100644 --- a/packages/protocol/src/messages.test.ts +++ b/packages/protocol/src/messages.test.ts @@ -5,6 +5,7 @@ import { parseServerInfoStatusPayload, SessionInboundMessageSchema, SessionOutboundMessageSchema, + WorkspaceProjectDescriptorPayloadSchema, } from "./messages.js"; function workspaceDescriptor(overrides: Record<string, unknown> = {}) { @@ -53,6 +54,28 @@ describe("project icon message security", () => { }); }); +describe("project icon revision compatibility", () => { + const project = { + projectId: "project-1", + projectDisplayName: "Project", + projectRootPath: "/repo/project", + projectKind: "git" as const, + }; + + test("accepts an old project snapshot without an effective icon revision", () => { + expect(WorkspaceProjectDescriptorPayloadSchema.parse(project)).toEqual(project); + }); + + test("accepts an effective icon revision on a new project snapshot", () => { + expect( + WorkspaceProjectDescriptorPayloadSchema.parse({ + ...project, + projectIconRevision: "automatic:none:v1", + }), + ).toEqual({ ...project, projectIconRevision: "automatic:none:v1" }); + }); +}); + describe("workspace descriptor message compatibility", () => { test("old-shaped fetch_workspaces_response without project still parses", () => { const parsed = SessionOutboundMessageSchema.parse( diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index 4ef4b6e9e9..0324acf3e7 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -136,6 +136,35 @@ export const TerminalProfileSchema = z export type TerminalProfile = z.infer<typeof TerminalProfileSchema>; +/** + * A named launch bundle: a provider plus the agent-config values a client would + * otherwise set one control at a time. Field names mirror `AgentSessionConfig` + * so applying a profile is a copy rather than a translation table. + * + * There is deliberately no system prompt here. `AgentSessionConfig.systemPrompt` + * is creation-only, so a profile carrying one would apply when starting a new + * agent and silently do nothing when applied to a running one. + */ +export const AgentProfileSchema = z + .object({ + id: z.string(), + name: z.string(), + /** A key into the client's icon registry, not a glyph. Unknown keys draw the default. */ + icon: z.string().optional(), + /** An identity colour name shared with host badges. Unknown values draw unthemed. */ + color: z.string().optional(), + provider: z.string(), + model: z.string().optional(), + modeId: z.string().optional(), + thinkingOptionId: z.string().optional(), + featureValues: z.record(z.string(), z.unknown()).optional(), + /** Free text, surfaced to orchestrating agents by the `list_profiles` MCP tool. */ + notes: z.string().optional(), + }) + .passthrough(); + +export type AgentProfile = z.infer<typeof AgentProfileSchema>; + const MutableBrowserToolsConfigSchema = z .object({ enabled: z.boolean().default(false), @@ -162,6 +191,7 @@ export const MutableDaemonConfigSchema = z enableTerminalAgentHooks: z.boolean().default(false), appendSystemPrompt: z.string().default(""), terminalProfiles: z.array(TerminalProfileSchema).optional(), + agentProfiles: z.array(AgentProfileSchema).optional(), }) .passthrough(); @@ -179,6 +209,7 @@ export const MutableDaemonConfigPatchSchema = z enableTerminalAgentHooks: z.boolean().optional(), appendSystemPrompt: z.string().optional(), terminalProfiles: z.array(TerminalProfileSchema).optional(), + agentProfiles: z.array(AgentProfileSchema).optional(), }) .partial() .passthrough(); @@ -630,6 +661,9 @@ export const AgentTimelineItemPayloadSchema: z.ZodType<AgentTimelineItem, unknow z.object({ text: z.string(), completed: z.boolean(), + id: z.string().optional(), + status: z.enum(["pending", "in_progress", "completed"]).optional(), + activeForm: z.string().optional(), }), ), }), @@ -1072,6 +1106,26 @@ export const SendAgentMessageSchema = z.object({ // Agent RPCs (requestId-correlated) // ============================================================================ +const DirectorySyncRequestSchema = z.object({ + generation: z.string().optional(), + afterSeq: z.number().int().nonnegative().optional(), +}); + +const DirectorySyncRemovalSchema = z.object({ + id: z.string(), + seq: z.number().int().positive(), +}); + +const DirectorySyncMetadataSchema = z.object({ + generation: z.string(), + headSeq: z.number().int().nonnegative(), + mode: z.enum(["snapshot", "changes"]), + reason: z + .enum(["no_cursor", "generation_changed", "cursor_expired", "changes_too_large"]) + .optional(), + removals: z.array(DirectorySyncRemovalSchema), +}); + export const FetchAgentsRequestMessageSchema = z.object({ type: z.literal("fetch_agents_request"), requestId: z.string(), @@ -1096,6 +1150,8 @@ export const FetchAgentsRequestMessageSchema = z.object({ subscriptionId: z.string().optional(), }) .optional(), + // COMPAT(directorySync): added in v0.3.x, remove optional after 2027-02-12. + sync: DirectorySyncRequestSchema.optional(), }); const WorkspaceStateBucketSchema = z.enum([ @@ -1136,11 +1192,15 @@ export const FetchWorkspacesRequestMessageSchema = z.object({ subscriptionId: z.string().optional(), }) .optional(), + // COMPAT(directorySync): added in v0.3.x, remove optional after 2027-02-12. + sync: DirectorySyncRequestSchema.optional(), }); export const ProjectListRequestMessageSchema = z.object({ type: z.literal("project.list.request"), requestId: z.string(), + // COMPAT(directorySync): added in v0.3.x, remove optional after 2027-02-12. + sync: DirectorySyncRequestSchema.optional(), }); export const FetchAgentHistoryRequestMessageSchema = z.object({ @@ -1549,6 +1609,38 @@ export const SetAgentFeatureResponseMessageSchema = z.object({ payload: AgentActionResponsePayloadSchema, }); +/** + * Every agent-config value a client can change in one shot. Each field is + * optional and an omitted field is left alone; `null` on model and thinking + * clears them, matching the single-field RPCs above. + */ +export const AgentConfigApplySchema = z.object({ + modelId: z.string().nullable().optional(), + modeId: z.string().optional(), + thinkingOptionId: z.string().nullable().optional(), + featureValues: z.record(z.string(), z.unknown()).optional(), +}); + +export type AgentConfigApply = z.infer<typeof AgentConfigApplySchema>; + +/** + * Applies a whole config bundle to one agent. The four single-field RPCs above + * stay for individual control edits. One request prevents client interruption + * and other mutations from interleaving between bundle steps; provider-level + * rejection can still leave earlier steps applied. + */ +export const AgentConfigApplyRequestMessageSchema = z.object({ + type: z.literal("agent.config.apply.request"), + agentId: z.string(), + config: AgentConfigApplySchema, + requestId: z.string(), +}); + +export const AgentConfigApplyResponseMessageSchema = z.object({ + type: z.literal("agent.config.apply.response"), + payload: AgentActionResponsePayloadSchema, +}); + export const AgentDetachRequestMessageSchema = z.object({ type: z.literal("agent.detach.request"), agentId: z.string(), @@ -1793,6 +1885,14 @@ export const CheckoutRefreshRequestSchema = z.object({ requestId: z.string(), }); +export const CheckoutDiscardChangesRequestSchema = z.object({ + type: z.literal("checkout.discard_changes.request"), + cwd: z.string(), + workspaceId: OptionalWorkspaceGitIdSchema, + paths: z.array(z.string()).min(1), + requestId: z.string(), +}); + export const CheckoutPrCreateRequestSchema = z.object({ type: z.literal("checkout_pr_create_request"), cwd: WorkspaceGitCwdSchema, @@ -2258,6 +2358,8 @@ const DiffHunkSchema = z.object({ const ParsedDiffFileSchema = z.object({ path: z.string(), + // COMPAT(diffOldPath): added in v0.3.0, remove gate after 2027-02-09. + oldPath: z.string().optional(), isNew: z.boolean(), isDeleted: z.boolean(), additions: z.number(), @@ -2348,6 +2450,37 @@ export const FileWriteRequestSchema = z.object({ workspaceId: z.string().optional(), }); +export const FileEntryCreateRequestSchema = z.object({ + type: z.literal("fs.entry.create.request"), + cwd: z.string(), + parentPath: z.string(), + name: z.string(), + kind: z.enum(["file", "directory"]), + requestId: z.string(), +}); + +export const FileEntryRenameRequestSchema = z.object({ + type: z.literal("fs.entry.rename.request"), + cwd: z.string(), + path: z.string(), + name: z.string(), + requestId: z.string(), +}); + +export const FileEntryDuplicateRequestSchema = z.object({ + type: z.literal("fs.entry.duplicate.request"), + cwd: z.string(), + path: z.string(), + requestId: z.string(), +}); + +export const FileEntryDeleteRequestSchema = z.object({ + type: z.literal("fs.entry.delete.request"), + cwd: z.string(), + path: z.string(), + requestId: z.string(), +}); + export const ProjectIconRequestSchema = z.object({ type: z.literal("project_icon_request"), cwd: z.string(), @@ -2428,6 +2561,19 @@ export const RegisterPushTokenMessageSchema = z.object({ token: z.string(), }); +export const PushUnregisterRequestSchema = z.object({ + type: z.literal("push.unregister.request"), + token: z.string(), + requestId: z.string(), +}); + +export const PushUnregisterResponseSchema = z.object({ + type: z.literal("push.unregister.response"), + payload: z.object({ + requestId: z.string(), + }), +}); + // ============================================================================ // Terminal Messages // ============================================================================ @@ -2708,6 +2854,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [ SetAgentModelRequestMessageSchema, SetAgentThinkingRequestMessageSchema, SetAgentFeatureRequestMessageSchema, + AgentConfigApplyRequestMessageSchema, AgentDetachRequestMessageSchema, AgentRewindRequestMessageSchema, AgentPermissionResponseMessageSchema, @@ -2720,6 +2867,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [ CheckoutPullRequestSchema, CheckoutPushRequestSchema, CheckoutRefreshRequestSchema, + CheckoutDiscardChangesRequestSchema, CheckoutPrCreateRequestSchema, CheckoutPrMergeRequestSchema, CheckoutForgeSetAutoMergeRequestSchema, @@ -2760,6 +2908,10 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [ FileSubscribeRequestSchema, FileUnsubscribeRequestSchema, FileWriteRequestSchema, + FileEntryCreateRequestSchema, + FileEntryRenameRequestSchema, + FileEntryDuplicateRequestSchema, + FileEntryDeleteRequestSchema, ProjectIconRequestSchema, ProjectIconGetRequestSchema, FileDownloadTokenRequestSchema, @@ -2769,6 +2921,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [ PingMessageSchema, ListCommandsRequestSchema, RegisterPushTokenMessageSchema, + PushUnregisterRequestSchema, ListTerminalsRequestSchema, SubscribeTerminalsRequestSchema, UnsubscribeTerminalsRequestSchema, @@ -2973,6 +3126,8 @@ export const ServerInfoStatusPayloadSchema = z providersSnapshot: z.boolean().optional(), // COMPAT(providersSnapshotCwd): added in v0.3.2, remove gate after 2027-02-10. providersSnapshotCwd: z.boolean().optional(), + // COMPAT(directorySync): added in v0.3.x, remove gate after 2027-02-12. + directorySync: z.boolean().optional(), // COMPAT(checkoutForgeSetAutoMerge): added in v0.1.106, remove old // checkoutGithubSetAutoMerge fallback after 2026-12-28. checkoutForgeSetAutoMerge: z.boolean().optional(), @@ -2987,6 +3142,8 @@ export const ServerInfoStatusPayloadSchema = z daemonStatusRpc: z.boolean().optional(), // COMPAT(relayConfig): added in v0.2.6, remove gate after 2027-01-31. relayConfig: z.boolean().optional(), + // COMPAT(pushTokenRevocation): added in v0.3.2, remove gate after 2027-02-10. + pushTokenRevocation: z.boolean().optional(), // COMPAT(terminalRestoreModes): added in v0.1.81, remove gate after 2026-11-23. "terminal-restore-modes": z.boolean().optional(), // COMPAT(terminalInputModeReplay): added in v0.2.6, remove gate after 2027-02-02. @@ -3067,6 +3224,19 @@ export const ServerInfoStatusPayloadSchema = z workspaceScriptManagement: z.boolean().optional(), // COMPAT(projectCustomIcon): added in v0.2.0, remove after 2027-01-20. projectCustomIcon: z.boolean().optional(), + // COMPAT(fsEntryOps): added in v0.3.0, remove gate after 2027-02-08. + fsEntryOps: z.boolean().optional(), + // COMPAT(fsEntryDuplicate): added in v0.3.0, remove gate after 2027-02-09. + fsEntryDuplicate: z.boolean().optional(), + // COMPAT(checkoutDiscardChanges): added in v0.3.0, remove gate after 2027-02-08. + checkoutDiscardChanges: z.boolean().optional(), + // COMPAT(agentProfiles): added in v0.3.2, remove gate after 2027-02-11. + // An older daemon parses its persisted config strictly, so writing + // agentProfiles to one is silently dropped. The client hides the feature + // rather than letting a save appear to succeed. + agentProfiles: z.boolean().optional(), + // COMPAT(agentConfigApply): added in v0.3.2, remove gate after 2027-02-11. + agentConfigApply: z.boolean().optional(), }) .optional(), }) @@ -3382,6 +3552,8 @@ export const WorkspaceDescriptorPayloadSchema = z // Old daemons omit it; absent means the client falls back to GitHub. forge: z.string().optional(), project: ProjectPlacementPayloadSchema.optional(), + // COMPAT(directorySync): sequence of this latest directory projection. + syncSeq: z.number().int().positive().optional(), }) .transform((workspace) => ({ ...workspace, @@ -3395,10 +3567,14 @@ export const AgentUpdateMessageSchema = z.object({ kind: z.literal("upsert"), agent: AgentSnapshotPayloadSchema, project: ProjectPlacementPayloadSchema.nullable().optional(), + generation: z.string().optional(), + seq: z.number().int().positive().optional(), }), z.object({ kind: z.literal("remove"), agentId: z.string(), + generation: z.string().optional(), + seq: z.number().int().positive().optional(), }), ]), }); @@ -3456,6 +3632,8 @@ const AgentDirectoryResponseEntrySchema = z.object({ // anyway; sending it keeps the client from re-deriving a second opinion that // could disagree with the ranking it is explaining. searchMatches: z.array(AgentSearchMatchSchema).optional(), + // COMPAT(directorySync): sequence of this latest directory projection. + syncSeq: z.number().int().positive().optional(), }); const AgentDirectoryPageInfoSchema = z.object({ @@ -3471,6 +3649,7 @@ export const FetchAgentsResponseMessageSchema = z.object({ subscriptionId: z.string().nullable().optional(), entries: z.array(AgentDirectoryResponseEntrySchema), pageInfo: AgentDirectoryPageInfoSchema, + sync: DirectorySyncMetadataSchema.optional(), }), }); @@ -3508,8 +3687,14 @@ export const WorkspaceProjectDescriptorPayloadSchema = z.object({ projectCustomName: z.string().nullable().optional(), // COMPAT(projectCustomIcon): added in v0.2.0, remove after 2027-01-20. projectCustomIconRevision: z.string().nullable().optional(), + // Fingerprints the effective icon, including automatic discovery and the + // absence of an icon. Clients may persist icon results against this value. + // COMPAT(projectIconCache): added in v0.2.7, remove optional after 2027-02-12. + projectIconRevision: z.string().optional(), projectRootPath: z.string(), projectKind: z.enum(["git", "non_git", "directory"]), + // COMPAT(directorySync): sequence of this latest directory projection. + syncSeq: z.number().int().positive().optional(), }); export const FetchWorkspacesResponseMessageSchema = z.object({ @@ -3527,6 +3712,7 @@ export const FetchWorkspacesResponseMessageSchema = z.object({ prevCursor: z.string().nullable(), hasMore: z.boolean(), }), + sync: DirectorySyncMetadataSchema.optional(), }), }); @@ -3536,6 +3722,8 @@ export const WorkspaceUpdateMessageSchema = z.object({ z.object({ kind: z.literal("upsert"), workspace: WorkspaceDescriptorPayloadSchema, + generation: z.string().optional(), + seq: z.number().int().positive().optional(), }), z.object({ kind: z.literal("remove"), @@ -3550,6 +3738,8 @@ export const WorkspaceUpdateMessageSchema = z.object({ // Project removal is represented on the existing workspace update channel // so old clients can still parse the message and ignore the extra field. removedProjectId: z.string().optional(), + generation: z.string().optional(), + seq: z.number().int().positive().optional(), }), ]), }); @@ -3557,8 +3747,18 @@ export const WorkspaceUpdateMessageSchema = z.object({ export const ProjectUpdateMessageSchema = z.object({ type: z.literal("project.update"), payload: z.discriminatedUnion("kind", [ - z.object({ kind: z.literal("upsert"), project: WorkspaceProjectDescriptorPayloadSchema }), - z.object({ kind: z.literal("remove"), projectId: z.string() }), + z.object({ + kind: z.literal("upsert"), + project: WorkspaceProjectDescriptorPayloadSchema, + generation: z.string().optional(), + seq: z.number().int().positive().optional(), + }), + z.object({ + kind: z.literal("remove"), + projectId: z.string(), + generation: z.string().optional(), + seq: z.number().int().positive().optional(), + }), ]), }); @@ -3567,6 +3767,7 @@ export const ProjectListResponseMessageSchema = z.object({ payload: z.object({ requestId: z.string(), projects: z.array(WorkspaceProjectDescriptorPayloadSchema), + sync: DirectorySyncMetadataSchema.optional(), }), }); @@ -4162,6 +4363,7 @@ export const ReadProjectConfigResponseMessageSchema = z.object({ ok: z.literal(true), config: PaseoConfigRawSchema.nullable(), revision: PaseoConfigRevisionSchema.nullable(), + hasUncommittedWorktreeSetupChanges: z.boolean().optional(), }), z.object({ requestId: z.string(), @@ -4183,6 +4385,7 @@ export const WriteProjectConfigResponseMessageSchema = z.object({ ok: z.literal(true), config: PaseoConfigRawSchema, revision: PaseoConfigRevisionSchema, + hasUncommittedWorktreeSetupChanges: z.boolean().optional(), }), z.object({ requestId: z.string(), @@ -4579,6 +4782,16 @@ export const CheckoutGithubSetAutoMergeResponseSchema = z.object({ }), }); +export const CheckoutDiscardChangesResponseSchema = z.object({ + type: z.literal("checkout.discard_changes.response"), + payload: z.object({ + cwd: z.string(), + success: z.boolean(), + error: CheckoutErrorSchema.nullable(), + requestId: z.string(), + }), +}); + export const CheckoutCommitsListResponseSchema = z.object({ type: z.literal("checkout.commits.list.response"), payload: z.object({ @@ -5043,6 +5256,53 @@ export const FileWriteResponseSchema = z.object({ }), }); +export const FileEntryCreateResponseSchema = z.object({ + type: z.literal("fs.entry.create.response"), + payload: z.object({ + cwd: z.string(), + parentPath: z.string(), + path: z.string().nullable(), + success: z.boolean(), + error: z.string().nullable(), + requestId: z.string(), + }), +}); + +export const FileEntryRenameResponseSchema = z.object({ + type: z.literal("fs.entry.rename.response"), + payload: z.object({ + cwd: z.string(), + path: z.string(), + renamedPath: z.string().nullable(), + success: z.boolean(), + error: z.string().nullable(), + requestId: z.string(), + }), +}); + +export const FileEntryDuplicateResponseSchema = z.object({ + type: z.literal("fs.entry.duplicate.response"), + payload: z.object({ + cwd: z.string(), + path: z.string(), + duplicatedPath: z.string().nullable(), + success: z.boolean(), + error: z.string().nullable(), + requestId: z.string(), + }), +}); + +export const FileEntryDeleteResponseSchema = z.object({ + type: z.literal("fs.entry.delete.response"), + payload: z.object({ + cwd: z.string(), + path: z.string(), + success: z.boolean(), + error: z.string().nullable(), + requestId: z.string(), + }), +}); + export const FileUpdateSchema = z.object({ type: z.literal("fs.file.update"), payload: z.object({ @@ -5554,6 +5814,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ DictationStreamErrorMessageSchema, StatusMessageSchema, PongMessageSchema, + PushUnregisterResponseSchema, RpcErrorMessageSchema, ArtifactMessageSchema, AgentUpdateMessageSchema, @@ -5612,6 +5873,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ SetAgentModelResponseMessageSchema, SetAgentThinkingResponseMessageSchema, SetAgentFeatureResponseMessageSchema, + AgentConfigApplyResponseMessageSchema, AgentDetachResponseMessageSchema, AgentRewindResponseMessageSchema, UpdateAgentResponseMessageSchema, @@ -5638,6 +5900,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ CheckoutPullResponseSchema, CheckoutPushResponseSchema, CheckoutRefreshResponseSchema, + CheckoutDiscardChangesResponseSchema, CheckoutPrCreateResponseSchema, CheckoutPrMergeResponseSchema, CheckoutForgeSetAutoMergeResponseSchema, @@ -5665,6 +5928,10 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ FileSubscribeResponseSchema, FileUnsubscribeResponseSchema, FileWriteResponseSchema, + FileEntryCreateResponseSchema, + FileEntryRenameResponseSchema, + FileEntryDuplicateResponseSchema, + FileEntryDeleteResponseSchema, FileUpdateSchema, ProjectIconResponseSchema, ProjectIconGetResponseSchema, @@ -5801,6 +6068,7 @@ export type SetAgentModeResponseMessage = z.infer<typeof SetAgentModeResponseMes export type SetAgentModelResponseMessage = z.infer<typeof SetAgentModelResponseMessageSchema>; export type SetAgentThinkingResponseMessage = z.infer<typeof SetAgentThinkingResponseMessageSchema>; export type SetAgentFeatureResponseMessage = z.infer<typeof SetAgentFeatureResponseMessageSchema>; +export type AgentConfigApplyResponseMessage = z.infer<typeof AgentConfigApplyResponseMessageSchema>; export type AgentDetachResponseMessage = z.infer<typeof AgentDetachResponseMessageSchema>; export type AgentRewindResponseMessage = z.infer<typeof AgentRewindResponseMessageSchema>; export type UpdateAgentResponseMessage = z.infer<typeof UpdateAgentResponseMessageSchema>; @@ -5971,6 +6239,7 @@ export type SetAgentModeRequestMessage = z.infer<typeof SetAgentModeRequestMessa export type SetAgentModelRequestMessage = z.infer<typeof SetAgentModelRequestMessageSchema>; export type SetAgentThinkingRequestMessage = z.infer<typeof SetAgentThinkingRequestMessageSchema>; export type SetAgentFeatureRequestMessage = z.infer<typeof SetAgentFeatureRequestMessageSchema>; +export type AgentConfigApplyRequestMessage = z.infer<typeof AgentConfigApplyRequestMessageSchema>; export type AgentDetachRequestMessage = z.infer<typeof AgentDetachRequestMessageSchema>; export type AgentPermissionResponseMessage = z.infer<typeof AgentPermissionResponseMessageSchema>; export type CheckoutStatusRequest = z.infer<typeof CheckoutStatusRequestSchema>; @@ -5992,6 +6261,8 @@ export type CheckoutPushRequest = z.infer<typeof CheckoutPushRequestSchema>; export type CheckoutPushResponse = z.infer<typeof CheckoutPushResponseSchema>; export type CheckoutRefreshRequest = z.infer<typeof CheckoutRefreshRequestSchema>; export type CheckoutRefreshResponse = z.infer<typeof CheckoutRefreshResponseSchema>; +export type CheckoutDiscardChangesRequest = z.infer<typeof CheckoutDiscardChangesRequestSchema>; +export type CheckoutDiscardChangesResponse = z.infer<typeof CheckoutDiscardChangesResponseSchema>; export type CheckoutCommitFile = z.infer<typeof CheckoutCommitFileSchema>; export type CheckoutCommit = z.infer<typeof CheckoutCommitSchema>; export type CheckoutCommitsListRequest = z.infer<typeof CheckoutCommitsListRequestSchema>; @@ -6095,6 +6366,14 @@ export type FileUnsubscribeRequest = z.infer<typeof FileUnsubscribeRequestSchema export type FileUnsubscribeResponse = z.infer<typeof FileUnsubscribeResponseSchema>; export type FileWriteRequest = z.infer<typeof FileWriteRequestSchema>; export type FileWriteResponse = z.infer<typeof FileWriteResponseSchema>; +export type FileEntryCreateRequest = z.infer<typeof FileEntryCreateRequestSchema>; +export type FileEntryCreateResponse = z.infer<typeof FileEntryCreateResponseSchema>; +export type FileEntryRenameRequest = z.infer<typeof FileEntryRenameRequestSchema>; +export type FileEntryRenameResponse = z.infer<typeof FileEntryRenameResponseSchema>; +export type FileEntryDuplicateRequest = z.infer<typeof FileEntryDuplicateRequestSchema>; +export type FileEntryDuplicateResponse = z.infer<typeof FileEntryDuplicateResponseSchema>; +export type FileEntryDeleteRequest = z.infer<typeof FileEntryDeleteRequestSchema>; +export type FileEntryDeleteResponse = z.infer<typeof FileEntryDeleteResponseSchema>; export type FileWriteResult = z.infer<typeof FileWriteResultSchema>; export type FileUpdate = z.infer<typeof FileUpdateSchema>; export type ProjectIconRequest = z.infer<typeof ProjectIconRequestSchema>; @@ -6116,6 +6395,8 @@ export type ClientHeartbeatMessage = z.infer<typeof ClientHeartbeatMessageSchema export type ListCommandsRequest = z.infer<typeof ListCommandsRequestSchema>; export type ListCommandsResponse = z.infer<typeof ListCommandsResponseSchema>; export type RegisterPushTokenMessage = z.infer<typeof RegisterPushTokenMessageSchema>; +export type PushUnregisterRequest = z.infer<typeof PushUnregisterRequestSchema>; +export type PushUnregisterResponse = z.infer<typeof PushUnregisterResponseSchema>; // Terminal message types export type ListTerminalsRequest = z.infer<typeof ListTerminalsRequestSchema>; diff --git a/packages/protocol/src/messages.wire-compat.test.ts b/packages/protocol/src/messages.wire-compat.test.ts index ebf47fe19e..22d22acacf 100644 --- a/packages/protocol/src/messages.wire-compat.test.ts +++ b/packages/protocol/src/messages.wire-compat.test.ts @@ -117,6 +117,40 @@ describe("wire schema compatibility", () => { }); }); + test("task progress fields are optional on the wire", () => { + expect( + AgentTimelineItemPayloadSchema.parse({ + type: "todo", + items: [{ text: "Legacy task", completed: false }], + }), + ).toEqual({ type: "todo", items: [{ text: "Legacy task", completed: false }] }); + expect( + AgentTimelineItemPayloadSchema.parse({ + type: "todo", + items: [ + { + id: "task-1", + text: "Current task", + activeForm: "Working on current task", + status: "in_progress", + completed: false, + }, + ], + }), + ).toEqual({ + type: "todo", + items: [ + { + id: "task-1", + text: "Current task", + activeForm: "Working on current task", + status: "in_progress", + completed: false, + }, + ], + }); + }); + test("sub_agent tool-call payload still parses against the v0.1.65-beta.3 schema", () => { const parsed = LegacySubAgentToolCallSchema.parse({ type: "tool_call", diff --git a/packages/protocol/src/messages.workspaces.test.ts b/packages/protocol/src/messages.workspaces.test.ts index bedeeb0829..aaceaafd6f 100644 --- a/packages/protocol/src/messages.workspaces.test.ts +++ b/packages/protocol/src/messages.workspaces.test.ts @@ -97,6 +97,59 @@ describe("workspace message schemas", () => { expect(activeScoped.scope).toBe("active"); }); + test("parses optional sequenced directory requests and responses", () => { + expect( + SessionInboundMessageSchema.parse({ + type: "project.list.request", + requestId: "projects-sync", + sync: { generation: "daemon-generation", afterSeq: 7 }, + }), + ).toMatchObject({ sync: { generation: "daemon-generation", afterSeq: 7 } }); + expect( + SessionInboundMessageSchema.parse({ + type: "fetch_workspaces_request", + requestId: "workspaces-sync", + sync: { generation: "daemon-generation", afterSeq: 11 }, + }), + ).toMatchObject({ sync: { generation: "daemon-generation", afterSeq: 11 } }); + expect( + SessionInboundMessageSchema.parse({ + type: "fetch_agents_request", + requestId: "agents-sync", + scope: "active", + sync: { generation: "daemon-generation", afterSeq: 13 }, + }), + ).toMatchObject({ sync: { generation: "daemon-generation", afterSeq: 13 } }); + + const response = SessionOutboundMessageSchema.parse({ + type: "project.list.response", + payload: { + requestId: "projects-sync", + projects: [ + { + projectId: "project-1", + projectDisplayName: "Project", + projectRootPath: "/repo", + projectKind: "git", + syncSeq: 12, + }, + ], + sync: { + generation: "daemon-generation", + headSeq: 12, + mode: "changes", + removals: [{ id: "project-removed", seq: 10 }], + }, + }, + }); + expect(response).toMatchObject({ + payload: { + projects: [{ projectId: "project-1", syncSeq: 12 }], + sync: { headSeq: 12, removals: [{ id: "project-removed", seq: 10 }] }, + }, + }); + }); + test("parses agent_update without project placement", () => { const result = SessionOutboundMessageSchema.safeParse({ type: "agent_update", diff --git a/packages/protocol/tests/validation/ws-outbound.test.ts b/packages/protocol/tests/validation/ws-outbound.test.ts index 3aede5cb2b..e2e3e3170a 100644 --- a/packages/protocol/tests/validation/ws-outbound.test.ts +++ b/packages/protocol/tests/validation/ws-outbound.test.ts @@ -138,6 +138,34 @@ const SourceSchema = z.object({ ); }); + it("accepts project config responses with and without setup commit status", () => { + const payload = { + requestId: "project-config-read", + repoRoot: "/repo", + ok: true, + config: null, + revision: null, + }; + const envelope = ( + responsePayload: typeof payload & { + hasUncommittedWorktreeSetupChanges?: boolean; + }, + ) => ({ + type: "session", + message: { + type: "read_project_config_response", + payload: responsePayload, + }, + }); + + expect(GeneratedWSOutboundMessageSchema.safeParse(envelope(payload)).success).toBe(true); + expect( + GeneratedWSOutboundMessageSchema.safeParse( + envelope({ ...payload, hasUncommittedWorktreeSetupChanges: true }), + ).success, + ).toBe(true); + }); + it("accepts a compact provider snapshot envelope", () => { const envelope = { type: "session", diff --git a/packages/relay/package.json b/packages/relay/package.json index 8a8172adb3..fc8f9ffd95 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/relay", - "version": "0.3.1", + "version": "0.4.0", "description": "Paseo relay for bridging daemon and client connections", "files": [ "dist", diff --git a/packages/server/package.json b/packages/server/package.json index 97afa0b88f..a210a6130d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/server", - "version": "0.3.1", + "version": "0.4.0", "description": "Paseo backend server", "files": [ "dist/server", @@ -72,12 +72,12 @@ "@agentclientprotocol/sdk": "^0.17.1", "@anthropic-ai/claude-agent-sdk": "^0.3.220", "@anthropic-ai/sdk": "^0.104.2", - "@getpaseo/client": "0.3.1", - "@getpaseo/highlight": "0.3.1", - "@getpaseo/protocol": "0.3.1", - "@getpaseo/relay": "0.3.1", - "@getpaseo/workspace-helper": "0.3.1", - "@getpaseo/workspace-runtime-contract": "0.3.1", + "@getpaseo/client": "0.4.0", + "@getpaseo/highlight": "0.4.0", + "@getpaseo/protocol": "0.4.0", + "@getpaseo/relay": "0.4.0", + "@getpaseo/workspace-helper": "0.4.0", + "@getpaseo/workspace-runtime-contract": "0.4.0", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "^1.20.1", "@opencode-ai/sdk": "1.14.46", diff --git a/packages/server/scripts/supervisor.logging.test.ts b/packages/server/scripts/supervisor.logging.test.ts index 61255358f2..9f61aa283d 100644 --- a/packages/server/scripts/supervisor.logging.test.ts +++ b/packages/server/scripts/supervisor.logging.test.ts @@ -189,46 +189,21 @@ describe("supervisor durable logging", () => { expect(result.log).toContain('"workerPid":'); }); - test("keeps a worker alive while heartbeats continue", async () => { + test("does not restart a worker based on heartbeat absence", async () => { const result = await runSupervisorFixture({ - timeoutMs: 10_000, - workerSource: ` - setInterval(() => { - process.send?.({ type: "paseo:worker-heartbeat" }); - }, 250); - setTimeout(() => { - process.send?.({ type: "paseo:shutdown", reason: "healthy_heartbeat_test_complete" }); - }, 6000); - `, - }); - - expect(result.code).toBe(0); - expect(result.signal).toBeNull(); - expect(result.log).toContain('"reason":"healthy_heartbeat_test_complete"'); - expect(result.log).not.toContain('"msg":"Worker heartbeat timed out; restarting worker"'); - }, 10_000); - - test("tolerates a transient seven-second heartbeat pause", async () => { - const result = await runSupervisorFixture({ - timeoutMs: 10_000, + timeoutMs: 20_000, workerSource: ` import { existsSync, writeFileSync } from "node:fs"; const marker = process.argv[1] + ".started"; if (!existsSync(marker)) { writeFileSync(marker, "started"); - let heartbeatCount = 0; - const heartbeat = setInterval(() => { - process.send?.({ type: "paseo:worker-heartbeat" }); - heartbeatCount += 1; - if (heartbeatCount === 3) clearInterval(heartbeat); - }, 100); setTimeout(() => { - process.send?.({ type: "paseo:shutdown", reason: "heartbeat_pause_tolerated" }); - }, 7_000); + process.send?.({ type: "paseo:shutdown", reason: "silent_worker_test_complete" }); + }, 16_000); setInterval(() => {}, 1_000); } else { - process.send?.({ type: "paseo:shutdown", reason: "unexpected_heartbeat_restart" }); + process.send?.({ type: "paseo:shutdown", reason: "unexpected_silent_worker_restart" }); setInterval(() => {}, 1_000); } `, @@ -236,10 +211,10 @@ describe("supervisor durable logging", () => { expect(result.code).toBe(0); expect(result.signal).toBeNull(); - expect(result.log).toContain('"reason":"heartbeat_pause_tolerated"'); - expect(result.log).not.toContain('"reason":"unexpected_heartbeat_restart"'); + expect(result.log).toContain('"reason":"silent_worker_test_complete"'); + expect(result.log).not.toContain('"reason":"unexpected_silent_worker_restart"'); expect(result.log).not.toContain('"msg":"Worker heartbeat timed out; restarting worker"'); - }, 10_000); + }, 25_000); test.skipIf(isPlatform("win32"))( "forces shutdown when a worker ignores SIGTERM", @@ -262,42 +237,6 @@ describe("supervisor durable logging", () => { 20_000, ); - // POSIX-only: the watchdog uses SIGKILL after its graceful shutdown window. - test.skipIf(isPlatform("win32"))( - "restarts a worker that stops heartbeating", - async () => { - const result = await runSupervisorFixture({ - timeoutMs: 35_000, - workerSource: ` - import { existsSync, writeFileSync } from "node:fs"; - - const marker = process.argv[1] + ".started"; - if (!existsSync(marker)) { - writeFileSync(marker, "started"); - let heartbeatCount = 0; - const heartbeat = setInterval(() => { - process.send?.({ type: "paseo:worker-heartbeat" }); - heartbeatCount += 1; - if (heartbeatCount === 3) clearInterval(heartbeat); - }, 100); - process.on("SIGTERM", () => {}); - setInterval(() => {}, 1000); - } else { - process.send?.({ type: "paseo:shutdown", reason: "watchdog_test_complete" }); - setInterval(() => {}, 1000); - } - `, - }); - - expect(result.code).toBe(0); - expect(result.signal).toBeNull(); - expect(result.log).toContain('"msg":"Worker heartbeat timed out; restarting worker"'); - expect(result.log).toContain('"msg":"Worker did not exit after SIGTERM; forcing SIGKILL"'); - expect(result.log).toContain('"signal":"SIGKILL"'); - }, - 40_000, - ); - test.skipIf(isPlatform("win32"))( "restarts after worker exit while a descendant retains the worker stdio", async () => { diff --git a/packages/server/scripts/supervisor.ts b/packages/server/scripts/supervisor.ts index 4554e8d548..9c6b33f06b 100644 --- a/packages/server/scripts/supervisor.ts +++ b/packages/server/scripts/supervisor.ts @@ -5,7 +5,6 @@ import { createStream as createRotatingFileStream } from "rotating-file-stream"; import { signalProcessTree } from "../src/utils/tree-kill.js"; const WORKER_HEARTBEAT_INTERVAL_MS = 1_000; -const WORKER_HEARTBEAT_TIMEOUT_MS = 15_000; const WORKER_TERMINATION_GRACE_MS = 10_000; interface SupervisorLogFileOptions { @@ -34,10 +33,6 @@ interface SupervisorHeartbeatMessage { type: "paseo:supervisor-heartbeat"; } -interface WorkerHeartbeatMessage { - type: "paseo:worker-heartbeat"; -} - interface SupervisorOptions { name: string; startupMessage: string; @@ -93,15 +88,6 @@ function parseLifecycleMessage(msg: unknown): WorkerLifecycleMessage | null { return null; } -function isWorkerHeartbeatMessage(msg: unknown): msg is WorkerHeartbeatMessage { - return ( - typeof msg === "object" && - msg !== null && - "type" in msg && - (msg as { type?: unknown }).type === "paseo:worker-heartbeat" - ); -} - function toRotatingFileStreamSize(size: string): string { const trimmed = size.trim(); const match = trimmed.match(/^(\d+)\s*([bBkKmMgG])?$/); @@ -250,7 +236,6 @@ export function runSupervisor(options: SupervisorOptions): SupervisorController } const currentChild = child; - let lastWorkerHeartbeatAt = Date.now(); const heartbeat = setInterval(() => { const message: SupervisorHeartbeatMessage = { type: "paseo:supervisor-heartbeat" }; if (currentChild.connected) { @@ -267,23 +252,6 @@ export function runSupervisor(options: SupervisorOptions): SupervisorController }, WORKER_HEARTBEAT_INTERVAL_MS); heartbeat.unref(); - const workerWatchdog = setInterval(() => { - if (child !== currentChild || restarting || shuttingDown) { - return; - } - const heartbeatAgeMs = Date.now() - lastWorkerHeartbeatAt; - if (heartbeatAgeMs < WORKER_HEARTBEAT_TIMEOUT_MS) { - return; - } - writeLifecycleLog("Worker heartbeat timed out; restarting worker", { - heartbeatAgeMs, - supervisorPid: process.pid, - workerPid: currentChild.pid ?? null, - }); - requestRestart("worker_heartbeat_timeout"); - }, WORKER_HEARTBEAT_INTERVAL_MS); - workerWatchdog.unref(); - child.on("disconnect", () => { writeLifecycleLog("Worker IPC channel disconnected"); }); @@ -299,10 +267,6 @@ export function runSupervisor(options: SupervisorOptions): SupervisorController }); child.on("message", (msg: unknown) => { - if (isWorkerHeartbeatMessage(msg)) { - lastWorkerHeartbeatAt = Date.now(); - return; - } const lifecycleMessage = parseLifecycleMessage(msg); if (!lifecycleMessage) { return; @@ -333,7 +297,6 @@ export function runSupervisor(options: SupervisorOptions): SupervisorController child.on("exit", (code, signal) => { clearInterval(heartbeat); - clearInterval(workerWatchdog); clearForceKillTimer(); const exitDescriptor = describeExit(code, signal); writeLifecycleLog("Worker exited", { code, signal, exit: exitDescriptor }); diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index dba4b071d9..50773a7a61 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -15,7 +15,7 @@ import { } from "./agent-manager.js"; import { AgentStorage } from "./agent-storage.js"; import { toAgentPayload } from "./agent-projections.js"; -import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; +import { getOpenAgentTabLabel, PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; import { formatSystemNotificationPrompt, startAgentRun } from "./agent-prompt.js"; import { ensureAgentLoaded, ensureUnarchivedAgentLoaded } from "./agent-loading.js"; import type { StoredAgentRecord } from "./agent-storage.js"; @@ -43,6 +43,9 @@ import type { import type { PaseoToolCatalog } from "./tools/types.js"; import type { ProviderDefinition } from "./provider-registry.js"; +const DESKTOP_OPEN_AGENT_TAB_LABEL = getOpenAgentTabLabel("desktop-client"); +const MOBILE_OPEN_AGENT_TAB_LABEL = getOpenAgentTabLabel("mobile-client"); + interface Deferred<T> { promise: Promise<T>; resolve: (value: T) => void; @@ -3712,7 +3715,7 @@ test("setLabels merges and persists labels", async () => { }); }); -test("detachAgent removes only the parent label from a live agent and emits state", async () => { +test("detachAgent removes relationship lifecycle labels from a live agent and emits state", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-detach-live-")); const storagePath = join(workdir, "agents"); const storage = new AgentStorage(storagePath, logger); @@ -3743,6 +3746,8 @@ test("detachAgent removes only the parent label from a live agent and emits stat { labels: { [PARENT_AGENT_ID_LABEL]: parent.id, + [DESKTOP_OPEN_AGENT_TAB_LABEL]: "true", + [MOBILE_OPEN_AGENT_TAB_LABEL]: "false", team: "infra", }, workspaceId: undefined, @@ -3770,7 +3775,7 @@ test("detachAgent removes only the parent label from a live agent and emits stat expect(emittedLabels).toContainEqual({ team: "infra" }); }); -test("detachAgent removes the parent label from a stored-only agent", async () => { +test("detachAgent removes relationship lifecycle labels from a stored-only agent", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-detach-stored-")); const storagePath = join(workdir, "agents"); const storage = new AgentStorage(storagePath, logger); @@ -3801,6 +3806,8 @@ test("detachAgent removes the parent label from a stored-only agent", async () = { labels: { [PARENT_AGENT_ID_LABEL]: parent.id, + [DESKTOP_OPEN_AGENT_TAB_LABEL]: "true", + [MOBILE_OPEN_AGENT_TAB_LABEL]: "false", role: "reviewer", }, workspaceId: undefined, @@ -6846,6 +6853,182 @@ test("archiveAgent cascade archives in-memory children with the full archive con expect(storedUnrelated?.archivedAt).toBeUndefined(); }); +test("archiveAgent detaches an open same-workspace child instead of cascading", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-cascade-open-child-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const manager = new AgentManager({ + clients: { codex: new TestAgentClient() }, + registry: storage, + logger, + }); + const parent = await manager.createAgent( + { provider: "codex", cwd: workdir, title: "Parent" }, + undefined, + { workspaceId: "workspace-a" }, + ); + const child = await manager.createAgent( + { provider: "codex", cwd: workdir, title: "Open child" }, + undefined, + { + workspaceId: "workspace-a", + labels: { + [PARENT_AGENT_ID_LABEL]: parent.id, + [DESKTOP_OPEN_AGENT_TAB_LABEL]: "false", + [MOBILE_OPEN_AGENT_TAB_LABEL]: "true", + }, + }, + ); + + await manager.archiveAgent(parent.id); + + const storedChild = await storage.get(child.id); + expect(storedChild?.archivedAt).toBeUndefined(); + expect(storedChild?.labels[PARENT_AGENT_ID_LABEL]).toBeUndefined(); + expect(storedChild?.labels[DESKTOP_OPEN_AGENT_TAB_LABEL]).toBeUndefined(); + expect(storedChild?.labels[MOBILE_OPEN_AGENT_TAB_LABEL]).toBeUndefined(); + expect(manager.getAgent(child.id)?.id).toBe(child.id); +}); + +test("archiveAgent detaches a cross-workspace child even when its tab is closed", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-cascade-cross-workspace-child-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const manager = new AgentManager({ + clients: { codex: new TestAgentClient() }, + registry: storage, + logger, + }); + const parent = await manager.createAgent( + { provider: "codex", cwd: workdir, title: "Parent" }, + undefined, + { workspaceId: "workspace-a" }, + ); + const child = await manager.createAgent( + { provider: "codex", cwd: workdir, title: "Cross-workspace child" }, + undefined, + { + workspaceId: "workspace-b", + labels: { + [PARENT_AGENT_ID_LABEL]: parent.id, + [DESKTOP_OPEN_AGENT_TAB_LABEL]: "false", + }, + }, + ); + + await manager.archiveAgent(parent.id); + + const storedChild = await storage.get(child.id); + expect(storedChild?.archivedAt).toBeUndefined(); + expect(storedChild?.workspaceId).toBe("workspace-b"); + expect(storedChild?.labels[PARENT_AGENT_ID_LABEL]).toBeUndefined(); +}); + +test("archiveAgent re-reads a child before deciding whether to cascade", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-cascade-fresh-child-")); + + class ChildOpensAfterCascadeListStorage extends AgentStorage { + childId: string | null = null; + + override async list(): Promise<StoredAgentRecord[]> { + const records = await super.list(); + const parentIsArchived = records.some( + (record) => record.id !== this.childId && Boolean(record.archivedAt), + ); + const staleChild = records.find((record) => record.id === this.childId); + if (parentIsArchived && staleChild) { + await super.upsert({ + ...staleChild, + labels: { ...staleChild.labels, [MOBILE_OPEN_AGENT_TAB_LABEL]: "true" }, + }); + } + return records; + } + } + + const storage = new ChildOpensAfterCascadeListStorage(join(workdir, "agents"), logger); + const manager = new AgentManager({ + clients: { codex: new TestAgentClient() }, + registry: storage, + logger, + }); + const parent = await manager.createAgent( + { provider: "codex", cwd: workdir, title: "Parent" }, + undefined, + { workspaceId: "workspace-a" }, + ); + const child = await manager.createAgent( + { provider: "codex", cwd: workdir, title: "Late-open child" }, + undefined, + { + workspaceId: "workspace-a", + labels: { + [PARENT_AGENT_ID_LABEL]: parent.id, + [MOBILE_OPEN_AGENT_TAB_LABEL]: "false", + }, + }, + ); + storage.childId = child.id; + + await manager.archiveAgent(parent.id); + + const storedChild = await storage.get(child.id); + expect(storedChild?.archivedAt).toBeUndefined(); + expect(storedChild?.labels[PARENT_AGENT_ID_LABEL]).toBeUndefined(); +}); + +test("archiveAgent cannot overtake a received child open-tab update", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-cascade-open-race-")); + const markerWriteStarted = deferred<void>(); + const releaseMarkerWrite = deferred<void>(); + + class BlockingOpenMarkerStorage extends AgentStorage { + childId: string | null = null; + + override async applySnapshot( + agent: ManagedAgent, + options?: { title?: string | null; internal?: boolean }, + ): Promise<void> { + if (agent.id === this.childId && agent.labels[MOBILE_OPEN_AGENT_TAB_LABEL] === "true") { + markerWriteStarted.resolve(); + await releaseMarkerWrite.promise; + } + await super.applySnapshot(agent, options); + } + } + + const storage = new BlockingOpenMarkerStorage(join(workdir, "agents"), logger); + const manager = new AgentManager({ + clients: { codex: new TestAgentClient() }, + registry: storage, + logger, + }); + const parent = await manager.createAgent( + { provider: "codex", cwd: workdir, title: "Parent" }, + undefined, + { workspaceId: "workspace-a" }, + ); + const child = await manager.createAgent( + { provider: "codex", cwd: workdir, title: "Opening child" }, + undefined, + { + workspaceId: "workspace-a", + labels: { [PARENT_AGENT_ID_LABEL]: parent.id }, + }, + ); + storage.childId = child.id; + + const markOpen = manager.updateAgentMetadata(child.id, { + labels: { [MOBILE_OPEN_AGENT_TAB_LABEL]: "true" }, + }); + await markerWriteStarted.promise; + const archiveParent = manager.archiveAgent(parent.id); + releaseMarkerWrite.resolve(); + await Promise.all([markOpen, archiveParent]); + + const storedChild = await storage.get(child.id); + expect(storedChild?.archivedAt).toBeUndefined(); + expect(storedChild?.labels[PARENT_AGENT_ID_LABEL]).toBeUndefined(); +}); + test("archiveAgent cascade closes a running child runtime", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-cascade-running-child-")); const storagePath = join(workdir, "agents"); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 47a20d2fd0..37b8a7778e 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -7,7 +7,9 @@ import { } from "@getpaseo/protocol/agent-lifecycle"; import { getParentAgentIdFromLabels, + hasOpenAgentTab, isDelegatedAgent, + isOpenAgentTabLabel, PARENT_AGENT_ID_LABEL, } from "@getpaseo/protocol/agent-labels"; import type { Logger } from "pino"; @@ -620,6 +622,27 @@ function getFirstUserMessageTextFromRows(rows: readonly AgentTimelineRow[]): str return null; } +function shouldDetachFromArchivedParent( + parent: StoredAgentRecord, + child: StoredAgentRecord, +): boolean { + const isCrossWorkspace = + parent.workspaceId !== undefined && + child.workspaceId !== undefined && + parent.workspaceId !== child.workspaceId; + return isCrossWorkspace || hasOpenAgentTab(child.labels); +} + +function detachedAgentLabelPatch(labels: Record<string, string>): AgentLabelPatch { + const patch: AgentLabelPatch = { [PARENT_AGENT_ID_LABEL]: null }; + for (const label of Object.keys(labels)) { + if (isOpenAgentTabLabel(label)) { + patch[label] = null; + } + } + return patch; +} + export class AgentManager { private readonly clients = new Map<AgentProvider, AgentClient>(); private readonly providerEnabled = new Map<AgentProvider, boolean>(); @@ -638,6 +661,7 @@ export class AgentManager { private readonly backgroundTasks = new Set<Promise<void>>(); private readonly agentRegistrationTasks = new Set<Promise<void>>(); private readonly inFlightAgentCloses = new Map<string, Promise<void>>(); + private readonly lifecycleMutationTails = new Map<string, Promise<void>>(); private readonly agentStreamCoalescer: AgentStreamCoalescer; private mcpBaseUrl: string | null; private readonly mcpAuthToken: string | null; @@ -1568,6 +1592,10 @@ export class AgentManager { } async archiveAgent(agentId: string): Promise<{ archivedAt: string }> { + return this.runLifecycleMutation(agentId, () => this.archiveAgentUnlocked(agentId)); + } + + private async archiveAgentUnlocked(agentId: string): Promise<{ archivedAt: string }> { const agent = this.requireAgent(agentId); if (!this.registry) { throw new Error("Agent storage is not configured"); @@ -1601,6 +1629,10 @@ export class AgentManager { return; } const records = await registry.list(); + const parent = records.find((record) => record.id === parentAgentId); + if (!parent) { + throw new Error(`Archived parent ${parentAgentId} not found in storage`); + } for (const record of records) { if (record.archivedAt) { continue; @@ -1608,11 +1640,27 @@ export class AgentManager { if (record.labels?.[PARENT_AGENT_ID_LABEL] !== parentAgentId) { continue; } - if (this.agents.has(record.id)) { - await this.archiveAgent(record.id); - } else { - await this.archiveSnapshot(record.id, new Date().toISOString()); + const child = await registry.get(record.id); + if (!child || child.archivedAt || child.labels?.[PARENT_AGENT_ID_LABEL] !== parentAgentId) { + continue; } + await this.runLifecycleMutation(child.id, async () => { + const currentChild = await registry.get(child.id); + if ( + !currentChild || + currentChild.archivedAt || + currentChild.labels?.[PARENT_AGENT_ID_LABEL] !== parentAgentId + ) { + return; + } + if (shouldDetachFromArchivedParent(parent, currentChild)) { + await this.detachAgentUnlocked(currentChild.id); + } else if (this.agents.has(currentChild.id)) { + await this.archiveAgentUnlocked(currentChild.id); + } else { + await this.archiveSnapshot(currentChild.id, new Date().toISOString()); + } + }); } } @@ -1785,8 +1833,10 @@ export class AgentManager { } async setLabels(agentId: string, labels: Record<string, string>): Promise<void> { - const agent = this.requireAgent(agentId); - await this.writeLabels(agent.id, labels); + await this.runLifecycleMutation(agentId, async () => { + const agent = this.requireAgent(agentId); + await this.writeLabels(agent.id, labels); + }); } private async writeLabels(agentId: string, patch: AgentLabelPatch): Promise<WriteLabelsResult> { @@ -1828,6 +1878,14 @@ export class AgentManager { record: StoredAgentRecord; live: boolean; previousParentAgentId: string | null; + }> { + return this.runLifecycleMutation(agentId, () => this.detachAgentUnlocked(agentId)); + } + + private async detachAgentUnlocked(agentId: string): Promise<{ + record: StoredAgentRecord; + live: boolean; + previousParentAgentId: string | null; }> { const registry = this.requireRegistry(); const liveAgent = this.agents.get(agentId); @@ -1842,7 +1900,7 @@ export class AgentManager { return { record, live: true, previousParentAgentId: null }; } - const { record } = await this.writeLabels(agentId, { [PARENT_AGENT_ID_LABEL]: null }); + const { record } = await this.writeLabels(agentId, detachedAgentLabelPatch(liveAgent.labels)); if (!record) { throw new Error(`Agent not found in storage after detach: ${agentId}`); } @@ -1858,7 +1916,7 @@ export class AgentManager { return { record, live: false, previousParentAgentId: null }; } - const result = await this.writeLabels(agentId, { [PARENT_AGENT_ID_LABEL]: null }); + const result = await this.writeLabels(agentId, detachedAgentLabelPatch(record.labels)); if (!result.record) { throw new Error(`Agent not found in storage after detach: ${agentId}`); } @@ -1964,6 +2022,18 @@ export class AgentManager { title?: string; labels?: Record<string, string>; }, + ): Promise<void> { + await this.runLifecycleMutation(agentId, () => + this.updateAgentMetadataUnlocked(agentId, updates), + ); + } + + private async updateAgentMetadataUnlocked( + agentId: string, + updates: { + title?: string; + labels?: Record<string, string>; + }, ): Promise<void> { const liveAgent = this.getAgent(agentId); if (liveAgent) { @@ -1979,6 +2049,24 @@ export class AgentManager { await this.writeStoredMetadata(agentId, updates); } + private async runLifecycleMutation<T>(agentId: string, mutation: () => Promise<T>): Promise<T> { + // Parent cascade classifies a child inside the same lane used by open-tab + // label writes, so a received ownership update cannot be overtaken. + const previous = this.lifecycleMutationTails.get(agentId) ?? Promise.resolve(); + const result = previous.catch(() => undefined).then(mutation); + const tail = result.then( + () => undefined, + () => undefined, + ); + this.lifecycleMutationTails.set(agentId, tail); + void tail.finally(() => { + if (this.lifecycleMutationTails.get(agentId) === tail) { + this.lifecycleMutationTails.delete(agentId); + } + }); + return result; + } + async runAgent( agentId: string, prompt: AgentPromptInput, diff --git a/packages/server/src/server/agent/agent-prompt.test.ts b/packages/server/src/server/agent/agent-prompt.test.ts index ba3c64f5e1..cc84c88d61 100644 --- a/packages/server/src/server/agent/agent-prompt.test.ts +++ b/packages/server/src/server/agent/agent-prompt.test.ts @@ -46,8 +46,14 @@ interface FinishNotificationScenarioOptions { interface FinishNotificationScenario { startWatchingChild(): void; + requestChildPermission(requestId?: string): void; + resolveChildPermission(requestId?: string): void; + resolveChildPermissionFromState(requestId?: string): void; + resolveChildPermissionWhileIdle(requestId?: string): void; finishChild(): void; finishChildAndReadParentPrompt(): Promise<string>; + closeChildAndReadParentPrompt(): Promise<string>; + parentPrompts(): string[]; wasParentPrompted(): boolean; } @@ -57,11 +63,13 @@ function createFinishNotificationScenario( let subscriber: ((event: AgentManagerEvent) => void) | null = null; let resolveParentPrompt: ((prompt: string) => void) | null = null; let parentPrompted = false; + const parentPrompts: string[] = []; const childAgent: ManagedAgent = Object.create(null); Reflect.set(childAgent, "id", "child-agent"); Reflect.set(childAgent, "lifecycle", "idle"); Reflect.set(childAgent, "config", { title: "Child Agent" }); + Reflect.set(childAgent, "pendingPermissions", new Map()); const callerAgent: ManagedAgent = Object.create(null); Reflect.set(callerAgent, "id", "caller-agent"); @@ -91,6 +99,7 @@ function createFinishNotificationScenario( Reflect.set(agentManager, "hasInFlightRun", () => Boolean(options?.parentPromptError)); Reflect.set(agentManager, "streamAgent", (_agentId: string, prompt: string) => { parentPrompted = true; + parentPrompts.push(prompt); resolveParentPrompt?.(prompt); return (async function* noop() {})(); }); @@ -123,6 +132,65 @@ function createFinishNotificationScenario( logger: options?.logger ?? createTestLogger(), }); }, + requestChildPermission(requestId = "permission-1") { + childAgent.lifecycle = "running"; + childAgent.pendingPermissions.set(requestId, { + id: requestId, + provider: "claude", + kind: "tool", + name: "Run command", + description: "Write the QA sentinel", + input: { + file_path: "/tmp/permission-qa.txt", + content: "PASEO_PERMISSION_NOTIFY_QA_OK\n", + }, + }); + subscriber?.({ + type: "agent_state", + agent: childAgent, + }); + subscriber?.({ + type: "agent_stream", + agentId: "child-agent", + event: { + type: "permission_requested", + provider: "codex", + request: childAgent.pendingPermissions.get(requestId)!, + }, + }); + }, + resolveChildPermission(requestId = "permission-1") { + childAgent.pendingPermissions.delete(requestId); + subscriber?.({ + type: "agent_stream", + agentId: "child-agent", + event: { + type: "permission_resolved", + provider: "codex", + requestId, + resolution: { behavior: "allow" }, + }, + }); + }, + resolveChildPermissionFromState(requestId = "permission-1") { + childAgent.pendingPermissions.delete(requestId); + subscriber?.({ type: "agent_state", agent: childAgent }); + }, + resolveChildPermissionWhileIdle(requestId = "permission-1") { + childAgent.pendingPermissions.delete(requestId); + childAgent.lifecycle = "idle"; + subscriber?.({ type: "agent_state", agent: childAgent }); + subscriber?.({ + type: "agent_stream", + agentId: "child-agent", + event: { + type: "permission_resolved", + provider: "codex", + requestId, + resolution: { behavior: "allow" }, + }, + }); + }, finishChild() { childAgent.lifecycle = "running"; subscriber?.({ @@ -144,6 +212,28 @@ function createFinishNotificationScenario( return parentPrompt; }, + async closeChildAndReadParentPrompt() { + const parentPrompt = new Promise<string>((resolve) => { + resolveParentPrompt = resolve; + }); + + childAgent.lifecycle = "running"; + subscriber?.({ + type: "agent_state", + agent: childAgent, + }); + + childAgent.lifecycle = "closed"; + subscriber?.({ + type: "agent_state", + agent: childAgent, + }); + + return parentPrompt; + }, + parentPrompts() { + return parentPrompts; + }, wasParentPrompted() { return parentPrompted; }, @@ -209,6 +299,135 @@ test("finish notifications tell the parent the child's last assistant message", ); }); +test("finish notifications truncate oversized child responses", async () => { + const included = "x".repeat(4000); + const omitted = "TAIL-MARKER".repeat(50); + const scenario = createFinishNotificationScenario({ + childLastAssistantMessage: included + omitted, + }); + + scenario.startWatchingChild(); + const parentPrompt = await scenario.finishChildAndReadParentPrompt(); + + expect(parentPrompt).toContain(included); + expect(parentPrompt).toContain( + `[truncated ${omitted.length} chars; use get_agent_activity for the full response]`, + ); + expect(parentPrompt).not.toContain("TAIL-MARKER"); +}); + +test("closing a watched child notifies the caller", async () => { + const scenario = createFinishNotificationScenario(); + + scenario.startWatchingChild(); + const parentPrompt = await scenario.closeChildAndReadParentPrompt(); + + expect(parentPrompt).toEqual( + formatSystemNotificationPrompt("Agent child-agent (Child Agent) was closed."), + ); +}); + +test("finish notifications survive permission responses", async () => { + const scenario = createFinishNotificationScenario(); + + scenario.startWatchingChild(); + scenario.requestChildPermission(); + + await vi.waitFor(() => { + expect(scenario.parentPrompts()).toHaveLength(1); + }); + expect(scenario.parentPrompts()[0]).toContain("needs permission."); + const permissionPayload = scenario + .parentPrompts()[0] + .match(/<permission-request>\n([\s\S]+?)\n<\/permission-request>/)?.[1]; + expect(permissionPayload).toBeDefined(); + expect(JSON.parse(permissionPayload!)).toEqual({ + agentId: "child-agent", + requestId: "permission-1", + request: { + id: "permission-1", + provider: "claude", + kind: "tool", + name: "Run command", + description: "Write the QA sentinel", + input: { + file_path: "/tmp/permission-qa.txt", + content: "PASEO_PERMISSION_NOTIFY_QA_OK\n", + }, + }, + }); + + scenario.resolveChildPermission(); + scenario.finishChild(); + + await vi.waitFor(() => { + expect(scenario.parentPrompts()).toHaveLength(2); + }); + expect(scenario.parentPrompts()[1]).toContain("finished."); +}); + +test("an idle permission resolution waits for the resumed run to finish", async () => { + const scenario = createFinishNotificationScenario(); + + scenario.startWatchingChild(); + scenario.requestChildPermission(); + await vi.waitFor(() => expect(scenario.parentPrompts()).toHaveLength(1)); + + scenario.resolveChildPermissionWhileIdle(); + scenario.requestChildPermission("permission-2"); + await vi.waitFor(() => expect(scenario.parentPrompts()).toHaveLength(2)); + expect(scenario.parentPrompts().every((prompt) => prompt.includes("needs permission."))).toBe( + true, + ); + + scenario.resolveChildPermission("permission-2"); + scenario.finishChild(); + await vi.waitFor(() => expect(scenario.parentPrompts()).toHaveLength(3)); + expect(scenario.parentPrompts()[2]).toContain("finished."); +}); + +test("finish notifications report every concurrently pending permission", async () => { + const scenario = createFinishNotificationScenario(); + + scenario.startWatchingChild(); + scenario.requestChildPermission("permission-1"); + scenario.requestChildPermission("permission-2"); + + await vi.waitFor(() => expect(scenario.parentPrompts()).toHaveLength(2)); + expect( + scenario.parentPrompts().map((prompt) => { + const payload = prompt.match(/<permission-request>\n([\s\S]+?)\n<\/permission-request>/)?.[1]; + return JSON.parse(payload!).requestId; + }), + ).toEqual(["permission-1", "permission-2"]); + + scenario.resolveChildPermission("permission-1"); + scenario.resolveChildPermission("permission-2"); + scenario.finishChild(); + + await vi.waitFor(() => expect(scenario.parentPrompts()).toHaveLength(3)); + expect(scenario.parentPrompts()[2]).toContain("finished."); +}); + +test("finish notifications survive repeated permission cycles", async () => { + const scenario = createFinishNotificationScenario(); + + scenario.startWatchingChild(); + scenario.requestChildPermission(); + await vi.waitFor(() => expect(scenario.parentPrompts()).toHaveLength(1)); + scenario.resolveChildPermissionFromState(); + + scenario.requestChildPermission(); + await vi.waitFor(() => expect(scenario.parentPrompts()).toHaveLength(2)); + scenario.resolveChildPermission(); + scenario.finishChild(); + + await vi.waitFor(() => expect(scenario.parentPrompts()).toHaveLength(3)); + expect( + scenario.parentPrompts().map((prompt) => prompt.match(/(needs permission|finished)\./)?.[1]), + ).toEqual(["needs permission", "needs permission", "finished"]); +}); + test("detaching a child ends its parent-owned finish notification", async () => { const scenario = createFinishNotificationScenario({ childParentAgentId: null, @@ -258,6 +477,7 @@ it("does not notify archived callers", async () => { Reflect.set(childAgent, "id", "child-agent"); Reflect.set(childAgent, "lifecycle", "idle"); Reflect.set(childAgent, "config", { title: "Child Agent" }); + Reflect.set(childAgent, "pendingPermissions", new Map()); const callerAgent: ManagedAgent = Object.create(null); Reflect.set(callerAgent, "id", "caller-agent"); diff --git a/packages/server/src/server/agent/agent-prompt.ts b/packages/server/src/server/agent/agent-prompt.ts index 53d3674162..d92b225b39 100644 --- a/packages/server/src/server/agent/agent-prompt.ts +++ b/packages/server/src/server/agent/agent-prompt.ts @@ -1,6 +1,10 @@ import type { Logger } from "pino"; -import type { AgentPromptInput, AgentRunOptions } from "./agent-sdk-types.js"; +import type { + AgentPermissionRequest, + AgentPromptInput, + AgentRunOptions, +} from "./agent-sdk-types.js"; import type { AgentManager, ManagedAgent } from "./agent-manager.js"; import type { AgentStorage } from "./agent-storage.js"; import { ensureAgentLoaded } from "./agent-loading.js"; @@ -249,20 +253,49 @@ export interface SetupFinishNotificationParams { logger: Logger; } +type FinishNotificationReason = "finished" | "errored" | "needs permission" | "was closed"; + +const FINISH_NOTIFICATION_MESSAGE_LIMIT = 4000; + interface FinishNotificationBodyInput { childAgentId: string; title: string; - reason: "finished" | "errored" | "needs permission"; + reason: FinishNotificationReason; lastAssistantMessage: string | null; + permissionRequest?: AgentPermissionRequest; } function formatFinishNotificationBody(params: FinishNotificationBodyInput): string { const statusLine = `Agent ${params.childAgentId} (${params.title}) ${params.reason}.`; - const lastAssistantMessage = params.lastAssistantMessage?.trim(); - if (!lastAssistantMessage) { - return statusLine; + const sections = [statusLine]; + if (params.reason === "needs permission" && params.permissionRequest) { + sections.push( + "Respond with `respond_to_permission` using the `agentId` and `requestId` below.", + `<permission-request>\n${JSON.stringify( + { + agentId: params.childAgentId, + requestId: params.permissionRequest.id, + request: params.permissionRequest, + }, + null, + 2, + )}\n</permission-request>`, + ); } - return `${statusLine}\n\n<agent-response>\n${lastAssistantMessage}\n</agent-response>`; + let lastAssistantMessage = params.lastAssistantMessage?.trim(); + if (lastAssistantMessage) { + if (lastAssistantMessage.length > FINISH_NOTIFICATION_MESSAGE_LIMIT) { + const omitted = lastAssistantMessage.length - FINISH_NOTIFICATION_MESSAGE_LIMIT; + lastAssistantMessage = `${lastAssistantMessage.slice(0, FINISH_NOTIFICATION_MESSAGE_LIMIT)}\n[truncated ${omitted} chars; use get_agent_activity for the full response]`; + } + sections.push(`<agent-response>\n${lastAssistantMessage}\n</agent-response>`); + } + return sections.join("\n\n"); +} + +interface NotifySafelyOptions { + terminal?: boolean; + permissionRequest?: AgentPermissionRequest; } export function setupFinishNotification(params: SetupFinishNotificationParams): void { @@ -275,16 +308,21 @@ export function setupFinishNotification(params: SetupFinishNotificationParams): logger, } = params; let hasSeenRunning = false; - let fired = false; + let stopped = false; + const notifiedPermissionRequestIds = new Set<string>(); let unsubscribe: (() => void) | null = null; + let notificationQueue = Promise.resolve(); - async function notify(reason: "finished" | "errored" | "needs permission"): Promise<void> { - if (fired) { - return; - } - fired = true; + function stop(): void { + if (stopped) return; + stopped = true; unsubscribe?.(); + } + async function notify( + reason: FinishNotificationReason, + permissionRequest?: AgentPermissionRequest, + ): Promise<void> { const callerRecord = await agentStorage.get(callerAgentId); if (callerRecord?.archivedAt) { return; @@ -301,6 +339,7 @@ export function setupFinishNotification(params: SetupFinishNotificationParams): title, reason, lastAssistantMessage, + permissionRequest, }); await sendPromptToAgent({ @@ -313,24 +352,35 @@ export function setupFinishNotification(params: SetupFinishNotificationParams): }); } - function notifySafely(reason: "finished" | "errored" | "needs permission"): void { - void notify(reason).catch((error) => { - logger.error( - { err: error, childAgentId, callerAgentId, reason }, - "Failed to notify caller agent", - ); - }); + function notifySafely(reason: FinishNotificationReason, options: NotifySafelyOptions = {}): void { + if (stopped) return; + if (options.terminal ?? true) stop(); + notificationQueue = notificationQueue + .then(() => notify(reason, options.permissionRequest)) + .catch((error) => { + logger.error( + { err: error, childAgentId, callerAgentId, reason }, + "Failed to notify caller agent", + ); + }); } unsubscribe = agentManager.subscribe( (event) => { - if (fired) { + if (stopped) { return; } if (event.type === "agent_state") { + for (const requestId of notifiedPermissionRequestIds) { + if (!event.agent.pendingPermissions.has(requestId)) { + notifiedPermissionRequestIds.delete(requestId); + } + } if (event.agent.lifecycle === "running") { - hasSeenRunning = true; + if (event.agent.pendingPermissions.size === 0) { + hasSeenRunning = true; + } return; } if (event.agent.lifecycle === "error") { @@ -342,15 +392,33 @@ export function setupFinishNotification(params: SetupFinishNotificationParams): return; } if (event.agent.lifecycle === "closed") { - fired = true; - unsubscribe?.(); + notifySafely("was closed"); return; } return; } if (event.event.type === "permission_requested") { - notifySafely("needs permission"); + // A permission pause is an intermediate checkpoint. Forget the run + // observed before it so an idle state during follow-up startup cannot + // masquerade as the final completion. + hasSeenRunning = false; + if (!notifiedPermissionRequestIds.has(event.event.request.id)) { + notifiedPermissionRequestIds.add(event.event.request.id); + notifySafely("needs permission", { + terminal: false, + permissionRequest: event.event.request, + }); + } + return; + } + + if (event.event.type === "permission_resolved") { + notifiedPermissionRequestIds.delete(event.event.requestId); + const childAgent = agentManager.getAgent(childAgentId); + if (childAgent?.pendingPermissions.size === 0) { + hasSeenRunning = childAgent.lifecycle === "running"; + } } }, { agentId: childAgentId, replayState: false }, @@ -363,7 +431,7 @@ export function setupFinishNotification(params: SetupFinishNotificationParams): // transitioning to "running"). const childSnapshot = agentManager.getAgent(childAgentId); if (!childSnapshot || childSnapshot.lifecycle === "closed") { - unsubscribe(); + stop(); return; } if (childSnapshot.lifecycle === "running") { diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index f3a675f9a7..91f2eb52f9 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -1,5 +1,6 @@ import type { AgentProviderNotice, + AgentTaskItem, ProviderOptions, ToolPolicy, } from "@getpaseo/protocol/agent-types"; @@ -7,7 +8,7 @@ import type { AgentAttachment } from "@getpaseo/protocol/messages"; import type { PaseoToolCatalog } from "./tools/types.js"; import type { ProviderWorkspace } from "./providers/workspace/index.js"; -export type { AgentProviderNotice }; +export type { AgentProviderNotice, AgentTaskItem }; export type { ProviderWorkspace } from "./providers/workspace/index.js"; export type AgentProvider = string; @@ -385,7 +386,7 @@ export type AgentTimelineItem = | { type: "assistant_message"; text: string; messageId?: string } | { type: "reasoning"; text: string } | ToolCallTimelineItem - | { type: "todo"; items: { text: string; completed: boolean }[] } + | { type: "todo"; items: AgentTaskItem[] } | { type: "error"; message: string } | CompactionTimelineItem; @@ -671,7 +672,6 @@ export type FetchCatalogOptions = | { scope: "global"; force: boolean; - timeoutMs?: number; } | { scope: "workspace"; @@ -679,9 +679,14 @@ export type FetchCatalogOptions = workspaceId?: string; workspace?: ProviderWorkspace; force: boolean; - timeoutMs?: number; }; +export interface ProviderRefreshContext { + readonly signal: AbortSignal; + /** Track an upstream operation so timeout errors identify the work still pending. */ + runActivity<T>(name: string, operation: () => Promise<T>): Promise<T>; +} + export interface ProviderCatalog { models: AgentModelDefinition[]; modes: AgentMode[]; @@ -691,6 +696,7 @@ export interface ProviderCatalog { export interface ResolveAgentDefaultModeInput { config: AgentSessionConfig; env?: Record<string, string>; + signal?: AbortSignal; } export interface AgentClient { @@ -712,8 +718,13 @@ export interface AgentClient { * process, separate upstream calls, static modes, or private helpers; callers * outside the provider do not get separate runtime model/mode probes. * The registry is responsible for merging configured model overrides. + * ProviderSnapshotManager supplies a shared context. Providers must pass its + * signal downstream and finish resource cleanup before rejecting on abort. */ - fetchCatalog(options: FetchCatalogOptions): Promise<ProviderCatalog>; + fetchCatalog( + options: FetchCatalogOptions, + context?: ProviderRefreshContext, + ): Promise<ProviderCatalog>; /** Apply provider-owned defaults to a model supplied through provider configuration. */ resolveConfiguredModel?(model: AgentModelDefinition): AgentModelDefinition; resolveDefaultModeId?(input: ResolveAgentDefaultModeInput): Promise<string | undefined>; @@ -732,7 +743,7 @@ export interface AgentClient { * Check if this provider is available (CLI binary is installed). * Returns true if available, false otherwise. */ - isAvailable(options?: FetchCatalogOptions): Promise<boolean>; + isAvailable(options?: FetchCatalogOptions, signal?: AbortSignal): Promise<boolean>; getDiagnostic?(): Promise<{ diagnostic: string }>; /** * Archive a durable native session (best-effort). Runtime release belongs to AgentSession.close(). diff --git a/packages/server/src/server/agent/agent-storage.test.ts b/packages/server/src/server/agent/agent-storage.test.ts index 2258826505..723f612085 100644 --- a/packages/server/src/server/agent/agent-storage.test.ts +++ b/packages/server/src/server/agent/agent-storage.test.ts @@ -102,6 +102,7 @@ function createManagedAgent(overrides: ManagedAgentOverrides = {}): ManagedAgent id: overrides.id ?? "agent-test", provider: core.provider, cwd: core.cwd, + workspaceId: overrides.workspaceId, session: core.session, capabilities: overrides.capabilities ?? buildDefaultCapabilities(), config: core.config, @@ -433,6 +434,47 @@ describe("AgentStorage", () => { expect(record?.internal).toBe(true); }); + test("queries agents by provider session and native handle", async () => { + await storage.applySnapshot( + createManagedAgent({ + id: "matching-session", + provider: "codex", + persistence: { + provider: "codex", + sessionId: "session-1", + nativeHandle: "thread-1", + }, + }), + ); + await storage.applySnapshot( + createManagedAgent({ + id: "other-session", + provider: "codex", + persistence: { provider: "codex", sessionId: "session-2" }, + }), + ); + + await expect(storage.listByProviderSession("codex", "session-1")).resolves.toMatchObject([ + { id: "matching-session" }, + ]); + await expect(storage.listByProviderSession("codex", "thread-1")).resolves.toMatchObject([ + { id: "matching-session" }, + ]); + }); + + test("queries agents by workspace", async () => { + await storage.applySnapshot( + createManagedAgent({ id: "workspace-agent", workspaceId: "workspace-1" }), + ); + await storage.applySnapshot( + createManagedAgent({ id: "other-workspace-agent", workspaceId: "workspace-2" }), + ); + + await expect(storage.listByWorkspace("workspace-1")).resolves.toMatchObject([ + { id: "workspace-agent" }, + ]); + }); + test("internal flag is persisted and reloaded", async () => { await storage.applySnapshot( createManagedAgent({ diff --git a/packages/server/src/server/agent/agent-storage.ts b/packages/server/src/server/agent/agent-storage.ts index f3645c7bb2..b41bc75cfc 100644 --- a/packages/server/src/server/agent/agent-storage.ts +++ b/packages/server/src/server/agent/agent-storage.ts @@ -126,6 +126,24 @@ export class AgentStorage { return this.cache.get(agentId) ?? null; } + async listByProviderSession( + provider: string, + providerHandleId: string, + ): Promise<StoredAgentRecord[]> { + await this.load(); + return Array.from(this.cache.values()).filter( + (record) => + record.persistence?.provider === provider && + (record.persistence.sessionId === providerHandleId || + record.persistence.nativeHandle === providerHandleId), + ); + } + + async listByWorkspace(workspaceId: string): Promise<StoredAgentRecord[]> { + await this.load(); + return Array.from(this.cache.values()).filter((record) => record.workspaceId === workspaceId); + } + async findByDaemonExecution(owner: DaemonAgentOwner): Promise<StoredAgentRecord | null> { await this.load(); const agentId = this.daemonAgentIdsByExecution.get(daemonExecutionKey(owner)); diff --git a/packages/server/src/server/agent/import-sessions.ts b/packages/server/src/server/agent/import-sessions.ts index 092c2f2cd9..ad9a8c94ae 100644 --- a/packages/server/src/server/agent/import-sessions.ts +++ b/packages/server/src/server/agent/import-sessions.ts @@ -192,8 +192,9 @@ async function importProviderSessionNow( ): Promise<ImportedProviderSession> { const { provider, providerHandleId, labels } = input.request; - const matchingRecords = (await input.agentStorage.list()).filter((record) => - recordMatchesProviderHandle(record, { provider, providerHandleId }), + const matchingRecords = await input.agentStorage.listByProviderSession( + provider, + providerHandleId, ); const activeRecord = matchingRecords.find((record) => !record.archivedAt); if (activeRecord) { @@ -273,16 +274,18 @@ async function serializeProviderSessionImport<T>( async function resolveProviderSessionImportMutationKey( input: ImportProviderSessionInput, ): Promise<string> { - const identity = { - provider: input.request.provider, - providerHandleId: input.request.providerHandleId, - }; - const matchingRecord = (await input.agentStorage.list()).find((record) => - recordMatchesProviderHandle(record, identity), - ); + const matchingRecord = ( + await input.agentStorage.listByProviderSession( + input.request.provider, + input.request.providerHandleId, + ) + ).at(0); return matchingRecord ? `agent\0${matchingRecord.id}` - : `handle\0${toProviderSessionHandleKey(identity.provider, identity.providerHandleId)}`; + : `handle\0${toProviderSessionHandleKey( + input.request.provider, + input.request.providerHandleId, + )}`; } async function rollbackArchivedImport( @@ -312,17 +315,6 @@ async function rollbackArchivedImport( } } -function recordMatchesProviderHandle( - record: StoredAgentRecord, - identity: { provider: string; providerHandleId: string }, -): boolean { - return ( - record.persistence?.provider === identity.provider && - (record.persistence.sessionId === identity.providerHandleId || - record.persistence.nativeHandle === identity.providerHandleId) - ); -} - function parseRecentProviderSessionsSince(since: string | undefined): number | null { if (!since) { return null; diff --git a/packages/server/src/server/agent/mcp-parity.e2e.test.ts b/packages/server/src/server/agent/mcp-parity.e2e.test.ts index 209befc21f..50867f10d9 100644 --- a/packages/server/src/server/agent/mcp-parity.e2e.test.ts +++ b/packages/server/src/server/agent/mcp-parity.e2e.test.ts @@ -146,6 +146,15 @@ let parentAgentCwd: string; let worktreeRepoCwd: string; let launchConfigsByProvider: Record<AgentProvider, AgentSessionConfig[]>; +const seededAgentProfile = { + id: "ui-profile", + name: "UI work", + provider: "claude", + model: "claude-test-model", + modeId: "bypassPermissions", + notes: "Use for UI work: components, layout, design tokens. Not for backend.", +}; + function createRecordingAgentClients(): Record<AgentProvider, AgentClient> { const baseClients = createTestAgentClients(); launchConfigsByProvider = {}; @@ -285,7 +294,10 @@ beforeAll(async () => { parentAgentCwd = await makeCwd("parent-agent-cwd"); worktreeRepoCwd = await makeCwd("worktree-repo"); - daemonHandle = await createTestPaseoDaemon({ agentClients: createRecordingAgentClients() }); + daemonHandle = await createTestPaseoDaemon({ + agentClients: createRecordingAgentClients(), + agentProfiles: [seededAgentProfile], + }); topLevelClient = await createMcpClient(`http://127.0.0.1:${daemonHandle.port}/mcp/agents`); const parentPayload = await callToolStructured(topLevelClient, "create_agent", { @@ -825,6 +837,12 @@ describe("Suite D: Provider Tools", () => { expect(payload.provider).toBe("claude"); expect(Array.isArray(payload.models)).toBe(true); }); + + test("list_profiles returns configured agent profiles, including notes", async () => { + const payload = await callToolStructured(topLevelClient, "list_profiles"); + const profiles = recordArr(payload.profiles); + expect(profiles).toEqual([seededAgentProfile]); + }); }); describe("Suite E: Worktree Tools", () => { diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts index ce5367663a..349eb15c53 100644 --- a/packages/server/src/server/agent/mcp-server.test.ts +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -52,6 +52,8 @@ import type { ForgeService } from "../../services/forge-service.js"; import { areEquivalentPaths } from "../../utils/path.js"; import type { TerminalManager } from "../../terminal/terminal-manager.js"; import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; +import { MutableDaemonConfigSchema, type AgentProfile } from "@getpaseo/protocol/messages"; +import type { DaemonConfigStore } from "../daemon-config-store.js"; import type { BrowserToolsBroker, BrowserToolsExecuteInput } from "../browser-tools/broker.js"; import type { BrowserToolsResponsePayload } from "../browser-tools/errors.js"; import { readPaseoWorktreeMetadata } from "../../utils/worktree-metadata.js"; @@ -4901,6 +4903,78 @@ describe("provider listing MCP tool", () => { }); }); +function daemonConfigStoreStub(agentProfiles?: AgentProfile[]): Pick<DaemonConfigStore, "get"> { + const config = MutableDaemonConfigSchema.parse({ + relay: { enabled: true }, + mcp: { injectIntoAgents: true }, + ...(agentProfiles !== undefined ? { agentProfiles } : {}), + }); + return { get: () => config }; +} + +describe("agent profile listing MCP tool", () => { + const logger = createTestLogger(); + + it("returns configured profiles, including notes", async () => { + const { agentManager, agentStorage } = createTestDeps(); + const profiles: AgentProfile[] = [ + { + id: "ui-profile", + name: "UI work", + provider: "claude", + model: "claude-test-model", + modeId: "bypassPermissions", + thinkingOptionId: "high", + featureValues: { fast_mode: true }, + notes: "Use for UI work: components, layout, design tokens. Not for backend.", + }, + ]; + const server = await createAgentMcpServer({ + agentManager, + agentStorage, + providerSnapshotManager: createOpenCodeManager().manager, + daemonConfigStore: daemonConfigStoreStub(profiles), + logger, + }); + const tool = registeredTool(server, "list_profiles"); + + const response = await tool.handler({}); + + expect(response.structuredContent).toEqual({ profiles }); + }); + + it("returns an empty array when no profiles are configured", async () => { + const { agentManager, agentStorage } = createTestDeps(); + const server = await createAgentMcpServer({ + agentManager, + agentStorage, + providerSnapshotManager: createOpenCodeManager().manager, + daemonConfigStore: daemonConfigStoreStub(), + logger, + }); + const tool = registeredTool(server, "list_profiles"); + + const response = await tool.handler({}); + + expect(response.structuredContent).toEqual({ profiles: [] }); + }); + + it("returns an empty array when no daemon config store is provided", async () => { + const { agentManager, agentStorage } = createTestDeps(); + const server = await createAgentMcpServer({ + agentManager, + agentStorage, + providerSnapshotManager: createOpenCodeManager().manager, + logger, + }); + const tool = registeredTool(server, "list_profiles"); + + const response = await tool.handler({}); + + expect(response.structuredContent).toEqual({ profiles: [] }); + }); +}); + describe("provider MCP tools", () => { const logger = createTestLogger(); diff --git a/packages/server/src/server/agent/provider-refresh-deadline.ts b/packages/server/src/server/agent/provider-refresh-deadline.ts new file mode 100644 index 0000000000..12528cfe81 --- /dev/null +++ b/packages/server/src/server/agent/provider-refresh-deadline.ts @@ -0,0 +1,80 @@ +import type { ProviderRefreshContext } from "./agent-sdk-types.js"; + +interface RunProviderRefreshOptions<T> { + label: string; + timeoutMs: number; + operation: (context: ProviderRefreshContext) => Promise<T>; +} + +export function runProviderRefreshActivity<T>( + context: ProviderRefreshContext | undefined, + name: string, + operation: () => Promise<T>, +): Promise<T> { + return context ? context.runActivity(name, operation) : operation(); +} + +export async function raceProviderRefreshAbort<T>( + signal: AbortSignal | undefined, + operation: Promise<T>, +): Promise<T> { + if (!signal) return await operation; + signal.throwIfAborted(); + let handleAbort: (() => void) | undefined; + const aborted = new Promise<never>((_resolve, reject) => { + handleAbort = () => reject(signal.reason); + signal.addEventListener("abort", handleAbort, { once: true }); + }); + try { + return await Promise.race([operation, aborted]); + } finally { + if (handleAbort) signal.removeEventListener("abort", handleAbort); + } +} + +export async function runProviderRefreshWithDeadline<T>( + options: RunProviderRefreshOptions<T>, +): Promise<T> { + const controller = new AbortController(); + const activityCounts = new Map<string, number>(); + let timeoutError: Error | undefined; + + const context: ProviderRefreshContext = { + signal: controller.signal, + async runActivity(name, operation) { + controller.signal.throwIfAborted(); + activityCounts.set(name, (activityCounts.get(name) ?? 0) + 1); + try { + const result = await operation(); + controller.signal.throwIfAborted(); + return result; + } finally { + const remaining = (activityCounts.get(name) ?? 1) - 1; + if (remaining === 0) { + activityCounts.delete(name); + } else { + activityCounts.set(name, remaining); + } + } + }, + }; + + const timer = setTimeout(() => { + const pending = Array.from(activityCounts.keys()); + const suffix = pending.length > 0 ? `; pending: ${pending.join(", ")}` : ""; + timeoutError = new Error( + `Timed out refreshing ${options.label} after ${options.timeoutMs}ms${suffix}`, + ); + controller.abort(timeoutError); + }, options.timeoutMs); + + try { + const result = await options.operation(context); + if (timeoutError) throw timeoutError; + return result; + } catch (error) { + throw timeoutError ?? error; + } finally { + clearTimeout(timer); + } +} diff --git a/packages/server/src/server/agent/provider-registry.ts b/packages/server/src/server/agent/provider-registry.ts index b673e2bd75..58e45d37c7 100644 --- a/packages/server/src/server/agent/provider-registry.ts +++ b/packages/server/src/server/agent/provider-registry.ts @@ -13,6 +13,7 @@ import type { AgentSession, AgentStreamEvent, FetchCatalogOptions, + ProviderRefreshContext, ProviderCatalog, ResolveAgentCreateConfigInput, ResolveAgentCreateConfigResult, @@ -24,6 +25,7 @@ import { resolveDefaultAgentCreateConfig, } from "./create-agent-mode.js"; import { normalizeAgentModelDefinition } from "./agent-sdk-types.js"; +import { runProviderRefreshActivity } from "./provider-refresh-deadline.js"; import type { WorkspaceGitService } from "../workspace-git-service.js"; import type { ManagedProcessRegistry } from "../managed-processes/managed-processes.js"; import type { @@ -92,7 +94,11 @@ export interface ProviderDefinition extends AgentProviderDefinition { * Single catalog discovery call used by ProviderSnapshotManager. Should spawn * at most one provider runtime process and return both models and modes. */ - fetchCatalog: (options: FetchCatalogOptions, client?: AgentClient) => Promise<ProviderCatalog>; + fetchCatalog: ( + options: FetchCatalogOptions, + client?: AgentClient, + context?: ProviderRefreshContext, + ) => Promise<ProviderCatalog>; } export interface BuildProviderRegistryOptions { @@ -507,8 +513,8 @@ function wrapClientProvider( options, ), ), - fetchCatalog: async (options) => { - const catalog = await inner.fetchCatalog(options); + fetchCatalog: async (options, context) => { + const catalog = await inner.fetchCatalog(options, context); return { ...catalog, models: mergeModels(provider, profileModels, additionalModels, catalog.models, { @@ -518,10 +524,11 @@ function wrapClientProvider( }; }, resolveDefaultModeId: inner.resolveDefaultModeId - ? async ({ config, env }: ResolveAgentDefaultModeInput) => + ? async ({ config, env, signal }: ResolveAgentDefaultModeInput) => await inner.resolveDefaultModeId?.({ config: { ...config, provider: inner.provider }, env, + signal, }) : undefined, resolveCreateConfig: inner.resolveCreateConfig?.bind(inner), @@ -561,7 +568,7 @@ function wrapClientProvider( }; } : undefined, - isAvailable: () => inner.isAvailable(), + isAvailable: (signal) => inner.isAvailable(signal), getDiagnostic: inner.getDiagnostic?.bind(inner), }; } @@ -621,7 +628,11 @@ function createRegistryEntry( resolveCreateConfig: modelClient.resolveCreateConfig ?? resolveDefaultAgentCreateConfig, isCreateConfigUnattended: modelClient.isCreateConfigUnattended ?? isDefaultAgentCreateConfigUnattended, - fetchCatalog: async (options: FetchCatalogOptions, client?: AgentClient) => { + fetchCatalog: async ( + options: FetchCatalogOptions, + client?: AgentClient, + context?: ProviderRefreshContext, + ) => { const catalogClient = client ?? modelClient; if (hasReplacementModels) { // Replacement models skip runtime model discovery, but additionalModels @@ -629,23 +640,29 @@ function createRegistryEntry( // the single catalog API; otherwise use static/empty modes with no runtime. const models = mergeModelAdditions(provider, replacementModels, additionalModels); if (hasStaticModes) { - const defaultModeId = await catalogClient.resolveDefaultModeId?.({ - config: { - provider, - cwd: options.scope === "workspace" ? options.cwd : process.cwd(), - }, - }); + const defaultModeId = await runProviderRefreshActivity( + context, + "default-mode", + async () => + await catalogClient.resolveDefaultModeId?.({ + config: { + provider, + cwd: options.scope === "workspace" ? options.cwd : process.cwd(), + }, + signal: context?.signal, + }), + ); return { models, modes: decorateModes(resolved.definition.modes), defaultModeId, }; } - const catalog = await catalogClient.fetchCatalog(options); + const catalog = await catalogClient.fetchCatalog(options, context); return { ...catalog, models, modes: decorateModes(catalog.modes) }; } - const catalog = await catalogClient.fetchCatalog(options); + const catalog = await catalogClient.fetchCatalog(options, context); return { ...catalog, models: mergeModels(provider, profileModels, additionalModels, catalog.models, { diff --git a/packages/server/src/server/agent/provider-snapshot-manager.test.ts b/packages/server/src/server/agent/provider-snapshot-manager.test.ts index 63c14e0e7a..f7118c2dbb 100644 --- a/packages/server/src/server/agent/provider-snapshot-manager.test.ts +++ b/packages/server/src/server/agent/provider-snapshot-manager.test.ts @@ -10,6 +10,7 @@ import type { AgentProvider, FetchCatalogOptions, ProviderWorkspace, + ProviderRefreshContext, ResolveAgentCreateConfigInput, } from "./agent-sdk-types.js"; import type { ManagedAgent } from "./agent-manager.js"; @@ -79,6 +80,48 @@ async function withEnv(key: string, value: string, run: () => Promise<void>): Pr } } +function waitUntilAborted(signal?: AbortSignal): Promise<boolean> { + return new Promise((_resolve, reject) => { + signal?.addEventListener("abort", () => reject(signal.reason), { once: true }); + }); +} + +function waitUntilAvailabilityAborted( + _options?: FetchCatalogOptions, + signal?: AbortSignal, +): Promise<boolean> { + return waitUntilAborted(signal); +} + +function waitForDelay(delayMs: number): Promise<void> { + return new Promise((finish) => setTimeout(finish, delayMs)); +} + +function waitForAbortWithCleanup( + signal: AbortSignal, + cleanupState: { cleanedUp: boolean }, +): Promise<void> { + return new Promise((_resolve, reject) => { + const handleAbort = () => { + cleanupState.cleanedUp = true; + reject(signal.reason); + }; + signal.addEventListener("abort", handleAbort, { once: true }); + }); +} + +async function runTestCatalogActivities( + context: ProviderRefreshContext, + cleanupState: { cleanedUp: boolean }, +): Promise<void> { + const waitForAgents = () => waitForDelay(50); + const waitForProviders = () => waitForAbortWithCleanup(context.signal, cleanupState); + await Promise.all([ + context.runActivity("app.agents", waitForAgents), + context.runActivity("provider.list", waitForProviders), + ]); +} + describe("ProviderSnapshotManager public surface", () => { test("validates complete Hub agent configurations through the current provider contract", async () => { const manager = new ProviderSnapshotManager({ @@ -391,7 +434,9 @@ describe("ProviderSnapshotManager public surface", () => { test("refreshTimeoutMs option overrides the default and yields a timeout error", async () => { // never-resolving isAvailable forces the timeout path - const isAvailable = vi.fn(() => new Promise<boolean>(() => {})); + const isAvailable = vi.fn((_options?: FetchCatalogOptions, signal?: AbortSignal) => + waitUntilAborted(signal), + ); const manager = new ProviderSnapshotManager({ logger: createTestLogger(), refreshTimeoutMs: 1, @@ -417,9 +462,192 @@ describe("ProviderSnapshotManager public surface", () => { } }); + test("defaults provider refreshes to a two-minute deadline", async () => { + vi.useFakeTimers(); + const manager = new ProviderSnapshotManager({ + logger: createTestLogger(), + providerOverrides: { + claude: { enabled: false }, + copilot: { enabled: false }, + opencode: { enabled: false }, + pi: { enabled: false }, + }, + extraClients: { + codex: createExtraClient("codex", { isAvailable: waitUntilAvailabilityAborted }), + }, + }); + + try { + const entryPromise = manager.getProvider({ + cwd: "/tmp/project", + provider: "codex", + wait: true, + }); + + await vi.advanceTimersByTimeAsync(120_000); + + await expect(entryPromise).resolves.toMatchObject({ + status: "error", + error: "Timed out refreshing Codex after 120000ms; pending: availability", + }); + } finally { + manager.destroy(); + vi.useRealTimers(); + } + }); + + test("one refresh timeout covers availability and catalog discovery", async () => { + vi.useFakeTimers(); + const manager = new ProviderSnapshotManager({ + logger: createTestLogger(), + refreshTimeoutMs: 100, + extraClients: { + codex: createExtraClient("codex", { + isAvailable: () => + new Promise((settle) => { + setTimeout(() => settle(true), 60); + }), + fetchCatalog: () => + new Promise((settle) => { + setTimeout(() => settle({ models: [], modes: [] }), 60); + }), + }), + }, + }); + + try { + const entryPromise = manager.getProvider({ + cwd: "/tmp/project", + provider: "codex", + wait: true, + }); + + await vi.advanceTimersByTimeAsync(120); + + await expect(entryPromise).resolves.toMatchObject({ + provider: "codex", + status: "error", + error: "Timed out refreshing Codex after 100ms", + }); + } finally { + manager.destroy(); + vi.useRealTimers(); + } + }); + + test("timeout names pending catalog activities, aborts them, and waits for cleanup", async () => { + vi.useFakeTimers(); + let attempt = 0; + const cleanupState = { cleanedUp: false }; + const fetchCatalog = vi.fn( + async (_options: FetchCatalogOptions, context?: ProviderRefreshContext) => { + attempt += 1; + if (attempt > 1) { + expect(cleanupState.cleanedUp).toBe(true); + return { models: [], modes: [] }; + } + + if (!context) throw new Error("missing refresh context"); + await runTestCatalogActivities(context, cleanupState); + return { models: [], modes: [] }; + }, + ); + const manager = new ProviderSnapshotManager({ + logger: createTestLogger(), + refreshTimeoutMs: 100, + extraClients: { + codex: createExtraClient("codex", { + isAvailable: async () => true, + fetchCatalog, + }), + }, + }); + + try { + const first = manager.getProvider({ + cwd: "/tmp/project", + provider: "codex", + wait: true, + }); + await vi.advanceTimersByTimeAsync(100); + await expect(first).resolves.toMatchObject({ + status: "error", + error: "Timed out refreshing Codex after 100ms; pending: provider.list", + }); + expect(cleanupState.cleanedUp).toBe(true); + + await manager.refreshSnapshotForCwd({ cwd: "/tmp/project", providers: ["codex"] }); + expect(fetchCatalog).toHaveBeenCalledTimes(2); + expect( + manager.getSnapshot("/tmp/project").find((entry) => entry.provider === "codex"), + ).toMatchObject({ provider: "codex", status: "ready" }); + } finally { + manager.destroy(); + vi.useRealTimers(); + } + }); + + test("each provider refresh gets its own timeout", async () => { + vi.useFakeTimers(); + const neverReturnsCatalog = (_options: FetchCatalogOptions, context?: ProviderRefreshContext) => + new Promise<never>((_resolve, reject) => { + context?.signal.addEventListener("abort", () => reject(context.signal.reason), { + once: true, + }); + }); + const manager = new ProviderSnapshotManager({ + logger: createTestLogger(), + refreshTimeoutMs: 100, + extraClients: { + codex: createExtraClient("codex", { + isAvailable: async () => true, + fetchCatalog: neverReturnsCatalog, + }), + claude: createExtraClient("claude", { + isAvailable: async () => true, + fetchCatalog: neverReturnsCatalog, + }), + }, + }); + + try { + const codexPromise = manager.getProvider({ + cwd: "/tmp/project", + provider: "codex", + wait: true, + }); + await vi.advanceTimersByTimeAsync(50); + + const claudePromise = manager.getProvider({ + cwd: "/tmp/project", + provider: "claude", + wait: true, + }); + await vi.advanceTimersByTimeAsync(50); + + await expect(codexPromise).resolves.toMatchObject({ + provider: "codex", + status: "error", + }); + await expect( + manager.getProvider({ cwd: "/tmp/project", provider: "claude", wait: false }), + ).resolves.toMatchObject({ provider: "claude", status: "loading" }); + + await vi.advanceTimersByTimeAsync(50); + + await expect(claudePromise).resolves.toMatchObject({ + provider: "claude", + status: "error", + }); + } finally { + manager.destroy(); + vi.useRealTimers(); + } + }); + test("PASEO_PROVIDER_REFRESH_TIMEOUT_MS env var is honored when no option is given", async () => { vi.stubEnv("PASEO_PROVIDER_REFRESH_TIMEOUT_MS", "1"); - const isAvailable = vi.fn(() => new Promise<boolean>(() => {})); + const isAvailable = vi.fn(waitUntilAvailabilityAborted); const manager = new ProviderSnapshotManager({ logger: createTestLogger(), providerOverrides: { @@ -446,7 +674,7 @@ describe("ProviderSnapshotManager public surface", () => { test("PASEO_PROVIDER_REFRESH_TIMEOUT_MS env var is ignored when option is provided", async () => { vi.stubEnv("PASEO_PROVIDER_REFRESH_TIMEOUT_MS", "1"); - const isAvailable = vi.fn(() => new Promise<boolean>(() => {})); + const isAvailable = vi.fn(waitUntilAvailabilityAborted); const manager = new ProviderSnapshotManager({ logger: createTestLogger(), refreshTimeoutMs: 5, diff --git a/packages/server/src/server/agent/provider-snapshot-manager.ts b/packages/server/src/server/agent/provider-snapshot-manager.ts index a0f1342774..ee2df16b28 100644 --- a/packages/server/src/server/agent/provider-snapshot-manager.ts +++ b/packages/server/src/server/agent/provider-snapshot-manager.ts @@ -17,6 +17,10 @@ import { type ProviderSnapshotEntry, type ProviderWorkspace, } from "./agent-sdk-types.js"; +import { + raceProviderRefreshAbort, + runProviderRefreshWithDeadline, +} from "./provider-refresh-deadline.js"; import type { ManagedAgent } from "./agent-manager.js"; import type { WorkspaceGitService } from "../workspace-git-service.js"; import type { ManagedProcessRegistry } from "../managed-processes/managed-processes.js"; @@ -42,7 +46,8 @@ import { validateAgentConfigurationAgainstProvider, } from "./agent-configuration-validator.js"; -const DEFAULT_REFRESH_TIMEOUT_MS = 60_000; +const DEFAULT_REFRESH_TIMEOUT_MS = 120_000; +const MAX_REFRESH_TIMEOUT_MS = 2_147_483_647; const DEFAULT_DIAGNOSTIC_TIMEOUT_MS = 120_000; const REFRESH_TIMEOUT_ENV_VAR = "PASEO_PROVIDER_REFRESH_TIMEOUT_MS"; export const GLOBAL_PROVIDER_SNAPSHOT_KEY = "paseo:global"; @@ -51,14 +56,19 @@ export const GLOBAL_PROVIDER_SNAPSHOT_KEY = "paseo:global"; // `copilot --acp` invocation, OpenCode workspace probes with many MCP servers). // Allow operators to bump the ceiling via env var without rebuilding. function resolveRefreshTimeoutMs(option: number | undefined): number { - if (typeof option === "number" && Number.isFinite(option) && option > 0) { + if ( + typeof option === "number" && + Number.isSafeInteger(option) && + option > 0 && + option <= MAX_REFRESH_TIMEOUT_MS + ) { return option; } const fromEnv = process.env[REFRESH_TIMEOUT_ENV_VAR]; if (fromEnv) { // Number() handles scientific notation (e.g. "6e4") which parseInt would silently truncate. const parsed = Number(fromEnv); - if (Number.isFinite(parsed) && parsed > 0) { + if (Number.isSafeInteger(parsed) && parsed > 0 && parsed <= MAX_REFRESH_TIMEOUT_MS) { return parsed; } } @@ -580,7 +590,7 @@ export class ProviderSnapshotManager { client.resolveCreateConfig?.bind(client) ?? definition.resolveCreateConfig, isCreateConfigUnattended: client.isCreateConfigUnattended?.bind(client) ?? definition.isCreateConfigUnattended, - fetchCatalog: client.fetchCatalog.bind(client), + fetchCatalog: (options, _client, context) => client.fetchCatalog(options, context), }; } @@ -899,23 +909,29 @@ export class ProviderSnapshotManager { } const client = this.ensureClient(provider, definition); - const catalogOptions = createFetchCatalogOptions(catalogScope, force); - const available = await withTimeout( - client.isAvailable(catalogOptions), - this.refreshTimeoutMs, - `Timed out checking ${definition.label} availability after ${this.refreshTimeoutMs}ms`, - ); - if (!available) { + const catalog = await runProviderRefreshWithDeadline({ + label: definition.label, + timeoutMs: this.refreshTimeoutMs, + operation: async (context) => { + const catalogOptions = createFetchCatalogOptions(catalogScope, force); + const available = await context.runActivity("availability", () => + raceProviderRefreshAbort( + context.signal, + client.isAvailable(catalogOptions, context.signal), + ), + ); + if (!available) { + return null; + } + + return await definition.fetchCatalog(catalogOptions, client, context); + }, + }); + if (!catalog) { setEntry({ ...base, status: "unavailable", enabled: true }); return; } - const catalog = await withTimeout( - definition.fetchCatalog({ ...catalogOptions, timeoutMs: this.refreshTimeoutMs }, client), - this.refreshTimeoutMs, - `Timed out refreshing ${definition.label} after ${this.refreshTimeoutMs}ms`, - ); - setEntry({ ...base, defaultModeId: diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index 3b16406849..68d1ce7615 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -85,6 +85,7 @@ import { type AgentTimelineItem, type AgentUsage, type FetchCatalogOptions, + type ProviderRefreshContext, type ImportableProviderSession, type ImportProviderSessionContext, type ImportProviderSessionInput, @@ -97,6 +98,10 @@ import { type ToolCallDetail, type ToolCallTimelineItem, } from "../agent-sdk-types.js"; +import { + raceProviderRefreshAbort, + runProviderRefreshActivity, +} from "../provider-refresh-deadline.js"; import { isDefaultAgentCreateConfigUnattended, resolveDefaultAgentCreateConfig, @@ -274,7 +279,6 @@ export function buildACPClientCapabilities( // sign-in URL in the browser) when probing an ACP agent for models/modes. // NO_BROWSER is honored by Gemini CLI; other ACP agents ignore it. const PROBE_ENV: Record<string, string> = { NO_BROWSER: "true" }; -const ACP_CATALOG_TIMEOUT_MS = 60_000; const ACP_DIAGNOSTIC_PHASE_TIMEOUT_MS = 20_000; function summarizeMalformedACPStdoutError(error: unknown): { type: string; message: string } { @@ -949,68 +953,85 @@ export class ACPAgentClient implements AgentClient { return session; } - async fetchCatalog(options: FetchCatalogOptions): Promise<ProviderCatalog> { + async fetchCatalog( + options: FetchCatalogOptions, + context?: ProviderRefreshContext, + ): Promise<ProviderCatalog> { const workspace = providerWorkspaceFromCatalogOptions(options); const cwd = workspace?.cwd ?? (options.scope === "global" ? homedir() : options.cwd); - const timeoutMs = options.timeoutMs ?? ACP_CATALOG_TIMEOUT_MS; let probe: UninitializedACPProcess | null = null; - try { - const catalogProbe = (async () => { - const initializedProbe = await this.spawnProcess(PROBE_ENV, { - workspace, - initializeTimeoutMs: timeoutMs, - onSpawned: (spawned) => { - probe = spawned; - }, - }); - probe = initializedProbe; - const response = await this.runACPRequest(() => - initializedProbe.connection.newSession({ - cwd, - mcpServers: [], - }), - ); - const transformed = this.transformSessionResponse(response); - const derivedModels = deriveModelDefinitionsFromACP( - this.provider, - transformed.models, - transformed.configOptions, - ); - const models = this.catalogModelResolver - ? await this.catalogModelResolver({ - connection: initializedProbe.connection, - sessionId: response.sessionId, - models: derivedModels, - configOptions: transformed.configOptions, - runRequest: (request) => this.runACPRequest(request), - transformConfigOptions: (configOptions) => - this.configOptionsTransformer - ? this.configOptionsTransformer(configOptions) - : configOptions, - logger: this.logger, - provider: this.provider, - }) - : derivedModels; - const modeInfo = deriveModesFromACP( - this.defaultModes, - transformed.modes, - transformed.configOptions, - ); - return { - models: this.modelTransformer ? this.modelTransformer(models) : models, - modes: modeInfo.modes, - }; - })(); + let closePromise: Promise<void> | null = null; + const closeProbe = (): Promise<void> => { + if (!probe) return Promise.resolve(); + closePromise ??= this.closeProbe(probe); + return closePromise; + }; + const handleAbort = () => void closeProbe().catch(() => undefined); + context?.signal.addEventListener("abort", handleAbort, { once: true }); - return await withTimeout( - catalogProbe, - timeoutMs, - `ACP catalog probe timed out after ${timeoutMs}ms`, + try { + const initializedProbe = await runProviderRefreshActivity(context, "initialize", () => + raceProviderRefreshAbort( + context?.signal, + this.spawnProcess(PROBE_ENV, { + workspace, + onSpawned: (spawned) => { + probe = spawned; + if (context?.signal.aborted) void closeProbe().catch(() => undefined); + }, + }), + ), ); + probe = initializedProbe; + const response = await runProviderRefreshActivity(context, "session/new", () => + raceProviderRefreshAbort( + context?.signal, + this.runACPRequest(() => + initializedProbe.connection.newSession({ + cwd, + mcpServers: [], + }), + ), + ), + ); + const transformed = this.transformSessionResponse(response); + const derivedModels = deriveModelDefinitionsFromACP( + this.provider, + transformed.models, + transformed.configOptions, + ); + const models = this.catalogModelResolver + ? await runProviderRefreshActivity(context, "catalog.resolve", () => + raceProviderRefreshAbort( + context?.signal, + this.catalogModelResolver?.({ + connection: initializedProbe.connection, + sessionId: response.sessionId, + models: derivedModels, + configOptions: transformed.configOptions, + runRequest: (request) => this.runACPRequest(request), + transformConfigOptions: (configOptions) => + this.configOptionsTransformer + ? this.configOptionsTransformer(configOptions) + : configOptions, + logger: this.logger, + provider: this.provider, + }) ?? Promise.resolve(derivedModels), + ), + ) + : derivedModels; + const modeInfo = deriveModesFromACP( + this.defaultModes, + transformed.modes, + transformed.configOptions, + ); + return { + models: this.modelTransformer ? this.modelTransformer(models) : models, + modes: modeInfo.modes, + }; } finally { - if (probe) { - await this.closeProbe(probe); - } + context?.signal.removeEventListener("abort", handleAbort); + await closeProbe(); } } diff --git a/packages/server/src/server/agent/providers/claude/agent.env.test.ts b/packages/server/src/server/agent/providers/claude/agent.env.test.ts index 018894af65..642e562f61 100644 --- a/packages/server/src/server/agent/providers/claude/agent.env.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.env.test.ts @@ -68,6 +68,12 @@ describe("Claude SDK env", () => { logger: createTestLogger(), queryFactory, resolveBinary: async () => "/test/claude/bin", + runtimeSettings: { + env: { + MCP_TIMEOUT: "claude-startup-timeout", + MCP_TOOL_TIMEOUT: "claude-tool-timeout", + }, + }, }); const session = await client.createSession( { @@ -82,6 +88,8 @@ describe("Claude SDK env", () => { expect(result.sessionId).toBe("managed-agent-env-session"); expect(capturedEnv?.PASEO_AGENT_ID).toBe(launchContext.env?.PASEO_AGENT_ID); expect(capturedEnv?.PASEO_TEST_FLAG).toBe(launchContext.env?.PASEO_TEST_FLAG); + expect(capturedEnv?.MCP_TIMEOUT).toBe("claude-startup-timeout"); + expect(capturedEnv?.MCP_TOOL_TIMEOUT).toBe("claude-tool-timeout"); } finally { await session.close(); } diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index 3fae8de5b6..96730bb751 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -26,6 +26,7 @@ import type { interface TestClaudeSession { translateMessageToEvents(message: SDKMessage): AgentStreamEvent[]; + close(): Promise<void>; } function createStateWorkspace(overrides: Partial<ProviderWorkspace> = {}): ProviderWorkspace { @@ -1436,6 +1437,52 @@ describe("ClaudeAgentSession context window usage", () => { }); } + test("emits canonical task snapshots from Claude TaskCreate results", async () => { + const session = await createSessionForTest(); + try { + session.translateMessageToEvents({ + type: "assistant", + message: { + content: [ + { + type: "tool_use", + id: "task-create-1", + name: "TaskCreate", + input: { subject: "Inspect provider", activeForm: "Inspecting provider" }, + }, + ], + }, + } as unknown as SDKMessage); + + const events = session.translateMessageToEvents({ + type: "user", + message: { + content: [{ type: "tool_result", tool_use_id: "task-create-1", content: "created" }], + }, + toolUseResult: { task: { id: "1", subject: "Inspect provider" } }, + } as unknown as SDKMessage); + + expect(events).toContainEqual({ + type: "timeline", + provider: "claude", + item: { + type: "todo", + items: [ + { + id: "1", + text: "Inspect provider", + activeForm: "Inspecting provider", + status: "pending", + completed: false, + }, + ], + }, + }); + } finally { + await session.close(); + } + }); + async function collectStreamEvents(session: AgentSession, prompt = "turn") { const events: AgentStreamEvent[] = []; for await (const event of streamSession(session, prompt)) { diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index fc047eb498..6e6b0a682f 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -45,6 +45,7 @@ import { } from "./model-manifest.js"; import { parsePartialJsonObject } from "./partial-json.js"; import { ClaudeSidechainTracker } from "./sidechain-tracker.js"; +import { ClaudeTaskState } from "./task-state.js"; import { ClaudeTaskProtocolSource, type ClaudeHookObservationInput, @@ -122,9 +123,11 @@ import { type McpServerConfig, type ProviderCatalog, type ProviderWorkspace, + type ProviderRefreshContext, type ResolveAgentDefaultModeInput, } from "../../agent-sdk-types.js"; import { importSessionFromPersistence } from "../../provider-session-import.js"; +import { runProviderRefreshActivity } from "../../provider-refresh-deadline.js"; import { checkProviderLaunchAvailable, createProviderEnv, @@ -409,7 +412,7 @@ interface ClaudeAgentClientOptions { runtimeSettings?: ProviderRuntimeSettings; queryFactory?: ClaudeQueryFactory; resolveBinary?: () => Promise<string>; - resolveVersion?: () => Promise<string>; + resolveVersion?: (signal?: AbortSignal) => Promise<string>; configDir?: string; } @@ -1494,7 +1497,7 @@ export class ClaudeAgentClient implements AgentClient { private readonly runtimeSettings?: ProviderRuntimeSettings; private readonly queryFactory?: ClaudeQueryFactory; private readonly resolveBinary: () => Promise<string>; - private readonly resolveVersion: () => Promise<string>; + private readonly resolveVersion: (signal?: AbortSignal) => Promise<string>; private readonly configDir?: string; constructor(options: ClaudeAgentClientOptions) { @@ -1504,7 +1507,8 @@ export class ClaudeAgentClient implements AgentClient { this.queryFactory = options.queryFactory; this.resolveBinary = options.resolveBinary ?? (() => resolveClaudeBinary(this.runtimeSettings)); this.resolveVersion = - options.resolveVersion ?? (() => resolveClaudeCodeVersion(this.runtimeSettings)); + options.resolveVersion ?? + ((signal) => resolveClaudeCodeVersion(this.runtimeSettings, signal)); this.configDir = options.configDir; } @@ -1560,19 +1564,26 @@ export class ClaudeAgentClient implements AgentClient { }); } - async fetchCatalog(options: FetchCatalogOptions): Promise<ProviderCatalog> { + async fetchCatalog( + options: FetchCatalogOptions, + context?: ProviderRefreshContext, + ): Promise<ProviderCatalog> { const workspace = providerWorkspaceFromCatalogOptions(options); let claudeCodeVersion: string | undefined; try { - claudeCodeVersion = workspace - ? await resolveWorkspaceClaudeCodeVersion(workspace, this.runtimeSettings) - : await this.resolveVersion(); + claudeCodeVersion = await runProviderRefreshActivity(context, "version", () => + workspace + ? resolveWorkspaceClaudeCodeVersion(workspace, this.runtimeSettings) + : this.resolveVersion(context?.signal), + ); } catch (error) { this.logger.warn({ err: error }, "Failed to resolve Claude Code version for model catalog"); } const models = workspace ? getClaudeModels(claudeCodeVersion) - : await getClaudeModelsWithSettings(this.logger, this.configDir, claudeCodeVersion); + : await runProviderRefreshActivity(context, "settings", () => + getClaudeModelsWithSettings(this.logger, this.configDir, claudeCodeVersion), + ); const modes = detectIneligibleAutoModeTransport( workspace ? providerWorkspaceEnvironment([this.runtimeSettings?.env]) @@ -1714,6 +1725,7 @@ async function resolveClaudeBinary(runtimeSettings?: ProviderRuntimeSettings): P export async function resolveClaudeCodeVersion( runtimeSettings?: ProviderRuntimeSettings, + signal?: AbortSignal, ): Promise<string> { const launch = await resolveProviderLaunch({ commandConfig: runtimeSettings?.command, @@ -1727,6 +1739,7 @@ export async function resolveClaudeCodeVersion( const { stdout, stderr } = await execCommand(executable, [...launch.args, "--version"], { ...createProviderEnvSpec({ runtimeSettings }), timeout: 5_000, + signal, }); const version = parseClaudeCodeVersion(`${stdout}\n${stderr}`); if (!version) { @@ -2081,6 +2094,7 @@ class ClaudeAgentSession implements AgentSession { private autonomousTurn: AutonomousTurnState | null = null; private readonly subscribers = new Set<(event: AgentStreamEvent) => void>(); private readonly timelineAssembler = new TimelineAssembler(); + private readonly taskState = new ClaudeTaskState(); private readonly taskProtocolSource = new ClaudeTaskProtocolSource({ getToolInput: (toolUseId) => this.toolUseCache.get(toolUseId)?.input ?? null, readWorkflowResult: (outputFile) => @@ -2855,6 +2869,7 @@ class ClaudeAgentSession implements AgentSession { this.userMessageIds = []; this.emittedUserMessageIds.clear(); this.rewindTurnAnchors.length = 0; + this.taskState.reset(); this.historyLoadPromise = this.loadPersistedHistory(sessionId); if (oldSessionId && oldSessionId !== sessionId) { this.dispatchEvents([ @@ -2885,6 +2900,7 @@ class ClaudeAgentSession implements AgentSession { this.userMessageIds = []; this.emittedUserMessageIds.clear(); this.rewindTurnAnchors.length = 0; + this.taskState.reset(); } private rememberUserMessageId(messageId: string | null | undefined): void { @@ -3134,14 +3150,7 @@ class ClaudeAgentSession implements AgentSession { return createProviderEnv({ baseEnv: process.env, runtimeSettings: this.runtimeSettings, - overlays: [ - { - // Increase MCP timeouts for long-running tool calls (10 minutes) - MCP_TIMEOUT: "600000", - MCP_TOOL_TIMEOUT: "600000", - }, - this.launchEnv, - ], + overlays: [this.launchEnv], }); } @@ -3900,6 +3909,7 @@ class ClaudeAgentSession implements AgentSession { } const events: AgentStreamEvent[] = []; + this.appendTaskStateEvent(message, events); // Subagent identity and lifecycle are announced by Claude Code's task protocol, so they are // read rather than inferred from sidechain frames. `task_started` precedes the child's first @@ -3964,6 +3974,11 @@ class ClaudeAgentSession implements AgentSession { return events; } + private appendTaskStateEvent(message: SDKMessage, events: AgentStreamEvent[]): void { + const item = this.taskState.observe(message); + if (item) events.push({ type: "timeline", provider: "claude", item }); + } + /** * A frame from inside a subagent, routed to that child rather than the parent's transcript. */ @@ -4622,6 +4637,7 @@ class ClaudeAgentSession implements AgentSession { private async loadPersistedHistory(sessionId: string): Promise<void> { try { + this.taskState.reset(); const historyPath = await this.resolveHistoryPath(sessionId); if (!historyPath) return; const content = this.workspace @@ -4750,7 +4766,8 @@ class ClaudeAgentSession implements AgentSession { } const historyTimestamp = normalizeProviderReplayTimestamp(entry.timestamp); - const items = this.convertHistoryEntry(entry); + const taskSnapshot = this.taskState.observe(entry); + const items = [...(taskSnapshot ? [taskSnapshot] : []), ...this.convertHistoryEntry(entry)]; const isVisibleUserEntry = entry.type === "user" && typeof entry.uuid === "string" && diff --git a/packages/server/src/server/agent/providers/claude/task-state.test.ts b/packages/server/src/server/agent/providers/claude/task-state.test.ts new file mode 100644 index 0000000000..b175cc58ea --- /dev/null +++ b/packages/server/src/server/agent/providers/claude/task-state.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, test } from "vitest"; +import { ClaudeTaskState } from "./task-state.js"; + +function toolUse(id: string, name: string, input: Record<string, unknown>) { + return { type: "assistant", message: { content: [{ type: "tool_use", id, name, input }] } }; +} + +function toolResult(id: string, result: Record<string, unknown>) { + return { + type: "user", + message: { content: [{ type: "tool_result", tool_use_id: id, content: "ok" }] }, + toolUseResult: result, + }; +} + +describe("ClaudeTaskState", () => { + test("accumulates TaskCreate and TaskUpdate mutations by task id", () => { + const state = new ClaudeTaskState(); + state.observe( + toolUse("create-1", "TaskCreate", { subject: "Alpha", activeForm: "Doing alpha" }), + ); + expect(state.observe(toolResult("create-1", { task: { id: "1", subject: "Alpha" } }))).toEqual({ + type: "todo", + items: [ + { id: "1", text: "Alpha", activeForm: "Doing alpha", status: "pending", completed: false }, + ], + }); + + state.observe(toolUse("update-1", "TaskUpdate", { taskId: "1", status: "in_progress" })); + expect(state.observe(toolResult("update-1", { success: true, taskId: "1" }))).toEqual({ + type: "todo", + items: [ + { + id: "1", + text: "Alpha", + activeForm: "Doing alpha", + status: "in_progress", + completed: false, + }, + ], + }); + }); + + test("removes deleted tasks and ignores replayed results", () => { + const state = new ClaudeTaskState(); + state.observe(toolUse("create", "TaskCreate", { subject: "Disposable" })); + state.observe(toolResult("create", { task: { id: "1", subject: "Disposable" } })); + state.observe(toolUse("delete", "TaskUpdate", { taskId: "1", status: "deleted" })); + const result = toolResult("delete", { success: true, taskId: "1" }); + expect(state.observe(result)).toEqual({ type: "todo", items: [] }); + expect(state.observe(result)).toBeNull(); + }); + + test("preserves status when TaskUpdate changes only descriptive fields", () => { + const state = new ClaudeTaskState(); + state.observe(toolUse("create", "TaskCreate", { subject: "Original" })); + state.observe(toolResult("create", { task: { id: "1", subject: "Original" } })); + state.observe(toolUse("complete", "TaskUpdate", { taskId: "1", status: "completed" })); + state.observe(toolResult("complete", { success: true, taskId: "1" })); + state.observe(toolUse("rename", "TaskUpdate", { taskId: "1", subject: "Renamed" })); + + expect(state.observe(toolResult("rename", { success: true, taskId: "1" }))).toEqual({ + type: "todo", + items: [{ id: "1", text: "Renamed", status: "completed", completed: true }], + }); + }); + + test("replaces state from TodoWrite and TaskList snapshots", () => { + const state = new ClaudeTaskState(); + expect( + state.observe( + toolUse("legacy", "TodoWrite", { + todos: [{ content: "Legacy", status: "in_progress", activeForm: "Working" }], + }), + ), + ).toEqual({ + type: "todo", + items: [ + { + id: "legacy:0", + text: "Legacy", + activeForm: "Working", + status: "in_progress", + completed: false, + }, + ], + }); + state.observe(toolUse("list", "TaskList", {})); + expect( + state.observe( + toolResult("list", { + tasks: [{ id: "7", subject: "Current", status: "completed", activeForm: "Finishing" }], + }), + ), + ).toEqual({ + type: "todo", + items: [ + { id: "7", text: "Current", activeForm: "Finishing", status: "completed", completed: true }, + ], + }); + }); + + test("keeps synthetic TodoWrite task ids stable across snapshots", () => { + const state = new ClaudeTaskState(); + state.observe( + toolUse("first", "TodoWrite", { + todos: [{ content: "Stable", status: "pending" }], + }), + ); + + expect( + state.observe( + toolUse("second", "TodoWrite", { + todos: [{ content: "Stable", status: "completed" }], + }), + ), + ).toEqual({ + type: "todo", + items: [{ id: "legacy:0", text: "Stable", status: "completed", completed: true }], + }); + }); + + test("does not confuse Claude's subagent Task tool with task tracking", () => { + const state = new ClaudeTaskState(); + expect(state.observe(toolUse("subagent", "Task", { description: "delegate" }))).toBeNull(); + }); +}); diff --git a/packages/server/src/server/agent/providers/claude/task-state.ts b/packages/server/src/server/agent/providers/claude/task-state.ts new file mode 100644 index 0000000000..bcb4df26bc --- /dev/null +++ b/packages/server/src/server/agent/providers/claude/task-state.ts @@ -0,0 +1,202 @@ +import type { AgentTaskItem, AgentTimelineItem } from "../../agent-sdk-types.js"; + +type TaskToolName = "TodoWrite" | "TaskCreate" | "TaskUpdate" | "TaskList"; + +interface PendingTaskTool { + name: TaskToolName; + input: Record<string, unknown>; +} + +function record(value: unknown): Record<string, unknown> | null { + return typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Record<string, unknown>) + : null; +} + +function string(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +function taskStatus(value: unknown): AgentTaskItem["status"] | "deleted" { + if (value === "completed" || value === "deleted" || value === "in_progress") return value; + return "pending"; +} + +function retainedTaskStatus(task: AgentTaskItem): AgentTaskItem["status"] { + if (task.status) return task.status; + return task.completed ? "completed" : "pending"; +} + +function toTaskItem(value: unknown): AgentTaskItem | null { + const task = record(value); + if (!task) return null; + const text = string(task.subject) ?? string(task.content) ?? string(task.text); + if (!text) return null; + const status = taskStatus(task.status); + if (status === "deleted") return null; + const id = string(task.id) ?? string(task.taskId); + const activeForm = string(task.activeForm) ?? string(task.active_form); + return { + ...(id ? { id } : {}), + text, + status, + completed: status === "completed", + ...(activeForm ? { activeForm } : {}), + }; +} + +function toolUses(message: Record<string, unknown>): Array<Record<string, unknown>> { + const content = record(message.message)?.content; + return Array.isArray(content) + ? content.flatMap((block) => { + const candidate = record(block); + return candidate?.type === "tool_use" ? [candidate] : []; + }) + : []; +} + +function toolResultId(message: Record<string, unknown>): string | undefined { + const content = record(message.message)?.content; + if (!Array.isArray(content)) return undefined; + for (const block of content) { + const candidate = record(block); + if (candidate?.type === "tool_result") return string(candidate.tool_use_id); + } + return undefined; +} + +function structuredResult(message: Record<string, unknown>): Record<string, unknown> | null { + return record(message.toolUseResult) ?? record(message.tool_use_result); +} + +/** Accumulates Claude's snapshot and ID-based task tools into canonical todo snapshots. */ +export class ClaudeTaskState { + private readonly tasks = new Map<string, AgentTaskItem>(); + private readonly calls = new Map<string, PendingTaskTool>(); + private readonly appliedResults = new Set<string>(); + + observe(value: unknown): Extract<AgentTimelineItem, { type: "todo" }> | null { + const message = record(value); + if (!message) return null; + + let snapshot: Extract<AgentTimelineItem, { type: "todo" }> | null = null; + for (const block of toolUses(message)) { + const id = string(block.id); + const name = string(block.name); + if (!id || !isTaskToolName(name)) continue; + const input = record(block.input) ?? {}; + this.calls.set(id, { name, input }); + if (name === "TodoWrite") snapshot = this.replaceLegacyTodos(input.todos); + } + + const resultId = toolResultId(message); + if (!resultId || this.appliedResults.has(resultId)) return snapshot; + const call = this.calls.get(resultId); + if (!call) return snapshot; + this.appliedResults.add(resultId); + this.calls.delete(resultId); + return this.applyResult(call, structuredResult(message)) ?? snapshot; + } + + reset(): void { + this.tasks.clear(); + this.calls.clear(); + this.appliedResults.clear(); + } + + private replaceLegacyTodos(value: unknown): Extract<AgentTimelineItem, { type: "todo" }> { + this.tasks.clear(); + if (Array.isArray(value)) { + for (const [index, taskValue] of value.entries()) { + const item = toTaskItem(taskValue); + if (!item) continue; + const id = item.id ?? `legacy:${index}`; + this.tasks.set(id, { ...item, id }); + } + } + return this.snapshot(); + } + + private applyResult( + call: PendingTaskTool, + result: Record<string, unknown> | null, + ): Extract<AgentTimelineItem, { type: "todo" }> | null { + if (result?.success === false) return null; + if (call.name === "TaskCreate") return this.applyCreate(call.input, result); + if (call.name === "TaskUpdate") return this.applyUpdate(call.input, result); + if (call.name === "TaskList") return this.applyList(result); + return null; + } + + private applyCreate( + input: Record<string, unknown>, + result: Record<string, unknown> | null, + ): Extract<AgentTimelineItem, { type: "todo" }> | null { + const resultTask = record(result?.task); + const id = string(resultTask?.id) ?? string(result?.taskId); + const text = string(resultTask?.subject) ?? string(input.subject); + if (!id || !text) return null; + const activeForm = string(input.activeForm); + this.tasks.set(id, { + id, + text, + status: "pending", + completed: false, + ...(activeForm ? { activeForm } : {}), + }); + return this.snapshot(); + } + + private applyUpdate( + input: Record<string, unknown>, + result: Record<string, unknown> | null, + ): Extract<AgentTimelineItem, { type: "todo" }> | null { + const id = string(input.taskId) ?? string(result?.taskId); + if (!id) return null; + const current = this.tasks.get(id); + if (!current) return null; + const statusValue = input.status ?? record(result?.statusChange)?.to; + const status = + statusValue === undefined ? retainedTaskStatus(current) : taskStatus(statusValue); + if (status === "deleted") { + this.tasks.delete(id); + return this.snapshot(); + } + const text = string(input.subject); + const activeForm = string(input.activeForm); + this.tasks.set(id, { + ...current, + ...(text ? { text } : {}), + ...(activeForm ? { activeForm } : {}), + status, + completed: status === "completed", + }); + return this.snapshot(); + } + + private applyList( + result: Record<string, unknown> | null, + ): Extract<AgentTimelineItem, { type: "todo" }> | null { + const tasks = result?.tasks; + if (!Array.isArray(tasks)) return null; + this.tasks.clear(); + for (const taskValue of tasks) { + const item = toTaskItem(taskValue); + if (item?.id) this.tasks.set(item.id, item); + } + return this.snapshot(); + } + + private snapshot(): Extract<AgentTimelineItem, { type: "todo" }> { + return { type: "todo", items: [...this.tasks.values()] }; + } +} + +function isTaskToolName(value: string | undefined): value is TaskToolName { + return ( + value === "TodoWrite" || + value === "TaskCreate" || + value === "TaskUpdate" || + value === "TaskList" + ); +} diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index b878acf5ec..83879ab45f 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -23,10 +23,30 @@ import { codexAppServerTurnInputFromPrompt, listCodexSkills, mapCodexPatchNotificationToToolCall, + mapCodexPlanUpdateToTodo, mapCodexPlanToToolCall, normalizeCodexOutputSchema, toAgentUsage, } from "./codex-app-server-agent.js"; + +describe("mapCodexPlanUpdateToTodo", () => { + test("preserves checklist progress without creating a plan card", () => { + expect( + mapCodexPlanUpdateToTodo([ + { step: "Inspect", status: "completed" }, + { step: "Implement", status: "inProgress" }, + { step: "Verify", status: "pending" }, + ]), + ).toEqual({ + type: "todo", + items: [ + { id: "0", text: "Inspect", status: "completed", completed: true }, + { id: "1", text: "Implement", status: "in_progress", completed: false }, + { id: "2", text: "Verify", status: "pending", completed: false }, + ], + }); + }); +}); import { CodexAppServerClient } from "./codex/app-server-transport.js"; import { createFakeCodexAppServer, @@ -71,6 +91,11 @@ type CodexTestSession = AgentSession & { client: CodexClientLike | null; }; +type TurnTerminalEvent = Extract< + AgentStreamEvent, + { type: "turn_completed" | "turn_failed" | "turn_canceled" } +>; + const ONE_BY_ONE_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X1r0AAAAASUVORK5CYII="; const CODEX_PROVIDER = "codex"; @@ -180,6 +205,41 @@ function createProviderWithFakeAppServer(appServer: FakeCodexAppServer): CodexAp return provider; } +async function startCompactionTurnTest(): Promise<{ + appServer: FakeCodexAppServer; + session: AgentSession; + events: AgentStreamEvent[]; + terminalEvent: Promise<TurnTerminalEvent>; +}> { + const appServer = createFakeCodexAppServer(); + const session = new CodexAppServerAgentSession( + createConfig({ cwd: "/workspace/project" }), + null, + createTestLogger(), + async () => appServer.child, + ); + const events: AgentStreamEvent[] = []; + const terminalEvent = new Promise<TurnTerminalEvent>((resolve) => { + session.subscribe((event) => { + const isCompaction = event.type === "timeline" && event.item.type === "compaction"; + const isTerminal = + event.type === "turn_completed" || + event.type === "turn_failed" || + event.type === "turn_canceled"; + if (isCompaction || isTerminal) { + events.push(event); + } + if (isTerminal) { + resolve(event); + } + }); + }); + + await session.startTurn("exercise compaction lifecycle"); + appServer.startsTurn({ threadId: "thread-1", turnId: "codex-turn-1" }); + return { appServer, session, events, terminalEvent }; +} + function archivedThreadHandle() { return { sessionId: "archived-thread-id", @@ -2365,6 +2425,80 @@ describe("Codex app-server provider", () => { }); }); + test("keeps a settled child completed until Codex starts another child turn", async () => { + const appServer = createFakeCodexAppServer(); + const session = new CodexAppServerAgentSession( + createConfig({ cwd: "/workspace/project" }), + null, + createTestLogger(), + async () => appServer.child, + ); + const events: AgentStreamEvent[] = []; + session.subscribe((event) => events.push(event)); + + try { + const resultPromise = session.run("Delegate the investigation."); + await appServer.waitForTurnStart(); + + appServer.startsSubAgent({ + callId: "call-settled-child", + threadId: "settled-child-thread", + agentPath: "/root/settled-child", + }); + appServer.completeTurn({ threadId: "settled-child-thread" }); + appServer.says({ + threadId: "settled-child-thread", + itemId: "late-child-message", + text: "Late trailing output.", + }); + + const providerUpserts = events.flatMap((event) => + event.type === "provider_subagent" && event.event.type === "upsert" ? [event.event] : [], + ); + expect(providerUpserts.at(-1)).toMatchObject({ + id: "settled-child-thread", + status: "completed", + }); + expect(events.at(-1)).toMatchObject({ + type: "timeline", + item: { + callId: "call-settled-child", + status: "completed", + detail: { type: "sub_agent", log: "[Assistant] Late trailing output." }, + }, + }); + + appServer.completesSubAgentActivity({ + callId: "late-child-interaction", + threadId: "settled-child-thread", + agentPath: "/root/settled-child", + kind: "interacted", + }); + expect( + events.findLast( + (event) => event.type === "provider_subagent" && event.event.type === "upsert", + ), + ).toMatchObject({ event: { id: "settled-child-thread", status: "completed" } }); + + appServer.startsTurn({ + threadId: "settled-child-thread", + turnId: "next-child-turn", + }); + expect( + events.findLast( + (event) => event.type === "provider_subagent" && event.event.type === "upsert", + ), + ).toMatchObject({ event: { id: "settled-child-thread", status: "running" } }); + + appServer.completeTurn({ threadId: "settled-child-thread" }); + appServer.completeTurn(); + await resultPromise; + appServer.assertNoErrors(); + } finally { + await session.close(); + } + }); + test("updates a registered child with its later native activity name", () => { const session = createSession(); const events: AgentStreamEvent[] = []; @@ -3577,6 +3711,10 @@ describe("Codex app-server provider", () => { const liveEvents: AgentStreamEvent[] = []; session.subscribe((event) => liveEvents.push(event)); + asInternals(session).handleNotification("turn/started", { + threadId: "v2-child-thread", + turn: { id: "v2-child-turn-after-resume" }, + }); asInternals(session).handleNotification("item/completed", { threadId: "test-thread", item: { @@ -3605,6 +3743,10 @@ describe("Codex app-server provider", () => { }); liveEvents.length = 0; + asInternals(session).handleNotification("turn/started", { + threadId: "legacy-child-thread", + turn: { id: "legacy-child-turn-after-resume" }, + }); asInternals(session).handleNotification("item/agentMessage/delta", { threadId: "legacy-child-thread", itemId: "legacy-child-message-after-resume", @@ -4007,6 +4149,99 @@ describe("Codex app-server provider", () => { ]); }); + test("completes a pending Codex compaction when its turn ends", async () => { + const { appServer, session, events, terminalEvent } = await startCompactionTurnTest(); + + try { + appServer.startsCompaction({ + threadId: "thread-1", + itemId: "compact-without-completion", + }); + appServer.completeTurn(); + await terminalEvent; + + expect( + events.map((event) => + event.type === "timeline" ? `${event.item.type}:${event.item.status}` : event.type, + ), + ).toEqual(["compaction:loading", "compaction:completed", "turn_completed"]); + appServer.assertNoErrors(); + } finally { + await session.close(); + } + }); + + test("does not complete a Codex compaction twice when its item finishes before the turn", async () => { + const { appServer, session, events, terminalEvent } = await startCompactionTurnTest(); + + try { + appServer.startsCompaction({ + threadId: "thread-1", + itemId: "compact-completed-normally", + }); + appServer.completesCompaction({ + threadId: "thread-1", + itemId: "compact-completed-normally", + }); + appServer.completeTurn(); + await terminalEvent; + + expect( + events.map((event) => + event.type === "timeline" ? `${event.item.type}:${event.item.status}` : event.type, + ), + ).toEqual(["compaction:loading", "compaction:completed", "turn_completed"]); + appServer.assertNoErrors(); + } finally { + await session.close(); + } + }); + + test("does not let a late compaction completion consume the current pending item", async () => { + const { appServer, session, events, terminalEvent } = await startCompactionTurnTest(); + + try { + appServer.startsCompaction({ threadId: "thread-1", itemId: "current-compaction" }); + appServer.completesCompaction({ threadId: "thread-1", itemId: "older-compaction" }); + appServer.completeTurn(); + await terminalEvent; + + expect( + events.map((event) => + event.type === "timeline" ? `${event.item.type}:${event.item.status}` : event.type, + ), + ).toEqual(["compaction:loading", "compaction:completed", "turn_completed"]); + appServer.assertNoErrors(); + } finally { + await session.close(); + } + }); + + test.each([ + { status: "failed", terminalType: "turn_failed" }, + { status: "interrupted", terminalType: "turn_canceled" }, + ])("completes a pending compaction before a $status turn", async ({ status, terminalType }) => { + const { appServer, session, events, terminalEvent } = await startCompactionTurnTest(); + + try { + appServer.startsCompaction({ threadId: "thread-1", itemId: `compact-${status}` }); + appServer.completeTurn({ + status, + error: status === "failed" ? { message: "Compaction failed" } : null, + }); + await terminalEvent; + + expect( + events.map((event) => + event.type === "timeline" ? `${event.item.type}:${event.item.status}` : event.type, + ), + ).toEqual(["compaction:loading", "compaction:completed", terminalType]); + appServer.assertNoErrors(); + } finally { + await session.close(); + } + }); + test("emits and dedupes Codex thread/compacted notifications", () => { const session = createSession(); session.activeForegroundTurnId = null; diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index b5c0c0d3b0..7c345ddae3 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -34,8 +34,11 @@ import { type ListImportableSessionsOptions, type ProviderCatalog, type ProviderWorkspace, + type ProviderRefreshContext, + type ResolveAgentDefaultModeInput, } from "../agent-sdk-types.js"; import { importSessionFromPersistence } from "../provider-session-import.js"; +import { runProviderRefreshActivity } from "../provider-refresh-deadline.js"; import type { Logger } from "pino"; import type { ChildProcess, ChildProcessWithoutNullStreams } from "node:child_process"; @@ -968,6 +971,26 @@ export function planStepsToMarkdown(steps: Array<{ step: string; status: string return normalizePlanMarkdown(lines.join("\n")); } +export function mapCodexPlanUpdateToTodo( + steps: Array<{ step?: string | null; status?: string | null }>, +): Extract<AgentTimelineItem, { type: "todo" }> { + return { + type: "todo", + items: steps.flatMap((entry, index) => { + const text = entry.step?.trim(); + if (!text) return []; + const status = normalizeCodexTaskStatus(entry.status); + return [{ id: String(index), text, status, completed: status === "completed" }]; + }), + }; +} + +function normalizeCodexTaskStatus(status: string | null | undefined) { + if (status === "completed") return "completed" as const; + if (status === "inProgress" || status === "in_progress") return "in_progress" as const; + return "pending" as const; +} + export function mapCodexPlanToToolCall(params: { callId: string; text: string; @@ -1669,6 +1692,12 @@ interface CodexSubAgentActivity { kind: "started" | "interacted" | "interrupted"; } +function isTerminalSubAgentStatus( + status: ToolCallTimelineItem["status"], +): status is "completed" | "failed" | "canceled" { + return status === "completed" || status === "failed" || status === "canceled"; +} + function readCodexSubAgentActivity(item: unknown): CodexSubAgentActivity | null { const record = toObjectRecord(item); if (!record) { @@ -3189,6 +3218,10 @@ interface CodexPendingPermissionHandler { planText?: string; } +interface ConsumedRootCompaction { + itemId?: string; +} + export class CodexAppServerAgentSession implements AgentSession { readonly provider = CODEX_PROVIDER; readonly capabilities = CODEX_APP_SERVER_CAPABILITIES; @@ -3254,6 +3287,8 @@ export class CodexAppServerAgentSession implements AgentSession { private readonly userMessageTurnIds: string[] = []; private pendingManualCompactionStarts = 0; private compactionTriggerByItemId = new Map<string, "auto" | "manual">(); + private pendingRootCompactionItemIds = new Set<string>(); + private pendingAnonymousRootCompactions = 0; // Codex can report one completed compaction through both channels: // `thread/compacted` and a completed `contextCompaction` item. private unpairedCompactionNotificationCompletions = 0; @@ -4937,7 +4972,7 @@ export class CodexAppServerAgentSession implements AgentSession { private dispatchSubAgentNotification(parsed: ParsedCodexNotification, callId: string): void { switch (parsed.kind) { case "thread_started": - this.emitSubAgentActivityUpdate(callId, "running"); + this.emitSubAgentActivityUpdate(callId, "running", { reopen: true }); return; case "turn_started": case "turn_completed": @@ -5210,10 +5245,13 @@ export class CodexAppServerAgentSession implements AgentSession { }, }; } - this.emitSubAgentActivityUpdate( - callId, - activity.kind === "interrupted" ? "canceled" : "running", - ); + let nextStatus: ToolCallTimelineItem["status"] | undefined = "running"; + if (activity.kind === "interrupted") { + nextStatus = "canceled"; + } else if (isTerminalSubAgentStatus(state.toolCall.status)) { + nextStatus = undefined; + } + this.emitSubAgentActivityUpdate(callId, nextStatus); return true; } @@ -5225,6 +5263,16 @@ export class CodexAppServerAgentSession implements AgentSession { if (!this.isContextCompactionItem(item)) { return false; } + const consumedPendingCompaction = this.consumePendingRootCompaction(item.id); + const hasDifferentPendingCompaction = + this.pendingRootCompactionItemIds.size > 0 || this.pendingAnonymousRootCompactions > 0; + const isLateCompletionForOlderItem = + item.id !== undefined && + consumedPendingCompaction === undefined && + hasDifferentPendingCompaction; + if (isLateCompletionForOlderItem) { + return true; + } if (this.unpairedCompactionNotificationCompletions > 0) { this.unpairedCompactionNotificationCompletions -= 1; return true; @@ -5297,6 +5345,7 @@ export class CodexAppServerAgentSession implements AgentSession { private emitSubAgentActivityUpdate( callId: string, status?: ToolCallTimelineItem["status"], + options?: { reopen?: boolean }, ): void { const state = this.subAgentCallsByCallId.get(callId); if (!state || state.toolCall.detail.type !== "sub_agent") { @@ -5307,7 +5356,14 @@ export class CodexAppServerAgentSession implements AgentSession { childTimeline.length > 0 ? curateAgentActivity(childTimeline, { labelAssistantMessages: true }) : ""; - const resolvedStatus = status ?? state.toolCall.status; + let resolvedStatus = status ?? state.toolCall.status; + if ( + status === "running" && + !options?.reopen && + isTerminalSubAgentStatus(state.toolCall.status) + ) { + resolvedStatus = state.toolCall.status; + } for (const childThreadId of state.childThreadIds) { this.emitProviderSubagentUpsert(childThreadId, state, resolvedStatus); } @@ -5446,7 +5502,7 @@ export class CodexAppServerAgentSession implements AgentSession { this.pendingCommandOutputDeltas.delete(itemId); this.pendingFileChangeOutputDeltas.delete(itemId); } - this.emitSubAgentActivityUpdate(callId, "running"); + this.emitSubAgentActivityUpdate(callId); } private handleSubAgentContextCompactionItem( @@ -5582,7 +5638,7 @@ export class CodexAppServerAgentSession implements AgentSession { ): void { const subAgentCallId = this.getSubAgentCallIdForThread(parsed.threadId); if (subAgentCallId) { - this.emitSubAgentActivityUpdate(subAgentCallId, "running"); + this.emitSubAgentActivityUpdate(subAgentCallId, "running", { reopen: true }); return; } this.currentTurnId = parsed.turnId; @@ -5612,6 +5668,7 @@ export class CodexAppServerAgentSession implements AgentSession { this.emitSubAgentActivityUpdate(subAgentCallId, status); return; } + this.completePendingRootCompactions(); if (parsed.status === "failed") { this.emitEvent({ type: "turn_failed", @@ -5652,6 +5709,8 @@ export class CodexAppServerAgentSession implements AgentSession { this.pendingFileChangeOutputDeltas.clear(); this.pendingAssistantMessageBoundary = false; this.warnedIncompleteEditToolCallIds.clear(); + this.pendingRootCompactionItemIds.clear(); + this.pendingAnonymousRootCompactions = 0; this.unpairedCompactionNotificationCompletions = 0; this.unpairedCompactionItemCompletions = 0; } @@ -5659,6 +5718,14 @@ export class CodexAppServerAgentSession implements AgentSession { private handlePlanUpdatedNotification( parsed: Extract<ParsedCodexNotification, { kind: "plan_updated" }>, ): void { + if (!this.planModeEnabled) { + this.emitEvent({ + type: "timeline", + provider: CODEX_PROVIDER, + item: mapCodexPlanUpdateToTodo(parsed.plan), + }); + return; + } const timelineItem = mapCodexPlanToToolCall({ callId: `plan:${this.currentTurnId ?? this.currentThreadId ?? "current"}`, text: planStepsToMarkdown( @@ -5670,13 +5737,9 @@ export class CodexAppServerAgentSession implements AgentSession { }); if (timelineItem) { this.rememberPlanResult(timelineItem); - // In plan mode, the same plan is rendered through the synthetic approval - // permission. Keep the remembered text for that card, but do not also - // emit a static timeline plan panel. - if (this.planModeEnabled) { - return; - } - this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem }); + // Older Codex app-server builds reported Plan-mode proposals through + // turn/plan/updated. Retain that compatibility path only while Plan mode is active. + return; } } @@ -5707,6 +5770,61 @@ export class CodexAppServerAgentSession implements AgentSession { return undefined; } + private trackPendingRootCompaction(itemId?: string): void { + if (itemId) { + this.pendingRootCompactionItemIds.add(itemId); + return; + } + this.pendingAnonymousRootCompactions += 1; + } + + private consumePendingRootCompaction(itemId?: string): ConsumedRootCompaction | undefined { + if (itemId) { + if (this.pendingRootCompactionItemIds.delete(itemId)) { + return { itemId }; + } + if ( + this.pendingRootCompactionItemIds.size === 0 && + this.pendingAnonymousRootCompactions > 0 + ) { + this.pendingAnonymousRootCompactions -= 1; + return {}; + } + return undefined; + } + const pendingItemId = this.pendingRootCompactionItemIds.values().next().value; + if (typeof pendingItemId === "string") { + this.pendingRootCompactionItemIds.delete(pendingItemId); + return { itemId: pendingItemId }; + } + if (this.pendingAnonymousRootCompactions > 0) { + this.pendingAnonymousRootCompactions -= 1; + return {}; + } + return undefined; + } + + private completePendingRootCompactions(): void { + // Some Codex builds end a turn without completing the contextCompaction + // item. Close every loading timeline row before emitting the terminal turn. + for (const itemId of this.pendingRootCompactionItemIds) { + this.emitEvent({ + type: "timeline", + provider: CODEX_PROVIDER, + item: this.createContextCompactionTimelineItem("completed", itemId), + }); + } + for (let index = 0; index < this.pendingAnonymousRootCompactions; index += 1) { + this.emitEvent({ + type: "timeline", + provider: CODEX_PROVIDER, + item: this.createContextCompactionTimelineItem("completed"), + }); + } + this.pendingRootCompactionItemIds.clear(); + this.pendingAnonymousRootCompactions = 0; + } + private createContextCompactionTimelineItem( status: "loading" | "completed", itemId?: string, @@ -5756,11 +5874,12 @@ export class CodexAppServerAgentSession implements AgentSession { this.unpairedCompactionItemCompletions -= 1; return; } + const pendingItemId = this.consumePendingRootCompaction()?.itemId; this.unpairedCompactionNotificationCompletions += 1; this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, - item: this.createContextCompactionTimelineItem("completed"), + item: this.createContextCompactionTimelineItem("completed", pendingItemId), ...(parsed.turnId ? { turnId: parsed.turnId } : {}), }); } @@ -6085,6 +6204,7 @@ export class CodexAppServerAgentSession implements AgentSession { return; } if (this.isContextCompactionItem(parsed.item)) { + this.trackPendingRootCompaction(parsed.item.id); this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, @@ -6565,7 +6685,10 @@ export class CodexAppServerAgentClient implements AgentClient { return this.goalsEnabledPromise; } - private resolveAutoReviewEnabled(workspace?: ProviderWorkspace): Promise<boolean> { + private resolveAutoReviewEnabled( + workspace?: ProviderWorkspace, + signal?: AbortSignal, + ): Promise<boolean> { if (workspace) { const cached = this.autoReviewEnabledByRuntime.get(workspace); if (cached) return cached; @@ -6577,30 +6700,32 @@ export class CodexAppServerAgentClient implements AgentClient { this.autoReviewEnabledByRuntime.set(workspace, pending); return pending; } + if (signal) return this.probeAutoReviewEnabled(signal); if (!this.autoReviewEnabledPromise) { - this.autoReviewEnabledPromise = (async () => { - try { - const launchPrefix = await resolveCodexLaunchPrefix(this.runtimeSettings); - const versionOutput = await resolveBinaryVersion(launchPrefix.command); - const enabled = codexVersionAtLeast(versionOutput, CODEX_AUTO_REVIEW_MIN_VERSION); - this.logger.trace( - { - provider: CODEX_PROVIDER, - versionOutput, - enabled, - }, - "provider.codex.config.auto_review_resolved", - ); - return enabled; - } catch (error) { - this.logger.warn({ err: error }, "Failed to probe codex version for auto-review gate"); - return false; - } - })(); + this.autoReviewEnabledPromise = this.probeAutoReviewEnabled(); } return this.autoReviewEnabledPromise; } + private async probeAutoReviewEnabled(signal?: AbortSignal): Promise<boolean> { + try { + const launchPrefix = await resolveCodexLaunchPrefix(this.runtimeSettings); + signal?.throwIfAborted(); + const versionOutput = await resolveBinaryVersion(launchPrefix.command, signal); + signal?.throwIfAborted(); + const enabled = codexVersionAtLeast(versionOutput, CODEX_AUTO_REVIEW_MIN_VERSION); + this.logger.trace( + { provider: CODEX_PROVIDER, versionOutput, enabled }, + "provider.codex.config.auto_review_resolved", + ); + return enabled; + } catch (error) { + if (signal?.aborted) throw signal.reason; + this.logger.warn({ err: error }, "Failed to probe codex version for auto-review gate"); + return false; + } + } + private async spawnAppServer( launchEnv?: Record<string, string>, options?: { goalsEnabled?: boolean; agentId?: string; workspace?: ProviderWorkspace }, @@ -6801,11 +6926,16 @@ export class CodexAppServerAgentClient implements AgentClient { }); } - async fetchCatalog(options: FetchCatalogOptions): Promise<ProviderCatalog> { + async fetchCatalog( + options: FetchCatalogOptions, + context?: ProviderRefreshContext, + ): Promise<ProviderCatalog> { const workspace = providerWorkspaceFromCatalogOptions(options); const [models, autoReviewEnabled] = await Promise.all([ - this.fetchModelsFromAppServer(workspace), - this.resolveAutoReviewEnabled(workspace), + this.fetchModelsFromAppServer(workspace, context), + runProviderRefreshActivity(context, "version", () => + this.resolveAutoReviewEnabled(workspace, context?.signal), + ), ]); return { models, @@ -6816,25 +6946,47 @@ export class CodexAppServerAgentClient implements AgentClient { }; } - async resolveDefaultModeId(): Promise<string> { - return (await this.resolveAutoReviewEnabled()) ? "auto-review" : DEFAULT_CODEX_MODE_ID; + async resolveDefaultModeId(input: ResolveAgentDefaultModeInput): Promise<string> { + return (await this.resolveAutoReviewEnabled(undefined, input.signal)) + ? "auto-review" + : DEFAULT_CODEX_MODE_ID; } private async fetchModelsFromAppServer( workspace?: ProviderWorkspace, + context?: ProviderRefreshContext, ): Promise<AgentModelDefinition[]> { // Codex model/list is global to the app server in this flow; cwd/force are intentionally ignored. - const child = await this.spawnAppServer(undefined, { workspace }); - const client = new CodexAppServerClient(child, this.logger); + let client: CodexAppServerClient | undefined; + let disposePromise: Promise<void> | undefined; + const dispose = () => { + if (!client) return Promise.resolve(); + disposePromise ??= client.dispose(); + return disposePromise; + }; + const handleAbort = () => void dispose().catch(() => undefined); + context?.signal.addEventListener("abort", handleAbort, { once: true }); try { - await client.request("initialize", buildCodexAppServerInitializeParams()); + await runProviderRefreshActivity(context, "app-server.start", async () => { + const child = await this.spawnAppServer(undefined, { workspace }); + client = new CodexAppServerClient(child, this.logger); + if (context?.signal.aborted) await dispose(); + }); + if (!client) throw new Error("Codex app-server did not start"); + await runProviderRefreshActivity(context, "initialize", () => + client!.request("initialize", buildCodexAppServerInitializeParams()), + ); client.notify("initialized", {}); - const rawResponse = await client.request("model/list", {}); + const rawResponse = await runProviderRefreshActivity(context, "model/list", () => + client!.request("model/list", {}), + ); const parsedResponse = CodexModelListResponseSchema.safeParse(rawResponse); const models = parsedResponse.success ? (parsedResponse.data.data ?? []) : []; - const configuredDefaults = await readCodexConfiguredDefaults(client, this.logger); + const configuredDefaults = await runProviderRefreshActivity(context, "config/read", () => + readCodexConfiguredDefaults(client!, this.logger), + ); const configuredDefaultModelId = configuredDefaults.model; const configuredDefaultThinkingOptionId = configuredDefaults.thinkingOptionId; const hasConfiguredDefaultModel = @@ -6849,7 +7001,8 @@ export class CodexAppServerAgentClient implements AgentClient { }), ); } finally { - await client.dispose(); + context?.signal.removeEventListener("abort", handleAbort); + await dispose(); } } diff --git a/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts b/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts index dc04a31f09..fc0031818c 100644 --- a/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts +++ b/packages/server/src/server/agent/providers/codex/test-utils/fake-app-server.ts @@ -51,8 +51,13 @@ export interface FakeCodexAppServer { waitForTurnStart(): Promise<JsonObject>; nextResponse(): Promise<string>; startsTurn(params: { threadId: string; turnId?: string }): void; + startsCompaction(params: { threadId: string; itemId: string }): void; updatesPlan(params: { threadId: string; steps: string[] }): void; - completeTurn(params?: { threadId?: string }): void; + completeTurn(params?: { + threadId?: string; + status?: "completed" | "failed" | "interrupted"; + error?: { message: string } | null; + }): void; startsSubAgent(params: { callId: string; threadId: string; @@ -320,6 +325,12 @@ export function createFakeCodexAppServer( })}\n`, ); }, + startsCompaction(params) { + writeNotification("item/started", { + threadId: params.threadId, + item: { type: "contextCompaction", id: params.itemId }, + }); + }, updatesPlan(params) { child.stdout.write( `${JSON.stringify({ @@ -337,7 +348,10 @@ export function createFakeCodexAppServer( method: "turn/completed", params: { threadId: params.threadId ?? "thread-1", - turn: { status: "completed" }, + turn: { + status: params.status ?? "completed", + error: params.error ?? null, + }, }, })}\n`, ); diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts index f9df7ee62b..35a96b6db9 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts @@ -379,6 +379,28 @@ describe("codex tool-call mapper", () => { }); }); + it.each([ + ["shutdown", "canceled"], + ["notFound", "failed"], + ] as const)("maps terminal child state %s to %s", (childStatus, expectedStatus) => { + const item = mapCodexToolCallFromThreadItem({ + type: "collabAgentToolCall", + id: `call-sub-agent-${childStatus}`, + tool: "closeAgent", + status: "completed", + receiverThreadIds: ["child-thread-1"], + agentsStates: { + "child-thread-1": { status: childStatus, message: null }, + }, + }); + + expect(item).toMatchObject({ + type: "tool_call", + callId: `call-sub-agent-${childStatus}`, + status: expectedStatus, + }); + }); + it("maps mcp read_file completion with detail", () => { const item = expectMapped( mapCodexToolCallFromThreadItem( diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts index b3775e42ce..7884c9bc78 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts @@ -569,10 +569,17 @@ function readStatus(value: unknown): string | undefined { function normalizeCollabAgentChildStatus(status: string): ToolCallTimelineItem["status"] { const normalized = status.trim().toLowerCase(); - if (normalized === "error" || normalized === "errored") { - return "running"; + switch (normalized) { + case "error": + case "errored": + return "running"; + case "shutdown": + return "canceled"; + case "notfound": + return "failed"; + default: + return normalizeToolCallStatus(status, null, null); } - return normalizeToolCallStatus(status, null, null); } function resolveCollabAgentStatus( diff --git a/packages/server/src/server/agent/providers/diagnostic-utils.ts b/packages/server/src/server/agent/providers/diagnostic-utils.ts index 0cf2b44b02..670bd80755 100644 --- a/packages/server/src/server/agent/providers/diagnostic-utils.ts +++ b/packages/server/src/server/agent/providers/diagnostic-utils.ts @@ -135,11 +135,15 @@ export function toDiagnosticErrorMessage(error: unknown): string { return formatNonErrorDiagnostic(error); } -export async function resolveBinaryVersion(binaryPath: string): Promise<string> { +export async function resolveBinaryVersion( + binaryPath: string, + signal?: AbortSignal, +): Promise<string> { try { const { stdout } = await execCommand(binaryPath, ["--version"], { ...createProviderEnvSpec(), timeout: 5_000, + signal, }); return stdout.trim() || "unknown"; } catch (error) { diff --git a/packages/server/src/server/agent/providers/generic-acp-agent.diagnostic.test.ts b/packages/server/src/server/agent/providers/generic-acp-agent.diagnostic.test.ts index 785838bb64..5bac343fdc 100644 --- a/packages/server/src/server/agent/providers/generic-acp-agent.diagnostic.test.ts +++ b/packages/server/src/server/agent/providers/generic-acp-agent.diagnostic.test.ts @@ -5,6 +5,7 @@ import path from "node:path"; import { describe, expect, test } from "vitest"; import { createTestLogger } from "../../../test-utils/test-logger.js"; +import { runProviderRefreshWithDeadline } from "../provider-refresh-deadline.js"; import { buildVersionProbeCommand, GenericACPAgentClient } from "./generic-acp-agent.js"; const TEST_ACP_TIMEOUT_MS = 1_000; @@ -91,8 +92,15 @@ describe("GenericACPAgentClient diagnostics", () => { }); await expect( - client.fetchCatalog({ cwd: tmpdir(), force: true, timeoutMs: TEST_ACP_TIMEOUT_MS }), - ).rejects.toThrow(`ACP catalog probe timed out after ${TEST_ACP_TIMEOUT_MS}ms`); + runProviderRefreshWithDeadline({ + label: "Grok", + timeoutMs: TEST_ACP_TIMEOUT_MS, + operation: (context) => + client.fetchCatalog({ scope: "workspace", cwd: tmpdir(), force: true }, context), + }), + ).rejects.toThrow( + `Timed out refreshing Grok after ${TEST_ACP_TIMEOUT_MS}ms; pending: session/new`, + ); const pid = Number(await readFile(pidPath, "utf8")); await expectProcessExit(pid); @@ -116,7 +124,7 @@ describe("GenericACPAgentClient diagnostics", () => { }, }); - await client.fetchCatalog({ cwd: testDir, force: true, timeoutMs: TEST_ACP_TIMEOUT_MS }); + await client.fetchCatalog({ scope: "workspace", cwd: testDir, force: true }); const session = await client.createSession({ provider: "acp", cwd: testDir }); await session.close(); diff --git a/packages/server/src/server/agent/providers/jsonl-rpc-process.test.ts b/packages/server/src/server/agent/providers/jsonl-rpc-process.test.ts index 6d5928ac6c..025f1e7b80 100644 --- a/packages/server/src/server/agent/providers/jsonl-rpc-process.test.ts +++ b/packages/server/src/server/agent/providers/jsonl-rpc-process.test.ts @@ -219,4 +219,123 @@ describe("JsonlRpcProcess", () => { await rejection; }); + + test("reassembles protocol v2 chunked responses into the logical frame", async () => { + const child = createInMemoryChildProcess(); + const transport = startProcess({ child }); + + try { + let buffer = ""; + const sentRequest = new Promise<Record<string, unknown>>((resolve) => { + child.stdin.on("data", (chunk) => { + buffer += chunk.toString(); + const newlineIndex = buffer.indexOf("\n"); + if (newlineIndex === -1) return; + resolve(JSON.parse(buffer.slice(0, newlineIndex)) as Record<string, unknown>); + }); + }); + + const request = transport.request({ type: "chunked" }); + const command = await sentRequest; + + // Emit a logical response split into protocol v2 chunk frames. + const value = "x".repeat(1024 * 1024); + const logical = JSON.stringify({ + id: command.id, + type: "response", + command: command.type, + success: true, + data: { value }, + }); + const bytes = Buffer.from(logical, "utf8"); + expect(bytes.byteLength).toBeGreaterThan(1024 * 1024); + const chunkPayload = 256 * 1024; + const count = Math.ceil(bytes.length / chunkPayload); + expect(count).toBeGreaterThan(1); + for (let index = 0; index < count; index++) { + child.stdout.write( + `${JSON.stringify({ + type: "rpc_chunk", + chunkId: "seq1", + index, + count, + byteLength: bytes.length, + data: bytes + .subarray(index * chunkPayload, (index + 1) * chunkPayload) + .toString("base64"), + })}\n`, + ); + } + + await expect(request).resolves.toEqual({ value }); + } finally { + await transport.close(); + } + }); + + test("rejects protocol v2 chunks larger than the transport payload limit", async () => { + const child = createInMemoryChildProcess(); + const transport = startProcess({ child }); + + try { + let buffer = ""; + const sentRequest = new Promise<Record<string, unknown>>((resolve) => { + child.stdin.on("data", (chunk) => { + buffer += chunk.toString(); + const newlineIndex = buffer.indexOf("\n"); + if (newlineIndex === -1) return; + resolve(JSON.parse(buffer.slice(0, newlineIndex)) as Record<string, unknown>); + }); + }); + + const request = transport.request({ type: "oversized-chunks" }); + const command = await sentRequest; + const logicalResponse = (value: string): Buffer => + Buffer.from( + JSON.stringify({ + id: command.id, + type: "response", + command: command.type, + success: true, + data: { value }, + }), + "utf8", + ); + const oversized = logicalResponse(`invalid-${"x".repeat(1024 * 1024)}`); + const split = Math.ceil(oversized.byteLength / 2); + expect(split).toBeGreaterThan(256 * 1024); + for (let index = 0; index < 2; index += 1) { + child.stdout.write( + `${JSON.stringify({ + type: "rpc_chunk", + chunkId: "oversized", + index, + count: 2, + byteLength: oversized.byteLength, + data: oversized.subarray(index * split, (index + 1) * split).toString("base64"), + })}\n`, + ); + } + + const valid = logicalResponse(`valid-${"x".repeat(1024 * 1024)}`); + const count = Math.ceil(valid.byteLength / (256 * 1024)); + for (let index = 0; index < count; index += 1) { + child.stdout.write( + `${JSON.stringify({ + type: "rpc_chunk", + chunkId: "valid", + index, + count, + byteLength: valid.byteLength, + data: valid.subarray(index * 256 * 1024, (index + 1) * 256 * 1024).toString("base64"), + })}\n`, + ); + } + + const response = (await request) as { value: string }; + expect(response.value.startsWith("valid-")).toBe(true); + } finally { + await transport.close(); + } + }); }); diff --git a/packages/server/src/server/agent/providers/jsonl-rpc-process.ts b/packages/server/src/server/agent/providers/jsonl-rpc-process.ts index 2e56a0659f..ddcbaae8a9 100644 --- a/packages/server/src/server/agent/providers/jsonl-rpc-process.ts +++ b/packages/server/src/server/agent/providers/jsonl-rpc-process.ts @@ -15,6 +15,11 @@ export const JSONL_RPC_NO_TIMEOUT = null; const STDERR_BUFFER_LIMIT = 8192; const GRACEFUL_SHUTDOWN_TIMEOUT_MS = 2_000; const FORCE_SHUTDOWN_TIMEOUT_MS = 1_000; +const RPC_V2_MAX_FRAME_BYTES = 1024 * 1024; +const RPC_V2_MAX_REASSEMBLED_BYTES = 64 * 1024 * 1024; +const RPC_V2_CHUNK_PAYLOAD_BYTES = 256 * 1024; +const RPC_V2_MAX_ENCODED_CHUNK_LENGTH = Math.ceil(RPC_V2_CHUNK_PAYLOAD_BYTES / 3) * 4; +const RPC_V2_MAX_CHUNK_COUNT = Math.ceil(RPC_V2_MAX_REASSEMBLED_BYTES / RPC_V2_CHUNK_PAYLOAD_BYTES); export interface JsonlRpcLaunch { command: string; @@ -38,6 +43,30 @@ interface PendingRequest { timer: NodeJS.Timeout | null; } +/** + * Reassembly state for a protocol v2 chunked frame sequence. Some agents that + * speak this JSONL RPC protocol (OMP) cap single-line frames at 1 MiB and emit + * larger frames as ordered `rpc_chunk` frames; the transport reassembles them + * into the original logical frame before routing it. + */ +interface PendingChunkSequence { + chunkId: string; + count: number; + byteLength: number; + nextIndex: number; + chunks: Buffer[]; + receivedBytes: number; +} + +/** Validated metadata fields of a protocol v2 `rpc_chunk` frame. */ +interface RpcChunkMetadata { + chunkId: string; + index: number; + count: number; + byteLength: number; + data: string; +} + export interface JsonlRpcExit { code: number | null; signal: NodeJS.Signals | null; @@ -51,6 +80,16 @@ export interface JsonlRpcProcessOptions { spawn?: (launch: JsonlRpcLaunch) => ChildProcessWithoutNullStreams; } +export function supportsJsonlRpcProtocolV2(message: Record<string, unknown>): boolean { + return ( + message.type === "ready" && + Array.isArray(message.supportedProtocolVersions) && + message.supportedProtocolVersions.includes(2) && + message.maxFrameBytes === RPC_V2_MAX_FRAME_BYTES && + message.maxReassembledFrameBytes === RPC_V2_MAX_REASSEMBLED_BYTES + ); +} + function assertChildWithPipes( child: ChildProcess, ): asserts child is ChildProcessWithoutNullStreams { @@ -79,6 +118,7 @@ export class JsonlRpcProcess { private nextRequestId = 1; private disposed = false; private stdoutBuffer = ""; + private pendingChunks: PendingChunkSequence | null = null; constructor(private readonly options: JsonlRpcProcessOptions) { this.diagnosticName = options.diagnosticName ?? "JSONL RPC"; @@ -218,6 +258,20 @@ export class JsonlRpcProcess { return; } const message = parsed as Record<string, unknown>; + if (message.type === "rpc_chunk") { + const reassembled = this.consumeRpcChunk(message); + if (reassembled !== undefined) { + // The reassembled frame is a complete logical frame; route it through + // the same path as any other parsed line. + this.handleLine(JSON.stringify(reassembled)); + } + return; + } + this.dispatchFrame(message); + } + + /** Route a complete logical frame to its consumer (response or subscriber). */ + private dispatchFrame(message: Record<string, unknown>): void { if (message.type === "response") { this.handleResponse(message as unknown as JsonlRpcResponse); return; @@ -227,6 +281,173 @@ export class JsonlRpcProcess { } } + /** + * Reassemble a protocol v2 `rpc_chunk` frame. Returns the reconstructed + * logical frame once the final chunk of a sequence arrives, `undefined` while + * a sequence is still in progress, and drops (returning `undefined`) any + * malformed or out-of-order chunk after logging it. + */ + private consumeRpcChunk(chunk: Record<string, unknown>): Record<string, unknown> | undefined { + const metadata = this.parseChunkMetadata(chunk); + if (metadata === undefined) { + return undefined; + } + const bytes = this.decodeChunkPayload(metadata.data); + if (bytes === undefined) { + return undefined; + } + return this.appendChunk(metadata, bytes); + } + + /** Validate a `rpc_chunk` frame's metadata, or reset the sequence and return undefined. */ + private parseChunkMetadata(chunk: Record<string, unknown>): RpcChunkMetadata | undefined { + const chunkId = chunk.chunkId; + const data = chunk.data; + const index = chunk.index; + const count = chunk.count; + const byteLength = chunk.byteLength; + if ( + typeof chunkId !== "string" || + chunkId.length === 0 || + chunkId.length > 128 || + typeof data !== "string" || + data.length === 0 || + typeof index !== "number" || + !Number.isSafeInteger(index) || + index < 0 || + typeof count !== "number" || + !Number.isSafeInteger(count) || + count < 2 || + count > RPC_V2_MAX_CHUNK_COUNT || + index >= count || + typeof byteLength !== "number" || + !Number.isSafeInteger(byteLength) || + byteLength < RPC_V2_MAX_FRAME_BYTES || + byteLength > RPC_V2_MAX_REASSEMBLED_BYTES + ) { + this.options.logger.warn( + { chunk }, + `Ignoring malformed ${this.diagnosticName} rpc_chunk frame`, + ); + this.pendingChunks = null; + return undefined; + } + return { chunkId, index, count, byteLength, data }; + } + + /** Decode a chunk payload, or reset the sequence and return undefined on bad base64. */ + private decodeChunkPayload(data: string): Buffer | undefined { + try { + if (data.length > RPC_V2_MAX_ENCODED_CHUNK_LENGTH) { + throw new Error("encoded payload exceeds the transport limit"); + } + const bytes = Buffer.from(data, "base64"); + if (bytes.byteLength > RPC_V2_CHUNK_PAYLOAD_BYTES || bytes.toString("base64") !== data) { + throw new Error("invalid base64 payload"); + } + return bytes; + } catch { + this.options.logger.warn({}, `Ignoring ${this.diagnosticName} rpc_chunk with invalid base64`); + this.pendingChunks = null; + return undefined; + } + } + + /** Append a chunk to the in-progress sequence, finalizing it once complete. */ + private appendChunk( + metadata: RpcChunkMetadata, + bytes: Buffer, + ): Record<string, unknown> | undefined { + let pending = this.pendingChunks; + if (pending === null) { + if (metadata.index !== 0) { + this.options.logger.warn( + {}, + `Ignoring ${this.diagnosticName} rpc_chunk sequence that did not start at index 0`, + ); + return undefined; + } + pending = { + chunkId: metadata.chunkId, + count: metadata.count, + byteLength: metadata.byteLength, + nextIndex: 0, + chunks: [], + receivedBytes: 0, + }; + this.pendingChunks = pending; + } + + if ( + pending.chunkId !== metadata.chunkId || + pending.count !== metadata.count || + pending.byteLength !== metadata.byteLength || + pending.nextIndex !== metadata.index + ) { + this.options.logger.warn( + {}, + `Ignoring out-of-order ${this.diagnosticName} rpc_chunk; dropping sequence`, + ); + this.pendingChunks = null; + return undefined; + } + + pending.chunks.push(bytes); + pending.receivedBytes += bytes.byteLength; + pending.nextIndex += 1; + + if (pending.receivedBytes > pending.byteLength) { + this.options.logger.warn( + {}, + `Ignoring ${this.diagnosticName} rpc_chunk sequence exceeding its declared length`, + ); + this.pendingChunks = null; + return undefined; + } + if (pending.nextIndex < pending.count) { + return undefined; + } + if (pending.receivedBytes !== pending.byteLength) { + this.options.logger.warn( + {}, + `Ignoring ${this.diagnosticName} rpc_chunk sequence with a length mismatch`, + ); + this.pendingChunks = null; + return undefined; + } + + this.pendingChunks = null; + return this.finalizeChunks(pending.chunks); + } + + /** Reassemble and parse a complete chunk sequence into its logical frame. */ + private finalizeChunks(chunks: Buffer[]): Record<string, unknown> | undefined { + let json: string; + try { + json = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)); + } catch { + this.options.logger.warn( + {}, + `Ignoring ${this.diagnosticName} rpc_chunk sequence with invalid UTF-8`, + ); + return undefined; + } + let frame: unknown; + try { + frame = JSON.parse(json); + } catch { + this.options.logger.warn( + {}, + `Ignoring ${this.diagnosticName} rpc_chunk sequence with invalid JSON`, + ); + return undefined; + } + if (!frame || typeof frame !== "object" || Array.isArray(frame)) { + return undefined; + } + return frame as Record<string, unknown>; + } + private handleResponse(response: JsonlRpcResponse): void { if (!response.id) { return; diff --git a/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts b/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts index 99ce0596c3..3e2374282b 100644 --- a/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts +++ b/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts @@ -68,6 +68,47 @@ describe("MockLoadTestAgentClient", () => { await session.interrupt(); }); + test("streams a configured assistant response through the normal timeline", async () => { + vi.useFakeTimers(); + const response = [ + "```mermaid", + "flowchart LR", + " Start --> Middle", + ' Middle --> End["<i>Done</i>"]', + "```", + ].join("\n"); + const client = new MockLoadTestAgentClient(); + const session = await client.createSession({ + provider: "mock", + cwd: process.cwd(), + model: "ten-second-stream", + featureValues: { + mockStreamingAssistantResponse: response, + mockStreamingAssistantIntervalMs: 20, + }, + }); + const events: AgentStreamEvent[] = []; + session.subscribe((event) => events.push(event)); + + const resultPromise = session.run("Render a diagram while streaming."); + await vi.runAllTimersAsync(); + + await expect(resultPromise).resolves.toMatchObject({ finalText: response, canceled: false }); + const streamedText = events + .flatMap((event) => + event.type === "timeline" && event.item.type === "assistant_message" + ? [event.item.text] + : [], + ) + .join(""); + expect(streamedText).toBe(response); + expect( + events.filter((event) => event.type === "timeline" && event.item.type === "assistant_message") + .length, + ).toBeGreaterThan(5); + expect(events.at(-1)).toMatchObject({ type: "turn_completed", provider: "mock" }); + }); + test("can withhold the provider user-message echo until an immediate interrupt", async () => { vi.useFakeTimers(); const client = new MockLoadTestAgentClient(); diff --git a/packages/server/src/server/agent/providers/mock-load-test-agent.ts b/packages/server/src/server/agent/providers/mock-load-test-agent.ts index 147045ea0d..78a13ea188 100644 --- a/packages/server/src/server/agent/providers/mock-load-test-agent.ts +++ b/packages/server/src/server/agent/providers/mock-load-test-agent.ts @@ -644,6 +644,8 @@ export class MockLoadTestAgentSession implements AgentSession { private modeId: string | null; private modelId: string | null; private readonly assistantResponse: string | null; + private readonly streamingAssistantResponse: string | null; + private readonly streamingAssistantIntervalMs: number; private readonly rewindError: string | null; private remainingPromptRejections: number; @@ -656,6 +658,18 @@ export class MockLoadTestAgentSession implements AgentSession { typeof options.config.featureValues?.mockAssistantResponse === "string" ? options.config.featureValues.mockAssistantResponse : null; + this.streamingAssistantResponse = + typeof options.config.featureValues?.mockStreamingAssistantResponse === "string" + ? options.config.featureValues.mockStreamingAssistantResponse + : null; + const requestedStreamingInterval = + options.config.featureValues?.mockStreamingAssistantIntervalMs; + this.streamingAssistantIntervalMs = + typeof requestedStreamingInterval === "number" && + Number.isFinite(requestedStreamingInterval) && + requestedStreamingInterval >= 1 + ? Math.min(requestedStreamingInterval, 1_000) + : MOCK_LOAD_TEST_INTERVAL_MS; this.rewindError = typeof options.config.featureValues?.mockRewindError === "string" ? options.config.featureValues.mockRewindError @@ -721,6 +735,8 @@ export class MockLoadTestAgentSession implements AgentSession { const scheduleTurn = () => { if (shouldEmitTurnFailure(prompt)) { this.scheduleFailedTurn(turn); + } else if (this.streamingAssistantResponse !== null) { + this.scheduleStreamingAssistantTurn(turn, this.streamingAssistantResponse); } else if (this.assistantResponse !== null) { this.scheduleSettledAssistantTurn(turn, this.assistantResponse); } else if (structuredBranchName) { @@ -1040,6 +1056,32 @@ export class MockLoadTestAgentSession implements AgentSession { turn.timer.unref?.(); } + private scheduleStreamingAssistantTurn(turn: ActiveTurn, finalText: string): void { + const tokens = tokenize(finalText); + const emitNext = () => { + if (this.activeTurn !== turn) { + return; + } + this.clearTurnTimer(turn); + this.emitTurnStarted(turn); + const token = tokens.shift(); + if (token === undefined) { + this.finishTurnWithText(turn, finalText); + return; + } + turn.emittedTokens += 1; + this.emitTimeline(turn.turnId, { + type: "assistant_message", + text: token, + messageId: turn.assistantMessageId, + }); + turn.timer = setTimeout(emitNext, this.streamingAssistantIntervalMs); + turn.timer.unref?.(); + }; + turn.timer = setTimeout(emitNext, 0); + turn.timer.unref?.(); + } + private emitSettledAssistantTurn(turn: ActiveTurn, finalText: string): void { if (this.activeTurn !== turn) { return; diff --git a/packages/server/src/server/agent/providers/mock-slow-provider.ts b/packages/server/src/server/agent/providers/mock-slow-provider.ts index ef2ad42df7..19817d29c6 100644 --- a/packages/server/src/server/agent/providers/mock-slow-provider.ts +++ b/packages/server/src/server/agent/providers/mock-slow-provider.ts @@ -7,6 +7,7 @@ import type { AgentSession, AgentSessionConfig, FetchCatalogOptions, + ProviderRefreshContext, ProviderCatalog, } from "../agent-sdk-types.js"; @@ -24,10 +25,6 @@ const CAPABILITIES: AgentCapabilityFlags = { supportsRewindBoth: false, }; -function neverResolves<T>(): Promise<T> { - return new Promise<T>(() => {}); -} - export class MockSlowProviderClient implements AgentClient { readonly provider: AgentProvider = MOCK_SLOW_PROVIDER_ID; readonly capabilities = CAPABILITIES; @@ -36,8 +33,19 @@ export class MockSlowProviderClient implements AgentClient { return process.env.PASEO_ENABLE_MOCK_SLOW === "true"; } - async fetchCatalog(_options: FetchCatalogOptions): Promise<ProviderCatalog> { - return neverResolves<ProviderCatalog>(); + async fetchCatalog( + _options: FetchCatalogOptions, + context?: ProviderRefreshContext, + ): Promise<ProviderCatalog> { + return await (context?.runActivity( + "mock.catalog", + () => + new Promise<ProviderCatalog>((_resolve, reject) => { + context.signal.addEventListener("abort", () => reject(context.signal.reason), { + once: true, + }); + }), + ) ?? new Promise<ProviderCatalog>(() => {})); } async getDiagnostic(): Promise<{ diagnostic: string }> { diff --git a/packages/server/src/server/agent/providers/omp/agent.ts b/packages/server/src/server/agent/providers/omp/agent.ts index 0d2e66d87e..defee7495e 100644 --- a/packages/server/src/server/agent/providers/omp/agent.ts +++ b/packages/server/src/server/agent/providers/omp/agent.ts @@ -34,10 +34,12 @@ import { type ListImportableSessionsOptions, type ProviderCatalog, type ProviderWorkspace, + type ProviderRefreshContext, type ToolCallDetail, } from "../../agent-sdk-types.js"; import type { PaseoToolCatalog } from "../../tools/types.js"; import { importSessionFromPersistence } from "../../provider-session-import.js"; +import { runProviderRefreshActivity } from "../../provider-refresh-deadline.js"; import { runProviderTurn } from "../provider-runner.js"; import { checkProviderLaunchAvailable, @@ -110,7 +112,6 @@ import { import { DEFAULT_OMP_THINKING_LEVEL, mapOmpModel } from "./map-omp-model.js"; const OMP_PROVIDER = "omp"; -const OMP_CATALOG_REQUEST_TIMEOUT_MS = 120_000; const QUESTION_RESPONSE_HEADER = "Response"; const QUESTION_COMMENT_HEADER = "Comment"; const OMP_ASK_USER_FREEFORM_SENTINEL = "✏️ Type custom response..."; @@ -2325,26 +2326,47 @@ export class OmpAgentClient implements AgentClient { } } - async fetchCatalog(options: FetchCatalogOptions): Promise<ProviderCatalog> { + async fetchCatalog( + options: FetchCatalogOptions, + context?: ProviderRefreshContext, + ): Promise<ProviderCatalog> { const launchMode = this.resolveLaunchMode(undefined); const workspace = providerWorkspaceFromCatalogOptions(options); const cwd = workspace?.cwd ?? (options.scope === "global" ? homedir() : options.cwd); - const runtimeSession = await this.runtime.startSession({ - cwd, - workspace, - protocolMode: "rpc-ui", - modeId: launchMode.modeId, - extraArgs: launchMode.extraArgs, - }); + let runtimeSession: OmpRuntimeSession | undefined; + let closePromise: Promise<void> | undefined; + const closeSession = () => { + if (!runtimeSession) return Promise.resolve(); + closePromise ??= runtimeSession.close(); + return closePromise; + }; + const handleAbort = () => void closeSession().catch(() => undefined); + context?.signal.addEventListener("abort", handleAbort, { once: true }); try { + await runProviderRefreshActivity(context, "runtime.start", async () => { + runtimeSession = await this.runtime.startSession({ + cwd, + workspace, + protocolMode: "rpc-ui", + modeId: launchMode.modeId, + extraArgs: launchMode.extraArgs, + signal: context?.signal, + }); + if (context?.signal.aborted) await closeSession(); + }); + if (!runtimeSession) throw new Error("OMP catalog runtime did not start"); + const catalogSession = runtimeSession; const models = transformOmpModels( - (await runtimeSession.getAvailableModels(OMP_CATALOG_REQUEST_TIMEOUT_MS)).map((model) => - mapOmpModel(model, this.provider), - ), + ( + await runProviderRefreshActivity(context, "get_available_models", () => + catalogSession.getAvailableModels(null), + ) + ).map((model) => mapOmpModel(model, this.provider)), ); return { models, modes: [...OMP_MODES] }; } finally { - await runtimeSession.close(); + context?.signal.removeEventListener("abort", handleAbort); + await closeSession(); } } diff --git a/packages/server/src/server/agent/providers/omp/cli-runtime.test.ts b/packages/server/src/server/agent/providers/omp/cli-runtime.test.ts index da195f057f..66cd3cb0c1 100644 --- a/packages/server/src/server/agent/providers/omp/cli-runtime.test.ts +++ b/packages/server/src/server/agent/providers/omp/cli-runtime.test.ts @@ -14,7 +14,11 @@ type OmpChild = ChildProcessWithoutNullStreams & { killedSignals: Array<NodeJS.Signals | number | undefined>; }; -function createOmpChild(): OmpChild { +function createOmpChild(options?: { + supportedProtocolVersions?: number[]; + emitReady?: boolean; + maxFrameBytes?: number; +}): OmpChild { const child = Object.assign(new EventEmitter(), { stdin: new PassThrough(), stdout: new PassThrough(), @@ -28,6 +32,20 @@ function createOmpChild(): OmpChild { queueMicrotask(() => child.emit("exit", null, signal ?? null)); return true; }) as ChildProcessWithoutNullStreams["kill"]; + // Real OMP writes a `ready` frame immediately after launch; the runtime waits + // for it before negotiating the RPC protocol. Advertise v1-only by default so + // the session stays on protocol v1 and command streams are unaffected. + if (options?.emitReady !== false) { + child.stdout.write( + `${JSON.stringify({ + type: "ready", + protocolVersion: 1, + supportedProtocolVersions: options?.supportedProtocolVersions ?? [1], + maxFrameBytes: options?.maxFrameBytes ?? 1024 * 1024, + maxReassembledFrameBytes: 64 * 1024 * 1024, + })}\n`, + ); + } return child; } @@ -243,4 +261,136 @@ describe("OMP CLI runtime", () => { await expect(session.prompt("hello")).resolves.toEqual({ requestId: "req_1" }); }); + + test("negotiates RPC protocol v2 when OMP advertises it", async () => { + const child = createOmpChild({ supportedProtocolVersions: [1, 2] }); + const commands: Record<string, unknown>[] = []; + replyToCommands(child, (command) => { + commands.push(command); + if (command.type === "negotiate_protocol") { + return { protocolVersion: command.protocolVersion }; + } + if (command.type === "get_available_models") { + return { + models: [{ provider: "opencode-go", id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" }], + }; + } + return undefined; + }); + const session = await createRuntime(child).startSession({ cwd: "/workspace/project" }); + + await expect(session.getAvailableModels()).resolves.toEqual([ + expect.objectContaining({ provider: "opencode-go", id: "deepseek-v4-flash" }), + ]); + expect(commands.map(withoutRequestId)).toContainEqual({ + type: "negotiate_protocol", + protocolVersion: 2, + }); + }); + + test("stays on protocol v1 when OMP advertises incompatible framing limits", async () => { + const child = createOmpChild({ + supportedProtocolVersions: [1, 2], + maxFrameBytes: 512 * 1024, + }); + const commands: Record<string, unknown>[] = []; + replyToCommands(child, (command) => { + commands.push(command); + return { models: [] }; + }); + + const session = await createRuntime(child).startSession({ cwd: "/workspace/project" }); + await session.getAvailableModels(); + + expect(commands.map(withoutRequestId)).toEqual([{ type: "get_available_models" }]); + await session.close(); + }); + + test("rejects startup when OMP exits before advertising readiness", async () => { + const child = createOmpChild({ emitReady: false }); + const startup = createRuntime(child).startSession({ cwd: "/workspace/project" }); + + child.stderr.write("startup exploded"); + child.emit("exit", 7, null); + + await expect(startup).rejects.toThrow("startup exploded"); + }); + + test("rejects startup when OMP exits during protocol negotiation", async () => { + const child = createOmpChild({ supportedProtocolVersions: [1, 2] }); + child.stdin.on("data", () => { + child.stderr.write("negotiation exploded"); + child.emit("exit", 8, null); + }); + + await expect(createRuntime(child).startSession({ cwd: "/workspace/project" })).rejects.toThrow( + "negotiation exploded", + ); + }); + + test("reassembles chunked protocol v2 responses for large payloads", async () => { + const child = createOmpChild({ supportedProtocolVersions: [1, 2] }); + const models = Array.from({ length: 2_000 }, (_, index) => ({ + provider: "p", + id: `model-${index}`, + name: `model-${index}-${"x".repeat(512)}`, + })); + let buffer = ""; + child.stdin.on("data", (chunk) => { + buffer += chunk.toString(); + for (;;) { + const newlineIndex = buffer.indexOf("\n"); + if (newlineIndex === -1) break; + const line = buffer.slice(0, newlineIndex); + buffer = buffer.slice(newlineIndex + 1); + const command = JSON.parse(line) as Record<string, unknown>; + if (command.type === "negotiate_protocol") { + child.stdout.write( + `${JSON.stringify({ + id: command.id, + type: "response", + command: command.type, + success: true, + data: { protocolVersion: 2 }, + })}\n`, + ); + continue; + } + if (command.type === "get_available_models") { + // Emit a logical response larger than the 1 MiB protocol-v1 cap as a + // chunked (protocol v2) frame sequence, like real OMP does. + const logical = JSON.stringify({ + id: command.id, + type: "response", + command: command.type, + success: true, + data: { models }, + }); + const bytes = Buffer.from(logical, "utf8"); + expect(bytes.byteLength).toBeGreaterThan(1024 * 1024); + const chunkPayload = 256 * 1024; + const count = Math.ceil(bytes.length / chunkPayload); + for (let index = 0; index < count; index++) { + child.stdout.write( + `${JSON.stringify({ + type: "rpc_chunk", + chunkId: "c1", + index, + count, + byteLength: bytes.length, + data: bytes + .subarray(index * chunkPayload, (index + 1) * chunkPayload) + .toString("base64"), + })}\n`, + ); + } + } + } + }); + const session = await createRuntime(child).startSession({ cwd: "/workspace/project" }); + + const result = await session.getAvailableModels(); + expect(result).toHaveLength(2_000); + expect(result[0]).toEqual(expect.objectContaining({ id: "model-0" })); + }); }); diff --git a/packages/server/src/server/agent/providers/omp/cli-runtime.ts b/packages/server/src/server/agent/providers/omp/cli-runtime.ts index b2070ef629..156bc374fe 100644 --- a/packages/server/src/server/agent/providers/omp/cli-runtime.ts +++ b/packages/server/src/server/agent/providers/omp/cli-runtime.ts @@ -2,7 +2,12 @@ import type { ChildProcessWithoutNullStreams } from "node:child_process"; import type { Logger } from "pino"; import type { ProviderRuntimeSettings } from "../../provider-launch-config.js"; -import { JsonlRpcProcess, type JsonlRpcLaunch } from "../jsonl-rpc-process.js"; +import { + JSONL_RPC_DEFAULT_TIMEOUT_MS, + JsonlRpcProcess, + supportsJsonlRpcProtocolV2, + type JsonlRpcLaunch, +} from "../jsonl-rpc-process.js"; import { buildOmpLaunch, type OmpRuntime, @@ -45,6 +50,8 @@ import { const DEFAULT_OMP_COMMAND: [string, ...string[]] = [process.env.OMP_COMMAND ?? "omp"]; const DEFAULT_COMMANDS_RPC_NAME = "get_available_commands"; +/** How long to wait for OMP's startup `ready` frame before failing startup. */ +const OMP_READY_TIMEOUT_MS = 10_000; export interface OmpCliRuntimeOptions { logger: Logger; @@ -99,10 +106,76 @@ export class OmpCliRuntime implements OmpRuntime { ...(runtimeSpawn ? { spawn: runtimeSpawn } : {}), }; const process = new JsonlRpcProcess(processOptions); - return new OmpCliRuntimeSession(process, this.commandsRpcName); + const handleAbort = () => void process.close(input.signal?.reason).catch(() => undefined); + input.signal?.addEventListener("abort", handleAbort, { once: true }); + try { + await negotiateOmpProtocolV2(process, this.options.logger); + input.signal?.throwIfAborted(); + return new OmpCliRuntimeSession(process, this.commandsRpcName); + } catch (error) { + const startupError = error instanceof Error ? error : new Error(String(error)); + await process.close(startupError); + throw startupError; + } finally { + input.signal?.removeEventListener("abort", handleAbort); + } } } +/** + * Wait for OMP to advertise readiness and negotiate RPC protocol v2 when it is + * supported. OMP caps protocol-v1 single-line frames at 1 MiB; `get_available_models` + * (and other large payloads) can exceed that and are returned as an overflow error. + * Protocol v2 lifts the ceiling to 64 MiB by chunking oversized frames, which the + * JSONL transport reassembles. Supported OMP versions send a `ready` frame immediately + * after launch; startup fails if the process exits or never becomes ready. + */ +async function negotiateOmpProtocolV2(process: JsonlRpcProcess, logger: Logger): Promise<void> { + const ready = await waitForOmpReadyFrame(process); + if (!supportsJsonlRpcProtocolV2(ready)) { + return; + } + const response = (await process.request( + { type: "negotiate_protocol", protocolVersion: 2 }, + JSONL_RPC_DEFAULT_TIMEOUT_MS, + )) as { protocolVersion?: unknown } | undefined; + if (response?.protocolVersion !== 2) { + throw new Error("OMP did not accept RPC protocol v2"); + } + logger.debug({}, "Negotiated OMP RPC protocol v2 (chunked frame transport)"); +} + +function waitForOmpReadyFrame(process: JsonlRpcProcess): Promise<Record<string, unknown>> { + return new Promise((resolve, reject) => { + let settled = false; + let unsubscribeMessage = (): void => {}; + let unsubscribeExit = (): void => {}; + let timer: NodeJS.Timeout | undefined; + const finish = (result: Record<string, unknown> | Error): void => { + if (settled) return; + settled = true; + if (timer) clearTimeout(timer); + unsubscribeMessage(); + unsubscribeExit(); + if (result instanceof Error) { + reject(result); + } else { + resolve(result); + } + }; + unsubscribeMessage = process.onMessage((message) => { + if (message.type === "ready") { + finish(message); + } + }); + unsubscribeExit = process.onExit(({ error }) => finish(error)); + timer = setTimeout( + () => finish(new Error("Timed out waiting for OMP to become ready")), + OMP_READY_TIMEOUT_MS, + ); + }); +} + class OmpCliRuntimeSession implements OmpRuntimeSession { private readonly subscribers = new Set<(event: OmpRuntimeEvent) => void>(); activeBranchEntryId?: string; @@ -166,7 +239,7 @@ class OmpCliRuntimeSession implements OmpRuntimeSession { return data.messages ?? []; } - async getAvailableModels(timeoutMs?: number): Promise<OmpModel[]> { + async getAvailableModels(timeoutMs?: number | null): Promise<OmpModel[]> { const data = OmpModelsResultSchema.parse( await this.request({ type: "get_available_models" }, timeoutMs), ); @@ -287,7 +360,7 @@ class OmpCliRuntimeSession implements OmpRuntimeSession { await this.process.close(new Error("OMP RPC session is closed")); } - private request(command: OmpRpcCommand, timeoutMs?: number): Promise<unknown> { + private request(command: OmpRpcCommand, timeoutMs?: number | null): Promise<unknown> { return this.process.request(OmpRpcCommandSchema.parse(command), timeoutMs); } diff --git a/packages/server/src/server/agent/providers/omp/runtime.ts b/packages/server/src/server/agent/providers/omp/runtime.ts index eb5a948e8a..219939bc26 100644 --- a/packages/server/src/server/agent/providers/omp/runtime.ts +++ b/packages/server/src/server/agent/providers/omp/runtime.ts @@ -33,6 +33,7 @@ export interface OmpStartSessionInput { workspace?: ProviderWorkspace; agentId?: string; cwd: string; + signal?: AbortSignal; env?: Record<string, string>; protocolMode?: "rpc" | "rpc-ui"; model?: string; @@ -55,7 +56,7 @@ export interface OmpRuntimeSession { abort(): Promise<void>; getState(): Promise<OmpSessionState>; getMessages(): Promise<OmpAgentMessage[]>; - getAvailableModels(timeoutMs?: number): Promise<OmpModel[]>; + getAvailableModels(timeoutMs?: number | null): Promise<OmpModel[]>; setModel(provider: string, modelId: string): Promise<OmpModel>; setThinkingLevel(level: OmpThinkingLevel): Promise<void>; getSessionStats(): Promise<OmpSessionStats>; diff --git a/packages/server/src/server/agent/providers/omp/test-utils/fake-omp.ts b/packages/server/src/server/agent/providers/omp/test-utils/fake-omp.ts index 6d46c50f1d..0b17fe8c5b 100644 --- a/packages/server/src/server/agent/providers/omp/test-utils/fake-omp.ts +++ b/packages/server/src/server/agent/providers/omp/test-utils/fake-omp.ts @@ -274,7 +274,7 @@ export class FakeOmpSession implements OmpRuntimeSession { return this.messages; } - async getAvailableModels(_timeoutMs?: number): Promise<OmpModel[]> { + async getAvailableModels(_timeoutMs?: number | null): Promise<OmpModel[]> { return this.models; } diff --git a/packages/server/src/server/agent/providers/omp/todo-mapper.test.ts b/packages/server/src/server/agent/providers/omp/todo-mapper.test.ts index b427e5ba65..e04ddee2bc 100644 --- a/packages/server/src/server/agent/providers/omp/todo-mapper.test.ts +++ b/packages/server/src/server/agent/providers/omp/todo-mapper.test.ts @@ -15,7 +15,7 @@ const TODO_PHASES = [ ] as const; describe("OMP todo mapper", () => { - test("maps todo tool results and collapses statuses to completed booleans", () => { + test("maps todo tool results without losing progress status", () => { expect( mapOmpTodoToolResult( parseToolResult({ @@ -37,9 +37,9 @@ describe("OMP todo mapper", () => { ).toEqual({ type: "todo", items: [ - { text: "alpha task", completed: false }, - { text: "beta task", completed: false }, - { text: "gamma task", completed: false }, + { text: "alpha task", status: "in_progress", completed: false }, + { text: "beta task", status: "pending", completed: false }, + { text: "gamma task", status: "pending", completed: false }, ], }); @@ -48,9 +48,9 @@ describe("OMP todo mapper", () => { ).toEqual({ type: "todo", items: [ - { text: "alpha task", completed: true }, - { text: "beta task", completed: false }, - { text: "gamma task", completed: false }, + { text: "alpha task", status: "completed", completed: true }, + { text: "beta task", status: "in_progress", completed: false }, + { text: "gamma task", status: "pending", completed: false }, ], }); }); @@ -67,8 +67,8 @@ describe("OMP todo mapper", () => { ).toEqual({ type: "todo", items: [ - { text: "beta task", completed: false }, - { text: "gamma task", completed: false }, + { text: "beta task", status: "in_progress", completed: false }, + { text: "gamma task", status: "pending", completed: false }, ], }); }); @@ -89,9 +89,9 @@ describe("OMP todo mapper", () => { { type: "todo", items: [ - { text: "alpha task", completed: true }, - { text: "beta task", completed: false }, - { text: "gamma task", completed: false }, + { text: "alpha task", status: "completed", completed: true }, + { text: "beta task", status: "in_progress", completed: false }, + { text: "gamma task", status: "pending", completed: false }, ], }, ]); diff --git a/packages/server/src/server/agent/providers/omp/todo-mapper.ts b/packages/server/src/server/agent/providers/omp/todo-mapper.ts index 1d8af90834..0b41d5a43f 100644 --- a/packages/server/src/server/agent/providers/omp/todo-mapper.ts +++ b/packages/server/src/server/agent/providers/omp/todo-mapper.ts @@ -41,11 +41,18 @@ function mapOmpTodoItems(items: readonly OmpTodoItem[]): AgentTimelineItem | nul type: "todo", items: items.map((item) => ({ text: item.content, + status: normalizeOmpTodoStatus(item.status), completed: item.status === "completed", })), }; } +function normalizeOmpTodoStatus(status: OmpTodoItem["status"]) { + if (status === "completed") return "completed" as const; + if (status === "in_progress") return "in_progress" as const; + return "pending" as const; +} + function resultDetails(result: OmpToolResult): Record<string, unknown> | null { if (typeof result === "string" || result === null) { return null; diff --git a/packages/server/src/server/agent/providers/opencode-agent.list-models-timeout.test.ts b/packages/server/src/server/agent/providers/opencode-agent.list-models-timeout.test.ts index ffa85ce38b..a3aed018bc 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.list-models-timeout.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.list-models-timeout.test.ts @@ -2,6 +2,7 @@ import { afterEach, expect, test, vi } from "vitest"; import { createTestLogger } from "../../../test-utils/test-logger.js"; import { OpenCodeAgentClient } from "./opencode-agent.js"; +import { runProviderRefreshWithDeadline } from "../provider-refresh-deadline.js"; import { TestOpenCodeClient, TestOpenCodeHarness, @@ -11,32 +12,15 @@ afterEach(() => { vi.useRealTimers(); }); -test("allows a slow provider.list call to succeed instead of failing after 10 seconds", async () => { +test("the catalog deadline aborts provider.list and releases the server", async () => { vi.useFakeTimers(); const runtime = new TestOpenCodeHarness(); const openCodeClient = new TestOpenCodeClient(); - openCodeClient.providerListImplementation = () => - new Promise((resolve) => { - setTimeout(() => { - resolve({ - data: { - connected: ["zai"], - all: [ - { - id: "zai", - name: "Z.AI", - models: { - "glm-5.1": { - name: "GLM 5.1", - limit: { context: 128_000 }, - }, - }, - }, - ], - }, - }); - }, 15_000); + openCodeClient.providerListImplementation = (_parameters, options) => + new Promise((_resolve, reject) => { + const signal = (options as { signal: AbortSignal }).signal; + signal.addEventListener("abort", () => reject(signal.reason), { once: true }); }); runtime.enqueueClient(openCodeClient); @@ -44,24 +28,72 @@ test("allows a slow provider.list call to succeed instead of failing after 10 se serverManager: runtime, createClient: runtime.createClient, }); - const modelsPromise = client.fetchCatalog({ - scope: "workspace", - cwd: "/tmp/opencode-models", - force: false, + const modelsPromise = runProviderRefreshWithDeadline({ + label: "OpenCode", + timeoutMs: 100, + operation: (context) => + client.fetchCatalog( + { scope: "workspace", cwd: "/tmp/opencode-models", force: false }, + context, + ), }); + const rejection = expect(modelsPromise).rejects.toThrow( + "Timed out refreshing OpenCode after 100ms; pending: provider.list", + ); - await vi.advanceTimersByTimeAsync(15_000); + await vi.advanceTimersByTimeAsync(100); - await expect(modelsPromise).resolves.toMatchObject({ - models: [ - { - provider: "opencode", - id: "zai/glm-5.1", - label: "GLM 5.1", - }, - ], - }); + await rejection; expect(openCodeClient.calls.providerList).toHaveLength(1); + expect(openCodeClient.calls.providerListOptions[0]).toMatchObject({ + signal: expect.objectContaining({ aborted: true }), + }); + expect(runtime.acquisitions).toEqual([{ kind: "current", releaseCount: 1 }]); +}); + +test("the catalog deadline aborts app.agents and releases the server", async () => { + vi.useFakeTimers(); + + const runtime = new TestOpenCodeHarness(); + const openCodeClient = new TestOpenCodeClient(); + openCodeClient.providerListResponse = { + data: { + connected: ["openai"], + all: [{ id: "openai", name: "OpenAI", models: {} }], + }, + }; + openCodeClient.appAgentsImplementation = (_parameters, options) => + new Promise((_resolve, reject) => { + const signal = (options as { signal: AbortSignal }).signal; + signal.addEventListener("abort", () => reject(signal.reason), { once: true }); + }); + runtime.enqueueClient(openCodeClient); + + const client = new OpenCodeAgentClient(createTestLogger(), undefined, { + serverManager: runtime, + createClient: runtime.createClient, + }); + const catalogPromise = runProviderRefreshWithDeadline({ + label: "OpenCode", + timeoutMs: 100, + operation: (context) => + client.fetchCatalog( + { scope: "workspace", cwd: "/tmp/opencode-agents", force: false }, + context, + ), + }); + const rejection = expect(catalogPromise).rejects.toThrow( + "Timed out refreshing OpenCode after 100ms; pending: app.agents", + ); + + await vi.advanceTimersByTimeAsync(100); + + await rejection; + expect(openCodeClient.calls.appAgents).toHaveLength(1); + expect(openCodeClient.calls.appAgentsOptions[0]).toMatchObject({ + signal: expect.objectContaining({ aborted: true }), + }); + expect(runtime.acquisitions).toEqual([{ kind: "current", releaseCount: 1 }]); }); test("uses a new server for explicit catalog refresh", async () => { diff --git a/packages/server/src/server/agent/providers/opencode-agent.test.ts b/packages/server/src/server/agent/providers/opencode-agent.test.ts index 2c31762e37..3931fe0358 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.test.ts @@ -480,6 +480,7 @@ describe("OpenCodeAgentClient adapter smoke tests", () => { models: { "big-pickle": { name: "Big Pickle", + variants: { max: { reasoningEffort: "max" } }, limit: { context: 200_000, }, @@ -529,6 +530,11 @@ describe("OpenCodeAgentClient adapter smoke tests", () => { expect(catalog.models[0]).toMatchObject({ id: TEST_MODEL, label: "Big Pickle", + thinkingOptions: [ + { id: "default", label: "Default", isDefault: true }, + { id: "max", label: "max" }, + ], + defaultThinkingOptionId: "default", metadata: { providerId: "opencode", modelId: "big-pickle", @@ -824,7 +830,40 @@ describe("OpenCodeAgentClient adapter smoke tests", () => { }, 180_000); }); -describe("OpenCode adapter context-window normalization", () => { +describe("OpenCode adapter normalization", () => { + test("omits OpenCode's implicit default variant from new and updated sessions", async () => { + const runtime = new TestOpenCodeHarness(); + const openCode = new TestOpenCodeClient(); + openCode.sessionCreateResponse = { data: { id: "ses_default_variant" } }; + openCode.sessionPromptAsyncEvents = [ + { type: "session.idle", properties: { sessionID: "ses_default_variant" } }, + ]; + runtime.enqueueClient(openCode); + const client = new OpenCodeAgentClient(createTestLogger(), undefined, { + serverManager: runtime, + createClient: runtime.createClient, + }); + const session = await client.createSession({ + provider: "opencode", + cwd: "/workspace/repo", + model: "catalog-provider/single-variant-model", + thinkingOptionId: "default", + }); + + await collectTurnEvents(streamSession(session, "Use the model default")); + await session.setThinkingOption?.("max"); + await collectTurnEvents(streamSession(session, "Use max")); + await session.setThinkingOption?.("default"); + await collectTurnEvents(streamSession(session, "Return to the model default")); + + expect(openCode.calls.sessionPromptAsync).toEqual([ + expect.not.objectContaining({ variant: expect.anything() }), + expect.objectContaining({ variant: "max" }), + expect.not.objectContaining({ variant: expect.anything() }), + ]); + await session.close(); + }); + test("builds OpenCode file parts for image prompt blocks", () => { expect( __openCodeInternals.buildOpenCodePromptParts([ @@ -1878,7 +1917,13 @@ describe("OpenCode adapter startTurn error handling", () => { provider: "opencode", item: { type: "todo", - items: [{ text: "Inspect current directory and existing files", completed: true }], + items: [ + { + text: "Inspect current directory and existing files", + status: "completed", + completed: true, + }, + ], }, }, ]); diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index f4bb284093..43c684ccb9 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -41,6 +41,7 @@ import { type AgentTimelineItem, type AgentUsage, type FetchCatalogOptions, + type ProviderRefreshContext, type ImportableProviderSession, type ImportProviderSessionContext, type ImportProviderSessionInput, @@ -53,6 +54,10 @@ import { type ToolCallTimelineItem, } from "../agent-sdk-types.js"; import { importSessionFromPersistence } from "../provider-session-import.js"; +import { + raceProviderRefreshAbort, + runProviderRefreshActivity, +} from "../provider-refresh-deadline.js"; import { isDefaultAgentCreateConfigUnattended, resolveDefaultAgentCreateConfig, @@ -68,6 +73,7 @@ import { execCommand } from "../../../utils/spawn.js"; import { mapOpencodeToolCall } from "./opencode/tool-call-mapper.js"; import { OpenCodeServerManager, + type OpenCodeServerAcquisition, type OpenCodeServerManagerLike, } from "./opencode/server-manager.js"; import { resolveOpenCodeHomeDir } from "./opencode/paths.js"; @@ -112,6 +118,7 @@ const OPENCODE_CAPABILITIES: AgentCapabilityFlags = { const OPENCODE_BUILD_MODE_ID = "build"; const OPENCODE_LEGACY_FULL_ACCESS_MODE_ID = "full-access"; +const OPENCODE_DEFAULT_VARIANT_ID = "default"; const OPENCODE_AUTO_ACCEPT_FEATURE_ID = "auto_accept"; const OPENCODE_PERSISTED_SESSION_LIMIT = 200; const OPENCODE_PENDING_ABORT_START_TIMEOUT_MS = 10_000; @@ -297,9 +304,9 @@ type OpenCodeMcpConfig = }; const MCP_ALREADY_PRESENT_ERROR_TOKENS = ["already", "exists", "connected"] as const; -const OPENCODE_PROVIDER_LIST_TIMEOUT_MS = 30_000; const OPENCODE_METADATA_CONCURRENCY = 4; const openCodeMetadataLimit = pLimit(OPENCODE_METADATA_CONCURRENCY); + const OPENCODE_HANDLED_BUILTIN_SLASH_COMMANDS: AgentSlashCommand[] = [ { name: "compact", @@ -581,6 +588,14 @@ function normalizeOpenCodeModeId(modeId: string | null | undefined): string | nu return trimmed; } +function normalizeOpenCodeVariantId(variantId: string | null | undefined): string | null { + const trimmed = typeof variantId === "string" ? variantId.trim() : ""; + if (!trimmed || trimmed === OPENCODE_DEFAULT_VARIANT_ID) { + return null; + } + return trimmed; +} + function resolveOpenCodeRuntimeAgentId(modeId: string | null | undefined): string | undefined { const normalizedModeId = normalizeOpenCodeModeId(modeId); if (normalizedModeId === null) { @@ -592,15 +607,19 @@ function resolveOpenCodeRuntimeAgentId(modeId: string | null | undefined): strin } function normalizeOpenCodeConfig(config: OpenCodeAgentConfig): OpenCodeAgentConfig { - if (normalizeOpenCodeModeId(config.modeId) !== OPENCODE_LEGACY_FULL_ACCESS_MODE_ID) { - return { ...config }; + const normalized = { + ...config, + thinkingOptionId: normalizeOpenCodeVariantId(config.thinkingOptionId) ?? undefined, + }; + if (normalizeOpenCodeModeId(normalized.modeId) !== OPENCODE_LEGACY_FULL_ACCESS_MODE_ID) { + return normalized; } return { - ...config, + ...normalized, modeId: OPENCODE_BUILD_MODE_ID, featureValues: { - ...config.featureValues, + ...normalized.featureValues, [OPENCODE_AUTO_ACCEPT_FEATURE_ID]: true, }, }; @@ -724,11 +743,13 @@ function buildOpenCodeModelDefinition( }, ): AgentModelDefinition { const rawVariants = model.variants ? Object.keys(model.variants) : []; - const thinkingOptions = rawVariants.map((id, index) => ({ - id, - label: id, - isDefault: index === 0, - })); + // OpenCode lists only overrides; its base model behavior is selected by omitting `variant`. + const thinkingOptions = rawVariants.length + ? [ + { id: OPENCODE_DEFAULT_VARIANT_ID, label: "Default", isDefault: true }, + ...rawVariants.map((id) => ({ id, label: id })), + ] + : []; return { provider: "opencode", @@ -1408,18 +1429,26 @@ export class OpenCodeAgentClient implements AgentClient { } } - async fetchCatalog(options: FetchCatalogOptions): Promise<ProviderCatalog> { + async fetchCatalog( + options: FetchCatalogOptions, + context?: ProviderRefreshContext, + ): Promise<ProviderCatalog> { const workspace = providerWorkspaceFromCatalogOptions(options); if (workspace && !workspace.allowsHostService("opencode")) { throw new Error("OpenCode is unavailable in the selected workspace runtime"); } - const acquisition = options.force - ? await this.serverManager.acquireNew() - : await this.serverManager.acquireCurrent(); - const { url } = acquisition.server; - const isGlobalCatalog = options.scope === "global"; - + let acquisition: OpenCodeServerAcquisition | undefined; try { + await runProviderRefreshActivity(context, "server.acquire", async () => { + acquisition = options.force + ? await this.serverManager.acquireNew(context?.signal) + : await this.serverManager.acquireCurrent(context?.signal); + }); + if (!acquisition) throw new Error("OpenCode server acquisition did not complete"); + context?.signal.throwIfAborted(); + const { url } = acquisition.server; + const isGlobalCatalog = options.scope === "global"; + // OpenCode treats the catalog directory as a workspace. The global catalog // is not a project, so use the neutral OpenCode home instead of user home. const directory = isGlobalCatalog ? this.resolveHomeDir() : options.cwd; @@ -1434,12 +1463,12 @@ export class OpenCodeAgentClient implements AgentClient { const client = this.createOpenCodeClient({ baseUrl: url, directory }); const [models, modes] = await Promise.all([ - this.fetchModelsFromClient(client, directory), - this.fetchModesFromClient(client, directory), + this.fetchModelsFromClient(client, directory, context), + this.fetchModesFromClient(client, directory, context), ]); return { models, modes }; } finally { - await acquisition.release(); + await acquisition?.release(); } } @@ -1636,12 +1665,18 @@ export class OpenCodeAgentClient implements AgentClient { private async fetchModelsFromClient( client: OpencodeClient, directory: string, + context?: ProviderRefreshContext, ): Promise<AgentModelDefinition[]> { - const response = await openCodeMetadataLimit(() => - withTimeout( - client.provider.list({ directory }), - OPENCODE_PROVIDER_LIST_TIMEOUT_MS, - `OpenCode provider.list timed out after ${OPENCODE_PROVIDER_LIST_TIMEOUT_MS / 1000}s - server may not be authenticated or connected to any providers`, + const response = await runProviderRefreshActivity(context, "provider.list", () => + raceProviderRefreshAbort( + context?.signal, + openCodeMetadataLimit(() => { + context?.signal.throwIfAborted(); + return client.provider.list( + { directory }, + context ? { signal: context.signal } : undefined, + ); + }), ), ); @@ -1668,7 +1703,7 @@ export class OpenCodeAgentClient implements AgentClient { } const models: AgentModelDefinition[] = []; - this.modelContextWindows.clear(); + const contextWindows = new Map<string, number>(); for (const provider of providers.all) { if (!isAccessible(provider)) { continue; @@ -1678,7 +1713,7 @@ export class OpenCodeAgentClient implements AgentClient { const definition = buildOpenCodeModelDefinition(provider, modelId, model); const contextWindowMaxTokens = extractOpenCodeModelContextWindow(model); if (contextWindowMaxTokens !== undefined) { - this.modelContextWindows.set( + contextWindows.set( buildOpenCodeModelLookupKey(provider.id, modelId), contextWindowMaxTokens, ); @@ -1687,18 +1722,25 @@ export class OpenCodeAgentClient implements AgentClient { } } + context?.signal.throwIfAborted(); + this.modelContextWindows.clear(); + for (const [key, value] of contextWindows) this.modelContextWindows.set(key, value); + return models; } private async fetchModesFromClient( client: OpencodeClient, directory: string, + context?: ProviderRefreshContext, ): Promise<AgentMode[]> { - const response = await openCodeMetadataLimit(() => - withTimeout( - client.app.agents({ directory }), - 10_000, - "OpenCode app.agents timed out after 10s", + const response = await runProviderRefreshActivity(context, "app.agents", () => + raceProviderRefreshAbort( + context?.signal, + openCodeMetadataLimit(() => { + context?.signal.throwIfAborted(); + return client.app.agents({ directory }, context ? { signal: context.signal } : undefined); + }), ), ); @@ -1919,6 +1961,7 @@ function mapOpenCodeTodosToTimelineItems( return [ { text, + status: normalizeOpenCodeTodoStatus(todo.status), completed: todo.status === "completed", }, ]; @@ -1926,6 +1969,12 @@ function mapOpenCodeTodosToTimelineItems( }; } +function normalizeOpenCodeTodoStatus(status?: string | null) { + if (status === "completed") return "completed" as const; + if (status === "in_progress" || status === "inProgress") return "in_progress" as const; + return "pending" as const; +} + function createCompactionTimelineItem( status: Extract<AgentTimelineItem, { type: "compaction" }>["status"], trigger?: Extract<AgentTimelineItem, { type: "compaction" }>["trigger"], @@ -3202,10 +3251,7 @@ class OpenCodeAgentSession implements AgentSession { } async setThinkingOption(thinkingOptionId: string | null): Promise<void> { - const normalizedThinkingOptionId = - typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0 - ? thinkingOptionId - : null; + const normalizedThinkingOptionId = normalizeOpenCodeVariantId(thinkingOptionId); this.config.thinkingOptionId = normalizedThinkingOptionId ?? undefined; } diff --git a/packages/server/src/server/agent/providers/opencode-server-manager.test.ts b/packages/server/src/server/agent/providers/opencode-server-manager.test.ts index 38f698b473..d6a1d5578a 100644 --- a/packages/server/src/server/agent/providers/opencode-server-manager.test.ts +++ b/packages/server/src/server/agent/providers/opencode-server-manager.test.ts @@ -138,6 +138,25 @@ describe("OpenCodeServerManager generations", () => { expect(await runtime.managedProcesses.list()).toEqual([]); }); + test("aborted acquisition transfers no reference and leaves startup reusable", async () => { + const { manager, runtime } = createTestManager([4477], { autoAnnounce: false }); + const controller = new AbortController(); + + const abortedAcquisition = manager.acquireCurrent(controller.signal); + await runtime.settle(); + controller.abort(new Error("catalog refresh expired")); + + await expect(abortedAcquisition).rejects.toThrow("catalog refresh expired"); + runtime.processForPort(4477).announceListening(); + + const nextAcquisition = await manager.acquireCurrent(); + expect(nextAcquisition.server.url).toBe("http://127.0.0.1:4477"); + expect(runtime.launchedPorts).toEqual([4477]); + + await nextAcquisition.release(); + expect(runtime.terminatedPorts).toEqual([4477]); + }); + test("shutdown kills a server that is still starting", async () => { const { manager, runtime } = createTestManager([4472], { autoAnnounce: false }); diff --git a/packages/server/src/server/agent/providers/opencode/event-translator.test.ts b/packages/server/src/server/agent/providers/opencode/event-translator.test.ts index e8f7f5d94a..2c5d6792cc 100644 --- a/packages/server/src/server/agent/providers/opencode/event-translator.test.ts +++ b/packages/server/src/server/agent/providers/opencode/event-translator.test.ts @@ -680,8 +680,8 @@ describe("translateOpenCodeEvent", () => { item: { type: "todo", items: [ - { text: "Outline", completed: false }, - { text: "Ship", completed: true }, + { text: "Outline", status: "pending", completed: false }, + { text: "Ship", status: "completed", completed: true }, ], }, }, diff --git a/packages/server/src/server/agent/providers/opencode/server-manager.ts b/packages/server/src/server/agent/providers/opencode/server-manager.ts index 2baa07bbd3..0c850514da 100644 --- a/packages/server/src/server/agent/providers/opencode/server-manager.ts +++ b/packages/server/src/server/agent/providers/opencode/server-manager.ts @@ -25,8 +25,8 @@ export interface OpenCodeServerAcquisition { } export interface OpenCodeServerManagerLike { - acquireCurrent(): Promise<OpenCodeServerAcquisition>; - acquireNew(): Promise<OpenCodeServerAcquisition>; + acquireCurrent(signal?: AbortSignal): Promise<OpenCodeServerAcquisition>; + acquireNew(signal?: AbortSignal): Promise<OpenCodeServerAcquisition>; acquireDedicated(env: Record<string, string>): Promise<OpenCodeServerAcquisition>; acquireExisting(url: string): OpenCodeServerAcquisition | null; shutdown(): Promise<void>; @@ -137,13 +137,17 @@ export class OpenCodeServerManager implements OpenCodeServerManagerLike { process.on("SIGINT", cleanup); } - async acquireCurrent(): Promise<OpenCodeServerAcquisition> { - const server = await this.getCurrentServer(); + async acquireCurrent(signal?: AbortSignal): Promise<OpenCodeServerAcquisition> { + signal?.throwIfAborted(); + const server = await waitForServerAcquisition(this.getCurrentServer(), signal); + signal?.throwIfAborted(); return this.acquireServer(server); } - async acquireNew(): Promise<OpenCodeServerAcquisition> { - const server = await this.getNewServer(); + async acquireNew(signal?: AbortSignal): Promise<OpenCodeServerAcquisition> { + signal?.throwIfAborted(); + const server = await waitForServerAcquisition(this.getNewServer(), signal); + signal?.throwIfAborted(); return this.acquireServer(server); } @@ -533,6 +537,23 @@ export class OpenCodeServerManager implements OpenCodeServerManagerLike { } } +async function waitForServerAcquisition<T>( + operation: Promise<T>, + signal: AbortSignal | undefined, +): Promise<T> { + if (!signal) return await operation; + let handleAbort: (() => void) | undefined; + const aborted = new Promise<never>((_resolve, reject) => { + handleAbort = () => reject(signal.reason); + signal.addEventListener("abort", handleAbort, { once: true }); + }); + try { + return await Promise.race([operation, aborted]); + } finally { + if (handleAbort) signal.removeEventListener("abort", handleAbort); + } +} + async function resolveOpenCodeBinary(): Promise<string> { const found = await findExecutable("opencode"); if (!found) { diff --git a/packages/server/src/server/agent/providers/opencode/test-utils/test-opencode-harness.ts b/packages/server/src/server/agent/providers/opencode/test-utils/test-opencode-harness.ts index 7433532f0f..f17ba9da6a 100644 --- a/packages/server/src/server/agent/providers/opencode/test-utils/test-opencode-harness.ts +++ b/packages/server/src/server/agent/providers/opencode/test-utils/test-opencode-harness.ts @@ -71,6 +71,7 @@ export class TestOpenCodeHarness implements OpenCodeServerManagerLike { export class TestOpenCodeClient { readonly calls = { appAgents: [] as unknown[], + appAgentsOptions: [] as unknown[], commandList: [] as unknown[], eventSubscribe: [] as unknown[], experimentalSessionList: [] as unknown[], @@ -79,6 +80,7 @@ export class TestOpenCodeClient { mcpConnect: [] as unknown[], permissionReply: [] as unknown[], providerList: [] as unknown[], + providerListOptions: [] as unknown[], questionReject: [] as unknown[], questionReply: [] as unknown[], sessionAbort: [] as unknown[], @@ -95,6 +97,9 @@ export class TestOpenCodeClient { }; appAgentsResponse: OpenCodeResponse = { data: [] }; + appAgentsImplementation: + | ((parameters: unknown, options: unknown) => Promise<OpenCodeResponse>) + | null = null; commandListResponse: OpenCodeResponse = { data: [] }; eventStream: AsyncIterable<unknown>; experimentalSessionListResponse: OpenCodeResponse = { data: [] }; @@ -102,7 +107,9 @@ export class TestOpenCodeClient { mcpConnectResponse: OpenCodeResponse = {}; permissionReplyResponse: OpenCodeResponse = {}; providerListResponse: OpenCodeResponse = { data: { connected: [], all: [] } }; - providerListImplementation: (() => Promise<OpenCodeResponse>) | null = null; + providerListImplementation: + | ((parameters: unknown, options: unknown) => Promise<OpenCodeResponse>) + | null = null; globalEventImplementation: | ((options: unknown) => Promise<{ stream: AsyncIterable<unknown> }>) | null = null; @@ -141,8 +148,12 @@ export class TestOpenCodeClient { asSdkClient(): OpencodeClient { return { app: { - agents: async (parameters: unknown) => { + agents: async (parameters: unknown, options: unknown) => { this.calls.appAgents.push(parameters); + this.calls.appAgentsOptions.push(options); + if (this.appAgentsImplementation) { + return await this.appAgentsImplementation(parameters, options); + } return this.appAgentsResponse; }, }, @@ -195,10 +206,11 @@ export class TestOpenCodeClient { }, }, provider: { - list: async (parameters: unknown) => { + list: async (parameters: unknown, options: unknown) => { this.calls.providerList.push(parameters); + this.calls.providerListOptions.push(options); return this.providerListImplementation - ? await this.providerListImplementation() + ? await this.providerListImplementation(parameters, options) : this.providerListResponse; }, }, diff --git a/packages/server/src/server/agent/providers/pi/agent.ts b/packages/server/src/server/agent/providers/pi/agent.ts index d5214d3e10..c49de3aea2 100644 --- a/packages/server/src/server/agent/providers/pi/agent.ts +++ b/packages/server/src/server/agent/providers/pi/agent.ts @@ -37,9 +37,11 @@ import { type ListImportableSessionsOptions, type ProviderCatalog, type ProviderWorkspace, + type ProviderRefreshContext, type ToolCallDetail, } from "../../agent-sdk-types.js"; import { importSessionFromPersistence } from "../../provider-session-import.js"; +import { runProviderRefreshActivity } from "../../provider-refresh-deadline.js"; import { runProviderTurn } from "../provider-runner.js"; import { checkProviderLaunchAvailable, @@ -90,7 +92,6 @@ import { const PI_PROVIDER = "pi"; const DEFAULT_PI_THINKING_LEVEL: PiThinkingLevel = "medium"; const PI_BINARY_COMMAND = process.env.PI_COMMAND ?? process.env.PI_ACP_PI_COMMAND ?? "pi"; -const PI_CATALOG_REQUEST_TIMEOUT_MS = 120_000; const PASEO_PI_TREE_EXTENSION_COMMAND = "paseo_tree"; const PASEO_PI_CAPTURE_EXTENSION_COMMAND = "paseo_capture_entries"; const PASEO_PI_ENTRY_CAPTURE_MARKER = "PASEO_ENTRY_CAPTURE"; @@ -2517,22 +2518,43 @@ export class PiRpcAgentClient implements AgentClient { } } - async fetchCatalog(options: FetchCatalogOptions): Promise<ProviderCatalog> { + async fetchCatalog( + options: FetchCatalogOptions, + context?: ProviderRefreshContext, + ): Promise<ProviderCatalog> { const workspace = providerWorkspaceFromCatalogOptions(options); const cwd = workspace?.cwd ?? (options.scope === "global" ? homedir() : options.cwd); - const runtimeSession = await this.runtime.startSession({ - cwd, - workspace, - }); + let runtimeSession: PiRuntimeSession | undefined; + let closePromise: Promise<void> | undefined; + const closeSession = () => { + if (!runtimeSession) return Promise.resolve(); + closePromise ??= runtimeSession.close(); + return closePromise; + }; + const handleAbort = () => void closeSession().catch(() => undefined); + context?.signal.addEventListener("abort", handleAbort, { once: true }); try { + await runProviderRefreshActivity(context, "runtime.start", async () => { + runtimeSession = await this.runtime.startSession({ + cwd, + workspace, + signal: context?.signal, + }); + if (context?.signal.aborted) await closeSession(); + }); + if (!runtimeSession) throw new Error("Pi catalog runtime did not start"); + const catalogSession = runtimeSession; const models = transformPiModels( - (await runtimeSession.getAvailableModels(PI_CATALOG_REQUEST_TIMEOUT_MS)).map((model) => - mapPiModel(model, PI_PROVIDER), - ), + ( + await runProviderRefreshActivity(context, "get_available_models", () => + catalogSession.getAvailableModels(null), + ) + ).map((model) => mapPiModel(model, PI_PROVIDER)), ); return { models, modes: [] }; } finally { - await runtimeSession.close(); + context?.signal.removeEventListener("abort", handleAbort); + await closeSession(); } } diff --git a/packages/server/src/server/agent/providers/pi/cli-runtime.ts b/packages/server/src/server/agent/providers/pi/cli-runtime.ts index d6245648b6..51f4c5c6f1 100644 --- a/packages/server/src/server/agent/providers/pi/cli-runtime.ts +++ b/packages/server/src/server/agent/providers/pi/cli-runtime.ts @@ -96,6 +96,10 @@ export class PiCliRuntime implements PiRuntime { ...(runtimeSpawn ? { spawn: runtimeSpawn } : {}), }; const process = new JsonlRpcProcess(processOptions); + if (input.signal?.aborted) { + await process.close(input.signal.reason); + input.signal.throwIfAborted(); + } return new PiCliRuntimeSession(process, this.commandsRpcName); } } diff --git a/packages/server/src/server/agent/providers/pi/runtime.ts b/packages/server/src/server/agent/providers/pi/runtime.ts index f057b29209..4215930546 100644 --- a/packages/server/src/server/agent/providers/pi/runtime.ts +++ b/packages/server/src/server/agent/providers/pi/runtime.ts @@ -29,6 +29,7 @@ export interface PiStartSessionInput { cwd: string; workspace?: ProviderWorkspace; agentId?: string; + signal?: AbortSignal; env?: Record<string, string>; protocolMode?: "rpc" | "rpc-ui"; model?: string; diff --git a/packages/server/src/server/agent/structured-generation-providers.test.ts b/packages/server/src/server/agent/structured-generation-providers.test.ts index 06c8bed6bd..d34aff9640 100644 --- a/packages/server/src/server/agent/structured-generation-providers.test.ts +++ b/packages/server/src/server/agent/structured-generation-providers.test.ts @@ -18,8 +18,17 @@ class ProviderSnapshots { } describe("resolveStructuredGenerationProviders", () => { - test("uses explicit configured provider models without refreshing provider snapshots", async () => { - const snapshots = new ProviderSnapshots([]); + test("tries the configured model before dynamically discovered fallbacks", async () => { + const snapshots = new ProviderSnapshots([ + { + provider: "work-claude", + status: READY, + enabled: true, + models: [ + { provider: "work-claude", id: "claude-haiku-2026", label: "Haiku", isDefault: true }, + ], + }, + ]); const providers = await resolveStructuredGenerationProviders({ cwd: "/tmp/repo", @@ -31,8 +40,11 @@ describe("resolveStructuredGenerationProviders", () => { }, }); - expect(providers).toEqual([{ provider: "mock", model: "ten-second-stream" }]); - expect(snapshots.calls).toEqual([]); + expect(providers).toEqual([ + { provider: "mock", model: "ten-second-stream" }, + { provider: "work-claude", model: "claude-haiku-2026" }, + ]); + expect(snapshots.calls).toEqual([{ cwd: "/tmp/repo", wait: true }]); }); test("falls back to dynamic defaults and current selection when no provider is configured", async () => { @@ -155,7 +167,7 @@ describe("resolveStructuredGenerationProviders", () => { ]); }); - test("uses explicit configured provider models as-is instead of waiting to normalize aliases", async () => { + test("normalizes configured nested-provider aliases from the provider snapshot", async () => { const snapshots = new ProviderSnapshots([ { provider: "opencode", @@ -186,8 +198,8 @@ describe("resolveStructuredGenerationProviders", () => { }, }); - expect(providers).toEqual([{ provider: "plexus", model: "small-fast" }]); - expect(snapshots.calls).toEqual([]); + expect(providers).toEqual([{ provider: "opencode", model: "plexus/small-fast" }]); + expect(snapshots.calls).toEqual([{ cwd: "/tmp/repo", wait: true }]); }); test("keeps explicit candidates when provider snapshots are in error state", async () => { @@ -215,7 +227,10 @@ describe("resolveStructuredGenerationProviders", () => { }, }); - expect(providers).toEqual([{ provider: "current-provider", model: "configured-model" }]); - expect(snapshots.calls).toEqual([]); + expect(providers).toEqual([ + { provider: "current-provider", model: "configured-model" }, + { provider: "current-provider", model: "selected-model", thinkingOptionId: "medium" }, + ]); + expect(snapshots.calls).toEqual([{ cwd: "/tmp/repo", wait: true }]); }); }); diff --git a/packages/server/src/server/agent/structured-generation-providers.ts b/packages/server/src/server/agent/structured-generation-providers.ts index 89a331b66d..2a09ee1844 100644 --- a/packages/server/src/server/agent/structured-generation-providers.ts +++ b/packages/server/src/server/agent/structured-generation-providers.ts @@ -44,22 +44,6 @@ export async function resolveStructuredGenerationProviders( options: ResolveStructuredGenerationProvidersOptions, ): Promise<StructuredGenerationProvider[]> { const configuredProviders = readConfiguredProviders(options.daemonConfig); - if (configuredProviders.length > 0) { - const explicitProviders = resolveExplicitConfiguredProviders(configuredProviders); - if (explicitProviders.length === configuredProviders.length) { - return dedupeProviders(explicitProviders); - } - - const providerEntries = await options.providerSnapshotManager.listProviders({ - cwd: options.cwd, - wait: false, - }); - const providers = resolveConfiguredProviders(configuredProviders, providerEntries); - if (providers.length > 0) { - return dedupeProviders(providers); - } - } - const providerEntries = await options.providerSnapshotManager.listProviders({ cwd: options.cwd, wait: true, @@ -100,42 +84,6 @@ export async function resolveStructuredGenerationProviders( return dedupeProviders(providers); } -function resolveConfiguredProviders( - configuredProviders: readonly { provider: string; model?: string; thinkingOptionId?: string }[], - providerEntries: readonly ProviderSnapshotEntry[], -): StructuredGenerationProvider[] { - const enabledEntries = providerEntries.filter((entry) => entry.enabled); - const modelEntries = enabledEntries.filter((entry) => (entry.models?.length ?? 0) > 0); - const entriesByProvider = new Map(enabledEntries.map((entry) => [entry.provider, entry])); - const providers: StructuredGenerationProvider[] = []; - for (const configured of configuredProviders) { - const resolved = resolveConfiguredCandidate(configured, modelEntries, entriesByProvider); - if (resolved) { - providers.push(resolved); - } - } - return providers; -} - -function resolveExplicitConfiguredProviders( - configuredProviders: readonly { provider: string; model?: string; thinkingOptionId?: string }[], -): StructuredGenerationProvider[] { - const providers: StructuredGenerationProvider[] = []; - for (const configured of configuredProviders) { - const provider = configured.provider.trim(); - const model = configured.model?.trim(); - if (!provider || !model) { - continue; - } - providers.push({ - provider, - model, - ...(configured.thinkingOptionId ? { thinkingOptionId: configured.thinkingOptionId } : {}), - }); - } - return providers; -} - function resolveCurrentSelection( selection: ResolveStructuredGenerationProvidersOptions["currentSelection"], readyEntries: readonly ProviderSnapshotEntry[], diff --git a/packages/server/src/server/agent/tools/paseo-tools.ts b/packages/server/src/server/agent/tools/paseo-tools.ts index 2aa69906d0..fcc314ac11 100644 --- a/packages/server/src/server/agent/tools/paseo-tools.ts +++ b/packages/server/src/server/agent/tools/paseo-tools.ts @@ -4,6 +4,8 @@ import type { Logger } from "pino"; import type { AgentMode, AgentProvider, AgentSessionConfig } from "../agent-sdk-types.js"; import type { AgentManager } from "../agent-manager.js"; +import { AgentProfileSchema } from "@getpaseo/protocol/messages"; +import type { DaemonConfigStore } from "../../daemon-config-store.js"; import { AgentFeatureSchema, AgentPermissionRequestPayloadSchema, @@ -98,6 +100,7 @@ export interface PaseoToolHostDependencies { getDaemonTcpPort?: () => number | null; scheduleService?: ScheduleService | null; providerSnapshotManager: ProviderSnapshotManager; + daemonConfigStore?: Pick<DaemonConfigStore, "get">; github?: ForgeService; workspaceGitService?: Pick< WorkspaceGitService, @@ -543,6 +546,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase workspaceScripts, scheduleService, providerSnapshotManager, + daemonConfigStore, callerAgentId, resolveSpeakHandler, resolveCallerContext, @@ -2919,6 +2923,30 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase }, ); + registerTool( + "list_profiles", + { + title: "List agent profiles", + description: + "List agent profiles: named provider/model/mode bundles a human configured for specific " + + "kinds of work. Read each profile's `notes` to pick the one that fits the task you're " + + "delegating, then copy its `provider`, `model`, `modeId`, `thinkingOptionId`, and " + + "`featureValues` into create_agent (there is no `profile` parameter). Returns an empty " + + "list if none are configured.", + inputSchema: {}, + outputSchema: { + profiles: z.array(AgentProfileSchema), + }, + }, + async () => { + const profiles = daemonConfigStore?.get().agentProfiles ?? []; + return { + content: [], + structuredContent: ensureValidJson({ profiles }), + }; + }, + ); + registerTool( "inspect_provider", { diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index a8214ed98b..f89f8a4420 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -158,9 +158,7 @@ import { type WorkspaceRuntimeService, } from "./workspace-runtime/index.js"; import { createProviderProbeService, type ProviderProbeService } from "./provider-probe/index.js"; -import { FileBackedChatService } from "./chat/chat-service.js"; import { CheckoutDiffManager } from "./checkout-diff-manager.js"; -import { LoopService } from "./loop-service.js"; import { ScheduleService } from "./schedule/service.js"; import { DaemonConfigStore, type MutableDaemonConfig } from "./daemon-config-store.js"; import { BrowserToolsBroker } from "./browser-tools/broker.js"; @@ -181,11 +179,11 @@ import { createConfiguredTerminalManager } from "../terminal/terminal-manager-fa import { applyTerminalAgentHookSetting } from "../terminal/agent-hooks/terminal-agent-hook-setting.js"; import { loadOrCreateDaemonKeyPair } from "./daemon-keypair.js"; import { createRelayRuntime, type RelayRuntime } from "./relay-runtime.js"; -import type { PushNotificationSender } from "./push/notifications.js"; +import type { PushNotificationSender } from "./push/index.js"; import { getOrCreateServerId } from "./server-id.js"; import { resolveDaemonVersion } from "./daemon-version.js"; import type { AgentClient, AgentProvider } from "./agent/agent-sdk-types.js"; -import type { FirstAgentContext, TerminalProfile } from "@getpaseo/protocol/messages"; +import type { AgentProfile, FirstAgentContext, TerminalProfile } from "@getpaseo/protocol/messages"; import type { AgentProviderRuntimeSettingsMap, ProviderOverride, @@ -419,6 +417,7 @@ export interface PaseoDaemonConfig { enableTerminalAgentHooks?: boolean; appendSystemPrompt?: string; terminalProfiles?: TerminalProfile[]; + agentProfiles?: AgentProfile[]; staticDir: string; mcpDebug: boolean; isDev?: boolean; @@ -448,6 +447,7 @@ export interface PaseoDaemonConfig { dictationFinalTimeoutMs?: number; downloadTokenTtlMs?: number; agentProviderSettings?: AgentProviderRuntimeSettingsMap; + providerCatalogRefreshTimeoutMs?: number; metadataGeneration?: { providers?: Array<{ provider: string; @@ -558,6 +558,10 @@ function createInitialMutableDaemonConfig(config: PaseoDaemonConfig): MutableDae initialConfig.terminalProfiles = config.terminalProfiles; } + if (config.agentProfiles !== undefined) { + initialConfig.agentProfiles = config.agentProfiles; + } + return initialConfig; } @@ -1021,10 +1025,6 @@ export async function createPaseoDaemon( await workspaceRegistry?.remove(workspaceId); providerWorkspaceBindings.delete(workspaceId); } - const chatService = new FileBackedChatService({ - paseoHome: config.paseoHome, - logger, - }); const github = createGitHubService(); const workspaceGitService = new WorkspaceGitServiceImpl({ logger, @@ -1058,6 +1058,7 @@ export async function createPaseoDaemon( }; const providerSnapshotManager = new ProviderSnapshotManager({ logger: providerSnapshotLogger, + refreshTimeoutMs: config.providerCatalogRefreshTimeoutMs, runtimeSettings: config.agentProviderSettings, providerOverrides: config.providerOverrides, workspaceGitService, @@ -1134,8 +1135,6 @@ export async function createPaseoDaemon( void workspaceReconciliation.reconcileNow().catch((error) => { logger.warn({ err: error }, "Initial workspace reconciliation failed"); }); - await chatService.initialize(); - logger.info({ elapsed: elapsed() }, "Chat service initialized"); const checkoutDiffManager = new CheckoutDiffManager({ logger, paseoHome: config.paseoHome, @@ -1388,6 +1387,7 @@ export async function createPaseoDaemon( }); const hubRelationships = new HubRelationshipController({ paseoHome: config.paseoHome, + hostname: getHostname(), serverId, daemonPublicKey: daemonKeyPair.publicKeyB64, logger, @@ -1412,15 +1412,6 @@ export async function createPaseoDaemon( }), }); - const loopService = new LoopService({ - paseoHome: config.paseoHome, - logger, - agentManager, - createAgent, - ensureWorkspaceForCreate: ensureWorkspaceForCreateAndBroadcastExternal, - }); - await loopService.initialize(); - logger.info({ elapsed: elapsed() }, "Loop service initialized"); const createScheduleLocalWorkspaceExternal = async (input: { cwd: string; firstAgentContext: FirstAgentContext; @@ -1520,6 +1511,7 @@ export async function createPaseoDaemon( getDaemonTcpPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null), scheduleService, providerSnapshotManager, + daemonConfigStore, github, workspaceGitService, findWorkspaceIdForCwd: findWorkspaceIdForCwdExternal, @@ -1808,8 +1800,6 @@ export async function createPaseoDaemon( }, projectRegistry, workspaceRegistry, - chatService, - loopService, scheduleService, checkoutDiffManager, serviceProxy, diff --git a/packages/server/src/server/chat/chat-mentions.test.ts b/packages/server/src/server/chat/chat-mentions.test.ts deleted file mode 100644 index b209abb0d3..0000000000 --- a/packages/server/src/server/chat/chat-mentions.test.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import type pino from "pino"; -import type { StoredAgentRecord } from "../agent/agent-storage.js"; -import type { ManagedAgent } from "../agent/agent-manager.js"; -import { - buildChatMentionNotification, - notifyChatMentions, - prepareChatMentionFanout, -} from "./chat-mentions.js"; - -function storedAgent(overrides: Partial<StoredAgentRecord> & { id: string }): StoredAgentRecord { - return { - internal: false, - archivedAt: null, - lastStatus: "idle", - ...overrides, - } as StoredAgentRecord; -} - -function liveAgent(overrides: Partial<ManagedAgent> & { id: string }): ManagedAgent { - return { internal: false, lifecycle: "idle", ...overrides } as ManagedAgent; -} - -async function prepare(input: { - authorAgentId: string; - mentionAgentIds: string[]; - storedAgents?: StoredAgentRecord[]; - liveAgents?: ManagedAgent[]; - roomPosterAgentIds?: string[]; - limit?: number; -}) { - const result = await prepareChatMentionFanout({ - authorAgentId: input.authorAgentId, - mentionAgentIds: input.mentionAgentIds, - storedAgents: input.storedAgents ?? [], - liveAgents: input.liveAgents ?? [], - listRoomPosterAgentIds: async () => input.roomPosterAgentIds ?? [], - limit: input.limit, - }); - if (!result.ok) { - throw new Error(`expected ok prepare, got error: ${result.error}`); - } - return result.prepared; -} - -describe("chat mentions", () => { - test("@everyone in an empty room resolves to no targets", async () => { - const prepared = await prepare({ - authorAgentId: "author-agent", - mentionAgentIds: ["everyone"], - storedAgents: [storedAgent({ id: "unrelated-agent" })], - liveAgents: [liveAgent({ id: "live-unrelated-agent" })], - roomPosterAgentIds: [], - }); - - expect(prepared.targetMentionAgentIds).toEqual([]); - }); - - test("@everyone in a single-poster room excludes the author", async () => { - const prepared = await prepare({ - authorAgentId: "author-agent", - mentionAgentIds: ["everyone"], - storedAgents: [storedAgent({ id: "author-agent" })], - roomPosterAgentIds: ["author-agent"], - }); - - expect(prepared.targetMentionAgentIds).toEqual([]); - }); - - test("@everyone only expands to active posters in the room", async () => { - const prepared = await prepare({ - authorAgentId: "author-agent", - mentionAgentIds: ["everyone"], - storedAgents: [ - storedAgent({ id: "agent-a" }), - storedAgent({ id: "agent-b" }), - storedAgent({ id: "unrelated-agent" }), - ], - liveAgents: [liveAgent({ id: "agent-c" }), liveAgent({ id: "live-unrelated-agent" })], - roomPosterAgentIds: ["agent-a", "agent-b", "agent-c"], - }); - - expect([...prepared.targetMentionAgentIds].sort()).toEqual(["agent-a", "agent-b", "agent-c"]); - }); - - test("@everyone excludes archived and error-state agents", async () => { - const prepared = await prepare({ - authorAgentId: "author-agent", - mentionAgentIds: ["everyone"], - storedAgents: [ - storedAgent({ id: "active-agent" }), - storedAgent({ id: "archived-agent", archivedAt: "2026-03-28T00:00:00.000Z" }), - storedAgent({ id: "stored-error-agent", lastStatus: "error" }), - ], - liveAgents: [liveAgent({ id: "live-error-agent", lifecycle: "error" })], - roomPosterAgentIds: [ - "active-agent", - "archived-agent", - "stored-error-agent", - "live-error-agent", - ], - }); - - expect(prepared.targetMentionAgentIds).toEqual(["active-agent"]); - }); - - test("@everyone deduplicates with explicit mentions and keeps explicit non-everyone mentions", async () => { - const prepared = await prepare({ - authorAgentId: "author-agent", - mentionAgentIds: ["everyone", "agent-a", "custom-title"], - storedAgents: [storedAgent({ id: "agent-a" })], - liveAgents: [liveAgent({ id: "agent-b" })], - roomPosterAgentIds: ["agent-a", "agent-b"], - }); - - expect([...prepared.targetMentionAgentIds].sort()).toEqual([ - "agent-a", - "agent-b", - "custom-title", - ]); - }); - - test("does not list room posters when @everyone is not mentioned", async () => { - const listRoomPosterAgentIds = vi.fn(async () => []); - const result = await prepareChatMentionFanout({ - authorAgentId: "author-agent", - mentionAgentIds: ["agent-a"], - storedAgents: [storedAgent({ id: "agent-a" })], - liveAgents: [], - listRoomPosterAgentIds, - }); - - expect(result.ok).toBe(true); - expect(listRoomPosterAgentIds).not.toHaveBeenCalled(); - }); - - test("rejects @everyone fan-out above the hard cap", async () => { - const posters = Array.from({ length: 26 }, (_, index) => `agent-${index}`); - const result = await prepareChatMentionFanout({ - authorAgentId: "author-agent", - mentionAgentIds: ["everyone"], - storedAgents: posters.map((id) => storedAgent({ id })), - liveAgents: [], - listRoomPosterAgentIds: async () => posters, - }); - - expect(result).toEqual({ - ok: false, - error: - "@everyone would notify 26 agents, which exceeds the limit of 25. Narrow the room or mention specific agents.", - }); - }); - - test("notification body strips inline mentions but keeps the room context", () => { - expect( - buildChatMentionNotification({ - room: "coord-room", - authorAgentId: "author-agent", - body: "@agent-a @everyone Check the latest status.", - mentionAgentIds: ["agent-a", "everyone"], - }), - ).toContain("Check the latest status."); - }); - - test("notifyChatMentions delegates sends for resolved targets", async () => { - const resolveAgentIdentifier = vi.fn(async (identifier: string) => ({ - ok: true as const, - agentId: identifier, - })); - const sendAgentMessage = vi.fn(async () => {}); - const logger = { - warn: vi.fn(), - } as unknown as pino.Logger; - - const storedAgents = [storedAgent({ id: "agent-a" })]; - const liveAgents = [liveAgent({ id: "agent-b" })]; - - await notifyChatMentions({ - room: "coord-room", - authorAgentId: "author-agent", - body: "@everyone Check status", - mentionAgentIds: ["everyone"], - logger, - storedAgents, - liveAgents, - prepared: { - targetMentionAgentIds: ["agent-a", "agent-b"], - roomPosterAgentIds: ["agent-a", "agent-b"], - }, - resolveAgentIdentifier, - sendAgentMessage, - }); - - expect(resolveAgentIdentifier).toHaveBeenCalledTimes(2); - expect(sendAgentMessage).toHaveBeenCalledTimes(2); - expect(sendAgentMessage).toHaveBeenCalledWith( - "agent-a", - expect.stringContaining('room "coord-room"'), - ); - expect(sendAgentMessage).toHaveBeenCalledWith( - "agent-b", - expect.stringContaining("Check status"), - ); - }); -}); diff --git a/packages/server/src/server/chat/chat-mentions.ts b/packages/server/src/server/chat/chat-mentions.ts deleted file mode 100644 index f07237dc41..0000000000 --- a/packages/server/src/server/chat/chat-mentions.ts +++ /dev/null @@ -1,194 +0,0 @@ -import type pino from "pino"; -import type { StoredAgentRecord } from "../agent/agent-storage.js"; -import type { ManagedAgent } from "../agent/agent-manager.js"; - -export const CHAT_MENTION_FANOUT_LIMIT = 25; - -export interface ChatMentionNotificationInput { - room: string; - authorAgentId: string; - body: string; - mentionAgentIds: string[]; -} - -export interface PreparedChatMentionFanout { - targetMentionAgentIds: string[]; - roomPosterAgentIds: string[]; -} - -export type PrepareChatMentionFanoutResult = - | { ok: true; prepared: PreparedChatMentionFanout } - | { ok: false; error: string }; - -export interface PrepareChatMentionFanoutInput { - authorAgentId: string; - mentionAgentIds: string[]; - storedAgents: StoredAgentRecord[]; - liveAgents: ManagedAgent[]; - listRoomPosterAgentIds: () => Promise<string[]>; - limit?: number; -} - -export interface NotifyChatMentionsInput extends ChatMentionNotificationInput { - logger: pino.Logger; - storedAgents: StoredAgentRecord[]; - liveAgents: ManagedAgent[]; - prepared: PreparedChatMentionFanout; - resolveAgentIdentifier: ( - identifier: string, - ) => Promise<{ ok: true; agentId: string } | { ok: false; error: string }>; - sendAgentMessage: (agentId: string, text: string) => Promise<void>; -} - -export async function prepareChatMentionFanout( - input: PrepareChatMentionFanoutInput, -): Promise<PrepareChatMentionFanoutResult> { - const mentionsEveryone = input.mentionAgentIds.includes("everyone"); - const roomPosterAgentIds = mentionsEveryone ? await input.listRoomPosterAgentIds() : []; - const targetMentionAgentIds = expandChatMentionTargets({ - authorAgentId: input.authorAgentId, - mentionAgentIds: input.mentionAgentIds, - storedAgents: input.storedAgents, - liveAgents: input.liveAgents, - roomPosterAgentIds, - }); - - if (mentionsEveryone) { - const limit = input.limit ?? CHAT_MENTION_FANOUT_LIMIT; - if (targetMentionAgentIds.length > limit) { - return { - ok: false, - error: `@everyone would notify ${targetMentionAgentIds.length} agents, which exceeds the limit of ${limit}. Narrow the room or mention specific agents.`, - }; - } - } - - return { ok: true, prepared: { targetMentionAgentIds, roomPosterAgentIds } }; -} - -export async function notifyChatMentions(input: NotifyChatMentionsInput): Promise<void> { - const { targetMentionAgentIds } = input.prepared; - if (targetMentionAgentIds.length === 0) { - return; - } - - const notification = buildChatMentionNotification({ - room: input.room, - authorAgentId: input.authorAgentId, - body: input.body, - mentionAgentIds: targetMentionAgentIds, - }); - - await Promise.all( - targetMentionAgentIds.map(async (mentionedAgentId) => { - const resolved = await input.resolveAgentIdentifier(mentionedAgentId); - if (!resolved.ok) { - input.logger.warn( - { mentionedAgentId, room: input.room, error: resolved.error }, - "Failed to resolve chat mention target", - ); - return; - } - - // Re-check eligibility on the resolved canonical id: explicit mentions may - // be custom titles that resolve to an archived or error-state agent. - if ( - !isChatMentionTargetEligible({ - agentId: resolved.agentId, - authorAgentId: input.authorAgentId, - storedAgents: input.storedAgents, - liveAgents: input.liveAgents, - }) - ) { - return; - } - - try { - await input.sendAgentMessage(resolved.agentId, notification); - } catch (error) { - input.logger.warn( - { err: error, mentionedAgentId: resolved.agentId, room: input.room }, - "Failed to notify mentioned agent about chat message", - ); - } - }), - ); -} - -export function buildChatMentionNotification(input: ChatMentionNotificationInput): string { - const mentioned = input.mentionAgentIds.map((agentId) => `@${agentId}`).join(", "); - const bodyWithoutMentions = input.body.replace(/(^|\s)@[A-Za-z0-9][A-Za-z0-9._-]*/g, "$1").trim(); - const body = bodyWithoutMentions.length > 0 ? bodyWithoutMentions : input.body; - - return [ - `Chat mention from ${input.authorAgentId} in room "${input.room}".`, - `Mentioned agents: ${mentioned}.`, - "Message:", - body, - `Read the room with: paseo chat read ${input.room} --limit 20`, - ].join("\n"); -} - -function expandChatMentionTargets(input: { - authorAgentId: string; - mentionAgentIds: string[]; - storedAgents: StoredAgentRecord[]; - liveAgents: ManagedAgent[]; - roomPosterAgentIds: string[]; -}): string[] { - const candidates = new Set<string>(); - const mentionsEveryone = input.mentionAgentIds.includes("everyone"); - - for (const mentionAgentId of input.mentionAgentIds) { - if (mentionAgentId === "everyone" || mentionAgentId === input.authorAgentId) { - continue; - } - candidates.add(mentionAgentId); - } - - if (mentionsEveryone) { - for (const posterAgentId of input.roomPosterAgentIds) { - if (posterAgentId !== input.authorAgentId) { - candidates.add(posterAgentId); - } - } - } - - const targets: string[] = []; - for (const candidate of candidates) { - if ( - isChatMentionTargetEligible({ - agentId: candidate, - authorAgentId: input.authorAgentId, - storedAgents: input.storedAgents, - liveAgents: input.liveAgents, - }) - ) { - targets.push(candidate); - } - } - return targets; -} - -function isChatMentionTargetEligible(input: { - agentId: string; - authorAgentId: string; - storedAgents: StoredAgentRecord[]; - liveAgents: ManagedAgent[]; -}): boolean { - if (input.agentId === input.authorAgentId) { - return false; - } - - const stored = input.storedAgents.find((record) => record.id === input.agentId); - if (stored?.internal || stored?.archivedAt || stored?.lastStatus === "error") { - return false; - } - - const live = input.liveAgents.find((agent) => agent.id === input.agentId); - if (live) { - return !live.internal && live.lifecycle !== "error"; - } - - return true; -} diff --git a/packages/server/src/server/chat/chat-service.test.ts b/packages/server/src/server/chat/chat-service.test.ts deleted file mode 100644 index 0b9ed6a1f9..0000000000 --- a/packages/server/src/server/chat/chat-service.test.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { mkdtemp, readFile, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import pino from "pino"; -import { - type ChatServiceError, - FileBackedChatService, - parseMentionAgentIds, - type PostChatMessageInput, -} from "./chat-service.js"; - -describe("FileBackedChatService", () => { - let paseoHome: string; - let service: FileBackedChatService; - - async function sendChatMessage(input: PostChatMessageInput) { - return await service.dispatchMessage(input); - } - - beforeEach(async () => { - paseoHome = await mkdtemp(path.join(tmpdir(), "paseo-chat-service-")); - service = new FileBackedChatService({ - paseoHome, - logger: pino({ level: "silent" }), - }); - await service.initialize(); - }); - - afterEach(async () => { - await rm(paseoHome, { recursive: true, force: true }); - }); - - test("creates rooms, enforces unique names, and persists to disk", async () => { - const created = await service.createRoom({ - name: "cli-features-epic", - purpose: "Coordination room", - }); - - await expect( - service.createRoom({ - name: "CLI-FEATURES-EPIC", - }), - ).rejects.toMatchObject<Partial<ChatServiceError>>({ - code: "chat_room_name_taken", - }); - - const raw = await readFile(path.join(paseoHome, "chat", "rooms.json"), "utf8"); - expect(raw).toContain("cli-features-epic"); - expect(created.name).toBe("cli-features-epic"); - expect(created.purpose).toBe("Coordination room"); - expect(created.messageCount).toBe(0); - }); - - test("resolves rooms by name or ID, validates replies, and reads filtered messages", async () => { - const room = await service.createRoom({ name: "auth-refactor" }); - const first = await sendChatMessage({ - room: room.name, - authorAgentId: "agent-a", - body: "first message for @agent-b and @agent-c and again @agent-b", - }); - await sendChatMessage({ - room: room.id, - authorAgentId: "agent-b", - body: "reply", - replyToMessageId: first.id, - }); - - await expect( - sendChatMessage({ - room: room.name, - authorAgentId: "agent-b", - body: "bad reply", - replyToMessageId: "missing", - }), - ).rejects.toMatchObject<Partial<ChatServiceError>>({ - code: "chat_message_not_found", - }); - - const all = await service.readMessages({ room: room.name, limit: 10 }); - expect(all).toHaveLength(2); - expect(all[0]?.mentionAgentIds).toEqual(["agent-b", "agent-c"]); - - const byAuthor = await service.readMessages({ - room: room.id, - authorAgentId: "agent-b", - limit: 10, - }); - expect(byAuthor).toHaveLength(1); - expect(byAuthor[0]?.body).toBe("reply"); - - const detail = await service.inspectRoom({ room: room.name }); - expect(detail.room.messageCount).toBe(2); - expect(detail.room.lastMessageAt).toBeTruthy(); - }); - - test("lists unique agents who have posted to a room", async () => { - const room = await service.createRoom({ name: "incident-room" }); - const otherRoom = await service.createRoom({ name: "other-room" }); - await sendChatMessage({ - room: room.name, - authorAgentId: "agent-a", - body: "first", - }); - await sendChatMessage({ - room: room.name, - authorAgentId: "agent-b", - body: "second", - }); - await sendChatMessage({ - room: room.name, - authorAgentId: "agent-a", - body: "third", - }); - await sendChatMessage({ - room: otherRoom.name, - authorAgentId: "unrelated-agent", - body: "different room", - }); - - await expect(service.listRoomPosterAgentIds({ room: room.name })).resolves.toEqual([ - "agent-a", - "agent-b", - ]); - }); - - test("waits for new messages after a cursor and times out with an empty result", async () => { - const room = await service.createRoom({ name: "loop-status" }); - const first = await sendChatMessage({ - room: room.name, - authorAgentId: "agent-a", - body: "ready", - }); - - const waitPromise = service.waitForMessages({ - room: room.name, - afterMessageId: first.id, - timeoutMs: 1000, - }); - await sendChatMessage({ - room: room.name, - authorAgentId: "agent-b", - body: "new work", - }); - - const waited = await waitPromise; - expect(waited).toHaveLength(1); - expect(waited[0]?.body).toBe("new work"); - - const timedOut = await service.waitForMessages({ - room: room.name, - afterMessageId: waited[0]?.id, - timeoutMs: 10, - }); - expect(timedOut).toEqual([]); - }); - - test("deletes rooms, removes messages, and rejects pending waiters", async () => { - const room = await service.createRoom({ name: "schedule-jobs" }); - await sendChatMessage({ - room: room.name, - authorAgentId: "agent-a", - body: "hello", - }); - - const waitPromise = service.waitForMessages({ - room: room.name, - timeoutMs: 1000, - }); - const deleted = await service.deleteRoom({ room: room.id }); - expect(deleted.room.messageCount).toBe(1); - - await expect(waitPromise).rejects.toMatchObject<Partial<ChatServiceError>>({ - code: "chat_room_deleted", - }); - await expect(service.inspectRoom({ room: room.name })).rejects.toMatchObject< - Partial<ChatServiceError> - >({ - code: "chat_room_not_found", - }); - }); - - test("extracts inline mentions from chat bodies", () => { - expect( - parseMentionAgentIds( - "Checking with @agent-a, (@agent_b), @everyone, and duplicate @agent-a again.", - ), - ).toEqual(["agent-a", "agent_b", "everyone"]); - expect(parseMentionAgentIds("email@example.com is not a mention")).toEqual([]); - }); -}); diff --git a/packages/server/src/server/chat/chat-service.ts b/packages/server/src/server/chat/chat-service.ts deleted file mode 100644 index 08706d5ee8..0000000000 --- a/packages/server/src/server/chat/chat-service.ts +++ /dev/null @@ -1,465 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { promises as fs } from "node:fs"; -import path from "node:path"; -import type pino from "pino"; -import { z } from "zod"; -import { writeJsonFileAtomic } from "../atomic-file.js"; -import { - ChatMessageSchema, - ChatRoomDetailSchema, - ChatRoomSchema, - type ChatMessage, - type ChatRoom, - type ChatRoomDetail, -} from "@getpaseo/protocol/chat/types"; - -const ChatStorePayloadSchema = z.object({ - rooms: z.array(ChatRoomSchema), - messages: z.array(ChatMessageSchema), -}); - -type ChatStorePayload = z.infer<typeof ChatStorePayloadSchema>; - -function normalizeRoomName(name: string): string { - return name.trim().toLocaleLowerCase(); -} - -function trimToNull(value: string | null | undefined): string | null { - if (typeof value !== "string") { - return null; - } - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -const CHAT_MENTION_PATTERN = /(?:^|[\s(])@([A-Za-z0-9][A-Za-z0-9._-]*)/g; - -export function parseMentionAgentIds(body: string): string[] { - const mentionAgentIds = new Set<string>(); - for (const match of body.matchAll(CHAT_MENTION_PATTERN)) { - const agentId = match[1]?.trim(); - if (agentId) { - mentionAgentIds.add(agentId); - } - } - return Array.from(mentionAgentIds).sort(); -} - -export class ChatServiceError extends Error { - readonly code: string; - - constructor(code: string, message: string) { - super(message); - this.name = "ChatServiceError"; - this.code = code; - } -} - -interface Waiter { - roomId: string; - afterMessageId: string | null; - resolve: (messages: ChatMessage[]) => void; - reject: (error: Error) => void; - timeout: ReturnType<typeof setTimeout> | null; -} - -export interface CreateChatRoomInput { - name: string; - purpose?: string | null; -} - -export interface InspectChatRoomInput { - room: string; -} - -export interface DeleteChatRoomInput { - room: string; -} - -export interface PostChatMessageInput { - room: string; - authorAgentId: string; - body: string; - replyToMessageId?: string | null; -} - -export interface ReadChatMessagesInput { - room: string; - limit?: number; - since?: string; - authorAgentId?: string; -} - -export interface ListChatRoomPosterAgentIdsInput { - room: string; -} - -export interface WaitForChatMessagesInput { - room: string; - afterMessageId?: string | null; - timeoutMs?: number; -} - -export interface DeleteChatRoomResult { - room: ChatRoomDetail; -} - -export interface InspectChatRoomResult { - room: ChatRoomDetail; -} - -export class FileBackedChatService { - private readonly filePath: string; - private readonly logger: pino.Logger; - private loaded = false; - private readonly rooms = new Map<string, ChatRoom>(); - private readonly messagesByRoomId = new Map<string, ChatMessage[]>(); - private persistQueue: Promise<void> = Promise.resolve(); - private readonly waitersByRoomId = new Map<string, Set<Waiter>>(); - - constructor(options: { paseoHome: string; logger: pino.Logger }) { - this.filePath = path.join(options.paseoHome, "chat", "rooms.json"); - this.logger = options.logger.child({ component: "chat-service" }); - } - - async initialize(): Promise<void> { - await this.load(); - } - - async createRoom(input: CreateChatRoomInput): Promise<ChatRoomDetail> { - await this.load(); - const name = input.name.trim(); - if (name.length === 0) { - throw new ChatServiceError("invalid_chat_room_name", "Chat room name is required"); - } - if (this.findRoomByName(name)) { - throw new ChatServiceError( - "chat_room_name_taken", - `Chat room already exists with name: ${name}`, - ); - } - - const now = new Date().toISOString(); - const room = ChatRoomSchema.parse({ - id: randomUUID(), - name, - purpose: trimToNull(input.purpose), - createdAt: now, - updatedAt: now, - }); - this.rooms.set(room.id, room); - await this.enqueuePersist(); - return this.toRoomDetail(room); - } - - async listRooms(): Promise<ChatRoomDetail[]> { - await this.load(); - return Array.from(this.rooms.values()) - .map((room) => this.toRoomDetail(room)) - .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)); - } - - async inspectRoom(input: InspectChatRoomInput): Promise<InspectChatRoomResult> { - await this.load(); - const room = this.resolveRoom(input.room); - return { - room: this.toRoomDetail(room), - }; - } - - async deleteRoom(input: DeleteChatRoomInput): Promise<DeleteChatRoomResult> { - await this.load(); - const room = this.resolveRoom(input.room); - const detail = this.toRoomDetail(room); - this.rooms.delete(room.id); - this.messagesByRoomId.delete(room.id); - await this.enqueuePersist(); - this.rejectWaiters( - room.id, - new ChatServiceError("chat_room_deleted", `Chat room deleted: ${room.name}`), - ); - return { room: detail }; - } - - async dispatchMessage(input: PostChatMessageInput): Promise<ChatMessage> { - await this.load(); - const room = this.resolveRoom(input.room); - const body = input.body.trim(); - if (body.length === 0) { - throw new ChatServiceError("invalid_chat_message", "Chat message body is required"); - } - const authorAgentId = input.authorAgentId.trim(); - if (authorAgentId.length === 0) { - throw new ChatServiceError("invalid_chat_author", "Chat message author is required"); - } - - const messages = this.getRoomMessages(room.id); - const replyToMessageId = trimToNull(input.replyToMessageId); - if (replyToMessageId) { - const replyTarget = messages.find((message) => message.id === replyToMessageId); - if (!replyTarget) { - throw new ChatServiceError( - "chat_message_not_found", - `Reply target not found: ${replyToMessageId}`, - ); - } - } - - const createdAt = new Date().toISOString(); - const message = ChatMessageSchema.parse({ - id: randomUUID(), - roomId: room.id, - authorAgentId, - body, - replyToMessageId, - mentionAgentIds: parseMentionAgentIds(body), - createdAt, - }); - - messages.push(message); - this.messagesByRoomId.set(room.id, messages); - this.rooms.set( - room.id, - ChatRoomSchema.parse({ - ...room, - updatedAt: createdAt, - }), - ); - await this.enqueuePersist(); - this.notifyWaiters(room.id); - return message; - } - - async readMessages(input: ReadChatMessagesInput): Promise<ChatMessage[]> { - await this.load(); - const room = this.resolveRoom(input.room); - const messages = [...this.getRoomMessages(room.id)]; - const since = trimToNull(input.since); - const authorAgentId = trimToNull(input.authorAgentId); - const limit = this.normalizeLimit(input.limit); - - const filtered = messages.filter((message) => { - if (since && message.createdAt < since) { - return false; - } - if (authorAgentId && message.authorAgentId !== authorAgentId) { - return false; - } - return true; - }); - - if (limit === 0 || filtered.length <= limit) { - return filtered; - } - return filtered.slice(filtered.length - limit); - } - - async listRoomPosterAgentIds(input: ListChatRoomPosterAgentIdsInput): Promise<string[]> { - await this.load(); - const room = this.resolveRoom(input.room); - const posters = new Set<string>(); - for (const message of this.getRoomMessages(room.id)) { - posters.add(message.authorAgentId); - } - return Array.from(posters); - } - - async waitForMessages(input: WaitForChatMessagesInput): Promise<ChatMessage[]> { - await this.load(); - const room = this.resolveRoom(input.room); - const timeoutMs = Math.max(0, Math.floor(input.timeoutMs ?? 0)); - const afterMessageId = trimToNull(input.afterMessageId); - - if (afterMessageId) { - const existing = this.selectMessagesAfter(room.id, afterMessageId); - if (existing.length > 0) { - return existing; - } - const knownMessage = this.getRoomMessages(room.id).some( - (message) => message.id === afterMessageId, - ); - if (!knownMessage) { - throw new ChatServiceError( - "chat_message_not_found", - `Wait cursor not found: ${afterMessageId}`, - ); - } - } - - return new Promise<ChatMessage[]>((resolve, reject) => { - const waiter: Waiter = { - roomId: room.id, - afterMessageId, - resolve: (messages) => { - if (waiter.timeout) { - clearTimeout(waiter.timeout); - waiter.timeout = null; - } - this.removeWaiter(waiter); - resolve(messages); - }, - reject: (error) => { - if (waiter.timeout) { - clearTimeout(waiter.timeout); - waiter.timeout = null; - } - this.removeWaiter(waiter); - reject(error); - }, - timeout: null, - }; - - if (timeoutMs > 0) { - waiter.timeout = setTimeout(() => { - waiter.resolve([]); - }, timeoutMs); - } - - const roomWaiters = this.waitersByRoomId.get(room.id) ?? new Set<Waiter>(); - roomWaiters.add(waiter); - this.waitersByRoomId.set(room.id, roomWaiters); - }); - } - - private async load(): Promise<void> { - if (this.loaded) { - return; - } - - this.rooms.clear(); - this.messagesByRoomId.clear(); - - try { - const raw = await fs.readFile(this.filePath, "utf8"); - const parsed = ChatStorePayloadSchema.parse(JSON.parse(raw)); - for (const room of parsed.rooms) { - this.rooms.set(room.id, room); - } - for (const message of parsed.messages) { - const messages = this.messagesByRoomId.get(message.roomId) ?? []; - messages.push(message); - this.messagesByRoomId.set(message.roomId, messages); - } - } catch (error) { - const code = (error as NodeJS.ErrnoException).code; - if (code !== "ENOENT") { - this.logger.error({ err: error, filePath: this.filePath }, "Failed to load chat store"); - } - } - - this.loaded = true; - } - - private async enqueuePersist(): Promise<void> { - const nextPersist = this.persistQueue.then(() => this.persist()); - this.persistQueue = nextPersist.catch(() => {}); - await nextPersist; - } - - private async persist(): Promise<void> { - const payload: ChatStorePayload = { - rooms: Array.from(this.rooms.values()).sort((left, right) => - left.createdAt.localeCompare(right.createdAt), - ), - messages: Array.from(this.messagesByRoomId.values()) - .flat() - .sort((left, right) => left.createdAt.localeCompare(right.createdAt)), - }; - await writeJsonFileAtomic(this.filePath, payload); - } - - private findRoomByName(name: string): ChatRoom | null { - const normalizedName = normalizeRoomName(name); - for (const room of this.rooms.values()) { - if (normalizeRoomName(room.name) === normalizedName) { - return room; - } - } - return null; - } - - private resolveRoom(roomSelector: string): ChatRoom { - const selector = roomSelector.trim(); - if (selector.length === 0) { - throw new ChatServiceError("invalid_chat_room", "Chat room name or ID is required"); - } - const byId = this.rooms.get(selector); - if (byId) { - return byId; - } - const byName = this.findRoomByName(selector); - if (byName) { - return byName; - } - throw new ChatServiceError("chat_room_not_found", `Chat room not found: ${selector}`); - } - - private getRoomMessages(roomId: string): ChatMessage[] { - return this.messagesByRoomId.get(roomId) ?? []; - } - - private toRoomDetail(room: ChatRoom): ChatRoomDetail { - const messages = this.getRoomMessages(room.id); - return ChatRoomDetailSchema.parse({ - ...room, - messageCount: messages.length, - lastMessageAt: messages[messages.length - 1]?.createdAt ?? null, - }); - } - - private normalizeLimit(limit: number | undefined): number { - if (limit === undefined) { - return 20; - } - const normalized = Math.max(0, Math.floor(limit)); - return normalized; - } - - private selectMessagesAfter(roomId: string, afterMessageId: string): ChatMessage[] { - const messages = this.getRoomMessages(roomId); - const index = messages.findIndex((message) => message.id === afterMessageId); - if (index === -1) { - return []; - } - return messages.slice(index + 1); - } - - private notifyWaiters(roomId: string): void { - const waiters = this.waitersByRoomId.get(roomId); - if (!waiters || waiters.size === 0) { - return; - } - - for (const waiter of Array.from(waiters)) { - const messages = - waiter.afterMessageId === null - ? this.getRoomMessages(roomId).slice(-1) - : this.selectMessagesAfter(roomId, waiter.afterMessageId); - if (messages.length === 0) { - continue; - } - waiter.resolve(messages); - } - } - - private removeWaiter(waiter: Waiter): void { - const waiters = this.waitersByRoomId.get(waiter.roomId); - if (!waiters) { - return; - } - waiters.delete(waiter); - if (waiters.size === 0) { - this.waitersByRoomId.delete(waiter.roomId); - } - } - - private rejectWaiters(roomId: string, error: Error): void { - const waiters = this.waitersByRoomId.get(roomId); - if (!waiters) { - return; - } - for (const waiter of Array.from(waiters)) { - waiter.reject(error); - } - } -} diff --git a/packages/server/src/server/client-activity.e2e.test.ts b/packages/server/src/server/client-activity.e2e.test.ts index eaf5ae0541..5348ea44d8 100644 --- a/packages/server/src/server/client-activity.e2e.test.ts +++ b/packages/server/src/server/client-activity.e2e.test.ts @@ -3,7 +3,7 @@ import { createTestPaseoDaemon, type TestPaseoDaemon } from "./test-utils/paseo- import { DaemonClient } from "./test-utils/daemon-client.js"; import type { AgentStreamEventPayload } from "@getpaseo/protocol/messages"; import type { AgentSnapshotPayload } from "./messages.js"; -import type { PushNotificationSender, PushPayload } from "./push/notifications.js"; +import type { PushNotificationSender, PushPayload } from "./push/index.js"; import { PRESENCE_THRESHOLD_MS } from "./agent-attention-policy.js"; class RecordingPushNotificationSender implements PushNotificationSender { diff --git a/packages/server/src/server/config.test.ts b/packages/server/src/server/config.test.ts index d1315eeaa4..8ca42657d3 100644 --- a/packages/server/src/server/config.test.ts +++ b/packages/server/src/server/config.test.ts @@ -136,7 +136,6 @@ describe("server config", () => { workspaceId: "configured-runtime", runtimeId: "fixture", cwd: source, - hostVisiblePath: source, materializedFreshContent: true, }); await expect( @@ -146,6 +145,19 @@ describe("server config", () => { expect(runtimeIds.has("configured-runtime")).toBe(false); }); + test("loads the provider catalog refresh timeout", async () => { + const paseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-config-provider-timeout-")); + roots.push(paseoHome); + await writeFile( + path.join(paseoHome, "config.json"), + JSON.stringify({ agents: { catalogRefreshTimeoutMs: 180_000 } }), + ); + + const config = loadConfig(paseoHome, { env: {} }); + + expect(config.providerCatalogRefreshTimeoutMs).toBe(180_000); + }); + test("resolves bundled web UI path from source-tree modules", () => { const root = path.parse(process.cwd()).root; expect( diff --git a/packages/server/src/server/config.ts b/packages/server/src/server/config.ts index 894e3e048f..42f2322c6e 100644 --- a/packages/server/src/server/config.ts +++ b/packages/server/src/server/config.ts @@ -439,6 +439,18 @@ function resolveBrowserToolsEnabled(persisted: ReturnType<typeof loadPersistedCo return persisted.daemon?.browserTools?.enabled ?? false; } +/** + * Both profile lists stay `undefined` when absent rather than defaulting to an + * empty array: for terminal profiles that is what selects the built-in + * defaults, so an empty array has to keep meaning "the user removed them all". + */ +function resolveProfileLists(persisted: ReturnType<typeof loadPersistedConfig>) { + return { + terminalProfiles: persisted.daemon?.terminalProfiles, + agentProfiles: persisted.daemon?.agentProfiles, + }; +} + function resolveStaticLoadConfigSettings( env: NodeJS.ProcessEnv, cli: CliConfigOverrides | undefined, @@ -451,7 +463,7 @@ function resolveStaticLoadConfigSettings( browserToolsEnabled: resolveBrowserToolsEnabled(persisted), autoArchiveAfterMerge: persisted.daemon?.autoArchiveAfterMerge ?? false, appendSystemPrompt: resolveAppendSystemPrompt(persisted), - terminalProfiles: persisted.daemon?.terminalProfiles, + ...resolveProfileLists(persisted), hostnames: mergeHostnames([ persisted.daemon?.hostnames, parseHostnamesEnv(env.PASEO_HOSTNAMES ?? env.PASEO_ALLOWED_HOSTS), @@ -484,6 +496,7 @@ export function loadConfig( autoArchiveAfterMerge, appendSystemPrompt, terminalProfiles, + agentProfiles, hostnames, trustedProxies, appBaseUrl, @@ -527,6 +540,7 @@ export function loadConfig( enableTerminalAgentHooks: persisted.daemon?.enableTerminalAgentHooks ?? false, appendSystemPrompt, terminalProfiles, + agentProfiles, mcpDebug: env.MCP_DEBUG === "1", isDev: resolvePaseoNodeEnv(env) === "development", agentStoragePath: path.join(paseoHome, "agents"), @@ -548,6 +562,7 @@ export function loadConfig( voiceLlmProviderExplicit: voiceLlm.providerExplicit, voiceLlmModel: voiceLlm.model, agentProviderSettings: extractAgentProviderSettings(providerOverrides), + providerCatalogRefreshTimeoutMs: persisted.agents?.catalogRefreshTimeoutMs, metadataGeneration: persisted.agents?.metadataGeneration, providerOverrides, log: resolveLogConfigFromEnv(env, persisted), diff --git a/packages/server/src/server/daemon-config-store.test.ts b/packages/server/src/server/daemon-config-store.test.ts index 316fe76b01..878ca16423 100644 --- a/packages/server/src/server/daemon-config-store.test.ts +++ b/packages/server/src/server/daemon-config-store.test.ts @@ -85,6 +85,76 @@ describe("DaemonConfigStore", () => { expect(loadPersistedConfig(paseoHome).daemon?.relay?.enabled).toBe(true); }); + test("patch round-trips agent profiles through the strictly-parsed persisted config", () => { + const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-")); + tempDirs.push(paseoHome); + const store = new DaemonConfigStore(paseoHome, { + relay: { enabled: false }, + mcp: { injectIntoAgents: false }, + browserTools: { enabled: false }, + providers: {}, + metadataGeneration: { providers: [] }, + autoArchiveAfterMerge: false, + enableTerminalAgentHooks: false, + appendSystemPrompt: "", + }); + + store.patch({ + agentProfiles: [ + { + id: "profile_ui", + name: "UI work", + icon: "🎨", + provider: "claude", + model: "claude-opus-5", + modeId: "plan", + thinkingOptionId: "think-hard", + featureValues: { webSearch: true }, + notes: "Use for components, layout and design tokens.", + }, + ], + }); + + expect(loadPersistedConfig(paseoHome).daemon?.agentProfiles).toEqual([ + { + id: "profile_ui", + name: "UI work", + icon: "🎨", + provider: "claude", + model: "claude-opus-5", + modeId: "plan", + thinkingOptionId: "think-hard", + featureValues: { webSearch: true }, + notes: "Use for components, layout and design tokens.", + }, + ]); + expect(store.get().agentProfiles).toHaveLength(1); + }); + + test("patch replaces the whole agent profile list rather than merging entries", () => { + const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-")); + tempDirs.push(paseoHome); + const store = new DaemonConfigStore(paseoHome, { + relay: { enabled: false }, + mcp: { injectIntoAgents: false }, + browserTools: { enabled: false }, + providers: {}, + metadataGeneration: { providers: [] }, + autoArchiveAfterMerge: false, + enableTerminalAgentHooks: false, + appendSystemPrompt: "", + agentProfiles: [ + { id: "a", name: "Keep", provider: "claude" }, + { id: "b", name: "Drop", provider: "codex" }, + ], + }); + + store.patch({ agentProfiles: [{ id: "a", name: "Keep", provider: "claude" }] }); + + expect(store.get().agentProfiles).toEqual([{ id: "a", name: "Keep", provider: "claude" }]); + expect(loadPersistedConfig(paseoHome).daemon?.agentProfiles).toHaveLength(1); + }); + test("rolls back config when a field transition fails", () => { const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-")); tempDirs.push(paseoHome); diff --git a/packages/server/src/server/daemon-config-store.ts b/packages/server/src/server/daemon-config-store.ts index f77fccc06e..9ed029e088 100644 --- a/packages/server/src/server/daemon-config-store.ts +++ b/packages/server/src/server/daemon-config-store.ts @@ -359,6 +359,7 @@ function mergeMutableConfigIntoPersistedConfig(params: { ...(mutable.terminalProfiles !== undefined ? { terminalProfiles: mutable.terminalProfiles } : {}), + ...(mutable.agentProfiles !== undefined ? { agentProfiles: mutable.agentProfiles } : {}), }, agents: nextAgents, } as PersistedConfig; diff --git a/packages/server/src/server/daemon-worker.ts b/packages/server/src/server/daemon-worker.ts index 4188e97d2c..4656523ef7 100644 --- a/packages/server/src/server/daemon-worker.ts +++ b/packages/server/src/server/daemon-worker.ts @@ -27,10 +27,6 @@ interface SupervisorHeartbeatMessage { type: "paseo:supervisor-heartbeat"; } -interface WorkerHeartbeatMessage { - type: "paseo:worker-heartbeat"; -} - interface BootstrapResult { paseoHome: string; logger: ReturnType<typeof createRootLogger>; @@ -232,13 +228,6 @@ async function main() { const supervisorPid = process.ppid; let lastSupervisorHeartbeatAt = Date.now(); let supervisorExitRequested = false; - const sendWorkerHeartbeat = () => { - try { - process.send?.({ type: "paseo:worker-heartbeat" } satisfies WorkerHeartbeatMessage); - } catch { - // The disconnect handler below owns supervisor-loss shutdown. - } - }; const exitAfterSupervisorLoss = (reason: string) => { if (supervisorExitRequested) { return; @@ -272,10 +261,6 @@ async function main() { }); process.on("disconnect", () => exitAfterSupervisorLoss("ipc_disconnect_event")); - sendWorkerHeartbeat(); - const workerHeartbeat = setInterval(sendWorkerHeartbeat, 1_000); - workerHeartbeat.unref(); - const timer = setInterval(() => { const ipcConnected = typeof process.connected === "boolean" ? process.connected : true; const heartbeatExpired = Date.now() - lastSupervisorHeartbeatAt > 3500; diff --git a/packages/server/src/server/directory-sync/index.test.ts b/packages/server/src/server/directory-sync/index.test.ts new file mode 100644 index 0000000000..0d41d449ac --- /dev/null +++ b/packages/server/src/server/directory-sync/index.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, test } from "vitest"; +import { DirectorySyncService } from "./index.js"; + +function project(projectId: string, name: string) { + return { + projectId, + projectDisplayName: name, + projectRootPath: `/${projectId}`, + projectKind: "git" as const, + }; +} + +describe("DirectorySyncService", () => { + test("owns sequencing and returns wire-ready latest project changes", () => { + const service = new DirectorySyncService("generation"); + expect(service.synchronizeProjects([project("one", "One")], {}).sync).toMatchObject({ + mode: "snapshot", + headSeq: 1, + }); + + service.sequenceProjectUpdate({ kind: "upsert", project: project("one", "Renamed") }, true); + service.sequenceProjectUpdate({ kind: "upsert", project: project("one", "Latest") }, true); + expect( + service.synchronizeProjects([project("one", "Latest")], { + generation: "generation", + afterSeq: 1, + }), + ).toEqual({ + projects: [{ ...project("one", "Latest"), syncSeq: 3 }], + sync: { generation: "generation", mode: "changes", headSeq: 3, removals: [] }, + }); + }); + + test("sequences a hard project removal once", () => { + const service = new DirectorySyncService("generation"); + service.synchronizeProjects([project("one", "One")], {}); + const update = { kind: "remove" as const, projectId: "one" }; + expect(service.sequenceProjectUpdate(update, true)).toEqual({ + ...update, + generation: "generation", + seq: 2, + }); + expect(service.sequenceProjectUpdate(update, true)).toEqual({ + ...update, + generation: "generation", + seq: 2, + }); + expect( + service.synchronizeProjects([], { generation: "generation", afterSeq: 1 }).sync.removals, + ).toEqual([{ id: "one", seq: 2 }]); + }); + + test("keeps one daemon-wide version for repeated observations", () => { + const service = new DirectorySyncService("generation"); + const update = { kind: "upsert" as const, project: project("one", "One") }; + const sequenced = { ...update, generation: "generation", seq: 1 }; + expect(service.sequenceProjectUpdate(update, true)).toEqual(sequenced); + expect(service.sequenceProjectUpdate(update, true)).toEqual(sequenced); + }); + + test("sequences only the project whose effective icon changed", () => { + const service = new DirectorySyncService("generation"); + const one = { ...project("one", "One"), projectIconRevision: "icon-a" }; + const two = { ...project("two", "Two"), projectIconRevision: "icon-a" }; + service.synchronizeProjects([one, two], {}); + const changed = { ...one, projectIconRevision: "icon-b" }; + service.sequenceProjectUpdate({ kind: "upsert", project: changed }, true); + + expect( + service.synchronizeProjects([changed, two], { + generation: "generation", + afterSeq: 2, + }), + ).toEqual({ + projects: [{ ...changed, syncSeq: 3 }], + sync: { generation: "generation", mode: "changes", headSeq: 3, removals: [] }, + }); + }); + + test("falls back to a snapshot when the daemon generation changes", () => { + const service = new DirectorySyncService("new-generation"); + const read = service.synchronizeProjects([project("one", "One")], { + generation: "old-generation", + afterSeq: 10, + }); + expect(read.sync).toMatchObject({ + mode: "snapshot", + reason: "generation_changed", + headSeq: 1, + }); + }); +}); diff --git a/packages/server/src/server/directory-sync/index.ts b/packages/server/src/server/directory-sync/index.ts new file mode 100644 index 0000000000..1b08360cb7 --- /dev/null +++ b/packages/server/src/server/directory-sync/index.ts @@ -0,0 +1,174 @@ +import { randomUUID } from "node:crypto"; +import type { + AgentSnapshotPayload, + ProjectPlacementPayload, + SessionOutboundMessage, + WorkspaceDescriptorPayload, + WorkspaceProjectDescriptorPayload, +} from "@getpaseo/protocol/messages"; +import { type CollectionRead, VersionedCollection } from "./internal/versioned-collection.js"; + +interface AgentDirectoryEntry { + agent: AgentSnapshotPayload; + project: ProjectPlacementPayload; +} + +interface DirectorySyncCursor { + generation?: string; + afterSeq?: number; +} + +interface DirectoryVersion { + generation: string; + seq: number; +} + +type ProjectUpdate = Extract<SessionOutboundMessage, { type: "project.update" }>; +type ProjectListResponse = Extract< + SessionOutboundMessage, + { type: "project.list.response" } +>["payload"]; +type FetchWorkspacesResponse = Extract< + SessionOutboundMessage, + { type: "fetch_workspaces_response" } +>["payload"]; +type FetchAgentsResponse = Extract< + SessionOutboundMessage, + { type: "fetch_agents_response" } +>["payload"]; + +/** Daemon-global latest-state sequence owner for the active app directory. */ +export class DirectorySyncService { + private readonly generation: string; + private readonly projects = new VersionedCollection<WorkspaceProjectDescriptorPayload>({ + getId: (project) => project.projectId, + }); + private readonly workspaces = new VersionedCollection<WorkspaceDescriptorPayload>({ + getId: (workspace) => workspace.id, + }); + private readonly agents = new VersionedCollection<AgentDirectoryEntry>({ + getId: (entry) => entry.agent.id, + }); + + constructor(generation = randomUUID()) { + this.generation = generation; + } + + synchronizeProjects( + snapshot: Iterable<WorkspaceProjectDescriptorPayload>, + cursor: DirectorySyncCursor, + ): Pick<ProjectListResponse, "projects" | "sync"> { + this.projects.replaceAll(snapshot); + const read = this.read(this.projects, cursor); + return { + projects: read.values.map(({ seq, value }) => ({ ...value, syncSeq: seq })), + sync: this.metadata(read), + }; + } + + synchronizeWorkspaces( + snapshot: Iterable<WorkspaceDescriptorPayload>, + cursor: DirectorySyncCursor, + ): Pick<FetchWorkspacesResponse, "entries" | "emptyProjects" | "pageInfo" | "sync"> { + this.workspaces.replaceAll(snapshot); + const read = this.read(this.workspaces, cursor); + return { + entries: read.values.map(({ seq, value }) => ({ ...value, syncSeq: seq })), + emptyProjects: [], + pageInfo: { nextCursor: null, prevCursor: null, hasMore: false }, + sync: this.metadata(read), + }; + } + + synchronizeAgents( + snapshot: Iterable<AgentDirectoryEntry>, + cursor: DirectorySyncCursor, + ): Pick<FetchAgentsResponse, "entries" | "pageInfo" | "sync"> { + this.agents.replaceAll(snapshot); + const read = this.read(this.agents, cursor); + return { + entries: read.values.map(({ seq, value }) => ({ ...value, syncSeq: seq })), + pageInfo: { nextCursor: null, prevCursor: null, hasMore: false }, + sync: this.metadata(read), + }; + } + + sequenceProjectUpdate( + update: ProjectUpdate["payload"], + includeSequence: boolean, + ): ProjectUpdate["payload"] { + if (update.kind === "upsert") { + this.projects.replace(update.project); + } else { + this.projects.remove(update.projectId); + } + const id = update.kind === "upsert" ? update.project.projectId : update.projectId; + return this.withVersion(update, includeSequence, this.version(this.projects, id)); + } + + sequenceWorkspaceUpdate<T extends object>( + payload: T, + value: WorkspaceDescriptorPayload | null, + id: string, + includeSequence: boolean, + ): T & Partial<DirectoryVersion> { + if (value) { + this.workspaces.replace(value); + } else { + this.workspaces.remove(id); + } + return this.withVersion(payload, includeSequence, this.version(this.workspaces, id)); + } + + sequenceAgentUpdate<T extends object>( + payload: T, + value: AgentDirectoryEntry | null, + id: string, + includeSequence: boolean, + ): T & Partial<DirectoryVersion> { + if (value) { + this.agents.replace(value); + } else { + this.agents.remove(id); + } + return this.withVersion(payload, includeSequence, this.version(this.agents, id)); + } + + private metadata(read: CollectionRead<unknown>) { + return { + generation: this.generation, + headSeq: read.headSeq, + mode: read.mode, + ...(read.reason ? { reason: read.reason } : {}), + removals: read.removals, + }; + } + + private withVersion<T extends object>( + payload: T, + includeSequence: boolean, + version: DirectoryVersion | null, + ): T & Partial<DirectoryVersion> { + return version && includeSequence ? { ...payload, ...version } : payload; + } + + private read<T>( + collection: VersionedCollection<T>, + cursor: DirectorySyncCursor, + ): CollectionRead<T> { + if (cursor.generation !== this.generation) { + return collection.readSnapshot( + cursor.generation === undefined ? "no_cursor" : "generation_changed", + ); + } + if (cursor.afterSeq === undefined) { + return collection.readSnapshot("no_cursor"); + } + return collection.readAfter(cursor.afterSeq); + } + + private version<T>(collection: VersionedCollection<T>, id: string): DirectoryVersion | null { + const seq = collection.sequenceFor(id); + return seq === null ? null : { generation: this.generation, seq }; + } +} diff --git a/packages/server/src/server/directory-sync/internal/versioned-collection.test.ts b/packages/server/src/server/directory-sync/internal/versioned-collection.test.ts new file mode 100644 index 0000000000..f74cdd7874 --- /dev/null +++ b/packages/server/src/server/directory-sync/internal/versioned-collection.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, test } from "vitest"; +import { VersionedCollection } from "./versioned-collection.js"; + +describe("VersionedCollection", () => { + test("returns only each entity's latest visible snapshot after a sequence", () => { + const collection = new VersionedCollection<{ id: string; title: string }>({ + getId: (value) => value.id, + }); + collection.replace({ id: "one", title: "first" }); + const cursor = collection.headSeq; + collection.replace({ id: "one", title: "second" }); + collection.replace({ id: "one", title: "latest" }); + collection.replace({ id: "two", title: "other" }); + + expect(collection.readAfter(cursor)).toEqual({ + mode: "changes", + headSeq: 4, + values: [ + { seq: 3, value: { id: "one", title: "latest" } }, + { seq: 4, value: { id: "two", title: "other" } }, + ], + removals: [], + }); + }); + + test("does not advance for an unchanged projection or repeated removal", () => { + const collection = new VersionedCollection<{ id: string; title: string }>({ + getId: (value) => value.id, + }); + expect(collection.replace({ id: "one", title: "same" })?.seq).toBe(1); + expect(collection.replace({ id: "one", title: "same" })).toBeNull(); + expect(collection.remove("one")?.seq).toBe(2); + expect(collection.remove("one")).toBeNull(); + expect(collection.headSeq).toBe(2); + }); + + test("returns tombstones and lets a later value replace them", () => { + const collection = new VersionedCollection<{ id: string; title: string }>({ + getId: (value) => value.id, + }); + collection.replace({ id: "one", title: "before" }); + collection.remove("one"); + expect(collection.readAfter(1)).toEqual({ + mode: "changes", + headSeq: 2, + values: [], + removals: [{ id: "one", seq: 2 }], + }); + + collection.replace({ id: "one", title: "after" }); + expect(collection.readAfter(2)).toEqual({ + mode: "changes", + headSeq: 3, + values: [{ seq: 3, value: { id: "one", title: "after" } }], + removals: [], + }); + }); + + test("falls back to a latest snapshot when a required tombstone expired", () => { + const collection = new VersionedCollection<{ id: string }>({ + getId: (value) => value.id, + maxTombstones: 1, + }); + collection.replace({ id: "one" }); + collection.replace({ id: "two" }); + collection.remove("one"); + collection.remove("two"); + + expect(collection.readAfter(2)).toEqual({ + mode: "snapshot", + reason: "cursor_expired", + headSeq: 4, + values: [], + removals: [], + }); + }); +}); diff --git a/packages/server/src/server/directory-sync/internal/versioned-collection.ts b/packages/server/src/server/directory-sync/internal/versioned-collection.ts new file mode 100644 index 0000000000..e25bc958f2 --- /dev/null +++ b/packages/server/src/server/directory-sync/internal/versioned-collection.ts @@ -0,0 +1,141 @@ +export interface VersionedValue<T> { + seq: number; + value: T; +} + +export interface VersionedRemoval { + id: string; + seq: number; +} + +export interface CollectionRead<T> { + mode: "changes" | "snapshot"; + reason?: "no_cursor" | "generation_changed" | "cursor_expired"; + headSeq: number; + values: VersionedValue<T>[]; + removals: VersionedRemoval[]; +} + +export type PublishedChange<T> = + | { kind: "upsert"; seq: number; value: T } + | { kind: "remove"; seq: number; id: string }; + +interface StoredValue<T> extends VersionedValue<T> { + fingerprint: string; +} + +interface VersionedCollectionOptions<T> { + getId(value: T): string; + fingerprint?(value: T): string; + maxTombstones?: number; +} + +/** Owns latest-state sequencing for one directory entity type. */ +export class VersionedCollection<T> { + private readonly valuesById = new Map<string, StoredValue<T>>(); + private readonly tombstonesById = new Map<string, number>(); + private readonly getId: (value: T) => string; + private readonly fingerprint: (value: T) => string; + private readonly maxTombstones: number; + private expiredThroughSeq = 0; + private nextSeq = 0; + + constructor(options: VersionedCollectionOptions<T>) { + this.getId = options.getId; + this.fingerprint = options.fingerprint ?? ((value) => JSON.stringify(value)); + this.maxTombstones = options.maxTombstones ?? 5_000; + } + + get headSeq(): number { + return this.nextSeq; + } + + sequenceFor(id: string): number | null { + return this.valuesById.get(id)?.seq ?? this.tombstonesById.get(id) ?? null; + } + + replace(value: T): Extract<PublishedChange<T>, { kind: "upsert" }> | null { + const id = this.getId(value); + const fingerprint = this.fingerprint(value); + const previous = this.valuesById.get(id); + if (previous?.fingerprint === fingerprint) return null; + + const seq = this.advance(); + this.valuesById.set(id, { seq, fingerprint, value }); + this.tombstonesById.delete(id); + return { kind: "upsert", seq, value }; + } + + remove(id: string): Extract<PublishedChange<T>, { kind: "remove" }> | null { + if (!this.valuesById.has(id) && this.tombstonesById.has(id)) return null; + if (!this.valuesById.delete(id) && !this.tombstonesById.has(id)) return null; + + const seq = this.advance(); + this.tombstonesById.delete(id); + this.tombstonesById.set(id, seq); + this.trimTombstones(); + return { kind: "remove", seq, id }; + } + + replaceAll(values: Iterable<T>): void { + const retainedIds = new Set<string>(); + for (const value of values) { + const id = this.getId(value); + retainedIds.add(id); + this.replace(value); + } + for (const id of this.valuesById.keys()) { + if (!retainedIds.has(id)) this.remove(id); + } + } + + readSnapshot( + reason: "no_cursor" | "generation_changed" | "cursor_expired" = "no_cursor", + ): CollectionRead<T> { + return { + mode: "snapshot", + reason, + headSeq: this.headSeq, + values: this.sortedValues(), + removals: [], + }; + } + + readAfter(afterSeq: number): CollectionRead<T> { + if (afterSeq < this.expiredThroughSeq || afterSeq > this.headSeq) { + return this.readSnapshot("cursor_expired"); + } + return { + mode: "changes", + headSeq: this.headSeq, + values: this.sortedValues().filter((entry) => entry.seq > afterSeq), + removals: Array.from(this.tombstonesById, ([id, seq]) => ({ id, seq })) + .filter((entry) => entry.seq > afterSeq) + .sort((left, right) => left.seq - right.seq || left.id.localeCompare(right.id)), + }; + } + + private advance(): number { + this.nextSeq += 1; + return this.nextSeq; + } + + private sortedValues(): VersionedValue<T>[] { + return Array.from(this.valuesById.values()) + .map(({ seq, value }) => ({ seq, value })) + .sort( + (left, right) => + left.seq - right.seq || this.getId(left.value).localeCompare(this.getId(right.value)), + ); + } + + private trimTombstones(): void { + while (this.tombstonesById.size > this.maxTombstones) { + const oldest = this.tombstonesById.entries().next().value; + if (!oldest) return; + const [id, seq] = oldest; + this.tombstonesById.delete(id); + this.expiredThroughSeq = Math.max(this.expiredThroughSeq, seq); + } + } +} diff --git a/packages/server/src/server/file-explorer/service.test.ts b/packages/server/src/server/file-explorer/service.test.ts index 07f451e83c..a191a5fabf 100644 --- a/packages/server/src/server/file-explorer/service.test.ts +++ b/packages/server/src/server/file-explorer/service.test.ts @@ -1,10 +1,25 @@ -import { appendFile, chmod, mkdtemp, rm, stat, truncate, writeFile } from "node:fs/promises"; +import { + appendFile, + chmod, + mkdir, + mkdtemp, + readFile, + rm, + stat, + truncate, + writeFile, +} from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import { runGitCommand } from "../../utils/run-git-command.js"; import { + createExplorerEntry, + deleteExplorerEntry, + duplicateExplorerEntry, getExplorerFileVersion, readExplorerFile, + renameExplorerEntry, streamExplorerFile, writeExplorerFile, } from "./service.js"; @@ -373,4 +388,202 @@ describe("file explorer service", () => { await rm(root, { recursive: true, force: true }); } }); + + it("creates files and directories, refusing duplicates and separator names", async () => { + const root = await createTempDir("paseo-entry-create-"); + try { + const file = await createExplorerEntry({ + root, + parentPath: ".", + name: "notes.txt", + kind: "file", + }); + expect(file).toEqual({ status: "ok", path: "notes.txt" }); + expect((await stat(path.join(root, "notes.txt"))).isFile()).toBe(true); + + const dir = await createExplorerEntry({ + root, + parentPath: ".", + name: "docs", + kind: "directory", + }); + expect(dir).toEqual({ status: "ok", path: "docs" }); + const nested = await createExplorerEntry({ + root, + parentPath: "docs", + name: "guide.md", + kind: "file", + }); + expect(nested).toEqual({ status: "ok", path: "docs/guide.md" }); + + const duplicate = await createExplorerEntry({ + root, + parentPath: ".", + name: "notes.txt", + kind: "file", + }); + expect(duplicate.status).toBe("error"); + + const traversal = await createExplorerEntry({ + root, + parentPath: ".", + name: "../escape", + kind: "directory", + }); + expect(traversal.status).toBe("error"); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it("duplicates files and folders with collision-free sibling names", async () => { + const root = await createTempDir("paseo-entry-duplicate-"); + try { + await writeFile(path.join(root, "notes.txt"), "original", "utf8"); + const firstFileCopy = await duplicateExplorerEntry({ + root, + relativePath: "notes.txt", + }); + expect(firstFileCopy).toEqual({ status: "ok", path: "notes copy.txt" }); + expect(await readFile(path.join(root, "notes copy.txt"), "utf8")).toBe("original"); + + const secondFileCopy = await duplicateExplorerEntry({ + root, + relativePath: "notes.txt", + }); + expect(secondFileCopy).toEqual({ status: "ok", path: "notes copy 2.txt" }); + + await mkdir(path.join(root, "docs")); + await writeFile(path.join(root, "docs", "guide.md"), "guide", "utf8"); + const folderCopy = await duplicateExplorerEntry({ root, relativePath: "docs" }); + expect(folderCopy).toEqual({ status: "ok", path: "docs copy" }); + expect(await readFile(path.join(root, "docs copy", "guide.md"), "utf8")).toBe("guide"); + + await expect(duplicateExplorerEntry({ root, relativePath: "." })).resolves.toMatchObject({ + status: "error", + }); + await expect(duplicateExplorerEntry({ root, relativePath: "missing.txt" })).resolves.toEqual({ + status: "error", + error: "File or folder no longer exists", + }); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it("renames tracked entries with Git and untracked entries on the filesystem", async () => { + const root = await createTempDir("paseo-entry-rename-"); + try { + await runGitCommand(["init"], { cwd: root }); + await writeFile(path.join(root, "tracked.txt"), "tracked", "utf8"); + await mkdir(path.join(root, "tracked-folder")); + await writeFile(path.join(root, "tracked-folder", "inside.txt"), "tracked", "utf8"); + await runGitCommand(["add", "."], { cwd: root }); + await runGitCommand( + ["-c", "user.name=Paseo Test", "-c", "user.email=test@paseo.local", "commit", "-m", "base"], + { cwd: root }, + ); + + const tracked = await renameExplorerEntry({ + root, + relativePath: "tracked.txt", + name: "renamed.txt", + }); + expect(tracked).toEqual({ status: "ok", path: "renamed.txt" }); + expect((await runGitCommand(["status", "--short"], { cwd: root })).stdout.trim()).toBe( + "R tracked.txt -> renamed.txt", + ); + + const trackedFolder = await renameExplorerEntry({ + root, + relativePath: "tracked-folder", + name: "renamed-folder", + }); + expect(trackedFolder).toEqual({ status: "ok", path: "renamed-folder" }); + const gitStatus = (await runGitCommand(["status", "--short"], { cwd: root })).stdout; + expect(gitStatus).toContain("tracked-folder/inside.txt -> renamed-folder/inside.txt"); + + await writeFile(path.join(root, "untracked.txt"), "untracked", "utf8"); + const untracked = await renameExplorerEntry({ + root, + relativePath: "untracked.txt", + name: "moved.txt", + }); + expect(untracked).toEqual({ status: "ok", path: "moved.txt" }); + expect((await stat(path.join(root, "moved.txt"))).isFile()).toBe(true); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it("supports case-only renames for tracked and untracked files", async () => { + const root = await createTempDir("paseo-entry-case-rename-"); + try { + await runGitCommand(["init"], { cwd: root }); + await writeFile(path.join(root, "Tracked.txt"), "tracked", "utf8"); + await writeFile(path.join(root, "Loose.txt"), "untracked", "utf8"); + await runGitCommand(["add", "Tracked.txt"], { cwd: root }); + await runGitCommand( + ["-c", "user.name=Paseo Test", "-c", "user.email=test@paseo.local", "commit", "-m", "base"], + { cwd: root }, + ); + + await expect( + renameExplorerEntry({ root, relativePath: "Tracked.txt", name: "tracked.txt" }), + ).resolves.toEqual({ status: "ok", path: "tracked.txt" }); + expect((await stat(path.join(root, "tracked.txt"))).isFile()).toBe(true); + + await expect( + renameExplorerEntry({ root, relativePath: "Loose.txt", name: "loose.txt" }), + ).resolves.toEqual({ status: "ok", path: "loose.txt" }); + expect((await stat(path.join(root, "loose.txt"))).isFile()).toBe(true); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it("refuses invalid renames, existing targets, and the workspace root", async () => { + const root = await createTempDir("paseo-entry-rename-errors-"); + try { + await writeFile(path.join(root, "source.txt"), "source", "utf8"); + await writeFile(path.join(root, "existing.txt"), "existing", "utf8"); + + await expect( + renameExplorerEntry({ root, relativePath: "source.txt", name: "existing.txt" }), + ).resolves.toEqual({ status: "error", error: '"existing.txt" already exists' }); + await expect( + renameExplorerEntry({ root, relativePath: "source.txt", name: "../outside.txt" }), + ).resolves.toEqual({ status: "error", error: "Name cannot contain path separators" }); + await expect( + renameExplorerEntry({ root, relativePath: ".", name: "renamed-root" }), + ).resolves.toEqual({ status: "error", error: "Cannot rename the workspace root" }); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it("deletes files and directories but never the workspace root or outside paths", async () => { + const root = await createTempDir("paseo-entry-delete-"); + try { + await writeFile(path.join(root, "doomed.txt"), "bye", "utf8"); + const removedFile = await deleteExplorerEntry({ root, relativePath: "doomed.txt" }); + expect(removedFile).toEqual({ status: "ok", path: "doomed.txt" }); + await expect(stat(path.join(root, "doomed.txt"))).rejects.toThrow(); + + await createExplorerEntry({ root, parentPath: ".", name: "nested", kind: "directory" }); + await writeFile(path.join(root, "nested", "inner.txt"), "hi", "utf8"); + const removedDir = await deleteExplorerEntry({ root, relativePath: "nested" }); + expect(removedDir).toEqual({ status: "ok", path: "nested" }); + await expect(stat(path.join(root, "nested"))).rejects.toThrow(); + + const rootDelete = await deleteExplorerEntry({ root, relativePath: "." }); + expect(rootDelete.status).toBe("error"); + + await expect( + deleteExplorerEntry({ root, relativePath: "../outside" }), + ).resolves.toMatchObject({ status: "error" }); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); }); diff --git a/packages/server/src/server/file-explorer/service.ts b/packages/server/src/server/file-explorer/service.ts index a33007b596..dbc319c10b 100644 --- a/packages/server/src/server/file-explorer/service.ts +++ b/packages/server/src/server/file-explorer/service.ts @@ -3,6 +3,7 @@ import type { FileHandle } from "fs/promises"; import path from "path"; import { randomUUID } from "crypto"; import { expandUserPath, resolvePathFromBase } from "../path-utils.js"; +import { runGitCommand } from "../../utils/run-git-command.js"; export type ExplorerEntryKind = "file" | "directory"; export type ExplorerFileKind = "text" | "image" | "binary"; @@ -572,6 +573,215 @@ export async function getDownloadableFileInfo({ root, relativePath }: ReadFilePa } } +export interface ExplorerCreateEntryParams { + root: string; + parentPath: string; + name: string; + kind: ExplorerEntryKind; +} + +export interface ExplorerRenameEntryParams { + root: string; + relativePath: string; + name: string; +} + +export type ExplorerEntryMutationResult = + | { status: "ok"; path: string } + | { status: "error"; error: string }; + +export async function createExplorerEntry({ + root, + parentPath, + name, + kind, +}: ExplorerCreateEntryParams): Promise<ExplorerEntryMutationResult> { + const trimmedName = name.trim(); + if (!trimmedName || trimmedName === "." || trimmedName === "..") { + return { status: "error", error: "Invalid name" }; + } + if (trimmedName.includes("/") || trimmedName.includes("\\")) { + return { status: "error", error: "Name cannot contain path separators" }; + } + + try { + const parent = await resolveScopedPath({ root, relativePath: parentPath }); + const parentStats = await fs.stat(parent.resolvedPath); + if (!parentStats.isDirectory()) { + return { status: "error", error: "Parent path is not a directory" }; + } + const targetPath = path.join(parent.resolvedPath, trimmedName); + if (kind === "directory") { + await fs.mkdir(targetPath); + } else { + const handle = await fs.open(targetPath, "wx", 0o644); + await handle.close(); + } + return { + status: "ok", + path: normalizeRelativePath({ + root, + targetPath: path.join(parent.requestedPath, trimmedName), + }), + }; + } catch (error) { + if (isEntryExistsError(error)) { + return { status: "error", error: `"${trimmedName}" already exists` }; + } + return { status: "error", error: error instanceof Error ? error.message : String(error) }; + } +} + +export async function duplicateExplorerEntry({ + root, + relativePath, +}: ReadFileParams): Promise<ExplorerEntryMutationResult> { + try { + const source = await resolveScopedPath({ root, relativePath }); + const realRoot = await fs.realpath(expandUserPath(root)); + if (source.resolvedPath === realRoot) { + return { status: "error", error: "Cannot duplicate the workspace root" }; + } + + const stats = await fs.lstat(source.requestedPath); + const sourceName = path.basename(source.requestedPath); + const extension = stats.isDirectory() ? "" : path.extname(sourceName); + const baseName = extension ? sourceName.slice(0, -extension.length) : sourceName; + let targetPath = ""; + for (let copyNumber = 1; ; copyNumber += 1) { + const suffix = copyNumber === 1 ? " copy" : ` copy ${copyNumber}`; + targetPath = path.join( + path.dirname(source.requestedPath), + `${baseName}${suffix}${extension}`, + ); + try { + await fs.lstat(targetPath); + } catch (error) { + if (isMissingEntryError(error)) { + break; + } + throw error; + } + } + + await fs.cp(source.requestedPath, targetPath, { + recursive: stats.isDirectory(), + errorOnExist: true, + force: false, + preserveTimestamps: true, + }); + return { status: "ok", path: normalizeRelativePath({ root, targetPath }) }; + } catch (error) { + if (isMissingEntryError(error)) { + return { status: "error", error: "File or folder no longer exists" }; + } + return { status: "error", error: error instanceof Error ? error.message : String(error) }; + } +} + +export async function renameExplorerEntry({ + root, + relativePath, + name, +}: ExplorerRenameEntryParams): Promise<ExplorerEntryMutationResult> { + const trimmedName = name.trim(); + if (!trimmedName || trimmedName === "." || trimmedName === "..") { + return { status: "error", error: "Invalid name" }; + } + if (trimmedName.includes("/") || trimmedName.includes("\\")) { + return { status: "error", error: "Name cannot contain path separators" }; + } + + try { + const source = await resolveScopedPath({ root, relativePath }); + const realRoot = await fs.realpath(expandUserPath(root)); + if (source.resolvedPath === realRoot) { + return { status: "error", error: "Cannot rename the workspace root" }; + } + + const targetPath = path.join(path.dirname(source.requestedPath), trimmedName); + const sourceStats = await fs.lstat(source.requestedPath); + const targetStats = await fs.lstat(targetPath).catch((error: unknown) => { + if (isMissingEntryError(error)) { + return null; + } + throw error; + }); + if ( + targetStats && + (targetStats.dev !== sourceStats.dev || targetStats.ino !== sourceStats.ino) + ) { + return { status: "error", error: `"${trimmedName}" already exists` }; + } + + const sourcePath = normalizeRelativePath({ root, targetPath: source.requestedPath }); + const renamedPath = normalizeRelativePath({ root, targetPath }); + if (sourcePath === renamedPath) { + return { status: "ok", path: renamedPath }; + } + const repository = await runGitCommand(["rev-parse", "--is-inside-work-tree"], { + cwd: realRoot, + acceptExitCodes: [0, 128], + }); + const tracked = + repository.exitCode === 0 + ? await runGitCommand(["ls-files", "-z", "--", sourcePath], { cwd: realRoot }) + : null; + if (tracked?.stdout) { + await runGitCommand(["mv", "--", sourcePath, renamedPath], { cwd: realRoot }); + } else { + await fs.rename(source.requestedPath, targetPath); + } + return { status: "ok", path: renamedPath }; + } catch (error) { + if (isEntryExistsError(error)) { + return { status: "error", error: `"${trimmedName}" already exists` }; + } + if (isMissingEntryError(error)) { + return { status: "error", error: "File or folder no longer exists" }; + } + return { status: "error", error: error instanceof Error ? error.message : String(error) }; + } +} + +export async function deleteExplorerEntry({ + root, + relativePath, +}: ReadFileParams): Promise<ExplorerEntryMutationResult> { + try { + const scoped = await resolveScopedPath({ root, relativePath }); + const realRoot = await fs.realpath(expandUserPath(root)); + if (scoped.resolvedPath === realRoot) { + return { status: "error", error: "Cannot delete the workspace root" }; + } + // Remove the requested path, not the realpath: deleting a symlink must + // remove the link, never its target. lstat keeps the same guarantee. + const stats = await fs.lstat(scoped.requestedPath); + await fs.rm(scoped.requestedPath, { + recursive: stats.isDirectory(), + force: false, + }); + return { + status: "ok", + path: normalizeRelativePath({ root, targetPath: scoped.requestedPath }), + }; + } catch (error) { + if (isMissingEntryError(error)) { + return { status: "error", error: "File or folder no longer exists" }; + } + return { status: "error", error: error instanceof Error ? error.message : String(error) }; + } +} + +function isEntryExistsError(error: unknown): boolean { + return ( + typeof error === "object" && + error !== null && + "code" in error && + (error as NodeJS.ErrnoException).code === "EEXIST" + ); +} + async function resolveScopedPath({ root, relativePath = ".", diff --git a/packages/server/src/server/file-observer/index.test.ts b/packages/server/src/server/file-observer/index.test.ts index 1123023211..0427cd823b 100644 --- a/packages/server/src/server/file-observer/index.test.ts +++ b/packages/server/src/server/file-observer/index.test.ts @@ -237,7 +237,7 @@ test("observes a thousand concurrent writes and remains healthy after delete and await Promise.all(paths.map((path, index) => writeFile(path, `${index}`))); await expect - .poll(() => paths.filter((path) => !observed.has(path)), { timeout: 10_000 }) + .poll(() => paths.filter((path) => !observed.has(path)), { timeout: 60_000 }) .toEqual([]); const removedPaths = paths.slice(0, 100); @@ -258,7 +258,7 @@ test("observes a thousand concurrent writes and remains healthy after delete and await writeFile(sentinel, "alive"); await expect.poll(() => observed.has(sentinel)).toBe(true); await subscription.unsubscribe(); -}, 30_000); +}, 90_000); test("unsubscribe cancels reconciliation queued by a write burst", async () => { const root = await createRoot(); diff --git a/packages/server/src/server/hub/execution-session.websocket.test.ts b/packages/server/src/server/hub/execution-session.websocket.test.ts index 589b82794b..6a14cbae2b 100644 --- a/packages/server/src/server/hub/execution-session.websocket.test.ts +++ b/packages/server/src/server/hub/execution-session.websocket.test.ts @@ -246,7 +246,7 @@ test("a sibling workspace keeps an archived execution's worktree directory alive expect(await hub.ownedAgentArchivedAt(created.payload.agentId!)).toEqual(expect.any(String)); }); -test("archiving an execution in a reused worktree leaves the existing workspace intact", async () => { +test("archiving a second same-slug execution leaves the first worktree intact", async () => { const hub = await launchRelationship(); const worktree = { mode: "branch-off" as const, @@ -267,6 +267,7 @@ test("archiving an execution in a reused worktree leaves the existing workspace prompt: "sleep 30", }); const reused = await hub.ownedCreateResult("reused-worktree-create"); + const secondWorktreeCwd = hub.latestCreatedCwd()!; const reusedWorkspaceId = reused.payload.agent?.workspaceId; await hub.ownedRunningUpdate(reused.payload.agentId!); @@ -278,8 +279,10 @@ test("archiving an execution in a reused worktree leaves the existing workspace expect(response).toMatchObject({ success: true, error: null }); expect(reusedWorkspaceId).toEqual(expect.any(String)); expect(reusedWorkspaceId).not.toBe(originalWorkspaceId); - expect(hub.pathsReferToSameLocation(reused.payload.agent!.cwd, worktreeCwd)).toBe(true); + expect(hub.pathsReferToSameLocation(reused.payload.agent!.cwd, worktreeCwd)).toBe(false); + expect(hub.pathsReferToSameLocation(reused.payload.agent!.cwd, secondWorktreeCwd)).toBe(true); expect(await hub.worktreeState(worktreeCwd)).toEqual({ exists: true, listed: true }); + expect(await hub.worktreeState(secondWorktreeCwd)).toEqual({ exists: false, listed: false }); expect(await hub.agentRemainsAvailable(original.payload.agentId!)).toBe(true); expect(await hub.ownedAgentArchivedAt(reused.payload.agentId!)).toEqual(expect.any(String)); expect(await hub.ownedWorkspaceArchivedAt(reused.payload.agentId!)).toEqual(expect.any(String)); diff --git a/packages/server/src/server/hub/relationship-controller.test.ts b/packages/server/src/server/hub/relationship-controller.test.ts index 78b4481ece..9f83a5a6a9 100644 --- a/packages/server/src/server/hub/relationship-controller.test.ts +++ b/packages/server/src/server/hub/relationship-controller.test.ts @@ -1,5 +1,5 @@ import { createHash } from "node:crypto"; -import { platform } from "node:os"; +import { hostname, platform } from "node:os"; import { afterEach, describe, expect, test } from "vitest"; import { HubRelationshipHarness } from "./test-utils/relationship-harness.js"; @@ -88,6 +88,7 @@ describe("Hub relationship", () => { .update(secret ?? "") .digest("base64url"), ); + expect(enrollment.hostname).toBe(hostname()); relationship.completeEnrollment(); await connecting.result; await relationship.socketDialed(); @@ -343,16 +344,18 @@ describe("Hub relationship", () => { const credential = relationship.relationshipFile()?.credential?.secret; relationship.loseEnrollmentResponse(); await connecting.result; - relationship.failRevocations(2); + relationship.failRevocations(1); const disconnected = await relationship.disconnect(); - expect(disconnected.state).toBe("disconnecting"); - expect(relationship.relationshipFile()?.state).toBe("disconnecting"); + const reconnected = await relationship.beginConnect("fresh-token").result; - await relationship.restartDaemon(); - await relationship.retry(); - - expect(relationship.revocationAttempts()).toBe(3); + expect(disconnected).toMatchObject({ + state: "not_connected", + hub: null, + warning: expect.stringContaining("server-side revocation may remain pending"), + }); + expect(reconnected.state).toBe("connecting"); + expect(relationship.revocationAttempts()).toBe(1); expect(relationship.latestRevocation()).toEqual( expect.objectContaining({ daemonId: enrollment.daemonId, @@ -360,7 +363,10 @@ describe("Hub relationship", () => { credential, }), ); - expect(relationship.relationshipFile()).toBeNull(); + expect(relationship.pendingRelationshipRetries()).toBe(0); + expect(relationship.loggableValues(disconnected)).toContain( + "Failed to notify Hub before removing local relationship", + ); }); test("disconnect revokes only after an in-flight enrollment settles", async () => { @@ -370,7 +376,8 @@ describe("Hub relationship", () => { const enrollment = await relationship.enrollmentBegins(); const disconnecting = relationship.beginDisconnect(); - await relationship.relationshipStateBecomes("disconnecting"); + await relationship.connectionStateBecomes("disconnecting"); + expect(relationship.relationshipFile()?.state).toBe("disconnecting"); expect(relationship.revocationAttempts()).toBe(0); relationship.completeEnrollment(); @@ -384,6 +391,40 @@ describe("Hub relationship", () => { expect(relationship.relationshipFile()).toBeNull(); }); + test("disconnect persists terminal intent only while remote notification is in flight", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + relationship.holdRevocation(); + + const disconnecting = relationship.beginDisconnect(); + await relationship.relationshipStateBecomes("disconnecting"); + + expect(await relationship.status()).toMatchObject({ state: "disconnecting" }); + expect(relationship.pendingRelationshipRetries()).toBe(0); + + relationship.completeRevocation(); + await expect(disconnecting.result).resolves.toMatchObject({ state: "not_connected" }); + expect(relationship.relationshipFile()).toBeNull(); + }); + + test("force disconnect does not wait for or notify an in-flight enrollment", async () => { + relationship = await HubRelationshipHarness.start(); + relationship.holdEnrollment(); + const connecting = relationship.beginConnect("one-time-token"); + await relationship.enrollmentBegins(); + + const disconnected = await relationship.disconnect(true); + + expect(disconnected).toMatchObject({ state: "not_connected", hub: null }); + expect(relationship.revocationAttempts()).toBe(0); + expect(relationship.relationshipFile()).toBeNull(); + + relationship.completeEnrollment(); + await connecting.result; + expect(relationship.socketAttempts()).toBe(0); + expect(await relationship.status()).toMatchObject({ state: "not_connected", hub: null }); + }); + test("daemon restart reconnects the same durable relationship", async () => { relationship = await HubRelationshipHarness.start(); await relationship.beginConnect().result; @@ -728,48 +769,73 @@ describe("Hub relationship", () => { expect(loggable).not.toContain(enrollment.idempotencyKey); }); - test("offline disconnect retries across runtime and restart without opening a socket", async () => { + test("offline disconnect removes local authority after one notify attempt", async () => { relationship = await HubRelationshipHarness.start(); await relationship.beginConnect().result; relationship.connectLatestSocket(); relationship.failRevocations(3); - const disconnecting = await relationship.disconnect(); - await relationship.retry(); + const disconnected = await relationship.disconnect(); await relationship.restartDaemon(); - await relationship.retry(); - expect(disconnecting.state).toBe("disconnecting"); - expect(relationship.revocationAttempts()).toBe(4); + expect(disconnected).toMatchObject({ + state: "not_connected", + hub: null, + warning: expect.stringContaining("server-side revocation may remain pending"), + }); + expect(await relationship.status()).toMatchObject({ state: "not_connected", hub: null }); + expect(relationship.revocationAttempts()).toBe(1); expect(relationship.socketAttempts()).toBe(1); + expect(relationship.pendingRelationshipRetries()).toBe(0); expect(relationship.relationshipFile()).toBeNull(); }); - test("successful disconnect clears a transient revocation error", async () => { + test("successful disconnect notifies the Hub and returns clean local status", async () => { relationship = await HubRelationshipHarness.start(); await relationship.beginConnect().result; - relationship.failRevocations(1); - const disconnecting = await relationship.disconnect(); - await relationship.retry(); + const disconnected = await relationship.disconnect(); - expect(disconnecting).toMatchObject({ state: "disconnecting", error: expect.any(String) }); + expect(disconnected).toMatchObject({ state: "not_connected", hub: null, error: null }); + expect(disconnected).not.toHaveProperty("warning"); + expect(relationship.revocationAttempts()).toBe(1); expect(await relationship.status()).toMatchObject({ state: "not_connected", error: null }); }); - test("force disconnect removes local authority and reports the remote warning", async () => { + test("force disconnect removes local authority without notifying the Hub", async () => { relationship = await HubRelationshipHarness.start(); await relationship.beginConnect().result; - relationship.failRevocations(1); - await relationship.disconnect(); const forced = await relationship.disconnect(true); - expect(forced.state).toBe("not_connected"); - expect(forced.warning).toContain("remote revocation"); + expect(forced).toMatchObject({ state: "not_connected", hub: null }); + expect(forced).not.toHaveProperty("warning"); + expect(relationship.revocationAttempts()).toBe(0); + expect(relationship.loggableValues(forced)).not.toContain( + "Failed to notify Hub before removing local relationship", + ); expect(relationship.relationshipFile()).toBeNull(); }); + test("startup removes a legacy persisted disconnecting relationship", async () => { + relationship = await HubRelationshipHarness.start(); + await relationship.beginConnect().result; + const active = relationship.relationshipFile(); + await relationship.corruptRelationshipFile( + JSON.stringify({ ...active, state: "disconnecting" }), + ); + + await relationship.startStoppedDaemon(); + + expect(await relationship.status()).toMatchObject({ state: "not_connected", hub: null }); + expect(relationship.relationshipFile()).toBeNull(); + expect(relationship.revocationAttempts()).toBe(0); + expect(relationship.pendingRelationshipRetries()).toBe(0); + expect(relationship.loggableValues(await relationship.status())).toContain( + "Removed legacy disconnecting Hub relationship during startup", + ); + }); + test("disconnect leaves no intent artifacts and shutdown closes the owned agent", async () => { relationship = await HubRelationshipHarness.start(); await relationship.beginConnect().result; diff --git a/packages/server/src/server/hub/relationship-controller.ts b/packages/server/src/server/hub/relationship-controller.ts index 457a5b701d..8ce6f8050b 100644 --- a/packages/server/src/server/hub/relationship-controller.ts +++ b/packages/server/src/server/hub/relationship-controller.ts @@ -137,6 +137,7 @@ export interface HubRelationshipRetryPolicy { export interface HubRelationshipControllerOptions { paseoHome: string; + hostname: string; serverId: string; daemonPublicKey: string; logger: pino.Logger; @@ -196,11 +197,19 @@ export class HubRelationshipController implements HubRelationshipManagement { this.clock = options.clock ?? systemClock; this.retryPolicy = options.retryPolicy ?? new BoundedExponentialHubRetryPolicy(); this.record = this.load(); + if (this.record?.state === "disconnecting") { + // COMPAT(hubUnilateralDisconnect): added in v0.4.0, remove after 2027-02-13 once legacy records have aged out. + this.options.logger.warn( + { daemonId: this.record.relationship.daemonId }, + "Removed legacy disconnecting Hub relationship during startup", + ); + rmSync(this.filePath, { force: true }); + this.record = null; + } if (this.record?.state === "revoked") { this.state = "revoked"; this.lastError = this.record.reason ?? null; - } else if (this.record?.state === "disconnecting") this.state = "disconnecting"; - else if (this.record) this.state = "connecting"; + } else if (this.record) this.state = "connecting"; } async start(): Promise<void> { @@ -217,7 +226,6 @@ export class HubRelationshipController implements HubRelationshipManagement { ); } } - if (this.record?.state === "disconnecting") await this.tryRevocation(this.record); } async stop(): Promise<void> { @@ -290,14 +298,6 @@ export class HubRelationshipController implements HubRelationshipManagement { await pendingCreateCleanup; return { status: this.status() }; } - if (input.force) { - this.remove(); - await pendingCreateCleanup; - return { - status: this.status(), - warning: "Local Hub credential removed; remote revocation may remain pending.", - }; - } const disconnecting: DisconnectingRecord = { version: 1, state: "disconnecting", @@ -308,12 +308,29 @@ export class HubRelationshipController implements HubRelationshipManagement { this.persist(disconnecting); this.record = disconnecting; this.state = "disconnecting"; - if (waitForEnrollment) { + if (waitForEnrollment && !input.force) { await Promise.all(this.inFlightEnrollments); } - await this.tryRevocation(disconnecting); + let warning: string | undefined; + if (!input.force) { + try { + await this.options.remote.revoke({ + daemonId: disconnecting.relationship.daemonId, + hubOrigin: disconnecting.relationship.hubOrigin, + credential: disconnecting.credential.secret, + }); + } catch (error) { + this.options.logger.warn( + { err: error, daemonId: disconnecting.relationship.daemonId }, + "Failed to notify Hub before removing local relationship", + ); + warning = + "Hub could not be reached; local relationship removed, but server-side revocation may remain pending."; + } + } + this.remove(); await pendingCreateCleanup; - return { status: this.status() }; + return { status: this.status(), ...(warning ? { warning } : {}) }; } private async tryEnrollment(pending: PendingRecord, enrollmentGeneration: number): Promise<void> { @@ -324,6 +341,7 @@ export class HubRelationshipController implements HubRelationshipManagement { idempotencyKey: pending.relationship.idempotencyKey, hubOrigin: pending.relationship.hubOrigin, token: pending.enrollment.token, + hostname: this.options.hostname, serverId: pending.identity.serverId, daemonPublicKey: pending.identity.daemonPublicKey, credentialVerifier: verifier, @@ -455,24 +473,6 @@ export class HubRelationshipController implements HubRelationshipManagement { }); } - private async tryRevocation(record: DisconnectingRecord): Promise<void> { - const generation = this.generation; - try { - await this.options.remote.revoke({ - daemonId: record.relationship.daemonId, - hubOrigin: record.relationship.hubOrigin, - credential: record.credential.secret, - }); - if (generation !== this.generation) return; - this.remove(); - } catch (error) { - if (generation !== this.generation) return; - this.lastError = error instanceof Error ? error.message : String(error); - this.state = "disconnecting"; - this.schedule(() => void this.tryRevocation(record)); - } - } - private schedule(task: () => void): void { this.retry?.cancel(); const generation = this.generation; diff --git a/packages/server/src/server/hub/relationship-remote.test.ts b/packages/server/src/server/hub/relationship-remote.test.ts index e4950adaba..8591ad8adc 100644 --- a/packages/server/src/server/hub/relationship-remote.test.ts +++ b/packages/server/src/server/hub/relationship-remote.test.ts @@ -61,6 +61,7 @@ test.each([408, 429])("transient enrollment status %s remains retryable", async idempotencyKey: "ceremony-1", hubOrigin, token: "token", + hostname: "test-daemon.local", serverId: "server-1", daemonPublicKey: "public-key", credentialVerifier: "verifier", @@ -74,7 +75,10 @@ test.each([408, 429])("transient enrollment status %s remains retryable", async }); test("enrollment rejects a transport URL that cannot open a WebSocket", async () => { - const hubOrigin = await startEnrollmentHub("ftp://hub.test/daemon"); + let enrolledHostname: string | undefined; + const hubOrigin = await startEnrollmentHub("ftp://hub.test/daemon", (enrollment) => { + enrolledHostname = enrollment.hostname; + }); const remote = new DirectHubRelationshipRemote(); await expect( @@ -83,12 +87,14 @@ test("enrollment rejects a transport URL that cannot open a WebSocket", async () idempotencyKey: "ceremony-1", hubOrigin, token: "token", + hostname: "test-daemon.local", serverId: "server-1", daemonPublicKey: "public-key", credentialVerifier: "verifier", scopes: ["hub.execution.*"], }), ).rejects.toThrow("Hub WebSocket URL must use ws or wss"); + expect(enrolledHostname).toBe("test-daemon.local"); }); test("enrollment rejects a WebSocket URL with a fragment", async () => { @@ -101,6 +107,7 @@ test("enrollment rejects a WebSocket URL with a fragment", async () => { idempotencyKey: "ceremony-1", hubOrigin, token: "token", + hostname: "test-daemon.local", serverId: "server-1", daemonPublicKey: "public-key", credentialVerifier: "verifier", @@ -119,6 +126,7 @@ test("enrollment rejects a WebSocket outside the enrolled Hub authority", async idempotencyKey: "ceremony-1", hubOrigin, token: "token", + hostname: "test-daemon.local", serverId: "server-1", daemonPublicKey: "public-key", credentialVerifier: "verifier", @@ -138,6 +146,7 @@ test.each(["enrollment", "revocation"])("%s HTTP calls are bounded", async (oper idempotencyKey: "ceremony-1", hubOrigin, token: "token", + hostname: "test-daemon.local", serverId: "server-1", daemonPublicKey: "public-key", credentialVerifier: "verifier", @@ -290,11 +299,15 @@ async function startHubReturning(status: number): Promise<string> { return `http://127.0.0.1:${address.port}`; } -async function startEnrollmentHub(webSocketUrl: string): Promise<string> { +async function startEnrollmentHub( + webSocketUrl: string, + onEnrollment?: (enrollment: { daemonId: string; hostname: string }) => void, +): Promise<string> { const server = createServer(async (request, response) => { let body = ""; for await (const chunk of request) body += chunk; - const enrollment = JSON.parse(body) as { daemonId: string }; + const enrollment = JSON.parse(body) as { daemonId: string; hostname: string }; + onEnrollment?.(enrollment); response.writeHead(200, { "content-type": "application/json" }).end( JSON.stringify({ daemonId: enrollment.daemonId, @@ -523,6 +536,7 @@ async function connectController( openPaseoHomes.push(paseoHome); const controller = new HubRelationshipController({ paseoHome, + hostname: "test-daemon.local", serverId: "server-1", daemonPublicKey: "daemon-public-key", logger: pino({ level: "silent" }), diff --git a/packages/server/src/server/hub/relationship-remote.ts b/packages/server/src/server/hub/relationship-remote.ts index 82172a411a..a5321634fa 100644 --- a/packages/server/src/server/hub/relationship-remote.ts +++ b/packages/server/src/server/hub/relationship-remote.ts @@ -7,6 +7,7 @@ export interface HubEnrollment { idempotencyKey: string; hubOrigin: string; token: string; + hostname: string; serverId: string; daemonPublicKey: string; credentialVerifier: string; @@ -96,6 +97,7 @@ export class DirectHubRelationshipRemote implements HubRelationshipRemote { body: JSON.stringify({ daemonId: input.daemonId, idempotencyKey: input.idempotencyKey, + hostname: input.hostname, serverId: input.serverId, daemonPublicKey: input.daemonPublicKey, credentialVerifier: input.credentialVerifier, diff --git a/packages/server/src/server/hub/test-utils/relationship-harness.ts b/packages/server/src/server/hub/test-utils/relationship-harness.ts index a74ebe0681..ac34086703 100644 --- a/packages/server/src/server/hub/test-utils/relationship-harness.ts +++ b/packages/server/src/server/hub/test-utils/relationship-harness.ts @@ -284,6 +284,7 @@ class InMemoryHubRelationships implements HubRelationshipRemote { private enrollmentRejection: 401 | 403 | null = null; private enrollmentScopes = ["hub.execution.*"]; private revokeFailures = 0; + private revocationGate: Deferred<void> | null = null; private readonly relationships = new Set<string>(); readonly enrollmentSnapshots: RelationshipInvocationSnapshot[] = []; readonly socketSnapshots: RelationshipInvocationSnapshot[] = []; @@ -353,8 +354,19 @@ class InMemoryHubRelationships implements HubRelationshipRemote { this.revokeFailures = count; } + holdRevocation(): void { + this.revocationGate = deferred<void>(); + } + + completeRevocation(): void { + if (!this.revocationGate) throw new Error("No revocation is waiting"); + this.revocationGate.resolve(); + this.revocationGate = null; + } + async revoke(input: HubRevocation): Promise<void> { this.revocations.push({ ...input }); + await this.revocationGate?.promise; if (this.revokeFailures > 0) { this.revokeFailures--; throw new Error("Hub is offline"); @@ -510,6 +522,14 @@ export class HubRelationshipHarness { } } + async connectionStateBecomes(expected: string): Promise<void> { + for (let attempt = 0; attempt < 100; attempt++) { + if ((await this.status()).state === expected) return; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + throw new Error(`Hub connection state did not become ${expected}`); + } + async manageRelationshipFromExternalSocket(): Promise<SessionOutboundMessage[]> { const address = Object.values(networkInterfaces()) .flat() @@ -596,6 +616,14 @@ export class HubRelationshipHarness { this.remote.failRevocations(count); } + holdRevocation(): void { + this.remote.holdRevocation(); + } + + completeRevocation(): void { + this.remote.completeRevocation(); + } + failProviderPromptStart(prompt = "Create through the Hub"): void { this.promptsToFail.add(prompt); } diff --git a/packages/server/src/server/loop-service.test.ts b/packages/server/src/server/loop-service.test.ts deleted file mode 100644 index 917a04f1e3..0000000000 --- a/packages/server/src/server/loop-service.test.ts +++ /dev/null @@ -1,1386 +0,0 @@ -import os from "node:os"; -import path from "node:path"; -import { - existsSync, - mkdirSync, - mkdtempSync, - readFileSync, - realpathSync, - rmSync, - writeFileSync, -} from "node:fs"; -import { randomUUID } from "node:crypto"; -import { beforeEach, afterEach, describe, expect, test } from "vitest"; -import type { - AgentCapabilityFlags, - AgentClient, - AgentLaunchContext, - AgentMode, - AgentModelDefinition, - AgentPersistenceHandle, - AgentPromptInput, - AgentRunOptions, - AgentRunResult, - AgentSession, - AgentSessionConfig, - AgentStreamEvent, - AgentSlashCommand, - AgentRuntimeInfo, - AgentProvider, -} from "./agent/agent-sdk-types.js"; -import { AgentStorage } from "./agent/agent-storage.js"; -import { AgentManager } from "./agent/agent-manager.js"; -import { createAgentCommand } from "./agent/create-agent/create.js"; -import type { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js"; -import { createWorkspaceProvisioningService } from "./session/workspace-provisioning/workspace-provisioning-service.js"; -import { createNoopWorkspaceGitService } from "./test-utils/workspace-git-service-stub.js"; -import { FileBackedProjectRegistry, FileBackedWorkspaceRegistry } from "./workspace-registry.js"; -import { LoopService } from "./loop-service.js"; -import { isPlatform } from "../test-utils/platform.js"; -import { createTestLogger } from "../test-utils/test-logger.js"; - -const TEST_CAPABILITIES: AgentCapabilityFlags = { - supportsStreaming: true, - supportsSessionPersistence: true, - supportsDynamicModes: false, - supportsMcpServers: false, - supportsReasoningStream: false, - supportsToolInvocations: false, -}; - -const NO_UNATTENDED_LOOP_POLICY: Pick<ProviderSnapshotManager, "resolveCreateConfig"> = { - async resolveCreateConfig(input) { - expect(input).toMatchObject({ parent: null, unattended: true }); - return { - modeId: input.unattended ? input.requestedMode : "interactive", - featureValues: input.featureValues, - }; - }, -}; - -interface TestLoopServiceOptions { - paseoHome: string; - agentManager: AgentManager; - agentStorage: AgentStorage; - logger: ReturnType<typeof createTestLogger>; - providerSnapshotManager?: Pick<ProviderSnapshotManager, "resolveCreateConfig">; - ensureWorkspaceForCreate?: ( - cwd: string, - firstAgentContext?: { prompt: string }, - ) => Promise<string>; -} - -function createLoopService(options: TestLoopServiceOptions): LoopService { - const providerSnapshotManager = options.providerSnapshotManager ?? NO_UNATTENDED_LOOP_POLICY; - const ensureWorkspaceForCreate = - options.ensureWorkspaceForCreate ?? (async () => "workspace-created-for-loop"); - return new LoopService({ - paseoHome: options.paseoHome, - agentManager: options.agentManager, - logger: options.logger, - ensureWorkspaceForCreate, - createAgent: (input) => - createAgentCommand( - { - agentManager: options.agentManager, - agentStorage: options.agentStorage, - logger: options.logger, - providerSnapshotManager: providerSnapshotManager as ProviderSnapshotManager, - ensureWorkspaceForCreate, - }, - input, - ), - }); -} - -async function createRegistryBackedWorkspaceEnsure(rootDir: string): Promise<{ - workspaceRegistry: FileBackedWorkspaceRegistry; - ensureWorkspaceForCreate: TestLoopServiceOptions["ensureWorkspaceForCreate"]; -}> { - const workspaceRegistry = new FileBackedWorkspaceRegistry( - path.join(rootDir, "projects", "workspaces.json"), - createTestLogger(), - ); - const projectRegistry = new FileBackedProjectRegistry( - path.join(rootDir, "projects", "projects.json"), - createTestLogger(), - ); - await workspaceRegistry.initialize(); - await projectRegistry.initialize(); - const workspaceGitService = createNoopWorkspaceGitService(); - const workspaceProvisioning = createWorkspaceProvisioningService({ - projectRegistry, - workspaceRegistry, - workspaceGitService, - }); - return { - workspaceRegistry, - ensureWorkspaceForCreate: async (cwd, firstAgentContext) => { - const workspace = await workspaceProvisioning.createWorkspaceForDirectory( - cwd, - firstAgentContext?.prompt ?? null, - ); - return workspace.workspaceId; - }, - }; -} - -interface ScriptedAgentBehavior { - onRun(input: { config: AgentSessionConfig; prompt: string; turnId: string }): Promise<string>; -} - -class ScriptedAgentClient implements AgentClient { - readonly provider: AgentProvider; - readonly capabilities = TEST_CAPABILITIES; - - constructor( - provider: AgentProvider, - private readonly behavior: ScriptedAgentBehavior, - ) { - this.provider = provider; - } - - async isAvailable(): Promise<boolean> { - return true; - } - - async createSession( - config: AgentSessionConfig, - _launchContext?: AgentLaunchContext, - ): Promise<AgentSession> { - return new ScriptedAgentSession(config, this.provider, this.behavior); - } - - async resumeSession( - _handle: AgentPersistenceHandle, - overrides?: Partial<AgentSessionConfig>, - ): Promise<AgentSession> { - return new ScriptedAgentSession( - { - provider: this.provider, - cwd: overrides?.cwd ?? process.cwd(), - ...overrides, - }, - this.provider, - this.behavior, - ); - } - - async fetchCatalog(): Promise<{ models: AgentModelDefinition[]; modes: AgentMode[] }> { - return { models: [], modes: [] }; - } -} - -class ScriptedAgentSession implements AgentSession { - readonly capabilities = TEST_CAPABILITIES; - readonly id = randomUUID(); - private readonly subscribers = new Set<(event: AgentStreamEvent) => void>(); - private turnCount = 0; - private interrupted = false; - - constructor( - private readonly config: AgentSessionConfig, - readonly provider: AgentProvider, - private readonly behavior: ScriptedAgentBehavior, - ) {} - - async run(): Promise<AgentRunResult> { - return { - sessionId: this.id, - finalText: "", - timeline: [], - }; - } - - async startTurn( - prompt: AgentPromptInput, - _options?: AgentRunOptions, - ): Promise<{ turnId: string }> { - const promptText = typeof prompt === "string" ? prompt : JSON.stringify(prompt); - const turnId = `turn-${++this.turnCount}`; - this.interrupted = false; - queueMicrotask(() => { - void this.runScript(promptText, turnId); - }); - return { turnId }; - } - - subscribe(callback: (event: AgentStreamEvent) => void): () => void { - this.subscribers.add(callback); - return () => { - this.subscribers.delete(callback); - }; - } - - async *streamHistory(): AsyncGenerator<AgentStreamEvent> {} - - async getRuntimeInfo(): Promise<AgentRuntimeInfo> { - return { - provider: this.provider, - sessionId: this.id, - model: this.config.model ?? null, - modeId: this.config.modeId ?? null, - }; - } - - async getAvailableModes(): Promise<AgentMode[]> { - return []; - } - - async getCurrentMode(): Promise<string | null> { - return this.config.modeId ?? null; - } - - async setMode(): Promise<void> {} - - getPendingPermissions() { - return []; - } - - async respondToPermission(): Promise<void> {} - - describePersistence(): AgentPersistenceHandle { - return { - provider: this.provider, - sessionId: this.id, - }; - } - - async interrupt(): Promise<void> { - this.interrupted = true; - } - - async close(): Promise<void> {} - - async listCommands(): Promise<AgentSlashCommand[]> { - return []; - } - - private emit(event: AgentStreamEvent): void { - for (const subscriber of this.subscribers) { - subscriber(event); - } - } - - private async runScript(prompt: string, turnId: string): Promise<void> { - this.emit({ type: "turn_started", provider: this.provider, turnId }); - if (this.interrupted) { - this.emit({ type: "turn_canceled", provider: this.provider, reason: "interrupted", turnId }); - return; - } - - try { - const responseText = await this.behavior.onRun({ - config: this.config, - prompt, - turnId, - }); - if (this.interrupted) { - this.emit({ - type: "turn_canceled", - provider: this.provider, - reason: "interrupted", - turnId, - }); - return; - } - this.emit({ - type: "timeline", - provider: this.provider, - turnId, - item: { type: "assistant_message", text: responseText }, - }); - this.emit({ type: "turn_completed", provider: this.provider, turnId }); - } catch (error) { - this.emit({ - type: "turn_failed", - provider: this.provider, - turnId, - error: error instanceof Error ? error.message : String(error), - }); - } - } -} - -describe("LoopService", () => { - const logger = createTestLogger(); - let tmpDir: string; - let paseoHome: string; - let workspaceDir: string; - let storage: AgentStorage; - - beforeEach(() => { - tmpDir = realpathSync.native(mkdtempSync(path.join(os.tmpdir(), "loop-service-"))); - paseoHome = path.join(tmpDir, "paseo-home"); - workspaceDir = path.join(tmpDir, "workspace"); - storage = new AgentStorage(path.join(tmpDir, "agents"), logger); - mkdirSync(workspaceDir, { recursive: true }); - workspaceDir = realpathSync.native(workspaceDir); - }); - - afterEach(() => { - rmSync(tmpDir, { recursive: true, force: true }); - }); - - // POSIX-only: real worker agent spawns a PTY whose Windows ConPTY path resolution still fails (error 267) after realpathSync; revisit when we have a Windows dev box. - test.skipIf(isPlatform("win32"))( - "runs fresh worker agents until verify-check passes", - async () => { - const state = { workerRuns: 0 }; - const verifyScriptPath = path.join(workspaceDir, "verify-check.cjs"); - writeFileSync(verifyScriptPath, 'require("fs").accessSync("done.txt");\n'); - const manager = new AgentManager({ - clients: { - claude: new ScriptedAgentClient("claude", { - async onRun({ config }) { - state.workerRuns += 1; - if (config.title?.includes("worker") && state.workerRuns >= 2) { - writeFileSync(path.join(workspaceDir, "done.txt"), "ok"); - } - if (config.title?.includes("worker")) { - return `worker run ${state.workerRuns}`; - } - return '{"passed":true,"reason":"not used"}'; - }, - }), - }, - registry: storage, - logger, - }); - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Create done.txt when the task is actually fixed.", - cwd: workspaceDir, - model: "test-model", - verifyChecks: [ - `${JSON.stringify(process.execPath)} ${JSON.stringify(path.basename(verifyScriptPath))}`, - ], - sleepMs: 1, - maxIterations: 3, - }); - - await waitForLoopCompletion(service, loop.id); - - const finalLoop = await service.inspectLoop(loop.id); - expect(finalLoop.status).toBe("succeeded"); - expect(finalLoop.iterations).toHaveLength(2); - expect(finalLoop.iterations[0]?.workerAgentId).not.toBe( - finalLoop.iterations[1]?.workerAgentId, - ); - expect(finalLoop.iterations[0]?.status).toBe("failed"); - expect(finalLoop.iterations[1]?.status).toBe("succeeded"); - expect(finalLoop.iterations[0]?.verifyChecks[0]?.passed).toBe(false); - expect(finalLoop.iterations[1]?.verifyChecks[0]?.passed).toBe(true); - expect(readFileSync(path.join(paseoHome, "loops", "loops.json"), "utf8")).toContain(loop.id); - }, - ); - - test("uses worker and verifier provider-model settings when provided", async () => { - const workerConfigs: AgentSessionConfig[] = []; - const verifierConfigs: AgentSessionConfig[] = []; - const manager = new AgentManager({ - clients: { - codex: new ScriptedAgentClient("codex", { - async onRun({ config }) { - workerConfigs.push(config); - writeFileSync(path.join(workspaceDir, "done.txt"), "ok"); - return "done"; - }, - }), - claude: new ScriptedAgentClient("claude", { - async onRun({ config }) { - verifierConfigs.push(config); - return '{"passed":true,"reason":"verified"}'; - }, - }), - }, - registry: storage, - logger, - }); - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Create done.txt", - cwd: workspaceDir, - provider: "codex", - model: "fallback-model", - workerModel: "gpt-5.4", - verifyPrompt: "Confirm that done.txt exists in the workspace.", - verifierProvider: "claude", - verifierModel: "sonnet", - maxIterations: 1, - }); - - await waitForLoopCompletion(service, loop.id); - - const finalLoop = await service.inspectLoop(loop.id); - expect(finalLoop.status).toBe("succeeded"); - expect(finalLoop.provider).toBe("codex"); - expect(finalLoop.model).toBe("fallback-model"); - expect(finalLoop.workerProvider).toBeNull(); - expect(finalLoop.workerModel).toBe("gpt-5.4"); - expect(finalLoop.verifierProvider).toBe("claude"); - expect(finalLoop.verifierModel).toBe("sonnet"); - expect(workerConfigs).toHaveLength(1); - expect(workerConfigs[0]).toMatchObject({ - provider: "codex", - model: "gpt-5.4", - }); - expect(verifierConfigs).toHaveLength(1); - expect(verifierConfigs[0]).toMatchObject({ - provider: "claude", - model: "sonnet", - }); - }); - - test("loop worker and verifier agents share one registry workspace across iterations", async () => { - const { workspaceRegistry, ensureWorkspaceForCreate } = - await createRegistryBackedWorkspaceEnsure(tmpDir); - let verifierCount = 0; - const manager = new AgentManager({ - clients: { - claude: new ScriptedAgentClient("claude", { - async onRun({ config }) { - if (config.title?.includes("worker")) { - return "worker finished"; - } - verifierCount += 1; - return verifierCount >= 2 - ? '{"passed":true,"reason":"second verifier passed"}' - : '{"passed":false,"reason":"try again"}'; - }, - }), - }, - registry: storage, - logger, - }); - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - ensureWorkspaceForCreate, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Keep trying until the verifier passes.", - cwd: workspaceDir, - model: "test-model", - verifyPrompt: "Report whether the loop has passed.", - archive: true, - sleepMs: 1, - maxIterations: 2, - }); - - await waitForLoopCompletion(service, loop.id); - - const finalLoop = await service.inspectLoop(loop.id); - expect(finalLoop.status).toBe("succeeded"); - expect(finalLoop.iterations).toHaveLength(2); - const firstWorker = await storage.get(finalLoop.iterations[0]!.workerAgentId!); - const firstVerifier = await storage.get(finalLoop.iterations[0]!.verifierAgentId!); - const secondWorker = await storage.get(finalLoop.iterations[1]!.workerAgentId!); - const secondVerifier = await storage.get(finalLoop.iterations[1]!.verifierAgentId!); - const workspaceId = firstWorker?.workspaceId; - expect(workspaceId).toMatch(/^wks_/); - expect(firstVerifier?.workspaceId).toBe(workspaceId); - expect(secondWorker?.workspaceId).toBe(workspaceId); - expect(secondVerifier?.workspaceId).toBe(workspaceId); - expect(await workspaceRegistry.get(workspaceId!)).toMatchObject({ - workspaceId, - cwd: workspaceDir, - }); - expect(await workspaceRegistry.list()).toHaveLength(1); - }); - - test("rejects non-directory cwd before minting a loop workspace", async () => { - const filePath = path.join(tmpDir, "not-a-directory.txt"); - writeFileSync(filePath, "not a directory"); - let ensureCalls = 0; - const manager = new AgentManager({ registry: storage, logger }); - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - ensureWorkspaceForCreate: async () => { - ensureCalls += 1; - return "workspace-created-for-file-cwd"; - }, - }); - await service.initialize(); - - await expect( - service.runLoop({ - prompt: "Use a file as cwd", - cwd: filePath, - verifyChecks: ["true"], - }), - ).rejects.toThrow("is not a directory"); - expect(ensureCalls).toBe(0); - }); - - test("model-less loop workers use provider defaults and keep fast worker logs", async () => { - const workerConfigs: AgentSessionConfig[] = []; - const manager = new AgentManager({ - clients: { - claude: new ScriptedAgentClient("claude", { - async onRun({ config }) { - if (config.title?.includes("worker")) { - workerConfigs.push(config); - return "worker default model output"; - } - return '{"passed":true,"reason":"ok"}'; - }, - }), - }, - registry: storage, - logger, - }); - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Use provider default model", - cwd: workspaceDir, - verifyChecks: ["true"], - maxIterations: 1, - }); - - await waitForLoopCompletion(service, loop.id); - - const finalLoop = await service.inspectLoop(loop.id); - expect(finalLoop.status).toBe("succeeded"); - expect(workerConfigs).toHaveLength(1); - expect(workerConfigs[0]).toMatchObject({ - provider: "claude", - model: undefined, - }); - expect( - finalLoop.logs.some( - (entry) => entry.source === "worker" && entry.text.includes("worker default model output"), - ), - ).toBe(true); - }); - - test("archives worker and verifier agents after each iteration when requested", async () => { - const archivedAgentIds: string[] = []; - const manager = new AgentManager({ - clients: { - claude: new ScriptedAgentClient("claude", { - async onRun({ config }) { - if (config.title?.includes("worker")) { - writeFileSync(path.join(workspaceDir, "done.txt"), "ok"); - return "created done.txt"; - } - return '{"passed":true,"reason":"done.txt exists"}'; - }, - }), - }, - registry: storage, - logger, - }); - const archiveAgent = manager.archiveAgent.bind(manager); - manager.archiveAgent = async (agentId) => { - archivedAgentIds.push(agentId); - await archiveAgent(agentId); - }; - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Create done.txt", - cwd: workspaceDir, - model: "test-model", - verifyPrompt: "Confirm that done.txt exists in the workspace.", - archive: true, - maxIterations: 1, - }); - - await waitForLoopCompletion(service, loop.id); - - const finalLoop = await service.inspectLoop(loop.id); - const iteration = finalLoop.iterations[0]; - expect(finalLoop.archive).toBe(true); - expect(iteration?.workerAgentId).toBeTruthy(); - expect(iteration?.verifierAgentId).toBeTruthy(); - expect(archivedAgentIds).toEqual([iteration.workerAgentId!, iteration.verifierAgentId!]); - await storage.flush(); - await expect(storage.get(iteration.workerAgentId!)).resolves.toMatchObject({ - id: iteration.workerAgentId!, - archivedAt: expect.any(String), - internal: true, - }); - await expect(storage.get(iteration.verifierAgentId!)).resolves.toMatchObject({ - id: iteration.verifierAgentId!, - archivedAt: expect.any(String), - internal: true, - }); - }); - - test("worker prompt-start failures fail the loop and archive the worker", async () => { - class StartFailureLoopSession implements AgentSession { - readonly provider = "claude"; - readonly capabilities = TEST_CAPABILITIES; - readonly id = randomUUID(); - - async run(): Promise<AgentRunResult> { - return { - sessionId: this.id, - finalText: "", - timeline: [], - }; - } - - async startTurn(): Promise<{ turnId: string }> { - throw new Error("worker failed before starting"); - } - - subscribe(): () => void { - return () => {}; - } - - async *streamHistory(): AsyncGenerator<AgentStreamEvent> {} - - async getRuntimeInfo(): Promise<AgentRuntimeInfo> { - return { - provider: this.provider, - sessionId: this.id, - model: null, - modeId: null, - }; - } - - async getAvailableModes(): Promise<AgentMode[]> { - return []; - } - - async getCurrentMode(): Promise<string | null> { - return null; - } - - async setMode(): Promise<void> {} - - getPendingPermissions(): AgentPermissionRequest[] { - return []; - } - - async respondToPermission(): Promise<void> {} - - describePersistence(): AgentPersistenceHandle { - return { - provider: this.provider, - sessionId: this.id, - }; - } - - async interrupt(): Promise<void> {} - - async close(): Promise<void> {} - - async listCommands(): Promise<AgentSlashCommand[]> { - return []; - } - } - - const manager = new AgentManager({ - clients: { - claude: { - provider: "claude", - capabilities: TEST_CAPABILITIES, - createSession: async () => new StartFailureLoopSession(), - resumeSession: async () => new StartFailureLoopSession(), - fetchCatalog: async () => ({ models: [], modes: [] }), - isAvailable: async () => true, - }, - }, - registry: storage, - logger, - }); - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Fail before starting", - cwd: workspaceDir, - model: "test-model", - verifyChecks: ["true"], - archive: true, - maxIterations: 1, - }); - - await waitForLoopCompletion(service, loop.id); - - const finalLoop = await service.inspectLoop(loop.id); - expect(finalLoop.status).toBe("failed"); - expect(finalLoop.iterations[0]).toMatchObject({ - status: "failed", - workerOutcome: "failed", - failureReason: expect.stringContaining("worker failed before starting"), - }); - const workerAgentId = finalLoop.iterations[0]?.workerAgentId; - expect(workerAgentId).toMatch(/^[0-9a-f-]{36}$/); - expect(await storage.get(workerAgentId!)).toMatchObject({ - archivedAt: expect.any(String), - }); - }); - - test("uses verifier prompt when provided", async () => { - const manager = new AgentManager({ - clients: { - claude: new ScriptedAgentClient("claude", { - async onRun({ config }) { - if (config.title?.includes("worker")) { - await fsMkdir(workspaceDir); - writeFileSync(path.join(workspaceDir, "done.txt"), "ok"); - return "created done.txt"; - } - const exists = pathExists(path.join(workspaceDir, "done.txt")); - return exists - ? '{"passed":true,"reason":"done.txt exists"}' - : '{"passed":false,"reason":"done.txt missing"}'; - }, - }), - }, - registry: storage, - logger, - }); - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Create done.txt", - cwd: workspaceDir, - model: "test-model", - verifyPrompt: "Confirm that done.txt exists in the workspace.", - maxIterations: 1, - }); - - await waitForLoopCompletion(service, loop.id); - - const finalLoop = await service.inspectLoop(loop.id); - expect(finalLoop.status).toBe("succeeded"); - expect(finalLoop.iterations[0]?.verifyPrompt).toMatchObject({ - passed: true, - reason: "done.txt exists", - }); - const logs = await service.getLoopLogs(loop.id); - expect(logs.entries.some((entry) => entry.text.includes("Verifier result"))).toBe(true); - }); - - test("defaults worker and verifier modeId to provider's unattended mode", async () => { - const workerConfigs: AgentSessionConfig[] = []; - const verifierConfigs: AgentSessionConfig[] = []; - const manager = new AgentManager({ - clients: { - claude: new ScriptedAgentClient("claude", { - async onRun({ config }) { - if (config.title?.includes("worker")) { - workerConfigs.push(config); - writeFileSync(path.join(workspaceDir, "done.txt"), "ok"); - return "created done.txt"; - } - verifierConfigs.push(config); - return '{"passed":true,"reason":"ok"}'; - }, - }), - }, - registry: storage, - logger, - }); - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - providerSnapshotManager: { - async resolveCreateConfig(input) { - expect(input).toMatchObject({ - parent: null, - unattended: true, - requestedMode: undefined, - }); - return { - modeId: input.unattended ? "bypassPermissions" : "interactive", - featureValues: input.featureValues, - }; - }, - }, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Create done.txt", - cwd: workspaceDir, - model: "test-model", - verifyPrompt: "Confirm that done.txt exists in the workspace.", - maxIterations: 1, - }); - - await waitForLoopCompletion(service, loop.id); - - expect(workerConfigs[0]?.modeId).toBe("bypassPermissions"); - expect(verifierConfigs[0]?.modeId).toBe("bypassPermissions"); - }); - - test("defaults OpenCode workers and verifiers to build plus auto accept", async () => { - class CapturingScriptedAgentClient extends ScriptedAgentClient { - readonly createdConfigs: AgentSessionConfig[] = []; - - override async createSession( - config: AgentSessionConfig, - launchContext?: AgentLaunchContext, - ): Promise<AgentSession> { - this.createdConfigs.push(config); - return super.createSession(config, launchContext); - } - } - - const opencodeClient = new CapturingScriptedAgentClient("opencode", { - async onRun({ config }) { - if (config.title?.includes("worker")) { - writeFileSync(path.join(workspaceDir, "done.txt"), "ok"); - return "created done.txt"; - } - return '{"passed":true,"reason":"ok"}'; - }, - }); - const manager = new AgentManager({ - clients: { - opencode: opencodeClient, - }, - registry: storage, - logger, - }); - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - providerSnapshotManager: { - async resolveCreateConfig(input) { - expect(input).toMatchObject({ - parent: null, - unattended: true, - requestedMode: undefined, - }); - return { - modeId: input.unattended ? "build" : "interactive", - featureValues: input.unattended - ? { ...input.featureValues, auto_accept: true } - : input.featureValues, - }; - }, - }, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Create done.txt", - cwd: workspaceDir, - provider: "opencode", - model: "test-model", - verifyPrompt: "Confirm that done.txt exists in the workspace.", - maxIterations: 1, - }); - - await waitForLoopCompletion(service, loop.id); - - expect(opencodeClient.createdConfigs[0]).toMatchObject({ - modeId: "build", - featureValues: { auto_accept: true }, - }); - expect(opencodeClient.createdConfigs[1]).toMatchObject({ - modeId: "build", - featureValues: { auto_accept: true }, - }); - }); - - test("explicit modeId wins over unattended default", async () => { - const workerConfigs: AgentSessionConfig[] = []; - const verifierConfigs: AgentSessionConfig[] = []; - const manager = new AgentManager({ - clients: { - claude: new ScriptedAgentClient("claude", { - async onRun({ config }) { - if (config.title?.includes("worker")) { - workerConfigs.push(config); - writeFileSync(path.join(workspaceDir, "done.txt"), "ok"); - return "created done.txt"; - } - verifierConfigs.push(config); - return '{"passed":true,"reason":"ok"}'; - }, - }), - }, - registry: storage, - logger, - }); - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Create done.txt", - cwd: workspaceDir, - model: "test-model", - modeId: "acceptEdits", - verifierModeId: "plan", - verifyPrompt: "Confirm that done.txt exists in the workspace.", - maxIterations: 1, - }); - - await waitForLoopCompletion(service, loop.id); - - expect(workerConfigs[0]?.modeId).toBe("acceptEdits"); - expect(verifierConfigs[0]?.modeId).toBe("plan"); - }); - - test("stops a running loop and cancels the active worker", async () => { - let release: (() => void) | null = null; - const cancelledAgentIds: string[] = []; - const blocker = new Promise<void>((resolve) => { - release = resolve; - }); - const manager = new AgentManager({ - clients: { - claude: new ScriptedAgentClient("claude", { - async onRun({ config }) { - if (config.title?.includes("worker")) { - await blocker; - return "finished"; - } - return '{"passed":true,"reason":"ok"}'; - }, - }), - }, - registry: storage, - logger, - }); - const cancelAgentRun = manager.cancelAgentRun.bind(manager); - manager.cancelAgentRun = async (agentId) => { - cancelledAgentIds.push(agentId); - return cancelAgentRun(agentId); - }; - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Wait forever", - cwd: workspaceDir, - model: "test-model", - verifyChecks: ["test -f never.txt"], - }); - - const workerAgentId = await waitForActiveWorkerRun(service, manager, loop.id); - const stopPromise = service.stopLoop(loop.id); - let cancelWaitError: unknown; - try { - await waitForCancelledAgent(cancelledAgentIds, workerAgentId); - } catch (error) { - cancelWaitError = error; - } finally { - release?.(); - } - const stopped = await stopPromise; - if (cancelWaitError) { - throw cancelWaitError; - } - - expect(stopped.status).toBe("stopped"); - const finalLoop = await service.inspectLoop(loop.id); - expect(finalLoop.status).toBe("stopped"); - expect(finalLoop.iterations[0]?.status).toBe("stopped"); - expect(cancelledAgentIds).toEqual([workerAgentId]); - expect(finalLoop.logs.some((entry) => entry.text.includes("Stop requested"))).toBe(true); - }); - - test("force-closes a loop worker when graceful cancellation is refused", async () => { - let release: (() => void) | null = null; - const blocker = new Promise<void>((resolve) => { - release = resolve; - }); - const cancelledAgentIds: string[] = []; - const closedAgentIds: string[] = []; - const manager = new AgentManager({ - clients: { - claude: new ScriptedAgentClient("claude", { - async onRun({ config }) { - if (config.title?.includes("worker")) { - await blocker; - return "finished"; - } - return '{"passed":true,"reason":"ok"}'; - }, - }), - }, - registry: storage, - logger, - }); - manager.cancelAgentRun = async (agentId) => { - cancelledAgentIds.push(agentId); - return { status: "refused" }; - }; - const closeAgent = manager.closeAgent.bind(manager); - manager.closeAgent = async (agentId) => { - closedAgentIds.push(agentId); - await closeAgent(agentId); - }; - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Wait forever", - cwd: workspaceDir, - model: "test-model", - verifyChecks: ["test -f never.txt"], - }); - const workerAgentId = await waitForActiveWorkerRun(service, manager, loop.id); - const stopPromise = service.stopLoop(loop.id); - let closeWaitError: unknown; - - try { - await waitForCancelledAgent(cancelledAgentIds, workerAgentId); - await waitForCancelledAgent(closedAgentIds, workerAgentId); - } catch (error) { - closeWaitError = error; - } finally { - release?.(); - } - - const stopped = await stopPromise; - if (closeWaitError) { - throw closeWaitError; - } - expect(stopped.status).toBe("stopped"); - expect(cancelledAgentIds).toEqual([workerAgentId]); - expect(closedAgentIds).toContain(workerAgentId); - }); - - test("tolerates a loop worker closing while graceful cancellation is refused", async () => { - let release: (() => void) | null = null; - const blocker = new Promise<void>((resolve) => { - release = resolve; - }); - const cancelledAgentIds: string[] = []; - const manager = new AgentManager({ - clients: { - claude: new ScriptedAgentClient("claude", { - async onRun({ config }) { - if (config.title?.includes("worker")) { - await blocker; - return "finished"; - } - return '{"passed":true,"reason":"ok"}'; - }, - }), - }, - registry: storage, - logger, - }); - const closeAgent = manager.closeAgent.bind(manager); - manager.cancelAgentRun = async (agentId) => { - cancelledAgentIds.push(agentId); - await closeAgent(agentId); - return { status: "refused" }; - }; - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Finish while Stop is canceling", - cwd: workspaceDir, - model: "test-model", - verifyChecks: ["test -f never.txt"], - }); - const workerAgentId = await waitForActiveWorkerRun(service, manager, loop.id); - const stopPromise = service.stopLoop(loop.id); - - await waitForCancelledAgent(cancelledAgentIds, workerAgentId); - release?.(); - - await expect(stopPromise).resolves.toMatchObject({ status: "stopped" }); - }); - - test("reports unexpected loop worker cancellation errors", async () => { - let release: (() => void) | null = null; - const blocker = new Promise<void>((resolve) => { - release = resolve; - }); - const manager = new AgentManager({ - clients: { - claude: new ScriptedAgentClient("claude", { - async onRun({ config }) { - if (config.title?.includes("worker")) { - await blocker; - return "finished"; - } - return '{"passed":true,"reason":"ok"}'; - }, - }), - }, - registry: storage, - logger, - }); - manager.cancelAgentRun = async () => { - throw new Error("cancellation transport failed"); - }; - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Fail while Stop is canceling", - cwd: workspaceDir, - model: "test-model", - verifyChecks: ["test -f never.txt"], - }); - await waitForActiveWorkerRun(service, manager, loop.id); - const execution = ( - service as unknown as { running: Map<string, { promise: Promise<void> }> } - ).running.get(loop.id)?.promise; - - try { - await expect(service.stopLoop(loop.id)).rejects.toThrow("cancellation transport failed"); - } finally { - release?.(); - await execution; - } - }); - - test("stops while waiting for loop workspace provisioning without starting a worker", async () => { - let resolveWorkspace: ((workspaceId: string) => void) | null = null; - const workspaceProvisioned = new Promise<string>((resolve) => { - resolveWorkspace = resolve; - }); - const manager = new AgentManager({ - clients: { - claude: new ScriptedAgentClient("claude", { - async onRun() { - return "worker should not start"; - }, - }), - }, - registry: storage, - logger, - }); - const createAgent = manager.createAgent.bind(manager); - let createAgentCalls = 0; - manager.createAgent = async (...args) => { - createAgentCalls += 1; - return createAgent(...args); - }; - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - ensureWorkspaceForCreate: async () => workspaceProvisioned, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Stop before workspace is ready", - cwd: workspaceDir, - model: "test-model", - verifyPrompt: "Should not run.", - maxIterations: 1, - }); - await waitForLoopIteration(service, loop.id); - - const stopPromise = service.stopLoop(loop.id); - await waitForStopRequested(service, loop.id); - resolveWorkspace?.("workspace-created-after-stop"); - const stopped = await stopPromise; - - expect(stopped.status).toBe("stopped"); - expect(createAgentCalls).toBe(0); - const finalLoop = await service.inspectLoop(loop.id); - expect(finalLoop.status).toBe("stopped"); - expect(finalLoop.activeWorkerAgentId).toBeNull(); - expect(finalLoop.iterations[0]).toMatchObject({ - workerAgentId: null, - status: "stopped", - failureReason: "Loop stopped", - }); - }); - - test("treats externally canceled worker turns as failures", async () => { - let release: (() => void) | null = null; - const blocker = new Promise<void>((resolve) => { - release = resolve; - }); - const manager = new AgentManager({ - clients: { - claude: new ScriptedAgentClient("claude", { - async onRun({ config }) { - if (config.title?.includes("worker")) { - await blocker; - return "finished"; - } - return '{"passed":true,"reason":"should not verify canceled worker"}'; - }, - }), - }, - registry: storage, - logger, - }); - const service = createLoopService({ - paseoHome, - agentManager: manager, - agentStorage: storage, - logger, - }); - await service.initialize(); - - const loop = await service.runLoop({ - prompt: "Wait until canceled", - cwd: workspaceDir, - model: "test-model", - verifyPrompt: "Should not run.", - maxIterations: 1, - }); - - const workerAgentId = await waitForActiveWorkerRun(service, manager, loop.id); - await manager.cancelAgentRun(workerAgentId); - release?.(); - await waitForLoopCompletion(service, loop.id); - - const finalLoop = await service.inspectLoop(loop.id); - expect(finalLoop.status).toBe("failed"); - expect(finalLoop.iterations).toHaveLength(1); - expect(finalLoop.iterations[0]).toMatchObject({ - workerAgentId, - workerOutcome: "failed", - status: "failed", - }); - expect(finalLoop.iterations[0]?.failureReason).toContain("was canceled"); - expect(finalLoop.iterations[0]?.verifierAgentId).toBeNull(); - }); -}); - -async function fsMkdir(target: string): Promise<void> { - await import("node:fs/promises").then(({ mkdir }) => mkdir(target, { recursive: true })); -} - -function pathExists(target: string): boolean { - return existsSync(target); -} - -async function waitForLoopCompletion(service: LoopService, loopId: string): Promise<void> { - while ((await service.inspectLoop(loopId)).status === "running") { - await new Promise((resolve) => setTimeout(resolve, 10)); - } -} - -async function waitForActiveWorkerRun( - service: LoopService, - manager: AgentManager, - loopId: string, -): Promise<string> { - const deadline = Date.now() + 2_000; - while (Date.now() < deadline) { - const loop = await service.inspectLoop(loopId); - const workerAgentId = loop.activeWorkerAgentId; - if (workerAgentId && manager.getAgent(workerAgentId)?.activeForegroundTurnId) { - return workerAgentId; - } - await new Promise((resolve) => setTimeout(resolve, 10)); - } - throw new Error("Timed out waiting for loop worker run to start"); -} - -async function waitForLoopIteration(service: LoopService, loopId: string): Promise<void> { - const deadline = Date.now() + 2_000; - while (Date.now() < deadline) { - const loop = await service.inspectLoop(loopId); - if (loop.iterations.length > 0) { - return; - } - await new Promise((resolve) => setTimeout(resolve, 10)); - } - throw new Error("Timed out waiting for loop iteration to start"); -} - -async function waitForStopRequested(service: LoopService, loopId: string): Promise<void> { - const deadline = Date.now() + 2_000; - while (Date.now() < deadline) { - const loop = await service.inspectLoop(loopId); - if (loop.stopRequestedAt) { - return; - } - await new Promise((resolve) => setTimeout(resolve, 10)); - } - throw new Error("Timed out waiting for loop stop request"); -} - -async function waitForCancelledAgent( - cancelledAgentIds: readonly string[], - agentId: string, -): Promise<void> { - const deadline = Date.now() + 2_000; - while (Date.now() < deadline) { - if (cancelledAgentIds.includes(agentId)) { - return; - } - await new Promise((resolve) => setTimeout(resolve, 10)); - } - throw new Error("Timed out waiting for loop worker cancellation"); -} diff --git a/packages/server/src/server/loop-service.ts b/packages/server/src/server/loop-service.ts deleted file mode 100644 index 43474796cb..0000000000 --- a/packages/server/src/server/loop-service.ts +++ /dev/null @@ -1,1029 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { promises as fs } from "node:fs"; -import path from "node:path"; -import { z } from "zod"; -import type { Logger } from "pino"; -import { writeJsonFileAtomic } from "./atomic-file.js"; -import { curateAgentActivity } from "./agent/activity-curator.js"; -import { - type BoundCreateAgentCommand, - type EnsureWorkspaceForCreate, - formatProviderModel, -} from "./agent/create-agent/create.js"; -import type { AgentManager, AgentRunCancellationResult } from "./agent/agent-manager.js"; -import { - buildStructuredAgentResponsePrompt, - getStructuredAgentResponse, -} from "./agent/agent-response-loop.js"; -import type { - AgentPromptInput, - AgentStreamEvent, - AgentTimelineItem, - AgentProvider, -} from "./agent/agent-sdk-types.js"; -import { - buildStringCommandShellInvocation, - createStringCommandShellEnvOverlay, -} from "../utils/string-command-shell.js"; -import { execCommand } from "../utils/spawn.js"; - -const LOOP_ID_LENGTH = 8; -const DEFAULT_LOOP_PROVIDER: AgentProvider = "claude"; -const MAX_VERIFY_OUTPUT_BYTES = 64 * 1024; - -const LoopVerifyPromptSchema = z.object({ - passed: z.boolean(), - reason: z.string().min(1), -}); - -const LoopLogEntrySchema = z.object({ - seq: z.number().int().positive(), - timestamp: z.string(), - iteration: z.number().int().positive().nullable(), - source: z.enum(["loop", "worker", "verifier", "verify-check"]), - level: z.enum(["info", "error"]), - text: z.string(), -}); - -const LoopVerifyCheckResultSchema = z.object({ - command: z.string(), - exitCode: z.number().int(), - passed: z.boolean(), - stdout: z.string(), - stderr: z.string(), - startedAt: z.string(), - completedAt: z.string(), -}); - -const LoopVerifyPromptResultSchema = z.object({ - passed: z.boolean(), - reason: z.string(), - verifierAgentId: z.string().nullable(), - startedAt: z.string(), - completedAt: z.string(), -}); - -const LoopIterationRecordSchema = z.object({ - index: z.number().int().positive(), - workerAgentId: z.string().nullable(), - workerStartedAt: z.string(), - workerCompletedAt: z.string().nullable(), - verifierAgentId: z.string().nullable(), - status: z.enum(["running", "succeeded", "failed", "stopped"]), - workerOutcome: z.enum(["completed", "failed", "canceled"]).nullable(), - failureReason: z.string().nullable(), - verifyChecks: z.array(LoopVerifyCheckResultSchema), - verifyPrompt: LoopVerifyPromptResultSchema.nullable(), -}); - -const LoopRecordSchema = z.object({ - id: z.string(), - name: z.string().nullable(), - prompt: z.string(), - cwd: z.string(), - provider: z.string(), - model: z.string().nullable(), - modeId: z.string().nullable().default(null), - workerProvider: z.string().nullable(), - workerModel: z.string().nullable(), - verifierProvider: z.string().nullable(), - verifierModel: z.string().nullable(), - verifierModeId: z.string().nullable().default(null), - verifyPrompt: z.string().nullable(), - verifyChecks: z.array(z.string()), - archive: z.boolean(), - sleepMs: z.number().int().nonnegative(), - maxIterations: z.number().int().positive().nullable(), - maxTimeMs: z.number().int().positive().nullable(), - status: z.enum(["running", "succeeded", "failed", "stopped"]), - createdAt: z.string(), - updatedAt: z.string(), - startedAt: z.string(), - completedAt: z.string().nullable(), - stopRequestedAt: z.string().nullable(), - iterations: z.array(LoopIterationRecordSchema), - logs: z.array(LoopLogEntrySchema), - nextLogSeq: z.number().int().positive(), - activeIteration: z.number().int().positive().nullable(), - activeWorkerAgentId: z.string().nullable(), - activeVerifierAgentId: z.string().nullable(), -}); - -const StoredLoopsSchema = z.array(LoopRecordSchema); - -export type LoopStatus = z.infer<typeof LoopRecordSchema>["status"]; -export type LoopLogEntry = z.infer<typeof LoopLogEntrySchema>; -export type LoopVerifyCheckResult = z.infer<typeof LoopVerifyCheckResultSchema>; -export type LoopVerifyPromptResult = z.infer<typeof LoopVerifyPromptResultSchema>; -export type LoopIterationRecord = z.infer<typeof LoopIterationRecordSchema>; -export type LoopRecord = z.infer<typeof LoopRecordSchema>; - -export interface LoopRunOptions { - prompt: string; - cwd: string; - provider?: AgentProvider; - model?: string; - modeId?: string; - workerProvider?: AgentProvider; - workerModel?: string; - verifierProvider?: AgentProvider; - verifierModel?: string; - verifierModeId?: string; - verifyPrompt?: string; - verifyChecks?: string[]; - archive?: boolean; - name?: string; - sleepMs?: number; - maxIterations?: number; - maxTimeMs?: number; -} - -export interface LoopListItem { - id: string; - name: string | null; - status: LoopStatus; - cwd: string; - createdAt: string; - updatedAt: string; - activeIteration: number | null; -} - -export interface LoopLogsResult { - loop: LoopRecord; - entries: LoopLogEntry[]; - nextCursor: number; -} - -interface RunningLoopState { - abortController: AbortController; - promise: Promise<void>; -} - -function nowIso(): string { - return new Date().toISOString(); -} - -function cloneLoop(record: LoopRecord): LoopRecord { - return LoopRecordSchema.parse(record); -} - -function createLoopId(): string { - return randomUUID().replace(/-/g, "").slice(0, LOOP_ID_LENGTH); -} - -function normalizeName(name: string | undefined): string | null { - const trimmed = name?.trim(); - return trimmed ? trimmed : null; -} - -function normalizePrompt(value: string | undefined, field: string): string | null { - if (value === undefined) { - return null; - } - const trimmed = value.trim(); - if (!trimmed) { - throw new Error(`${field} cannot be empty`); - } - return trimmed; -} - -function ensurePositiveInteger(value: number | undefined, field: string): number | null { - if (value === undefined) { - return null; - } - if (!Number.isInteger(value) || value <= 0) { - throw new Error(`${field} must be a positive integer`); - } - return value; -} - -function ensureNonNegativeInteger(value: number | undefined, field: string): number { - if (value === undefined) { - return 0; - } - if (!Number.isInteger(value) || value < 0) { - throw new Error(`${field} must be a non-negative integer`); - } - return value; -} - -async function assertLoopCwdDirectory(cwd: string): Promise<void> { - let stats; - try { - stats = await fs.stat(cwd); - } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") { - throw new Error(`Working directory ${cwd} no longer exists`, { cause: error }); - } - throw error; - } - if (!stats.isDirectory()) { - throw new Error(`Working directory ${cwd} is not a directory`); - } -} - -function buildWorkerTitle(loop: LoopRecord, iterationIndex: number): string { - const prefix = loop.name ?? loop.id; - return `${prefix} [loop ${iterationIndex} worker]`; -} - -function buildVerifierTitle(loop: LoopRecord, iterationIndex: number): string { - const prefix = loop.name ?? loop.id; - return `${prefix} [loop ${iterationIndex} verifier]`; -} - -function isUnknownLoopAgentError(error: unknown, agentId: string): boolean { - return error instanceof Error && error.message === `Unknown agent '${agentId}'`; -} - -type LoopAgentManager = Pick< - AgentManager, - | "archiveAgent" - | "cancelAgentRun" - | "closeAgent" - | "deleteAgentState" - | "getAgent" - | "runAgent" - | "subscribe" - | "waitForAgentEvent" ->; - -interface LoopExecutionContext { - workspaceId: Promise<string>; -} - -function formatStreamLog(event: AgentStreamEvent): string | null { - switch (event.type) { - case "timeline": { - const rendered = curateAgentActivity([event.item]); - return rendered === "No activity to display." ? null : rendered; - } - case "turn_failed": - return `[Turn Failed] ${event.error}`; - case "turn_canceled": - return `[Turn Canceled] ${event.reason}`; - case "permission_requested": - return `[Permission Requested] ${event.request.name}`; - case "permission_resolved": - return `[Permission Resolved] ${event.requestId}`; - default: - return null; - } -} - -async function sleepWithAbort(ms: number, signal: AbortSignal): Promise<void> { - if (ms <= 0) { - return; - } - await new Promise<void>((resolve, reject) => { - const timer = setTimeout(() => { - cleanup(); - resolve(); - }, ms); - const onAbort = () => { - cleanup(); - reject(signal.reason instanceof Error ? signal.reason : new Error("Loop aborted")); - }; - const cleanup = () => { - clearTimeout(timer); - signal.removeEventListener("abort", onAbort); - }; - signal.addEventListener("abort", onAbort, { once: true }); - }); -} - -function isAbortError(error: unknown): boolean { - if (error instanceof Error && error.message === "Loop aborted") { - return true; - } - return false; -} - -async function runVerifyCheck(options: { - cwd: string; - command: string; -}): Promise<LoopVerifyCheckResult> { - const startedAt = nowIso(); - try { - const shell = buildStringCommandShellInvocation({ - command: options.command, - windowsShell: "cmd", - }); - const result = await execCommand(shell.shell, shell.args, { - cwd: options.cwd, - envOverlay: createStringCommandShellEnvOverlay(), - maxBuffer: MAX_VERIFY_OUTPUT_BYTES, - shell: false, - }); - return { - command: options.command, - exitCode: 0, - passed: true, - stdout: result.stdout ?? "", - stderr: result.stderr ?? "", - startedAt, - completedAt: nowIso(), - }; - } catch (error) { - const childError = error as Error & { - code?: number | string; - stdout?: string; - stderr?: string; - }; - return { - command: options.command, - exitCode: typeof childError.code === "number" ? childError.code : 1, - passed: false, - stdout: childError.stdout ?? "", - stderr: childError.stderr ?? "", - startedAt, - completedAt: nowIso(), - }; - } -} - -export class LoopService { - private readonly storePath: string; - private readonly logger: Logger; - private loaded = false; - private readonly loops = new Map<string, LoopRecord>(); - private persistQueue: Promise<void> = Promise.resolve(); - private readonly running = new Map<string, RunningLoopState>(); - - constructor( - private readonly options: { - paseoHome: string; - agentManager: LoopAgentManager; - logger: Logger; - createAgent: BoundCreateAgentCommand; - ensureWorkspaceForCreate: EnsureWorkspaceForCreate; - }, - ) { - this.storePath = path.join(options.paseoHome, "loops", "loops.json"); - this.logger = options.logger.child({ module: "loop-service" }); - } - - async initialize(): Promise<void> { - if (this.loaded) { - return; - } - this.loops.clear(); - try { - const raw = await fs.readFile(this.storePath, "utf8"); - const parsed = StoredLoopsSchema.parse(JSON.parse(raw)); - for (const record of parsed) { - if (record.status === "running") { - const recovered = cloneLoop(record); - recovered.status = "stopped"; - recovered.updatedAt = nowIso(); - recovered.completedAt = recovered.updatedAt; - recovered.stopRequestedAt = recovered.updatedAt; - recovered.activeIteration = null; - recovered.activeWorkerAgentId = null; - recovered.activeVerifierAgentId = null; - this.appendLog(recovered, { - iteration: null, - source: "loop", - level: "error", - text: "Loop was interrupted by daemon restart.", - }); - const lastIteration = recovered.iterations.at(-1); - if (lastIteration && lastIteration.status === "running") { - lastIteration.status = "stopped"; - lastIteration.failureReason = "Daemon restarted"; - lastIteration.workerCompletedAt = recovered.updatedAt; - } - this.loops.set(recovered.id, recovered); - continue; - } - this.loops.set(record.id, record); - } - } catch (error) { - const code = (error as NodeJS.ErrnoException).code; - if (code !== "ENOENT") { - this.logger.error({ err: error, storePath: this.storePath }, "Failed to load loops"); - } - } - this.loaded = true; - await this.persist(); - } - - async runLoop(input: LoopRunOptions): Promise<LoopRecord> { - await this.initialize(); - const prompt = normalizePrompt(input.prompt, "prompt"); - if (!prompt) { - throw new Error("prompt cannot be empty"); - } - const verifyPrompt = normalizePrompt(input.verifyPrompt, "verifyPrompt"); - const verifyChecks = (input.verifyChecks ?? []).map((command) => { - const trimmed = command.trim(); - if (!trimmed) { - throw new Error("verifyChecks cannot contain empty commands"); - } - return trimmed; - }); - if (!verifyPrompt && verifyChecks.length === 0) { - throw new Error("Loop requires --verify or at least one --verify-check"); - } - const cwd = path.resolve(input.cwd); - await assertLoopCwdDirectory(cwd); - - const createdAt = nowIso(); - const record = LoopRecordSchema.parse({ - id: createLoopId(), - name: normalizeName(input.name), - prompt, - cwd, - provider: input.provider ?? DEFAULT_LOOP_PROVIDER, - model: normalizePrompt(input.model, "model"), - modeId: normalizePrompt(input.modeId, "modeId"), - workerProvider: input.workerProvider ?? null, - workerModel: normalizePrompt(input.workerModel, "workerModel"), - verifierProvider: input.verifierProvider ?? null, - verifierModel: normalizePrompt(input.verifierModel, "verifierModel"), - verifierModeId: normalizePrompt(input.verifierModeId, "verifierModeId"), - verifyPrompt, - verifyChecks, - archive: input.archive ?? false, - sleepMs: ensureNonNegativeInteger(input.sleepMs, "sleepMs"), - maxIterations: ensurePositiveInteger(input.maxIterations, "maxIterations"), - maxTimeMs: ensurePositiveInteger(input.maxTimeMs, "maxTimeMs"), - status: "running", - createdAt, - updatedAt: createdAt, - startedAt: createdAt, - completedAt: null, - stopRequestedAt: null, - iterations: [], - logs: [], - nextLogSeq: 1, - activeIteration: null, - activeWorkerAgentId: null, - activeVerifierAgentId: null, - }); - - this.loops.set(record.id, record); - this.appendLog(record, { - iteration: null, - source: "loop", - level: "info", - text: `Loop created in ${record.cwd}`, - }); - await this.persist(); - - const abortController = new AbortController(); - const promise = this.executeLoop(record.id, abortController.signal).finally(() => { - this.running.delete(record.id); - }); - this.running.set(record.id, { abortController, promise }); - - return cloneLoop(record); - } - - async listLoops(): Promise<LoopListItem[]> { - await this.initialize(); - return Array.from(this.loops.values()) - .sort((left, right) => right.createdAt.localeCompare(left.createdAt)) - .map((record) => ({ - id: record.id, - name: record.name, - status: record.status, - cwd: record.cwd, - createdAt: record.createdAt, - updatedAt: record.updatedAt, - activeIteration: record.activeIteration, - })); - } - - async inspectLoop(idOrPrefix: string): Promise<LoopRecord> { - await this.initialize(); - const loop = this.requireLoop(idOrPrefix); - return cloneLoop(loop); - } - - async getLoopLogs(idOrPrefix: string, afterSeq = 0): Promise<LoopLogsResult> { - await this.initialize(); - const loop = this.requireLoop(idOrPrefix); - const entries = loop.logs.filter((entry) => entry.seq > afterSeq); - return { - loop: cloneLoop(loop), - entries: entries.map((entry) => LoopLogEntrySchema.parse(entry)), - nextCursor: loop.nextLogSeq - 1, - }; - } - - async stopLoop(idOrPrefix: string): Promise<LoopRecord> { - await this.initialize(); - const loop = this.requireLoop(idOrPrefix); - if (loop.status !== "running") { - return cloneLoop(loop); - } - const running = this.running.get(loop.id); - loop.stopRequestedAt = loop.stopRequestedAt ?? nowIso(); - loop.updatedAt = loop.stopRequestedAt; - this.appendLog(loop, { - iteration: loop.activeIteration, - source: "loop", - level: "info", - text: "Stop requested.", - }); - if (running) { - running.abortController.abort(new Error("Loop aborted")); - } - await this.persist(); - - if (running) { - if (loop.activeWorkerAgentId) { - await this.stopInternalAgent(loop.activeWorkerAgentId, loop.archive); - } - if (loop.activeVerifierAgentId) { - await this.stopInternalAgent(loop.activeVerifierAgentId, loop.archive); - } - await running.promise.catch(() => {}); - } else { - loop.status = "stopped"; - loop.completedAt = nowIso(); - loop.updatedAt = loop.completedAt; - await this.persist(); - } - - return cloneLoop(loop); - } - - private async stopInternalAgent(agentId: string, archive: boolean): Promise<void> { - let cancellation: AgentRunCancellationResult; - try { - cancellation = await this.options.agentManager.cancelAgentRun(agentId); - } catch (error) { - if (isUnknownLoopAgentError(error, agentId)) { - return; - } - throw error; - } - if (cancellation.status !== "refused") { - return; - } - if (!this.options.agentManager.getAgent(agentId)) { - return; - } - try { - if (archive) { - await this.options.agentManager.archiveAgent(agentId); - return; - } - await this.closeInternalAgent(agentId); - } catch (error) { - if (!isUnknownLoopAgentError(error, agentId)) { - throw error; - } - } - } - - private async closeInternalAgent(agentId: string): Promise<void> { - try { - await this.options.agentManager.closeAgent(agentId); - } finally { - await this.options.agentManager.deleteAgentState(agentId); - } - } - - private async executeLoop(loopId: string, signal: AbortSignal): Promise<void> { - const loop = this.requireLoop(loopId); - const deadline = loop.maxTimeMs ? Date.now() + loop.maxTimeMs : null; - const workspaceId = this.options.ensureWorkspaceForCreate(loop.cwd, { prompt: loop.prompt }); - workspaceId.catch(() => {}); - const context: LoopExecutionContext = { - workspaceId, - }; - - try { - for (let index = 1; ; index += 1) { - if (signal.aborted) { - throw new Error("Loop aborted"); - } - if (loop.maxIterations && index > loop.maxIterations) { - this.finishLoop(loop, "failed", `Reached max iterations (${loop.maxIterations}).`); - return; - } - if (deadline !== null && Date.now() > deadline) { - this.finishLoop(loop, "failed", `Reached max time (${loop.maxTimeMs}ms).`); - return; - } - - const iteration = LoopIterationRecordSchema.parse({ - index, - workerAgentId: null, - workerStartedAt: nowIso(), - workerCompletedAt: null, - verifierAgentId: null, - status: "running", - workerOutcome: null, - failureReason: null, - verifyChecks: [], - verifyPrompt: null, - }); - loop.iterations.push(iteration); - loop.activeIteration = index; - loop.updatedAt = nowIso(); - this.appendLog(loop, { - iteration: index, - source: "loop", - level: "info", - text: `Starting iteration ${index}.`, - }); - await this.persist(); - - const workerPassed = await this.runWorkerIteration(loop, iteration, signal, context); - if (signal.aborted) { - throw new Error("Loop aborted"); - } - if (!workerPassed) { - iteration.status = iteration.status === "stopped" ? "stopped" : "failed"; - } else { - const verificationPassed = await this.runVerification(loop, iteration, signal, context); - if (verificationPassed) { - iteration.status = "succeeded"; - this.finishLoop(loop, "succeeded", `Iteration ${index} passed verification.`); - return; - } - if (iteration.status === "running") { - iteration.status = "failed"; - } - } - - loop.activeIteration = null; - loop.activeWorkerAgentId = null; - loop.activeVerifierAgentId = null; - loop.updatedAt = nowIso(); - await this.persist(); - - if (loop.sleepMs > 0) { - this.appendLog(loop, { - iteration: index, - source: "loop", - level: "info", - text: `Sleeping ${loop.sleepMs}ms before next iteration.`, - }); - await this.persist(); - await sleepWithAbort(loop.sleepMs, signal); - } - } - } catch (error) { - await this.handleExecuteLoopError(loop, loopId, error); - } - } - - private async handleExecuteLoopError( - loop: LoopRecord, - loopId: string, - error: unknown, - ): Promise<void> { - const iteration = loop.activeIteration - ? loop.iterations.find((candidate) => candidate.index === loop.activeIteration) - : null; - if (isAbortError(error)) { - this.finishLoop(loop, "stopped", "Loop stopped."); - if (iteration && iteration.status === "running") { - iteration.status = "stopped"; - iteration.failureReason = "Loop stopped"; - iteration.workerCompletedAt = nowIso(); - } - await this.persist(); - return; - } - - const message = error instanceof Error ? error.message : String(error); - this.logger.error({ err: error, loopId }, "Loop execution failed"); - this.finishLoop(loop, "failed", message); - if (iteration && iteration.status === "running") { - iteration.status = "failed"; - iteration.failureReason = message; - iteration.workerCompletedAt = nowIso(); - } - await this.persist(); - } - - private async runWorkerIteration( - loop: LoopRecord, - iteration: LoopIterationRecord, - signal: AbortSignal, - context: LoopExecutionContext, - ): Promise<boolean> { - const workspaceId = await context.workspaceId; - if (signal.aborted) { - throw new Error("Loop aborted"); - } - const created = await this.options.createAgent({ - kind: "mcp", - provider: this.formatWorkerProviderModel(loop), - cwd: loop.cwd, - workspaceId, - title: buildWorkerTitle(loop, iteration.index), - mode: loop.modeId ?? undefined, - unattended: true, - promptFailure: "return-error", - background: true, - notifyOnFinish: false, - internal: true, - }); - const agent = created.snapshot; - iteration.workerAgentId = agent.id; - loop.activeWorkerAgentId = agent.id; - loop.updatedAt = nowIso(); - await this.persist(); - - let workerCanceledReason: string | null = null; - const unsubscribe = this.options.agentManager.subscribe( - (event) => { - if (event.type !== "agent_stream") { - return; - } - if (event.event.type === "turn_canceled") { - workerCanceledReason = event.event.reason; - } - const text = formatStreamLog(event.event); - if (!text) { - return; - } - this.appendLog(loop, { - iteration: iteration.index, - source: "worker", - level: event.event.type === "turn_failed" ? "error" : "info", - text, - }); - void this.persist(); - }, - { agentId: agent.id, replayState: false }, - ); - - try { - const result = await this.options.agentManager.runAgent(agent.id, this.toPrompt(loop.prompt)); - iteration.workerCompletedAt = nowIso(); - iteration.workerOutcome = "completed"; - if (result.canceled || workerCanceledReason) { - throw new Error(`Loop worker ${agent.id} was canceled: ${workerCanceledReason}`); - } - return true; - } catch (error) { - iteration.workerCompletedAt = nowIso(); - iteration.workerOutcome = "failed"; - iteration.failureReason = error instanceof Error ? error.message : String(error); - this.appendLog(loop, { - iteration: iteration.index, - source: "loop", - level: "error", - text: `Worker failed: ${iteration.failureReason}`, - }); - return false; - } finally { - unsubscribe(); - loop.activeWorkerAgentId = null; - loop.updatedAt = nowIso(); - await this.persist(); - try { - if (loop.archive) { - await this.options.agentManager.archiveAgent(agent.id); - } else { - await this.closeInternalAgent(agent.id); - } - } catch { - // Ignore cleanup errors for internal loop workers. - } - if (signal.aborted && iteration.status === "running") { - iteration.status = "stopped"; - } - } - } - - private async runVerification( - loop: LoopRecord, - iteration: LoopIterationRecord, - signal: AbortSignal, - context: LoopExecutionContext, - ): Promise<boolean> { - for (const command of loop.verifyChecks) { - if (signal.aborted) { - throw new Error("Loop aborted"); - } - this.appendLog(loop, { - iteration: iteration.index, - source: "verify-check", - level: "info", - text: `$ ${command}`, - }); - const result = await runVerifyCheck({ cwd: loop.cwd, command }); - iteration.verifyChecks.push(result); - const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n"); - this.appendLog(loop, { - iteration: iteration.index, - source: "verify-check", - level: result.passed ? "info" : "error", - text: output ? `exit ${result.exitCode}\n${output}` : `exit ${result.exitCode}`, - }); - loop.updatedAt = nowIso(); - await this.persist(); - if (!result.passed) { - iteration.failureReason = `Verify check failed: ${command}`; - return false; - } - } - - if (!loop.verifyPrompt) { - return true; - } - - const startedAt = nowIso(); - const initialVerifierPrompt = buildStructuredAgentResponsePrompt({ - prompt: loop.verifyPrompt, - schema: LoopVerifyPromptSchema, - schemaName: "LoopVerifierResult", - }); - const created = await this.options.createAgent({ - kind: "mcp", - provider: this.formatVerifierProviderModel(loop), - cwd: loop.cwd, - workspaceId: await context.workspaceId, - title: buildVerifierTitle(loop, iteration.index), - mode: loop.verifierModeId ?? loop.modeId ?? undefined, - unattended: true, - promptFailure: "return-error", - background: true, - notifyOnFinish: false, - internal: true, - }); - const verifierAgent = created.snapshot; - iteration.verifierAgentId = verifierAgent.id; - loop.activeVerifierAgentId = verifierAgent.id; - loop.updatedAt = nowIso(); - await this.persist(); - - const unsubscribe = this.options.agentManager.subscribe( - (event) => { - if (event.type !== "agent_stream") { - return; - } - const text = formatStreamLog(event.event); - if (!text) { - return; - } - this.appendLog(loop, { - iteration: iteration.index, - source: "verifier", - level: event.event.type === "turn_failed" ? "error" : "info", - text, - }); - void this.persist(); - }, - { agentId: verifierAgent.id, replayState: false }, - ); - - try { - let waitingForInitialResponse = true; - const result = await getStructuredAgentResponse({ - caller: async (nextPrompt) => { - if (waitingForInitialResponse) { - waitingForInitialResponse = false; - const run = await this.options.agentManager.runAgent( - verifierAgent.id, - initialVerifierPrompt, - ); - return this.resolveFinalText(run.timeline, run.finalText); - } - const run = await this.options.agentManager.runAgent( - verifierAgent.id, - this.toPrompt(nextPrompt), - ); - return this.resolveFinalText(run.timeline, run.finalText); - }, - prompt: loop.verifyPrompt, - schema: LoopVerifyPromptSchema, - maxRetries: 2, - schemaName: "LoopVerifierResult", - }); - iteration.verifyPrompt = { - passed: result.passed, - reason: result.reason, - verifierAgentId: verifierAgent.id, - startedAt, - completedAt: nowIso(), - }; - this.appendLog(loop, { - iteration: iteration.index, - source: "loop", - level: result.passed ? "info" : "error", - text: `Verifier result: ${result.reason}`, - }); - if (!result.passed) { - iteration.failureReason = result.reason; - } - return result.passed; - } finally { - unsubscribe(); - loop.activeVerifierAgentId = null; - loop.updatedAt = nowIso(); - await this.persist(); - try { - if (loop.archive) { - await this.options.agentManager.archiveAgent(verifierAgent.id); - } else { - await this.closeInternalAgent(verifierAgent.id); - } - } catch { - // Ignore cleanup errors for internal loop verifiers. - } - } - } - - private formatWorkerProviderModel(loop: LoopRecord): string { - return formatProviderModel( - loop.workerProvider ?? loop.provider, - loop.workerModel ?? loop.model, - ); - } - - private formatVerifierProviderModel(loop: LoopRecord): string { - return formatProviderModel( - loop.verifierProvider ?? loop.provider, - loop.verifierModel ?? loop.model, - ); - } - - private resolveFinalText(timeline: AgentTimelineItem[], finalText: string): string { - if (finalText.trim()) { - return finalText; - } - for (let index = timeline.length - 1; index >= 0; index -= 1) { - const item = timeline[index]; - if (item?.type === "assistant_message" && item.text.trim()) { - return item.text; - } - } - return ""; - } - - private toPrompt(text: string): AgentPromptInput { - return text; - } - - private finishLoop( - loop: LoopRecord, - status: Exclude<LoopStatus, "running">, - message: string, - ): void { - loop.status = status; - loop.completedAt = nowIso(); - loop.updatedAt = loop.completedAt; - loop.activeIteration = null; - loop.activeWorkerAgentId = null; - loop.activeVerifierAgentId = null; - this.appendLog(loop, { - iteration: null, - source: "loop", - level: status === "succeeded" ? "info" : "error", - text: message, - }); - } - - private appendLog(loop: LoopRecord, entry: Omit<LoopLogEntry, "seq" | "timestamp">): void { - loop.logs.push({ - seq: loop.nextLogSeq, - timestamp: nowIso(), - ...entry, - }); - loop.nextLogSeq += 1; - loop.updatedAt = nowIso(); - } - - private requireLoop(idOrPrefix: string): LoopRecord { - const trimmed = idOrPrefix.trim(); - if (!trimmed) { - throw new Error("Loop id is required"); - } - const exact = this.loops.get(trimmed); - if (exact) { - return exact; - } - const matches = Array.from(this.loops.values()).filter((record) => - record.id.startsWith(trimmed), - ); - if (matches.length === 1) { - return matches[0]; - } - if (matches.length > 1) { - throw new Error(`Loop id prefix is ambiguous: ${trimmed}`); - } - throw new Error(`Loop not found: ${trimmed}`); - } - - private async persist(): Promise<void> { - const nextPersist = this.persistQueue.then(async () => { - const records = Array.from(this.loops.values()).sort((left, right) => - left.createdAt.localeCompare(right.createdAt), - ); - await writeJsonFileAtomic(this.storePath, records); - return; - }); - this.persistQueue = nextPersist.catch(() => {}); - await nextPersist; - } -} diff --git a/packages/server/src/server/paseo-worktree-service.test.ts b/packages/server/src/server/paseo-worktree-service.test.ts index 83241388f6..c9e66e4af1 100644 --- a/packages/server/src/server/paseo-worktree-service.test.ts +++ b/packages/server/src/server/paseo-worktree-service.test.ts @@ -260,6 +260,39 @@ test("seeds an uncommitted exact-project config into the mapped worktree directo expect(existsSync(path.join(result.worktree.worktreePath, "paseo.json"))).toBe(false); }); +test("does not overwrite a committed exact-project config with source checkout edits", async () => { + const { repoDir, tempDir } = createGitRepo(); + cleanupPaths.push(tempDir); + const sourceDir = path.join(repoDir, "packages", "app"); + mkdirSync(sourceDir, { recursive: true }); + writeFileSync(path.join(sourceDir, "package.json"), "{}\n"); + const committedConfig = JSON.stringify({ worktree: { setup: ["npm ci"] } }); + writeFileSync(path.join(sourceDir, "paseo.json"), committedConfig); + commitAll(repoDir, "add subproject config"); + writeFileSync( + path.join(sourceDir, "paseo.json"), + JSON.stringify({ worktree: { setup: ["npm install"] } }), + ); + + const result = await createPaseoWorktree( + { + cwd: sourceDir, + worktreeSlug: "preserve-nested-config", + runSetup: false, + paseoHome: path.join(tempDir, ".paseo"), + }, + createDeps(), + ); + + expect(readFileSync(path.join(result.workspace.cwd, "paseo.json"), "utf8")).toBe(committedConfig); + expect( + execFileSync("git", ["status", "--porcelain"], { + cwd: result.worktree.worktreePath, + encoding: "utf8", + }), + ).toBe(""); +}); + test("removes a new worktree when its ref does not contain the selected project directory", async () => { const { repoDir, tempDir } = createGitRepo(); cleanupPaths.push(tempDir); @@ -459,14 +492,14 @@ test("an explicit project FK remains unchanged when its worktree comes from anot }); }); -// POSIX-only: Windows git worktree paths need separate canonicalization coverage. test.skipIf(isPlatform("win32"))( - "keeps repeated worktree requests isolated by workspace identity", + "creates a suffixed worktree when the preferred slug is occupied", async () => { const { repoDir, tempDir } = createGitRepo(); cleanupPaths.push(tempDir); const paseoHome = path.join(tempDir, ".paseo"); - const firstDeps = createDeps(); + const runtimeHome = path.join(tempDir, "runtime-home"); + const firstDeps = createDeps({ runtimeHome }); const first = await createPaseoWorktree( { cwd: repoDir, @@ -481,6 +514,7 @@ test.skipIf(isPlatform("win32"))( events, projects: firstDeps.projects, workspaces: firstDeps.workspaces, + runtimeHome, }); const second = await createPaseoWorktree( @@ -495,9 +529,10 @@ test.skipIf(isPlatform("win32"))( expect(second.created).toBe(true); expect(second.worktree.worktreePath).not.toBe(first.worktree.worktreePath); - expect(events).toContain(`workspace:${second.workspace.workspaceId}`); // Compatibility cwd is presentation data; repeated requests get distinct // runtime-owned placement and identity. + expect(path.basename(second.worktree.worktreePath)).toBe("reuse-me-1"); + expect(events).toContain(`workspace:${second.workspace.workspaceId}`); expect(second.workspace.workspaceId).not.toBe(first.workspace.workspaceId); }, ); @@ -933,7 +968,7 @@ test.skipIf(isPlatform("win32"))( expect(worktreeList).toContain(created.worktreePath); // Recreating without pruning fails with the stale registration pinning the - // branch — this is the case restore must heal. + // path — this is the case restore must heal. await expect( createWorktree({ cwd: repoDir, @@ -942,7 +977,7 @@ test.skipIf(isPlatform("win32"))( runSetup: false, paseoHome, }), - ).rejects.toMatchObject({ name: "BranchAlreadyCheckedOutError" }); + ).rejects.toThrow("missing but already registered worktree"); // The restore-side prune frees the stale registration; recreate then succeeds. execFileSync("git", ["worktree", "prune"], { cwd: repoDir, stdio: "pipe" }); @@ -987,7 +1022,7 @@ test.skipIf(isPlatform("win32"))( ); test.skipIf(isPlatform("win32"))( - "rejects with BranchAlreadyCheckedOutError when the kept branch is checked out elsewhere", + "creates a suffixed branch when the requested branch is checked out elsewhere", async () => { const { repoDir, tempDir } = createGitRepo(); cleanupPaths.push(tempDir); @@ -1003,15 +1038,16 @@ test.skipIf(isPlatform("win32"))( }); expect(existsSync(first.worktreePath)).toBe(true); - await expect( - createWorktree({ - cwd: repoDir, - worktreeSlug: "busy-branch-again", - source: { kind: "checkout-branch", branchName: "busy-branch" }, - runSetup: false, - paseoHome, - }), - ).rejects.toMatchObject({ name: "BranchAlreadyCheckedOutError" }); + const second = await createWorktree({ + cwd: repoDir, + worktreeSlug: "busy-branch-again", + source: { kind: "checkout-branch", branchName: "busy-branch" }, + runSetup: false, + paseoHome, + }); + + expect(second.branchName).toBe("busy-branch-1"); + expect(existsSync(second.worktreePath)).toBe(true); }, ); @@ -1024,6 +1060,7 @@ function createDeps(options?: { events?: string[]; projects?: Map<string, PersistedProjectRecord>; workspaces?: Map<string, PersistedWorkspaceRecord>; + runtimeHome?: string; }): TestDeps { const events = options?.events ?? []; const projects = options?.projects ?? new Map<string, PersistedProjectRecord>(); @@ -1088,8 +1125,9 @@ function createDeps(options?: { workspaceGitService, logger: createTestLogger(), }); - const runtimeHome = mkdtempSync(path.join(tmpdir(), "paseo-worktree-service-runtime-")); - cleanupPaths.push(runtimeHome); + const runtimeHome = + options?.runtimeHome ?? mkdtempSync(path.join(tmpdir(), "paseo-worktree-service-runtime-")); + if (!options?.runtimeHome) cleanupPaths.push(runtimeHome); const workspaceRuntime = createWorkspaceRuntimeService({ paseoHome: runtimeHome, worktreesRoot: path.join(runtimeHome, "worktrees"), diff --git a/packages/server/src/server/persisted-config.test.ts b/packages/server/src/server/persisted-config.test.ts index 603c9bd554..76b430bc06 100644 --- a/packages/server/src/server/persisted-config.test.ts +++ b/packages/server/src/server/persisted-config.test.ts @@ -296,6 +296,20 @@ describe("PersistedConfigSchema agent provider runtime settings", () => { ], }); }); + + test("accepts a custom provider catalog refresh timeout", () => { + const parsed = PersistedConfigSchema.parse({ + agents: { catalogRefreshTimeoutMs: 180_000 }, + }); + + expect(parsed.agents?.catalogRefreshTimeoutMs).toBe(180_000); + }); + + test("rejects provider catalog refresh timeouts that overflow Node timers", () => { + expect(() => + PersistedConfigSchema.parse({ agents: { catalogRefreshTimeoutMs: 2_147_483_648 } }), + ).toThrow(); + }); }); describe("provider overrides (new format)", () => { diff --git a/packages/server/src/server/persisted-config.ts b/packages/server/src/server/persisted-config.ts index 9da53a63c6..b2eadf8d60 100644 --- a/packages/server/src/server/persisted-config.ts +++ b/packages/server/src/server/persisted-config.ts @@ -9,7 +9,7 @@ import { } from "./agent/provider-launch-config.js"; import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js"; import { ensurePrivateFile, writePrivateFileAtomicSync } from "./private-files.js"; -import { TerminalProfileSchema } from "@getpaseo/protocol/messages"; +import { AgentProfileSchema, TerminalProfileSchema } from "@getpaseo/protocol/messages"; import { PaseoServicePortAllocationSchema } from "@getpaseo/protocol/paseo-config-schema"; export const LogLevelSchema = z.enum(["trace", "debug", "info", "warn", "error", "fatal"]); @@ -276,6 +276,7 @@ export const PersistedConfigSchema = z enableTerminalAgentHooks: z.boolean().optional(), appendSystemPrompt: z.string().optional(), terminalProfiles: z.array(TerminalProfileSchema).optional(), + agentProfiles: z.array(AgentProfileSchema).optional(), cors: z .object({ allowedOrigins: z.array(z.string()).optional(), @@ -325,6 +326,7 @@ export const PersistedConfigSchema = z agents: z .object({ providers: z.preprocess(normalizeAgentProviders, ProviderOverridesSchema).optional(), + catalogRefreshTimeoutMs: z.number().int().positive().max(2_147_483_647).optional(), metadataGeneration: AgentMetadataGenerationSchema.optional(), }) .strict() diff --git a/packages/server/src/server/push/index.test.ts b/packages/server/src/server/push/index.test.ts new file mode 100644 index 0000000000..1cd4c1baa6 --- /dev/null +++ b/packages/server/src/server/push/index.test.ts @@ -0,0 +1,67 @@ +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import type pino from "pino"; +import { afterEach, describe, expect, test } from "vitest"; + +import { createPushNotifications } from "./index.js"; + +function createLogger(): pino.Logger { + const logger = { + child: () => logger, + debug: () => undefined, + info: () => undefined, + warn: () => undefined, + error: () => undefined, + }; + return logger as unknown as pino.Logger; +} + +describe("push notifications", () => { + const homes: string[] = []; + + afterEach(() => { + for (const home of homes.splice(0)) { + rmSync(home, { recursive: true, force: true }); + } + }); + + test("an offline device stops receiving notifications after 48 hours", async () => { + const home = mkdtempSync(path.join(tmpdir(), "paseo-push-notifications-")); + homes.push(home); + const filePath = path.join(home, "push-tokens.json"); + let now = Date.parse("2026-08-10T00:00:00.000Z"); + const deliveries: string[][] = []; + const pushNotifications = createPushNotifications({ + logger: createLogger(), + filePath, + now: () => now, + deliver: async (tokens) => deliveries.push(tokens), + }); + + pushNotifications.renew("ExponentPushToken[offline-device]"); + now += 48 * 60 * 60 * 1000; + await pushNotifications.send({ title: "Agent finished", body: "Done" }); + + expect(deliveries).toEqual([]); + expect(JSON.parse(readFileSync(filePath, "utf8"))).toEqual({ subscriptions: [] }); + }); + + test("online revocation stops notifications immediately", async () => { + const home = mkdtempSync(path.join(tmpdir(), "paseo-push-notifications-")); + homes.push(home); + const deliveries: string[][] = []; + const pushNotifications = createPushNotifications({ + logger: createLogger(), + filePath: path.join(home, "push-tokens.json"), + now: () => Date.parse("2026-08-10T00:00:00.000Z"), + deliver: async (tokens) => deliveries.push(tokens), + }); + + pushNotifications.renew("ExponentPushToken[online-device]"); + pushNotifications.revoke("ExponentPushToken[online-device]"); + await pushNotifications.send({ title: "Agent finished", body: "Done" }); + + expect(deliveries).toEqual([]); + }); +}); diff --git a/packages/server/src/server/push/index.ts b/packages/server/src/server/push/index.ts new file mode 100644 index 0000000000..3f1a8e0900 --- /dev/null +++ b/packages/server/src/server/push/index.ts @@ -0,0 +1,45 @@ +import type pino from "pino"; + +import { PushService, type PushPayload } from "./push-service.js"; +import { PushTokenStore } from "./token-store.js"; + +export type { PushPayload }; + +const PUSH_TOKEN_LEASE_MS = 48 * 60 * 60 * 1000; + +export interface PushNotifications { + renew(token: string): void; + revoke(token: string): void; + send(payload: PushPayload): Promise<void>; +} + +export type PushNotificationSender = Pick<PushNotifications, "send">; + +export function createPushNotifications(options: { + logger: pino.Logger; + filePath: string; + now?: () => number; + deliver?: (tokens: string[], payload: PushPayload) => Promise<void>; +}): PushNotifications { + const now = options.now ?? Date.now; + const store = new PushTokenStore(options.logger, options.filePath, now, PUSH_TOKEN_LEASE_MS); + const service = new PushService(options.logger, (token) => store.revokeToken(token)); + const deliver = + options.deliver ?? + ((tokens: string[], payload: PushPayload) => service.sendPush(tokens, payload)); + + return { + renew(token) { + store.renewToken(token); + }, + revoke(token) { + store.revokeToken(token); + }, + async send(payload) { + const tokens = store.getActiveTokens(); + options.logger.info({ tokenCount: tokens.length }, "Sending push notification"); + if (tokens.length === 0) return; + await deliver(tokens, payload); + }, + }; +} diff --git a/packages/server/src/server/push/notifications.ts b/packages/server/src/server/push/notifications.ts deleted file mode 100644 index 07c3a5bcd4..0000000000 --- a/packages/server/src/server/push/notifications.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type pino from "pino"; - -import { PushService, type PushPayload } from "./push-service.js"; -import type { PushTokenStore } from "./token-store.js"; - -export type { PushPayload }; - -export interface PushNotificationSender { - send(payload: PushPayload): Promise<void>; -} - -export function createPushNotificationSender( - logger: pino.Logger, - tokenStore: PushTokenStore, -): PushNotificationSender { - const pushService = new PushService(logger, tokenStore); - - return { - async send(payload) { - const tokens = tokenStore.getAllTokens(); - logger.info({ tokenCount: tokens.length }, "Sending push notification"); - if (tokens.length === 0) { - return; - } - - await pushService.sendPush(tokens, payload); - }, - }; -} diff --git a/packages/server/src/server/push/push-service.ts b/packages/server/src/server/push/push-service.ts index dcfadc1225..b91de5fbd0 100644 --- a/packages/server/src/server/push/push-service.ts +++ b/packages/server/src/server/push/push-service.ts @@ -1,4 +1,3 @@ -import type { PushTokenStore } from "./token-store.js"; import type pino from "pino"; export interface PushPayload { @@ -31,11 +30,11 @@ const MAX_BATCH_SIZE = 100; */ export class PushService { private readonly logger: pino.Logger; - private readonly tokenStore: PushTokenStore; + private readonly revokeToken: (token: string) => void; - constructor(logger: pino.Logger, tokenStore: PushTokenStore) { + constructor(logger: pino.Logger, revokeToken: (token: string) => void) { this.logger = logger.child({ component: "push-service" }); - this.tokenStore = tokenStore; + this.revokeToken = revokeToken; } async sendPush(tokens: string[], payload: PushPayload): Promise<void> { @@ -102,7 +101,7 @@ export class PushService { ticket.details?.error === "DeviceNotRegistered" || ticket.details?.error === "InvalidCredentials" ) { - this.tokenStore.removeToken(message.to); + this.revokeToken(message.to); } } } diff --git a/packages/server/src/server/push/token-store.test.ts b/packages/server/src/server/push/token-store.test.ts index b5c66e10cb..afd7d601c3 100644 --- a/packages/server/src/server/push/token-store.test.ts +++ b/packages/server/src/server/push/token-store.test.ts @@ -5,6 +5,7 @@ import type pino from "pino"; import { describe, expect, test } from "vitest"; import { PRIVATE_FILE_MODE } from "../private-files.js"; +import { createPushNotifications } from "./index.js"; import { PushTokenStore } from "./token-store.js"; const MODE_MASK = 0o777; @@ -16,6 +17,7 @@ function createLogger(): pino.Logger { debug: () => undefined, info: () => undefined, warn: () => undefined, + error: () => undefined, }; return logger as unknown as pino.Logger; } @@ -29,9 +31,12 @@ describe.skipIf(process.platform === "win32")("PushTokenStore file permissions", const home = mkdtempSync(path.join(tmpdir(), "paseo-push-tokens-")); const tokenPath = path.join(home, "push-tokens.json"); try { - const store = new PushTokenStore(createLogger(), tokenPath); + const pushNotifications = createPushNotifications({ + logger: createLogger(), + filePath: tokenPath, + }); - store.addToken("ExponentPushToken[test]"); + pushNotifications.renew("ExponentPushToken[test]"); expect(modeOf(tokenPath)).toBe(PRIVATE_FILE_MODE); } finally { @@ -39,19 +44,61 @@ describe.skipIf(process.platform === "win32")("PushTokenStore file permissions", } }); - test("repairs existing push token file permissions when loading", () => { + test("repairs existing push token file permissions when loading", async () => { const home = mkdtempSync(path.join(tmpdir(), "paseo-push-tokens-")); const tokenPath = path.join(home, "push-tokens.json"); try { writeFileSync(tokenPath, JSON.stringify({ tokens: ["ExponentPushToken[test]"] })); chmodSync(tokenPath, PERMISSIVE_FILE_MODE); - const store = new PushTokenStore(createLogger(), tokenPath); + const deliveries: string[][] = []; + const pushNotifications = createPushNotifications({ + logger: createLogger(), + filePath: tokenPath, + deliver: async (tokens) => deliveries.push(tokens), + }); + await pushNotifications.send({ title: "Agent finished", body: "Done" }); - expect(store.getAllTokens()).toEqual(["ExponentPushToken[test]"]); + expect(deliveries).toEqual([["ExponentPushToken[test]"]]); expect(modeOf(tokenPath)).toBe(PRIVATE_FILE_MODE); } finally { rmSync(home, { recursive: true, force: true }); } }); }); + +describe("PushTokenStore persistence", () => { + test("does not apply a revocation until the updated subscriptions are durable", () => { + let rejectWrites = false; + let persisted = ""; + const store = new PushTokenStore( + createLogger(), + path.join(tmpdir(), `missing-push-token-store-${process.pid}`, "push-tokens.json"), + () => Date.parse("2026-08-10T00:00:00.000Z"), + 48 * 60 * 60 * 1000, + (_filePath, payload) => { + if (rejectWrites) throw new Error("disk full"); + persisted = String(payload); + }, + ); + + store.renewToken("ExponentPushToken[test]"); + rejectWrites = true; + + expect(() => store.revokeToken("ExponentPushToken[test]")).toThrow("disk full"); + expect(store.getActiveTokens()).toEqual(["ExponentPushToken[test]"]); + expect(JSON.parse(persisted)).toEqual({ + subscriptions: [ + { + token: "ExponentPushToken[test]", + expiresAt: "2026-08-12T00:00:00.000Z", + }, + ], + }); + + rejectWrites = false; + store.revokeToken("ExponentPushToken[test]"); + expect(store.getActiveTokens()).toEqual([]); + expect(JSON.parse(persisted)).toEqual({ subscriptions: [] }); + }); +}); diff --git a/packages/server/src/server/push/token-store.ts b/packages/server/src/server/push/token-store.ts index 438c87af27..540f053138 100644 --- a/packages/server/src/server/push/token-store.ts +++ b/packages/server/src/server/push/token-store.ts @@ -10,36 +10,69 @@ import { ensurePrivateFile, writePrivateFileAtomicSync } from "../private-files. */ export class PushTokenStore { private readonly logger: pino.Logger; - private tokens: Set<string> = new Set(); + private subscriptions = new Map<string, number>(); private readonly filePath: string; + private readonly now: () => number; + private readonly leaseMs: number; + private readonly write: typeof writePrivateFileAtomicSync; - constructor(logger: pino.Logger, filePath: string) { + constructor( + logger: pino.Logger, + filePath: string, + now: () => number, + leaseMs: number, + write: typeof writePrivateFileAtomicSync = writePrivateFileAtomicSync, + ) { this.logger = logger.child({ component: "token-store" }); this.filePath = filePath; + this.now = now; + this.leaseMs = leaseMs; + this.write = write; this.loadFromDisk(); } - addToken(token: string): void { + renewToken(token: string): void { const normalized = token.trim(); if (!normalized) return; - if (this.tokens.has(normalized)) return; - this.tokens.add(normalized); - this.persist(); - this.logger.debug({ total: this.tokens.size }, "Added token"); + const now = this.now(); + const currentExpiry = this.subscriptions.get(normalized); + if (currentExpiry !== undefined && currentExpiry - now > this.leaseMs / 2) return; + const next = new Map(this.subscriptions); + next.set(normalized, now + this.leaseMs); + this.persist(next); + this.subscriptions = next; + this.logger.debug({ total: this.subscriptions.size }, "Renewed token"); } - removeToken(token: string): void { + revokeToken(token: string): void { const normalized = token.trim(); if (!normalized) return; - const deleted = this.tokens.delete(normalized); - if (deleted) { - this.persist(); - this.logger.debug({ total: this.tokens.size }, "Removed token"); + if (!this.subscriptions.has(normalized)) return; + const next = new Map(this.subscriptions); + next.delete(normalized); + this.persist(next); + this.subscriptions = next; + this.logger.debug({ total: this.subscriptions.size }, "Revoked token"); + } + + getActiveTokens(): string[] { + const now = this.now(); + const active = new Map(this.subscriptions); + for (const [token, expiresAt] of this.subscriptions) { + if (expiresAt <= now) { + active.delete(token); + } } - } - - getAllTokens(): string[] { - return Array.from(this.tokens); + if (active.size !== this.subscriptions.size) { + try { + this.persist(active); + this.subscriptions = active; + } catch { + // Keep the previous state so a later send retries pruning. Expired tokens + // are still excluded from this delivery. + } + } + return Array.from(active.keys()); } private loadFromDisk(): void { @@ -49,25 +82,60 @@ export class PushTokenStore { } ensurePrivateFile(this.filePath); const raw = readFileSync(this.filePath, "utf-8"); - const parsed = JSON.parse(raw) as { tokens?: unknown }; - const tokens = Array.isArray(parsed.tokens) - ? parsed.tokens.filter((t): t is string => typeof t === "string" && t.trim().length > 0) + const parsed = JSON.parse(raw) as { subscriptions?: unknown; tokens?: unknown }; + const loaded = new Map<string, number>(); + const subscriptions = Array.isArray(parsed.subscriptions) ? parsed.subscriptions : []; + for (const value of subscriptions) { + if (!value || typeof value !== "object") continue; + const candidate = value as { token?: unknown; expiresAt?: unknown }; + if (typeof candidate.token !== "string" || typeof candidate.expiresAt !== "string") + continue; + const token = candidate.token.trim(); + const expiresAt = Date.parse(candidate.expiresAt); + if (token && Number.isFinite(expiresAt)) { + loaded.set(token, expiresAt); + } + } + this.subscriptions = loaded; + + const legacyTokens = Array.isArray(parsed.tokens) + ? parsed.tokens.filter((token): token is string => typeof token === "string") : []; - this.tokens = new Set(tokens.map((t) => t.trim())); - this.logger.info({ total: this.tokens.size }, "Loaded push tokens"); + if (legacyTokens.length > 0) { + const migrated = new Map(loaded); + const expiresAt = this.now() + this.leaseMs; + for (const token of legacyTokens) { + const normalized = token.trim(); + if (normalized) migrated.set(normalized, expiresAt); + } + this.persist(migrated); + this.subscriptions = migrated; + } + this.logger.info({ total: this.subscriptions.size }, "Loaded push tokens"); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); this.logger.warn({ err }, "Failed to load push tokens"); } } - private persist(): void { + private persist(subscriptions: ReadonlyMap<string, number>): void { try { - const payload = JSON.stringify({ tokens: Array.from(this.tokens) }, null, 2) + "\n"; - writePrivateFileAtomicSync(this.filePath, payload); + const payload = + JSON.stringify( + { + subscriptions: Array.from(subscriptions, ([token, expiresAt]) => ({ + token, + expiresAt: new Date(expiresAt).toISOString(), + })), + }, + null, + 2, + ) + "\n"; + this.write(this.filePath, payload); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); this.logger.warn({ err }, "Failed to persist push tokens"); + throw err; } } } diff --git a/packages/server/src/server/session.test.ts b/packages/server/src/server/session.test.ts index f0c3a1a712..bba131a1ab 100644 --- a/packages/server/src/server/session.test.ts +++ b/packages/server/src/server/session.test.ts @@ -33,10 +33,8 @@ import { asAgentManager, asAgentStorage, asDownloadTokenStore, - asPushTokenStore, - asChatService, + asPushNotifications, asScheduleService, - asLoopService, asCheckoutDiffManager, asGitHubService, asWorkspaceGitService, @@ -321,6 +319,7 @@ interface SessionForTestOptions { daemonVersion?: SessionOptions["daemonVersion"]; daemonRuntimeConfig?: SessionOptions["daemonRuntimeConfig"]; downloadTokenStore?: SessionOptions["downloadTokenStore"]; + pushNotifications?: SessionOptions["pushNotifications"]; messages?: unknown[]; targetedMessages?: Array<{ source: object; message: SessionOutboundMessage }>; binaryMessages?: Uint8Array[]; @@ -450,7 +449,7 @@ function createSessionForTest(options: SessionForTestOptions = {}): Session { onBinaryMessage: createBinaryMessageHandler(options.binaryMessages), logger, downloadTokenStore: options.downloadTokenStore ?? asDownloadTokenStore(), - pushTokenStore: asPushTokenStore(), + pushNotifications: options.pushNotifications ?? asPushNotifications(), paseoHome: options.paseoHome ?? "/tmp/paseo-home", agentManager: asAgentManager({ listAgents: vi.fn(() => []), @@ -478,9 +477,7 @@ function createSessionForTest(options: SessionForTestOptions = {}): Session { get: vi.fn(), list: vi.fn().mockResolvedValue([]), }, - chatService: asChatService(), scheduleService: asScheduleService(), - loopService: asLoopService(), checkoutDiffManager: asCheckoutDiffManager(checkoutDiffManager), github: asGitHubService(github), workspaceGitService: asWorkspaceGitService(workspaceGitService), @@ -796,6 +793,7 @@ describe("project command-center RPCs", () => { projectDisplayName: "new-project", projectCustomName: null, projectCustomIconRevision: null, + projectIconRevision: "automatic:none:v1", projectRootPath: directoryPath, projectKind: "non_git", }, @@ -1530,6 +1528,88 @@ describe("project config RPC authorization", () => { }); }); +test("push token registration can be revoked by the connected client", async () => { + const renewed: string[] = []; + const revoked: string[] = []; + const messages: unknown[] = []; + const session = createSessionForTest({ + messages, + pushNotifications: asPushNotifications({ + renew: (token: string) => renewed.push(token), + revoke: (token: string) => revoked.push(token), + }), + }); + + await session.handleMessage({ + type: "register_push_token", + token: "ExponentPushToken[test-device]", + }); + await session.handleMessage({ + type: "push.unregister.request", + token: "ExponentPushToken[test-device]", + requestId: "revoke-1", + }); + await session.handleMessage({ + type: "client_heartbeat", + deviceType: "mobile", + focusedAgentId: null, + lastActivityAt: "2026-08-10T00:00:00.000Z", + appVisible: false, + }); + + expect(renewed).toEqual(["ExponentPushToken[test-device]"]); + expect(revoked).toEqual(["ExponentPushToken[test-device]"]); + expect(messages).toEqual([ + { + type: "push.unregister.response", + payload: { requestId: "revoke-1" }, + }, + ]); +}); + +test("push token revocation only acknowledges durable removal", async () => { + const renewed: string[] = []; + const messages: SessionOutboundMessage[] = []; + const session = createSessionForTest({ + messages, + pushNotifications: asPushNotifications({ + renew: (token: string) => renewed.push(token), + revoke: () => { + throw new Error("disk full"); + }, + }), + }); + + await session.handleMessage({ + type: "register_push_token", + token: "ExponentPushToken[test-device]", + }); + await session.handleMessage({ + type: "push.unregister.request", + token: "ExponentPushToken[test-device]", + requestId: "revoke-failed", + }); + await session.handleMessage({ + type: "client_heartbeat", + deviceType: "mobile", + focusedAgentId: null, + lastActivityAt: "2026-08-10T00:00:00.000Z", + appVisible: false, + }); + + expect(renewed).toEqual(["ExponentPushToken[test-device]", "ExponentPushToken[test-device]"]); + expect(messages.some((message) => message.type === "push.unregister.response")).toBe(false); + expect(messages).toContainEqual({ + type: "rpc_error", + payload: { + requestId: "revoke-failed", + requestType: "push.unregister.request", + error: "Request failed: disk full", + code: "handler_error", + }, + }); +}); + describe("daemon status + pairing RPC", () => { const tempDirs: string[] = []; @@ -4698,57 +4778,12 @@ describe("session pull request timeline handling", () => { }); }); -describe("chat/schedule/loop dispatch routing (behavior preservation)", () => { - // Each chat/*, loop/*, and schedule/* type must reach its domain handler. The - // injected service stubs are unstubbed, so every handler's own try/catch fires - // and emits its domain rpc_error code — proving the message routed (a dropped - // case would silently no-op and emit nothing). schedule/* historically routed - // via the chat dispatcher's fall-through arm; this guards that path explicitly. +describe("schedule dispatch routing", () => { + // Each schedule/* type must reach its domain handler. The injected service stub + // is unstubbed, so every handler's own try/catch emits its domain rpc_error code. // handleMessage receives already-parsed messages, so these fixtures only need to // satisfy the TS union here — zod parsing happens upstream at the transport. const routingCases: Array<{ msg: SessionInboundMessage; code: string }> = [ - { - msg: { type: "chat/create", requestId: "rt-chat-create", name: "room" }, - code: "chat_request_failed", - }, - { msg: { type: "chat/list", requestId: "rt-chat-list" }, code: "chat_request_failed" }, - { - msg: { type: "chat/inspect", requestId: "rt-chat-inspect", room: "room" }, - code: "chat_request_failed", - }, - { - msg: { type: "chat/delete", requestId: "rt-chat-delete", room: "room" }, - code: "chat_request_failed", - }, - { - msg: { type: "chat/post", requestId: "rt-chat-post", room: "room", body: "hi" }, - code: "chat_request_failed", - }, - { - msg: { type: "chat/read", requestId: "rt-chat-read", room: "room" }, - code: "chat_request_failed", - }, - { - msg: { type: "chat/wait", requestId: "rt-chat-wait", room: "room" }, - code: "chat_request_failed", - }, - { - msg: { type: "loop/run", requestId: "rt-loop-run", prompt: "p", cwd: "/tmp/loop" }, - code: "loop_request_failed", - }, - { msg: { type: "loop/list", requestId: "rt-loop-list" }, code: "loop_request_failed" }, - { - msg: { type: "loop/inspect", requestId: "rt-loop-inspect", id: "loop-1" }, - code: "loop_request_failed", - }, - { - msg: { type: "loop/logs", requestId: "rt-loop-logs", id: "loop-1" }, - code: "loop_request_failed", - }, - { - msg: { type: "loop/stop", requestId: "rt-loop-stop", id: "loop-1" }, - code: "loop_request_failed", - }, { msg: { type: "schedule/create", @@ -5016,7 +5051,7 @@ test("keeps selective delivery scoped per socket when a retained session also ha ]); }); -test("sends project updates only to capable sockets in a retained session", () => { +test("sends project updates only to capable sockets in a retained session", async () => { const messages: SessionOutboundMessage[] = []; const targetedMessages: Array<{ source: object; message: SessionOutboundMessage }> = []; const session = createSessionForTest({ messages, targetedMessages }); @@ -5025,7 +5060,7 @@ test("sends project updates only to capable sockets in a retained session", () = session.updateClientCapabilities(null, legacySocket); session.updateClientCapabilities({ [CLIENT_CAPS.projectUpdates]: true }, capableSocket); - session.emitProjectUpdate({ + await session.emitProjectUpdate({ kind: "upsert", project: createPersistedProjectRecord({ projectId: "project-capable-socket", @@ -5088,6 +5123,7 @@ test("project.list returns every active project descriptor", async () => { projectDisplayName: "acme/app", projectCustomName: null, projectCustomIconRevision: null, + projectIconRevision: "automatic:none:v1", projectRootPath: "/tmp/project-active", projectKind: "git", }, diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 6ef074d361..a3b814fab0 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -41,7 +41,6 @@ import { } from "./persistence-hooks.js"; import { ensureAgentLoaded, ensureUnarchivedAgentLoaded } from "./agent/agent-loading.js"; import { - formatSystemNotificationPrompt, sendPromptToAgent, waitForAgentRunStartWithTimeout, unarchiveAgentState, @@ -75,6 +74,7 @@ import { CLIENT_SHUTDOWN_RPC_REASON, normalizeClientRestartRpcReason, } from "./lifecycle-reasons.js"; +import { DirectorySyncService } from "./directory-sync/index.js"; import { AgentManager, AgentRunCancellationError } from "./agent/agent-manager.js"; import { buildTimelinePromptIndex } from "./agent/timeline-prompt-index.js"; @@ -152,7 +152,7 @@ import type { ProviderProbeService } from "./provider-probe/index.js"; import { wrapSpokenInput } from "./voice-config.js"; import { isVoicePermissionAllowed } from "./voice-permission-policy.js"; import { - readProjectIcon, + ProjectIconReader, removeProjectCustomIcon, setProjectCustomIcon, } from "../utils/project-custom-icon.js"; @@ -166,7 +166,7 @@ import { createAgentStructuredTextGeneration, createGitMetadataGenerator, } from "./session/checkout/git-metadata-generator.js"; -import { ChatScheduleLoopSession } from "./session/chat/chat-schedule-loop-session.js"; +import { ScheduleSession } from "./session/schedule/schedule-session.js"; import { ProviderCatalogSession } from "./session/provider/provider-catalog-session.js"; import { WorkspaceFilesSession } from "./session/files/workspace-files-session.js"; import { AgentConfigSession } from "./session/agent-config/agent-config-session.js"; @@ -177,7 +177,7 @@ import type { HubRelationshipManagement } from "./hub/relationship-controller.js import { HubExecutionController } from "./hub/execution-controller.js"; import type { HubExecutionAgents } from "./hub/daemon-executions.js"; import { DownloadTokenStore } from "./file-download/token-store.js"; -import { PushTokenStore } from "./push/token-store.js"; +import type { PushNotifications } from "./push/index.js"; import { archivePersistedWorkspaceRecord, archiveWorkspaceContents, @@ -210,8 +210,6 @@ import type { CheckoutDiffManager } from "./checkout-diff-manager.js"; import type { Resolvable } from "./speech/provider-resolver.js"; import type { SpeechReadinessSnapshot } from "./speech/speech-runtime.js"; import type pino from "pino"; -import { FileBackedChatService } from "./chat/chat-service.js"; -import { LoopService } from "./loop-service.js"; import { ScheduleService } from "./schedule/service.js"; import { createGitHubService, @@ -404,6 +402,7 @@ type WorkspaceUpdatePayload = Extract< >["payload"]; interface WorkspaceUpdatesSubscriptionState { subscriptionId: string; + syncEnabled?: boolean; filter?: WorkspaceUpdatesFilter; isBootstrapping: boolean; pendingUpdatesByWorkspaceId: Map<string, WorkspaceUpdatePayload>; @@ -449,7 +448,7 @@ export interface SessionOptions { onWorkspaceRecovered?: (workspace: PersistedWorkspaceRecord) => Promise<void>; logger: pino.Logger; downloadTokenStore: DownloadTokenStore; - pushTokenStore: PushTokenStore; + pushNotifications: PushNotifications; paseoHome: string; worktreesRoot?: string; agentManager: AgentManager; @@ -458,10 +457,9 @@ export interface SessionOptions { workspaceRegistry: WorkspaceRegistry; workspaceRuntime?: WorkspaceRuntimeService; providerProbe?: ProviderProbeService; + directorySync?: DirectorySyncService; filesystem?: SessionFileSystem; - chatService: FileBackedChatService; scheduleService: ScheduleService; - loopService: LoopService; checkoutDiffManager: CheckoutDiffManager; github?: ForgeService; createAgentMcpTransport?: AgentMcpTransportFactory; @@ -590,6 +588,10 @@ interface WorkspaceUpdateOptions { optimisticStatus?: WorkspaceDescriptorPayload["status"]; } +function resolveDirectorySync(service: DirectorySyncService | undefined): DirectorySyncService { + return service ?? new DirectorySyncService(); +} + function describeRegistryTransition(record: ArchivedRecordSnapshot | null): RegistryTransition { if (!record) { return "created"; @@ -623,6 +625,7 @@ export class Session { | null; private readonly sessionLogger: pino.Logger; private readonly paseoHome: string; + private readonly projectIcons: ProjectIconReader; private readonly worktreesRoot: string | undefined; private agentManager: AgentManager; @@ -631,6 +634,7 @@ export class Session { private readonly workspaceRegistry: WorkspaceRegistry; private readonly workspaceRuntime: WorkspaceRuntimeService | undefined; private readonly providerProbe: ProviderProbeService | undefined; + private readonly directorySync: DirectorySyncService; private readonly filesystem: SessionFileSystem; private readonly github: ForgeService; private readonly workspaceGitService: WorkspaceGitService; @@ -640,12 +644,13 @@ export class Session { private readonly workspaceProvisioning: WorkspaceProvisioningService; private readonly workspaceRecovery: WorkspaceRecoveryService; private readonly daemonConfigStore: DaemonConfigStore; - private readonly pushTokenStore: PushTokenStore; + private readonly pushNotifications: PushNotifications; private unsubscribeAgentEvents: (() => void) | null = null; private unsubscribeProjectMutations: (() => void) | null = null; private unsubscribeWorkspaceMutations: (() => void) | null = null; private registryMutationQueue: Promise<void> = Promise.resolve(); private readonly workspacesAwaitingInitialAgent = new Set<string>(); + private projectUpdateQueue: Promise<void> = Promise.resolve(); private isCleanedUp = false; private viewedTimelineAgentIds = new Set<string>(); private readonly viewedTimelineAgentIdsBySource = new Map<object, Set<string>>(); @@ -654,6 +659,7 @@ export class Session { private unsubscribeTerminalWorkspaceContributionEvents: (() => void) | null = null; private readonly agentUpdates: AgentUpdatesService; private workspaceUpdatesSubscription: WorkspaceUpdatesSubscriptionState | null = null; + private projectSyncEnabled = false; private readonly workspaceUpdateTails = new Map<string, Promise<void>>(); private clientActivity: { deviceType: "web" | "mobile"; @@ -663,6 +669,7 @@ export class Session { appVisible: boolean; appVisibilityChangedAt: Date; } | null = null; + private registeredPushToken: string | null = null; private readonly terminalManager: TerminalManager | null; private readonly providerSnapshotManager: ProviderSnapshotManager; private readonly serviceProxy: ServiceProxySubsystem | null; @@ -680,7 +687,7 @@ export class Session { private readonly workspaceDirectory: WorkspaceDirectory; private readonly voiceSession: VoiceSession; private readonly checkoutSession: CheckoutSession; - private readonly chatScheduleLoopSession: ChatScheduleLoopSession; + private readonly scheduleSession: ScheduleSession; private readonly providerCatalogSession: ProviderCatalogSession; private readonly workspaceFilesSession: WorkspaceFilesSession; private readonly agentConfigSession: AgentConfigSession; @@ -705,7 +712,7 @@ export class Session { onWorkspaceRecovered, logger, downloadTokenStore, - pushTokenStore, + pushNotifications, paseoHome, worktreesRoot, agentManager, @@ -714,10 +721,9 @@ export class Session { workspaceRegistry, workspaceRuntime, providerProbe, + directorySync, filesystem, - chatService, scheduleService, - loopService, checkoutDiffManager, github, workspaceGitService, @@ -758,8 +764,9 @@ export class Session { this.getTransportBufferedAmount = getTransportBufferedAmount ?? (() => 0); this.onLifecycleIntent = onLifecycleIntent ?? null; this.onWorkspaceRecovered = onWorkspaceRecovered ?? null; - this.pushTokenStore = pushTokenStore; + this.pushNotifications = pushNotifications; this.paseoHome = paseoHome; + this.projectIcons = new ProjectIconReader(paseoHome); this.worktreesRoot = worktreesRoot; this.sessionLogger = logger.child({ module: "session", @@ -784,6 +791,7 @@ export class Session { this.workspaceRegistry = workspaceRegistry; this.workspaceRuntime = workspaceRuntime; this.providerProbe = providerProbe; + this.directorySync = resolveDirectorySync(directorySync); this.filesystem = filesystem ?? nodeSessionFileSystem; this.github = github ?? createGitHubService(); this.workspaceGitService = workspaceGitService; @@ -851,27 +859,9 @@ export class Session { onBranchChanged, logger: this.sessionLogger, }); - this.chatScheduleLoopSession = new ChatScheduleLoopSession({ - host: { - emit: (msg) => this.emit(msg), - listStoredAgents: () => this.agentStorage.list(), - listLiveAgents: () => this.agentManager.listAgents(), - resolveAgentIdentifier: (identifier) => this.resolveAgentIdentifier(identifier), - sendAgentMessage: async (agentId, text) => { - await sendPromptToAgent({ - agentManager: this.agentManager, - agentStorage: this.agentStorage, - agentId, - prompt: formatSystemNotificationPrompt(text), - unarchive: false, - logger: this.sessionLogger, - }); - }, - }, - chatService, + this.scheduleSession = new ScheduleSession({ + host: { emit: (msg) => this.emit(msg) }, scheduleService, - loopService, - clientId: this.clientId, logger: this.sessionLogger, }); this.providerCatalogSession = new ProviderCatalogSession({ @@ -965,6 +955,13 @@ export class Session { this.buildProjectPlacementForWorkspaceId(workspaceId), emitWorkspaceUpdateForWorkspaceId: (workspaceId) => this.emitWorkspaceUpdateForWorkspaceId(workspaceId), + sequenceAgentUpdate: (payload, agent, project, agentId, includeSequence) => + this.directorySync.sequenceAgentUpdate( + payload, + agent && project ? { agent, project } : null, + agentId, + includeSequence, + ), logger: this.sessionLogger, }); this.createAgentLifecycleDispatch = new CreateAgentLifecycleDispatch({ @@ -1185,13 +1182,22 @@ export class Session { ); } - emitProjectUpdate(update: ProjectUpdate): void { + emitProjectUpdate(update: ProjectUpdate): Promise<void> { + const queued = this.projectUpdateQueue + .catch(() => undefined) + .then(() => this.publishProjectUpdate(update)); + this.projectUpdateQueue = queued; + return queued; + } + + private async publishProjectUpdate(update: ProjectUpdate): Promise<void> { + const projectedPayload = + update.kind === "upsert" + ? { kind: "upsert" as const, project: await this.buildProjectDescriptor(update.project) } + : update; const message: SessionOutboundMessage = { type: "project.update", - payload: - update.kind === "upsert" - ? { kind: "upsert", project: this.buildProjectDescriptor(update.project) } - : update, + payload: this.directorySync.sequenceProjectUpdate(projectedPayload, this.projectSyncEnabled), }; if (this.clientCapabilitiesBySource.size === 0 || !this.onMessageToSource) { if (this.supports(CLIENT_CAPS.projectUpdates)) this.emit(message); @@ -1890,7 +1896,7 @@ export class Session { this.dispatchWorkspaceFileMessage(msg, source) ?? this.dispatchProviderMessage(msg) ?? this.dispatchTerminalMessage(msg) ?? - this.dispatchChatScheduleLoopMessage(msg) ?? + this.dispatchScheduleMessage(msg) ?? this.dispatchMiscMessage(msg); if (promise) await promise; } @@ -2068,6 +2074,8 @@ export class Session { return this.agentConfigSession.handleSetAgentFeatureRequest(msg); case "set_agent_thinking_request": return this.agentConfigSession.handleSetAgentThinkingRequest(msg); + case "agent.config.apply.request": + return this.agentConfigSession.handleAgentConfigApplyRequest(msg); case "get_daemon_config_request": this.emit({ type: "get_daemon_config_response", @@ -2140,6 +2148,8 @@ export class Session { return this.checkoutSession.handleCheckoutPushRequest(msg); case "checkout.refresh.request": return this.checkoutSession.handleRefreshRequest(msg); + case "checkout.discard_changes.request": + return this.checkoutSession.handleCheckoutDiscardChangesRequest(msg); case "checkout_pr_create_request": return this.checkoutSession.handleCheckoutPrCreateRequest(msg); case "checkout_pr_merge_request": @@ -2175,7 +2185,7 @@ export class Session { case "fetch_workspaces_request": return this.handleFetchWorkspacesRequest(msg); case "project.list.request": - return this.handleProjectListRequest(msg.requestId); + return this.handleProjectListRequest(msg); case "paseo_worktree_list_request": return this.handlePaseoWorktreeListRequest(msg); case "paseo_worktree_archive_request": @@ -2240,6 +2250,14 @@ export class Session { return this.workspaceFilesSession.handleFileUnsubscribeRequest(msg); case "fs.file.write.request": return this.workspaceFilesSession.handleFileWriteRequest(msg); + case "fs.entry.create.request": + return this.workspaceFilesSession.handleFileEntryCreateRequest(msg); + case "fs.entry.rename.request": + return this.workspaceFilesSession.handleFileEntryRenameRequest(msg); + case "fs.entry.duplicate.request": + return this.workspaceFilesSession.handleFileEntryDuplicateRequest(msg); + case "fs.entry.delete.request": + return this.workspaceFilesSession.handleFileEntryDeleteRequest(msg); case "project_icon_request": return this.workspaceFilesSession.handleProjectIconRequest(msg); case "project.icon.get.request": @@ -2303,51 +2321,26 @@ export class Session { } } - // eslint-disable-next-line complexity - private dispatchChatScheduleLoopMessage(msg: SessionInboundMessage): Promise<void> | undefined { + private dispatchScheduleMessage(msg: SessionInboundMessage): Promise<void> | undefined { switch (msg.type) { - case "chat/create": - return this.chatScheduleLoopSession.handleChatCreateRequest(msg); - case "chat/list": - return this.chatScheduleLoopSession.handleChatListRequest(msg); - case "chat/inspect": - return this.chatScheduleLoopSession.handleChatInspectRequest(msg); - case "chat/delete": - return this.chatScheduleLoopSession.handleChatDeleteRequest(msg); - case "chat/post": - return this.chatScheduleLoopSession.handleChatPostRequest(msg); - case "chat/read": - return this.chatScheduleLoopSession.handleChatReadRequest(msg); - case "chat/wait": - return this.chatScheduleLoopSession.handleChatWaitRequest(msg); - case "loop/run": - return this.chatScheduleLoopSession.handleLoopRunRequest(msg); - case "loop/list": - return this.chatScheduleLoopSession.handleLoopListRequest(msg); - case "loop/inspect": - return this.chatScheduleLoopSession.handleLoopInspectRequest(msg); - case "loop/logs": - return this.chatScheduleLoopSession.handleLoopLogsRequest(msg); - case "loop/stop": - return this.chatScheduleLoopSession.handleLoopStopRequest(msg); case "schedule/create": - return this.chatScheduleLoopSession.handleScheduleCreateRequest(msg); + return this.scheduleSession.handleScheduleCreateRequest(msg); case "schedule/list": - return this.chatScheduleLoopSession.handleScheduleListRequest(msg); + return this.scheduleSession.handleScheduleListRequest(msg); case "schedule/inspect": - return this.chatScheduleLoopSession.handleScheduleInspectRequest(msg); + return this.scheduleSession.handleScheduleInspectRequest(msg); case "schedule/logs": - return this.chatScheduleLoopSession.handleScheduleLogsRequest(msg); + return this.scheduleSession.handleScheduleLogsRequest(msg); case "schedule/pause": - return this.chatScheduleLoopSession.handleSchedulePauseRequest(msg); + return this.scheduleSession.handleSchedulePauseRequest(msg); case "schedule/resume": - return this.chatScheduleLoopSession.handleScheduleResumeRequest(msg); + return this.scheduleSession.handleScheduleResumeRequest(msg); case "schedule/delete": - return this.chatScheduleLoopSession.handleScheduleDeleteRequest(msg); + return this.scheduleSession.handleScheduleDeleteRequest(msg); case "schedule/run-once": - return this.chatScheduleLoopSession.handleScheduleRunOnceRequest(msg); + return this.scheduleSession.handleScheduleRunOnceRequest(msg); case "schedule/update": - return this.chatScheduleLoopSession.handleScheduleUpdateRequest(msg); + return this.scheduleSession.handleScheduleUpdateRequest(msg); default: return undefined; } @@ -2361,6 +2354,16 @@ export class Session { case "register_push_token": this.handleRegisterPushToken(msg.token); return; + case "push.unregister.request": + this.pushNotifications.revoke(msg.token); + if (this.registeredPushToken?.trim() === msg.token.trim()) { + this.registeredPushToken = null; + } + this.emit({ + type: "push.unregister.response", + payload: { requestId: msg.requestId }, + }); + return; } } @@ -2618,25 +2621,22 @@ export class Session { didUnarchive: boolean; originalArchivedAt: string | null; } | null> { - const records = await this.agentStorage.list(); - const matched = records - .filter( - (record) => - record.persistence?.provider === handle.provider && - record.persistence?.sessionId === handle.sessionId, - ) - .reduce<StoredAgentRecord | null>((latest, candidate) => { - if (!latest) { - return candidate; - } - const updatedDelta = - Date.parse(resolveStoredAgentPayloadUpdatedAt(candidate)) - - Date.parse(resolveStoredAgentPayloadUpdatedAt(latest)); - if (updatedDelta !== 0) { - return updatedDelta > 0 ? candidate : latest; - } - return Date.parse(candidate.createdAt) > Date.parse(latest.createdAt) ? candidate : latest; - }, null); + const records = await this.agentStorage.listByProviderSession( + handle.provider, + handle.sessionId, + ); + const matched = records.reduce<StoredAgentRecord | null>((latest, candidate) => { + if (!latest) { + return candidate; + } + const updatedDelta = + Date.parse(resolveStoredAgentPayloadUpdatedAt(candidate)) - + Date.parse(resolveStoredAgentPayloadUpdatedAt(latest)); + if (updatedDelta !== 0) { + return updatedDelta > 0 ? candidate : latest; + } + return Date.parse(candidate.createdAt) > Date.parse(latest.createdAt) ? candidate : latest; + }, null); if (!matched) { return null; } @@ -2766,7 +2766,7 @@ export class Session { // Emit a project.update so clients that track the project as an empty // project (no workspaces yet) receive the resolved name immediately. - this.emitProjectUpdate({ kind: "upsert", project: updated }); + await this.emitProjectUpdate({ kind: "upsert", project: updated }); // Re-emit descriptors for every workspace under this project so the new // resolved name lands in the UI immediately. @@ -2820,7 +2820,7 @@ export class Session { type: "project.icon.set.response", payload: { requestId, projectId, accepted: true, error: null }, }); - this.emitProjectUpdate({ kind: "upsert", project: updated }); + await this.emitProjectUpdate({ kind: "upsert", project: updated }); const affectedWorkspaceIds = (await this.workspaceRegistry.list()) .filter((workspace) => workspace.projectId === projectId) @@ -2846,7 +2846,7 @@ export class Session { const project = await this.projectRegistry.get(projectId); if (!project) throw new Error("Project not found"); - const icon = await readProjectIcon({ paseoHome: this.paseoHome, project }); + const icon = await this.projectIcons.read(project); this.emit({ type: "project.icon.get.response", payload: { projectId, icon, error: null, requestId }, @@ -3839,6 +3839,9 @@ export class Session { if (msg.appVisible && focusedTerminalId) { void this.clearFocusedTerminalAttention(focusedTerminalId); } + if (this.registeredPushToken) { + this.pushNotifications.renew(this.registeredPushToken); + } } private async clearFocusedTerminalAttention(terminalId: string): Promise<void> { @@ -3857,7 +3860,8 @@ export class Session { * Handle push token registration */ private handleRegisterPushToken(token: string): void { - this.pushTokenStore.addToken(token); + this.registeredPushToken = token; + this.pushNotifications.renew(token); this.sessionLogger.info("Registered push token"); } @@ -4785,15 +4789,17 @@ export class Session { } } - private buildProjectDescriptor( + private async buildProjectDescriptor( project: PersistedProjectRecord, - ): WorkspaceProjectDescriptorPayload { + ): Promise<WorkspaceProjectDescriptorPayload> { + const icon = await this.projectIcons.snapshot(project); return { projectId: project.projectId, ...(project.projectKey ? { projectKey: project.projectKey } : {}), projectDisplayName: resolveProjectDisplayName(project), projectCustomName: project.customName ?? null, projectCustomIconRevision: project.customIconRevision ?? null, + projectIconRevision: icon.revision, projectRootPath: project.rootPath, projectKind: project.kind, }; @@ -5013,7 +5019,6 @@ export class Session { continue; } this.workspaceGitObserver.recordDescriptorState(workspaceId, nextWorkspace); - if (!nextWorkspace) { if (this.shouldSkipWorkspaceRemoval(lastEmitted, options?.removedProjectId)) { continue; @@ -5022,17 +5027,28 @@ export class Session { return; } subscription.lastEmittedByWorkspaceId.delete(workspaceId); + const removePayload = await this.buildWorkspaceRemoveUpdatePayload( + workspaceId, + options?.removedProjectId, + ); this.bufferOrEmitWorkspaceUpdate( subscription, - await this.buildWorkspaceRemoveUpdatePayload(workspaceId, options?.removedProjectId), + this.directorySync.sequenceWorkspaceUpdate( + removePayload, + workspace ?? null, + workspaceId, + subscription.syncEnabled === true, + ), ); continue; } - const nextPayload: WorkspaceUpdatePayload = { - kind: "upsert", - workspace: nextWorkspace, - }; + const nextPayload: WorkspaceUpdatePayload = this.directorySync.sequenceWorkspaceUpdate( + { kind: "upsert", workspace: nextWorkspace }, + workspace ?? null, + workspaceId, + subscription.syncEnabled === true, + ); if ( lastEmitted && @@ -5136,10 +5152,13 @@ export class Session { this.agentUpdates.beginSubscription({ subscriptionId, filter: request.filter, + syncEnabled: Boolean(request.sync), }); } - const payload = await this.listFetchAgentsEntries(request); + const payload = request.sync + ? await this.readAgentDirectorySync(request) + : await this.listFetchAgentsEntries(request); const snapshotUpdatedAtByAgentId = new Map<string, number>(); for (const entry of payload.entries) { const parsedUpdatedAt = Date.parse(entry.agent.updatedAt); @@ -5270,6 +5289,7 @@ export class Session { if (subscriptionId) { this.workspaceUpdatesSubscription = { subscriptionId, + syncEnabled: Boolean(request.sync), filter: request.filter, isBootstrapping: true, pendingUpdatesByWorkspaceId: new Map(), @@ -5278,7 +5298,9 @@ export class Session { }; } - const payload = await this.listFetchWorkspacesEntries(request); + const payload = request.sync + ? await this.readWorkspaceDirectorySync(request) + : await this.listFetchWorkspacesEntries(request); this.workspaceGitObserver.syncObservers(payload.entries); this.sessionLogger.debug( { @@ -5328,21 +5350,32 @@ export class Session { } } - private async handleProjectListRequest(requestId: string): Promise<void> { + private async handleProjectListRequest( + request: Extract<SessionInboundMessage, { type: "project.list.request" }>, + ): Promise<void> { try { - const projects = (await this.projectRegistry.list()) - .filter((project) => !project.archivedAt) - .map((project) => this.buildProjectDescriptor(project)); + const projects = await Promise.all( + (await this.projectRegistry.list()) + .filter((project) => !project.archivedAt) + .map((project) => this.buildProjectDescriptor(project)), + ); + const synchronized = request.sync + ? this.directorySync.synchronizeProjects(projects, request.sync) + : null; + if (synchronized) this.projectSyncEnabled = true; this.emit({ type: "project.list.response", - payload: { requestId, projects }, + payload: { + requestId: request.requestId, + ...(synchronized ?? { projects }), + }, }); } catch (error) { this.sessionLogger.error({ err: error }, "Failed to handle project.list.request"); this.emit({ type: "rpc_error", payload: { - requestId, + requestId: request.requestId, requestType: "project.list.request", error: error instanceof Error ? error.message : "Failed to list projects", code: "project_list_failed", @@ -5351,6 +5384,37 @@ export class Session { } } + private async readAgentDirectorySync( + request: Extract<SessionInboundMessage, { type: "fetch_agents_request" }>, + ) { + if (request.scope !== "active" || request.filter) { + throw new SessionRequestError( + "invalid_request", + "Sequenced agent directory reads require scope=active and no filter.", + ); + } + const snapshot = await this.listFetchAgentsEntries({ + ...request, + page: { limit: Number.MAX_SAFE_INTEGER }, + }); + return this.directorySync.synchronizeAgents(snapshot.entries, request.sync ?? {}); + } + + private async readWorkspaceDirectorySync( + request: Extract<SessionInboundMessage, { type: "fetch_workspaces_request" }>, + ) { + if (request.filter) { + throw new SessionRequestError( + "invalid_request", + "Sequenced workspace directory reads do not support filters.", + ); + } + return this.directorySync.synchronizeWorkspaces( + await this.workspaceDirectory.listDescriptors(), + request.sync ?? {}, + ); + } + // Build the bootstrap snapshot used by `flushBootstrappedWorkspaceUpdates` // to decide which pending updates to drop. Captures the status, // statusEnteredAt, and activityAt (parsed to ms) for each workspace entry @@ -5818,7 +5882,7 @@ export class Session { type: "project.add.response", payload: { requestId: request.requestId, - project: this.buildProjectDescriptor(project), + project: await this.buildProjectDescriptor(project), error: null, }, }); @@ -5852,7 +5916,7 @@ export class Session { payload: { requestId: request.requestId, directoryPath: result.directoryPath, - project: this.buildProjectDescriptor(result.project), + project: await this.buildProjectDescriptor(result.project), error: null, errorCode: null, }, @@ -6017,7 +6081,7 @@ export class Session { requestId: request.requestId, repo: repo.displayName, checkoutPath, - project: this.buildProjectDescriptor(project), + project: await this.buildProjectDescriptor(project), error: null, }, }); diff --git a/packages/server/src/server/session.workspace-git-watch.test.ts b/packages/server/src/server/session.workspace-git-watch.test.ts index 9f92e6f228..d17586223b 100644 --- a/packages/server/src/server/session.workspace-git-watch.test.ts +++ b/packages/server/src/server/session.workspace-git-watch.test.ts @@ -205,7 +205,7 @@ function createSessionForWorkspaceGitWatchTests(options?: { onMessage: (message) => emitted.push(message as { type: string; payload: unknown }), logger: createStub<pino.Logger>(logger), downloadTokenStore: createStub<SessionOptions["downloadTokenStore"]>({}), - pushTokenStore: createStub<SessionOptions["pushTokenStore"]>({}), + pushNotifications: createStub<SessionOptions["pushNotifications"]>({}), paseoHome: "/tmp/paseo-test", agentManager: createStub<SessionOptions["agentManager"]>({ subscribe: () => () => {}, diff --git a/packages/server/src/server/session.workspace-resolution-invariants.test.ts b/packages/server/src/server/session.workspace-resolution-invariants.test.ts index fdc3a8aa42..911890fdc8 100644 --- a/packages/server/src/server/session.workspace-resolution-invariants.test.ts +++ b/packages/server/src/server/session.workspace-resolution-invariants.test.ts @@ -93,7 +93,7 @@ function createHarness(input: { onMessage: (m) => emitted.push(m), logger: createStub<SessionOptions["logger"]>(logger), downloadTokenStore: createStub<SessionOptions["downloadTokenStore"]>({}), - pushTokenStore: createStub<SessionOptions["pushTokenStore"]>({}), + pushNotifications: createStub<SessionOptions["pushNotifications"]>({}), paseoHome: mkdtempSync(path.join(tmpdir(), "paseo-invariant-test-")), agentManager: createStub<SessionOptions["agentManager"]>({ subscribe: () => () => {}, @@ -159,9 +159,7 @@ function createHarness(input: { }, }), filesystem: { isDirectory: async () => true }, - chatService: createStub<SessionOptions["chatService"]>({}), scheduleService: createStub<SessionOptions["scheduleService"]>({}), - loopService: createStub<SessionOptions["loopService"]>({}), checkoutDiffManager: createStub<SessionOptions["checkoutDiffManager"]>({ subscribe: async () => ({ initial: { cwd: "/tmp", files: [], error: null }, diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index 63c815a765..a26169f3fe 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -62,10 +62,8 @@ import { asAgentManager, asAgentStorage, asDownloadTokenStore, - asPushTokenStore, - asChatService, + asPushNotifications, asScheduleService, - asLoopService, asCheckoutDiffManager, asDaemonConfigStore, asTerminalManager, @@ -667,7 +665,7 @@ function createSessionForWorkspaceTests( onWorkspaceRecovered: options.onWorkspaceRecovered, logger: asSessionLogger(logger), downloadTokenStore: asDownloadTokenStore(), - pushTokenStore: asPushTokenStore(), + pushNotifications: asPushNotifications(), paseoHome: options.paseoHome ?? "/tmp/paseo-test", worktreesRoot: options.worktreesRoot, agentManager, @@ -741,9 +739,7 @@ function createSessionForWorkspaceTests( removeWorkspaceRecord: (workspaceId) => workspaceRegistry.remove(workspaceId), }), filesystem: { isDirectory: async () => true }, - chatService: asChatService(), scheduleService: asScheduleService(), - loopService: asLoopService(), checkoutDiffManager: asCheckoutDiffManager({ subscribe: async () => ({ initial: { cwd: "/tmp", files: [], error: null }, @@ -793,6 +789,55 @@ function createSessionForWorkspaceTests( return session; } +test("project.list.request catches up by sequence on the existing RPC", async () => { + const emitted: SessionOutboundMessage[] = []; + let project = createPersistedProjectRecord({ + projectId: "project-sequenced", + rootPath: "/repo/sequenced", + kind: "git", + displayName: "Before", + createdAt: "2026-08-12T08:00:00.000Z", + updatedAt: "2026-08-12T08:00:00.000Z", + }); + const session = createSessionForWorkspaceTests({ + onMessage: (message) => emitted.push(message), + projectRegistry: { + initialize: async () => {}, + existsOnDisk: async () => true, + list: async () => [project], + get: async () => project, + getOrCreateActiveByRoot: async () => project, + upsert: async () => {}, + archive: async () => {}, + remove: async () => {}, + }, + }); + + await session.handleMessage({ + type: "project.list.request", + requestId: "initial", + sync: {}, + }); + const initial = findByType(emitted, "project.list.response")?.payload; + expect(initial?.sync).toMatchObject({ mode: "snapshot", headSeq: 1 }); + + project = { ...project, displayName: "After", updatedAt: "2026-08-12T08:01:00.000Z" }; + await session.handleMessage({ + type: "project.list.request", + requestId: "catch-up", + sync: { + generation: initial?.sync?.generation, + afterSeq: initial?.sync?.headSeq, + }, + }); + const responses = filterByType(emitted, "project.list.response"); + expect(responses.at(-1)?.payload).toMatchObject({ + requestId: "catch-up", + projects: [{ projectId: "project-sequenced", projectDisplayName: "After", syncSeq: 2 }], + sync: { mode: "changes", headSeq: 2, removals: [] }, + }); +}); + test("agent updates preserve queued live transitions across stored metadata reads", async () => { const running = makeManagedAgent({ id: "agent-coherent", @@ -1008,15 +1053,13 @@ test("create_agent_request keeps requested child cwd when grouped under an exist onMessage: (message) => emitted.push(message), logger: asSessionLogger(logger), downloadTokenStore: asDownloadTokenStore(), - pushTokenStore: asPushTokenStore(), + pushNotifications: asPushNotifications(), paseoHome: path.join(workdir, "paseo-home"), agentManager, agentStorage, projectRegistry, workspaceRegistry, - chatService: asChatService(), scheduleService: asScheduleService(), - loopService: asLoopService(), checkoutDiffManager: asCheckoutDiffManager({ subscribe: async () => ({ initial: { cwd: child, files: [], error: null }, @@ -1194,16 +1237,14 @@ test("create_agent_request launches from an exact subdirectory in a created work onMessage: (message) => emitted.push(message), logger: asSessionLogger(logger), downloadTokenStore: asDownloadTokenStore(), - pushTokenStore: asPushTokenStore(), + pushNotifications: asPushNotifications(), paseoHome: path.join(workdir, "paseo-home"), agentManager, agentStorage, projectRegistry, workspaceRegistry, workspaceRuntime, - chatService: asChatService(), scheduleService: asScheduleService(), - loopService: asLoopService(), checkoutDiffManager: asCheckoutDiffManager({ subscribe: async () => ({ initial: { cwd: child, files: [], error: null }, @@ -1335,15 +1376,13 @@ test("create_agent_request does not title an existing workspace from the agent p onMessage: vi.fn(), logger: asSessionLogger(logger), downloadTokenStore: asDownloadTokenStore(), - pushTokenStore: asPushTokenStore(), + pushNotifications: asPushNotifications(), paseoHome: path.join(workdir, "paseo-home"), agentManager, agentStorage, projectRegistry, workspaceRegistry, - chatService: asChatService(), scheduleService: asScheduleService(), - loopService: asLoopService(), checkoutDiffManager: asCheckoutDiffManager({ subscribe: async () => ({ initial: { cwd, files: [], error: null }, @@ -1605,7 +1644,7 @@ test("archive emits an authoritative agent_update upsert for subscribed clients" onMessage: (message) => emitted.push(message), logger: asSessionLogger(logger), downloadTokenStore: asDownloadTokenStore(), - pushTokenStore: asPushTokenStore(), + pushNotifications: asPushNotifications(), paseoHome: "/tmp/paseo-test", agentManager: asAgentManager({ subscribe: () => () => {}, @@ -1672,9 +1711,7 @@ test("archive emits an authoritative agent_update upsert for subscribed clients" remove: async () => {}, }; })(), - chatService: asChatService(), scheduleService: asScheduleService(), - loopService: asLoopService(), checkoutDiffManager: asCheckoutDiffManager({ subscribe: async () => ({ initial: { cwd: REPO_CWD, files: [], error: null }, @@ -1972,7 +2009,7 @@ test("close_items_request archives agents and kills terminals in one batch", asy onMessage: (message) => emitted.push(message), logger: asSessionLogger(sessionLogger), downloadTokenStore: asDownloadTokenStore(), - pushTokenStore: asPushTokenStore(), + pushNotifications: asPushNotifications(), paseoHome: "/tmp/paseo-test", agentManager: asAgentManager({ subscribe: () => () => {}, @@ -2034,9 +2071,7 @@ test("close_items_request archives agents and kills terminals in one batch", asy remove: async () => {}, }; })(), - chatService: asChatService(), scheduleService: asScheduleService(), - loopService: asLoopService(), checkoutDiffManager: asCheckoutDiffManager({ subscribe: async () => ({ initial: { cwd: "/tmp", files: [], error: null }, @@ -2142,7 +2177,7 @@ test("close_items_request archives stored agents that are not currently loaded", onMessage: (message) => emitted.push(message), logger: asSessionLogger(sessionLogger), downloadTokenStore: asDownloadTokenStore(), - pushTokenStore: asPushTokenStore(), + pushNotifications: asPushNotifications(), paseoHome: "/tmp/paseo-test", agentManager: asAgentManager({ subscribe: () => () => {}, @@ -2221,9 +2256,7 @@ test("close_items_request archives stored agents that are not currently loaded", remove: async () => {}, }; })(), - chatService: asChatService(), scheduleService: asScheduleService(), - loopService: asLoopService(), checkoutDiffManager: asCheckoutDiffManager({ subscribe: async () => ({ initial: { cwd: "/tmp", files: [], error: null }, @@ -2303,7 +2336,7 @@ test("close_items_request continues after an archive failure", async () => { onMessage: (message) => emitted.push(message), logger: asSessionLogger(sessionLogger), downloadTokenStore: asDownloadTokenStore(), - pushTokenStore: asPushTokenStore(), + pushNotifications: asPushNotifications(), paseoHome: "/tmp/paseo-test", agentManager: asAgentManager({ subscribe: () => () => {}, @@ -2370,9 +2403,7 @@ test("close_items_request continues after an archive failure", async () => { remove: async () => {}, }; })(), - chatService: asChatService(), scheduleService: asScheduleService(), - loopService: asLoopService(), checkoutDiffManager: asCheckoutDiffManager({ subscribe: async () => ({ initial: { cwd: "/tmp", files: [], error: null }, @@ -3566,7 +3597,7 @@ test("workspace update stream keeps persisted workspace visible after agents sto onMessage: (message) => emitted.push(message), logger: asSessionLogger(logger), downloadTokenStore: asDownloadTokenStore(), - pushTokenStore: asPushTokenStore(), + pushNotifications: asPushNotifications(), paseoHome: "/tmp/paseo-test", agentManager: asAgentManager({ subscribe: () => () => {}, @@ -3616,9 +3647,7 @@ test("workspace update stream keeps persisted workspace visible after agents sto archive: async () => {}, remove: async () => {}, }, - chatService: asChatService(), scheduleService: asScheduleService(), - loopService: asLoopService(), checkoutDiffManager: asCheckoutDiffManager({ subscribe: async () => ({ initial: { cwd: "/tmp", files: [], error: null }, @@ -7886,15 +7915,17 @@ const ICON_PNG_1X1 = Buffer.from([ test("project.icon.set.request publishes a custom icon that project.icon.get serves back", async () => { const emitted: SessionOutboundMessage[] = []; + const tempDir = realpathSync(mkdtempSync(path.join(tmpdir(), "session-project-icon-test-"))); const session = asTestSession( - createSessionForWorkspaceTests({ onMessage: (message) => emitted.push(message) }), + createSessionForWorkspaceTests({ + paseoHome: path.join(tempDir, "paseo-home"), + onMessage: (message) => emitted.push(message), + }), ); session.updateClientCapabilities({ [CLIENT_CAPS.projectUpdates]: true }); - const tempDir = realpathSync(mkdtempSync(path.join(tmpdir(), "session-project-icon-test-"))); const projectRoot = path.join(tempDir, "project-without-icons"); mkdirSync(projectRoot, { recursive: true }); - session.paseoHome = path.join(tempDir, "paseo-home"); const project = createPersistedProjectRecord({ projectId: "prj_icon", diff --git a/packages/server/src/server/session/agent-config/agent-config-session.test.ts b/packages/server/src/server/session/agent-config/agent-config-session.test.ts index f6d35a1f3d..1592c5bb3d 100644 --- a/packages/server/src/server/session/agent-config/agent-config-session.test.ts +++ b/packages/server/src/server/session/agent-config/agent-config-session.test.ts @@ -14,6 +14,8 @@ class FakeAgentConfigOperations implements AgentConfigOperations { readonly modelCalls: Array<{ agentId: string; modelId: string | null }> = []; readonly featureCalls: Array<{ agentId: string; featureId: string; value: unknown }> = []; readonly thinkingCalls: Array<{ agentId: string; thinkingOptionId: string | null }> = []; + /** Cross-operation ordering, which the per-operation arrays above cannot show. */ + readonly callLog: string[] = []; modeNotice: AgentProviderNotice | null = null; thinkingNotice: AgentProviderNotice | null = null; loadFailure: Error | null = null; @@ -26,17 +28,20 @@ class FakeAgentConfigOperations implements AgentConfigOperations { async setMode(agentId: string, modeId: string): Promise<AgentProviderNotice | null> { this.modeCalls.push({ agentId, modeId }); + this.callLog.push("mode"); if (this.failWith) throw this.failWith; return this.modeNotice; } async setModel(agentId: string, modelId: string | null): Promise<void> { this.modelCalls.push({ agentId, modelId }); + this.callLog.push("model"); if (this.failWith) throw this.failWith; } async setFeature(agentId: string, featureId: string, value: unknown): Promise<void> { this.featureCalls.push({ agentId, featureId, value }); + this.callLog.push(`feature:${featureId}`); if (this.failWith) throw this.failWith; } @@ -45,6 +50,7 @@ class FakeAgentConfigOperations implements AgentConfigOperations { thinkingOptionId: string | null, ): Promise<AgentProviderNotice | null> { this.thinkingCalls.push({ agentId, thinkingOptionId }); + this.callLog.push("thinking"); if (this.failWith) throw this.failWith; return this.thinkingNotice; } @@ -302,4 +308,123 @@ describe("AgentConfigSession", () => { payload: { requestId: "req-1", agentId: "agent-1", accepted: false, error: "thinking boom" }, }); }); + + test("apply: sets model before thinking and features last, in one accepted response", async () => { + const { subsystem, emitted, operations } = makeSubsystem(); + + await subsystem.handleAgentConfigApplyRequest({ + type: "agent.config.apply.request", + agentId: "agent-1", + config: { + modelId: "opus-5", + modeId: "plan", + thinkingOptionId: "high", + featureValues: { webSearch: true }, + }, + requestId: "req-1", + }); + + expect(operations.callLog).toEqual(["model", "mode", "thinking", "feature:webSearch"]); + expect(operations.modelCalls).toEqual([{ agentId: "agent-1", modelId: "opus-5" }]); + expect(operations.modeCalls).toEqual([{ agentId: "agent-1", modeId: "plan" }]); + expect(operations.thinkingCalls).toEqual([{ agentId: "agent-1", thinkingOptionId: "high" }]); + expect(operations.featureCalls).toEqual([ + { agentId: "agent-1", featureId: "webSearch", value: true }, + ]); + expect(operations.loadedAgentIds).toEqual(["agent-1"]); + expect(emitted).toEqual([ + { + type: "agent.config.apply.response", + payload: { + requestId: "req-1", + agentId: "agent-1", + accepted: true, + error: null, + notice: null, + }, + }, + ]); + }); + + test("apply: leaves omitted fields alone", async () => { + const { subsystem, operations } = makeSubsystem(); + + await subsystem.handleAgentConfigApplyRequest({ + type: "agent.config.apply.request", + agentId: "agent-1", + config: { modelId: "haiku-4.5" }, + requestId: "req-1", + }); + + expect(operations.callLog).toEqual(["model"]); + }); + + test("apply: clears model and thinking when they are explicitly null", async () => { + const { subsystem, operations } = makeSubsystem(); + + await subsystem.handleAgentConfigApplyRequest({ + type: "agent.config.apply.request", + agentId: "agent-1", + config: { modelId: null, thinkingOptionId: null }, + requestId: "req-1", + }); + + expect(operations.modelCalls).toEqual([{ agentId: "agent-1", modelId: null }]); + expect(operations.thinkingCalls).toEqual([{ agentId: "agent-1", thinkingOptionId: null }]); + }); + + test("apply: reports the first notice and still runs the rest of the bundle", async () => { + const { subsystem, emitted, operations } = makeSubsystem(); + operations.modeNotice = { type: "info", message: "Switched to plan mode" }; + operations.thinkingNotice = { type: "warning", message: "Thinking budget reduced" }; + + await subsystem.handleAgentConfigApplyRequest({ + type: "agent.config.apply.request", + agentId: "agent-1", + config: { modeId: "plan", thinkingOptionId: "high", featureValues: { webSearch: true } }, + requestId: "req-1", + }); + + expect(operations.callLog).toEqual(["mode", "thinking", "feature:webSearch"]); + expect(emitted).toEqual([ + { + type: "agent.config.apply.response", + payload: { + requestId: "req-1", + agentId: "agent-1", + accepted: true, + error: null, + notice: { type: "info", message: "Switched to plan mode" }, + }, + }, + ]); + }); + + test("apply: a mid-bundle failure rejects and leaves the earlier steps applied", async () => { + const { subsystem, emitted, operations } = makeSubsystem(); + operations.failWith = new Error("apply boom"); + + await subsystem.handleAgentConfigApplyRequest({ + type: "agent.config.apply.request", + agentId: "agent-1", + config: { modelId: "opus-5", modeId: "plan" }, + requestId: "req-1", + }); + + expect(operations.callLog).toEqual(["model"]); + expect(emitted.map((m) => m.type)).toEqual(["activity_log", "agent.config.apply.response"]); + expect(emitted[0]).toEqual({ + type: "activity_log", + payload: { + id: expect.any(String), + timestamp: expect.any(Date), + type: "error", + content: "Failed to apply agent config: apply boom", + }, + }); + expect(emitted[1]).toEqual({ + type: "agent.config.apply.response", + payload: { requestId: "req-1", agentId: "agent-1", accepted: false, error: "apply boom" }, + }); + }); }); diff --git a/packages/server/src/server/session/agent-config/agent-config-session.ts b/packages/server/src/server/session/agent-config/agent-config-session.ts index dab811b980..8cf2394192 100644 --- a/packages/server/src/server/session/agent-config/agent-config-session.ts +++ b/packages/server/src/server/session/agent-config/agent-config-session.ts @@ -1,12 +1,13 @@ import type pino from "pino"; import { v4 as uuidv4 } from "uuid"; import { getErrorMessage, getErrorMessageOr } from "@getpaseo/protocol/error-utils"; +import type { AgentConfigApply } from "@getpaseo/protocol/messages"; import type { AgentProviderNotice } from "../../agent/agent-sdk-types.js"; import type { SessionInboundMessage, SessionOutboundMessage } from "../../messages.js"; /** - * The four agent-config response messages share one payload shape; deriving the - * type keeps it pinned to the protocol schema instead of restating it. + * The agent-config response messages share one payload shape; deriving the type + * keeps it pinned to the protocol schema instead of restating it. */ type AgentActionResponsePayload = Extract< SessionOutboundMessage, @@ -51,11 +52,11 @@ interface ConfigChange { } /** - * A client's per-agent config surface: set mode, model, feature, and thinking - * option. Each request shares one envelope — log, run the mutation, then emit the - * accepted response, or on failure emit an activity_log error frame followed by - * the rejected response. Reaches no state beyond the injected operations and the - * outbound channel. + * A client's per-agent config surface: set mode, model, feature, or thinking + * option one at a time, or apply all of them together. Each request shares one + * envelope — log, run the mutation, then emit the accepted response, or on + * failure emit an activity_log error frame followed by the rejected response. + * Reaches no state beyond the injected operations and the outbound channel. */ export class AgentConfigSession { private readonly host: AgentConfigSessionHost; @@ -134,6 +135,67 @@ export class AgentConfigSession { }); } + handleAgentConfigApplyRequest( + msg: Extract<SessionInboundMessage, { type: "agent.config.apply.request" }>, + ): Promise<void> { + const { agentId, config, requestId } = msg; + return this.applyConfigChange({ + agentId, + requestId, + logLabel: "agent.config.apply.request", + logFields: { agentId, requestId, config }, + failureText: "Failed to apply agent config", + run: () => this.applyBundle(agentId, config), + emitResponse: (payload) => this.host.emit({ type: "agent.config.apply.response", payload }), + }); + } + + /** + * Model first, because a thinking option only means anything against the model + * that offers it; features last so they settle on top of the rest. An omitted + * field is left alone. The first notice produced is the one the client sees — + * later steps still run, so a notice never truncates the bundle. + * + * This is not a transaction. A step that rejects aborts the rest, and steps + * that already applied stay applied — provider setters validate against the + * agent's live model, so a bundle that omits `modelId` can have its mode land + * and its feature rejected. Nothing is hidden by that: every AgentManager + * setter emits agent state before returning, so what applied is already + * streamed to the client alongside the rejected response and the error frame. + * + * Rolling back would re-invoke the setters that just failed, most often + * against a session that is already gone. Pre-flight validation would restate + * each provider's model-dependent checks somewhere they would drift from the + * setters that own them. What sending the bundle in one request does remove is + * the multi-round-trip failure class: interruption between hops, and + * interleaving with another client's mutation. + */ + private async applyBundle( + agentId: string, + config: AgentConfigApply, + ): Promise<AgentProviderNotice | null> { + let notice: AgentProviderNotice | null = null; + + if (config.modelId !== undefined) { + await this.operations.setModel(agentId, config.modelId); + } + if (config.modeId !== undefined) { + // Await first, assign second: `notice ??= await ...` would skip the call + // entirely once an earlier step produced a notice. + const modeNotice = await this.operations.setMode(agentId, config.modeId); + notice ??= modeNotice; + } + if (config.thinkingOptionId !== undefined) { + const thinkingNotice = await this.operations.setThinking(agentId, config.thinkingOptionId); + notice ??= thinkingNotice; + } + for (const [featureId, value] of Object.entries(config.featureValues ?? {})) { + await this.operations.setFeature(agentId, featureId, value); + } + + return notice; + } + private async applyConfigChange(change: ConfigChange): Promise<void> { const { agentId, requestId, logLabel, logFields, failureText, run, emitResponse } = change; this.logger.info(logFields, `session: ${logLabel}`); diff --git a/packages/server/src/server/session/agent-updates/agent-updates-service.test.ts b/packages/server/src/server/session/agent-updates/agent-updates-service.test.ts index 29da3106f0..66d58042bf 100644 --- a/packages/server/src/server/session/agent-updates/agent-updates-service.test.ts +++ b/packages/server/src/server/session/agent-updates/agent-updates-service.test.ts @@ -8,6 +8,7 @@ import type { } from "../../messages.js"; import type { ManagedAgent } from "../../agent/agent-manager.js"; import type { StoredAgentRecord } from "../../agent/agent-storage.js"; +import { DirectorySyncService } from "../../directory-sync/index.js"; // No mocks — every dependency is an injected in-memory fake. The agent payloads // are supplied through the fake builders, so each test fully controls the @@ -100,6 +101,7 @@ function buildHarness() { let providerVisible: (provider: string) => boolean = () => true; let buildAgentPayloadError: Error | null = null; let enrichProjectedPayload = false; + const directorySync = new DirectorySyncService("test-generation"); const service = createAgentUpdatesService({ emit: (message) => emitted.push(message), @@ -133,6 +135,13 @@ function buildHarness() { emitWorkspaceUpdateForWorkspaceId: async (workspaceId) => { workspaceUpdates.push(workspaceId); }, + sequenceAgentUpdate: (payload, agent, project, agentId, includeSequence) => + directorySync.sequenceAgentUpdate( + payload, + agent && project ? { agent, project } : null, + agentId, + includeSequence, + ), logger: { error: (...args: unknown[]) => loggedErrors.push(args) } as unknown as pino.Logger, }); diff --git a/packages/server/src/server/session/agent-updates/agent-updates-service.ts b/packages/server/src/server/session/agent-updates/agent-updates-service.ts index ac4f3c8d7a..763275a395 100644 --- a/packages/server/src/server/session/agent-updates/agent-updates-service.ts +++ b/packages/server/src/server/session/agent-updates/agent-updates-service.ts @@ -16,6 +16,7 @@ type AgentUpdatesFilter = NonNullable< interface AgentUpdatesSubscriptionState { subscriptionId: string; + syncEnabled?: boolean; filter?: AgentUpdatesFilter; isBootstrapping: boolean; pendingUpdatesByAgentId: Map<string, AgentUpdatePayload>; @@ -37,7 +38,11 @@ interface AgentUpdatesSubscriptionState { * stay consistent. */ export interface AgentUpdatesService { - beginSubscription(input: { subscriptionId: string; filter?: AgentUpdatesFilter }): void; + beginSubscription(input: { + subscriptionId: string; + filter?: AgentUpdatesFilter; + syncEnabled?: boolean; + }): void; flushBootstrapped( subscriptionId: string, options?: { snapshotUpdatedAtByAgentId?: Map<string, number> }, @@ -57,6 +62,13 @@ export interface AgentUpdatesServiceDeps { isProviderVisibleToClient(provider: string): boolean; buildProjectPlacementForWorkspaceId(workspaceId: string): Promise<ProjectPlacementPayload | null>; emitWorkspaceUpdateForWorkspaceId(workspaceId: string): Promise<void>; + sequenceAgentUpdate<T extends AgentUpdatePayload>( + payload: T, + agent: AgentSnapshotPayload | null, + project: ProjectPlacementPayload | null, + agentId: string, + includeSequence: boolean, + ): T; logger: pino.Logger; } @@ -151,6 +163,13 @@ function agentUpdateTargetId(update: AgentUpdatePayload): string { export function createAgentUpdatesService(deps: AgentUpdatesServiceDeps): AgentUpdatesService { let subscription: AgentUpdatesSubscriptionState | null = null; const liveAgentUpdateTails = new Map<string, Promise<void>>(); + const sequence = <T extends AgentUpdatePayload>( + sub: AgentUpdatesSubscriptionState, + payload: T, + agent: AgentSnapshotPayload | null, + project: ProjectPlacementPayload | null, + agentId: string, + ) => deps.sequenceAgentUpdate(payload, agent, project, agentId, sub.syncEnabled === true); function bufferOrEmit(sub: AgentUpdatesSubscriptionState, payload: AgentUpdatePayload): void { if (payload.kind === "upsert" && !deps.isProviderVisibleToClient(payload.agent.provider)) { @@ -167,9 +186,14 @@ export function createAgentUpdatesService(deps: AgentUpdatesServiceDeps): AgentU }); } - function beginSubscription(input: { subscriptionId: string; filter?: AgentUpdatesFilter }): void { + function beginSubscription(input: { + subscriptionId: string; + filter?: AgentUpdatesFilter; + syncEnabled?: boolean; + }): void { subscription = { subscriptionId: input.subscriptionId, + syncEnabled: input.syncEnabled, filter: input.filter, isBootstrapping: true, pendingUpdatesByAgentId: new Map(), @@ -230,10 +254,10 @@ export function createAgentUpdatesService(deps: AgentUpdatesServiceDeps): AgentU ? await deps.buildProjectPlacementForWorkspaceId(payload.workspaceId) : null; if (!project) { - bufferOrEmit(sub, { - kind: "remove", - agentId: payload.id, - }); + bufferOrEmit( + sub, + sequence(sub, { kind: "remove", agentId: payload.id }, null, null, payload.id), + ); return payload; } @@ -244,16 +268,22 @@ export function createAgentUpdatesService(deps: AgentUpdatesServiceDeps): AgentU }); bufferOrEmit( sub, - matches - ? { - kind: "upsert", - agent: payload, - project, - } - : { - kind: "remove", - agentId: payload.id, - }, + sequence( + sub, + matches + ? { + kind: "upsert", + agent: payload, + project, + } + : { + kind: "remove", + agentId: payload.id, + }, + payload, + project, + payload.id, + ), ); return payload; } @@ -267,10 +297,10 @@ export function createAgentUpdatesService(deps: AgentUpdatesServiceDeps): AgentU ? await deps.buildProjectPlacementForWorkspaceId(payload.workspaceId) : null; if (!project) { - bufferOrEmit(sub, { - kind: "remove", - agentId: payload.id, - }); + bufferOrEmit( + sub, + sequence(sub, { kind: "remove", agentId: payload.id }, null, null, payload.id), + ); } else { const matches = matchesAgentUpdatesFilter({ agent: payload, @@ -279,16 +309,34 @@ export function createAgentUpdatesService(deps: AgentUpdatesServiceDeps): AgentU }); if (matches) { - bufferOrEmit(sub, { - kind: "upsert", - agent: payload, - project, - }); + bufferOrEmit( + sub, + sequence( + sub, + { + kind: "upsert", + agent: payload, + project, + }, + payload, + project, + payload.id, + ), + ); } else { - bufferOrEmit(sub, { - kind: "remove", - agentId: payload.id, - }); + bufferOrEmit( + sub, + sequence( + sub, + { + kind: "remove", + agentId: payload.id, + }, + payload, + project, + payload.id, + ), + ); } } } @@ -326,7 +374,10 @@ export function createAgentUpdatesService(deps: AgentUpdatesServiceDeps): AgentU function removeAgent(agentId: string): Promise<void> { return enqueueAgentUpdate(agentId, () => { if (subscription) { - bufferOrEmit(subscription, { kind: "remove", agentId }); + bufferOrEmit( + subscription, + sequence(subscription, { kind: "remove", agentId }, null, null, agentId), + ); } }); } diff --git a/packages/server/src/server/session/chat/chat-schedule-loop-session.test.ts b/packages/server/src/server/session/chat/chat-schedule-loop-session.test.ts deleted file mode 100644 index a121d967e3..0000000000 --- a/packages/server/src/server/session/chat/chat-schedule-loop-session.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { describe, expect, it } from "vitest"; -import pino from "pino"; -import { - ChatScheduleLoopSession, - type ChatScheduleLoopSessionHost, -} from "./chat-schedule-loop-session.js"; -import { createStub } from "../../test-utils/class-mocks.js"; -import { findByType } from "../../test-utils/session-stubs.js"; -import type { SessionOutboundMessage } from "../../messages.js"; -import type { FileBackedChatService } from "../../chat/chat-service.js"; -import type { ScheduleService } from "../../schedule/service.js"; -import type { LoopService } from "../../loop-service.js"; - -type ChatMessageFixture = Awaited<ReturnType<FileBackedChatService["dispatchMessage"]>>; - -interface MakeOptions { - chat?: { [K in keyof FileBackedChatService]?: unknown }; - schedule?: { [K in keyof ScheduleService]?: unknown }; - loop?: { [K in keyof LoopService]?: unknown }; - host?: Partial<ChatScheduleLoopSessionHost>; -} - -function makeSubsystem(options: MakeOptions = {}) { - const emitted: SessionOutboundMessage[] = []; - const sentAgentMessages: Array<{ agentId: string; text: string }> = []; - let onSend: (() => void) | null = null; - const host: ChatScheduleLoopSessionHost = { - emit: (msg) => emitted.push(msg), - listStoredAgents: async () => [], - listLiveAgents: () => [], - resolveAgentIdentifier: async (identifier) => ({ ok: true, agentId: identifier }), - sendAgentMessage: async (agentId, text) => { - sentAgentMessages.push({ agentId, text }); - onSend?.(); - }, - ...options.host, - }; - const subsystem = new ChatScheduleLoopSession({ - host, - chatService: createStub<FileBackedChatService>(options.chat ?? {}), - scheduleService: createStub<ScheduleService>(options.schedule ?? {}), - loopService: createStub<LoopService>(options.loop ?? {}), - clientId: "client-1", - logger: pino({ level: "silent" }), - }); - // notifyChatMentions is fire-and-forget; arm the signal before dispatching so the - // mentioned-agent send is observed deterministically without polling. - function waitForSend(): Promise<void> { - return new Promise((resolve) => { - onSend = resolve; - }); - } - return { subsystem, emitted, sentAgentMessages, waitForSend }; -} - -describe("ChatScheduleLoopSession", () => { - it("chat/post emits the stored message and does not fan out without mentions", async () => { - const message: ChatMessageFixture = { - id: "m1", - roomId: "r1", - authorAgentId: "client-1", - body: "hello", - replyToMessageId: null, - mentionAgentIds: [], - createdAt: "2026-01-01T00:00:00.000Z", - }; - const { subsystem, emitted, sentAgentMessages } = makeSubsystem({ - chat: { dispatchMessage: async () => message, listRoomPosterAgentIds: async () => [] }, - }); - - await subsystem.handleChatPostRequest({ - type: "chat/post", - requestId: "p1", - room: "r1", - body: "hello", - }); - - const res = findByType(emitted, "chat/post/response"); - expect(res?.payload.message).toEqual(message); - expect(res?.payload.error).toBeNull(); - expect(sentAgentMessages).toEqual([]); - }); - - it("chat/post notifies a mentioned agent through the host send seam", async () => { - const message: ChatMessageFixture = { - id: "m2", - roomId: "r1", - authorAgentId: "client-1", - body: "@agent-2 ping", - replyToMessageId: null, - mentionAgentIds: ["agent-2"], - createdAt: "2026-01-01T00:00:00.000Z", - }; - const { subsystem, emitted, sentAgentMessages, waitForSend } = makeSubsystem({ - chat: { dispatchMessage: async () => message, listRoomPosterAgentIds: async () => [] }, - }); - - const sent = waitForSend(); - await subsystem.handleChatPostRequest({ - type: "chat/post", - requestId: "p2", - room: "r1", - body: "@agent-2 ping", - }); - await sent; - - expect(findByType(emitted, "chat/post/response")?.payload.error).toBeNull(); - expect(sentAgentMessages).toHaveLength(1); - expect(sentAgentMessages[0]?.agentId).toBe("agent-2"); - expect(sentAgentMessages[0]?.text).toContain('in room "r1"'); - }); - - it("chat/post rejects @everyone past the fanout limit with the chat error code", async () => { - const posters = Array.from({ length: 26 }, (_, i) => `poster-${i}`); - const { subsystem, emitted } = makeSubsystem({ - chat: { listRoomPosterAgentIds: async () => posters }, - }); - - await subsystem.handleChatPostRequest({ - type: "chat/post", - requestId: "p3", - room: "r1", - body: "@everyone go", - }); - - const err = findByType(emitted, "rpc_error"); - expect(err?.payload.code).toBe("chat_mention_fanout_limit_exceeded"); - expect(err?.payload.requestId).toBe("p3"); - }); - - it("schedule/create returns a summary with the runs stripped", async () => { - const stored = { - id: "s1", - name: null, - prompt: "p", - cadence: { type: "every" as const, everyMs: 1000 }, - target: { type: "agent" as const, agentId: "a" }, - status: "active" as const, - createdAt: "2026-01-01T00:00:00.000Z", - updatedAt: "2026-01-01T00:00:00.000Z", - nextRunAt: null, - lastRunAt: null, - pausedAt: null, - expiresAt: null, - maxRuns: null, - runs: [ - { - id: "run-1", - scheduledFor: "2026-01-01T00:00:00.000Z", - startedAt: "2026-01-01T00:00:00.000Z", - endedAt: null, - status: "running" as const, - agentId: null, - output: null, - error: null, - }, - ], - }; - const { subsystem, emitted } = makeSubsystem({ schedule: { create: async () => stored } }); - - await subsystem.handleScheduleCreateRequest({ - type: "schedule/create", - requestId: "sc1", - prompt: "p", - cadence: { type: "every", everyMs: 1000 }, - target: { type: "agent", agentId: "a" }, - }); - - const res = findByType(emitted, "schedule/create/response"); - expect(res?.payload.schedule).toBeDefined(); - expect(res?.payload.schedule).not.toHaveProperty("runs"); - expect(res?.payload.schedule.id).toBe("s1"); - }); - - it("schedule/create remaps a self target to an agent target before creating", async () => { - let received: Parameters<ScheduleService["create"]>[0] | undefined; - const stored = { - id: "s2", - name: null, - prompt: "p", - cadence: { type: "every" as const, everyMs: 1000 }, - target: { type: "agent" as const, agentId: "agent-9" }, - status: "active" as const, - createdAt: "2026-01-01T00:00:00.000Z", - updatedAt: "2026-01-01T00:00:00.000Z", - nextRunAt: null, - lastRunAt: null, - pausedAt: null, - expiresAt: null, - maxRuns: null, - runs: [], - }; - const { subsystem, emitted } = makeSubsystem({ - schedule: { - create: async (input: Parameters<ScheduleService["create"]>[0]) => { - received = input; - return stored; - }, - }, - }); - - await subsystem.handleScheduleCreateRequest({ - type: "schedule/create", - requestId: "sc2", - prompt: "p", - cadence: { type: "every", everyMs: 1000 }, - target: { type: "self", agentId: "agent-9" }, - }); - - expect(received?.target).toEqual({ type: "agent", agentId: "agent-9" }); - expect(findByType(emitted, "schedule/create/response")?.payload.error).toBeNull(); - }); - - it("loop/run emits the loop summary from the loop service", async () => { - const loop = { id: "loop-1", status: "running" }; - const { subsystem, emitted } = makeSubsystem({ loop: { runLoop: async () => loop } }); - - await subsystem.handleLoopRunRequest({ - type: "loop/run", - requestId: "l1", - prompt: "p", - cwd: "/tmp/loop", - }); - - const res = findByType(emitted, "loop/run/response"); - expect(res?.payload.loop).toEqual(loop); - }); -}); diff --git a/packages/server/src/server/session/chat/chat-schedule-loop-session.ts b/packages/server/src/server/session/chat/chat-schedule-loop-session.ts deleted file mode 100644 index 70a2417a5b..0000000000 --- a/packages/server/src/server/session/chat/chat-schedule-loop-session.ts +++ /dev/null @@ -1,609 +0,0 @@ -import type pino from "pino"; -import type { SessionInboundMessage, SessionOutboundMessage } from "../../messages.js"; -import type { StoredAgentRecord } from "../../agent/agent-storage.js"; -import type { ManagedAgent } from "../../agent/agent-manager.js"; -import { - ChatServiceError, - type FileBackedChatService, - parseMentionAgentIds, -} from "../../chat/chat-service.js"; -import { notifyChatMentions, prepareChatMentionFanout } from "../../chat/chat-mentions.js"; -import type { LoopService } from "../../loop-service.js"; -import type { ScheduleService } from "../../schedule/service.js"; - -/** - * The collaborators a chat command reaches that are NOT part of the chat/schedule/loop - * domain: the agent roster reads and the agent-message send used only by chat/post - * mention fanout. The Session shell owns the agent lifecycle; this subsystem orchestrates - * a notification through it but does not own it. - */ -export interface ChatScheduleLoopSessionHost { - emit(msg: SessionOutboundMessage): void; - listStoredAgents(): Promise<StoredAgentRecord[]>; - listLiveAgents(): ManagedAgent[]; - resolveAgentIdentifier( - identifier: string, - ): Promise<{ ok: true; agentId: string } | { ok: false; error: string }>; - sendAgentMessage(agentId: string, text: string): Promise<void>; -} - -export interface ChatScheduleLoopSessionOptions { - host: ChatScheduleLoopSessionHost; - chatService: FileBackedChatService; - scheduleService: ScheduleService; - loopService: LoopService; - clientId: string; - logger: pino.Logger; -} - -/** - * A client's chat, schedule, and loop request surface. The three families are the - * least-coupled in the session: each is a stateless request/response over its own - * service (chat rooms, cron routines, autonomous loops), with no shared observer, - * git, or voice state and no subscriptions to tear down. They live in one subsystem - * because they are dispatched together — schedule/* was historically reached through - * the chat dispatcher's fall-through arm. The three rpc-error emitters stay separate: - * they differ by default code, and only the chat one reads ChatServiceError.code. - */ -export class ChatScheduleLoopSession { - private readonly host: ChatScheduleLoopSessionHost; - private readonly chatService: FileBackedChatService; - private readonly scheduleService: ScheduleService; - private readonly loopService: LoopService; - private readonly clientId: string; - private readonly logger: pino.Logger; - - constructor(options: ChatScheduleLoopSessionOptions) { - this.host = options.host; - this.chatService = options.chatService; - this.scheduleService = options.scheduleService; - this.loopService = options.loopService; - this.clientId = options.clientId; - this.logger = options.logger; - } - - private emitChatRpcError(request: { requestId: string; type: string }, error: unknown): void { - const message = error instanceof Error ? error.message : "Chat request failed"; - const code = error instanceof ChatServiceError ? error.code : "chat_request_failed"; - this.logger.error({ err: error, requestType: request.type }, "Chat request failed"); - this.host.emit({ - type: "rpc_error", - payload: { - requestId: request.requestId, - requestType: request.type, - error: message, - code, - }, - }); - } - - async handleChatCreateRequest( - request: Extract<SessionInboundMessage, { type: "chat/create" }>, - ): Promise<void> { - try { - const room = await this.chatService.createRoom({ - name: request.name, - purpose: request.purpose, - }); - this.host.emit({ - type: "chat/create/response", - payload: { - requestId: request.requestId, - room, - error: null, - }, - }); - } catch (error) { - this.emitChatRpcError(request, error); - } - } - - async handleChatListRequest( - request: Extract<SessionInboundMessage, { type: "chat/list" }>, - ): Promise<void> { - try { - const rooms = await this.chatService.listRooms(); - this.host.emit({ - type: "chat/list/response", - payload: { - requestId: request.requestId, - rooms, - error: null, - }, - }); - } catch (error) { - this.emitChatRpcError(request, error); - } - } - - async handleChatInspectRequest( - request: Extract<SessionInboundMessage, { type: "chat/inspect" }>, - ): Promise<void> { - try { - const result = await this.chatService.inspectRoom({ - room: request.room, - }); - this.host.emit({ - type: "chat/inspect/response", - payload: { - requestId: request.requestId, - room: result.room, - error: null, - }, - }); - } catch (error) { - this.emitChatRpcError(request, error); - } - } - - async handleChatDeleteRequest( - request: Extract<SessionInboundMessage, { type: "chat/delete" }>, - ): Promise<void> { - try { - const result = await this.chatService.deleteRoom({ - room: request.room, - }); - this.host.emit({ - type: "chat/delete/response", - payload: { - requestId: request.requestId, - room: result.room, - error: null, - }, - }); - } catch (error) { - this.emitChatRpcError(request, error); - } - } - - async handleChatPostRequest( - request: Extract<SessionInboundMessage, { type: "chat/post" }>, - ): Promise<void> { - try { - const authorAgentId = request.authorAgentId?.trim() || this.clientId; - const mentionAgentIds = parseMentionAgentIds(request.body); - const storedAgents = await this.host.listStoredAgents(); - const liveAgents = this.host.listLiveAgents(); - const fanout = await prepareChatMentionFanout({ - authorAgentId, - mentionAgentIds, - storedAgents, - liveAgents, - listRoomPosterAgentIds: () => - this.chatService.listRoomPosterAgentIds({ room: request.room }), - }); - if (!fanout.ok) { - throw new ChatServiceError("chat_mention_fanout_limit_exceeded", fanout.error); - } - const message = await this.chatService.dispatchMessage({ - room: request.room, - authorAgentId, - body: request.body, - replyToMessageId: request.replyToMessageId, - }); - this.host.emit({ - type: "chat/post/response", - payload: { - requestId: request.requestId, - message, - error: null, - }, - }); - void notifyChatMentions({ - room: request.room, - authorAgentId, - body: request.body, - mentionAgentIds: message.mentionAgentIds, - logger: this.logger, - storedAgents, - liveAgents, - prepared: fanout.prepared, - resolveAgentIdentifier: (identifier) => this.host.resolveAgentIdentifier(identifier), - sendAgentMessage: (agentId, text) => this.host.sendAgentMessage(agentId, text), - }); - } catch (error) { - this.emitChatRpcError(request, error); - } - } - - async handleChatReadRequest( - request: Extract<SessionInboundMessage, { type: "chat/read" }>, - ): Promise<void> { - try { - const messages = await this.chatService.readMessages({ - room: request.room, - limit: request.limit, - since: request.since, - authorAgentId: request.authorAgentId, - }); - this.host.emit({ - type: "chat/read/response", - payload: { - requestId: request.requestId, - messages, - error: null, - }, - }); - } catch (error) { - this.emitChatRpcError(request, error); - } - } - - async handleChatWaitRequest( - request: Extract<SessionInboundMessage, { type: "chat/wait" }>, - ): Promise<void> { - try { - const messages = await this.chatService.waitForMessages({ - room: request.room, - afterMessageId: request.afterMessageId, - timeoutMs: request.timeoutMs, - }); - this.host.emit({ - type: "chat/wait/response", - payload: { - requestId: request.requestId, - messages, - timedOut: messages.length === 0, - error: null, - }, - }); - } catch (error) { - this.emitChatRpcError(request, error); - } - } - - private toScheduleSummary( - schedule: Awaited<ReturnType<ScheduleService["inspect"]>>, - ): Extract< - SessionOutboundMessage, - { type: "schedule/list/response" } - >["payload"]["schedules"][number] { - const { runs: _runs, ...summary } = schedule; - return summary; - } - - private emitScheduleRpcError( - request: Extract< - SessionInboundMessage, - { - type: - | "schedule/create" - | "schedule/list" - | "schedule/inspect" - | "schedule/logs" - | "schedule/pause" - | "schedule/resume" - | "schedule/delete" - | "schedule/run-once" - | "schedule/update"; - } - >, - error: unknown, - ): void { - const message = error instanceof Error ? error.message : String(error); - this.logger.error({ err: error, requestType: request.type }, "Schedule request failed"); - this.host.emit({ - type: "rpc_error", - payload: { - requestId: request.requestId, - requestType: request.type, - error: message, - code: "schedule_request_failed", - }, - }); - } - - async handleScheduleCreateRequest( - request: Extract<SessionInboundMessage, { type: "schedule/create" }>, - ): Promise<void> { - try { - const target = - request.target.type === "self" - ? { type: "agent" as const, agentId: request.target.agentId } - : request.target; - const schedule = await this.scheduleService.create({ - prompt: request.prompt, - name: request.name, - cadence: request.cadence, - target, - maxRuns: request.maxRuns, - expiresAt: request.expiresAt, - runOnCreate: request.runOnCreate, - }); - this.host.emit({ - type: "schedule/create/response", - payload: { - requestId: request.requestId, - schedule: this.toScheduleSummary(schedule), - error: null, - }, - }); - } catch (error) { - this.emitScheduleRpcError(request, error); - } - } - - async handleScheduleListRequest( - request: Extract<SessionInboundMessage, { type: "schedule/list" }>, - ): Promise<void> { - try { - const schedules = await this.scheduleService.list(); - this.host.emit({ - type: "schedule/list/response", - payload: { - requestId: request.requestId, - schedules: schedules.map((schedule) => this.toScheduleSummary(schedule)), - error: null, - }, - }); - } catch (error) { - this.emitScheduleRpcError(request, error); - } - } - - async handleScheduleInspectRequest( - request: Extract<SessionInboundMessage, { type: "schedule/inspect" }>, - ): Promise<void> { - try { - const schedule = await this.scheduleService.inspect(request.scheduleId); - this.host.emit({ - type: "schedule/inspect/response", - payload: { - requestId: request.requestId, - schedule, - error: null, - }, - }); - } catch (error) { - this.emitScheduleRpcError(request, error); - } - } - - async handleScheduleLogsRequest( - request: Extract<SessionInboundMessage, { type: "schedule/logs" }>, - ): Promise<void> { - try { - const runs = await this.scheduleService.logs(request.scheduleId); - this.host.emit({ - type: "schedule/logs/response", - payload: { - requestId: request.requestId, - runs, - error: null, - }, - }); - } catch (error) { - this.emitScheduleRpcError(request, error); - } - } - - async handleSchedulePauseRequest( - request: Extract<SessionInboundMessage, { type: "schedule/pause" }>, - ): Promise<void> { - try { - const schedule = await this.scheduleService.pause(request.scheduleId); - this.host.emit({ - type: "schedule/pause/response", - payload: { - requestId: request.requestId, - schedule: this.toScheduleSummary(schedule), - error: null, - }, - }); - } catch (error) { - this.emitScheduleRpcError(request, error); - } - } - - async handleScheduleResumeRequest( - request: Extract<SessionInboundMessage, { type: "schedule/resume" }>, - ): Promise<void> { - try { - const schedule = await this.scheduleService.resume(request.scheduleId); - this.host.emit({ - type: "schedule/resume/response", - payload: { - requestId: request.requestId, - schedule: this.toScheduleSummary(schedule), - error: null, - }, - }); - } catch (error) { - this.emitScheduleRpcError(request, error); - } - } - - async handleScheduleDeleteRequest( - request: Extract<SessionInboundMessage, { type: "schedule/delete" }>, - ): Promise<void> { - try { - await this.scheduleService.delete(request.scheduleId); - this.host.emit({ - type: "schedule/delete/response", - payload: { - requestId: request.requestId, - scheduleId: request.scheduleId, - error: null, - }, - }); - } catch (error) { - this.emitScheduleRpcError(request, error); - } - } - - async handleScheduleRunOnceRequest( - request: Extract<SessionInboundMessage, { type: "schedule/run-once" }>, - ): Promise<void> { - try { - const schedule = await this.scheduleService.runOnce(request.scheduleId); - this.host.emit({ - type: "schedule/run-once/response", - payload: { - requestId: request.requestId, - schedule, - error: null, - }, - }); - } catch (error) { - this.emitScheduleRpcError(request, error); - } - } - - async handleScheduleUpdateRequest( - request: Extract<SessionInboundMessage, { type: "schedule/update" }>, - ): Promise<void> { - try { - const schedule = await this.scheduleService.update({ - id: request.scheduleId, - ...(request.name !== undefined ? { name: request.name } : {}), - ...(request.prompt !== undefined ? { prompt: request.prompt } : {}), - ...(request.cadence !== undefined ? { cadence: request.cadence } : {}), - ...(request.newAgentConfig !== undefined ? { newAgentConfig: request.newAgentConfig } : {}), - ...(request.maxRuns !== undefined ? { maxRuns: request.maxRuns } : {}), - ...(request.expiresAt !== undefined ? { expiresAt: request.expiresAt } : {}), - }); - this.host.emit({ - type: "schedule/update/response", - payload: { - requestId: request.requestId, - schedule, - error: null, - }, - }); - } catch (error) { - this.emitScheduleRpcError(request, error); - } - } - - private emitLoopRpcError( - request: Extract< - SessionInboundMessage, - { - type: "loop/run" | "loop/list" | "loop/inspect" | "loop/logs" | "loop/stop"; - } - >, - error: unknown, - ): void { - const message = error instanceof Error ? error.message : String(error); - this.logger.error({ err: error, requestType: request.type }, "Loop request failed"); - this.host.emit({ - type: "rpc_error", - payload: { - requestId: request.requestId, - requestType: request.type, - error: message, - code: "loop_request_failed", - }, - }); - } - - async handleLoopRunRequest( - request: Extract<SessionInboundMessage, { type: "loop/run" }>, - ): Promise<void> { - try { - const loop = await this.loopService.runLoop({ - prompt: request.prompt, - cwd: request.cwd, - provider: request.provider, - model: request.model, - modeId: request.modeId, - workerProvider: request.workerProvider, - workerModel: request.workerModel, - verifierProvider: request.verifierProvider, - verifierModel: request.verifierModel, - verifierModeId: request.verifierModeId, - verifyPrompt: request.verifyPrompt, - verifyChecks: request.verifyChecks, - archive: request.archive, - name: request.name, - sleepMs: request.sleepMs, - maxIterations: request.maxIterations, - maxTimeMs: request.maxTimeMs, - }); - this.host.emit({ - type: "loop/run/response", - payload: { - requestId: request.requestId, - loop, - error: null, - }, - }); - } catch (error) { - this.emitLoopRpcError(request, error); - } - } - - async handleLoopListRequest( - request: Extract<SessionInboundMessage, { type: "loop/list" }>, - ): Promise<void> { - try { - const loops = await this.loopService.listLoops(); - this.host.emit({ - type: "loop/list/response", - payload: { - requestId: request.requestId, - loops, - error: null, - }, - }); - } catch (error) { - this.emitLoopRpcError(request, error); - } - } - - async handleLoopInspectRequest( - request: Extract<SessionInboundMessage, { type: "loop/inspect" }>, - ): Promise<void> { - try { - const loop = await this.loopService.inspectLoop(request.id); - this.host.emit({ - type: "loop/inspect/response", - payload: { - requestId: request.requestId, - loop, - error: null, - }, - }); - } catch (error) { - this.emitLoopRpcError(request, error); - } - } - - async handleLoopLogsRequest( - request: Extract<SessionInboundMessage, { type: "loop/logs" }>, - ): Promise<void> { - try { - const result = await this.loopService.getLoopLogs(request.id, request.afterSeq ?? 0); - this.host.emit({ - type: "loop/logs/response", - payload: { - requestId: request.requestId, - loop: result.loop, - entries: result.entries, - nextCursor: result.nextCursor, - error: null, - }, - }); - } catch (error) { - this.emitLoopRpcError(request, error); - } - } - - async handleLoopStopRequest( - request: Extract<SessionInboundMessage, { type: "loop/stop" }>, - ): Promise<void> { - try { - const loop = await this.loopService.stopLoop(request.id); - this.host.emit({ - type: "loop/stop/response", - payload: { - requestId: request.requestId, - loop, - error: null, - }, - }); - } catch (error) { - this.emitLoopRpcError(request, error); - } - } -} diff --git a/packages/server/src/server/session/checkout/checkout-session.test.ts b/packages/server/src/server/session/checkout/checkout-session.test.ts index 5631f7df22..bbfb9e199b 100644 --- a/packages/server/src/server/session/checkout/checkout-session.test.ts +++ b/packages/server/src/server/session/checkout/checkout-session.test.ts @@ -1,3 +1,7 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { describe, expect, it } from "vitest"; import pino from "pino"; import { @@ -24,6 +28,7 @@ import { createNoopWorkspaceGitService, } from "../../test-utils/workspace-git-service-stub.js"; import { expandTilde } from "../../../utils/path.js"; +import { discardChanges } from "../../../utils/checkout-git.js"; import type { GitMetadataGenerator } from "./git-metadata-generator.js"; function isCheckDetailsResponse(msg: SessionOutboundMessage): boolean { @@ -454,6 +459,94 @@ describe("CheckoutSession", () => { }); }); + describe("discard changes", () => { + it("discards the paths, notifies git mutation, refreshes diffs, and confirms success", async () => { + const tempDir = mkdtempSync(join(tmpdir(), "checkout-session-discard-")); + const cwd = realpathSync(tempDir); + try { + execFileSync("git", ["init", "-q"], { cwd }); + execFileSync("git", ["config", "user.email", "test@example.com"], { cwd }); + execFileSync("git", ["config", "user.name", "Test User"], { cwd }); + writeFileSync(join(cwd, "file.txt"), "original\n"); + execFileSync("git", ["add", "file.txt"], { cwd }); + execFileSync("git", ["commit", "-qm", "initial"], { cwd }); + writeFileSync(join(cwd, "file.txt"), "changed\n"); + + const { subscriber, refreshedCwds } = createFakeDiffSubscriber({ + cwd: "", + files: [], + error: null, + }); + const { checkout, emitted, gitMutationCalls } = makeCheckoutSession({ + diff: subscriber, + git: { discardChanges }, + }); + + await checkout.handleCheckoutDiscardChangesRequest({ + type: "checkout.discard_changes.request", + cwd, + paths: ["file.txt"], + requestId: "discard-1", + }); + + expect(readFileSync(join(cwd, "file.txt"), "utf8").replaceAll("\r\n", "\n")).toBe( + "original\n", + ); + expect(gitMutationCalls.notifyGitMutation).toEqual([ + { cwd, reason: "discard-changes", options: undefined }, + ]); + expect(refreshedCwds).toEqual([cwd]); + expect(emitted).toEqual([ + { + type: "checkout.discard_changes.response", + payload: { cwd, success: true, error: null, requestId: "discard-1" }, + }, + ]); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("returns the checkout error without notifying git or refreshing diffs", async () => { + const tempDir = mkdtempSync(join(tmpdir(), "checkout-session-discard-error-")); + const cwd = realpathSync(tempDir); + try { + const { subscriber, refreshedCwds } = createFakeDiffSubscriber({ + cwd: "", + files: [], + error: null, + }); + const { checkout, emitted, gitMutationCalls } = makeCheckoutSession({ + diff: subscriber, + git: { discardChanges }, + }); + + await checkout.handleCheckoutDiscardChangesRequest({ + type: "checkout.discard_changes.request", + cwd, + paths: ["file.txt"], + requestId: "discard-error", + }); + + expect(gitMutationCalls.notifyGitMutation).toEqual([]); + expect(refreshedCwds).toEqual([]); + expect(emitted).toEqual([ + { + type: "checkout.discard_changes.response", + payload: { + cwd, + success: false, + error: { code: "NOT_GIT_REPO", message: `Not a git repository: ${cwd}` }, + requestId: "discard-error", + }, + }, + ]); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + }); + describe("diff subscriptions", () => { it("opens a subscription, streams updates tagged with the id, and tears down on unsubscribe", async () => { const { subscriber, subscriptions } = createFakeDiffSubscriber({ @@ -578,6 +671,16 @@ describe("CheckoutSession", () => { }, ]); }); + + it("does not emit the same checkout status twice", () => { + const { checkout, emitted } = makeCheckoutSession(); + const snapshot = createGitSnapshot("/repo", "main"); + + checkout.emitStatusUpdate("workspace-1", "/repo", snapshot); + checkout.emitStatusUpdate("workspace-1", "/repo", snapshot); + + expect(emitted).toHaveLength(1); + }); }); describe("switch branch", () => { diff --git a/packages/server/src/server/session/checkout/checkout-session.ts b/packages/server/src/server/session/checkout/checkout-session.ts index 058570e24d..0769577c78 100644 --- a/packages/server/src/server/session/checkout/checkout-session.ts +++ b/packages/server/src/server/session/checkout/checkout-session.ts @@ -135,6 +135,7 @@ export class CheckoutSession { private readonly gitMetadataGenerator: GitMetadataGenerator; private readonly logger: pino.Logger; private readonly diffSubscriptions = new Map<string, () => void>(); + private readonly statusUpdateFingerprints = new Map<string, string>(); constructor(options: CheckoutSessionOptions) { this.host = options.host; @@ -479,21 +480,25 @@ export class CheckoutSession { emitStatusUpdate(workspaceId: string, cwd: string, snapshot: WorkspaceGitRuntimeSnapshot): void { try { const requestId = `subscription:${workspaceId}`; + const payload = { + workspaceId, + ...buildCheckoutStatusPayloadFromSnapshot({ + cwd, + requestId, + snapshot, + }), + prStatus: buildCheckoutPrStatusPayloadFromSnapshot({ + cwd, + requestId, + snapshot, + }), + }; + const fingerprint = JSON.stringify(payload); + if (this.statusUpdateFingerprints.get(workspaceId) === fingerprint) return; + this.statusUpdateFingerprints.set(workspaceId, fingerprint); this.host.emit({ type: "checkout_status_update", - payload: { - workspaceId, - ...buildCheckoutStatusPayloadFromSnapshot({ - cwd, - requestId, - snapshot, - }), - prStatus: buildCheckoutPrStatusPayloadFromSnapshot({ - cwd, - requestId, - snapshot, - }), - }, + payload, }); } catch (error) { this.logger.warn({ err: error, cwd }, "Failed to emit workspace checkout status update"); @@ -617,6 +622,27 @@ export class CheckoutSession { } } + async handleCheckoutDiscardChangesRequest( + msg: Extract<SessionInboundMessage, { type: "checkout.discard_changes.request" }>, + ): Promise<void> { + const { cwd, paths, requestId } = msg; + try { + const workspaceGit = await this.resolveWorkspaceGit(msg); + await workspaceGit.discardChanges(paths); + await this.gitMutation.bind(workspaceGit).notify("discard-changes"); + this.scheduleDiffRefresh(workspaceGit); + this.host.emit({ + type: "checkout.discard_changes.response", + payload: { cwd, success: true, error: null, requestId }, + }); + } catch (error) { + this.host.emit({ + type: "checkout.discard_changes.response", + payload: { cwd, success: false, error: toCheckoutError(error), requestId }, + }); + } + } + async handleStashSaveRequest( msg: Extract<SessionInboundMessage, { type: "stash_save_request" }>, ): Promise<void> { @@ -1426,6 +1452,7 @@ export class CheckoutSession { unsubscribe(); } this.diffSubscriptions.clear(); + this.statusUpdateFingerprints.clear(); } } diff --git a/packages/server/src/server/session/files/workspace-files-session.test.ts b/packages/server/src/server/session/files/workspace-files-session.test.ts index fee5f78a0b..cc7516e383 100644 --- a/packages/server/src/server/session/files/workspace-files-session.test.ts +++ b/packages/server/src/server/session/files/workspace-files-session.test.ts @@ -1,4 +1,11 @@ -import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdtempSync, + readFileSync, + realpathSync, + rmSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, test } from "vitest"; @@ -292,6 +299,227 @@ describe("WorkspaceFilesSession", () => { await runtime.destroy(record.workspaceId); }); + test("creates an entry and emits the complete success response", async () => { + const cwd = makeDir("workspace-files-create-"); + const { subsystem, emitted } = makeSubsystem(); + + await subsystem.handleFileEntryCreateRequest({ + type: "fs.entry.create.request", + cwd, + parentPath: ".", + name: "notes.txt", + kind: "file", + requestId: "req-create", + }); + + expect(existsSync(join(cwd, "notes.txt"))).toBe(true); + expect(emitted).toEqual([ + { + type: "fs.entry.create.response", + payload: { + cwd, + parentPath: ".", + path: "notes.txt", + success: true, + error: null, + requestId: "req-create", + }, + }, + ]); + }); + + test("passes entry creation errors through in the response", async () => { + const cwd = makeDir("workspace-files-create-error-"); + writeFileSync(join(cwd, "notes.txt"), "existing"); + const { subsystem, emitted } = makeSubsystem(); + + await subsystem.handleFileEntryCreateRequest({ + type: "fs.entry.create.request", + cwd, + parentPath: ".", + name: "notes.txt", + kind: "file", + requestId: "req-create-error", + }); + + expect(emitted).toEqual([ + { + type: "fs.entry.create.response", + payload: { + cwd, + parentPath: ".", + path: null, + success: false, + error: '"notes.txt" already exists', + requestId: "req-create-error", + }, + }, + ]); + }); + + test("renames an entry and emits the resulting path", async () => { + const cwd = makeDir("workspace-files-rename-"); + writeFileSync(join(cwd, "notes.txt"), "rename me"); + const { subsystem, emitted } = makeSubsystem(); + + await subsystem.handleFileEntryRenameRequest({ + type: "fs.entry.rename.request", + cwd, + path: "notes.txt", + name: "renamed.txt", + requestId: "req-rename", + }); + + expect(existsSync(join(cwd, "notes.txt"))).toBe(false); + expect(existsSync(join(cwd, "renamed.txt"))).toBe(true); + expect(emitted).toEqual([ + { + type: "fs.entry.rename.response", + payload: { + cwd, + path: "notes.txt", + renamedPath: "renamed.txt", + success: true, + error: null, + requestId: "req-rename", + }, + }, + ]); + }); + + test("passes entry rename errors through in the response", async () => { + const cwd = makeDir("workspace-files-rename-error-"); + const { subsystem, emitted } = makeSubsystem(); + + await subsystem.handleFileEntryRenameRequest({ + type: "fs.entry.rename.request", + cwd, + path: "missing.txt", + name: "renamed.txt", + requestId: "req-rename-error", + }); + + expect(emitted).toEqual([ + { + type: "fs.entry.rename.response", + payload: { + cwd, + path: "missing.txt", + renamedPath: null, + success: false, + error: "File or folder no longer exists", + requestId: "req-rename-error", + }, + }, + ]); + }); + + test("duplicates an entry and emits the resulting path", async () => { + const cwd = makeDir("workspace-files-duplicate-"); + writeFileSync(join(cwd, "notes.txt"), "duplicate me"); + const { subsystem, emitted } = makeSubsystem(); + + await subsystem.handleFileEntryDuplicateRequest({ + type: "fs.entry.duplicate.request", + cwd, + path: "notes.txt", + requestId: "req-duplicate", + }); + + expect(readFileSync(join(cwd, "notes copy.txt"), "utf8")).toBe("duplicate me"); + expect(emitted).toEqual([ + { + type: "fs.entry.duplicate.response", + payload: { + cwd, + path: "notes.txt", + duplicatedPath: "notes copy.txt", + success: true, + error: null, + requestId: "req-duplicate", + }, + }, + ]); + }); + + test("passes entry duplication errors through in the response", async () => { + const cwd = makeDir("workspace-files-duplicate-error-"); + const { subsystem, emitted } = makeSubsystem(); + + await subsystem.handleFileEntryDuplicateRequest({ + type: "fs.entry.duplicate.request", + cwd, + path: "missing.txt", + requestId: "req-duplicate-error", + }); + + expect(emitted).toEqual([ + { + type: "fs.entry.duplicate.response", + payload: { + cwd, + path: "missing.txt", + duplicatedPath: null, + success: false, + error: "File or folder no longer exists", + requestId: "req-duplicate-error", + }, + }, + ]); + }); + + test("deletes an entry and emits the complete success response", async () => { + const cwd = makeDir("workspace-files-delete-"); + writeFileSync(join(cwd, "notes.txt"), "delete me"); + const { subsystem, emitted } = makeSubsystem(); + + await subsystem.handleFileEntryDeleteRequest({ + type: "fs.entry.delete.request", + cwd, + path: "notes.txt", + requestId: "req-delete", + }); + + expect(existsSync(join(cwd, "notes.txt"))).toBe(false); + expect(emitted).toEqual([ + { + type: "fs.entry.delete.response", + payload: { + cwd, + path: "notes.txt", + success: true, + error: null, + requestId: "req-delete", + }, + }, + ]); + }); + + test("passes entry deletion errors through in the response", async () => { + const cwd = makeDir("workspace-files-delete-error-"); + const { subsystem, emitted } = makeSubsystem(); + + await subsystem.handleFileEntryDeleteRequest({ + type: "fs.entry.delete.request", + cwd, + path: "missing.txt", + requestId: "req-delete-error", + }); + + expect(emitted).toEqual([ + { + type: "fs.entry.delete.response", + payload: { + cwd, + path: "missing.txt", + success: false, + error: "File or folder no longer exists", + requestId: "req-delete-error", + }, + }, + ]); + }); + test("lists directory entries", async () => { const cwd = makeDir("workspace-files-list-"); writeFileSync(join(cwd, "a.txt"), "alpha"); diff --git a/packages/server/src/server/session/files/workspace-files-session.ts b/packages/server/src/server/session/files/workspace-files-session.ts index 452a8926ca..97be4ba1ca 100644 --- a/packages/server/src/server/session/files/workspace-files-session.ts +++ b/packages/server/src/server/session/files/workspace-files-session.ts @@ -8,6 +8,10 @@ import { } from "@getpaseo/protocol/binary-frames/index"; import type { FileDownloadTokenRequest, + FileEntryCreateRequest, + FileEntryDeleteRequest, + FileEntryDuplicateRequest, + FileEntryRenameRequest, FileExplorerRequest, FileUploadRequest, FileSubscribeRequest, @@ -22,9 +26,13 @@ import type { WorkspaceRegistry } from "../../workspace-registry.js"; import { FileUploadStore } from "../../file-upload/index.js"; import type { DownloadTokenStore } from "../../file-download/token-store.js"; import { + createExplorerEntry, + deleteExplorerEntry, + duplicateExplorerEntry, getDownloadableFileInfo, listDirectoryEntries, readExplorerFile, + renameExplorerEntry, streamExplorerFile, writeExplorerFile, } from "../../file-explorer/service.js"; @@ -214,6 +222,80 @@ export class WorkspaceFilesSession { }); } + async handleFileEntryCreateRequest(request: FileEntryCreateRequest): Promise<void> { + const result = await createExplorerEntry({ + root: request.cwd, + parentPath: request.parentPath, + name: request.name, + kind: request.kind, + }); + this.host.emit({ + type: "fs.entry.create.response", + payload: { + cwd: request.cwd, + parentPath: request.parentPath, + path: result.status === "ok" ? result.path : null, + success: result.status === "ok", + error: result.status === "ok" ? null : result.error, + requestId: request.requestId, + }, + }); + } + + async handleFileEntryRenameRequest(request: FileEntryRenameRequest): Promise<void> { + const result = await renameExplorerEntry({ + root: request.cwd, + relativePath: request.path, + name: request.name, + }); + this.host.emit({ + type: "fs.entry.rename.response", + payload: { + cwd: request.cwd, + path: request.path, + renamedPath: result.status === "ok" ? result.path : null, + success: result.status === "ok", + error: result.status === "ok" ? null : result.error, + requestId: request.requestId, + }, + }); + } + + async handleFileEntryDuplicateRequest(request: FileEntryDuplicateRequest): Promise<void> { + const result = await duplicateExplorerEntry({ + root: request.cwd, + relativePath: request.path, + }); + this.host.emit({ + type: "fs.entry.duplicate.response", + payload: { + cwd: request.cwd, + path: request.path, + duplicatedPath: result.status === "ok" ? result.path : null, + success: result.status === "ok", + error: result.status === "ok" ? null : result.error, + requestId: request.requestId, + }, + }); + } + + async handleFileEntryDeleteRequest(request: FileEntryDeleteRequest): Promise<void> { + const result = await deleteExplorerEntry({ + root: request.cwd, + relativePath: request.path, + }); + this.host.emit({ + type: "fs.entry.delete.response", + payload: { + cwd: request.cwd, + path: request.path, + success: result.status === "ok", + error: result.status === "ok" ? null : result.error, + requestId: request.requestId, + }, + }); + } + async dispose(): Promise<void> { await Promise.all([...this.fileSubscriptions.values()].map((unsubscribe) => unsubscribe())); this.fileSubscriptions.clear(); diff --git a/packages/server/src/server/session/project-config/project-config-session.test.ts b/packages/server/src/server/session/project-config/project-config-session.test.ts index 0faa95404a..6e93dff805 100644 --- a/packages/server/src/server/session/project-config/project-config-session.test.ts +++ b/packages/server/src/server/session/project-config/project-config-session.test.ts @@ -1,9 +1,11 @@ import { mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { execFileSync } from "node:child_process"; import { afterEach, describe, expect, test } from "vitest"; import pino from "pino"; import { ProjectConfigSession, type ProjectConfigSessionHost } from "./project-config-session.js"; +import { statPaseoConfigPath } from "../../../utils/paseo-config-file.js"; import type { PersistedProjectRecord } from "../../workspace-registry.js"; import type { SessionOutboundMessage } from "../../messages.js"; @@ -45,6 +47,38 @@ function makeSubsystem(records: PersistedProjectRecord[]) { } describe("ProjectConfigSession", () => { + test("read reports when the saved worktree setup differs from HEAD", async () => { + const repoRoot = makeRoot(); + execFileSync("git", ["init", "-b", "main"], { cwd: repoRoot }); + execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: repoRoot }); + execFileSync("git", ["config", "user.name", "Test"], { cwd: repoRoot }); + writeFileSync(join(repoRoot, "paseo.json"), JSON.stringify({ worktree: { setup: "npm ci" } })); + execFileSync("git", ["add", "paseo.json"], { cwd: repoRoot }); + execFileSync("git", ["commit", "-m", "add config"], { cwd: repoRoot }); + writeFileSync( + join(repoRoot, "paseo.json"), + JSON.stringify({ worktree: { setup: "npm install" } }), + ); + const { subsystem, emitted } = makeSubsystem([projectRecord(repoRoot)]); + + await subsystem.handleReadProjectConfigRequest({ + type: "read_project_config_request", + requestId: "read-uncommitted-setup", + repoRoot, + }); + + expect(emitted).toEqual([ + { + type: "read_project_config_response", + payload: expect.objectContaining({ + requestId: "read-uncommitted-setup", + ok: true, + hasUncommittedWorktreeSetupChanges: true, + }), + }, + ]); + }); + test("read resolves a known root despite a trailing slash and returns the raw config + revision", async () => { const repoRoot = makeRoot(); writeFileSync(join(repoRoot, "paseo.json"), JSON.stringify({ worktree: { setup: "npm ci" } })); @@ -179,6 +213,36 @@ describe("ProjectConfigSession", () => { ]); }); + test("write recomputes the worktree setup commit status", async () => { + const repoRoot = makeRoot(); + execFileSync("git", ["init", "-b", "main"], { cwd: repoRoot }); + execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: repoRoot }); + execFileSync("git", ["config", "user.name", "Test"], { cwd: repoRoot }); + writeFileSync(join(repoRoot, "paseo.json"), JSON.stringify({ worktree: { setup: "npm ci" } })); + execFileSync("git", ["add", "paseo.json"], { cwd: repoRoot }); + execFileSync("git", ["commit", "-m", "add config"], { cwd: repoRoot }); + const { subsystem, emitted } = makeSubsystem([projectRecord(repoRoot)]); + + await subsystem.handleWriteProjectConfigRequest({ + type: "write_project_config_request", + requestId: "write-uncommitted-setup", + repoRoot, + config: { worktree: { setup: "npm install" } }, + expectedRevision: statPaseoConfigPath(repoRoot), + }); + + expect(emitted).toEqual([ + { + type: "write_project_config_response", + payload: expect.objectContaining({ + requestId: "write-uncommitted-setup", + ok: true, + hasUncommittedWorktreeSetupChanges: true, + }), + }, + ]); + }); + test("write rejects a stale revision and an unknown root with their inline domain failures", async () => { const staleRoot = makeRoot(); writeFileSync(join(staleRoot, "paseo.json"), JSON.stringify({ worktree: { setup: "old" } })); diff --git a/packages/server/src/server/session/project-config/project-config-session.ts b/packages/server/src/server/session/project-config/project-config-session.ts index ae62bd3d2b..6056d08826 100644 --- a/packages/server/src/server/session/project-config/project-config-session.ts +++ b/packages/server/src/server/session/project-config/project-config-session.ts @@ -8,6 +8,7 @@ import { writePaseoConfigForEdit, type ProjectConfigRpcError, } from "../../../utils/paseo-config-file.js"; +import { hasUncommittedWorktreeSetupChanges } from "./worktree-setup-commit-status.js"; export interface ProjectConfigSessionHost { emit(msg: SessionOutboundMessage): void; @@ -63,6 +64,8 @@ export class ProjectConfigSession { ); } + const setupCommitStatus = await this.readSetupCommitStatus(repoRoot, result.config); + this.host.emit({ type: "read_project_config_response", payload: { @@ -71,6 +74,9 @@ export class ProjectConfigSession { ok: true, config: result.config, revision: result.revision, + ...(setupCommitStatus === null + ? {} + : { hasUncommittedWorktreeSetupChanges: setupCommitStatus }), }, }); } @@ -106,6 +112,7 @@ export class ProjectConfigSession { { repoRoot, requestId: msg.requestId, outcome: "written" }, "Project config written", ); + const setupCommitStatus = await this.readSetupCommitStatus(repoRoot, result.config); this.host.emit({ type: "write_project_config_response", payload: { @@ -114,10 +121,25 @@ export class ProjectConfigSession { ok: true, config: result.config, revision: result.revision, + ...(setupCommitStatus === null + ? {} + : { hasUncommittedWorktreeSetupChanges: setupCommitStatus }), }, }); } + private async readSetupCommitStatus( + repoRoot: string, + config: Parameters<typeof hasUncommittedWorktreeSetupChanges>[0]["currentConfig"], + ): Promise<boolean | null> { + try { + return await hasUncommittedWorktreeSetupChanges({ repoRoot, currentConfig: config }); + } catch (error) { + this.logger.debug({ repoRoot, error }, "Could not inspect committed worktree setup"); + return null; + } + } + private emitProjectConfigReadFailure( msg: Extract<SessionInboundMessage, { type: "read_project_config_request" }>, error: ProjectConfigRpcError, diff --git a/packages/server/src/server/session/project-config/worktree-setup-commit-status.test.ts b/packages/server/src/server/session/project-config/worktree-setup-commit-status.test.ts new file mode 100644 index 0000000000..73c54014af --- /dev/null +++ b/packages/server/src/server/session/project-config/worktree-setup-commit-status.test.ts @@ -0,0 +1,97 @@ +import { execFileSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, test } from "vitest"; +import type { PaseoConfigRaw } from "@getpaseo/protocol/paseo-config-schema"; +import { hasUncommittedWorktreeSetupChanges } from "./worktree-setup-commit-status.js"; + +const tempDirs: string[] = []; + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } +}); + +function makeGitRepo(config?: PaseoConfigRaw): string { + const repoRoot = realpathSync(mkdtempSync(join(tmpdir(), "worktree-setup-status-test-"))); + tempDirs.push(repoRoot); + execFileSync("git", ["init", "-b", "main"], { cwd: repoRoot }); + execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: repoRoot }); + execFileSync("git", ["config", "user.name", "Test"], { cwd: repoRoot }); + writeFileSync(join(repoRoot, "README.md"), "test\n"); + if (config) writeConfig(repoRoot, config); + execFileSync("git", ["add", "."], { cwd: repoRoot }); + execFileSync("git", ["commit", "-m", "initial"], { cwd: repoRoot }); + return repoRoot; +} + +function writeConfig(repoRoot: string, config: PaseoConfigRaw): void { + writeFileSync(join(repoRoot, "paseo.json"), `${JSON.stringify(config, null, 2)}\n`); +} + +describe("worktree setup commit status", () => { + test("reports matching committed setup as clean", async () => { + const repoRoot = makeGitRepo({ worktree: { setup: "npm ci" } }); + await expect( + hasUncommittedWorktreeSetupChanges({ + repoRoot, + currentConfig: { worktree: { setup: "npm ci" } }, + }), + ).resolves.toBe(false); + }); + + test("reports modified and staged setup as uncommitted", async () => { + const repoRoot = makeGitRepo({ worktree: { setup: "npm ci" } }); + const currentConfig = { worktree: { setup: "npm install" } }; + writeConfig(repoRoot, currentConfig); + await expect(hasUncommittedWorktreeSetupChanges({ repoRoot, currentConfig })).resolves.toBe( + true, + ); + + execFileSync("git", ["add", "paseo.json"], { cwd: repoRoot }); + await expect(hasUncommittedWorktreeSetupChanges({ repoRoot, currentConfig })).resolves.toBe( + true, + ); + }); + + test("reports an untracked config with setup as uncommitted", async () => { + const repoRoot = makeGitRepo(); + const currentConfig = { worktree: { setup: "npm ci" } }; + writeConfig(repoRoot, currentConfig); + await expect(hasUncommittedWorktreeSetupChanges({ repoRoot, currentConfig })).resolves.toBe( + true, + ); + }); + + test("ignores representation-only and unrelated config changes", async () => { + const repoRoot = makeGitRepo({ + worktree: { setup: "npm ci", teardown: "echo old" }, + scripts: { dev: { command: "npm run dev" } }, + }); + const currentConfig = { + worktree: { setup: ["npm ci"], teardown: "echo new" }, + scripts: { dev: { command: "npm run start" } }, + metadataGeneration: { branchName: { instructions: "Use fix/" } }, + }; + writeConfig(repoRoot, currentConfig); + await expect(hasUncommittedWorktreeSetupChanges({ repoRoot, currentConfig })).resolves.toBe( + false, + ); + }); + + test("resolves paseo.json relative to a nested project root", async () => { + const repoRoot = makeGitRepo(); + const projectRoot = join(repoRoot, "packages", "app"); + mkdirSync(projectRoot, { recursive: true }); + writeConfig(projectRoot, { worktree: { setup: "npm ci" } }); + execFileSync("git", ["add", "packages/app/paseo.json"], { cwd: repoRoot }); + execFileSync("git", ["commit", "-m", "add nested config"], { cwd: repoRoot }); + const currentConfig = { worktree: { setup: "npm install" } }; + writeConfig(projectRoot, currentConfig); + await expect( + hasUncommittedWorktreeSetupChanges({ repoRoot: projectRoot, currentConfig }), + ).resolves.toBe(true); + }); +}); diff --git a/packages/server/src/server/session/project-config/worktree-setup-commit-status.ts b/packages/server/src/server/session/project-config/worktree-setup-commit-status.ts new file mode 100644 index 0000000000..dca1eca8e0 --- /dev/null +++ b/packages/server/src/server/session/project-config/worktree-setup-commit-status.ts @@ -0,0 +1,54 @@ +import { + PaseoConfigRawSchema, + normalizeLifecycleCommands, + type PaseoConfigRaw, +} from "@getpaseo/protocol/paseo-config-schema"; +import { READ_ONLY_GIT_ENV } from "../../checkout-git-utils.js"; +import { runGitCommand } from "../../../utils/run-git-command.js"; + +export async function hasUncommittedWorktreeSetupChanges(input: { + repoRoot: string; + currentConfig: PaseoConfigRaw | null; +}): Promise<boolean> { + const gitPath = await resolveConfigGitPath(input.repoRoot); + const committedConfig = await readCommittedConfig(input.repoRoot, gitPath); + const currentSetup = normalizeLifecycleCommands(input.currentConfig?.worktree?.setup); + const committedSetup = normalizeLifecycleCommands(committedConfig?.worktree?.setup); + return !stringArraysEqual(currentSetup, committedSetup); +} + +async function resolveConfigGitPath(repoRoot: string): Promise<string> { + const { stdout } = await runGitCommand(["rev-parse", "--show-prefix"], { + cwd: repoRoot, + envOverlay: READ_ONLY_GIT_ENV, + }); + return `${stdout.trim()}paseo.json`; +} + +async function readCommittedConfig( + repoRoot: string, + gitPath: string, +): Promise<PaseoConfigRaw | null> { + await runGitCommand(["rev-parse", "--verify", "HEAD"], { + cwd: repoRoot, + envOverlay: READ_ONLY_GIT_ENV, + }); + const { stdout: trackedPath } = await runGitCommand( + ["ls-tree", "--name-only", "HEAD", "--", gitPath], + { + cwd: repoRoot, + envOverlay: READ_ONLY_GIT_ENV, + }, + ); + if (trackedPath.trim().length === 0) return null; + + const { stdout } = await runGitCommand(["show", `HEAD:${gitPath}`], { + cwd: repoRoot, + envOverlay: READ_ONLY_GIT_ENV, + }); + return PaseoConfigRawSchema.parse(JSON.parse(stdout)); +} + +function stringArraysEqual(left: string[], right: string[]): boolean { + return left.length === right.length && left.every((value, index) => value === right[index]); +} diff --git a/packages/server/src/server/session/schedule/schedule-session.test.ts b/packages/server/src/server/session/schedule/schedule-session.test.ts new file mode 100644 index 0000000000..361d2318ff --- /dev/null +++ b/packages/server/src/server/session/schedule/schedule-session.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "vitest"; +import pino from "pino"; +import { ScheduleSession } from "./schedule-session.js"; +import { createStub } from "../../test-utils/class-mocks.js"; +import { findByType } from "../../test-utils/session-stubs.js"; +import type { SessionOutboundMessage } from "../../messages.js"; +import type { ScheduleService } from "../../schedule/service.js"; + +function makeSession(schedule: { [K in keyof ScheduleService]?: unknown }) { + const emitted: SessionOutboundMessage[] = []; + const session = new ScheduleSession({ + host: { emit: (message) => emitted.push(message) }, + scheduleService: createStub<ScheduleService>(schedule), + logger: pino({ level: "silent" }), + }); + return { session, emitted }; +} + +describe("ScheduleSession", () => { + it("schedule/create returns a summary with the runs stripped", async () => { + const stored = { + id: "s1", + name: null, + prompt: "p", + cadence: { type: "every" as const, everyMs: 1000 }, + target: { type: "agent" as const, agentId: "a" }, + status: "active" as const, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + nextRunAt: null, + lastRunAt: null, + pausedAt: null, + expiresAt: null, + maxRuns: null, + runs: [ + { + id: "run-1", + scheduledFor: "2026-01-01T00:00:00.000Z", + startedAt: "2026-01-01T00:00:00.000Z", + endedAt: null, + status: "running" as const, + agentId: null, + output: null, + error: null, + }, + ], + }; + const { session, emitted } = makeSession({ create: async () => stored }); + + await session.handleScheduleCreateRequest({ + type: "schedule/create", + requestId: "sc1", + prompt: "p", + cadence: { type: "every", everyMs: 1000 }, + target: { type: "agent", agentId: "a" }, + }); + + const response = findByType(emitted, "schedule/create/response"); + expect(response?.payload.schedule).toBeDefined(); + expect(response?.payload.schedule).not.toHaveProperty("runs"); + expect(response?.payload.schedule.id).toBe("s1"); + }); + + it("schedule/create remaps a self target to an agent target before creating", async () => { + let received: Parameters<ScheduleService["create"]>[0] | undefined; + const stored = { + id: "s2", + name: null, + prompt: "p", + cadence: { type: "every" as const, everyMs: 1000 }, + target: { type: "agent" as const, agentId: "agent-9" }, + status: "active" as const, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + nextRunAt: null, + lastRunAt: null, + pausedAt: null, + expiresAt: null, + maxRuns: null, + runs: [], + }; + const { session, emitted } = makeSession({ + create: async (input: Parameters<ScheduleService["create"]>[0]) => { + received = input; + return stored; + }, + }); + + await session.handleScheduleCreateRequest({ + type: "schedule/create", + requestId: "sc2", + prompt: "p", + cadence: { type: "every", everyMs: 1000 }, + target: { type: "self", agentId: "agent-9" }, + }); + + expect(received?.target).toEqual({ type: "agent", agentId: "agent-9" }); + expect(findByType(emitted, "schedule/create/response")?.payload.error).toBeNull(); + }); +}); diff --git a/packages/server/src/server/session/schedule/schedule-session.ts b/packages/server/src/server/session/schedule/schedule-session.ts new file mode 100644 index 0000000000..ef1d02d1d4 --- /dev/null +++ b/packages/server/src/server/session/schedule/schedule-session.ts @@ -0,0 +1,248 @@ +import type pino from "pino"; +import type { SessionInboundMessage, SessionOutboundMessage } from "../../messages.js"; +import type { ScheduleService } from "../../schedule/service.js"; + +export interface ScheduleSessionHost { + emit(msg: SessionOutboundMessage): void; +} + +export interface ScheduleSessionOptions { + host: ScheduleSessionHost; + scheduleService: ScheduleService; + logger: pino.Logger; +} + +export class ScheduleSession { + private readonly host: ScheduleSessionHost; + private readonly scheduleService: ScheduleService; + private readonly logger: pino.Logger; + + constructor(options: ScheduleSessionOptions) { + this.host = options.host; + this.scheduleService = options.scheduleService; + this.logger = options.logger; + } + + private toScheduleSummary( + schedule: Awaited<ReturnType<ScheduleService["inspect"]>>, + ): Extract< + SessionOutboundMessage, + { type: "schedule/list/response" } + >["payload"]["schedules"][number] { + const { runs: _runs, ...summary } = schedule; + return summary; + } + + private emitScheduleRpcError( + request: Extract< + SessionInboundMessage, + { + type: + | "schedule/create" + | "schedule/list" + | "schedule/inspect" + | "schedule/logs" + | "schedule/pause" + | "schedule/resume" + | "schedule/delete" + | "schedule/run-once" + | "schedule/update"; + } + >, + error: unknown, + ): void { + const message = error instanceof Error ? error.message : String(error); + this.logger.error({ err: error, requestType: request.type }, "Schedule request failed"); + this.host.emit({ + type: "rpc_error", + payload: { + requestId: request.requestId, + requestType: request.type, + error: message, + code: "schedule_request_failed", + }, + }); + } + + async handleScheduleCreateRequest( + request: Extract<SessionInboundMessage, { type: "schedule/create" }>, + ): Promise<void> { + try { + const target = + request.target.type === "self" + ? { type: "agent" as const, agentId: request.target.agentId } + : request.target; + const schedule = await this.scheduleService.create({ + prompt: request.prompt, + name: request.name, + cadence: request.cadence, + target, + maxRuns: request.maxRuns, + expiresAt: request.expiresAt, + runOnCreate: request.runOnCreate, + }); + this.host.emit({ + type: "schedule/create/response", + payload: { + requestId: request.requestId, + schedule: this.toScheduleSummary(schedule), + error: null, + }, + }); + } catch (error) { + this.emitScheduleRpcError(request, error); + } + } + + async handleScheduleListRequest( + request: Extract<SessionInboundMessage, { type: "schedule/list" }>, + ): Promise<void> { + try { + const schedules = await this.scheduleService.list(); + this.host.emit({ + type: "schedule/list/response", + payload: { + requestId: request.requestId, + schedules: schedules.map((schedule) => this.toScheduleSummary(schedule)), + error: null, + }, + }); + } catch (error) { + this.emitScheduleRpcError(request, error); + } + } + + async handleScheduleInspectRequest( + request: Extract<SessionInboundMessage, { type: "schedule/inspect" }>, + ): Promise<void> { + try { + const schedule = await this.scheduleService.inspect(request.scheduleId); + this.host.emit({ + type: "schedule/inspect/response", + payload: { + requestId: request.requestId, + schedule, + error: null, + }, + }); + } catch (error) { + this.emitScheduleRpcError(request, error); + } + } + + async handleScheduleLogsRequest( + request: Extract<SessionInboundMessage, { type: "schedule/logs" }>, + ): Promise<void> { + try { + const runs = await this.scheduleService.logs(request.scheduleId); + this.host.emit({ + type: "schedule/logs/response", + payload: { + requestId: request.requestId, + runs, + error: null, + }, + }); + } catch (error) { + this.emitScheduleRpcError(request, error); + } + } + + async handleSchedulePauseRequest( + request: Extract<SessionInboundMessage, { type: "schedule/pause" }>, + ): Promise<void> { + try { + const schedule = await this.scheduleService.pause(request.scheduleId); + this.host.emit({ + type: "schedule/pause/response", + payload: { + requestId: request.requestId, + schedule: this.toScheduleSummary(schedule), + error: null, + }, + }); + } catch (error) { + this.emitScheduleRpcError(request, error); + } + } + + async handleScheduleResumeRequest( + request: Extract<SessionInboundMessage, { type: "schedule/resume" }>, + ): Promise<void> { + try { + const schedule = await this.scheduleService.resume(request.scheduleId); + this.host.emit({ + type: "schedule/resume/response", + payload: { + requestId: request.requestId, + schedule: this.toScheduleSummary(schedule), + error: null, + }, + }); + } catch (error) { + this.emitScheduleRpcError(request, error); + } + } + + async handleScheduleDeleteRequest( + request: Extract<SessionInboundMessage, { type: "schedule/delete" }>, + ): Promise<void> { + try { + await this.scheduleService.delete(request.scheduleId); + this.host.emit({ + type: "schedule/delete/response", + payload: { + requestId: request.requestId, + scheduleId: request.scheduleId, + error: null, + }, + }); + } catch (error) { + this.emitScheduleRpcError(request, error); + } + } + + async handleScheduleRunOnceRequest( + request: Extract<SessionInboundMessage, { type: "schedule/run-once" }>, + ): Promise<void> { + try { + const schedule = await this.scheduleService.runOnce(request.scheduleId); + this.host.emit({ + type: "schedule/run-once/response", + payload: { + requestId: request.requestId, + schedule, + error: null, + }, + }); + } catch (error) { + this.emitScheduleRpcError(request, error); + } + } + + async handleScheduleUpdateRequest( + request: Extract<SessionInboundMessage, { type: "schedule/update" }>, + ): Promise<void> { + try { + const schedule = await this.scheduleService.update({ + id: request.scheduleId, + ...(request.name !== undefined ? { name: request.name } : {}), + ...(request.prompt !== undefined ? { prompt: request.prompt } : {}), + ...(request.cadence !== undefined ? { cadence: request.cadence } : {}), + ...(request.newAgentConfig !== undefined ? { newAgentConfig: request.newAgentConfig } : {}), + ...(request.maxRuns !== undefined ? { maxRuns: request.maxRuns } : {}), + ...(request.expiresAt !== undefined ? { expiresAt: request.expiresAt } : {}), + }); + this.host.emit({ + type: "schedule/update/response", + payload: { + requestId: request.requestId, + schedule, + error: null, + }, + }); + } catch (error) { + this.emitScheduleRpcError(request, error); + } + } +} diff --git a/packages/server/src/server/snapshot-mutation-ownership.test.ts b/packages/server/src/server/snapshot-mutation-ownership.test.ts index 3733643484..126f06678b 100644 --- a/packages/server/src/server/snapshot-mutation-ownership.test.ts +++ b/packages/server/src/server/snapshot-mutation-ownership.test.ts @@ -103,7 +103,7 @@ describe("snapshot mutation ownership boundary", () => { onMessage, logger: createStub<SessionOptions["logger"]>(logger), downloadTokenStore: createStub<SessionOptions["downloadTokenStore"]>({}), - pushTokenStore: createStub<SessionOptions["pushTokenStore"]>({}), + pushNotifications: createStub<SessionOptions["pushNotifications"]>({}), paseoHome: "/tmp/paseo-test", agentManager: createStub<SessionOptions["agentManager"]>({ subscribe: () => () => {}, diff --git a/packages/server/src/server/test-utils/paseo-daemon.ts b/packages/server/src/server/test-utils/paseo-daemon.ts index 3eae85edff..fb90856d5c 100644 --- a/packages/server/src/server/test-utils/paseo-daemon.ts +++ b/packages/server/src/server/test-utils/paseo-daemon.ts @@ -11,7 +11,8 @@ import { } from "../bootstrap.js"; import type { AgentClient, AgentProvider } from "../agent/agent-sdk-types.js"; import { createTestAgentClients } from "./fake-agent-client.js"; -import type { PushNotificationSender } from "../push/notifications.js"; +import type { PushNotificationSender } from "../push/index.js"; +import type { AgentProfile } from "@getpaseo/protocol/messages"; interface TestPaseoDaemonOptions { daemonVersion?: string; @@ -46,6 +47,7 @@ interface TestPaseoDaemonOptions { serviceProxy?: PaseoDaemonConfig["serviceProxy"]; webUi?: PaseoDaemonConfig["webUi"]; trustedProxies?: PaseoDaemonConfig["trustedProxies"]; + agentProfiles?: AgentProfile[]; } export interface TestPaseoDaemon { @@ -196,6 +198,7 @@ async function prepareTestDaemonConfig( voiceLlmModel: options.voiceLlmModel ?? null, dictationFinalTimeoutMs: options.dictationFinalTimeoutMs, downloadTokenTtlMs: options.downloadTokenTtlMs, + agentProfiles: options.agentProfiles, }; return { config, paseoHomeRoot, paseoHome, staticDir }; } diff --git a/packages/server/src/server/test-utils/session-stubs.ts b/packages/server/src/server/test-utils/session-stubs.ts index 333c7bb40b..46db4e1749 100644 --- a/packages/server/src/server/test-utils/session-stubs.ts +++ b/packages/server/src/server/test-utils/session-stubs.ts @@ -42,29 +42,29 @@ export function asAgentManager(stub: { export function asAgentStorage(stub: { [K in keyof SessionOptions["agentStorage"]]?: unknown; }): SessionOptions["agentStorage"] { - return createStub<SessionOptions["agentStorage"]>(stub); + return createStub<SessionOptions["agentStorage"]>({ + listByProviderSession: async () => [], + listByWorkspace: async () => [], + ...stub, + }); } export function asDownloadTokenStore(): SessionOptions["downloadTokenStore"] { return createStub<SessionOptions["downloadTokenStore"]>({}); } -export function asPushTokenStore(): SessionOptions["pushTokenStore"] { - return createStub<SessionOptions["pushTokenStore"]>({}); -} - -export function asChatService(): SessionOptions["chatService"] { - return createStub<SessionOptions["chatService"]>({}); +export function asPushNotifications( + stub: { + [K in keyof SessionOptions["pushNotifications"]]?: unknown; + } = {}, +): SessionOptions["pushNotifications"] { + return createStub<SessionOptions["pushNotifications"]>(stub); } export function asScheduleService(): SessionOptions["scheduleService"] { return createStub<SessionOptions["scheduleService"]>({}); } -export function asLoopService(): SessionOptions["loopService"] { - return createStub<SessionOptions["loopService"]>({}); -} - export function asCheckoutDiffManager(stub: { [K in keyof SessionOptions["checkoutDiffManager"]]?: unknown; }): SessionOptions["checkoutDiffManager"] { diff --git a/packages/server/src/server/test-utils/workspace-git-service-stub.ts b/packages/server/src/server/test-utils/workspace-git-service-stub.ts index 95712a88d2..3e7966241e 100644 --- a/packages/server/src/server/test-utils/workspace-git-service-stub.ts +++ b/packages/server/src/server/test-utils/workspace-git-service-stub.ts @@ -68,6 +68,7 @@ export function createNoopWorkspaceGitService( }, resolveForge: async () => null, commit: async () => {}, + discardChanges: async () => {}, createBranch: async () => {}, switchBranch: async () => ({ source: "local" }), fetch: async () => {}, @@ -175,6 +176,7 @@ export function bindWorkspaceGitService( resolveDefaultBranch: (options) => service.resolveDefaultBranch(cwd, options), resolveRepoRemoteUrl: (options) => service.resolveRepoRemoteUrl(cwd, options), commit: (options) => service.commit(cwd, options), + discardChanges: (paths) => service.discardChanges(cwd, paths), createBranch: (options) => service.createBranch(cwd, options), switchBranch: (branch) => service.switchBranch(cwd, branch), fetch: () => service.fetch(cwd), diff --git a/packages/server/src/server/watcher-liveness-canary.test.ts b/packages/server/src/server/watcher-liveness-canary.test.ts new file mode 100644 index 0000000000..4da1481408 --- /dev/null +++ b/packages/server/src/server/watcher-liveness-canary.test.ts @@ -0,0 +1,33 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, expect, test } from "vitest"; +import { createWatcherLivenessCanary } from "./watcher-liveness-canary.js"; + +const cleanupPaths: string[] = []; + +afterEach(async () => { + await Promise.all( + cleanupPaths.splice(0).map((path) => rm(path, { recursive: true, force: true })), + ); +}); + +test("requires the canary event to round-trip through the watcher callback", async () => { + const watchRoot = await mkdtemp(join(tmpdir(), "paseo-watcher-canary-")); + cleanupPaths.push(watchRoot); + const canary = createWatcherLivenessCanary(watchRoot, { timeoutMs: 1_000 }); + + const verification = canary.verify(); + const canaryPath = canary.path; + expect(canary.filterEvents([{ path: canaryPath, type: "create" }])).toEqual([]); + + await expect(verification).resolves.toBeUndefined(); +}); + +test("rejects when the watcher never reports the canary", async () => { + const watchRoot = await mkdtemp(join(tmpdir(), "paseo-watcher-canary-")); + cleanupPaths.push(watchRoot); + const canary = createWatcherLivenessCanary(watchRoot, { timeoutMs: 10 }); + + await expect(canary.verify()).rejects.toThrow("did not report its liveness canary"); +}); diff --git a/packages/server/src/server/watcher-liveness-canary.ts b/packages/server/src/server/watcher-liveness-canary.ts new file mode 100644 index 0000000000..a366f0229a --- /dev/null +++ b/packages/server/src/server/watcher-liveness-canary.ts @@ -0,0 +1,66 @@ +import { randomUUID } from "node:crypto"; +import { rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { FileChange } from "./file-observer/index.js"; + +export const WATCHER_LIVENESS_CANARY_TIMEOUT_MS = 10_000; + +export interface WatcherLivenessCanary { + readonly path: string; + filterEvents(events: FileChange[]): FileChange[]; + verify(signal?: AbortSignal): Promise<void>; +} + +export function createWatcherLivenessCanary( + watchRoot: string, + options: { timeoutMs?: number } = {}, +): WatcherLivenessCanary { + const canaryPath = join(watchRoot, `.paseo-watcher-canary-${randomUUID()}`); + const timeoutMs = options.timeoutMs ?? WATCHER_LIVENESS_CANARY_TIMEOUT_MS; + let reportCanary!: () => void; + const reported = new Promise<void>((resolve) => { + reportCanary = resolve; + }); + + return { + path: canaryPath, + filterEvents(events) { + const filtered = events.filter((event) => event.path !== canaryPath); + if (filtered.length !== events.length) { + reportCanary(); + } + return filtered; + }, + async verify(signal) { + await writeFile(canaryPath, "paseo watcher liveness canary\n", { flag: "wx" }); + let timeout: NodeJS.Timeout | null = null; + let removeAbortListener = () => {}; + try { + const timeoutPromise = new Promise<never>((_resolve, reject) => { + timeout = setTimeout(() => { + reject( + new Error( + `Watcher for ${watchRoot} did not report its liveness canary within ${timeoutMs}ms`, + ), + ); + }, timeoutMs); + }); + const abortPromise = new Promise<never>((_resolve, reject) => { + if (!signal) return; + const rejectForAbort = () => reject(signal.reason); + if (signal.aborted) { + rejectForAbort(); + return; + } + signal.addEventListener("abort", rejectForAbort, { once: true }); + removeAbortListener = () => signal.removeEventListener("abort", rejectForAbort); + }); + await Promise.race([reported, timeoutPromise, abortPromise]); + } finally { + if (timeout) clearTimeout(timeout); + removeAbortListener(); + await rm(canaryPath, { force: true }); + } + }, + }; +} diff --git a/packages/server/src/server/websocket-server.browser-tools.test.ts b/packages/server/src/server/websocket-server.browser-tools.test.ts index 4ee596c420..7eb238cc66 100644 --- a/packages/server/src/server/websocket-server.browser-tools.test.ts +++ b/packages/server/src/server/websocket-server.browser-tools.test.ts @@ -15,10 +15,8 @@ import type { AgentManager } from "./agent/agent-manager.js"; import type { AgentStorage } from "./agent/agent-storage.js"; import { BrowserToolsBroker } from "./browser-tools/broker.js"; import type { CheckoutDiffManager } from "./checkout-diff-manager.js"; -import type { FileBackedChatService } from "./chat/chat-service.js"; import type { DaemonConfigStore } from "./daemon-config-store.js"; import type { DownloadTokenStore } from "./file-download/token-store.js"; -import type { LoopService } from "./loop-service.js"; import type { ScheduleService } from "./schedule/service.js"; import { createStub } from "./test-utils/class-mocks.js"; import { DaemonClient } from "./test-utils/daemon-client.js"; @@ -307,8 +305,6 @@ function createVoiceAssistantWebSocketServer(params: { undefined, undefined, undefined, - createStub<FileBackedChatService>({}), - createStub<LoopService>({}), createStub<ScheduleService>({}), createStub<CheckoutDiffManager>({ subscribe: () => {}, diff --git a/packages/server/src/server/websocket-server.notifications.test.ts b/packages/server/src/server/websocket-server.notifications.test.ts index f662a044a9..e2bec9c710 100644 --- a/packages/server/src/server/websocket-server.notifications.test.ts +++ b/packages/server/src/server/websocket-server.notifications.test.ts @@ -5,13 +5,11 @@ import type { AgentManager } from "./agent/agent-manager.js"; import type { AgentStorage } from "./agent/agent-storage.js"; import type { DownloadTokenStore } from "./file-download/token-store.js"; import type { DaemonConfigStore } from "./daemon-config-store.js"; -import type { FileBackedChatService } from "./chat/chat-service.js"; -import type { LoopService } from "./loop-service.js"; import type { ScheduleService } from "./schedule/service.js"; import type { CheckoutDiffManager } from "./checkout-diff-manager.js"; import { asInternals, createStub } from "./test-utils/class-mocks.js"; import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js"; -import type { PushNotificationSender, PushPayload } from "./push/notifications.js"; +import type { PushNotificationSender, PushPayload } from "./push/index.js"; import type { WorkspaceAutoName } from "./workspace-auto-name.js"; const WORKSPACE_ID = "workspace-1"; @@ -125,8 +123,6 @@ function createServer(agentManagerOverrides?: Record<string, unknown>) { undefined, undefined, undefined, - createStub<FileBackedChatService>({}), - createStub<LoopService>({}), createStub<ScheduleService>({}), createStub<CheckoutDiffManager>({ subscribe: vi.fn(), diff --git a/packages/server/src/server/websocket-server.relay-reconnect.test.ts b/packages/server/src/server/websocket-server.relay-reconnect.test.ts index 790d06338c..b14e90c7a0 100644 --- a/packages/server/src/server/websocket-server.relay-reconnect.test.ts +++ b/packages/server/src/server/websocket-server.relay-reconnect.test.ts @@ -5,8 +5,6 @@ import type { AgentManager } from "./agent/agent-manager.js"; import type { AgentStorage } from "./agent/agent-storage.js"; import type { DownloadTokenStore } from "./file-download/token-store.js"; import type { DaemonConfigStore } from "./daemon-config-store.js"; -import type { FileBackedChatService } from "./chat/chat-service.js"; -import type { LoopService } from "./loop-service.js"; import type { ScheduleService } from "./schedule/service.js"; import type { CheckoutDiffManager } from "./checkout-diff-manager.js"; import type { WorkspaceAutoName } from "./workspace-auto-name.js"; @@ -88,20 +86,12 @@ vi.mock("./session.js", () => ({ Session: sessionMock.MockSession, })); -vi.mock("./push/token-store.js", () => ({ - PushTokenStore: class { - getAllTokens(): string[] { - return []; - } - }, -})); - -vi.mock("./push/push-service.js", () => ({ - PushService: class { - async sendPush(): Promise<void> { - // no-op - } - }, +vi.mock("./push/index.js", () => ({ + createPushNotifications: () => ({ + renew: () => undefined, + revoke: () => undefined, + send: async () => undefined, + }), })); import { z } from "zod"; @@ -281,8 +271,6 @@ function createServer(options?: { undefined, undefined, undefined, - createStub<FileBackedChatService>({}), - createStub<LoopService>({}), createStub<ScheduleService>({}), createStub<CheckoutDiffManager>({ subscribe: vi.fn(), diff --git a/packages/server/src/server/websocket-server.terminal-notifications.test.ts b/packages/server/src/server/websocket-server.terminal-notifications.test.ts index 7d3cba294f..1d7160ed30 100644 --- a/packages/server/src/server/websocket-server.terminal-notifications.test.ts +++ b/packages/server/src/server/websocket-server.terminal-notifications.test.ts @@ -5,8 +5,6 @@ import type { AgentManager } from "./agent/agent-manager.js"; import type { AgentStorage } from "./agent/agent-storage.js"; import type { DownloadTokenStore } from "./file-download/token-store.js"; import type { DaemonConfigStore } from "./daemon-config-store.js"; -import type { FileBackedChatService } from "./chat/chat-service.js"; -import type { LoopService } from "./loop-service.js"; import type { ScheduleService } from "./schedule/service.js"; import type { CheckoutDiffManager } from "./checkout-diff-manager.js"; import type { @@ -17,7 +15,7 @@ import type { import type { PersistedWorkspaceRecord, WorkspaceRegistry } from "./workspace-registry.js"; import { asInternals, createStub } from "./test-utils/class-mocks.js"; import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js"; -import type { PushNotificationSender, PushPayload } from "./push/notifications.js"; +import type { PushNotificationSender, PushPayload } from "./push/index.js"; import type { WorkspaceAutoName } from "./workspace-auto-name.js"; const wsModuleMock = vi.hoisted(() => { @@ -156,8 +154,6 @@ function createServer(terminalManager: TerminalManager, workspaceRegistry?: Work undefined, undefined, workspaceRegistry, - createStub<FileBackedChatService>({}), - createStub<LoopService>({}), createStub<ScheduleService>({}), createStub<CheckoutDiffManager>({ subscribe: vi.fn(), diff --git a/packages/server/src/server/websocket-server.ts b/packages/server/src/server/websocket-server.ts index 0278e0316f..893e5b9c86 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -11,8 +11,6 @@ import type { TerminalManager } from "../terminal/terminal-manager.js"; import type pino from "pino"; import type { ProjectRegistry, WorkspaceRegistry } from "./workspace-registry.js"; import type { ProjectUpdate } from "./workspace-reconciliation-service.js"; -import type { FileBackedChatService } from "./chat/chat-service.js"; -import type { LoopService } from "./loop-service.js"; import type { ScheduleService } from "./schedule/service.js"; import type { CheckoutDiffManager, CheckoutDiffMetrics } from "./checkout-diff-manager.js"; import type { DaemonConfigStore, MutableDaemonConfig } from "./daemon-config-store.js"; @@ -49,8 +47,11 @@ import type { WorkspaceAutoName } from "./workspace-auto-name.js"; import type { WorkspaceRuntimeService } from "./workspace-runtime/index.js"; import type { ProviderProbeService } from "./provider-probe/index.js"; import { deriveProjectSlug } from "./workspace-git-metadata.js"; -import { PushTokenStore } from "./push/token-store.js"; -import { createPushNotificationSender, type PushNotificationSender } from "./push/notifications.js"; +import { + createPushNotifications, + type PushNotifications, + type PushNotificationSender, +} from "./push/index.js"; import type { ScriptHealthState } from "./script-health-monitor.js"; import type { ServiceProxySubsystem } from "./service-proxy.js"; import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js"; @@ -92,6 +93,7 @@ import { } from "@getpaseo/protocol/browser-automation/capabilities"; import type { BrowserToolsBroker } from "./browser-tools/broker.js"; import type { DaemonRuntimeConfig } from "./session/daemon/daemon-session.js"; +import { DirectorySyncService } from "./directory-sync/index.js"; import { APPLICATION_SOCKET_LEASE_CHECK_INTERVAL_MS, ApplicationSocketLease, @@ -223,6 +225,7 @@ function createFallbackWorkspaceGitService(): WorkspaceGitService { getSnapshot: async (cwd: string) => createFallbackWorkspaceGitSnapshot(cwd), resolveForge: async () => null, commit: async () => {}, + discardChanges: async () => {}, createBranch: async () => {}, switchBranch: async () => ({ source: "local" }), fetch: async () => {}, @@ -514,32 +517,22 @@ export class MissingDaemonVersionError extends Error { } interface RequiredWebSocketServices { - chatService: FileBackedChatService; - loopService: LoopService; scheduleService: ScheduleService; checkoutDiffManager: CheckoutDiffManager; } function requireWebSocketServices(params: { - chatService?: FileBackedChatService; - loopService?: LoopService; scheduleService?: ScheduleService; checkoutDiffManager?: CheckoutDiffManager; }): RequiredWebSocketServices { - const { chatService, loopService, scheduleService, checkoutDiffManager } = params; - if (!chatService) { - throw new Error("VoiceAssistantWebSocketServer requires a chat service."); - } - if (!loopService) { - throw new Error("VoiceAssistantWebSocketServer requires a loop service."); - } + const { scheduleService, checkoutDiffManager } = params; if (!scheduleService) { throw new Error("VoiceAssistantWebSocketServer requires a schedule service."); } if (!checkoutDiffManager) { throw new Error("VoiceAssistantWebSocketServer requires a checkout diff manager."); } - return { chatService, loopService, scheduleService, checkoutDiffManager }; + return { scheduleService, checkoutDiffManager }; } /** @@ -561,8 +554,6 @@ export class VoiceAssistantWebSocketServer { private readonly workspaceRegistry: WorkspaceRegistry; private readonly workspaceRuntime: WorkspaceRuntimeService | undefined; private readonly providerProbe: ProviderProbeService | undefined; - private readonly chatService: FileBackedChatService; - private readonly loopService: LoopService; private readonly scheduleService: ScheduleService; private readonly checkoutDiffManager: CheckoutDiffManager; private readonly github: ForgeService; @@ -572,7 +563,7 @@ export class VoiceAssistantWebSocketServer { private readonly paseoHome: string; private readonly worktreesRoot: string | undefined; private readonly daemonConfigStore: DaemonConfigStore; - private readonly pushTokenStore: PushTokenStore; + private readonly pushNotifications: PushNotifications; private readonly pushNotificationSender: PushNotificationSender; private readonly mcpBaseUrl: string | null; private speech!: SpeechService | null; @@ -612,6 +603,7 @@ export class VoiceAssistantWebSocketServer { private acceptingConnections = true; private readonly advertiseDaemonStatusRpc: boolean; private readonly advertiseRelayConfig: boolean; + private readonly directorySync = new DirectorySyncService(); constructor( server: HTTPServer, @@ -635,8 +627,6 @@ export class VoiceAssistantWebSocketServer { onLifecycleIntent?: (intent: SessionLifecycleIntent) => void, projectRegistry?: ProjectRegistry, workspaceRegistry?: WorkspaceRegistry, - chatService?: FileBackedChatService, - loopService?: LoopService, scheduleService?: ScheduleService, checkoutDiffManager?: CheckoutDiffManager, serviceProxy?: ServiceProxySubsystem | null, @@ -680,13 +670,9 @@ export class VoiceAssistantWebSocketServer { this.workspaceRuntime = workspaceRuntime; this.providerProbe = providerProbe; const requiredServices = requireWebSocketServices({ - chatService, - loopService, scheduleService, checkoutDiffManager, }); - this.chatService = requiredServices.chatService; - this.loopService = requiredServices.loopService; this.scheduleService = requiredServices.scheduleService; this.checkoutDiffManager = requiredServices.checkoutDiffManager; this.github = github ?? createGitHubService(); @@ -731,9 +717,11 @@ export class VoiceAssistantWebSocketServer { }); const pushLogger = this.logger.child({ module: "push" }); - this.pushTokenStore = new PushTokenStore(pushLogger, join(paseoHome, "push-tokens.json")); - this.pushNotificationSender = - pushNotificationSender ?? createPushNotificationSender(pushLogger, this.pushTokenStore); + this.pushNotifications = createPushNotifications({ + logger: pushLogger, + filePath: join(paseoHome, "push-tokens.json"), + }); + this.pushNotificationSender = pushNotificationSender ?? this.pushNotifications; this.agentManager.setAgentAttentionCallback((params) => { void this.broadcastAgentAttention(params).catch((err) => { @@ -943,7 +931,11 @@ export class VoiceAssistantWebSocketServer { } public publishProjectUpdate(update: ProjectUpdate): void { - for (const session of this.listTrustedSessions()) session.emitProjectUpdate(update); + for (const session of this.listTrustedSessions()) { + void session + .emitProjectUpdate(update) + .catch((error) => this.logger.warn({ err: error }, "Failed to publish project update")); + } } public publishSpeechReadiness(readiness: SpeechReadinessSnapshot | null): void { @@ -1383,7 +1375,7 @@ export class VoiceAssistantWebSocketServer { ); }, downloadTokenStore: this.downloadTokenStore, - pushTokenStore: this.pushTokenStore, + pushNotifications: this.pushNotifications, paseoHome: this.paseoHome, worktreesRoot: this.worktreesRoot, agentManager: this.agentManager, @@ -1392,8 +1384,7 @@ export class VoiceAssistantWebSocketServer { workspaceRegistry: this.workspaceRegistry, workspaceRuntime: this.workspaceRuntime, providerProbe: this.providerProbe, - chatService: this.chatService, - loopService: this.loopService, + directorySync: this.directorySync, scheduleService: this.scheduleService, checkoutDiffManager: this.checkoutDiffManager, github: this.github, @@ -1579,6 +1570,8 @@ export class VoiceAssistantWebSocketServer { desktopManaged: this.daemonRuntimeConfig?.desktopManaged === true, ...(this.serverCapabilities ? { capabilities: this.serverCapabilities } : {}), features: { + // COMPAT(directorySync): added in v0.3.x, remove gate after 2027-02-12. + directorySync: true, // COMPAT(providersSnapshot): keep optional until all clients rely on snapshot flow. providersSnapshot: true, // COMPAT(providersSnapshotCwd): added in v0.3.2, remove gate after 2027-02-10. @@ -1597,6 +1590,8 @@ export class VoiceAssistantWebSocketServer { ...(this.advertiseDaemonStatusRpc ? { daemonStatusRpc: true } : {}), // COMPAT(relayConfig): added in v0.2.6, remove gate after 2027-01-31. ...(this.advertiseRelayConfig ? { relayConfig: true } : {}), + // COMPAT(pushTokenRevocation): added in v0.3.2, remove gate after 2027-02-10. + pushTokenRevocation: true, // COMPAT(terminalRestoreModes): added in v0.1.81, remove gate after 2026-11-23. "terminal-restore-modes": true, // COMPAT(terminalInputModeReplay): added in v0.2.6, remove gate after 2027-02-02. @@ -1673,6 +1668,16 @@ export class VoiceAssistantWebSocketServer { workspaceScriptManagement: true, // COMPAT(projectCustomIcon): added in v0.2.0, remove after 2027-01-20. projectCustomIcon: true, + // COMPAT(fsEntryOps): added in v0.3.0, remove gate after 2027-02-08. + fsEntryOps: true, + // COMPAT(fsEntryDuplicate): added in v0.3.0, remove gate after 2027-02-09. + fsEntryDuplicate: true, + // COMPAT(checkoutDiscardChanges): added in v0.3.0, remove gate after 2027-02-08. + checkoutDiscardChanges: true, + // COMPAT(agentProfiles): added in v0.3.2, remove gate after 2027-02-11. + agentProfiles: true, + // COMPAT(agentConfigApply): added in v0.3.2, remove gate after 2027-02-11. + agentConfigApply: true, }, }; } diff --git a/packages/server/src/server/wire-compat.test.ts b/packages/server/src/server/wire-compat.test.ts index 85e402e61c..89c4419823 100644 --- a/packages/server/src/server/wire-compat.test.ts +++ b/packages/server/src/server/wire-compat.test.ts @@ -11,6 +11,7 @@ import { type SessionOutboundMessage, } from "@getpaseo/protocol/messages"; import { Session, type SessionOptions } from "./session.js"; +import { DirectorySyncService } from "./directory-sync/index.js"; import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js"; import type { AgentTimelineRow } from "./agent/agent-manager.js"; import { InMemoryAgentTimelineStore } from "./agent/agent-timeline-store.js"; @@ -180,6 +181,7 @@ class InMemoryWorktreeWorkflow { function createSessionForWireCompatTest(options?: { clientCapabilities?: Record<string, unknown> | null; + directorySync?: DirectorySyncService; messages?: SessionOutboundMessage[]; rows?: AgentTimelineRow[]; }): Session { @@ -209,7 +211,7 @@ function createSessionForWireCompatTest(options?: { onMessage: (message) => messages.push(message), logger: pino({ level: "silent" }), downloadTokenStore: {} as SessionOptions["downloadTokenStore"], - pushTokenStore: {} as SessionOptions["pushTokenStore"], + pushNotifications: {} as SessionOptions["pushNotifications"], paseoHome: "/tmp/paseo-home", agentManager: new InMemoryAgentManager( options?.rows ?? rows, @@ -218,9 +220,8 @@ function createSessionForWireCompatTest(options?: { projectRegistry: new EmptyProjectRegistry() as unknown as SessionOptions["projectRegistry"], workspaceRegistry: new EmptyWorkspaceRegistry() as unknown as SessionOptions["workspaceRegistry"], - chatService: {} as SessionOptions["chatService"], + directorySync: options?.directorySync, scheduleService: {} as SessionOptions["scheduleService"], - loopService: {} as SessionOptions["loopService"], checkoutDiffManager: { scheduleRefreshForCwd() {}, onWorkspaceStateMayHaveChanged() {}, @@ -306,7 +307,7 @@ async function emitTimelineResponse(options?: { } describe("wire compatibility", () => { - test("sends project updates only to clients that declare support", () => { + test("sends project updates only to clients that declare support", async () => { const project = createPersistedProjectRecord({ projectId: "project-1", rootPath: "/tmp/project", @@ -324,10 +325,12 @@ describe("wire compatibility", () => { messages: capableMessages, }); - legacy.emitProjectUpdate({ kind: "upsert", project }); - legacy.emitProjectUpdate({ kind: "remove", projectId: project.projectId }); - capable.emitProjectUpdate({ kind: "upsert", project }); - capable.emitProjectUpdate({ kind: "remove", projectId: project.projectId }); + await Promise.all([ + legacy.emitProjectUpdate({ kind: "upsert", project }), + legacy.emitProjectUpdate({ kind: "remove", projectId: project.projectId }), + capable.emitProjectUpdate({ kind: "upsert", project }), + capable.emitProjectUpdate({ kind: "remove", projectId: project.projectId }), + ]); expect(legacyMessages).toEqual([]); expect(capableMessages.map((message) => SessionOutboundMessageSchema.parse(message))).toEqual([ @@ -340,6 +343,7 @@ describe("wire compatibility", () => { projectDisplayName: "Favorite project", projectCustomName: "Favorite project", projectCustomIconRevision: null, + projectIconRevision: "automatic:none:v1", projectRootPath: "/tmp/project", projectKind: "git", }, @@ -352,6 +356,50 @@ describe("wire compatibility", () => { ]); }); + test("publishes rapid project mutations in order before incremental reconciliation", async () => { + const directorySync = new DirectorySyncService("generation"); + const initial = directorySync.synchronizeProjects([], {}); + const messages: SessionOutboundMessage[] = []; + const session = createSessionForWireCompatTest({ + clientCapabilities: { [CLIENT_CAPS.projectUpdates]: true }, + directorySync, + messages, + }); + const project = createPersistedProjectRecord({ + projectId: "project-ordered", + rootPath: "/tmp/project-ordered", + kind: "git", + displayName: "Ordered project", + createdAt: "2026-07-15T00:00:00.000Z", + updatedAt: "2026-07-15T00:00:00.000Z", + }); + + await Promise.all([ + session.emitProjectUpdate({ kind: "upsert", project }), + session.emitProjectUpdate({ kind: "remove", projectId: project.projectId }), + ]); + + expect( + messages.flatMap((message) => + message.type === "project.update" ? [message.payload.kind] : [], + ), + ).toEqual(["upsert", "remove"]); + expect( + directorySync.synchronizeProjects([], { + generation: initial.sync.generation, + afterSeq: initial.sync.headSeq, + }), + ).toEqual({ + projects: [], + sync: { + generation: "generation", + mode: "changes", + headSeq: 2, + removals: [{ id: "project-ordered", seq: 2 }], + }, + }); + }); + test("downgrades reasoning_merge for clients that do not declare the capability", async () => { const response = await emitTimelineResponse(); diff --git a/packages/server/src/server/workspace-archive-service.test.ts b/packages/server/src/server/workspace-archive-service.test.ts index e5d505d754..9d789e1d23 100644 --- a/packages/server/src/server/workspace-archive-service.test.ts +++ b/packages/server/src/server/workspace-archive-service.test.ts @@ -161,8 +161,8 @@ function createArchiveDeps(input: ArchiveDepsInput): ArchiveTestDependencies { }), }, agentStorage: { - list: async (): Promise<StoredAgentRecord[]> => [], - } as Pick<AgentStorage, "list">, + listByWorkspace: async (): Promise<StoredAgentRecord[]> => [], + } as Pick<AgentStorage, "listByWorkspace">, findWorkspaceIdForCwd: input.findWorkspaceIdForCwd ?? vi.fn(async () => null), listActiveWorkspaces: async () => active.filter((workspace) => !archivedWorkspaceIds.has(workspace.workspaceId)), @@ -711,12 +711,15 @@ describe("archiveByScope", () => { }), }; deps.agentStorage = { - list: async () => - [ - { id: targetStoredAgentId, workspaceId: targetWorkspaceId, archivedAt: null }, - { id: otherStoredAgentId, workspaceId: otherWorkspaceId, archivedAt: null }, - ] as StoredAgentRecord[], - } as Pick<AgentStorage, "list">; + listByWorkspace: async (workspaceId: string) => + workspaceId === targetWorkspaceId + ? ([ + { id: targetStoredAgentId, workspaceId: targetWorkspaceId, archivedAt: null }, + ] as StoredAgentRecord[]) + : ([ + { id: otherStoredAgentId, workspaceId: otherWorkspaceId, archivedAt: null }, + ] as StoredAgentRecord[]), + } as Pick<AgentStorage, "listByWorkspace">; const result = await archiveByScope(deps, { scope: { kind: "workspace", workspaceId: targetWorkspaceId }, diff --git a/packages/server/src/server/workspace-archive-service.ts b/packages/server/src/server/workspace-archive-service.ts index 4b4ff5d4f3..5b1fb5ad5f 100644 --- a/packages/server/src/server/workspace-archive-service.ts +++ b/packages/server/src/server/workspace-archive-service.ts @@ -33,7 +33,7 @@ export interface ArchiveDependencies { github: ForgeService; workspaceGitService: Pick<WorkspaceGitService, "getSnapshot">; agentManager: Pick<AgentManager, "listAgents" | "getAgent" | "archiveAgent" | "archiveSnapshot">; - agentStorage: Pick<AgentStorage, "list">; + agentStorage: Pick<AgentStorage, "listByWorkspace">; // Resolves the worktree at a path to its workspaceId for archive-by-path. The // path uniquely identifies a worktree workspace; this is a directory lookup for // the archive target, not status/ownership. @@ -476,16 +476,14 @@ export async function archiveWorkspaceContents( let storedRecords: StoredAgentRecord[] = []; try { - storedRecords = await dependencies.agentStorage.list(); + storedRecords = await dependencies.agentStorage.listByWorkspace(workspaceId); } catch (error) { dependencies.sessionLogger?.warn( { err: error, workspaceId }, "Failed to list stored agents during workspace archive; continuing", ); } - const matchingStoredRecords = storedRecords.filter( - (record) => record.workspaceId === workspaceId, - ); + const matchingStoredRecords = storedRecords; for (const record of matchingStoredRecords) { archivedAgents.add(record.id); } diff --git a/packages/server/src/server/workspace-create-git-fanout.local.e2e.test.ts b/packages/server/src/server/workspace-create-git-fanout.local.e2e.test.ts index 7037d7f177..82f580ef1b 100644 --- a/packages/server/src/server/workspace-create-git-fanout.local.e2e.test.ts +++ b/packages/server/src/server/workspace-create-git-fanout.local.e2e.test.ts @@ -153,6 +153,18 @@ async function startObservedFixture(siblingCount: number): Promise<ReturnType<ty return fixture; } +test("quiet observed workspaces spawn no git commands beyond the old self-heal interval", async () => { + await startObservedFixture(2); + await settleGitCommands(); + + startGitCommandMetrics(); + await new Promise((resolve) => setTimeout(resolve, 61_000)); + await waitForGitCommandMetricsIdle({ quietMs: 1_500, timeoutMs: 5_000 }); + const idle = stopGitCommandMetrics(); + + expect(idle.submissions).toEqual([]); +}, 90_000); + interface GitRuntimeExpectation { currentBranch?: string; remoteUrl?: string; @@ -377,21 +389,21 @@ test.each([1, 2, 10])( const creation = stopGitCommandMetrics(); expect(response?.workspace?.name).toBe("created-during-measurement"); - expect(creation.submitted).toBe(90); expect(countGitOperations(creation.submissions)).toEqual({ branch: 4, - config: 19, - diff: 2, - "for-each-ref": 6, - "ls-files": 3, - "merge-base": 2, - "rev-list": 2, - "rev-parse": 34, - "show-ref": 6, - status: 6, + config: 22, + diff: 3, + "for-each-ref": 7, + "ls-files": 4, + "merge-base": 3, + "rev-list": 3, + "rev-parse": 38, + "show-ref": 7, + status: 7, "symbolic-ref": 4, - worktree: 2, + worktree: 1, }); + expect(creation.submitted).toBe(103); const existingWorktrees = new Set(fixture.siblingWorktrees.map((cwd) => realpathSync(cwd))); expect( creation.submissions.filter((command) => existingWorktrees.has(realpathSync(command.cwd))), diff --git a/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts b/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts index 2fbdc930cc..0ae15e526c 100644 --- a/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts +++ b/packages/server/src/server/workspace-create-worktree-source.e2e.test.ts @@ -95,3 +95,132 @@ test("workspace.create keeps a branch-off name separate from its worktree slug", rmSync(tempRoot, { recursive: true, force: true }); } }, 180000); + +test("workspace.create always creates a new worktree when the slug is already occupied", async () => { + const daemon = await createTestPaseoDaemon(); + const { repoDir, tempRoot } = createGitRepoWithBranch(); + const client = new DaemonClient({ + url: `ws://127.0.0.1:${daemon.port}/ws`, + appVersion: "0.1.82", + }); + + try { + await client.connect(); + + const first = await client.createWorkspace({ + source: { + kind: "worktree", + cwd: repoDir, + action: "branch-off", + worktreeSlug: "same-slug", + baseBranch: "main", + }, + }); + const second = await client.createWorkspace({ + source: { + kind: "worktree", + cwd: repoDir, + action: "branch-off", + worktreeSlug: "same-slug", + baseBranch: "main", + }, + }); + + expect(first.error).toBeNull(); + expect(second.error).toBeNull(); + expect(path.basename(first.workspace?.workspaceDirectory ?? "")).toBe("same-slug"); + expect(path.basename(second.workspace?.workspaceDirectory ?? "")).toBe("same-slug-1"); + expect(second.workspace?.id).not.toBe(first.workspace?.id); + } finally { + await client.close().catch(() => undefined); + await daemon.close(); + rmSync(tempRoot, { recursive: true, force: true }); + } +}, 180000); + +test("workspace.create suffixes past an occupied detached worktree", async () => { + const daemon = await createTestPaseoDaemon(); + const { repoDir, tempRoot } = createGitRepoWithBranch(); + const client = new DaemonClient({ + url: `ws://127.0.0.1:${daemon.port}/ws`, + appVersion: "0.1.82", + }); + + try { + await client.connect(); + + const first = await client.createWorkspace({ + source: { + kind: "worktree", + cwd: repoDir, + action: "branch-off", + worktreeSlug: "detached-slug", + baseBranch: "main", + }, + }); + expect(first.error).toBeNull(); + expect(first.workspace?.workspaceDirectory).toBeTypeOf("string"); + const firstPath = first.workspace?.workspaceDirectory as string; + execFileSync("git", ["checkout", "--detach"], { cwd: firstPath, stdio: "pipe" }); + + const second = await client.createWorkspace({ + source: { + kind: "worktree", + cwd: repoDir, + action: "branch-off", + worktreeSlug: "detached-slug", + baseBranch: "main", + }, + }); + + expect(second.error).toBeNull(); + expect(path.basename(second.workspace?.workspaceDirectory ?? "")).toBe("detached-slug-1"); + expect(second.workspace?.gitRuntime?.currentBranch).toBe("detached-slug-1"); + } finally { + await client.close().catch(() => undefined); + await daemon.close(); + rmSync(tempRoot, { recursive: true, force: true }); + } +}, 180000); + +test("workspace.create suffixes an occupied checkout branch", async () => { + const daemon = await createTestPaseoDaemon(); + const { repoDir, tempRoot } = createGitRepoWithBranch(); + const client = new DaemonClient({ + url: `ws://127.0.0.1:${daemon.port}/ws`, + appVersion: "0.1.82", + }); + + try { + await client.connect(); + + const first = await client.createWorkspace({ + source: { + kind: "worktree", + cwd: repoDir, + action: "checkout", + refName: "feature/existing-branch", + worktreeSlug: "existing-branch", + }, + }); + const second = await client.createWorkspace({ + source: { + kind: "worktree", + cwd: repoDir, + action: "checkout", + refName: "feature/existing-branch", + worktreeSlug: "existing-branch", + }, + }); + + expect(first.error).toBeNull(); + expect(first.workspace?.gitRuntime?.currentBranch).toBe("feature/existing-branch"); + expect(second.error).toBeNull(); + expect(path.basename(second.workspace?.workspaceDirectory ?? "")).toBe("existing-branch-1"); + expect(second.workspace?.gitRuntime?.currentBranch).toBe("feature/existing-branch-1"); + } finally { + await client.close().catch(() => undefined); + await daemon.close(); + rmSync(tempRoot, { recursive: true, force: true }); + } +}, 180000); diff --git a/packages/server/src/server/workspace-git-service.observation.integration.test.ts b/packages/server/src/server/workspace-git-service.observation.integration.test.ts index 3d0d3b757c..d8c4bc04a5 100644 --- a/packages/server/src/server/workspace-git-service.observation.integration.test.ts +++ b/packages/server/src/server/workspace-git-service.observation.integration.test.ts @@ -184,7 +184,6 @@ test("recursive observation updates tracked state and prunes ignored storms", as getCheckoutWorktreeState, getCheckoutDiff, runGitCommand, - getWorkspaceGitSelfHealPhaseMs: () => 7_000, } as never, }); const diffManager = new CheckoutDiffManager({ @@ -324,7 +323,7 @@ test("recursive observation updates tracked state and prunes ignored storms", as editedDuringIgnoreUpdate = true; writeFileSync(newlyTrackedPath, "first\nsecond\n"); }; - writeFileSync(path.join(repoDir, ".git", "index"), "force-added\n"); + writeFileSync(path.join(repoDir, ".gitignore"), "cache/\n"); await vi.waitFor( () => { expect(editedDuringIgnoreUpdate).toBe(true); diff --git a/packages/server/src/server/workspace-git-service.observation.test.ts b/packages/server/src/server/workspace-git-service.observation.test.ts index 61d90f212a..e863ebb4a0 100644 --- a/packages/server/src/server/workspace-git-service.observation.test.ts +++ b/packages/server/src/server/workspace-git-service.observation.test.ts @@ -191,6 +191,11 @@ function createService( exitCode: 0, signal: null, })), + createWatcherLivenessCanary: vi.fn(() => ({ + path: "", + filterEvents: (events) => events, + verify: vi.fn(async () => {}), + })), ...overrides, } as never, }); @@ -503,6 +508,15 @@ describe("WorkspaceGitService checkout observation", () => { ...current, upstreamStatus: facts.upstreamStatus, })); + const runGitFetch = vi.fn(async (_cwd, observer) => { + observer?.onRefSnapshot("before"); + await releaseFetch.promise; + observer?.onRefSnapshot("after"); + return { + changes: [{ kind: "moved" as const, ref: "origin/main", beforeOid: "a", afterOid: "b" }], + error: null, + }; + }); const service = createService(watcher, { getCheckoutSnapshotFacts, getCheckoutRefDerivedState, @@ -515,22 +529,16 @@ describe("WorkspaceGitService checkout observation", () => { }), ), hasOriginRemote: vi.fn(async () => true), - runGitFetch: vi.fn(async (_cwd, observer) => { - observer?.onRefSnapshot("before"); - await releaseFetch.promise; - observer?.onRefSnapshot("after"); - return { - changes: [{ kind: "moved" as const, ref: "origin/main", beforeOid: "a", afterOid: "b" }], - error: null, - }; - }), + runGitFetch, }); const listener = vi.fn(); const subscription = service.registerWorkspace({ cwd: REPO_CWD }, listener); + await service.getSnapshot(REPO_CWD); await vi.waitFor(() => { + expect(runGitFetch).toHaveBeenCalledTimes(1); expect(service.getMetrics().fetchInFlightCount).toBe(1); - expect(service.getMetrics().workspaceRefreshInFlightCount).toBe(0); }); + expect(service.getMetrics().workspaceRefreshInFlightCount).toBe(0); listener.mockClear(); watcher.records @@ -539,7 +547,9 @@ describe("WorkspaceGitService checkout observation", () => { { path: path.join(GIT_DIR, "refs", "remotes", "origin", "main"), type: "create" }, ]); releaseFetch.resolve(); - await flushPromises(); + await vi.waitFor(() => { + expect(service.getMetrics().fetchInFlightCount).toBe(0); + }); await vi.advanceTimersByTimeAsync(1_000); await vi.waitFor(() => { expect(getCheckoutRefDerivedState).toHaveBeenCalledTimes(1); @@ -798,6 +808,10 @@ describe("WorkspaceGitService checkout observation", () => { ...current, upstreamStatus: facts.upstreamStatus, })); + const runGitFetch = vi.fn(async () => ({ + changes: [{ kind: "moved" as const, ref: "origin/main", beforeOid: "a", afterOid: "b" }], + error: null, + })); const service = createService(watcher, { getCheckoutSnapshotFacts, getCheckoutRefDerivedState, @@ -810,13 +824,16 @@ describe("WorkspaceGitService checkout observation", () => { }), ), hasOriginRemote: vi.fn(async () => true), - runGitFetch: vi.fn(async () => ({ - changes: [{ kind: "moved" as const, ref: "origin/main", beforeOid: "a", afterOid: "b" }], - error: null, - })), + runGitFetch, }); const subscription = service.registerWorkspace({ cwd: REPO_CWD }, vi.fn()); + await service.getSnapshot(REPO_CWD); + await vi.waitFor(() => { + expect(runGitFetch).toHaveBeenCalledTimes(1); + expect(service.getMetrics().fetchInFlightCount).toBe(0); + }); + expect(service.getMetrics().workspaceRefreshInFlightCount).toBe(0); await vi.advanceTimersByTimeAsync(1_000); await vi.waitFor(() => { expect(getCheckoutRefDerivedState).toHaveBeenCalledTimes(1); @@ -1571,6 +1588,47 @@ describe("WorkspaceGitService checkout observation", () => { service.dispose(); }); + test("repository watcher canary timeout closes the subscription and starts fallback polling", async () => { + const watcher = createWatcherHarness(); + const logger = createLogger(); + const getCheckoutStatus = vi.fn(async (cwd: string) => createCheckoutStatus(cwd)); + const verifyCanary = vi.fn(async () => { + throw new Error("watcher did not report its liveness canary"); + }); + const service = createService( + watcher, + { + getCheckoutStatus, + createWatcherLivenessCanary: vi.fn(() => ({ + path: path.join(GIT_DIR, "paseo", ".watcher-canary-timeout"), + filterEvents: (events: WatchEvent[]) => events, + verify: verifyCanary, + })), + }, + logger, + ); + const subscription = service.registerWorkspace({ cwd: REPO_CWD }, vi.fn()); + + await vi.waitFor(() => { + expect(service.getMetrics().workspaceObservationSetupInFlightCount).toBe(0); + }); + const repositoryWatcher = getWatcherRecordsForDirectory(watcher, GIT_DIR)[0]; + expect(repositoryWatcher?.unsubscribe).toHaveBeenCalledTimes(1); + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ repoGitRoot: GIT_DIR }), + "Failed to start repository metadata watcher; using degraded polling", + ); + + const statusCallsBeforePoll = getCheckoutStatus.mock.calls.length; + await vi.advanceTimersByTimeAsync(5_000); + await vi.waitFor(() => { + expect(getCheckoutStatus.mock.calls.length).toBeGreaterThan(statusCallsBeforePoll); + }); + + subscription.unsubscribe(); + service.dispose(); + }); + test("setup-time watcher error defers recovery until subscribe settles", async () => { const watcher = createWatcherHarness(); const openedSubscription = createDeferred<{ @@ -1850,7 +1908,7 @@ describe("WorkspaceGitService checkout observation", () => { expect(checkoutWatcher?.ignore).toContain(path.join(REPO_CWD, "node_modules")); ignoredDirectories = "node_modules/\nbuild/\n"; - await vi.advanceTimersByTimeAsync(1_000); + checkoutWatcher?.callback(null, [{ path: path.join(REPO_CWD, ".gitignore"), type: "update" }]); await vi.waitFor(() => { expect(runGitCommand).toHaveBeenCalledTimes(3); expect(checkoutWatcher?.updateIgnore).toHaveBeenCalledTimes(1); @@ -1906,7 +1964,7 @@ describe("WorkspaceGitService checkout observation", () => { const originalWatcher = watcher.records.find((record) => record.directory === REPO_CWD); ignoredDirectories = "node_modules/\n"; - await vi.advanceTimersByTimeAsync(1_000); + originalWatcher?.callback(null, [{ path: path.join(REPO_CWD, ".gitignore"), type: "update" }]); await vi.waitFor(() => { expect(originalWatcher?.updateIgnore).toHaveBeenCalledTimes(1); expect(service.getMetrics().workspaceRefreshInFlightCount).toBe(0); @@ -1928,6 +1986,93 @@ describe("WorkspaceGitService checkout observation", () => { service.dispose(); }); + test("repository exclude changes update working-tree filtering", async () => { + const watcher = createWatcherHarness(); + let ignoredDirectories = "node_modules/\n"; + const runGitCommand = vi.fn(async (args: string[]) => ({ + stdout: args[0] === "rev-parse" ? `${REPO_CWD}\n` : ignoredDirectories, + stderr: "", + truncated: false, + exitCode: 0, + signal: null, + })); + const service = createService(watcher, { runGitCommand }); + const subscription = service.registerWorkspace({ cwd: REPO_CWD }, vi.fn()); + + await vi.waitFor(() => { + expect(service.getMetrics().workspaceObservationSetupInFlightCount).toBe(0); + }); + const checkoutWatcher = getWatcherRecordsForDirectory(watcher, REPO_CWD)[0]; + const repositoryWatcher = getWatcherRecordsForDirectory(watcher, GIT_DIR)[0]; + ignoredDirectories = "node_modules/\nbuild/\n"; + + repositoryWatcher?.callback(null, [ + { path: path.join(GIT_DIR, "info", "exclude"), type: "update" }, + ]); + await vi.waitFor(() => { + expect(checkoutWatcher?.updateIgnore).toHaveBeenCalledTimes(1); + }); + expect(checkoutWatcher?.ignore).toContain(path.join(REPO_CWD, "build")); + + subscription.unsubscribe(); + service.dispose(); + }); + + test("ignore events during a refresh trigger a trailing recomputation", async () => { + const watcher = createWatcherHarness(); + const blockedRefresh = createDeferred<{ + stdout: string; + stderr: string; + truncated: boolean; + exitCode: number; + signal: null; + }>(); + let lsFilesCallCount = 0; + const runGitCommand = vi.fn(async (args: string[]) => { + if (args[0] === "rev-parse") { + return { + stdout: `${REPO_CWD}\n`, + stderr: "", + truncated: false, + exitCode: 0, + signal: null, + }; + } + lsFilesCallCount += 1; + if (lsFilesCallCount === 2) return blockedRefresh.promise; + return { + stdout: lsFilesCallCount === 1 ? "node_modules/\n" : "node_modules/\nbuild/\n", + stderr: "", + truncated: false, + exitCode: 0, + signal: null, + }; + }); + const service = createService(watcher, { runGitCommand }); + const subscription = service.registerWorkspace({ cwd: REPO_CWD }, vi.fn()); + + await vi.waitFor(() => { + expect(service.getMetrics().workspaceObservationSetupInFlightCount).toBe(0); + }); + const checkoutWatcher = getWatcherRecordsForDirectory(watcher, REPO_CWD)[0]; + checkoutWatcher?.callback(null, [{ path: path.join(REPO_CWD, ".gitignore"), type: "update" }]); + await vi.waitFor(() => expect(lsFilesCallCount).toBe(2)); + checkoutWatcher?.callback(null, [{ path: path.join(REPO_CWD, ".gitignore"), type: "update" }]); + blockedRefresh.resolve({ + stdout: "node_modules/\n", + stderr: "", + truncated: false, + exitCode: 0, + signal: null, + }); + + await vi.waitFor(() => expect(lsFilesCallCount).toBe(3)); + expect(checkoutWatcher?.ignore).toContain(path.join(REPO_CWD, "build")); + + subscription.unsubscribe(); + service.dispose(); + }); + test("ignore watcher update failure enters polling with a distinct reason", async () => { const watcher = createWatcherHarness(); const logger = createLogger(); @@ -1959,7 +2104,7 @@ describe("WorkspaceGitService checkout observation", () => { checkoutWatcher?.updateIgnore.mockRejectedValueOnce(new Error("update failed")); ignoredDirectories = "node_modules/\n"; - await vi.advanceTimersByTimeAsync(1_000); + checkoutWatcher?.callback(null, [{ path: path.join(REPO_CWD, ".gitignore"), type: "update" }]); await vi.waitFor(() => { expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ reason: "watcher_update_failed" }), @@ -1981,7 +2126,7 @@ describe("WorkspaceGitService checkout observation", () => { service.dispose(); }); - test("a missed watcher event converges through the deterministic audit", async () => { + test("a working-tree watcher event refreshes summary and diff subscribers", async () => { const watcher = createWatcherHarness(); let isDirty = false; let diffStat = { additions: 0, deletions: 0 }; @@ -1998,7 +2143,6 @@ describe("WorkspaceGitService checkout observation", () => { getCheckoutStatus, getCheckoutShortstat, getCheckoutDiff, - getWorkspaceGitSelfHealPhaseMs: () => 10_000, }); const diffManager = new CheckoutDiffManager({ logger: createLogger(), @@ -2021,7 +2165,10 @@ describe("WorkspaceGitService checkout observation", () => { diffStat = { additions: 7, deletions: 3 }; diffFiles = [{ path: "tracked.txt", additions: 7, deletions: 3, status: "modified" }]; - await vi.advanceTimersByTimeAsync(70_000); + getWatcherRecordsForDirectory(watcher, REPO_CWD)[0]?.callback(null, [ + { path: path.join(REPO_CWD, "tracked.txt"), type: "update" }, + ]); + await vi.advanceTimersByTimeAsync(1_000); await vi.waitFor(() => { expect(listener).toHaveBeenLastCalledWith( expect.objectContaining({ diff --git a/packages/server/src/server/workspace-git-service.primitive.test.ts b/packages/server/src/server/workspace-git-service.primitive.test.ts index 34478784c1..1eaf0cf3ae 100644 --- a/packages/server/src/server/workspace-git-service.primitive.test.ts +++ b/packages/server/src/server/workspace-git-service.primitive.test.ts @@ -370,6 +370,11 @@ function buildDefaultServiceDeps() { signal: null, })), getWorkspaceGitSelfHealPhaseMs: vi.fn(() => 30_000), + createWatcherLivenessCanary: vi.fn(() => ({ + path: "", + filterEvents: (events) => events, + verify: vi.fn(async () => {}), + })), now: () => new Date("2026-04-12T00:00:00.000Z"), }; } @@ -869,7 +874,7 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => { service.dispose(); }); - test("self-heal timer refreshes git without refreshing GitHub", async () => { + test("quiet observed workspaces do not refresh git on the observation re-ensure timer", async () => { let nowMs = 0; const getCheckoutStatus = vi.fn(async (cwd: string) => createCheckoutStatus(cwd)); const getPullRequestStatus = vi.fn(async () => createPullRequestStatusResult()); @@ -888,7 +893,7 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => { await vi.advanceTimersByTimeAsync(120_000); await flushPromises(); - expect(getCheckoutStatus).toHaveBeenCalledTimes(2); + expect(getCheckoutStatus).toHaveBeenCalledTimes(1); expect(getPullRequestStatus).toHaveBeenCalledTimes(1); expect(getPullRequestStatus).toHaveBeenCalledWith( REPO_CWD, @@ -901,50 +906,12 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => { service.dispose(); }); - test("self-heal phase is stable across process restarts", () => { + test("observation re-ensure phase is stable across process restarts", () => { expect(getWorkspaceGitSelfHealPhaseMs("/tmp/repo")).toBe(54_185); expect(getWorkspaceGitSelfHealPhaseMs("/tmp/staggered-repo")).toBe(9_817); }); - test("self-heal staggers workspaces and settles into a 120 second cadence", async () => { - vi.setSystemTime(0); - const otherRepoCwd = resolvePath("/tmp/staggered-repo"); - const refreshes = new Map<string, number[]>(); - const getCheckoutStatus = vi.fn(async (cwd: string) => { - const timestamps = refreshes.get(cwd) ?? []; - timestamps.push(Date.now()); - refreshes.set(cwd, timestamps); - return createCheckoutStatus(cwd, { hasRemote: false, remoteUrl: null }); - }); - const service = createService({ - getCheckoutSnapshotFacts: vi.fn(async (cwd: string) => - createCheckoutFacts(cwd, { remoteUrl: null }), - ), - getCheckoutStatus, - getWorkspaceGitSelfHealPhaseMs: (cwd) => (cwd === REPO_CWD ? 10_000 : 40_000), - now: () => new Date(Date.now()), - }); - const first = service.registerWorkspace({ cwd: REPO_CWD }, vi.fn()); - const second = service.registerWorkspace({ cwd: otherRepoCwd }, vi.fn()); - await Promise.all([service.getSnapshot(REPO_CWD), service.getSnapshot(otherRepoCwd)]); - - await vi.advanceTimersByTimeAsync(300_000); - await flushPromises(); - - const firstSelfHeals = refreshes.get(REPO_CWD)?.slice(1) ?? []; - const secondSelfHeals = refreshes.get(otherRepoCwd)?.slice(1) ?? []; - expect(firstSelfHeals).toHaveLength(2); - expect(secondSelfHeals).toHaveLength(2); - expect(firstSelfHeals[0]).not.toBe(secondSelfHeals[0]); - expect(firstSelfHeals[1] - firstSelfHeals[0]).toBe(120_000); - expect(secondSelfHeals[1] - secondSelfHeals[0]).toBe(120_000); - - first.unsubscribe(); - second.unsubscribe(); - service.dispose(); - }); - - test("self-heal retries observation setup even when the git snapshot is still recent", async () => { + test("observation re-ensure retries setup even when the git snapshot is still recent", async () => { let nowMs = 0; const getCheckoutSnapshotFacts = vi .fn<(cwd: string) => Promise<CheckoutSnapshotFacts>>() @@ -1485,7 +1452,7 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => { service.dispose(); }); - test("multiple subscribers on the same target share one self-heal timer", async () => { + test("multiple subscribers on the same target do not duplicate periodic git work", async () => { let nowMs = 0; const getCheckoutSnapshotFacts = vi.fn(async (cwd: string) => createCheckoutFacts(cwd, { remoteUrl: null }), @@ -1506,14 +1473,14 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => { await vi.advanceTimersByTimeAsync(120_000); await flushPromises(); - expect(getCheckoutStatus).toHaveBeenCalledTimes(2); + expect(getCheckoutStatus).toHaveBeenCalledTimes(1); first.unsubscribe(); second.unsubscribe(); service.dispose(); }); - test("unsubscribe with no remaining subscribers clears the self-heal timer", async () => { + test("unsubscribe with no remaining subscribers clears the observation re-ensure timer", async () => { let nowMs = 0; const getCheckoutStatus = vi.fn(async (cwd: string) => createCheckoutStatus(cwd)); const service = createService({ @@ -1533,7 +1500,7 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => { service.dispose(); }); - test("service disposal clears all self-heal timers", async () => { + test("service disposal clears all observation re-ensure timers", async () => { let nowMs = 0; const getCheckoutStatus = vi.fn(async (cwd: string) => createCheckoutStatus(cwd)); const service = createService({ @@ -1549,42 +1516,6 @@ describe("WorkspaceGitServiceImpl primitive refresh entrypoint", () => { expect(getCheckoutStatus).toHaveBeenCalledTimes(0); }); - - test("direct getSnapshot returns current snapshot during a self-heal refresh", async () => { - let nowMs = 0; - const selfHealRefresh = createDeferred<CheckoutStatusGit>(); - const getCheckoutStatus = vi - .fn<() => Promise<CheckoutStatusGit>>() - .mockImplementationOnce(async () => createCheckoutStatus(REPO_CWD)) - .mockImplementationOnce(async () => selfHealRefresh.promise); - const getCheckoutSnapshotFacts = vi.fn(async (cwd: string) => - createCheckoutFacts(cwd, { remoteUrl: null }), - ); - const service = createService({ - getCheckoutSnapshotFacts, - getCheckoutStatus, - now: () => new Date(nowMs), - }); - const subscription = service.registerWorkspace({ cwd: REPO_CWD }, vi.fn()); - await service.getSnapshot(REPO_CWD); - - nowMs = 120_000; - await vi.advanceTimersByTimeAsync(120_000); - await flushPromises(); - const directRead = service.getSnapshot(REPO_CWD); - await flushPromises(); - - expect(getCheckoutStatus).toHaveBeenCalledTimes(2); - await expect(directRead).resolves.toEqual(createSnapshot(REPO_CWD)); - - selfHealRefresh.resolve(createCheckoutStatus(REPO_CWD)); - await flushPromises(); - - expect(getCheckoutStatus).toHaveBeenCalledTimes(2); - - subscription.unsubscribe(); - service.dispose(); - }); }); describe("WorkspaceGitServiceImpl D2 read methods", () => { diff --git a/packages/server/src/server/workspace-git-service.test.ts b/packages/server/src/server/workspace-git-service.test.ts index 914fde1ff7..a1d6b392e7 100644 --- a/packages/server/src/server/workspace-git-service.test.ts +++ b/packages/server/src/server/workspace-git-service.test.ts @@ -293,6 +293,11 @@ function buildDefaultTestServiceDeps() { signal: null, })), getWorkspaceGitSelfHealPhaseMs: vi.fn(() => 30_000), + createWatcherLivenessCanary: vi.fn(() => ({ + path: "", + filterEvents: (events) => events, + verify: vi.fn(async () => {}), + })), now: () => new Date("2026-04-12T00:00:00.000Z"), }; } diff --git a/packages/server/src/server/workspace-git-service.ts b/packages/server/src/server/workspace-git-service.ts index 74596ef6c0..93f7338a9d 100644 --- a/packages/server/src/server/workspace-git-service.ts +++ b/packages/server/src/server/workspace-git-service.ts @@ -1,6 +1,6 @@ import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; -import { join, resolve } from "node:path"; +import { basename, join, resolve } from "node:path"; import { LRUCache } from "lru-cache"; import pLimit from "p-limit"; import type pino from "pino"; @@ -29,6 +29,7 @@ import { resolveAbsoluteGitDir, checkoutResolvedBranch, commitChanges, + discardChanges, getCommitFileDiff, listCheckoutCommits, mergeFromBase, @@ -85,11 +86,12 @@ import { import { checkoutLiteFromGitSnapshot } from "./workspace-registry-model.js"; import type { BoundWorkspaceRuntime, WorkspaceRuntimeService } from "./workspace-runtime/index.js"; import { observeWorkspaceGit } from "./workspace-git-observation.js"; +import { createWatcherLivenessCanary } from "./watcher-liveness-canary.js"; const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 1_000; const BACKGROUND_GIT_FETCH_INTERVAL_MS = 180_000; const FETCH_METADATA_ECHO_TTL_MS = 5_000; -export const WORKSPACE_GIT_SELF_HEAL_INTERVAL_MS = 60_000; +export const WORKSPACE_GIT_OBSERVATION_REENSURE_INTERVAL_MS = 60_000; const FORGE_PR_STATUS_POLL_FAST_INTERVAL_MS = 20_000; const FORGE_PR_STATUS_POLL_SLOW_INTERVAL_MS = 120_000; const FORGE_PR_STATUS_POLL_ERROR_BACKOFF_CAP_MS = 300_000; @@ -125,12 +127,16 @@ function mergeSets<T>( return { merged, added }; } -export function getWorkspaceGitSelfHealPhaseMs(cwd: string): number { +export function getWorkspaceGitObservationReensurePhaseMs(cwd: string): number { return ( - createHash("sha256").update(cwd).digest().readUInt32BE(0) % WORKSPACE_GIT_SELF_HEAL_INTERVAL_MS + createHash("sha256").update(cwd).digest().readUInt32BE(0) % + WORKSPACE_GIT_OBSERVATION_REENSURE_INTERVAL_MS ); } +// Kept for local diagnostic fixtures that only use the stable phase calculation. +export const getWorkspaceGitSelfHealPhaseMs = getWorkspaceGitObservationReensurePhaseMs; + export interface WorkspaceGitRuntimeSnapshot { cwd: string; git: { @@ -232,6 +238,7 @@ export interface WorkspaceGitService { resolveDefaultBranch(cwdOrRepoRoot: string, options?: WorkspaceGitReadOptions): Promise<string>; resolveRepoRemoteUrl(cwd: string, options?: WorkspaceGitReadOptions): Promise<string | null>; commit(cwd: string, options: { message: string; addAll: boolean }): Promise<void>; + discardChanges(cwd: string, paths: string[]): Promise<void>; createBranch(cwd: string, options: { branch: string; baseRef: string }): Promise<void>; switchBranch(cwd: string, branch: string): Promise<CheckoutExistingBranchResult>; fetch(cwd: string): Promise<void>; @@ -294,6 +301,7 @@ export interface WorkspaceGitWorkspace { resolveDefaultBranch(options?: WorkspaceGitReadOptions): Promise<string>; resolveRepoRemoteUrl(options?: WorkspaceGitReadOptions): Promise<string | null>; commit(options: { message: string; addAll: boolean }): Promise<void>; + discardChanges(paths: string[]): Promise<void>; createBranch(options: { branch: string; baseRef: string }): Promise<void>; switchBranch(branch: string): Promise<CheckoutExistingBranchResult>; fetch(): Promise<void>; @@ -446,7 +454,8 @@ interface WorkspaceGitServiceDependencies { observer: WorkspaceGitFetchObserver, ) => Promise<WorkspaceGitFetchResult>; runGitCommand: typeof runGitCommand; - getWorkspaceGitSelfHealPhaseMs: typeof getWorkspaceGitSelfHealPhaseMs; + getWorkspaceGitSelfHealPhaseMs: typeof getWorkspaceGitObservationReensurePhaseMs; + createWatcherLivenessCanary: typeof createWatcherLivenessCanary; now: () => Date; } @@ -488,15 +497,12 @@ interface WorkspaceGitTarget { workingTreeWatchTarget: WorkingTreeWatchTarget | null; debounceTimer: NodeJS.Timeout | null; pendingDebounceRequest: WorkspaceGitRefreshRequest | null; - selfHealTimer: NodeJS.Timeout | null; + observationReensureTimer: NodeJS.Timeout | null; forgePrStatusPollSubscription: { unsubscribe: () => void } | null; forgePrStatusPollKey: string | null; refreshState: WorkspaceGitRefreshState; latestGit: WorkspaceGitRuntimeSnapshot["git"] | null; latestGitLoadedAtMs: number | null; - latestStructuralRefreshAtMs: number | null; - latestWorktreeRefreshAtMs: number | null; - auditWindowStartedAtMs: number | null; latestForge: WorkspaceGitRuntimeSnapshot["forge"] | null; latestForgeLoadedAtMs: number | null; latestSnapshot: WorkspaceGitRuntimeSnapshot | null; @@ -544,6 +550,7 @@ interface WorkingTreeWatchTarget { subscription: FileObserverSubscription | null; ignoredDirectories: Set<string>; ignoredDirectoriesRefreshPromise: Promise<void> | null; + ignoredDirectoriesRefreshRequested: boolean; aliases: Set<string>; workspaceKeys: Set<string>; fallbackPolling: boolean; @@ -601,7 +608,8 @@ function buildDefaultWorkspaceGitServiceDeps( hasOriginRemote, runGitFetch: fetchWorkspaceGitRemote, runGitCommand, - getWorkspaceGitSelfHealPhaseMs, + getWorkspaceGitSelfHealPhaseMs: getWorkspaceGitObservationReensurePhaseMs, + createWatcherLivenessCanary, now: () => new Date(), }; } @@ -766,6 +774,7 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { resolveDefaultBranch: (options) => service.resolveDefaultBranch(cwd, options), resolveRepoRemoteUrl: (options) => service.resolveRepoRemoteUrl(cwd, options), commit: (options) => service.commit(cwd, options), + discardChanges: (paths) => service.discardChanges(cwd, paths), createBranch: (options) => service.createBranch(cwd, options), switchBranch: (branch) => service.switchBranch(cwd, branch), fetch: () => service.fetch(cwd), @@ -1015,6 +1024,16 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { }); } + async discardChanges(cwd: string, paths: string[]): Promise<void> { + const normalizedCwd = resolve(cwd); + await this.withWorkspaceRuntime(normalizedCwd, () => discardChanges(normalizedCwd, paths)); + await this.getSnapshot(normalizedCwd, { + force: true, + includeForge: false, + reason: "discard-changes", + }); + } + async createBranch(cwd: string, options: { branch: string; baseRef: string }): Promise<void> { const normalizedCwd = resolve(cwd); await this.withWorkspaceRuntime(normalizedCwd, () => @@ -1578,15 +1597,12 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { workingTreeWatchTarget: null, debounceTimer: null, pendingDebounceRequest: null, - selfHealTimer: null, + observationReensureTimer: null, forgePrStatusPollSubscription: null, forgePrStatusPollKey: null, refreshState: { status: "idle" }, latestGit: null, latestGitLoadedAtMs: null, - latestStructuralRefreshAtMs: null, - latestWorktreeRefreshAtMs: null, - auditWindowStartedAtMs: null, latestForge: null, latestForgeLoadedAtMs: null, latestSnapshot: null, @@ -1756,6 +1772,7 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { subscription: null, ignoredDirectories, ignoredDirectoriesRefreshPromise: null, + ignoredDirectoriesRefreshRequested: false, aliases: new Set([cwd]), workspaceKeys: new Set(), fallbackPolling: false, @@ -1899,6 +1916,9 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { if (watcherErrored) { return; } + if (events.some((event) => basename(event.path) === ".gitignore")) { + void this.refreshWorkingTreeIgnoredDirectories(target); + } if (!this.hasRelevantWorkingTreeEvent(target, events)) { return; } @@ -2090,22 +2110,28 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { if (target.closed || target.repoRoot === null) { return Promise.resolve(); } + target.ignoredDirectoriesRefreshRequested = true; if (target.ignoredDirectoriesRefreshPromise) { return target.ignoredDirectoriesRefreshPromise; } - const refreshPromise = this.replaceWorkingTreeIgnoredDirectories(target) - .catch((error) => { - this.logger.warn( - { err: error, cwd: target.cwd }, - "Failed to refresh working tree watcher ignore paths", - ); - }) - .finally(() => { - if (target.ignoredDirectoriesRefreshPromise === refreshPromise) { - target.ignoredDirectoriesRefreshPromise = null; + const refreshPromise = (async () => { + while (!target.closed && target.ignoredDirectoriesRefreshRequested) { + target.ignoredDirectoriesRefreshRequested = false; + try { + await this.replaceWorkingTreeIgnoredDirectories(target); + } catch (error) { + this.logger.warn( + { err: error, cwd: target.cwd }, + "Failed to refresh working tree watcher ignore paths", + ); } - }); + } + })().finally(() => { + if (target.ignoredDirectoriesRefreshPromise === refreshPromise) { + target.ignoredDirectoriesRefreshPromise = null; + } + }); target.ignoredDirectoriesRefreshPromise = refreshPromise; return refreshPromise; } @@ -2283,6 +2309,8 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { join(target.repoGitRoot, path), ); const matchesRepoGitRoot = createRealpathAwarePathMatcher(target.repoGitRoot); + const canary = this.deps.createWatcherLivenessCanary(target.repoGitRoot); + let openedSubscription: FileObserverSubscription | null = null; let watcherErrored = false; let subscribeSettled = false; const markSubscribeSettled = () => { @@ -2295,6 +2323,7 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { const subscription = await this.subscribeWithDeadline( target.repoGitRoot, (error, events) => { + const liveEvents = canary.filterEvents(events); if (error) { if (watcherErrored) { return; @@ -2314,7 +2343,10 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { if (watcherErrored) { return; } - const relevantEvents = events.filter( + if (liveEvents.length === 0) { + return; + } + const relevantEvents = liveEvents.filter( (event) => !matchesRepoGitRoot(event.path) && ignore.every((ignoredPath) => !isRealpathInsideRoot(ignoredPath, event.path)), @@ -2330,6 +2362,7 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { }) : relevantEvents; if (immediateEvents.length > 0) { + this.refreshWorkingTreeIgnoresFromRepoMetadataEvents(target, immediateEvents); const routedRefreshes = this.routeRepoMetadataEvents(target, immediateEvents); this.scheduleRepoMetadataRefresh( target, @@ -2343,6 +2376,8 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { { ignore }, markSubscribeSettled, ); + openedSubscription = subscription; + await canary.verify(this.disposeController.signal); if (watcherErrored) { await this.unsubscribeWatcherSubscription(subscription, target.repoGitRoot); return false; @@ -2365,6 +2400,9 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { } return true; } catch (error) { + if (openedSubscription) { + await this.unsubscribeWatcherSubscription(openedSubscription, target.repoGitRoot); + } if (watcherErrored) { return false; } @@ -2435,6 +2473,41 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { return refreshes; } + private refreshWorkingTreeIgnoresFromRepoMetadataEvents( + target: RepoGitTarget, + events: FileChange[], + ): void { + const workspaceKeys = new Set<string>(); + for (const event of events) { + const commonRelativePath = getRealpathAwareRelativePath( + target.repoGitRoot, + event.path, + )?.replaceAll("\\", "/"); + if (commonRelativePath === "config" || commonRelativePath === "info/exclude") { + for (const workspaceKey of target.workspaceKeys) workspaceKeys.add(workspaceKey); + continue; + } + for (const workspaceKey of target.workspaceKeys) { + const facts = this.workspaceTargets.get(workspaceKey)?.latestFacts; + if ( + facts?.isGit && + facts.absoluteGitDir && + getRealpathAwareRelativePath(facts.absoluteGitDir, event.path)?.replaceAll("\\", "/") === + "config.worktree" + ) { + workspaceKeys.add(workspaceKey); + } + } + } + for (const workspaceKey of workspaceKeys) { + const workspaceTarget = this.workspaceTargets.get(workspaceKey); + const workingTreeTarget = workspaceTarget + ? this.getWorkingTreeWatchTargetForWorkspace(workspaceTarget) + : null; + if (workingTreeTarget) void this.refreshWorkingTreeIgnoredDirectories(workingTreeTarget); + } + } + private isFetchRemoteMetadataEvent(target: RepoGitTarget, event: FileChange): boolean { const relativePath = getRealpathAwareRelativePath(target.repoGitRoot, event.path); const effect = classifyGitMetadataPath("common", relativePath ?? ""); @@ -2770,57 +2843,20 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { } private startWorkspaceSubscriptionTimers(target: WorkspaceGitTarget): void { - if (!target.selfHealTimer) { - target.auditWindowStartedAtMs = this.deps.now().getTime(); - const runSelfHealTick = () => { + if (!target.observationReensureTimer) { + const reensureObservation = () => { if (!this.isActiveObservedWorkspaceTarget(target)) { - target.selfHealTimer = null; + target.observationReensureTimer = null; return; } - target.selfHealTimer = setTimeout(runSelfHealTick, WORKSPACE_GIT_SELF_HEAL_INTERVAL_MS); + target.observationReensureTimer = setTimeout( + reensureObservation, + WORKSPACE_GIT_OBSERVATION_REENSURE_INTERVAL_MS, + ); this.scheduleWorkspaceObservationSetup(target); - const auditWindowStartedAtMs = target.auditWindowStartedAtMs; - target.auditWindowStartedAtMs = this.deps.now().getTime(); - if (auditWindowStartedAtMs === null) { - return; - } - const workingTreeTarget = this.getWorkingTreeWatchTargetForWorkspace(target); - if (workingTreeTarget) { - void this.refreshWorkingTreeIgnoredDirectories(workingTreeTarget); - } - const refreshStructure = - target.latestStructuralRefreshAtMs === null || - target.latestStructuralRefreshAtMs < auditWindowStartedAtMs; - const refreshWorktree = - refreshStructure || - target.latestWorktreeRefreshAtMs === null || - target.latestWorktreeRefreshAtMs < auditWindowStartedAtMs; - if (!refreshStructure && !refreshWorktree) { - return; - } - if (refreshWorktree) { - if (workingTreeTarget) { - this.notifyWorkingTreeConsumers(workingTreeTarget); - } - } - this.refreshWorkspaceTarget(target, { - force: false, - refreshStructure, - refreshWorktree, - includeForge: false, - reason: "self-heal-git", - notify: true, - queueIfBusy: true, - movedRemoteRefs: new Set(), - }).catch((error) => { - this.logger.warn( - { err: error, cwd: target.cwd, reason: "self-heal-git" }, - "Failed to run workspace git self-heal refresh", - ); - }); }; - target.selfHealTimer = setTimeout( - runSelfHealTick, + target.observationReensureTimer = setTimeout( + reensureObservation, this.deps.getWorkspaceGitSelfHealPhaseMs(target.cwd), ); } @@ -3388,7 +3424,6 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { }; const loadedAtMs = this.deps.now().getTime(); target.latestGitLoadedAtMs = loadedAtMs; - target.latestWorktreeRefreshAtMs = loadedAtMs; } private async refreshGitSnapshot( @@ -3417,8 +3452,6 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { target.latestGit = buildNotGitSnapshot(cwd).git; const loadedAtMs = this.deps.now().getTime(); target.latestGitLoadedAtMs = loadedAtMs; - target.latestStructuralRefreshAtMs = loadedAtMs; - target.latestWorktreeRefreshAtMs = loadedAtMs; target.latestForge = buildForgeUnavailableSnapshot(); target.latestForgeLoadedAtMs = target.latestGitLoadedAtMs; return facts; @@ -3453,10 +3486,6 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { }; const loadedAtMs = this.deps.now().getTime(); target.latestGitLoadedAtMs = loadedAtMs; - target.latestStructuralRefreshAtMs = loadedAtMs; - if (refreshWorktree) { - target.latestWorktreeRefreshAtMs = loadedAtMs; - } if (previousForgePrStatusPollKey !== this.getForgePrStatusPollKey(target)) { target.latestForge = buildForgeUnavailableSnapshot(); @@ -3779,9 +3808,9 @@ export class WorkspaceGitServiceImpl implements WorkspaceGitService { clearTimeout(target.debounceTimer); target.debounceTimer = null; } - if (target.selfHealTimer) { - clearTimeout(target.selfHealTimer); - target.selfHealTimer = null; + if (target.observationReensureTimer) { + clearTimeout(target.observationReensureTimer); + target.observationReensureTimer = null; } this.stopForgePrStatusPollForTarget(target); target.listeners.clear(); diff --git a/packages/server/src/server/worktree-core.posix.test.ts b/packages/server/src/server/worktree-core.posix.test.ts index 40bd983c7b..94f654de12 100644 --- a/packages/server/src/server/worktree-core.posix.test.ts +++ b/packages/server/src/server/worktree-core.posix.test.ts @@ -1438,8 +1438,7 @@ describe.skipIf(isPlatform("win32"))("worktree-core POSIX-only", () => { expect(result.worktree.branchName).toBe("agent-worktree"); }); - // POSIX-only: Windows git worktree paths need separate canonicalization coverage. - test("reuses an existing branch-off worktree for the same slug", async () => { + test("creates a suffixed branch-off worktree for the same slug", async () => { const { tempDir, repoDir, paseoHome } = createGitRepo(); cleanupPaths.push(tempDir); const deps = createCoreDeps(); @@ -1454,12 +1453,12 @@ describe.skipIf(isPlatform("win32"))("worktree-core POSIX-only", () => { ); expect(first.created).toBe(true); - expect(second.created).toBe(false); - expect(second.worktree).toEqual(first.worktree); + expect(second.created).toBe(true); + expect(path.basename(second.worktree.worktreePath)).toBe("reused-worktree-1"); + expect(second.worktree.branchName).toBe("reused-worktree-1"); }); - // POSIX-only: Windows git worktree paths need separate canonicalization coverage. - test("reuses an existing GitHub PR worktree for the resolved slug", async () => { + test("creates a suffixed GitHub PR worktree for the resolved slug", async () => { const { tempDir, repoDir, paseoHome } = createGitHubPrRemoteRepo(); cleanupPaths.push(tempDir); const deps = createCoreDeps(); @@ -1475,8 +1474,9 @@ describe.skipIf(isPlatform("win32"))("worktree-core POSIX-only", () => { const second = await createCoreWorktree(input, deps); expect(first.created).toBe(true); - expect(second.created).toBe(false); - expect(second.worktree).toEqual(first.worktree); + expect(second.created).toBe(true); + expect(path.basename(second.worktree.worktreePath)).toBe("feature-review-pr-1"); + expect(second.worktree.branchName).toBe("feature/review-pr-1"); }); test("uses an injectable ForgeService dependency for missing PR head refs", async () => { diff --git a/packages/server/src/server/worktree-core.ts b/packages/server/src/server/worktree-core.ts index 479d0e03bb..611398bd90 100644 --- a/packages/server/src/server/worktree-core.ts +++ b/packages/server/src/server/worktree-core.ts @@ -3,7 +3,6 @@ import { createNameId } from "mnemonic-id"; import type { ForgeService } from "../services/forge-service.js"; import { createWorktree, - resolveExistingWorktreeForSlug, slugify, validateBranchSlug, type WorktreeConfig, @@ -67,16 +66,6 @@ async function createWorktreeCoreWithPriority( ): Promise<CreateWorktreeCoreResult> { const plan = await planWorktreeCore(input, deps); const { repoRoot, intent, worktreeSlug: normalizedSlug } = plan; - const existingWorktree = await resolveExistingWorktreeForSlug({ - slug: normalizedSlug, - repoRoot, - paseoHome: input.paseoHome, - worktreesRoot: input.worktreesRoot, - }); - if (existingWorktree) { - return { worktree: existingWorktree, intent, repoRoot, created: false }; - } - return { worktree: await createWorktree({ cwd: repoRoot, diff --git a/packages/server/src/services/quota-fetcher/providers/cursor.ts b/packages/server/src/services/quota-fetcher/providers/cursor.ts index 5f30cae5fc..98b499b8da 100644 --- a/packages/server/src/services/quota-fetcher/providers/cursor.ts +++ b/packages/server/src/services/quota-fetcher/providers/cursor.ts @@ -1,8 +1,6 @@ -import { execFile } from "node:child_process"; import { existsSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; -import { promisify } from "node:util"; import type { Logger } from "pino"; import { z } from "zod"; import type { ProviderUsage, ProviderUsageBalance } from "../../../server/messages.js"; @@ -16,8 +14,25 @@ import { unavailableUsage, } from "../usage.js"; -const execFileAsync = promisify(execFile); -const CURSOR_SQLITE_TIMEOUT_MS = 2_000; +// Cursor stores auth in VS Code's ItemTable (state.vscdb). Modern builds keep the +// access token as a plain JWT string under `cursorAuth/accessToken`; older builds +// kept a JSON blob under `cursorAuthStatus`. Read it with node:sqlite so we don't +// depend on a `sqlite3` CLI, which isn't installed by default on Windows (or on many +// Linux hosts) — a missing binary silently rendered Cursor usage unavailable. +const CURSOR_ACCESS_TOKEN_KEY = "cursorAuth/accessToken"; +const CURSOR_LEGACY_AUTH_KEY = "cursorAuthStatus"; + +// @types/node@20 predates the node:sqlite typings; declare the slice we use. +interface CursorStateStatement { + get(...params: unknown[]): Record<string, unknown> | undefined; +} +interface CursorStateDatabase { + prepare(sql: string): CursorStateStatement; + close(): void; +} +interface NodeSqliteModule { + DatabaseSync: new (path: string, options?: { readOnly?: boolean }) => CursorStateDatabase; +} const CursorBillingCycleTimestampSchema = z.preprocess( (value) => (typeof value === "string" || typeof value === "number" ? value : null), @@ -47,6 +62,7 @@ type CursorUsageResponse = z.infer<typeof CursorUsageResponseSchema>; interface CursorQuotaProviderOptions { logger: Logger; fetch?: ProviderApiFetch; + homeDir?: string; } function parseCursorBillingCycleTimestamp( @@ -70,14 +86,38 @@ function centsToDollars(value: number | null): number | null { return value === null ? null : value / 100; } -async function readCursorTokenFromSqlite(): Promise<string | null> { +function readItemTableValue(db: CursorStateDatabase, key: string): string | null { + const row = db.prepare("SELECT value FROM ItemTable WHERE key = ?").get(key); + const value = row?.["value"]; + if (typeof value === "string") return value; + if (value instanceof Uint8Array) return Buffer.from(value).toString("utf8"); + return null; +} + +function cursorTokenFromDb(db: CursorStateDatabase): string | null { + const modern = readItemTableValue(db, CURSOR_ACCESS_TOKEN_KEY)?.trim(); + if (modern) return modern; + + const legacy = readItemTableValue(db, CURSOR_LEGACY_AUTH_KEY); + if (legacy) { + try { + const parsed = CursorAuthStatusSchema.parse(JSON.parse(legacy)); + if (parsed.accessToken) return parsed.accessToken; + } catch { + // ignore a malformed legacy blob + } + } + return null; +} + +async function readCursorTokenFromSqlite(homeDir: string, logger: Logger): Promise<string | null> { const dbPaths: string[] = []; if (process.env["APPDATA"]) { dbPaths.push(join(process.env["APPDATA"], "Cursor", "User", "globalStorage", "state.vscdb")); } dbPaths.push( join( - homedir(), + homeDir, "Library", "Application Support", "Cursor", @@ -86,22 +126,32 @@ async function readCursorTokenFromSqlite(): Promise<string | null> { "state.vscdb", ), ); - dbPaths.push(join(homedir(), ".config", "Cursor", "User", "globalStorage", "state.vscdb")); + dbPaths.push(join(homeDir, ".config", "Cursor", "User", "globalStorage", "state.vscdb")); + + // Held in a variable so TypeScript skips module resolution: @types/node@20 has no + // node:sqlite typings yet, while the runtime (Node 22+ / Electron) provides it. + const sqliteSpecifier: string = "node:sqlite"; + let sqlite: NodeSqliteModule; + try { + sqlite = (await import(sqliteSpecifier)) as unknown as NodeSqliteModule; + } catch (err) { + logger.debug({ err }, "node:sqlite unavailable; cannot read Cursor state.vscdb"); + return null; // runtime without node:sqlite + } for (const path of dbPaths) { if (!existsSync(path)) continue; + let db: CursorStateDatabase | undefined; try { - const { stdout } = await execFileAsync( - "sqlite3", - [path, "SELECT value FROM ItemTable WHERE key = 'cursorAuthStatus'"], - { timeout: CURSOR_SQLITE_TIMEOUT_MS }, - ); - if (stdout) { - const parsed = CursorAuthStatusSchema.parse(JSON.parse(stdout.trim())); - if (parsed.accessToken) return parsed.accessToken; - } - } catch { - continue; + db = new sqlite.DatabaseSync(path, { readOnly: true }); + const token = cursorTokenFromDb(db); + if (token) return token; + } catch (err) { + // Locked/permission/corrupt/schema failures all land here; log so an + // unavailable Cursor card is diagnosable, then try the next candidate. + logger.debug({ err, path }, "Failed to read Cursor token from state.vscdb"); + } finally { + db?.close(); } } return null; @@ -113,17 +163,19 @@ export class CursorQuotaProvider implements ProviderUsageFetcher { private readonly logger: Logger; private readonly fetchApi: ProviderApiFetch; + private readonly homeDir: string; constructor(options: CursorQuotaProviderOptions) { this.logger = options.logger; this.fetchApi = options.fetch ?? fetch; + this.homeDir = options.homeDir ?? homedir(); } async fetchUsage(): Promise<ProviderUsage> { const token = process.env["CURSOR_ACCESS_TOKEN"] || process.env["CURSOR_TOKEN"] || - (await readCursorTokenFromSqlite()); + (await readCursorTokenFromSqlite(this.homeDir, this.logger)); if (!token) return unavailableUsage(this); diff --git a/packages/server/src/services/quota-fetcher/service.test.ts b/packages/server/src/services/quota-fetcher/service.test.ts index 087f7fcf7d..3d4c6ed29b 100644 --- a/packages/server/src/services/quota-fetcher/service.test.ts +++ b/packages/server/src/services/quota-fetcher/service.test.ts @@ -1,6 +1,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { mkdtempSync, rmSync } from "node:fs"; import { promises as fsPromises } from "node:fs"; +import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -58,6 +59,31 @@ function writeKimiCredentials(dir: string, accessToken: string, overrides: objec ); } +// node:sqlite has no @types/node@20 typings; require it with a narrow local type. +const testRequire = createRequire(import.meta.url); +interface TestSqliteDb { + exec(sql: string): void; + prepare(sql: string): { run(...params: unknown[]): void }; + close(): void; +} + +// Cursor builds have stored ItemTable values as both TEXT and BLOB. Keys map to the +// real layouts: a plain modern token or the legacy JSON object. +function writeCursorStateDb(homeDir: string, rows: Record<string, string | Uint8Array>): void { + const dir = join(homeDir, ".config", "Cursor", "User", "globalStorage"); + mkdirSync(dir, { recursive: true }); + const { DatabaseSync } = testRequire("node:sqlite") as { + DatabaseSync: new (path: string) => TestSqliteDb; + }; + const db = new DatabaseSync(join(dir, "state.vscdb")); + db.exec("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)"); + const insert = db.prepare("INSERT INTO ItemTable (key, value) VALUES (?, ?)"); + for (const [key, value] of Object.entries(rows)) { + insert.run(key, value); + } + db.close(); +} + function writeGrokAuth(home: string, auth: Record<string, unknown>): void { mkdirSync(join(home, ".grok"), { recursive: true }); writeFileSync(join(home, ".grok", "auth.json"), JSON.stringify(auth)); @@ -372,6 +398,7 @@ describe("real provider usage fetchers", () => { platform?: typeof process.platform; keychain?: () => Promise<unknown | null>; kimiHomeDir?: string; + cursorHomeDir?: string; miniMaxConfigPath?: string; miniMaxCredentialsPath?: string; } = {}, @@ -392,7 +419,11 @@ describe("real provider usage fetchers", () => { }), new CodexQuotaProvider({ logger, codexHome, fetch: fetchThroughTestDouble }), new CopilotQuotaProvider({ logger, fetch: fetchThroughTestDouble }), - new CursorQuotaProvider({ logger, fetch: fetchThroughTestDouble }), + new CursorQuotaProvider({ + logger, + fetch: fetchThroughTestDouble, + homeDir: options.cursorHomeDir, + }), new ZaiQuotaProvider({ logger, fetch: fetchThroughTestDouble }), new GrokQuotaProvider({ logger, @@ -702,6 +733,75 @@ describe("real provider usage fetchers", () => { }); }); + it("reads the Cursor token from the modern cursorAuth/accessToken key in state.vscdb", async () => { + writeCursorStateDb(homeDir, { "cursorAuth/accessToken": "cursor_state_jwt" }); + let authorization: string | null = null; + fetchApi = (async (url: RequestInfo | URL, init?: RequestInit) => { + authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null; + return jsonResponse({ + planUsage: { + totalSpend: "1500", + includedSpend: "1000", + bonusSpend: "500", + remaining: "2500", + limit: "4000", + }, + billingCycleStart: "2026-01-14T12:42:14.000Z", + billingCycleEnd: "2026-02-14T12:42:14.000Z", + }); + }) as unknown as typeof fetch; + + const cursor = findProvider(await service({ cursorHomeDir: homeDir }).listUsage(), "cursor"); + + expect(authorization).toBe("Bearer cursor_state_jwt"); + expect(cursor).toMatchObject({ + status: "available", + balances: [expect.objectContaining({ id: "plan_usage", used: 15, remaining: 25, limit: 40 })], + }); + }); + + it("falls back to the legacy cursorAuthStatus JSON blob when the modern key is absent", async () => { + writeCursorStateDb(homeDir, { + cursorAuthStatus: Buffer.from(JSON.stringify({ accessToken: "cursor_legacy_jwt" }), "utf8"), + }); + let authorization: string | null = null; + fetchApi = (async (url: RequestInfo | URL, init?: RequestInit) => { + authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null; + return jsonResponse({ + planUsage: { totalSpend: "0", remaining: "100", limit: "100" }, + billingCycleStart: null, + billingCycleEnd: null, + }); + }) as unknown as typeof fetch; + + const cursor = findProvider(await service({ cursorHomeDir: homeDir }).listUsage(), "cursor"); + + expect(authorization).toBe("Bearer cursor_legacy_jwt"); + expect(cursor.status).toBe("available"); + }); + + it("logs a debug diagnostic and stays unavailable when state.vscdb is unreadable", async () => { + const dir = join(homeDir, ".config", "Cursor", "User", "globalStorage"); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, "state.vscdb"), "not a sqlite database"); + const logger = createLogger(); + const provider = new CursorQuotaProvider({ + logger, + fetch: (() => { + throw new Error("usage API should not be called without a token"); + }) as unknown as typeof fetch, + homeDir, + }); + + const usage = await provider.fetchUsage(); + + expect(usage.status).toBe("unavailable"); + expect((logger as unknown as { debug: ReturnType<typeof vi.fn> }).debug).toHaveBeenCalledWith( + expect.objectContaining({ path: expect.stringContaining("state.vscdb") }), + expect.stringContaining("Failed to read Cursor token"), + ); + }); + it("fetches Z.ai usage from ZAI_API_KEY", async () => { process.env["ZAI_API_KEY"] = "zai_test_token"; fetchApi = mockFetch( diff --git a/packages/server/src/utils/checkout-git.test.ts b/packages/server/src/utils/checkout-git.test.ts index 0c6978407a..ba5a7fde16 100644 --- a/packages/server/src/utils/checkout-git.test.ts +++ b/packages/server/src/utils/checkout-git.test.ts @@ -20,6 +20,7 @@ import { __resetPullRequestStatusCacheForTests, __setPullRequestStatusCacheTtlForTests, commitAll, + discardChanges, CHECKOUT_DIFF_MAX_STRUCTURED_BYTES, createPullRequest, getCachedCheckoutShortstat, @@ -108,6 +109,10 @@ function initRepo(): { tempDir: string; repoDir: string } { return { tempDir, repoDir }; } +function readTextFile(path: string): string { + return readFileSync(path, "utf8").replaceAll("\r\n", "\n"); +} + function sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -516,6 +521,19 @@ describe("checkout git utilities", () => { expect(message).toBe("update file"); }); + it("includes both paths for a staged rename in structured diffs", async () => { + execFileSync("git", ["mv", "file.txt", "renamed.txt"], { cwd: repoDir }); + + const diff = await getCheckoutDiff(repoDir, { + mode: "uncommitted", + includeStructured: true, + }); + + expect(diff.structured).toContainEqual( + expect.objectContaining({ path: "renamed.txt", oldPath: "file.txt" }), + ); + }); + it("reads the origin URL once when collecting facts for an origin-tracking branch", async () => { setupRemoteTrackingMain(repoDir, tempDir); @@ -3698,3 +3716,132 @@ const x = 1; }); }); }); + +describe("discardChanges", () => { + it("discards staged and unstaged modifications, deletions, and untracked files", async () => { + const { tempDir, repoDir } = initRepo(); + try { + writeFileSync(join(repoDir, "file.txt"), "changed\n"); + writeFileSync(join(repoDir, "staged.txt"), "staged\n"); + execFileSync("git", ["add", "staged.txt"], { cwd: repoDir }); + mkdirSync(join(repoDir, "junk"), { recursive: true }); + writeFileSync(join(repoDir, "junk", "scratch.txt"), "scratch\n"); + + await discardChanges(repoDir, ["file.txt", "staged.txt", "junk"]); + + expect(readTextFile(join(repoDir, "file.txt"))).toBe("hello\n"); + expect(existsSync(join(repoDir, "staged.txt"))).toBe(false); + expect(existsSync(join(repoDir, "junk"))).toBe(false); + const status = execFileSync("git", ["status", "--porcelain"], { cwd: repoDir }) + .toString() + .trim(); + expect(status).toBe(""); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("restores both sides of a staged rename", async () => { + const { tempDir, repoDir } = initRepo(); + try { + execFileSync("git", ["mv", "file.txt", "renamed.txt"], { cwd: repoDir }); + + await discardChanges(repoDir, ["file.txt", "renamed.txt"]); + + expect(readTextFile(join(repoDir, "file.txt"))).toBe("hello\n"); + expect(existsSync(join(repoDir, "renamed.txt"))).toBe(false); + expect(execFileSync("git", ["status", "--porcelain"], { cwd: repoDir }).toString()).toBe(""); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("unstages and removes files in a repository with an unborn HEAD", async () => { + const tempDir = realpathSync.native(mkdtempSync(join(tmpdir(), "checkout-git-unborn-"))); + const repoDir = join(tempDir, "repo"); + mkdirSync(repoDir, { recursive: true }); + try { + execFileSync("git", ["init", "-b", "main"], { cwd: repoDir }); + writeFileSync(join(repoDir, "new.txt"), "new\n"); + execFileSync("git", ["add", "new.txt"], { cwd: repoDir }); + + await discardChanges(repoDir, ["new.txt"]); + + expect(existsSync(join(repoDir, "new.txt"))).toBe(false); + expect(execFileSync("git", ["status", "--porcelain"], { cwd: repoDir }).toString()).toBe(""); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("discards a nested folder pathspec without touching sibling changes", async () => { + const { tempDir, repoDir } = initRepo(); + try { + mkdirSync(join(repoDir, "nested")); + writeFileSync(join(repoDir, "nested", "tracked.txt"), "original\n"); + execFileSync("git", ["add", "nested/tracked.txt"], { cwd: repoDir }); + execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "add nested"], { + cwd: repoDir, + }); + writeFileSync(join(repoDir, "nested", "tracked.txt"), "changed\n"); + writeFileSync(join(repoDir, "nested", "untracked.txt"), "remove\n"); + writeFileSync(join(repoDir, "outside.txt"), "keep\n"); + + await discardChanges(repoDir, ["nested"]); + + expect(readTextFile(join(repoDir, "nested", "tracked.txt"))).toBe("original\n"); + expect(existsSync(join(repoDir, "nested", "untracked.txt"))).toBe(false); + expect(readFileSync(join(repoDir, "outside.txt"), "utf8")).toBe("keep\n"); + expect(execFileSync("git", ["status", "--porcelain"], { cwd: repoDir }).toString()).toBe( + "?? outside.txt\n", + ); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("restores a staged deletion", async () => { + const { tempDir, repoDir } = initRepo(); + try { + execFileSync("git", ["rm", "file.txt"], { cwd: repoDir }); + await discardChanges(repoDir, ["file.txt"]); + expect(readTextFile(join(repoDir, "file.txt"))).toBe("hello\n"); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("leaves files outside the given pathspecs untouched", async () => { + const { tempDir, repoDir } = initRepo(); + try { + writeFileSync(join(repoDir, "file.txt"), "changed\n"); + writeFileSync(join(repoDir, "other.txt"), "keep\n"); + await discardChanges(repoDir, ["file.txt"]); + expect(readTextFile(join(repoDir, "file.txt"))).toBe("hello\n"); + expect(readFileSync(join(repoDir, "other.txt"), "utf8")).toBe("keep\n"); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + it("treats requested filenames as literal pathspecs", async () => { + const { tempDir, repoDir } = initRepo(); + try { + writeFileSync(join(repoDir, "foo[ab].txt"), "literal original\n"); + writeFileSync(join(repoDir, "fooa.txt"), "sibling original\n"); + execFileSync("git", ["add", "foo[ab].txt", "fooa.txt"], { cwd: repoDir }); + execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "add pathspec files"], { + cwd: repoDir, + }); + writeFileSync(join(repoDir, "foo[ab].txt"), "literal changed\n"); + writeFileSync(join(repoDir, "fooa.txt"), "sibling changed\n"); + + await discardChanges(repoDir, ["foo[ab].txt"]); + + expect(readTextFile(join(repoDir, "foo[ab].txt"))).toBe("literal original\n"); + expect(readTextFile(join(repoDir, "fooa.txt"))).toBe("sibling changed\n"); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); +}); diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts index c0cdc54cf5..d51ad346f9 100644 --- a/packages/server/src/utils/checkout-git.ts +++ b/packages/server/src/utils/checkout-git.ts @@ -61,8 +61,10 @@ export type GitMutationRefreshReason = | "create-branch" | "stash-push" | "stash-pop" + | "discard-changes" | "create-worktree"; +const DISCARD_CHANGES_TIMEOUT_MS = 120_000; const DEFAULT_PULL_REQUEST_STATUS_CACHE_TTL_MS = 30_000; const PULL_REQUEST_STATUS_CACHE_MAX = 1_000; const DEFAULT_SHORTSTAT_CACHE_TTL_MS = 15_000; @@ -2019,6 +2021,7 @@ function buildPlaceholderParsedDiffFile( ): ParsedDiffFile { return { path: change.path, + ...(change.oldPath ? { oldPath: change.oldPath } : {}), isNew: change.isNew, isDeleted: change.isDeleted, additions: options.stat?.additions ?? 0, @@ -2856,6 +2859,7 @@ async function buildHighlightedTrackedDiffFile(input: { return { ...highlightedFile, path: change.path, + ...(change.oldPath ? { oldPath: change.oldPath } : {}), isNew: change.isNew, isDeleted: change.isDeleted, status: "ok", @@ -2933,6 +2937,7 @@ async function appendStructuredTrackedDiffs( const file = { path: change.path, + ...(change.oldPath ? { oldPath: change.oldPath } : {}), isNew: change.isNew, isDeleted: change.isDeleted, additions: stat?.additions ?? 0, @@ -3272,6 +3277,71 @@ export async function commitAll(cwd: string, message: string): Promise<void> { await commitChanges(cwd, { message, addAll: true }); } +export async function discardChanges(cwd: string, pathspecs: string[]): Promise<void> { + await requireGitRepo(cwd); + if (pathspecs.length === 0) { + return; + } + try { + await runGitCommand(["--literal-pathspecs", "reset", "-q", "HEAD", "--", ...pathspecs], { + cwd, + timeout: DISCARD_CHANGES_TIMEOUT_MS, + }); + } catch { + // Why: unborn HEAD has no commit for reset, so remove the paths directly from the index. + await runGitCommand( + ["--literal-pathspecs", "rm", "--cached", "-r", "-q", "--ignore-unmatch", "--", ...pathspecs], + { + cwd, + timeout: DISCARD_CHANGES_TIMEOUT_MS, + }, + ); + } + // With everything unstaged, the remaining state is only worktree + // modifications/deletions (restore from the index) and untracked files + // (clean). Classify from porcelain so each path gets the command that + // actually applies to it. + const status = await runGitCommand( + ["--literal-pathspecs", "status", "--porcelain=v1", "-z", "--", ...pathspecs], + { + cwd, + timeout: DISCARD_CHANGES_TIMEOUT_MS, + }, + ); + const tracked: string[] = []; + const untracked: string[] = []; + const tokens = status.stdout.split("\0"); + for (let index = 0; index < tokens.length; index++) { + const token = tokens[index]; + if (token.length < 4) { + continue; + } + const state = token.slice(0, 2); + const filePath = token.slice(3); + if (state.startsWith("R") || state.startsWith("C")) { + // Rename/copy entries carry the source path as the next NUL token. + index += 1; + } + if (state === "??") { + untracked.push(filePath); + continue; + } + tracked.push(filePath); + } + if (tracked.length > 0) { + await runGitCommand(["--literal-pathspecs", "checkout", "-q", "--", ...tracked], { + cwd, + timeout: DISCARD_CHANGES_TIMEOUT_MS, + }); + } + if (untracked.length > 0) { + await runGitCommand(["--literal-pathspecs", "clean", "-fd", "-q", "--", ...untracked], { + cwd, + timeout: DISCARD_CHANGES_TIMEOUT_MS, + }); + } +} + interface DetectMergeToBaseConflictInput { operationCwd: string; error: unknown; diff --git a/packages/server/src/utils/project-custom-icon.test.ts b/packages/server/src/utils/project-custom-icon.test.ts index fa1a81ba88..dc9568d1d0 100644 --- a/packages/server/src/utils/project-custom-icon.test.ts +++ b/packages/server/src/utils/project-custom-icon.test.ts @@ -10,7 +10,9 @@ import { type ProjectRegistry, } from "../server/workspace-registry.js"; import { + ProjectIconReader, readProjectIcon, + readProjectIconSnapshot, removeProjectCustomIcon, setProjectCustomIcon, } from "./project-custom-icon.js"; @@ -52,6 +54,7 @@ async function project() { return record; }, } as unknown as ProjectRegistry; + const reader = new ProjectIconReader(paseoHome); return { paseoHome, @@ -59,6 +62,9 @@ async function project() { set: (source: ProjectIconSource) => setProjectCustomIcon({ paseoHome, projectId: "project-a", source, projects }), read: () => readProjectIcon({ paseoHome, project: record }), + snapshot: () => readProjectIconSnapshot({ paseoHome, project: record }), + advertisedSnapshot: () => reader.snapshot(record), + readAdvertised: () => reader.read(record), revision: () => record.customIconRevision, remove: () => removeProjectCustomIcon({ paseoHome, projectId: "project-a" }), }; @@ -88,6 +94,40 @@ describe("project custom icon", () => { await expect(target.read()).resolves.toEqual(PNG_1X1_ICON); }); + it("fingerprints automatic icons and explicit no-icon results", async () => { + const target = await project(); + + const missing = await target.snapshot(); + expect(missing).toEqual({ icon: null, revision: "automatic:none:v1" }); + + await mkdir(join(target.rootPath, "public")); + await writeFile(join(target.rootPath, "public", "favicon.png"), PNG_1X1); + const first = await target.snapshot(); + await writeFile( + join(target.rootPath, "public", "favicon.png"), + Buffer.concat([PNG_1X1, Buffer.from([1])]), + ); + const changed = await target.snapshot(); + + expect(first.icon).toEqual(PNG_1X1_ICON); + expect(first.revision).toMatch(/^automatic:[a-f0-9]{64}$/); + expect(changed.revision).not.toBe(first.revision); + }); + + it("serves the bytes that match the advertised revision", async () => { + const target = await project(); + await mkdir(join(target.rootPath, "public")); + await writeFile(join(target.rootPath, "public", "favicon.png"), PNG_1X1); + const advertised = await target.advertisedSnapshot(); + + await writeFile( + join(target.rootPath, "public", "favicon.png"), + Buffer.concat([PNG_1X1, Buffer.from([1])]), + ); + + await expect(target.readAdvertised()).resolves.toEqual(advertised.icon); + }); + it("drops the stored image when the project goes back to automatic", async () => { const target = await project(); await target.set({ type: "upload", data: PNG_1X1.toString("base64") }); diff --git a/packages/server/src/utils/project-custom-icon.ts b/packages/server/src/utils/project-custom-icon.ts index 412ca831ce..7d5aba2fa1 100644 --- a/packages/server/src/utils/project-custom-icon.ts +++ b/packages/server/src/utils/project-custom-icon.ts @@ -57,12 +57,63 @@ export async function readProjectIcon(input: { paseoHome: string; project: PersistedProjectRecord; }): Promise<ProjectIcon | null> { - if (!input.project.customIconRevision) return getProjectIcon(input.project.rootPath); - try { - return validateIcon(await readFile(cachePath(input.paseoHome, input.project.projectId))); - } catch { - return null; + return (await readProjectIconSnapshot(input)).icon; +} + +export interface ProjectIconSnapshot { + icon: ProjectIcon | null; + revision: string; +} + +/** Keeps the icon bytes served by a session aligned with its advertised revision. */ +export class ProjectIconReader { + private readonly snapshots = new Map<string, ProjectIconSnapshot>(); + + constructor(private readonly paseoHome: string) {} + + async snapshot(project: PersistedProjectRecord): Promise<ProjectIconSnapshot> { + const snapshot = await readProjectIconSnapshot({ paseoHome: this.paseoHome, project }); + this.snapshots.set(project.projectId, snapshot); + return snapshot; } + + async read(project: PersistedProjectRecord): Promise<ProjectIcon | null> { + return (this.snapshots.get(project.projectId) ?? (await this.snapshot(project))).icon; + } +} + +/** Reads the effective icon and gives that exact result a stable identity. */ +export async function readProjectIconSnapshot(input: { + paseoHome: string; + project: PersistedProjectRecord; +}): Promise<ProjectIconSnapshot> { + if (input.project.customIconRevision) { + let icon: ProjectIcon | null = null; + try { + icon = validateIcon(await readFile(cachePath(input.paseoHome, input.project.projectId))); + } catch { + // A missing/corrupt custom icon is itself a cacheable result until the + // registry revision changes. + } + return { + icon, + revision: icon + ? iconRevision("custom", icon) + : `custom:none:${input.project.customIconRevision}`, + }; + } + const icon = await getProjectIcon(input.project.rootPath); + if (!icon) return { icon: null, revision: "automatic:none:v1" }; + return { icon, revision: iconRevision("automatic", icon) }; +} + +function iconRevision(source: "automatic" | "custom", icon: ProjectIcon): string { + const fingerprint = createHash("sha256") + .update(icon.mimeType) + .update("\0") + .update(icon.data) + .digest("hex"); + return `${source}:${fingerprint}`; } export async function removeProjectCustomIcon(input: { diff --git a/packages/server/src/utils/project-icon.test.ts b/packages/server/src/utils/project-icon.test.ts index 18289d8480..e424fd1ded 100644 --- a/packages/server/src/utils/project-icon.test.ts +++ b/packages/server/src/utils/project-icon.test.ts @@ -125,11 +125,11 @@ describe("findProjectIcon", () => { }); it("prioritizes favicon over logo", async () => { - writeFileSync(join(tempDir, "favicon.ico"), "favicon"); + writeFileSync(join(tempDir, "favicon.png"), "favicon"); writeFileSync(join(tempDir, "logo.png"), "logo"); const result = await findProjectIcon(tempDir); - expect(result).toBe(join(tempDir, "favicon.ico")); + expect(result).toBe(join(tempDir, "favicon.png")); }); it("prioritizes priority dirs over root", async () => { @@ -180,6 +180,14 @@ describe("findProjectIcon", () => { expect(result).toBe(join(tempDir, "apple-touch-icon.png")); }); + it("prefers an Apple touch PNG over ICO", async () => { + writeFileSync(join(tempDir, "favicon.ico"), "ico"); + writeFileSync(join(tempDir, "apple-touch-icon.png"), "apple icon"); + + const result = await findProjectIcon(tempDir); + expect(result).toBe(join(tempDir, "apple-touch-icon.png")); + }); + it("finds icon-*.png patterns", async () => { writeFileSync(join(tempDir, "icon-192.png"), "192 icon"); @@ -187,6 +195,19 @@ describe("findProjectIcon", () => { expect(result).toBe(join(tempDir, "icon-192.png")); }); + it.each([ + "favicon-32x32.png", + "apple-touch-icon-180x180.png", + "android-chrome-192x192.png", + "safari-pinned-tab.svg", + "mstile-150x150.png", + ])("finds common sized icon %s", async (fileName) => { + writeFileSync(join(tempDir, fileName), "icon"); + + const result = await findProjectIcon(tempDir); + expect(result).toBe(join(tempDir, fileName)); + }); + it("handles non-existent directory gracefully", async () => { const result = await findProjectIcon(join(tempDir, "nonexistent")); expect(result).toBeNull(); diff --git a/packages/server/src/utils/project-icon.ts b/packages/server/src/utils/project-icon.ts index db7091a875..eb9719a987 100644 --- a/packages/server/src/utils/project-icon.ts +++ b/packages/server/src/utils/project-icon.ts @@ -5,25 +5,31 @@ import { extname, join } from "path"; * Icon file patterns to search for, in priority order. * Patterns starting with '*' are glob patterns (e.g., icon-*.png). * - * Within each name, SVG and PNG outrank ICO: every client renders them (React - * Native's <Image> cannot decode ICO at all) and they scale better. ICO is the - * last resort for each name, not the first. + * SVG and PNG outrank ICO across all known names: every client renders them + * and they scale better. ICO is the final fallback because native clients + * cannot decode containers whose frames use the legacy bitmap format. */ export const ICON_PATTERNS = [ "favicon.svg", "favicon.png", - "favicon.ico", + "favicon-*.svg", + "favicon-*.png", "favico.svg", "favico.png", - "favico.ico", "icon.svg", "icon.png", "app-icon.svg", "app-icon.png", "apple-touch-icon.png", + "apple-touch-icon-*.png", "icon-*.png", + "android-chrome-*.png", + "safari-pinned-tab.svg", + "mstile-*.png", "logo.svg", "logo.png", + "favicon.ico", + "favico.ico", ]; /** diff --git a/packages/server/src/utils/spawn.ts b/packages/server/src/utils/spawn.ts index ac27572c24..8fac115f00 100644 --- a/packages/server/src/utils/spawn.ts +++ b/packages/server/src/utils/spawn.ts @@ -27,6 +27,7 @@ interface ExecCommandOptions extends ExternalEnvOptions { timeout?: number; maxBuffer?: number; shell?: boolean | string; + signal?: AbortSignal; } interface ExecCommandResult { @@ -77,6 +78,7 @@ export function spawnProcess( ...spawnOptions, env: childEnv, shell, + signal: options?.signal, windowsHide: true, }); } diff --git a/packages/server/src/utils/worktree.posix.test.ts b/packages/server/src/utils/worktree.posix.test.ts index 74e2fcf479..36e1642f7d 100644 --- a/packages/server/src/utils/worktree.posix.test.ts +++ b/packages/server/src/utils/worktree.posix.test.ts @@ -2,7 +2,6 @@ /* eslint-disable max-nested-callbacks */ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { - BranchAlreadyCheckedOutError, createWorktree as createWorktreePrimitive, deriveWorktreeProjectHash, deletePaseoWorktree, @@ -41,6 +40,9 @@ import { writeFileSync, readFileSync, chmodSync, + lstatSync, + readlinkSync, + symlinkSync, } from "fs"; import { delimiter, dirname, join } from "path"; import { tmpdir } from "os"; @@ -299,22 +301,17 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => { expect(currentBranch).toBe("release/1.1.15"); }); - it("throws a typed error when checking out a branch already checked out in the main repo", async () => { - let caughtError: unknown; - try { - await createLegacyWorktreeForTest({ - cwd: repoDir, - worktreeSlug: "dev-worktree", - source: { kind: "checkout-branch", branchName: "main" }, - runSetup: true, - paseoHome, - }); - } catch (error) { - caughtError = error; - } + it("creates a suffixed branch when checking out a branch already checked out in the main repo", async () => { + const result = await createLegacyWorktreeForTest({ + cwd: repoDir, + worktreeSlug: "dev-worktree", + source: { kind: "checkout-branch", branchName: "main" }, + runSetup: true, + paseoHome, + }); - expect(caughtError).toBeInstanceOf(BranchAlreadyCheckedOutError); - expect((caughtError as BranchAlreadyCheckedOutError).branchName).toBe("main"); + expect(result.branchName).toBe("main-1"); + expect(existsSync(result.worktreePath)).toBe(true); }); it("fetches a GitHub PR branch, checks it out, writes metadata, and runs setup", async () => { @@ -1194,33 +1191,95 @@ describe.skipIf(isPlatform("win32"))("worktree POSIX-only", () => { }); }); - it("does not overwrite a committed paseo.json with uncommitted edits in the main repo", async () => { + it("keeps a new worktree clean when its upstream ref has a newer paseo.json", async () => { + const remoteDir = join(tempDir, "remote.git"); + const updaterDir = join(tempDir, "updater"); writeFileSync( join(repoDir, "paseo.json"), - JSON.stringify({ scripts: { dev: { command: "committed" } } }), + JSON.stringify({ worktree: { setup: "echo old" } }), ); execFileSync("git", ["add", "paseo.json"], { cwd: repoDir }); execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "add paseo.json"], { cwd: repoDir, }); - + execFileSync("git", ["clone", "--bare", repoDir, remoteDir]); + execFileSync("git", ["remote", "add", "origin", remoteDir], { cwd: repoDir }); + execFileSync("git", ["clone", remoteDir, updaterDir]); + execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: updaterDir }); + execFileSync("git", ["config", "user.name", "Test"], { cwd: updaterDir }); writeFileSync( - join(repoDir, "paseo.json"), - JSON.stringify({ scripts: { dev: { command: "uncommitted" } } }), + join(updaterDir, "paseo.json"), + JSON.stringify({ worktree: { setup: "echo new" } }), ); + execFileSync("git", ["add", "paseo.json"], { cwd: updaterDir }); + execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "update paseo.json"], { + cwd: updaterDir, + }); + execFileSync("git", ["push", "origin", "main"], { cwd: updaterDir }); + execFileSync("git", ["fetch", "origin"], { cwd: repoDir }); const result = await createLegacyWorktreeForTest({ cwd: repoDir, - worktreeSlug: "preserve-committed", - source: { kind: "branch-off", baseBranch: "main", branchName: "feature/preserve" }, + worktreeSlug: "upstream-config", + source: { + kind: "branch-off", + baseBranch: "origin/main", + branchName: "feature/upstream-config", + }, runSetup: false, paseoHome, }); const worktreeConfigPath = join(result.worktreePath, "paseo.json"); expect(JSON.parse(readFileSync(worktreeConfigPath, "utf8"))).toEqual({ - scripts: { dev: { command: "committed" } }, + worktree: { setup: "echo new" }, }); + expect( + execFileSync("git", ["status", "--porcelain"], { + cwd: result.worktreePath, + encoding: "utf8", + }), + ).toBe(""); + }); + + it("preserves a dangling paseo.json symlink from the selected ref", async () => { + const externalConfigPath = join(tempDir, "outside-paseo.json"); + execFileSync("git", ["checkout", "-b", "symlink-config"], { cwd: repoDir }); + symlinkSync(externalConfigPath, join(repoDir, "paseo.json")); + execFileSync("git", ["add", "paseo.json"], { cwd: repoDir }); + execFileSync( + "git", + ["-c", "commit.gpgsign=false", "commit", "-m", "add paseo.json symlink"], + { cwd: repoDir }, + ); + execFileSync("git", ["checkout", "main"], { cwd: repoDir }); + writeFileSync( + join(repoDir, "paseo.json"), + JSON.stringify({ worktree: { setup: "echo source" } }), + ); + + const result = await createLegacyWorktreeForTest({ + cwd: repoDir, + worktreeSlug: "symlink-config", + source: { + kind: "branch-off", + baseBranch: "symlink-config", + branchName: "feature/symlink-config", + }, + runSetup: false, + paseoHome, + }); + + const worktreeConfigPath = join(result.worktreePath, "paseo.json"); + expect(lstatSync(worktreeConfigPath).isSymbolicLink()).toBe(true); + expect(readlinkSync(worktreeConfigPath)).toBe(externalConfigPath); + expect(existsSync(externalConfigPath)).toBe(false); + expect( + execFileSync("git", ["status", "--porcelain"], { + cwd: result.worktreePath, + encoding: "utf8", + }), + ).toBe(""); }); it("creates a worktree without error when no paseo.json exists in the main repo", async () => { diff --git a/packages/server/src/utils/worktree.ts b/packages/server/src/utils/worktree.ts index 8b7d1eb24f..9159a2accc 100644 --- a/packages/server/src/utils/worktree.ts +++ b/packages/server/src/utils/worktree.ts @@ -1,6 +1,14 @@ import { execFile } from "child_process"; import { promisify } from "util"; -import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync } from "fs"; +import { + constants as fsConstants, + existsSync, + mkdirSync, + readFileSync, + realpathSync, + rmSync, + statSync, +} from "fs"; import { copyFile, readFile, rm, stat } from "fs/promises"; import { join, basename, dirname, isAbsolute, resolve, sep } from "path"; import net from "node:net"; @@ -230,13 +238,6 @@ export interface CreateWorktreeOptions { worktreesRoot?: string; } -interface ResolveExistingWorktreeForSlugOptions { - slug: string; - repoRoot: string; - paseoHome?: string; - worktreesRoot?: string; -} - export class BranchAlreadyCheckedOutError extends Error { readonly branchName: string; @@ -947,14 +948,9 @@ export async function seedPaseoConfigFile(options: { }): Promise<void> { const sourceConfigPath = join(options.sourceCwd, "paseo.json"); const targetConfigPath = join(options.targetCwd, "paseo.json"); - try { - await stat(targetConfigPath); - return; - } catch (error) { - if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; - } - await copyFile(sourceConfigPath, targetConfigPath).catch((error) => { - if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + await copyFile(sourceConfigPath, targetConfigPath, fsConstants.COPYFILE_EXCL).catch((error) => { + const code = (error as NodeJS.ErrnoException).code; + if (code !== "EEXIST" && code !== "ENOENT") throw error; }); } @@ -1204,38 +1200,6 @@ export async function listPaseoWorktrees({ ); } -export async function resolveExistingWorktreeForSlug({ - slug, - repoRoot, - paseoHome, - worktreesRoot, -}: ResolveExistingWorktreeForSlugOptions): Promise<WorktreeConfig | null> { - const worktrees = await listPaseoWorktrees({ - cwd: repoRoot, - paseoHome, - worktreesRoot, - }); - const slugSuffix = `${sep}${slug}`; - const existingWorktree = worktrees.find((worktree) => worktree.path.endsWith(slugSuffix)); - if (!existingWorktree) { - return null; - } - - const { stdout } = await runGitCommand(["branch", "--show-current"], { - cwd: existingWorktree.path, - envOverlay: READ_ONLY_GIT_ENV, - }); - const branchName = stdout.trim(); - if (!branchName) { - throw new Error(`Unable to resolve branch for existing worktree: ${existingWorktree.path}`); - } - - return { - branchName, - worktreePath: existingWorktree.path, - }; -} - export interface DeletePaseoWorktreeOptions { cwd: string | null; worktreePath?: string; @@ -1516,7 +1480,12 @@ async function resolveWorktreeSourcePlan({ } } if (await isBranchCheckedOut(cwd, source.branchName)) { - throw new BranchAlreadyCheckedOutError(source.branchName); + const branchName = await resolveUniqueLocalBranchName(cwd, source.branchName); + return { + branchName, + metadataBaseRefName: source.branchName, + addArguments: ["-b", branchName, "--no-track", source.branchName], + }; } return { diff --git a/packages/website/package.json b/packages/website/package.json index 5f83185f3b..78b5bf6a89 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/website", - "version": "0.3.1", + "version": "0.4.0", "private": true, "type": "module", "scripts": { diff --git a/packages/website/posts/why-agent-chat-rooms-beat-agent-to-agent-prompting.md b/packages/website/posts/why-agent-chat-rooms-beat-agent-to-agent-prompting.md deleted file mode 100644 index 308317cc05..0000000000 --- a/packages/website/posts/why-agent-chat-rooms-beat-agent-to-agent-prompting.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: "Why agent chat rooms beat agent-to-agent prompting" -description: "Why shared chat rooms are a better coordination model than hidden agent-to-agent prompt chains once tasks stop being trivial." -date: "2026-03-29" -draft: "true" ---- - -I keep seeing people try to wire agents together by having one agent prompt another directly. - -Planner prompts implementer. Implementer prompts reviewer. Reviewer prompts planner again. Maybe it works for a demo. Maybe it even works for a narrow task. - -But I think it breaks down pretty quickly. - -The problem is that agent-to-agent prompting turns coordination into hidden private messages. The whole workflow disappears into a chain of opaque handoffs. Context gets duplicated, instructions drift, and it becomes hard for the human to see what is going on, step in, or redirect things cleanly. - -What has been working much better for me is a shared chat room model. - -Instead of agents calling each other directly, they all communicate in a room that the human can also read and participate in. The room becomes the coordination surface. Not just a log, but the place where work gets assigned, clarified, reviewed, and handed off. - -That changes a few things. - -First, coordination becomes visible. If a planning agent says "I think we should split this into 3 steps", that is just there in the room. If an implementation agent finishes and asks for review, that is there too. The human does not need to reconstruct the workflow from tool logs or imagine what one agent told another 5 minutes ago. - -Second, handoff gets cleaner. You are no longer depending on one giant prompt chain staying intact. Agents can join, leave, catch up, and respond to the current shared context. That matters a lot once tasks stop being trivial. - -Third, the human stays in the loop without micromanaging. This is the part I care about most. I do not want to disappear into a black box where agents recursively talk to each other and I only see the final result. I want to be able to glance at the room, redirect something, answer a question, or spawn another agent when needed. - -Fourth, it matches how collaborative work already feels. A lot of software development is not one linear prompt. It is more like a room with planning, implementation, review, clarification, waiting, and resuming. If that is true for humans, I think it will be true for agents too. - -This also connects to a broader point I keep coming back to: a real agent development environment probably needs organizational entities above a single chat. - -If every task is just one long chat with one agent, things get messy fast. Once you have parallel tasks, multiple models, review loops, different hosts, or work that continues while you are away from your desk, you need structure. Projects, workspaces, rooms, whatever you want to call them. Some stable place where the work lives. - -I think people underestimate this because current demos are still too simple. One agent edits files, another reviews, done. But once you actually live in these tools every day, the bottleneck is not just model quality. It is coordination. - -My current belief is that the future is less "one super agent in one chat box" and more "teams of agents collaborating in shared contexts that humans can also inhabit". - -Not because it sounds grand. Just because it seems to work better. - -If you're building in this space, I'm curious what has been working for you: - -- direct agent-to-agent prompting -- shared rooms -- workspaces -- something else entirely diff --git a/packages/website/public/schemas/paseo.config.v1.json b/packages/website/public/schemas/paseo.config.v1.json index 985539dc79..783bbe7fd4 100644 --- a/packages/website/public/schemas/paseo.config.v1.json +++ b/packages/website/public/schemas/paseo.config.v1.json @@ -501,6 +501,11 @@ "required": ["provider"], "additionalProperties": false } + }, + "catalogRefreshTimeoutMs": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 2147483647 } }, "additionalProperties": false diff --git a/packages/website/src/components/landing-page.tsx b/packages/website/src/components/landing-page.tsx index a3195f1777..87f12f69d9 100644 --- a/packages/website/src/components/landing-page.tsx +++ b/packages/website/src/components/landing-page.tsx @@ -1157,19 +1157,6 @@ paseo run --host devbox:6767 "run the full test suite" paseo ls # list running agents paseo attach abc123 # stream live output paseo send abc123 "also add tests" # follow-up task`, - }, - { - title: "Loops", - description: - "Have one agent do the work, another verify the result, and loop until it passes. Built-in, no shell scripting needed.", - code: `# Worker-verifier loop: fix tests until they pass -paseo loop run "make all tests pass" \\ - --verify "verify tests pass and the code is production-ready" \\ - --verify-check "npm test" \\ - --max-iterations 5 - -paseo loop ls # list running loops -paseo loop logs abc123 # stream loop output`, }, { title: "Schedules", diff --git a/packages/website/src/content/alternatives/claude-desktop.md b/packages/website/src/content/alternatives/claude-desktop.md index c03cf14c96..b4f55f9fba 100644 --- a/packages/website/src/content/alternatives/claude-desktop.md +++ b/packages/website/src/content/alternatives/claude-desktop.md @@ -79,7 +79,7 @@ paseo send <agent-id> "add tests" paseo schedule create --cron "0 9 * * 1" "audit the codebase" ``` -`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes. The MCP server lets other agents create worktrees, launch agents, open terminals, and send prompts. +`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. The MCP server lets other agents create worktrees, launch agents, open terminals, and send prompts. ## Worktrees and services diff --git a/packages/website/src/content/alternatives/codex-app.md b/packages/website/src/content/alternatives/codex-app.md index 1b6f67116a..af29184926 100644 --- a/packages/website/src/content/alternatives/codex-app.md +++ b/packages/website/src/content/alternatives/codex-app.md @@ -79,7 +79,7 @@ paseo send <agent-id> "add tests" paseo schedule create --cron "0 9 * * 1" "audit the codebase" ``` -`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes. The MCP server lets other agents create worktrees, launch agents, open terminals, and send prompts. +`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. The MCP server lets other agents create worktrees, launch agents, open terminals, and send prompts. ## Voice diff --git a/packages/website/src/content/alternatives/conductor.md b/packages/website/src/content/alternatives/conductor.md index a72dc6b36b..49ffb867f2 100644 --- a/packages/website/src/content/alternatives/conductor.md +++ b/packages/website/src/content/alternatives/conductor.md @@ -58,7 +58,7 @@ paseo send <agent-id> "add tests" paseo schedule create --cron "0 9 * * 1" "audit the codebase" ``` -`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes. +`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. Conductor does not have a CLI. diff --git a/packages/website/src/content/alternatives/happy-coder.md b/packages/website/src/content/alternatives/happy-coder.md index 29e8ebed89..8af4c2f21f 100644 --- a/packages/website/src/content/alternatives/happy-coder.md +++ b/packages/website/src/content/alternatives/happy-coder.md @@ -70,7 +70,7 @@ paseo send <agent-id> "add tests" paseo schedule create --cron "0 9 * * 1" "audit the codebase" ``` -`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes. +`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. Happy Coder has a CLI to launch the wrapped session. It does not have schedules or loops. diff --git a/packages/website/src/content/alternatives/openchamber.md b/packages/website/src/content/alternatives/openchamber.md index 77ca2f0219..6980a97d15 100644 --- a/packages/website/src/content/alternatives/openchamber.md +++ b/packages/website/src/content/alternatives/openchamber.md @@ -60,7 +60,7 @@ paseo send <agent-id> "add tests" paseo schedule create --cron "0 9 * * 1" "audit the codebase" ``` -`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes. +`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. OpenChamber does not have a CLI. diff --git a/packages/website/src/content/alternatives/opencode-desktop.md b/packages/website/src/content/alternatives/opencode-desktop.md index 1555cbaf86..eac0e40d09 100644 --- a/packages/website/src/content/alternatives/opencode-desktop.md +++ b/packages/website/src/content/alternatives/opencode-desktop.md @@ -75,7 +75,7 @@ paseo send <agent-id> "add tests" paseo schedule create --cron "0 9 * * 1" "audit the codebase" ``` -`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes. The MCP server lets other agents create worktrees, launch agents, open terminals, and send prompts. +`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. The MCP server lets other agents create worktrees, launch agents, open terminals, and send prompts. ## Worktrees and services diff --git a/packages/website/src/content/alternatives/superset.md b/packages/website/src/content/alternatives/superset.md index 7c338dd5c1..43c440cb10 100644 --- a/packages/website/src/content/alternatives/superset.md +++ b/packages/website/src/content/alternatives/superset.md @@ -70,7 +70,7 @@ paseo send <agent-id> "add tests" paseo schedule create --cron "0 9 * * 1" "audit the codebase" ``` -`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes. +`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. Superset is a desktop app and does not have a CLI. diff --git a/packages/workspace-helper/package.json b/packages/workspace-helper/package.json index cd511e940c..a1ddf164da 100644 --- a/packages/workspace-helper/package.json +++ b/packages/workspace-helper/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/workspace-helper", - "version": "0.3.1", + "version": "0.4.0", "description": "Official confined workspace filesystem helper and typed binding for Paseo runtimes", "bin": { "paseo-workspace-helper": "./dist/executable.mjs" diff --git a/packages/workspace-runtime-contract/package.json b/packages/workspace-runtime-contract/package.json index d9fafb40aa..a961c9b6d0 100644 --- a/packages/workspace-runtime-contract/package.json +++ b/packages/workspace-runtime-contract/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/workspace-runtime-contract", - "version": "0.3.1", + "version": "0.4.0", "description": "Versioned command workspace runtime author contract for Paseo", "files": [ "dist", diff --git a/paseo.json b/paseo.json index 076d1f2c58..4e7b0e8116 100644 --- a/paseo.json +++ b/paseo.json @@ -19,7 +19,7 @@ }, "desktop": { "type": "service", - "command": "PASEO_DEV_MANAGED_HOME=1 PASEO_DEV_ROOT=\"${PASEO_WORKTREE_PATH:-$PWD}\" PASEO_HOME=\"${PASEO_WORKTREE_PATH:-$PWD}/.dev/paseo-home\" PASEO_LISTEN=0.0.0.0:${PASEO_SERVICE_DAEMON_PORT:-6768} PASEO_DEV_DAEMON_ENDPOINT=localhost:${PASEO_SERVICE_DAEMON_PORT:-6768} EXPO_PORT=${PASEO_PORT:-} npm run dev --workspace=@getpaseo/desktop" + "command": "PASEO_DEV_MANAGED_HOME=1 PASEO_DEV_ROOT=\"${PASEO_WORKTREE_PATH:-$PWD}\" PASEO_HOME=\"${PASEO_WORKTREE_PATH:-$PWD}/.dev/paseo-home\" PASEO_LISTEN=0.0.0.0:${PASEO_SERVICE_DAEMON_PORT:-6768} PASEO_DEV_DAEMON_ENDPOINT=localhost:${PASEO_SERVICE_DAEMON_PORT:-6768} EXPO_PORT=${PASEO_PORT:-} exec ./packages/desktop/scripts/dev.sh" }, "ios-simulator": { "type": "service", diff --git a/public-docs/cli.md b/public-docs/cli.md index c4880ba2e5..ae3491b0af 100644 --- a/public-docs/cli.md +++ b/public-docs/cli.md @@ -23,6 +23,18 @@ paseo logs <id> # View agent timeline paseo stop <id> # Stop an agent ``` +## Provider diagnostics + +Ask the daemon to inspect the provider environment it actually uses: + +```bash +paseo provider diagnostic claude +paseo provider diagnostic codex --json +paseo provider diagnostic opencode --host devbox:6767 +``` + +The diagnostic includes the configured command, daemon `PATH` and shell, matching binaries, resolved path, version, model count, and provider status. Use `--host` for a remote daemon. This is the same diagnostic shown under **Settings → your host → Providers → provider → Diagnostic**. + ## Running agents Use `paseo run` to start a new agent with a task: @@ -76,11 +88,13 @@ paseo workspace create \ --pr-number 2186 ``` -Then list, use, or archive it: +Then list, use, rename, or archive it: ```bash paseo workspace ls paseo run --workspace <workspace-id> "implement authentication" +paseo workspace rename <workspace-id> "Auth rework" +paseo workspace rename <workspace-id> --reset # back to the branch or directory name paseo workspace archive <workspace-id> ``` diff --git a/public-docs/hub/activity.md b/public-docs/hub/activity.md index b00f3e1f34..5abe91b2cf 100644 --- a/public-docs/hub/activity.md +++ b/public-docs/hub/activity.md @@ -12,34 +12,28 @@ Every event Hub accepts is recorded, whether or not it ran anything. That record ## Where to look -**Project → Activity** lists the events routed to that project: the event, its source, the result, and when it arrived. +**Project → Activity** lists routed trigger runs and their execution state. -The result column says what happened: +**Connections → Known unrouted events** is the organization-level view of accepted provider events that did not start a configured trigger. Hub records one of four bounded reasons: -| Result | Meaning | -| --------------------- | ----------------------------------------------------------------- | -| A trigger name | The event matched that trigger and dispatched | -| A lifecycle state | The execution is in progress or finished | -| `no_matching_trigger` | The event reached the project, but no trigger's filters matched | -| `provider_unrouted` | No project route existed for the resource that produced the event | - -**Project → Executions** lists the runs: which trigger, which configuration revision, which daemon, duration, and outcome. - -**Connections → Known unrouted events** is the organization-level view. It holds events whose credential belongs to your organization but which no project's configuration claimed. A repository you connected but never named in a trigger lands here. - -Those two are different problems. `no_matching_trigger` means your filters are wrong. An unrouted event means no configuration mentions that repository, workspace, or guild at all. +| Reason | Meaning | +| --------------------------- | ------------------------------------------------------------ | +| `no_project_route` | No project route is configured for the event | +| `no_trigger_for_source` | No configured trigger handles that event source | +| `trigger_filters_rejected` | A trigger handles the source, but its filters rejected it | +| `configuration_unavailable` | A relevant configuration or connection could not be resolved | ## Nothing happened when I mentioned the bot Work down this list. -1. **Is the event in the project's Activity?** If not, check **Connections → Known unrouted events**. If it is there, no trigger names that resource. Add `filters.repo`, `filters.workspace`, or `filters.guild` and push. +1. **Is the event in the project's Activity?** If not, check **Connections → Known unrouted events** and use its reason to distinguish routing, source, filter, and configuration failures. 2. **Is the event anywhere at all?** If not, the event never reached Hub. Check the provider's own delivery log: GitHub's App → Advanced → Recent Deliveries, or Slack's Event Subscriptions page. Then check that the app is subscribed to that event type. 3. **Is your user in `from_users`?** This is the most common cause. GitHub uses your login; Slack and Discord use the user ID, not the display name. -4. **Did the mention match?** On GitHub, `contains` must appear in the comment body. On Slack and Discord the bot must actually be mentioned, and `pattern` matches only at the start of the text after the mention. +4. **Did the invocation match?** On GitHub, `contains` must appear in the comment body. On Slack and Discord the bot must be mentioned, and `pattern` or its legacy `contains` alias must prefix the text after the mention. 5. **Is the configuration you think is active actually active?** The Configuration tab shows the active revision and the last sync attempt. A failed push leaves the old revision serving. 6. **Is the daemon connected?** An offline daemon fails dispatch with `daemon_not_connected`. -7. **Did it run and stop early?** Check the execution. An agent that never reports back is ended by `idle_timeout` after 5 minutes by default, or `timeout` after an hour. +7. **Did it run and stop early?** Compare the execution with the step's authored `idle_timeout` and `max_runtime`, and the workflow's `max_runtime`. All three limits are explicit in the workflow. 8. **Did it run, but deliver nothing?** Check the step's prompt. An agent that is not told to call `hub.reply` and `hub.finish_execution` can answer in its own transcript and never report back. See [Tell the agent which tool to call](/docs/hub/workflows#tell-the-agent-which-tool-to-call). ## Sync failures diff --git a/public-docs/hub/api.md b/public-docs/hub/api.md index 14d52b4b97..cfc0bbc7eb 100644 --- a/public-docs/hub/api.md +++ b/public-docs/hub/api.md @@ -54,10 +54,12 @@ API failures use RFC 9457 problem details. Missing, invalid, or revoked credenti ```json { - "type": "https://hub.example.com/problems/unauthorized", - "title": "Unauthorized", + "type": "https://paseo.sh/problems/unauthorized", + "title": "Authentication required", "status": 401, - "detail": "Provide a valid organization API key." + "detail": "Provide an active Paseo organization credential in the Authorization: Bearer header.", + "code": "unauthorized", + "requestId": "5e967c44-fc22-4f6d-8fc5-1bbff33121af" } ``` @@ -130,7 +132,8 @@ Limits: - Bundle: at most 100 files. - File path: at most 512 characters. -- File content: at most 1,000,000 characters; partial byte limits are enforced during compilation. +- File content: at most 1,000,000 characters. +- Prompt partials: at most 1,000,000 bytes each and 5,000,000 bytes in total. On success, Hub returns `201`: @@ -163,7 +166,7 @@ The trigger must exist in the active configuration, and `actor` must be listed in that trigger's `filters.from_users` allowlist. ```http -POST /api/manual-runs +POST /api/v1/manual-runs ``` Request body: @@ -181,32 +184,27 @@ Request body: - `expectedVersionId` is optional. When supplied, Hub rejects the dispatch if that revision is no longer active. - `input` is the same string a provider message uses: leading `key=value` tokens are parsed as declared inputs, and the remainder becomes `${{ paseo.prompt }}`. -- `deliveryKey` should be unique and stable per dispatch. Reusing one resolves to the existing trigger instead of starting a duplicate run. +- `deliveryKey` should be unique and stable per dispatch. Hub uses it for durable deduplication, but does not promise exactly-once dispatch or replay of an earlier response. On success, Hub returns `200`: ```json { "deliveryKey": "deploy-2026-08-04-001", - "triggerId": "00000000-0000-4000-8000-000000000000", - "executionId": "00000000-0000-4000-8000-000000000000", - "status": "spawning", - "daemonId": "00000000-0000-4000-8000-000000000000", - "agentId": "agent-id" + "providerEventReceiptId": "00000000-0000-4000-8000-000000000000", + "triggerRunId": "00000000-0000-4000-8000-000000000000", + "configuredTriggerName": "deploy", + "workflowStatus": "running" } ``` -Common responses are `400 {"error":"invalid_request"}`, `404` for an -unknown project, missing configuration, or missing trigger, `403 -{"error":"actor_forbidden"}` when the actor is not allowed by the trigger, -and `409` when the daemon is offline, the expected configuration is no longer -current, or the run could not be dispatched. +Common responses are `400` for an invalid request, `403` when the actor is not allowed by the trigger, `404` for an unknown project, configuration, or trigger, and `409` when the daemon is offline, the expected configuration is no longer current, or dispatch conflicts. Each uses the problem-details shape above. Example: ```bash -curl --fail-with-body -sS -X POST "$HUB_URL/api/manual-runs" \ - -H "Authorization: Bearer $PASEO_API_KEY" \ +curl --fail-with-body -sS -X POST "$PASEO_HUB_URL/api/v1/manual-runs" \ + -H "Authorization: Bearer $PASEO_HUB_API_KEY" \ -H "Content-Type: application/json" \ --data '{ "projectSlug": "my-project", @@ -230,7 +228,7 @@ be used as one. POST /api/v1/daemons/enrollment-tokens ``` -Send an empty JSON object as the request body. On success, Hub returns `201`: +No request body is required. On success, Hub returns `201`: ```json { @@ -245,10 +243,8 @@ The token expires after 10 minutes and is consumed when the daemon enrolls. ```bash curl --fail-with-body -sS -X POST \ - "$HUB_URL/api/v1/daemons/enrollment-tokens" \ - -H "Authorization: Bearer $PASEO_API_KEY" \ - -H "Content-Type: application/json" \ - --data '{}' + "$PASEO_HUB_URL/api/v1/daemons/enrollment-tokens" \ + -H "Authorization: Bearer $PASEO_HUB_API_KEY" ``` Direct API consumers can pass the returned token to the daemon enrollment protocol. The Paseo CLI intentionally does not accept raw enrollment tokens; `connect` owns the authenticated single-flow exchange. diff --git a/public-docs/hub/configuration/hub-yml.md b/public-docs/hub/configuration/hub-yml.md index 2ab9d28895..a478dcf1e9 100644 --- a/public-docs/hub/configuration/hub-yml.md +++ b/public-docs/hub/configuration/hub-yml.md @@ -64,6 +64,26 @@ The only top-level keys are `environments` and `agents`. A `triggers` key is rej For `worktree`, use `newBranch` and optional `base` with `branch-off`, `branch` with `checkout-branch`, or positive `prNumber` with `checkout-pr`. +```yaml +environments: + review: + kind: daemon + daemon: build-server + cwd: /workspace/project + worktree: + mode: branch-off + newBranch: trigger-${{ paseo.execution.id }} + base: origin/main +``` + +`newBranch` is a branch-name string. Embed `${{ paseo.execution.id }}`, which renders the execution's UUID, so every execution branches off `base` on its own branch and keeps it when Hub retries or recovers that execution. + +One execution is one step run, so two steps selecting the same environment get separate branches. + +`${{ paseo.execution.id }}` is the only expression `newBranch` accepts. `paseo.prompt`, `paseo.context`, `paseo.inputs.*`, `values.*`, `steps.<id>.outputs.*`, and provider event fields are unavailable here, and each one fails bundle activation at the authored field, such as `.paseo/hub.yml.environments.review.worktree.newBranch`. + +`${{ paseo.execution.id }}` fails activation the same way anywhere else in a bundle. `branch` and `prNumber` take literal values. + An environment is a complete named object. A step selects its name; objects are not inherited, merged, or partially overridden. ### Named agents @@ -80,6 +100,8 @@ Each agent is one complete provider configuration: A named selection preserves the complete object, including structured options. Named agents have no parent, patch, or per-step override. +Hub passes `model`, `mode`, `thinkingOptionId`, and `options` to the Paseo daemon without renaming or flattening provider fields. The selected daemon validates them against its current provider schema; Hub does not translate provider-native options. + ## Workflow files `.paseo/workflows/review.yml`: @@ -88,6 +110,8 @@ A named selection preserves the complete object, including structured options. N name: review on: manual.run max_runtime: 2h +filters: + from_users: [automation] inputs: repo: type: string @@ -103,15 +127,15 @@ steps: - text: ${{ paseo.prompt }} ``` -| Field | Required | Notes | -| ------------- | ------------------ | --------------------------------------------------------- | -| `name` | yes | Workflow name, unique across the bundle. | -| `on` | yes | Provider event such as `manual.run` or `discord.mention`. | -| `max_runtime` | yes | Hard limit for the complete run, up to 24h. | -| `filters` | provider-dependent | Provider resource and sender allowlists. | -| `inputs` | no | Typed invocation headers. | -| `values` | no | Named expressions. | -| `steps` | yes | One or more ordered inline steps. | +| Field | Required | Notes | +| ------------- | -------- | --------------------------------------------------------- | +| `name` | yes | Workflow name, unique across the bundle. | +| `on` | yes | Provider event such as `manual.run` or `discord.mention`. | +| `max_runtime` | yes | Hard limit for the complete run, up to 24h. | +| `filters` | yes | Provider resource filters and the sender allowlist. | +| `inputs` | no | Typed invocation headers. | +| `values` | no | Named expressions. | +| `steps` | yes | One or more ordered inline steps. | ### Inputs and values @@ -176,7 +200,7 @@ prompt: Includes resolve relative to `.paseo/workflows/`, so shared partials use `partials/<name>.md`. Missing files, absolute or traversing paths, symlinks, content mismatches, and files outside the partial tree are rejected. -### Output authority +### Output capabilities Authority stays on the step that uses it: @@ -189,6 +213,8 @@ allow_outputs: Slack workflows use `slack.reply`; Discord workflows use `discord.reply`. The declaration grants `hub.reply`, and the prompt must tell the agent to call it. GitHub has no reply output; use an explicit [`github` block](/docs/hub/github). +Every step receives `hub.finish_execution`. The prompt must tell the agent when to call it; Hub does not append completion or reply instructions. If `output.schema` is present, `hub.finish_execution` requires an `output` value that matches the schema. If an `allow_outputs` entry is `required: true`, the agent must emit that output before finishing. `max` defaults to `1`. + ## Migrating a monolithic file Keep `environments` in `hub.yml`, convert the environment list to a named map, and move each former trigger into its own `.paseo/workflows/<name>.yml` file. Move shared prompt files to `.paseo/workflows/partials/`. Define complete named agent configurations under `agents` and replace dynamic provider fields with finite named-agent selection. diff --git a/public-docs/hub/configuration/index.md b/public-docs/hub/configuration/index.md index f87e0314fe..f579995275 100644 --- a/public-docs/hub/configuration/index.md +++ b/public-docs/hub/configuration/index.md @@ -21,6 +21,8 @@ A project configuration is one versioned bundle: `hub.yml` owns named environments and agents. Each direct-child workflow file owns one trigger and its ordered inline steps. Prompt partials referenced by those workflows live below `workflows/partials/`. Workflow discovery is fixed by convention; there is no manifest or include list. +[single-repo-team-bot](https://github.com/getpaseo/hub/tree/main/examples/single-repo-team-bot) is a complete bundle in this shape: Discord, Slack, and GitHub workflows running a classifier and a worker on shared partials. Copy `.paseo/` into your repository and replace the placeholders its README lists. + ## Sources A configuration comes from one source: diff --git a/public-docs/hub/daemons.md b/public-docs/hub/daemons.md index 2b9ae1ec3c..24ba7b5c69 100644 --- a/public-docs/hub/daemons.md +++ b/public-docs/hub/daemons.md @@ -26,7 +26,9 @@ paseo hub connect `connect` uses the active login to request a single-use enrollment token. The daemon exchanges it for its own relationship credential; your CLI login is never stored as daemon authority. -Each daemon has two identifiers: an immutable generated ID and a friendly slug. Hub normalizes the slug you enter with lowercase words joined by hyphens, so `Build Studio` becomes `build-studio`. The slug is what the dashboard shows and what configuration references. +Hub derives the daemon's initial slug from its hostname. If that slug is already used in the organization, Hub adds a short daemon ID suffix. You can rename the daemon later in Hub. + +Each daemon has two identifiers: an immutable generated ID and a friendly slug. Hub normalizes slugs with lowercase words joined by hyphens, so `Build Studio` becomes `build-studio`. The slug is what the dashboard shows and what configuration references. You can rename the slug later without changing the daemon ID. Renaming after a configuration is active means updating that configuration. @@ -78,11 +80,13 @@ To keep executions off your working tree, add a worktree: ```yaml worktree: mode: branch-off - newBranch: hub/investigation + newBranch: trigger-${{ paseo.execution.id }} base: origin/main ``` -See [Git worktrees](/docs/worktrees) for setup hooks and scripts. +`${{ paseo.execution.id }}` renders the execution's UUID, so every execution gets its own branch off `origin/main`. + +[Environment fields](/docs/hub/configuration/hub-yml#environments) lists what `newBranch` accepts. See [Git worktrees](/docs/worktrees) for setup hooks and scripts. ## What Hub owns diff --git a/public-docs/hub/github.md b/public-docs/hub/github.md index 822082863f..51b9a0e28b 100644 --- a/public-docs/hub/github.md +++ b/public-docs/hub/github.md @@ -46,18 +46,21 @@ The agent can use `git` and `gh` within the declared repositories and permission | `connection` | Project GitHub connection slug. | | `repositories` | Repositories the token can reach. On a GitHub-triggered run, this defaults to the triggering repository. Required for other providers. | | `permissions` | Installation-token permissions such as `contents`, `pull_requests`, and `issues`. Defaults to `contents: read`. | -| `duration` | Token lifetime. Defaults to `1h`, GitHub's maximum. | +| `duration` | Positive token lifetime up to `1h`. Defaults to `1h`, GitHub's maximum. | Requested authority cannot exceed the GitHub App installation. Activation and dispatch fail clearly when the connection, repository, or permissions cannot be resolved. ## Agent environment -Hub supplies `GH_TOKEN` and git configuration through environment variables: +Hub supplies `GH_TOKEN` and process-scoped git configuration through environment variables: -- Commits use the App bot identity. -- `git@github.com` remotes are rewritten to HTTPS. -- Global and system git configuration are ignored. -- The daemon host's git credentials are not used or changed. +- Commits use the App bot login as `user.name` and `<app-id>+<bot-login>@users.noreply.github.com` as `user.email`. +- `git@github.com:` and `ssh://git@github.com/` remotes are rewritten to HTTPS. +- `gh auth git-credential` handles GitHub credentials for the step. +- User-global and system git configuration are ignored, and terminal credential prompts are disabled. +- The daemon host's git identity and credentials are not read or changed. + +`GH_TOKEN` and Hub's git configuration variables are reserved when a step has a `github` block; workflow `env` cannot replace them. ## Keep authority on the worker diff --git a/public-docs/hub/hosted.md b/public-docs/hub/hosted.md index f1c306b420..dd513ef46b 100644 --- a/public-docs/hub/hosted.md +++ b/public-docs/hub/hosted.md @@ -1,6 +1,6 @@ --- title: Hosted Hub -description: The managed Paseo Hub, not available yet. +description: Sign in to the managed Paseo Hub or self-host the same service. nav: Hosted order: 73 category: Hub @@ -8,6 +8,6 @@ category: Hub # Hosted Hub -Not available yet. For now, see [self-hosting](/docs/hub/self-hosting). +[Paseo Hub](https://hub.paseo.sh) is the managed service. Existing accounts can sign in; new account registration is currently closed. -When it ships, Paseo owns the GitHub App, Slack app, and Discord application, so connecting a provider is one click rather than a console walkthrough. Projects, configuration, triggers, daemons, and activity work the same way on both. +Paseo owns the hosted GitHub App, Slack app, and Discord application. Projects, configuration, triggers, daemons, and activity work the same way when you [self-host](/docs/hub/self-hosting). diff --git a/public-docs/hub/quickstart.md b/public-docs/hub/quickstart.md index b2a6ce24b3..51fbfc5958 100644 --- a/public-docs/hub/quickstart.md +++ b/public-docs/hub/quickstart.md @@ -8,20 +8,21 @@ category: Hub # Hub quickstart -## 1. Connect GitHub and a daemon +## 1. Create a project -Open **Connections** in Hub and connect the GitHub account or organization whose repositories you use. On the machine that will run agents: +Open **Connections** in Hub and connect the GitHub account or organization whose repositories you use. Then open **Projects → New project** and create a project. + +## 2. Log in and connect a daemon + +On the machine that will run agents: ```sh paseo hub login https://your-hub.example.com +paseo hub projects paseo hub connect ``` -Approve the browser login. See [Daemons](/docs/hub/daemons) for the separation between your CLI login and the daemon relationship. - -## 2. Create a project - -Open **Projects → New project**. In **Configuration**, choose the repository that will hold the configuration and select **Use for configuration**. +Approve the browser login. `projects` shows the slug needed by `deploy`. See [Daemons](/docs/hub/daemons) for the separation between your CLI login and the daemon relationship. ## 3. Add the bundle @@ -79,9 +80,7 @@ Workflow files are discovered as direct `.yml` children of `.paseo/workflows/`. ## 4. Validate and deploy -Push the files to the configuration repository's default branch. Hub fetches the complete bundle at that commit, validates it, and activates a revision. A failed sync leaves the previous revision active. - -You can validate or deploy the same local bundle from its project root: +From the project root: ```sh paseo hub deploy -p your-project --dry-run @@ -101,3 +100,15 @@ Comment from the account in `from_users`: Open the project's **Activity** tab to see routing and execution. If nothing runs, use the [Activity checklist](/docs/hub/activity). Before widening the allowlist or granting write authority, read [Hub security](/docs/hub/security). + +When you no longer need the local CLI login: + +```sh +paseo hub logout +``` + +Logging out does not disconnect the daemon. When the daemon is connected to the same Hub as the active CLI login, use `paseo hub logout --disconnect-daemon` to remove both relationships. + +## Next + +[single-repo-team-bot](https://github.com/getpaseo/hub/tree/main/examples/single-repo-team-bot) is a complete bundle covering all three providers, with a classifier, a worker, and shared prompt partials. diff --git a/public-docs/hub/self-hosting/github-app.md b/public-docs/hub/self-hosting/github-app.md index 7bb8e9acb9..24820ac893 100644 --- a/public-docs/hub/self-hosting/github-app.md +++ b/public-docs/hub/self-hosting/github-app.md @@ -16,6 +16,8 @@ Go to **Settings → Developer settings → GitHub Apps → New GitHub App** on Replace `hub.example.com` with your `PASEO_HUB_APP_URL`. +The webhook URL must be reachable from GitHub. Keep GitHub's default SSL verification enabled; see [Provider URLs](/docs/hub/self-hosting#provider-urls). + | Setting | Value | | ------------------ | ---------------------------------------------------------- | | Homepage URL | `https://hub.example.com` | diff --git a/public-docs/hub/self-hosting/index.md b/public-docs/hub/self-hosting/index.md index 295b142d17..431cc42e1f 100644 --- a/public-docs/hub/self-hosting/index.md +++ b/public-docs/hub/self-hosting/index.md @@ -20,11 +20,11 @@ Migrations run automatically at startup. Hub does not start listening when a mig Hub has one public URL and one persistent application secret: -| Variable | Purpose | -| ----------------------- | ------------------------------------------------------------------ | -| `PASEO_HUB_APP_URL` | Public origin used by the dashboard, authentication, and callbacks | -| `PASEO_HUB_AUTH_SECRET` | Protects browser sessions and derives execution credentials | -| `DATABASE_URL` | PostgreSQL connection string | +| Variable | Purpose | +| ----------------------- | ---------------------------------------------------------------------------- | +| `PASEO_HUB_APP_URL` | Public origin used by the dashboard, authentication, callbacks, and webhooks | +| `PASEO_HUB_AUTH_SECRET` | Protects browser sessions and derives execution credentials | +| `DATABASE_URL` | PostgreSQL connection string | Generate `PASEO_HUB_AUTH_SECRET` once and keep it across restarts: @@ -89,7 +89,26 @@ docker compose up -d The stack publishes Hub on port `3000` and stores PostgreSQL data in a named volume. The Hub image is `ghcr.io/getpaseo/hub:latest`. -When a reverse proxy terminates HTTPS, set `PASEO_HUB_TRUSTED_CLIENT_IP_HEADER` to the header carrying the original client IP. +### HTTPS with Caddy + +Compose serves plain HTTP on port `3000`. Run Caddy on the same host to terminate TLS: + +```caddyfile +hub.example.com { + reverse_proxy 127.0.0.1:3000 +} +``` + +Point `hub.example.com` at the host and open ports 80 and 443. Caddy [obtains and renews the certificate](https://caddyserver.com/docs/automatic-https). + +Then set in `.env`: + +```dotenv +PASEO_HUB_APP_URL=https://hub.example.com +PASEO_HUB_TRUSTED_CLIENT_IP_HEADER=x-forwarded-for +``` + +To keep port `3000` off the public interface, change the `hub` port in `compose.yml` to `"127.0.0.1:3000:3000"`. ## Fly @@ -122,6 +141,23 @@ fly deploy -a your-hub \ Keep one machine running. Hub holds the Discord gateway connection and dispatches events to daemons, so a stopped machine misses events. +## Provider URLs + +Slack and GitHub call Hub at `PASEO_HUB_APP_URL`: + +| Provider setting | URL | +| ---------------------------- | ------------------------------------------------------ | +| GitHub webhook | `<PASEO_HUB_APP_URL>/webhook` | +| GitHub OAuth callback | `<PASEO_HUB_APP_URL>/api/integrations/github/callback` | +| Slack Events API request URL | `<PASEO_HUB_APP_URL>/api/integrations/slack/events` | +| Slack OAuth callback | `<PASEO_HUB_APP_URL>/api/integrations/slack/callback` | + +Slack requires the Events API request URL to be publicly reachable over HTTPS with a valid certificate, and its [OAuth redirect URL](https://docs.slack.dev/authentication/installing-with-oauth/) must use HTTPS. See Slack's [HTTP request URL](https://docs.slack.dev/apis/events-api/using-http-request-urls/) requirements. + +GitHub must reach the webhook URL and [verifies SSL certificates](https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks#use-https-and-ssl-verification) by default. + +A Hub on `localhost` or a LAN address still runs manual and daemon workflows, but Slack and GitHub cannot reach it. To test provider setup locally, point `PASEO_HUB_APP_URL` at an HTTPS tunnel before starting Hub. A changed tunnel origin requires updated provider settings and a Hub restart. + ## Upgrades Pull the new image or source and deploy it. Migrations are forward-only. Back up PostgreSQL first; it contains configuration revisions, connections, and execution history. diff --git a/public-docs/hub/self-hosting/slack-app.md b/public-docs/hub/self-hosting/slack-app.md index b9264e4ed7..0661c3d29f 100644 --- a/public-docs/hub/self-hosting/slack-app.md +++ b/public-docs/hub/self-hosting/slack-app.md @@ -8,7 +8,7 @@ category: Hub # Slack for Hub -Hub receives Slack mentions over the Events API. Socket Mode is not used, so your Hub must be reachable from Slack. +Hub receives Slack mentions over the Events API. Socket Mode is not used, so your Hub needs a publicly reachable HTTPS origin with a valid certificate. See [Provider URLs](/docs/hub/self-hosting#provider-urls). ## Create the app diff --git a/public-docs/hub/triggers/discord.md b/public-docs/hub/triggers/discord.md index 6435680627..a5b46b4972 100644 --- a/public-docs/hub/triggers/discord.md +++ b/public-docs/hub/triggers/discord.md @@ -34,7 +34,7 @@ steps: - { type: discord.reply, max: 1, required: true } ``` -Turn on Discord Developer Mode to copy IDs. `from_users` matches the author; `guild` and `channels` constrain where the mention arrived. All filters must pass. +Turn on Discord Developer Mode to copy IDs. `from_users` matches the author; `guild` and `channels` constrain where the mention arrived. `pattern` is a required prefix after the mention; `contains` is its legacy alias and has the same prefix behavior. All filters must pass. The reply posts in the triggering thread or channel. `discord.reply` grants `hub.reply`, but does not rewrite the prompt. A Discord trigger grants no GitHub credential; add a [`github` block](/docs/hub/github) to the step that needs one. diff --git a/public-docs/hub/triggers/index.md b/public-docs/hub/triggers/index.md index 819c16a7c5..565f29aabf 100644 --- a/public-docs/hub/triggers/index.md +++ b/public-docs/hub/triggers/index.md @@ -66,8 +66,8 @@ An allowlist is one layer of defense. It does not make a permitted account trust | `workspace` | Slack | Team id, `T01234567` | | `guild` | Discord | Guild id | | `channels` | Slack, Discord | Channel ids | -| `contains` | all | Substring of the message text | -| `pattern` | all | Prefix of the message text | +| `contains` | all | GitHub substring; Slack and Discord invocation prefix | +| `pattern` | all | Invocation prefix | | `connection` | all | A connection slug, when the organization has several | All conditions must pass. There is no `any` mode. diff --git a/public-docs/hub/triggers/slack.md b/public-docs/hub/triggers/slack.md index 11bb7301eb..b6e24c6e7c 100644 --- a/public-docs/hub/triggers/slack.md +++ b/public-docs/hub/triggers/slack.md @@ -34,7 +34,7 @@ steps: - { type: slack.reply, max: 1, required: true } ``` -Slack filters use IDs, not display names. `from_users` matches the author, `workspace` the team, and `channels` the channel. `pattern` checks the beginning of text after the mention; `contains` checks a substring. All filters must pass. +Slack filters use IDs, not display names. `from_users` matches the author, `workspace` the team, and `channels` the channel. `pattern` is a required prefix after the mention; `contains` is its legacy alias and has the same prefix behavior. All filters must pass. The reply posts in the triggering thread. A root message gets a thread; a threaded message stays there. `slack.reply` grants `hub.reply`, but does not add reply instructions to the prompt. diff --git a/public-docs/hub/workflows.md b/public-docs/hub/workflows.md index 5e85ea89ba..74787fa557 100644 --- a/public-docs/hub/workflows.md +++ b/public-docs/hub/workflows.md @@ -2,7 +2,7 @@ title: Hub workflows description: Build ordered Hub workflows with prompts, routing, outputs, and provider authority. nav: Workflows -order: 65 +order: 64 category: Hub --- @@ -55,6 +55,8 @@ A finite input can select among complete named environments: name: route-repository on: manual.run max_runtime: 1h +filters: + from_users: [automation] inputs: repo: type: string @@ -94,6 +96,8 @@ For dynamic routing, select complete named agents from a finite expression: name: route-agent on: manual.run max_runtime: 1h +filters: + from_users: [automation] inputs: agent: type: string @@ -192,6 +196,8 @@ Steps run in file order. `if` may read inputs, values, and prior step output: name: conditional-review on: manual.run max_runtime: 1h +filters: + from_users: [automation] steps: - id: inspect environment: paseo diff --git a/public-docs/metadata-generation.md b/public-docs/metadata-generation.md index f9e7a7c218..1d23ec661d 100644 --- a/public-docs/metadata-generation.md +++ b/public-docs/metadata-generation.md @@ -37,9 +37,13 @@ Each default is a match on the model id or name, so the first enabled provider t The intent of the default order is to prefer small, fast, cheap models for these short tasks before falling back to whatever you have selected. -## Configuring the providers +## Choose a model -To control which models Paseo uses — for example to keep all metadata generation on one provider, or to prefer a local model — set `agents.metadataGeneration.providers` in `~/.paseo/config.json`. Your entries are tried before the built-in defaults. +Open **Settings → Host → Metadata** and select **Automatic** or **Manual**. In Manual mode, Paseo tries the model you choose first, then falls back to the remaining configured models and built-in candidates. Choosing a model replaces the first entry in `agents.metadataGeneration.providers` and preserves the rest of the list. + +## Configure a custom fallback order + +To configure more than one preferred model or control the exact order, set `agents.metadataGeneration.providers` in `~/.paseo/config.json`. Your entries are tried before the built-in defaults. ```json { @@ -60,7 +64,7 @@ Each entry accepts: - `model` (optional) — a specific model id. Omit it to use that provider's default model. - `thinkingOptionId` (optional) — a reasoning/thinking level for models that support one. Falls back to the model's default if the value isn't valid for that model. -Restart the daemon after editing the file. +The Settings screen replaces only the first entry and preserves the rest of a custom list. Restart the daemon after editing the file directly. ## Per-project instructions diff --git a/public-docs/skills.md b/public-docs/skills.md index 1c4bd6f86c..2cdeaf03ad 100644 --- a/public-docs/skills.md +++ b/public-docs/skills.md @@ -42,17 +42,6 @@ The receiving agent gets a self-contained briefing with the task, context, relev /paseo-handoff hand this to claude opus for review ``` -## `/paseo-loop`, Iterative Loops - -Runs an agent loop until an exit condition is met. Use it when you say "loop", "babysit", "keep trying until", "check every X", "watch", or want iterative autonomous execution. - -A loop is a worker/verifier cycle: launch a worker, check verification, repeat until done or limits hit. It can use a shell check, a verifier prompt, or both. Set a sensible `--max-iterations` or `--max-time`. - -``` -/paseo-loop keep trying until the changed test file passes, max 5 iterations -/paseo-loop babysit PR 123 until checks are green, check every 2m, max-time 1h -``` - ## `/paseo-committee`, Committee Planning Forms a committee of two high-reasoning agents to step back, do root cause analysis, and produce a plan. Use it when stuck, looping, tunnel-visioning, or facing a hard planning problem. diff --git a/public-docs/troubleshooting.md b/public-docs/troubleshooting.md index 70630e1ee9..0512a7aa3d 100644 --- a/public-docs/troubleshooting.md +++ b/public-docs/troubleshooting.md @@ -24,6 +24,15 @@ Open **Settings → your host → Providers**, tap the provider, then tap **Diag - **Daemon PATH** — the `PATH` Paseo is searching. Compare it to `echo $PATH` in a fresh terminal. - **Version** — whether the binary actually runs. +From a terminal or agent, request the same diagnostic from the affected daemon: + +```bash +paseo provider diagnostic <provider> +paseo provider diagnostic <provider> --host <host:port> --json +``` + +Use `--host` when the affected daemon is not the CLI's default local daemon. + `not found` together with a **Daemon PATH** that's missing your binary's directory is the common case: that directory is on your terminal's `PATH` but not on Paseo's. ### Fix it diff --git a/public-docs/worktrees.md b/public-docs/worktrees.md index db43e21c11..3302233851 100644 --- a/public-docs/worktrees.md +++ b/public-docs/worktrees.md @@ -257,6 +257,7 @@ Services additionally get: ```bash paseo workspace ls paseo run --workspace <workspace-id> "implement auth" +paseo workspace rename <workspace-id> "Auth rework" paseo workspace archive <workspace-id> ``` diff --git a/runtimes/fixture/package.json b/runtimes/fixture/package.json index 61143f2824..28de98c844 100644 --- a/runtimes/fixture/package.json +++ b/runtimes/fixture/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/fixture-workspace-runtime", - "version": "0.3.1", + "version": "0.4.0", "private": true, "description": "External command-runtime contract fixture", "bin": { @@ -16,8 +16,8 @@ "typecheck": "node --check src/index.mjs" }, "dependencies": { - "@getpaseo/workspace-helper": "0.3.1", - "@getpaseo/workspace-runtime-contract": "0.3.1", + "@getpaseo/workspace-helper": "0.4.0", + "@getpaseo/workspace-runtime-contract": "0.4.0", "node-pty": "1.2.0-beta.15" } } diff --git a/scripts/dev-app.sh b/scripts/dev-app.sh index be9857af65..a594f344fd 100755 --- a/scripts/dev-app.sh +++ b/scripts/dev-app.sh @@ -12,6 +12,7 @@ configure_dev_paseo_home EXPO_PORT="${EXPO_PORT:-8081}" DAEMON_ENDPOINT="$(resolve_dev_daemon_endpoint)" +DEV_BUILD_LABEL="$(git -C "$ROOT_DIR" branch --show-current 2>/dev/null || true)" echo "══════════════════════════════════════════════════════" echo " Paseo App Dev" @@ -24,5 +25,6 @@ echo "════════════════════════ exec cross-env \ BROWSER="${BROWSER:-none}" \ APP_VARIANT=development \ + EXPO_PUBLIC_PASEO_DEV_BUILD_LABEL="$DEV_BUILD_LABEL" \ EXPO_PUBLIC_LOCAL_DAEMON="$DAEMON_ENDPOINT" \ npm run start:expo --workspace=@getpaseo/app -- --port "$EXPO_PORT" diff --git a/scripts/dev.ps1 b/scripts/dev.ps1 index 5b390407b3..0e3be90c91 100644 --- a/scripts/dev.ps1 +++ b/scripts/dev.ps1 @@ -50,6 +50,7 @@ $env:PASEO_CORS_ORIGINS = "*" # Configure the app to auto-connect to this daemon on localhost $env:APP_VARIANT = "development" $env:EXPO_PUBLIC_LOCAL_DAEMON = "localhost:6768" +$env:EXPO_PUBLIC_PASEO_DEV_BUILD_LABEL = (git branch --show-current).Trim() $env:PASEO_LISTEN = "127.0.0.1:6768" $env:BROWSER = "none" diff --git a/skills/paseo-advisor/SKILL.md b/skills/paseo-advisor/SKILL.md index cca43f6ee4..94d4558e5a 100644 --- a/skills/paseo-advisor/SKILL.md +++ b/skills/paseo-advisor/SKILL.md @@ -2,7 +2,7 @@ name: paseo-advisor description: Spin up a single agent as an advisor — second opinion on the current task. Use when the user says "advisor", "second opinion", "what does X think", or wants an outside take without delegating the work itself. user-invocable: true -argument-hint: "[--provider <name>] <question or topic>" +argument-hint: "[--profile <name>] <question or topic>" --- # Paseo Advisor @@ -13,16 +13,15 @@ Single agent. Reads the situation you're in. Gives a judgment. You decide what t ## Prerequisites -Read the **paseo** skill. Before choosing a provider, read `~/.paseo/orchestration-preferences.json` unless the user explicitly named a provider in this request. Do not create the advisor until you have read it. +Read the **paseo** skill. Call `list_profiles` before choosing the advisor. Do not create the advisor until you have read the configured profiles and their `notes`. ## Picking the advisor -1. **User named one** (`--provider claude/opus`) → use it. -2. **Otherwise** resolve from preferences — pick the category that matches the question: - - Design / approach question → `planning` - - "Did I miss something" review → `audit` - - "Is this even right" → `research` -3. **Contrast helps.** If your own provider matches what preferences would pick, swap to a different family on purpose — fresh perspective is the point. +1. **User named a profile** (`--profile UI Work`) → select it by name. +2. **Otherwise** choose the profile whose `notes` best fit the question. Match the actual work: design and approach, audit and review, or research and root-cause analysis. +3. **Contrast helps.** When several profiles fit, prefer a different provider family from your own so the second opinion is genuinely fresh. + +Materialize the selected profile into `create_agent` as described by the **paseo** skill. If no profile fits, use Paseo's provider discovery fallback. ## The briefing diff --git a/skills/paseo-committee/SKILL.md b/skills/paseo-committee/SKILL.md index 274cf4e8f1..2b16189556 100644 --- a/skills/paseo-committee/SKILL.md +++ b/skills/paseo-committee/SKILL.md @@ -6,26 +6,24 @@ user-invocable: true # Committee Skill -Two agents from contrasting providers, fresh context, planning a solution in parallel. They stay alive for review after implementation. - -The purpose is to step back, not double down. The committee may propose a completely different approach. +Two agents from contrasting profiles, fresh context, planning a solution in parallel. **User's additional context:** $ARGUMENTS ## Prerequisites -Read the **paseo** skill. Before choosing committee members, read `~/.paseo/orchestration-preferences.json` unless the user explicitly named providers in this request. Do not create committee agents until you have read it. +Read the **paseo** skill. Call `list_profiles` before choosing committee members. Do not create committee agents until you have read the configured profiles and their `notes`. -Contrast is the point of a committee, so pick across providers deliberately using the configured preferences rather than hardcoded defaults. +Contrast is the point of a committee, so pick profiles from different provider families when possible. Materialize each profile into `create_agent`. ## Composition -Two members with different reasoning styles, selected from orchestration preferences: +Two members with different reasoning styles, selected from configured Agent profiles: -- one planning/research-strength provider -- one contrasting high-reasoning provider +- one whose notes fit planning, research, or root-cause analysis +- one contrasting high-reasoning profile from another provider family -Override only when the user explicitly asks for different members. +If the user names profiles, use those. If fewer than two suitable profiles are configured, use Paseo's provider discovery fallback for the missing member and tell the user. Override the selection only when the user explicitly asks for different members. ## Hard rules @@ -35,49 +33,14 @@ Override only when the user explicitly asks for different members. This is analysis only. Do NOT edit, create, or delete any files. Do NOT write code. ``` -- **Trust the wait.** Do not poll, send hurry-ups, or interrupt. GPT-5.4 can reason 15–30 minutes; Opus does extended thinking. Long waits mean it found something worth thinking about. -- **You are the middleman.** Drive plan → implement → review without yielding to the user, except for divergences that need their call. +- **Trust the finish notification.** Do not poll, send hurry-ups, or interrupt. Models can reason for 15–30 minutes. You can go idle and Paseo will notify you. -## Phase 1: Plan +## Workflow -Write a problem-level prompt: +1. Write a problem-level prompt +2. Create both agents in parallel via Paseo with `[Committee] <task>` titles and the same prompt +3. Wait for both responses +4. Resolve disagreements by passing their arguments between each other +5. Keep going until they converge into a response -- High-level goal and acceptance criteria -- Constraints -- Symptoms (if a bug) -- What you tried and why it failed -- Explicit: "do root cause analysis" -- Explicit: "state assumptions, ask why three levels deep, check whether you're patching a symptom or removing the problem" - -Create both agents in parallel via Paseo with `[Committee] <task>` titles and the same prompt. Wait for both — not just whichever finishes first. - -Read both responses. Challenge them — do not accept at face value: - -- "Why does <underlying thing> happen? Symptom or cause?" -- Verify any assumption the plan makes about the code. -- "What did you considered and reject?" - -Send follow-ups until the plan addresses root cause. - -Synthesize: - -- Convergence → unified plan. -- Significant divergence → involve the user. - -Confirm the merged plan with both members. Multi-turn until consensus. - -## Phase 2: Implement - -Default: implement yourself. If the user said **"delegate"**, launch one impl agent and pass the merged plan. - -The committee stays clean — not involved in implementation. - -## Phase 3: Review - -Send the diff to the committee: - -> Implementation is done. Review changes against the plan. Flag drift or missing pieces. <no-edits suffix> - -Apply feedback yourself, or send to the impl agent. Repeat 2 → 3 until consensus. - -After ~10 iterations without convergence, start a fresh committee with the full history of what was tried — the current committee's context may have drifted too far. +Share the consensus with the user. Summarize where the agents diverged and how they resolved it. diff --git a/skills/paseo-handoff/SKILL.md b/skills/paseo-handoff/SKILL.md index 102297544b..b321365306 100644 --- a/skills/paseo-handoff/SKILL.md +++ b/skills/paseo-handoff/SKILL.md @@ -12,11 +12,11 @@ Transfer the current task — context, decisions, failed attempts, constraints ## Prerequisites -Read the **paseo** skill. Before choosing a provider, read `~/.paseo/orchestration-preferences.json` unless the user explicitly named a provider in this request. Do not create the receiving agent until you have read it. +Read the **paseo** skill. Call `list_profiles` before choosing the receiving agent. Do not create it until you have read the configured profiles and their `notes`. ## Parsing arguments -1. **Provider** — explicit user request first; otherwise resolve from `impl` preference (or `ui` if the task is styling-only). +1. **Agent profile** — explicit profile name first; otherwise choose the profile whose `notes` best match the work. Materialize it into `create_agent` as described by the **paseo** skill. If no profile fits, use Paseo's provider discovery fallback. 2. **Isolation** — "in a worktree" / "worktree" → create a workspace with `isolation: "worktree"`, using a short branch name derived from the task. 3. **Task description** — anything else the user said. @@ -62,6 +62,4 @@ Prepare the handoff in a dedicated workspace: Do not encode independence as a create mode and do not invoke CLI or wire-level detach operations. Detach is a user gesture in the subagents track. -Leave `notifyOnFinish` omitted unless the user explicitly wants no callback. - -Don't wait by default — the user decides whether to follow along or move on. Tell them the agent ID and how to follow along (the paseo skill explains). +Do not wait or poll for the agent to finish. diff --git a/skills/paseo-help/SKILL.md b/skills/paseo-help/SKILL.md new file mode 100644 index 0000000000..951c1207bc --- /dev/null +++ b/skills/paseo-help/SKILL.md @@ -0,0 +1,92 @@ +--- +name: paseo-help +description: Answer questions about the Paseo product and app, including setup, configuration, connectivity, providers, workspaces, updates, logs, and troubleshooting. Use when a user inside Paseo asks how Paseo works, how to configure it, or why something is broken; use the paseo skill instead to operate agents and workspaces through MCP or the CLI. +--- + +# Paseo Help + +You are helping a user understand, configure, or troubleshoot Paseo itself. Answer their question directly, verify the answer against the current public documentation, and include the relevant documentation link. Do not send the user away to read the docs in place of helping them. + +**User's question:** $ARGUMENTS + +## Use current documentation + +Fetch [https://paseo.sh/llms.txt](https://paseo.sh/llms.txt) first. It is the current index of Paseo documentation, with a description and Markdown URL for each page. + +Use that index to select the page that owns the user's question, then fetch the linked `.md` page before answering. For troubleshooting, begin with [Common problems](https://paseo.sh/docs/troubleshooting.md) and follow its links when the issue belongs to a more specific page. + +Prefer the deployed docs over memory. Answer the user directly, then link the relevant `.md` page as supporting documentation. + +## Establish the topology first + +Identify the daemon involved before diagnosing versions, paths, providers, logs, updates, or connectivity. Do not infer the daemon from the client: Paseo Desktop can manage its bundled local daemon and connect to other remote daemons at the same time. + +Establish two facts: + +1. **Where and how the daemon runs** + - **Desktop-managed:** Paseo Desktop bundles, starts, and updates a daemon on that computer. No separate daemon install is required. + - **Standalone:** the daemon was installed separately, commonly through the npm CLI, and runs independently of the desktop app. + - **Docker:** the daemon, its home, provider CLIs, credentials, and code mounts live in the container runtime. +2. **How the affected client reaches it** + - same-machine local connection + - relay connection + - direct LAN, VPN, or Tailscale connection + - daemon-served web UI + +Use **Settings → About** to compare the app version with each connected host. For the affected host, open **Settings → your host → Overview → Full status**. On the daemon machine, `paseo daemon status --json` reports facts such as server ID, hostname, version, home, listen address, process owner, log path, and whether the daemon is desktop-managed. + +Record which host the user is viewing and which machine or container runs it. A local `paseo daemon status` describes the daemon for that CLI's local `PASEO_HOME`; it may not be the remote host visible in the app. + +Apply later checks to the daemon runtime, not automatically to the client device: + +- Provider binaries, credentials, `PATH`, workspaces, config, and daemon logs live on the daemon machine or inside its container. +- App version and app logs live on the client device. +- A desktop-managed daemon follows the Desktop app lifecycle and update path. +- A standalone daemon follows its own CLI/npm lifecycle and may use a different `PASEO_HOME` or listen address. +- A Docker daemon uses container paths, volumes, user permissions, image versions, and container lifecycle commands. + +## Diagnose before changing state + +After identifying the affected host, compare that daemon's version with the client app version. Ask the user to update both through the correct topology-specific update path. Old versions and app/daemon version skew cause many apparent bugs, and fixes ship frequently. Use the Updates page and the installation-specific docs for current instructions. + +Use the smallest relevant read-only checks: + +```bash +paseo --version +paseo daemon status --json +paseo provider diagnostic <provider> --json +``` + +Use the status-reported home, listen address, and log path for further checks. Probe `http://127.0.0.1:6767/api/health` or read `~/.paseo/daemon.log` only when those values match the affected daemon. Do not restart the daemon, edit config, update software, or expose a network listener without the user's explicit permission. A daemon restart can interrupt the agent doing the diagnosis. + +For a missing provider or `command not found`, run `paseo provider diagnostic <provider>` against the affected host, or open **Settings → your host → Providers → provider → Diagnostic**. Compare its resolved binary, daemon `PATH`, and provider version with a brand-new login shell. Shell aliases and functions are not executable paths. + +## Logs and local files + +Use these defaults on the machine where the daemon or Desktop app actually runs. Do not look for a remote daemon's files on the client device. + +- Daemon config: `~/.paseo/config.json` +- Daemon log: `~/.paseo/daemon.log` +- Agent state directory: `~/.paseo/agents/` +- Default managed worktree root: `~/.paseo/worktrees/` +- macOS desktop log: `~/Library/Logs/Paseo/main.log` +- Linux desktop log: `~/.config/Paseo/logs/main.log` +- Windows desktop log: `%APPDATA%\Paseo\logs\main.log` + +Substitute the status-reported `PASEO_HOME` for `~/.paseo`. In the official Docker image, the default is `/home/paseo/.paseo`; its host path depends on the volume mount, and container stdout is available through Docker. Desktop app logs describe the Desktop process; daemon logs describe the selected daemon. Read the narrowest useful slice and redact credentials, pairing offers, tokens, passwords, and user code before sharing logs. + +If diagnosing the bundled daemon on a computer with Paseo Desktop installed, but `paseo` is not on `PATH`, the bundled CLI is at: + +- macOS: `/Applications/Paseo.app/Contents/Resources/bin/paseo` +- Linux: `<install-dir>/resources/bin/paseo` +- Windows: `C:\Program Files\Paseo\resources\bin\paseo.cmd` + +Offer to fix the PATH or symlink; do not change shell configuration silently. + +## Escalate with evidence + +If the current docs and diagnostics do not resolve the problem, collect the app and daemon versions, OS, install method, connection method, exact error, minimal reproduction, and a small redacted log excerpt. + +- Bugs: [GitHub Issues](https://github.com/getpaseo/paseo/issues) +- Questions and quick help: [Paseo Discord](https://discord.gg/jz8T2uahpH) +- Product workflow discussions: [GitHub Discussions](https://github.com/getpaseo/paseo/discussions) or `#product` in Discord diff --git a/skills/paseo-loop/SKILL.md b/skills/paseo-loop/SKILL.md deleted file mode 100644 index 2ee7be549e..0000000000 --- a/skills/paseo-loop/SKILL.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -name: paseo-loop -description: Run an agent loop until an exit condition is met. Use when the user says "loop", "babysit", "keep trying until", "check every X", "watch", or wants iterative autonomous execution. -user-invocable: true ---- - -# Paseo Loop Skill - -A loop is a worker/verifier cycle: launch a worker → check verification → repeat until done or limits hit. Use for "keep trying", "babysit", or "watch this until X." - -For lightweight recurring checks that should return to this same conversation, prefer a cron heartbeat (`create_heartbeat`, then `delete_heartbeat` when finished). To change it, delete and recreate it. Use a loop when each iteration needs a worker/verifier lifecycle; use a schedule when each firing should create a fresh independent agent and workspace. - -**User's arguments:** $ARGUMENTS - -## Prerequisites - -Read the **paseo** skill. Before choosing worker or verifier providers, read `~/.paseo/orchestration-preferences.json` unless the user explicitly named providers in this request. Do not start the loop until you have read it. - -Loops are a CLI primitive: `paseo loop run`. Manage with `paseo loop ls`, `paseo loop inspect <id>`, `paseo loop logs <id>`, `paseo loop stop <id>`. - -## Your job - -1. Understand the user's intent from `$ARGUMENTS` and the conversation. -2. **Worker prompt** — self-contained, concrete about what to do this iteration, explicit about what counts as progress. -3. **Verification** — pick the right shape: - - Shell check (`--verify-check`) for objective criteria a command can answer (`gh pr checks --fail-fast`, `npm test`). - - Verifier prompt (`--verify`) for judgment ("Return done=true only if all tests pass and the changed files are coherent. Cite the command and the outcome."). - - Both, when shell rules out the obvious failures and the verifier judges the rest. -4. **Providers** — `--provider` for the worker, `--verify-provider` for the verifier. From preferences unless the user named them. For implementation loops, pair worker and verifier on different providers — each catches the other's blind spots. -5. **Sleep** — `--sleep` only when polling something external. Otherwise let it run as fast as the loop completes. -6. **Stops** — set a sensible `--max-iterations` and/or `--max-time`. Open-ended loops are how runaways happen. -7. **Archive** — `--archive` keeps agents after each iteration for inspection. -8. Launch with `paseo loop run`. - -## Common shapes - -**Babysit a PR** — worker checks PR state and fixes issues; shell check is `gh pr checks <n> --fail-fast`; sleep 2m; max-time 1h. - -**Drive tests to green** — worker investigates failures and fixes code; shell check is the test command; verifier confirms all tests pass; max-iterations 10. - -**Cross-provider implementation** — worker on `impl` provider, verifier on a different provider; verifier checks changed files, runs typecheck and tests; max-iterations and max-time both bounded; archive on so iterations can be inspected. - -## Prompt rules - -**Worker** — self-contained, concrete (commands, files, branches, tests, PRs, systems), explicit about what counts as progress this iteration. - -**Verifier** — checks facts, doesn't suggest fixes, cites commands/outputs/file evidence, specific about what "done" means. diff --git a/skills/paseo/SKILL.md b/skills/paseo/SKILL.md index 428217ecb0..8d686fb35c 100644 --- a/skills/paseo/SKILL.md +++ b/skills/paseo/SKILL.md @@ -3,7 +3,7 @@ name: paseo description: Paseo reference for managing workspaces, workspace scripts, agents, schedules, and heartbeats. --- -Paseo is a daemon that supervises AI coding agents on your machine. Control it through tools or a CLI. +Paseo is a remote daemon that manages coding agents, terminals. Control it through MCP tools or the CLI. ## Workspaces @@ -13,7 +13,7 @@ Paseo is a daemon that supervises AI coding agents on your machine. Control it t **`archive_workspace`** — `{ workspaceId }`. Archives the workspace, its agents, and its terminals. Local directories remain; Paseo removes an owned worktree only after its final active workspace reference is archived. -Worktree creation and reference accounting are implementation details of `isolation: "worktree"`. +**`rename_workspace`** — `{ workspaceId, name }`. Rename workspace. ## Workspace scripts @@ -37,7 +37,7 @@ paseo script stop <name> [--cwd <path> | --workspace <workspace-id>] **`create_agent`** — required: `title`, `provider` (`claude/opus`, `codex/gpt-5.4`, …), `initialPrompt`. Optional: `workspaceId`, `notifyOnFinish`, `settings`, `labels`. Returns `{ agentId, workspaceId, … }`. -Initial runtime settings live under `settings`: `modeId`, `thinkingOptionId`, and provider-specific `features`. For Codex fast mode, pass `settings: { features: { "fast_mode": true } }` when creating the agent. +Initial runtime settings live under `settings`: `modeId`, `thinkingOptionId`, and provider-specific `features`. Agent profiles are the preferred source for these values. For Codex fast mode, pass `settings: { features: { "fast_mode": true } }` when creating the agent. Agent-scoped creation always creates your subagent. Omit `workspaceId` to use your current workspace; pass a workspace returned by `create_workspace` for isolated delegation. Placement never changes parentage. @@ -53,7 +53,20 @@ Agent-scoped `create_agent` defaults `notifyOnFinish` to true. Set it to `false` **`archive_agent`** — `{ agentId }`. Interrupts if running, removes from active list. -## Provider discovery +## Agent profiles and provider discovery + +**`list_profiles`** — named launch bundles configured by the human. Before choosing how to launch a delegated agent, call this tool and read every profile's `notes`. Pick a named profile the user requested, or the profile whose notes best match the work. + +There is no `profile` parameter on `create_agent`. Materialize the selected profile into the call: + +- combine `provider` and `model` as the `provider/model` value for `create_agent.provider` +- copy `modeId` to `settings.modeId` +- copy `thinkingOptionId` to `settings.thinkingOptionId` +- copy `featureValues` to `settings.features` + +Omit absent values. Do not remember a selected profile or infer drift later; a profile is only launch configuration. + +If no profile fits, or no profiles are configured, use the provider discovery tools below rather than guessing. Tell the user when you fall back because no configured profile fits. **`list_providers`** — compact provider availability and modes. @@ -73,36 +86,6 @@ Only set feature IDs returned by `inspect_provider`. For Codex fast mode, look f Schedules have the full list/inspect/update/pause/resume/run-once/log/delete surface. Heartbeats deliberately do not. -## Orchestration preferences - -User-specific configuration at `~/.paseo/orchestration-preferences.json`. **Before any Paseo skill chooses a provider or creates an agent, it must read this file.** Reading means an actual file read, not relying on these examples or defaults. Never hardcode a provider string in another skill — resolve through this file. - -Two parts: - -- `providers` — map of role categories to provider strings. Pass straight to `create_agent`'s `provider` field. -- `preferences` — freeform string array. Read on startup; weave into agent prompts contextually. - -Categories: `impl`, `ui`, `research`, `planning`, `audit`. Skills pick the category that matches the role they're launching. - -```json -{ - "providers": { - "impl": "codex/gpt-5.4", - "ui": "claude/opus", - "research": "codex/gpt-5.4", - "planning": "codex/gpt-5.4", - "audit": "codex/gpt-5.4" - }, - "preferences": [ - "Claude Opus is the right choice for anything artistic or human-skill-oriented: copywriting, naming, UX copy, visual design, styling. Codex is the workhorse for mechanical work." - ] -} -``` - -If the file is missing, use sensible defaults and tell the user once. - -All providers shown in this document are examples, you must resolve real providers either found in the preferences or by calling Paseo provider tooling to find a valid provider. - ## Waiting Agents take time — 10–30+ minutes is routine. Favor asynchronous workflows. @@ -129,32 +112,4 @@ paseo heartbeat create --cron "*/15 * * * *" "check the build" Discover with `paseo --help` and `paseo <cmd> --help`. -**If `paseo` isn't on PATH but the desktop app is installed**, the bundled CLI is at: - -- macOS: `/Applications/Paseo.app/Contents/Resources/bin/paseo` -- Linux: `<install-dir>/resources/bin/paseo` -- Windows: `C:\Program Files\Paseo\resources\bin\paseo.cmd` - -The desktop app's first-run hook (`installCli`) symlinks this to `~/.local/bin/paseo` (macOS/Linux) or drops a `.cmd` trampoline (Windows) and adds `~/.local/bin` to PATH via shell rc files. If that didn't take, offer to symlink it — don't do it silently. - -## Ops and debugging - -Daemon-client architecture: the daemon owns agent lifecycle, state, and the WebSocket API. Tools, CLI, mobile, and desktop apps are all clients. - -| | Default | -| -------------- | --------------------------------------------------------------- | -| Listen address | `127.0.0.1:6767` (override `PASEO_LISTEN`) | -| Home | `~/.paseo` (override `PASEO_HOME`) | -| Daemon log | `$PASEO_HOME/daemon.log` | -| Agent state | `$PASEO_HOME/agents/<id>.json` | -| Worktrees | `$PASEO_HOME/worktrees/` (or `worktrees.root` in `config.json`) | -| PID file | `$PASEO_HOME/paseo.pid` | -| Health | `GET http://127.0.0.1:6767/api/health` | - -Debug order: - -1. `tail -n 200 ~/.paseo/daemon.log`. -2. `paseo daemon status` for liveness. -3. `curl -s localhost:6767/api/health` if the CLI itself is suspect. - -**Never restart the daemon without explicit user approval** — it kills every running agent, including, often, the one asking. +For product questions, setup, logs, version problems, or troubleshooting, use the **paseo-help** skill.