diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e49eec295e..7b6e4ea7f5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -138,3 +138,19 @@ updates: update-types: - minor - patch + + # Studio (the Node web client) — npm. + - package-ecosystem: npm + directory: "/studio" + schedule: + interval: weekly + open-pull-requests-limit: 5 + commit-message: + prefix: "chore(deps)" + labels: + - dependencies + groups: + npm-minor-patch: + update-types: + - minor + - patch diff --git a/.github/workflows/studio.yml b/.github/workflows/studio.yml new file mode 100644 index 0000000000..f89b9c1377 --- /dev/null +++ b/.github/workflows/studio.yml @@ -0,0 +1,90 @@ +name: Studio + +on: + pull_request: + paths: + - "studio/**" + - ".github/workflows/studio.yml" + push: + branches: [main] + paths: + - "studio/**" + - ".github/workflows/studio.yml" + +permissions: + contents: read + +jobs: + checks: + name: Build, test, lint, typecheck, audit + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: studio/.nvmrc + cache: npm + cache-dependency-path: studio/package-lock.json + + # No package in the tree needs install scripts (Next/sharp ship prebuilt + # binaries), so scripts stay off — supply-chain surface CI never runs. + - name: Install + working-directory: studio + run: npm ci --ignore-scripts + + - name: Lint, typecheck, dead-code + working-directory: studio + run: | + npm run lint + npm run typecheck + npm run knip + + - name: Unit tests + working-directory: studio + run: npx vitest run + + - name: Build + hermetic server-tier suite + working-directory: studio + run: npm run test:server + + - name: Audit + working-directory: studio + run: npm audit --audit-level=high + + - name: License headers + run: | + if git grep -l "SPDX-License-Identifier: Proprietary" -- studio/; then + echo "Proprietary SPDX headers are not allowed in studio/" >&2 + exit 1 + fi + + # Browser smoke over the real proxy tier against the fixture daemon. + # Separate job so a browser-infra flake never masks the checks above. + e2e: + name: Playwright (fixture daemon) + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: studio/.nvmrc + cache: npm + cache-dependency-path: studio/package-lock.json + + - name: Install + working-directory: studio + run: | + npm ci --ignore-scripts + npx playwright install --with-deps chromium + + - name: Build and test + working-directory: studio + run: npm run test:e2e diff --git a/.gitignore b/.gitignore index df9f60f552..735de8ed07 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,9 @@ coverage.* # Personal, local-only Claude Code instructions and running-state notes /CLAUDE.local.md /HANDOFF.md + +# Studio managed-mode local state (controller-pinned skills/memory dirs). +/.mecatl/ + +# Ad-hoc local attachment drops. +/attachments/ diff --git a/.matlatlignore b/.matlatlignore index c6803bde63..05b7a3dfc9 100644 --- a/.matlatlignore +++ b/.matlatlignore @@ -49,3 +49,10 @@ user-docs/ # website/CLAUDE.md is agent guidance for the Docusaurus site, not product docs. website/CLAUDE.md + +# Studio's managed-mode working state (the controller pins the project skills +# and memory dirs here) and ad-hoc local attachment drops — machine-local +# state, not documentation. CI checkouts never contain them; ignoring them +# keeps local `check`/`index` runs byte-identical to CI's. +/.mecatl/ +/attachments/ diff --git a/AGENTS.md b/AGENTS.md index 152238a17a..d59b6a80b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,6 +81,7 @@ the opt-in `provider/*` submodules (ADR 0093), and the root module all move in l - `contracts/proto/mecatl/v1/` — gRPC contract (source of truth); `contracts/proto/mecatl/driver/v1/` — the driver protocol (SessionStoreService/MemoryStoreService stores; SkillSourceService/SoulSourceService/AgentSourceService/CommandSourceService content sources) a remote driver process implements; `contracts/gen/` is generated, **never hand-edit**. - `cmd/mecated/` — standalone server (composition root): flags, TLS/auth/rate-limit, HTTP + metrics listeners. `cmd/mecademo/` — the offline demo. `cmd/mecatequi/` — single-shot HEADLESS composition root (peer of mecademo over `app.Build`): one prompt → a git-diff patch + a JSON Summary + an optional JSONL log + an exit code. It is **FORGE-AGNOSTIC** — knows nothing about GitHub; the glue that turns an issue into a PR lives ONLY in `.github/` + shell, NEVER the binary or `engine/`, and keeps a **split-privilege token boundary** (the agent job holds NO GitHub write token; the publish job runs NO agent code, applies the patch as DATA). See `docs/adr/0028-mecatequi.md`. `cmd/mecak8s/` — storage-free k8s-native agent (ADR 0048), a thin peer of mecated that composes `app.Build` with k8s-native defaults (Redis store + k8s lease + drain gate); no PVC, no local state — state is a managed service (Redis + k8s API server). The four real-provider mains share credential/base-URL wiring via `internal/cliconfig`. - `cmd/mecatui/` — optional gRPC **client** TUI; by default hosts a `mecated` in-process over a UNIX socket. `ui`/`theme`/`client` import no `engine/...` or `internal/...` and no proto directly — they render from relayed proto `Event`s. See `docs/tui.md`. +- `studio/` — the web client: a Next.js **Node module**, never a Go module (not in `go.work`, the layering DAG, depguard, or api-compat). A CLIENT like mecatui, consuming only the public HTTP/SSE API through its own server-side proxies; daemon-only (an unreachable daemon renders offline, never demo data). Commands run via `task studio:*` (npm underneath). **A breaking HTTP/SSE wire change owes a Studio update in the same PR.** See ADR 0233/0234 + `studio/CLAUDE.md`. - `perf/` — the OFFLINE scenario perf harness (perf-tracking Phase 2, `task perf:scenarios`, NOT part of `task test`): `perf/kpi` (stdlib-ONLY KPI capture — `ScenarioResult`/`Capture`/`/proc` RSS sampler; never imports `engine/...` or `internal/...`) + `perf/scenarios` (external-test `testing.B` whole-loop benchmarks over `engine/...` + `engine/adapter/*`, never `internal/...`). The TUI scrollback render bench lives in `cmd/mecatui/ui/scrollback_bench_test.go` (perf/kpi imported in the `_test` file only). `perf/cmd/perfconvert` (Phase 3; stdlib + `perf/kpi` only) reshapes the scenario JSON into the two github-action-benchmark suites the CI gate consumes, and `perf/cmd/allocsgate` (stdlib only, tested, FAIL-CLOSED) is the deterministic allocs/op gate over `task bench` — the gate DECISION (benchstat is the local human A/B tool only, never the CI gate); the gate is `.github/workflows/perf.yml` (split: allocsgate over `task bench` + github-action-benchmark over the scenarios; PR fails-but-never-pushes, main pushes the `gh-pages` trend store). See `docs/adr/0019-perf-tracking.md`. ## The layering rule (the thing to get right) diff --git a/Taskfile.yml b/Taskfile.yml index e4ecc7b2e9..368265045f 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -18,6 +18,9 @@ includes: site: taskfile: website/Taskfile.yml dir: website + studio: + taskfile: studio/Taskfile.yml + dir: studio vars: PKG: github.com/stacklok/mecatl diff --git a/docs/adr/0233-studio-atrium-module.md b/docs/adr/0233-studio-atrium-module.md new file mode 100644 index 0000000000..6482d4ae54 --- /dev/null +++ b/docs/adr/0233-studio-atrium-module.md @@ -0,0 +1,105 @@ +# ADR 0233 — Studio: the Atrium workspace as mecatl's daemon-only web client + +- Status: Accepted +- Date: 2026-08-18 +- Scope: `studio/` — what the web client is, what it may talk to, how it deploys, + and what it deliberately does not do + +> History: this decision subsumes the unmerged ADR drafts from PR #548 ("Studio +> module", numbered 0225 there before that id was taken on main) and carries its +> security posture forward under the new UI. PR #548 is superseded by the change +> that lands this ADR. + +## Context + +Mecatl needed a web client. Two candidates existed side by side: the original +Studio module on the unmerged `feat/studio-module` branch — a single-page chat +with a hardened server tier (origin-pinned proxies, a managed-`mecated` +supervisor, a typed wire seam, behavior tests) but a one-file UI — and the +Atrium workspace prototype in `stacklok/enterprise-ui-prototypes`, a designed +five-surface product (Chats · Scheduled · Skills · Memory · Settings) that +already spoke mecated's SSE protocol, but grew inside a ToolHive console fork: +its own OIDC stack, a mock server, generated clients for services mecatl does +not have, and per-surface demo fallbacks that rendered fabricated content +whenever the daemon was away. + +Neither was shippable alone. The prototype had the product; the module had the +posture. + +## Decision + +**Studio is the Atrium workspace UI mounted on the original module's server +tier, in-repo at `studio/`, and it is daemon-only.** + +- **A Node module, never a Go module.** Studio is a CLIENT of the daemon like + `mecatui`: it is not in `go.work`, the layering DAG, depguard, or the + api-compat gate. It consumes only the public HTTP/SSE API, through its own + server-side route handlers — the browser never holds a daemon address or + credential. +- **Two pure deployment modes.** Managed: `scripts/local-controller.mjs` + supervises a `mecated` it spawns from `../bin/mecated` on a random loopback + port with a generated bearer token. External: `MECATL_BASE_URL` selects a + remote daemon; no controller runs and every local control surface answers 409 + as owned by the deployment. Nothing in between. +- **Daemon-only.** The prototype's mock server, demo fixtures, and per-hook + fallbacks are excluded at import. An unreachable daemon is a rendered state — + a shared runtime-status provider polls the daemon and controller, shows the + offline banner, and gates every surface's loads. Probe failure must never + produce fabricated content. (The managed controller may still run + `mecated --mock`: that is a real daemon with a mock LLM provider, which is + what offline development means here.) +- **One typed wire seam.** `src/lib/protocol/` is the only reader of raw daemon + JSON: structural decoders that throw on malformed frames, surface unknown + event kinds as visible notices, and encode the request/response asymmetry + (protojson requests, stdlib-JSON responses). Generated TypeScript bindings + from `contracts/proto` remain deferred; the seam plus its behavior tests is + the stopgap, and a breaking wire change owes a Studio update in the same PR. +- **The security posture is inherited, not re-derived.** Host/Origin/CSRF + pinning at the Next tier (`MECATL_STUDIO_PUBLIC_ORIGIN`); bearer injection + server-side only; controller mutations require the server-set + `x-mecatl-studio-request` header, loopback Host, and an allowlisted Origin; + bounded bodies; MCP gateway egress is HTTPS-only (loopback HTTP behind an + operator env opt-in) with the gateway URL user-entered but always validated; + the session workspace is resolved server-side (controller status or + `MECATL_WORKSPACE`) and injected into create bodies so the browser never + learns or chooses paths. +- **Deliberate non-features.** No provider-credential entry anywhere: mecated + reads keys from its auth file, and Studio's provider card only reports + status. Memory is read-only (the daemon has no write API, by design — a + hand-typed value would enter turn-0 context without injection scanning). + Skill and agent-definition authoring, learned-skill review, team runs, the + plan-approval flow, and live re-attach to running sessions (gRPC-only today) + are named follow-ups, not silent gaps. +- **Toolchain.** npm with a committed lockfile (security pins carried from the + prototype as npm `overrides`), Node 22 LTS via `.nvmrc`, Biome for + lint+format, vitest for unit/decoder tests, and a hermetic `node --test` + harness that boots the production build in external mode against a fake + recording daemon to prove the server tier's behavior. CI runs all of it plus + `npm audit --audit-level=high` and a license-header guard; installs run with + scripts disabled. + +## Consequences + +One product instead of two halves: the designed workspace, on the hardened +tier, with the daemon as the single source of truth. + +The costs, stated plainly: + +- A fresh checkout without `bin/mecated` shows an offline screen, not a demo. + The screen names the fix (`task build`, then `task studio:dev`); losing the + zero-setup demo is the price of never rendering fabricated state. +- The deferred surfaces are real feature regressions against the prototype's + dormant code (authoring flows existed there, unmounted) and stay out until + they can land controller-mediated with the same posture. +- Vendoring a designed UI brings a large dependency tree (~40 runtime + packages) into the repo's audit surface; dependabot and the audit gate own + that from here. +- The same-PR rule now binds a much larger client: a daemon wire change costs + a Studio change in the same PR, every time. + +## See also + +- [ADR 0234](./0234-studio-server-backed-chats.md) — the chat list is the + daemon's session store +- `docs/architecture.md` — the Studio client section +- `user-docs/what-you-get/studio.md` — what operating it looks like diff --git a/docs/adr/0234-studio-server-backed-chats.md b/docs/adr/0234-studio-server-backed-chats.md new file mode 100644 index 0000000000..10384ea921 --- /dev/null +++ b/docs/adr/0234-studio-server-backed-chats.md @@ -0,0 +1,96 @@ +# ADR 0234 — Studio's chat list is the daemon's session store + +- Status: Accepted +- Date: 2026-08-18 +- Scope: `studio/` — where a chat's existence, title, and transcript live, and which + client actions are server calls + +> History: re-lands the unmerged draft from PR #615 (numbered 0227 there before +> that id was taken on main), adapted to the Atrium workspace UI. + +## Context + +The prototype Studio descended from kept its chat list in browser state — first +a `localStorage` key, then mock fixtures mirrored in module scope. `mecated` +persisted the same sessions the whole time: the daemon already exposed +`GET /v1/sessions`, `GET /v1/sessions/{id}/transcript`, +`POST /v1/sessions/{id}/rename`, and `POST /v1/sessions/{id}/delete` — and the +client called none of them. + +Every consequence followed from that one gap: + +- A second browser, or a second machine, saw nothing. The daemon held the work; + the client that opened it held the only record of what the work was called. +- Renaming a chat renamed a local label that no other client would ever see. +- Deleting a chat dropped the client's copy and left the session in the store + forever — "delete" quietly meant "hide". +- A reload during a run orphaned it. `mecated` keeps running after the page + that started it goes away, but the client had no way back to the result: the + run streams off the `POST /prompt` response body, and that body is gone. +- The client also minted its own session ids and mapped them lazily onto daemon + sessions, so the daemon's record and the sidebar disagreed about what even + existed. + +The persistence was never missing. It was unused. + +## Decision + +The daemon's session store is the record of which chats exist, what each is +called, and what was said in each. Studio reads and writes that record, and +**the sidebar id IS the daemon session id** — there is no client-side session +mapping. + +- **The chat list is the session inventory.** Studio walks `GET /v1/sessions` + when the daemon becomes reachable and on a slow poll, and merges the result + into its list. A row is removed only when a COMPLETE walk proves it gone — + a partial walk updates what it saw and never deletes. Rows whose one + not-a-chat reason is `inspect_only_kind` (subagents, team members, scheduled + fires) are filtered by the decoder, not by id-prefix guessing. +- **Rename is `POST …/rename`.** The local update is optimistic and rolls back + if the daemon refuses. Studio adopts the title the daemon actually stored, + which is clamped, rather than the one it asked for. +- **Delete is `POST …/delete`.** A refusal keeps the row: the chat still + exists, and hiding it locally is the exact failure this ADR is about. Only a + 404 — the daemon saying it has no such session — removes a row without a + witnessed walk. +- **Opening a chat reads `GET …/transcript`.** The authoritative message-level + snapshot, which also covers scheduler-tick fires whose conversation never + reached the durable event log, and works identically in external mode. A + transcript the daemon cannot prove whole says so in the UI. +- **Eligibility is the daemon's, not Studio's.** Each inventory row carries + `capabilities` plus a closed machine-readable reason per disabled action. + Studio renders the reason; it does not re-derive who may rename or delete + what, so tightening the rule server-side needs no client change. +- **A new chat is a draft.** No daemon session is created until the first + prompt is sent; the mint happens then, and the route adopts the daemon's id. + Empty sessions never accumulate in the store from idle "new chat" clicks. + +## Consequences + +A chat renamed or deleted on one machine is renamed or deleted on every +machine, and survives clearing the browser. Sessions can finally be removed +from the store through the UI. A reload during a run no longer loses it: the +inventory row shows the session still running on the daemon, and the +transcript is read back when it ends. + +The costs, stated plainly: + +- **The list reorders on rename.** A rename advances the daemon's stored + mtime, and that mtime is the only ordering every client can agree on. +- **A rehydrated transcript is not identical to the live one.** The transcript + endpoint returns the model's conversation, which includes the harness's own + synthetic continuations as user-role messages with no provenance to + distinguish them. Fixing this needs a provenance field on the daemon's + `ConversationMessage`, not string-sniffing in the client. +- **Reattaching to a LIVE run is still not possible.** Studio can see a run in + flight and read its transcript once it ends, but it cannot resume the + stream: `GET /v1/sessions/{id}/events` is a finite replay, and the live + per-session subscription is reachable only over gRPC `StreamSessionLive`. + Closing that gap is a daemon change, not a client one. +- **Polling.** The list reconciles on an interval rather than a push, so a + change made elsewhere appears within seconds, not instantly. + +## See also + +- [ADR 0233](./0233-studio-atrium-module.md) — the Studio module and its + daemon-only posture diff --git a/docs/adr/README.md b/docs/adr/README.md index dacd1a2fd5..b03ba327cc 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -181,6 +181,8 @@ Documentation/citation conventions are in [`docs/design/README.md`](../design/RE - [0087 — Staged mecatui transport migration](./0087-mecatui-staged-transport-migration.md) *(superseded by 0089)* - [0088 — Explicit daemon.yaml (listener topology config)](./0088-daemon-config-file.md) - [0222 — mecatui: ctrl+t routes by ask type; full-screen ask-args view](./0222-mecatui-ask-args-view.md) +- [0233 — Studio: the Atrium workspace as mecatl's daemon-only web client](./0233-studio-atrium-module.md) +- [0234 — Studio's chat list is the daemon's session store](./0234-studio-server-backed-chats.md) ### Retired - [0029 — Repo-map tree-sitter](./0029-repomap-tree-sitter.md) *(retired)* diff --git a/docs/architecture.md b/docs/architecture.md index 4dbeb2395e..5538da3a9f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -317,6 +317,22 @@ from proto `Event`s** and are bound by the inward-only layering rule. The model/update/view) and `theme` (pure styling) packages import no `engine/...` or `internal/...` package and no proto directly. Usage and theming are documented in `docs/tui.md`. +**Studio — the web client (`studio/`).** An optional Next.js *client* of the public +HTTP/SSE API, in-repo as a Node module (never a Go module — not in `go.work`, the +layering DAG, depguard, or the api-compat gate). The browser talks only to Studio's +own server-side proxy routes (`/api/mecatl/*`, `/api/mecatl-control/*`), which pin +Host/Origin, inject the bearer token and the session workspace server-side, and +allowlist headers in both directions. Two pure deployment modes: managed (a local +controller supervises a `mecated` spawned from `bin/mecated` on a random loopback +port with a generated bearer) or external (`MECATL_BASE_URL`; every local control +surface answers 409 as deployment-owned). Studio is daemon-only — an unreachable +daemon renders as an offline state, never demo content — and decodes the wire in +one typed seam (`studio/src/lib/protocol/`) that surfaces unknown event kinds +instead of dropping them. Live re-attach to a running session is a stated non-goal +today: the live tail is gRPC-only (`StreamSessionLive`), so Studio shows running +state from the session inventory and reads the transcript when the run ends. A +breaking wire change owes a Studio update in the same PR. See ADR 0233/0234. + **mecatequi — the single-shot headless runner (`cmd/mecatequi`).** A fourth composition root and a *peer of `mecademo`* over the same `app.Build`: it runs **one** prompt against an in-process `server.Service`, drives it to a terminal state, and emits three diff --git a/docs/architecture/api-surface.md b/docs/architecture/api-surface.md index f18ff38ceb..1fd372a10a 100644 --- a/docs/architecture/api-surface.md +++ b/docs/architecture/api-surface.md @@ -89,6 +89,8 @@ v1 enforces required checks in the Go server (protovalidate runtime is deferred) | `POST /v1/sessions/{id}/approve` | `Run.Approve` | resolves the paused ask (verdict or legacy `allow`) | | `POST /v1/sessions/{id}/cancel` | `Run.Cancel` | cancels the in-flight run | | `POST /v1/sessions/{id}/cancel-child` | `Run.CancelChild` | cancels ONE child of the in-flight run | +| `POST /v1/sessions/{id}/steer` | `Service.SteerEnqueue` | unary mid-run steer: enqueue to the live run's inbox (`accepted`/`appended`/`too_late`); never promotes — on `too_late` the caller keeps the text and drives its own follow-up prompt (the gRPC `steer` frame's in-server promote stays gRPC-only) | +| `POST /v1/sessions/{id}/steer-cancel` | `Service.CancelSteer` | retracts the pending (un-drained) steer (`retracted`/`none_pending`) | | `POST /v1/sessions/{id}/adoption:preflight` | `PreflightSessionAdoption` | caller-owned eligibility and explicit-binding preflight; no source mutation | | `POST /v1/sessions/{id}/adopt` | `AdoptSession` | idempotent atomic new-main copy; source remains inspect-only | | `DELETE /v1/sessions/{id}` | `CloseSession` | frees the per-session engine slot | diff --git a/docs/design/IMPLEMENTATION-NOTES.md b/docs/design/IMPLEMENTATION-NOTES.md index 2812d5c65e..3283ca9f92 100644 --- a/docs/design/IMPLEMENTATION-NOTES.md +++ b/docs/design/IMPLEMENTATION-NOTES.md @@ -7438,18 +7438,33 @@ costs no prompt-cache rebuild beyond normal history growth). The drain emits `EvSteer` carrying the committed text — the authoritative echo; the client renders the echoed truth (recorded == streamed == model-view). -**Wire (gRPC-only v1).** A `steer`/`steer_cancel` oneof arm on the bidi `Converse` -stream, a `ServerCapabilities.steer` bit (additive grow, computed once in -composition), and the `EvSteer` echo. The routing has ONE owner — -`Service.Steer`/`Service.CancelSteer` (`internal/adapter/server/service.go`); the -gRPC handler is a dumb frame→Service mapper. **Correlation (watermark).** Every +**Wire.** A `steer`/`steer_cancel` oneof arm on the bidi `Converse` stream +(gRPC, v1), a `ServerCapabilities.steer` bit (additive grow, computed once in +composition, mirrored onto the HTTP capabilities echo's `steer` field), the +`EvSteer` echo, and — the ADR-0232 named follow-up, landed — the unary HTTP pair +`POST /v1/sessions/{id}/steer` / `POST /v1/sessions/{id}/steer-cancel` +(`internal/adapter/server/http.go` (`steer`)/(`steerCancel`), mirroring +`approve`/`cancel`/`cancel-child`). The routing has ONE owner — +`Service.SteerEnqueue` is the live-run fast path (authorize → enqueue → track +the message id; NEVER promotes), `Service.Steer` composes it with the gRPC-only +promote (`promotedSteerRun`), and `Service.CancelSteer` owns the retract +(`internal/adapter/server/service.go`); both wire handlers are dumb +frame/body→Service mappers. **The HTTP tier never promotes:** a too_late steer +returns `{"outcome":"too_late"}` and the CALLER keeps the text (never-drop holds +caller-side — an ordinary follow-up `POST /prompt` is the re-send), whereas a +gRPC `steer` frame's text has no other home once the ack is sent, so only the +`Service.Steer` composition auto-promotes. **Correlation (watermark).** Every frame carries a client-minted `message_id`; the ack lane echoes its own frame's -id on each outcome. The engine inbox parks text only, so the Service keeps a -per-session FIFO of the ordered frame ids (`trackSteerMessageID`/ -`LookupSteerMessageID`/`dropSteerMessageID`); on drain the relay pops the whole -list and stamps the `EvSteer` echo with the LATEST (tail) id — the **watermark** -the client splits its ordered queue on (positional, never text-match — pinned by -`TestLookupSteerMessageIDExactUnderDuplicateTexts`). Ids are clamped to a 64-rune +id on each outcome (HTTP: the response body's `message_id`). The engine inbox +parks text only, so the Service keeps a per-session FIFO of the ordered frame +ids (`trackSteerMessageID`/`LookupSteerMessageID`/`dropSteerMessageID`); on +drain the relay pops the whole list and stamps the `EvSteer` echo with the +LATEST (tail) id — the **watermark** the client splits its ordered queue on +(positional, never text-match — pinned by +`TestLookupSteerMessageIDExactUnderDuplicateTexts`). The stamp + the +correlated-INFO/uncorrelated-WARN diagnostics have ONE owner — +`Service.stampSteerEcho` — called by BOTH relays (gRPC `sendEvent`, HTTP +`relayRunSSE`), so the two wires cannot drift. Ids are clamped to a 64-rune prefix at track before touching the FIFO or any log (CWE-770). **Lost terminal race → auto-promote + sequential handoff:** a steer arriving for a session whose run is already terminal is promoted to a fresh follow-up run through the hardened @@ -7464,10 +7479,9 @@ is not goroutine-safe), the control target (`ResumeApproval`/`Cancel`/ `CancelChild`) swaps to the promoted run atomically before its relay starts, and the promoted run is `FinishRun`-deregistered before the RPC returns (its terminal outcome is reported inline as the `steer.outcome` ack, `promoted=true`). -**HTTP/SSE and ACP steer are deferred** (no client→server mid-run channel; a -unary `POST .../steer` mirroring `approve`/`cancel` is the cheap follow-up -shape), as is **steer-to-child** (needs a richer parent→child channel than -`CancelChild`). +**HTTP/SSE steer is LANDED** (the unary pair above, no auto-promote). **ACP +steer stays deferred** (no client→server mid-run channel), as is +**steer-to-child** (needs a richer parent→child channel than `CancelChild`). **mecatui.** Reads the `steer` capability off the CreateSession echo: present → `enter` mid-run sends a `steer` frame (each `enter` mints a fresh `message_id`, diff --git a/docs/design/PRODUCTION-READINESS.md b/docs/design/PRODUCTION-READINESS.md index a2ad1308ce..db08183944 100644 --- a/docs/design/PRODUCTION-READINESS.md +++ b/docs/design/PRODUCTION-READINESS.md @@ -45,6 +45,7 @@ record; current behaviour is in the linked [architecture](../architecture.md) do | mecak8s (storage-free k8s-native agent) | ✅ shipped (MVP) · ✅ OPT-IN `/metrics` loopback scrape + OTLP push (ADR 0098) · ⛔ CRD/Operator · ⛔ HPA (custom-metrics on active-runs) · ⛔ managed Redis (ElastiCache/MemoryStore — manifest swap, no code) · ⛔ Redis auth · ⛔ fix `mecated`'s unbounded `GracefulStop` (pre-existing, follow-up) | [mecak8s.md](../adr/0048-mecak8s.md) · [0098](../adr/0098-headless-telemetry.md) · [MECAK8S-PLAN.md](./MECAK8S-PLAN.md) | [overview](../architecture.md) | | ACP adapter (editor stdio surface) | ✅ Phase 1+2 + bounded Phase 3 + multimodal shipped · ⛔ Phase 3 long-tail (rule persistence, grep-over-buffers, fs/* on resume) | [0001-acp-adapter.md](../adr/0001-acp-adapter.md) | [api surface](../architecture/api-surface.md) | | Conversation fork (peer session from a history snapshot) | ✅ shipped · ✅ effort override (mid-conversation effort switch, keeps the transcript — [0068](../adr/0068-effort-change-via-fork.md)) · ⛔ cross-provider/model fork (v2: replay-blob stripping) · ⛔ workspace-branching fork · ⛔ fork-from-event-log-at-arbitrary-point · ⛔ fork lineage (`forked_from` label) | [0065-conversation-fork.md](../adr/0065-conversation-fork.md) | [overview](../architecture.md) | +| Studio (web client) | ✅ Atrium workspace shipped daemon-only: five surfaces (Chats · Scheduled · Skills · Memory · Settings) on the hardened proxy tier, managed/external modes, hermetic server-tier + protocol + e2e suites · ⛔ skill & agent-definition authoring (controller-mediated) · ⛔ learned-skills / learning-proposals UI · ⛔ plan-mode approval flow · ⛔ live re-attach to running sessions (needs an HTTP/SSE mirror of gRPC `StreamSessionLive`) · ⛔ generated TS proto bindings | [0233](../adr/0233-studio-atrium-module.md) · [0234](../adr/0234-studio-server-backed-chats.md) | [overview](../architecture.md) | | _Historical / retired_ | — | [ARCHITECTURE.md](../adr/0004-v1-architecture.md) · [STEP-CHAIN.md](../adr/0006-v1-step-chain.md) · [TWELVE-PATTERNS-AUDIT.md](../adr/0007-twelve-patterns-audit.md) · [REPOMAP-TREE-SITTER.md](../adr/0029-repomap-tree-sitter.md) | — | ## Security diff --git a/docs/usage/http-sse-api.md b/docs/usage/http-sse-api.md index 3e4d658bbd..2c98689a88 100644 --- a/docs/usage/http-sse-api.md +++ b/docs/usage/http-sse-api.md @@ -20,6 +20,8 @@ share one event shape. | `POST /v1/sessions/{id}/plan:approve` | `{"target_mode": "default" \| "accept_edits" \| "plan", "note": "..."}` | `200` `text/event-stream` — atomically resolve a parked **plan-approval** ask ([ADR 0069](../adr/0069-plan-approval-gate.md)): on `default`/`accept_edits` resume the parked run AND start the continuation run (both streamed); on `plan`/`""` iterate (no continuation). `409` on a precondition failure (live run / not awaiting / not a plan ask), `404` on an unknown session | | `POST /v1/sessions/{id}/cancel` | — | `204` | | `POST /v1/sessions/{id}/cancel-child` | `{child_id}` | `204`; `404` for an unknown / already-finished child | +| `POST /v1/sessions/{id}/steer` | `{text, message_id?}` | `200` `{outcome, message_id}` — enqueue a mid-run steer into the live run's inbox (`accepted` \| `appended` \| `too_late`; on `too_late` the caller keeps the text — see the steer section) | +| `POST /v1/sessions/{id}/steer-cancel` | — | `200` `{outcome}` — retract the pending (un-drained) steer (`retracted` \| `none_pending`) | | `POST /v1/sessions/{id}/fork` | `{"title": "...", "reasoning_effort": "..."}` (both optional; empty/absent inherits the source's) | `201` `{session_id}` — create a peer session from `{id}`'s conversation history snapshot (ADR 0065); same provider/model only, with the ONE optional selector delta a reasoning-effort override (ADR 0068); `412` if `{id}` is not an idle/terminal main chat or is live in this process, `409` when another replica holds its lease | | `POST /v1/sessions/{id}/adoption:preflight` | `{workspace, environment_kind, environment_id, provider_id, model_id, profile?}` | `200` `{eligible, reason_code, bindings}`. Requires authenticated caller ownership; absent and foreign IDs are both `404`. Every binding is explicit and unresolved bindings return `binding_unresolved` rather than selecting a default | | `POST /v1/sessions/{id}/adopt` | the same explicit bindings plus `idempotency_key` | `201` `{session_id, source_session_id, capabilities, resolved_model}`. Revalidates under the source mutation lease; a retry returns the same complete target. The legacy source is unchanged | @@ -224,18 +226,60 @@ $ curl -s -X POST http://127.0.0.1:8081/v1/sessions//cancel The run terminates with a `result` whose `stop` is `cancelled`. No in-flight run → `404` `{"error":"no in-flight run for session"}`. -### Mid-run steer is gRPC-only (v1) +### Mid-run steer The **steer** capability (steer-while-running, issue #512 — inject an operator -instruction into an *in-flight* run, drained at the next turn boundary) rides the -bidi gRPC `Converse` stream as a `steer` / `steer_cancel` request arm. The HTTP/SSE -run path has **no mid-run client→server channel** — `POST /v1/sessions/{id}/runs` -streams server→client only — so an HTTP/SSE client **cannot steer** in v1. Read the -`steer` bit off the `CreateSession` capabilities echo: when present/true a gRPC -client may send `steer` frames; when absent/false the server reports `too_late` -(and, over gRPC, auto-promotes the text to a fresh follow-up run). A unary -`POST /v1/sessions/{id}/steer` endpoint is a possible cheap follow-up (mirroring -`approve`/`cancel`), deferred. +instruction into an *in-flight* run, drained at the next turn boundary) is +available on both wires: over bidi gRPC it rides the `Converse` stream as a +`steer` / `steer_cancel` request arm; over HTTP it is a pair of unary endpoints +(mirroring `approve`/`cancel` — the prompt SSE stream itself is server→client +only). Gate the affordance on the `steer` bit of the `POST /v1/sessions` +capabilities echo: when absent/false the server's steer knob is off and every +steer reports `too_late`. + +Enqueue a steer while the prompt SSE stream is still open (a **second** +connection, like `approve`): + +```console +$ curl -s -X POST http://127.0.0.1:8081/v1/sessions//steer \ + -d '{"text":"also check b.go","message_id":"m-1"}' +{"outcome":"accepted","message_id":"m-1"} +``` + +The `outcome` is the engine's authoritative verdict, verbatim: + +- `"accepted"` — the text parked in the run's (empty) steer inbox; the run + drains it at the next turn boundary as an ordinary user message. +- `"appended"` — the inbox already held a pending steer; the text merged into + that pending bundle (they drain together as one message). +- `"too_late"` — the run is already terminal, no run is live, or the server's + steer capability is off. **The text was NOT enqueued and is NOT promoted**: + unlike the gRPC `steer` frame (whose text has no other home once the ack is + sent, so the server auto-promotes it into a fresh follow-up run), the HTTP + caller still holds the text — re-send it as an ordinary + `POST /v1/sessions/{id}/prompt` follow-up. Never drop it silently. + +`text` is required (`400`); an unknown session is `404`; the body is bounded +like `/prompt` (oversized → `413`). + +`message_id` is an optional client-minted correlation id: it is echoed verbatim +on the response (the ack-side echo), and when the steer lands the run's SSE +stream emits a `steer` event — the drain echo, carrying the committed text plus +the `message_id` **watermark** (the latest contributing send's id of the bundle +that drained; sends up to and including it drained, later sends are still +pending), so a client correlates positionally, never by text-match. + +Retract a still-pending (un-drained) steer: + +```console +$ curl -s -X POST http://127.0.0.1:8081/v1/sessions//steer-cancel +{"outcome":"retracted"} +``` + +`"retracted"` means the pending bundle was cleared before it drained (no `steer` +echo will land); `"none_pending"` means there was nothing to retract — the steer +already drained at a boundary (it is ordinary recorded history now) or no run is +live. Unknown session → `404`. ### ACP over stdio (`mecated acp`) diff --git a/internal/adapter/server/classification.go b/internal/adapter/server/classification.go index e7b78307dd..68b923a3ff 100644 --- a/internal/adapter/server/classification.go +++ b/internal/adapter/server/classification.go @@ -277,7 +277,8 @@ var serviceAccessTable = map[string]ClassificationEntry{ "ApprovePlan": {KindCallerOwned, "authorizes the session before resolving the parked plan ask"}, "Cancel": {KindCallerOwned, "authorizes via GetSession before signalling the in-flight run"}, "CancelChild": {KindCallerOwned, "authorizes the PARENT session via GetSession before reaching into its child registry"}, - "Steer": {KindCallerOwned, "authorizes via GetSession before enqueueing to the live run's inbox or promoting through StartRunContent"}, + "Steer": {KindCallerOwned, "composes SteerEnqueue (which authorizes via GetSession) before promoting through StartRunContent"}, + "SteerEnqueue": {KindCallerOwned, "authorizes via GetSession before enqueueing to the live run's steer inbox (the unary HTTP tier's entry; never promotes)"}, "CancelSteer": {KindCallerOwned, "authorizes via GetSession before reaching into the live run's steer inbox"}, "Persist": {KindCallerOwned, "authorizes via GetSession before consulting the live run registry"}, diff --git a/internal/adapter/server/grpc.go b/internal/adapter/server/grpc.go index 4619fbacb7..8ceb57f647 100644 --- a/internal/adapter/server/grpc.go +++ b/internal/adapter/server/grpc.go @@ -487,25 +487,10 @@ func (h *HarnessServer) sendEvent(rl *runRelay, ev session.Event) { return // log-only event: consumed by the durable log, not relayed to the client wire } proto := toProto(ev) - if ev.Type == session.EvSteer && proto.GetSteer() != nil { - // The EvSteer drain echo echoes the client-minted message_id of the - // Steer frame that parked this text: the engine inbox carries text only, - // so the id lives at the Service's wire-correlation FIFO — popped here - // positionally (the TAIL). An unmatched echo (an id-less steer) rides - // with "". - id := h.svc.LookupSteerMessageID(rl.id) - if id == "" { - // The correlation FAILED: the echo carries "" and the client cannot - // match it to the frame it sent (the queue can stall — the exact - // symptom this WARN exists to make visible). No session.Event owns a - // correlation miss, so it goes to diagnostics, text clamped to a prefix. - h.svc.Diagnostics().Log(rl.logCtx, port.LevelWarn, "steer echo uncorrelated (no message_id for drained text)", "session", string(rl.id), "text_prefix", valid(firstRunes(ev.Steer.Text, 40))) - } else { - h.svc.Diagnostics().Log(rl.logCtx, port.LevelInfo, "steer drain echo correlated", - "session", string(rl.id), "message_id", id, "text_len", len(ev.Steer.Text)) - } - proto.GetSteer().MessageId = valid(id) - } + // The EvSteer drain-echo message_id stamp + its correlation diagnostics live + // in the ONE shared Service.stampSteerEcho (the HTTP SSE relay calls the + // same helper) — a non-steer event is a no-op inside it. + h.svc.stampSteerEcho(rl.logCtx, rl.id, ev, proto) if err := rl.snd.Send(&mecatlv1.ConverseResponse{Event: proto}); err != nil { rl.sendErr = err } diff --git a/internal/adapter/server/http.go b/internal/adapter/server/http.go index b047ca9204..1226160b04 100644 --- a/internal/adapter/server/http.go +++ b/internal/adapter/server/http.go @@ -32,6 +32,8 @@ import ( // POST /v1/sessions/{id}/approve -> resolve the paused ask on the run // POST /v1/sessions/{id}/cancel -> cancel the in-flight run // POST /v1/sessions/{id}/cancel-child -> cancel ONE child (subagent) of the run +// POST /v1/sessions/{id}/steer -> enqueue a mid-run steer (unary; outcome JSON) +// POST /v1/sessions/{id}/steer-cancel -> retract the pending (un-drained) steer // POST /v1/sessions/{id}/fork -> ForkSession (peer session from a history snapshot; 201) // // Every Event is emitted as one SSE `data:` line carrying the proto Event @@ -57,6 +59,8 @@ func NewHTTPHandler(svc *Service) *HTTPHandler { h.mux.HandleFunc("POST /v1/sessions/{id}/plan:approve", h.approvePlan) h.mux.HandleFunc("POST /v1/sessions/{id}/cancel", h.cancel) h.mux.HandleFunc("POST /v1/sessions/{id}/cancel-child", h.cancelChild) + h.mux.HandleFunc("POST /v1/sessions/{id}/steer", h.steer) + h.mux.HandleFunc("POST /v1/sessions/{id}/steer-cancel", h.steerCancel) h.mux.HandleFunc("POST /v1/sessions/{id}/fork", h.forkSession) h.mux.HandleFunc("POST /v1/sessions/{id}/adoption:preflight", h.preflightSessionAdoption) h.mux.HandleFunc("POST /v1/sessions/{id}/adopt", h.adoptSession) @@ -229,6 +233,11 @@ type serverCapabilitiesJSON struct { LegacyAdoption bool `json:"legacy_adoption"` ManualDream *mecatlv1.ManualDreamCapabilities `json:"manual_dream,omitempty"` Posture string `json:"posture,omitempty"` + // Steer mirrors ServerCapabilities.steer: true when the engine's steer + // inbox is armed (Deps.EnableSteer), so an HTTP client gates the + // POST /v1/sessions/{id}/steer affordance on the same bit gRPC clients + // read; absent/false means a steer reports too_late. + Steer bool `json:"steer,omitempty"` } // capabilitiesJSON projects the shared proto capabilities onto the JSON shape. @@ -255,6 +264,7 @@ func capabilitiesJSON(c *mecatlv1.ServerCapabilities) *serverCapabilitiesJSON { LegacyAdoption: c.GetLegacyAdoption(), ManualDream: c.GetManualDream(), Posture: c.GetPosture(), + Steer: c.GetSteer(), } } @@ -687,11 +697,16 @@ func (h *HTTPHandler) relayRunSSE(w http.ResponseWriter, r *http.Request, id ses if !h.svc.relayEvent(r.Context(), logCtx, id, ev, true) { continue // log-only event: consumed by the durable log, not relayed to the client wire } + p := toProto(ev) + // The EvSteer drain-echo message_id stamp + its correlation diagnostics + // live in the ONE shared Service.stampSteerEcho (the gRPC Converse relay + // calls the same helper) — a non-steer event is a no-op inside it. + h.svc.stampSteerEcho(logCtx, id, ev, p) if _, err := w.Write([]byte("data: ")); err != nil { fail() continue } - if err := enc.Encode(toProto(ev)); err != nil { // Encode appends a newline + if err := enc.Encode(p); err != nil { // Encode appends a newline fail() continue } @@ -972,6 +987,84 @@ func (h *HTTPHandler) cancelChild(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } +// steerBody is the JSON body of POST /v1/sessions/{id}/steer. +type steerBody struct { + // Text is the operator instruction to inject into the in-flight run, drained + // at the next turn boundary. Required. + Text string `json:"text"` + // MessageID is the CLIENT-MINTED correlation id for THIS send ("" = + // uncorrelated). It is echoed verbatim on the response and, when the steer + // lands, on the EvSteer drain echo's message_id (a WATERMARK — the latest + // contributing send's id of the bundle that drained), so the client splits + // its ordered pending queue positionally, never by text-match. + MessageID string `json:"message_id,omitempty"` +} + +// steerResp is the JSON response of POST /v1/sessions/{id}/steer and +// /steer-cancel: the agent.SteerOutcome string verbatim (steer: accepted | +// appended | too_late; steer-cancel: retracted | none_pending), plus the +// request's own message_id echoed back (steer only — the ACK-side echo; the +// drain-side echo rides the EvSteer event on the prompt SSE stream). +type steerResp struct { + Outcome string `json:"outcome"` + MessageID string `json:"message_id,omitempty"` +} + +// steer handles POST /v1/sessions/{id}/steer — the unary HTTP tier of +// steer-while-running (ADR 0232's deferred follow-up): enqueue an operator +// instruction into the session's IN-FLIGHT run via Service.SteerEnqueue, to be +// drained at the next turn boundary. 200 {"outcome": "accepted"|"appended", +// "message_id": } parks the text on the live run (the EvSteer echo on +// the run's SSE stream carries the message_id when it drains); 200 +// {"outcome": "too_late"} means the run is already terminal, no run is live, or +// the server's steer knob is off (capabilities "steer" false) — unlike the bidi +// gRPC Steer frame it is NEVER auto-promoted to a follow-up run: the caller +// keeps the text (never-drop holds caller-side) and re-sends it as an ordinary +// POST /prompt. Unknown/foreign session → 404; oversized body → 413. +func (h *HTTPHandler) steer(w http.ResponseWriter, r *http.Request) { + id := session.SessionID(r.PathValue("id")) + // Bound the body read exactly like /prompt (CWE-770): an oversized payload + // is a 413, never buffered into memory. + r.Body = http.MaxBytesReader(w, r.Body, maxPromptBodyBytes) + var body steerBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + var maxErr *http.MaxBytesError + if errors.As(err, &maxErr) { + writeError(w, http.StatusRequestEntityTooLarge, "request body too large") + return + } + writeError(w, http.StatusBadRequest, "invalid JSON body") + return + } + if body.Text == "" { + writeError(w, http.StatusBadRequest, "text is required") + return + } + outcome, err := h.svc.SteerEnqueue(r.Context(), id, body.Text, body.MessageID) + if err != nil { + writeServiceError(w, err) + return + } + writeJSON(w, http.StatusOK, steerResp{Outcome: string(outcome), MessageID: body.MessageID}) +} + +// steerCancel handles POST /v1/sessions/{id}/steer-cancel (mirroring the +// cancel-child naming), retracting the session's live run's PENDING +// (un-drained) steer via Service.CancelSteer: 200 {"outcome": "retracted"} +// (the pending bundle is gone, its message-id correlation dropped) or +// {"outcome": "none_pending"} (nothing parked — already drained at a boundary, +// or no live run). No body is required; any body is ignored. Unknown/foreign +// session → 404. +func (h *HTTPHandler) steerCancel(w http.ResponseWriter, r *http.Request) { + id := session.SessionID(r.PathValue("id")) + outcome, err := h.svc.CancelSteer(r.Context(), id) + if err != nil { + writeServiceError(w, err) + return + } + writeJSON(w, http.StatusOK, steerResp{Outcome: string(outcome)}) +} + // --- team request bodies ----------------------------------------------------- // teammateSpecBody is one member's enrolment fields, shared by createTeam's diff --git a/internal/adapter/server/service.go b/internal/adapter/server/service.go index dcf64d4405..dba948410b 100644 --- a/internal/adapter/server/service.go +++ b/internal/adapter/server/service.go @@ -2984,30 +2984,19 @@ func (s *Service) startRunContent(ctx context.Context, id session.SessionID, tex // caller supplied none). On an accepted steer it parks in the session's FIFO so // the EvSteer drain echo can echo it (LookupSteerMessageID); the ACK-side echo // is the caller's own frame field (it never crosses the Service). +// +// Steer is a COMPOSITION: the live-run fast path is SteerEnqueue (the unary +// HTTP tier's own entry, which never promotes), and only a (SteerTooLate, nil) +// from it falls through to the promotedSteerRun path — the promote is what the +// bidi gRPC surface adds, because a Steer FRAME's text has no other home once +// the ack is sent (the HTTP caller, by contrast, keeps its own text). func (s *Service) Steer(ctx context.Context, id session.SessionID, text, messageID string) (agent.SteerOutcome, bool, *agent.Run, error) { - // Authorize before touching the in-memory registry or the run-entry funnel: - // a steer injects caller input into a run / drives a follow-up, so a foreign - // request must be absence-equivalent (ErrNotFound), mirroring Cancel/Approve. - if _, err := s.GetSession(ctx, id); err != nil { - return agent.SteerTooLate, false, nil, err + outcome, err := s.SteerEnqueue(ctx, id, text, messageID) + if err != nil { + return outcome, false, nil, err } - // Live-run fast path: enqueue to the run's steer inbox. A live run whose - // engine disarmed steer (EnableSteer off) reports too_late; it is PROMOTED - // rather than dropped — same lost-race contract as a closed inbox. - if run, ok := s.LookupRun(id); ok { - outcome, err := run.EnqueueSteer(text) - if err == nil && outcome != agent.SteerTooLate { - // Track BOTH accepted (new bundle) and appended (merged into the pending - // bundle): the watermark echo needs the full ordered id-list of the - // bundle that drains. - if outcome == agent.SteerAccepted || outcome == agent.SteerAppended { - s.trackSteerMessageID(id, messageID) - } - return outcome, false, nil, nil - } - if err != nil { - return outcome, false, nil, fmt.Errorf("server: steer enqueue: %w", err) - } + if outcome != agent.SteerTooLate { + return outcome, false, nil, nil } // Terminal race → promote through the run-entry funnel. An unknown id or an // unrepaired-terminal-state error surfaces here rather than ever dropping. @@ -3030,6 +3019,51 @@ func (s *Service) Steer(ctx context.Context, id session.SessionID, text, message return agent.SteerTooLate, true, promotedRun, nil } +// SteerEnqueue is the LIVE-RUN fast path of Steer, exported on its own as the +// unary HTTP tier's entry (POST /v1/sessions/{id}/steer): authorize, then +// enqueue the text to the session's live run's steer inbox and report the +// engine's authoritative outcome (accepted / appended — the run drains it at +// the next turn boundary). Unlike Steer it NEVER promotes: a terminal race (no +// live run, a closed inbox, or a disarmed engine reporting too_late) returns +// (agent.SteerTooLate, nil) and stops. The never-drop contract still holds +// because the CALLER keeps the text on too_late and drives its own follow-up +// prompt (an ordinary POST /prompt) — unlike a bidi gRPC Steer frame, whose +// text has no other home once the ack is sent, so Steer promotes in-server. +// +// Authorization mirrors Steer/Cancel/Approve exactly: a foreign/unknown id is +// absence-equivalent (ErrNotFound) before the in-memory registry is touched. +// messageID is the client-minted correlation id ("" when the caller supplied +// none); on accepted/appended it parks in the session's FIFO so the EvSteer +// drain echo can echo it (LookupSteerMessageID) — the ACK-side echo is the +// caller's own request field (it never crosses the Service). +func (s *Service) SteerEnqueue(ctx context.Context, id session.SessionID, text, messageID string) (agent.SteerOutcome, error) { + // Authorize before touching the in-memory registry or the run-entry funnel: + // a steer injects caller input into a run / drives a follow-up, so a foreign + // request must be absence-equivalent (ErrNotFound), mirroring Cancel/Approve. + if _, err := s.GetSession(ctx, id); err != nil { + return agent.SteerTooLate, err + } + // Live-run fast path: enqueue to the run's steer inbox. A live run whose + // engine disarmed steer (EnableSteer off) reports too_late — the caller + // (Steer's promote path, or the HTTP client itself) owns the follow-up. + if run, ok := s.LookupRun(id); ok { + outcome, err := run.EnqueueSteer(text) + if err == nil && outcome != agent.SteerTooLate { + // Track BOTH accepted (new bundle) and appended (merged into the pending + // bundle): the watermark echo needs the full ordered id-list of the + // bundle that drains. + if outcome == agent.SteerAccepted || outcome == agent.SteerAppended { + s.trackSteerMessageID(id, messageID) + } + return outcome, nil + } + if err != nil { + return outcome, fmt.Errorf("server: steer enqueue: %w", err) + } + } + return agent.SteerTooLate, nil +} + // promotedSteerRun is Service.Steer's promote path: try the funnel // once, and only on a liveness conflict await the original run's deregister // (bounded by steerPromoteGrace) then retry — so the just-terminal, @@ -3167,6 +3201,37 @@ func (s *Service) LookupSteerMessageID(id session.SessionID) string { return watermark } +// stampSteerEcho stamps the client-minted message_id onto an EvSteer drain +// echo's proto projection, consuming the session's watermark FIFO +// (LookupSteerMessageID). It is the SINGLE owner of the echo correlation — the +// gRPC Converse relay (HarnessServer.sendEvent) and the HTTP SSE relay +// (HTTPHandler.relayRunSSE) both call it, so the stamped id and the +// correlated-INFO / uncorrelated-WARN diagnostics cannot drift between the two +// wires. A non-EvSteer event (or a projection without the Steer payload) is a +// no-op, so callers stamp unconditionally on the hot path. +func (s *Service) stampSteerEcho(logCtx context.Context, id session.SessionID, ev session.Event, proto *mecatlv1.Event) { + if ev.Type != session.EvSteer || proto.GetSteer() == nil { + return + } + // The EvSteer drain echo echoes the client-minted message_id of the + // Steer frame that parked this text: the engine inbox carries text only, + // so the id lives at the Service's wire-correlation FIFO — popped here + // positionally (the TAIL). An unmatched echo (an id-less steer) rides + // with "". + msgID := s.LookupSteerMessageID(id) + if msgID == "" { + // The correlation FAILED: the echo carries "" and the client cannot + // match it to the frame it sent (the queue can stall — the exact + // symptom this WARN exists to make visible). No session.Event owns a + // correlation miss, so it goes to diagnostics, text clamped to a prefix. + s.Diagnostics().Log(logCtx, port.LevelWarn, "steer echo uncorrelated (no message_id for drained text)", "session", string(id), "text_prefix", valid(firstRunes(ev.Steer.Text, 40))) + } else { + s.Diagnostics().Log(logCtx, port.LevelInfo, "steer drain echo correlated", + "session", string(id), "message_id", msgID, "text_len", len(ev.Steer.Text)) + } + proto.GetSteer().MessageId = valid(msgID) +} + // isDelegationChildSessionID reports whether id carries one of the delegation // families' child-session id prefixes: agent.SubagentSessionPrefix, // agent.ParallelSessionPrefix, agent.TeamSessionPrefix — the engine's exported diff --git a/internal/adapter/server/steer_http_test.go b/internal/adapter/server/steer_http_test.go new file mode 100644 index 0000000000..08080cd453 --- /dev/null +++ b/internal/adapter/server/steer_http_test.go @@ -0,0 +1,331 @@ +package server_test + +// The unary HTTP tier of steer-while-running (the deferred follow-up ADR 0232 +// names): POST /v1/sessions/{id}/steer enqueues to the LIVE run's steer inbox +// via Service.SteerEnqueue — never promoting; on too_late the CALLER keeps the +// text and drives its own follow-up POST /prompt — and POST +// /v1/sessions/{id}/steer-cancel retracts the pending (un-drained) steer via +// Service.CancelSteer. The EvSteer drain echo on the prompt SSE stream carries +// the client-minted message_id through the SAME shared Service.stampSteerEcho +// the gRPC Converse relay uses, and POST /v1/sessions advertises the steer bit +// on the capabilities echo like gRPC CreateSession does. + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + mecatlv1 "github.com/stacklok/mecatl/contracts/gen/go/mecatl/v1" + "github.com/stacklok/mecatl/engine/adapter/mockllm" + "github.com/stacklok/mecatl/engine/session" + "github.com/stacklok/mecatl/internal/adapter/server" +) + +// steerRespJSON decodes the steer / steer-cancel 200 body. +type steerRespJSON struct { + Outcome string `json:"outcome"` + MessageID string `json:"message_id"` +} + +// postSteer POSTs body to the session's steer (or steer-cancel) route and +// returns the status code plus the decoded outcome body (zero on a non-200). +func postSteer(t *testing.T, srv *httptest.Server, id, route, body string) (int, steerRespJSON) { + t.Helper() + resp, err := http.Post(srv.URL+"/v1/sessions/"+id+"/"+route, "application/json", strings.NewReader(body)) + if err != nil { + t.Fatalf("POST %s: %v", route, err) + } + defer resp.Body.Close() + var out steerRespJSON + if resp.StatusCode == http.StatusOK { + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + t.Fatalf("decode %s response: %v", route, err) + } + } + return resp.StatusCode, out +} + +// httpSessionState GETs the session snapshot and returns its state string. +func httpSessionState(t *testing.T, srv *httptest.Server, id string) string { + t.Helper() + resp, err := http.Get(srv.URL + "/v1/sessions/" + id) + if err != nil { + t.Fatalf("GET session: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("GET session status = %d", resp.StatusCode) + } + var out struct { + State string `json:"state"` + } + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + t.Fatalf("decode session: %v", err) + } + return out.State +} + +// TestHTTPSteer_AcceptedEchoesMessageID: a steer POSTed against a genuinely +// LIVE run (a tool holds it mid-dispatch) is accepted — 200 +// {"outcome":"accepted","message_id":} — and the EvSteer drain echo on +// the prompt SSE stream carries the SAME client-minted message_id (the shared +// stampSteerEcho path), proving the HTTP relay correlates like the gRPC one. +func TestHTTPSteer_AcceptedEchoesMessageID(t *testing.T) { + block := &blockingTextTool{started: make(chan struct{}), release: make(chan struct{})} + llm := mockllm.New( + mockllm.ToolCallTurn(call("c1", "Read", `{"path":"a.go"}`)), + mockllm.TextTurn("turn two done"), + ) + svc := newSteerService(t, llm, nil, block) + srv := httptest.NewServer(server.NewHTTPHandler(svc)) + defer srv.Close() + + id := createHTTPSession(t, srv) + + // Steer from a side goroutine once the tool genuinely holds the run, then + // release the tool so the steer drains at the next turn boundary. + type steerAck struct { + status int + body steerRespJSON + } + acked := make(chan steerAck, 1) + go func() { + <-block.started + status, body := postSteer(t, srv, id, "steer", `{"text":"also check b.go","message_id":"m-1"}`) + acked <- steerAck{status: status, body: body} + close(block.release) + }() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + req, _ := http.NewRequestWithContext(ctx, http.MethodPost, + srv.URL+"/v1/sessions/"+id+"/prompt", strings.NewReader(`{"text":"look"}`)) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("POST prompt: %v", err) + } + defer resp.Body.Close() + events := parseSSE(t, bufio.NewReader(resp.Body)) + + ack := <-acked + if ack.status != http.StatusOK { + t.Fatalf("steer status = %d, want 200", ack.status) + } + if ack.body.Outcome != "accepted" { + t.Fatalf("steer outcome = %q, want accepted", ack.body.Outcome) + } + if ack.body.MessageID != "m-1" { + t.Fatalf("steer ack message_id = %q, want m-1 (the request's own id echoed)", ack.body.MessageID) + } + + // The EvSteer drain echo rides the prompt SSE stream carrying the committed + // text AND the client-minted message_id (stamped by the shared helper). + var steerEv *mecatlv1.Event + for _, ev := range events { + if ev.GetType() == "steer" { + steerEv = ev + break + } + } + if steerEv == nil { + t.Fatalf("no steer echo event on the SSE stream: %v", typesOf(events)) + } + if got := steerEv.GetSteer().GetText(); got != "also check b.go" { + t.Fatalf("steer echo text = %q, want %q", got, "also check b.go") + } + if got := steerEv.GetSteer().GetMessageId(); got != "m-1" { + t.Fatalf("steer echo message_id = %q, want m-1 (the HTTP relay must stamp the drain echo like the gRPC relay)", got) + } + if res := lastResult(t, events); res.GetStop() != "end_turn" || res.GetText() != "turn two done" { + t.Fatalf("result = %+v", res) + } +} + +// TestHTTPSteer_TooLateNeverPromotes: a steer against a session with NO live +// run reports 200 {"outcome":"too_late"} and starts NOTHING — the session's +// terminal state is untouched, no run registers, and no extra model call fires. +// The caller kept the text; the follow-up prompt is the caller's own move +// (unlike Service.Steer, whose gRPC composition promotes in-server). +func TestHTTPSteer_TooLateNeverPromotes(t *testing.T) { + llm := mockllm.New(mockllm.TextTurn("first")) + svc := newSteerService(t, llm, nil) + srv := httptest.NewServer(server.NewHTTPHandler(svc)) + defer srv.Close() + + id := createHTTPSession(t, srv) + resp, err := http.Post(srv.URL+"/v1/sessions/"+id+"/prompt", "application/json", + strings.NewReader(`{"text":"hello"}`)) + if err != nil { + t.Fatalf("POST prompt: %v", err) + } + parseSSE(t, bufio.NewReader(resp.Body)) // drive the run to its terminal + resp.Body.Close() + if got := httpSessionState(t, srv, id); got != "completed" { + t.Fatalf("precondition: state = %q, want completed", got) + } + + status, body := postSteer(t, srv, id, "steer", `{"text":"late follow-up"}`) + if status != http.StatusOK { + t.Fatalf("steer status = %d, want 200", status) + } + if body.Outcome != "too_late" { + t.Fatalf("steer outcome = %q, want too_late", body.Outcome) + } + // NO promotion: no live run registered, state unchanged, no second model call. + if _, live := svc.LookupRun(session.SessionID(id)); live { + t.Fatalf("a too_late HTTP steer must NOT register a promoted run") + } + if got := httpSessionState(t, srv, id); got != "completed" { + t.Fatalf("state after too_late steer = %q, want completed (unchanged)", got) + } + if calls := llm.Calls(); calls != 1 { + t.Fatalf("provider calls = %d, want 1 (a promoted run would have driven a second)", calls) + } +} + +// TestHTTPSteer_Rejections pins the request-validation edges: unknown session → +// 404 (absence-equivalent, mirroring cancel/approve), empty text → 400, and an +// oversized body → 413 (the same MaxBytesReader bound as /prompt). +func TestHTTPSteer_Rejections(t *testing.T) { + svc := newSteerService(t, mockllm.New(mockllm.TextTurn("x")), nil) + srv := httptest.NewServer(server.NewHTTPHandler(svc)) + defer srv.Close() + id := createHTTPSession(t, srv) + + t.Run("unknown session", func(t *testing.T) { + status, _ := postSteer(t, srv, "does-not-exist", "steer", `{"text":"hi"}`) + if status != http.StatusNotFound { + t.Fatalf("status = %d, want 404", status) + } + }) + + t.Run("empty text", func(t *testing.T) { + status, _ := postSteer(t, srv, id, "steer", `{"text":""}`) + if status != http.StatusBadRequest { + t.Fatalf("status = %d, want 400", status) + } + }) + + t.Run("oversized body", func(t *testing.T) { + // A body just over the wire cap (37 MiB > 32 MiB maxPromptBodyBytes). + huge := bytes.Repeat([]byte("A"), 37<<20) + body := append([]byte(`{"text":"`), huge...) + body = append(body, []byte(`"}`)...) + resp, err := http.Post(srv.URL+"/v1/sessions/"+id+"/steer", "application/json", bytes.NewReader(body)) + if err != nil { + t.Fatalf("POST steer: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusRequestEntityTooLarge { + t.Fatalf("status = %d, want 413 for oversized body", resp.StatusCode) + } + }) +} + +// TestHTTPSteerCancel_RetractsPending: steer-cancel against a live run with a +// PENDING steer reports retracted, and the retracted text never drains (no +// EvSteer echo lands on the stream); with nothing pending it reports +// none_pending; an unknown session is 404. +func TestHTTPSteerCancel_RetractsPending(t *testing.T) { + block := &blockingTextTool{started: make(chan struct{}), release: make(chan struct{})} + llm := mockllm.New( + mockllm.ToolCallTurn(call("c1", "Read", `{"path":"a.go"}`)), + mockllm.TextTurn("done"), + ) + svc := newSteerService(t, llm, nil, block) + srv := httptest.NewServer(server.NewHTTPHandler(svc)) + defer srv.Close() + + id := createHTTPSession(t, srv) + + // Steer then immediately retract, both while the tool holds the run. + outcomes := make(chan [2]steerRespJSON, 1) + go func() { + <-block.started + _, first := postSteer(t, srv, id, "steer", `{"text":"scratch that"}`) + _, second := postSteer(t, srv, id, "steer-cancel", "") + outcomes <- [2]steerRespJSON{first, second} + close(block.release) + }() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + req, _ := http.NewRequestWithContext(ctx, http.MethodPost, + srv.URL+"/v1/sessions/"+id+"/prompt", strings.NewReader(`{"text":"look"}`)) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("POST prompt: %v", err) + } + defer resp.Body.Close() + events := parseSSE(t, bufio.NewReader(resp.Body)) + + got := <-outcomes + if got[0].Outcome != "accepted" { + t.Fatalf("steer outcome = %q, want accepted", got[0].Outcome) + } + if got[1].Outcome != "retracted" { + t.Fatalf("steer-cancel outcome = %q, want retracted", got[1].Outcome) + } + if hasType(events, "steer") { + t.Fatalf("a retracted steer must never drain/echo: %v", typesOf(events)) + } + + // Nothing pending any more (the run is over): none_pending, never an error. + status, none := postSteer(t, srv, id, "steer-cancel", "") + if status != http.StatusOK || none.Outcome != "none_pending" { + t.Fatalf("steer-cancel with nothing pending = (%d, %q), want (200, none_pending)", status, none.Outcome) + } + + // Unknown session: absence-equivalent 404. + if status, _ := postSteer(t, srv, "does-not-exist", "steer-cancel", ""); status != http.StatusNotFound { + t.Fatalf("steer-cancel unknown session status = %d, want 404", status) + } +} + +// TestHTTPCreateSession_AdvertisesSteer: the POST /v1/sessions capabilities +// echo carries the steer bit — true when the engine arms Deps.EnableSteer, +// absent/false otherwise — so an HTTP client gates the steer affordance on the +// same composition-computed value gRPC clients read. +func TestHTTPCreateSession_AdvertisesSteer(t *testing.T) { + decodeCaps := func(t *testing.T, srv *httptest.Server) bool { + t.Helper() + resp, err := http.Post(srv.URL+"/v1/sessions", "application/json", + strings.NewReader(`{"workspace":"/ws"}`)) + if err != nil { + t.Fatalf("POST /v1/sessions: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusCreated { + t.Fatalf("create status = %d", resp.StatusCode) + } + var out struct { + Capabilities struct { + Steer bool `json:"steer"` + } `json:"capabilities"` + } + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + t.Fatalf("decode create: %v", err) + } + return out.Capabilities.Steer + } + + on := httptest.NewServer(server.NewHTTPHandler(newSteerService(t, mockllm.New(mockllm.TextTurn("x")), nil))) + defer on.Close() + if !decodeCaps(t, on) { + t.Fatalf("capabilities steer = false, want true (EnableSteer armed)") + } + + off := httptest.NewServer(server.NewHTTPHandler(newService(t, mockllm.New(mockllm.TextTurn("x")), allowRules()))) + defer off.Close() + if decodeCaps(t, off) { + t.Fatalf("capabilities steer = true, want false (EnableSteer off)") + } +} diff --git a/llms.txt b/llms.txt index 33a7eaa150..97f406ad33 100644 --- a/llms.txt +++ b/llms.txt @@ -1,6 +1,6 @@ # mecatl -> A markdown documentation corpus of 218 document(s) across 11 component(s), with 2247 heading(s) and 2029 resolved reference(s). Entries are ordered by importance (most-connected first). 19 document(s) excluded from rendering by emitExclude (still in the corpus: link-checked and ranked). Compactness 0.83, docs ~2.2 clicks apart. +> A markdown documentation corpus of 220 document(s) across 12 component(s), with 2267 heading(s) and 2036 resolved reference(s). Entries are ordered by importance (most-connected first). 20 document(s) excluded from rendering by emitExclude (still in the corpus: link-checked and ranked). Compactness 0.81, docs ~2.2 clicks apart. ## Documentation @@ -30,9 +30,9 @@ - [ADR 0004 — v1 Architecture: hexagonal DDD harness](docs/adr/0004-v1-architecture.md): ADR 0004 — v1 Architecture: hexagonal DDD harness (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0005-driver-seams.md, docs/adr/0006-v1-step-chain.md, docs/adr/0016-multi-provider.md, docs/adr/0027-cloud-native.md, docs/adr/0029-repomap-tree-sitter.md, docs/adr/README.md, docs/design/IMPLEMENTATION-NOTES.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0028 — mecatequi: Single-Shot GitHub Action Runner](docs/adr/0028-mecatequi.md): ADR 0028 — mecatequi: Single-Shot GitHub Action Runner (linked from: README.md, docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0048-mecak8s.md, docs/adr/0082-factory-mcp-wiring.md, docs/adr/0098-headless-telemetry.md, docs/adr/0103-mecatui-seed-prompt.md, docs/adr/README.md, docs/design/MECAK8S-PLAN.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md, docs/usage/mecatequi-ci.md) - [ADR 0215 — OpenAI subscription with a manual access token](docs/adr/0215-openai-subscription-manual-token.md): ADR 0215 — OpenAI subscription with a manual access token (linked from: docs/README.md, docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/README.md, docs/architecture.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md, docs/usage/troubleshooting.md) -- [ADR 0204 — Caller identity: accept a principal, thread it everywhere, record the owner](docs/adr/0204-caller-identity-threading.md): ADR 0204 — Caller identity: accept a principal, thread it everywhere, record the owner (linked from: docs/acceptance/README.md, docs/acceptance/caller-identity.md, docs/acceptance/caller-separation.md, docs/acceptance/spine-convergence.md, docs/adr/0205-bounded-jwks-staleness.md, docs/adr/0206-oidc-authn-module.md, docs/adr/0209-schedule-origin-run-context.md, docs/adr/0212-caller-ownership-enforcement.md, docs/adr/0213-driver-caller-ownership.md, docs/adr/README.md, docs/architecture.md, docs/design/principles.md, docs/development-process.md, docs/usage/mecated.md, engine/CHANGELOG.md, engine/COMPATIBILITY.md) - [ADR 0046 — Guardrails slot enables (configure = enable)](docs/adr/0046-guardrails-slot-enable.md): ADR 0046 — Guardrails slot enables (configure = enable) (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0049-guardrails-remove-maxchecks.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md, docs/usage/guardrails.md, docs/usage/mecated.md, docs/usage/model-routing.md) - [ADR 0086 — Remove the output-economy control surface](docs/adr/0086-remove-output-economy-control.md): ADR 0086 — Remove the output-economy control surface (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0041-output-economy-default-prompt.md, docs/adr/0089-cli-clean-break-grammar.md, docs/adr/README.md, docs/architecture.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) +- [ADR 0204 — Caller identity: accept a principal, thread it everywhere, record the owner](docs/adr/0204-caller-identity-threading.md): ADR 0204 — Caller identity: accept a principal, thread it everywhere, record the owner (linked from: docs/acceptance/README.md, docs/acceptance/caller-identity.md, docs/acceptance/caller-separation.md, docs/acceptance/spine-convergence.md, docs/adr/0205-bounded-jwks-staleness.md, docs/adr/0206-oidc-authn-module.md, docs/adr/0209-schedule-origin-run-context.md, docs/adr/0212-caller-ownership-enforcement.md, docs/adr/0213-driver-caller-ownership.md, docs/adr/README.md, docs/architecture.md, docs/design/principles.md, docs/development-process.md, docs/usage/mecated.md, engine/CHANGELOG.md, engine/COMPATIBILITY.md) - [ADR 0113 — Operator-configured MCP authentication profiles](docs/adr/0113-operator-mcp-auth-profiles.md): ADR 0113 — Operator-configured MCP authentication profiles (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/README.md, docs/architecture/extensibility.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md, docs/usage/mecated.md) - [ADR 0024 — System-Prompt Research and Enhancement](docs/adr/0024-system-prompt-research.md): ADR 0024 — System-Prompt Research and Enhancement (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0007-twelve-patterns-audit.md, docs/adr/0041-output-economy-default-prompt.md, docs/adr/0054-reasoning-rebalance-default-prompt.md, docs/adr/0070-model-visible-affordance-gate.md, docs/adr/README.md, docs/design/IMPLEMENTATION-NOTES.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0015 — Background subagents and per-child cancel](docs/adr/0015-background-subagents.md): ADR 0015 — Background subagents and per-child cancel (linked from: docs/acceptance/README.md, docs/acceptance/session-continuity-ux.md, docs/acceptance/spine-convergence.md, docs/adr/0013-agent-definitions.md, docs/adr/0014-agent-teams.md, docs/adr/0027-cloud-native.md, docs/adr/0201-background-bash.md, docs/adr/0217-session-discovery-continuation.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) @@ -43,15 +43,15 @@ - [ADR 0050 — Remove the guardrails oversized-content inspection skip](docs/adr/0050-guardrails-remove-maxcontentbytes.md): ADR 0050 — Remove the guardrails oversized-content inspection skip (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/README.md, docs/design/IMPLEMENTATION-NOTES.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0221 — Read-only environment credential source and OAuth refresh posture](docs/adr/0221-read-only-credential-source.md): ADR 0221 — Read-only environment credential source and OAuth refresh posture (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/README.md, docs/design/IMPLEMENTATION-NOTES.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0106 — Optional completed-trajectory learning seam](docs/adr/0106-optional-learning-seam.md): ADR 0106 — Optional completed-trajectory learning seam (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0109-staged-learning-proposals.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md, engine/CHANGELOG.md) -- [ADR 0031 — The operator-gated semantic subagent model router](docs/adr/0031-subagent-model-router.md): ADR 0031 — The operator-gated semantic subagent model router (linked from: README.md, docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0030-model-selection-heuristics.md, docs/adr/0034-team-parallel-model-routing.md, docs/adr/0035-per-delegation-model-surface.md, docs/adr/0042-taxonomy-gated-model-router.md, docs/adr/0066-route-unpinned-and-writable-delegations.md, docs/adr/0083-routing-reason-on-delegation-start.md, docs/adr/README.md, docs/architecture/providers.md, docs/design/principles.md, docs/development-process.md, docs/usage/mecated.md, docs/usage/model-routing.md) - [ADR 0219 — Qualify the official MCP SDK authorization-code profile](docs/adr/0219-mcp-oauth-sdk-profile.md): ADR 0219 — Qualify the official MCP SDK authorization-code profile (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0112-mcp-oauth-loopback-runtime.md, docs/adr/0220-mcp-oauth-controller.md, docs/adr/0223-mcp-sdk-transport-error-semantics.md, docs/adr/README.md, docs/architecture/extensibility.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) -- [ADR 0030 — Layered model-selection heuristics](docs/adr/0030-model-selection-heuristics.md): ADR 0030 — Layered model-selection heuristics (linked from: README.md, docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0031-subagent-model-router.md, docs/adr/0032-worktree-binding.md, docs/adr/0034-team-parallel-model-routing.md, docs/adr/0046-guardrails-slot-enable.md, docs/adr/0066-route-unpinned-and-writable-delegations.md, docs/adr/0069-plan-approval-gate.md, docs/adr/README.md, docs/architecture/api-surface.md, docs/architecture/providers.md, docs/design/principles.md, docs/development-process.md, docs/usage/mecated.md) - [ADR 0220 — Adapter-local MCP OAuth controller](docs/adr/0220-mcp-oauth-controller.md): ADR 0220 — Adapter-local MCP OAuth controller (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0112-mcp-oauth-loopback-runtime.md, docs/adr/0221-read-only-credential-source.md, docs/adr/README.md, docs/architecture/extensibility.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) -- [ADR 0212 — Enforce caller ownership at every application access path](docs/adr/0212-caller-ownership-enforcement.md): ADR 0212 — Enforce caller ownership at every application access path (linked from: docs/acceptance/README.md, docs/acceptance/caller-separation.md, docs/acceptance/spine-convergence.md, docs/adr/0213-driver-caller-ownership.md, docs/adr/README.md, docs/architecture.md, docs/design/IMPLEMENTATION-NOTES.md, docs/design/principles.md, docs/development-process.md, engine/CHANGELOG.md) +- [ADR 0031 — The operator-gated semantic subagent model router](docs/adr/0031-subagent-model-router.md): ADR 0031 — The operator-gated semantic subagent model router (linked from: README.md, docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0030-model-selection-heuristics.md, docs/adr/0034-team-parallel-model-routing.md, docs/adr/0035-per-delegation-model-surface.md, docs/adr/0042-taxonomy-gated-model-router.md, docs/adr/0066-route-unpinned-and-writable-delegations.md, docs/adr/0083-routing-reason-on-delegation-start.md, docs/adr/README.md, docs/architecture/providers.md, docs/design/principles.md, docs/development-process.md, docs/usage/mecated.md, docs/usage/model-routing.md) +- [ADR 0030 — Layered model-selection heuristics](docs/adr/0030-model-selection-heuristics.md): ADR 0030 — Layered model-selection heuristics (linked from: README.md, docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0031-subagent-model-router.md, docs/adr/0032-worktree-binding.md, docs/adr/0034-team-parallel-model-routing.md, docs/adr/0046-guardrails-slot-enable.md, docs/adr/0066-route-unpinned-and-writable-delegations.md, docs/adr/0069-plan-approval-gate.md, docs/adr/README.md, docs/architecture/api-surface.md, docs/architecture/providers.md, docs/design/principles.md, docs/development-process.md, docs/usage/mecated.md) - [ADR 0053 — Flip guardrails default mode from advisory to block](docs/adr/0053-guardrails-default-block.md): ADR 0053 — Flip guardrails default mode from advisory to block (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0060-guardrails-bash-default.md, docs/adr/0061-guardrails-human-override.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md, docs/usage/guardrails.md) - [ADR 0049 — Remove the guardrails per-session checker call-count cap](docs/adr/0049-guardrails-remove-maxchecks.md): ADR 0049 — Remove the guardrails per-session checker call-count cap (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0021-guardrails.md, docs/adr/0050-guardrails-remove-maxcontentbytes.md, docs/adr/0051-guardrails-advisory-tui-visibility.md, docs/adr/0052-guardrails-checker-down-toggle.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0012 — Conversation compaction](docs/adr/0012-compaction.md): ADR 0012 — Conversation compaction (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0011-soul-and-user-model.md, docs/adr/0027-cloud-native.md, docs/adr/0043-ephemeral-turn0-instruction-fragments.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0051 — Surface advisory guardrail findings to the TUI](docs/adr/0051-guardrails-advisory-tui-visibility.md): ADR 0051 — Surface advisory guardrail findings to the TUI (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0021-guardrails.md, docs/adr/0053-guardrails-default-block.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md, docs/usage/guardrails.md) +- [ADR 0212 — Enforce caller ownership at every application access path](docs/adr/0212-caller-ownership-enforcement.md): ADR 0212 — Enforce caller ownership at every application access path (linked from: docs/acceptance/README.md, docs/acceptance/caller-separation.md, docs/acceptance/spine-convergence.md, docs/adr/0213-driver-caller-ownership.md, docs/adr/README.md, docs/architecture.md, docs/design/IMPLEMENTATION-NOTES.md, docs/design/principles.md, docs/development-process.md, engine/CHANGELOG.md) - [ADR 0018 — Performance observability](docs/adr/0018-perf-observability.md): ADR 0018 — Performance observability (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0019-perf-tracking.md, docs/adr/0020-diagnostics.md, docs/adr/0045-explicit-bucket-latency-histograms.md, docs/adr/0098-headless-telemetry.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md, docs/perf-measurement-survey.md) - [ADR 0112 — Host-owned loopback MCP OAuth login](docs/adr/0112-mcp-oauth-loopback-runtime.md): ADR 0112 — Host-owned loopback MCP OAuth login (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0113-operator-mcp-auth-profiles.md, docs/adr/README.md, docs/architecture/extensibility.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0033 — Dirty-aware read-only fork (uncommitted-state overlay)](docs/adr/0033-dirty-aware-readonly-fork.md): ADR 0033 — Dirty-aware read-only fork (uncommitted-state overlay) (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0065-conversation-fork.md, docs/adr/README.md, docs/architecture/parallelism.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) @@ -59,22 +59,24 @@ - [ADR 0020 — Diagnostics, audit, and the global-slog ban](docs/adr/0020-diagnostics.md): ADR 0020 — Diagnostics, audit, and the global-slog ban (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0018-perf-observability.md, docs/adr/0019-perf-tracking.md, docs/adr/0087-mecatui-staged-transport-migration.md, docs/adr/0104-session-family-physical-naming.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0010 — Semantic memory recall: BM25 shipped, semantic deferred](docs/adr/0010-semantic-memory-recall.md): ADR 0010 — Semantic memory recall: BM25 shipped, semantic deferred (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0008-memory-on-by-default.md, docs/adr/0009-tiered-memory.md, docs/adr/0011-soul-and-user-model.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0019 — Long-term performance and resource regression tracking](docs/adr/0019-perf-tracking.md): ADR 0019 — Long-term performance and resource regression tracking (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0018-perf-observability.md, docs/adr/0020-diagnostics.md, docs/adr/0045-explicit-bucket-latency-histograms.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) -- [ADR 0042 — Taxonomy-gated subagent model router (enable by config, not a flag)](docs/adr/0042-taxonomy-gated-model-router.md): ADR 0042 — Taxonomy-gated subagent model router (enable by config, not a flag) (linked from: README.md, docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0031-subagent-model-router.md, docs/adr/0046-guardrails-slot-enable.md, docs/adr/0066-route-unpinned-and-writable-delegations.md, docs/adr/README.md, docs/architecture/providers.md, docs/design/principles.md, docs/development-process.md, docs/usage/guardrails.md, docs/usage/mecated.md, docs/usage/model-routing.md) - [ADR 0007 — Twelve agentic-harness patterns: pluggability audit](docs/adr/0007-twelve-patterns-audit.md): ADR 0007 — Twelve agentic-harness patterns: pluggability audit (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0024-system-prompt-research.md, docs/adr/0069-plan-approval-gate.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0032 — First-class worktree binding for a session](docs/adr/0032-worktree-binding.md): ADR 0032 — First-class worktree binding for a session (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0033-dirty-aware-readonly-fork.md, docs/adr/README.md, docs/architecture/parallelism.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) -- [ADR 0055 — Reasoning-effort knob](docs/adr/0055-reasoning-effort.md): ADR 0055 — Reasoning-effort knob (linked from: docs/acceptance/README.md, docs/acceptance/schedule-tool.md, docs/acceptance/spine-convergence.md, docs/adr/0064-toolhive-llm-gateway-provider.md, docs/adr/0067-openai-chat-completions-adapter.md, docs/adr/0068-effort-change-via-fork.md, docs/adr/0100-provider-prompt-caching.md, docs/adr/README.md, docs/design/principles.md, docs/development-process.md, docs/usage/guardrails.md, docs/usage/mecak8s.md, docs/usage/mecated.md, engine/CHANGELOG.md) - [ADR 0107 — Live operator profiles and reversible memory lifecycle](docs/adr/0107-operator-profile-memory-lifecycle.md): ADR 0107 — Live operator profiles and reversible memory lifecycle (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0109-staged-learning-proposals.md, docs/adr/0227-dream-consolidation-safety-boundary.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) +- [ADR 0042 — Taxonomy-gated subagent model router (enable by config, not a flag)](docs/adr/0042-taxonomy-gated-model-router.md): ADR 0042 — Taxonomy-gated subagent model router (enable by config, not a flag) (linked from: README.md, docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0031-subagent-model-router.md, docs/adr/0046-guardrails-slot-enable.md, docs/adr/0066-route-unpinned-and-writable-delegations.md, docs/adr/README.md, docs/architecture/providers.md, docs/design/principles.md, docs/development-process.md, docs/usage/guardrails.md, docs/usage/mecated.md, docs/usage/model-routing.md) - [ADR 0052 — Global guardrails checker-down posture toggle](docs/adr/0052-guardrails-checker-down-toggle.md): ADR 0052 — Global guardrails checker-down posture toggle (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md, docs/usage/guardrails.md) - [ADR 0008 — Memory on by default in the embedded mecatui server](docs/adr/0008-memory-on-by-default.md): ADR 0008 — Memory on by default in the embedded mecatui server (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0009-tiered-memory.md, docs/adr/0010-semantic-memory-recall.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0013 — Agent definitions (Tier 1)](docs/adr/0013-agent-definitions.md): ADR 0013 — Agent definitions (Tier 1) (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0014-agent-teams.md, docs/adr/0015-background-subagents.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0201 — Background Bash commands](docs/adr/0201-background-bash.md): ADR 0201 — Background Bash commands (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/README.md, docs/architecture/ports.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0025 — mecatui UX Discoverability](docs/adr/0025-ux-discoverability.md): ADR 0025 — mecatui UX Discoverability (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0026-clipboard-image-paste.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md, docs/tui.md) - [ADR 0026 — Clipboard Image Paste](docs/adr/0026-clipboard-image-paste.md): ADR 0026 — Clipboard Image Paste (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0025-ux-discoverability.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md, docs/tui.md) +- [ADR 0055 — Reasoning-effort knob](docs/adr/0055-reasoning-effort.md): ADR 0055 — Reasoning-effort knob (linked from: docs/acceptance/README.md, docs/acceptance/schedule-tool.md, docs/acceptance/spine-convergence.md, docs/adr/0064-toolhive-llm-gateway-provider.md, docs/adr/0067-openai-chat-completions-adapter.md, docs/adr/0068-effort-change-via-fork.md, docs/adr/0100-provider-prompt-caching.md, docs/adr/README.md, docs/design/principles.md, docs/development-process.md, docs/usage/guardrails.md, docs/usage/mecak8s.md, docs/usage/mecated.md, engine/CHANGELOG.md) - [ADR 0054 — Reasoning rebalance of the default-tone prompt](docs/adr/0054-reasoning-rebalance-default-prompt.md): ADR 0054 — Reasoning rebalance of the default-tone prompt (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0086-remove-output-economy-control.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) -- [ADR 0062 — Out-of-band approve-once for guardrail blocks](docs/adr/0062-guardrails-approve-once.md): ADR 0062 — Out-of-band approve-once for guardrail blocks (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0061-guardrails-human-override.md, docs/adr/0069-plan-approval-gate.md, docs/adr/0080-guardrail-routed-escape-checking.md, docs/adr/README.md, docs/architecture/agent-loop.md, docs/design/IMPLEMENTATION-NOTES.md, docs/design/principles.md, docs/development-process.md, engine/CHANGELOG.md) +- [ADR 0233 — Studio: the Atrium workspace as mecatl's daemon-only web client](docs/adr/0233-studio-atrium-module.md): ADR 0233 — Studio: the Atrium workspace as mecatl's daemon-only web client (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0234-studio-server-backed-chats.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) +- [ADR 0234 — Studio's chat list is the daemon's session store](docs/adr/0234-studio-server-backed-chats.md): ADR 0234 — Studio's chat list is the daemon's session store (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0233-studio-atrium-module.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0029 — Repo-map Tree-sitter Binding](docs/adr/0029-repomap-tree-sitter.md): ADR 0029 — Repo-map Tree-sitter Binding (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0068 — Change reasoning effort via conversation fork (keep the transcript)](docs/adr/0068-effort-change-via-fork.md): ADR 0068 — Change reasoning effort via conversation fork (keep the transcript) (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) - [ADR 0098 — Telemetry for the headless binaries (mecatequi, mecak8s)](docs/adr/0098-headless-telemetry.md): ADR 0098 — Telemetry for the headless binaries (mecatequi, mecak8s) (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/README.md, docs/design/PRODUCTION-READINESS.md, docs/design/principles.md, docs/development-process.md) +- [ADR 0062 — Out-of-band approve-once for guardrail blocks](docs/adr/0062-guardrails-approve-once.md): ADR 0062 — Out-of-band approve-once for guardrail blocks (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0061-guardrails-human-override.md, docs/adr/0069-plan-approval-gate.md, docs/adr/0080-guardrail-routed-escape-checking.md, docs/adr/README.md, docs/architecture/agent-loop.md, docs/design/IMPLEMENTATION-NOTES.md, docs/design/principles.md, docs/development-process.md, engine/CHANGELOG.md) - [ADR 0209 — Attribute schedule origins through the run context](docs/adr/0209-schedule-origin-run-context.md): ADR 0209 — Attribute schedule origins through the run context (linked from: docs/acceptance/README.md, docs/acceptance/schedule-shared-catalog.md, docs/acceptance/spine-convergence.md, docs/adr/0075-fire-result-delivery.md, docs/adr/README.md, docs/architecture.md, docs/design/principles.md, docs/development-process.md, engine/CHANGELOG.md) - [ADR 0075 — Fire-result delivery: a scheduled fire reports back into the originating chat](docs/adr/0075-fire-result-delivery.md): ADR 0075 — Fire-result delivery: a scheduled fire reports back into the originating chat (linked from: docs/acceptance/README.md, docs/acceptance/fire-result-delivery.md, docs/acceptance/schedule-shared-catalog.md, docs/acceptance/spine-convergence.md, docs/adr/0076-schedule-shared-catalog.md, docs/adr/0096-live-feed-reconnect.md, docs/adr/0097-scheduled-fire-inflight-state.md, docs/adr/0209-schedule-origin-run-context.md, docs/adr/README.md, docs/architecture.md, docs/design/principles.md, docs/development-process.md) - [ADR 0206 — Ship reusable OIDC caller identity as an opt-in module](docs/adr/0206-oidc-authn-module.md): ADR 0206 — Ship reusable OIDC caller identity as an opt-in module (linked from: README.md, docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/README.md, docs/architecture.md, docs/design/principles.md, docs/development-process.md, engine/CHANGELOG.md) @@ -89,8 +91,8 @@ - [ADR 0039 — Parallel single-branch auto-merge](docs/adr/0039-parallel-auto-merge.md): ADR 0039 — Parallel single-branch auto-merge (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0040-writable-subagent-and-serialized-merge.md, docs/adr/README.md, docs/architecture/parallelism.md, docs/cloud-native-harness-systems.md, docs/design/principles.md, docs/development-process.md, docs/scoped-resource-grants.md, docs/usage/mecated.md) - [ADR 0217 — Session discovery uses durable kind metadata and an authoritative transcript](docs/adr/0217-session-discovery-continuation.md): ADR 0217 — Session discovery uses durable kind metadata and an authoritative transcript (linked from: docs/acceptance/README.md, docs/acceptance/session-continuity-ux.md, docs/acceptance/session-storage-continuity.md, docs/acceptance/spine-convergence.md, docs/adr/0226-session-storage-maintenance.md, docs/adr/README.md, docs/design/principles.md, docs/development-process.md, engine/CHANGELOG.md) - [ADR 0095 — Root-aware project trust](docs/adr/0095-root-aware-project-trust.md): ADR 0095 — Root-aware project trust (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0092-no-project-trust-pin.md, docs/adr/0094-opt-in-project-ingestion.md, docs/adr/0202-diagnostic-only-posture-reporting.md, docs/adr/README.md, docs/design/principles.md, docs/development-process.md, docs/usage/mecak8s.md, docs/usage/mecated.md, docs/usage/mecatequi-ci.md, docs/usage/workspace-trust.md) -- [ADR 0213 — Enforce caller ownership at remote driver boundaries](docs/adr/0213-driver-caller-ownership.md): ADR 0213 — Enforce caller ownership at remote driver boundaries (linked from: docs/acceptance/README.md, docs/acceptance/caller-separation.md, docs/acceptance/spine-convergence.md, docs/adr/0212-caller-ownership-enforcement.md, docs/adr/README.md, docs/architecture.md, docs/design/principles.md, docs/development-process.md) - [ADR 0089 — CLI clean break: one canonical spelling per action](docs/adr/0089-cli-clean-break-grammar.md): ADR 0089 — CLI clean break: one canonical spelling per action (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0086-remove-output-economy-control.md, docs/adr/0087-mecatui-staged-transport-migration.md, docs/adr/0103-mecatui-seed-prompt.md, docs/adr/README.md, docs/architecture.md, docs/design/principles.md, docs/development-process.md) +- [ADR 0213 — Enforce caller ownership at remote driver boundaries](docs/adr/0213-driver-caller-ownership.md): ADR 0213 — Enforce caller ownership at remote driver boundaries (linked from: docs/acceptance/README.md, docs/acceptance/caller-separation.md, docs/acceptance/spine-convergence.md, docs/adr/0212-caller-ownership-enforcement.md, docs/adr/README.md, docs/architecture.md, docs/design/principles.md, docs/development-process.md) - [ADR 0093 — Ship the real LLM provider adapters as opt-in Go modules under provider/](docs/adr/0093-provider-modules.md): ADR 0093 — Ship the real LLM provider adapters as opt-in Go modules under provider/ (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0100-provider-prompt-caching.md, docs/adr/0216-provider-session-correlation-header.md, docs/adr/README.md, docs/architecture.md, docs/design/principles.md, docs/development-process.md) - [ADR 0111 — Hardened publication and recovery for agent-owned skills](docs/adr/0111-hardened-agent-owned-skill-publication.md): ADR 0111 — Hardened publication and recovery for agent-owned skills (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0110-evaluated-agent-owned-skills.md, docs/adr/0224-validated-automatic-skill-activation.md, docs/adr/README.md, docs/architecture.md, docs/design/principles.md, docs/development-process.md) - [ADR 0043 — Ephemeral turn-0 instruction fragments](docs/adr/0043-ephemeral-turn0-instruction-fragments.md): ADR 0043 — Ephemeral turn-0 instruction fragments (linked from: docs/acceptance/README.md, docs/acceptance/spine-convergence.md, docs/adr/0009-tiered-memory.md, docs/adr/0011-soul-and-user-model.md, docs/adr/0012-compaction.md, docs/adr/0081-rules-source-port.md, docs/adr/README.md, docs/design/IMPLEMENTATION-NOTES.md, docs/design/principles.md, docs/development-process.md) @@ -261,8 +263,8 @@ _Associative trails (Bush 1945): a topologically-valid path through each cluster 27. [Extensibility — MCP, tools & progressive disclosure](docs/architecture/extensibility.md) 28. [mecatui — the terminal UI for mecatl](docs/tui.md) 29. [Platform principles](docs/design/principles.md) -30. [ADR 0005 — Driver seams: ports, the gRPC driver protocol, and conformance](docs/adr/0005-driver-seams.md) -31. [3. Running the server (mecated)](docs/usage/mecated.md) +30. [3. Running the server (mecated)](docs/usage/mecated.md) +31. [ADR 0005 — Driver seams: ports, the gRPC driver protocol, and conformance](docs/adr/0005-driver-seams.md) 32. [ADR 0014 — Agent teams](docs/adr/0014-agent-teams.md) 33. [ADR 0043 — Ephemeral turn-0 instruction fragments](docs/adr/0043-ephemeral-turn0-instruction-fragments.md) 34. [mecatl](README.md) @@ -309,8 +311,8 @@ _Associative trails (Bush 1945): a topologically-valid path through each cluster 75. [ADR 0025 — mecatui UX Discoverability](docs/adr/0025-ux-discoverability.md) 76. [ADR 0026 — Clipboard Image Paste](docs/adr/0026-clipboard-image-paste.md) 77. [ADR 0062 — Out-of-band approve-once for guardrail blocks](docs/adr/0062-guardrails-approve-once.md) -78. [ADR 0007 — Twelve agentic-harness patterns: pluggability audit](docs/adr/0007-twelve-patterns-audit.md) -79. [ADR 0095 — Root-aware project trust](docs/adr/0095-root-aware-project-trust.md) +78. [ADR 0095 — Root-aware project trust](docs/adr/0095-root-aware-project-trust.md) +79. [ADR 0007 — Twelve agentic-harness patterns: pluggability audit](docs/adr/0007-twelve-patterns-audit.md) 80. [ADR 0212 — Enforce caller ownership at every application access path](docs/adr/0212-caller-ownership-enforcement.md) 81. [Configuration reference](docs/configuration-reference.md) 82. [ADR 0071 — Seamless model switch: always keep the conversation](docs/adr/0071-seamless-model-switch.md) @@ -336,15 +338,15 @@ _Associative trails (Bush 1945): a topologically-valid path through each cluster 102. [ADR 0110 — Evaluated, versioned agent-owned skills](docs/adr/0110-evaluated-agent-owned-skills.md) 103. [ADR 0213 — Enforce caller ownership at remote driver boundaries](docs/adr/0213-driver-caller-ownership.md) 104. [ADR 0100 — Provider-side conversation prompt caching (Anthropic, OpenAI, OpenRouter, openaichat)](docs/adr/0100-provider-prompt-caching.md) -105. [ADR 0219 — Qualify the official MCP SDK authorization-code profile](docs/adr/0219-mcp-oauth-sdk-profile.md) -106. [ADR 0046 — Guardrails slot enables (configure = enable)](docs/adr/0046-guardrails-slot-enable.md) -107. [9. The gRPC API](docs/usage/grpc-api.md) -108. [10. The HTTP / SSE API](docs/usage/http-sse-api.md) +105. [ADR 0046 — Guardrails slot enables (configure = enable)](docs/adr/0046-guardrails-slot-enable.md) +106. [9. The gRPC API](docs/usage/grpc-api.md) +107. [10. The HTTP / SSE API](docs/usage/http-sse-api.md) +108. [ADR 0219 — Qualify the official MCP SDK authorization-code profile](docs/adr/0219-mcp-oauth-sdk-profile.md) 109. [ADR 0209 — Attribute schedule origins through the run context](docs/adr/0209-schedule-origin-run-context.md) 110. [Agent identity: mecatl as its own SPIFFE trust domain](docs/agent-identity-model.md) 111. [ADR 0054 — Reasoning rebalance of the default-tone prompt](docs/adr/0054-reasoning-rebalance-default-prompt.md) -112. [ADR 0215 — OpenAI subscription with a manual access token](docs/adr/0215-openai-subscription-manual-token.md) -113. [ADR 0039 — Parallel single-branch auto-merge](docs/adr/0039-parallel-auto-merge.md) +112. [ADR 0039 — Parallel single-branch auto-merge](docs/adr/0039-parallel-auto-merge.md) +113. [ADR 0215 — OpenAI subscription with a manual access token](docs/adr/0215-openai-subscription-manual-token.md) 114. [ADR 0082 — Factory MCP wiring for the one-shot mains](docs/adr/0082-factory-mcp-wiring.md) 115. [ADR 0045 — Explicit-bucket latency histograms (zero-config quantiles on /metrics)](docs/adr/0045-explicit-bucket-latency-histograms.md) 116. [ADR 0206 — Ship reusable OIDC caller identity as an opt-in module](docs/adr/0206-oidc-authn-module.md) @@ -357,8 +359,8 @@ _Associative trails (Bush 1945): a topologically-valid path through each cluster 123. [ADR 0053 — Flip guardrails default mode from advisory to block](docs/adr/0053-guardrails-default-block.md) 124. [ADR 0106 — Optional completed-trajectory learning seam](docs/adr/0106-optional-learning-seam.md) 125. [Scoped resource grants: a filesystem and tool substrate](docs/scoped-resource-grants.md) -126. [ADR 0107 — Live operator profiles and reversible memory lifecycle](docs/adr/0107-operator-profile-memory-lifecycle.md) -127. [ADR 0216 — Correlate provider requests with the active session](docs/adr/0216-provider-session-correlation-header.md) +126. [ADR 0216 — Correlate provider requests with the active session](docs/adr/0216-provider-session-correlation-header.md) +127. [ADR 0107 — Live operator profiles and reversible memory lifecycle](docs/adr/0107-operator-profile-memory-lifecycle.md) 128. [1. Prerequisites & install](docs/usage/install.md) 129. [ADR 0065 — Conversation fork: peer session from a history snapshot](docs/adr/0065-conversation-fork.md) 130. [ADR 0061 — Human one-shot guardrail override (/guardrail-allow)](docs/adr/0061-guardrails-human-override.md) @@ -378,8 +380,8 @@ _Associative trails (Bush 1945): a topologically-valid path through each cluster 144. [ADR 0203 — Neutral permanent-provider-error signal](docs/adr/0203-permanent-provider-error-signal.md) 145. [ADR 0105 — Built-in bounded WebFetch](docs/adr/0105-built-in-webfetch.md) 146. [ADR 0047 — Absolute path resolution inside the workspace root](docs/adr/0047-absolute-path-resolution.md) -147. [ADR 0113 — Operator-configured MCP authentication profiles](docs/adr/0113-operator-mcp-auth-profiles.md) -148. [ADR 0092 — Project-trust suppression pin](docs/adr/0092-no-project-trust-pin.md) +147. [ADR 0092 — Project-trust suppression pin](docs/adr/0092-no-project-trust-pin.md) +148. [ADR 0113 — Operator-configured MCP authentication profiles](docs/adr/0113-operator-mcp-auth-profiles.md) 149. [ADR 0207 — Operator-owned exact context-window overrides](docs/adr/0207-context-window-overrides.md) 150. [ADR 0202 — Diagnostic-only posture reporting](docs/adr/0202-diagnostic-only-posture-reporting.md) 151. [Caller identity — completed acceptance record](docs/acceptance/caller-identity.md) @@ -407,9 +409,9 @@ _Associative trails (Bush 1945): a topologically-valid path through each cluster 173. [Example skills](docs/examples/README.md) 174. [ADR 0104 — Session families get a bounded, injective, non-reversible physical name](docs/adr/0104-session-family-physical-naming.md) 175. [Agent identity, part 2: the outbound hop](docs/agent-identity-outbound.md) -176. [ADR 0052 — Global guardrails checker-down posture toggle](docs/adr/0052-guardrails-checker-down-toggle.md) -177. [ADR 0067 — OpenAI Chat Completions adapter (OpenCode Go provider)](docs/adr/0067-openai-chat-completions-adapter.md) -178. [ADR 0083 — Expose the routing miss/gate reason on delegation-start events](docs/adr/0083-routing-reason-on-delegation-start.md) +176. [ADR 0067 — OpenAI Chat Completions adapter (OpenCode Go provider)](docs/adr/0067-openai-chat-completions-adapter.md) +177. [ADR 0083 — Expose the routing miss/gate reason on delegation-start events](docs/adr/0083-routing-reason-on-delegation-start.md) +178. [ADR 0052 — Global guardrails checker-down posture toggle](docs/adr/0052-guardrails-checker-down-toggle.md) 179. [ADR 0101 — OpenAI reasoning multiplicity: adapter-packed items + bounded encrypted-content repair](docs/adr/0101-openai-reasoning-multiplicity.md) 180. [ADR 0079 — Converge delegation observability on two tiers (bounded previews for Subagent/Parallel)](docs/adr/0079-delegation-observability-convergence.md) 181. [ADR 0229 — Redis migration uses fenced renewable ownership and indexed coverage](docs/adr/0229-redis-migration-fencing.md) @@ -445,9 +447,11 @@ _Associative trails (Bush 1945): a topologically-valid path through each cluster 211. [Session continuity UX — acceptance plan](docs/acceptance/session-continuity-ux.md) 212. [Session storage continuity — acceptance plan](docs/acceptance/session-storage-continuity.md) 213. [Spine convergence — acceptance plan](docs/acceptance/spine-convergence.md) -214. [Commit style](docs/examples/skills/commit-style/SKILL.md) -215. [Go table-driven tests](docs/examples/skills/go-table-tests/SKILL.md) -216. [mecatl live e2e suite](e2e/README.md) +214. [ADR 0233 — Studio: the Atrium workspace as mecatl's daemon-only web client](docs/adr/0233-studio-atrium-module.md) +215. [ADR 0234 — Studio's chat list is the daemon's session store](docs/adr/0234-studio-server-backed-chats.md) +216. [Commit style](docs/examples/skills/commit-style/SKILL.md) +217. [Go table-driven tests](docs/examples/skills/go-table-tests/SKILL.md) +218. [mecatl live e2e suite](e2e/README.md) ## Known gaps diff --git a/studio/.env.example b/studio/.env.example new file mode 100644 index 0000000000..c403659c3d --- /dev/null +++ b/studio/.env.example @@ -0,0 +1,55 @@ +# Mecatl Studio — environment variables +# Copy this file to .env.local and fill in the values. +# +# Studio has two pure deployment modes: +# +# MANAGED (default): `npm run dev` (or `task studio:dev`) starts the local +# controller, which supervises a `mecated` it spawns from ../bin/mecated on a +# random loopback port with a generated bearer token. Requires `task build` +# at the repo root first. No variables are needed. +# +# EXTERNAL: set MECATL_BASE_URL and Studio proxies to that daemon instead. +# No controller runs; every local control surface (provider, model router, +# MCP gateway) answers 409 as owned by the deployment. + +# ── External mode ──────────────────────────────────────────────────────────── +# Base URL of an already-running mecated HTTP listener. Its presence selects +# external mode. +# MECATL_BASE_URL=https://mecated.internal.example.com:8081 + +# Bearer token for the external daemon (`mecated serve --auth-token ...`). +# Injected server-side only; the browser never sees or supplies it. +# MECATL_AUTH_TOKEN= + +# Absolute path every file and shell tool is rooted at, required on session +# create. Injected server-side so it never reaches the client bundle. +# MECATL_WORKSPACE=/absolute/path/to/workspace + +# ── Origin pinning (both modes) ────────────────────────────────────────────── +# Comma-separated list of origins Studio itself is served from. Requests whose +# Host or Origin fall outside this list are refused (CSRF/DNS-rebinding gate). +# MECATL_STUDIO_PUBLIC_ORIGIN=http://localhost:3000,http://127.0.0.1:3000 + +# Origin allowlist enforced by the local controller (managed mode). +# MECATL_STUDIO_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 + +# ── Managed mode options ───────────────────────────────────────────────────── +# Provider for the supervised daemon: mock, openrouter, or toolhive. When +# unset the controller probes for a local ToolHive LLM gateway and otherwise +# falls back to the offline mock provider. Credentials are never entered in +# Studio: mecated reads them from ~/.config/mecatl/auth.yaml. +# MECATL_STUDIO_PROVIDER= + +# Set to 1 to allow an MCP gateway URL on loopback HTTP (HTTPS is otherwise +# required). +# MECATL_ALLOW_INSECURE_LOOPBACK_MCP= + +# ── Branding (optional) ────────────────────────────────────────────────────── +# Display name used as the logo's alt text. +# BRAND_NAME= + +# URL of a custom logo image, proxied server-side by /brand/logo. +# BRAND_LOGO_URL= + +# URL of a custom favicon (ICO, PNG, SVG, JPEG). +# FAVICON_URL= diff --git a/studio/.gitignore b/studio/.gitignore new file mode 100644 index 0000000000..07027a34ca --- /dev/null +++ b/studio/.gitignore @@ -0,0 +1,57 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage +# Playwright/Cucumber E2E artifacts +test-results/ +playwright-report/ +blob-report/ + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# helm +*.tgz +helm/charts/ +helm/*.lock + +# Claude Code local settings +.claude/settings.local.json + +# task studio:dev's background log +dev.log diff --git a/studio/.nvmrc b/studio/.nvmrc new file mode 100644 index 0000000000..85e502778f --- /dev/null +++ b/studio/.nvmrc @@ -0,0 +1 @@ +22.22.0 diff --git a/studio/CLAUDE.md b/studio/CLAUDE.md new file mode 100644 index 0000000000..d4a59dee52 --- /dev/null +++ b/studio/CLAUDE.md @@ -0,0 +1,108 @@ +# CLAUDE.md — Mecatl Studio + +The web client for the mecatl harness: a Next.js app (App Router) serving the +Atrium workspace — Chats · Scheduled · Skills · Memory · Settings — against a +`mecated` daemon. See ADR 0233 (module + posture) and ADR 0234 (server-backed +chats). + +## Commands + +Run through the root Taskfile, not bare npm: + +```sh +task build # repo root first — studio's managed mode spawns ../bin/mecated +task studio:dev # start Studio + its mecated supervisor (background; logs in studio/dev.log) +task studio:stop # stop web server + controller + the mecated it supervises +task studio:test # vitest run + the hermetic server-tier suite (builds first) +task studio:lint # biome +task studio:typecheck +``` + +`npm run dev` = managed mode via `scripts/dev-local.mjs` (controller on :8788, +web on :3000). `npm run dev:web` = bare `next dev` (external mode or against an +already-running controller). Setting `MECATL_BASE_URL` selects external mode. +The hermetic suite (`npm run test:server`) builds Next first — a stale build is +the usual reason it fails mysteriously. + +## Module shape + +- `src/lib/protocol/` — the ONLY reader of raw daemon JSON: event decode + + StreamEvent translation, session inventory/transcript decoders, schedule + decode/encode. Generated TS proto bindings are deferred; this seam plus its + vitest suite is the stopgap. +- `src/lib/harness/client.ts` — browser transport: fetch + SSE buffering + + stream robustness (120s idle timeout, saw-result guard). +- `src/lib/server-proxy.ts` + `src/app/api/mecatl{,-control}/[...path]` — the + server tier: origin trust, header allowlists, bearer + workspace injection, + external-mode 409 policy. +- `scripts/local-controller.mjs` — managed-mode sidecar (supervises `mecated`, + owns model-router/MCP-gateway config + OAuth); policy helpers in + `src/lib/controller-security.mjs`. +- `src/features/agent/` — runtime-status provider + the daemon-backed hooks; + `src/app/workspace/**` — the five surfaces. + +## Rules that have teeth + +Each rule is backed by a test; break the rule and its test names you. + +1. **Daemon-only: an unreachable daemon renders offline, never demo data.** + There are no fixtures to fall back to — do not add any. The one sanctioned + exception is the explicit, default-off, clearly-labeled Labs mock content + (Settings → Labs → "Show mock features", `src/features/agent/mock-tour.ts`) + — an opt-in demo the user turns on, never a fallback for an unreachable + daemon. + (`tests/rendered-html.test.mjs`: unreachable daemon → friendly 503.) +2. **The workspace is resolved, never hardcoded and never browser-supplied.** + Managed: controller `/status`; external: `MECATL_WORKSPACE`; injected + server-side into session/team/schedule creation. + (hermetic: session creation carries the deployment workspace.) +3. **Credentials never cross the browser/controller boundary.** No key-paste + UI anywhere; `mecated` reads `~/.config/mecatl/auth.yaml`. The proxy's + header allowlist excludes `authorization` from the browser. + (hermetic: bearer injected server-side.) +4. **Controller mutations are server-only.** They require the server-set + `x-mecatl-studio-request` header, a loopback Host, and an allowlisted + Origin. (hermetic: CSRF/DNS-rebinding truth table.) +5. **External mode owns nothing locally.** Every control write answers 409. + (hermetic: control writes 409.) +6. **A failed turn renders as failed.** `result.stop === "error"` with no text + must never become a quiet success — the run_result event always reaches the + UI. (`src/lib/protocol/events.test.ts`.) +7. **Unknown event kinds are surfaced, never dropped.** A new daemon + capability shows up as "not rendered yet". (`events.test.ts`.) +8. **The memory panel is read-only.** A value typed into the UI would land in + turn-0 context bypassing injection scanning; the daemon has no write API by + design. Do not add an editor. +9. **Session rows obey the store.** Eligibility comes from row capabilities + (omitted = denied); a row is removed only by a complete inventory walk or a + 404; renames adopt the daemon's clamped echo. + (`src/lib/protocol/sessions.test.ts` + `use-agent-sessions`.) +10. **Schedule PUT replaces the whole spec.** Fields the form cannot edit ride + the row's `carried` spec and are re-encoded, or they are silently deleted. + (`src/lib/protocol/schedules.test.ts`: carried round-trip.) +11. **Requests are protojson; responses are stdlib JSON.** Never echo a decoded + response back as a request body. (`schedules.test.ts`: the asymmetry test.) +12. **Skills are project-scoped only.** The controller pins `--skills-dir` and + never passes `--skills-conventional`. + +## Gotchas + +- `npm test` runs vitest in watch mode; CI and `task studio:test` use + `npx vitest run` + `npm run test:server`. +- The controller restarts `mecated` on every config write; in-flight runs and + session ids die with it. Surfaces warn before writes that restart. +- FireNow (`POST /v1/schedules/{name}/fire`) is synchronous — the request lasts + the whole agent run. +- Live re-attach to a running session is impossible over HTTP today + (`StreamSessionLive` is gRPC-only): show running state from the inventory, + read the transcript at the end. + + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/studio/Taskfile.yml b/studio/Taskfile.yml new file mode 100644 index 0000000000..37a4fa37ac --- /dev/null +++ b/studio/Taskfile.yml @@ -0,0 +1,96 @@ +version: '3' + +# Mecatl Studio — the local web client for the harness. +# Run from the repo root via `task studio:*` (the root Taskfile includes this +# file under the `studio` namespace with dir: studio). +# +# Studio drives the mecated in THIS checkout: `task studio:dev` starts a +# supervisor that spawns ../bin/mecated against the repo root as its workspace, +# so `task build` must have run first. + +vars: + PORT: 3000 + LOG: dev.log + +tasks: + install: + desc: Install npm dependencies (skipped when package files are unchanged) + sources: + - package.json + - package-lock.json + generates: + - node_modules/.package-lock.json + cmds: + # Pinned via npx: a system npm 11 rewrites the lockfile in a shape + # npm 10 (what CI's Node 22 ships) rejects, breaking npm ci. + - npx -y npm@10.9.4 install + + dev: + desc: Start Studio + its mecated supervisor in the background (runs install if needed) + deps: [install] + cmds: + - | + if [ -z "${MECATL_BASE_URL:-}" ] && [ ! -x ../bin/mecated ]; then + echo "../bin/mecated not found — run 'task build' first" >&2 + exit 1 + fi + if lsof -t -i TCP:{{.PORT}} -s TCP:LISTEN > /dev/null 2>&1; then + echo "Studio already running — http://localhost:{{.PORT}}" + else + bash -c 'npm run dev >> {{.LOG}} 2>&1 & disown $!' + echo "Studio starting — http://localhost:{{.PORT}}" + echo "Logs: tail -f studio/{{.LOG}}" + fi + + stop: + desc: Stop Studio (web server + controller + the mecated it supervises) + cmds: + - | + # The controller (8788) owns the mecated child, so stopping it first + # lets the supervisor tear the daemon down rather than orphaning it. + for PORT in 8788 {{.PORT}}; do + PID=$(lsof -t -i TCP:$PORT -s TCP:LISTEN || true) + if [ -n "$PID" ]; then /bin/kill $PID && echo "Stopped listener on $PORT"; fi + done + + restart: + desc: Restart Studio + cmds: + - task: stop + - task: dev + + status: + desc: Show Studio dev server status + cmds: + - | + PID=$(lsof -t -i TCP:{{.PORT}} -s TCP:LISTEN || true) + if [ -n "$PID" ]; then + echo "Running (PID $PID) — http://localhost:{{.PORT}}" + else + echo "Not running" + fi + + build: + desc: Build Studio into studio/.next/ + deps: [install] + cmds: + - npm run build + + test: + desc: Run Studio's unit suite and the hermetic server-tier suite + deps: [install] + cmds: + - npx vitest run + - npm run test:server + + lint: + desc: Run Biome over studio/ + deps: [install] + cmds: + - npm run lint + + typecheck: + desc: Run TypeScript type-check over studio/ + deps: [install] + cmds: + - npm run typecheck diff --git a/studio/biome.json b/studio/biome.json new file mode 100644 index 0000000000..58eab024f1 --- /dev/null +++ b/studio/biome.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "includes": ["**", "!node_modules", "!.next", "!dist", "!build"] + }, + "overrides": [ + { + "includes": ["src/components/ui/sidebar.tsx"], + "linter": { + "rules": { + "suspicious": { + "noDocumentCookie": "off" + } + } + } + } + ], + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noUnknownAtRules": "off" + } + }, + "domains": { + "next": "recommended", + "react": "recommended" + } + }, + "css": { + "parser": { + "cssModules": false, + "tailwindDirectives": true + } + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/studio/components.json b/studio/components.json new file mode 100644 index 0000000000..f4fb4bea2d --- /dev/null +++ b/studio/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/studio/knip.ts b/studio/knip.ts new file mode 100644 index 0000000000..6b5cbbbf9b --- /dev/null +++ b/studio/knip.ts @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +import type { KnipConfig } from "knip"; + +const config: KnipConfig = { + entry: [ + "src/app/**/{page,layout,loading,error,not-found,global-error,route}.{ts,tsx}", + "src/**/*.test.{ts,tsx}", + ], + project: ["src/**/*.{ts,tsx}"], + paths: { + "@/*": ["src/*"], + }, + ignore: [ + // shadcn/ui components export all variants by convention — only a subset + // is used at any given time + "src/components/ui/**", + // Feature modules: internal exports consumed within the feature + "src/features/**", + ], + ignoreDependencies: [ + // Tailwind v4 is imported via CSS (@import "tailwindcss"), not JS + "tailwindcss", + // Used by shadcn/ui Form and Label components (in src/components/ui/ which knip ignores) + "react-hook-form", + // Used by shadcn/ui components (in src/components/ui/ which knip ignores) + "@radix-ui/react-avatar", + "@radix-ui/react-checkbox", + "@radix-ui/react-popover", + "@radix-ui/react-progress", + "@radix-ui/react-separator", + "@radix-ui/react-toggle", + "@radix-ui/react-toggle-group", + ], +}; + +export default config; diff --git a/studio/next.config.ts b/studio/next.config.ts new file mode 100644 index 0000000000..f8e1841c66 --- /dev/null +++ b/studio/next.config.ts @@ -0,0 +1,69 @@ +import type { NextConfig } from "next"; + +const isDev = process.env.NODE_ENV !== "production"; + +/** + * Content Security Policy header. + * Every daemon/controller call happens server-side through the /api/mecatl* + * proxy routes, so the browser CSP only needs 'self'. + */ +const cspHeader = ` + default-src 'self'; + script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ""}; + style-src 'self' 'unsafe-inline'; + img-src 'self' blob: data:; + font-src 'self'; + connect-src 'self'; + form-action 'self'; + frame-ancestors 'none'; + base-uri 'self'; + object-src 'none'; + ${process.env.NODE_ENV === "production" ? "upgrade-insecure-requests;" : ""} +` + .replace(/\s{2,}/g, " ") + .trim(); + +/** + * Hostnames allowed to request dev-server assets, derived from the same + * MECATL_STUDIO_PUBLIC_ORIGIN allowlist the API proxy trusts — one knob for + * LAN access. Without this, Next's dev cross-origin protection 403s the + * /_next/* chunks for a non-localhost Host, so the page renders but never + * hydrates. Dev-only: `next start` has no such gate. + */ +const allowedDevOrigins = (process.env.MECATL_STUDIO_PUBLIC_ORIGIN || "") + .split(",") + .map((origin) => origin.trim()) + .filter(Boolean) + .map((origin) => { + try { + return new URL(origin).hostname; + } catch { + return origin; + } + }); + +const nextConfig: NextConfig = { + reactCompiler: true, + poweredByHeader: false, + ...(allowedDevOrigins.length > 0 ? { allowedDevOrigins } : {}), + async headers() { + return [ + { + source: "/(.*)", + headers: [ + { key: "Content-Security-Policy", value: cspHeader }, + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "X-Frame-Options", value: "DENY" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { + key: "Permissions-Policy", + value: "camera=(), microphone=(), geolocation=()", + }, + { key: "Cross-Origin-Resource-Policy", value: "same-origin" }, + ], + }, + ]; + }, +}; + +export default nextConfig; diff --git a/studio/package-lock.json b/studio/package-lock.json new file mode 100644 index 0000000000..8de5a49b01 --- /dev/null +++ b/studio/package-lock.json @@ -0,0 +1,8909 @@ +{ + "name": "mecatl-studio", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mecatl-studio", + "version": "0.1.0", + "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-checkbox": "^1.1.5", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@tiptap/extension-mention": "^3.29.2", + "@tiptap/extension-placeholder": "^3.29.2", + "@tiptap/pm": "^3.27.1", + "@tiptap/react": "^3.27.1", + "@tiptap/starter-kit": "^3.27.1", + "@tiptap/suggestion": "^3.29.2", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "cmdk": "^1.1.1", + "lucide-react": "^0.577.0", + "next": "16.3.1", + "next-themes": "^0.4.6", + "nuqs": "^2.8.1", + "react": "19.2.4", + "react-dom": "19.2.4", + "react-hook-form": "^7.72.1", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", + "server-only": "^0.0.1", + "shiki": "^4.4.3", + "sonner": "^2.0.7", + "tailwind-merge": "3.5.0", + "tiptap-markdown": "^0.9.0" + }, + "devDependencies": { + "@biomejs/biome": "2.4.10", + "@playwright/test": "^1.56.1", + "@tailwindcss/postcss": "^4", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^22.13.0", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "^6.0.0", + "@vitest/coverage-v8": "^4.0.8", + "babel-plugin-react-compiler": "1.0.0", + "jsdom": "^29.0.0", + "knip": "^6.1.1", + "postcss": "^8.5.18", + "tailwindcss": "^4", + "typescript": "^6.0.2", + "vite": "^8.0.16", + "vite-tsconfig-paths": "^6.0.0", + "vitest": "^4.0.8", + "vitest-fail-on-console": "^0.10.1" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.10.tgz", + "integrity": "sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.4.10", + "@biomejs/cli-darwin-x64": "2.4.10", + "@biomejs/cli-linux-arm64": "2.4.10", + "@biomejs/cli-linux-arm64-musl": "2.4.10", + "@biomejs/cli-linux-x64": "2.4.10", + "@biomejs/cli-linux-x64-musl": "2.4.10", + "@biomejs/cli-win32-arm64": "2.4.10", + "@biomejs/cli-win32-x64": "2.4.10" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.10.tgz", + "integrity": "sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.10.tgz", + "integrity": "sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.10.tgz", + "integrity": "sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.10.tgz", + "integrity": "sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.10.tgz", + "integrity": "sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.10.tgz", + "integrity": "sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.10.tgz", + "integrity": "sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.10.tgz", + "integrity": "sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.0.tgz", + "integrity": "sha512-5+5LEmFuY1AjXdYhmgjTJogtQnP1evJ1zrBZGUNZ0thkpwnnmKxcHdAMn/OtFjAb25zA+jKDVYVRl+5G7rjv1A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.8.tgz", + "integrity": "sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "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", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "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.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "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.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "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.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "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.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "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.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "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.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "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.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "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.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "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.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@next/env": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.1.tgz", + "integrity": "sha512-35G3xwkQUb2oETSDjFXGrVugknoayLFBh7vSE+yAcl9IP2zT9wyGwq7297AYHR11kJld807t5f8AJBs6WBzXsQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.1.tgz", + "integrity": "sha512-ABMIu2zQ7cnNIHm5ivKGwZwUrm0pAai3yiJ/gK/rF1c1VP9UOnj7XECbMKFdVKp9I9eMYq9NoDs1WXOoowxzJw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.1.tgz", + "integrity": "sha512-gNG21e/UnrroeScbY/QndUEdl0mF1FRibW7BBeYUz/5ABCepjqDdEdgr592vpzMtCn/m7FTjYq3TN4TpyDnutw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.1.tgz", + "integrity": "sha512-6B6Lw016iwNUQuaJoraMMTLh6TwHzFUtxipSScD1F3YyymcrRWkobodRS2ftIOkF5vrs4zNlyUrTC5YZQ9Lz5w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.1.tgz", + "integrity": "sha512-JUiPXZKK9wOhjf4MgDiH29GZLxfqOesbLtHq2pDxwH/WwscTRV2ToymnOTh1egzaZf0ueUf8T2+CeYTGHjW0Iw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.1.tgz", + "integrity": "sha512-Uog9jsrmIRIL/lfvIp9htmskSNC7JcQsMVucXL2V2YY1y/D9IUN3LPEafqy0zRJ2cIU1SQ0V6F6TlffQ+pLAGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.1.tgz", + "integrity": "sha512-6yy3FT13KgUFOj5H8bl8w/6nKiJwHIvbtwh1V+1acsu+7y4tJjnemSa6mhsh53BeoVrlozE+fMgZhXH46WmjMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.1.tgz", + "integrity": "sha512-iOoN1QecUoGNZik536U/vtK43YwgyrCsGIkth52yIkl612n+0C9MjSnJbQAikISpb+WYRooBVhaDlUW7iZoKog==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.1.tgz", + "integrity": "sha512-d/k+PpAriUPaeMJJOG7HUSdqfEX46FEPWU1p3/nm2ACmXhj9hFEWdFODUBIpkuijXYkfL90qZzTqVPRp4BW/hw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.143.0.tgz", + "integrity": "sha512-n9uozULWflPqBtdmI8lAabLqGKNgLVNN0ZH8HfgCwpKGNtzRzauB76jTiW/3YLkcA7N1zskpi9GdVnZuu1SAvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.143.0.tgz", + "integrity": "sha512-9BbdjHETk6O3zH/DDid9IgBtF0GlpLabNKN231uraXpRDSfY+iiZxTP5bk1Z63GBownVdhdINFIeddmMz4MzpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.143.0.tgz", + "integrity": "sha512-gh+6ecoHUy4/sUcolBl/1qPXKBbYNxFY0Pk0ujgQvINTMSftJY7o4yb8gOkDJPeZeB8+a+u7xTe6umoP8N5HFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.143.0.tgz", + "integrity": "sha512-qd1hl2d+lXgHv/VQ/M9qm8TrMC5T4RqDBwtOnl+1D0QMjwcz+8AaB4JSg8STgeag0GP6a6L74XEGAsrTSJWNzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.143.0.tgz", + "integrity": "sha512-M5XXcNa7aOqLPKTR41msfghKu2yQ4xWvCm11/gwU0JzOzHNk5sgW//rVEjJ+LO48+VDAMzXTSzurUVxIDKwozw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.143.0.tgz", + "integrity": "sha512-T/GXusuOkPNQhCQCSBbcU/N8j0rAypuDBl1IyFK+lyYT594XsVz80clPC/OtbSSpBGyJxj8uYEfctxVuxVYoww==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.143.0.tgz", + "integrity": "sha512-oKu4RcBlXSqo3OC62dp6YTnQaZIurNDpCX3BnAM3+bJxt7s8J2TJKMnC0UYer1qhlRaDCg6wkTaTw+2IlsZ12w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.143.0.tgz", + "integrity": "sha512-WJBbD186AZmMGaSIhlktC+rPl8L3peCTXAh88Ih9uEvK0en2mPojGyCGYiL6mHtV1RPV3JyfJW5t6n5hh0lXhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.143.0.tgz", + "integrity": "sha512-t1AcYOwEzgceadT4v5e+vaCCb0AncCA3v5AyzfBAz/tMq11qzVccXKzNHtkWdjBsgvTKwRkaUF3QvT4kot8vcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.143.0.tgz", + "integrity": "sha512-RsnO/NoD8376LMJq8JS8TwI0ieNaFRTuNe2GVJntQg6gwZNMENZsEbknHdVwjpOmxdGLGodcwaGSbAeRr5Bgjw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.143.0.tgz", + "integrity": "sha512-48fSVfR9TZi5CASZFyv0VC6z6BCoeihFsX031mAD/oSH7d9PYsPgIqza7d9mjP7Z2KTEpTFyH6SIu0Ui6R1vdg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.143.0.tgz", + "integrity": "sha512-T8CpdD+SfE01DnIOD4HpVxu0ZJOfMJ/VhCvikKfaXAxkZ+9veyLM/D2hpi7Y2hFUyPmVQO3FNZHmYzV/WlVR4g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.143.0.tgz", + "integrity": "sha512-QLdeMsCcacenPEFsfxnBUDF1y6opyz5+fmOz9bfD5Y7fiGCMupUCuB3KTPQhNwshIG1P9fPqar9MHxuBDd4bwQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.143.0.tgz", + "integrity": "sha512-659ujfqLy6k7cuH3sbzhd8b+ztSq+i6E2E9pG78Q0BmHjAExfGIdgc8cGgMdwAozDXeZFHkJ+LXYJdWsaGdgyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.143.0.tgz", + "integrity": "sha512-/Mw/9j4TfZcnKphPrzOE6t4MMknXadcAAuVUlDRTF/ETWB5xOgQvOJV2Mh9We/bWxZdoxaGAdc+hy4GuYwQ2yQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.143.0.tgz", + "integrity": "sha512-8rIKWR2BFuifbIK/1XB9wTaSdtuJ25dlE7ZQYDnEwj/2xH2vHsxnvIjHT3ZjSVuLLwGGlSslIG/fbOJ8TV8rTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.143.0.tgz", + "integrity": "sha512-5U9kQYMfRRI6Zq7KDxgbIP0RMnKrfn3gLepRMgJuRkPSUALTiRCk9d/uyhb4lGDjUdzwK7mBkKqhLgzBPCmLpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.143.0.tgz", + "integrity": "sha512-25P7AaHk4R88Yv2XH4gToDVmh0cOu+bEURQU10CRrmvgabfRArSGAP5osmwUKeSUHj0VS50upbpbRWWW/m7mHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.143.0.tgz", + "integrity": "sha512-ORMh3JE1s6V7ySicdRK7vgaDQnn5o+UHg9ct989PlWHbel8O9ARrmWXM6kZjrBMtNucxNayQ8g69G0VfWzhANw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.24.2.tgz", + "integrity": "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.24.2.tgz", + "integrity": "sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.24.2.tgz", + "integrity": "sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.24.2.tgz", + "integrity": "sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.24.2.tgz", + "integrity": "sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.24.2.tgz", + "integrity": "sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.24.2.tgz", + "integrity": "sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.24.2.tgz", + "integrity": "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.24.2.tgz", + "integrity": "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.24.2.tgz", + "integrity": "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.24.2.tgz", + "integrity": "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.24.2.tgz", + "integrity": "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.24.2.tgz", + "integrity": "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.24.2.tgz", + "integrity": "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.24.2.tgz", + "integrity": "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.24.2.tgz", + "integrity": "sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.24.2.tgz", + "integrity": "sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.24.2.tgz", + "integrity": "sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.24.2.tgz", + "integrity": "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@playwright/test": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.23.tgz", + "integrity": "sha512-VAYOiQRqj3GPpYJE0I9J+X8Ip05cyVlNdKOFeiGS2Ou1HHGfpl0BxOyZm6nmVDyU+W+NF3/XLzmjHmVGydhwgA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dialog": "1.1.23", + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.6.tgz", + "integrity": "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.11.tgz", + "integrity": "sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.24.tgz", + "integrity": "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.15.tgz", + "integrity": "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.24.tgz", + "integrity": "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.23.tgz", + "integrity": "sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.16.tgz", + "integrity": "sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.18.tgz", + "integrity": "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.7.tgz", + "integrity": "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz", + "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.7.tgz", + "integrity": "sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", + "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.18.tgz", + "integrity": "sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.19.tgz", + "integrity": "sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-toggle": "1.1.18", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.16.tgz", + "integrity": "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", + "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@shikijs/core": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.3.tgz", + "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.4.3", + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz", + "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz", + "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.3.tgz", + "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.3.tgz", + "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.3.tgz", + "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.3.tgz", + "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "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/@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/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "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.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, + "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": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "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": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.5", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.5.tgz", + "integrity": "sha512-FhqjldLTpteueBaKflhNFlMT3+PM0O5fiBUivht6b9CZ1eesJyy7+g3Jr7XwJzt/Hip3ZG5hWwK1MX1FuDiE4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tiptap/core": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.30.1.tgz", + "integrity": "sha512-HWEO6Qi8fI8Zt8X4atVV6GxthJx7Vrjr6L5YFq4OkjYJ7HG2/bFw6IKsDA3jOYgY4DM9lv8IadaiWUQ2JJRIBA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "3.30.1" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.30.1.tgz", + "integrity": "sha512-nUBIPmf9fKfBzeMpQsexsHw3/ICzDfBK8DN9uLf5wO3I/gWycAByynvfNIqfHyl6Q77QgTIHrdjihEXUSYzeJg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1", + "@tiptap/pm": "3.30.1" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.30.1.tgz", + "integrity": "sha512-xnEGv7evFQquT0r/byjBt5iqDYQi6bF/W7DPO2rAG0z33mShDEUHhE407jakMiOSYNUilFjuQj1naLvwpQC3Bw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.30.1.tgz", + "integrity": "sha512-6asFH7ZW0gTh0aX7qrABYRVqiU/IOSjk4PKt6qdZM5YS455bjVLZDM0PcIccMDaYiomuCNjfuCSj5dxmPDoWiQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1", + "@tiptap/pm": "3.30.1" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.30.1.tgz", + "integrity": "sha512-9bf4VgIxaOe+c7W20WZGUU5d2SFNGSpVwDGU/7jGlnjobzR7ZhDK/THk9JQr8OpvZrVN8Nmf84ZD3axjt+h/hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.30.1" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.30.1.tgz", + "integrity": "sha512-oqLEOLZel3lrLhACP4vMhH5RNbV9yxFsJYiOmQjjEFEoCEWWxVfJuhZ+8NFS3mUCdgy77G62XimjEvqC3+7V7Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.30.1.tgz", + "integrity": "sha512-j19Ml9BpvHgTMSN4SfkJ26B5xSuHaxPXvMoXyAX1iOoSc4J92sCqShLGgpmtce42sIroYFRs4sBv0ndBRDcxtA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1", + "@tiptap/pm": "3.30.1" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.30.1.tgz", + "integrity": "sha512-aeMhO7EDoJyl5AGkF9hDQ9e52s18L8oYdbYjzcmXeo0YuXeaxbZCuUueVH4DnTfV7/vSkDRpDkk4TFF5LfKQpg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.30.1.tgz", + "integrity": "sha512-N3R5rA01WX/brGm1Qcnf2KLu0xkyGbRPMsHzzl1YG2sOLQM9t+FQi1T8WvSJiOpfYBVaWQkILUoCymADSpgUAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.30.1" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.30.1.tgz", + "integrity": "sha512-/Rlqsn7OXThZNzD0Qq/Ru4rZCFj3YnxL8Vf01cez+pvcyxgbwf4b9Wir+1JWJmaEMYsmWprSgEBW0l9ctbq97w==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.30.1", + "@tiptap/pm": "3.30.1" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.30.1.tgz", + "integrity": "sha512-dp13WaPNj32SkDN9tun0OtwtfdO/y52BJWigwXPgpUFLEyr8hQ84TxXL3rDshGHE4Eyjdd+erdBHisjBEV9vwQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.30.1" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.30.1.tgz", + "integrity": "sha512-2emcEkSSj+Y3dXXEugKQZDNeWCum0LBuKFmun7SiJyLOdxNuRnxwm2l4/LLiEHGDJBchDti4Oo8jZGLTRt4uog==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.30.1.tgz", + "integrity": "sha512-TGM1ZNeH/D8HQK59vFMZql2gesMoHy+6nLq3mFsYpiRT2TShMGoq+K7SC+ty6N5a/k4M82ZQrDq8t4zjiiS5xA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.30.1.tgz", + "integrity": "sha512-fTaSDapNV3cRgsek94z/Z4+Fyr0UpkE8/9PdSvt9MHYo/2yx3mmuamEJwUhUCpe7gDfzGdrMMsRpDOcd0b1ZEQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1", + "@tiptap/pm": "3.30.1" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.30.1.tgz", + "integrity": "sha512-KldMtozKk8OBHexo6akCQpJCWMgJ5OwPYuEOJO+vFjgw6VS+9N16peIo5dY+vXAiVIZL+swEsBp4barwz7NiLw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.30.1.tgz", + "integrity": "sha512-krq5yHDT4K+u2r7rS0CGk/yafyAKY1gbxexfbMmq+jyHtUUrr0UuCT1nOTa61bV3zX1nrObM1lKSpZ5FgbfdhQ==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1", + "@tiptap/pm": "3.30.1" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.30.1.tgz", + "integrity": "sha512-LizBu3fWrjifsL7QxKt+NhnHJDVGW/WWL+5asfqyznMqCZTy6tvPJ2W39pUHH2dBaVtzRIkAlsgWjDcNX/ep/Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1", + "@tiptap/pm": "3.30.1" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.30.1.tgz", + "integrity": "sha512-8QqIKvEqXKlT4Cv3ZySHAq1jzKimT/UnFxNBCnkBg+aZBgImvPlRFdN5nFs0elxiSQ7Jtt6k29aOyrkjxgyeew==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.30.1" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.30.1.tgz", + "integrity": "sha512-ju39AnbRyWr1vG4GnMM7BritV/9heGMeH103wccXgJU9hOEqzwmN9zpf5CZqeRnXOjzwQiojrEL+nwm5MnuboA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.30.1" + } + }, + "node_modules/@tiptap/extension-mention": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-mention/-/extension-mention-3.30.1.tgz", + "integrity": "sha512-XM0m73+pPgPB277oUGOs9L8Wf5kR9H8+IVkj/kGdEOr7gw98wWARc6lfEulSd9ZttbDXkPKGt4Gsz9PsBsKq1A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1", + "@tiptap/pm": "3.30.1", + "@tiptap/suggestion": "3.30.1" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.30.1.tgz", + "integrity": "sha512-kJ4+4I1n4b5jV8OpWH+dbKOlIWTWVPv/fxDQKZuAT/a0DRoowpRoAuGJ0Nk9/Azeqel6c8Ocb4n1J7xLR6Xl2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.30.1" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.30.1.tgz", + "integrity": "sha512-1kAYoyIF88l2qPgHi9ZeS5D5N7TzdJg3FvVCOJvUplQtBIwrsNAx6zaH/16NKN7kRiW7zETMKXMKkhL6MIvnDg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1" + } + }, + "node_modules/@tiptap/extension-placeholder": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.30.1.tgz", + "integrity": "sha512-z0WAMWLF6Hh3C1kuhcYVA3srK0J7d7UNdrY8hw4LlrhmJv1xIoZSUIwH+c8WkMJ5c79idL2zpeWVCtGEyH8UHw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.30.1" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.30.1.tgz", + "integrity": "sha512-vyF8TPTARaN5coGrFwqVREPXDONE4SXJMIxo9qMaBJWl7723m+Enpy7qmFscnwifW5XTpOA8jKl/QJ7qud/ljw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.30.1.tgz", + "integrity": "sha512-Zt3Ik95ZKJx5YNzC6TUXWTNBHUfRH/qVENmqfPjA7x7q4cqNdOEU+tX9DrlFjgxu/x0k48dlQS0Kaop24/vihA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.30.1.tgz", + "integrity": "sha512-Xro/EzEgWW+46ztrw5f4epDWQvcGpD+HUykzigj30bpIS8Vv4XDXFt+89dAcyDC7zRUMQULlWZV0UCyRLFEKBw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.30.1.tgz", + "integrity": "sha512-prNrvF1oMj+tngCLmZgXzQfuUuvYMbCe2sEPXXKJPqJNNffHd2wAuUZ0h6UQS5aRILaxB2kN1/yfwpxnhRpwYA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1", + "@tiptap/pm": "3.30.1" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.30.1.tgz", + "integrity": "sha512-2bHY+ihexKpfBURbAD+ahY0+lWle0TueTK80GAGYqZ/G7ErAPOmzN5RVAGMyZJI4wDkcFycRNLi3k0/3VT9a1A==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.4.1", + "prosemirror-commands": "^1.7.1", + "prosemirror-dropcursor": "^1.8.2", + "prosemirror-gapcursor": "^1.4.1", + "prosemirror-history": "^1.5.0", + "prosemirror-inputrules": "^1.5.1", + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.11", + "prosemirror-schema-list": "^1.5.1", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.5", + "prosemirror-transform": "^1.12.0", + "prosemirror-view": "^1.41.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.30.1.tgz", + "integrity": "sha512-SZvUkUlRpZxzNHcQsBeiExWG6vEAdk2y/YeiwHaVmTJAiFaA5+IhKW/MuN1qJEvYS5LXE0uFy/VJaZtfKV5uWw==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.30.1", + "@tiptap/extension-floating-menu": "^3.30.1" + }, + "peerDependencies": { + "@tiptap/core": "3.30.1", + "@tiptap/pm": "3.30.1", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.30.1.tgz", + "integrity": "sha512-6P2WEp2ZHsx8LV6hQ3K/MB3n39oiVhKESlzh0v+KhK8RR/iKSKGxvTFbi1D/1fo4j3fZf+43PGHjyKXskn9ZmQ==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "3.30.1", + "@tiptap/extension-blockquote": "3.30.1", + "@tiptap/extension-bold": "3.30.1", + "@tiptap/extension-bullet-list": "3.30.1", + "@tiptap/extension-code": "3.30.1", + "@tiptap/extension-code-block": "3.30.1", + "@tiptap/extension-document": "3.30.1", + "@tiptap/extension-dropcursor": "3.30.1", + "@tiptap/extension-gapcursor": "3.30.1", + "@tiptap/extension-hard-break": "3.30.1", + "@tiptap/extension-heading": "3.30.1", + "@tiptap/extension-horizontal-rule": "3.30.1", + "@tiptap/extension-italic": "3.30.1", + "@tiptap/extension-link": "3.30.1", + "@tiptap/extension-list": "3.30.1", + "@tiptap/extension-list-item": "3.30.1", + "@tiptap/extension-list-keymap": "3.30.1", + "@tiptap/extension-ordered-list": "3.30.1", + "@tiptap/extension-paragraph": "3.30.1", + "@tiptap/extension-strike": "3.30.1", + "@tiptap/extension-text": "3.30.1", + "@tiptap/extension-underline": "3.30.1", + "@tiptap/extensions": "3.30.1", + "@tiptap/pm": "3.30.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/suggestion": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-3.30.1.tgz", + "integrity": "sha512-jhzZGR+6SCCHCdfsAi8g5DRo+lvcTfsw/71s4IEki+SFL8ykpOAF4tpV+G51IcGLOhxvB6xfkWDvm4B4eACqjw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.30.1", + "@tiptap/pm": "3.30.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "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/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "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.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "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": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "13.0.9", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.9.tgz", + "integrity": "sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^3", + "@types/mdurl": "^1" + } + }, + "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": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", + "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": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "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/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.10", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "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": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/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": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "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": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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": { + "url": "https://opencollective.com/vitest" + } + }, + "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": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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/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" + }, + "engines": { + "node": ">=10" + } + }, + "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": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", + "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz", + "integrity": "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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": { + "node": ">=18" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "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/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "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/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "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" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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" + } + }, + "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==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/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/fast-equals": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz", + "integrity": "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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": { + "walk-up-path": "^4.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/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": { + "fd-package-json": "^2.0.0" + }, + "bin": { + "formatly": "bin/index.mjs" + }, + "engines": { + "node": ">=18.3.0" + } + }, + "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": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.1.tgz", + "integrity": "sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "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", + "url": "https://opencollective.com/unified" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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": { + "node": ">=8" + } + }, + "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/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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/knip": { + "version": "6.32.2", + "resolved": "https://registry.npmjs.org/knip/-/knip-6.32.2.tgz", + "integrity": "sha512-WXTXbmocrw7gqm1A1TQvFN0OgJ7hUSU6E1g6SPRIzzHFogUBhXByc7cYeOFVtJ2uODg7DP4VbESYBYnfbtBYsg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/knip" + } + ], + "license": "ISC", + "dependencies": { + "fdir": "^6.5.0", + "formatly": "^0.3.0", + "get-tsconfig": "4.14.1", + "jiti": "^2.7.0", + "oxc-parser": "^0.143.0", + "oxc-resolver": "11.24.2", + "picomatch": "^4.0.5", + "smol-toml": "^1.7.1", + "strip-json-comments": "5.0.3", + "tinyglobby": "^0.2.17", + "unbash": "^4.0.9", + "yaml": "^2.9.0", + "zod": "^4.4.3" + }, + "bin": { + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "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" + ], + "dev": true, + "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.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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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" + ], + "dev": true, + "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.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" + ], + "dev": true, + "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.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" + ], + "dev": true, + "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.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" + ], + "dev": true, + "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.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" + ], + "dev": true, + "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.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" + ], + "dev": true, + "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.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" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.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" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", + "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==", + "license": "MIT" + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz", + "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-it": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==", + "license": "ISC" + }, + "node_modules/markdown-it/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" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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/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", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "license": "MIT" + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "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", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "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": { + "node": ">=4" + } + }, + "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/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/next/-/next-16.3.1.tgz", + "integrity": "sha512-hsAp0i7Rh+/dhe7DGIeN2YlpLM1DP4MNxti9EtDMtqcO612X81MvvEj388/oTce9U1EcEIOWDlGq0zRwrBKvuA==", + "license": "MIT", + "dependencies": { + "@next/env": "16.3.1", + "@swc/helpers": "0.5.23", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.5.23", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.3.1", + "@next/swc-darwin-x64": "16.3.1", + "@next/swc-linux-arm64-gnu": "16.3.1", + "@next/swc-linux-arm64-musl": "16.3.1", + "@next/swc-linux-x64-gnu": "16.3.1", + "@next/swc-linux-x64-musl": "16.3.1", + "@next/swc-win32-arm64-msvc": "16.3.1", + "@next/swc-win32-x64-msvc": "16.3.1", + "sharp": "^0.35.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/nuqs": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.9.6.tgz", + "integrity": "sha512-JexjSkA6mPV5HjobxyYMMIoItbPLLz5KcVKkMJLLJJDRfd07nFZ1gr40du30l8KnsOtZ+yubhYkqRyRCQiqVDg==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "1.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/franky47" + }, + "peerDependencies": { + "@remix-run/react": ">=2", + "@tanstack/react-router": "^1", + "next": ">=14.2.0", + "react": ">=18.2.0 || ^19.0.0-0", + "react-router": "^5 || ^6 || ^7 || ^8", + "react-router-dom": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "@tanstack/react-router": { + "optional": true + }, + "next": { + "optional": true + }, + "react-router": { + "optional": true + }, + "react-router-dom": { + "optional": true + } + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "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", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, + "node_modules/oxc-parser": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.143.0.tgz", + "integrity": "sha512-ov0NzaDCOInknS7mP1cwKdJERt3utPW8ldjtdUXQ8Ty0GEFD08wk422vCUN0d7pST6kqtV7dxoI9w1Zi0l/9TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.143.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.143.0", + "@oxc-parser/binding-android-arm64": "0.143.0", + "@oxc-parser/binding-darwin-arm64": "0.143.0", + "@oxc-parser/binding-darwin-x64": "0.143.0", + "@oxc-parser/binding-freebsd-x64": "0.143.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.143.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.143.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.143.0", + "@oxc-parser/binding-linux-arm64-musl": "0.143.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.143.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.143.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.143.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.143.0", + "@oxc-parser/binding-linux-x64-gnu": "0.143.0", + "@oxc-parser/binding-linux-x64-musl": "0.143.0", + "@oxc-parser/binding-openharmony-arm64": "0.143.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.143.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.143.0", + "@oxc-parser/binding-win32-x64-msvc": "0.143.0" + } + }, + "node_modules/oxc-resolver": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.24.2.tgz", + "integrity": "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.24.2", + "@oxc-resolver/binding-android-arm64": "11.24.2", + "@oxc-resolver/binding-darwin-arm64": "11.24.2", + "@oxc-resolver/binding-darwin-x64": "11.24.2", + "@oxc-resolver/binding-freebsd-x64": "11.24.2", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", + "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", + "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", + "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-x64-musl": "11.24.2", + "@oxc-resolver/binding-openharmony-arm64": "11.24.2", + "@oxc-resolver/binding-wasm32-wasi": "11.24.2", + "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", + "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" + } + }, + "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", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/prosemirror-changeset": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz", + "integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz", + "integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.3.tgz", + "integrity": "sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-markdown": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.6.tgz", + "integrity": "sha512-dY6g2BXRjkHW2ldNRDKfTF0x0R4ifk5rgPaABd/UhvrymtsGVxTbHn01goEsHAvO9nN0O34cttlW1qg/XUcaIg==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-markdown/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==", + "license": "MIT" + }, + "node_modules/prosemirror-markdown/node_modules/@types/markdown-it": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/prosemirror-markdown/node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, + "node_modules/prosemirror-model": { + "version": "1.25.11", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz", + "integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", + "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.42.2", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.2.tgz", + "integrity": "sha512-Pdg0l5kXm8aLDquFAnQFTCITg0q44sLqBlHlpsVLD9segdOao8TOfQdAhCrCXyVgPSRr6UDDROOIWA3bIrN9YQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.8", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-hook-form": { + "version": "7.85.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.85.0.tgz", + "integrity": "sha512-U2MTriFXnclmV4rOE20p2DcRFv5WEg3FIcBFOKcOLFHDVvGIMPvLTkTWefUsonmlaVy23khVDxDWym6uJVGOzw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "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/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", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "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", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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" + } + }, + "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" + } + }, + "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/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", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rolldown": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.144.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" + } + }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, + "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": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "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" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/sharp": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/shiki": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz", + "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.4.3", + "@shikijs/engine-javascript": "4.4.3", + "@shikijs/engine-oniguruma": "4.4.3", + "@shikijs/langs": "4.4.3", + "@shikijs/themes": "4.4.3", + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "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/smol-toml": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz", + "integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/sonner": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.8.tgz", + "integrity": "sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "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", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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" + } + }, + "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" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "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": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tiptap-markdown": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/tiptap-markdown/-/tiptap-markdown-0.9.0.tgz", + "integrity": "sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ==", + "license": "MIT", + "workspaces": [ + "example" + ], + "dependencies": { + "@types/markdown-it": "^13.0.7", + "markdown-it": "^14.1.0", + "markdown-it-task-lists": "^2.1.1", + "prosemirror-markdown": "^1.11.1" + }, + "peerDependencies": { + "@tiptap/core": "^3.0.1" + } + }, + "node_modules/tldts": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.10" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/unbash": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/unbash/-/unbash-4.0.10.tgz", + "integrity": "sha512-b7zoBQvpWp0vuN5q2vK2RRBR2SvuruQAs50DApdDveBSn3eSYd84IaHodFqQIMlvY9K2VnyBUEXgwOBuGU9GBg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "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", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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" + } + }, + "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", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz", + "integrity": "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + } + }, + "node_modules/vite-tsconfig-paths/node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "deprecated": "unmaintained", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vite-tsconfig-paths/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest-fail-on-console": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/vitest-fail-on-console/-/vitest-fail-on-console-0.10.1.tgz", + "integrity": "sha512-Xjy2SpgND547qSy0s0zYVnh1G/WyGtdjAbi4PFV8mkYRmTq+6NzRUJYdc08BHrw7HJLpO2kMxHFB8PWn7FOVsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1" + }, + "peerDependencies": { + "@vitest/utils": ">=0.26.2", + "vite": ">=4.5.2", + "vitest": ">=0.26.2" + } + }, + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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": { + "node": "20 || >=22" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "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/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "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", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/studio/package.json b/studio/package.json new file mode 100644 index 0000000000..0701257145 --- /dev/null +++ b/studio/package.json @@ -0,0 +1,96 @@ +{ + "name": "mecatl-studio", + "version": "0.1.0", + "private": true, + "engines": { + "node": ">=22.13.0" + }, + "scripts": { + "dev": "node scripts/dev-local.mjs", + "dev:web": "next dev", + "mecatl": "node scripts/local-controller.mjs", + "build": "next build", + "start": "node scripts/dev-local.mjs --production", + "lint": "biome check", + "format": "biome format --write", + "test": "vitest", + "test:coverage": "vitest run --coverage", + "test:e2e": "npm run build && playwright test", + "typecheck": "tsc --noEmit", + "knip": "knip", + "test:server": "npm run build && node --test tests/rendered-html.test.mjs" + }, + "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-checkbox": "^1.1.5", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@tiptap/extension-mention": "^3.29.2", + "@tiptap/extension-placeholder": "^3.29.2", + "@tiptap/pm": "^3.27.1", + "@tiptap/react": "^3.27.1", + "@tiptap/starter-kit": "^3.27.1", + "@tiptap/suggestion": "^3.29.2", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "cmdk": "^1.1.1", + "lucide-react": "^0.577.0", + "next": "16.3.1", + "next-themes": "^0.4.6", + "nuqs": "^2.8.1", + "react": "19.2.4", + "react-dom": "19.2.4", + "react-hook-form": "^7.72.1", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", + "server-only": "^0.0.1", + "shiki": "^4.4.3", + "sonner": "^2.0.7", + "tailwind-merge": "3.5.0", + "tiptap-markdown": "^0.9.0" + }, + "devDependencies": { + "@biomejs/biome": "2.4.10", + "@playwright/test": "^1.56.1", + "@tailwindcss/postcss": "^4", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^22.13.0", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "^6.0.0", + "@vitest/coverage-v8": "^4.0.8", + "babel-plugin-react-compiler": "1.0.0", + "jsdom": "^29.0.0", + "knip": "^6.1.1", + "postcss": "^8.5.18", + "tailwindcss": "^4", + "typescript": "^6.0.2", + "vite": "^8.0.16", + "vite-tsconfig-paths": "^6.0.0", + "vitest": "^4.0.8", + "vitest-fail-on-console": "^0.10.1" + }, + "overrides": { + "undici": ">=7.29.0 <8", + "picomatch": ">=4.0.4", + "linkify-it": ">=5.0.2 <6", + "sharp": ">=0.35.0" + }, + "packageManager": "npm@10.9.4" +} diff --git a/studio/playwright.config.mts b/studio/playwright.config.mts new file mode 100644 index 0000000000..3d57d7875b --- /dev/null +++ b/studio/playwright.config.mts @@ -0,0 +1,47 @@ +import { defineConfig, devices } from "@playwright/test"; + +const BASE_URL = process.env.BASE_URL || "http://127.0.0.1:3300"; +const FIXTURE_PORT = 8099; + +/** + * E2E runs the production build in EXTERNAL mode against the fixture daemon + * (tests/e2e/fixture-daemon.mjs): browser → real proxy tier → fixture wire. + * `npm run test:e2e` builds first. + */ +export default defineConfig({ + testDir: "./tests/e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? "github" : "list", + timeout: 30_000, + use: { + baseURL: BASE_URL, + trace: "on-first-retry", + screenshot: "only-on-failure", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + webServer: [ + { + command: `node tests/e2e/fixture-daemon.mjs`, + url: `http://127.0.0.1:${FIXTURE_PORT}/v1/models`, + timeout: 15_000, + env: { FIXTURE_DAEMON_PORT: String(FIXTURE_PORT) }, + }, + { + command: `npx next start -p ${new URL(BASE_URL).port}`, + url: BASE_URL, + timeout: 120_000, + env: { + MECATL_BASE_URL: `http://127.0.0.1:${FIXTURE_PORT}`, + MECATL_WORKSPACE: "/workspace/fixture", + MECATL_STUDIO_PUBLIC_ORIGIN: BASE_URL, + }, + }, + ], +}); diff --git a/studio/postcss.config.mjs b/studio/postcss.config.mjs new file mode 100644 index 0000000000..61e36849cf --- /dev/null +++ b/studio/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/studio/public/.gitkeep b/studio/public/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/studio/public/apple-touch-icon.png b/studio/public/apple-touch-icon.png new file mode 100644 index 0000000000..9de0f1484f Binary files /dev/null and b/studio/public/apple-touch-icon.png differ diff --git a/studio/public/bg-pattern.png b/studio/public/bg-pattern.png new file mode 100644 index 0000000000..c364ae35b7 Binary files /dev/null and b/studio/public/bg-pattern.png differ diff --git a/studio/public/icon-192.png b/studio/public/icon-192.png new file mode 100644 index 0000000000..06a25afd71 Binary files /dev/null and b/studio/public/icon-192.png differ diff --git a/studio/public/icon-512.png b/studio/public/icon-512.png new file mode 100644 index 0000000000..5e2218850b Binary files /dev/null and b/studio/public/icon-512.png differ diff --git a/studio/public/icon-maskable-512.png b/studio/public/icon-maskable-512.png new file mode 100644 index 0000000000..a0832e38a3 Binary files /dev/null and b/studio/public/icon-maskable-512.png differ diff --git a/studio/public/signin-pattern.svg b/studio/public/signin-pattern.svg new file mode 100644 index 0000000000..e2bc6a600e --- /dev/null +++ b/studio/public/signin-pattern.svg @@ -0,0 +1,466 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/studio/public/stacklok-favicon.png b/studio/public/stacklok-favicon.png new file mode 100644 index 0000000000..d20ed901a2 Binary files /dev/null and b/studio/public/stacklok-favicon.png differ diff --git a/studio/public/stacklok-logo-mark.svg b/studio/public/stacklok-logo-mark.svg new file mode 100644 index 0000000000..0ab1ef082f --- /dev/null +++ b/studio/public/stacklok-logo-mark.svg @@ -0,0 +1,3 @@ + + + diff --git a/studio/public/stacklok-logo.svg b/studio/public/stacklok-logo.svg new file mode 100644 index 0000000000..aac75c4535 --- /dev/null +++ b/studio/public/stacklok-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/studio/scripts/dev-local.mjs b/studio/scripts/dev-local.mjs new file mode 100644 index 0000000000..b4946bc62d --- /dev/null +++ b/studio/scripts/dev-local.mjs @@ -0,0 +1,82 @@ +import { spawn } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const controllerScript = resolve(root, "scripts/local-controller.mjs"); +const next = resolve(root, "node_modules/.bin/next"); +const production = process.argv.includes("--production"); +const externalMode = Boolean(process.env.MECATL_BASE_URL?.trim()); + +let stopping = false; +let controller = null; +let web = null; +let controllerRestart = null; +let webRestart = null; + +const delayRestart = (callback) => setTimeout(callback, 750); + +async function controllerIsHealthy() { + try { + const response = await fetch("http://127.0.0.1:8788/status", { + signal: AbortSignal.timeout(1_500), + }); + return response.ok; + } catch { + return false; + } +} + +async function ensureController() { + if (externalMode || stopping || controller || (await controllerIsHealthy())) + return; + controller = spawn(process.execPath, [controllerScript], { + cwd: root, + env: process.env, + stdio: "inherit", + }); + controller.once("exit", () => { + controller = null; + if (!stopping) + controllerRestart = delayRestart(() => { + controllerRestart = null; + void ensureController(); + }); + }); +} + +function startWeb() { + if (stopping || web) return; + web = spawn(next, production ? ["start"] : ["dev"], { + cwd: root, + env: process.env, + stdio: "inherit", + }); + web.once("exit", () => { + web = null; + if (!stopping) + webRestart = delayRestart(() => { + webRestart = null; + startWeb(); + }); + }); +} + +async function shutdown() { + if (stopping) return; + stopping = true; + if (controllerRestart) clearTimeout(controllerRestart); + if (webRestart) clearTimeout(webRestart); + clearInterval(healthCheck); + controller?.kill("SIGTERM"); + web?.kill("SIGTERM"); + setTimeout(() => process.exit(0), 2_000).unref(); +} + +for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, shutdown); + +await ensureController(); +startWeb(); +const healthCheck = setInterval(() => { + void ensureController(); +}, 3_000); diff --git a/studio/scripts/local-controller.mjs b/studio/scripts/local-controller.mjs new file mode 100644 index 0000000000..b2d0ff62d7 --- /dev/null +++ b/studio/scripts/local-controller.mjs @@ -0,0 +1,1869 @@ +import { spawn } from "node:child_process"; +import { createHash, randomBytes } from "node:crypto"; +import { + lstat, + mkdir, + readdir, + readFile, + rename, + rm, + stat, + writeFile, +} from "node:fs/promises"; +import http from "node:http"; +import { homedir } from "node:os"; +import { dirname, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + requestIsAllowed, + validateGatewayURL, + validSkillName, +} from "../src/lib/controller-security.mjs"; +import { + KNOWN_AUTH_PROVIDERS, + listAuthFileProviders, + removeAuthFileProvider, + validProviderName, +} from "../src/lib/provider-auth.mjs"; + +const here = dirname(fileURLToPath(import.meta.url)); +// Studio is a module INSIDE the mecatl monorepo, so the harness it drives is +// the repo root one directory up — the workspace it edits, the source of +// `bin/mecated` (built by `task build`), and the cwd every run inherits. +const mecatlDir = resolve(here, "../.."); +const binary = resolve(mecatlDir, "bin/mecated"); +const workspace = mecatlDir; +const studioStateDir = resolve(here, "../.scratch"); +const operatorSettingsFile = resolve(studioStateDir, "operator-settings.yaml"); +const routerSettingsFile = resolve( + studioStateDir, + "model-router-settings.yaml", +); +const routerStateFile = resolve(studioStateDir, "model-router.json"); +// Project-scoped skills only. A SKILL.md steers the model the same way AGENTS.md +// does, so discovery is deliberately pinned to the workspace and we never pass +// --skills-conventional (which would also pull in ~/.claude/skills and the +// user-global mecatl dir — a much wider trust surface than this app should open). +const skillsDir = resolve(workspace, ".mecatl/skills"); +// Disabled skills are MOVED into a holding area inside the pinned skills dir, +// not deleted and not flagged: mecated's discovery walks only the direct +// children of --skills-dir looking for /SKILL.md, so a nested dir is +// invisible to it, and the skill-name grammar forbids a leading dot, so +// `.disabled` can never collide with a real skill. +const disabledSkillsDir = resolve(skillsDir, ".disabled"); +// Per-project memory (the Remember/Recall/SearchMemory tools) is OFF in mecated +// until --memory-dir is passed, unlike the user model which is on by default. The +// store is per-project by design, so it lives beside the session store rather than +// in a shared location. Consolidation stays off: it spends tokens in the background. +const memoryDir = resolve(workspace, ".scratch/studio-memory"); +const authFile = process.env.XDG_CONFIG_HOME + ? resolve(process.env.XDG_CONFIG_HOME, "mecatl/auth.yaml") + : resolve(homedir(), ".config/mecatl/auth.yaml"); + +/** auth.yaml's text, or "" when it does not exist / cannot be read. */ +async function readAuthFileText() { + try { + return await readFile(authFile, "utf8"); + } catch { + return ""; + } +} + +/** + * Names only of the providers configured in auth.yaml — never their values. + * The line scan lives in src/lib/provider-auth.mjs (shared with its vitest + * suite): it deliberately cannot read a credential, only detect that a + * `providers:` block names a key at one level of indent. + */ +async function listConfiguredProviderNames() { + return listAuthFileProviders(await readAuthFileText()).map( + (provider) => provider.name, + ); +} + +// ── Provider management ───────────────────────────────────────────────────── +// The provider inventory, guided add, key test, and removal are controller- +// owned for the same reason the skills routes are: mecated reads auth.yaml +// once at startup and has no HTTP write API for it. Every route keeps Studio +// rule 3 intact — a credential is read SERVER-SIDE here for exactly one +// outbound probe or removed from the file; no response body ever carries a +// key, not even a redacted preview, and there is no route that ACCEPTS one. + +/** + * One cheap authenticated read per testable provider, mirroring the base + * URLs the daemon itself defaults to (internal/cliconfig: the controller + * spawns mecated without --*-base-url overrides, so these defaults are what + * the key will actually be used against). OpenRouter's /models is public + * (the daemon's own lister is deliberately keyless), so its keyed metadata + * endpoint /key is the probe there. + */ +const providerKeyProbes = { + openrouter: (key) => ({ + url: "https://openrouter.ai/api/v1/key", + headers: { Authorization: `Bearer ${key}` }, + }), + openai: (key) => ({ + url: "https://api.openai.com/v1/models", + headers: { Authorization: `Bearer ${key}` }, + }), + anthropic: (key) => ({ + url: "https://api.anthropic.com/v1/models?limit=1", + headers: { "x-api-key": key, "anthropic-version": "2023-06-01" }, + }), + opencode: (key) => ({ + url: "https://opencode.ai/zen/go/v1/models", + headers: { Authorization: `Bearer ${key}` }, + }), +}; + +/** + * The named provider's api_key value, read server-side for the one outbound + * key probe. Deliberately controller-local (NOT in provider-auth.mjs, which + * the browser bundle imports) and deliberately api_key-only: openai-codex's + * oauth block is not key-testable. The value is never logged or echoed. + */ +function readProviderCredential(text, name) { + const lines = String(text ?? "").split("\n"); + const providersAt = lines.findIndex((line) => /^providers:\s*$/.test(line)); + if (providersAt === -1) return ""; + let inBlock = false; + for (const line of lines.slice(providersAt + 1)) { + if (/^\S/.test(line)) break; // dedented past the providers block + const key = line.match(/^ {2}([A-Za-z0-9_-]+):/); + if (key) { + inBlock = key[1] === name; + continue; + } + if (!inBlock) continue; + const credential = line.match(/^\s+api_key:\s*(.+)$/); + if (!credential) continue; + let value = credential[1].trim(); + const quote = value[0]; + if ((quote === '"' || quote === "'") && value.endsWith(quote)) { + value = value.slice(1, -1); + } + return value.startsWith("#") ? "" : value; + } + return ""; +} + +/** Provider error text, bounded and de-control-charred before it reaches a + * response body (it is provider-authored, not ours). */ +function clampProviderError(text) { + return ( + String(text ?? "") + // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping them is the point + .replace(/[\u0000-\u001f\u007f]+/g, " ") + .replace(/\s+/g, " ") + .trim() + .slice(0, 300) + ); +} + +/** + * Runs the one bounded probe for a provider's stored key. Returns the JSON + * the route answers with: {ok:true} | {ok:false, status, rejected?, error}. + * A 401/403 is the provider saying the KEY is bad; anything else (5xx, + * timeout, DNS) is an infrastructure answer, reported distinctly so a red + * "key rejected" dot is never shown for a provider outage. + */ +async function probeProviderKey(name, key) { + const probe = providerKeyProbes[name](key); + let response; + try { + response = await fetch(probe.url, { + headers: { Accept: "application/json", ...probe.headers }, + redirect: "manual", // never replay the credential to a redirect target + signal: AbortSignal.timeout(10_000), + }); + } catch (error) { + return { + ok: false, + status: 0, + error: clampProviderError(error?.message || "provider unreachable"), + }; + } + const body = await response.text().catch(() => ""); + if (response.ok) return { ok: true }; + if (response.status === 401 || response.status === 403) { + return { + ok: false, + status: response.status, + rejected: true, + error: `key rejected (HTTP ${response.status})`, + }; + } + return { + ok: false, + status: response.status, + error: clampProviderError(body) || `HTTP ${response.status}`, + }; +} +// ── Workspace skills management ──────────────────────────────────────────── +// The daemon has no HTTP write API for skills (Studio shipped them read-only; +// authoring is the ADR-0233 backlog item), and it resolves the skills dir +// ONCE at startup: skillfs's FSSource is a construction-time snapshot +// ("bodies are retained; no re-read") and internal/app/build.go registers +// "the skills resolved once at build time". So skill CRUD lives here, on the +// controller that owns --skills-dir, and every mutation the daemon can see +// restarts mecated through the same queueRestart machinery as config writes — +// in-flight runs and session ids die with it, exactly like a gateway or +// model-router write. + +/** Max SKILL.md body accepted on the edit path. */ +const maxSkillBodyBytes = 262_144; + +function skillClientError(message, statusCode = 400) { + return Object.assign(new Error(message), { statusCode }); +} + +/** + * The enabled/disabled directory pair for a validated skill name. The grammar + * already forbids separators, dots, and whitespace; the prefix check is + * defense-in-depth should the grammar ever loosen. + */ +function skillPaths(name) { + if (!validSkillName(name)) + throw skillClientError( + "Skill names use lowercase letters, digits, hyphens, and underscores (max 64 characters)", + ); + const enabled = resolve(skillsDir, name); + const disabled = resolve(disabledSkillsDir, name); + if ( + !enabled.startsWith(skillsDir + sep) || + !disabled.startsWith(disabledSkillsDir + sep) + ) + throw skillClientError("Skill name escapes the skills directory"); + return { enabled, disabled }; +} + +async function isDirectory(path) { + try { + return (await stat(path)).isDirectory(); + } catch { + return false; + } +} + +/** + * One-line `description:` scan of a SKILL.md frontmatter block — a line scan, + * never a YAML parse, mirroring listConfiguredProviderNames. Best-effort: a + * block-scalar or absent description simply lists as "". + */ +function skillDescription(markdown) { + const lines = markdown.split("\n"); + if (lines[0]?.trim() !== "---") return ""; + for (const line of lines.slice(1)) { + if (line.trim() === "---") break; + const match = line.match(/^description:\s*(.+)$/); + if (!match) continue; + const value = match[1].trim(); + if (/^[>|]/.test(value)) return ""; + return value.replace(/^["']|["']$/g, ""); + } + return ""; +} + +/** Cap on the bundled-file listing — a skill is a small folder, not a repo. */ +const maxSkillFiles = 500; + +/** + * Bounded recursive listing of one skill's folder: relative POSIX paths + + * sizes. Symlinks are never followed (a link could point outside the skills + * dir), dot-entries are skipped (.DS_Store noise), and the walk stops at + * maxSkillFiles entries / depth 8. + */ +async function listSkillFiles(root) { + const files = []; + async function walk(dir, prefix, depth) { + if (depth > 8 || files.length >= maxSkillFiles) return; + let entries; + try { + entries = await readdir(dir, { withFileTypes: true }); + } catch { + return; + } + entries.sort((a, b) => a.name.localeCompare(b.name)); + for (const entry of entries) { + if (files.length >= maxSkillFiles) return; + if (entry.name.startsWith(".") || entry.isSymbolicLink()) continue; + const rel = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + await walk(resolve(dir, entry.name), rel, depth + 1); + } else if (entry.isFile()) { + try { + files.push({ + path: rel, + size: (await stat(resolve(dir, entry.name))).size, + }); + } catch { + // Raced away between readdir and stat — skip it. + } + } + } + } + await walk(root, "", 0); + return files; +} + +/** Names (+ best-effort descriptions) in the `.disabled/` holding area. */ +async function listDisabledSkills() { + let entries; + try { + entries = await readdir(disabledSkillsDir, { withFileTypes: true }); + } catch { + return []; + } + const skills = []; + for (const entry of entries) { + if (!entry.isDirectory() || !validSkillName(entry.name)) continue; + let description = ""; + try { + description = skillDescription( + await readFile( + resolve(disabledSkillsDir, entry.name, "SKILL.md"), + "utf8", + ), + ); + } catch { + // A SKILL.md-less folder still lists — enabling it back is how the + // operator recovers it. + } + skills.push({ name: entry.name, description }); + } + return skills.sort((a, b) => a.name.localeCompare(b.name)); +} + +// The kinds startMecatl/preferredKind understand: the two synthetic ones plus +// every provider auth.yaml can name a block for (KNOWN_AUTH_PROVIDERS is the +// same registry the guided-add UI offers). +const KNOWN_PROVIDER_KINDS = new Set([ + "mock", + "toolhive", + ...KNOWN_AUTH_PROVIDERS.map((entry) => entry.name), +]); +const configuredProvider = + process.env.MECATL_STUDIO_PROVIDER?.trim().toLowerCase() || ""; +if (configuredProvider && !KNOWN_PROVIDER_KINDS.has(configuredProvider)) { + throw new Error( + `MECATL_STUDIO_PROVIDER must be one of: ${[...KNOWN_PROVIDER_KINDS].join(", ")}`, + ); +} +// The LIVE active-provider selection. Seeded from MECATL_STUDIO_PROVIDER at +// startup, but — unlike that env var, which is frozen for the process's +// lifetime — reassignable at runtime through POST /providers/active (the +// Studio settings UI's provider switch), so an operator can move between the +// offline mock and a real provider without restarting `npm run dev`. +let activeProviderOverride = configuredProvider || null; +const managedAuthToken = ( + process.env.MECATL_AUTH_TOKEN || randomBytes(32).toString("base64url") +).replace(/^Bearer\s+/i, ""); +const mcpProxySecret = randomBytes(24).toString("base64url"); +const studioPublicOrigin = + process.env.MECATL_STUDIO_PUBLIC_ORIGIN?.trim() || "http://localhost:3000"; +const allowedOrigins = new Set( + ( + process.env.MECATL_STUDIO_ORIGINS || + "http://localhost:3000,http://127.0.0.1:3000" + ) + .split(",") + .map((origin) => origin.trim()) + .filter(Boolean), +); +allowedOrigins.add(studioPublicOrigin); +let child = null; +let provider = "offline mock"; +let mecatlBaseURL = ""; +let gateway = null; +let modelRouterConfig = null; +let operatorSettingsActive = false; +let startupLog = ""; +let restartQueue = Promise.resolve(); +let gatewayRefresh = null; +let shuttingDown = false; +let restartTimer = null; +let restartFailures = 0; +let startupError = ""; +const expectedExits = new WeakSet(); +const oauthAttempts = new Map(); +const oauthRedirectUri = "http://127.0.0.1:8788/oauth/callback"; +// The ToolHive LLM gateway reaches mecated through "thv llm proxy", a LOOPBACK +// reverse proxy that injects a fresh OIDC token per request. The controller +// never holds a gateway credential itself — that is the whole point of routing +// through the proxy rather than pasting a key. mecated auto-detects the same +// proxy from ToolHive's own config, so the port here is only used for the +// readiness probe that decides whether "toolhive" is an offerable provider. +const toolhiveGatewayURL = "http://127.0.0.1:14000/v1"; +let toolhiveReady = false; + +const delay = (milliseconds) => + new Promise((done) => setTimeout(done, milliseconds)); + +function jsonError(response, status, message) { + response.statusCode = status; + response.end(JSON.stringify({ error: message })); +} + +async function readBody(request, limit = 1_048_576) { + const declared = Number(request.headers["content-length"] || 0); + if (declared > limit) + throw Object.assign(new Error("request too large"), { statusCode: 413 }); + const chunks = []; + let size = 0; + for await (const chunk of request) { + size += chunk.length; + if (size > limit) + throw Object.assign(new Error("request too large"), { statusCode: 413 }); + chunks.push(chunk); + } + return Buffer.concat(chunks); +} + +async function pickLoopbackPort() { + const probe = http.createServer(); + await new Promise((resolveListen, reject) => { + probe.once("error", reject); + probe.listen(0, "127.0.0.1", resolveListen); + }); + const address = probe.address(); + const port = typeof address === "object" && address ? address.port : 0; + await new Promise((resolveClose) => probe.close(resolveClose)); + if (!port) throw new Error("Could not allocate a loopback port for mecated"); + return port; +} + +function fetchMecatl(path, options = {}) { + if (!mecatlBaseURL) + throw new Error("mecated has not been assigned a listener yet"); + const headers = new Headers(options.headers); + headers.set("authorization", `Bearer ${managedAuthToken}`); + return fetch(new URL(path, mecatlBaseURL), { ...options, headers }); +} + +function providerLabel(kind) { + if (kind === "mock") return "offline mock"; + if (kind === "toolhive") return "ToolHive LLM gateway"; + return ( + KNOWN_AUTH_PROVIDERS.find((entry) => entry.name === kind)?.label ?? kind + ); +} + +function startupFailure(kind, message) { + startupError = + kind !== "mock" && kind !== "toolhive" + ? `${providerLabel(kind)} could not start. Add providers.${kind}.api_key to ${authFile}, then switch to it again. mecated: ${message}` + : message; + return new Error(startupError); +} + +// A short probe, deliberately: when no token is cached the proxy blocks on an +// interactive browser login, and a controller start must never hang on that. +// Timing out simply means "not offerable right now" and studio falls back. +async function detectToolhiveGateway() { + try { + const response = await fetch(`${toolhiveGatewayURL}/models`, { + signal: AbortSignal.timeout(2500), + }); + return response.ok; + } catch { + return false; + } +} + +// Provider credentials are owned by mecated's conventional auth file, never +// copied through a browser form or patched into the child's environment here. +// MECATL_STUDIO_PROVIDER / the /providers/active switch select a provider +// without carrying its credential. +const preferredKind = () => + activeProviderOverride || (toolhiveReady ? "toolhive" : "mock"); + +/** Whether `kind` is safe to hand to startMecatl right now: the two + * synthetic kinds (mock always, toolhive only while the gateway answers) or + * a provider that actually has a block in auth.yaml — never an arbitrary + * string, so a typo can't reach mecated's fail-fast --default-provider + * check and crash the child. */ +function isSelectableProviderKind(kind, configuredNames) { + if (kind === "mock") return true; + if (kind === "toolhive") return toolhiveReady; + return configuredNames.includes(kind); +} + +function normalizeModelRouter(input) { + const classifierModel = + typeof input?.classifierModel === "string" + ? input.classifierModel.trim() + : ""; + if (!classifierModel || classifierModel.length > 200) + throw new Error("Choose a valid classifier model"); + if ( + !Array.isArray(input?.categories) || + input.categories.length < 2 || + input.categories.length > 8 + ) { + throw new Error("Semantic routing needs between 2 and 8 categories"); + } + const seen = new Set(); + const categories = input.categories.map((category) => { + const name = + typeof category?.name === "string" + ? category.name.trim().toLowerCase() + : ""; + const description = + typeof category?.description === "string" + ? category.description.trim() + : ""; + const model = + typeof category?.model === "string" ? category.model.trim() : ""; + if (!/^[a-z][a-z0-9_-]{0,39}$/.test(name)) + throw new Error( + "Category names must start with a letter and use only letters, numbers, underscores, or dashes", + ); + if (seen.has(name)) throw new Error(`Category name ${name} is duplicated`); + if (!description || description.length > 300) + throw new Error( + `Category ${name} needs a distinct description of at most 300 characters`, + ); + if (!model || model.length > 200) + throw new Error(`Choose a model for category ${name}`); + seen.add(name); + return { name, description, model }; + }); + const defaultCategory = + typeof input?.defaultCategory === "string" + ? input.defaultCategory.trim().toLowerCase() + : ""; + if (!seen.has(defaultCategory)) + throw new Error( + "The default category must match one of the routing categories", + ); + return { + enabled: input?.enabled !== false, + classifierModel, + defaultCategory, + categories, + }; +} + +const yamlString = (value) => JSON.stringify(String(value)); + +function renderModelRouterYAML(config) { + const categories = config.categories + .map((category) => + [ + ` - name: ${yamlString(category.name)}`, + ` description: ${yamlString(category.description)}`, + ` model: ${yamlString(category.model)}`, + ].join("\n"), + ) + .join("\n"); + return [ + "# Managed by Mecatl Studio. This is loaded at the trusted CLI/operator tier.", + "models:", + " slots:", + ` router: ${yamlString(config.classifierModel)}`, + " router:", + ` disabled: ${config.enabled ? "false" : "true"}`, + " classifier-slot: router", + ` default-category: ${yamlString(config.defaultCategory)}`, + " categories:", + categories, + "", + ].join("\n"); +} + +async function persistModelRouter(config) { + await mkdir(studioStateDir, { recursive: true, mode: 0o700 }); + const yamlTemp = `${routerSettingsFile}.tmp`; + const jsonTemp = `${routerStateFile}.tmp`; + await writeFile(yamlTemp, renderModelRouterYAML(config), { mode: 0o600 }); + await writeFile(jsonTemp, `${JSON.stringify(config, null, 2)}\n`, { + mode: 0o600, + }); + await rename(yamlTemp, routerSettingsFile); + await rename(jsonTemp, routerStateFile); +} + +async function loadModelRouter() { + try { + return normalizeModelRouter( + JSON.parse(await readFile(routerStateFile, "utf8")), + ); + } catch (error) { + if (error?.code !== "ENOENT") + process.stderr.write( + `[router] saved configuration ignored: ${error.message || error}\n`, + ); + return null; + } +} + +async function hasOperatorSettings() { + try { + await readFile(operatorSettingsFile, "utf8"); + return true; + } catch (error) { + if (error?.code !== "ENOENT") + process.stderr.write( + `[settings] operator configuration ignored: ${error.message || error}\n`, + ); + return false; + } +} + +function wellKnownURL(base, name) { + const url = new URL(base); + const issuerPath = + url.pathname === "/" ? "" : url.pathname.replace(/\/$/, ""); + return new URL(`/.well-known/${name}${issuerPath}`, url.origin).toString(); +} + +async function fetchJSON(url) { + try { + const response = await fetch(url, { + headers: { Accept: "application/json" }, + redirect: "follow", + signal: AbortSignal.timeout(8_000), + }); + if (!response.ok) return null; + return await response.json(); + } catch { + return null; + } +} + +function requireHttpsEndpoint(value, label) { + if (!value) throw new Error(`OAuth metadata does not include ${label}`); + const endpoint = new URL(value); + if (endpoint.protocol !== "https:") + throw new Error(`OAuth ${label} must use HTTPS`); + return endpoint.toString(); +} + +async function discoverOAuth(gatewayURL) { + const resourceCandidates = []; + try { + const probe = await fetch(gatewayURL, { + method: "GET", + headers: { Accept: "text/event-stream" }, + redirect: "manual", + signal: AbortSignal.timeout(5_000), + }); + const challenge = probe.headers.get("www-authenticate") || ""; + const match = challenge.match( + /resource_metadata\s*=\s*(?:"([^"]+)"|([^,\s]+))/i, + ); + if (match?.[1] || match?.[2]) resourceCandidates.push(match[1] || match[2]); + await probe.body?.cancel().catch(() => undefined); + } catch { + /* fall through to RFC 9728 well-known locations */ + } + + resourceCandidates.push( + wellKnownURL(gatewayURL, "oauth-protected-resource"), + new URL( + "/.well-known/oauth-protected-resource", + gatewayURL.origin, + ).toString(), + ); + + let resourceMetadata = null; + for (const candidate of [...new Set(resourceCandidates)]) { + let candidateURL; + try { + candidateURL = requireHttpsEndpoint( + candidate, + "protected resource metadata URL", + ); + } catch { + continue; + } + resourceMetadata = await fetchJSON(candidateURL); + if (resourceMetadata) break; + } + + const authorizationServer = + resourceMetadata?.authorization_servers?.[0] || gatewayURL.origin; + const metadataCandidates = [ + wellKnownURL(authorizationServer, "oauth-authorization-server"), + wellKnownURL(authorizationServer, "openid-configuration"), + new URL( + "/.well-known/openid-configuration", + new URL(authorizationServer).origin, + ).toString(), + ]; + let metadata = null; + for (const candidate of [...new Set(metadataCandidates)]) { + metadata = await fetchJSON(candidate); + if (metadata) break; + } + if (!metadata) + throw new Error( + "The gateway did not advertise usable MCP OAuth authorization-server metadata", + ); + + const authorizationEndpoint = requireHttpsEndpoint( + metadata.authorization_endpoint, + "authorization endpoint", + ); + const tokenEndpoint = requireHttpsEndpoint( + metadata.token_endpoint, + "token endpoint", + ); + const registrationEndpoint = requireHttpsEndpoint( + metadata.registration_endpoint, + "dynamic client registration endpoint", + ); + const resourceScopes = Array.isArray(resourceMetadata?.scopes_supported) + ? resourceMetadata.scopes_supported + : []; + const authScopes = Array.isArray(metadata.scopes_supported) + ? metadata.scopes_supported + : []; + const defaultScopes = ["openid", "profile", "email", "offline_access"]; + const preferredAuthScopes = defaultScopes.filter((scope) => + authScopes.includes(scope), + ); + const scopes = [...new Set([...resourceScopes, ...preferredAuthScopes])]; + + return { + authorizationEndpoint, + tokenEndpoint, + registrationEndpoint, + resource: resourceMetadata?.resource || gatewayURL.toString(), + scope: scopes.join(" ") || "openid profile email offline_access", + }; +} + +function queueRestart(operation) { + const run = restartQueue.then(operation, operation); + restartQueue = run.catch(() => undefined); + return run; +} + +function scheduleMecatlRestart() { + if (shuttingDown || restartTimer || child) return; + const wait = Math.min(10_000, 750 * 2 ** restartFailures); + process.stderr.write( + `[supervisor] mecated stopped unexpectedly; restarting in ${wait}ms\n`, + ); + restartTimer = setTimeout(() => { + restartTimer = null; + queueRestart(async () => { + if (shuttingDown || child) return; + try { + await startMecatl(preferredKind()); + restartFailures = 0; + process.stderr.write("[supervisor] mecated restarted\n"); + } catch (error) { + restartFailures += 1; + process.stderr.write( + `[supervisor] restart failed: ${error.message || error}\n`, + ); + scheduleMecatlRestart(); + } + }); + }, wait); +} + +async function refreshGatewayAccessToken(force = false) { + if (!gateway?.refreshToken || !gateway.clientId || !gateway.tokenEndpoint) + return false; + if (!force && gateway.expiresAt && gateway.expiresAt > Date.now() + 60_000) + return true; + if (gatewayRefresh) return gatewayRefresh; + gatewayRefresh = (async () => { + // No `scope` on refresh: RFC 6749 §6 makes it optional (the server reuses + // the originally granted scopes), and this provider rejects the request as + // malformed when it is present — which silently killed every auto-refresh + // and made gateway auth die on the hour. + const refreshBody = new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: gateway.refreshToken, + client_id: gateway.clientId, + }); + if (gateway.resource) refreshBody.set("resource", gateway.resource); + const tokenResponse = await fetch(gateway.tokenEndpoint, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: refreshBody, + }); + const tokenResult = await tokenResponse.json().catch(() => ({})); + if (!tokenResponse.ok || !tokenResult.access_token) { + process.stderr.write( + `[oauth] refresh rejected (${tokenResponse.status}, ${String(tokenResult.error || "unknown_error").slice(0, 80)})\n`, + ); + throw new Error( + tokenResult.error_description || + tokenResult.error || + "Gateway token refresh failed", + ); + } + gateway.token = tokenResult.access_token; + if (tokenResult.refresh_token) + gateway.refreshToken = tokenResult.refresh_token; + gateway.expiresAt = + Date.now() + Math.max(60, Number(tokenResult.expires_in) || 300) * 1000; + process.stderr.write("[oauth] gateway access token refreshed\n"); + return true; + })().finally(() => { + gatewayRefresh = null; + }); + return gatewayRefresh; +} + +async function stopChild() { + if (!child) { + await delay(200); + return; + } + const current = child; + expectedExits.add(current); + await new Promise((done) => { + const timer = setTimeout(() => { + current.kill("SIGKILL"); + done(); + }, 2000); + current.once("exit", () => { + clearTimeout(timer); + done(); + }); + current.kill("SIGTERM"); + }); + if (child === current) child = null; + // Let loopback listeners finish closing before the next child binds them. + await delay(250); +} + +async function startMecatl(kind) { + if (restartTimer) { + clearTimeout(restartTimer); + restartTimer = null; + } + await stopChild(); + startupLog = ""; + startupError = ""; + const port = await pickLoopbackPort(); + mecatlBaseURL = `http://127.0.0.1:${port}`; + const args = [ + "serve", + "--workspace", + workspace, + "--store-dir", + ".scratch/studio-sessions", + "--grpc-addr", + "127.0.0.1:0", + "--http-addr", + `127.0.0.1:${port}`, + ]; + if (operatorSettingsActive) + args.push("--permission-config", operatorSettingsFile); + else if (modelRouterConfig) + args.push("--permission-config", routerSettingsFile); + // mecated refuses to start on a missing --skills-dir, and an empty directory is + // the correct "no skills yet" state, so create it before every spawn. + await mkdir(skillsDir, { recursive: true }); + args.push("--skills-dir", skillsDir); + await mkdir(memoryDir, { recursive: true }); + args.push("--memory-dir", memoryDir); + if (kind === "mock") { + args.push("--mock"); + } else if (kind === "toolhive") { + // No credential and no base-URL flag for the gateway: mecated finds the + // loopback proxy through ToolHive's own config and registers it as the + // "toolhive" provider on its own. Naming it as the default is all it takes. + args.push("--default-provider", "toolhive"); + } else { + // Any auth.yaml-configured provider (openrouter, anthropic, openai, + // opencode, …) — mecated validates the id fail-fast at startup. + args.push("--default-provider", kind); + } + const env = { ...process.env, MECATL_AUTH_TOKEN: managedAuthToken }; + if (gateway) { + // Mecatl's SDK opens the optional standalone SSE notification stream after + // initialization. Some authenticated gateways (including Connector Gateway) + // close that GET stream and thereby cancel an otherwise valid MCP session. + // Keep the optional stream on loopback and forward request/response traffic. + args.push( + "--mcp-server", + `${gateway.name}=http://127.0.0.1:8788/mcp-proxy/${mcpProxySecret}/${encodeURIComponent(gateway.name)}`, + ); + } + const proc = spawn(binary, args, { + cwd: mecatlDir, + env, + stdio: ["ignore", "ignore", "pipe"], + }); + child = proc; + proc.stderr.on("data", (chunk) => { + const text = chunk.toString(); + startupLog = (startupLog + text).slice(-24_000); + process.stderr.write(`[mecatl] ${text}`); + }); + proc.once("exit", () => { + if (child === proc) child = null; + if (!expectedExits.has(proc)) scheduleMecatlRestart(); + }); + provider = providerLabel(kind); + await delay(250); + // Remote MCP gateways may cold-start and mecatl intentionally gives their + // initialize handshake up to 30 seconds. Keep the controller's readiness + // window longer than that so it never kills a valid in-flight connection. + for (let attempt = 0; attempt < 320; attempt += 1) { + if (proc.exitCode !== null) + throw startupFailure( + kind, + `mecatl exited during startup (code ${proc.exitCode})`, + ); + await delay(125); + let ready = false; + try { + const response = await fetchMecatl("/v1/models"); + ready = response.ok; + } catch { + /* server is still starting */ + } + if (!ready) continue; + // A response from an older listener is not enough. Require this exact child + // to remain alive and answer again after a stability window. + await delay(450); + if (proc.exitCode !== null || child !== proc) + throw new Error("mecatl exited before the connection became stable"); + if (gateway && /Unauthorized/i.test(startupLog)) { + throw new Error( + "MCP Gateway rejected the bearer token (Unauthorized). Paste a current gateway access token and try again.", + ); + } + if ( + gateway && + /MCP manager construction failed|no servers could be connected/i.test( + startupLog, + ) + ) { + throw new Error( + "MCP Gateway could not be initialized. Check that the URL is a Streamable HTTP endpoint and that its credential is valid.", + ); + } + const stable = await fetchMecatl("/v1/models") + .then((response) => response.ok) + .catch(() => false); + if (!stable) continue; + restartFailures = 0; + return; + } + throw startupFailure(kind, "mecatl did not become ready"); +} + +const server = http.createServer(async (request, response) => { + const requestURL = new URL(request.url, "http://127.0.0.1:8788"); + const mcpProxyPrefix = `/mcp-proxy/${mcpProxySecret}/`; + if ( + !requestIsAllowed(request, requestURL, { allowedOrigins, mcpProxyPrefix }) + ) { + jsonError(response, 403, "request origin is not allowed"); + return; + } + if (requestURL.pathname.startsWith("/mecatl/")) { + try { + const body = + request.method === "GET" || request.method === "HEAD" + ? undefined + : await readBody(request); + const headers = {}; + for (const key of [ + "content-type", + "accept", + "mcp-session-id", + "mcp-protocol-version", + "last-event-id", + ]) { + if (request.headers[key]) headers[key] = request.headers[key]; + } + const path = + requestURL.pathname.slice("/mecatl".length) + requestURL.search; + const upstream = await fetchMecatl(path, { + method: request.method, + headers, + body, + redirect: "manual", + }); + response.statusCode = upstream.status; + for (const key of [ + "content-type", + "cache-control", + "mcp-session-id", + "www-authenticate", + ]) { + const value = upstream.headers.get(key); + if (value) response.setHeader(key, value); + } + if (upstream.body) + for await (const chunk of upstream.body) response.write(chunk); + response.end(); + } catch (error) { + jsonError( + response, + error.statusCode || 502, + error.message || "mecated proxy failed", + ); + } + return; + } + if (requestURL.pathname.startsWith(mcpProxyPrefix)) { + const proxyName = decodeURIComponent( + requestURL.pathname.slice(mcpProxyPrefix.length), + ); + if (!gateway || proxyName !== gateway.name) { + response.statusCode = 404; + response.end("gateway not configured"); + return; + } + if (request.method === "GET") { + response.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }); + response.write(": loopback notification channel\n\n"); + const heartbeat = setInterval( + () => response.write(": keepalive\n\n"), + 15_000, + ); + request.once("close", () => clearInterval(heartbeat)); + return; + } + if (!["POST", "DELETE"].includes(request.method || "")) { + response.statusCode = 405; + response.end("method not allowed"); + return; + } + try { + const requestBody = + request.method === "POST" ? await readBody(request) : undefined; + await refreshGatewayAccessToken(false); + const headers = { Authorization: `Bearer ${gateway.token}` }; + for (const key of [ + "content-type", + "accept", + "mcp-session-id", + "mcp-protocol-version", + "last-event-id", + ]) { + if (request.headers[key]) headers[key] = request.headers[key]; + } + let upstream = await fetch(gateway.url, { + method: request.method, + headers, + body: requestBody, + }); + if (upstream.status === 401 && gateway.refreshToken) { + await upstream.arrayBuffer(); + await refreshGatewayAccessToken(true); + headers.Authorization = `Bearer ${gateway.token}`; + upstream = await fetch(gateway.url, { + method: request.method, + headers, + body: requestBody, + }); + } + process.stderr.write( + `[mcp-proxy] ${request.method} ${upstream.status} ${upstream.headers.get("content-type") || ""}\n`, + ); + response.statusCode = upstream.status; + for (const key of [ + "content-type", + "cache-control", + "mcp-session-id", + "www-authenticate", + ]) { + const value = upstream.headers.get(key); + if (value) response.setHeader(key, value); + } + if (upstream.body) { + for await (const chunk of upstream.body) response.write(chunk); + } + response.end(); + } catch (error) { + process.stderr.write( + `[mcp-proxy] ${request.method} failed: ${error.message || error}\n`, + ); + jsonError( + response, + error.statusCode || 502, + error.message || "gateway proxy failed", + ); + } + return; + } + response.setHeader("Content-Type", "application/json"); + response.setHeader("Cache-Control", "no-store"); + if (request.method === "GET" && requestURL.pathname === "/mcp/oauth/start") { + try { + const name = requestURL.searchParams.get("name") || ""; + const gatewayURL = new URL(requestURL.searchParams.get("url") || ""); + if (!/^[A-Za-z0-9_]+$/.test(name)) + throw new Error( + "Gateway name may contain only letters, numbers, and underscores", + ); + if (gatewayURL.protocol !== "https:") + throw new Error("OAuth gateways must use HTTPS"); + const discovery = await discoverOAuth(gatewayURL); + const registrationResponse = await fetch(discovery.registrationEndpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + client_name: "Mecatl Studio", + application_type: "native", + redirect_uris: [oauthRedirectUri], + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code"], + token_endpoint_auth_method: "none", + }), + signal: AbortSignal.timeout(10_000), + }); + const registration = await registrationResponse.json().catch(() => ({})); + if (!registrationResponse.ok) + throw new Error( + registration.error_description || + registration.error || + "Gateway refused OAuth client registration", + ); + if (!registration.client_id) + throw new Error("Gateway registration did not return a client ID"); + const state = randomBytes(24).toString("base64url"); + const verifier = randomBytes(48).toString("base64url"); + const challenge = createHash("sha256") + .update(verifier) + .digest("base64url"); + const scope = registration.scope || discovery.scope; + oauthAttempts.set(state, { + name, + url: gatewayURL.toString(), + clientId: registration.client_id, + tokenEndpoint: discovery.tokenEndpoint, + scope, + resource: discovery.resource, + verifier, + expires: Date.now() + 10 * 60_000, + }); + const authorizationURL = new URL(discovery.authorizationEndpoint); + authorizationURL.searchParams.set("response_type", "code"); + authorizationURL.searchParams.set("client_id", registration.client_id); + authorizationURL.searchParams.set("redirect_uri", oauthRedirectUri); + authorizationURL.searchParams.set("scope", scope); + if (discovery.resource) + authorizationURL.searchParams.set("resource", discovery.resource); + authorizationURL.searchParams.set("state", state); + authorizationURL.searchParams.set("code_challenge", challenge); + authorizationURL.searchParams.set("code_challenge_method", "S256"); + if (requestURL.searchParams.get("redirect") === "1") { + response.writeHead(302, { Location: authorizationURL.toString() }); + response.end(); + } else { + response.end( + JSON.stringify({ authorizationUrl: authorizationURL.toString() }), + ); + } + } catch (error) { + response.statusCode = 400; + response.end( + JSON.stringify({ + error: error.message || "Could not start gateway sign-in", + }), + ); + } + return; + } + if (request.method === "GET" && requestURL.pathname === "/oauth/callback") { + response.setHeader("Content-Type", "text/html; charset=utf-8"); + const state = requestURL.searchParams.get("state") || ""; + const attempt = oauthAttempts.get(state); + oauthAttempts.delete(state); + try { + if (requestURL.searchParams.get("error")) + throw new Error( + requestURL.searchParams.get("error_description") || + requestURL.searchParams.get("error"), + ); + if (!attempt || attempt.expires < Date.now()) + throw new Error( + "The gateway sign-in attempt expired. Start it again from Mecatl Studio.", + ); + const code = requestURL.searchParams.get("code"); + if (!code) + throw new Error("Gateway sign-in did not return an authorization code"); + const tokenBody = new URLSearchParams({ + grant_type: "authorization_code", + code, + client_id: attempt.clientId, + redirect_uri: oauthRedirectUri, + code_verifier: attempt.verifier, + }); + if (attempt.resource) tokenBody.set("resource", attempt.resource); + const tokenResponse = await fetch(attempt.tokenEndpoint, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: tokenBody, + signal: AbortSignal.timeout(15_000), + }); + const tokenResult = await tokenResponse.json().catch(() => ({})); + if (!tokenResponse.ok || !tokenResult.access_token) + throw new Error( + tokenResult.error_description || + tokenResult.error || + "Gateway token exchange failed", + ); + await queueRestart(async () => { + const previousGateway = gateway; + gateway = { + name: attempt.name, + url: attempt.url, + token: tokenResult.access_token, + refreshToken: tokenResult.refresh_token || "", + clientId: attempt.clientId, + tokenEndpoint: attempt.tokenEndpoint, + scope: attempt.scope, + resource: attempt.resource, + expiresAt: + Date.now() + + Math.max(60, Number(tokenResult.expires_in) || 300) * 1000, + }; + try { + await startMecatl(preferredKind()); + } catch (error) { + gateway = previousGateway; + await startMecatl(preferredKind()); + throw error; + } + }); + response.end( + `Mecatl Gateway Connected

Gateway connected

You can close this window.

`, + ); + } catch (error) { + const message = String(error.message || "Gateway sign-in failed").replace( + /[<>&"']/g, + "", + ); + process.stderr.write(`[oauth] callback failed: ${message}\n`); + response.statusCode = 400; + response.end( + `Mecatl Gateway Error

Could not connect

${message}

`, + ); + } + return; + } + if ( + request.method === "POST" && + requestURL.pathname === "/mcp/oauth/refresh" + ) { + try { + if (!gateway?.refreshToken) + throw new Error("Gateway does not have a refresh token; sign in again"); + await refreshGatewayAccessToken(true); + response.end(JSON.stringify({ ok: true })); + } catch (error) { + response.statusCode = 400; + response.end( + JSON.stringify({ + error: error.message || "Gateway token refresh failed", + }), + ); + } + return; + } + // Restart mecated with the CURRENT provider, gateway and router config. The + // daemon resolves skills and agent definitions once at startup (ListSkills is + // a pure snapshot read), so a newly authored SKILL.md only reaches the model + // after a restart. Provider credentials remain owned by mecated's auth file. + if (request.method === "POST" && requestURL.pathname === "/restart") { + try { + await queueRestart(async () => { + await startMecatl(preferredKind()); + }); + response.end(JSON.stringify({ ok: true, provider })); + } catch (error) { + response.statusCode = 400; + response.end( + JSON.stringify({ error: error.message || "Could not restart mecated" }), + ); + } + return; + } + if (request.method === "GET" && requestURL.pathname === "/status") { + const configuredProviders = await listConfiguredProviderNames(); + response.end( + JSON.stringify({ + mode: "managed", + provider, + isMock: provider === "offline mock", + running: Boolean(child), + startupError, + authFile, + // Names only — never values. What MECATL_STUDIO_PROVIDER / the + // /providers/active switch may select among, and which one is + // active right now, if any. + configuredProviders, + selectedProvider: activeProviderOverride, + // The client has no other way to learn this: it is resolved from THIS + // file's location, so a clone anywhere works with no source edit. + workspace, + gateway: gateway ? { name: gateway.name, url: gateway.url } : null, + toolhiveGateway: { + available: toolhiveReady, + baseURL: toolhiveGatewayURL, + active: provider === "ToolHive LLM gateway", + }, + modelRouter: modelRouterConfig + ? { + enabled: modelRouterConfig.enabled, + categories: modelRouterConfig.categories.length, + } + : null, + operatorSettings: operatorSettingsActive, + skills: { dir: skillsDir, scope: "project" }, + memory: { dir: memoryDir, scope: "project" }, + }), + ); + return; + } + // Provider inventory: names + key-present booleans from auth.yaml, never + // values. Like the skill routes (and unlike /status) this is NOT in the + // header-free read-only allowlist — it needs the server-set studio header. + if (request.method === "GET" && requestURL.pathname === "/providers") { + const rows = listAuthFileProviders(await readAuthFileText()); + response.end( + JSON.stringify({ + providers: rows.map((row) => ({ + name: row.name, + configured: true, + keyPresent: row.keyPresent, + source: "auth.yaml", + testable: Object.hasOwn(providerKeyProbes, row.name), + })), + }), + ); + return; + } + // The provider kinds the daemon understands, with the guided-add snippet + // (a placeholder — this route never sees a real credential). + if (request.method === "GET" && requestURL.pathname === "/providers/known") { + response.end(JSON.stringify({ known: KNOWN_AUTH_PROVIDERS })); + return; + } + // Live provider switch: POST /providers/active { kind }. Unlike + // MECATL_STUDIO_PROVIDER (fixed for the process's lifetime), this + // reassigns activeProviderOverride and restarts mecated on the spot — the + // Studio settings UI's "switch to mock" / "switch to " control. + // Checked BEFORE the /providers/{name} regex below, which would otherwise + // match "active" as a provider name and 404 it first. + if ( + request.method === "POST" && + requestURL.pathname === "/providers/active" + ) { + try { + if ( + !String(request.headers["content-type"] || "") + .toLowerCase() + .startsWith("application/json") + ) + throw Object.assign( + new Error("Content-Type must be application/json"), + { statusCode: 415 }, + ); + const input = JSON.parse( + (await readBody(request, 4096)).toString("utf8"), + ); + const kind = + typeof input?.kind === "string" ? input.kind.trim().toLowerCase() : ""; + const configuredNames = await listConfiguredProviderNames(); + if (!kind || !isSelectableProviderKind(kind, configuredNames)) { + throw Object.assign( + new Error( + kind === "toolhive" + ? "The ToolHive LLM gateway is not reachable right now" + : `"${kind || "(empty)"}" is not mock, toolhive, or a provider configured in ${authFile}`, + ), + { statusCode: 400 }, + ); + } + await queueRestart(async () => { + const previous = activeProviderOverride; + activeProviderOverride = kind; + try { + await startMecatl(preferredKind()); + } catch (error) { + activeProviderOverride = previous; + await startMecatl(preferredKind()); + throw error; + } + }); + response.end( + JSON.stringify({ + ok: true, + provider, + selectedProvider: activeProviderOverride, + }), + ); + } catch (error) { + jsonError( + response, + error.statusCode || 400, + error.message || "Could not switch provider", + ); + } + return; + } + // Key test + removal: POST /providers/{name}/test, DELETE /providers/{name}. + const providerRoute = requestURL.pathname.match( + /^\/providers\/([^/]+?)(?:\/(test))?$/, + ); + if (providerRoute) { + const [, rawName, action] = providerRoute; + let name = rawName; + try { + name = decodeURIComponent(rawName); + } catch { + // Malformed escape: the grammar below rejects percent-shaped names. + } + if (!validProviderName(name)) { + jsonError(response, 400, "not a valid provider name"); + return; + } + if (request.method === "POST" && action === "test") { + // ONE cheap authenticated call with the STORED key, made entirely + // server-side. The key never appears in the response, the logs, or an + // error message; the probe is bounded (10s) and never follows a + // redirect with the credential attached. + if (!Object.hasOwn(providerKeyProbes, name)) { + jsonError( + response, + 400, + `Key testing is not supported for "${name}" — mecated will report an auth problem on first use instead.`, + ); + return; + } + const key = readProviderCredential(await readAuthFileText(), name); + if (!key) { + jsonError( + response, + 400, + `No api_key found for providers.${name} in ${authFile}`, + ); + return; + } + response.end(JSON.stringify(await probeProviderKey(name, key))); + return; + } + if (request.method === "DELETE" && !action) { + // Removing a provider block is a conservative line-range cut of the + // named top-level key (provider-auth.mjs), written temp-file+rename + // with owner-only permissions, then a daemon restart so the change is + // real. Removing the LAST provider is allowed: mecated runs on the + // offline mock without providers (the state the user already sees on + // first run) — the UI's confirm warns, the controller doesn't refuse. + // If the restart then fails (e.g. MECATL_STUDIO_PROVIDER still names + // the removed provider), the removal STANDS — the operator asked for + // the credential to be gone — and the startup error surfaces both in + // this response and on /status.startupError. + try { + await queueRestart(async () => { + const current = await readAuthFileText(); + const { text, removed } = removeAuthFileProvider(current, name); + if (!removed) { + throw Object.assign( + new Error(`No provider named "${name}" in ${authFile}`), + { statusCode: 404 }, + ); + } + const temp = `${authFile}.tmp`; + await writeFile(temp, text, { mode: 0o600 }); + await rename(temp, authFile); + await startMecatl(preferredKind()); + }); + response.end(JSON.stringify({ ok: true, restarted: true })); + } catch (error) { + jsonError( + response, + error.statusCode || 400, + error.message || "Provider removal failed", + ); + } + return; + } + response.statusCode = 404; + response.end(JSON.stringify({ error: "not found" })); + return; + } + // The disabled-skill inventory. Like every controller route this sits + // behind requestIsAllowed (loopback Host + allowlisted Origin + the + // server-set studio header) — the Next server proxy is the only caller. + if (request.method === "GET" && requestURL.pathname === "/skills/disabled") { + response.end(JSON.stringify({ disabled: await listDisabledSkills() })); + return; + } + // Skill creation: POST /skills with { name, body } → // + // SKILL.md. A brand-new skill is invisible until the daemon rebuilds its + // startup snapshot, so creation always restarts mecated — serialized through + // queueRestart like every sibling mutation (existence probes included, so a + // queued restart cannot race them). + if (request.method === "POST" && requestURL.pathname === "/skills") { + try { + if ( + !String(request.headers["content-type"] || "") + .toLowerCase() + .startsWith("application/json") + ) + throw skillClientError("Content-Type must be application/json", 415); + const input = JSON.parse( + (await readBody(request, maxSkillBodyBytes + 16_384)).toString("utf8"), + ); + if (typeof input?.name !== "string") + throw skillClientError("Provide the skill name as { name }"); + const name = input.name; + const paths = skillPaths(name); + if (typeof input?.body !== "string" || input.body.length === 0) + throw skillClientError("Provide the SKILL.md content as { body }"); + if (Buffer.byteLength(input.body, "utf8") > maxSkillBodyBytes) + throw skillClientError( + `SKILL.md is limited to ${maxSkillBodyBytes} bytes`, + 413, + ); + let restarted = false; + await queueRestart(async () => { + // A name taken on EITHER side is a collision: a same-named disabled + // skill would silently resurrect over this content when enabled. + if ( + (await isDirectory(paths.enabled)) || + (await isDirectory(paths.disabled)) + ) + throw skillClientError( + `A skill named "${name}" already exists under ${skillsDir}`, + 409, + ); + await mkdir(paths.enabled, { recursive: true }); + const target = resolve(paths.enabled, "SKILL.md"); + const temp = `${target}.tmp`; + await writeFile(temp, input.body); + await rename(temp, target); + restarted = true; + await startMecatl(preferredKind()); + }); + response.end(JSON.stringify({ ok: true, restarted })); + } catch (error) { + jsonError( + response, + error.statusCode || 400, + error.message || "Skill create failed", + ); + } + return; + } + // Skill CRUD: GET/PUT /skills/{name}/body, POST /skills/{name}/{enable| + // disable}, DELETE /skills/{name}. Mutations serialize through queueRestart + // (checks included, so a queued restart cannot race the side probes) and + // restart mecated whenever the change touches what its startup snapshot saw. + const skillRoute = requestURL.pathname.match( + /^\/skills\/([^/]+?)(?:\/(body|enable|disable|files|file))?$/, + ); + if (skillRoute) { + const [, rawName, action] = skillRoute; + let name = rawName; + try { + try { + name = decodeURIComponent(rawName); + } catch { + // Malformed escape: the raw segment is the only candidate, and the + // name grammar below rejects anything percent-shaped anyway. + } + const paths = skillPaths(name); + if (request.method === "GET" && action === "body") { + for (const side of [paths.enabled, paths.disabled]) { + let body; + try { + body = await readFile(resolve(side, "SKILL.md"), "utf8"); + } catch { + continue; // try the other side + } + response.end(JSON.stringify({ body })); + return; + } + throw skillClientError(`No skill named "${name}" has a SKILL.md`, 404); + } + // Read-only folder views: a skill can be a whole folder of assets + // (scripts/, references/, …), not just a SKILL.md. Listing and preview + // work on whichever side (enabled/disabled) holds the skill. + if (request.method === "GET" && action === "files") { + for (const side of [paths.enabled, paths.disabled]) { + if (!(await isDirectory(side))) continue; + response.end(JSON.stringify({ files: await listSkillFiles(side) })); + return; + } + throw skillClientError(`No skill named "${name}"`, 404); + } + if (request.method === "GET" && action === "file") { + const relPath = requestURL.searchParams.get("path") || ""; + const segments = relPath.split("/"); + if ( + !relPath || + relPath.includes("\\") || + segments.some( + (segment) => segment === "" || segment === "." || segment === "..", + ) + ) + throw skillClientError( + "Provide a relative file path inside the skill as ?path=", + ); + for (const side of [paths.enabled, paths.disabled]) { + if (!(await isDirectory(side))) continue; + const target = resolve(side, relPath); + if (!target.startsWith(side + sep)) + throw skillClientError("Path escapes the skill folder"); + let meta; + try { + meta = await lstat(target); + } catch { + throw skillClientError(`No file "${relPath}" in "${name}"`, 404); + } + if (!meta.isFile()) + throw skillClientError(`"${relPath}" is not a regular file`); + if (meta.size > maxSkillBodyBytes) + throw skillClientError( + `"${relPath}" is too large to preview (limit ${maxSkillBodyBytes} bytes)`, + 413, + ); + const bytes = await readFile(target); + if (bytes.includes(0)) + throw skillClientError( + `"${relPath}" is a binary file — no text preview`, + 415, + ); + response.end(JSON.stringify({ content: bytes.toString("utf8") })); + return; + } + throw skillClientError(`No skill named "${name}"`, 404); + } + if (request.method === "PUT" && action === "body") { + if ( + !String(request.headers["content-type"] || "") + .toLowerCase() + .startsWith("application/json") + ) + throw skillClientError("Content-Type must be application/json", 415); + const input = JSON.parse( + (await readBody(request, maxSkillBodyBytes + 16_384)).toString( + "utf8", + ), + ); + if (typeof input?.body !== "string" || input.body.length === 0) + throw skillClientError("Provide the SKILL.md content as { body }"); + if (Buffer.byteLength(input.body, "utf8") > maxSkillBodyBytes) + throw skillClientError( + `SKILL.md is limited to ${maxSkillBodyBytes} bytes`, + 413, + ); + let restarted = false; + await queueRestart(async () => { + const onEnabled = await isDirectory(paths.enabled); + const onDisabled = await isDirectory(paths.disabled); + if (!onEnabled && !onDisabled) + throw skillClientError(`No skill named "${name}"`, 404); + const target = resolve( + onEnabled ? paths.enabled : paths.disabled, + "SKILL.md", + ); + const temp = `${target}.tmp`; + await writeFile(temp, input.body); + await rename(temp, target); + // A disabled skill is invisible to the daemon's snapshot, so + // editing it owes no restart. + if (onEnabled) { + restarted = true; + await startMecatl(preferredKind()); + } + }); + response.end(JSON.stringify({ ok: true, restarted })); + return; + } + if ( + request.method === "POST" && + (action === "enable" || action === "disable") + ) { + let restarted = false; + await queueRestart(async () => { + const onEnabled = await isDirectory(paths.enabled); + const onDisabled = await isDirectory(paths.disabled); + if (!onEnabled && !onDisabled) + throw skillClientError(`No skill named "${name}"`, 404); + if (onEnabled && onDisabled) + throw skillClientError( + `Both an enabled and a disabled "${name}" exist under ${skillsDir}; resolve the collision on disk first`, + 409, + ); + // Idempotent-ish: already on the requested side moves nothing and + // restarts nothing. + if (action === "disable" ? onDisabled : onEnabled) return; + if (action === "disable") { + await mkdir(disabledSkillsDir, { recursive: true }); + await rename(paths.enabled, paths.disabled); + } else { + await rename(paths.disabled, paths.enabled); + } + restarted = true; + await startMecatl(preferredKind()); + }); + response.end(JSON.stringify({ ok: true, restarted })); + return; + } + if (request.method === "DELETE" && !action) { + let restarted = false; + await queueRestart(async () => { + const onEnabled = await isDirectory(paths.enabled); + const onDisabled = await isDirectory(paths.disabled); + if (!onEnabled && !onDisabled) + throw skillClientError(`No skill named "${name}"`, 404); + // A delete means gone from BOTH sides — never a hidden disabled + // copy waiting to resurrect under the same name. + if (onDisabled) + await rm(paths.disabled, { recursive: true, force: true }); + if (onEnabled) { + await rm(paths.enabled, { recursive: true, force: true }); + restarted = true; + await startMecatl(preferredKind()); + } + }); + response.end(JSON.stringify({ ok: true, restarted })); + return; + } + response.statusCode = 404; + response.end(JSON.stringify({ error: "not found" })); + } catch (error) { + jsonError( + response, + error.statusCode || 400, + error.message || "Skill update failed", + ); + } + return; + } + if (request.method === "GET" && requestURL.pathname === "/model-router") { + response.end( + JSON.stringify({ + config: modelRouterConfig, + managedBy: operatorSettingsActive ? "operator-settings" : "studio", + }), + ); + return; + } + if ( + request.method !== "POST" || + !["/mcp", "/model-router"].includes(requestURL.pathname) + ) { + response.statusCode = 404; + response.end(JSON.stringify({ error: "not found" })); + return; + } + try { + if ( + !String(request.headers["content-type"] || "") + .toLowerCase() + .startsWith("application/json") + ) { + throw Object.assign(new Error("Content-Type must be application/json"), { + statusCode: 415, + }); + } + const input = JSON.parse( + (await readBody(request, 16_384)).toString("utf8"), + ); + if (requestURL.pathname === "/model-router") { + if (operatorSettingsActive) + throw new Error( + "Routing is managed by the imported operator settings. Update the complete settings file to preserve its aliases, slots, and guardrails.", + ); + const nextConfig = normalizeModelRouter(input); + await queueRestart(async () => { + const previousConfig = modelRouterConfig; + modelRouterConfig = nextConfig; + await persistModelRouter(nextConfig); + try { + await startMecatl(preferredKind()); + } catch (error) { + modelRouterConfig = previousConfig; + if (previousConfig) await persistModelRouter(previousConfig); + else + await Promise.all([ + rm(routerSettingsFile, { force: true }), + rm(routerStateFile, { force: true }), + ]); + await startMecatl(preferredKind()); + throw error; + } + }); + response.end(JSON.stringify({ ok: true, config: modelRouterConfig })); + return; + } + if (!/^[A-Za-z0-9_]+$/.test(input.name || "")) + throw new Error( + "Gateway name may contain only letters, numbers, and underscores", + ); + const parsed = validateGatewayURL(input.url, { + allowLoopbackHTTP: process.env.MECATL_ALLOW_INSECURE_LOOPBACK_MCP === "1", + }); + const token = + typeof input.token === "string" + ? input.token.trim().replace(/^Bearer\s+/i, "") + : ""; + const candidate = { name: input.name, url: parsed.toString(), token }; + await queueRestart(async () => { + const previousGateway = gateway; + gateway = candidate; + try { + await startMecatl(preferredKind()); + } catch (error) { + // Keep the provider usable and keep rejected gateway credentials out of + // controller state. The caller still receives the original handshake error. + gateway = previousGateway; + await startMecatl(preferredKind()); + throw error; + } + }); + response.end( + JSON.stringify({ + ok: true, + gateway: { name: gateway.name, url: gateway.url }, + }), + ); + } catch (error) { + jsonError( + response, + error.statusCode || 400, + error.message || "Could not update the Mecatl controller", + ); + } +}); + +server.listen(8788, "127.0.0.1", async () => { + process.stdout.write("Mecatl local controller: http://127.0.0.1:8788\n"); + operatorSettingsActive = await hasOperatorSettings(); + modelRouterConfig = await loadModelRouter(); + toolhiveReady = await detectToolhiveGateway(); + process.stdout.write( + toolhiveReady + ? `ToolHive LLM gateway detected at ${toolhiveGatewayURL}\n` + : `ToolHive LLM gateway not reachable at ${toolhiveGatewayURL} (start it with "thv llm proxy start"); falling back to the offline mock\n`, + ); + try { + await startMecatl(preferredKind()); + } catch (error) { + startupError ||= error.message || "mecated could not start"; + process.stderr.write(`${startupError}\n`); + } +}); + +for (const signal of ["SIGINT", "SIGTERM"]) { + process.on(signal, async () => { + shuttingDown = true; + if (restartTimer) clearTimeout(restartTimer); + await stopChild(); + server.close(() => process.exit(0)); + }); +} diff --git a/studio/src/app/api/mecatl-control/[...path]/route.ts b/studio/src/app/api/mecatl-control/[...path]/route.ts new file mode 100644 index 0000000000..3bb3061b37 --- /dev/null +++ b/studio/src/app/api/mecatl-control/[...path]/route.ts @@ -0,0 +1,14 @@ +import { proxyControl } from "@/lib/server-proxy"; + +type Context = { params: Promise<{ path: string[] }> }; + +const handle = async (request: Request, context: Context) => + proxyControl(request, (await context.params).path); + +export { + handle as DELETE, + handle as GET, + handle as PATCH, + handle as POST, + handle as PUT, +}; diff --git a/studio/src/app/api/mecatl/[...path]/route.ts b/studio/src/app/api/mecatl/[...path]/route.ts new file mode 100644 index 0000000000..e6dff5be20 --- /dev/null +++ b/studio/src/app/api/mecatl/[...path]/route.ts @@ -0,0 +1,14 @@ +import { proxyMecatl } from "@/lib/server-proxy"; + +type Context = { params: Promise<{ path: string[] }> }; + +const handle = async (request: Request, context: Context) => + proxyMecatl(request, (await context.params).path); + +export { + handle as DELETE, + handle as GET, + handle as PATCH, + handle as POST, + handle as PUT, +}; diff --git a/studio/src/app/brand/logo/route.test.ts b/studio/src/app/brand/logo/route.test.ts new file mode 100644 index 0000000000..5f3a7149dd --- /dev/null +++ b/studio/src/app/brand/logo/route.test.ts @@ -0,0 +1,71 @@ +import { beforeEach, expect, it, vi } from "vitest"; +import { GET } from "./route"; + +const mockReadFile = vi.hoisted(() => vi.fn()); +vi.mock("node:fs/promises", () => ({ readFile: mockReadFile })); + +beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal("fetch", vi.fn()); + mockReadFile.mockResolvedValue("stacklok logo"); +}); + +it("serves the fallback SVG when BRAND_LOGO_URL is not set", async () => { + vi.stubEnv("BRAND_LOGO_URL", ""); + const response = await GET(); + expect(response.headers.get("Content-Type")).toBe("image/svg+xml"); + expect(mockReadFile).toHaveBeenCalledWith( + `${process.cwd()}/public/stacklok-logo.svg`, + ); +}); + +it("proxies BRAND_LOGO_URL and forwards the upstream Content-Type", async () => { + vi.stubEnv("BRAND_LOGO_URL", "https://example.com/logo.png"); + vi.stubGlobal( + "fetch", + vi + .fn() + .mockResolvedValue( + new Response("PNG data", { headers: { "Content-Type": "image/png" } }), + ), + ); + const response = await GET(); + expect(response.headers.get("Content-Type")).toBe("image/png"); +}); + +it("defaults Content-Type to image/svg+xml when upstream omits it", async () => { + vi.stubEnv("BRAND_LOGO_URL", "https://example.com/logo.svg"); + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode("brand")); + controller.close(); + }, + }); + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue(new Response(stream, { status: 200 })), + ); + const response = await GET(); + expect(response.headers.get("Content-Type")).toBe("image/svg+xml"); +}); + +it("returns 404 when upstream returns a non-ok status", async () => { + vi.stubEnv("BRAND_LOGO_URL", "https://example.com/logo.svg"); + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue(new Response(null, { status: 500 })), + ); + const response = await GET(); + expect(response.status).toBe(404); +}); + +it("returns 404 when fetch throws a network error", async () => { + vi.stubEnv("BRAND_LOGO_URL", "https://example.com/logo.svg"); + vi.stubGlobal( + "fetch", + vi.fn().mockRejectedValue(new Error("Network failure")), + ); + const response = await GET(); + expect(response.status).toBe(404); +}); diff --git a/studio/src/app/brand/logo/route.ts b/studio/src/app/brand/logo/route.ts new file mode 100644 index 0000000000..af7639cec8 --- /dev/null +++ b/studio/src/app/brand/logo/route.ts @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 + +import * as fsPromises from "node:fs/promises"; + +// force-dynamic: BRAND_LOGO_URL is a runtime env var set via Helm/Kubernetes. +export const dynamic = "force-dynamic"; + +async function serveFallback(): Promise { + const content = await fsPromises.readFile( + `${process.cwd()}/public/stacklok-logo.svg`, + ); + return new Response(content, { + headers: { "Content-Type": "image/svg+xml" }, + }); +} + +export async function GET(): Promise { + const brandLogoUrl = process.env.BRAND_LOGO_URL; + if (!brandLogoUrl) return serveFallback(); + try { + const upstream = await fetch(brandLogoUrl); + if (!upstream.ok) return new Response(null, { status: 404 }); + const contentType = upstream.headers.get("Content-Type") ?? "image/svg+xml"; + return new Response(upstream.body, { + headers: { "Content-Type": contentType }, + }); + } catch { + return new Response(null, { status: 404 }); + } +} diff --git a/studio/src/app/error.tsx b/studio/src/app/error.tsx new file mode 100644 index 0000000000..41472d4901 --- /dev/null +++ b/studio/src/app/error.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { ErrorPage } from "@/components/error-page/error-page-client"; + +interface ErrorProps { + error: Error & { digest?: string }; + reset: () => void; +} + +export default function RootErrorPage({ error, reset }: ErrorProps) { + return ; +} diff --git a/studio/src/app/global-error.tsx b/studio/src/app/global-error.tsx new file mode 100644 index 0000000000..3d3eba6369 --- /dev/null +++ b/studio/src/app/global-error.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { Inter } from "next/font/google"; +import { BrandLogo } from "@/components/brand-logo"; +import { Button } from "@/components/ui/button"; +import "./globals.css"; + +const inter = Inter({ + variable: "--font-inter", + subsets: ["latin"], +}); + +interface GlobalErrorProps { + reset: () => void; +} + +export default function GlobalError({ reset }: GlobalErrorProps) { + return ( + + +
+ + +

+ Something went wrong +

+ + +
+ + + ); +} diff --git a/studio/src/app/globals.css b/studio/src/app/globals.css new file mode 100644 index 0000000000..3ddfc4a91d --- /dev/null +++ b/studio/src/app/globals.css @@ -0,0 +1,409 @@ +@import "tailwindcss"; + +@custom-variant dark (&:is(.dark *)); + +/* Tokens hold complete CSS color values — not bare HSL triplets. + This keeps operator branding overrides format-agnostic (hex, hsl, oklch all work). */ +:root { + --background: hsl(0 0% 100%); + --foreground: hsl(240 10% 3.9%); + --card: hsl(0 0% 100%); + --card-foreground: hsl(240 10% 3.9%); + --popover: hsl(0 0% 100%); + --popover-foreground: hsl(240 10% 3.9%); + --primary: hsl(240 5.9% 10%); + --primary-foreground: hsl(0 0% 98%); + --secondary: hsl(240 4.8% 95.9%); + --secondary-foreground: hsl(240 5.9% 10%); + --muted: hsl(240 4.8% 95.9%); + --muted-foreground: hsl(240 3.8% 46.1%); + --accent: hsl(240 4.8% 95.9%); + --accent-foreground: hsl(240 5.9% 10%); + --destructive: hsl( + 0 72% 51% + ); /* darker than shadcn's default, per the design system */ + --destructive-foreground: hsl(0 0% 98%); + --destructive-strong: hsl( + 0 74% 40% + ); /* a darker red for icons over a destructive/10 tile */ + --border: hsl(240 5.9% 90%); + --input: hsl(240 5.9% 90%); + --ring: hsl(240 5% 65%); /* soft neutral focus ring — gray, not near-black */ + --radius: 0.5rem; + --sidebar: hsl(240 4.8% 98.5%); + --brand: hsl(161 94% 21%); /* #036a49 — Stacklok brand green */ + --brand-foreground: hsl(0 0% 100%); + --avatar-background: oklch(0.696 0 0 / 89.8%); + --nav-background: #18442e; + --nav-border: #265b41; + --nav-button-active-bg: #398560; + --nav-button-active-text: #ffffff; + + /* Status, display, and link tokens (ADR-0018 convergence palette). + Complete colour values, never bare HSL triplets. */ + --success: hsl(142 71% 29%); /* green — succeeded / healthy state */ + --warning: hsl(35 92% 42%); /* amber — needs attention */ + --info: hsl(211 92% 43%); /* blue — informational */ + --title: hsl(240 5% 18%); /* serif page-title ink — softer than body text */ + --logo: hsl(0 0% 28%); /* wordmark / logo fill — neutral ink */ + --brand-label: hsl(161 94% 21%); /* brand-coloured label text */ + --link: hsl(211 92% 43%); /* inline hyperlink */ + --brand-ink: hsl( + 161 94% 21% + ); /* brand accent ink: nav/tab active, avatar initials */ + --input-icon: hsl(240 3.8% 46.1%); /* leading input icon ink (muted) */ + --badge-outline-border: hsl( + 240 5.9% 90% + ); /* outline badge border (= border) */ + --badge-outline-text: hsl( + 240 10% 3.9% + ); /* outline badge ink (= foreground) */ + --btn-primary: hsl(161 94% 21%); /* primary CTA fill (= brand) */ + --btn-primary-hover: #02543a; /* primary CTA hover */ + + /* Legacy shadcn sidebar family — consumed by src/components/ui/sidebar.tsx + (assistant panel). Keep until that primitive is retired. */ + --sidebar-foreground: hsl(240 5.3% 26.1%); + --sidebar-primary: hsl(240 5.9% 10%); + --sidebar-primary-foreground: hsl(0 0% 98%); + --sidebar-accent: hsl(240 4.8% 95.9%); + --sidebar-accent-foreground: hsl(240 5.9% 10%); + --sidebar-border: hsl(220 13% 91%); + --sidebar-ring: hsl(217.2 91.2% 59.8%); +} + +.dark { + --background: hsl(240 6% 10%); + --foreground: hsl(0 0% 98%); + --card: hsl(240 6.1% 12.9%); /* cards are elevated in dark */ + --card-foreground: hsl(0 0% 98%); + --popover: hsl(240 6.3% 12.5%); + --popover-foreground: hsl(0 0% 98%); + --primary: hsl(210 34.5% 98%); + --primary-foreground: hsl(0 0% 9.1%); + --secondary: hsl(240 4% 15.9%); + --secondary-foreground: hsl(0 0% 98%); + --muted: hsl(240 6.3% 12.5%); + --muted-foreground: hsl(0 0% 63%); + --accent: hsl(240 4% 15.9%); + --accent-foreground: hsl(0 0% 98%); + --destructive: hsl(358.7 100% 69.6%); + --destructive-foreground: hsl(0 0% 98%); + --destructive-strong: hsl(358.7 78% 55%); + --border: hsl(0 0% 15.5%); + --input: hsl(0 0% 20.4%); + --ring: hsl(0 0% 45.2%); + --sidebar: hsl(240 6.3% 12.5%); + --brand-foreground: hsl(0 0% 100%); + --avatar-background: oklch(0.696 0 0 / 89.8%); + --nav-background: #0f2e1e; + --nav-border: #1a4430; + --nav-button-active-bg: #2d6b4a; + --nav-button-active-text: #f0f0f0; + + /* Status, display, and link tokens — dark-mode variants. */ + --success: hsl(142 61% 55%); /* lighter green for dark surfaces */ + --warning: hsl(35 92% 62%); /* lighter amber */ + --info: hsl(211 92% 66%); /* lighter blue */ + --title: hsl(0 0% 74%); /* serif page-title ink, softer than body */ + --logo: hsl(0 0% 58%); /* wordmark / logo fill — neutral ink */ + --brand-label: hsl(158 64% 52%); /* brand label text on dark */ + --link: hsl(211 92% 66%); /* inline hyperlink on dark */ + --brand-ink: hsl(142 50% 60%); /* softer brand ink on dark */ + --input-icon: hsl(0 0% 100%); /* near-white icon ink on dark */ + --badge-outline-border: hsl(0 0% 32%); /* lighter outline on dark surfaces */ + --badge-outline-text: hsl(0 0% 80%); /* dimmed outline ink on dark */ + --btn-primary: #024c34; /* darker primary CTA on dark */ + --btn-primary-hover: #013826; /* dark hover */ + + /* Legacy shadcn sidebar family — dark variants. */ + --sidebar-foreground: hsl(240 4.8% 95.9%); + --sidebar-primary: hsl(224.3 76.3% 48%); + --sidebar-primary-foreground: hsl(0 0% 100%); + --sidebar-accent: hsl(240 3.7% 15.9%); + --sidebar-accent-foreground: hsl(240 4.8% 95.9%); + --sidebar-border: hsl(240 3.7% 15.9%); + --sidebar-ring: hsl(217.2 91.2% 59.8%); +} + +/* Maps CSS tokens → Tailwind utility classes. + bg-background, text-foreground, border-border, bg-nav-background, etc. */ +@theme inline { + --font-sans: var(--font-inter); + --font-serif: var(--font-merriweather); + + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-destructive-strong: var(--destructive-strong); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + --color-avatar-background: var(--avatar-background); + --color-nav-background: var(--nav-background); + --color-nav-border: var(--nav-border); + --color-nav-button-active-bg: var(--nav-button-active-bg); + --color-nav-button-active-text: var(--nav-button-active-text); + --color-brand: var(--brand); + --color-brand-foreground: var(--brand-foreground); + + /* Convergence palette → utilities: bg-success, text-warning, text-info, + text-title, text-logo, text-brand-label, text-link, etc. */ + --color-success: var(--success); + --color-warning: var(--warning); + --color-info: var(--info); + --color-title: var(--title); + --color-logo: var(--logo); + --color-brand-label: var(--brand-label); + --color-link: var(--link); + --color-brand-ink: var(--brand-ink); + --color-input-icon: var(--input-icon); + --color-badge-outline-border: var(--badge-outline-border); + --color-badge-outline-text: var(--badge-outline-text); + --color-btn-primary: var(--btn-primary); + --color-btn-primary-hover: var(--btn-primary-hover); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply text-foreground; + font-family: var(--font-inter), sans-serif; + } + /* Paint the canvas AND body with the shell gradient's terminal colour: + iOS standalone computes dvh stale at launch (self-corrects on first + scroll), leaving a strip beside/below the app, and overscroll exposes + the canvas too. Every visible surface (the card, panels, dialogs) + declares its own bg-background, so nothing reads body's background — + matching the gradient's end keeps the stale strip invisible. */ + html, + body { + background-color: #06202a; + } + html.dark, + html.dark body { + background-color: #02141b; + } + /* Mobile is read at arm's length — bump the default root size past + native body text (~17pt). The Interface scale slider multiplies + through --ui-scale rather than replacing the base. */ + html { + font-size: calc(100% * var(--ui-scale, 1)); + } + @media (max-width: 499px) { + html { + font-size: calc(18px * var(--ui-scale, 1)); + } + } +} + +@layer utilities { + .text-page-title { + font-family: var(--font-merriweather), serif; + font-size: 34px; + line-height: 42px; + font-weight: 300; + letter-spacing: -0.85px; + } +} + +[data-sonner-toaster] { + --normal-bg: var(--popover); + --normal-text: var(--popover-foreground); + --normal-border: var(--border); +} + +/* Tiptap / ProseMirror editor styles */ +.tiptap-editor .ProseMirror { + outline: none; + min-height: 100%; + padding: 1.5rem; + color: var(--foreground); + font-size: 0.9375rem; + line-height: 1.75; +} + +.tiptap-editor .ProseMirror h1 { + font-size: 1.75rem; + font-weight: 700; + line-height: 1.25; + margin: 1.5rem 0 0.75rem; +} + +.tiptap-editor .ProseMirror h2 { + font-size: 1.35rem; + font-weight: 600; + line-height: 1.3; + margin: 1.25rem 0 0.5rem; +} + +.tiptap-editor .ProseMirror h3 { + font-size: 1.1rem; + font-weight: 600; + line-height: 1.4; + margin: 1rem 0 0.4rem; +} + +.tiptap-editor .ProseMirror p { + margin: 0.5rem 0; +} + +.tiptap-editor .ProseMirror ul, +.tiptap-editor .ProseMirror ol { + margin: 0.5rem 0; + padding-left: 1.5rem; +} + +.tiptap-editor .ProseMirror ul { + list-style-type: disc; +} + +.tiptap-editor .ProseMirror ol { + list-style-type: decimal; +} + +.tiptap-editor .ProseMirror li { + margin: 0.2rem 0; +} + +.tiptap-editor .ProseMirror blockquote { + border-left: 3px solid + color-mix(in oklab, var(--muted-foreground) 30%, transparent); + padding-left: 1rem; + margin: 0.75rem 0; + color: var(--muted-foreground); + font-style: italic; +} + +.tiptap-editor .ProseMirror code { + background: var(--muted); + border-radius: 0.25rem; + padding: 0.15rem 0.4rem; + font-size: 0.85em; + font-family: ui-monospace, monospace; +} + +.tiptap-editor .ProseMirror pre { + background: hsl(240 10% 10%); + color: hsl(0 0% 90%); + border-radius: 0.5rem; + padding: 1rem; + margin: 0.75rem 0; + overflow-x: auto; + font-size: 0.85em; + font-family: ui-monospace, monospace; + line-height: 1.6; +} + +.tiptap-editor .ProseMirror pre code { + background: transparent; + padding: 0; + color: inherit; +} + +.tiptap-editor .ProseMirror strong { + font-weight: 600; +} + +.tiptap-editor .ProseMirror em { + font-style: italic; +} + +.tiptap-editor .ProseMirror hr { + border: none; + border-top: 1px solid var(--border); + margin: 1rem 0; +} + +.tiptap-editor .ProseMirror a { + color: var(--link); + text-decoration: underline; + text-underline-offset: 2px; +} + +/* Placeholder */ +.tiptap-editor .ProseMirror p.is-editor-empty:first-child::before { + content: attr(data-placeholder); + color: var(--muted-foreground); + pointer-events: none; + float: left; + height: 0; +} + +/* Chat composer: a plain-text-style single field (no rich formatting). + Mirrors the old transparent-textarea look — 14px, tight leading, muted + placeholder — so swapping in ProseMirror keeps the composer identical. */ +.composer-editor .ProseMirror { + outline: none; + font-size: 0.875rem; + line-height: 1.5rem; + color: var(--foreground); + white-space: pre-wrap; + word-break: break-word; + overflow-wrap: break-word; +} + +.composer-editor .ProseMirror p { + margin: 0; +} + +.composer-editor .ProseMirror p.is-editor-empty:first-child::before { + content: attr(data-placeholder); + color: color-mix(in oklab, var(--muted-foreground) 60%, transparent); + pointer-events: none; + float: left; + height: 0; +} + +/* Mobile: small UI glyphs step up one notch for touch legibility. Unlayered + on purpose — it must override the size-* utilities, which live in a + cascade layer; larger deliberate icon sizes are untouched. */ +@media (max-width: 499px) { + svg.lucide.size-4, + svg.lucide.size-\[17px\] { + width: 1.25rem; + height: 1.25rem; + } + svg.lucide.size-3\.5 { + width: 1.125rem; + height: 1.125rem; + } +} + +/* Thinking-indicator dots: Tailwind's animate-bounce moves 25% of the + element's own height — invisible on a 6px dot. A fixed 5px hop with a + squash-and-stretch easing reads as an actual bounce. */ +@keyframes thinking-bounce { + 0%, + 100% { + transform: translateY(0); + animation-timing-function: cubic-bezier(0.8, 0, 1, 1); + } + 50% { + transform: translateY(-5px); + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); + } +} diff --git a/studio/src/app/icon.test.ts b/studio/src/app/icon.test.ts new file mode 100644 index 0000000000..7f746b8b2f --- /dev/null +++ b/studio/src/app/icon.test.ts @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) Stacklok, Inc. All rights reserved. + +import { expect, it, vi } from "vitest"; +import { GET } from "./icon"; + +it("returns a Response when FAVICON_URL is not set", async () => { + vi.stubEnv("FAVICON_URL", ""); + await expect(GET()).resolves.toBeInstanceOf(Response); +}); + +it("returns a Response when FAVICON_URL is set", async () => { + vi.stubEnv("FAVICON_URL", "https://www.google.com/favicon.ico"); + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue(new Response(new Uint8Array([0]))), + ); + await expect(GET()).resolves.toBeInstanceOf(Response); +}); diff --git a/studio/src/app/icon.ts b/studio/src/app/icon.ts new file mode 100644 index 0000000000..8b92b99787 --- /dev/null +++ b/studio/src/app/icon.ts @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) Stacklok, Inc. All rights reserved. + +import * as fs from "node:fs"; + +// force-dynamic prevents Next.js from caching this route at build time. +// FAVICON_URL is a runtime environment variable — it is set via Helm values +// and must be read on each request, not baked into the build output. +export const dynamic = "force-dynamic"; + +async function customFavicon(url: string): Promise { + const upstream = await fetch(url); + const body = await upstream.arrayBuffer(); + return new Response(body, { + headers: { + "Content-Type": upstream.headers.get("Content-Type") ?? "image/x-icon", + }, + }); +} + +function defaultFavicon(): Response { + const pngContent = fs.readFileSync( + `${process.cwd()}/public/stacklok-favicon.png`, + ); + return new Response(pngContent, { + headers: { "Content-Type": "image/png" }, + }); +} + +export async function GET(): Promise { + const faviconUrl = process.env.FAVICON_URL; + return faviconUrl ? customFavicon(faviconUrl) : defaultFavicon(); +} + +export default GET; diff --git a/studio/src/app/layout.tsx b/studio/src/app/layout.tsx new file mode 100644 index 0000000000..4691597bf8 --- /dev/null +++ b/studio/src/app/layout.tsx @@ -0,0 +1,67 @@ +import type { Metadata, Viewport } from "next"; +import { Inter, Merriweather } from "next/font/google"; +import { ClientProviders } from "@/components/client-providers"; +import { ServerProviders } from "@/components/server-providers"; +import "./globals.css"; + +const inter = Inter({ + variable: "--font-inter", + subsets: ["latin"], +}); + +const merriweather = Merriweather({ + variable: "--font-merriweather", + subsets: ["latin"], + weight: ["300", "400", "700"], +}); + +export const metadata: Metadata = { + title: { + template: "%s — Mecatl Studio", + default: "Mecatl Studio", + }, + description: "The web workspace for the Mecatl agent harness", + icons: { + apple: "/apple-touch-icon.png", + }, + appleWebApp: { + capable: true, + statusBarStyle: "black-translucent", + title: "Studio", + }, +}; + +export const viewport: Viewport = { + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "#03433e" }, + { media: "(prefers-color-scheme: dark)", color: "#02141b" }, + ], + viewportFit: "cover", + // App-like surface: pinch-zoom off (text sizes stay OS-controlled). + maximumScale: 1, + userScalable: false, + // Android Chrome's default keeps the LAYOUT viewport fixed under the + // on-screen keyboard (only the visual viewport shrinks), which buries the + // docked composer behind it. resizes-content restores layout resize so + // h-dvh — and the composer with it — shrinks above the keyboard. iOS + // ignores this key. + interactiveWidget: "resizes-content", +}; + +export default async function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + + ); +} diff --git a/studio/src/app/manifest.ts b/studio/src/app/manifest.ts new file mode 100644 index 0000000000..2df26b2c09 --- /dev/null +++ b/studio/src/app/manifest.ts @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) Stacklok, Inc. All rights reserved. + +import type { MetadataRoute } from "next"; + +// Deliberately NO service worker: Studio is a client for a live local +// `mecated` daemon, so offline caching would lie about live state (sessions, +// runs, schedules). Installability no longer requires a service worker on +// Chrome/Android, and never did on iOS — the manifest alone is enough. +export default function manifest(): MetadataRoute.Manifest { + return { + name: "Mecatl Studio", + short_name: "Studio", + description: "The web workspace for the Mecatl agent harness", + start_url: "/workspace/chat", + scope: "/", + display: "standalone", + background_color: "#03433e", + theme_color: "#03433e", + icons: [ + { + src: "/icon-192.png", + sizes: "192x192", + type: "image/png", + }, + { + src: "/icon-512.png", + sizes: "512x512", + type: "image/png", + }, + { + src: "/icon-maskable-512.png", + sizes: "512x512", + type: "image/png", + purpose: "maskable", + }, + ], + }; +} diff --git a/studio/src/app/not-found.test.tsx b/studio/src/app/not-found.test.tsx new file mode 100644 index 0000000000..41cb9f6c54 --- /dev/null +++ b/studio/src/app/not-found.test.tsx @@ -0,0 +1,42 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import NotFound from "./not-found"; + +describe("NotFound (root)", () => { + it("displays a page not found heading", async () => { + render(await NotFound()); + + expect( + screen.getByRole("heading", { name: /page not found/i }), + ).toBeVisible(); + }); + + it("displays a generic error message", async () => { + render(await NotFound()); + + expect( + screen.getByText(/the page you're looking for doesn't exist/i), + ).toBeVisible(); + }); + + it("has a link home", async () => { + render(await NotFound()); + + const link = screen.getByRole("link", { name: /go home/i }); + expect(link).toHaveAttribute("href", "/workspace/chat"); + }); + + it("does not have a back button", async () => { + render(await NotFound()); + + expect( + screen.queryByRole("button", { name: /back/i }), + ).not.toBeInTheDocument(); + }); + + it("displays the brand header", async () => { + render(await NotFound()); + + expect(screen.getByRole("banner")).toBeVisible(); + }); +}); diff --git a/studio/src/app/not-found.tsx b/studio/src/app/not-found.tsx new file mode 100644 index 0000000000..2ac3da25da --- /dev/null +++ b/studio/src/app/not-found.tsx @@ -0,0 +1,43 @@ +import Link from "next/link"; +import { ErrorPageLayout } from "@/components/error-page/error-page"; +import { Button } from "@/components/ui/button"; + +export default async function NotFound() { + return ( +
+
+ + {/* The stock wordmark asset is white, so it is rendered as a mask + tinted with the theme's --logo token (same as the shell sidebar). */} +
+
+ + Go home + + } + > + The page you're looking for doesn't exist or has been moved. + +
+
+ ); +} diff --git a/studio/src/app/page.tsx b/studio/src/app/page.tsx new file mode 100644 index 0000000000..39aab2bc67 --- /dev/null +++ b/studio/src/app/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function Home() { + redirect("/workspace/chat"); +} diff --git a/studio/src/app/workspace/_components/chat-input.test.tsx b/studio/src/app/workspace/_components/chat-input.test.tsx new file mode 100644 index 0000000000..c49bdcfdc0 --- /dev/null +++ b/studio/src/app/workspace/_components/chat-input.test.tsx @@ -0,0 +1,101 @@ +import { render, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { AttachmentPill, resolveComposerAction } from "./chat-input"; + +/** + * The Enter matrix is tested through `resolveComposerAction`, the pure + * decision function the composer's keydown handler and send button both call. + * Driving the TipTap editor's ProseMirror view with synthetic keydowns in + * jsdom is impractical (the editor mounts asynchronously and owns its own + * capture-phase handlers), so the decision table is extracted and tested + * exhaustively instead; "newline" means the key is NOT intercepted — the + * editor's own hardBreak inserts the newline and onSend is never called. + */ +describe("resolveComposerAction", () => { + const resolve = ( + shift: boolean, + isStreaming: boolean, + behavior: "queue" | "steer", + hasAttachments = false, + ) => resolveComposerAction({ shift, isStreaming, behavior, hasAttachments }); + + it("sends on idle Enter, whatever the preference", () => { + expect(resolve(false, false, "queue")).toBe("send"); + expect(resolve(false, false, "steer")).toBe("send"); + }); + + it("keeps idle Shift+Enter as a newline (the key is not intercepted, so onSend is never called)", () => { + expect(resolve(true, false, "queue")).toBe("newline"); + expect(resolve(true, false, "steer")).toBe("newline"); + }); + + it("queues on streaming Enter with the default preference", () => { + expect(resolve(false, true, "queue")).toBe("queue"); + }); + + it("steers on streaming Shift+Enter with the default preference", () => { + expect(resolve(true, true, "queue")).toBe("steer"); + }); + + it("inverts both keys when the preference is steer", () => { + expect(resolve(false, true, "steer")).toBe("steer"); + expect(resolve(true, true, "steer")).toBe("queue"); + }); + + it("always queues while streaming with files attached — a steer carries text only", () => { + expect(resolve(false, true, "queue", true)).toBe("queue"); + expect(resolve(true, true, "queue", true)).toBe("queue"); + expect(resolve(false, true, "steer", true)).toBe("queue"); + expect(resolve(true, true, "steer", true)).toBe("queue"); + }); + + it("sends attachments normally when idle", () => { + expect(resolve(false, false, "queue", true)).toBe("send"); + expect(resolve(false, false, "steer", true)).toBe("send"); + }); +}); + +describe("AttachmentPill", () => { + // jsdom has no object-URL implementation; stub the pair the pill uses. + const createObjectURL = vi.fn(() => "blob:thumb"); + const revokeObjectURL = vi.fn(); + + beforeEach(() => { + createObjectURL.mockClear(); + revokeObjectURL.mockClear(); + vi.stubGlobal("URL", { + ...URL, + createObjectURL, + revokeObjectURL, + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("shows a thumbnail for an image file and revokes its object URL on unmount", async () => { + const file = new File(["x"], "photo.png", { type: "image/png" }); + const { container, unmount } = render( + {}} />, + ); + await waitFor(() => + expect(container.querySelector("img")?.getAttribute("src")).toBe( + "blob:thumb", + ), + ); + unmount(); + expect(revokeObjectURL).toHaveBeenCalledWith("blob:thumb"); + }); + + it("shows a file-kind glyph, not a thumbnail, for a non-image file", () => { + const file = new File(["x"], "report.pdf", { type: "application/pdf" }); + const { container } = render( + {}} />, + ); + expect(container.querySelector("img")).toBeNull(); + expect(createObjectURL).not.toHaveBeenCalled(); + expect(container.querySelector('[aria-label="PDF"]')).toBeTruthy(); + expect(container.textContent).toContain("report.pdf"); + }); +}); diff --git a/studio/src/app/workspace/_components/chat-input.tsx b/studio/src/app/workspace/_components/chat-input.tsx new file mode 100644 index 0000000000..5fcca16cdb --- /dev/null +++ b/studio/src/app/workspace/_components/chat-input.tsx @@ -0,0 +1,1615 @@ +"use client"; + +import Placeholder from "@tiptap/extension-placeholder"; +import { EditorContent, useEditor } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import type { SuggestionProps } from "@tiptap/suggestion"; +import { + ArrowUp, + Bot, + Brain, + Check, + ChevronDown, + ChevronRight, + FolderClosed, + FolderPlus, + Mic, + Paperclip, + Plus, + RotateCcw, + Shield, + SlidersHorizontal, +} from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; +import { + getAgentMentions, + getSlashCommands, +} from "@/features/agent/composer-capabilities"; +import { usePrompt } from "@/hooks/use-prompt"; +import { fileKindMeta } from "@/lib/file-meta"; +import { + type EnterSendBehavior, + useEnterSendBehavior, +} from "@/lib/profile-preferences"; +import type { SessionPermissionMode } from "@/lib/protocol"; +import { cn } from "@/lib/utils"; +import { + type ComposerMenuItem, + composerText, + createComposerMentions, + setComposerText, +} from "./composer-mentions"; + +interface ProjectItem { + id: string; + name: string; + color: string; +} + +interface ChatInputProps { + placeholder?: string; + rows?: number; + projects?: ProjectItem[]; + selectedProjectId?: string | null; + onSelectProject?: (id: string | null) => void; + onCreateProject?: (name: string) => void; + compact?: boolean; + /** Docked at a screen edge on mobile: full-bleed single row with only a + hairline top border, instead of the floating rounded box. */ + mobileDocked?: boolean; + /** Refocuses the field when this changes (e.g. the open chat's id). */ + focusKey?: string; + onSend?: (content: string, files?: File[]) => void; + onQueue?: (content: string) => void; + /** Injects the text into the in-flight run at the next step (mid-run + steering). Only meaningful while `isStreaming`. */ + onSteer?: (content: string) => void; + onModelChange?: (alias: string) => void; + /** Live daemon models for the picker; absent = the sentinel only. */ + models?: ComposerModelOption[]; + /** Label for the empty (daemon-picks) entry. */ + autoModelLabel?: string; + /** Preview an attached file in the canvas panel. */ + onPreviewAttachment?: (file: File) => void; + disabled?: boolean; + isStreaming?: boolean; + appendText?: string | null; + onAppendConsumed?: () => void; + /** Plain-text seed dropped into an empty composer (e.g. a "next step" chip + that pre-fills a prompt without sending it). Unlike `appendText`, it is + not quoted and replaces rather than appends. */ + initialText?: string | null; + onInitialTextConsumed?: () => void; + /** Display-only model label for live-harness sessions (see ModelSelector). */ + modelLockedLabel?: string; + /** The session's current permission mode, shown by the Mode selector. */ + mode?: SessionPermissionMode; + /** Renders the Mode selector (first in the control bar) when provided. + Surfaces without a mode concept (the thread panel, the mock tour chat) + simply omit it. */ + onModeChange?: (mode: SessionPermissionMode) => void; +} + +/** + * Ghost-style trigger used by dropdowns rendered BELOW the input box + * (matches the "Build / Claude Opus 5 / Default" reference design). + * No pill background; subtle hover; small chevron via the trigger itself. + */ +const GHOST_TRIGGER_CLASS = + "h-7 gap-1 rounded-full px-2.5 text-sm font-normal text-foreground bg-transparent hover:bg-zinc-200 dark:hover:bg-zinc-700 border-0 shadow-none"; + +/** The composer's "+" button: opens the file picker directly to attach files. */ +function FilesDropdown({ + onFilesSelected, +}: { + onFilesSelected?: (files: File[]) => void; +}) { + const fileInputRef = useRef(null); + + return ( + <> + { + const selected = e.target.files; + if (selected && selected.length > 0) { + onFilesSelected?.(Array.from(selected)); + } + e.target.value = ""; + }} + /> + + + ); +} + +/** The daemon-picks sentinel (model_id omitted at create). Its label is + * supplied by the caller: "Auto-routed" only while the router is really on. */ +const AUTO_MODEL_ID = ""; +const autoModel = (label?: string) => ({ + id: AUTO_MODEL_ID, + label: label ?? "Default model", +}); + +export interface ComposerModelOption { + id: string; + label: string; +} + +const EFFORT_LEVELS = [ + { id: "light", label: "Light" }, + { id: "medium", label: "Medium" }, + { id: "high", label: "High" }, + { id: "extra-high", label: "Extra High" }, +] as const; + +type EffortId = (typeof EFFORT_LEVELS)[number]["id"]; + +const DEFAULT_EFFORT_ID: EffortId = "medium"; + +/** One tappable choice row inside a mobile picker sheet (MobileChatMenu's + * row idiom plus a trailing checkmark). */ +function SheetOptionRow({ + label, + description, + selected, + onSelect, +}: { + label: string; + description?: string; + selected: boolean; + onSelect: () => void; +}) { + return ( + + ); +} + +/** Muted section heading inside a mobile picker sheet. */ +function SheetSectionLabel({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +

+ ); +} + +/** + * Combined model + effort picker in a single menu: the trigger reads + * "{model} {effort}", and the menu drills into a Model submenu and an Effort + * submenu, with a reset. When `lockedLabel` is set (a live harness routes the + * model server-side) the trigger is display-only. On mobile the menu is a + * bottom sheet instead (the MobileChatMenu convention) with the submenus + * flattened into sections — it stays open across taps so model and effort + * can be set in one visit. + */ +function ModelEffortSelector({ + onModelChange, + lockedLabel, + models, + autoModelLabel, +}: { + onModelChange?: (id: string) => void; + lockedLabel?: string; + models?: ComposerModelOption[]; + autoModelLabel?: string; +}) { + const modelOptions = [autoModel(autoModelLabel), ...(models ?? [])]; + const [model, setModel] = useState(AUTO_MODEL_ID); + const [effort, setEffort] = useState(DEFAULT_EFFORT_ID); + const selectedModel = + modelOptions.find((m) => m.id === model) ?? modelOptions[0]; + const selectedEffort = + EFFORT_LEVELS.find((e) => e.id === effort) ?? EFFORT_LEVELS[1]; + + // The toolbar is a CSS container (@container on the footer row): below + // ~28rem — a narrow side-panel composer, not just mobile viewports — the + // value labels collapse to the static word "Model", with the full selection + // kept on the title attribute; in between, truncation caps a long model id. + if (lockedLabel) { + return ( + + ); + } + + return ( + + + + + e.preventDefault()} + align="start" + className="w-64" + > + + + Model + {selectedModel.label} + + + {modelOptions.map((m) => { + const isSelected = m.id === model; + return ( + { + setModel(m.id); + onModelChange?.(m.id); + }} + > + {m.label} + + + ); + })} + + + + + Effort + + {selectedEffort.label} + + + + {EFFORT_LEVELS.map((e) => { + const isSelected = e.id === effort; + return ( + setEffort(e.id)} + > + {e.label} + + + ); + })} + + + + { + setModel(AUTO_MODEL_ID); + setEffort(DEFAULT_EFFORT_ID); + onModelChange?.(AUTO_MODEL_ID); + }} + > + + Reset to default + + + + ); +} + +const PERMISSION_MODE_OPTIONS = [ + { + id: "default", + label: "Manual", + description: "Always ask before making changes", + }, + { + id: "acceptEdits", + label: "Accept edits", + description: "Automatically accept all file edits", + }, + { + id: "plan", + label: "Plan", + description: "Create a plan before making changes", + }, +] as const satisfies readonly { + id: SessionPermissionMode; + label: string; + description: string; +}[]; + +/** Display label for a session permission mode. */ +function permissionModeLabel(mode: SessionPermissionMode): string { + return ( + PERMISSION_MODE_OPTIONS.find((option) => option.id === mode)?.label ?? + "Manual" + ); +} + +/** + * The session permission-mode selector (Default / Plan / Accept edits), the + * first control in the composer bar. Same pill + container-collapse idiom as + * the model selector: the current mode wide, the bare word "Mode" narrow, + * always the full selection on the title. Disabled while a run streams — the + * daemon's session aggregate refuses a mid-turn mode change, so the control + * matches that reality instead of round-tripping a guaranteed refusal. + */ +function ModeSelector({ + mode, + onModeChange, + disabled, +}: { + mode: SessionPermissionMode; + onModeChange: (mode: SessionPermissionMode) => void; + disabled?: boolean; +}) { + const label = permissionModeLabel(mode); + return ( + + + + + e.preventDefault()} + align="start" + className="w-72" + > + {PERMISSION_MODE_OPTIONS.map((option) => ( + onModeChange(option.id)} + > + + + {option.label} + + {option.description} + + + + ))} + + + ); +} + +/** + * Controls whether the agent draws on (and writes to) its long-term memory for + * this conversation. A pill matching the model selector, opening a small On/Off + * menu; on by default. + */ +function MemoryToggle() { + const [on, setOn] = useState(true); + return ( + + + + + e.preventDefault()} + align="start" + className="w-40" + > + {[true, false].map((value) => ( + setOn(value)} + > + + {value ? "On" : "Off"} + + ))} + + + ); +} + +/** + * The mobile composer's left-hand options button: a + that opens a bottom + * sheet with the composer's secondary actions — Add a file, Model, Memory — + * replacing the desktop toolbar row (hidden on mobile). Model and Memory + * drill into their own sheets; a routed model renders display-only. + */ +function MobileComposerMenu({ + onFilesSelected, + onModelChange, + modelLockedLabel, + mode, + onModeChange, + modeDisabled, + models, + autoModelLabel, +}: { + onFilesSelected: (files: File[]) => void; + onModelChange?: (id: string) => void; + modelLockedLabel?: string; + mode?: SessionPermissionMode; + onModeChange?: (mode: SessionPermissionMode) => void; + modeDisabled?: boolean; + models?: ComposerModelOption[]; + autoModelLabel?: string; +}) { + const [menuOpen, setMenuOpen] = useState(false); + const [sub, setSub] = useState<"mode" | "model" | "memory" | null>(null); + const [model, setModel] = useState(AUTO_MODEL_ID); + const [effort, setEffort] = useState(DEFAULT_EFFORT_ID); + const [memoryOn, setMemoryOn] = useState(true); + const fileInputRef = useRef(null); + const modelOptions = [autoModel(autoModelLabel), ...(models ?? [])]; + const selectedModel = + modelOptions.find((m) => m.id === model) ?? modelOptions[0]; + const selectedEffort = + EFFORT_LEVELS.find((e) => e.id === effort) ?? EFFORT_LEVELS[1]; + + const menuRow = + "flex w-full items-center gap-3 px-4 py-3 text-sm transition-colors hover:bg-muted/50 disabled:opacity-50"; + + return ( + <> + { + const files = Array.from(event.target.files ?? []); + event.target.value = ""; + if (files.length > 0) onFilesSelected(files); + }} + /> + + + + + Composer options +
+ + {onModeChange && ( + + )} + + +
+
+
+ + { + if (!open) setSub(null); + }} + > + + Permission mode +
+ {PERMISSION_MODE_OPTIONS.map((option) => ( + { + onModeChange?.(option.id); + setSub(null); + }} + /> + ))} +
+
+
+ + { + if (!open) setSub(null); + }} + > + + Model and effort +
+ Model + {modelOptions.map((m) => ( + { + setModel(m.id); + onModelChange?.(m.id); + }} + /> + ))} + Effort + {EFFORT_LEVELS.map((e) => ( + setEffort(e.id)} + /> + ))} +
+ +
+ + + + { + if (!open) setSub(null); + }} + > + + Memory +
+ {[true, false].map((value) => ( + { + setMemoryOn(value); + setSub(null); + }} + /> + ))} +
+
+
+ + ); +} + +function ProjectsDropdown({ + projects, + selectedProjectId, + onSelect, + onCreate, +}: { + projects?: ProjectItem[]; + selectedProjectId?: string | null; + onSelect?: (id: string | null) => void; + onCreate?: (name: string) => void; +}) { + const { prompt, PromptDialog } = usePrompt(); + const selected = projects?.find((p) => p.id === selectedProjectId); + + return ( + <> + {PromptDialog} + + + + + e.preventDefault()} + align="start" + className="w-56" + > + Projects + + + onSelect?.(null)}> + + Chats + + + {projects?.map((p) => ( + onSelect?.(p.id)}> + + + {p.name} + + + ))} + + + { + const name = await prompt({ + title: "New project", + description: + "Give your project a name to organize related conversations.", + placeholder: "Project name", + confirmText: "Create project", + }); + if (name) onCreate?.(name); + }} + > + + New project + + + + + ); +} + +function useVoiceInput(onTranscript: (text: string) => void) { + const [isListening, setIsListening] = useState(false); + const [isSupported, setIsSupported] = useState(false); + const recognitionRef = useRef | null>(null); + + useEffect(() => { + const SR = + typeof window !== "undefined" + ? window.SpeechRecognition || window.webkitSpeechRecognition + : null; + setIsSupported(!!SR); + }, []); + + const toggle = useCallback(() => { + if (isListening) { + recognitionRef.current?.stop(); + setIsListening(false); + return; + } + + const SR = window.SpeechRecognition || window.webkitSpeechRecognition; + if (!SR) return; + + const recognition = new SR(); + recognition.continuous = false; + recognition.interimResults = true; + recognition.lang = "en-US"; + + let finalText = ""; + + recognition.onresult = (event: SpeechRecognitionEvent) => { + let interim = ""; + for (let i = event.resultIndex; i < event.results.length; i++) { + const transcript = event.results[i][0].transcript; + if (event.results[i].isFinal) { + finalText += transcript; + } else { + interim += transcript; + } + } + onTranscript(finalText + interim); + }; + + recognition.onend = () => { + setIsListening(false); + if (finalText) onTranscript(finalText); + }; + + recognition.onerror = () => { + setIsListening(false); + }; + + recognitionRef.current = recognition; + recognition.start(); + setIsListening(true); + }, [isListening, onTranscript]); + + return { isListening, isSupported, toggle }; +} + +type SpeechRecognitionType = new () => { + continuous: boolean; + interimResults: boolean; + lang: string; + onresult: ((event: SpeechRecognitionEvent) => void) | null; + onend: (() => void) | null; + onerror: ((event: Event) => void) | null; + start: () => void; + stop: () => void; +}; + +declare global { + interface Window { + SpeechRecognition: SpeechRecognitionType; + webkitSpeechRecognition: SpeechRecognitionType; + } +} + +const DEFAULT_PLACEHOLDER = + "Pull in tools and expertise by including @agent or +Files"; + +/** Filter the agent list for the `@`-mention menu by handle or name. */ +function agentMenuItems(query: string): ComposerMenuItem[] { + const q = query.toLowerCase(); + return getAgentMentions() + .filter((a) => a.handle.startsWith(q) || a.name.toLowerCase().includes(q)) + .map((a) => ({ + id: a.handle, + label: a.handle, + primary: a.name, + secondary: a.description, + })); +} + +/** Filter the slash-command list for the `/`-command menu by name prefix. */ +function commandMenuItems(query: string): ComposerMenuItem[] { + const q = query.toLowerCase(); + return getSlashCommands() + .filter((c) => c.name.startsWith(q)) + .map((c) => ({ + id: c.name, + label: c.name, + primary: `/${c.name}`, + secondary: c.description, + })); +} + +/** + * One attached-file chip. Image files show a small thumbnail of the file + * itself (an object URL — cheaper than a data-URL read) before the name; the + * URL is revoked when the pill unmounts (remove/send), so attach/remove + * cycles never leak blob URLs. Non-image files (and an image whose thumbnail + * has not resolved yet) show their file-kind glyph — image/PDF/code — with + * the paperclip as the fallback. Exported for its unit test. + */ +export function AttachmentPill({ + file, + onRemove, + onPreview, +}: { + file: File; + onRemove: () => void; + /** Opens the file in the canvas panel (main composer only). */ + onPreview?: () => void; +}) { + const [previewUrl, setPreviewUrl] = useState(null); + useEffect(() => { + if (!file.type.startsWith("image/")) { + setPreviewUrl(null); + return; + } + const url = URL.createObjectURL(file); + setPreviewUrl(url); + return () => URL.revokeObjectURL(url); + }, [file]); + const kind = fileKindMeta(file.name, file.type); + const KindIcon = kind.icon; + + return ( + + + + + ); +} + +/** What Enter resolves to in the composer. `newline` means "do not intercept + * the key" — the editor's own hardBreak handles it. */ +export type ComposerEnterAction = "send" | "queue" | "steer" | "newline"; + +/** + * The composer's Enter decision table, extracted pure so the whole matrix is + * testable without driving the TipTap editor: + * + * - Idle: Enter sends; Shift+Enter inserts a newline (fall through to the + * editor's hardBreak). + * - Streaming with files attached: ALWAYS queue, on both keys. A steer + * carries text only, so files force the queue path; the files stay attached + * in the composer (never silently dropped) and ride the next real send. + * - Streaming: Enter performs the preferred action (Settings → Personalize) and + * Shift+Enter the opposite. + */ +export function resolveComposerAction(input: { + shift: boolean; + isStreaming: boolean; + behavior: EnterSendBehavior; + hasAttachments: boolean; +}): ComposerEnterAction { + if (!input.isStreaming) return input.shift ? "newline" : "send"; + if (input.hasAttachments) return "queue"; + if (!input.shift) return input.behavior; + return input.behavior === "queue" ? "steer" : "queue"; +} + +/** Open autocomplete menu state, mirrored from TipTap's suggestion lifecycle. */ +interface ComposerMenu { + kind: "agent" | "command"; + items: ComposerMenuItem[]; + index: number; + select: (item: ComposerMenuItem) => void; +} + +/** + * Drive the open autocomplete menu from a keydown. Returns true when the key + * was for the menu (arrows move the highlight, Enter/Tab pick the row, Escape + * closes it). Selection is deferred a microtask so the chip insert lands after + * ProseMirror finishes dispatching the key. + */ +function handleMenuNavKey( + event: KeyboardEvent, + menu: ComposerMenu, + setMenu: React.Dispatch>, +): boolean { + if (menu.items.length === 0) return false; + switch (event.key) { + case "ArrowDown": + setMenu((m) => (m ? { ...m, index: (m.index + 1) % m.items.length } : m)); + return true; + case "ArrowUp": + setMenu((m) => + m + ? { ...m, index: (m.index - 1 + m.items.length) % m.items.length } + : m, + ); + return true; + case "Enter": + case "Tab": { + const item = menu.items[menu.index]; + if (item) queueMicrotask(() => menu.select(item)); + return true; + } + case "Escape": + setMenu(null); + return true; + default: + return false; + } +} + +export function ChatInput({ + placeholder: placeholderProp, + projects, + selectedProjectId, + onSelectProject, + onCreateProject, + compact = false, + mobileDocked = false, + focusKey, + onSend, + onQueue, + onSteer, + onModelChange, + disabled = false, + isStreaming = false, + appendText, + onAppendConsumed, + initialText, + onInitialTextConsumed, + modelLockedLabel, + models, + autoModelLabel, + onPreviewAttachment, + mode, + onModeChange, +}: ChatInputProps) { + const placeholder = placeholderProp ?? DEFAULT_PLACEHOLDER; + // Plain-text mirror of the editor, kept in sync via onUpdate. Used only for + // "is there something to send" checks and the streaming border state; + // the editor document is the source of truth for the message itself. + const [text, setText] = useState(""); + const [attachedFiles, setAttachedFiles] = useState([]); + const [isDragOver, setIsDragOver] = useState(false); + const [isWindowDrag, setIsWindowDrag] = useState(false); + const dragCountRef = useRef(0); + const windowDragCountRef = useRef(0); + + // Autocomplete menu, mirrored from TipTap's suggestion lifecycle so we can + // render the same full-width popover the pre-TipTap composer used. `menuRef` + // gives the suggestion keydown handler a synchronous read of current state. + const [menu, setMenu] = useState(null); + + // Wire each mention's TipTap suggestion to the shared menu state. `command` + // (from the suggestion props) inserts the atomic chip at the trigger range. + // Keyboard navigation is handled by the editor's handleKeyDown (below), not + // here, so the suggestion's own onKeyDown is intentionally omitted. + const makeRender = useCallback( + (kind: "agent" | "command") => () => ({ + onStart: (props: SuggestionProps) => { + setMenu({ + kind, + items: props.items, + index: 0, + select: (item) => props.command(item), + }); + }, + onUpdate: (props: SuggestionProps) => { + setMenu((m) => { + if (!m || m.kind !== kind) return m; + const index = props.items.length + ? Math.min(m.index, props.items.length - 1) + : 0; + return { + kind, + items: props.items, + index, + select: (item) => props.command(item), + }; + }); + }, + onExit: () => setMenu((m) => (m && m.kind === kind ? null : m)), + }), + [], + ); + + const mentions = useMemo( + () => + createComposerMentions({ + agentItems: agentMenuItems, + commandItems: commandMenuItems, + makeRender, + }), + [makeRender], + ); + + const editor = useEditor({ + immediatelyRender: false, + autofocus: "end", + extensions: [ + // A deliberately plain field: keep the editing primitives (undo, hard + // break, drop/gap cursors) but drop every rich-text mark and block so + // typing `# `, `**`, `- ` etc. stays literal, exactly like the textarea. + StarterKit.configure({ + heading: false, + bold: false, + italic: false, + strike: false, + code: false, + codeBlock: false, + blockquote: false, + bulletList: false, + orderedList: false, + listItem: false, + horizontalRule: false, + link: false, + underline: false, + }), + Placeholder.configure({ placeholder }), + ...mentions, + ], + onUpdate: ({ editor }) => setText(editor.getText({ blockSeparator: "\n" })), + }); + + // Entering a chat or thread puts the caret in the field; autofocus only + // covers the first mount, so a change of target refocuses explicitly. + useEffect(() => { + if (focusKey !== undefined) editor?.commands.focus("end"); + }, [focusKey, editor]); + + useEffect(() => { + editor?.setEditable(!disabled); + }, [editor, disabled]); + + useEffect(() => { + if (appendText && editor) { + const raw = editor.getText({ blockSeparator: "\n" }); + const sep = raw && !raw.endsWith("\n") ? "\n" : ""; + setComposerText(editor, `${raw}${sep}> ${appendText}\n`); + setText(editor.getText({ blockSeparator: "\n" })); + onAppendConsumed?.(); + requestAnimationFrame(() => editor.commands.focus("end")); + } + }, [appendText, onAppendConsumed, editor]); + + useEffect(() => { + if (initialText && editor) { + setComposerText(editor, initialText); + setText(editor.getText({ blockSeparator: "\n" })); + onInitialTextConsumed?.(); + requestAnimationFrame(() => editor.commands.focus("end")); + } + }, [initialText, onInitialTextConsumed, editor]); + + useEffect(() => { + const onEnter = (e: DragEvent) => { + if (e.dataTransfer?.types.includes("Files")) { + windowDragCountRef.current++; + setIsWindowDrag(true); + } + }; + const onLeave = () => { + windowDragCountRef.current--; + if (windowDragCountRef.current <= 0) { + windowDragCountRef.current = 0; + setIsWindowDrag(false); + } + }; + const onDrop = () => { + windowDragCountRef.current = 0; + setIsWindowDrag(false); + }; + window.addEventListener("dragenter", onEnter); + window.addEventListener("dragleave", onLeave); + window.addEventListener("drop", onDrop); + return () => { + window.removeEventListener("dragenter", onEnter); + window.removeEventListener("dragleave", onLeave); + window.removeEventListener("drop", onDrop); + }; + }, []); + + const voice = useVoiceInput( + useCallback( + (transcript: string) => { + if (editor) setComposerText(editor, transcript); + setText(transcript); + }, + [editor], + ), + ); + + // The preferred Enter action while a reply is streaming (Settings → Personalize). + // Hydrates on mount, so the first frame is always the "queue" default. + const { behavior: enterBehavior } = useEnterSendBehavior(); + + /** Executes one resolved Enter action against the current editor content. + * Missing handlers degrade toward the pre-steer behavior: steer without + * onSteer queues, queue without onQueue sends. */ + const performAction = useCallback( + (action: ComposerEnterAction) => { + if (action === "newline") return; + const trimmed = editor ? composerText(editor) : ""; + if (!trimmed || disabled) return; + const resolved = action === "steer" && !onSteer ? "queue" : action; + if ( + (resolved === "steer" && onSteer) || + (resolved === "queue" && onQueue) + ) { + if (resolved === "steer") onSteer?.(trimmed); + else onQueue?.(trimmed); + editor?.commands.clearContent(); + setText(""); + // Attached files deliberately stay attached: neither a queued text + // nor a steer can carry them, so they ride the next real send. + return; + } + onSend?.(trimmed, attachedFiles.length > 0 ? attachedFiles : undefined); + editor?.commands.clearContent(); + setText(""); + setAttachedFiles([]); + }, + [editor, disabled, onQueue, onSteer, onSend, attachedFiles], + ); + + /** Resolve + perform for one Enter press (or a send-button click, which is + * the plain-Enter path). */ + const actOnEnter = useCallback( + (shift: boolean) => { + performAction( + resolveComposerAction({ + shift, + isStreaming, + behavior: enterBehavior, + hasAttachments: attachedFiles.length > 0, + }), + ); + }, + [performAction, isStreaming, enterBehavior, attachedFiles], + ); + + const handleSend = useCallback(() => actOnEnter(false), [actOnEnter]); + + // Menu nav + Enter-to-send are wired with a native capture-phase keydown + // listener on the editor DOM, re-subscribed each render with fresh closures + // over `menu`/`actOnEnter`. Capture phase runs before ProseMirror's own + // (bubble-phase) handler, and stopPropagation keeps the base keymap and the + // suggestion plugins from also acting. This sidesteps both TipTap re-syncing + // its editorProps and the React Compiler not preserving render-phase refs. + useEffect(() => { + const dom = editor?.view.dom; + if (!dom) return; + const onKeyDown = (event: KeyboardEvent) => { + if (menu) { + if (handleMenuNavKey(event, menu, setMenu)) { + event.preventDefault(); + event.stopPropagation(); + } + return; + } + if (event.key !== "Enter") return; + if (event.shiftKey) { + // Shift+Enter acts (as the opposite of the Enter preference) only + // while a reply is streaming AND there is text to act on; otherwise + // it keeps its newline behavior — fall through to the editor's + // hardBreak without preventDefault. + const trimmed = editor ? composerText(editor) : ""; + if (!isStreaming || !trimmed) return; + event.preventDefault(); + event.stopPropagation(); + actOnEnter(true); + return; + } + event.preventDefault(); + event.stopPropagation(); + actOnEnter(false); + }; + dom.addEventListener("keydown", onKeyDown, true); + return () => dom.removeEventListener("keydown", onKeyDown, true); + }, [editor, menu, isStreaming, actOnEnter]); + + const hasText = text.trim().length > 0; + + return ( + // biome-ignore lint/a11y/noStaticElementInteractions: drop zone for file attachments +
{ + e.preventDefault(); + dragCountRef.current++; + setIsDragOver(true); + }} + onDragOver={(e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + }} + onDragLeave={(e) => { + e.preventDefault(); + dragCountRef.current--; + if (dragCountRef.current <= 0) { + dragCountRef.current = 0; + setIsDragOver(false); + } + }} + onDrop={(e) => { + e.preventDefault(); + dragCountRef.current = 0; + setIsDragOver(false); + // Only images can cross the wire (the daemon's prompt parts are + // image/audio only), so only images attach. + const droppedFiles = Array.from(e.dataTransfer.files).filter((f) => + f.type.startsWith("image/"), + ); + if (droppedFiles.length > 0) { + setAttachedFiles((prev) => [...prev, ...droppedFiles]); + } + }} + className={cn( + "relative rounded-2xl bg-zinc-50 dark:bg-zinc-900", + mobileDocked && "max-[499px]:rounded-none max-[499px]:bg-transparent", + )} + > + {/* Autocomplete popover (agent @-mentions or slash commands), floating + above the input box. Driven by TipTap's suggestion lifecycle. */} + {menu && menu.items.length > 0 && ( +
+
+ {menu.items.map((item, i) => ( + + ))} +
+
+ )} + {/* Input box: textarea + inline toolbar (+, mic) + send button. + Has its own rounded border. The toolbar row below has a matching + border on its top/sides/bottom; the two borders meet along the + input box's bottom edge, sharing a single visible line. */} +
+ {voice.isListening && ( +
+ + + Listening + +
+ )} + {isDragOver && ( +
+
+ + Drop files to attach +
+
+ )} + {attachedFiles.length > 0 && ( +
+ {attachedFiles.map((f, i) => ( + onPreviewAttachment(f) : undefined + } + // biome-ignore lint/suspicious/noArrayIndexKey: files may share names + key={`${f.name}-${i}`} + file={f} + onRemove={() => + setAttachedFiles((prev) => prev.filter((_, j) => j !== i)) + } + /> + ))} +
+ )} + {/* Mobile consolidates to ONE line — + on the left, the field, and a + single right slot that is the mic until there is text, then the + send button (native messaging convention). Desktop keeps the + two-row layout below. */} + {/* min-h-14 mirrors the chat header bar, so the docked composer and + the title bar read as symmetric top/bottom bands. */} +
+
+ + setAttachedFiles((prev) => [...prev, ...newFiles]) + } + onModelChange={onModelChange} + modelLockedLabel={modelLockedLabel} + mode={mode} + onModeChange={onModeChange} + modeDisabled={disabled || isStreaming} + /> +
+ {/* TipTap composer: resolved @agent / /skill mentions are atomic + green chips (Backspace removes a whole chip); Enter sends and + Shift+Enter inserts a newline (handled in the editor keymap). */} +
+ +
+
+ {!hasText && voice.isSupported ? ( + + ) : ( + + )} +
+
+ {/* Desktop-only bottom row: attach (+), mic on the left; send right */} +
+ + setAttachedFiles((prev) => [...prev, ...newFiles]) + } + /> + {voice.isSupported && ( + + )} + +
+
+ {/* Toolbar sits BEHIND the input box: negative top margin pulls it up + so its top edge overlaps the input box's bottom rounded corners, + making the two boxes appear to share a single outline. */} + {/* Hidden on mobile: Model and Memory live in the + options sheet. */} + {/* @container: the Model/Memory pills collapse their value labels via + container queries when THIS row runs narrow (a ~400px side-panel + composer), independent of the viewport width. */} +
+ {projects && ( + + )} + {!compact && ( + <> + {onModeChange && ( + + )} + { + onModelChange?.(id); + }} + /> + + + )} +
+
+ ); +} diff --git a/studio/src/app/workspace/_components/composer-mentions.ts b/studio/src/app/workspace/_components/composer-mentions.ts new file mode 100644 index 0000000000..6c5ba400de --- /dev/null +++ b/studio/src/app/workspace/_components/composer-mentions.ts @@ -0,0 +1,111 @@ +import Mention from "@tiptap/extension-mention"; +import { PluginKey } from "@tiptap/pm/state"; +import type { Editor } from "@tiptap/react"; +import type { SuggestionOptions } from "@tiptap/suggestion"; + +/** + * Atomic `@agent` / `/skill` chips for the TipTap composer. + * + * Each is a Mention node (an atom, so Backspace removes the whole chip in one + * press) rendered as the same green "connected" pill the old overlay painted. + * `renderText` serializes a chip back to `@handle` / `/name`, so `editor.getText` + * reconstructs the exact plain-text message the model receives. + * + * Autocomplete is delegated to the caller: the caller supplies the filtered + * items per keystroke and a `render` that drives its own menu UI (we reuse the + * existing full-width popover rather than TipTap's default), keeping the + * on-screen behaviour identical to the pre-TipTap composer. + */ + +/** One row in the autocomplete menu (and the data an inserted chip carries). */ +export interface ComposerMenuItem { + /** Text inserted after the trigger char — the agent handle or command name. */ + readonly id: string; + /** Chip label (same as `id`; the trigger char is added by `renderText`). */ + readonly label: string; + /** Primary text shown in the menu row. */ + readonly primary: string; + /** Muted description shown in the menu row. */ + readonly secondary: string; +} + +/** The trigger kind, so the caller can render agent vs command rows. */ +export type ComposerMentionKind = "agent" | "command"; + +/** `render` factory: given the kind, return a TipTap suggestion renderer. */ +type MakeRender = ( + kind: ComposerMentionKind, +) => SuggestionOptions["render"]; + +const CHIP_CLASS = + "rounded px-1 py-0.5 bg-emerald-500/15 text-emerald-700 dark:text-emerald-400"; + +function mentionNode( + name: string, + char: string, + pluginKeyName: string, + items: (query: string) => ComposerMenuItem[], + makeRender: MakeRender, + kind: ComposerMentionKind, + allow?: SuggestionOptions["allow"], +) { + return Mention.extend({ name }).configure({ + HTMLAttributes: { class: CHIP_CLASS, "data-composer-mention": name }, + // `id` is what renderText appends after the trigger char. + renderText: ({ node }) => `${char}${node.attrs.label ?? node.attrs.id}`, + suggestion: { + char, + pluginKey: new PluginKey(pluginKeyName), + items: ({ query }) => items(query), + allow, + render: makeRender(kind), + }, + }); +} + +/** + * Build the two mention extensions. `agentItems` / `commandItems` filter the + * live lists for the current query; `makeRender` wires each suggestion to the + * caller's menu. Commands only trigger at the very start of the message + * (`range.from === 1`), matching the old "slash menu only when the message is a + * single /token" rule; agents trigger anywhere after whitespace. + */ +export function createComposerMentions(opts: { + agentItems: (query: string) => ComposerMenuItem[]; + commandItems: (query: string) => ComposerMenuItem[]; + makeRender: MakeRender; +}) { + return [ + mentionNode( + "agentMention", + "@", + "composerAgentMention", + opts.agentItems, + opts.makeRender, + "agent", + ), + mentionNode( + "commandMention", + "/", + "composerCommandMention", + opts.commandItems, + opts.makeRender, + "command", + ({ range }) => range.from === 1, + ), + ]; +} + +/** Serialize the editor to the plain-text message (chips → `@handle`/`/name`). */ +export function composerText(editor: Editor): string { + return editor.getText({ blockSeparator: "\n" }).trim(); +} + +/** Replace the whole document with plain text (voice input, quoted replies). */ +export function setComposerText(editor: Editor, text: string) { + const content = text.split("\n").map((line) => ({ + type: "paragraph", + content: line ? [{ type: "text", text: line }] : [], + })); + editor.commands.setContent({ type: "doc", content }); +} diff --git a/studio/src/app/workspace/_components/resize-handle.tsx b/studio/src/app/workspace/_components/resize-handle.tsx new file mode 100644 index 0000000000..ca3e94dc01 --- /dev/null +++ b/studio/src/app/workspace/_components/resize-handle.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useRef } from "react"; +import { cn } from "@/lib/utils"; + +interface ResizeHandleProps { + direction?: "left" | "right"; + width: number; + onWidthChange: (width: number) => void; + min?: number; + max?: number; +} + +export function ResizeHandle({ + direction = "right", + width, + onWidthChange, + min = 200, + max = 720, +}: ResizeHandleProps) { + const isDraggingRef = useRef(false); + + return ( + // biome-ignore lint/a11y/noStaticElementInteractions: drag-only resize handle +
{ + e.preventDefault(); + isDraggingRef.current = true; + const startX = e.clientX; + const startW = width; + const sign = direction === "right" ? 1 : -1; + const onMove = (ev: MouseEvent) => { + if (!isDraggingRef.current) return; + onWidthChange( + Math.max(min, Math.min(max, startW + sign * (ev.clientX - startX))), + ); + }; + const onUp = () => { + isDraggingRef.current = false; + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); + }} + /> + ); +} diff --git a/studio/src/app/workspace/chat/[[...sessionId]]/page.tsx b/studio/src/app/workspace/chat/[[...sessionId]]/page.tsx new file mode 100644 index 0000000000..ef81125363 --- /dev/null +++ b/studio/src/app/workspace/chat/[[...sessionId]]/page.tsx @@ -0,0 +1,16 @@ +import { ChatWorkspace } from "../_components/chat-workspace"; + +/** + * `/workspace/chat` (a draft with no daemon session yet) and + * `/workspace/chat/` are served by one optional-catch-all route, + * so moving between them keeps `ChatWorkspace` mounted — a draft's in-flight + * stream survives the router.replace to its freshly minted session id. + */ +export default async function ChatPage({ + params, +}: { + params: Promise<{ sessionId?: string[] }>; +}) { + const { sessionId } = await params; + return ; +} diff --git a/studio/src/app/workspace/chat/_components/approval-panel.tsx b/studio/src/app/workspace/chat/_components/approval-panel.tsx new file mode 100644 index 0000000000..67a44e0e75 --- /dev/null +++ b/studio/src/app/workspace/chat/_components/approval-panel.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { ShieldAlert } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import type { ApprovalChoice, ApprovalRequest } from "@/features/agent"; +import { cn } from "@/lib/utils"; + +const DELETE_WORDS = /\b(delete|remove|drop|revoke|destroy|purge|rm)\b/i; + +/** + * The daemon's verdict is three-way (allow_once / allow_always / deny), so the + * panel offers exactly those scopes — no "for session" button, which would + * promise a grant scope the backend does not model. + */ +export function ApprovalPanel({ + approval, + onRespond, +}: { + approval: ApprovalRequest; + onRespond: (choice: ApprovalChoice) => void; +}) { + // ONE ask = ONE tool call. The pill names the tool; the args belong in the + // preview block below, never badge-ified (a Write ask's args are a whole + // file). Destructiveness is judged on the tool name alone — scanning file + // CONTENT for the word "delete" painted harmless writes red. + const toolName = + approval.toolName || + approval.description.replace(/ needs your approval\.?$/i, "").trim() || + "Tool"; + const destructive = DELETE_WORDS.test(toolName); + + return ( +
+
+ + + Permission required + +
+

{approval.description}

+
+ + {toolName} + +
+ {destructive && ( +

+ This action modifies or deletes data. +

+ )} +
+        {approval.details}
+      
+
+ + + +
+
+ ); +} diff --git a/studio/src/app/workspace/chat/_components/chat-view.tsx b/studio/src/app/workspace/chat/_components/chat-view.tsx new file mode 100644 index 0000000000..6be543b149 --- /dev/null +++ b/studio/src/app/workspace/chat/_components/chat-view.tsx @@ -0,0 +1,1338 @@ +"use client"; + +import { + AlertCircle, + ArrowDown, + ArrowLeft, + CirclePlus, + Ellipsis, + FileText, + ListEnd, + Loader2, + MessageCircle, + MessageSquareText, + PanelLeftClose, + PanelLeftOpen, + PanelRightClose, + PanelRightOpen, + Pencil, + RotateCcw, + Trash2, + Wrench, +} from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { + type AgentMessage, + type AgentSession, + type ApprovalChoice, + type ApprovalRequest, + type Artifact, + type Attachment, + type ClarificationRequest, + useAgentChat, +} from "@/features/agent"; +import type { QueuedMessage } from "@/features/agent/hooks/use-agent-chat"; +import { isMockTourSession } from "@/features/agent/mock-tour"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { formatTokens } from "@/lib/formatters"; +import { + createThreadHarnessSession, + ThreadSourceBusyError, +} from "@/lib/harness/client"; +import { + type SessionListSide, + useEnterSendBehavior, +} from "@/lib/profile-preferences"; +import type { SessionPermissionMode } from "@/lib/protocol"; +import { useShortcut } from "@/lib/shortcuts/use-shortcuts"; +import { + composeThreadPrompt, + getThreadSession, + registerThreadSession, + sliceThreadReplies, + stripRootQuote, + syncThreadActivity, + threadKeyForMessage, + threadTitleFromRoot, + unregisterThreadSession, + useThreadMap, +} from "@/lib/thread-map"; +import { cn } from "@/lib/utils"; +import { ChatInput } from "../../_components/chat-input"; +import { ApprovalPanel } from "./approval-panel"; +import { ClarificationPanel } from "./clarification-panel"; +import { FilePreview } from "./file-preview"; +import { MarkdownCanvasPanel } from "./markdown-canvas-panel"; +import { MessageBubble } from "./message-bubble"; +import { MockProviderNotice } from "./mock-provider-notice"; +import { SidePanel } from "./side-panel"; + +/** The single right-hand panel: exactly one kind is open at a time, or none. */ +type ActivePanel = + | { kind: "artifact"; artifact: Artifact } + | { kind: "attachment"; attachment: Attachment } + | { kind: "thread"; message: AgentMessage }; + +/** + * Bottom-of-transcript activity line while a turn is running: three + * staggered pulsing dots, a phase label derived from the streaming + * assistant message (thinking / running tools / writing), and elapsed time. + */ +function StreamingIndicator({ message }: { message?: AgentMessage }) { + const [elapsed, setElapsed] = useState(0); + const startedAt = message?.timestamp; + useEffect(() => { + if (!startedAt) return; + const tick = () => + setElapsed(Math.max(0, Math.round((Date.now() - startedAt) / 1000))); + tick(); + const timer = setInterval(tick, 1000); + return () => clearInterval(timer); + }, [startedAt]); + + const runningTool = message?.toolCalls?.some( + (call) => call.status === "running", + ); + const phase = runningTool + ? "Running tools" + : message?.content + ? "Writing" + : "Thinking"; + const time = + elapsed >= 60 + ? `${Math.floor(elapsed / 60)}m ${elapsed % 60}s` + : `${elapsed}s`; + + return ( + // Mirrors the message-row geometry (avatar column + gap) so the label + // lines up with message text; the dots sit centered in the avatar slot. +
+
+ ); +} + +/** Token counts as a read-only info row inside the chat context menus. */ +function UsageMenuRow({ + usage, +}: { + usage?: { inputTokens: number; outputTokens: number } | null; +}) { + if (!usage || usage.inputTokens + usage.outputTokens <= 0) return null; + return ( +
+

Token usage

+

+ {formatTokens(usage.inputTokens)} input +

+

+ {formatTokens(usage.outputTokens)} output +

+
+ ); +} + +/** + * Messages held while a run is active, shown above the composer. Steered + * messages the daemon accepted but has not yet applied render first, with a + * pulsing "steering…" marker and a single retract control for the whole + * bundle (the daemon retracts bundles, not single messages). Queued rows + * offer Steer (inject into the in-flight run), Edit (back into the composer), + * and Delete; they drain in order as runs complete. + */ +function QueuedMessageStrip({ + queued, + onSteer, + onEdit, + onDelete, +}: { + queued: QueuedMessage[]; + onSteer: (id: string) => void; + onEdit: (id: string) => void; + onDelete: (id: string) => void; +}) { + if (queued.length === 0) return null; + return ( + // ONE opaque group (the strip floats over the transcript): pending steers + // first, then the queue, as divided rows — never a stack of panels. +
+
+ {queued.map((message) => ( +
+ + + {message.text} + + + + + + + onSteer(message.id)}> + Steer + + onEdit(message.id)}> + Edit + + onDelete(message.id)} + > + Delete + + + +
+ ))} +
+
+ ); +} + +function MobileChatMenu({ + showActivity, + onToggleActivity, + onRename, + onDelete, + usage, +}: { + showActivity: boolean; + onToggleActivity: () => void; + onRename?: () => void; + onDelete?: () => void; + usage?: { inputTokens: number; outputTokens: number } | null; +}) { + const [open, setOpen] = useState(false); + + return ( + <> + + + + Chat options +
+ + + {onRename && ( + + )} + {onDelete && ( + + )} +
+
+
+ + ); +} + +function AttachmentPanel({ + attachment, + onClose, + maximized, + onToggleMaximize, + windowControls, +}: { + attachment: Attachment; + onClose: () => void; + maximized: boolean; + onToggleMaximize: () => void; + windowControls?: boolean; +}) { + return ( + +
+ +
+
+ ); +} + +/** + * A side-panel thread branched off a single message, backed by a REAL daemon + * session seeded with the parent conversation (source_session_id carryover). + * The root message is shown read-only at the top; replies genuinely converse + * with the agent, streaming through the same chat hook as the main view. The + * thread session is minted lazily on the first send, renamed "Thread: …", + * and remembered in the browser-local thread map so reopening the thread + * rehydrates its transcript. It also shows up in the sidebar under that + * name, which is the escape hatch for anything the panel keeps minimal. + */ +function ThreadPanel({ + parentSessionId, + rootMessage, + botName, + onClose, + onConvertToChat, + maximized, + onToggleMaximize, + windowControls, +}: { + parentSessionId: string; + rootMessage: AgentMessage; + botName: string; + onClose: () => void; + /** Detach the thread and open its session as an ordinary chat. */ + onConvertToChat?: (threadSessionId: string) => void; + maximized: boolean; + onToggleMaximize: () => void; + windowControls?: boolean; +}) { + const [showTools, setShowTools] = useState(false); + const rootKey = threadKeyForMessage(rootMessage); + // The persisted thread session, when this root message already has one — + // the hook rehydrates its transcript. A session minted DURING this panel's + // lifetime deliberately does NOT re-key the hook: it is adopted via + // adoptSession instead (the draft-minting pattern), because re-keying + // would refetch the transcript mid-stream and wipe the optimistic messages. + const [initialThreadId] = useState(() => + getThreadSession(parentSessionId, rootKey), + ); + const threadIdRef = useRef(initialThreadId); + // The parent was mid-run (daemon 412): the seeded fork has to wait. + const [sourceBusy, setSourceBusy] = useState(false); + const [createError, setCreateError] = useState(null); + // Re-seeds the composer after a refused first send (keep the text) or a + // queued-message edit. + const [seedText, setSeedText] = useState(null); + const threadScrollRef = useRef(null); + const endRef = useRef(null); + + const { + messages, + isStreaming, + status, + error, + sendMessage, + adoptSession, + queuedMessages, + queueMessage, + deleteQueued, + takeQueued, + steerQueued, + pendingApproval, + respondToApproval, + } = useAgentChat(initialThreadId); + + // The thread session's history starts with the seeded parent conversation; + // only the thread's own exchange (from the quoted first message) renders. + const replies = useMemo( + () => + stripRootQuote( + sliceThreadReplies(messages, rootMessage.content), + rootMessage.content, + ), + [messages, rootMessage.content], + ); + + // biome-ignore lint/correctness/useExhaustiveDependencies: the scroll follows every new reply by design + useEffect(() => { + endRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [replies.length]); + + // Mirror the thread's activity into the persisted map so the parent + // transcript's reply indicator stays live (count + last-reply time). + useEffect(() => { + if (!threadIdRef.current || replies.length === 0) return; + const countable = replies.filter( + (reply) => + reply.role === "user" || + Boolean(reply.content.trim()) || + Boolean(reply.failed), + ); + if (countable.length === 0) return; + let lastAt = 0; + for (const reply of countable) { + if (reply.timestamp > lastAt) lastAt = reply.timestamp; + } + syncThreadActivity(parentSessionId, rootKey, countable.length, lastAt); + }, [replies, parentSessionId, rootKey]); + + const handleSend = useCallback( + async (content: string, files?: File[]) => { + // Defense in depth: a mock session id must never mint a daemon thread + // session (the panel router already sends mock threads elsewhere). + if (isMockTourSession(parentSessionId)) return; + setSourceBusy(false); + setCreateError(null); + if (threadIdRef.current) { + void sendMessage(content, files); + return; + } + // First send: mint the seeded thread session up front — the hook's own + // lazy mint would create an UNSEEDED session with no parent context. + try { + const threadId = await createThreadHarnessSession( + parentSessionId, + threadTitleFromRoot(rootMessage.content), + ); + threadIdRef.current = threadId; + adoptSession(threadId); + registerThreadSession(parentSessionId, rootKey, threadId); + } catch (caught) { + // A refused first send keeps the text: re-seed the composer with it. + setSeedText(content); + if (caught instanceof ThreadSourceBusyError) { + setSourceBusy(true); + } else { + setCreateError( + caught instanceof Error ? caught.message : String(caught), + ); + } + return; + } + void sendMessage( + composeThreadPrompt(rootMessage.content, content), + files, + ); + }, + [parentSessionId, rootKey, rootMessage.content, sendMessage, adoptSession], + ); + + // Editing a queued reply pulls it out of the queue into the composer. + const handleEditQueued = (id: string) => { + const text = takeQueued(id); + if (text) setSeedText(text); + }; + + return ( + + + + + + setShowTools((v) => !v)}> + {showTools ? "Hide Tools" : "Show Tools"} + + { + const threadId = threadIdRef.current; + if (!threadId || !onConvertToChat) return; + unregisterThreadSession(parentSessionId, threadId); + onConvertToChat(threadId); + }} + > + Open as full chat + + + + } + > + {/* Body: root message, live replies, composer */} +
+
+
+ +
+ {replies.length > 0 && ( +
+ + {replies.length} {replies.length === 1 ? "reply" : "replies"} + +
+ )} + {replies.map((reply) => ( + + ))} + {pendingApproval && ( + + )} + {isStreaming && ( + + )} +
+
+ + setSeedText((prev) => { + const quoted = `${text + .split("\n") + .map((line) => `> ${line}`) + .join("\n")}\n\n`; + return prev ? prev + quoted : quoted; + }) + } + addLabel="Add to thread" + /> +
+
+ + {sourceBusy && ( +

+ Wait for the current response to finish before starting a + thread. +

+ )} + {createError && ( +

+ {createError} +

+ )} + {status === "error" && error && ( +
+ +

+ {error} +

+
+ )} + + setSeedText(null)} + onModelChange={() => {}} + placeholder={isStreaming ? "Queue a reply…" : "Reply in thread…"} + mobileDocked + /> +
+
+
+ + ); +} + +/** + * The thread panel for the Labs mock chat: a read-only view of the root + * message's canned replies. Entirely local — a mock session id must never + * mint a daemon thread session, so this replaces ThreadPanel outright. + */ +function MockThreadPanel({ + rootMessage, + botName, + onClose, + maximized, + onToggleMaximize, + windowControls, +}: { + rootMessage: AgentMessage; + botName: string; + onClose: () => void; + maximized: boolean; + onToggleMaximize: () => void; + windowControls?: boolean; +}) { + const replies = rootMessage.replies ?? []; + return ( + +
+
+ +
+ {replies.length > 0 && ( +
+ + {replies.length} {replies.length === 1 ? "reply" : "replies"} + +
+ )} + {replies.map((reply) => ( + + ))} +

+ Mock thread — read-only demo content. +

+
+
+ ); +} + +function TextSelectionToolbar({ + containerRef, + onAddToChat, + onAskInSideChat, + addLabel = "Add to chat", + askLabel = "Ask in a chat thread", +}: { + containerRef: React.RefObject; + onAddToChat: (text: string) => void; + /** Omitted = the toolbar offers only the add action (the thread panel). */ + onAskInSideChat?: (text: string) => void; + addLabel?: string; + askLabel?: string; +}) { + const [pos, setPos] = useState<{ x: number; y: number } | null>(null); + const [selectedText, setSelectedText] = useState(""); + const toolbarRef = useRef(null); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const handleMouseUp = () => { + requestAnimationFrame(() => { + const selection = window.getSelection(); + const text = selection?.toString().trim(); + if (!text || text.length < 3) { + setPos(null); + return; + } + + const range = selection?.getRangeAt(0); + if (!range) return; + + if (!container.contains(range.commonAncestorContainer)) { + setPos(null); + return; + } + + const rect = range.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + + setSelectedText(text); + setPos({ + x: rect.left + rect.width / 2 - containerRect.left, + y: rect.top - containerRect.top - 8, + }); + }); + }; + + const handleMouseDown = (e: MouseEvent) => { + if (toolbarRef.current?.contains(e.target as Node)) return; + setPos(null); + }; + + container.addEventListener("mouseup", handleMouseUp); + document.addEventListener("mousedown", handleMouseDown); + return () => { + container.removeEventListener("mouseup", handleMouseUp); + document.removeEventListener("mousedown", handleMouseDown); + }; + }, [containerRef]); + + if (!pos) return null; + + return ( +
+ + {onAskInSideChat && ( + <> +
+ + + )} +
+ ); +} + +export function ChatView({ + session, + messages, + isStreaming, + onSend, + botName, + live = false, + usage, + error, + onRetry, + sidebarOpen, + sidebarSide, + onToggleSidebar, + pendingApproval, + onRespondApproval, + pendingClarification, + onRespondClarification, + onRename, + onDelete, + onSidePanelOpenChange, + initialDraft, + onInitialDraftConsumed, + queuedMessages = [], + onQueueMessage, + onOpenSession, + onSteerQueued, + onDeleteQueued, + onTakeQueued, + onSteerMessage, + onCancelRun, + readOnlyPlaceholder, + mode, + onModeChange, +}: { + session: AgentSession; + messages: AgentMessage[]; + isStreaming: boolean; + onSend: (content: string, files?: File[]) => void; + botName: string; + /** True while the daemon connection is up. */ + live?: boolean; + usage?: { inputTokens: number; outputTokens: number }; + /** The last turn failed with this text; rendered as an inline strip. */ + error?: string | null; + onRetry?: () => void; + sidebarOpen: boolean; + sidebarSide: SessionListSide; + onToggleSidebar: () => void; + pendingApproval: ApprovalRequest | null; + onRespondApproval: (choice: ApprovalChoice) => void; + pendingClarification: ClarificationRequest | null; + onRespondClarification: (response: string) => void; + onRename?: () => void; + onDelete?: () => void; + /** Fires when the right-hand side panel (artifact/attachment) opens or + closes, so the parent can collapse the chat list while it's open. */ + onSidePanelOpenChange?: (open: boolean) => void; + /** Plain-text prompt to pre-fill the composer with on mount (a "next step" + chip that seeds the message without sending it). */ + initialDraft?: string | null; + onInitialDraftConsumed?: () => void; + /** Messages held while a run is active (see QueuedMessageStrip). */ + queuedMessages?: QueuedMessage[]; + onQueueMessage?: (text: string) => void; + /** Open a session as the main chat (thread → full chat conversion). */ + onOpenSession?: (sessionId: string) => void; + onSteerQueued?: (id: string) => void; + onDeleteQueued?: (id: string) => void; + /** Removes a queued message and returns its text (the Edit action). */ + onTakeQueued?: (id: string) => string | null; + /** Steers the daemon accepted but has not yet applied to the run. */ + /** Injects composer text into the in-flight run at the next step. */ + onSteerMessage?: (text: string) => void; + /** Retracts the whole pending steer bundle. */ + /** Cancels the in-flight run (Esc with no panel open). */ + onCancelRun?: () => void; + /** Disables the composer and shows this placeholder instead (the Labs + mock chat is read-only demo content). */ + readOnlyPlaceholder?: string; + /** The session's current permission mode, for the composer's Mode selector. */ + mode?: SessionPermissionMode; + /** Renders the composer's Mode selector when provided (the mock tour chat + omits it — a read-only demo has no permission posture to set). */ + onModeChange?: (mode: SessionPermissionMode) => void; +}) { + const messagesEndRef = useRef(null); + // Whether the transcript is scrolled to (near) the bottom; when it isn't, + // a floating control above the composer jumps back down. The ref mirror is + // what the follow effect reads — it must see the value as of the latest + // scroll, not the latest render. + const [atBottom, setAtBottom] = useState(true); + const atBottomRef = useRef(true); + const messagesContainerRef = useRef(null); + const [showActivity, setShowActivity] = useState(false); + // The single right-hand panel — a discriminated union makes "one panel at a + // time" structural rather than something to coordinate by hand. + const [panel, setPanel] = useState(null); + const [appendText, setAppendText] = useState(null); + // The Enter preference (Settings → Chat) decides the streaming placeholder. + const { behavior: enterBehavior } = useEnterSendBehavior(); + // Editing a queued message pulls it out of the queue into the composer. + const [editSeed, setEditSeed] = useState(null); + const handleEditQueued = (id: string) => { + const text = onTakeQueued?.(id); + if (text) setEditSeed(text); + }; + // When maximized, the panel fills the pane and the conversation column is + // hidden. Always reset when the panel is closed. + const [panelMaximized, setPanelMaximized] = useState(false); + const isMobile = useIsMobile(); + const handleAppendConsumed = useCallback(() => setAppendText(null), []); + // Threads branched off this chat's messages (browser-local), for the + // Slack-style reply indicators under their root messages. + const threadMap = useThreadMap(session.id); + + const closeSidePanel = useCallback(() => { + setPanel(null); + setPanelMaximized(false); + }, []); + const toggleMaximize = useCallback(() => setPanelMaximized((v) => !v), []); + // Previewing a composer attachment opens the canvas on an object URL; the + // previous URL is revoked when replaced or on unmount so attach/preview + // cycles never leak blobs. + const previewUrlRef = useRef(null); + const releasePreviewUrl = useCallback(() => { + if (previewUrlRef.current) { + URL.revokeObjectURL(previewUrlRef.current); + previewUrlRef.current = null; + } + }, []); + useEffect(() => releasePreviewUrl, [releasePreviewUrl]); + const handlePreviewFile = useCallback( + (file: File) => { + releasePreviewUrl(); + const url = URL.createObjectURL(file); + previewUrlRef.current = url; + setPanel({ + kind: "attachment", + attachment: { name: file.name, type: file.type, url }, + }); + }, + [releasePreviewUrl], + ); + + const handleConvertThread = useCallback( + (threadSessionId: string) => { + setPanel(null); + onOpenSession?.(threadSessionId); + }, + [onOpenSession], + ); + + const handleStartThread = useCallback( + (message: AgentMessage) => setPanel({ kind: "thread", message }), + [], + ); + + // Jump to the latest message whenever the active chat changes so users + // always land at the bottom (most-recent) of the conversation. + // biome-ignore lint/correctness/useExhaustiveDependencies: scrolling is intentionally driven by session.id changes + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "instant" }); + atBottomRef.current = true; + setAtBottom(true); + }, [session.id]); + + // Pinned-follow: while the user sits at the bottom, a sent message and the + // streaming response keep the view pinned there; once they scroll up, their + // position holds (the floating arrow offers the way back down). + // biome-ignore lint/correctness/useExhaustiveDependencies: re-runs on every transcript update by design + useEffect(() => { + if (!atBottomRef.current) return; + const el = messagesContainerRef.current; + if (el) el.scrollTop = el.scrollHeight; + }, [messages]); + + // Esc, layered (close.esc): an open Radix dialog/menu — and the composer's + // autocomplete — consume their own Escape before the dispatcher sees it + // (`defaultPrevented`), so by the time this fires nothing transient is + // open. Close the side panel if one is up; otherwise interrupt a streaming + // run (the Claude Code convention: Esc cancels). On mobile the panel lives + // in a Radix Sheet that owns its own Escape, so only the cancel arm fires + // there. + useShortcut("close.esc", () => { + if (panel !== null) { + closeSidePanel(); + return; + } + if (isStreaming) onCancelRun?.(); + }); + + // The right-hand panel only renders on non-mobile layouts; let the parent + // collapse the chat list while it's open so both panels fit side by side. + const sidePanelOpen = !isMobile && panel !== null; + useEffect(() => { + onSidePanelOpenChange?.(sidePanelOpen); + }, [sidePanelOpen, onSidePanelOpenChange]); + + // The sidebar toggle renders on the header edge nearest the panel it + // controls: leading when the session list docks left, trailing when right. + const sidebarToggle = !isMobile && ( + + + + + + {sidebarOpen ? "Hide sidebar" : "Show sidebar"} + + + ); + + return ( +
+
+
+ {isMobile && ( + + )} + {sidebarSide === "left" && sidebarToggle} + {isStreaming && ( + + )} +

+ {session.title || "Untitled"} +

+ {sidebarSide === "right" && sidebarToggle} + {!isMobile && ( + + + + + + {/* Token usage as the daemon reported it (was a header pill; + it lives in the menu now). Hidden until any lands. */} + + setShowActivity((v) => !v)}> + + {showActivity ? "Hide Tools" : "Show Tools"} + + {onRename && ( + + + Rename + + )} + {onDelete && ( + + + Delete + + )} + + + )} + {isMobile && ( + setShowActivity((v) => !v)} + onRename={onRename} + onDelete={onDelete} + usage={usage} + /> + )} +
+ +
+
{ + const el = event.currentTarget; + const pinned = + el.scrollHeight - el.scrollTop - el.clientHeight < 80; + atBottomRef.current = pinned; + setAtBottom(pinned); + }} + className="h-full overflow-y-auto px-3 pt-1 pb-48 max-[499px]:pb-24 lg:px-6 lg:pt-2 lg:pb-56" + > + setAppendText(text)} + onAskInSideChat={(text) => setAppendText(text)} + /> +
+ {messages.map((msg) => ( + + setPanel({ kind: "artifact", artifact }) + } + onOpenAttachment={(attachment) => + setPanel({ kind: "attachment", attachment }) + } + onStartThread={handleStartThread} + threadSummary={threadMap[threadKeyForMessage(msg)]} + botName={botName} + showActivity={showActivity} + /> + ))} + {pendingApproval && ( + + )} + {isStreaming && ( + + )} +
+
+
+
+ {!atBottom && ( +
+ +
+ )} +
+ onSteerQueued?.(id)} + onEdit={handleEditQueued} + onDelete={(id) => onDeleteQueued?.(id)} + /> + {error && ( +
+ +

+ {error} +

+ {onRetry && ( + + )} +
+ )} + + {pendingClarification ? ( + + ) : ( + {}} + mode={mode} + onModeChange={onModeChange} + isStreaming={isStreaming} + disabled={!!pendingApproval || readOnlyPlaceholder != null} + appendText={appendText} + onAppendConsumed={handleAppendConsumed} + initialText={editSeed ?? initialDraft} + onInitialTextConsumed={() => { + if (editSeed !== null) setEditSeed(null); + else onInitialDraftConsumed?.(); + }} + placeholder={ + readOnlyPlaceholder ?? + (isStreaming + ? enterBehavior === "steer" + ? "Steer the agent..." + : "Queue a message..." + : "Send a message...") + } + /> + )} +
+
+
+
+ {!isMobile && panel !== null && ( + + )} + {/* On mobile the same panels render as a full-height bottom sheet: the + grab handle owns dismissal (no window controls), and dvh keeps the + thread composer above the on-screen keyboard. */} + {isMobile && panel !== null && ( + { + if (!open) closeSidePanel(); + }} + > + + + {panel.kind === "thread" + ? "Thread" + : panel.kind === "attachment" + ? panel.attachment.name + : panel.artifact.name} + +
+ {}} + windowControls={false} + /> +
+
+
+ )} +
+ ); +} + +/** Renders the right-hand panel for the active kind. */ +function SidePanelForKind({ + panel, + parentSessionId, + botName, + onClose, + onConvertToChat, + maximized, + onToggleMaximize, + windowControls, +}: { + panel: ActivePanel; + parentSessionId: string; + botName: string; + onClose: () => void; + onConvertToChat?: (threadSessionId: string) => void; + maximized: boolean; + onToggleMaximize: () => void; + windowControls?: boolean; +}) { + const shared = { onClose, maximized, onToggleMaximize, windowControls }; + switch (panel.kind) { + case "artifact": + return ; + case "attachment": + return ; + case "thread": + // Mock chat threads stay local: read-only replies, no daemon session. + if (isMockTourSession(parentSessionId)) { + return ( + + ); + } + return ( + + ); + } +} diff --git a/studio/src/app/workspace/chat/_components/chat-workspace.tsx b/studio/src/app/workspace/chat/_components/chat-workspace.tsx new file mode 100644 index 0000000000..0953aca239 --- /dev/null +++ b/studio/src/app/workspace/chat/_components/chat-workspace.tsx @@ -0,0 +1,810 @@ +"use client"; + +import { Loader2, PanelLeft, PanelRight, SquarePen } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { + type AgentSession, + type RosterAgent, + useAgentChat, + useAgentRoster, + useAgentSessions, +} from "@/features/agent"; +import { useHarnessRuntime } from "@/features/agent/hooks/use-harness-runtime"; +import { useSessionMode } from "@/features/agent/hooks/use-session-mode"; +import { + isMockTourSession, + MOCK_TOUR_MESSAGES, + MOCK_TOUR_SESSION, +} from "@/features/agent/mock-tour"; +import { useConfirm } from "@/hooks/use-confirm"; +import { useIsCompact, useIsMobile } from "@/hooks/use-mobile"; +import { useNavReopenSidebar } from "@/hooks/use-nav-reopen-sidebar"; +import { usePanelWidth } from "@/hooks/use-panel-width"; +import { usePrompt } from "@/hooks/use-prompt"; +import { useDisabledModels } from "@/lib/model-preferences"; +import { + type SessionListSide, + useAgentDisplayName, + useMockFeatures, + useSessionListSide, +} from "@/lib/profile-preferences"; +import type { SessionPermissionMode } from "@/lib/protocol"; +import { useShortcut } from "@/lib/shortcuts/use-shortcuts"; +import { useThreadSessionIds } from "@/lib/thread-map"; +import { pageTitleClass } from "@/lib/typography"; +import { cn } from "@/lib/utils"; +import { + ChatInput, + type ComposerModelOption, +} from "../../_components/chat-input"; +import { ResizeHandle } from "../../_components/resize-handle"; +import { ChatView } from "./chat-view"; +import { + AgentList, + type SessionActions, + SessionList, + SidebarGroup, +} from "./session-sidebar"; + +/** Route for a chat, or the base (a new draft) when none is selected. */ +const chatHref = (id?: string) => + id ? `/workspace/chat/${id}` : "/workspace/chat"; + +/** One-click prompts on the draft state, to seed the first message. */ +const STARTER_PROMPTS = [ + "Summarise what changed in the repo this week", + "Draft a plan for a new feature", + "Review my open pull requests", + "Find and explain a bug in the codebase", +] as const; + +const DAY_MS = 86_400_000; + +/** + * Bucket recency-sorted sessions into Today / This week / Earlier for the + * flat sidebar list. Empty buckets are dropped. + */ +function groupSessionsByRecency( + sessions: AgentSession[], +): { label: string; sessions: AgentSession[] }[] { + const startOfToday = new Date(); + startOfToday.setHours(0, 0, 0, 0); + const t0 = startOfToday.getTime(); + const order = ["Today", "This week", "Earlier"] as const; + const buckets: Record<(typeof order)[number], AgentSession[]> = { + Today: [], + "This week": [], + Earlier: [], + }; + for (const s of sessions) { + const ts = s.updatedAt ?? 0; + const label = + ts >= t0 ? "Today" : ts >= t0 - 7 * DAY_MS ? "This week" : "Earlier"; + buckets[label].push(s); + } + return order + .filter((l) => buckets[l].length > 0) + .map((l) => ({ label: l, sessions: buckets[l] })); +} + +function SidebarContent({ + onNewChat, + isLoading, + error, + groups, + agents, + selectedId, + onSelect, + actions, +}: { + onNewChat: () => void; + isLoading: boolean; + error: string | null; + groups: { label: string; sessions: AgentSession[] }[]; + agents: RosterAgent[]; + selectedId: string; + onSelect: (id: string) => void; + actions: SessionActions; +}) { + return ( + <> +
+

+ Session List +

+ + + + + New chat + +
+ +
+ {error && ( +

+ {error} +

+ )} + {isLoading ? ( +
+ +
+ ) : groups.length > 0 ? ( +
+ {groups.map((group) => ( + + + + ))} +
+ ) : ( + !error && ( +

+ No chats yet +

+ ) + )} + {!isLoading && agents.length > 0 && ( +
0 ? "pt-3" : undefined}> + + + +
+ )} +
+ + ); +} + +/** Draft chat (no daemon session yet): greeting + starter chips, composer docked. */ +function DraftView({ + onSend, + seed, + onSeedConsumed, + onPickSeed, + error, + showSidebarButton, + sidebarSide, + onShowSidebar, + mode, + onModeChange, + models, + autoModelLabel, + onModelChange, +}: { + onSend: (content: string, files?: File[]) => void; + seed: string | null; + onSeedConsumed: () => void; + onPickSeed: (text: string) => void; + error: string | null; + showSidebarButton: boolean; + sidebarSide: SessionListSide; + onShowSidebar: () => void; + /** Pending permission mode, applied when the first send mints the session. */ + mode: SessionPermissionMode; + onModeChange: (mode: SessionPermissionMode) => void; + /** Live daemon models for the picker ("" = auto-routed). */ + models: ComposerModelOption[]; + autoModelLabel: string; + onModelChange: (id: string) => void; +}) { + return ( +
+ {/* Same header bar as an open chat, so a draft doesn't lose the title + row and its controls. */} +
+

+ New chat +

+ {showSidebarButton && ( + + )} +
+ {/* The composer docks at the bottom exactly like an open chat, so the + draft-to-chat transition doesn't move the input under your hands. */} +
+
+
+

+ What can I help you with? +

+
+ {STARTER_PROMPTS.map((p) => ( + + ))} +
+
+
+
+
+ {error &&

{error}

} + +
+
+
+
+ ); +} + +/** + * The chat workspace: one flat, daemon-backed session list plus the open + * conversation. Selection is driven by the URL (`/workspace/chat/`), + * so deep-links, back/forward, and hard reloads resolve to the same chat. + * `/workspace/chat` with no id is a draft whose daemon session is minted on + * the first send. + */ +export function ChatWorkspace({ sessionId }: { sessionId?: string }) { + const { + sessions, + isLoading: sessionsLoading, + error: sessionsError, + deleteSession, + renameSession, + refreshSessions, + } = useAgentSessions(); + const { agents } = useAgentRoster(); + const router = useRouter(); + const { name: agentName } = useAgentDisplayName(); + const { side: sidebarSide } = useSessionListSide(); + // Labs preference: list the mock feature tour. The mock id is treated as + // mock UNCONDITIONALLY below (never handed to the daemon) — the toggle + // only controls whether the row is offered. + const { enabled: mockFeatures } = useMockFeatures(); + const isMobile = useIsMobile(); + const isCompact = useIsCompact(); + const { confirm, ConfirmDialog } = useConfirm(); + const { prompt, PromptDialog } = usePrompt(); + + // Selection is URL-driven; the state mirror keeps it in sync while also + // allowing an optimistic update before the client navigation settles. + const [selectedId, setSelectedIdState] = useState(sessionId ?? ""); + // Mirror for effects that must read the current selection without + // re-firing on every selection change. + const selectedIdRef = useRef(selectedId); + useEffect(() => { + selectedIdRef.current = selectedId; + }, [selectedId]); + // The id minted for a draft on first send. While the URL settles on that id + // the chat hook keeps its draft binding (it already owns the live stream); + // re-keying it would wipe the in-flight messages with a transcript refetch. + const draftMintedIdRef = useRef(null); + useEffect(() => { + setSelectedIdState(sessionId ?? ""); + if (sessionId !== draftMintedIdRef.current) draftMintedIdRef.current = null; + }, [sessionId]); + + /** + * Select a chat: optimistic state + a native history push. Deliberately not + * router.push — moving the optional catch-all between zero and one segments + * changes the route shape, which remounts this page; the remount re-runs + * the pre-measurement useState inits (sidebarOpen assumes desktop until the + * viewport is measured), so on a phone the list rendered straight over the + * chat that was just tapped. The App Router syncs its state from native + * history updates without remounting. + */ + const selectId = useCallback((id: string) => { + setSelectedIdState(id); + window.history.pushState(null, "", chatHref(id)); + }, []); + + const [sidebarOpen, setSidebarOpen] = useState(!isMobile && !isCompact); + // The init above runs before the viewport is measured (useIsMobile is + // undefined on the first render), so a phone mounts with the list open even + // over a deep-linked chat. Once mobile is a measured fact, the selection + // wins; a list the user opens later is untouched. + useEffect(() => { + if (isMobile && selectedIdRef.current) setSidebarOpen(false); + }, [isMobile]); + const wasCompactRef = useRef(isCompact); + // Mirrors `sidebarOpen` for reads inside the stable side-panel handler. + const sidebarOpenRef = useRef(sidebarOpen); + useEffect(() => { + sidebarOpenRef.current = sidebarOpen; + }, [sidebarOpen]); + // Remembers whether the chat list was open before a side panel forced it + // closed, so closing the panel restores the user's prior choice. + const sidebarBeforeSidePanelRef = useRef(null); + + const handleSidePanelOpenChange = useCallback((open: boolean) => { + if (open) { + if (sidebarBeforeSidePanelRef.current === null) { + sidebarBeforeSidePanelRef.current = sidebarOpenRef.current; + setSidebarOpen(false); + } + } else if (sidebarBeforeSidePanelRef.current !== null) { + setSidebarOpen(sidebarBeforeSidePanelRef.current); + sidebarBeforeSidePanelRef.current = null; + } + }, []); + + // Sync sidebar visibility with viewport transitions: entering compact + // closes it; leaving compact reopens it. + useEffect(() => { + const wasCompact = wasCompactRef.current; + if (!wasCompact && isCompact) { + setSidebarOpen(false); + } else if (wasCompact && !isCompact) { + setSidebarOpen(true); + } + wasCompactRef.current = isCompact; + }, [isCompact]); + const [sidebarWidth, setSidebarWidth] = usePanelWidth(); + + const handleSessionCreated = useCallback( + (id: string) => { + draftMintedIdRef.current = id; + setSelectedIdState(id); + // Native replaceState, deliberately not router.replace: moving the + // optional catch-all from zero segments to one changes the route + // shape, which remounts this page — and a remount replaces the chat + // hook instance, so the in-flight stream would render into dead + // state and the pane would sit empty until a reload. The App Router + // syncs its state from native history updates without remounting. + window.history.replaceState(null, "", chatHref(id)); + void refreshSessions(); + }, + [refreshSessions], + ); + + // The Labs mock chat never talks to the daemon: its transcript is a local + // constant and its id must never reach the chat hook. + const isMockSelected = isMockTourSession(selectedId); + + // The draft keeps a null hook id even after its session is minted and the + // URL updates — the hook already streams against the minted id internally. + const hookSessionId = + selectedId && !isMockSelected && selectedId !== draftMintedIdRef.current + ? selectedId + : null; + + // The composer's permission mode. For an open chat this reads/writes the + // live session (POST /mode); for a draft it is pending local state, read + // via modeRef when the first send mints the daemon session below. + const { mode, modeRef, changeMode } = useSessionMode( + isMockSelected ? null : selectedId || null, + ); + const getCreateMode = useCallback(() => modeRef.current, [modeRef]); + + // The composer's model picker: live daemon models minus the Studio-side + // disabled set; "" = auto-routed. Like mode, the pick is pending local + // state read via ref when the first send mints the session. + const { models: liveModels, status: runtimeStatus } = useHarnessRuntime(); + const { disabled: disabledModels } = useDisabledModels(); + const modelOptions = useMemo( + () => + liveModels + .filter((m) => !disabledModels.has(m.id)) + .map((m) => ({ id: m.id, label: m.displayName || m.id })), + [liveModels, disabledModels], + ); + // "Auto-routed" is only an honest name for the empty pick while the model + // router is actually on; otherwise the daemon just uses its default model. + const routingEnabled = Boolean(runtimeStatus?.modelRouter?.enabled); + const draftModelRef = useRef(""); + const handleDraftModelChange = useCallback((id: string) => { + draftModelRef.current = id; + }, []); + const getCreateModel = useCallback(() => draftModelRef.current, []); + + const { + messages, + isStreaming, + status, + error: chatError, + harnessLive, + sendMessage, + retryLast, + pendingApproval, + respondToApproval, + pendingClarification, + respondToClarification, + usage, + queuedMessages, + queueMessage, + deleteQueued, + takeQueued, + steerQueued, + steerMessage, + cancelChat, + } = useAgentChat(hookSessionId, { + onSessionCreated: handleSessionCreated, + createMode: getCreateMode, + createModel: getCreateModel, + }); + + /** Esc with nothing else open interrupts the in-flight run (close.esc). */ + const handleCancelRun = useCallback(() => { + void cancelChat(); + }, [cancelChat]); + + useNavReopenSidebar(setSidebarOpen); + + // Seed for the draft composer, set when a starter prompt is picked. + const [draftSeed, setDraftSeed] = useState(null); + const clearDraftSeed = useCallback(() => setDraftSeed(null), []); + + // Sessions minted to back message threads. Filtered HERE, at the + // presentation seam, deliberately not inside use-agent-sessions: rule 9's + // test pins that hook to the daemon store verbatim (rows obey the store), + // and a thread session IS a real store row — it is only this list, the + // keyboard order derived from it, and search that hide it. Its sole entry + // point is the reply indicator on its parent message; deep-linking to + // /workspace/chat/ still works (selectedSession reads the + // unfiltered `sessions`), which stays the escape hatch for parked + // approvals. The registry — never the "Thread: " title — decides, so a + // user's own chat named "Thread: …" is never hidden. + const threadSessionIds = useThreadSessionIds(); + const orderedSessions = useMemo( + () => + sessions + .filter((s) => !threadSessionIds.has(s.id)) + .sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)), + [sessions, threadSessionIds], + ); + const groups = useMemo(() => { + const recency = groupSessionsByRecency(orderedSessions); + // The Labs mock tour pins atop the list under its own clearly-labeled + // group — local demo content, never a daemon row. + return mockFeatures + ? [{ label: "Mock", sessions: [MOCK_TOUR_SESSION] }, ...recency] + : recency; + }, [orderedSessions, mockFeatures]); + + const selectedSession = useMemo(() => { + if (!selectedId) return undefined; + if (isMockTourSession(selectedId)) return MOCK_TOUR_SESSION; + const found = sessions.find((s) => s.id === selectedId); + if (found) return found; + // A just-minted draft's row may not have landed in the polled list yet; + // a stand-in keeps the conversation rendered until the walk catches up. + return { + id: selectedId, + title: "Untitled chat", + projectId: null, + model: "", + createdAt: 0, + updatedAt: 0, + pinned: false, + archived: false, + messageCount: 0, + isStreaming: false, + inputTokens: 0, + outputTokens: 0, + unread: false, + estimatedCost: null, + contextLength: null, + lastPromptTokens: null, + thresholdTokens: null, + }; + }, [selectedId, sessions]); + + const handleSelectSession = useCallback( + (id: string) => { + selectId(id); + if (isMobile || isCompact) setSidebarOpen(false); + }, + [selectId, isMobile, isCompact], + ); + + /** "New chat" opens the draft route; the daemon session is minted on send. */ + const handleNewChat = useCallback(() => { + setSelectedIdState(""); + router.push(chatHref()); + if (isMobile || isCompact) setSidebarOpen(false); + }, [router, isMobile, isCompact]); + + const deselectIfActive = useCallback( + (id: string) => { + if (id === selectedId) { + setSelectedIdState(""); + router.push(chatHref()); + } + }, + [selectedId, router], + ); + + const sessionActions: SessionActions = useMemo( + () => ({ + onRename: async (id: string) => { + const s = sessions.find((x) => x.id === id); + const name = await prompt({ + title: "Rename chat", + placeholder: "Chat name", + defaultValue: s?.title ?? "", + confirmText: "Rename", + }); + if (name) await renameSession(id, name); + }, + onDelete: async (id: string) => { + const ok = await confirm({ + title: "Delete chat", + description: + "This chat and its messages will be permanently deleted.", + confirmText: "Delete", + destructive: true, + }); + if (!ok) return; + await deleteSession(id); + deselectIfActive(id); + }, + }), + [sessions, prompt, renameSession, confirm, deleteSession, deselectIfActive], + ); + + // Flattened, in-display-order chat ids for keyboard navigation. + const navOrder = useMemo( + () => groups.flatMap((g) => g.sessions.map((s) => s.id)), + [groups], + ); + + const navigateBy = useCallback( + (forward: boolean) => { + if (navOrder.length === 0) return; + const cur = navOrder.indexOf(selectedId); + const idx = + cur === -1 + ? forward + ? 0 + : navOrder.length - 1 + : forward + ? Math.min(cur + 1, navOrder.length - 1) + : Math.max(cur - 1, 0); + handleSelectSession(navOrder[idx]); + }, + [navOrder, selectedId, handleSelectSession], + ); + + useShortcut("chat.new", handleNewChat); + useShortcut("chat.toggleList", () => setSidebarOpen((o) => !o)); + useShortcut("chat.next", () => navigateBy(true)); + useShortcut("chat.next.vim", () => navigateBy(true)); + useShortcut("chat.prev", () => navigateBy(false)); + useShortcut("chat.prev.vim", () => navigateBy(false)); + + const sidebarContentProps = { + onNewChat: handleNewChat, + isLoading: sessionsLoading, + error: sessionsError, + groups, + agents, + selectedId, + onSelect: handleSelectSession, + actions: sessionActions, + }; + + const dialogs = ( + <> + {ConfirmDialog} + {PromptDialog} + + ); + + const turnError = + status === "error" ? (chatError ?? "The last turn failed.") : null; + + const chatView = (open: boolean, onToggle: () => void) => + selectedSession ? ( + isMockSelected ? ( + // The mock feature tour: a canned local transcript, a disabled + // composer, and zero daemon traffic (the hook id above is null). + {}} + readOnlyPlaceholder="Mock chat — read-only" + botName={agentName} + sidebarOpen={open} + sidebarSide={sidebarSide} + onToggleSidebar={onToggle} + pendingApproval={null} + onRespondApproval={() => {}} + pendingClarification={null} + onRespondClarification={() => {}} + onSidePanelOpenChange={ + isMobile ? undefined : handleSidePanelOpenChange + } + /> + ) : ( + sessionActions.onRename(selectedSession.id) + : undefined + } + onDelete={ + selectedSession.canDelete === true + ? () => sessionActions.onDelete(selectedSession.id) + : undefined + } + onSidePanelOpenChange={ + isMobile ? undefined : handleSidePanelOpenChange + } + mode={mode} + onModeChange={changeMode} + /> + ) + ) : null; + + if (isMobile) { + return ( +
+ {dialogs} + {sidebarOpen ? ( +
+ +
+ ) : selectedSession ? ( + chatView(false, () => setSidebarOpen(true)) + ) : ( + setSidebarOpen(true)} + mode={mode} + onModeChange={changeMode} + /> + )} +
+ ); + } + + return ( +
+ {dialogs} +
+ {selectedSession ? ( + chatView(sidebarOpen, () => setSidebarOpen((o) => !o)) + ) : ( + setSidebarOpen(true)} + mode={mode} + onModeChange={changeMode} + /> + )} +
+ + {sidebarOpen && + (isCompact ? ( + <> + {/* Backdrop closes the overlay sidebar on outside click */} +
+ ); +} diff --git a/studio/src/app/workspace/chat/_components/clarification-panel.tsx b/studio/src/app/workspace/chat/_components/clarification-panel.tsx new file mode 100644 index 0000000000..2627d16de8 --- /dev/null +++ b/studio/src/app/workspace/chat/_components/clarification-panel.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { CircleHelp } from "lucide-react"; +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import type { ClarificationRequest } from "@/features/agent"; + +export function ClarificationPanel({ + clarification, + onRespond, +}: { + clarification: ClarificationRequest; + onRespond: (response: string) => void; +}) { + const [input, setInput] = useState(""); + const [choices, setChoices] = useState([]); + + useEffect(() => { + const lines = clarification.question.split("\n").filter((l) => l.trim()); + const numbered = lines.filter((l) => /^\d+[.)]\s/.test(l.trim())); + if (numbered.length > 0) { + setChoices(numbered.map((l) => l.replace(/^\d+[.)]\s*/, "").trim())); + } + }, [clarification.question]); + + const questionText = clarification.question + .split("\n") + .filter((l) => !/^\d+[.)]\s/.test(l.trim())) + .join("\n") + .trim(); + + return ( +
+
+ + + Clarification needed + +
+

{questionText || clarification.question}

+ {choices.length > 0 && ( +
+ {choices.map((choice, i) => ( + + ))} + +
+ )} +
+ setInput(e.target.value)} + placeholder="Type your response..." + className="flex-1 rounded-lg border border-info/30 bg-background px-3 py-2 text-sm focus:outline-none focus:border-info" + onKeyDown={(e) => { + if (e.key === "Enter" && input.trim()) { + onRespond(input.trim()); + setInput(""); + } + }} + /> + +
+
+ ); +} diff --git a/studio/src/app/workspace/chat/_components/code-block.test.tsx b/studio/src/app/workspace/chat/_components/code-block.test.tsx new file mode 100644 index 0000000000..3261ce461d --- /dev/null +++ b/studio/src/app/workspace/chat/_components/code-block.test.tsx @@ -0,0 +1,33 @@ +import { render, waitFor } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { CodeBlock } from "./code-block"; + +/** + * `CodeBlock` renders a synchronous plain fallback and then swaps in Shiki's + * highlighted tokens once the grammar loads. Each test waits for that swap + * (`data-highlighted="true"`) so the async state update settles inside `act`. + * Assertions stay engine-agnostic — row count and preserved text hold in both + * the fallback and highlighted states. + */ +describe("CodeBlock", () => { + it("renders one numbered row per line and preserves the code text", async () => { + const code = "const x = 1;\nfunction f() {\n return x;\n}"; + const { container } = render(); + await waitFor(() => + expect(container.querySelector("[data-highlighted='true']")).toBeTruthy(), + ); + const rows = container.querySelectorAll("tbody tr"); + expect(rows.length).toBe(4); + // Text is preserved (gutter digits are interleaved but fragments survive). + expect(container.textContent).toContain("const x = 1;"); + expect(container.textContent).toContain("return x;"); + }); + + it("drops a single trailing newline so there's no phantom last line", async () => { + const { container } = render(); + await waitFor(() => + expect(container.querySelector("[data-highlighted='true']")).toBeTruthy(), + ); + expect(container.querySelectorAll("tbody tr").length).toBe(2); + }); +}); diff --git a/studio/src/app/workspace/chat/_components/code-block.tsx b/studio/src/app/workspace/chat/_components/code-block.tsx new file mode 100644 index 0000000000..573ae7b8c1 --- /dev/null +++ b/studio/src/app/workspace/chat/_components/code-block.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { type CSSProperties, useEffect, useState } from "react"; +import { cn } from "@/lib/utils"; +import { type HLine, highlightCode } from "./code-highlighter"; + +/** + * Theme-aware, line-numbered code viewer for the file-preview panel. + * + * Tokenizing runs through Shiki (real TextMate grammars, JS engine — see + * `code-highlighter.ts`), which is async: the first paint renders the raw lines + * synchronously in the exact same gutter/table layout, then the highlighted + * tokens swap in with no layout shift once the grammar has loaded. Each token + * carries a light and a dark colour, selected via a CSS `.dark` variant. + */ + +/** Wrap a single raw line of text as one uncoloured (inherit) token run. */ +function plainLine(text: string): HLine { + return text + ? [ + { + content: text, + light: "inherit", + dark: "inherit", + italic: false, + bold: false, + }, + ] + : []; +} + +export function CodeBlock({ + code, + lang = "text", +}: { + code: string; + /** Shiki language id (see `langForExtension`); defaults to plain text. */ + lang?: string; +}) { + // Drop a single trailing newline so we don't render a phantom last line. + const source = code.replace(/\n$/, ""); + const [highlighted, setHighlighted] = useState(null); + + useEffect(() => { + let active = true; + setHighlighted(null); + highlightCode(source, lang) + .then((lines) => { + if (active) setHighlighted(lines); + }) + .catch(() => { + // Leave the plain fallback in place; highlighting is best-effort. + }); + return () => { + active = false; + }; + }, [source, lang]); + + const lines: HLine[] = highlighted ?? source.split("\n").map(plainLine); + const gutterWidth = `${String(lines.length).length + 1}ch`; + + return ( +
+ + + {lines.map((line, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: lines are positional + + + + + ))} + +
+ {i + 1} + + {line.length === 0 + ? " " + : line.map((tok, j) => ( + + {tok.content} + + ))} +
+
+ ); +} diff --git a/studio/src/app/workspace/chat/_components/code-highlighter.test.ts b/studio/src/app/workspace/chat/_components/code-highlighter.test.ts new file mode 100644 index 0000000000..867bdfa655 --- /dev/null +++ b/studio/src/app/workspace/chat/_components/code-highlighter.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import { highlightCode, langForExtension } from "./code-highlighter"; + +describe("langForExtension", () => { + it("maps known extensions to Shiki language ids", () => { + expect(langForExtension("ts")).toBe("typescript"); + expect(langForExtension("tsx")).toBe("tsx"); + expect(langForExtension("py")).toBe("python"); + expect(langForExtension("sh")).toBe("bash"); + expect(langForExtension("rs")).toBe("rust"); + }); + + it("falls back to plain text for unknown extensions", () => { + expect(langForExtension("")).toBe("text"); + expect(langForExtension("xyz")).toBe("text"); + }); +}); + +describe("highlightCode", () => { + function textOf(lines: readonly (readonly { content: string }[])[]): string { + return lines.map((line) => line.map((t) => t.content).join("")).join("\n"); + } + + it("splits into one token line per source line and preserves the text", async () => { + const code = "const x = 1;\nconst y = 2;"; + const lines = await highlightCode(code, "typescript"); + expect(lines.length).toBe(2); + expect(textOf(lines)).toBe(code); + }); + + it("assigns real colours to a known language", async () => { + const lines = await highlightCode("const x = 1;", "typescript"); + const colours = lines.flat().map((t) => t.light); + // At least one token is coloured rather than inheriting the foreground. + expect(colours.some((c) => c !== "inherit" && c.startsWith("#"))).toBe( + true, + ); + }); + + it("returns plain tokens for an unknown language without throwing", async () => { + const code = "just some words"; + const lines = await highlightCode(code, "text"); + expect(textOf(lines)).toBe(code); + }); +}); diff --git a/studio/src/app/workspace/chat/_components/code-highlighter.ts b/studio/src/app/workspace/chat/_components/code-highlighter.ts new file mode 100644 index 0000000000..3b1d13b538 --- /dev/null +++ b/studio/src/app/workspace/chat/_components/code-highlighter.ts @@ -0,0 +1,145 @@ +import { createHighlighterCore, type HighlighterCore } from "shiki/core"; +import { createJavaScriptRegexEngine } from "shiki/engine/javascript"; +import { bundledLanguages } from "shiki/langs"; +import { bundledThemes } from "shiki/themes"; + +/** + * Shiki-backed syntax highlighting for the file-preview code viewer. + * + * Kept self-contained and CSP-safe: the JavaScript RegExp engine (no + * WebAssembly, no `eval`) tokenizes with real TextMate grammars, and both + * grammars and themes are loaded from Shiki's own lazy `import()` thunks — one + * grammar chunk per language, fetched only the first time that language is + * previewed. A single core highlighter is memoized for the session. + * + * The two GitHub themes are tokenized together (`defaultColor: false`) so each + * token carries both a light and a dark colour; the component picks between + * them with a CSS `.dark` variant, matching the app's class-based theme switch. + */ + +const LIGHT_THEME = "github-light-default"; +const DARK_THEME = "github-dark-default"; + +/** File extension → Shiki language id. Unknowns fall back to plain text. */ +const EXT_TO_LANG: Record = { + ts: "typescript", + tsx: "tsx", + js: "javascript", + jsx: "jsx", + mjs: "javascript", + cjs: "javascript", + json: "json", + py: "python", + sh: "bash", + bash: "bash", + zsh: "bash", + go: "go", + rs: "rust", + java: "java", + rb: "ruby", + php: "php", + c: "c", + cpp: "cpp", + h: "c", + hpp: "cpp", + cs: "csharp", + kt: "kotlin", + swift: "swift", + yml: "yaml", + yaml: "yaml", + toml: "toml", + sql: "sql", + css: "css", + scss: "scss", + html: "html", + xml: "xml", + graphql: "graphql", + prisma: "prisma", +}; + +/** Resolve a file extension to a Shiki language id (`"text"` when unknown). */ +export function langForExtension(ext: string): string { + return EXT_TO_LANG[ext] ?? "text"; +} + +let highlighterPromise: Promise | null = null; + +function getHighlighter(): Promise { + if (!highlighterPromise) { + highlighterPromise = createHighlighterCore({ + themes: [bundledThemes[LIGHT_THEME], bundledThemes[DARK_THEME]], + langs: [], + engine: createJavaScriptRegexEngine(), + }); + } + return highlighterPromise; +} + +interface HToken { + readonly content: string; + /** Colour under the light theme (or `"inherit"` for plain text). */ + readonly light: string; + /** Colour under the dark theme. */ + readonly dark: string; + readonly italic: boolean; + readonly bold: boolean; +} + +export type HLine = readonly HToken[]; + +/** + * Shape of a token when `codeToTokens` runs with multiple themes and + * `defaultColor: false`: instead of a single resolved colour, each token gets an + * `htmlStyle` map holding one CSS custom property per theme (`--shiki-light`, + * `--shiki-dark`) plus any `font-style` / `font-weight`. + */ +interface MultiThemeToken { + readonly content: string; + readonly htmlStyle?: Record; +} + +/** + * Tokenize `code` into per-line coloured runs. Unknown/`"text"` languages (or a + * grammar that fails to load) come back as one plain token per line so the + * caller always renders something. + */ +export async function highlightCode( + code: string, + lang: string, +): Promise { + const highlighter = await getHighlighter(); + + let resolvedLang = lang; + if (resolvedLang !== "text" && resolvedLang in bundledLanguages) { + if (!highlighter.getLoadedLanguages().includes(resolvedLang)) { + try { + await highlighter.loadLanguage( + bundledLanguages[resolvedLang as keyof typeof bundledLanguages], + ); + } catch { + resolvedLang = "text"; + } + } + } else { + resolvedLang = "text"; + } + + const { tokens } = highlighter.codeToTokens(code, { + lang: resolvedLang, + themes: { light: LIGHT_THEME, dark: DARK_THEME }, + defaultColor: false, + }); + + return (tokens as unknown as MultiThemeToken[][]).map((line) => + line.map((token) => { + const style = token.htmlStyle ?? {}; + return { + content: token.content, + light: style["--shiki-light"] ?? "inherit", + dark: style["--shiki-dark"] ?? "inherit", + italic: style["font-style"] === "italic", + bold: style["font-weight"] === "bold", + }; + }), + ); +} diff --git a/studio/src/app/workspace/chat/_components/file-preview.test.tsx b/studio/src/app/workspace/chat/_components/file-preview.test.tsx new file mode 100644 index 0000000000..583ef08a18 --- /dev/null +++ b/studio/src/app/workspace/chat/_components/file-preview.test.tsx @@ -0,0 +1,80 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { FilePreview, previewKind } from "./file-preview"; + +const PNG_DATA_URI = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; + +/** + * FilePreview routes on the shared file-kind classification: images and PDFs + * render from a url/data-URI source, markdown as prose, recognised source + * files through the code viewer, and only a file with nothing displayable + * hits the "No preview available" floor. + */ +describe("previewKind", () => { + it("routes each kind to its renderer", () => { + expect(previewKind("photo.png", undefined, PNG_DATA_URI)).toBe("image"); + expect( + previewKind("chart.svg", undefined, "data:image/svg+xml;utf8,x"), + ).toBe("image"); + expect( + previewKind("report.pdf", undefined, "data:application/pdf;base64,x"), + ).toBe("pdf"); + expect(previewKind("README.md", "# Hello")).toBe("markdown"); + expect(previewKind("logger.ts", "const x = 1;")).toBe("code"); + expect(previewKind("notes.log", "plain text")).toBe("text"); + }); + + it("accepts a data-URI riding content for binary kinds", () => { + expect(previewKind("photo.png", PNG_DATA_URI)).toBe("image"); + }); + + it("floors to none only when nothing is displayable", () => { + expect(previewKind("photo.png")).toBe("none"); + expect(previewKind("report.pdf")).toBe("none"); + expect(previewKind("anything.txt")).toBe("none"); + // An image whose content is NOT displayable text keeps the text fallback + // instead of a broken . + expect(previewKind("photo.png", "not a data uri")).toBe("text"); + }); +}); + +describe("FilePreview", () => { + it("renders an image from its url", () => { + render(); + const img = screen.getByAltText("photo.png"); + expect(img).toBeInTheDocument(); + expect(img).toHaveAttribute("src", PNG_DATA_URI); + }); + + it("renders a PDF in an iframe titled by the file name", () => { + const src = "data:application/pdf;base64,JVBERi0="; + render(); + const frame = screen.getByTitle("report.pdf"); + expect(frame.tagName).toBe("IFRAME"); + expect(frame).toHaveAttribute("src", src); + }); + + it("renders markdown as prose, not raw source", () => { + render(); + expect(screen.getByRole("heading", { name: "Title" })).toBeInTheDocument(); + }); + + it("renders recognised source files through the code viewer", async () => { + const { container } = render( + , + ); + await waitFor(() => + expect(container.querySelector("[data-highlighted='true']")).toBeTruthy(), + ); + expect(container.querySelectorAll("tbody tr").length).toBe(2); + expect(container.textContent).toContain("const x = 1;"); + }); + + it("shows the no-preview floor when nothing is displayable", () => { + render(); + expect( + screen.getByText("No preview available for this file"), + ).toBeInTheDocument(); + }); +}); diff --git a/studio/src/app/workspace/chat/_components/file-preview.tsx b/studio/src/app/workspace/chat/_components/file-preview.tsx new file mode 100644 index 0000000000..5cdabdd72c --- /dev/null +++ b/studio/src/app/workspace/chat/_components/file-preview.tsx @@ -0,0 +1,119 @@ +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { + CODE_FILE_EXTENSIONS, + extensionOf, + fileKindMeta, +} from "@/lib/file-meta"; +import { CodeBlock } from "./code-block"; +import { langForExtension } from "./code-highlighter"; +import { mdComponents } from "./markdown-components"; + +/** + * Renders a file's contents in the right-hand preview panel, choosing how to + * display it from the file name (the kind classification is shared with the + * file-kind icons in src/lib/file-meta.ts): + * + * - Images render inline via when a displayable source exists (a `url` + * — data: URI or fetchable — or `content` that is itself a data: URI). + * - PDFs render in an