diff --git a/.gitattributes b/.gitattributes
index a7f2b7f3b..068cf7f02 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,2 @@
lib/crates/fabro-spa/assets/** linguist-generated=true -diff
+lib/packages/fabro-api-client/src/** linguist-generated=true
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9ac7fcd02..5a5c64bb4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,6 +8,7 @@ Outside contributions are welcome! Whether it's a bug fix, a new feature, docume
- **Bug fixes and small improvements** -- Send a pull request directly. No need to open an issue first.
- **Larger features or changes** -- Please open a [GitHub Issue](https://github.com/fabro-sh/fabro/issues) or start a [Discussion](https://github.com/fabro-sh/fabro/discussions) first so we can align on the approach before you invest significant time.
+- **Prefer not to write the code yourself?** -- As an alternative to opening a PR, you can file a [GitHub Issue](https://github.com/fabro-sh/fabro/issues) describing the bug or feature. A Fabro maintainer will implement it (supervising AI coding agents and workflows) and include you as a co-author on the commit that lands the change.
- **Questions** -- Open a Discussion or email [bryan@qlty.sh](mailto:bryan@qlty.sh).
## Development setup
diff --git a/Cargo.lock b/Cargo.lock
index de2596ff0..709e4d2f8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1644,7 +1644,7 @@ dependencies = [
[[package]]
name = "fabro-acp"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"agent-client-protocol",
"agent-client-protocol-tokio",
@@ -1666,7 +1666,7 @@ dependencies = [
[[package]]
name = "fabro-agent"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"async-trait",
@@ -1705,7 +1705,7 @@ dependencies = [
[[package]]
name = "fabro-api"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"chrono",
"fabro-config",
@@ -1726,7 +1726,7 @@ dependencies = [
[[package]]
name = "fabro-auth"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"async-trait",
@@ -1751,11 +1751,11 @@ dependencies = [
[[package]]
name = "fabro-build-support"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
[[package]]
name = "fabro-checkpoint"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"chrono",
"fabro-config",
@@ -1771,7 +1771,7 @@ dependencies = [
[[package]]
name = "fabro-cli"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"assert_cmd",
@@ -1872,7 +1872,7 @@ dependencies = [
[[package]]
name = "fabro-client"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"bytes",
@@ -1901,7 +1901,7 @@ dependencies = [
[[package]]
name = "fabro-config"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"chrono",
@@ -1929,7 +1929,7 @@ dependencies = [
[[package]]
name = "fabro-core"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"async-trait",
"fabro-types",
@@ -1944,7 +1944,7 @@ dependencies = [
[[package]]
name = "fabro-dev"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"assert_cmd",
@@ -1963,7 +1963,7 @@ dependencies = [
[[package]]
name = "fabro-devcontainer"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"fabro-http",
"fabro-static",
@@ -1980,7 +1980,7 @@ dependencies = [
[[package]]
name = "fabro-dump"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"bytes",
@@ -1994,7 +1994,7 @@ dependencies = [
[[package]]
name = "fabro-github"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"base64",
@@ -2016,7 +2016,7 @@ dependencies = [
[[package]]
name = "fabro-graphviz"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"fabro-types",
@@ -2030,7 +2030,7 @@ dependencies = [
[[package]]
name = "fabro-hooks"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"async-trait",
"fabro-agent",
@@ -2054,7 +2054,7 @@ dependencies = [
[[package]]
name = "fabro-http"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"fabro-static",
"http",
@@ -2064,7 +2064,7 @@ dependencies = [
[[package]]
name = "fabro-install"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"base64",
@@ -2079,7 +2079,7 @@ dependencies = [
[[package]]
name = "fabro-interview"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"async-trait",
"dialoguer",
@@ -2094,7 +2094,7 @@ dependencies = [
[[package]]
name = "fabro-llm"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"async-trait",
@@ -2128,7 +2128,7 @@ dependencies = [
[[package]]
name = "fabro-macros"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"clap",
"fabro-options-metadata",
@@ -2139,14 +2139,13 @@ dependencies = [
[[package]]
name = "fabro-manifest"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"fabro-api",
"fabro-config",
"fabro-github",
"fabro-graphviz",
- "fabro-template",
"fabro-types",
"fabro-workflow",
"git2",
@@ -2157,7 +2156,7 @@ dependencies = [
[[package]]
name = "fabro-mcp"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"fabro-config",
@@ -2173,7 +2172,7 @@ dependencies = [
[[package]]
name = "fabro-mcp-server"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"chrono",
@@ -2196,7 +2195,7 @@ dependencies = [
[[package]]
name = "fabro-model"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"fabro-static",
"insta",
@@ -2210,7 +2209,7 @@ dependencies = [
[[package]]
name = "fabro-oauth"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"axum",
@@ -2232,7 +2231,7 @@ dependencies = [
[[package]]
name = "fabro-options-metadata"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"serde",
"serde_json",
@@ -2240,7 +2239,7 @@ dependencies = [
[[package]]
name = "fabro-proc"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"cc",
"libc",
@@ -2249,7 +2248,7 @@ dependencies = [
[[package]]
name = "fabro-redact"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"aho-corasick",
"ref-cast",
@@ -2265,7 +2264,7 @@ dependencies = [
[[package]]
name = "fabro-sandbox"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"async-trait",
@@ -2308,7 +2307,7 @@ dependencies = [
[[package]]
name = "fabro-server"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"async-trait",
@@ -2390,7 +2389,7 @@ dependencies = [
[[package]]
name = "fabro-slack"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"fabro-http",
"fabro-interview",
@@ -2411,18 +2410,18 @@ dependencies = [
[[package]]
name = "fabro-spa"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"rust-embed",
]
[[package]]
name = "fabro-static"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
[[package]]
name = "fabro-store"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"async-trait",
"bytes",
@@ -2449,7 +2448,7 @@ dependencies = [
[[package]]
name = "fabro-telemetry"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"base64",
@@ -2475,10 +2474,11 @@ dependencies = [
[[package]]
name = "fabro-template"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"fabro-util",
+ "miette",
"minijinja",
"serde",
"thiserror 2.0.18",
@@ -2487,7 +2487,7 @@ dependencies = [
[[package]]
name = "fabro-test"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"assert_cmd",
"axum",
@@ -2510,7 +2510,7 @@ dependencies = [
[[package]]
name = "fabro-tracker"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"async-trait",
@@ -2524,7 +2524,7 @@ dependencies = [
[[package]]
name = "fabro-types"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"chrono",
"clap",
@@ -2545,7 +2545,7 @@ dependencies = [
[[package]]
name = "fabro-util"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"console 0.15.11",
@@ -2565,7 +2565,7 @@ dependencies = [
[[package]]
name = "fabro-validate"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"fabro-graphviz",
"fabro-model",
@@ -2577,7 +2577,7 @@ dependencies = [
[[package]]
name = "fabro-vault"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"chrono",
"fabro-types",
@@ -2589,7 +2589,7 @@ dependencies = [
[[package]]
name = "fabro-workflow"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"assert_cmd",
@@ -2630,6 +2630,7 @@ dependencies = [
"hex",
"httpmock",
"md5",
+ "miette",
"mime_guess",
"object_store",
"predicates",
@@ -7491,7 +7492,7 @@ dependencies = [
[[package]]
name = "twin-github"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"axum",
"base64",
@@ -7510,7 +7511,7 @@ dependencies = [
[[package]]
name = "twin-openai"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
dependencies = [
"anyhow",
"async-stream",
diff --git a/Cargo.toml b/Cargo.toml
index 1ccdb3caa..4cfb73dbe 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
edition = "2021"
-version = "0.234.0-nightly.0"
+version = "0.236.0-nightly.0"
license = "MIT"
[workspace.dependencies]
diff --git a/apps/fabro-web/app/app.css b/apps/fabro-web/app/app.css
index da290d54d..d269e21b7 100644
--- a/apps/fabro-web/app/app.css
+++ b/apps/fabro-web/app/app.css
@@ -1,7 +1,27 @@
@import "@xterm/xterm/css/xterm.css";
+
+/*
+ * Do NOT `import "./app.css"` from JS — Bun's CSS bundler rejects the
+ * `@layer name, name;` ordering rule between `@import` rules below. This file
+ * is built standalone by the Tailwind CLI step in `scripts/build.ts`; the
+ * generated `assets/app.css` is linked from `index.template.html`.
+ */
+
+/*
+ * Cascade layer order. @assistant-ui/react-ui ships unlayered CSS authored
+ * against Tailwind v3; in Tailwind v4 utilities live in @layer utilities, and
+ * any unlayered CSS wins over layered CSS regardless of selector specificity.
+ * Putting assistant-ui in a named layer that we declare BEFORE utilities makes
+ * Tailwind v4 utility classes cascade above assistant-ui's scoped preflight.
+ */
+@layer theme, base, assistant-ui, components, utilities;
+
@import "tailwindcss";
@plugin "@tailwindcss/typography";
+@import "@assistant-ui/react-ui/styles/index.css" layer(assistant-ui);
+@import "@assistant-ui/react-ui/styles/markdown.css" layer(assistant-ui);
+
@layer base {
a[href],
button,
@@ -127,3 +147,41 @@
.graph-svg ellipse[stroke="#357f9e"],
.graph-svg polygon[stroke="#357f9e"] { stroke: #67B2D7; }
.graph-svg [fill="#1a1a1a"] { fill: #c6d4e0; }
+
+/* ---------------------------------------------------------------------------
+ * assistant-ui theme overrides (--aui-* variables) — mapped to Fabro tokens.
+ * Values are HSL component triples so assistant-ui's hsl(var(...)) wrapper
+ * works. Scoped to .fabro-chat so the shadcn theme cannot leak out.
+ *
+ * Source colors come from the @theme block above:
+ * navy-950 #0F1729 = 220 47% 11%
+ * panel #252C3D = 222 24% 19%
+ * panel-alt #1a2133 = 223 33% 15%
+ * teal-500 #67B2D7 = 200 60% 62% (Fabro's "teal" is a sky blue)
+ * mint #5AC8A8 = 163 49% 57%
+ * ice-100 #E8EDF3 = 213 27% 93%
+ * ice-300 #A8B5C5 = 213 22% 72%
+ * ------------------------------------------------------------------------- */
+.fabro-chat {
+ --aui-background: 220 47% 11%;
+ --aui-foreground: 0 0% 100%;
+ --aui-card: 222 24% 19%;
+ --aui-card-foreground: 0 0% 100%;
+ --aui-popover: 222 24% 19%;
+ --aui-popover-foreground: 0 0% 100%;
+ --aui-primary: 200 60% 62%;
+ --aui-primary-foreground: 220 47% 11%;
+ --aui-secondary: 223 33% 15%;
+ --aui-secondary-foreground: 0 0% 100%;
+ --aui-muted: 223 33% 15%;
+ --aui-muted-foreground: 213 22% 72%;
+ --aui-accent: 163 49% 57%;
+ --aui-accent-foreground: 220 47% 11%;
+ --aui-destructive: 0 76% 66%;
+ --aui-destructive-foreground: 0 0% 100%;
+ --aui-border: 218 28% 17%;
+ --aui-input: 218 28% 17%;
+ --aui-ring: 200 60% 62%;
+ --aui-radius: 0.5rem;
+ --aui-thread-max-width: 44rem;
+}
diff --git a/apps/fabro-web/app/components/chats/composer-chips.tsx b/apps/fabro-web/app/components/chats/composer-chips.tsx
new file mode 100644
index 000000000..fc07d7838
--- /dev/null
+++ b/apps/fabro-web/app/components/chats/composer-chips.tsx
@@ -0,0 +1,98 @@
+import { useState } from "react";
+import {
+ Listbox,
+ ListboxButton,
+ ListboxOption,
+ ListboxOptions,
+} from "@headlessui/react";
+import {
+ ChevronUpDownIcon,
+ CheckIcon,
+ FolderIcon,
+ CpuChipIcon,
+} from "@heroicons/react/16/solid";
+
+type Choice = { id: string; label: string };
+
+const DEFAULT_PROJECT: Choice = { id: "fabro-web", label: "fabro-web" };
+const PROJECTS: Choice[] = [
+ DEFAULT_PROJECT,
+ { id: "fabro-workflows", label: "fabro-workflows" },
+ { id: "fabro-cli", label: "fabro-cli" },
+];
+
+const DEFAULT_BRANCH: Choice = { id: "main", label: "main" };
+const BRANCHES: Choice[] = [
+ DEFAULT_BRANCH,
+ { id: "develop", label: "develop" },
+ { id: "feature/start-page", label: "feature/start-page" },
+];
+
+const DEFAULT_MODEL: Choice = { id: "claude-opus-4-7", label: "Claude Opus 4.7" };
+const MODELS: Choice[] = [
+ DEFAULT_MODEL,
+ { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
+ { id: "gpt-5", label: "GPT-5" },
+];
+
+function BranchIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
+
+type IconComponent = React.ComponentType<{ className?: string }>;
+
+function Chip({
+ options,
+ value,
+ onChange,
+ Icon,
+}: {
+ options: Choice[];
+ value: Choice;
+ onChange: (c: Choice) => void;
+ Icon: IconComponent;
+}) {
+ return (
+
+ {toolName}
+
+
+ {formatJson(args)}
+
+
+ {formatJson(result)}
+
+
+ {toolName}
+
+
+ {formatJson(args)}
+
+
+ {formatJson(result)}
+
+ That chat doesn’t exist.
+ +` losing its right padding to overflow.
+
+## What it does NOT prove
+
+- No backend integration. All replies come from `src/lib/chats-script.ts`.
+- No persistence. Refreshing the page wipes new chats.
+- No real auth, no real project/branch/model selection (chips are decorative).
+- The mock top-nav links other than `Sample` are dead.
+
+## Porting to fabro-web
+
+When the integration plan is written, key files to port (with adaptations):
+
+- `src/index.css` — the `@layer assistant-ui` block and `.fabro-chat` /
+ `.ask-fabro-sidebar` override blocks. Drop the local Fabro theme tokens
+ (already in `apps/fabro-web/app/app.css`).
+- `src/components/{composer-chips,custom-composer,sidebar-composer,tool-fallback,ask-fabro-sidebar}.tsx`
+ — copy and adapt to fabro-web conventions (use existing UI helpers from
+ `apps/fabro-web/app/components/ui.tsx`).
+- `src/lib/chats-runtime.ts` (the `ChatModelAdapter` + `toThreadMessages`
+ conversion) — replace the scripted bank with a real completions client.
+- `src/lib/ask-fabro-context.tsx` — lift state into fabro-web's AppShell.
+- `src/routes/chats-{layout,new,detail}.tsx` and `src/routes/sample.tsx`
+ — adapt to fabro-web's `app/routes/` and `app/router.tsx`.
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/bun.lock b/docs/superpowers/prototypes/2026-05-16-chats-new/bun.lock
new file mode 100644
index 000000000..fec233fdf
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/bun.lock
@@ -0,0 +1,819 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "chats-prototype",
+ "dependencies": {
+ "@assistant-ui/react": "0.14.5",
+ "@assistant-ui/react-markdown": "0.14.0",
+ "@assistant-ui/react-ui": "0.2.1",
+ "@headlessui/react": "^2.2.9",
+ "@heroicons/react": "^2.2.0",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "react-router": "^7.12.0",
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.3.0",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.2",
+ "tailwindcss": "^4.3.0",
+ "typescript": "^5.9.2",
+ "vite": "^8.0.13",
+ },
+ },
+ },
+ "packages": {
+ "@assistant-ui/core": ["@assistant-ui/core@0.2.2", "", { "dependencies": { "assistant-stream": "^0.3.14", "nanoid": "^5.1.11" }, "peerDependencies": { "@assistant-ui/store": "^0.2.10", "@assistant-ui/tap": "^0.5.11", "@types/react": "*", "assistant-cloud": "^0.1.27", "react": "^18 || ^19", "zustand": "^5.0.11" }, "optionalPeers": ["@types/react", "assistant-cloud", "react", "zustand"] }, "sha512-y1LY7P7T+X3zAZZpfwYppaCkuscDCvbtDtIx87E9fdsY3t9o0h6tjunYdRgExcRQyzfCOE4bECeylYTs4Ky/NA=="],
+
+ "@assistant-ui/react": ["@assistant-ui/react@0.14.5", "", { "dependencies": { "@assistant-ui/core": "^0.2.2", "@assistant-ui/store": "^0.2.10", "@assistant-ui/tap": "^0.5.11", "@radix-ui/primitive": "^1.1.3", "@radix-ui/react-compose-refs": "^1.1.2", "@radix-ui/react-context": "^1.1.3", "@radix-ui/react-primitive": "^2.1.4", "@radix-ui/react-use-callback-ref": "^1.1.1", "@radix-ui/react-use-escape-keydown": "^1.1.1", "assistant-cloud": "^0.1.27", "assistant-stream": "^0.3.14", "nanoid": "^5.1.11", "radix-ui": "^1.4.3", "react-textarea-autosize": "^8.5.9", "safe-content-frame": "^0.0.19", "zod": "^4.4.3", "zustand": "^5.0.13" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^18 || ^19", "react-dom": "^18 || ^19" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Zx0Dtk39MEE+Bj3FFjCy9AylsXk2sN7sVqM5/ReVTtJOyUPk6c5NjvY2ovtMbtksXH3ZYVVP25J2AHBEwngruA=="],
+
+ "@assistant-ui/react-markdown": ["@assistant-ui/react-markdown@0.14.0", "", { "dependencies": { "@radix-ui/react-primitive": "^2.1.4", "@radix-ui/react-use-callback-ref": "^1.1.1", "classnames": "^2.5.1", "react-markdown": "^10.1.0" }, "peerDependencies": { "@assistant-ui/react": "^0.14.0", "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-G29cz4UHnQFyxTXCHsloMfyqHSuKSiPhkaAa069tWdf/Px3ZKCyhcP0TENCnsjqsw2iTFMVm8/Nqa0d5OwsdYw=="],
+
+ "@assistant-ui/react-ui": ["@assistant-ui/react-ui@0.2.1", "", { "dependencies": { "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-primitive": "^2.0.2", "@radix-ui/react-tooltip": "^1.1.8", "class-variance-authority": "^0.7.1", "classnames": "^2.5.1", "lucide-react": "^0.475.0", "zustand": "^5.0.3" }, "peerDependencies": { "@assistant-ui/react": "*", "@assistant-ui/react-markdown": "*", "@types/react": "*", "@types/react-dom": "*", "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-p1sYQR2YLFyXT63R2pdeMGESccqJNn6wxNbms9/srxg1eH343l09+zLGJpF5laMamLvY9gfNNuW29QpSPWU63Q=="],
+
+ "@assistant-ui/store": ["@assistant-ui/store@0.2.10", "", { "dependencies": { "use-effect-event": "^2.0.3" }, "peerDependencies": { "@assistant-ui/tap": "^0.5.11", "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-cgbSFIv0Ovu6yls4GQy7brLVx6qwUyLTf1Ki/lkj3UFJrO6oktxstosWvQBwk5mNgH6t3DOIrGSBDJSKRfCW5Q=="],
+
+ "@assistant-ui/tap": ["@assistant-ui/tap@0.5.11", "", { "peerDependencies": { "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@types/react", "react"] }, "sha512-wsEp6mn6BOQnP56OksWHarIQiMeCDcTzEiAORTUq0yxWa/co6a06UowFe6zZS6WQ56EQ3w02bfSBFrGnsrIv5A=="],
+
+ "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
+
+ "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
+
+ "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
+
+ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
+
+ "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
+
+ "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
+
+ "@floating-ui/react": ["@floating-ui/react@0.26.28", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.2", "@floating-ui/utils": "^0.2.8", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw=="],
+
+ "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="],
+
+ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
+
+ "@headlessui/react": ["@headlessui/react@2.2.10", "", { "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.20.2", "@react-aria/interactions": "^3.25.0", "@tanstack/react-virtual": "^3.13.9", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA=="],
+
+ "@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="],
+
+ "@internationalized/date": ["@internationalized/date@3.12.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ=="],
+
+ "@internationalized/number": ["@internationalized/number@3.6.6", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ=="],
+
+ "@internationalized/string": ["@internationalized/string@3.2.8", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-NdbMQUSfXLYIQol5VyMtinm9pZDciiMfN7RtmSuSB78io1hqwJ0naYfxyW6vgxWBkzWymQa/3uLDlbfmshtCaA=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
+
+ "@oxc-project/types": ["@oxc-project/types@0.130.0", "", {}, "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q=="],
+
+ "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
+
+ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
+
+ "@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="],
+
+ "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="],
+
+ "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
+
+ "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
+
+ "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g=="],
+
+ "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
+
+ "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
+
+ "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
+
+ "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
+
+ "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
+
+ "@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
+
+ "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="],
+
+ "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
+
+ "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
+
+ "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
+
+ "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
+
+ "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
+
+ "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
+
+ "@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="],
+
+ "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="],
+
+ "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
+
+ "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
+
+ "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
+
+ "@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA=="],
+
+ "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="],
+
+ "@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="],
+
+ "@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="],
+
+ "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
+
+ "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
+
+ "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
+
+ "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
+
+ "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
+
+ "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],
+
+ "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="],
+
+ "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
+
+ "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
+
+ "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
+
+ "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
+
+ "@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="],
+
+ "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
+
+ "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
+
+ "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
+
+ "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="],
+
+ "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
+
+ "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="],
+
+ "@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="],
+
+ "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
+
+ "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
+
+ "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
+
+ "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
+
+ "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
+
+ "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="],
+
+ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
+ "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
+
+ "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
+
+ "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
+
+ "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
+
+ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
+
+ "@react-aria/focus": ["@react-aria/focus@3.22.0", "", { "dependencies": { "@swc/helpers": "^0.5.0", "react-aria": "3.48.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-ZfDOVuVhqDsM9mkNji3QUZ/d40JhlVgXrDkrfXylM1035QCrcTHN7m2DpbE95sU2A8EQb4wikvt5jM6K/73BPg=="],
+
+ "@react-aria/interactions": ["@react-aria/interactions@3.28.0", "", { "dependencies": { "@react-types/shared": "^3.34.0", "@swc/helpers": "^0.5.0", "react-aria": "3.48.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-OXwdU1EWFdMxmr/K1CXNGJzmNlCClByb+PuCaqUyzBymHPCGVhawirLIon/CrIN5psh3AiWpHSh4H0WeJdVpng=="],
+
+ "@react-types/shared": ["@react-types/shared@3.34.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-gp6xo/s2lX54AlTjOiqwDnxA7UW79BNvI9dB9pr3LZTzRKCd1ZA+ZbgKw/ReIiWuvvVw/8QFJpnqeeFyLocMcQ=="],
+
+ "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.1", "", { "os": "android", "cpu": "arm64" }, "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg=="],
+
+ "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg=="],
+
+ "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg=="],
+
+ "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw=="],
+
+ "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ=="],
+
+ "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A=="],
+
+ "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg=="],
+
+ "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg=="],
+
+ "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ=="],
+
+ "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.1", "", { "os": "linux", "cpu": "x64" }, "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw=="],
+
+ "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ=="],
+
+ "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.1", "", { "os": "none", "cpu": "arm64" }, "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ=="],
+
+ "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.1", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ=="],
+
+ "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw=="],
+
+ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.1", "", { "os": "win32", "cpu": "x64" }, "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
+
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
+ "@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="],
+
+ "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="],
+
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="],
+
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="],
+
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="],
+
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="],
+
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="],
+
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="],
+
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="],
+
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="],
+
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="],
+
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="],
+
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="],
+
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="],
+
+ "@tailwindcss/vite": ["@tailwindcss/vite@4.3.0", "", { "dependencies": { "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "tailwindcss": "4.3.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw=="],
+
+ "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.24", "", { "dependencies": { "@tanstack/virtual-core": "3.14.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg=="],
+
+ "@tanstack/virtual-core": ["@tanstack/virtual-core@3.14.0", "", {}, "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q=="],
+
+ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
+
+ "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
+
+ "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
+
+ "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
+
+ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
+
+ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
+
+ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
+
+ "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
+
+ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+
+ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+
+ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="],
+
+ "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="],
+
+ "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
+
+ "assistant-cloud": ["assistant-cloud@0.1.27", "", { "dependencies": { "assistant-stream": "^0.3.12" } }, "sha512-BGfVnx7YFN5xtB/kbrgGxRI0TfSWq4yxB3MwYn6RDPlv4JvdtPupvDC1Y6An0EhAe42Z0AYtSmDSsR6p6eeBng=="],
+
+ "assistant-stream": ["assistant-stream@0.3.14", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "nanoid": "^5.1.11", "secure-json-parse": "^4.1.0" }, "peerDependencies": { "ioredis": "^5.10.1", "redis": "^5.12.1" }, "optionalPeers": ["ioredis", "redis"] }, "sha512-LWJt+6cjukoEKaN3LHwx40QbnODnoMmGCPkF4Tjg3fwTjgUTWsYnNJ5H2dnRmJFbxVgKTNMdJHjkCIOSemE2tg=="],
+
+ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
+
+ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
+
+ "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
+
+ "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
+
+ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
+
+ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
+
+ "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
+
+ "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
+
+ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+
+ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
+
+ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
+
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
+
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
+ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
+
+ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
+
+ "enhanced-resolve": ["enhanced-resolve@5.21.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q=="],
+
+ "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
+
+ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
+
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+ "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "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" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
+
+ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
+
+ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
+
+ "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
+
+ "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
+
+ "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
+
+ "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
+
+ "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
+
+ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
+
+ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
+
+ "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "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" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
+
+ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
+
+ "lucide-react": ["lucide-react@0.475.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "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" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
+
+ "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "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" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
+
+ "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "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" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
+
+ "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "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" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
+
+ "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
+
+ "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "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" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
+
+ "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "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" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
+
+ "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
+
+ "micromark": ["micromark@4.0.2", "", { "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" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
+
+ "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "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" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
+
+ "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
+
+ "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
+
+ "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
+
+ "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "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" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
+
+ "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "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" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
+
+ "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
+
+ "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
+
+ "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
+
+ "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
+
+ "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
+
+ "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "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" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
+
+ "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
+
+ "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
+
+ "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
+
+ "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
+
+ "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
+
+ "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
+
+ "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
+
+ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="],
+
+ "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
+ "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
+
+ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
+
+ "radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="],
+
+ "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
+
+ "react-aria": ["react-aria@3.48.0", "", { "dependencies": { "@internationalized/date": "^3.12.1", "@internationalized/number": "^3.6.6", "@internationalized/string": "^3.2.8", "@react-types/shared": "^3.34.0", "@swc/helpers": "^0.5.0", "aria-hidden": "^1.2.3", "clsx": "^2.0.0", "react-stately": "3.46.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-jQjd4rBEIMqecBaAKYJbVGK6EqIHLa5znVQ7jwFyK5vCyljoj6KhgtiahmcIPsG5vG5vEDLw+ba+bEWn6A2P4w=="],
+
+ "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
+
+ "react-markdown": ["react-markdown@10.1.0", "", { "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" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
+
+ "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "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" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
+
+ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
+
+ "react-router": ["react-router@7.15.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A=="],
+
+ "react-stately": ["react-stately@3.46.0", "", { "dependencies": { "@internationalized/date": "^3.12.1", "@internationalized/number": "^3.6.6", "@internationalized/string": "^3.2.8", "@react-types/shared": "^3.34.0", "@swc/helpers": "^0.5.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-OdxhWvHgs2L4OJGIs7hnuTr5WjjMM6enhNEAMRqiekhF8+ITvA2LRwNftOZwcogaoCslGYq5S2VQTQwnm0GbCA=="],
+
+ "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
+
+ "react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A=="],
+
+ "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
+
+ "remark-rehype": ["remark-rehype@11.1.2", "", { "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" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
+
+ "rolldown": ["rolldown@1.0.1", "", { "dependencies": { "@oxc-project/types": "=0.130.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.1", "@rolldown/binding-darwin-arm64": "1.0.1", "@rolldown/binding-darwin-x64": "1.0.1", "@rolldown/binding-freebsd-x64": "1.0.1", "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", "@rolldown/binding-linux-arm64-gnu": "1.0.1", "@rolldown/binding-linux-arm64-musl": "1.0.1", "@rolldown/binding-linux-ppc64-gnu": "1.0.1", "@rolldown/binding-linux-s390x-gnu": "1.0.1", "@rolldown/binding-linux-x64-gnu": "1.0.1", "@rolldown/binding-linux-x64-musl": "1.0.1", "@rolldown/binding-openharmony-arm64": "1.0.1", "@rolldown/binding-wasm32-wasi": "1.0.1", "@rolldown/binding-win32-arm64-msvc": "1.0.1", "@rolldown/binding-win32-x64-msvc": "1.0.1" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ=="],
+
+ "safe-content-frame": ["safe-content-frame@0.0.19", "", {}, "sha512-+R0IHHjvghT5O8bc8itf9AoS9MvzhUcD0p+hNINLgyEuFQJug3wt3ZuhLFZFG3bUzHi8UfQED4p6J3/Ft9oCtg=="],
+
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+ "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
+
+ "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
+
+ "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
+
+ "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
+
+ "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
+
+ "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
+
+ "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="],
+
+ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
+
+ "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+
+ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
+
+ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
+
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
+
+ "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
+
+ "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
+
+ "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
+
+ "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
+
+ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
+
+ "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
+
+ "use-composed-ref": ["use-composed-ref@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w=="],
+
+ "use-effect-event": ["use-effect-event@2.0.3", "", { "peerDependencies": { "react": "^18.3 || ^19.0.0-0" } }, "sha512-fz1en+z3fYXCXx3nMB8hXDMuygBltifNKZq29zDx+xNJ+1vEs6oJlYd9sK31vxJ0YI534VUsHEBY0k2BATsmBQ=="],
+
+ "use-isomorphic-layout-effect": ["use-isomorphic-layout-effect@1.2.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA=="],
+
+ "use-latest": ["use-latest@1.3.0", "", { "dependencies": { "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ=="],
+
+ "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
+
+ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
+
+ "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
+
+ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
+
+ "vite": ["vite@8.0.13", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.1", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "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" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw=="],
+
+ "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
+
+ "zustand": ["zustand@5.0.13", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ=="],
+
+ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+
+ "@radix-ui/react-accordion/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-accordion/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-alert-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-alert-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-aspect-ratio/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-avatar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-checkbox/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-checkbox/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-collapsible/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-collapsible/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-collection/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-context-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-context-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-dropdown-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-form/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-form/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-hover-card/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-hover-card/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-menubar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-menubar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-navigation-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-navigation-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-one-time-password-field/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-one-time-password-field/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-password-toggle-field/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-password-toggle-field/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-popover/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-popover/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-progress/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-progress/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-radio-group/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-radio-group/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-scroll-area/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-scroll-area/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-select/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-slider/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-slider/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-switch/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-switch/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-tabs/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-toast/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-toast/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-toggle/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-toggle-group/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-toggle-group/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-toolbar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-toolbar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-tooltip/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
+
+ "postcss/nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
+
+ "radix-ui/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "radix-ui/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "radix-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-accordion/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-aspect-ratio/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-avatar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-checkbox/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-collapsible/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-context-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-form/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-hover-card/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-label/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-menubar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-navigation-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-one-time-password-field/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-password-toggle-field/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-progress/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-radio-group/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-scroll-area/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-separator/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-slider/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-switch/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-toast/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-toggle-group/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-toggle/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-toolbar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+ }
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/index.html b/docs/superpowers/prototypes/2026-05-16-chats-new/index.html
new file mode 100644
index 000000000..5f8bb6273
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Fabro chats prototype
+
+
+
+
+
+
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/package.json b/docs/superpowers/prototypes/2026-05-16-chats-new/package.json
new file mode 100644
index 000000000..604f15ae4
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "chats-prototype",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@assistant-ui/react": "0.14.5",
+ "@assistant-ui/react-markdown": "0.14.0",
+ "@assistant-ui/react-ui": "0.2.1",
+ "@headlessui/react": "^2.2.9",
+ "@heroicons/react": "^2.2.0",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "react-router": "^7.12.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.3.0",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.2",
+ "tailwindcss": "^4.3.0",
+ "typescript": "^5.9.2",
+ "vite": "^8.0.13"
+ }
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/screenshots/chats-detail-v4.png b/docs/superpowers/prototypes/2026-05-16-chats-new/screenshots/chats-detail-v4.png
new file mode 100644
index 000000000..7d2c32e91
Binary files /dev/null and b/docs/superpowers/prototypes/2026-05-16-chats-new/screenshots/chats-detail-v4.png differ
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/screenshots/chats-new-v4.png b/docs/superpowers/prototypes/2026-05-16-chats-new/screenshots/chats-new-v4.png
new file mode 100644
index 000000000..2e956c1e4
Binary files /dev/null and b/docs/superpowers/prototypes/2026-05-16-chats-new/screenshots/chats-new-v4.png differ
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/app.tsx b/docs/superpowers/prototypes/2026-05-16-chats-new/src/app.tsx
new file mode 100644
index 000000000..89ffe25cf
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/app.tsx
@@ -0,0 +1,81 @@
+import { Link, NavLink, Outlet } from "react-router";
+import {
+ ChartBarIcon,
+ Cog6ToothIcon,
+ PlayIcon,
+ SparklesIcon,
+} from "@heroicons/react/24/outline";
+
+import { AskFabroProvider } from "./lib/ask-fabro-context";
+import AskFabroSidebar from "./components/ask-fabro-sidebar";
+
+const NAV = [
+ { name: "Automations", to: "/automations", icon: SparklesIcon },
+ { name: "Runs", to: "/runs", icon: PlayIcon },
+ { name: "Insights", to: "/insights", icon: ChartBarIcon },
+ { name: "Sample", to: "/sample", icon: ChartBarIcon },
+ { name: "Settings", to: "/settings", icon: Cog6ToothIcon },
+];
+
+/**
+ * Mock of the Fabro AppShell top nav. The chat surface mounts beneath it via
+ * , exactly the way /chats/new would slot into the real AppShell.
+ */
+export default function App() {
+ return (
+
+
+
+ );
+}
+
+function AppLayout() {
+ return (
+
+
+
+
+
+
+
+ fabro
+
+
+ prototype
+
+
+
+
+ bryan@brynary.com
+
+ BH
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/components/ask-fabro-sidebar.tsx b/docs/superpowers/prototypes/2026-05-16-chats-new/src/components/ask-fabro-sidebar.tsx
new file mode 100644
index 000000000..155818fcd
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/components/ask-fabro-sidebar.tsx
@@ -0,0 +1,80 @@
+import { useMemo, useRef } from "react";
+import {
+ AssistantRuntimeProvider,
+ useLocalRuntime,
+} from "@assistant-ui/react";
+import { Thread, makeMarkdownText } from "@assistant-ui/react-ui";
+import { XMarkIcon } from "@heroicons/react/24/outline";
+
+import { createScriptedAdapter } from "../lib/chats-runtime";
+import { useAskFabro } from "../lib/ask-fabro-context";
+import SidebarComposer from "./sidebar-composer";
+import ToolFallback from "./tool-fallback";
+import type { Chat } from "../lib/types";
+
+const MarkdownText = makeMarkdownText();
+
+const EMPTY_CHAT: Chat = {
+ id: "ask-fabro",
+ title: "",
+ createdAt: 0,
+ scriptIndex: 0,
+};
+
+const SIDEBAR_WIDTH = 420;
+
+export default function AskFabroSidebar() {
+ const { isOpen, close } = useAskFabro();
+ const scriptIndexRef = useRef(0);
+ const adapter = useMemo(
+ () =>
+ createScriptedAdapter({
+ getChat: () => ({ ...EMPTY_CHAT, scriptIndex: scriptIndexRef.current }),
+ onReplyComplete: () => {
+ scriptIndexRef.current += 1;
+ },
+ }),
+ [],
+ );
+ const runtime = useLocalRuntime(adapter);
+
+ return (
+
+ );
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/components/composer-chips.tsx b/docs/superpowers/prototypes/2026-05-16-chats-new/src/components/composer-chips.tsx
new file mode 100644
index 000000000..f9372a6dc
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/components/composer-chips.tsx
@@ -0,0 +1,95 @@
+import { useState } from "react";
+import {
+ Listbox,
+ ListboxButton,
+ ListboxOption,
+ ListboxOptions,
+} from "@headlessui/react";
+import {
+ ChevronUpDownIcon,
+ CheckIcon,
+ FolderIcon,
+ CpuChipIcon,
+} from "@heroicons/react/16/solid";
+
+type Choice = { id: string; label: string };
+
+const PROJECTS: Choice[] = [
+ { id: "fabro-web", label: "fabro-web" },
+ { id: "fabro-workflows", label: "fabro-workflows" },
+ { id: "fabro-cli", label: "fabro-cli" },
+];
+
+const BRANCHES: Choice[] = [
+ { id: "main", label: "main" },
+ { id: "develop", label: "develop" },
+ { id: "feature/start-page", label: "feature/start-page" },
+];
+
+const MODELS: Choice[] = [
+ { id: "claude-opus-4-7", label: "Claude Opus 4.7" },
+ { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
+ { id: "gpt-5", label: "GPT-5" },
+];
+
+function BranchIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
+
+type IconComponent = React.ComponentType<{ className?: string }>;
+
+function Chip({
+ options,
+ value,
+ onChange,
+ Icon,
+}: {
+ options: Choice[];
+ value: Choice;
+ onChange: (c: Choice) => void;
+ Icon: IconComponent;
+}) {
+ return (
+
+
+
+
+ {value.label}
+
+
+
+ {options.map((opt) => (
+
+
+ {opt.label}
+
+ ))}
+
+
+
+ );
+}
+
+export default function ComposerChips() {
+ const [project, setProject] = useState(PROJECTS[0]!);
+ const [branch, setBranch] = useState(BRANCHES[0]!);
+ const [model, setModel] = useState(MODELS[0]!);
+ return (
+
+
+
+
+
+ );
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/components/custom-composer.tsx b/docs/superpowers/prototypes/2026-05-16-chats-new/src/components/custom-composer.tsx
new file mode 100644
index 000000000..adbb56d97
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/components/custom-composer.tsx
@@ -0,0 +1,43 @@
+import { ComposerPrimitive, ThreadPrimitive } from "@assistant-ui/react";
+import { ArrowUpIcon } from "@heroicons/react/24/solid";
+import { StopIcon } from "@heroicons/react/24/outline";
+
+import ComposerChips from "./composer-chips";
+
+export default function CustomComposer() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/components/sidebar-composer.tsx b/docs/superpowers/prototypes/2026-05-16-chats-new/src/components/sidebar-composer.tsx
new file mode 100644
index 000000000..ff345ad72
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/components/sidebar-composer.tsx
@@ -0,0 +1,43 @@
+import { ComposerPrimitive, ThreadPrimitive } from "@assistant-ui/react";
+import { ArrowUpIcon } from "@heroicons/react/24/solid";
+import { StopIcon } from "@heroicons/react/24/outline";
+
+/**
+ * Compact single-line composer for the Ask-Fabro sidebar. Stripped of the
+ * project/branch/model chips; textarea + send button live on one row inside
+ * a rounded pill.
+ */
+export default function SidebarComposer() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/components/tool-fallback.tsx b/docs/superpowers/prototypes/2026-05-16-chats-new/src/components/tool-fallback.tsx
new file mode 100644
index 000000000..32fa82062
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/components/tool-fallback.tsx
@@ -0,0 +1,48 @@
+import type { ToolCallMessagePartProps } from "@assistant-ui/react";
+import { WrenchScrewdriverIcon } from "@heroicons/react/24/outline";
+
+export default function ToolFallback(props: ToolCallMessagePartProps) {
+ const { toolName, args, result } = props;
+ return (
+
+
+
+ tool
+
+ {toolName}
+
+
+
+
+ {formatJson(args)}
+
+
+ {result !== undefined && (
+
+
+ {formatJson(result)}
+
+
+ )}
+
+ );
+}
+
+function Section({ label, children }: { label: string; children: React.ReactNode }) {
+ return (
+
+
+ {label}
+
+ {children}
+
+ );
+}
+
+function formatJson(value: unknown): string {
+ try {
+ return JSON.stringify(value, null, 2);
+ } catch {
+ return String(value);
+ }
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/index.css b/docs/superpowers/prototypes/2026-05-16-chats-new/src/index.css
new file mode 100644
index 000000000..6fc9d2498
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/index.css
@@ -0,0 +1,271 @@
+@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Geist+Mono:wght@100..900&display=swap")
+ layer(base);
+
+/*
+ * Cascade layer order. assistant-ui ships unlayered CSS authored against
+ * Tailwind v3; in Tailwind v4 utilities live in @layer utilities, and any
+ * unlayered CSS wins over layered CSS regardless of selector specificity.
+ * Putting assistant-ui in a named layer that we declare BEFORE utilities
+ * makes Tailwind v4 utility classes (e.g. text-5xl, font-semibold) cascade
+ * above assistant-ui's scoped preflight resets — so they apply correctly
+ * inside .aui-root.
+ */
+@layer theme, base, assistant-ui, components, utilities;
+
+@import "tailwindcss";
+
+@import "@assistant-ui/react-ui/styles/index.css" layer(assistant-ui);
+@import "@assistant-ui/react-ui/styles/markdown.css" layer(assistant-ui);
+
+/* ---------------------------------------------------------------------------
+ * Fabro color tokens, copied verbatim from apps/fabro-web/app/app.css so this
+ * prototype shares the real product palette.
+ * ------------------------------------------------------------------------- */
+@theme {
+ --font-sans:
+ "Geist", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
+ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ --font-mono: "Geist Mono", "JetBrains Mono", ui-monospace, monospace;
+
+ /* Primary — Teal (actually sky blue in Fabro's palette) */
+ --color-teal-300: #b5ddef;
+ --color-teal-500: #67b2d7;
+ --color-teal-700: #357f9e;
+
+ /* Neutrals Dark — Navy */
+ --color-navy-950: #0f1729;
+ --color-navy-800: #252c3d;
+ --color-navy-600: #4b5768;
+
+ /* Neutrals Light — Ice */
+ --color-ice-50: #f7f9fb;
+ --color-ice-100: #e8edf3;
+ --color-ice-300: #a8b5c5;
+
+ /* Accents */
+ --color-mint: #5ac8a8;
+ --color-amber: #f0a45b;
+ --color-coral: #e86b6b;
+
+ /* Semantic tokens */
+ --color-page: #0f1729;
+ --color-panel: #252c3d;
+ --color-panel-alt: #1a2133;
+ --color-overlay: rgba(255, 255, 255, 0.04);
+ --color-overlay-strong: rgba(255, 255, 255, 0.1);
+ --color-fg: #ffffff;
+ --color-fg-2: #e8edf3;
+ --color-fg-3: #a8b5c5;
+ --color-fg-muted: #4b5768;
+ --color-line: rgba(255, 255, 255, 0.06);
+ --color-line-strong: rgba(255, 255, 255, 0.12);
+ --color-focus: rgba(103, 178, 215, 0.4);
+ --color-on-primary: #0f1729;
+}
+
+@layer base {
+ html {
+ font-feature-settings: "ss03", "cv11";
+ }
+ /* Mirror fabro-web's cursor base layer for interactive elements. */
+ a[href],
+ button,
+ select,
+ summary,
+ [role="button"],
+ [role="link"],
+ [role="option"],
+ [role="menuitem"],
+ [role="tab"] {
+ cursor: pointer;
+ }
+}
+
+html,
+body,
+#root {
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ background-color: var(--color-page);
+ color: var(--color-fg);
+ -webkit-font-smoothing: antialiased;
+}
+
+/* ---------------------------------------------------------------------------
+ * Atmospheric backdrop — copied from fabro-web's .bg-atmosphere. Two soft
+ * radial gradients (teal top-left, mint bottom-right) + a faint noise texture.
+ * ------------------------------------------------------------------------- */
+.bg-atmosphere {
+ background-color: var(--color-page);
+ background-image:
+ radial-gradient(
+ ellipse 120% 60% at 15% 5%,
+ rgba(53, 127, 158, 0.12) 0%,
+ transparent 50%
+ ),
+ radial-gradient(
+ ellipse 80% 50% at 85% 90%,
+ rgba(90, 200, 168, 0.07) 0%,
+ transparent 45%
+ );
+ background-attachment: fixed;
+}
+
+.bg-atmosphere::before {
+ content: "";
+ position: fixed;
+ inset: 0;
+ z-index: 1;
+ pointer-events: none;
+ opacity: 0.07;
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
+ background-repeat: repeat;
+ background-size: 200px 200px;
+}
+
+/* ---------------------------------------------------------------------------
+ * assistant-ui theme overrides (--aui-* variables) — mapped to Fabro tokens.
+ * Values are HSL component triples so assistant-ui's hsl(var(...)) wrapper
+ * works. Scoped to .fabro-chat so the shadcn theme cannot leak out.
+ *
+ * Source colors (Fabro):
+ * navy-950 #0F1729 = 220 47% 11%
+ * panel #252C3D = 222 24% 19%
+ * panel-alt #1a2133 = 223 33% 15%
+ * teal-500 #67B2D7 = 200 60% 62% (Fabro's "teal" is a sky blue)
+ * teal-700 #357F9E = 198 50% 41%
+ * ice-100 #E8EDF3 = 213 27% 93%
+ * ice-300 #A8B5C5 = 213 22% 72%
+ * navy-600 #4B5768 = 218 16% 35%
+ * line/strong are translucent whites; approximated to opaque navys.
+ * ------------------------------------------------------------------------- */
+.fabro-chat {
+ --aui-background: 220 47% 11%;
+ --aui-foreground: 0 0% 100%;
+ --aui-card: 222 24% 19%;
+ --aui-card-foreground: 0 0% 100%;
+ --aui-popover: 222 24% 19%;
+ --aui-popover-foreground: 0 0% 100%;
+ --aui-primary: 200 60% 62%;
+ --aui-primary-foreground: 220 47% 11%;
+ --aui-secondary: 223 33% 15%;
+ --aui-secondary-foreground: 0 0% 100%;
+ --aui-muted: 223 33% 15%;
+ --aui-muted-foreground: 213 22% 72%;
+ --aui-accent: 163 49% 57%;
+ --aui-accent-foreground: 220 47% 11%;
+ --aui-destructive: 0 76% 66%;
+ --aui-destructive-foreground: 0 0% 100%;
+ --aui-border: 218 28% 17%;
+ --aui-input: 218 28% 17%;
+ --aui-ring: 200 60% 62%;
+ --aui-radius: 0.5rem;
+ --aui-thread-max-width: 44rem;
+}
+
+/* In the narrow Ask-Fabro sidebar column, drop the message-column max-width so
+ * messages and the composer span the full column width. Declared AFTER
+ * .fabro-chat so it wins on cascade order (the element has both classes).
+ * Also nudge the base font size down to 14px for a tighter feel. */
+.ask-fabro-sidebar {
+ --aui-thread-max-width: 100%;
+ font-size: 0.875rem; /* 14px */
+}
+
+/* Override assistant-ui defaults that are tuned for the full-page Thread but
+ * waste space inside the narrow sidebar column. Layered into assistant-ui so
+ * we beat the unlayered Tailwind v3 rules without `!important`. */
+@layer assistant-ui {
+ /* Let user and assistant message bubbles span the full sidebar column
+ * (default clips them to 80% of an already-narrow column). */
+ .ask-fabro-sidebar .aui-user-message-content,
+ .ask-fabro-sidebar .aui-assistant-message-content {
+ max-width: 100%;
+ font-size: 0.875rem;
+ line-height: 1.5rem;
+ }
+ /* Shrink the right-justified spacer on user messages so short replies do
+ * not crowd the right edge unnecessarily. */
+ .ask-fabro-sidebar .aui-user-message-root {
+ grid-template-columns: minmax(24px, 1fr) auto;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ }
+ .ask-fabro-sidebar .aui-assistant-message-root {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ }
+ /* min-width: 0 on the grid track + min-width: 0 / overflow on the content
+ * so wide children (long lines in code blocks, tables) honour the column
+ * width and let their own overflow-x scroll instead of stretching the
+ * sidebar. Same fix on user messages for symmetry. */
+ .ask-fabro-sidebar .aui-assistant-message-content,
+ .ask-fabro-sidebar .aui-user-message-content {
+ min-width: 0;
+ overflow-x: hidden;
+ }
+ /* Inset the assistant message content from the column's right edge so the
+ * code block (dark pre box) has visible right margin and doesn't appear to
+ * bleed against the sidebar edge when its content overflows. */
+ .ask-fabro-sidebar .aui-assistant-message-content {
+ padding-right: 1rem;
+ }
+ /* The viewport-footer centers children; stretch instead so the composer
+ * fills the column width (otherwise it shrinks to its intrinsic content).
+ * `margin-top: auto` pushes the footer to the bottom of the flex column so
+ * the composer sits at the bottom in the empty state (without it, the
+ * footer flows after the empty Welcome/Messages area at the top). */
+ .ask-fabro-sidebar .aui-thread-viewport-footer {
+ align-items: stretch;
+ padding-bottom: 0;
+ margin-top: auto;
+ }
+ /* Hide the assistant avatar to reclaim horizontal space — the column is
+ * narrow enough that the 2.5rem avatar + 1rem gutter is too much chrome.
+ * Also hide the welcome-state avatar for the same reason. */
+ .ask-fabro-sidebar .aui-assistant-message-root > .aui-avatar-root,
+ .ask-fabro-sidebar .aui-thread-welcome-root .aui-avatar-root {
+ display: none;
+ }
+ .ask-fabro-sidebar .aui-assistant-message-root {
+ grid-template-columns: minmax(0, 1fr);
+ }
+ .ask-fabro-sidebar .aui-assistant-message-content {
+ grid-column: 1;
+ }
+ /* Hide the per-message Copy / Refresh action bar to reclaim vertical space. */
+ .ask-fabro-sidebar .aui-assistant-action-bar-root,
+ .ask-fabro-sidebar .aui-user-action-bar-root {
+ display: none;
+ }
+ /* Welcome message also wants smaller text. */
+ .ask-fabro-sidebar .aui-thread-welcome-message {
+ font-size: 0.875rem;
+ line-height: 1.5rem;
+ }
+ /* Drop the solid navy background that assistant-ui paints on the thread
+ * root + viewport; the sidebar already supplies its own translucent panel
+ * background and the painted navy created a visible block around the
+ * composer at the bottom of the column. */
+ .ask-fabro-sidebar .aui-thread-root,
+ .ask-fabro-sidebar .aui-thread-viewport,
+ .ask-fabro-sidebar .aui-thread-viewport-footer {
+ background-color: transparent;
+ }
+ /* Code blocks: the is already constrained to the column via the
+ * parent's overflow-x: hidden, but Chromium drops the pre's padding-right
+ * when content overflows, so long code lines visually bleed up to the
+ * pre's right edge. Promoting to inline-block lets its own
+ * padding-right become part of the scrollable content. */
+ .ask-fabro-sidebar .aui-md-pre {
+ max-width: 100%;
+ }
+ .ask-fabro-sidebar .aui-md-pre > code {
+ display: inline-block;
+ min-width: 100%;
+ padding-right: 1rem;
+ }
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/lib/ask-fabro-context.tsx b/docs/superpowers/prototypes/2026-05-16-chats-new/src/lib/ask-fabro-context.tsx
new file mode 100644
index 000000000..14cd7f2b9
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/lib/ask-fabro-context.tsx
@@ -0,0 +1,40 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+ useState,
+ type ReactNode,
+} from "react";
+
+/**
+ * App-level state for the Ask-Fabro right sidebar. Lifted out of any single
+ * route so the sidebar can be rendered at the App layout level and span the
+ * full window height (above the top nav and main content).
+ */
+type AskFabroContextValue = {
+ isOpen: boolean;
+ open: () => void;
+ close: () => void;
+ toggle: () => void;
+};
+
+const AskFabroContext = createContext(null);
+
+export function AskFabroProvider({ children }: { children: ReactNode }) {
+ const [isOpen, setIsOpen] = useState(false);
+ const open = useCallback(() => setIsOpen(true), []);
+ const close = useCallback(() => setIsOpen(false), []);
+ const toggle = useCallback(() => setIsOpen((v) => !v), []);
+ const value = useMemo(
+ () => ({ isOpen, open, close, toggle }),
+ [isOpen, open, close, toggle],
+ );
+ return {children} ;
+}
+
+export function useAskFabro(): AskFabroContextValue {
+ const value = useContext(AskFabroContext);
+ if (!value) throw new Error("useAskFabro must be used inside ");
+ return value;
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/lib/chats-runtime.ts b/docs/superpowers/prototypes/2026-05-16-chats-new/src/lib/chats-runtime.ts
new file mode 100644
index 000000000..34b363b02
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/lib/chats-runtime.ts
@@ -0,0 +1,146 @@
+import type {
+ ChatModelAdapter,
+ ChatModelRunResult,
+ ThreadAssistantMessagePart,
+ ThreadMessageLike,
+} from "@assistant-ui/react";
+
+import type {
+ Chat,
+ CompletionContentPart,
+ CompletionMessage,
+} from "./types";
+import { pickReply } from "./chats-script";
+
+const STREAM_CHUNK_CHARS = 28;
+const STREAM_CHUNK_INTERVAL_MS = 55;
+
+function sleep(ms: number, signal: AbortSignal): Promise {
+ return new Promise((resolve, reject) => {
+ if (signal.aborted) {
+ reject(new DOMException("Aborted", "AbortError"));
+ return;
+ }
+ const handle = setTimeout(resolve, ms);
+ signal.addEventListener(
+ "abort",
+ () => {
+ clearTimeout(handle);
+ reject(new DOMException("Aborted", "AbortError"));
+ },
+ { once: true },
+ );
+ });
+}
+
+function toAssistantParts(
+ content: CompletionContentPart[],
+): ThreadAssistantMessagePart[] {
+ const out: ThreadAssistantMessagePart[] = [];
+ for (const part of content) {
+ if (part.kind === "text") {
+ out.push({ type: "text", text: part.data.text });
+ } else if (part.kind === "tool_call") {
+ out.push({
+ type: "tool-call",
+ toolCallId: part.data.tool_call_id,
+ toolName: part.data.name,
+ args: part.data.arguments,
+ argsText: JSON.stringify(part.data.arguments),
+ });
+ } else if (part.kind === "tool_result") {
+ // Attach the result to the matching tool-call part already emitted.
+ const target = out
+ .slice()
+ .reverse()
+ .find(
+ (p): p is Extract =>
+ p.type === "tool-call" && p.toolCallId === part.data.tool_call_id,
+ );
+ if (target) {
+ const idx = out.lastIndexOf(target);
+ out[idx] = { ...target, result: part.data.content };
+ }
+ }
+ }
+ return out;
+}
+
+/**
+ * Build an assistant-ui adapter that streams scripted replies from the bank,
+ * advancing the chat's scriptIndex on completion via onReplyComplete.
+ */
+export function createScriptedAdapter(args: {
+ getChat: () => Chat | undefined;
+ onReplyComplete: (reply: CompletionMessage) => void;
+}): ChatModelAdapter {
+ return {
+ async *run({ abortSignal }) {
+ const chat = args.getChat();
+ const reply = pickReply(chat?.scriptIndex ?? 0);
+
+ // Stream piece-by-piece. Text content streams in chunks; tool-call
+ // and tool-result parts emit as single steps for visibility.
+ const accumulated: CompletionContentPart[] = [];
+
+ for (const part of reply.content) {
+ if (part.kind === "text") {
+ const text = part.data.text;
+ let cursor = 0;
+ accumulated.push({ kind: "text", data: { text: "" } });
+ const accIndex = accumulated.length - 1;
+ while (cursor < text.length) {
+ cursor = Math.min(cursor + STREAM_CHUNK_CHARS, text.length);
+ accumulated[accIndex] = {
+ kind: "text",
+ data: { text: text.slice(0, cursor) },
+ };
+ yield buildUpdate(accumulated);
+ if (cursor < text.length) {
+ await sleep(STREAM_CHUNK_INTERVAL_MS, abortSignal);
+ }
+ }
+ } else {
+ accumulated.push(part);
+ yield buildUpdate(accumulated);
+ await sleep(STREAM_CHUNK_INTERVAL_MS * 3, abortSignal);
+ }
+ }
+
+ // Persist the full reply to the store. The store advances scriptIndex.
+ args.onReplyComplete(reply);
+ },
+ };
+}
+
+function buildUpdate(parts: CompletionContentPart[]): ChatModelRunResult {
+ return { content: toAssistantParts(parts) };
+}
+
+/**
+ * Convert stored CompletionMessage[] to assistant-ui's ThreadMessageLike[] for
+ * use as initialMessages when remounting a runtime.
+ */
+export function toThreadMessages(
+ messages: CompletionMessage[],
+): ThreadMessageLike[] {
+ return messages.map((msg) => {
+ if (msg.role === "user") {
+ return {
+ role: "user",
+ content: msg.content
+ .filter((p): p is Extract =>
+ p.kind === "text",
+ )
+ .map((p) => ({ type: "text", text: p.data.text }) as const),
+ };
+ }
+ if (msg.role === "assistant") {
+ return {
+ role: "assistant",
+ content: toAssistantParts(msg.content),
+ };
+ }
+ return { role: "system", content: [] };
+ });
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/lib/chats-script.ts b/docs/superpowers/prototypes/2026-05-16-chats-new/src/lib/chats-script.ts
new file mode 100644
index 000000000..f46a493f0
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/lib/chats-script.ts
@@ -0,0 +1,182 @@
+import type { CompletionMessage } from "./types";
+
+/**
+ * Scripted assistant replies cycled through per chat. Generic content,
+ * intentionally not Fabro-specific. Each entry is a single assistant
+ * CompletionMessage; tool calls and their results are siblings in the
+ * content array so the renderer can pair them.
+ */
+export const SCRIPTED_REPLIES: CompletionMessage[] = [
+ {
+ role: "assistant",
+ content: [
+ {
+ kind: "text",
+ data: {
+ text:
+ "Hi! I'm a scripted prototype reply. A few things I can show off:\n\n" +
+ "- Markdown rendering (lists, **bold**, *italics*, `code`)\n" +
+ "- Streaming text appearing incrementally\n" +
+ "- Tool calls with arguments and results\n" +
+ "- Multi-paragraph responses with code blocks\n\n" +
+ "Send another message to see the next response in the bank.",
+ },
+ },
+ ],
+ },
+ {
+ role: "assistant",
+ content: [
+ {
+ kind: "text",
+ data: {
+ text:
+ "Here's a TypeScript snippet that debounces a function:\n\n" +
+ "```ts\n" +
+ "export function debounce void>(\n" +
+ " fn: T,\n" +
+ " ms: number,\n" +
+ "): (...args: Parameters) => void {\n" +
+ " let handle: ReturnType | undefined;\n" +
+ " return (...args) => {\n" +
+ " if (handle) clearTimeout(handle);\n" +
+ " handle = setTimeout(() => fn(...args), ms);\n" +
+ " };\n" +
+ "}\n" +
+ "```\n\n" +
+ "The trailing-edge variant is the most common; a leading-edge variant fires immediately then suppresses subsequent calls.",
+ },
+ },
+ ],
+ },
+ {
+ role: "assistant",
+ content: [
+ {
+ kind: "text",
+ data: {
+ text: "Let me search for that real quick.",
+ },
+ },
+ {
+ kind: "tool_call",
+ data: {
+ tool_call_id: "call_search_1",
+ name: "search_web",
+ arguments: {
+ query: "current best practices for rate limiting an HTTP API",
+ max_results: 5,
+ },
+ },
+ },
+ {
+ kind: "tool_result",
+ data: {
+ tool_call_id: "call_search_1",
+ content: {
+ results: [
+ {
+ title: "Token bucket vs leaky bucket",
+ url: "https://example.com/rate-limit-algorithms",
+ snippet:
+ "Token bucket allows bursts, leaky bucket smooths traffic.",
+ },
+ {
+ title: "Distributed rate limiting with Redis",
+ url: "https://example.com/redis-rate-limit",
+ snippet:
+ "INCR + EXPIRE is the simplest fixed-window approach.",
+ },
+ ],
+ },
+ },
+ },
+ {
+ kind: "text",
+ data: {
+ text:
+ "\n\nTwo solid starting points. For most APIs, a Redis-backed sliding window keyed by API key gives you per-tenant fairness without a lot of moving parts. For burst tolerance, a token-bucket per route is a nice layer on top.",
+ },
+ },
+ ],
+ },
+ {
+ role: "assistant",
+ content: [
+ {
+ kind: "text",
+ data: {
+ text:
+ "## The 4-fold path of refactoring a hook\n\n" +
+ "When a React hook starts feeling tangled, work the corners in order:\n\n" +
+ "### 1. Extract pure computation\n" +
+ "Anything that is a function of inputs (no side effects, no state) leaves the hook entirely.\n\n" +
+ "### 2. Collapse derived state into `useMemo`\n" +
+ "State that is computable from other state shouldn't be its own state.\n\n" +
+ "### 3. Split orthogonal concerns into sibling hooks\n" +
+ "If two effects don't share dependencies, they don't belong in the same hook.\n\n" +
+ "### 4. Promote to a reducer\n" +
+ "Once there are 3+ related `useState` calls coordinating updates, `useReducer` makes the state machine explicit.\n\n" +
+ "> The honest test: can you write a one-sentence description of what the hook is responsible for? If not, it's doing too much.",
+ },
+ },
+ ],
+ },
+ {
+ role: "assistant",
+ content: [
+ {
+ kind: "text",
+ data: {
+ text: "I'll compute that for you.",
+ },
+ },
+ {
+ kind: "tool_call",
+ data: {
+ tool_call_id: "call_calc_1",
+ name: "run_calculation",
+ arguments: {
+ expression: "compound_interest(principal=10000, rate=0.05, years=10)",
+ },
+ },
+ },
+ {
+ kind: "tool_result",
+ data: {
+ tool_call_id: "call_calc_1",
+ content: {
+ value: 16288.95,
+ currency: "USD",
+ note: "Annual compounding; rounded to cents.",
+ },
+ },
+ },
+ {
+ kind: "text",
+ data: {
+ text:
+ "\n\n**$16,288.95** after 10 years. Bumping the rate to 7% would put you at roughly $19,672, and continuous compounding at 5% lands at $16,487 — so the extra two points of rate matters more than the compounding cadence.",
+ },
+ },
+ ],
+ },
+ {
+ role: "assistant",
+ content: [
+ {
+ kind: "text",
+ data: {
+ text:
+ "Good question. The short answer: it depends on whether you need transactions across multiple writes.\n\n" +
+ "If you do — Postgres. If everything you do is single-row, SQLite is faster, simpler to operate, and easier to back up. A surprising amount of production traffic can live happily on SQLite if you accept its one-writer-at-a-time constraint.\n\n" +
+ "Next step: tell me about your read/write ratio and I can be more specific.",
+ },
+ },
+ ],
+ },
+];
+
+export function pickReply(scriptIndex: number): CompletionMessage {
+ return SCRIPTED_REPLIES[scriptIndex % SCRIPTED_REPLIES.length]!;
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/lib/chats-store.tsx b/docs/superpowers/prototypes/2026-05-16-chats-new/src/lib/chats-store.tsx
new file mode 100644
index 000000000..46b722f55
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/lib/chats-store.tsx
@@ -0,0 +1,172 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+ useReducer,
+ useRef,
+ type ReactNode,
+} from "react";
+
+import type { Chat } from "./types";
+import { pickReply } from "./chats-script";
+
+type State = {
+ chats: Record;
+ order: string[]; // newest first
+};
+
+type Action =
+ | {
+ type: "create";
+ id: string;
+ title: string;
+ createdAt: number;
+ }
+ | { type: "advance_script"; chatId: string };
+
+function deriveTitle(text: string): string {
+ const trimmed = text.trim().replace(/\s+/g, " ");
+ if (trimmed.length <= 40) return trimmed || "New chat";
+ const cut = trimmed.slice(0, 40);
+ const lastSpace = cut.lastIndexOf(" ");
+ const base = lastSpace > 20 ? cut.slice(0, lastSpace) : cut;
+ return `${base}…`;
+}
+
+function seedChat(
+ id: string,
+ title: string,
+ ageDays: number,
+ scriptIndex: number,
+ userText: string,
+): Chat {
+ return {
+ id,
+ title,
+ createdAt: Date.now() - ageDays * 86_400_000,
+ scriptIndex: scriptIndex + 1, // seeded reply already "consumed"
+ seedMessages: [
+ {
+ role: "user",
+ content: [{ kind: "text", data: { text: userText } }],
+ },
+ pickReply(scriptIndex),
+ ],
+ };
+}
+
+const initialState: State = (() => {
+ const seeds: Chat[] = [
+ seedChat(
+ "seed_email",
+ "Draft a launch email",
+ 0.5,
+ 0,
+ "Help me draft a launch announcement email for our new analytics dashboard.",
+ ),
+ seedChat(
+ "seed_hook",
+ "Refactor a React hook",
+ 2,
+ 3,
+ "My useChat hook has grown to 200 lines and I keep tangling concerns. How should I think about refactoring it?",
+ ),
+ seedChat(
+ "seed_db",
+ "Compare Postgres vs SQLite",
+ 6,
+ 5,
+ "For a side project with ~50 daily users, should I reach for Postgres or stick with SQLite?",
+ ),
+ ];
+ const chats: Record = {};
+ for (const s of seeds) chats[s.id] = s;
+ return { chats, order: seeds.map((s) => s.id) };
+})();
+
+function reducer(state: State, action: Action): State {
+ switch (action.type) {
+ case "create": {
+ const chat: Chat = {
+ id: action.id,
+ title: action.title,
+ createdAt: action.createdAt,
+ scriptIndex: 0,
+ };
+ return {
+ chats: { ...state.chats, [action.id]: chat },
+ order: [action.id, ...state.order],
+ };
+ }
+ case "advance_script": {
+ const existing = state.chats[action.chatId];
+ if (!existing) return state;
+ return {
+ ...state,
+ chats: {
+ ...state.chats,
+ [action.chatId]: {
+ ...existing,
+ scriptIndex: existing.scriptIndex + 1,
+ },
+ },
+ };
+ }
+ }
+}
+
+type ChatsContextValue = {
+ state: State;
+ /** Create a chat with a derived title and return its id. */
+ createChat: (firstUserText: string) => string;
+ /** Read current scriptIndex without subscribing. */
+ peekScriptIndex: (chatId: string) => number;
+ /** Bump scriptIndex after a scripted reply completes. */
+ advanceScriptIndex: (chatId: string) => void;
+};
+
+const ChatsContext = createContext(null);
+
+function shortId(): string {
+ return `c_${Math.random().toString(36).slice(2, 8)}`;
+}
+
+export function ChatsProvider({ children }: { children: ReactNode }) {
+ const [state, dispatch] = useReducer(reducer, initialState);
+ const stateRef = useRef(state);
+ stateRef.current = state;
+
+ const createChat = useCallback((firstUserText: string) => {
+ const id = shortId();
+ dispatch({
+ type: "create",
+ id,
+ title: deriveTitle(firstUserText),
+ createdAt: Date.now(),
+ });
+ return id;
+ }, []);
+
+ const peekScriptIndex = useCallback(
+ (chatId: string) => stateRef.current.chats[chatId]?.scriptIndex ?? 0,
+ [],
+ );
+
+ const advanceScriptIndex = useCallback((chatId: string) => {
+ dispatch({ type: "advance_script", chatId });
+ }, []);
+
+ const value = useMemo(
+ () => ({ state, createChat, peekScriptIndex, advanceScriptIndex }),
+ [state, createChat, peekScriptIndex, advanceScriptIndex],
+ );
+
+ return {children} ;
+}
+
+export function useChatsStore(): ChatsContextValue {
+ const value = useContext(ChatsContext);
+ if (!value) throw new Error("useChatsStore must be used inside ");
+ return value;
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/lib/types.ts b/docs/superpowers/prototypes/2026-05-16-chats-new/src/lib/types.ts
new file mode 100644
index 000000000..711102452
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/lib/types.ts
@@ -0,0 +1,58 @@
+/**
+ * Local mirror of the relevant subset of @qltysh/fabro-api-client types.
+ * In the real fabro-web integration we import these directly from the client.
+ */
+
+export type CompletionRole =
+ | "system"
+ | "user"
+ | "assistant"
+ | "tool"
+ | "developer";
+
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+export type CompletionContentPart =
+ | { kind: "text"; data: { text: string } }
+ | {
+ kind: "tool_call";
+ data: {
+ tool_call_id: string;
+ name: string;
+ arguments: { [key: string]: JsonValue };
+ };
+ }
+ | {
+ kind: "tool_result";
+ data: {
+ tool_call_id: string;
+ content: JsonValue;
+ is_error?: boolean;
+ };
+ };
+
+export type CompletionMessage = {
+ role: CompletionRole;
+ content: CompletionContentPart[];
+ name?: string;
+ tool_call_id?: string;
+};
+
+/**
+ * Sidebar/store wrapper around a single chat thread. Messages themselves live
+ * in assistant-ui's runtime; the store holds only the metadata needed to render
+ * the sidebar and drive the scripted reply bank.
+ */
+export type Chat = {
+ id: string;
+ title: string;
+ createdAt: number;
+ scriptIndex: number;
+ seedMessages?: CompletionMessage[];
+};
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/main.tsx b/docs/superpowers/prototypes/2026-05-16-chats-new/src/main.tsx
new file mode 100644
index 000000000..41904fa91
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/main.tsx
@@ -0,0 +1,33 @@
+import { createRoot } from "react-dom/client";
+import { createBrowserRouter, Navigate, RouterProvider } from "react-router";
+
+import "./index.css";
+
+import App from "./app";
+import ChatsLayout from "./routes/chats-layout";
+import ChatsNew from "./routes/chats-new";
+import ChatsDetail from "./routes/chats-detail";
+import Sample from "./routes/sample";
+
+const router = createBrowserRouter([
+ {
+ path: "/",
+ Component: App,
+ children: [
+ { index: true, Component: () => },
+ {
+ path: "chats",
+ Component: ChatsLayout,
+ children: [
+ { path: "new", Component: ChatsNew },
+ { path: ":chatId", Component: ChatsDetail },
+ ],
+ },
+ { path: "sample", Component: Sample },
+ ],
+ },
+]);
+
+createRoot(document.getElementById("root")!).render(
+ ,
+);
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/routes/chats-detail.tsx b/docs/superpowers/prototypes/2026-05-16-chats-new/src/routes/chats-detail.tsx
new file mode 100644
index 000000000..839559a3b
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/routes/chats-detail.tsx
@@ -0,0 +1,94 @@
+import { useEffect, useMemo, useRef } from "react";
+import { useLocation, useNavigate, useParams } from "react-router";
+import {
+ AssistantRuntimeProvider,
+ useLocalRuntime,
+} from "@assistant-ui/react";
+import { Thread, makeMarkdownText } from "@assistant-ui/react-ui";
+
+import { useChatsStore } from "../lib/chats-store";
+import {
+ createScriptedAdapter,
+ toThreadMessages,
+} from "../lib/chats-runtime";
+import CustomComposer from "../components/custom-composer";
+import ToolFallback from "../components/tool-fallback";
+import type { CompletionMessage } from "../lib/types";
+
+const MarkdownText = makeMarkdownText();
+
+export default function ChatsDetail() {
+ const { chatId } = useParams<{ chatId: string }>();
+ const navigate = useNavigate();
+ const { state } = useChatsStore();
+ const chat = chatId ? state.chats[chatId] : undefined;
+
+ if (!chatId || !chat) {
+ return (
+
+
+ That chat doesn't exist.
+
+
+
+ );
+ }
+
+ return ;
+}
+
+function ChatRuntime({ chatId }: { chatId: string }) {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const { state, peekScriptIndex, advanceScriptIndex } = useChatsStore();
+ const chat = state.chats[chatId]!;
+ const pendingText =
+ (location.state as { pendingText?: string } | null)?.pendingText ?? null;
+
+ const initialMessages = useMemo(
+ () => toThreadMessages(chat.seedMessages ?? []),
+ [chat.seedMessages],
+ );
+
+ const adapter = useMemo(
+ () =>
+ createScriptedAdapter({
+ getChat: () => ({
+ ...chat,
+ scriptIndex: peekScriptIndex(chatId),
+ }),
+ onReplyComplete: (_reply: CompletionMessage) => advanceScriptIndex(chatId),
+ }),
+ [chat, chatId, peekScriptIndex, advanceScriptIndex],
+ );
+
+ const runtime = useLocalRuntime(adapter, { initialMessages });
+
+ const didSendPendingRef = useRef(false);
+ useEffect(() => {
+ if (!pendingText || didSendPendingRef.current) return;
+ didSendPendingRef.current = true;
+ // Clear router state so a refresh or back-nav doesn't resend.
+ navigate(`/chats/${chatId}`, { replace: true, state: null });
+ runtime.thread.append(pendingText);
+ }, [pendingText, chatId, navigate, runtime]);
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/routes/chats-layout.tsx b/docs/superpowers/prototypes/2026-05-16-chats-new/src/routes/chats-layout.tsx
new file mode 100644
index 000000000..3f76a8028
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/routes/chats-layout.tsx
@@ -0,0 +1,58 @@
+import { NavLink, Outlet, useNavigate } from "react-router";
+import { PencilSquareIcon } from "@heroicons/react/24/outline";
+
+import { ChatsProvider, useChatsStore } from "../lib/chats-store";
+
+export default function ChatsLayout() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+function Sidebar() {
+ const { state } = useChatsStore();
+ const navigate = useNavigate();
+ return (
+
+ );
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/routes/chats-new.tsx b/docs/superpowers/prototypes/2026-05-16-chats-new/src/routes/chats-new.tsx
new file mode 100644
index 000000000..7847c2456
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/routes/chats-new.tsx
@@ -0,0 +1,65 @@
+import { useRef, useState, type FormEvent } from "react";
+import { useNavigate } from "react-router";
+import { ArrowUpIcon } from "@heroicons/react/24/solid";
+
+import { useChatsStore } from "../lib/chats-store";
+import ComposerChips from "../components/composer-chips";
+
+export default function ChatsNew() {
+ const navigate = useNavigate();
+ const { createChat } = useChatsStore();
+ const [text, setText] = useState("");
+ const textareaRef = useRef(null);
+
+ function submit(value: string) {
+ const trimmed = value.trim();
+ if (!trimmed) return;
+ const id = createChat(trimmed);
+ navigate(`/chats/${id}`, { state: { pendingText: trimmed } });
+ }
+
+ function onSubmit(e: FormEvent) {
+ e.preventDefault();
+ submit(text);
+ }
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/src/routes/sample.tsx b/docs/superpowers/prototypes/2026-05-16-chats-new/src/routes/sample.tsx
new file mode 100644
index 000000000..983e2384a
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/src/routes/sample.tsx
@@ -0,0 +1,119 @@
+import { SparklesIcon } from "@heroicons/react/24/solid";
+import {
+ ArrowTopRightOnSquareIcon,
+ ChartBarIcon,
+ CheckCircleIcon,
+ ClockIcon,
+} from "@heroicons/react/24/outline";
+
+import { useAskFabro } from "../lib/ask-fabro-context";
+
+/**
+ * Demo workspace page. Placeholder content; its only purpose is to host the
+ * "Ask Fabro" trigger button. The sidebar itself lives at the App layout level
+ * (so it spans full window height).
+ */
+export default function Sample() {
+ const { isOpen, open } = useAskFabro();
+
+ return (
+
+
+
+ Runs
+ fabro-web · main
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Recent runs
+
+
+ {RUN_ROWS.map((row, i) => (
+ -
+
+ {row.id}
+ {row.title}
+
+ {row.duration}
+
+
+
+ ))}
+
+
+
+
+ );
+}
+
+function StatCard({
+ Icon,
+ label,
+ value,
+ accent,
+}: {
+ Icon: React.ComponentType<{ className?: string }>;
+ label: string;
+ value: string;
+ accent: string;
+}) {
+ return (
+
+
+
+ {label}
+
+
+ {value}
+
+
+ );
+}
+
+const RUN_ROWS = [
+ { id: "r_8f2a", title: "Update copy on the landing page", duration: "1m 42s", dot: "bg-mint" },
+ { id: "r_8f29", title: "Refactor the useChat hook", duration: "3m 08s", dot: "bg-mint" },
+ { id: "r_8f28", title: "Investigate flaky test in fabro-server", duration: "2m 51s", dot: "bg-amber" },
+ { id: "r_8f27", title: "Bump assistant-ui to latest", duration: "0m 47s", dot: "bg-mint" },
+ { id: "r_8f26", title: "Generate release notes for v0.42", duration: "1m 19s", dot: "bg-mint" },
+ { id: "r_8f25", title: "Audit unused dependencies", duration: "5m 33s", dot: "bg-coral" },
+];
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/tsconfig.json b/docs/superpowers/prototypes/2026-05-16-chats-new/tsconfig.json
new file mode 100644
index 000000000..bb3174dc0
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["src"]
+}
diff --git a/docs/superpowers/prototypes/2026-05-16-chats-new/vite.config.ts b/docs/superpowers/prototypes/2026-05-16-chats-new/vite.config.ts
new file mode 100644
index 000000000..252faff04
--- /dev/null
+++ b/docs/superpowers/prototypes/2026-05-16-chats-new/vite.config.ts
@@ -0,0 +1,11 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import tailwindcss from "@tailwindcss/vite";
+
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+ server: {
+ port: 5173,
+ host: "127.0.0.1",
+ },
+});
diff --git a/docs/superpowers/specs/2026-05-16-chats-new-prototype-design.md b/docs/superpowers/specs/2026-05-16-chats-new-prototype-design.md
new file mode 100644
index 000000000..58abaed76
--- /dev/null
+++ b/docs/superpowers/specs/2026-05-16-chats-new-prototype-design.md
@@ -0,0 +1,319 @@
+# /chats/new — assistant-ui prototype
+
+Status: design approved, runnable prototype built, ready for implementation plan.
+Owner: fabro-web.
+Scope: client-side prototype only. No backend, no persistence.
+
+Reference implementation: `../prototypes/2026-05-16-chats-new/` (runnable Vite
+app — `bun install && bun run dev`). Includes `/chats/new`, `/chats/:id`, and
+`/sample` with a right-side Ask-Fabro assistant.
+
+## Goal
+
+Add a new route `/chats/new` (and `/chats/:id`) to `apps/fabro-web` that delivers a
+ChatGPT/Claude-style chat experience inside the existing Fabro AppShell, built on
+[assistant-ui](https://www.assistant-ui.com/). The page is positioned as the
+eventual replacement for `/start` as Fabro's "kick off agent work" entry point,
+but in this phase it is a working mockup with scripted, generic replies — not
+wired to any LLM or to Fabro's run engine.
+
+## Non-goals
+
+- No backend integration, no real LLM, no real tool execution.
+- No persistence — refreshing the page wipes all chats. No `localStorage`, no
+ cookie, no server call.
+- No attachments, voice, message editing, branching, regenerate.
+- No removal of `/start` in this phase; it stays alongside `/chats/new`. Removal
+ happens later, when `/chats/new` is wired to the run engine.
+- No tool-call wiring to Fabro's actual tools. The prototype renders generic
+ scripted tool-call cards purely for visual demonstration.
+
+## Route and layout
+
+`/chats/new` and `/chats/:id` mount **inside** Fabro's existing `AppShell`,
+sibling to `/runs`, `/automations`, etc. The Fabro top nav stays visible.
+
+Routes use AppShell's existing route handles for both `fullHeight: true` (so
+the chat surface fills the viewport below the top nav) and `wide: true` (so
+the default `max-w-5xl` content constraint is skipped). Both are already
+supported by `app/layouts/app-shell.tsx`.
+
+Below the top nav the chat page is a two-column layout:
+
+- **Left chat sidebar** (~260px): "New chat" button at top, then a list of past
+ chats (3 hardcoded seeds + any created in-session). No duplicate user menu —
+ Fabro's top nav already has it.
+- **Main pane**: empty state (centered composer, no message list) on
+ `/chats/new`, active conversation on `/chats/:id`.
+
+### Files
+
+- `app/routes/chats.tsx` — layout component for the two-column shell, mounts the
+ in-memory chat store via React Context, renders ` ` for the main pane.
+- `app/routes/chats-new.tsx` — empty-state landing.
+- `app/routes/chats-detail.tsx` — active conversation view.
+- `app/router.tsx` — wires the new nested route group inside the AppShell tree.
+- `app/lib/chats-store.tsx` — Context + reducer, in-memory only.
+- `app/lib/chats-script.ts` — the scripted reply bank (see Section: Scripted
+ replies).
+- `app/lib/chats-runtime.ts` — `ChatModelAdapter` factory that consumes the
+ store and produces streaming replies for assistant-ui; also exports
+ `toThreadMessages()` for converting `CompletionMessage[]` to
+ assistant-ui's `ThreadMessageLike[]` for `initialMessages`.
+- `app/components/composer-chips.tsx` — the three decorative Listbox chips
+ (project, branch, model).
+- `app/components/custom-composer.tsx` — wraps `ComposerPrimitive.Root` with the
+ textarea, chip row, and send / cancel buttons; passed to `` via
+ `components={{ Composer }}`.
+- `app/components/tool-fallback.tsx` — required `ToolFallback` renderer for tool
+ calls (header with tool name, arguments JSON, result JSON). Without this,
+ Thread renders nothing for tool calls.
+- `app/app.css` — adds the cascade-layer declaration and CSS-variable theme
+ overrides on `.fabro-chat` (see Visual styling).
+
+## Visual styling
+
+Use assistant-ui's pre-styled components (shadcn-flavored) and override its CSS
+variables to align with Fabro's existing palette.
+
+- Install `@assistant-ui/react`, `@assistant-ui/react-ui`, and
+ `@assistant-ui/react-markdown`.
+- Use `` from `@assistant-ui/react-ui` as the message list + composer
+ surface. Customize the composer footer via assistant-ui's slot props to inject
+ the Fabro chips described below.
+- Add a small block to `apps/fabro-web/app/app.css` that maps assistant-ui's CSS
+ variables — they are prefixed `--aui-*` (e.g. `--aui-background`,
+ `--aui-foreground`, `--aui-primary`, `--aui-accent`, `--aui-border`,
+ `--aui-muted`, `--aui-ring`) — to Fabro tokens (page bg, fg, teal, line, etc.).
+ Values are HSL component triples (`220 12% 6%`), wrapped at consumption time
+ with `hsl(var(--aui-*))`.
+- Scope the override to a `.fabro-chat` wrapper around the chat layout so the
+ shadcn theme cannot bleed into the rest of the app.
+- Opt into markdown rendering by passing
+ `assistantMessage={{ components: { Text: makeMarkdownText(), ToolFallback } }}`
+ to ``. Without `Text: makeMarkdownText()` the renderer outputs plain
+ `` text and ignores all formatting. Without `ToolFallback` the renderer
+ outputs **nothing** for tool calls (silent failure).
+
+### Tailwind v4 + cascade-layer ordering — required
+
+This is a real Tailwind v3 vs v4 interaction and is **not optional**.
+Prototype verification confirmed the behavior and the fix below.
+
+- `@assistant-ui/react-ui/styles/index.css` is authored against Tailwind v3 and
+ ships **unlayered**. It scopes its preflight reset to `:where(.aui-root)`
+ descendants (e.g. `:where(.aui-root) h1, h2, h3, h4, h5, h6 { font-size:
+ inherit; font-weight: inherit }`).
+- Tailwind v4 places all utility classes inside `@layer utilities`.
+- CSS cascade-layer rule: **unlayered CSS wins over any `@layer` regardless of
+ selector specificity.** So inside `.aui-root`, assistant-ui's scoped reset
+ beats Tailwind v4's `text-5xl` / `font-semibold` even though specificity would
+ predict the utility wins. The symptom is silent — h1/h2/h3 inside Thread (or
+ inside any `.aui-root` wrapper) render at base font-size / weight, with no
+ console warning.
+- **Fix:** wrap assistant-ui's CSS imports in a named cascade layer that is
+ declared before `utilities`:
+
+ ```css
+ @layer theme, base, assistant-ui, components, utilities;
+
+ @import "tailwindcss";
+ @import "@assistant-ui/react-ui/styles/index.css" layer(assistant-ui);
+ @import "@assistant-ui/react-ui/styles/markdown.css" layer(assistant-ui);
+ ```
+
+ With this in place, Tailwind v4 utilities cascade above assistant-ui's scoped
+ resets and any custom Thread children (custom message renderers, ToolFallback
+ variants, embedded forms) can use the full Tailwind utility surface safely.
+
+- **Verification:** add a temporary `
`
+ inside a `.aui-root` wrapper and assert computed `font-size: 48px` and
+ `font-weight: 600`. Remove once confirmed. (Prototype script confirmed
+ `{"fontSize":"48px","fontWeight":"600","parentClass":true}`.)
+
+### `.aui-root` scoping
+
+`` from `@assistant-ui/react-ui` already wraps itself in
+``. Do not add an outer `.aui-root`
+wrapper around the chat shell — it adds the scoped preflight to the empty
+state, sidebar, and any other non-Thread content for no benefit. The chat shell
+lives outside `.aui-root`; the Thread brings its own scope.
+
+## Composer chips (decorative)
+
+Below the textarea the composer footer renders three `@headlessui/react`
+`Listbox` buttons styled with Fabro's existing `SECONDARY_BUTTON_CLASS` from
+`app/components/ui.tsx`:
+
+- **Project** — hardcoded list: `fabro-web`, `fabro-workflows`, `fabro-cli`.
+- **Branch** — hardcoded list: `main`, `develop`, `feature/start-page`.
+- **Model** — hardcoded list: `Claude Opus 4.7`, `Claude Sonnet 4.6`, `GPT-5`.
+
+Selections live in component state and update the chip label, but **do not
+affect** the scripted reply. Project/branch values match those already seeded in
+`app/routes/start.tsx` for visual continuity.
+
+## Data model
+
+The prototype reuses the existing TS API client types verbatim. No new message
+types are introduced.
+
+```ts
+import type {
+ CompletionMessage,
+ CompletionContentPart,
+} from "@qltysh/fabro-api-client";
+```
+
+- `CompletionMessage` — `{ role, content, name?, tool_call_id? }`, with `role`
+ enum `system | user | assistant | tool | developer`.
+- `CompletionContentPart` — `{ kind, data }`, where `kind` is `text`,
+ `tool_call`, `tool_result`, `thinking`, `image`, etc.
+
+These match what `fabro-llm`, the OpenAPI spec, and (serialized)
+`fabro_agent::Turn` already use. Scripted replies are written directly as
+arrays of `CompletionMessage`, so the bank is wire-shaped from day one.
+
+### Only new type: a chat-collection wrapper
+
+The codebase does not have a user-facing "conversation in a list" type;
+`Session` already means an auth session or a Rust runtime session. The prototype
+adds one tightly-scoped local type:
+
+```ts
+type Chat = {
+ id: string; // e.g. "c_a1b2"
+ title: string; // derived from first user message
+ createdAt: number;
+ messages: CompletionMessage[];
+ scriptIndex: number; // prototype-only; points into reply bank
+};
+
+type ChatStore = {
+ chats: Record;
+ order: string[]; // sidebar order, newest first
+ createChat(): string; // returns new id
+ appendUserMessage(id: string, text: string): void;
+ appendAssistantReply(id: string, reply: CompletionMessage): void;
+};
+```
+
+Store lives in `app/lib/chats-store.tsx` (Context + `useReducer`). No
+persistence.
+
+### assistant-ui adapter boundary
+
+assistant-ui's runtime uses its own internal `ThreadMessage` shape. Conversion
+between `CompletionMessage[]` and `ThreadMessage[]` happens in exactly one
+place: the `ChatModelAdapter` factory in `app/lib/chats-runtime.ts`. This
+mirrors the OpenAPI/internal-type boundary pattern already established in the
+top-level `CLAUDE.md` ("API DTOs are projections; convert at the boundary").
+
+Streaming chunks stay inside assistant-ui's runtime. The store only sees the
+completed reply, written back as a single `CompletionMessage`. This keeps the
+canonical message model free of simulated-streaming state.
+
+## Scripted replies
+
+`app/lib/chats-script.ts` exports a `CompletionMessage[]` of length ~6, all
+with `role: "assistant"`. Content parts mix `text` (markdown), `tool_call`,
+and matching `tool_result`. The bank is generic, not Fabro-specific:
+
+1. Plain markdown paragraph (greeting + short list).
+2. Markdown with a fenced TypeScript code block.
+3. Reply containing one `tool_call` + `tool_result` pair — generic
+ `search_web` with mock args and a short result.
+4. Long markdown with headings and a blockquote.
+5. Reply containing one `tool_call` + `tool_result` pair — generic
+ `run_calculation`.
+6. Short reply mentioning a "next step".
+
+Each seed chat starts at a staggered `scriptIndex` so the sidebar does not look
+uniform.
+
+### Seed data
+
+Three hardcoded past chats are inserted into the store on first mount, so the
+sidebar is populated on initial page load:
+
+- "Draft a launch email"
+- "Refactor a React hook"
+- "Compare Postgres vs SQLite"
+
+Each has 2-3 messages pre-populated from the reply bank.
+
+### Title derivation
+
+When a new chat's first user message is sent, the chat title is set to the
+first ~40 characters of that message's first `text` content part (trimmed at a
+word boundary if convenient, otherwise hard-truncated with an ellipsis).
+
+## Behavior
+
+1. First visit to `/chats/new` renders the empty state: centered composer, no
+ message list, sidebar shows seed chats.
+2. User types and sends:
+ - Reducer creates a new `Chat`, appends the user `CompletionMessage`,
+ navigates to `/chats/:id`.
+3. The per-chat `ChatModelAdapter` fires:
+ - Reads `scriptIndex` from the store, picks `bank[scriptIndex % bank.length]`.
+ - Simulates streaming by yielding `text` content parts in ~30-char chunks
+ every ~60ms via `setTimeout`. `tool_call` and `tool_result` parts emit as
+ single chunks.
+4. On stream completion the reducer appends the full `CompletionMessage` to
+ the chat and bumps `scriptIndex` by one.
+5. Clicking a chat in the sidebar navigates to `/chats/:id`; the main pane
+ swaps the runtime instance; history is preserved in memory.
+6. Clicking "New chat" navigates back to `/chats/new`; composer state resets.
+
+## Error handling
+
+Minimal — there is no network and no LLM.
+
+- Empty/whitespace-only sends are ignored (assistant-ui composer's built-in
+ behavior).
+- Navigating to `/chats/:id` for an unknown id renders an empty state with a
+ "Start a new chat" link (no crash, no redirect loop).
+- The scripted adapter never throws. If the bank is somehow empty, render a
+ one-line fallback assistant message.
+
+## Testing
+
+Proportionate to a UI prototype, using existing `bun test` and
+`react-test-renderer` patterns in `apps/fabro-web`.
+
+1. `chats-router.test.tsx` — `/chats/new` and `/chats/:id` resolve to the
+ right components inside the AppShell tree.
+2. `chats-store.test.tsx` — reducer: create, append user message, append
+ assistant reply, `scriptIndex` advance and wrap, title derivation from a
+ long input, title derivation from input with leading whitespace.
+3. `chats-script-adapter.test.ts` — given a `Chat`, the adapter returns the
+ next bank entry as a streaming sequence in the correct order and advances
+ `scriptIndex` exactly once per send.
+
+No snapshot tests for assistant-ui's rendered DOM — too volatile, low signal.
+
+## Verification before declaring done
+
+- `cd apps/fabro-web && bun run typecheck` — clean.
+- `cd apps/fabro-web && bun test` — green.
+- `fabro server start` and `cd apps/fabro-web && bun run dev`, then a manual
+ browser walkthrough:
+ - `/chats/new` renders empty state with Fabro top nav visible.
+ - Sending a message navigates to `/chats/:id`, the reply streams in, and a
+ tool-call card renders correctly for the relevant bank entries.
+ - Switching between seed chats in the sidebar preserves message history.
+ - Creating a new chat from the sidebar returns to the empty state.
+ - Composer chips open popovers and update their labels.
+ - Chat surface fills viewport height; AppShell top nav stays put.
+
+## Open items deferred to follow-up work
+
+- Wiring the composer chips to actual project/branch/model state, and to a real
+ agent runner.
+- Replacing scripted replies with a real LLM stream (likely via the existing
+ fabro-api completions endpoint).
+- Deciding how chats persist server-side and how they relate to runs.
+- Removing `/start` once `/chats/new` is fully wired.
+- Tool-call rendering for Fabro's actual tools (file ops, bash, etc.).
+- Attachments, voice, branching, regenerate.
diff --git a/lib/crates/fabro-acp/src/error.rs b/lib/crates/fabro-acp/src/error.rs
index 37ae511e5..9e84978a2 100644
--- a/lib/crates/fabro-acp/src/error.rs
+++ b/lib/crates/fabro-acp/src/error.rs
@@ -1,5 +1,27 @@
+use fabro_types::{CommandTermination, ExecOutputTail};
+
use crate::command::AcpCommandError;
+#[derive(Debug)]
+pub struct AcpProcessExit {
+ pub termination: CommandTermination,
+ pub exit_code: Option,
+ pub exec_output_tail: Option,
+}
+
+impl std::fmt::Display for AcpProcessExit {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let exit_code = self
+ .exit_code
+ .map_or_else(|| "unknown".to_string(), |code| code.to_string());
+ write!(
+ f,
+ "ACP process exited before protocol completed: termination={}, exit_code={exit_code}",
+ self.termination
+ )
+ }
+}
+
#[derive(Debug, thiserror::Error)]
pub enum AcpError {
#[error(transparent)]
@@ -15,7 +37,12 @@ pub enum AcpError {
Cancelled,
#[error("ACP turn timed out")]
- TimedOut { stderr: String },
+ TimedOut {
+ exec_output_tail: Option,
+ },
+
+ #[error("{0}")]
+ ProcessExited(AcpProcessExit),
#[error("ACP prompt stopped with {stop_reason}: {text}")]
StopReason {
@@ -24,6 +51,18 @@ pub enum AcpError {
},
}
+impl AcpError {
+ #[must_use]
+ pub fn exec_output_tail(&self) -> Option {
+ match self {
+ Self::TimedOut { exec_output_tail } => exec_output_tail.clone(),
+ Self::ProcessExited(exit) => exit.exec_output_tail.clone(),
+ Self::Sandbox(source) => source.default_redacted_output_tail(),
+ _ => None,
+ }
+ }
+}
+
impl From for AcpError {
fn from(error: agent_client_protocol::Error) -> Self {
Self::Protocol(error)
diff --git a/lib/crates/fabro-acp/src/lib.rs b/lib/crates/fabro-acp/src/lib.rs
index 41f4d2fbe..74ba22cca 100644
--- a/lib/crates/fabro-acp/src/lib.rs
+++ b/lib/crates/fabro-acp/src/lib.rs
@@ -8,5 +8,5 @@ pub mod test_support;
mod transport;
pub use command::{AcpCommand, AcpCommandError, resolve_acp_command};
-pub use error::AcpError;
+pub use error::{AcpError, AcpProcessExit};
pub use session::{AcpRunRequest, AcpRunResult, render_stop_reason, run_acp_turn};
diff --git a/lib/crates/fabro-acp/src/session.rs b/lib/crates/fabro-acp/src/session.rs
index bdff151d3..e0dd343b5 100644
--- a/lib/crates/fabro-acp/src/session.rs
+++ b/lib/crates/fabro-acp/src/session.rs
@@ -102,7 +102,7 @@ pub async fn run_acp_turn(request: AcpRunRequest) -> Result Result>>,
stderr: Arc>>,
startup_error: Arc>>,
+ process_exit: Arc>>,
}
impl TransportState {
@@ -37,6 +39,7 @@ impl TransportState {
handle: Arc::new(TokioMutex::new(None)),
stderr: Arc::new(TokioMutex::new(None)),
startup_error: Arc::new(TokioMutex::new(None)),
+ process_exit: Arc::new(TokioMutex::new(None)),
}
}
@@ -49,10 +52,22 @@ impl TransportState {
*self.startup_error.lock().await = Some(error);
}
+ async fn set_process_exit(&self, termination: StdioProcessTermination, stderr: &str) {
+ *self.process_exit.lock().await = Some(AcpProcessExit {
+ termination: termination.termination,
+ exit_code: termination.exit_code,
+ exec_output_tail: redacted_stderr_tail(stderr),
+ });
+ }
+
pub(crate) async fn take_startup_error(&self) -> Option {
self.startup_error.lock().await.take()
}
+ pub(crate) async fn take_process_exit(&self) -> Option {
+ self.process_exit.lock().await.take()
+ }
+
pub(crate) async fn terminate(&self) -> SandboxResult<()> {
if let Some(handle) = self.handle.lock().await.as_ref().cloned() {
handle.terminate().await?;
@@ -66,6 +81,11 @@ impl TransportState {
}
String::new()
}
+
+ pub(crate) async fn exec_output_tail(&self) -> Option {
+ let stderr = self.stderr_tail().await;
+ redacted_stderr_tail(&stderr)
+ }
}
pub(crate) struct SandboxAcpTransport {
@@ -156,21 +176,17 @@ impl ConnectTo for SandboxAcpTransport {
return result;
}
}
- Err(process_exited_before_protocol_completed(termination, &stderr))
+ self.state.set_process_exit(termination, &stderr).await;
+ Err(process_exited_before_protocol_completed())
}
}
}
}
-fn process_exited_before_protocol_completed(
- termination: StdioProcessTermination,
- stderr: &str,
-) -> ProtocolError {
- let exit_code = termination
- .exit_code
- .map_or_else(|| "unknown".to_string(), |code| code.to_string());
- internal_error(format!(
- "ACP process exited before protocol completed: termination={}, exit_code={exit_code}, stderr={stderr}",
- termination.termination,
- ))
+fn redacted_stderr_tail(stderr: &str) -> Option {
+ fabro_sandbox::redacted_output_tail("", stderr, DEFAULT_EXEC_OUTPUT_TAIL_BYTES)
+}
+
+fn process_exited_before_protocol_completed() -> ProtocolError {
+ internal_error("ACP process exited before protocol completed")
}
diff --git a/lib/crates/fabro-acp/tests/session.rs b/lib/crates/fabro-acp/tests/session.rs
index 88fa6e297..e537e027b 100644
--- a/lib/crates/fabro-acp/tests/session.rs
+++ b/lib/crates/fabro-acp/tests/session.rs
@@ -370,7 +370,7 @@ async fn timeout_terminates_process_and_returns_timeout() {
}
#[tokio::test]
-async fn malformed_json_returns_protocol_error() {
+async fn malformed_json_returns_diagnostic_without_raw_stderr_in_message() {
let tempdir = tempfile::tempdir().expect("create tempdir");
let err = run_fake_agent(
@@ -382,11 +382,33 @@ async fn malformed_json_returns_protocol_error() {
.await
.expect_err("malformed JSON should error");
- assert!(matches!(err, AcpError::Protocol(_)));
+ match err {
+ AcpError::Protocol(error) => {
+ let message = error.to_string();
+ assert!(
+ !message.contains("malformed json"),
+ "protocol error display should not include raw stderr: {message}"
+ );
+ }
+ AcpError::ProcessExited(exit) => {
+ let message = exit.to_string();
+ assert!(
+ !message.contains("malformed json"),
+ "process exit display should not include raw stderr: {message}"
+ );
+ assert_eq!(
+ exit.exec_output_tail
+ .as_ref()
+ .and_then(|tail| tail.stderr.as_deref()),
+ Some("malformed json\n")
+ );
+ }
+ other => panic!("expected protocol or process exit error, got {other:?}"),
+ }
}
#[tokio::test]
-async fn early_exit_returns_protocol_error_with_stderr() {
+async fn early_exit_returns_process_exit_with_redacted_stderr_tail() {
let tempdir = tempfile::tempdir().expect("create tempdir");
let err = run_fake_agent(
@@ -398,17 +420,23 @@ async fn early_exit_returns_protocol_error_with_stderr() {
.await
.expect_err("early exit should error");
- let AcpError::Protocol(error) = err else {
- panic!("expected protocol error");
+ let AcpError::ProcessExited(exit) = err else {
+ panic!("expected process exit error");
};
- let message = error.to_string();
+ let message = exit.to_string();
assert!(
message.contains("exit_code=2"),
"early exit should include exit code in diagnostic: {message}"
);
assert!(
- message.contains("early boom"),
- "early exit should include stderr tail in diagnostic: {message}"
+ !message.contains("early boom"),
+ "early exit display should not include raw stderr: {message}"
+ );
+ assert_eq!(
+ exit.exec_output_tail
+ .as_ref()
+ .and_then(|tail| tail.stderr.as_deref()),
+ Some("early boom\n")
);
}
diff --git a/lib/crates/fabro-agent/src/agent_profile.rs b/lib/crates/fabro-agent/src/agent_profile.rs
index 048449dff..632ef4655 100644
--- a/lib/crates/fabro-agent/src/agent_profile.rs
+++ b/lib/crates/fabro-agent/src/agent_profile.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use fabro_llm::types::ToolDefinition;
-use fabro_model::{Catalog, Model, Provider, ProviderId};
+use fabro_model::{AgentProfileKind, Catalog, Model, ProviderId};
use tokio::sync::Mutex;
use crate::profiles::EnvContext;
@@ -14,10 +14,8 @@ use crate::subagent::{
use crate::tool_registry::ToolRegistry;
pub trait AgentProfile: Send + Sync {
- fn provider(&self) -> Provider;
- fn provider_id(&self) -> ProviderId {
- self.provider().id()
- }
+ fn profile_kind(&self) -> AgentProfileKind;
+ fn provider_id(&self) -> ProviderId;
fn model(&self) -> &str;
fn catalog(&self) -> Option<&Catalog> {
None
@@ -79,7 +77,7 @@ pub trait AgentProfile: Send + Sync {
#[cfg(test)]
mod tests {
- use fabro_model::Provider;
+ use fabro_model::{AgentProfileKind, ProviderId};
use super::*;
use crate::test_support::{MockSandbox, TestProfile};
@@ -87,7 +85,8 @@ mod tests {
#[test]
fn profile_provider_and_model() {
let profile = TestProfile::new();
- assert_eq!(profile.provider(), Provider::Anthropic);
+ assert_eq!(profile.profile_kind(), AgentProfileKind::Anthropic);
+ assert_eq!(profile.provider_id(), ProviderId::anthropic());
assert_eq!(profile.model(), "mock-model");
}
diff --git a/lib/crates/fabro-agent/src/cli.rs b/lib/crates/fabro-agent/src/cli.rs
index f51595185..af12f575b 100644
--- a/lib/crates/fabro-agent/src/cli.rs
+++ b/lib/crates/fabro-agent/src/cli.rs
@@ -18,8 +18,9 @@ use fabro_llm::middleware::{Middleware, NextFn, NextStreamFn};
use fabro_llm::provider::StreamEventStream;
use fabro_llm::types::{Request, Response};
use fabro_mcp::config::McpServerSettings;
+#[cfg(test)]
use fabro_model::catalog::LlmCatalogSettings;
-use fabro_model::{Catalog, ModelHandle, Provider};
+use fabro_model::{AgentProfileKind, Catalog, ModelHandle, ProviderId};
use fabro_util::terminal::Styles;
use fabro_vault::Vault;
use tokio::io::{AsyncWriteExt, stdout};
@@ -31,8 +32,8 @@ use crate::error::InterruptReason;
use crate::subagent::{SessionFactory, SubAgentManager};
use crate::tools::WebFetchSummarizer;
use crate::{
- AgentEvent, AgentProfile, AnthropicProfile, GeminiProfile, LocalSandbox, OpenAiProfile,
- Sandbox, Session, SessionOptions, Turn,
+ AgentEvent, AgentProfile, AnthropicProfile, GeminiProfile, LocalSandbox, Message,
+ OpenAiProfile, Sandbox, Session, SessionOptions,
};
/// Public arguments for the agent command, usable from an external CLI.
@@ -41,7 +42,7 @@ pub struct AgentArgs {
/// Task prompt
pub prompt: String,
- /// LLM provider (anthropic, openai, gemini, kimi, zai, minimax, inception)
+ /// LLM provider (built-in or configured provider ID)
#[arg(long)]
pub provider: Option,
@@ -205,64 +206,86 @@ fn build_tool_approval(
})
}
-fn summarizer_model_id(provider: Provider) -> ModelHandle {
+fn summarizer_model_id(
+ provider_id: &ProviderId,
+ catalog: &Catalog,
+ selected_model: &str,
+) -> ModelHandle {
ModelHandle::ByName {
- provider: provider.id(),
- model: match provider {
- Provider::OpenAi | Provider::OpenAiCompatible => "gpt-4o-mini",
- Provider::Gemini => "gemini-2.0-flash",
- Provider::Anthropic | Provider::Vertex => "claude-haiku-4-5",
- Provider::Kimi => "kimi-k2.5",
- Provider::Zai => "glm-4.7",
- Provider::Minimax => "minimax-m2.5",
- Provider::Inception => "mercury",
- }
- .to_string(),
+ provider: provider_id.clone(),
+ model: catalog
+ .default_for_provider(provider_id)
+ .map_or_else(
+ || match provider_id.as_str() {
+ ProviderId::ANTHROPIC => "claude-haiku-4-5",
+ ProviderId::GEMINI => "gemini-2.0-flash",
+ _ => selected_model,
+ },
+ |model| model.id.as_str(),
+ )
+ .to_string(),
}
}
-fn build_summarizer(provider: Provider, llm_client: Client) -> WebFetchSummarizer {
+fn build_summarizer(
+ provider_id: &ProviderId,
+ model: &str,
+ catalog: &Catalog,
+ llm_client: Client,
+) -> WebFetchSummarizer {
WebFetchSummarizer {
client: llm_client,
- model_id: summarizer_model_id(provider),
+ model_id: summarizer_model_id(provider_id, catalog, model),
}
}
fn build_profile(
- provider: Provider,
+ profile_kind: AgentProfileKind,
+ provider_id: ProviderId,
model: &str,
summarizer: Option,
catalog: Arc,
) -> Box {
- match provider {
- Provider::OpenAi => {
- Box::new(OpenAiProfile::with_summarizer(model, summarizer).with_catalog(catalog))
- }
- Provider::Kimi
- | Provider::Zai
- | Provider::Minimax
- | Provider::Inception
- | Provider::OpenAiCompatible => Box::new(
+ match profile_kind {
+ AgentProfileKind::OpenAi => Box::new(
OpenAiProfile::with_summarizer(model, summarizer)
- .with_provider(provider)
+ .with_provider_id(provider_id)
.with_catalog(catalog),
),
- Provider::Gemini => {
- Box::new(GeminiProfile::with_summarizer(model, summarizer).with_catalog(catalog))
- }
- Provider::Anthropic | Provider::Vertex => Box::new(
+ AgentProfileKind::Gemini => Box::new(
+ GeminiProfile::with_summarizer(model, summarizer)
+ .with_provider_id(provider_id)
+ .with_catalog(catalog),
+ ),
+ AgentProfileKind::Anthropic => Box::new(
AnthropicProfile::with_summarizer(model, summarizer)
- .with_provider(provider)
+ .with_provider_id(provider_id)
.with_catalog(catalog),
),
}
}
-fn parse_provider(args: &AgentArgs) -> anyhow::Result {
+fn parse_provider(args: &AgentArgs) -> anyhow::Result {
let provider_str = args.provider.as_deref().unwrap_or("anthropic");
- provider_str
- .parse()
- .map_err(|_| anyhow::anyhow!("unknown provider: {provider_str}"))
+ Ok(provider_str.parse()?)
+}
+
+fn resolve_provider_id(catalog: &Catalog, args: &AgentArgs) -> anyhow::Result {
+ if args.provider.is_some() {
+ let requested = parse_provider(args)?;
+ return Ok(catalog
+ .provider(&requested)
+ .map_or(requested, |provider| provider.id.clone()));
+ }
+ if let Some(model_id) = args.model.as_deref() {
+ if let Some(model) = catalog.get(model_id) {
+ return Ok(model.provider.clone());
+ }
+ }
+ let requested = parse_provider(args)?;
+ Ok(catalog
+ .provider(&requested)
+ .map_or(requested, |provider| provider.id.clone()))
}
fn standalone_llm_source() -> Arc {
@@ -275,16 +298,26 @@ fn standalone_llm_source() -> Arc {
}
}
-fn ensure_provider_registered(client: &Client, provider: Provider) -> anyhow::Result<()> {
+fn profile_kind_for_provider(
+ catalog: &Catalog,
+ provider_id: &ProviderId,
+ model: Option<&str>,
+) -> anyhow::Result {
+ catalog
+ .effective_agent_profile(provider_id, model)
+ .ok_or_else(|| anyhow::anyhow!("provider '{provider_id}' is not configured"))
+}
+
+fn ensure_provider_registered(client: &Client, provider_id: &ProviderId) -> anyhow::Result<()> {
if client
.provider_names()
.iter()
- .any(|name| *name == <&'static str>::from(provider))
+ .any(|name| *name == provider_id.as_str())
{
return Ok(());
}
- anyhow::bail!("LLM credentials not configured for provider '{provider}'");
+ anyhow::bail!("LLM credentials not configured for provider '{provider_id}'");
}
fn format_tool_args(args: &serde_json::Value, cwd: &str) -> String {
@@ -319,7 +352,7 @@ fn format_tool_args(args: &serde_json::Value, cwd: &str) -> String {
)]
fn print_output(session: &Session, styles: &Styles) {
for turn in session.history().turns() {
- if let Turn::Assistant { content, .. } = turn {
+ if let Message::Assistant { content, .. } = turn {
if !content.is_empty() {
println!("{}", styles.render_markdown(content));
}
@@ -334,7 +367,7 @@ fn print_output(session: &Session, styles: &Styles) {
fn print_summary(session: &Session, styles: &Styles) {
let (mut turn_count, mut tool_call_count, mut total_tokens) = (0usize, 0usize, 0i64);
for turn in session.history().turns() {
- if let Turn::Assistant {
+ if let Message::Assistant {
tool_calls, usage, ..
} = turn
{
@@ -447,7 +480,9 @@ pub async fn run_with_args(
mcp_servers: Vec,
) -> anyhow::Result<()> {
let llm_source = standalone_llm_source();
- run_with_args_and_source(args, llm_source, mcp_servers).await
+ let catalog =
+ Arc::new(Catalog::from_builtin().context("failed to build standalone agent LLM catalog")?);
+ run_with_args_and_source_and_catalog(args, llm_source, mcp_servers, catalog).await
}
#[allow(
@@ -460,16 +495,26 @@ pub async fn run_with_args_and_source(
llm_source: Arc,
mcp_servers: Vec,
) -> anyhow::Result<()> {
- let provider = parse_provider(&args)?;
- let catalog = Arc::new(
- Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default())
- .context("failed to build standalone agent LLM catalog")?,
- );
+ let catalog =
+ Arc::new(Catalog::from_builtin().context("failed to build standalone agent LLM catalog")?);
+ run_with_args_and_source_and_catalog(args, llm_source, mcp_servers, catalog).await
+}
+
+#[allow(
+ clippy::print_stdout,
+ clippy::print_stderr,
+ reason = "Assistant output stays on stdout while prompts and diagnostics use stderr."
+)]
+pub async fn run_with_args_and_source_and_catalog(
+ args: AgentArgs,
+ llm_source: Arc,
+ mcp_servers: Vec,
+ catalog: Arc,
+) -> anyhow::Result<()> {
let client = Client::from_source(llm_source.as_ref(), Arc::clone(&catalog))
.await
.context("Failed to create LLM client")?;
- ensure_provider_registered(&client, provider)?;
- run_with_args_and_client(args, client, mcp_servers).await
+ run_with_args_and_client_and_catalog(args, client, mcp_servers, catalog).await
}
#[allow(
@@ -478,16 +523,32 @@ pub async fn run_with_args_and_source(
reason = "Assistant output stays on stdout while prompts and diagnostics use stderr."
)]
pub async fn run_with_args_and_client(
+ args: AgentArgs,
+ client: Client,
+ mcp_servers: Vec,
+) -> anyhow::Result<()> {
+ let catalog =
+ Arc::new(Catalog::from_builtin().context("failed to build standalone agent LLM catalog")?);
+ run_with_args_and_client_and_catalog(args, client, mcp_servers, catalog).await
+}
+
+#[allow(
+ clippy::print_stdout,
+ clippy::print_stderr,
+ reason = "Assistant output stays on stdout while prompts and diagnostics use stderr."
+)]
+pub async fn run_with_args_and_client_and_catalog(
args: AgentArgs,
mut client: Client,
mcp_servers: Vec,
+ catalog: Arc,
) -> anyhow::Result<()> {
// Resolve color support once, leak to get 'static lifetime for use across
// threads
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
- let provider = parse_provider(&args)?;
- ensure_provider_registered(&client, provider)?;
+ let provider_id = resolve_provider_id(&catalog, &args)?;
+ ensure_provider_registered(&client, &provider_id)?;
if args.verbose {
client.add_middleware(Arc::new(VerboseMiddleware { styles }));
@@ -495,28 +556,30 @@ pub async fn run_with_args_and_client(
client.add_middleware(Arc::new(DebugMiddleware { styles }));
}
- // Resolve model and build profile
- let catalog = Arc::new(
- Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default())
- .context("failed to build standalone agent LLM catalog")?,
- );
let model = if let Some(model) = args.model.clone() {
model
} else {
catalog
- .default_for_provider(&provider.id())
+ .default_for_provider(&provider_id)
.map(|model| model.id.clone())
.ok_or_else(|| {
anyhow::anyhow!(
- "provider '{provider}' has no default model in the catalog; pass --model explicitly"
+ "provider '{provider_id}' has no default model in the catalog; pass --model explicitly"
)
})?
};
+ let profile_kind = profile_kind_for_provider(&catalog, &provider_id, Some(&model))?;
eprintln!("{}", styles.dim.apply_to(format!("Using model: {model}")));
let mut profile = build_profile(
- provider,
+ profile_kind,
+ provider_id.clone(),
&model,
- Some(build_summarizer(provider, client.clone())),
+ Some(build_summarizer(
+ &provider_id,
+ &model,
+ &catalog,
+ client.clone(),
+ )),
Arc::clone(&catalog),
);
@@ -552,12 +615,20 @@ pub async fn run_with_args_and_client(
let factory_client = client.clone();
let factory_model = model.clone();
let factory_catalog = Arc::clone(&catalog);
+ let factory_provider_id = provider_id.clone();
+ let factory_profile_kind = profile_kind;
let factory_env = Arc::clone(&env);
let factory_hooks = config.tool_hooks.clone();
let factory: SessionFactory = Arc::new(move || {
- let child_summarizer = Some(build_summarizer(provider, factory_client.clone()));
+ let child_summarizer = Some(build_summarizer(
+ &factory_provider_id,
+ &factory_model,
+ &factory_catalog,
+ factory_client.clone(),
+ ));
let child_profile: Arc = Arc::from(build_profile(
- provider,
+ factory_profile_kind,
+ factory_provider_id.clone(),
&factory_model,
child_summarizer,
Arc::clone(&factory_catalog),
@@ -757,7 +828,9 @@ pub async fn run() -> anyhow::Result<()> {
mod tests {
use std::collections::HashMap;
- use fabro_model::Provider;
+ use fabro_model::catalog::{
+ ModelCatalogSettings, ProviderCatalogSettings, SettingsModelFeatures, SettingsModelLimits,
+ };
use serde_json::json;
use super::*;
@@ -859,25 +932,39 @@ mod tests {
// build_profile tests
fn test_catalog() -> Arc {
- Arc::new(Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default()).unwrap())
+ Arc::new(Catalog::from_builtin().unwrap())
}
#[test]
fn build_profile_anthropic() {
- let profile = build_profile(Provider::Anthropic, "model", None, test_catalog());
- assert_eq!(profile.provider(), Provider::Anthropic);
+ let profile = build_profile(
+ AgentProfileKind::Anthropic,
+ ProviderId::anthropic(),
+ "model",
+ None,
+ test_catalog(),
+ );
+ assert_eq!(profile.profile_kind(), AgentProfileKind::Anthropic);
+ assert_eq!(profile.provider_id(), ProviderId::anthropic());
}
#[test]
fn build_profile_openai() {
- let profile = build_profile(Provider::OpenAi, "model", None, test_catalog());
- assert_eq!(profile.provider(), Provider::OpenAi);
+ let profile = build_profile(
+ AgentProfileKind::OpenAi,
+ ProviderId::openai(),
+ "model",
+ None,
+ test_catalog(),
+ );
+ assert_eq!(profile.profile_kind(), AgentProfileKind::OpenAi);
+ assert_eq!(profile.provider_id(), ProviderId::openai());
}
#[test]
fn ensure_provider_registered_reports_missing_credentials() {
let client = Client::new(HashMap::new(), None, vec![]);
- let error = ensure_provider_registered(&client, Provider::Anthropic).unwrap_err();
+ let error = ensure_provider_registered(&client, &ProviderId::anthropic()).unwrap_err();
assert_eq!(
error.to_string(),
"LLM credentials not configured for provider 'anthropic'"
@@ -886,15 +973,228 @@ mod tests {
#[test]
fn build_profile_gemini() {
- let profile = build_profile(Provider::Gemini, "model", None, test_catalog());
- assert_eq!(profile.provider(), Provider::Gemini);
+ let profile = build_profile(
+ AgentProfileKind::Gemini,
+ ProviderId::gemini(),
+ "model",
+ None,
+ test_catalog(),
+ );
+ assert_eq!(profile.profile_kind(), AgentProfileKind::Gemini);
+ assert_eq!(profile.provider_id(), ProviderId::gemini());
+ }
+
+ #[test]
+ fn profile_kind_accepts_custom_catalog_provider() {
+ let mut settings = LlmCatalogSettings::default();
+ settings
+ .providers
+ .insert("bedrock".to_string(), ProviderCatalogSettings {
+ display_name: Some("Bedrock".to_string()),
+ adapter: Some("openai_compatible".to_string()),
+ base_url: Some("https://example.invalid/v1".to_string()),
+ ..ProviderCatalogSettings::default()
+ });
+ let catalog = Catalog::from_builtin_with_overrides(&settings).unwrap();
+ let args = AgentArgs {
+ prompt: "test".to_string(),
+ provider: Some("bedrock".to_string()),
+ model: None,
+ permissions: None,
+ auto_approve: false,
+ debug: false,
+ verbose: false,
+ skills_dir: None,
+ output_format: None,
+ };
+
+ let provider_id = parse_provider(&args).unwrap();
+ assert_eq!(provider_id, ProviderId::new("bedrock"));
+ assert_eq!(
+ profile_kind_for_provider(&catalog, &provider_id, None).unwrap(),
+ AgentProfileKind::OpenAi
+ );
+ }
+
+ #[test]
+ fn standalone_provider_resolution_uses_catalog_model_provider_when_provider_omitted() {
+ let mut settings = LlmCatalogSettings::default();
+ settings
+ .providers
+ .insert("bedrock".to_string(), ProviderCatalogSettings {
+ display_name: Some("Bedrock".to_string()),
+ adapter: Some("openai_compatible".to_string()),
+ base_url: Some("https://example.invalid/v1".to_string()),
+ ..ProviderCatalogSettings::default()
+ });
+ settings
+ .models
+ .insert("bedrock-claude".to_string(), ModelCatalogSettings {
+ provider: Some("bedrock".to_string()),
+ display_name: Some("Bedrock Claude".to_string()),
+ family: Some("claude".to_string()),
+ default: Some(true),
+ limits: Some(SettingsModelLimits {
+ context_window: Some(1000),
+ max_output: None,
+ }),
+ features: Some(SettingsModelFeatures {
+ tools: Some(true),
+ vision: Some(false),
+ reasoning: Some(false),
+ reasoning_effort: None,
+ prompt_cache: None,
+ }),
+ ..ModelCatalogSettings::default()
+ });
+ let catalog = Catalog::from_builtin_with_overrides(&settings).unwrap();
+ let args = AgentArgs {
+ prompt: "test".to_string(),
+ provider: None,
+ model: Some("bedrock-claude".to_string()),
+ permissions: None,
+ auto_approve: false,
+ debug: false,
+ verbose: false,
+ skills_dir: None,
+ output_format: None,
+ };
+
+ assert_eq!(
+ resolve_provider_id(&catalog, &args).unwrap(),
+ ProviderId::new("bedrock")
+ );
+ }
+
+ #[test]
+ fn standalone_provider_resolution_canonicalizes_explicit_provider_alias() {
+ let mut settings = LlmCatalogSettings::default();
+ settings
+ .providers
+ .insert("bedrock".to_string(), ProviderCatalogSettings {
+ display_name: Some("Bedrock".to_string()),
+ adapter: Some("openai_compatible".to_string()),
+ base_url: Some("https://example.invalid/v1".to_string()),
+ aliases: Some(vec!["br".to_string()]),
+ ..ProviderCatalogSettings::default()
+ });
+ let catalog = Catalog::from_builtin_with_overrides(&settings).unwrap();
+ let args = AgentArgs {
+ prompt: "test".to_string(),
+ provider: Some("br".to_string()),
+ model: None,
+ permissions: None,
+ auto_approve: false,
+ debug: false,
+ verbose: false,
+ skills_dir: None,
+ output_format: None,
+ };
+
+ assert_eq!(
+ resolve_provider_id(&catalog, &args).unwrap(),
+ ProviderId::new("bedrock")
+ );
+ }
+
+ #[test]
+ fn standalone_profile_kind_uses_model_agent_profile_override() {
+ let mut settings = LlmCatalogSettings::default();
+ settings
+ .providers
+ .insert("bedrock".to_string(), ProviderCatalogSettings {
+ display_name: Some("Bedrock".to_string()),
+ adapter: Some("openai_compatible".to_string()),
+ base_url: Some("https://example.invalid/v1".to_string()),
+ agent_profile: Some(AgentProfileKind::OpenAi),
+ ..ProviderCatalogSettings::default()
+ });
+ settings
+ .models
+ .insert("bedrock-claude".to_string(), ModelCatalogSettings {
+ provider: Some("bedrock".to_string()),
+ display_name: Some("Bedrock Claude".to_string()),
+ family: Some("claude".to_string()),
+ default: Some(true),
+ agent_profile: Some(AgentProfileKind::Anthropic),
+ limits: Some(SettingsModelLimits {
+ context_window: Some(1000),
+ max_output: None,
+ }),
+ features: Some(SettingsModelFeatures {
+ tools: Some(true),
+ vision: Some(false),
+ reasoning: Some(false),
+ reasoning_effort: None,
+ prompt_cache: None,
+ }),
+ ..ModelCatalogSettings::default()
+ });
+ let catalog = Catalog::from_builtin_with_overrides(&settings).unwrap();
+
+ assert_eq!(
+ profile_kind_for_provider(
+ &catalog,
+ &ProviderId::new("bedrock"),
+ Some("bedrock-claude")
+ )
+ .unwrap(),
+ AgentProfileKind::Anthropic
+ );
+ }
+
+ #[test]
+ fn summarizer_model_id_uses_selected_model_for_custom_provider_without_default() {
+ let mut settings = LlmCatalogSettings::default();
+ settings
+ .providers
+ .insert("bedrock".to_string(), ProviderCatalogSettings {
+ display_name: Some("Bedrock".to_string()),
+ adapter: Some("openai_compatible".to_string()),
+ base_url: Some("https://example.invalid/v1".to_string()),
+ ..ProviderCatalogSettings::default()
+ });
+ let catalog = Catalog::from_builtin_with_overrides(&settings).unwrap();
+ let provider_id = ProviderId::new("bedrock");
+
+ let model_id = summarizer_model_id(&provider_id, &catalog, "bedrock-claude-sonnet-4-6");
+
+ assert_eq!(model_id.provider(), &provider_id);
+ assert_eq!(model_id.model_id(), "bedrock-claude-sonnet-4-6");
+ }
+
+ #[test]
+ fn summarizer_model_id_ignores_profile_for_custom_provider_without_default() {
+ let mut settings = LlmCatalogSettings::default();
+ settings
+ .providers
+ .insert("bedrock".to_string(), ProviderCatalogSettings {
+ display_name: Some("Bedrock".to_string()),
+ adapter: Some("openai_compatible".to_string()),
+ base_url: Some("https://example.invalid/v1".to_string()),
+ agent_profile: Some(AgentProfileKind::Anthropic),
+ ..ProviderCatalogSettings::default()
+ });
+ let catalog = Catalog::from_builtin_with_overrides(&settings).unwrap();
+ let provider_id = ProviderId::new("bedrock");
+
+ let model_id = summarizer_model_id(&provider_id, &catalog, "bedrock-claude-sonnet-4-6");
+
+ assert_eq!(model_id.provider(), &provider_id);
+ assert_eq!(model_id.model_id(), "bedrock-claude-sonnet-4-6");
}
// subagent tool registration tests
#[test]
fn build_profile_can_register_subagent_tools() {
- let mut profile = build_profile(Provider::Anthropic, "model", None, test_catalog());
+ let mut profile = build_profile(
+ AgentProfileKind::Anthropic,
+ ProviderId::anthropic(),
+ "model",
+ None,
+ test_catalog(),
+ );
let manager = Arc::new(AsyncMutex::new(SubAgentManager::new(1)));
let factory: SessionFactory = Arc::new(|| {
panic!("factory should not be called in this test");
diff --git a/lib/crates/fabro-agent/src/compaction.rs b/lib/crates/fabro-agent/src/compaction.rs
index 31c4e886e..01c756dc5 100644
--- a/lib/crates/fabro-agent/src/compaction.rs
+++ b/lib/crates/fabro-agent/src/compaction.rs
@@ -1,7 +1,7 @@
use std::fmt::Write;
use fabro_llm::client::Client;
-use fabro_llm::types::{Message, Request};
+use fabro_llm::types::{Message as LlmMessage, Request};
use tracing::debug;
use crate::agent_profile::AgentProfile;
@@ -9,7 +9,7 @@ use crate::error::Error;
use crate::event::Emitter;
use crate::file_tracker::FileTracker;
use crate::history::History;
-use crate::types::{AgentEvent, Turn};
+use crate::types::{AgentEvent, Message};
/// Check whether the context window usage exceeds the configured threshold.
/// Emits a `Warning` event with kind `"context_window"` when over the
@@ -106,8 +106,8 @@ function names, error messages, and exact values. Omit pleasantries and conversa
let summary_request = Request {
model: provider_profile.model().to_string(),
messages: vec![
- Message::system(summarization_prompt),
- Message::user(format!(
+ LlmMessage::system(summarization_prompt),
+ LlmMessage::user(format!(
"Here is the conversation to summarize:\n\n{rendered}"
)),
],
@@ -160,8 +160,8 @@ pub fn estimate_token_count(system_prompt: &str, history: &History) -> usize {
for turn in history.turns() {
match turn {
- Turn::User { content, .. } => total_chars += content.len(),
- Turn::Assistant {
+ Message::User { content, .. } => total_chars += content.len(),
+ Message::Assistant {
content,
tool_calls,
..
@@ -175,12 +175,12 @@ pub fn estimate_token_count(system_prompt: &str, history: &History) -> usize {
total_chars += tc.arguments.to_string().len();
}
}
- Turn::ToolResults { results, .. } => {
+ Message::ToolResults { results, .. } => {
for r in results {
total_chars += r.content.to_string().len();
}
}
- Turn::System { content, .. } | Turn::Steering { content, .. } => {
+ Message::System { content, .. } | Message::Steering { content, .. } => {
total_chars += content.len();
}
}
@@ -191,14 +191,14 @@ pub fn estimate_token_count(system_prompt: &str, history: &History) -> usize {
/// Render conversation turns into a human-readable summary format for the
/// compaction LLM call.
-pub fn render_turns_for_summary(turns: &[Turn]) -> String {
+pub fn render_turns_for_summary(turns: &[Message]) -> String {
let mut out = String::new();
for turn in turns {
match turn {
- Turn::User { content, .. } => {
+ Message::User { content, .. } => {
let _ = writeln!(out, "User: {content}");
}
- Turn::Assistant {
+ Message::Assistant {
content,
tool_calls,
..
@@ -216,7 +216,7 @@ pub fn render_turns_for_summary(turns: &[Turn]) -> String {
let _ = writeln!(out, "[Tool call: {}] {truncated}", tc.name);
}
}
- Turn::ToolResults { results, .. } => {
+ Message::ToolResults { results, .. } => {
for r in results {
let content_str = r.content.to_string();
let truncated = if content_str.len() > 500 {
@@ -230,10 +230,10 @@ pub fn render_turns_for_summary(turns: &[Turn]) -> String {
let _ = writeln!(out, "[Tool result: {}] {truncated}", r.tool_call_id);
}
}
- Turn::System { content, .. } => {
+ Message::System { content, .. } => {
let _ = writeln!(out, "System: {content}");
}
- Turn::Steering { content, .. } => {
+ Message::Steering { content, .. } => {
let _ = writeln!(out, "Steering: {content}");
}
}
@@ -252,16 +252,16 @@ mod tests {
use crate::history::History;
use crate::test_support::TestProfile;
use crate::tool_registry::ToolRegistry;
- use crate::types::Turn;
+ use crate::types::Message;
#[test]
fn render_turns_produces_labeled_text() {
let turns = vec![
- Turn::User {
+ Message::User {
content: "Hello".into(),
timestamp: SystemTime::now(),
},
- Turn::Assistant {
+ Message::Assistant {
content: "Let me check".into(),
tool_calls: vec![ToolCall::new(
"c1",
@@ -273,7 +273,7 @@ mod tests {
response_id: "resp_1".into(),
timestamp: SystemTime::now(),
},
- Turn::ToolResults {
+ Message::ToolResults {
results: vec![ToolResult {
tool_call_id: "c1".into(),
content: serde_json::json!("file contents here"),
@@ -296,7 +296,7 @@ mod tests {
#[test]
fn render_turns_truncates_long_tool_output() {
let long_output = "x".repeat(1000);
- let turns = vec![Turn::ToolResults {
+ let turns = vec![Message::ToolResults {
results: vec![ToolResult {
tool_call_id: "c1".into(),
content: serde_json::json!(long_output),
@@ -315,7 +315,7 @@ mod tests {
#[test]
fn estimate_token_count_basic() {
let mut history = History::default();
- history.push(Turn::User {
+ history.push(Message::User {
content: "Hello world".into(), // 11 chars
timestamp: SystemTime::now(),
});
@@ -337,7 +337,7 @@ mod tests {
fn check_context_usage_above_threshold() {
let mut history = History::default();
// Push enough content to exceed a tiny context window
- history.push(Turn::User {
+ history.push(Message::User {
content: "x".repeat(1000),
timestamp: SystemTime::now(),
});
diff --git a/lib/crates/fabro-agent/src/history.rs b/lib/crates/fabro-agent/src/history.rs
index 74593dbc2..497900eae 100644
--- a/lib/crates/fabro-agent/src/history.rs
+++ b/lib/crates/fabro-agent/src/history.rs
@@ -1,22 +1,37 @@
-use fabro_llm::types::{ContentPart, Message, Role};
+use fabro_llm::types::{ContentPart, Message as LlmMessage, Role};
+use fabro_types::SessionMessage;
-use crate::types::Turn;
+use crate::types::Message;
#[derive(Debug, Clone, Default)]
pub struct History {
- turns: Vec,
+ turns: Vec,
}
impl History {
- pub fn push(&mut self, turn: Turn) {
+ pub fn from_session_messages(messages: &[SessionMessage]) -> Result {
+ Ok(Self {
+ turns: messages
+ .iter()
+ .map(Message::from_session_message)
+ .collect::, _>>()?,
+ })
+ }
+
+ pub fn push(&mut self, turn: Message) {
self.turns.push(turn);
}
#[must_use]
- pub fn turns(&self) -> &[Turn] {
+ pub fn turns(&self) -> &[Message] {
&self.turns
}
+ #[must_use]
+ pub fn to_session_messages(&self) -> Vec {
+ self.turns.iter().map(Message::to_session_message).collect()
+ }
+
pub fn compact(&mut self, preserve_count: usize, summary: String) {
if self.turns.len() <= preserve_count {
return;
@@ -25,7 +40,7 @@ impl History {
let discarded = std::mem::take(&mut self.turns);
let extracted_user_messages =
extract_recent_user_messages(discarded, COMPACTION_USER_MESSAGE_TOKEN_BUDGET);
- self.turns.push(Turn::System {
+ self.turns.push(Message::System {
content: summary,
timestamp: std::time::SystemTime::now(),
});
@@ -42,19 +57,19 @@ impl History {
/// output, identified by the message item's `id`).
fn strip_opaque_provider_items(&mut self) {
for turn in &mut self.turns {
- if let Turn::Assistant { provider_parts, .. } = turn {
+ if let Message::Assistant { provider_parts, .. } = turn {
provider_parts.retain(|p| !p.is_opaque_openai());
}
}
}
#[must_use]
- pub fn convert_to_messages(&self) -> Vec {
+ pub fn convert_to_messages(&self) -> Vec {
self.turns
.iter()
.map(|turn| match turn {
- Turn::User { content, .. } => Message::user(content),
- Turn::Assistant {
+ Message::User { content, .. } => LlmMessage::user(content),
+ Message::Assistant {
content,
tool_calls,
provider_parts,
@@ -71,29 +86,29 @@ impl History {
for tc in tool_calls {
parts.push(ContentPart::ToolCall(tc.clone()));
}
- Message {
+ LlmMessage {
role: Role::Assistant,
content: parts,
name: None,
tool_call_id: None,
}
}
- Turn::ToolResults { results, .. } => {
+ Message::ToolResults { results, .. } => {
let content: Vec = results
.iter()
.map(|r| ContentPart::ToolResult(r.clone()))
.collect();
// Use the first result's tool_call_id if available
let tool_call_id = results.first().map(|r| r.tool_call_id.clone());
- Message {
+ LlmMessage {
role: Role::Tool,
content,
name: None,
tool_call_id,
}
}
- Turn::System { content, .. } => Message::system(content),
- Turn::Steering { content, .. } => Message {
+ Message::System { content, .. } => LlmMessage::system(content),
+ Message::Steering { content, .. } => LlmMessage {
role: Role::User,
content: vec![ContentPart::text(content)],
name: None,
@@ -108,18 +123,18 @@ impl History {
/// compaction.
const COMPACTION_USER_MESSAGE_TOKEN_BUDGET: usize = 20_000;
-/// Walk discarded turns in reverse, collecting `Turn::User` variants up to
+/// Walk discarded turns in reverse, collecting `Message::User` variants up to
/// a token budget (estimated at ~4 chars per token). Returns them in
/// chronological order so they can be inserted between the summary and the
/// preserved tail.
-fn extract_recent_user_messages(discarded: Vec, token_budget: usize) -> Vec {
+fn extract_recent_user_messages(discarded: Vec, token_budget: usize) -> Vec {
let char_budget = token_budget * 4;
let mut total_chars = 0;
let mut first_kept_index = discarded.len();
// Walk backward to find the earliest user message within budget
for (i, turn) in discarded.iter().enumerate().rev() {
- if let Turn::User { content, .. } = turn {
+ if let Message::User { content, .. } = turn {
if total_chars + content.len() > char_budget {
break;
}
@@ -132,7 +147,7 @@ fn extract_recent_user_messages(discarded: Vec, token_budget: usize) -> Ve
discarded
.into_iter()
.skip(first_kept_index)
- .filter(|t| matches!(t, Turn::User { .. }))
+ .filter(|t| matches!(t, Message::User { .. }))
.collect()
}
@@ -148,7 +163,7 @@ mod tests {
fn compact_replaces_old_turns_with_summary() {
let mut history = History::default();
for i in 0..8 {
- history.push(Turn::User {
+ history.push(Message::User {
content: format!("msg {i}"),
timestamp: SystemTime::now(),
});
@@ -162,7 +177,7 @@ mod tests {
fn compact_noop_when_fewer_turns_than_preserve() {
let mut history = History::default();
for i in 0..3 {
- history.push(Turn::User {
+ history.push(Message::User {
content: format!("msg {i}"),
timestamp: SystemTime::now(),
});
@@ -175,7 +190,7 @@ mod tests {
fn compact_preserves_recent_turns() {
let mut history = History::default();
for i in 0..8 {
- history.push(Turn::User {
+ history.push(Message::User {
content: format!("msg {i}"),
timestamp: SystemTime::now(),
});
@@ -183,22 +198,22 @@ mod tests {
history.compact(4, "Summary".into());
let turns = history.turns();
// Layout: summary, extracted user msgs (0..3), preserved (4..7)
- assert!(matches!(&turns[0], Turn::System { .. }));
- assert!(matches!(&turns[1], Turn::User { content, .. } if content == "msg 0"));
- assert!(matches!(&turns[2], Turn::User { content, .. } if content == "msg 1"));
- assert!(matches!(&turns[3], Turn::User { content, .. } if content == "msg 2"));
- assert!(matches!(&turns[4], Turn::User { content, .. } if content == "msg 3"));
- assert!(matches!(&turns[5], Turn::User { content, .. } if content == "msg 4"));
- assert!(matches!(&turns[6], Turn::User { content, .. } if content == "msg 5"));
- assert!(matches!(&turns[7], Turn::User { content, .. } if content == "msg 6"));
- assert!(matches!(&turns[8], Turn::User { content, .. } if content == "msg 7"));
+ assert!(matches!(&turns[0], Message::System { .. }));
+ assert!(matches!(&turns[1], Message::User { content, .. } if content == "msg 0"));
+ assert!(matches!(&turns[2], Message::User { content, .. } if content == "msg 1"));
+ assert!(matches!(&turns[3], Message::User { content, .. } if content == "msg 2"));
+ assert!(matches!(&turns[4], Message::User { content, .. } if content == "msg 3"));
+ assert!(matches!(&turns[5], Message::User { content, .. } if content == "msg 4"));
+ assert!(matches!(&turns[6], Message::User { content, .. } if content == "msg 5"));
+ assert!(matches!(&turns[7], Message::User { content, .. } if content == "msg 6"));
+ assert!(matches!(&turns[8], Message::User { content, .. } if content == "msg 7"));
}
#[test]
fn compact_summary_maps_to_system_message() {
let mut history = History::default();
for i in 0..6 {
- history.push(Turn::User {
+ history.push(Message::User {
content: format!("msg {i}"),
timestamp: SystemTime::now(),
});
@@ -219,7 +234,7 @@ mod tests {
#[test]
fn user_turn_maps_to_user_message() {
let mut history = History::default();
- history.push(Turn::User {
+ history.push(Message::User {
content: "Hello".into(),
timestamp: SystemTime::now(),
});
@@ -232,7 +247,7 @@ mod tests {
#[test]
fn assistant_turn_maps_to_assistant_message() {
let mut history = History::default();
- history.push(Turn::Assistant {
+ history.push(Message::Assistant {
content: "Hi there".into(),
tool_calls: vec![],
provider_parts: vec![],
@@ -250,7 +265,7 @@ mod tests {
fn assistant_turn_with_tool_calls() {
let mut history = History::default();
let tc = ToolCall::new("call_1", "read_file", serde_json::json!({"path": "foo.rs"}));
- history.push(Turn::Assistant {
+ history.push(Message::Assistant {
content: "Let me read that".into(),
tool_calls: vec![tc],
provider_parts: vec![],
@@ -276,7 +291,7 @@ mod tests {
signature: None,
redacted: false,
});
- history.push(Turn::Assistant {
+ history.push(Message::Assistant {
content: "The answer is 42".into(),
tool_calls: vec![],
provider_parts: vec![thinking],
@@ -301,7 +316,7 @@ mod tests {
signature: Some("sig_abc123".into()),
redacted: false,
});
- history.push(Turn::Assistant {
+ history.push(Message::Assistant {
content: "The answer".into(),
tool_calls: vec![],
provider_parts: vec![thinking],
@@ -332,7 +347,7 @@ mod tests {
data: serde_json::json!({"type": "reasoning", "id": "rs_abc"}),
};
let tc = ToolCall::new("call_1", "search", serde_json::json!({}));
- history.push(Turn::Assistant {
+ history.push(Message::Assistant {
content: String::new(),
tool_calls: vec![tc],
provider_parts: vec![reasoning_item],
@@ -353,7 +368,7 @@ mod tests {
fn tool_results_turn_maps_to_tool_message() {
let mut history = History::default();
let result = ToolResult::success("call_1", serde_json::json!("file contents here"));
- history.push(Turn::ToolResults {
+ history.push(Message::ToolResults {
results: vec![result],
timestamp: SystemTime::now(),
});
@@ -366,7 +381,7 @@ mod tests {
#[test]
fn system_turn_maps_to_system_message() {
let mut history = History::default();
- history.push(Turn::System {
+ history.push(Message::System {
content: "You are a coding assistant".into(),
timestamp: SystemTime::now(),
});
@@ -379,7 +394,7 @@ mod tests {
#[test]
fn steering_turn_maps_to_user_message() {
let mut history = History::default();
- history.push(Turn::Steering {
+ history.push(Message::Steering {
content: "Focus on the main task".into(),
timestamp: SystemTime::now(),
});
@@ -389,16 +404,59 @@ mod tests {
assert_eq!(messages[0].text(), "Focus on the main task");
}
+ #[test]
+ fn session_message_roundtrip_preserves_runtime_history() {
+ let mut history = History::default();
+ let tool_call = ToolCall::new("call_1", "read_file", serde_json::json!({"path": "a.rs"}));
+ let tool_result = ToolResult::success("call_1", serde_json::json!("ok"));
+ history.push(Message::User {
+ content: "Read a file".into(),
+ timestamp: SystemTime::now(),
+ });
+ history.push(Message::Assistant {
+ content: "Reading".into(),
+ tool_calls: vec![tool_call],
+ provider_parts: vec![],
+ usage: Box::new(TokenCounts {
+ input_tokens: 10,
+ output_tokens: 3,
+ ..TokenCounts::default()
+ }),
+ response_id: "resp_1".into(),
+ timestamp: SystemTime::now(),
+ });
+ history.push(Message::ToolResults {
+ results: vec![tool_result],
+ timestamp: SystemTime::now(),
+ });
+
+ let persisted = history.to_session_messages();
+ let restored =
+ History::from_session_messages(&persisted).expect("persisted messages should hydrate");
+
+ assert_eq!(restored.turns().len(), 3);
+ assert!(
+ matches!(&restored.turns()[0], Message::User { content, .. } if content == "Read a file")
+ );
+ assert!(
+ matches!(&restored.turns()[1], Message::Assistant { content, tool_calls, usage, .. }
+ if content == "Reading" && tool_calls.len() == 1 && usage.input_tokens == 10)
+ );
+ assert!(
+ matches!(&restored.turns()[2], Message::ToolResults { results, .. } if results.len() == 1)
+ );
+ }
+
#[test]
fn turns_len_matches_push_count() {
let mut history = History::default();
assert_eq!(history.turns().len(), 0);
- history.push(Turn::User {
+ history.push(Message::User {
content: "First".into(),
timestamp: SystemTime::now(),
});
assert_eq!(history.turns().len(), 1);
- history.push(Turn::Assistant {
+ history.push(Message::Assistant {
content: "Second".into(),
tool_calls: vec![],
provider_parts: vec![],
@@ -412,11 +470,11 @@ mod tests {
#[test]
fn round_trip_preserves_content() {
let mut history = History::default();
- history.push(Turn::User {
+ history.push(Message::User {
content: "Hello".into(),
timestamp: SystemTime::now(),
});
- history.push(Turn::Assistant {
+ history.push(Message::Assistant {
content: "Hi".into(),
tool_calls: vec![ToolCall::new(
"c1",
@@ -436,7 +494,7 @@ mod tests {
response_id: "resp_1".into(),
timestamp: SystemTime::now(),
});
- history.push(Turn::ToolResults {
+ history.push(Message::ToolResults {
results: vec![ToolResult::success(
"c1",
serde_json::json!("file1.rs\nfile2.rs"),
@@ -454,11 +512,11 @@ mod tests {
#[test]
fn compact_strips_openai_reasoning_from_preserved_turns() {
let mut history = History::default();
- history.push(Turn::User {
+ history.push(Message::User {
content: "old msg".into(),
timestamp: SystemTime::now(),
});
- history.push(Turn::User {
+ history.push(Message::User {
content: "recent msg".into(),
timestamp: SystemTime::now(),
});
@@ -467,7 +525,7 @@ mod tests {
data: serde_json::json!({"type": "reasoning", "id": "rs_abc"}),
};
let tc = ToolCall::new("call_1", "search", serde_json::json!({}));
- history.push(Turn::Assistant {
+ history.push(Message::Assistant {
content: "response".into(),
tool_calls: vec![tc],
provider_parts: vec![reasoning],
@@ -481,7 +539,7 @@ mod tests {
// Layout: summary, extracted User("old msg"), preserved User("recent msg"),
// preserved Assistant
let assistant_turn = &history.turns()[3];
- if let Turn::Assistant {
+ if let Message::Assistant {
provider_parts,
tool_calls,
content,
@@ -502,11 +560,11 @@ mod tests {
#[test]
fn compact_preserves_anthropic_thinking_blocks() {
let mut history = History::default();
- history.push(Turn::User {
+ history.push(Message::User {
content: "old msg".into(),
timestamp: SystemTime::now(),
});
- history.push(Turn::User {
+ history.push(Message::User {
content: "recent msg".into(),
timestamp: SystemTime::now(),
});
@@ -515,7 +573,7 @@ mod tests {
signature: Some("sig_xyz".into()),
redacted: false,
});
- history.push(Turn::Assistant {
+ history.push(Message::Assistant {
content: "answer".into(),
tool_calls: vec![],
provider_parts: vec![thinking],
@@ -529,7 +587,7 @@ mod tests {
// Layout: summary, extracted User("old msg"), preserved User("recent msg"),
// preserved Assistant
let assistant_turn = &history.turns()[3];
- if let Turn::Assistant { provider_parts, .. } = assistant_turn {
+ if let Message::Assistant { provider_parts, .. } = assistant_turn {
assert_eq!(
provider_parts.len(),
1,
@@ -544,13 +602,13 @@ mod tests {
#[test]
fn compact_strips_reasoning_from_all_preserved_assistant_turns() {
let mut history = History::default();
- history.push(Turn::User {
+ history.push(Message::User {
content: "old msg".into(),
timestamp: SystemTime::now(),
});
// Two assistant turns that will both be preserved
for i in 0..2 {
- history.push(Turn::Assistant {
+ history.push(Message::Assistant {
content: format!("response {i}"),
tool_calls: vec![],
provider_parts: vec![ContentPart::Other {
@@ -566,7 +624,7 @@ mod tests {
history.compact(2, "Summary".into());
for turn in history.turns() {
- if let Turn::Assistant { provider_parts, .. } = turn {
+ if let Message::Assistant { provider_parts, .. } = turn {
assert!(
provider_parts.is_empty(),
"all reasoning items should be stripped from all assistant turns"
@@ -578,11 +636,11 @@ mod tests {
#[test]
fn extract_recent_user_messages_collects_in_chronological_order() {
let turns = vec![
- Turn::User {
+ Message::User {
content: "first".into(),
timestamp: SystemTime::now(),
},
- Turn::Assistant {
+ Message::Assistant {
content: "reply".into(),
tool_calls: vec![],
provider_parts: vec![],
@@ -590,25 +648,25 @@ mod tests {
response_id: "r1".into(),
timestamp: SystemTime::now(),
},
- Turn::User {
+ Message::User {
content: "second".into(),
timestamp: SystemTime::now(),
},
];
let extracted = extract_recent_user_messages(turns, 20_000);
assert_eq!(extracted.len(), 2);
- assert!(matches!(&extracted[0], Turn::User { content, .. } if content == "first"));
- assert!(matches!(&extracted[1], Turn::User { content, .. } if content == "second"));
+ assert!(matches!(&extracted[0], Message::User { content, .. } if content == "first"));
+ assert!(matches!(&extracted[1], Message::User { content, .. } if content == "second"));
}
#[test]
fn extract_recent_user_messages_respects_token_budget() {
let turns = vec![
- Turn::User {
+ Message::User {
content: "a".repeat(100),
timestamp: SystemTime::now(),
},
- Turn::User {
+ Message::User {
content: "b".repeat(100),
timestamp: SystemTime::now(),
},
@@ -617,17 +675,17 @@ mod tests {
// exceed
let extracted = extract_recent_user_messages(turns, 30);
assert_eq!(extracted.len(), 1);
- assert!(matches!(&extracted[0], Turn::User { content, .. } if content.starts_with('b')));
+ assert!(matches!(&extracted[0], Message::User { content, .. } if content.starts_with('b')));
}
#[test]
fn compact_extracts_only_user_turns_from_discarded() {
let mut history = History::default();
- history.push(Turn::User {
+ history.push(Message::User {
content: "user msg".into(),
timestamp: SystemTime::now(),
});
- history.push(Turn::Assistant {
+ history.push(Message::Assistant {
content: "assistant msg".into(),
tool_calls: vec![],
provider_parts: vec![],
@@ -635,7 +693,7 @@ mod tests {
response_id: "r1".into(),
timestamp: SystemTime::now(),
});
- history.push(Turn::User {
+ history.push(Message::User {
content: "preserved".into(),
timestamp: SystemTime::now(),
});
@@ -644,10 +702,12 @@ mod tests {
// Layout: summary, extracted User("user msg"), preserved User("preserved")
assert_eq!(history.turns().len(), 3);
- assert!(matches!(&history.turns()[0], Turn::System { .. }));
- assert!(matches!(&history.turns()[1], Turn::User { content, .. } if content == "user msg"));
+ assert!(matches!(&history.turns()[0], Message::System { .. }));
assert!(
- matches!(&history.turns()[2], Turn::User { content, .. } if content == "preserved")
+ matches!(&history.turns()[1], Message::User { content, .. } if content == "user msg")
+ );
+ assert!(
+ matches!(&history.turns()[2], Message::User { content, .. } if content == "preserved")
);
}
}
diff --git a/lib/crates/fabro-agent/src/lib.rs b/lib/crates/fabro-agent/src/lib.rs
index db293eda5..765bec6be 100644
--- a/lib/crates/fabro-agent/src/lib.rs
+++ b/lib/crates/fabro-agent/src/lib.rs
@@ -59,7 +59,7 @@ pub use tools::{
make_shell_tool, make_shell_tool_with_config, make_write_file_tool, register_core_tools,
};
pub use truncation::{TruncationMode, truncate_lines, truncate_output, truncate_tool_output};
-pub use types::{AgentEvent, SessionEvent, SessionState, Turn};
+pub use types::{AgentEvent, Message, SessionEvent, SessionState};
#[cfg(test)]
#[allow(
diff --git a/lib/crates/fabro-agent/src/loop_detection.rs b/lib/crates/fabro-agent/src/loop_detection.rs
index 926bd2a3c..c81996c78 100644
--- a/lib/crates/fabro-agent/src/loop_detection.rs
+++ b/lib/crates/fabro-agent/src/loop_detection.rs
@@ -2,7 +2,7 @@ use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use crate::history::History;
-use crate::types::Turn;
+use crate::types::Message;
fn tool_call_signature(name: &str, arguments: &serde_json::Value) -> u64 {
let mut hasher = DefaultHasher::new();
@@ -12,8 +12,8 @@ fn tool_call_signature(name: &str, arguments: &serde_json::Value) -> u64 {
hasher.finish()
}
-fn extract_signatures_from_assistant(turn: &Turn) -> Vec {
- let Turn::Assistant { tool_calls, .. } = turn else {
+fn extract_signatures_from_assistant(turn: &Message) -> Vec {
+ let Message::Assistant { tool_calls, .. } = turn else {
return vec![];
};
tool_calls
@@ -101,8 +101,8 @@ mod tests {
use super::*;
- fn assistant_with_tool(name: &str, args: serde_json::Value) -> Turn {
- Turn::Assistant {
+ fn assistant_with_tool(name: &str, args: serde_json::Value) -> Message {
+ Message::Assistant {
content: String::new(),
tool_calls: vec![ToolCall::new("call_1", name, args)],
provider_parts: vec![],
@@ -265,15 +265,15 @@ mod tests {
#[test]
fn user_turns_are_ignored() {
let mut history = History::default();
- history.push(Turn::User {
+ history.push(Message::User {
content: "hello".into(),
timestamp: SystemTime::now(),
});
- history.push(Turn::User {
+ history.push(Message::User {
content: "hello".into(),
timestamp: SystemTime::now(),
});
- history.push(Turn::User {
+ history.push(Message::User {
content: "hello".into(),
timestamp: SystemTime::now(),
});
diff --git a/lib/crates/fabro-agent/src/memory.rs b/lib/crates/fabro-agent/src/memory.rs
index 10b201aaf..a86626846 100644
--- a/lib/crates/fabro-agent/src/memory.rs
+++ b/lib/crates/fabro-agent/src/memory.rs
@@ -1,6 +1,6 @@
use std::collections::HashSet;
-use fabro_model::Provider;
+use fabro_model::AgentProfileKind;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, warn};
@@ -13,22 +13,15 @@ pub async fn discover_memory(
env: &dyn Sandbox,
git_root: &str,
working_dir: &str,
- provider: Provider,
+ profile_kind: AgentProfileKind,
cancel_token: &CancellationToken,
) -> Result, Error> {
let directories = build_directory_walk(git_root, working_dir);
- let candidate_filenames: Vec<&str> = match provider {
- Provider::Anthropic | Provider::Vertex => vec!["AGENTS.md", "CLAUDE.md"],
- Provider::OpenAi
- | Provider::Kimi
- | Provider::Zai
- | Provider::Minimax
- | Provider::Inception
- | Provider::OpenAiCompatible => {
- vec!["AGENTS.md", ".codex/instructions.md"]
- }
- Provider::Gemini => vec!["AGENTS.md", "GEMINI.md"],
+ let candidate_filenames: Vec<&str> = match profile_kind {
+ AgentProfileKind::Anthropic => vec!["AGENTS.md", "CLAUDE.md"],
+ AgentProfileKind::OpenAi => vec!["AGENTS.md", ".codex/instructions.md"],
+ AgentProfileKind::Gemini => vec!["AGENTS.md", "GEMINI.md"],
};
let mut results = Vec::new();
@@ -145,7 +138,7 @@ mod tests {
env.as_ref(),
"/repo",
"/repo",
- Provider::Anthropic,
+ AgentProfileKind::Anthropic,
&CancellationToken::new(),
)
.await
@@ -170,7 +163,7 @@ mod tests {
env.as_ref(),
"/repo",
"/repo",
- Provider::Anthropic,
+ AgentProfileKind::Anthropic,
&CancellationToken::new(),
)
.await
@@ -187,7 +180,7 @@ mod tests {
env.as_ref(),
"/repo",
"/repo",
- Provider::OpenAi,
+ AgentProfileKind::OpenAi,
&CancellationToken::new(),
)
.await
@@ -204,7 +197,7 @@ mod tests {
env.as_ref(),
"/repo",
"/repo",
- Provider::Gemini,
+ AgentProfileKind::Gemini,
&CancellationToken::new(),
)
.await
@@ -231,7 +224,7 @@ mod tests {
env.as_ref(),
"/repo",
"/repo",
- Provider::Anthropic,
+ AgentProfileKind::Anthropic,
&CancellationToken::new(),
)
.await
@@ -256,7 +249,7 @@ mod tests {
env.as_ref(),
"/repo",
"/repo",
- Provider::Anthropic,
+ AgentProfileKind::Anthropic,
&CancellationToken::new(),
)
.await
@@ -278,7 +271,7 @@ mod tests {
env.as_ref(),
"/repo",
"/repo/src",
- Provider::Anthropic,
+ AgentProfileKind::Anthropic,
&CancellationToken::new(),
)
.await
@@ -302,7 +295,7 @@ mod tests {
env.as_ref(),
"/repo",
"/repo/src/app",
- Provider::Anthropic,
+ AgentProfileKind::Anthropic,
&CancellationToken::new(),
)
.await
diff --git a/lib/crates/fabro-agent/src/profiles/anthropic.rs b/lib/crates/fabro-agent/src/profiles/anthropic.rs
index 379fe7c07..6059cfec5 100644
--- a/lib/crates/fabro-agent/src/profiles/anthropic.rs
+++ b/lib/crates/fabro-agent/src/profiles/anthropic.rs
@@ -1,6 +1,6 @@
use std::sync::Arc;
-use fabro_model::{Catalog, Provider, ProviderId};
+use fabro_model::{AgentProfileKind, Catalog, ProviderId};
use super::EnvContext;
use crate::agent_profile::AgentProfile;
@@ -37,8 +37,8 @@ impl AnthropicProfile {
Self {
base: BaseProfile {
- provider: Provider::Anthropic,
- provider_id: Provider::Anthropic.id(),
+ profile_kind: AgentProfileKind::Anthropic,
+ provider_id: ProviderId::anthropic(),
model: model.into(),
catalog: None,
registry,
@@ -46,15 +46,6 @@ impl AnthropicProfile {
}
}
- /// Override the provider identity (e.g. for Kimi, which uses the Anthropic
- /// Messages protocol but routes to a different adapter).
- #[must_use]
- pub fn with_provider(mut self, provider: Provider) -> Self {
- self.base.provider = provider;
- self.base.provider_id = provider.id();
- self
- }
-
/// Override the provider ID while retaining the adapter/profile behavior.
#[must_use]
pub fn with_provider_id(mut self, provider_id: ProviderId) -> Self {
@@ -70,8 +61,8 @@ impl AnthropicProfile {
}
impl AgentProfile for AnthropicProfile {
- fn provider(&self) -> Provider {
- self.base.provider
+ fn profile_kind(&self) -> AgentProfileKind {
+ self.base.profile_kind
}
fn provider_id(&self) -> ProviderId {
@@ -193,7 +184,6 @@ in the project. Keep changes minimal and focused on the task.";
mod tests {
use std::sync::Arc;
- use fabro_model::catalog::LlmCatalogSettings;
use tokio::sync::Mutex as AsyncMutex;
use super::*;
@@ -201,13 +191,14 @@ mod tests {
use crate::test_support::MockSandbox;
fn test_catalog() -> Arc {
- Arc::new(Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default()).unwrap())
+ Arc::new(Catalog::from_builtin().unwrap())
}
#[test]
fn anthropic_profile_identity() {
let profile = AnthropicProfile::new("claude-sonnet-4-20250514");
- assert_eq!(profile.provider(), Provider::Anthropic);
+ assert_eq!(profile.profile_kind(), AgentProfileKind::Anthropic);
+ assert_eq!(profile.provider_id(), ProviderId::anthropic());
assert_eq!(profile.model(), "claude-sonnet-4-20250514");
}
diff --git a/lib/crates/fabro-agent/src/profiles/gemini.rs b/lib/crates/fabro-agent/src/profiles/gemini.rs
index 9ba16df92..b726b4d2a 100644
--- a/lib/crates/fabro-agent/src/profiles/gemini.rs
+++ b/lib/crates/fabro-agent/src/profiles/gemini.rs
@@ -1,6 +1,6 @@
use std::sync::Arc;
-use fabro_model::{Catalog, Provider, ProviderId};
+use fabro_model::{AgentProfileKind, Catalog, ProviderId};
use super::EnvContext;
use crate::agent_profile::AgentProfile;
@@ -39,8 +39,8 @@ impl GeminiProfile {
Self {
base: BaseProfile {
- provider: Provider::Gemini,
- provider_id: Provider::Gemini.id(),
+ profile_kind: AgentProfileKind::Gemini,
+ provider_id: ProviderId::gemini(),
model: model.into(),
catalog: None,
registry,
@@ -48,14 +48,6 @@ impl GeminiProfile {
}
}
- /// Override the provider identity.
- #[must_use]
- pub fn with_provider(mut self, provider: Provider) -> Self {
- self.base.provider = provider;
- self.base.provider_id = provider.id();
- self
- }
-
/// Override the provider ID while retaining the adapter/profile behavior.
#[must_use]
pub fn with_provider_id(mut self, provider_id: ProviderId) -> Self {
@@ -71,8 +63,8 @@ impl GeminiProfile {
}
impl AgentProfile for GeminiProfile {
- fn provider(&self) -> Provider {
- self.base.provider
+ fn profile_kind(&self) -> AgentProfileKind {
+ self.base.profile_kind
}
fn provider_id(&self) -> ProviderId {
@@ -235,7 +227,6 @@ in the project.";
mod tests {
use std::sync::Arc;
- use fabro_model::catalog::LlmCatalogSettings;
use tokio::sync::Mutex as AsyncMutex;
use super::*;
@@ -243,13 +234,14 @@ mod tests {
use crate::test_support::MockSandbox;
fn test_catalog() -> Arc {
- Arc::new(Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default()).unwrap())
+ Arc::new(Catalog::from_builtin().unwrap())
}
#[test]
fn gemini_profile_identity() {
let profile = GeminiProfile::new("gemini-2.0-flash");
- assert_eq!(profile.provider(), Provider::Gemini);
+ assert_eq!(profile.profile_kind(), AgentProfileKind::Gemini);
+ assert_eq!(profile.provider_id(), ProviderId::gemini());
assert_eq!(profile.model(), "gemini-2.0-flash");
}
diff --git a/lib/crates/fabro-agent/src/profiles/mod.rs b/lib/crates/fabro-agent/src/profiles/mod.rs
index 72c9cceb6..fbc8de5e9 100644
--- a/lib/crates/fabro-agent/src/profiles/mod.rs
+++ b/lib/crates/fabro-agent/src/profiles/mod.rs
@@ -1,6 +1,6 @@
use std::sync::Arc;
-use fabro_model::{Catalog, Provider, ProviderId};
+use fabro_model::{AgentProfileKind, Catalog, ProviderId};
pub mod anthropic;
pub mod gemini;
@@ -16,14 +16,14 @@ use crate::tool_registry::ToolRegistry;
/// Common fields shared by all provider profiles.
///
-/// Each concrete profile embeds this struct and delegates `provider()`,
+/// Each concrete profile embeds this struct and delegates `profile_kind()`,
/// `model()`, `tool_registry()`, and `tool_registry_mut()` to it.
pub struct BaseProfile {
- pub provider: Provider,
- pub provider_id: ProviderId,
- pub model: String,
- pub catalog: Option>,
- pub registry: ToolRegistry,
+ pub profile_kind: AgentProfileKind,
+ pub provider_id: ProviderId,
+ pub model: String,
+ pub catalog: Option>,
+ pub registry: ToolRegistry,
}
/// Additional context for building environment blocks
diff --git a/lib/crates/fabro-agent/src/profiles/openai.rs b/lib/crates/fabro-agent/src/profiles/openai.rs
index 90e382045..c03685f86 100644
--- a/lib/crates/fabro-agent/src/profiles/openai.rs
+++ b/lib/crates/fabro-agent/src/profiles/openai.rs
@@ -1,6 +1,6 @@
use std::sync::Arc;
-use fabro_model::{Catalog, Provider, ProviderId};
+use fabro_model::{AgentProfileKind, Catalog, ProviderId};
use super::EnvContext;
use crate::agent_profile::AgentProfile;
@@ -35,8 +35,8 @@ impl OpenAiProfile {
Self {
base: BaseProfile {
- provider: Provider::OpenAi,
- provider_id: Provider::OpenAi.id(),
+ profile_kind: AgentProfileKind::OpenAi,
+ provider_id: ProviderId::openai(),
model: model.into(),
catalog: None,
registry,
@@ -44,15 +44,6 @@ impl OpenAiProfile {
}
}
- /// Override the provider identity (e.g. for Z.AI or Minimax, which use the
- /// OpenAI Chat Completions protocol but route to different adapters).
- #[must_use]
- pub fn with_provider(mut self, provider: Provider) -> Self {
- self.base.provider = provider;
- self.base.provider_id = provider.id();
- self
- }
-
/// Override the provider ID while retaining the adapter/profile behavior.
#[must_use]
pub fn with_provider_id(mut self, provider_id: ProviderId) -> Self {
@@ -72,15 +63,15 @@ impl OpenAiProfile {
.as_ref()
.and_then(|catalog| catalog.provider(&self.base.provider_id))
.map_or_else(
- || self.base.provider.display_name().to_string(),
+ || self.base.provider_id.display_name(),
|provider| provider.display_name.clone(),
)
}
}
impl AgentProfile for OpenAiProfile {
- fn provider(&self) -> Provider {
- self.base.provider
+ fn profile_kind(&self) -> AgentProfileKind {
+ self.base.profile_kind
}
fn provider_id(&self) -> ProviderId {
@@ -226,7 +217,6 @@ in the project.");
mod tests {
use std::sync::Arc;
- use fabro_model::catalog::LlmCatalogSettings;
use tokio::sync::Mutex as AsyncMutex;
use super::*;
@@ -234,13 +224,14 @@ mod tests {
use crate::test_support::MockSandbox;
fn test_catalog() -> Arc {
- Arc::new(Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default()).unwrap())
+ Arc::new(Catalog::from_builtin().unwrap())
}
#[test]
fn openai_profile_identity() {
let profile = OpenAiProfile::new("o3-mini");
- assert_eq!(profile.provider(), Provider::OpenAi);
+ assert_eq!(profile.profile_kind(), AgentProfileKind::OpenAi);
+ assert_eq!(profile.provider_id(), ProviderId::openai());
assert_eq!(profile.model(), "o3-mini");
}
@@ -249,7 +240,7 @@ mod tests {
let profile = OpenAiProfile::new("o3-mini");
let env = MockSandbox::linux();
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
- assert!(prompt.contains("You are a coding agent powered by OpenAI"));
+ assert!(prompt.contains("You are a coding agent powered by openai"));
assert!(prompt.contains(""));
assert!(prompt.contains("linux"));
assert!(prompt.contains("v4a patch format"));
@@ -333,7 +324,7 @@ mod tests {
#[test]
fn kimi_provider_prompt_uses_catalog_display_name() {
let profile = OpenAiProfile::new("kimi-k2.5")
- .with_provider(Provider::Kimi)
+ .with_provider_id(ProviderId::new("kimi"))
.with_catalog(test_catalog());
let env = MockSandbox::linux();
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
@@ -344,7 +335,7 @@ mod tests {
#[test]
fn zai_provider_prompt_uses_catalog_display_name() {
let profile = OpenAiProfile::new("glm-4.7")
- .with_provider(Provider::Zai)
+ .with_provider_id(ProviderId::new("zai"))
.with_catalog(test_catalog());
let env = MockSandbox::linux();
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
@@ -354,7 +345,7 @@ mod tests {
#[test]
fn minimax_provider_prompt_uses_catalog_display_name() {
let profile = OpenAiProfile::new("minimax-m2.5")
- .with_provider(Provider::Minimax)
+ .with_provider_id(ProviderId::new("minimax"))
.with_catalog(test_catalog());
let env = MockSandbox::linux();
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
@@ -364,7 +355,7 @@ mod tests {
#[test]
fn inception_provider_prompt_uses_catalog_display_name() {
let profile = OpenAiProfile::new("mercury-2")
- .with_provider(Provider::Inception)
+ .with_provider_id(ProviderId::new("inception"))
.with_catalog(test_catalog());
let env = MockSandbox::linux();
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None, &[]);
diff --git a/lib/crates/fabro-agent/src/session.rs b/lib/crates/fabro-agent/src/session.rs
index b67426159..ce73a8fcc 100644
--- a/lib/crates/fabro-agent/src/session.rs
+++ b/lib/crates/fabro-agent/src/session.rs
@@ -8,13 +8,14 @@ use fabro_llm::error::ProviderErrorKind;
use fabro_llm::generate::StreamAccumulator;
use fabro_llm::provider::StreamEventStream;
use fabro_llm::types::{
- ContentPart, Message, ReasoningEffort, Request, RetryPolicy, StreamEvent, ToolChoice,
+ ContentPart, Message as LlmMessage, ReasoningEffort, Request, RetryPolicy, StreamEvent,
+ ToolChoice,
};
use fabro_llm::{Error as LlmError, retry};
use fabro_mcp::config::{McpServerSettings, McpTransport};
use fabro_mcp::connection_manager::McpConnectionManager;
-use fabro_model::{Catalog, ModelRef, Provider, Speed};
-use fabro_types::Principal;
+use fabro_model::{AgentProfileKind, Catalog, ModelRef, Speed};
+use fabro_types::{Principal, SessionRecord, SessionStatus};
use futures::StreamExt;
use tokio::sync::{Mutex as AsyncMutex, Notify, broadcast};
use tokio::time;
@@ -38,7 +39,7 @@ use crate::skills::{
};
use crate::subagent::{SubAgentCallbackEvent, SubAgentEventCallback, SubAgentManager};
use crate::tool_execution::execute_tool_calls;
-use crate::types::{AgentEvent, SessionEvent, SessionState, Turn};
+use crate::types::{AgentEvent, Message, SessionEvent, SessionState};
/// One queued steering message: text + the principal that authored it (None
/// for direct internal callers like loop-detection).
@@ -330,6 +331,49 @@ impl Session {
))
}
+ pub fn from_record(
+ record: &SessionRecord,
+ llm_client: Client,
+ provider_profile: Arc,
+ sandbox: Arc,
+ config: SessionOptions,
+ subagent_manager: Option>>,
+ ) -> Result {
+ let mut session = Self::new(
+ llm_client,
+ provider_profile,
+ sandbox,
+ config,
+ subagent_manager,
+ );
+ session.id = record.id.to_string();
+ session.history =
+ History::from_session_messages(&record.runtime_context).map_err(|err| {
+ Error::InvalidState(format!("invalid persisted session context: {err}"))
+ })?;
+ session.state = match record.status {
+ SessionStatus::Closed | SessionStatus::Deleted => SessionState::Closed,
+ SessionStatus::Running | SessionStatus::Failed | SessionStatus::Idle => {
+ SessionState::Idle
+ }
+ };
+ Ok(session)
+ }
+
+ #[must_use]
+ pub fn to_record(&self, mut record: SessionRecord) -> SessionRecord {
+ record.status = match self.state {
+ SessionState::Closed => SessionStatus::Closed,
+ SessionState::Thinking | SessionState::Executing => SessionStatus::Running,
+ SessionState::Idle => SessionStatus::Idle,
+ };
+ record.provider = Some(self.provider_id().to_string());
+ record.model = Some(self.model().to_string());
+ record.runtime_context = self.history.to_session_messages();
+ record.updated_at = chrono::Utc::now();
+ record
+ }
+
pub fn set_tool_env_provider(&mut self, provider: Arc) {
self.tool_env_provider = Some(provider);
}
@@ -344,8 +388,8 @@ impl Session {
}
#[must_use]
- pub fn provider(&self) -> Provider {
- self.provider_profile.provider()
+ pub fn profile_kind(&self) -> AgentProfileKind {
+ self.provider_profile.profile_kind()
}
#[must_use]
@@ -387,7 +431,7 @@ impl Session {
self.sandbox.as_ref(),
&doc_root,
self.sandbox.working_directory(),
- self.provider_profile.provider(),
+ self.provider_profile.profile_kind(),
&cancel_token,
)
.await?;
@@ -1048,7 +1092,7 @@ impl Session {
let expanded_input = expanded.text;
// Append user turn and emit event
- self.history.push(Turn::User {
+ self.history.push(Message::User {
content: expanded_input.clone(),
timestamp: SystemTime::now(),
});
@@ -1317,7 +1361,7 @@ impl Session {
.collect();
let usage = response.usage.clone();
- self.history.push(Turn::Assistant {
+ self.history.push(Message::Assistant {
content: text.clone(),
tool_calls: tool_calls.clone(),
provider_parts,
@@ -1402,7 +1446,7 @@ impl Session {
// Always append tool_results so the tool_use ↔ tool_result
// invariant holds, regardless of which token fired.
- self.history.push(Turn::ToolResults {
+ self.history.push(Message::ToolResults {
results,
timestamp: SystemTime::now(),
});
@@ -1426,7 +1470,7 @@ impl Session {
if self.config.enable_loop_detection
&& detect_loop(&self.history, self.config.loop_detection_window)
{
- self.history.push(Turn::Steering {
+ self.history.push(Message::Steering {
content: "WARNING: Loop detected. You appear to be repeating the same tool calls. Please try a different approach or ask for clarification.".to_string(),
timestamp: SystemTime::now(),
});
@@ -1476,7 +1520,7 @@ impl Session {
control.queue.drain(..).collect()
};
for (text, actor) in messages {
- self.history.push(Turn::Steering {
+ self.history.push(Message::Steering {
content: text.clone(),
timestamp: SystemTime::now(),
});
@@ -1516,7 +1560,7 @@ impl Session {
fn build_request(&self) -> Request {
let mut messages = Vec::new();
if !self.system_prompt.trim().is_empty() {
- messages.push(Message::system(self.system_prompt.clone()));
+ messages.push(LlmMessage::system(self.system_prompt.clone()));
}
messages.extend(self.history.convert_to_messages());
@@ -1710,8 +1754,10 @@ mod tests {
let turns = session.history().turns();
// UserTurn + AssistantTurn = 2
assert_eq!(turns.len(), 2);
- assert!(matches!(&turns[0], Turn::User { content, .. } if content == "Hi"));
- assert!(matches!(&turns[1], Turn::Assistant { content, .. } if content == "Hello there!"));
+ assert!(matches!(&turns[0], Message::User { content, .. } if content == "Hi"));
+ assert!(
+ matches!(&turns[1], Message::Assistant { content, .. } if content == "Hello there!")
+ );
}
#[tokio::test]
@@ -1731,13 +1777,15 @@ mod tests {
let turns = session.history().turns();
// UserTurn + AssistantTurn(tool_call) + ToolResults + AssistantTurn(text) = 4
assert_eq!(turns.len(), 4);
- assert!(matches!(&turns[0], Turn::User { .. }));
- assert!(matches!(&turns[1], Turn::Assistant { tool_calls, .. } if tool_calls.len() == 1));
- assert!(matches!(&turns[2], Turn::ToolResults { results, .. } if results.len() == 1));
- assert!(matches!(&turns[3], Turn::Assistant { content, .. } if content == "Done!"));
+ assert!(matches!(&turns[0], Message::User { .. }));
+ assert!(
+ matches!(&turns[1], Message::Assistant { tool_calls, .. } if tool_calls.len() == 1)
+ );
+ assert!(matches!(&turns[2], Message::ToolResults { results, .. } if results.len() == 1));
+ assert!(matches!(&turns[3], Message::Assistant { content, .. } if content == "Done!"));
// Verify tool result content
- if let Turn::ToolResults { results, .. } = &turns[2] {
+ if let Message::ToolResults { results, .. } = &turns[2] {
assert_eq!(results[0].tool_call_id, "call_1");
assert!(!results[0].is_error);
}
@@ -1870,11 +1918,11 @@ mod tests {
let turns = session.history().turns();
// User + Steering + Assistant = 3
assert_eq!(turns.len(), 3);
- assert!(matches!(&turns[0], Turn::User { .. }));
+ assert!(matches!(&turns[0], Message::User { .. }));
assert!(
- matches!(&turns[1], Turn::Steering { content, .. } if content == "Focus on the task")
+ matches!(&turns[1], Message::Steering { content, .. } if content == "Focus on the task")
);
- assert!(matches!(&turns[2], Turn::Assistant { .. }));
+ assert!(matches!(&turns[2], Message::Assistant { .. }));
}
#[tokio::test]
@@ -1924,7 +1972,7 @@ mod tests {
.unwrap();
let turns = session.history().turns();
- assert!(matches!(&turns[1], Turn::Steering { content, .. } if content == "resume now"));
+ assert!(matches!(&turns[1], Message::Steering { content, .. } if content == "resume now"));
assert!(!handle.is_waiting_for_steer());
}
@@ -1989,11 +2037,13 @@ mod tests {
let turns = session.history().turns();
// User + Assistant + Steering + Assistant = 4
assert_eq!(turns.len(), 4);
- assert!(matches!(&turns[0], Turn::User { .. }));
- assert!(matches!(&turns[1], Turn::Assistant { content, .. } if content == "First reply"));
- assert!(matches!(&turns[2], Turn::Steering { content, .. }
+ assert!(matches!(&turns[0], Message::User { .. }));
+ assert!(
+ matches!(&turns[1], Message::Assistant { content, .. } if content == "First reply")
+ );
+ assert!(matches!(&turns[2], Message::Steering { content, .. }
if content == "after-completion steer"));
- assert!(matches!(&turns[3], Turn::Assistant { content, .. }
+ assert!(matches!(&turns[3], Message::Assistant { content, .. }
if content == "Second reply, after steer"));
}
@@ -2013,13 +2063,15 @@ mod tests {
// Second cycle: User + Assistant = 2
// Total = 4
assert_eq!(turns.len(), 4);
- assert!(matches!(&turns[0], Turn::User { content, .. } if content == "initial message"));
+ assert!(matches!(&turns[0], Message::User { content, .. } if content == "initial message"));
assert!(
- matches!(&turns[1], Turn::Assistant { content, .. } if content == "First response")
+ matches!(&turns[1], Message::Assistant { content, .. } if content == "First response")
);
- assert!(matches!(&turns[2], Turn::User { content, .. } if content == "followup message"));
assert!(
- matches!(&turns[3], Turn::Assistant { content, .. } if content == "Followup response")
+ matches!(&turns[2], Message::User { content, .. } if content == "followup message")
+ );
+ assert!(
+ matches!(&turns[3], Message::Assistant { content, .. } if content == "Followup response")
);
}
@@ -2105,7 +2157,7 @@ mod tests {
let turns = session.history().turns();
// User + Asst(tool_call) + ToolResults + Asst(text) = 4
assert_eq!(turns.len(), 4);
- if let Turn::ToolResults { results, .. } = &turns[2] {
+ if let Message::ToolResults { results, .. } = &turns[2] {
assert!(results[0].is_error);
assert_eq!(
results[0].content,
@@ -2130,7 +2182,7 @@ mod tests {
session.process_input("Use fail tool").await.unwrap();
let turns = session.history().turns();
- if let Turn::ToolResults { results, .. } = &turns[2] {
+ if let Message::ToolResults { results, .. } = &turns[2] {
assert!(results[0].is_error);
assert_eq!(
results[0].content,
@@ -2176,7 +2228,7 @@ mod tests {
// Check for Steering turn with warning in history
let has_steering_warning = session.history().turns().iter().any(
- |t| matches!(t, Turn::Steering { content, .. } if content.contains("Loop detected")),
+ |t| matches!(t, Message::Steering { content, .. } if content.contains("Loop detected")),
);
assert!(has_steering_warning);
}
@@ -2208,7 +2260,7 @@ mod tests {
// Should have stopped immediately: User turn only, no LLM call
let turns = session.history().turns();
assert_eq!(turns.len(), 1);
- assert!(matches!(&turns[0], Turn::User { .. }));
+ assert!(matches!(&turns[0], Message::User { .. }));
}
#[tokio::test]
@@ -2263,9 +2315,11 @@ mod tests {
// The tool cancelled the token, so the loop breaks before the next LLM call
let turns = session.history().turns();
assert_eq!(turns.len(), 3);
- assert!(matches!(&turns[0], Turn::User { .. }));
- assert!(matches!(&turns[1], Turn::Assistant { tool_calls, .. } if tool_calls.len() == 1));
- assert!(matches!(&turns[2], Turn::ToolResults { .. }));
+ assert!(matches!(&turns[0], Message::User { .. }));
+ assert!(
+ matches!(&turns[1], Message::Assistant { tool_calls, .. } if tool_calls.len() == 1)
+ );
+ assert!(matches!(&turns[2], Message::ToolResults { .. }));
}
#[tokio::test]
@@ -2301,10 +2355,10 @@ mod tests {
let turns = session.history().turns();
assert_eq!(turns.len(), 4);
- assert!(matches!(&turns[0], Turn::User { content, .. } if content == "one"));
- assert!(matches!(&turns[1], Turn::Assistant { content, .. } if content == "First"));
- assert!(matches!(&turns[2], Turn::User { content, .. } if content == "two"));
- assert!(matches!(&turns[3], Turn::Assistant { content, .. } if content == "Second"));
+ assert!(matches!(&turns[0], Message::User { content, .. } if content == "one"));
+ assert!(matches!(&turns[1], Message::Assistant { content, .. } if content == "First"));
+ assert!(matches!(&turns[2], Message::User { content, .. } if content == "two"));
+ assert!(matches!(&turns[3], Message::Assistant { content, .. } if content == "Second"));
}
#[tokio::test]
@@ -2386,7 +2440,7 @@ mod tests {
assert_eq!(turns.len(), 4);
// Verify all 3 tool results collected
- if let Turn::ToolResults { results, .. } = &turns[2] {
+ if let Message::ToolResults { results, .. } = &turns[2] {
assert_eq!(results.len(), 3);
assert_eq!(results[0].tool_call_id, "call_1");
assert_eq!(results[1].tool_call_id, "call_2");
@@ -2515,7 +2569,7 @@ mod tests {
session.process_input("Use strict tool").await.unwrap();
let turns = session.history().turns();
- if let Turn::ToolResults { results, .. } = &turns[2] {
+ if let Message::ToolResults { results, .. } = &turns[2] {
assert!(results[0].is_error);
let content_str = results[0].content.to_string();
assert!(
@@ -2560,7 +2614,7 @@ mod tests {
session.process_input("Use strict tool").await.unwrap();
let turns = session.history().turns();
- if let Turn::ToolResults { results, .. } = &turns[2] {
+ if let Message::ToolResults { results, .. } = &turns[2] {
assert!(!results[0].is_error);
} else {
panic!("Expected ToolResults turn at index 2");
@@ -2677,7 +2731,7 @@ mod tests {
// User + Assistant(tool_call) + ToolResults + Assistant(text) = 4
assert_eq!(turns.len(), 4);
- if let Turn::ToolResults { results, .. } = &turns[2] {
+ if let Message::ToolResults { results, .. } = &turns[2] {
assert!(results[0].is_error);
let content_str = results[0].content.to_string();
assert!(
@@ -2689,7 +2743,7 @@ mod tests {
}
assert!(
- matches!(&turns[3], Turn::Assistant { content, .. } if content == "OK after denial")
+ matches!(&turns[3], Message::Assistant { content, .. } if content == "OK after denial")
);
}
@@ -2714,7 +2768,7 @@ mod tests {
session.process_input("Use echo").await.unwrap();
let turns = session.history().turns();
- if let Turn::ToolResults { results, .. } = &turns[2] {
+ if let Message::ToolResults { results, .. } = &turns[2] {
assert!(!results[0].is_error);
let content_str = results[0].content.to_string();
assert!(
@@ -2779,7 +2833,7 @@ mod tests {
session.process_input("Use echo").await.unwrap();
let turns = session.history().turns();
- if let Turn::ToolResults { results, .. } = &turns[2] {
+ if let Message::ToolResults { results, .. } = &turns[2] {
assert!(!results[0].is_error);
let content_str = results[0].content.to_string();
assert!(
@@ -2912,7 +2966,7 @@ mod tests {
let turns = session.history().turns();
assert!(matches!(
turns.last(),
- Some(Turn::Assistant { content, .. }) if content == "Recovered"
+ Some(Message::Assistant { content, .. }) if content == "Recovered"
));
let mut assistant_text_start_count = 0;
@@ -2950,7 +3004,7 @@ mod tests {
let turns = session.history().turns();
assert!(matches!(
turns.last(),
- Some(Turn::Assistant { content, .. }) if content == "Hello"
+ Some(Message::Assistant { content, .. }) if content == "Hello"
));
let mut observed = Vec::new();
@@ -3084,7 +3138,7 @@ mod tests {
// History should have been compacted: summary turn + preserved turns
let turns = session.history().turns();
assert!(
- turns.iter().any(|t| matches!(t, Turn::System { content, .. } if content.contains("A different assistant began this task"))),
+ turns.iter().any(|t| matches!(t, Message::System { content, .. } if content.contains("A different assistant began this task"))),
"Should contain a summary system turn"
);
}
@@ -3425,15 +3479,17 @@ mod tests {
4,
"Expected User + Assistant(tool) + ToolResults + Assistant(text)"
);
- assert!(matches!(&turns[0], Turn::User { .. }));
- assert!(matches!(&turns[1], Turn::Assistant { tool_calls, .. } if tool_calls.len() == 1));
- assert!(matches!(&turns[2], Turn::ToolResults { results, .. } if results.len() == 1));
+ assert!(matches!(&turns[0], Message::User { .. }));
assert!(
- matches!(&turns[3], Turn::Assistant { content, .. } if content == "The echo server replied!")
+ matches!(&turns[1], Message::Assistant { tool_calls, .. } if tool_calls.len() == 1)
+ );
+ assert!(matches!(&turns[2], Message::ToolResults { results, .. } if results.len() == 1));
+ assert!(
+ matches!(&turns[3], Message::Assistant { content, .. } if content == "The echo server replied!")
);
// Verify the MCP tool result content — the echo server returns the message
- if let Turn::ToolResults { results, .. } = &turns[2] {
+ if let Message::ToolResults { results, .. } = &turns[2] {
assert_eq!(results[0].tool_call_id, "mcp_call_1");
assert!(!results[0].is_error);
let output = results[0].content.as_str().unwrap_or("");
@@ -3533,7 +3589,9 @@ mod tests {
assert_eq!(session.state(), SessionState::Idle);
let turns = session.history().turns();
assert_eq!(turns.len(), 2);
- assert!(matches!(&turns[1], Turn::Assistant { content, .. } if content == "Fast response"));
+ assert!(
+ matches!(&turns[1], Message::Assistant { content, .. } if content == "Fast response")
+ );
}
#[tokio::test]
diff --git a/lib/crates/fabro-agent/src/subagent.rs b/lib/crates/fabro-agent/src/subagent.rs
index 04a58d18b..4b08a5b18 100644
--- a/lib/crates/fabro-agent/src/subagent.rs
+++ b/lib/crates/fabro-agent/src/subagent.rs
@@ -10,7 +10,7 @@ use crate::error::Error;
use crate::session::Session;
use crate::tool_registry::RegisteredTool;
use crate::tools::required_str;
-use crate::types::{AgentEvent, SessionEvent, Turn};
+use crate::types::{AgentEvent, Message, SessionEvent};
pub type SessionFactory = Arc Session + Send + Sync>;
@@ -115,7 +115,7 @@ impl SubAgentManager {
session.process_input(&task_prompt_for_spawn).await?;
let turns = session.history().turns();
let last_text = turns.iter().rev().find_map(|t| match t {
- Turn::Assistant { content, .. } => Some(content.clone()),
+ Message::Assistant { content, .. } => Some(content.clone()),
_ => None,
});
Ok(SubAgentResult {
diff --git a/lib/crates/fabro-agent/src/test_support.rs b/lib/crates/fabro-agent/src/test_support.rs
index d7284bc11..06c75a163 100644
--- a/lib/crates/fabro-agent/src/test_support.rs
+++ b/lib/crates/fabro-agent/src/test_support.rs
@@ -9,7 +9,7 @@ use fabro_llm::provider::{ProviderAdapter, StreamEventStream};
use fabro_llm::types::{
ContentPart, FinishReason, Message, Request, Response, StreamEvent, TokenCounts,
};
-use fabro_model::Provider;
+use fabro_model::{AgentProfileKind, ProviderId};
pub use fabro_sandbox::test_support::{MockSandbox, MutableMockSandbox};
use futures::stream;
@@ -52,8 +52,12 @@ impl TestProfile {
}
impl AgentProfile for TestProfile {
- fn provider(&self) -> Provider {
- Provider::Anthropic
+ fn profile_kind(&self) -> AgentProfileKind {
+ AgentProfileKind::Anthropic
+ }
+
+ fn provider_id(&self) -> ProviderId {
+ ProviderId::anthropic()
}
fn model(&self) -> &'static str {
@@ -189,8 +193,8 @@ pub fn text_response(text: &str) -> Response {
pub async fn make_client(provider: Arc) -> Client {
let mut providers = HashMap::new();
providers.insert(provider.name().to_string(), provider.clone());
- // Also register under "anthropic" so TestProfile (Provider::Anthropic) routes
- // correctly
+ // Also register under "anthropic" so TestProfile (ProviderId::anthropic())
+ // routes correctly
providers.insert("anthropic".to_string(), provider);
Client::new(providers, Some("mock".into()), vec![])
}
diff --git a/lib/crates/fabro-agent/src/tools.rs b/lib/crates/fabro-agent/src/tools.rs
index 719799fd6..a01850738 100644
--- a/lib/crates/fabro-agent/src/tools.rs
+++ b/lib/crates/fabro-agent/src/tools.rs
@@ -686,6 +686,7 @@ mod tests {
use std::collections::HashMap;
use fabro_llm::provider::ProviderAdapter;
+ use fabro_model::ProviderId;
use fabro_types::CommandTermination;
use tokio_util::sync::CancellationToken;
@@ -1410,7 +1411,7 @@ mod tests {
let summarizer = WebFetchSummarizer {
client,
model_id: ModelHandle::ByName {
- provider: fabro_model::Provider::Anthropic.id(),
+ provider: ProviderId::anthropic(),
model: "mock-model".to_string(),
},
};
@@ -1511,7 +1512,7 @@ mod tests {
let summarizer = WebFetchSummarizer {
client,
model_id: ModelHandle::ByName {
- provider: fabro_model::Provider::Anthropic.id(),
+ provider: ProviderId::anthropic(),
model: "target-model".to_string(),
},
};
diff --git a/lib/crates/fabro-agent/src/types.rs b/lib/crates/fabro-agent/src/types.rs
index 925b3c12a..fe2350d04 100644
--- a/lib/crates/fabro-agent/src/types.rs
+++ b/lib/crates/fabro-agent/src/types.rs
@@ -1,8 +1,11 @@
use std::time::SystemTime;
+use chrono::{DateTime, Utc};
use fabro_llm::Error as LlmError;
use fabro_llm::types::{ContentPart, ThinkingData, TokenCounts, ToolCall, ToolResult};
use fabro_model::ModelRef;
+use fabro_types::SessionMessage;
+use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use crate::error::Error;
@@ -33,7 +36,7 @@ mod system_time_iso8601 {
}
#[derive(Debug, Clone)]
-pub enum Turn {
+pub enum Message {
User {
content: String,
timestamp: SystemTime,
@@ -69,7 +72,7 @@ pub enum Turn {
},
}
-impl Turn {
+impl Message {
/// Extract the first non-redacted thinking/reasoning text from an
/// `Assistant` turn's `provider_parts`, if any.
#[must_use]
@@ -86,6 +89,101 @@ impl Turn {
_ => None,
})
}
+
+ #[must_use]
+ pub fn to_session_message(&self) -> SessionMessage {
+ match self {
+ Self::User { content, timestamp } => SessionMessage::User {
+ content: content.clone(),
+ timestamp: system_time_to_utc(*timestamp),
+ },
+ Self::Assistant {
+ content,
+ tool_calls,
+ provider_parts,
+ usage,
+ response_id,
+ timestamp,
+ } => SessionMessage::Assistant {
+ content: content.clone(),
+ tool_calls: values_or_empty(tool_calls),
+ provider_parts: values_or_empty(provider_parts),
+ usage: value_or_null(&**usage),
+ response_id: response_id.clone(),
+ timestamp: system_time_to_utc(*timestamp),
+ },
+ Self::ToolResults { results, timestamp } => SessionMessage::ToolResults {
+ results: values_or_empty(results),
+ timestamp: system_time_to_utc(*timestamp),
+ },
+ Self::System { content, timestamp } => SessionMessage::System {
+ content: content.clone(),
+ timestamp: system_time_to_utc(*timestamp),
+ },
+ Self::Steering { content, timestamp } => SessionMessage::Steering {
+ content: content.clone(),
+ timestamp: system_time_to_utc(*timestamp),
+ },
+ }
+ }
+
+ pub fn from_session_message(message: &SessionMessage) -> Result {
+ Ok(match message {
+ SessionMessage::User { content, timestamp } => Self::User {
+ content: content.clone(),
+ timestamp: utc_to_system_time(*timestamp),
+ },
+ SessionMessage::Assistant {
+ content,
+ tool_calls,
+ provider_parts,
+ usage,
+ response_id,
+ timestamp,
+ } => Self::Assistant {
+ content: content.clone(),
+ tool_calls: values_from_json(tool_calls)?,
+ provider_parts: values_from_json(provider_parts)?,
+ usage: Box::new(serde_json::from_value(usage.clone())?),
+ response_id: response_id.clone(),
+ timestamp: utc_to_system_time(*timestamp),
+ },
+ SessionMessage::ToolResults { results, timestamp } => Self::ToolResults {
+ results: values_from_json(results)?,
+ timestamp: utc_to_system_time(*timestamp),
+ },
+ SessionMessage::System { content, timestamp } => Self::System {
+ content: content.clone(),
+ timestamp: utc_to_system_time(*timestamp),
+ },
+ SessionMessage::Steering { content, timestamp } => Self::Steering {
+ content: content.clone(),
+ timestamp: utc_to_system_time(*timestamp),
+ },
+ })
+ }
+}
+
+fn system_time_to_utc(timestamp: SystemTime) -> DateTime {
+ timestamp.into()
+}
+
+fn utc_to_system_time(timestamp: DateTime) -> SystemTime {
+ timestamp.into()
+}
+
+fn value_or_null(value: &T) -> serde_json::Value {
+ serde_json::to_value(value).unwrap_or(serde_json::Value::Null)
+}
+
+fn values_or_empty(values: &[T]) -> Vec {
+ values.iter().map(value_or_null).collect()
+}
+
+fn values_from_json(
+ values: &[serde_json::Value],
+) -> Result, serde_json::Error> {
+ values.iter().cloned().map(serde_json::from_value).collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -311,7 +409,7 @@ impl AgentEvent {
warn!(session_id, "Loop detected");
}
Self::TurnLimitReached { max_turns } => {
- warn!(session_id, max_turns, "Turn limit reached");
+ warn!(session_id, max_turns, "Message limit reached");
}
Self::SkillExpanded { skill_name } => {
debug!(session_id, skill = skill_name.as_str(), "Skill expanded");
@@ -429,7 +527,7 @@ pub struct SessionEvent {
#[cfg(test)]
mod tests {
- use fabro_model::Provider;
+ use fabro_model::ProviderId;
use super::*;
@@ -671,7 +769,7 @@ mod tests {
let event = AgentEvent::AssistantMessage {
text: "Hello".into(),
model: ModelRef {
- provider: Provider::OpenAi.id(),
+ provider: ProviderId::openai(),
model_id: "test-model".into(),
speed: None,
},
diff --git a/lib/crates/fabro-agent/tests/it/guardrails.rs b/lib/crates/fabro-agent/tests/it/guardrails.rs
index 1d810d45b..9fca7223d 100644
--- a/lib/crates/fabro-agent/tests/it/guardrails.rs
+++ b/lib/crates/fabro-agent/tests/it/guardrails.rs
@@ -1,41 +1,38 @@
use std::sync::Arc;
use fabro_agent::{AgentProfile, AnthropicProfile, GeminiProfile, OpenAiProfile};
-use fabro_model::catalog::LlmCatalogSettings;
-use fabro_model::{Catalog, Provider};
+use fabro_model::{Catalog, ProviderId};
#[test]
fn profile_context_window_matches_catalog_for_default_models() {
- let catalog =
- Arc::new(Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default()).unwrap());
- for &provider in Provider::ALL {
+ let catalog = Arc::new(Catalog::from_builtin().unwrap());
+ for provider in catalog.providers() {
let catalog_info = catalog
- .default_for_provider(&provider.id())
+ .default_for_provider(&provider.id)
.cloned()
- .unwrap_or_else(|| panic!("no default model for {provider:?} in catalog"));
+ .unwrap_or_else(|| panic!("no default model for {:?} in catalog", provider.id));
let model = &catalog_info.id;
let context_window = usize::try_from(catalog_info.context_window())
.expect("catalog context window should be non-negative and fit in usize");
- let profile: Box = match provider {
- Provider::OpenAi => {
+ let profile: Box = match provider.agent_profile {
+ fabro_model::AgentProfileKind::OpenAi if provider.id == ProviderId::openai() => {
Box::new(OpenAiProfile::new(model).with_catalog(Arc::clone(&catalog)))
}
- Provider::Kimi
- | Provider::Zai
- | Provider::Minimax
- | Provider::Inception
- | Provider::OpenAiCompatible => Box::new(
+ fabro_model::AgentProfileKind::OpenAi => Box::new(
OpenAiProfile::new(model)
- .with_provider(provider)
+ .with_provider_id(provider.id.clone())
.with_catalog(Arc::clone(&catalog)),
),
- Provider::Gemini => {
+ fabro_model::AgentProfileKind::Gemini => {
Box::new(GeminiProfile::new(model).with_catalog(Arc::clone(&catalog)))
}
- Provider::Anthropic | Provider::Vertex => Box::new(
+ fabro_model::AgentProfileKind::Anthropic if provider.id == ProviderId::anthropic() => {
+ Box::new(AnthropicProfile::new(model).with_catalog(Arc::clone(&catalog)))
+ }
+ fabro_model::AgentProfileKind::Anthropic => Box::new(
AnthropicProfile::new(model)
- .with_provider(provider)
+ .with_provider_id(provider.id.clone())
.with_catalog(Arc::clone(&catalog)),
),
};
@@ -44,7 +41,7 @@ fn profile_context_window_matches_catalog_for_default_models() {
profile.context_window_size(),
context_window,
"context_window_size mismatch for {:?} model '{}': profile={} catalog={}",
- provider,
+ provider.id,
model,
profile.context_window_size(),
context_window
diff --git a/lib/crates/fabro-agent/tests/it/parity_matrix.rs b/lib/crates/fabro-agent/tests/it/parity_matrix.rs
index 2915b92dc..5f51e6589 100644
--- a/lib/crates/fabro-agent/tests/it/parity_matrix.rs
+++ b/lib/crates/fabro-agent/tests/it/parity_matrix.rs
@@ -15,63 +15,62 @@ use fabro_agent::{
};
use fabro_auth::EnvCredentialSource;
use fabro_llm::client::Client;
-use fabro_llm::provider::{Provider, ProviderAdapter};
+use fabro_llm::provider::ProviderAdapter;
use fabro_llm::providers::OpenAiAdapter;
-use fabro_model::catalog::LlmCatalogSettings;
-use fabro_model::{Catalog, ModelHandle};
+use fabro_model::{Catalog, ModelHandle, ProviderId};
use fabro_test::{TwinScenario, TwinScenarios, TwinToolCall, twin_openai};
use tokio::sync::Mutex as AsyncMutex;
+type Provider = ProviderId;
+
#[derive(Clone)]
struct OpenAiTwinOptions {
base_url: String,
api_key: String,
}
-fn summarizer_model_id(provider: Provider) -> ModelHandle {
- match provider {
- Provider::OpenAi
- | Provider::Kimi
- | Provider::Zai
- | Provider::Minimax
- | Provider::Inception
- | Provider::OpenAiCompatible => ModelHandle::ByName {
- provider: Provider::OpenAi.id(),
+fn summarizer_model_id(provider: &Provider) -> ModelHandle {
+ match provider.as_str() {
+ ProviderId::OPENAI | "kimi" | "zai" | "minimax" | "inception" => ModelHandle::ByName {
+ provider: ProviderId::openai(),
model: "gpt-5.4-mini".to_string(),
},
- Provider::Gemini => ModelHandle::ByName {
- provider: Provider::Gemini.id(),
+ ProviderId::GEMINI => ModelHandle::ByName {
+ provider: ProviderId::gemini(),
model: "gemini-3-flash-preview".to_string(),
},
- Provider::Anthropic | Provider::Vertex => ModelHandle::ByName {
- provider: provider.id(),
+ ProviderId::ANTHROPIC => ModelHandle::ByName {
+ provider: ProviderId::anthropic(),
model: "claude-haiku-4-5".to_string(),
},
+ "vertex" => ModelHandle::ByName {
+ provider: provider.clone(),
+ model: "vertex-claude-haiku-4-5".to_string(),
+ },
+ other => panic!("unexpected provider {other}"),
}
}
-fn build_summarizer(provider: Provider, client: &Client) -> WebFetchSummarizer {
+fn build_summarizer(provider: &Provider, client: &Client) -> WebFetchSummarizer {
WebFetchSummarizer {
client: client.clone(),
model_id: summarizer_model_id(provider),
}
}
-fn build_profile(provider: Provider, model: &str, client: &Client) -> Box {
+fn build_profile(provider: &Provider, model: &str, client: &Client) -> Box {
let summarizer = Some(build_summarizer(provider, client));
- match provider {
- Provider::Anthropic | Provider::Vertex => {
- Box::new(AnthropicProfile::with_summarizer(model, summarizer).with_provider(provider))
- }
- Provider::OpenAi => Box::new(OpenAiProfile::with_summarizer(model, summarizer)),
- Provider::Kimi
- | Provider::Zai
- | Provider::Minimax
- | Provider::Inception
- | Provider::OpenAiCompatible => {
- Box::new(OpenAiProfile::with_summarizer(model, summarizer).with_provider(provider))
- }
- Provider::Gemini => Box::new(GeminiProfile::with_summarizer(model, summarizer)),
+ match provider.as_str() {
+ ProviderId::ANTHROPIC => Box::new(AnthropicProfile::with_summarizer(model, summarizer)),
+ "vertex" => Box::new(
+ AnthropicProfile::with_summarizer(model, summarizer).with_provider_id(provider.clone()),
+ ),
+ ProviderId::OPENAI => Box::new(OpenAiProfile::with_summarizer(model, summarizer)),
+ "kimi" | "zai" | "minimax" | "inception" => Box::new(
+ OpenAiProfile::with_summarizer(model, summarizer).with_provider_id(provider.clone()),
+ ),
+ ProviderId::GEMINI => Box::new(GeminiProfile::with_summarizer(model, summarizer)),
+ other => panic!("unexpected provider {other}"),
}
}
@@ -81,8 +80,8 @@ async fn make_session(
cwd: &Path,
twin: Option,
) -> Session {
- let client = make_client(provider, twin.as_ref()).await;
- let mut profile = build_profile(provider, model, &client);
+ let client = make_client(&provider, twin.as_ref()).await;
+ let mut profile = build_profile(&provider, model, &client);
let env = Arc::new(LocalSandbox::new(cwd.to_path_buf()));
// Register subagent tools so spawn_agent / wait / send_input / close_agent are
@@ -91,28 +90,30 @@ async fn make_session(
let factory_client = client.clone();
let factory_model: String = model.to_string();
let factory_cwd = cwd.to_path_buf();
+ let factory_provider = provider.clone();
let factory: SessionFactory = Arc::new(move || {
let sub_profile: Arc = {
- let summarizer = Some(build_summarizer(provider, &factory_client));
- match provider {
- Provider::Anthropic | Provider::Vertex => Arc::new(
+ let summarizer = Some(build_summarizer(&factory_provider, &factory_client));
+ match factory_provider.as_str() {
+ ProviderId::ANTHROPIC => Arc::new(AnthropicProfile::with_summarizer(
+ &factory_model,
+ summarizer,
+ )),
+ "vertex" => Arc::new(
AnthropicProfile::with_summarizer(&factory_model, summarizer)
- .with_provider(provider),
+ .with_provider_id(factory_provider.clone()),
),
- Provider::OpenAi => {
+ ProviderId::OPENAI => {
Arc::new(OpenAiProfile::with_summarizer(&factory_model, summarizer))
}
- Provider::Kimi
- | Provider::Zai
- | Provider::Minimax
- | Provider::Inception
- | Provider::OpenAiCompatible => Arc::new(
+ "kimi" | "zai" | "minimax" | "inception" => Arc::new(
OpenAiProfile::with_summarizer(&factory_model, summarizer)
- .with_provider(provider),
+ .with_provider_id(factory_provider.clone()),
),
- Provider::Gemini => {
+ ProviderId::GEMINI => {
Arc::new(GeminiProfile::with_summarizer(&factory_model, summarizer))
}
+ other => panic!("unexpected provider {other}"),
}
};
let sub_env = Arc::new(LocalSandbox::new(factory_cwd.clone()));
@@ -141,22 +142,19 @@ async fn make_session_with_config(
config: SessionOptions,
twin: Option,
) -> Session {
- let client = make_client(provider, twin.as_ref()).await;
- let profile: Arc = Arc::from(build_profile(provider, model, &client));
+ let client = make_client(&provider, twin.as_ref()).await;
+ let profile: Arc = Arc::from(build_profile(&provider, model, &client));
let env = Arc::new(LocalSandbox::new(cwd.to_path_buf()));
Session::new(client, profile, env, config, None)
}
-async fn make_client(provider: Provider, twin: Option<&OpenAiTwinOptions>) -> Client {
- if provider == Provider::OpenAi && fabro_test::TestMode::from_env().is_twin() {
+async fn make_client(provider: &Provider, twin: Option<&OpenAiTwinOptions>) -> Client {
+ if provider == &ProviderId::openai() && fabro_test::TestMode::from_env().is_twin() {
return make_twin_client(twin.expect("openai twin config should be provided"));
}
let source = EnvCredentialSource::new();
- let catalog = Arc::new(
- Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default())
- .expect("default catalog should build"),
- );
+ let catalog = Arc::new(Catalog::from_builtin().expect("default catalog should build"));
Client::from_source(&source, catalog)
.await
.expect("Client::from_source failed")
@@ -197,7 +195,7 @@ macro_rules! openai_twin_provider_test {
.await;
}
let mut session = make_session(
- Provider::OpenAi,
+ ProviderId::openai(),
"gpt-5.4-mini",
tmp.path(),
Some(twin),
@@ -213,21 +211,21 @@ macro_rules! provider_tests {
($scenario:ident) => {
provider_test!(
$scenario,
- Provider::Anthropic,
+ ProviderId::anthropic(),
"claude-haiku-4-5",
anthropic,
keys = ["ANTHROPIC_API_KEY"]
);
provider_test!(
$scenario,
- Provider::Gemini,
+ ProviderId::gemini(),
"gemini-3-flash-preview",
gemini,
keys = ["GEMINI_API_KEY"]
);
provider_test!(
$scenario,
- Provider::Kimi,
+ ProviderId::new("kimi"),
"kimi-k2.5",
kimi,
keys = ["KIMI_API_KEY"]
@@ -235,14 +233,14 @@ macro_rules! provider_tests {
#[cfg(feature = "quarantine")]
provider_test!(
$scenario,
- Provider::Zai,
+ ProviderId::new("zai"),
"glm-4.7",
zai,
keys = ["ZAI_API_KEY"]
);
provider_test!(
$scenario,
- Provider::Minimax,
+ ProviderId::new("minimax"),
"minimax-m2.5",
minimax,
keys = ["MINIMAX_API_KEY"]
@@ -250,7 +248,7 @@ macro_rules! provider_tests {
#[cfg(feature = "quarantine")]
provider_test!(
$scenario,
- Provider::Inception,
+ ProviderId::new("inception"),
"mercury-2",
inception,
keys = ["INCEPTION_API_KEY"]
@@ -283,28 +281,28 @@ provider_tests!(subagent_spawn);
provider_test!(
web_fetch,
- Provider::Anthropic,
+ ProviderId::anthropic(),
"claude-haiku-4-5",
anthropic,
keys = ["ANTHROPIC_API_KEY"]
);
provider_test!(
web_fetch,
- Provider::OpenAi,
+ ProviderId::openai(),
"gpt-5.4-mini",
openai,
keys = ["OPENAI_API_KEY"]
);
provider_test!(
web_fetch,
- Provider::Gemini,
+ ProviderId::gemini(),
"gemini-3-flash-preview",
gemini,
keys = ["GEMINI_API_KEY"]
);
provider_test!(
web_fetch,
- Provider::Kimi,
+ ProviderId::new("kimi"),
"kimi-k2.5",
kimi,
keys = ["KIMI_API_KEY", "OPENAI_API_KEY"]
@@ -312,14 +310,14 @@ provider_test!(
#[cfg(feature = "quarantine")]
provider_test!(
web_fetch,
- Provider::Zai,
+ ProviderId::new("zai"),
"glm-4.7",
zai,
keys = ["ZAI_API_KEY", "OPENAI_API_KEY"]
);
provider_test!(
web_fetch,
- Provider::Minimax,
+ ProviderId::new("minimax"),
"minimax-m2.5",
minimax,
keys = ["MINIMAX_API_KEY", "OPENAI_API_KEY"]
@@ -327,7 +325,7 @@ provider_test!(
#[cfg(feature = "quarantine")]
provider_test!(
web_fetch,
- Provider::Inception,
+ ProviderId::new("inception"),
"mercury-2",
inception,
keys = ["INCEPTION_API_KEY", "OPENAI_API_KEY"]
@@ -335,28 +333,28 @@ provider_test!(
provider_test!(
web_search,
- Provider::Anthropic,
+ ProviderId::anthropic(),
"claude-haiku-4-5",
anthropic,
keys = ["ANTHROPIC_API_KEY", "BRAVE_SEARCH_API_KEY"]
);
provider_test!(
web_search,
- Provider::OpenAi,
+ ProviderId::openai(),
"gpt-5.4-mini",
openai,
keys = ["OPENAI_API_KEY", "BRAVE_SEARCH_API_KEY"]
);
provider_test!(
web_search,
- Provider::Gemini,
+ ProviderId::gemini(),
"gemini-3-flash-preview",
gemini,
keys = ["GEMINI_API_KEY", "BRAVE_SEARCH_API_KEY"]
);
provider_test!(
web_search,
- Provider::Kimi,
+ ProviderId::new("kimi"),
"kimi-k2.5",
kimi,
keys = ["KIMI_API_KEY", "BRAVE_SEARCH_API_KEY"]
@@ -364,14 +362,14 @@ provider_test!(
#[cfg(feature = "quarantine")]
provider_test!(
web_search,
- Provider::Zai,
+ ProviderId::new("zai"),
"glm-4.7",
zai,
keys = ["ZAI_API_KEY", "BRAVE_SEARCH_API_KEY"]
);
provider_test!(
web_search,
- Provider::Minimax,
+ ProviderId::new("minimax"),
"minimax-m2.5",
minimax,
keys = ["MINIMAX_API_KEY", "BRAVE_SEARCH_API_KEY"]
@@ -379,7 +377,7 @@ provider_test!(
#[cfg(feature = "quarantine")]
provider_test!(
web_search,
- Provider::Inception,
+ ProviderId::new("inception"),
"mercury-2",
inception,
keys = ["INCEPTION_API_KEY", "BRAVE_SEARCH_API_KEY"]
@@ -402,21 +400,21 @@ macro_rules! non_openai_provider_tests {
($scenario:ident) => {
provider_test!(
$scenario,
- Provider::Anthropic,
+ ProviderId::anthropic(),
"claude-haiku-4-5",
anthropic,
keys = ["ANTHROPIC_API_KEY"]
);
provider_test!(
$scenario,
- Provider::Gemini,
+ ProviderId::gemini(),
"gemini-3-flash-preview",
gemini,
keys = ["GEMINI_API_KEY"]
);
provider_test!(
$scenario,
- Provider::Kimi,
+ ProviderId::new("kimi"),
"kimi-k2.5",
kimi,
keys = ["KIMI_API_KEY"]
@@ -424,14 +422,14 @@ macro_rules! non_openai_provider_tests {
#[cfg(feature = "quarantine")]
provider_test!(
$scenario,
- Provider::Zai,
+ ProviderId::new("zai"),
"glm-4.7",
zai,
keys = ["ZAI_API_KEY"]
);
provider_test!(
$scenario,
- Provider::Minimax,
+ ProviderId::new("minimax"),
"minimax-m2.5",
minimax,
keys = ["MINIMAX_API_KEY"]
@@ -439,7 +437,7 @@ macro_rules! non_openai_provider_tests {
#[cfg(feature = "quarantine")]
provider_test!(
$scenario,
- Provider::Inception,
+ ProviderId::new("inception"),
"mercury-2",
inception,
keys = ["INCEPTION_API_KEY"]
@@ -685,7 +683,7 @@ macro_rules! reasoning_effort_tests {
}
reasoning_effort_tests!(
- Provider::Anthropic,
+ ProviderId::anthropic(),
"claude-haiku-4-5",
anthropic_reasoning_effort,
keys = ["ANTHROPIC_API_KEY"]
@@ -693,33 +691,33 @@ reasoning_effort_tests!(
// gpt-5-mini does not support the reasoning.effort parameter, so no OpenAI
// test.
reasoning_effort_tests!(
- Provider::Gemini,
+ ProviderId::gemini(),
"gemini-3-flash-preview",
gemini_reasoning_effort,
keys = ["GEMINI_API_KEY"]
);
reasoning_effort_tests!(
- Provider::Kimi,
+ ProviderId::new("kimi"),
"kimi-k2.5",
kimi_reasoning_effort,
keys = ["KIMI_API_KEY"]
);
#[cfg(feature = "quarantine")]
reasoning_effort_tests!(
- Provider::Zai,
+ ProviderId::new("zai"),
"glm-4.7",
zai_reasoning_effort,
keys = ["ZAI_API_KEY"]
);
reasoning_effort_tests!(
- Provider::Minimax,
+ ProviderId::new("minimax"),
"minimax-m2.5",
minimax_reasoning_effort,
keys = ["MINIMAX_API_KEY"]
);
#[cfg(feature = "quarantine")]
reasoning_effort_tests!(
- Provider::Inception,
+ ProviderId::new("inception"),
"mercury-2",
inception_reasoning_effort,
keys = ["INCEPTION_API_KEY"]
@@ -764,45 +762,45 @@ macro_rules! loop_detection_tests {
}
loop_detection_tests!(
- Provider::Anthropic,
+ ProviderId::anthropic(),
"claude-haiku-4-5",
anthropic_loop_detection,
keys = ["ANTHROPIC_API_KEY"]
);
loop_detection_tests!(
- Provider::OpenAi,
+ ProviderId::openai(),
"gpt-5.4-mini",
openai_loop_detection,
keys = ["OPENAI_API_KEY"]
);
loop_detection_tests!(
- Provider::Gemini,
+ ProviderId::gemini(),
"gemini-3-flash-preview",
gemini_loop_detection,
keys = ["GEMINI_API_KEY"]
);
loop_detection_tests!(
- Provider::Kimi,
+ ProviderId::new("kimi"),
"kimi-k2.5",
kimi_loop_detection,
keys = ["KIMI_API_KEY"]
);
#[cfg(feature = "quarantine")]
loop_detection_tests!(
- Provider::Zai,
+ ProviderId::new("zai"),
"glm-4.7",
zai_loop_detection,
keys = ["ZAI_API_KEY"]
);
loop_detection_tests!(
- Provider::Minimax,
+ ProviderId::new("minimax"),
"minimax-m2.5",
minimax_loop_detection,
keys = ["MINIMAX_API_KEY"]
);
#[cfg(feature = "quarantine")]
loop_detection_tests!(
- Provider::Inception,
+ ProviderId::new("inception"),
"mercury-2",
inception_loop_detection,
keys = ["INCEPTION_API_KEY"]
diff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs
index 1ecf7aaa1..92d4d43b6 100644
--- a/lib/crates/fabro-api/build.rs
+++ b/lib/crates/fabro-api/build.rs
@@ -191,6 +191,7 @@ fn main() {
("FailureReason", "fabro_types::status::FailureReason", &[]),
("FailureCategory", "fabro_types::FailureCategory", &[]),
("FailureSignature", "fabro_types::FailureSignature", &[]),
+ ("FailureDetail", "fabro_types::FailureDetail", &[]),
("RunFailure", "fabro_types::RunFailure", &[]),
("BlockedReason", "fabro_types::status::BlockedReason", &[]),
(
@@ -378,7 +379,24 @@ fn main() {
("RunEvent", "fabro_types::RunEvent", &[]),
("EventEnvelope", "fabro_types::EventEnvelope", &[]),
("PullRequest", "fabro_types::PullRequest", &[]),
+ ("PullRequestLink", "fabro_types::PullRequestLink", &[]),
("PullRequestDetails", "fabro_types::PullRequestDetails", &[]),
+ ("PullRequestMeta", "fabro_types::PullRequestMeta", &[]),
+ (
+ "PullRequestResponse",
+ "fabro_types::PullRequestResponse",
+ &[],
+ ),
+ (
+ "PullRequestDetailsStatus",
+ "fabro_types::PullRequestDetailsStatus",
+ &[],
+ ),
+ (
+ "PullRequestDetailsUnavailableReason",
+ "fabro_types::PullRequestDetailsUnavailableReason",
+ &[],
+ ),
("RunSandboxRuntime", "fabro_types::RunSandboxRuntime", &[]),
("PullRequestUser", "fabro_types::PullRequestUser", &[]),
("PullRequestRef", "fabro_types::PullRequestRef", &[]),
@@ -395,6 +413,17 @@ fn main() {
("SandboxProvider", "fabro_types::SandboxProvider", &[]),
("RunSandbox", "fabro_types::RunSandbox", &[]),
("SandboxDetails", "fabro_types::SandboxDetails", &[]),
+ ("SandboxNetwork", "fabro_types::SandboxNetwork", &[]),
+ (
+ "SandboxNetworkPolicy",
+ "fabro_types::SandboxNetworkPolicy",
+ &[],
+ ),
+ (
+ "SandboxNetworkPolicyMode",
+ "fabro_types::SandboxNetworkPolicyMode",
+ &[],
+ ),
("SandboxService", "fabro_types::SandboxService", &[]),
(
"SandboxServiceDiscoverySource",
@@ -414,6 +443,19 @@ fn main() {
("SandboxState", "fabro_types::SandboxState", &[]),
("SandboxResources", "fabro_types::SandboxResources", &[]),
("SandboxTimestamps", "fabro_types::SandboxTimestamps", &[]),
+ ("SessionId", "fabro_types::SessionId", &[]),
+ ("TurnId", "fabro_types::TurnId", &[]),
+ ("SessionStatus", "fabro_types::SessionStatus", &[]),
+ ("TurnStatus", "fabro_types::TurnStatus", &[]),
+ ("SessionMessage", "fabro_types::SessionMessage", &[]),
+ ("SessionRecord", "fabro_types::SessionRecord", &[]),
+ ("SessionSummary", "fabro_types::SessionSummary", &[]),
+ ("TurnRecord", "fabro_types::TurnRecord", &[]),
+ (
+ "SessionEventEnvelope",
+ "fabro_types::SessionEventEnvelope",
+ &[],
+ ),
];
for (name, path, impls) in replacements {
settings.with_replacement(*name, *path, impls.iter().copied());
diff --git a/lib/crates/fabro-api/src/lib.rs b/lib/crates/fabro-api/src/lib.rs
index c72597ac5..4d8786b9f 100644
--- a/lib/crates/fabro-api/src/lib.rs
+++ b/lib/crates/fabro-api/src/lib.rs
@@ -16,7 +16,7 @@ mod generated {
pub mod types {
pub use fabro_model::{
Model, ModelCosts, ModelFeatures, ModelLimits, ModelRef as BillingModelRef, ModelTestMode,
- Provider, ReasoningEffortFeature, Speed as BillingSpeed,
+ ReasoningEffortFeature, Speed as BillingSpeed,
};
pub use fabro_types::settings::server::{
GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings,
@@ -33,18 +33,21 @@ pub mod types {
};
pub use fabro_types::{
AuthMethod, BilledTokenCounts, CommandTermination, Conclusion, DiffStats, DiffSummary,
- DirtyStatus, EventEnvelope, ExecOutputTail, FailureCategory, FailureSignature, GitContext,
- IdpIdentity, InterviewOption, InterviewQuestionRecord, PendingInterviewRecord,
- PreRunPushOutcome, Principal, PullRequest, PullRequestDetails, QuestionType, RepositoryRef,
- Run, RunClientProvenance, RunEvent, RunFailure, RunProjection, RunProvenance, RunSandbox,
- RunSandboxRuntime, RunServerProvenance, SandboxDetails, SandboxProvider, SandboxResources,
+ DirtyStatus, EventEnvelope, ExecOutputTail, FailureCategory, FailureDetail,
+ FailureSignature, GitContext, IdpIdentity, InterviewOption, InterviewQuestionRecord,
+ PendingInterviewRecord, PreRunPushOutcome, Principal, PullRequest, PullRequestDetails,
+ PullRequestDetailsStatus, PullRequestDetailsUnavailableReason, PullRequestLink,
+ PullRequestMeta, PullRequestResponse, QuestionType, RepositoryRef, Run,
+ RunClientProvenance, RunEvent, RunFailure, RunProjection, RunProvenance, RunSandbox,
+ RunSandboxRuntime, RunServerProvenance, SandboxDetails, SandboxNetwork,
+ SandboxNetworkPolicy, SandboxNetworkPolicyMode, SandboxProvider, SandboxResources,
SandboxService, SandboxServiceListResponse, SandboxState, SandboxTimestamps,
- SecretMetadata, SecretType, ServerSettings, StageCompletion, StageHandler, StageOutcome,
- StageProjection, StageState, SystemActorKind, UserPrincipal, WorkflowSettings,
+ SecretMetadata, SecretType, ServerSettings, SessionEventEnvelope, SessionId,
+ SessionMessage, SessionRecord, SessionStatus, SessionSummary, StageCompletion,
+ StageHandler, StageOutcome, StageProjection, StageState, SystemActorKind, TurnId,
+ TurnRecord, TurnStatus, UserPrincipal, WorkflowSettings,
};
pub use crate::generated::types::*;
-
- pub type RunSummary = fabro_types::Run;
}
pub use generated::Client as ApiClient;
diff --git a/lib/crates/fabro-api/tests/model_features_round_trip.rs b/lib/crates/fabro-api/tests/model_features_round_trip.rs
index 1cffcfe0a..033da8269 100644
--- a/lib/crates/fabro-api/tests/model_features_round_trip.rs
+++ b/lib/crates/fabro-api/tests/model_features_round_trip.rs
@@ -16,7 +16,6 @@ fn model_features_json_matches_openapi_shape() {
reasoning: true,
reasoning_effort: ReasoningEffortFeature::Levels,
prompt_cache: false,
- effort: true,
};
let json = serde_json::to_value(&features).unwrap();
@@ -25,7 +24,6 @@ fn model_features_json_matches_openapi_shape() {
assert_eq!(json["reasoning"], true);
assert_eq!(json["reasoning_effort"], "levels");
assert_eq!(json["prompt_cache"], false);
- assert_eq!(json["effort"], true);
let round_trip: ApiModelFeatures = serde_json::from_value(json).unwrap();
assert_eq!(round_trip, features);
diff --git a/lib/crates/fabro-api/tests/model_round_trip.rs b/lib/crates/fabro-api/tests/model_round_trip.rs
index 8baeb6447..6e89a980a 100644
--- a/lib/crates/fabro-api/tests/model_round_trip.rs
+++ b/lib/crates/fabro-api/tests/model_round_trip.rs
@@ -2,7 +2,7 @@ use std::any::{TypeId, type_name};
use fabro_api::types::Model as ApiModel;
use fabro_model::{
- Model, ModelCosts, ModelFeatures, ModelLimits, Provider, ReasoningEffortFeature,
+ Model, ModelCosts, ModelFeatures, ModelLimits, ProviderId, ReasoningEffortFeature,
};
#[test]
@@ -14,7 +14,7 @@ fn model_reuses_canonical_type() {
fn model_json_matches_openapi_shape() {
let model = Model {
id: "claude-opus-4-7".to_string(),
- provider: Provider::Anthropic.id(),
+ provider: ProviderId::anthropic(),
family: "claude-4".to_string(),
display_name: "Claude Opus 4.7".to_string(),
limits: ModelLimits {
@@ -29,7 +29,6 @@ fn model_json_matches_openapi_shape() {
reasoning: true,
reasoning_effort: ReasoningEffortFeature::Levels,
prompt_cache: true,
- effort: true,
},
costs: ModelCosts {
input_cost_per_mtok: Some(5.0),
@@ -48,7 +47,6 @@ fn model_json_matches_openapi_shape() {
assert_eq!(json["knowledge_cutoff"], "May 2025");
assert_eq!(json["features"]["reasoning_effort"], "levels");
assert_eq!(json["features"]["prompt_cache"], true);
- assert_eq!(json["features"]["effort"], true);
assert_eq!(json["estimated_output_tps"], 25.0);
assert_eq!(json["configured"], true);
diff --git a/lib/crates/fabro-api/tests/provider_round_trip.rs b/lib/crates/fabro-api/tests/provider_round_trip.rs
index bcfbd5cc0..b20e15370 100644
--- a/lib/crates/fabro-api/tests/provider_round_trip.rs
+++ b/lib/crates/fabro-api/tests/provider_round_trip.rs
@@ -2,7 +2,7 @@ use std::any::{TypeId, type_name};
use fabro_api::types::Model as ApiModel;
use fabro_model::{
- Model, ModelCosts, ModelFeatures, ModelLimits, Provider, ProviderId, ReasoningEffortFeature,
+ Model, ModelCosts, ModelFeatures, ModelLimits, ProviderId, ReasoningEffortFeature,
};
use serde_json::json;
@@ -14,17 +14,13 @@ fn provider_id_reuses_canonical_model_field_type() {
#[test]
fn provider_id_json_matches_openapi_shape_through_model() {
assert_eq!(
- serde_json::to_value(Provider::Anthropic.id()).unwrap(),
+ serde_json::to_value(ProviderId::anthropic()).unwrap(),
json!("anthropic")
);
assert_eq!(
- serde_json::to_value(Provider::OpenAi.id()).unwrap(),
+ serde_json::to_value(ProviderId::openai()).unwrap(),
json!("openai")
);
- assert_eq!(
- serde_json::to_value(Provider::OpenAiCompatible.id()).unwrap(),
- json!("openai_compatible")
- );
let model = Model {
id: "venice-custom".to_string(),
@@ -43,7 +39,6 @@ fn provider_id_json_matches_openapi_shape_through_model() {
reasoning: false,
reasoning_effort: ReasoningEffortFeature::None,
prompt_cache: false,
- effort: false,
},
costs: ModelCosts {
input_cost_per_mtok: None,
diff --git a/lib/crates/fabro-api/tests/pull_request_round_trip.rs b/lib/crates/fabro-api/tests/pull_request_round_trip.rs
index 3ed430fb3..98ca72c92 100644
--- a/lib/crates/fabro-api/tests/pull_request_round_trip.rs
+++ b/lib/crates/fabro-api/tests/pull_request_round_trip.rs
@@ -2,43 +2,30 @@ use std::any::{TypeId, type_name};
use fabro_api::types::MergeRunPullRequestRequest;
use fabro_types::settings::run::MergeStrategy;
-use fabro_types::{PullRequestDetail, PullRequestRecord};
+use fabro_types::{PullRequest, PullRequestLink, PullRequestResponse};
use serde_json::json;
#[test]
-fn pull_request_detail_reuses_domain_record_type() {
- let detail: PullRequestDetail = serde_json::from_value(json!({
- "pull_request": {
- "provider": "github",
- "html_url": "https://github.com/fabro-sh/fabro/pull/123",
- "number": 123,
- "owner": "fabro-sh",
- "repo": "fabro",
- "base_branch": "main",
- "head_branch": "fabro/run/demo",
- "title": "Move PR commands server-side"
+fn pull_request_response_reuses_domain_types() {
+ let response: PullRequestResponse = serde_json::from_value(json!({
+ "data": {
+ "link": {
+ "owner": "fabro-sh",
+ "repo": "fabro",
+ "number": 123,
+ "html_url": "https://github.com/fabro-sh/fabro/pull/123"
+ },
+ "details": null
},
- "state": "closed",
- "draft": false,
- "merged": true,
- "merged_at": "2026-04-23T15:45:00Z",
- "mergeable": false,
- "additions": 234,
- "deletions": 67,
- "changed_files": 5,
- "comments": 3,
- "checks": [],
- "author": {
- "login": "octocat"
- },
- "timestamps": {
- "created_at": "2026-04-23T15:40:00Z",
- "updated_at": "2026-04-23T15:45:00Z"
+ "meta": {
+ "details_status": "unavailable",
+ "details_unavailable_reason": "integration_unavailable"
}
}))
- .expect("detail should deserialize");
+ .expect("response should deserialize");
- assert_same_type_as_pull_request_record(&detail.pull_request);
+ assert_same_type_as_pull_request(&response.data);
+ assert_same_type_as_pull_request_link(&response.data.link);
}
#[test]
@@ -63,69 +50,80 @@ fn merge_strategy_json_matches_openapi_shape() {
}
#[test]
-fn pull_request_record_json_matches_openapi_shape() {
+fn pull_request_link_json_matches_openapi_shape() {
let fixture = json!({
- "provider": "github",
- "html_url": "https://github.com/fabro-sh/fabro/pull/123",
- "number": 123,
"owner": "fabro-sh",
"repo": "fabro",
- "base_branch": "main",
- "head_branch": "fabro/run/demo",
- "title": "Move PR commands server-side"
+ "number": 123,
+ "html_url": "https://github.com/fabro-sh/fabro/pull/123"
});
- let domain_record: PullRequestRecord =
- serde_json::from_value(fixture.clone()).expect("domain record should deserialize");
+ let domain_record: PullRequestLink =
+ serde_json::from_value(fixture.clone()).expect("domain link should deserialize");
assert_eq!(serde_json::to_value(domain_record).unwrap(), fixture);
}
#[test]
-fn pull_request_detail_json_matches_openapi_shape() {
+fn pull_request_response_json_matches_openapi_shape() {
let fixture = json!({
- "pull_request": {
- "provider": "github",
- "html_url": "https://github.com/fabro-sh/fabro/pull/123",
- "number": 123,
- "owner": "fabro-sh",
- "repo": "fabro",
- "base_branch": "main",
- "head_branch": "fabro/run/demo",
- "title": "Move PR commands server-side"
+ "data": {
+ "link": {
+ "owner": "fabro-sh",
+ "repo": "fabro",
+ "number": 123,
+ "html_url": "https://github.com/fabro-sh/fabro/pull/123"
+ },
+ "details": {
+ "title": "Move PR commands server-side",
+ "body": "Detailed description",
+ "state": "closed",
+ "draft": false,
+ "merged": true,
+ "merged_at": "2026-04-23T15:45:00Z",
+ "mergeable": false,
+ "additions": 234,
+ "deletions": 67,
+ "changed_files": 5,
+ "author": {
+ "login": "octocat"
+ },
+ "head_branch": "fabro/run/demo",
+ "base_branch": "main",
+ "timestamps": {
+ "created_at": "2026-04-23T15:40:00Z",
+ "updated_at": "2026-04-23T15:45:00Z"
+ }
+ }
},
- "state": "closed",
- "draft": false,
- "merged": true,
- "merged_at": "2026-04-23T15:45:00Z",
- "mergeable": false,
- "additions": 234,
- "deletions": 67,
- "changed_files": 5,
- "comments": 3,
- "checks": [],
- "author": {
- "login": "octocat"
- },
- "timestamps": {
- "created_at": "2026-04-23T15:40:00Z",
- "updated_at": "2026-04-23T15:45:00Z"
+ "meta": {
+ "details_status": "available"
}
});
- let detail: PullRequestDetail =
- serde_json::from_value(fixture.clone()).expect("detail should deserialize");
+ let detail: PullRequestResponse =
+ serde_json::from_value(fixture.clone()).expect("response should deserialize");
assert_eq!(serde_json::to_value(detail).unwrap(), fixture);
}
-fn assert_same_type_as_pull_request_record(_: &T) {
+fn assert_same_type_as_pull_request(_: &T) {
assert_eq!(
TypeId::of::(),
- TypeId::of::(),
+ TypeId::of::(),
"{} should be the same type as {}",
type_name::(),
- type_name::()
+ type_name::()
+ );
+}
+
+fn assert_same_type_as_pull_request_link(_: &T) {
+ assert_eq!(
+ TypeId::of::(),
+ TypeId::of::(),
+ "{} should be the same type as {}",
+ type_name::(),
+ type_name::()
);
}
diff --git a/lib/crates/fabro-api/tests/run_failure_round_trip.rs b/lib/crates/fabro-api/tests/run_failure_round_trip.rs
index 418688c09..caeabcfb7 100644
--- a/lib/crates/fabro-api/tests/run_failure_round_trip.rs
+++ b/lib/crates/fabro-api/tests/run_failure_round_trip.rs
@@ -2,12 +2,12 @@ use std::any::{TypeId, type_name};
use fabro_api::types::{
Conclusion as ApiConclusion, ExecOutputTail as ApiExecOutputTail,
- FailureCategory as ApiFailureCategory, FailureSignature as ApiFailureSignature,
- RunFailure as ApiRunFailure,
+ FailureCategory as ApiFailureCategory, FailureDetail as ApiFailureDetail,
+ FailureSignature as ApiFailureSignature, RunFailure as ApiRunFailure,
};
use fabro_types::{
- Conclusion, ExecOutputTail, FailureCategory, FailureReason, FailureSignature, RunFailure,
- StageOutcome,
+ Conclusion, ExecOutputTail, FailureCategory, FailureDetail, FailureReason, FailureSignature,
+ RunFailure, StageOutcome,
};
use serde::Serialize;
use serde_json::{Value, json};
@@ -16,6 +16,7 @@ use serde_json::{Value, json};
fn run_failure_family_reuses_domain_types() {
assert_same_type::