Merge origin/main into feat/vertex-adapter

Resolve conflicts introduced by the provider-identity refactor on main (#280)
while preserving the Vertex adapter intent from this branch:

- Adopt main's typed `AdapterKind` enum; add `Vertex` variant and `VERTEX`
  metadata entry rather than reintroducing the deleted `Provider` enum.
- Replace `AdapterMetadata.api_key_header: ApiKeyHeaderPolicy` with
  `auth_strategy: AdapterAuthStrategy` (`ApiKey(policy)` | `GoogleApplicationDefault`)
  so credential-resolution code can short-circuit for adapter-managed auth.
- Update `fabro_auth::{env_source,resolve}` to use catalog-driven adapter
  metadata and look up Vertex via `ProviderId::new("vertex")`.
- Wire Vertex into `fabro_llm::adapter_registry::factory_for`.
- Route `fabro_workflow::handler::llm::cli` and `fabro_agent::cli`/`memory`
  off `AgentProfileKind` rather than the closed Provider enum so Vertex
  inherits the Anthropic profile via catalog metadata.
- Map Vertex to the Anthropic billing/pricing algorithm.
- Drop `lib/crates/fabro-model/src/provider.rs` (deleted by main).
- Drop the legacy `provider_key_url` / `unsupported_install_provider_error`
  helpers; vertex.toml now drives provider catalog behavior.

cargo build --workspace, cargo nextest run -p fabro-model -p fabro-auth
-p fabro-llm -p fabro-agent -p fabro-workflow -p fabro-server -p fabro-cli,
cargo +nightly-2026-04-14 fmt --check --all, and cargo +nightly-2026-04-14
clippy --workspace --all-targets -- -D warnings all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-05-17 08:57:55 -04:00
commit 74aaed2611
No known key found for this signature in database
459 changed files with 24790 additions and 5034 deletions

1
.gitattributes vendored
View file

@ -1 +1,2 @@
lib/crates/fabro-spa/assets/** linguist-generated=true -diff
lib/packages/fabro-api-client/src/** linguist-generated=true

View file

@ -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

95
Cargo.lock generated
View file

@ -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",

View file

@ -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]

View file

@ -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;
}

View file

@ -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 (
<svg viewBox="0 0 16 16" fill="currentColor" className={className}>
<path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.5 2.5 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z" />
</svg>
);
}
type IconComponent = React.ComponentType<{ className?: string }>;
function Chip({
options,
value,
onChange,
Icon,
}: {
options: Choice[];
value: Choice;
onChange: (c: Choice) => void;
Icon: IconComponent;
}) {
return (
<Listbox value={value} onChange={onChange}>
<div className="relative">
<ListboxButton className="inline-flex items-center gap-1.5 rounded-full bg-overlay px-2.5 py-1 text-xs font-medium text-fg-3 transition-colors hover:bg-overlay-strong hover:text-fg focus:outline-2 focus:outline-offset-1 focus:outline-teal-500">
<Icon className="size-3.5 text-fg-muted" />
<span>{value.label}</span>
<ChevronUpDownIcon className="size-3.5 text-fg-muted" />
</ListboxButton>
<ListboxOptions
anchor={{ to: "bottom start", gap: 6 }}
className="z-50 w-52 rounded-lg bg-panel p-1 shadow-2xl shadow-black/60 ring-1 ring-line-strong focus:outline-none"
>
{options.map((opt) => (
<ListboxOption
key={opt.id}
value={opt}
className="group flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-sm text-fg-2 data-focus:bg-overlay data-focus:text-fg data-selected:text-fg"
>
<CheckIcon className="invisible size-4 text-teal-300 group-data-selected:visible" />
<span>{opt.label}</span>
</ListboxOption>
))}
</ListboxOptions>
</div>
</Listbox>
);
}
export default function ComposerChips() {
const [project, setProject] = useState(DEFAULT_PROJECT);
const [branch, setBranch] = useState(DEFAULT_BRANCH);
const [model, setModel] = useState(DEFAULT_MODEL);
return (
<div className="flex flex-wrap items-center gap-1.5">
<Chip options={PROJECTS} value={project} onChange={setProject} Icon={FolderIcon} />
<Chip options={BRANCHES} value={branch} onChange={setBranch} Icon={BranchIcon} />
<Chip options={MODELS} value={model} onChange={setModel} Icon={CpuChipIcon} />
</div>
);
}

View file

@ -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 (
<ComposerPrimitive.Root className="mx-auto mb-4 flex w-full max-w-2xl flex-col overflow-hidden rounded-2xl bg-panel-alt/80 shadow-lg shadow-black/30 ring-1 ring-line-strong backdrop-blur-sm focus-within:ring-teal-500/40">
<ComposerPrimitive.Input
rows={1}
autoFocus
placeholder="Ask anything…"
className="block max-h-48 w-full resize-none bg-transparent px-4 pt-4 pb-2 text-sm text-fg placeholder:text-fg-muted focus:outline-none"
/>
<div className="flex items-center justify-between gap-3 px-4 pt-2 pb-3">
<ComposerChips />
<ThreadPrimitive.If running>
<ComposerPrimitive.Cancel asChild>
<button
type="button"
aria-label="Stop"
className="inline-flex size-9 items-center justify-center rounded-full bg-overlay-strong text-fg transition-colors hover:bg-overlay"
>
<StopIcon className="size-4" />
</button>
</ComposerPrimitive.Cancel>
</ThreadPrimitive.If>
<ThreadPrimitive.If running={false}>
<ComposerPrimitive.Send asChild>
<button
type="submit"
aria-label="Send message"
className="inline-flex size-9 items-center justify-center rounded-full bg-teal-500 text-on-primary transition-colors hover:bg-teal-300 disabled:cursor-not-allowed disabled:opacity-50"
>
<ArrowUpIcon className="size-4" />
</button>
</ComposerPrimitive.Send>
</ThreadPrimitive.If>
</div>
</ComposerPrimitive.Root>
);
}

View file

@ -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 (
<div className="my-2 rounded-lg border border-line bg-overlay/70 text-fg-2">
<div className="flex items-center gap-2 border-b border-line px-3 py-2 text-xs font-medium uppercase tracking-wide text-fg-muted">
<WrenchScrewdriverIcon className="size-3.5" />
<span>tool</span>
<code className="rounded bg-overlay-strong px-1.5 py-0.5 font-mono text-[11px] normal-case tracking-normal text-fg">
{toolName}
</code>
</div>
<Section label="arguments">
<pre className="whitespace-pre-wrap break-words font-mono text-[12px] leading-relaxed text-fg-2">
{formatJson(args)}
</pre>
</Section>
{result !== undefined && (
<Section label="result">
<pre className="whitespace-pre-wrap break-words font-mono text-[12px] leading-relaxed text-fg-2">
{formatJson(result)}
</pre>
</Section>
)}
</div>
);
}
function Section({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="border-b border-line last:border-b-0">
<div className="px-3 pt-2 text-[10px] font-medium uppercase tracking-wider text-fg-muted">
{label}
</div>
<div className="px-3 pb-2">{children}</div>
</div>
);
}
function formatJson(value: unknown): string {
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}

View file

@ -0,0 +1,23 @@
import { describe, expect, test } from "bun:test";
import TestRenderer, { act } from "react-test-renderer";
import { PullRequestChip } from "./pull-request-chip";
describe("PullRequestChip", () => {
test("renders a linked pull request number", () => {
let renderer: TestRenderer.ReactTestRenderer | undefined;
act(() => {
renderer = TestRenderer.create(
<PullRequestChip
number={42}
url="https://github.com/acme/widgets/pull/42"
/>,
);
});
const link = renderer!.root.findByType("a");
const rendered = JSON.stringify(renderer!.toJSON());
expect(link.props.href).toBe("https://github.com/acme/widgets/pull/42");
expect(rendered).toContain("#42");
});
});

View file

@ -18,7 +18,7 @@ export function PullRequestChip({
const content = (
<>
<GitPullRequestIcon className={iconClassName} />
#{number}
{`#${number}`}
{children}
</>
);

View file

@ -5,7 +5,7 @@ import {
columnStatusDisplay,
isRunStatus,
mapRunListItem,
mapRunSummaryToRunItem,
mapRunToRunItem,
runStatusDisplay,
} from "./runs";
@ -68,14 +68,10 @@ describe("mapRunListItem", () => {
title: "Server supplied title",
...withStatus({ kind: "paused", prior_block: null }),
pull_request: {
number: 123,
html_url: "https://github.com/fabro-sh/fabro/pull/123",
provider: "github",
owner: "fabro-sh",
repo: "fabro",
base_branch: "main",
head_branch: "fabro/run/demo",
title: "Add run PR chip",
number: 123,
html_url: "https://github.com/fabro-sh/fabro/pull/123",
},
});
const item = mapRunListItem(summary);
@ -98,21 +94,17 @@ describe("mapRunListItem", () => {
});
});
describe("mapRunSummaryToRunItem", () => {
describe("mapRunToRunItem", () => {
test("maps canonical run summary to RunItem", () => {
const summary = makeRun({
pull_request: {
html_url: "https://github.com/fabro-sh/fabro/pull/456",
number: 456,
provider: "github",
owner: "fabro-sh",
repo: "fabro",
base_branch: "main",
head_branch: "fabro/run/demo",
title: "Add run PR chip",
number: 456,
html_url: "https://github.com/fabro-sh/fabro/pull/456",
},
});
const item = mapRunSummaryToRunItem(summary);
const item = mapRunToRunItem(summary);
expect(item.id).toBe("01ABC");
expect(item.title).toBe("Fix the build");
expect(item.workflow).toBe("fix_build");
@ -143,7 +135,7 @@ describe("mapRunSummaryToRunItem", () => {
},
billing: null,
});
const item = mapRunSummaryToRunItem(summary);
const item = mapRunToRunItem(summary);
expect(item.id).toBe("01DEF");
expect(item.title).toBe("Untitled run");
expect(item.workflow).toBe("unknown");

View file

@ -1,7 +1,6 @@
import { formatElapsedSecs, formatDurationSecs } from "../lib/format";
import {
BoardColumn,
type BoardColumn as ApiBoardColumn,
type Run,
type RunStatus as ApiRunStatus,
} from "@qltysh/fabro-api-client";
@ -21,7 +20,7 @@ export interface RunItem {
repo: string;
title: string;
workflow: string;
column?: ColumnStatus;
column?: BoardColumn;
lifecycleStatus?: RunStatus | null;
lifecycleStatusLabel?: string;
number?: number;
@ -41,8 +40,6 @@ export interface RunItem {
lastEventAt?: string;
}
export type ColumnStatus = ApiBoardColumn;
export const columnStatuses = [
BoardColumn.QUEUED,
BoardColumn.INITIALIZING,
@ -51,9 +48,9 @@ export const columnStatuses = [
BoardColumn.SUCCEEDED,
BoardColumn.FAILED,
BoardColumn.ARCHIVED,
] as const satisfies readonly ColumnStatus[];
] as const satisfies readonly BoardColumn[];
export const columnStatusDisplay: Record<ColumnStatus, { label: string; dot: string; text: string }> = {
export const columnStatusDisplay: Record<BoardColumn, { label: string; dot: string; text: string }> = {
queued: { label: "Queued", dot: "bg-fg-muted", text: "text-fg-muted" },
initializing: { label: "Initializing", dot: "bg-amber", text: "text-amber" },
running: { label: "Running", dot: "bg-teal-500", text: "text-teal-500" },
@ -64,7 +61,7 @@ export const columnStatusDisplay: Record<ColumnStatus, { label: string; dot: str
};
export interface RunWithStatus extends RunItem {
status: ColumnStatus;
status: BoardColumn;
statusLabel: string;
}
@ -106,13 +103,12 @@ export function mapRunListItem(item: Run): RunItem {
}
export type { Run };
export type RunSummary = Run;
export function mapRunSummaryToRunItem(summary: Run): RunItem {
return mapRunListItem(summary);
export function mapRunToRunItem(run: Run): RunItem {
return mapRunListItem(run);
}
export function columnForStatus(status: ApiRunStatus | null | undefined): ColumnStatus | null {
export function columnForStatus(status: ApiRunStatus | null | undefined): BoardColumn | null {
switch (status?.kind) {
case "submitted":
case "queued":
@ -135,7 +131,7 @@ export function columnForStatus(status: ApiRunStatus | null | undefined): Column
}
}
export function columnForRun(run: Run): ColumnStatus | null {
export function columnForRun(run: Run): BoardColumn | null {
if (run.lifecycle.archived) return "archived";
return columnForStatus(run.lifecycle.status);
}

View file

@ -6,15 +6,17 @@ describe("getVisibleNavigation", () => {
const items = getVisibleNavigation(true);
const names = items.map((i) => i.name);
expect(names[0]).toBe("Automations");
expect(names).toContain("Chats");
expect(names).toContain("Runs");
expect(names).toContain("Insights");
expect(names).toContain("Settings");
});
test("hides Automations and Insights in production mode", () => {
test("hides Automations, Chats, and Insights in production mode", () => {
const items = getVisibleNavigation(false);
const names = items.map((i) => i.name);
expect(names).not.toContain("Automations");
expect(names).not.toContain("Chats");
expect(names).not.toContain("Insights");
expect(names).toContain("Runs");
expect(names).toContain("Settings");

View file

@ -11,6 +11,7 @@ import {
Bars3Icon,
BeakerIcon,
ChartBarIcon,
ChatBubbleLeftRightIcon,
Cog6ToothIcon,
PlayIcon,
SparklesIcon,
@ -25,11 +26,24 @@ import { useAuthMe } from "../lib/queries";
const allNavigation = [
{ name: "Automations", href: "/automations", icon: SparklesIcon, demoOnly: true },
{
name: "Chats",
href: "/chats/new",
icon: ChatBubbleLeftRightIcon,
demoOnly: true,
// /chats/new is the click target, but the entry stays active on any
// /chats/:id child route as well.
activePathPrefix: "/chats",
},
{ name: "Runs", href: "/runs", icon: PlayIcon, demoOnly: false },
{ name: "Insights", href: "/insights", icon: ChartBarIcon, demoOnly: true },
{ name: "Settings", href: "/settings", icon: Cog6ToothIcon, demoOnly: false },
];
function activeFor(item: (typeof allNavigation)[number], pathname: string): boolean {
return pathname.startsWith(item.activePathPrefix ?? item.href);
}
export function getVisibleNavigation(demoMode: boolean) {
return allNavigation.filter((item) => !item.demoOnly || demoMode);
}
@ -61,7 +75,7 @@ export default function AppShell() {
const { user, provider, demoMode } = auth;
const navigation = getVisibleNavigation(demoMode);
const currentNav = navigation.find((item) => pathname.startsWith(item.href));
const currentNav = navigation.find((item) => activeFor(item, pathname));
const title = currentNav?.name ?? "";
const lastMatch = matches[matches.length - 1];
const handle = lastMatch?.handle as { headerExtra?: React.ReactNode } | undefined;
@ -101,7 +115,7 @@ export default function AppShell() {
<div className="hidden md:block">
<div className="ml-10 flex items-baseline space-x-4">
{navigation.map((item) => {
const current = pathname.startsWith(item.href);
const current = activeFor(item, pathname);
return (
<Link
key={item.name}
@ -193,7 +207,7 @@ export default function AppShell() {
<DisclosurePanel className="md:hidden">
<div className="space-y-1 px-2 pt-2 pb-3 sm:px-3">
{navigation.map((item) => {
const current = pathname.startsWith(item.href);
const current = activeFor(item, pathname);
return (
<DisclosureButton
key={item.name}

View file

@ -9,7 +9,8 @@ import {
type BroadcastChannelLike,
} from "./cross-tab-sse";
import { queryKeys } from "./query-keys";
import type { EventSourceLike, SseKey } from "./sse";
import type { Key } from "swr";
import type { EventSourceLike } from "./sse";
type MessageHandler = ((event: { data: string }) => void) | null;
@ -47,12 +48,12 @@ describe("subscribeToBoardEvents", () => {
test("coordinated mode shares one global source and invalidates the board runs key", async () => {
const source = new FakeEventSource();
const created: string[] = [];
const keys: SseKey[] = [];
const keys: Key[] = [];
const coordinator = createCoordinator((url) => {
created.push(url);
return source;
});
const mutate = (key: SseKey) => {
const mutate = (key: Key) => {
keys.push(key);
return Promise.resolve();
};
@ -82,9 +83,9 @@ describe("subscribeToBoardEvents", () => {
test("fallback mode preserves the existing shared board EventSource", () => {
const source = new FakeEventSource();
const created: string[] = [];
const keys: SseKey[] = [];
const keys: Key[] = [];
const coordinator = createFallbackCoordinator();
const mutate = (key: SseKey) => {
const mutate = (key: Key) => {
keys.push(key);
return Promise.resolve();
};

View file

@ -39,6 +39,9 @@ const BOARD_STATUS_EVENTS = new Set([
"interview.completed",
"interview.timeout",
"interview.interrupted",
"pull_request.created",
"pull_request.linked",
"pull_request.unlinked",
]);
const subscriptions = new Map<string, SharedEventSubscription>();

View file

@ -0,0 +1,131 @@
import { describe, expect, test } from "bun:test";
import type { ChatModelAdapter } from "@assistant-ui/react";
import { createScriptedAdapter, toThreadMessages } from "./chats-runtime";
import { SCRIPTED_REPLIES } from "./chats-script";
import type { Chat, ChatMessage } from "./chats-types";
const emptyChat: Chat = {
id: "c_test",
title: "",
createdAt: 0,
scriptIndex: 0,
pendingResponse: false,
seedMessages: [],
};
type RunArgs = Parameters<ChatModelAdapter["run"]>[0];
// The scripted adapter only reads `abortSignal` from RunArgs; the other fields
// belong to assistant-ui's full ModelContext surface and have no test value.
// One centralized factory keeps the unavoidable casts off the call sites.
function fakeRunArgs(abortSignal: AbortSignal): RunArgs {
return {
messages: [],
abortSignal,
runConfig: {},
context: { tools: [] } as unknown as RunArgs["context"],
unstable_getMessage: () => ({}) as never,
};
}
async function runAll(
adapter: ChatModelAdapter,
abortSignal: AbortSignal,
): Promise<Array<{ content?: readonly { type: string; text?: string }[] }>> {
const result = adapter.run(fakeRunArgs(abortSignal));
if (Symbol.asyncIterator in result) {
return await Array.fromAsync(result);
}
return [await result];
}
describe("createScriptedAdapter", () => {
test("yields chunks ending in the full scripted reply content", async () => {
let onCompleteCalled = false;
let completedReply: ChatMessage | null = null;
const adapter = createScriptedAdapter({
getChat: () => ({ ...emptyChat, scriptIndex: 0 }),
onReplyComplete: (reply) => {
onCompleteCalled = true;
completedReply = reply;
},
});
const controller = new AbortController();
const runResults = await runAll(adapter, controller.signal);
expect(onCompleteCalled).toBe(true);
expect(completedReply).toBe(SCRIPTED_REPLIES[0]);
// Final result must contain at least one text part with the full text from
// the first scripted reply.
const finalContent = runResults[runResults.length - 1]?.content;
expect(finalContent).toBeDefined();
const finalText = finalContent
?.filter((p) => p.type === "text")
.map((p) => p.text ?? "")
.join("");
const expectedText = SCRIPTED_REPLIES[0]!.content
.filter((p) => p.kind === "text")
.map((p) => p.data.text)
.join("");
expect(finalText).toBe(expectedText);
});
test("picks reply based on getChat().scriptIndex (wraps modulo bank length)", async () => {
let completed: ChatMessage | null = null;
const adapter = createScriptedAdapter({
getChat: () => ({ ...emptyChat, scriptIndex: SCRIPTED_REPLIES.length + 2 }),
onReplyComplete: (reply) => {
completed = reply;
},
});
const controller = new AbortController();
await runAll(adapter, controller.signal);
expect(completed).toBe(SCRIPTED_REPLIES[2]);
});
});
describe("toThreadMessages", () => {
test("converts a user text message", () => {
const out = toThreadMessages([
{ role: "user", content: [{ kind: "text", data: { text: "hi" } }] },
]);
expect(out).toEqual([
{ role: "user", content: [{ type: "text", text: "hi" }] },
]);
});
test("converts an assistant message with paired tool_call + tool_result", () => {
const out = toThreadMessages([
{
role: "assistant",
content: [
{
kind: "tool_call",
data: {
tool_call_id: "t1",
name: "search",
arguments: { q: "hello" },
},
},
{
kind: "tool_result",
data: { tool_call_id: "t1", content: { ok: true } },
},
],
},
]);
expect(out).toHaveLength(1);
expect(out[0]?.role).toBe("assistant");
const parts = out[0]?.content;
expect(Array.isArray(parts)).toBe(true);
if (!Array.isArray(parts)) throw new Error("expected array content");
expect(parts).toHaveLength(1);
const first = parts[0];
expect(first?.type).toBe("tool-call");
if (first?.type !== "tool-call") throw new Error("expected tool-call part");
expect(first.toolCallId).toBe("t1");
expect(first.result).toEqual({ ok: true });
});
});

View file

@ -0,0 +1,126 @@
import type {
ChatModelAdapter,
ChatModelRunResult,
ThreadAssistantMessagePart,
ThreadMessageLike,
} from "@assistant-ui/react";
import type { Chat, ChatContentPart, ChatMessage } from "./chats-types";
import { pickReply } from "./chats-script";
const STREAM_CHUNK_CHARS = 28;
const STREAM_CHUNK_INTERVAL_MS = 55;
function sleep(ms: number, signal: AbortSignal): Promise<void> {
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: readonly ChatContentPart[],
): 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") {
for (let i = out.length - 1; i >= 0; i--) {
const candidate = out[i];
if (
candidate?.type === "tool-call" &&
candidate.toolCallId === part.data.tool_call_id
) {
out[i] = { ...candidate, result: part.data.content };
break;
}
}
}
}
return out;
}
export function createScriptedAdapter(args: {
getChat: () => Chat | undefined;
onReplyComplete: (reply: ChatMessage) => void;
}): ChatModelAdapter {
return {
async *run({ abortSignal }) {
const chat = args.getChat();
const reply = pickReply(chat?.scriptIndex ?? 0);
const accumulated: ChatContentPart[] = [];
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);
}
}
args.onReplyComplete(reply);
},
};
}
function buildUpdate(parts: ChatContentPart[]): ChatModelRunResult {
return { content: toAssistantParts(parts) };
}
export function toThreadMessages(
messages: readonly ChatMessage[],
): ThreadMessageLike[] {
return messages.map((msg) => {
if (msg.role === "user") {
return {
role: "user",
content: msg.content
.filter((p) => 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: [] };
});
}

View file

@ -0,0 +1,189 @@
import type { ChatMessage } from "./chats-types";
/**
* Scripted assistant replies cycled through per chat. Generic content,
* intentionally not Fabro-specific. Each entry is a single assistant
* ChatMessage; tool calls and their results are siblings in the content
* array so the renderer can pair them.
*/
export const SCRIPTED_REPLIES: ChatMessage[] = [
{
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<T extends (...args: any[]) => void>(\n" +
" fn: T,\n" +
" ms: number,\n" +
"): (...args: Parameters<T>) => void {\n" +
" let handle: ReturnType<typeof setTimeout> | 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.",
},
},
],
},
];
const FALLBACK_REPLY: ChatMessage = {
role: "assistant",
content: [{ kind: "text", data: { text: "(No reply available.)" } }],
};
export function pickReply(scriptIndex: number): ChatMessage {
return (
SCRIPTED_REPLIES[scriptIndex % SCRIPTED_REPLIES.length] ?? FALLBACK_REPLY
);
}

View file

@ -0,0 +1,95 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { act } from "react-test-renderer";
import { renderHook, setupReactTestEnv } from "./test-utils";
import {
ChatsProvider,
useChatsActions,
useChatsState,
} from "./chats-store";
function useStore() {
return { ...useChatsActions(), state: useChatsState() };
}
function wrapper({ children }: { children: React.ReactNode }) {
return <ChatsProvider>{children}</ChatsProvider>;
}
describe("chats-store reducer", () => {
let teardown: () => void = () => {};
beforeEach(() => {
teardown = setupReactTestEnv();
});
afterEach(() => {
teardown();
});
test("createChatWithFirstMessage seeds title and user message", () => {
const { result } = renderHook(() => useStore(), { wrapper });
let id = "";
act(() => {
id = result.current.createChatWithFirstMessage("Help me with React");
});
const chat = result.current.state.chats[id];
expect(chat?.title).toBe("Help me with React");
expect(chat?.pendingResponse).toBe(true);
expect(chat?.seedMessages).toHaveLength(1);
expect(chat?.seedMessages[0]?.role).toBe("user");
expect(chat?.seedMessages[0]?.content[0]).toEqual({
kind: "text",
data: { text: "Help me with React" },
});
});
test("title is truncated to 40 chars at word boundary", () => {
const { result } = renderHook(() => useStore(), { wrapper });
let id = "";
act(() => {
id = result.current.createChatWithFirstMessage(
"Help me draft a launch announcement email for our new analytics dashboard",
);
});
expect(result.current.state.chats[id]?.title).toBe(
"Help me draft a launch announcement…",
);
});
test("consumePendingResponse clears the flag", () => {
const { result } = renderHook(() => useStore(), { wrapper });
let id = "";
act(() => {
id = result.current.createChatWithFirstMessage("hi");
});
expect(result.current.state.chats[id]?.pendingResponse).toBe(true);
act(() => {
result.current.consumePendingResponse(id);
});
expect(result.current.state.chats[id]?.pendingResponse).toBe(false);
});
test("advanceScriptIndex increments by one", () => {
const { result } = renderHook(() => useStore(), { wrapper });
let id = "";
act(() => {
id = result.current.createChatWithFirstMessage("hi");
});
expect(result.current.state.chats[id]?.scriptIndex).toBe(0);
act(() => {
result.current.advanceScriptIndex(id);
});
expect(result.current.state.chats[id]?.scriptIndex).toBe(1);
});
test("seed chats appear in order on mount", () => {
const { result } = renderHook(() => useStore(), { wrapper });
expect(result.current.state.order.length).toBeGreaterThanOrEqual(3);
const titles = result.current.state.order.map(
(id) => result.current.state.chats[id]?.title,
);
expect(titles).toContain("Draft a launch email");
expect(titles).toContain("Refactor a React hook");
expect(titles).toContain("Compare Postgres vs SQLite");
});
});

View file

@ -0,0 +1,201 @@
import {
createContext,
useCallback,
useContext,
useMemo,
useReducer,
type ReactNode,
} from "react";
import type { Chat, ChatMessage } from "./chats-types";
import { pickReply } from "./chats-script";
type State = {
chats: Record<string, Chat>;
order: string[]; // newest first
};
type Action =
| {
type: "create";
id: string;
title: string;
createdAt: number;
userMessage: ChatMessage;
}
| { type: "consume_pending"; chatId: string }
| { 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 userMessage(text: string): ChatMessage {
return {
role: "user",
content: [{ kind: "text", data: { text } }],
};
}
function seedChat(args: {
id: string;
title: string;
ageDays: number;
scriptIndex: number;
userText: string;
}): Chat {
return {
id: args.id,
title: args.title,
createdAt: Date.now() - args.ageDays * 86_400_000,
scriptIndex: args.scriptIndex + 1, // seeded reply already "consumed"
pendingResponse: false,
seedMessages: [userMessage(args.userText), pickReply(args.scriptIndex)],
};
}
const initialState: State = (() => {
const seeds: Chat[] = [
seedChat({
id: "seed_email",
title: "Draft a launch email",
ageDays: 0.5,
scriptIndex: 0,
userText:
"Help me draft a launch announcement email for our new analytics dashboard.",
}),
seedChat({
id: "seed_hook",
title: "Refactor a React hook",
ageDays: 2,
scriptIndex: 3,
userText:
"My useChat hook has grown to 200 lines and I keep tangling concerns. How should I think about refactoring it?",
}),
seedChat({
id: "seed_db",
title: "Compare Postgres vs SQLite",
ageDays: 6,
scriptIndex: 5,
userText:
"For a side project with ~50 daily users, should I reach for Postgres or stick with SQLite?",
}),
];
const chats: Record<string, Chat> = {};
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,
pendingResponse: true,
seedMessages: [action.userMessage],
};
return {
chats: { ...state.chats, [action.id]: chat },
order: [action.id, ...state.order],
};
}
case "consume_pending": {
const existing = state.chats[action.chatId];
if (!existing || !existing.pendingResponse) return state;
return {
...state,
chats: {
...state.chats,
[action.chatId]: { ...existing, pendingResponse: false },
},
};
}
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,
},
},
};
}
}
}
export type ChatsActions = {
createChatWithFirstMessage: (text: string) => string;
consumePendingResponse: (chatId: string) => void;
advanceScriptIndex: (chatId: string) => void;
};
const ChatsStateContext = createContext<State | null>(null);
const ChatsActionsContext = createContext<ChatsActions | null>(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 actions = useMemo<ChatsActions>(
() => ({
createChatWithFirstMessage(text: string) {
const id = shortId();
dispatch({
type: "create",
id,
title: deriveTitle(text),
createdAt: Date.now(),
userMessage: userMessage(text),
});
return id;
},
consumePendingResponse(chatId: string) {
dispatch({ type: "consume_pending", chatId });
},
advanceScriptIndex(chatId: string) {
dispatch({ type: "advance_script", chatId });
},
}),
[],
);
return (
<ChatsStateContext.Provider value={state}>
<ChatsActionsContext.Provider value={actions}>
{children}
</ChatsActionsContext.Provider>
</ChatsStateContext.Provider>
);
}
export function useChatsState(): State {
const value = useContext(ChatsStateContext);
if (!value) throw new Error("useChatsState must be used inside <ChatsProvider>");
return value;
}
export function useChatsActions(): ChatsActions {
const value = useContext(ChatsActionsContext);
if (!value) throw new Error("useChatsActions must be used inside <ChatsProvider>");
return value;
}
export function useChat(chatId: string | undefined): Chat | undefined {
const state = useChatsState();
return chatId ? state.chats[chatId] : undefined;
}

View file

@ -0,0 +1,59 @@
/**
* Stricter discriminated-union view over @qltysh/fabro-api-client's
* `CompletionContentPart` ({ kind: string; data: any }). Each variant in our
* union is assignable to the API client type at the boundary, but inside the
* chat code we get exhaustive switch checking.
*/
export type ChatContentPart =
| { 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 JsonValue =
| null
| string
| number
| boolean
| JsonValue[]
| { [key: string]: JsonValue };
export type ChatRole = "user" | "assistant" | "system";
/** Strict in-app message shape; widens to the API's CompletionMessage at the
* wire boundary. Keeping this strict inside the chat code lets every
* `switch (part.kind)` be exhaustive. */
export type ChatMessage = {
role: ChatRole;
content: ChatContentPart[];
};
/**
* Sidebar/store wrapper around a single chat. Messages and the in-flight
* stream live inside assistant-ui's runtime; the store holds the metadata
* needed to render the sidebar, derive titles, and drive the scripted
* reply bank. `seedMessages` is the initial history fed to the runtime via
* `initialMessages` on mount. `pendingResponse` flags a chat where the user
* sent the first message but the assistant has not yet replied.
*/
export type Chat = {
id: string;
title: string;
createdAt: number;
scriptIndex: number;
seedMessages: ChatMessage[];
pendingResponse: boolean;
};

View file

@ -1,3 +1,5 @@
import type { Key } from "swr";
import { queryKeys } from "./query-keys";
import {
createBrowserEventSource,
@ -5,7 +7,6 @@ import {
type EventPayload,
type EventSourceLike,
type MutateFn,
type SseKey,
sseKeyDedupeId,
} from "./sse";
import { getNumber, getString, isRecord, type UnknownRecord } from "./unknown";
@ -113,7 +114,7 @@ interface SubscribeOptions<TPayload extends EventPayload> {
subscriptionKey: string;
mutate: MutateFn;
resolveInvalidation: (payload: TPayload) => EventInvalidation;
resyncKeys: () => SseKey[];
resyncKeys: () => Key[];
fallbackSubscribe: () => () => void;
debounceMs?: number;
}
@ -133,11 +134,11 @@ interface LocalSubscription {
refcount: number;
mutators: Map<MutateFn, number>;
fallbacks: Map<MutateFn, FallbackEntry>;
pendingKeys: Map<string, SseKey>;
pendingKeys: Map<string, Key>;
debounceTimer: ReturnType<typeof setTimeout> | null;
debounceMs: number;
resolveInvalidation: (payload: EventPayload) => EventInvalidation;
resyncKeys: () => SseKey[];
resyncKeys: () => Key[];
}
interface LeaderState {
@ -689,7 +690,7 @@ export class CrossTabSseCoordinator {
private queueInvalidations(
subscription: LocalSubscription,
keys: SseKey[],
keys: Key[],
{ immediate = false }: { immediate?: boolean } = {},
) {
if (keys.length === 0) return;

View file

@ -1,3 +1,5 @@
import type { Key } from "swr";
import {
subscribeToCrossTabSse,
type CrossTabSseCoordinator,
@ -9,7 +11,6 @@ import {
type EventPayload,
type EventSourceLike,
type SharedEventSubscription,
type SseKey,
} from "./sse";
export interface LiveEventPayload extends EventPayload {
@ -29,7 +30,7 @@ interface LiveEventOptions {
const subscriptions = new Map<string, SharedEventSubscription>();
const SUBSCRIPTION_KEY = "live-events";
const NO_KEYS: SseKey[] = [];
const NO_KEYS: Key[] = [];
const NOOP_MUTATE = () => Promise.resolve();
export function subscribeToLiveEvents(

View file

@ -148,8 +148,6 @@ export function useSubmitInterviewAnswer(runId: string | undefined) {
);
}
export type SteerRunArg = SteerRunRequest;
export function useInterruptRun(runId: string | undefined) {
const { mutate } = useSWRConfig();
return useSWRMutation(
@ -171,7 +169,7 @@ export function useSteerRun(runId: string | undefined) {
const { mutate } = useSWRConfig();
return useSWRMutation(
runId ? `steer-run:${runId}` : null,
async (_key: string, { arg }: { arg: SteerRunArg }) => {
async (_key: string, { arg }: { arg: SteerRunRequest }) => {
if (!runId) throw new Error("runId is required");
await apiData(() => humanInTheLoopApi.steerRun(runId, arg));
},

View file

@ -1,4 +1,5 @@
import { describe, expect, test } from "bun:test";
import type { Key } from "swr";
import {
queryKeysForRunEvent,
@ -9,7 +10,7 @@ import {
type BroadcastChannelLike,
} from "./cross-tab-sse";
import { queryKeys } from "./query-keys";
import type { EventSourceLike, SseKey } from "./sse";
import type { EventSourceLike } from "./sse";
type MessageHandler = ((event: { data: string }) => void) | null;
@ -82,7 +83,7 @@ describe("subscribeToRunEvents", () => {
test("coordinated mode uses the global attach stream and filters by run_id", async () => {
const source = new FakeEventSource();
const created: string[] = [];
const keys: SseKey[] = [];
const keys: Key[] = [];
const coordinator = createCoordinator((url) => {
created.push(url);
return source;
@ -118,7 +119,7 @@ describe("subscribeToRunEvents", () => {
test("coordinated terminal events invalidate without closing the global stream", async () => {
const source = new FakeEventSource();
const keys: SseKey[] = [];
const keys: Key[] = [];
const coordinator = createCoordinator(() => source);
const cleanup = subscribeToRunEvents(
"run-terminal",
@ -150,9 +151,9 @@ describe("subscribeToRunEvents", () => {
test("fallback refcounts run-scoped sources and keeps mutators active until final unsubscribe", () => {
const source = new FakeEventSource();
const created: string[] = [];
const keys: SseKey[] = [];
const keys: Key[] = [];
const coordinator = createFallbackCoordinator();
const mutate = (key: SseKey) => {
const mutate = (key: Key) => {
keys.push(key);
return Promise.resolve();
};
@ -184,9 +185,9 @@ describe("subscribeToRunEvents", () => {
test("fallback runs payload callbacks for later subscribers on a shared source", () => {
const source = new FakeEventSource();
const seen: string[] = [];
const keys: SseKey[] = [];
const keys: Key[] = [];
const coordinator = createFallbackCoordinator();
const mutate = (key: SseKey) => {
const mutate = (key: Key) => {
keys.push(key);
return Promise.resolve();
};
@ -218,7 +219,7 @@ describe("subscribeToRunEvents", () => {
test("fallback terminal events close the source after invalidating keys", () => {
const source = new FakeEventSource();
const keys: SseKey[] = [];
const keys: Key[] = [];
const coordinator = createFallbackCoordinator();
const cleanup = subscribeToRunEvents(
"run-terminal",
@ -242,7 +243,7 @@ describe("subscribeToRunEvents", () => {
test("envelope with suffixed stage_id invalidates stageEvents(runId, stageId)", async () => {
const source = new FakeEventSource();
const keys: SseKey[] = [];
const keys: Key[] = [];
const coordinator = createCoordinator(() => source);
const cleanup = subscribeToRunEvents(
"run-stage",
@ -275,7 +276,7 @@ describe("subscribeToRunEvents", () => {
test("falls back to node_id when an event has no stage_id", async () => {
const source = new FakeEventSource();
const keys: SseKey[] = [];
const keys: Key[] = [];
const coordinator = createCoordinator(() => source);
const cleanup = subscribeToRunEvents(
"run-stage-node",
@ -301,7 +302,7 @@ describe("subscribeToRunEvents", () => {
const firstSource = new FakeEventSource();
const secondSource = new FakeEventSource();
const sources = [firstSource, secondSource];
const keys: SseKey[] = [];
const keys: Key[] = [];
const coordinator = createFallbackCoordinator();
const firstCleanup = subscribeToRunEvents(

View file

@ -1,5 +1,5 @@
import { useEffect } from "react";
import { useSWRConfig } from "swr";
import { useSWRConfig, type Key } from "swr";
import {
subscribeToCrossTabSse,
@ -12,7 +12,6 @@ import {
type EventPayload,
type EventSourceLike,
type MutateFn,
type SseKey,
type SharedEventSubscription,
} from "./sse";
@ -47,6 +46,9 @@ const RUN_SUMMARY_EVENTS = new Set([
"run.archived",
"run.unarchived",
"run.title.updated",
"pull_request.created",
"pull_request.linked",
"pull_request.unlinked",
]);
const STAGE_EVENTS = new Set([
"stage.started",
@ -97,7 +99,7 @@ export function queryKeysForRunEvent(
runId: string,
event: string,
stageId?: string,
): SseKey[] {
): Key[] {
if (event === "checkpoint.completed") {
return [
...queryKeys.runs.filesAllScopes(runId),
@ -129,7 +131,7 @@ export function queryKeysForRunEvent(
}
if (STAGE_EVENTS.has(event)) {
const keys: SseKey[] = [
const keys: Key[] = [
queryKeys.runs.stages(runId),
queryKeys.runs.billing(runId),
queryKeys.runs.events(runId, 1000),
@ -144,7 +146,7 @@ export function queryKeysForRunEvent(
}
if (STEERING_EVENTS.has(event)) {
const keys: SseKey[] = [queryKeys.runs.events(runId, 1000)];
const keys: Key[] = [queryKeys.runs.events(runId, 1000)];
if (stageId) {
keys.push(queryKeys.runs.stageEvents(runId, stageId));
}

View file

@ -1,9 +1,5 @@
import type { WorkflowSettings } from "@qltysh/fabro-api-client";
import { getObject } from "./unknown";
export type Snapshot = WorkflowSettings;
export {
getArray,
getBool,

View file

@ -1,7 +1,6 @@
import type { Key, MutatorCallback } from "swr";
export type SseKey = Key;
export type MutateFn = (key: SseKey) => ReturnType<MutatorCallback>;
export type MutateFn = (key: Key) => ReturnType<MutatorCallback>;
export interface EventPayload {
event?: string;
@ -14,7 +13,7 @@ export interface EventSourceLike {
}
export interface EventInvalidation {
keys: SseKey[];
keys: Key[];
close?: boolean;
immediate?: boolean;
}
@ -26,11 +25,11 @@ export interface SharedEventSubscription {
refcount: number;
mutators: Map<MutateFn, number>;
resolvers: Map<symbol, EventResolver>;
pendingKeys: Map<string, SseKey>;
pendingKeys: Map<string, Key>;
debounceTimer: ReturnType<typeof setTimeout> | null;
}
export function sseKeyDedupeId(key: SseKey): string {
export function sseKeyDedupeId(key: Key): string {
return stringifyKeyValue(key);
}
@ -79,7 +78,7 @@ export function subscribeToSharedEventSource<TPayload extends EventPayload>({
return;
}
const keys = new Map<string, SseKey>();
const keys = new Map<string, Key>();
let close = false;
let immediate = false;
for (const resolver of current.resolvers.values()) {
@ -129,7 +128,7 @@ export function subscribeToSharedEventSource<TPayload extends EventPayload>({
function queueInvalidations(
subscription: SharedEventSubscription,
keys: SseKey[],
keys: Key[],
{
debounceMs,
immediate,

View file

@ -0,0 +1,57 @@
import { createElement, type ReactNode } from "react";
import TestRenderer, { act } from "react-test-renderer";
const IS_REACT_ACT_ENV = "IS_REACT_ACT_ENVIRONMENT" as const;
/**
* Per-test setup for code that uses react-test-renderer:
* - Sets IS_REACT_ACT_ENVIRONMENT (required by act()).
* - Silences react-test-renderer's deprecation warning.
*
* Returns a teardown function; pair with beforeEach/afterEach so the global
* state is scoped to the test rather than leaking process-wide.
*/
export function setupReactTestEnv(): () => void {
type Globals = { [IS_REACT_ACT_ENV]?: boolean };
const globals = globalThis as Globals;
const hadEnv = IS_REACT_ACT_ENV in globals;
const previousEnv = globals[IS_REACT_ACT_ENV];
globals[IS_REACT_ACT_ENV] = true;
const originalConsoleError = console.error;
console.error = ((...args: unknown[]) => {
if (
typeof args[0] === "string" &&
args[0].startsWith("react-test-renderer is deprecated")
) {
return;
}
originalConsoleError(...args);
}) as typeof console.error;
return () => {
console.error = originalConsoleError;
if (hadEnv) {
globals[IS_REACT_ACT_ENV] = previousEnv;
} else {
delete globals[IS_REACT_ACT_ENV];
}
};
}
export function renderHook<T>(
hook: () => T,
options: { wrapper: React.ComponentType<{ children: ReactNode }> },
): { result: { current: T } } {
const result = { current: undefined as unknown as T };
function HookHost() {
result.current = hook();
return null;
}
act(() => {
TestRenderer.create(
createElement(options.wrapper, null, createElement(HookHost)),
);
});
return { result };
}

View file

@ -1,5 +1,4 @@
import { isRouteErrorResponse, Outlet } from "react-router";
import "./app.css";
export default function Root() {
return <Outlet />;

View file

@ -6,6 +6,9 @@ import * as RedirectHome from "./routes/redirect-home";
import * as Setup from "./routes/setup";
import * as AuthLogin from "./routes/auth-login";
import * as Start from "./routes/start";
import * as ChatsLayout from "./routes/chats-layout";
import * as ChatsNew from "./routes/chats-new";
import * as ChatsDetail from "./routes/chats-detail";
import * as Automations from "./routes/automations";
import * as AutomationDetail from "./routes/automation-detail";
import * as AutomationDefinition from "./routes/automation-definition";
@ -87,6 +90,12 @@ export const routes: RouteObject[] = [
}),
children: [
route("start", Start),
route("chats", ChatsLayout, {
children: [
route("new", ChatsNew),
route(":chatId", ChatsDetail),
],
}),
route("automations", Automations),
route("automations/:name", AutomationDetail, {
children: [

View file

@ -3,10 +3,10 @@ import { ChevronDownIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outlin
import { Link, useParams } from "react-router";
import { InlineMarkdown } from "../components/inline-markdown";
import { PullRequestChip } from "../components/pull-request-chip";
import { ciConfig, columnForRun, columnStatusDisplay, deriveCiStatus, mapRunSummaryToRunItem } from "../data/runs";
import type { ColumnStatus, RunWithStatus } from "../data/runs";
import { ciConfig, columnForRun, columnStatusDisplay, deriveCiStatus, mapRunToRunItem } from "../data/runs";
import type { RunWithStatus } from "../data/runs";
import { useWorkflowRuns } from "../lib/queries";
import type { PaginatedRunList } from "@qltysh/fabro-api-client";
import type { BoardColumn, PaginatedRunList } from "@qltysh/fabro-api-client";
function mapWorkflowRuns(result: PaginatedRunList | null | undefined): RunWithStatus[] {
const apiRuns = result?.data ?? [];
@ -15,7 +15,7 @@ function mapWorkflowRuns(result: PaginatedRunList | null | undefined): RunWithSt
const column = columnForRun(r);
if (column == null) return null;
return {
...mapRunSummaryToRunItem(r),
...mapRunToRunItem(r),
status: column,
statusLabel: columnStatusDisplay[column].label,
};
@ -56,7 +56,7 @@ function RunRow({ run }: { run: RunWithStatus }) {
</Link>
<span className="inline-flex items-center justify-end gap-1.5 font-mono text-xs text-fg-muted">
{run.number != null && (
{run.pullRequestUrl && run.number != null && (
<PullRequestChip number={run.number} url={run.pullRequestUrl}>
{run.checks != null && <span className={`size-1.5 rounded-full ${ciConfig[deriveCiStatus(run.checks)].dot}`} />}
</PullRequestChip>
@ -71,7 +71,7 @@ export default function AutomationRuns() {
const runsQuery = useWorkflowRuns(name);
const runs = mapWorkflowRuns(runsQuery.data);
const [query, setQuery] = useState("");
const [statusFilter, setStatusFilter] = useState<ColumnStatus | "all">("all");
const [statusFilter, setStatusFilter] = useState<BoardColumn | "all">("all");
const filtered = runs.filter(
(r) =>
(statusFilter === "all" || r.status === statusFilter) &&
@ -96,11 +96,11 @@ export default function AutomationRuns() {
<div className="relative">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as ColumnStatus | "all")}
onChange={(e) => setStatusFilter(e.target.value as BoardColumn | "all")}
className="appearance-none rounded-md border border-line bg-panel/80 py-2 pl-3 pr-8 text-sm text-fg-2 outline-none transition-colors focus:border-focus focus:ring-0"
>
<option value="all">All statuses</option>
{(Object.entries(columnStatusDisplay) as [ColumnStatus, { label: string }][]).map(([id, { label }]) => (
{(Object.entries(columnStatusDisplay) as [BoardColumn, { label: string }][]).map(([id, { label }]) => (
<option key={id} value={id}>{label}</option>
))}
</select>

View file

@ -0,0 +1,103 @@
import { useEffect, useMemo, useRef } from "react";
import { useNavigate, useParams } from "react-router";
import {
AssistantRuntimeProvider,
useLocalRuntime,
} from "@assistant-ui/react";
import { Thread, makeMarkdownText } from "@assistant-ui/react-ui";
import { useChat, useChatsActions } from "../lib/chats-store";
import {
createScriptedAdapter,
toThreadMessages,
} from "../lib/chats-runtime";
import CustomComposer from "../components/chats/custom-composer";
import ToolFallback from "../components/chats/tool-fallback";
import { EmptyState } from "../components/state";
import type { Chat, ChatMessage } from "../lib/chats-types";
// AppShell handle lives on the parent chats-layout route; do not redeclare it
// here.
const MarkdownText = makeMarkdownText();
export default function ChatsDetail() {
const { chatId } = useParams<{ chatId: string }>();
const navigate = useNavigate();
const chat = useChat(chatId);
if (!chatId || !chat) {
return (
<div className="flex h-full items-center justify-center p-8">
<EmptyState
title="That chat doesnt exist."
action={
<button
type="button"
onClick={() => navigate("/chats/new")}
className="text-sm font-medium text-teal-300 hover:text-teal-500"
>
Start a new chat
</button>
}
/>
</div>
);
}
return <ChatRuntime key={chatId} chatId={chatId} chat={chat} />;
}
function ChatRuntime({ chatId, chat }: { chatId: string; chat: Chat }) {
const { advanceScriptIndex, consumePendingResponse } = useChatsActions();
// Keep latest `chat` accessible to the stable adapter closure below without
// recreating the adapter (and the assistant-ui runtime) on every store dispatch.
const chatRef = useRef(chat);
useEffect(() => {
chatRef.current = chat;
});
const initialMessages = useMemo(
() => toThreadMessages(chat.seedMessages),
[chat.seedMessages],
);
const adapter = useMemo(
() =>
createScriptedAdapter({
getChat: () => chatRef.current,
onReplyComplete: (_reply: ChatMessage) => advanceScriptIndex(chatId),
}),
[chatId, advanceScriptIndex],
);
const runtime = useLocalRuntime(adapter, { initialMessages });
// Autorespond: chats arriving here from /chats/new carry the user's first
// message in seedMessages with pendingResponse=true. Trigger one startRun
// once per mount; the ref dedupes within a StrictMode mount cycle (state
// updates from consumePendingResponse aren't visible to the re-fired effect
// closure), and the store flag dedupes across mounts (e.g. navigating away
// and back to the same chat).
const didStartRef = useRef(false);
useEffect(() => {
if (!chat.pendingResponse || didStartRef.current) return;
didStartRef.current = true;
consumePendingResponse(chatId);
runtime.thread.startRun({ parentId: null });
}, [chat.pendingResponse, chatId, consumePendingResponse, runtime]);
return (
<AssistantRuntimeProvider runtime={runtime}>
<div className="h-full">
<Thread
components={{ Composer: CustomComposer }}
assistantMessage={{
components: { Text: MarkdownText, ToolFallback },
}}
/>
</div>
</AssistantRuntimeProvider>
);
}

View file

@ -0,0 +1,64 @@
import { NavLink, Outlet, useNavigate } from "react-router";
import { PencilSquareIcon } from "@heroicons/react/24/outline";
import { ChatsProvider, useChatsState } from "../lib/chats-store";
export const handle = { hideHeader: true, fullHeight: true, wide: true };
export default function ChatsLayout() {
return (
<ChatsProvider>
{/* Cancel AppShell's content padding (px-4 py-6 / sm:px-6 / lg:px-8) so the
* chat surface bleeds edge-to-edge below the top nav. The height grows
* by the vertical padding amount to recover the full viewport area. */}
<div className="fabro-chat relative isolate -mx-4 -my-6 flex h-[calc(100%+3rem)] sm:-mx-6 lg:-mx-8">
<Sidebar />
<div className="min-h-0 flex-1">
<Outlet />
</div>
</div>
</ChatsProvider>
);
}
function Sidebar() {
const state = useChatsState();
const navigate = useNavigate();
return (
<aside className="flex w-64 shrink-0 flex-col border-r border-line bg-panel">
<div className="p-3">
<button
type="button"
onClick={() => navigate("/chats/new")}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-overlay-strong px-3 py-2 text-sm font-medium text-fg ring-1 ring-line-strong transition-all hover:bg-overlay-strong hover:ring-line-strong focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500"
>
<PencilSquareIcon className="size-4" />
New chat
</button>
</div>
<nav className="flex-1 overflow-y-auto px-2 pt-2 pb-3">
{state.order.map((id) => {
const chat = state.chats[id];
if (!chat) return null;
return (
<NavLink
key={id}
to={`/chats/${id}`}
className={({ isActive }) =>
[
"relative block truncate rounded-md px-3 py-2 text-sm transition-colors",
isActive
? "bg-overlay text-fg before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:rounded-full before:bg-teal-500"
: "text-fg-3 hover:bg-overlay/60 hover:text-fg",
].join(" ")
}
title={chat.title}
>
{chat.title || "New chat"}
</NavLink>
);
})}
</nav>
</aside>
);
}

View file

@ -0,0 +1,69 @@
import { useRef, useState, type FormEvent } from "react";
import { useNavigate } from "react-router";
import { ArrowUpIcon } from "@heroicons/react/24/solid";
import { useChatsActions } from "../lib/chats-store";
import ComposerChips from "../components/chats/composer-chips";
export function meta() {
return [{ title: "New chat — Fabro" }];
}
export default function ChatsNew() {
const navigate = useNavigate();
const { createChatWithFirstMessage } = useChatsActions();
const [text, setText] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
function submit(value: string) {
const trimmed = value.trim();
if (!trimmed) return;
const id = createChatWithFirstMessage(trimmed);
navigate(`/chats/${id}`);
}
function onSubmit(e: FormEvent) {
e.preventDefault();
submit(text);
}
return (
<div className="flex h-full flex-col items-center px-6 pt-[18vh] pb-10">
<div className="w-full max-w-2xl">
<form
onSubmit={onSubmit}
className="w-full overflow-hidden rounded-2xl bg-panel-alt/80 shadow-2xl shadow-black/40 ring-1 ring-line-strong backdrop-blur-sm transition-all focus-within:ring-teal-500/40"
>
<textarea
ref={textareaRef}
name="prompt"
aria-label="Message"
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
submit(text);
}
}}
placeholder="Ask anything…"
rows={2}
autoFocus
className="block max-h-72 w-full resize-none bg-transparent px-4 pt-4 pb-2 text-base text-fg placeholder:text-fg-muted focus:outline-none"
/>
<div className="flex items-center justify-between gap-3 px-4 pt-2 pb-3">
<ComposerChips />
<button
type="submit"
disabled={!text.trim()}
aria-label="Send message"
className="inline-flex size-9 items-center justify-center rounded-full bg-teal-500 text-on-primary transition-colors hover:bg-teal-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-teal-500"
>
<ArrowUpIcon className="size-4" />
</button>
</div>
</form>
</div>
</div>
);
}

View file

@ -0,0 +1,53 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { MemoryRouter, Routes, Route } from "react-router";
import TestRenderer, { act } from "react-test-renderer";
import { setupReactTestEnv } from "../lib/test-utils";
import { ChatsProvider } from "../lib/chats-store";
import * as ChatsLayoutModule from "./chats-layout";
import * as ChatsNewModule from "./chats-new";
import * as ChatsDetailModule from "./chats-detail";
describe("chats route module exports", () => {
let teardown: () => void = () => {};
beforeEach(() => {
teardown = setupReactTestEnv();
});
afterEach(() => {
teardown();
});
test("each route exports a default component", () => {
expect(typeof ChatsLayoutModule.default).toBe("function");
expect(typeof ChatsNewModule.default).toBe("function");
expect(typeof ChatsDetailModule.default).toBe("function");
});
test("chats-layout declares the AppShell handle (children inherit via useMatches)", () => {
expect(ChatsLayoutModule.handle).toEqual({
hideHeader: true,
fullHeight: true,
wide: true,
});
});
test("chats-new renders inside MemoryRouter without crashing", () => {
let tree: TestRenderer.ReactTestRenderer | null = null;
act(() => {
tree = TestRenderer.create(
<ChatsProvider>
<MemoryRouter initialEntries={["/"]}>
<Routes>
<Route path="/" Component={ChatsNewModule.default} />
</Routes>
</MemoryRouter>
</ChatsProvider>,
);
});
expect(tree).not.toBeNull();
act(() => {
tree?.unmount();
});
});
});

View file

@ -105,7 +105,7 @@ function makeRunSummary(
},
billing: null,
diff: diffSummary,
pull_request: pullRequest ? { provider: "github", ...pullRequest } : null,
pull_request: pullRequest,
current_question: null,
superseded_by: null,
links: { web: null },
@ -449,13 +449,10 @@ describe("RunDetail full-height child routes", () => {
const renderer = await renderRunDetail({
initialEntry: "/runs/run_1",
pullRequest: {
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: "Add run PR chip",
number: 123,
html_url: "https://github.com/fabro-sh/fabro/pull/123",
},
});

View file

@ -25,9 +25,9 @@ import { useToast } from "../components/toast";
import { ConfirmDialog, SECONDARY_BUTTON_CLASS, Tooltip } from "../components/ui";
import {
isRunStatus,
mapRunSummaryToRunItem,
mapRunToRunItem,
runStatusDisplay,
type RunSummary,
type Run,
} from "../data/runs";
import { useDemoMode } from "../lib/demo-mode";
import { useSWRConfig } from "swr";
@ -107,7 +107,7 @@ function useTickingNow(intervalMs: number): number {
return now;
}
type RunDetailRun = ReturnType<typeof mapRunSummaryToRunItem> & {
type RunDetailRun = ReturnType<typeof mapRunToRunItem> & {
statusLabel: string;
statusDot: string;
statusText: string;
@ -145,8 +145,8 @@ function runHasSandbox(runState: unknown): boolean {
);
}
function buildRunDetailRun(summary: RunSummary): RunDetailRun {
const item = mapRunSummaryToRunItem(summary);
function buildRunDetailRun(summary: Run): RunDetailRun {
const item = mapRunToRunItem(summary);
const rawStatus = summary.lifecycle.status;
const statusKind = rawStatus.kind;
const display = isRunStatus(statusKind)
@ -337,7 +337,7 @@ export default function RunDetail({ params }: { params: { id: string } }) {
</span>
</Tooltip>
)}
{run.number != null && run.pullRequestUrl && (
{run.pullRequestUrl && run.number != null && (
<PullRequestChip
number={run.number}
url={run.pullRequestUrl}

View file

@ -113,12 +113,38 @@ function sandboxDetails(
native_state: null,
region: null,
resources: { cpu_cores: null, memory_bytes: null, disk_bytes: null },
network: networkDetails(),
labels: {},
timestamps: { created_at: null, last_activity_at: null },
...overrides,
};
}
function networkDetails(
overrides: Partial<SandboxDetails["network"]> = {},
): SandboxDetails["network"] {
return {
egress: networkPolicy("unknown"),
ingress: networkPolicy("unknown"),
...overrides,
};
}
function networkPolicy(
mode: SandboxDetails["network"]["egress"]["mode"],
cidrs: string[] = [],
): SandboxDetails["network"]["egress"] {
return { mode, cidrs };
}
function textContent(renderer: TestRenderer.ReactTestRenderer): string {
return renderer.root
.findAll((node) => typeof node.type === "string")
.flatMap((node) => node.children)
.filter((child): child is string => typeof child === "string")
.join(" ");
}
function renderRoute(initialPath: string = "/runs/run_1/sandbox") {
let renderer!: TestRenderer.ReactTestRenderer;
act(() => {
@ -176,6 +202,10 @@ describe("RunSandbox route", () => {
memory_bytes: 4 * 1024 * 1024 * 1024,
disk_bytes: undefined,
},
network: networkDetails({
egress: networkPolicy("open"),
ingress: networkPolicy("blocked"),
}),
labels: { run: "abc" },
timestamps: {
created_at: "2026-05-09T12:00:00Z",
@ -188,7 +218,37 @@ describe("RunSandbox route", () => {
.findAll((node) => node.type === "h3")
.map((node) => node.children.find((child) => typeof child === "string"))
.filter((text): text is string => typeof text === "string");
expect(panelHeadings).toEqual(["Overview", "Resources", "Labels", "Timestamps"]);
expect(panelHeadings).toEqual(["Overview", "Resources", "Network", "Labels", "Timestamps"]);
const copy = textContent(renderer);
expect(copy).toContain("Open");
expect(copy).toContain("Blocked");
});
test("links to the provider dashboard when a sandbox web URL is present", () => {
currentDetails = sandboxDetails({
sandbox: {
provider: "daytona",
runtime: {
id: "ad65029a-2d01-421e-8936-49451653fcd9",
working_directory: "/workspace",
},
},
web_url:
"https://app.daytona.io/dashboard/sandboxes?sandboxId=ad65029a-2d01-421e-8936-49451653fcd9",
});
const renderer = renderRoute();
const providerLinks = renderer.root.findAll(
(node) =>
node.type === "a" &&
node.props.href ===
"https://app.daytona.io/dashboard/sandboxes?sandboxId=ad65029a-2d01-421e-8936-49451653fcd9",
);
expect(providerLinks).toHaveLength(1);
expect(providerLinks[0]?.props.target).toBe("_blank");
expect(providerLinks[0]?.props.rel).toBe("noopener noreferrer");
const linkText = providerLinks[0]?.findByType("span");
expect(linkText?.children).toContain("Open in Daytona");
});
test("renders without crashing when most fields are null", () => {
@ -232,6 +292,37 @@ describe("RunSandbox route", () => {
expect(noLabelsCopy).toHaveLength(1);
});
test("renders unknown network policies", () => {
currentDetails = sandboxDetails({
network: networkDetails({
egress: networkPolicy("unknown"),
ingress: networkPolicy("unknown"),
}),
});
const renderer = renderRoute();
const copy = textContent(renderer);
expect(copy).toContain("Network");
expect(copy).toContain("Egress");
expect(copy).toContain("Ingress");
expect(copy).toContain("Unknown");
});
test("renders blocked, essentials, and CIDR network policies", () => {
currentDetails = sandboxDetails({
network: networkDetails({
egress: networkPolicy("cidr_allow_list", ["10.0.0.0/8", "192.168.0.0/16"]),
ingress: networkPolicy("essentials_only"),
}),
});
const renderer = renderRoute();
const copy = textContent(renderer);
expect(copy).toContain("CIDR allow list");
expect(copy).toContain("10.0.0.0/8, 192.168.0.0/16");
expect(copy).toContain("Essentials only");
});
test("shows the empty state when no sandbox is reported", () => {
currentDetails = null;
const renderer = renderRoute();
@ -300,7 +391,7 @@ describe("RunSandbox route", () => {
.findAll((node) => node.type === "h3")
.map((node) => node.children.find((child) => typeof child === "string"))
.filter((text): text is string => typeof text === "string");
expect(panelHeadings).toEqual(["Overview", "Resources", "Labels", "Timestamps"]);
expect(panelHeadings).toEqual(["Overview", "Resources", "Network", "Labels", "Timestamps"]);
const tabs = renderer.root.findAll(
(node) => node.type === "button" && node.props.role === "tab",

View file

@ -1,11 +1,17 @@
import { useMemo } from "react";
import { useSearchParams } from "react-router";
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid";
import TerminalView, { TERMINAL_DOCK_CLEARANCE_CLASS } from "../components/terminal-view";
import { EmptyState, ErrorState } from "../components/state";
import { formatAbsoluteTs } from "../lib/format";
import { useRunSandboxDetails } from "../lib/queries";
import type { SandboxDetails, SandboxResources, SandboxState } from "@qltysh/fabro-api-client";
import type {
SandboxDetails,
SandboxNetwork,
SandboxResources,
SandboxState,
} from "@qltysh/fabro-api-client";
import FilesystemPanel from "./run-sandbox/filesystem-panel";
import ServicesPanel from "./run-sandbox/services-panel";
import VncPanel from "./run-sandbox/vnc-panel";
@ -81,16 +87,39 @@ function nullableCpu(cores: number | null | undefined): string {
return cores != null ? formatCpuCores(cores) : EMPTY_VALUE;
}
type SandboxNetworkPolicy = SandboxNetwork["egress"];
type SandboxNetworkPolicyMode = SandboxNetworkPolicy["mode"];
const NETWORK_POLICY_DISPLAY: Record<SandboxNetworkPolicyMode, string> = {
unknown: "Unknown",
open: "Open",
blocked: "Blocked",
cidr_allow_list: "CIDR allow list",
essentials_only: "Essentials only",
};
function networkPolicySummary(policy: SandboxNetworkPolicy): string {
return NETWORK_POLICY_DISPLAY[policy.mode] ?? policy.mode;
}
interface RowProps {
label: string;
value: string;
value: React.ReactNode;
valueClassName?: string;
}
function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="flex items-center justify-between gap-4 px-4 py-2.5 text-sm">
<span className="text-fg-3">{label}</span>
{children}
</div>
);
}
function Row({ label, value, valueClassName }: RowProps) {
return (
<div className="flex items-center justify-between gap-4 px-4 py-2.5 text-sm">
<span className="text-fg-3">{label}</span>
<DetailRow label={label}>
<span
className={`text-right font-mono text-xs text-fg-2 ${
valueClassName ?? ""
@ -98,7 +127,23 @@ function Row({ label, value, valueClassName }: RowProps) {
>
{value}
</span>
</div>
</DetailRow>
);
}
function LinkRow({ label, href, text }: { label: string; href: string; text: string }) {
return (
<DetailRow label={label}>
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-w-0 items-center gap-1.5 text-right font-mono text-xs text-teal-500 transition-colors hover:text-teal-300 focus-visible:rounded-sm focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-teal-500"
>
<span className="truncate">{text}</span>
<ArrowTopRightOnSquareIcon className="size-3.5 shrink-0" aria-hidden="true" />
</a>
</DetailRow>
);
}
@ -154,6 +199,17 @@ function OverviewPanel({ details }: { details: SandboxDetails }) {
value={details.region ? details.region : sandbox.provider === "docker" ? "local" : EMPTY_VALUE}
/>
<Row label="Image" value={nullable(sandbox.image ?? sandbox.snapshot)} />
{details.web_url && (
<LinkRow
label="Provider"
href={details.web_url}
text={
sandbox.provider === "daytona"
? "Open in Daytona"
: `Open in ${sandbox.provider}`
}
/>
)}
</Panel>
);
}
@ -168,6 +224,23 @@ function ResourcesPanel({ resources }: { resources: SandboxResources }) {
);
}
function NetworkPanel({ network }: { network: SandboxNetwork }) {
const cidrRows: Array<{ label: string; policy: SandboxNetworkPolicy }> = [
{ label: "Egress CIDRs", policy: network.egress },
{ label: "Ingress CIDRs", policy: network.ingress },
].filter(({ policy }) => policy.mode === "cidr_allow_list");
return (
<Panel title="Network">
<Row label="Egress" value={networkPolicySummary(network.egress)} />
<Row label="Ingress" value={networkPolicySummary(network.ingress)} />
{cidrRows.map(({ label, policy }) => (
<Row key={label} label={label} value={policy.cidrs.join(", ") || EMPTY_VALUE} />
))}
</Panel>
);
}
function LabelsPanel({ labels }: { labels: { [key: string]: string } | null | undefined }) {
const entries = labels ? Object.entries(labels) : [];
return (
@ -209,6 +282,7 @@ function DetailsColumn({ details }: { details: SandboxDetails | null }) {
<StatusStrip details={details} />
<OverviewPanel details={details} />
<ResourcesPanel resources={details.resources} />
<NetworkPanel network={details.network} />
<LabelsPanel labels={details.labels} />
<TimestampsPanel details={details} />
</div>

View file

@ -1,5 +1,6 @@
import { useMemo, useState } from "react";
import { useParams } from "react-router";
import type { WorkflowSettings } from "@qltysh/fabro-api-client";
import { CollapsibleFile } from "../components/collapsible-file";
import { StageSidebar } from "../components/stage-sidebar";
import {
@ -17,7 +18,6 @@ import {
import { useRunSettings, useRunStages } from "../lib/queries";
import { mapRunStagesToSidebarStages } from "../lib/stage-sidebar";
import {
type Snapshot,
type UnknownRecord,
getArray,
getBool,
@ -31,7 +31,7 @@ export const handle = { wide: true };
export default function RunSettingsPage() {
const { id } = useParams();
const stagesQuery = useRunStages(id);
const settingsQuery = useRunSettings<Snapshot>(id);
const settingsQuery = useRunSettings<WorkflowSettings>(id);
const stages = useMemo(
() => mapRunStagesToSidebarStages(stagesQuery.data),
[stagesQuery.data],
@ -94,7 +94,7 @@ function PageIntro({
);
}
function WorkflowPanel({ snapshot }: { snapshot: Snapshot }) {
function WorkflowPanel({ snapshot }: { snapshot: WorkflowSettings }) {
const workflow = getObject(snapshot, "workflow");
const run = getObject(snapshot, "run");
const name = getString(workflow, "name");
@ -135,7 +135,7 @@ function WorkflowPanel({ snapshot }: { snapshot: Snapshot }) {
);
}
function SandboxPanel({ snapshot }: { snapshot: Snapshot }) {
function SandboxPanel({ snapshot }: { snapshot: WorkflowSettings }) {
const sandbox = getObject(getObject(snapshot, "run"), "sandbox");
const provider = getString(sandbox, "provider");
const docker = getObject(sandbox, "docker");
@ -166,7 +166,7 @@ function SandboxPanel({ snapshot }: { snapshot: Snapshot }) {
);
}
function GitPanel({ snapshot }: { snapshot: Snapshot }) {
function GitPanel({ snapshot }: { snapshot: WorkflowSettings }) {
const run = getObject(snapshot, "run");
const author = getObject(getObject(run, "git"), "author");
const authorName = getString(author, "name");
@ -199,7 +199,7 @@ function GitPanel({ snapshot }: { snapshot: Snapshot }) {
);
}
function ArtifactsPanel({ snapshot }: { snapshot: Snapshot }) {
function ArtifactsPanel({ snapshot }: { snapshot: WorkflowSettings }) {
const artifacts = getObject(getObject(snapshot, "run"), "artifacts");
const include = getArray(artifacts, "include");
return (

View file

@ -22,7 +22,7 @@ import {
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { ciConfig, columnForRun, columnStatusDisplay, columnStatuses, deriveCiStatus, mapRunListItem } from "../data/runs";
import type { CiStatus, CheckRun, CheckStatus, RunItem, RunWithStatus, ColumnStatus } from "../data/runs";
import type { CiStatus, CheckRun, CheckStatus, RunItem, RunWithStatus } from "../data/runs";
import { formatRelativeTime } from "../lib/format";
import { EmptyState } from "../components/state";
import { InlineMarkdown } from "../components/inline-markdown";
@ -32,7 +32,7 @@ import { shouldRefreshBoardForEvent, useBoardEvents } from "../lib/board-events"
import { useAuthConfig, useBoardsRuns, useSystemInfo } from "../lib/queries";
import { queryKeys } from "../lib/query-keys";
import { archiveRun, canArchive } from "../lib/run-actions";
import type { PaginatedBoardRunList } from "@qltysh/fabro-api-client";
import type { BoardColumn, PaginatedBoardRunList } from "@qltysh/fabro-api-client";
export { shouldRefreshBoardForEvent };
@ -44,7 +44,7 @@ interface ColumnStyle {
actions: string[];
}
const columnStyles: Record<ColumnStatus, ColumnStyle> = {
const columnStyles: Record<BoardColumn, ColumnStyle> = {
queued: { actions: [] },
initializing: { actions: [] },
running: { actions: [] },
@ -64,7 +64,7 @@ interface BoardRunsResponse {
}
type Column = {
id: ColumnStatus;
id: BoardColumn;
name: string;
dot: string;
text: string;
@ -287,7 +287,7 @@ function PrCard({
{lifecycleLabel}
</span>
)}
{pr.number != null && (
{pr.pullRequestUrl && pr.number != null && (
<PullRequestChip
number={pr.number}
url={pr.pullRequestUrl}
@ -499,7 +499,7 @@ function ColumnActionsMenu({ column }: { column: Column }) {
);
}
function BoardColumn({ column }: { column: Column }) {
function BoardColumnView({ column }: { column: Column }) {
const actions = column.actions;
return (
<div className="flex min-w-0 flex-col">
@ -632,7 +632,7 @@ function RunRow({ run }: { run: RunWithStatus }) {
</Link>
<span className="inline-flex items-center justify-end gap-1.5 font-mono text-xs text-fg-muted">
{run.number != null && (
{run.pullRequestUrl && run.number != null && (
<PullRequestChip number={run.number} url={run.pullRequestUrl}>
{run.checks != null && <span className={`size-1.5 rounded-full ${ciConfig[deriveCiStatus(run.checks)].dot}`} />}
</PullRequestChip>
@ -974,7 +974,7 @@ export default function Runs() {
<div className="flex gap-5 overflow-x-auto pb-4">
{visibleColumns.map((col) => (
<div key={col.id} className="w-72 shrink-0">
<BoardColumn column={col} />
<BoardColumnView column={col} />
</div>
))}
</div>

View file

@ -9,6 +9,9 @@
"typecheck": "tsc"
},
"dependencies": {
"@assistant-ui/react": "0.14.5",
"@assistant-ui/react-markdown": "0.14.0",
"@assistant-ui/react-ui": "0.2.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",

449
bun.lock
View file

@ -6,6 +6,9 @@
"apps/fabro-web": {
"name": "fabro-web",
"dependencies": {
"@assistant-ui/react": "0.14.5",
"@assistant-ui/react-markdown": "0.14.0",
"@assistant-ui/react-ui": "0.2.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@ -79,6 +82,18 @@
},
},
"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=="],
"@astrojs/compiler": ["@astrojs/compiler@3.0.1", "", {}, "sha512-z97oYbdebO5aoWzuJ/8q5hLK232+17KcLZ7cJ8BCWk6+qNzVxn/gftC0KzMBUTD8WAaBkPpNSQK6PXLnNrZ0CA=="],
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.9.0", "", { "dependencies": { "picomatch": "^4.0.4" } }, "sha512-GdYkzR26re8izmyYlBqf4z2s7zNngmWLFuxw0UKiPNqHraZGS6GKWIwSHgS22RDlu2ePFJ8bzmpBcUszut/SDg=="],
@ -123,6 +138,8 @@
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
@ -335,6 +352,126 @@
"@qltysh/fabro-api-client": ["@qltysh/fabro-api-client@workspace:lib/packages/fabro-api-client"],
"@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.11", "", { "dependencies": { "@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-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-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="],
"@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.21.4", "", { "dependencies": { "@react-aria/interactions": "^3.27.0", "@react-aria/utils": "^3.33.0", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.0", "clsx": "^2.0.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-6gz+j9ip0/vFRTKJMl3R30MHopn4i19HqqLfSQfElxJD+r9hBnYG1Q6Wd/kl/WRR1+CALn2F+rn06jUnf5sT8Q=="],
"@react-aria/interactions": ["@react-aria/interactions@3.27.0", "", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-aria/utils": "^3.33.0", "@react-stately/flags": "^3.1.2", "@react-types/shared": "^3.33.0", "@swc/helpers": "^0.5.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-D27pOy+0jIfHK60BB26AgqjjRFOYdvVSkwC31b2LicIzRCSPOSP06V4gMHuGmkhNTF4+YWDi1HHYjxIvMeiSlA=="],
@ -489,6 +626,8 @@
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@swc/helpers": ["@swc/helpers@0.5.19", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA=="],
"@tailwindcss/cli": ["@tailwindcss/cli@4.2.2", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "enhanced-resolve": "^5.19.0", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.2.2" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-iJS+8kAFZ8HPqnh0O5DHCLjo4L6dD97DBQEkrhfSO4V96xeefUus2jqsBs1dUMt3OU9Ks4qIkiY0mpL5UW+4LQ=="],
@ -551,6 +690,8 @@
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@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/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
@ -629,10 +770,16 @@
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="],
"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=="],
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
"astro": ["astro@6.1.9", "", { "dependencies": { "@astrojs/compiler": "^3.0.1", "@astrojs/internal-helpers": "0.9.0", "@astrojs/markdown-remark": "7.1.1", "@astrojs/telemetry": "3.3.1", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^1.1.1", "devalue": "^5.6.3", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.27.3", "flattie": "^1.1.1", "fontace": "~0.4.1", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "rehype": "^13.0.2", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unist-util-visit": "^5.1.0", "unstorage": "^1.17.5", "vfile": "^6.0.3", "vite": "^7.3.2", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "bin/astro.mjs" } }, "sha512-NsAHzMzpznB281g2aM5qnBt2QjfH6ttKiZ3hSZw52If8JJ+62kbnBKbyKhR2glQcJLl7Jfe4GSl0DihFZ36rRQ=="],
@ -669,12 +816,18 @@
"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=="],
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="],
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
"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=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
@ -725,6 +878,8 @@
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"devalue": ["devalue@5.7.1", "", {}, "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
@ -783,6 +938,8 @@
"estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
@ -835,6 +992,8 @@
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
@ -869,6 +1028,8 @@
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"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-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
"hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="],
@ -881,6 +1042,8 @@
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
@ -889,14 +1052,24 @@
"icss-utils": ["icss-utils@5.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA=="],
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
"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-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
@ -961,6 +1134,8 @@
"lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="],
"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=="],
"magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="],
@ -991,6 +1166,12 @@
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
"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=="],
@ -1077,7 +1258,7 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nanoid": ["nanoid@5.1.11", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="],
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
@ -1123,6 +1304,8 @@
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
"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=="],
"parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="],
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
@ -1169,6 +1352,8 @@
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"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=="],
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
@ -1177,12 +1362,22 @@
"react-is": ["react-is@19.2.5", "", {}, "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ=="],
"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-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
"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.12.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw=="],
"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-test-renderer": ["react-test-renderer@19.2.0", "", { "dependencies": { "react-is": "^19.2.0", "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-zLCFMHFE9vy/w3AxO0zNxy6aAupnCuLSVOJYDe/Tp+ayGI1f2PLQsFVPANSD42gdSbmYx5oN+1VWDhcXtq7hAQ=="],
"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=="],
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
@ -1225,12 +1420,16 @@
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"safe-content-frame": ["safe-content-frame@0.0.19", "", {}, "sha512-+R0IHHjvghT5O8bc8itf9AoS9MvzhUcD0p+hNINLgyEuFQJug3wt3ZuhLFZFG3bUzHi8UfQED4p6J3/Ft9oCtg=="],
"sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="],
"secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
@ -1265,6 +1464,10 @@
"style-loader": ["style-loader@4.0.0", "", { "peerDependencies": { "webpack": "^5.27.0" } }, "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA=="],
"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=="],
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="],
@ -1339,6 +1542,18 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"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=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
@ -1383,7 +1598,9 @@
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"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=="],
@ -1405,14 +1622,166 @@
"@parcel/watcher/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"@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-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=="],
"@remotion/bundler/esbuild": ["esbuild@0.25.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.0", "@esbuild/android-arm": "0.25.0", "@esbuild/android-arm64": "0.25.0", "@esbuild/android-x64": "0.25.0", "@esbuild/darwin-arm64": "0.25.0", "@esbuild/darwin-x64": "0.25.0", "@esbuild/freebsd-arm64": "0.25.0", "@esbuild/freebsd-x64": "0.25.0", "@esbuild/linux-arm": "0.25.0", "@esbuild/linux-arm64": "0.25.0", "@esbuild/linux-ia32": "0.25.0", "@esbuild/linux-loong64": "0.25.0", "@esbuild/linux-mips64el": "0.25.0", "@esbuild/linux-ppc64": "0.25.0", "@esbuild/linux-riscv64": "0.25.0", "@esbuild/linux-s390x": "0.25.0", "@esbuild/linux-x64": "0.25.0", "@esbuild/netbsd-arm64": "0.25.0", "@esbuild/netbsd-x64": "0.25.0", "@esbuild/openbsd-arm64": "0.25.0", "@esbuild/openbsd-x64": "0.25.0", "@esbuild/sunos-x64": "0.25.0", "@esbuild/win32-arm64": "0.25.0", "@esbuild/win32-ia32": "0.25.0", "@esbuild/win32-x64": "0.25.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw=="],
"@remotion/renderer/source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="],
"@remotion/studio/semver": ["semver@7.5.3", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ=="],
"@remotion/studio/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"@remotion/studio-server/semver": ["semver@7.5.3", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ=="],
"@remotion/zod-types/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"@rollup/pluginutils/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"@shikijs/primitive/@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="],
@ -1445,6 +1814,8 @@
"astro/shiki": ["shiki@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="],
"astro/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
@ -1461,10 +1832,22 @@
"open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"postcss-modules-local-by-default/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
"postcss-modules-scope/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
"radix-ui/@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/@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=="],
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
@ -1491,6 +1874,68 @@
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@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-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=="],
"@remotion/bundler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ=="],
"@remotion/bundler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.0", "", { "os": "android", "cpu": "arm" }, "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g=="],

View file

@ -2042,6 +2042,49 @@ These legacy events may appear in older run logs. Current CLI backend runs do no
| `pr_number` | number | Pull request number |
| `draft` | boolean | Whether the PR is a draft |
### `pull_request.linked`
```json
{
"id": "...", "ts": "...", "run_id": "...",
"event": "pull_request.linked",
"properties": {
"pull_request": {
"provider": "github",
"html_url": "https://github.com/org/repo/pull/42",
"number": 42,
"owner": "org",
"repo": "repo",
"title": "Review deployment chart"
}
}
}
```
| Property | Type | Description |
|----------|------|-------------|
| `pull_request` | object | Stored GitHub pull request association. `title`, `base_branch`, and `head_branch` may be included when live GitHub metadata is available. |
### `pull_request.unlinked`
```json
{
"id": "...", "ts": "...", "run_id": "...",
"event": "pull_request.unlinked",
"properties": {
"pull_request": {
"provider": "github",
"html_url": "https://github.com/org/repo/pull/42",
"number": 42
}
}
}
```
| Property | Type | Description |
|----------|------|-------------|
| `pull_request` | object | Pull request association removed from the run. |
### `pull_request.failed`
```json

View file

@ -57,14 +57,14 @@ digraph Pipeline {
}
```
At runtime, `{{ goal }}` becomes `Add a /health endpoint to the API server`.
Before execution, `{{ goal }}` becomes `Add a /health endpoint to the API server`.
| Expression | Resolves to |
|---|---|
| `{{ goal }}` | The graph-level `goal` attribute |
| `{{ inputs.name }}` | A value from `[run.inputs]` |
Prompt templates use strict undefined handling, so an expression like `{{ inputs.foo }}` fails fast if `foo` is not defined. Environment variables are not available in prompt templates.
Undefined prompt variables render as empty text and produce a `template_undefined_variable` diagnostic. `fabro validate` reports that diagnostic as a warning; run-style commands promote it to an error before proceeding. Environment variables are not available in prompt templates.
### Fallback to label

File diff suppressed because it is too large Load diff

View file

@ -47,6 +47,7 @@ Fabro's catalog starts with the built-in providers and models, then merges any `
[llm.providers.proxy]
display_name = "Acme Gateway"
adapter = "openai_compatible"
agent_profile = "openai"
base_url = "https://llm-gateway.example.com/v1"
credentials = ["env:ACME_GATEWAY_API_KEY"]
aliases = ["gateway"]
@ -58,6 +59,7 @@ x-portkey-config = { literal = "@bedrock-prod" }
[llm.models."team-code-large"]
provider = "proxy"
api_id = "provider-wire-model-name"
agent_profile = "anthropic"
display_name = "Team Code Large"
family = "team-code"
default = true
@ -90,14 +92,51 @@ output_cost_per_mtok = 16.00
cache_input_cost_per_mtok = 0.60
```
For [LiteLLM](/integrations/litellm), Fabro ships a disabled provider entry. Enable it in settings and declare the models your proxy exposes:
```toml title="settings.toml"
[llm.providers.litellm]
enabled = true
base_url = "http://localhost:4000/v1"
[llm.models."litellm-gpt-5"]
provider = "litellm"
api_id = "gpt-5"
display_name = "LiteLLM GPT-5"
family = "litellm"
default = true
[llm.models."litellm-gpt-5".limits]
context_window = 128000
max_output = 8192
[llm.models."litellm-gpt-5".features]
tools = true
vision = false
reasoning = false
```
`api_id` is the model name sent to the provider API. Omit it when the Fabro model ID and provider model ID are the same.
Header values and credentials are typed references, not raw secrets. Use `env:<NAME>` or `credential:<id>` for provider credentials, and `{ env = "NAME" }`, `{ credential = "id" }`, or `{ literal = "value" }` for extra headers.
`agent_profile` controls profile-specific behavior such as project-memory filenames, CLI/ACP command selection, and native session routing. Valid values are `anthropic`, `openai`, and `gemini`; model-level values override provider-level values, which override the adapter default.
<Note>
Provider fields in configuration, APIs, and model routing are provider ID strings. Built-in names like `anthropic`, `openai`, and `gemini` still work, but custom IDs like `proxy` work anywhere a provider ID is accepted.
</Note>
### Ollama
Fabro ships an Ollama provider definition that is disabled by default. Enable it in settings when you want Fabro to route through a local Ollama server:
```toml title="settings.toml"
[llm.providers.ollama]
enabled = true
```
Enabling the provider alone does not expose any models — until #267 adds auto-discovery, add explicit `[llm.models.<id>]` blocks for each Ollama model you have pulled locally. Ollama's OpenAI-compatible endpoint accepts any bearer token, so local users can set `OLLAMA_API_KEY=ollama`.
## Default models
When no model or provider is specified, Fabro checks configured provider credentials and chooses the first configured provider by catalog priority. If no provider credentials are configured, it uses the catalog's global default model. Each provider has a default model:

View file

@ -93,6 +93,7 @@
"pages": [
"integrations/github",
"integrations/daytona",
"integrations/litellm",
"integrations/slack",
"integrations/brave-search"
]

View file

@ -355,7 +355,7 @@ exclude_globs = ["**/node_modules/**", "**/.cache/**", "**/dist/**"]
### `[run.inputs]`
Define inputs that are expanded into the Graphviz source before the graph is parsed. See [Variables](/workflows/variables) for the full reference.
Define inputs that are rendered into final workflow string attributes. See [Variables](/workflows/variables) for the full reference.
```toml title="run.toml"
[run.inputs]
@ -364,7 +364,7 @@ repo_url = "https://github.com/fabro-sh/fabro"
language = "rust"
```
Inputs can be used anywhere in the Graphviz file with `{{ inputs.name }}` syntax:
Inputs can be used in final string attributes with `{{ inputs.name }}` syntax:
```dot title="c-i.fabro"
digraph CI {
@ -374,7 +374,9 @@ digraph CI {
}
```
If a workflow template references an undefined input like `{{ inputs.langauge }}`, Fabro raises an error immediately.
Inputs cannot parameterize workflow structure or file references such as node IDs, edges, `import` paths, `@file` paths, or child workflow paths.
If a workflow template references an undefined input like `{{ inputs.langauge }}`, `fabro validate` reports a warning. Run-style commands promote that diagnostic to an error before creating or starting a run.
TOML `[run.inputs]` tables replace wholesale across layers. Unlike labels, TOML input tables do not merge by key — the highest-precedence config layer that sets `inputs` wins its entire map.
@ -457,7 +459,7 @@ id = "pre-check"
name = "Pre-check script"
event = "stage_start"
script = "./scripts/pre-check.sh"
matcher = "agent_loop"
matcher = "agent"
blocking = true
timeout = "30s"
sandbox = false

View file

@ -0,0 +1,133 @@
---
title: "LiteLLM"
description: "Route Fabro models through a LiteLLM proxy"
---
[LiteLLM](https://docs.litellm.ai/) can run as an OpenAI-compatible proxy in front of many model providers. Fabro includes a disabled `litellm` provider entry so you can opt in from `settings.toml` without changing Fabro code.
## Prerequisites
- A running LiteLLM proxy reachable from the Fabro process
- At least one LiteLLM model name you want Fabro to route to
- A LiteLLM key or placeholder key available to Fabro
Fabro's built-in LiteLLM provider points at `http://localhost:4000/v1`. Change `base_url` if your proxy is hosted elsewhere.
## Enable the provider
Add the provider override and one or more model entries to `~/.fabro/settings.toml`:
```toml title="settings.toml"
_version = 1
[llm.providers.litellm]
enabled = true
base_url = "http://localhost:4000/v1"
[llm.models."litellm-gpt-5"]
provider = "litellm"
api_id = "gpt-5"
display_name = "LiteLLM GPT-5"
family = "litellm"
default = true
[llm.models."litellm-gpt-5".limits]
context_window = 128000
max_output = 8192
[llm.models."litellm-gpt-5".features]
tools = true
vision = false
reasoning = false
```
`api_id` is the model name Fabro sends to LiteLLM. It should match a model name configured in your LiteLLM proxy.
## Configure credentials
The LiteLLM provider checks `credential:litellm` first, then `LITELLM_API_KEY` from the Fabro process environment.
For a server-owned secret:
```bash
fabro secret set litellm sk-proxy-key
```
For a process environment variable:
```bash
export LITELLM_API_KEY=sk-proxy-key
```
If your local LiteLLM proxy does not enforce authentication, use a placeholder value such as `anything`; the OpenAI-compatible client still needs a credential value.
## Use LiteLLM models
Once the provider is enabled and at least one model is declared, use the Fabro model ID like any other catalog model:
```bash
fabro model list --provider litellm
fabro model test --model litellm-gpt-5
fabro run workflow.fabro --model litellm-gpt-5
```
In workflow stylesheets:
```dot title="workflow.fabro"
digraph Example {
graph [
model_stylesheet="
* { model: litellm-gpt-5; }
"
]
start [shape=Mdiamond, label="Start"]
work [label="Work", prompt="Use the configured LiteLLM model."]
exit [shape=Msquare, label="Exit"]
start -> work -> exit
}
```
## Declaring more models
Declare each LiteLLM-routed model explicitly so Fabro knows its provider, context window, tool support, and routing defaults:
```toml title="settings.toml"
[llm.models."litellm-fast"]
provider = "litellm"
api_id = "fast-model"
display_name = "LiteLLM Fast"
family = "litellm"
aliases = ["fast"]
[llm.models."litellm-fast".limits]
context_window = 64000
max_output = 4096
[llm.models."litellm-fast".features]
tools = true
vision = false
reasoning = false
```
Only one model for a provider should set `default = true`.
## Troubleshooting
**"No API key configured"** — Set `credential:litellm` with `fabro secret set litellm ...` or export `LITELLM_API_KEY` in the Fabro process environment.
**Connection refused** — Confirm the LiteLLM proxy is running and that `base_url` is reachable from the Fabro process. For Docker deployments, `localhost` means the Fabro container unless you point it at a host or service name.
**Unknown model from LiteLLM** — Check that the model's `api_id` matches the model name configured in LiteLLM, then run `fabro model test --model <fabro-model-id>`.
## Further reading
<Columns cols={2}>
<Card title="Models" icon="microchip" href="/core-concepts/models">
How Fabro routes model IDs, providers, and fallbacks.
</Card>
<Card title="Settings Configuration" icon="gear" href="/reference/user-configuration">
Full reference for `[llm.providers.<id>]` and `[llm.models.<id>]`.
</Card>
</Columns>

View file

@ -81,6 +81,7 @@ fabro [OPTIONS] [COMMAND]
| `fabro logs` | View the raw worker tracing log of a workflow run |
| `fabro mcp` | Model Context Protocol server |
| `fabro model` | List and test LLM models |
| `fabro parent` | Manage run parent links |
| `fabro pr` | Pull request operations |
| `fabro preflight` | Validate run configuration without executing |
| `fabro provider` | Provider operations |
@ -92,6 +93,7 @@ fabro [OPTIONS] [COMMAND]
| `fabro sandbox` | Sandbox operations (cp, ssh, preview) |
| `fabro secret` | Manage server-owned secrets |
| `fabro server` | Server operations |
| `fabro session` | Run a persistent Fabro agent session |
| `fabro settings` | Inspect effective settings |
| `fabro start` | Start a created workflow run on the server |
| `fabro steer` | Steer a running agent mid-execution |
@ -306,6 +308,7 @@ fabro create [OPTIONS] <WORKFLOW>
| `--goal-file <goal_file>` | Read the workflow goal from a file |
| `--label <key=value>` | Attach a label to this run (repeatable, format: KEY=VALUE) |
| `--model <model>` | Override default LLM model |
| `--parent <run>` | Link this run to an existing orchestration parent run |
| `--preserve-sandbox` | Keep the sandbox alive after the run finishes (for debugging) |
| `--provider <provider>` | Override default LLM provider |
| `--sandbox <sandbox>` | Sandbox for agent tools<br />Values: `local`, `docker`, `daytona` |
@ -627,6 +630,62 @@ fabro model test [OPTIONS]
| `-p, --provider <provider>` | Filter by provider |
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
### `fabro parent`
Manage run parent links
```bash
fabro parent [OPTIONS] <COMMAND>
```
#### Subcommands
| Command | Description |
| --- | --- |
| `fabro parent link` | Link or replace a run's orchestration parent |
| `fabro parent unlink` | Unlink a run from its orchestration parent |
#### `fabro parent link`
Link or replace a run's orchestration parent
```bash
fabro parent link [OPTIONS] <CHILD_RUN> <PARENT_RUN>
```
#### Arguments
| Name | Description |
| --- | --- |
| `CHILD_RUN` | Child run selector |
| `PARENT_RUN` | Parent run selector |
#### Options
| Option | Description |
| --- | --- |
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
#### `fabro parent unlink`
Unlink a run from its orchestration parent
```bash
fabro parent unlink [OPTIONS] <CHILD_RUN>
```
#### Arguments
| Name | Description |
| --- | --- |
| `CHILD_RUN` | Child run selector |
#### Options
| Option | Description |
| --- | --- |
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
### `fabro pr`
Pull request operations
@ -641,7 +700,9 @@ fabro pr [OPTIONS] <COMMAND>
| --- | --- |
| `fabro pr close` | Close a pull request |
| `fabro pr create` | Create a pull request from a completed run |
| `fabro pr link` | Link or replace the GitHub pull request associated with a run |
| `fabro pr merge` | Merge a pull request |
| `fabro pr unlink` | Unlink the pull request associated with a run |
| `fabro pr view` | View pull request details |
#### `fabro pr close`
@ -686,6 +747,27 @@ fabro pr create [OPTIONS] <RUN_ID>
| `--model <model>` | LLM model for generating PR description |
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
#### `fabro pr link`
Link or replace the GitHub pull request associated with a run
```bash
fabro pr link [OPTIONS] <RUN_ID> <URL>
```
#### Arguments
| Name | Description |
| --- | --- |
| `RUN_ID` | Run ID or prefix |
| `URL` | GitHub pull request URL to associate with the run |
#### Options
| Option | Description |
| --- | --- |
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
#### `fabro pr merge`
Merge a pull request
@ -707,6 +789,26 @@ fabro pr merge [OPTIONS] <RUN_ID>
| `--method <method>` | Merge method: merge, squash, or rebase<br />Values: `merge`, `squash`, `rebase`<br />Default: `squash` |
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
#### `fabro pr unlink`
Unlink the pull request associated with a run
```bash
fabro pr unlink [OPTIONS] <RUN_ID>
```
#### Arguments
| Name | Description |
| --- | --- |
| `RUN_ID` | Run ID or prefix |
#### Options
| Option | Description |
| --- | --- |
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
#### `fabro pr view`
View pull request details
@ -910,6 +1012,7 @@ fabro run [OPTIONS] <WORKFLOW>
| `--goal-file <goal_file>` | Read the workflow goal from a file |
| `--label <key=value>` | Attach a label to this run (repeatable, format: KEY=VALUE) |
| `--model <model>` | Override default LLM model |
| `--parent <run>` | Link this run to an existing orchestration parent run |
| `--preserve-sandbox` | Keep the sandbox alive after the run finishes (for debugging) |
| `--provider <provider>` | Override default LLM provider |
| `--sandbox <sandbox>` | Sandbox for agent tools<br />Values: `local`, `docker`, `daytona` |
@ -1164,6 +1267,25 @@ fabro server stop [OPTIONS]
| `--storage-dir <storage_dir>` | Local storage directory (default: ~/.fabro/storage) |
| `--timeout <timeout>` | Seconds to wait for graceful shutdown before SIGKILL<br />Default: `10` |
### `fabro session`
Run a persistent Fabro agent session
```bash
fabro session [OPTIONS] --prompt <PROMPT>
```
#### Options
| Option | Description |
| --- | --- |
| `--model <model>` | Model name (defaults per provider) |
| `--permissions <level>` | Permission level for tool execution<br />Values: `read-only`, `read-write`, `full` |
| `-p, --prompt <prompt>` | Task prompt |
| `--provider <provider>` | LLM provider (anthropic, openai, gemini, kimi, zai, minimax, inception) |
| `--server <server>` | Fabro server target: http(s) URL or absolute Unix socket path |
| `--storage-dir <storage_dir>` | Local storage directory (default: ~/.fabro/storage) |
### `fabro settings`
Inspect effective settings

View file

@ -87,6 +87,7 @@ level = "info"
[llm.providers.proxy]
display_name = "Acme Gateway"
adapter = "openai_compatible"
agent_profile = "openai"
base_url = "https://llm-gateway.example.com/v1"
credentials = ["env:ACME_GATEWAY_API_KEY"]
aliases = ["gateway"]
@ -98,6 +99,7 @@ x-portkey-config = { literal = "@bedrock-prod" }
[llm.models."team-code-large"]
provider = "proxy"
api_id = "provider-wire-model-name"
agent_profile = "anthropic"
display_name = "Team Code Large"
default = true
aliases = ["team-code"]
@ -173,6 +175,7 @@ providers can be added when they use an adapter Fabro already supports.
[llm.providers.proxy]
display_name = "Acme Gateway"
adapter = "openai_compatible"
agent_profile = "openai"
base_url = "https://llm-gateway.example.com/v1"
credentials = ["env:ACME_GATEWAY_API_KEY"]
priority = 50
@ -189,6 +192,7 @@ x-team-secret = { credential = "gateway_team_secret" }
|---|---|---|---|
| `display_name` | string | provider ID | Human-readable provider name. |
| `adapter` | string | inferred for built-ins | Adapter registry key, such as `"anthropic"`, `"openai"`, `"gemini"`, or `"openai_compatible"`. Custom providers normally use `"openai_compatible"`. |
| `agent_profile` | `"anthropic"` \| `"openai"` \| `"gemini"` | adapter default | Agent profile used for project memory, CLI/ACP command selection, and native session routing. |
| `base_url` | string | adapter default | Provider API base URL. Required for most custom OpenAI-compatible providers. |
| `credentials` | array<string> | built-in env refs | Ordered credential refs. Accepted string forms are `credential:<id>` and `env:<NAME>`. Literal secret strings are rejected. |
| `extra_headers` | table | `{}` | Additional headers attached to provider requests. Values must be typed refs: `{ literal = "..." }`, `{ env = "NAME" }`, or `{ credential = "id" }`. |
@ -206,6 +210,7 @@ provider API.
[llm.models."team-code-large"]
provider = "proxy"
api_id = "provider-wire-model-name"
agent_profile = "anthropic"
display_name = "Team Code Large"
family = "team-code"
default = true
@ -223,7 +228,6 @@ vision = false
reasoning = true
reasoning_effort = "levels"
prompt_cache = true
effort = true
[llm.models."team-code-large".controls]
reasoning_effort = ["low", "medium", "high"]
@ -244,6 +248,7 @@ cache_input_cost_per_mtok = 0.60
|---|---|---|---|
| `provider` | string | None | Provider ID this model belongs to. |
| `api_id` | string | model ID | Identifier sent to the provider API. |
| `agent_profile` | `"anthropic"` \| `"openai"` \| `"gemini"` | provider profile | Agent profile override for this model. Model overrides take precedence over provider overrides. |
| `display_name` | string | model ID | Human-readable model name. |
| `family` | string | model ID | Family label used for catalog display and matching. |
| `training` | string | None | Training data cutoff label. |
@ -269,7 +274,6 @@ cache_input_cost_per_mtok = 0.60
| `reasoning` | boolean | `false` | Whether the model has reasoning behavior. |
| `reasoning_effort` | `"levels"` \| `"none"` | `"none"` | How Fabro may expose reasoning effort for this model. |
| `prompt_cache` | boolean | `false` | Whether prompt cache pricing/usage applies. |
| `effort` | boolean | `false` | Whether the provider exposes a native effort parameter. |
## `[llm.models.<id>.controls]`

View file

@ -67,7 +67,7 @@ fabro run docs/internal/demo/12-sub-workflow.fabro
## The house node
The `impl` node has `shape=house`, which makes it a **sub-workflow node**. Instead of running an LLM or a script, it launches an entirely separate workflow engine to execute the child workflow file:
The `impl` node has `shape=house`, which makes it a **sub-workflow node**. Instead of running an LLM or a script, it launches a separate engine to execute the child workflow file:
```dot
impl [label="Implement & Test", shape=house, stack.child_workflow="implement-and-test.fabro", manager.max_cycles=50]
@ -79,8 +79,7 @@ The child workflow runs through its own start → implement → validate → gat
| Attribute | Description |
|---|---|
| `stack.child_workflow` | Path to the child workflow file (resolved relative to the parent). Preferred. |
| `stack.child_dotfile` | Backward-compatible alias for `stack.child_workflow`. |
| `stack.child_workflow` | Path to the child workflow file (resolved relative to the parent). |
| `stack.child_dot_source` | Inline child Graphviz source (alternative to `child_workflow`) |
| `manager.max_cycles` | Safety limit on poll cycles before the child is cancelled (default: 1000) |
| `manager.poll_interval` | How often to check for completion or stop conditions (default: `45s`) |
@ -88,9 +87,11 @@ The child workflow runs through its own start → implement → validate → gat
Use `stack.child_workflow` when you want to reuse the child workflow across multiple parents. Use `stack.child_dot_source` for one-off child workflows that are specific to the parent.
## Context flow
## What's shared and what's isolated
Context flows bidirectionally between parent and child:
The child shares the parent's **run ID and event stream** — its stage events appear in the same run, so `fabro events` and `fabro inspect` see parent and child interleaved. The child gets its own **checkpoints, artifacts, and run directory**, so its working state can't pollute the parent.
Context flows bidirectionally:
1. **Parent → child:** The child receives a clone of the parent's context. In this example, the `plan` node writes `plan.md` to disk and the child's `implement` node reads it.
2. **Child → parent:** When the child finishes, any context values it added or changed are merged back into the parent. The `review` node sees the results of the child's work.
@ -115,7 +116,7 @@ The parent polls at `manager.poll_interval` (default 45 seconds). On each poll,
Sub-workflows are most valuable when:
- **Reusability** — the same child workflow is used by multiple parents (e.g., a standard test-and-fix loop, a deploy pipeline, a review checklist)
- **Encapsulation** — the child runs its own engine with isolated logs and checkpoints, keeping the parent's execution trace clean
- **Encapsulation** — the child has its own checkpoints and artifacts, so its working state can't pollute the parent's
- **Supervisor patterns** — the parent needs to monitor or cancel a complex child process based on external conditions
For simpler cases, just add more nodes to a single workflow. Sub-workflows add a layer of indirection — use them when the benefits of reuse or encapsulation justify it.
@ -123,7 +124,7 @@ For simpler cases, just add more nodes to a single workflow. Sub-workflows add a
## What you've learned
- **Sub-workflow nodes** (`shape=house`) run a child workflow inside a parent
- **`stack.child_workflow`** references an external workflow file for reuse (`stack.child_dotfile` is also supported for backward compatibility)
- **`stack.child_workflow`** references an external workflow file for reuse
- **Context flows** from parent to child and back via diff merging
- **`manager.max_cycles`** prevents runaway child workflows
- **`manager.stop_condition`** cancels the child when an external signal arrives

View file

@ -107,7 +107,7 @@ After import under the placeholder `validate`, `validate.lint`'s retry target be
## Templating
Imported files are rendered through the template engine **before** being parsed, using the parent run's inputs. This means `{{ inputs.* }}` can parameterize the structure of an imported graph:
Imported files are parsed as static DOT before template expansion. The import path and graph structure must be literal, but templates remain supported inside final string attributes such as `prompt`, `script`, and `label`:
```dot title="shared/lint.fabro"
digraph Lint {
@ -119,7 +119,7 @@ digraph Lint {
}
```
`{{ goal }}` is preserved through the pre-parse step and rendered later, the same as in inline graphs. See [Variables](/workflows/variables#expansion-timing) for the full template-rendering pipeline.
Do not put templates in `import` paths, node IDs, edge definitions, or other structural references. See [Variables](/workflows/variables#expansion-timing) for the rendering pipeline.
## Nested imports

View file

@ -35,7 +35,7 @@ repo_url = "https://github.com/fabro-sh/fabro"
language = "rust"
```
These values are available throughout the workflow as `{{ inputs.* }}`:
These values are available in final string attributes such as `goal`, `prompt`, `script`, `label`, and edge labels:
```dot title="check.fabro"
digraph Check {
@ -75,17 +75,19 @@ That prompt becomes `Create a plan for: Implement the login feature`.
## Expansion timing
Fabro expands templates in multiple passes:
Fabro keeps workflow structure static and renders workflow templates once:
1. Before DOT parsing, `{{ inputs.* }}` can parameterize structural parts of the graph, including imported `.fabro` files.
2. After parsing, all string graph, node, and edge attributes are rendered again with the real `{ goal, inputs }` context.
3. Agent and prompt handlers do a final runtime render pass as a safety net.
1. Fabro parses the root DOT and imported `.fabro` files without rendering them.
2. Literal `import`, `@file`, graph-goal file, and child-workflow references are resolved.
3. Final graph, node, and edge string attributes are rendered with the `{ goal, inputs }` context.
`{{ goal }}` is preserved through the pre-parse step so it can be resolved later. That means goal-dependent MiniJinja control flow such as `{% if goal %}` is not useful in structural pre-parse templates.
Templates are not supported in graph syntax, node IDs, edge structure, `import` paths, `@file` paths, child workflow paths, or other file references.
Fabro renders the graph `goal` first and stores the rendered value back onto the graph. Other attributes that use `{{ goal }}` receive that rendered value.
## Undefined variables
Fabro uses strict undefined-variable handling. If a workflow template references an unknown value such as `{{ inputs.langauge }}`, validation fails instead of passing the literal text through to the model.
Fabro renders undefined workflow variables as empty text and records a `template_undefined_variable` diagnostic. `fabro validate` reports that diagnostic as a warning so you can validate workflow structure before all inputs are known. Run-style commands such as `fabro run`, `fabro create`, and preflight promote the same diagnostic to an error before proceeding.
## Escaping

View file

@ -0,0 +1,109 @@
# Sandbox Network Details Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add required provider-neutral public network policy data to `SandboxDetails` and show it clearly on the Sandbox tab.
**Architecture:** Extend the shared `fabro-types` sandbox-details model with a required `network` object that separates egress and ingress policy. Populate it from provider inspection where available, default to explicit `unknown` policies when Fabro cannot assert the policy, and reuse the shared types through OpenAPI, Rust API generation, and the generated TypeScript client.
**Tech Stack:** Rust, serde, OpenAPI/progenitor, TypeScript Axios client, React, Bun tests, cargo nextest.
---
## Summary
Add public-network policy reporting to `SandboxDetails`. The model reports only high-level public-network policy for egress and ingress; it does not include ports, previews, IP addresses, DNS, routes, Docker network IDs, or service discovery.
## Key Changes
- Add shared domain/API types in `fabro-types` and re-export them from `fabro-api`:
```rust
pub struct SandboxNetwork {
pub egress: SandboxNetworkPolicy,
pub ingress: SandboxNetworkPolicy,
}
pub struct SandboxNetworkPolicy {
pub mode: SandboxNetworkPolicyMode,
pub cidrs: Vec<String>,
}
#[serde(rename_all = "snake_case")]
pub enum SandboxNetworkPolicyMode {
Unknown,
Open,
Blocked,
CidrAllowList,
EssentialsOnly,
}
```
- Add required `network: SandboxNetwork` to `SandboxDetails`.
- Serialize `network` always.
- Give `network` a serde default of egress/ingress `unknown` so older persisted/API JSON can still deserialize.
- Update OpenAPI with required `network` and schemas for the new types.
- Add `fabro-api` replacement mappings for the new shared types.
- Regenerate the TypeScript API client after the OpenAPI/Rust type changes.
## Provider Mapping
- Daytona:
- `network_block_all == true` maps egress to `blocked`.
- Non-empty `network_allow_list` maps egress to `cidr_allow_list`, with comma-separated CIDRs trimmed.
- `network_block_all == false` and no allow list maps egress to `unknown`, because current SDK fields do not distinguish full default access from Daytona tier-managed essentials-only behavior.
- Ingress maps to `unknown`.
- Docker:
- If `inspect.host_config.network_mode == "none"`, map egress and ingress to `blocked`.
- Other Docker modes map to `unknown`.
- Local:
- Egress and ingress map to `unknown`.
## Web UI
- Add a compact `Network` panel to the Sandbox tab after `Resources`.
- Show two primary rows: `Egress` and `Ingress`.
- Use concise summaries:
- `Unknown`
- `Open`
- `Blocked`
- `Essentials only`
- `CIDR allow list`
- When a direction has `mode == "cidr_allow_list"`, add one extra row for that CIDR list, comma-separated and styled consistently with existing detail rows.
## Implementation Tasks
- [x] Add `SandboxNetwork`, `SandboxNetworkPolicy`, and `SandboxNetworkPolicyMode` to `lib/crates/fabro-types/src/sandbox_details.rs`.
- [x] Add constructors/default helpers for `unknown`, `open`, `blocked`, `allow_cidrs`, and `essentials_only` policies to keep provider mapping code readable.
- [x] Add `network: SandboxNetwork` to every `SandboxDetails` construction site.
- [x] Update local sandbox details to return `SandboxNetwork::unknown()`.
- [x] Update Docker sandbox details to inspect `host_config.network_mode` and emit blocked policy only for `"none"`, otherwise unknown.
- [x] Update Daytona sandbox details to derive egress policy from `network_block_all` and `network_allow_list`, and ingress as unknown.
- [x] Update OpenAPI schemas and `fabro-api/build.rs` replacements for the new shared types.
- [x] Run `cargo build -p fabro-api` so Rust API generation validates the schema.
- [x] Regenerate the TypeScript client with `cd lib/packages/fabro-api-client && bun run generate`.
- [x] Add the Sandbox tab `Network` panel and formatting helpers in `apps/fabro-web/app/routes/run-sandbox.tsx`.
- [x] Update tests and snapshots affected by the now-required `network` field.
## Test Plan
- Update `fabro-types` serde tests to prove `network` serializes, defaults to unknown when absent, and supports `open`, `blocked`, `cidr_allow_list`, and `essentials_only`.
- Update `fabro-api` round-trip/type-identity tests for the new shared types and OpenAPI JSON shape.
- Add `fabro-sandbox` mapper tests for Daytona block-all, Daytona CIDR allow list, Daytona ambiguous default, local unknown, Docker `network_mode = none`, and Docker non-`none` unknown.
- Update server sandbox-details tests for the required `network` field.
- Update `apps/fabro-web` Sandbox tab tests to expect the `Network` panel and verify unknown, blocked, essentials-only, and CIDR allow-list display.
- Run:
```sh
cargo nextest run -p fabro-types -p fabro-api -p fabro-sandbox -p fabro-server sandbox_details
cd apps/fabro-web && bun test app/routes/run-sandbox.test.tsx
cd apps/fabro-web && bun run typecheck
```
## Assumptions
- This is policy/reporting data, not live connectivity probing.
- No IPs, listening services, port mappings, preview URLs, DNS, routes, or Docker network IDs are added to `SandboxDetails`.
- The `essentials_only` mode is included in the API now, but v1 only emits it when Fabro can assert it from provider data.
- Current Daytona SDK fields do not distinguish full default access from org-tier essentials-only access.
- Daytona behavior follows the current docs: `networkBlockAll` takes precedence, `networkAllowList` is IPv4 CIDR-only, and essential services are Daytona-managed.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,647 @@
# Source-Aware Template Diagnostics Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make `fabro run` and `fabro validate` template errors point to the real source file, line/column, and node context instead of MiniJinja's generic `<string>`.
**Architecture:** Preserve source provenance through template rendering instead of flattening errors to strings. `fabro-template` owns named MiniJinja rendering and typed span metadata; `fabro-workflow` owns workflow/node/attribute context; CLI and API layers render or serialize the structured diagnostics at their boundaries.
**Tech Stack:** Rust, MiniJinja, miette, thiserror, serde, OpenAPI/progenitor, cargo-nextest, insta snapshots.
---
## File Structure
- Modify `lib/crates/fabro-template/src/lib.rs` to add named render APIs and miette-aware `TemplateError` metadata.
- Modify `lib/crates/fabro-workflow/src/operations/create.rs`, `lib/crates/fabro-workflow/src/transforms/variable_expansion.rs`, `lib/crates/fabro-workflow/src/transforms/file_inlining.rs`, and `lib/crates/fabro-workflow/src/transforms/import.rs` to render templates with source and owner context.
- Modify `lib/crates/fabro-workflow/src/error.rs` to preserve source-aware template errors instead of converting them to `String`.
- Modify `lib/crates/fabro-validate/src/lib.rs`, `docs/public/api-reference/fabro-api.yaml`, and `lib/crates/fabro-server/src/run_manifest.rs` to expose optional source coordinates in validation diagnostics.
- Modify `lib/crates/fabro-cli/src/main.rs`, `lib/crates/fabro-cli/src/shared/utilities.rs`, and `lib/crates/fabro-cli/src/commands/run/output.rs` to render structured source metadata.
- Update or add tests in `lib/crates/fabro-template/src/lib.rs`, `lib/crates/fabro-workflow/src/operations/create.rs`, `lib/crates/fabro-cli/tests/it/cmd/validate.rs`, `lib/crates/fabro-cli/tests/it/cmd/run.rs`, and `lib/crates/fabro-server/src/server/tests.rs`.
## Task 1: Add Named Template Rendering
**Files:**
- Modify: `lib/crates/fabro-template/src/lib.rs`
- [x] **Step 1: Add failing unit tests for named template diagnostics**
Add tests that call the new API names below:
```rust
#[test]
fn render_named_reports_source_name_expression_and_span() {
let ctx = TemplateContext::new();
let err = render_named("prompts/test.md", "Hello {{ inputs.foo }}", &ctx).unwrap_err();
let TemplateError::UndefinedVariable {
expression,
line,
source_name,
span,
..
} = err
else {
panic!("expected undefined variable error");
};
assert_eq!(expression.as_deref(), Some("inputs.foo"));
assert_eq!(line, Some(1));
assert_eq!(source_name.as_deref(), Some("prompts/test.md"));
assert!(span.is_some());
}
#[test]
fn render_lenient_named_preserves_source_name_for_syntax_errors() {
let ctx = TemplateContext::new();
let err = render_lenient_named("workflow.fabro", "{{ unterminated", &ctx).unwrap_err();
let TemplateError::Syntax {
source_name, ..
} = err
else {
panic!("expected syntax error");
};
assert_eq!(source_name.as_deref(), Some("workflow.fabro"));
}
```
- [x] **Step 2: Run the tests and verify they fail**
Run:
```bash
cargo nextest run -p fabro-template render_named_reports_source_name_expression_and_span render_lenient_named_preserves_source_name_for_syntax_errors
```
Expected: tests fail because `render_named` and `render_lenient_named` do not exist.
- [x] **Step 3: Implement named rendering APIs**
Add these public functions and route existing `render` functions through an internal helper:
```rust
pub fn render(template: &str, ctx: &TemplateContext) -> Result<String, TemplateError> {
render_with(None, template, ctx, UndefinedBehavior::Strict)
}
pub fn render_named(
name: impl Into<String>,
template: &str,
ctx: &TemplateContext,
) -> Result<String, TemplateError> {
render_with(Some(name.into()), template, ctx, UndefinedBehavior::Strict)
}
pub fn render_lenient(template: &str, ctx: &TemplateContext) -> Result<String, TemplateError> {
render_with(None, template, ctx, UndefinedBehavior::Chainable)
}
pub fn render_lenient_named(
name: impl Into<String>,
template: &str,
ctx: &TemplateContext,
) -> Result<String, TemplateError> {
render_with(Some(name.into()), template, ctx, UndefinedBehavior::Chainable)
}
```
In `render_with`, use `env.render_named_str(&name, template, ctx.clone().into_value())` when a name is present, and `env.render_str(...)` otherwise. Keep the current plain-text fast path.
- [x] **Step 4: Store source metadata on `TemplateError`**
Extend each `TemplateError` variant with:
```rust
source_name: Option<String>,
source_text: Option<String>,
span: Option<miette::SourceSpan>,
```
In `From<minijinja::Error> for TemplateError`, populate:
```rust
let source_name = error.name().map(str::to_owned);
let source_text = error.template_source().map(str::to_owned);
let span = error
.range()
.and_then(|range| {
let start = range.start;
let len = range.end.checked_sub(range.start)?;
Some((start, len).into())
});
```
Preserve the existing `expression` and `line` behavior.
- [x] **Step 5: Derive or implement `miette::Diagnostic` for `TemplateError`**
Use `#[derive(miette::Diagnostic)]` if it stays readable. If derive becomes awkward because of enum variants, implement `miette::Diagnostic` manually so `source_code()` returns a `NamedSource<String>` when both `source_name` and `source_text` exist, and `labels()` returns a label for `span`.
- [x] **Step 6: Verify template tests**
Run:
```bash
cargo nextest run -p fabro-template
```
Expected: all `fabro-template` tests pass.
## Task 2: Preserve Template Errors in Workflow Errors
**Files:**
- Modify: `lib/crates/fabro-workflow/src/error.rs`
- Modify: `lib/crates/fabro-workflow/Cargo.toml`
- [x] **Step 1: Add a failing error-chain regression test**
Add a test showing that a workflow template error remains visible as a source cause:
```rust
#[test]
fn template_error_variant_preserves_source_chain() {
let template_err = fabro_template::render_named(
"workflow.fabro",
"{{ inputs.missing }}",
&fabro_template::TemplateContext::new(),
)
.unwrap_err();
let err = Error::template("template expansion failed", template_err);
let chain = collect_chain(&err);
assert!(chain.iter().any(|part| part.contains("template expansion failed")));
assert!(chain.iter().any(|part| part.contains("undefined template variable")));
}
```
- [x] **Step 2: Run the test and verify it fails**
Run:
```bash
cargo nextest run -p fabro-workflow template_error_variant_preserves_source_chain
```
Expected: test fails because `Error::template` does not exist.
- [x] **Step 3: Add a structured workflow error variant**
Add a new `Error` variant:
```rust
#[error("{message}")]
Template {
message: String,
#[source]
source: fabro_template::TemplateError,
}
```
Add:
```rust
pub fn template(message: impl Into<String>, source: fabro_template::TemplateError) -> Self {
Self::Template {
message: message.into(),
source,
}
}
```
Update failure classification to treat `Template` as deterministic validation/parse failure.
- [x] **Step 4: Delegate miette metadata from workflow template errors**
Implement `miette::Diagnostic` for `Error` or add a wrapper diagnostic used by the CLI so `Error::Template { source, .. }` delegates `source_code`, `labels`, `code`, and `help` to the underlying `TemplateError`.
- [x] **Step 5: Replace string conversion helper**
Remove the current `template_parse_error(&TemplateError) -> Error` stringification path in `operations/create.rs` and convert call sites to:
```rust
.map_err(|err| Error::template("template expansion failed", err))?
```
- [x] **Step 6: Verify workflow error tests**
Run:
```bash
cargo nextest run -p fabro-workflow template_error_variant_preserves_source_chain
```
Expected: test passes.
## Task 3: Render DOT Attributes Source-Aware After Parse
**Files:**
- Modify: `lib/crates/fabro-workflow/src/operations/create.rs`
- Modify: `lib/crates/fabro-workflow/src/transforms/variable_expansion.rs`
- Modify: `lib/crates/fabro-workflow/src/pipeline/transform.rs`
- [x] **Step 1: Add a failing workflow test for inline prompt source context**
Add a test in `operations/create.rs`:
```rust
#[test]
fn strict_template_error_for_inline_prompt_names_workflow_file_and_node() {
let dot = r#"digraph ValidatePlan {
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
test_inline_prompt [label="moo" prompt="{{ inputs.foo }}"]
start -> test_inline_prompt -> exit
}"#;
let err = preprocess_and_validate(
dot,
Some(PathBuf::from(".")),
None,
Vec::new(),
None,
None,
RenderMode::Strict,
&test_catalog(),
)
.unwrap_err();
let rendered = collect_chain(&err).join(": ");
assert!(rendered.contains("test_inline_prompt"));
assert!(rendered.contains("prompt"));
assert!(!rendered.contains("<string>"));
}
```
- [x] **Step 2: Run the test and verify it fails**
Run:
```bash
cargo nextest run -p fabro-workflow strict_template_error_for_inline_prompt_names_workflow_file_and_node
```
Expected: test fails because the pre-parse render path still reports a generic template source.
- [x] **Step 3: Remove strict pre-parse DOT rendering**
In `preprocess_and_validate`, delete the initial strict whole-file `render_template(dot_source, ...)` phase. Always parse `dot_source` first, then apply transforms. For `RenderMode::Structural`, keep undefined input warnings by letting the template transform collect diagnostics instead of rendering the whole DOT file.
- [x] **Step 4: Add source options to the template transform**
Change `TemplateTransform` to carry:
```rust
pub struct TemplateTransform {
pub inputs: HashMap<String, toml::Value>,
pub source_name: Option<String>,
pub render_mode: RenderMode,
}
```
Render graph attrs, node attrs, and edge attrs one value at a time. Use names like:
```rust
workflow.fabro graph attribute `goal`
workflow.fabro node `test_inline_prompt` attribute `prompt`
workflow.fabro edge `start -> test_inline_prompt` attribute `label`
```
When source path is unknown, use `workflow`.
- [x] **Step 5: Attach owner context to errors and diagnostics**
For strict mode, wrap template errors with context:
```text
template expansion failed in node `test_inline_prompt` attribute `prompt`
```
For structural mode undefined variables, produce a warning diagnostic with `node_id` set to the owning node id and `message` including the source path and line when available.
- [x] **Step 6: Verify workflow transform tests**
Run:
```bash
cargo nextest run -p fabro-workflow strict_template_error_for_inline_prompt_names_workflow_file_and_node
cargo nextest run -p fabro-workflow template_transform
```
Expected: source-aware test and existing template transform tests pass.
## Task 4: Make Prompt File Rendering First-Class
**Files:**
- Modify: `lib/crates/fabro-workflow/src/transforms/file_inlining.rs`
- Modify: `lib/crates/fabro-workflow/src/transforms/variable_expansion.rs`
- [x] **Step 1: Add a failing test for imported prompt source attribution**
Add a test that builds a graph with `prompt="@test.md"` and a resolver returning `{{ inputs.foo }}` for `test.md`. Assert strict mode errors mention `test.md`, `test_imported_prompt`, and `prompt`.
- [x] **Step 2: Run the test and verify it fails**
Run:
```bash
cargo nextest run -p fabro-workflow imported_prompt_template_error_names_prompt_file_and_node
```
Expected: test fails because file inlining currently loses the prompt file source.
- [x] **Step 3: Render prompt attribute paths before resolving file references**
For node `prompt` and graph `goal`, first render the attribute value with the workflow source name so templated paths like `@{{ inputs.prompt_file }}` continue to work.
- [x] **Step 4: Resolve `@file` references and render file contents with resolved path**
When a prompt or goal value resolves to a file, render the file contents using:
```rust
render_named(resolved.path.display().to_string(), &resolved.content, &ctx)
```
Wrap failures with node/attr context from the referencing graph value.
- [x] **Step 5: Keep non-file string attributes on workflow source**
Only prompt and goal `@file` references switch to the resolved file source. Other string attributes render against their owning workflow/import file source.
- [x] **Step 6: Verify prompt source tests**
Run:
```bash
cargo nextest run -p fabro-workflow imported_prompt_template_error_names_prompt_file_and_node
```
Expected: test passes.
## Task 5: Make Import Rendering Source-Aware
**Files:**
- Modify: `lib/crates/fabro-workflow/src/transforms/import.rs`
- [x] **Step 1: Add failing tests for import source attribution**
Add tests for:
```rust
#[test]
fn templated_import_path_error_names_importing_node() { /* import="{{ inputs.path }}" */ }
#[test]
fn imported_workflow_template_error_names_imported_file() { /* imported file contains "{{ inputs.foo }}" */ }
```
- [x] **Step 2: Run the tests and verify they fail**
Run:
```bash
cargo nextest run -p fabro-workflow templated_import_path_error_names_importing_node imported_workflow_template_error_names_imported_file
```
Expected: tests fail with stringified or generic template errors.
- [x] **Step 3: Render import attributes with node context**
Before resolving an import path, render the placeholder node's `import` attr with a named template source and wrap failures with the placeholder node id.
- [x] **Step 4: Parse imported workflows from their own source name**
When rendering imported workflow content for parse preparation, call `render_named(resolved_file.path.display().to_string(), ...)`. Preserve errors as `Error::Template`.
- [x] **Step 5: Verify import tests**
Run:
```bash
cargo nextest run -p fabro-workflow templated_import_path_error_names_importing_node imported_workflow_template_error_names_imported_file
```
Expected: tests pass.
## Task 6: Serialize Source Coordinates in Validation Diagnostics
**Files:**
- Modify: `lib/crates/fabro-validate/src/lib.rs`
- Modify: `docs/public/api-reference/fabro-api.yaml`
- Modify: `lib/crates/fabro-server/src/run_manifest.rs`
- Modify: `lib/crates/fabro-cli/src/commands/run/output.rs`
- [x] **Step 1: Add optional fields to `fabro_validate::Diagnostic`**
Add:
```rust
pub source_path: Option<String>,
pub line: Option<u32>,
pub column: Option<u32>,
pub span_start: Option<usize>,
pub span_len: Option<usize>,
pub related: Vec<RelatedDiagnostic>,
```
Add:
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelatedDiagnostic {
pub message: String,
pub source_path: Option<String>,
pub line: Option<u32>,
pub column: Option<u32>,
}
```
Update all existing diagnostic constructors with `..Diagnostic::default()` by deriving or implementing `Default` for `Diagnostic`.
- [x] **Step 2: Update OpenAPI schema**
Add the same optional fields to `WorkflowDiagnostic` in `docs/public/api-reference/fabro-api.yaml`, and add a `RelatedWorkflowDiagnostic` schema.
- [x] **Step 3: Regenerate Rust API types**
Run:
```bash
cargo build -p fabro-api
```
Expected: build succeeds and generated types include the new optional fields.
- [x] **Step 4: Map diagnostics to and from API DTOs**
Update `diagnostics_to_api` and CLI `api_diagnostic_to_local` to preserve all new optional fields and related diagnostics.
- [x] **Step 5: Verify API build**
Run:
```bash
cargo build -p fabro-api
```
Expected: build succeeds.
## Task 7: Render CLI Output with Source Context
**Files:**
- Modify: `lib/crates/fabro-cli/src/main.rs`
- Modify: `lib/crates/fabro-cli/src/shared/utilities.rs`
- Modify: `lib/crates/fabro-cli/tests/it/cmd/validate.rs`
- Modify: `lib/crates/fabro-cli/tests/it/cmd/run.rs`
- [x] **Step 1: Add CLI snapshot coverage for issue #287**
Add fixtures or temp workflows for:
```dot
digraph ValidatePlan {
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
test_inline_prompt [label="moo" prompt="{{ inputs.foo }}"]
start -> test_inline_prompt -> exit
}
```
and:
```dot
digraph ValidatePlan {
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
test_imported_prompt [label="moo" prompt="@test.md"]
start -> test_imported_prompt -> exit
}
```
with `test.md` containing:
```markdown
{{ inputs.foo }}
```
- [x] **Step 2: Run snapshots and verify they fail**
Run:
```bash
cargo nextest run -p fabro-cli validate_template_source_attribution run_rejects_unbound_template_inputs_before_creating_remote_run
```
Expected: snapshots fail because output still contains `<string>` or lacks source context.
- [x] **Step 3: Delegate fatal miette diagnostics through CLI wrapper**
Update `CliDiagnostic` so `impl miette::Diagnostic for CliDiagnostic` delegates `source_code`, `labels`, `code`, and `help` to the wrapped error when the wrapped error chain contains a `miette::Diagnostic`.
- [x] **Step 4: Print validation diagnostics with source coordinates**
Update `print_diagnostics` so diagnostics with `source_path`, `line`, and `column` print a compact prefix:
```text
warning: test.md:1:1: undefined template variable `inputs.foo` (template_undefined_variable)
```
Keep the existing message shape for diagnostics without source coordinates.
- [x] **Step 5: Verify CLI snapshots**
Run:
```bash
cargo nextest run -p fabro-cli validate_template_source_attribution run_rejects_unbound_template_inputs_before_creating_remote_run
cargo insta pending-snapshots
```
Expected: only the intended snapshots are pending. Inspect them, then accept only the intended updates:
```bash
cargo insta accept
```
## Task 8: Add API Validation Coverage
**Files:**
- Modify: `lib/crates/fabro-server/src/server/tests.rs`
- [x] **Step 1: Add a validation endpoint test for source fields**
Add a test that posts a run manifest containing an imported prompt with an undefined template variable and asserts the first diagnostic includes:
```rust
assert_eq!(diagnostic["source_path"], "test.md");
assert_eq!(diagnostic["line"], 1);
assert!(diagnostic["node_id"].as_str().unwrap().contains("test_imported_prompt"));
```
- [x] **Step 2: Run the server test**
Run:
```bash
cargo nextest run -p fabro-server validate_endpoint_returns_template_source_coordinates
```
Expected: test passes.
## Task 9: Final Verification
**Files:**
- No additional edits.
- [x] **Step 1: Run focused test suite**
Run:
```bash
cargo nextest run -p fabro-template
cargo nextest run -p fabro-workflow
cargo nextest run -p fabro-cli validate_template_source_attribution run_rejects_unbound_template_inputs_before_creating_remote_run
cargo nextest run -p fabro-server validate_endpoint_returns_template_source_coordinates
```
Expected: all tests pass.
- [x] **Step 2: Run API build**
Run:
```bash
cargo build -p fabro-api
```
Expected: build succeeds.
- [x] **Step 3: Run formatting check**
Run:
```bash
cargo +nightly-2026-04-14 fmt --check --all
```
Expected: formatting check passes.
- [x] **Step 4: Run clippy if the touched crates compile cleanly**
Run:
```bash
cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings
```
Expected: clippy passes.
## Assumptions
- This is a greenfield app, so internal workflow/template APIs and OpenAPI diagnostic DTOs may change.
- Template support after this change is scoped to parsed string attributes and referenced workflow/prompt files, not arbitrary structural DOT templating.
- API responses expose coordinates and context, not full source text.
- Rich source snippets are rendered in local CLI output through `miette`; server/API clients receive structured metadata.
- Existing diagnostics without source metadata keep their current behavior.

View file

@ -0,0 +1,5 @@
node_modules
dist
.vite
*.log
.DS_Store

View file

@ -0,0 +1,68 @@
# chats-new + Ask-Fabro prototype
Isolated, client-side prototype validating the design for the upcoming
`/chats/new` page and the right-sidebar "Ask Fabro" assistant in fabro-web.
**Status:** reference implementation. Not deployed; not part of the build.
Lives here so the visual and integration learnings survive until they land in
`apps/fabro-web`.
**Companion spec:** `../../specs/2026-05-16-chats-new-prototype-design.md`
## Run it
```bash
cd docs/superpowers/prototypes/2026-05-16-chats-new
bun install
bun run dev
```
Then open <http://127.0.0.1:5173/>.
## Routes
- `/chats/new` — full-page ChatGPT-style empty composer.
- `/chats/:id` — full-page chat thread with sidebar of past chats.
Seeded with three example threads (`seed_email`, `seed_hook`, `seed_db`).
- `/sample` — demo workspace page with an "Ask Fabro" button in the header that
toggles a right-side assistant sidebar.
## What it proves
- assistant-ui's `<Thread>` + `useLocalRuntime` integrate cleanly with React 19,
React Router 7, Tailwind v4, and the Fabro color palette.
- A custom `ChatModelAdapter` can stream scripted replies (text + tool calls)
through assistant-ui's runtime.
- The `@layer assistant-ui` import wrap is required for Tailwind v4 utilities
to win over assistant-ui's unlayered scoped preflight. See spec.
- A right-sidebar assistant pattern works by rendering `<Thread>` inside a
fixed-height side panel with `--aui-thread-max-width: 100%` and a stripped
composer (no chips, single-line, rounded pill).
- Sidebar customizations needed for a narrow column: hide avatar, hide
action bar, override `align-items: stretch` on the viewport footer,
`margin-top: auto` so the composer pins to the bottom in the empty state,
`padding-right` on assistant message content to keep code blocks inside
the column without `<pre>` 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`.

View file

@ -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=="],
}
}

View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fabro chats prototype</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -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
* <Outlet />, exactly the way /chats/new would slot into the real AppShell.
*/
export default function App() {
return (
<AskFabroProvider>
<AppLayout />
</AskFabroProvider>
);
}
function AppLayout() {
return (
<div className="bg-atmosphere flex h-full text-fg">
<div className="flex min-w-0 flex-1 flex-col">
<header className="relative z-10 border-b border-line bg-panel/60 backdrop-blur-sm">
<div className="flex h-14 items-center gap-6 px-6">
<Link to="/chats/new" className="flex items-center gap-2">
<span className="inline-block size-2.5 rounded-full bg-teal-500" />
<span className="text-sm font-semibold tracking-wide text-fg">
fabro
</span>
<span className="text-xs font-medium text-fg-muted">
prototype
</span>
</Link>
<nav className="flex items-center gap-1">
{NAV.map((item) => (
<NavLink
key={item.name}
to={item.to}
className={({ isActive }) =>
[
"inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm font-medium transition-colors",
isActive
? "bg-overlay text-fg"
: "text-fg-muted hover:bg-overlay hover:text-fg",
].join(" ")
}
>
<item.icon className="size-4" />
{item.name}
</NavLink>
))}
</nav>
<div className="ml-auto flex items-center gap-3 text-sm text-fg-muted">
<span className="hidden sm:inline">bryan@brynary.com</span>
<span className="inline-flex size-7 items-center justify-center rounded-full bg-overlay text-xs font-medium text-fg ring-1 ring-line-strong">
BH
</span>
</div>
</div>
</header>
<main className="min-h-0 flex-1">
<Outlet />
</main>
</div>
<AskFabroSidebar />
</div>
);
}

View file

@ -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 (
<aside
aria-label="Ask Fabro"
aria-hidden={!isOpen}
style={{ width: isOpen ? SIDEBAR_WIDTH : 0 }}
className="h-full shrink-0 overflow-hidden transition-[width] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]"
>
<div
className="fabro-chat ask-fabro-sidebar relative isolate flex h-full flex-col border-l border-line bg-panel/40 backdrop-blur-sm"
style={{ width: SIDEBAR_WIDTH }}
>
<header className="flex h-12 shrink-0 items-center justify-end px-2">
<button
type="button"
onClick={close}
aria-label="Close assistant"
className="inline-flex size-8 items-center justify-center rounded-md text-fg-3 transition-colors hover:bg-overlay hover:text-fg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500"
>
<XMarkIcon className="size-4" />
</button>
</header>
<div className="min-h-0 flex-1">
<AssistantRuntimeProvider runtime={runtime}>
<Thread
components={{ Composer: SidebarComposer, ThreadWelcome: () => null }}
assistantMessage={{
components: { Text: MarkdownText, ToolFallback },
allowCopy: false,
allowReload: false,
allowSpeak: false,
allowFeedbackPositive: false,
allowFeedbackNegative: false,
}}
/>
</AssistantRuntimeProvider>
</div>
</div>
</aside>
);
}

View file

@ -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 (
<svg viewBox="0 0 16 16" fill="currentColor" className={className}>
<path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.5 2.5 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z" />
</svg>
);
}
type IconComponent = React.ComponentType<{ className?: string }>;
function Chip({
options,
value,
onChange,
Icon,
}: {
options: Choice[];
value: Choice;
onChange: (c: Choice) => void;
Icon: IconComponent;
}) {
return (
<Listbox value={value} onChange={onChange}>
<div className="relative">
<ListboxButton className="inline-flex items-center gap-1.5 rounded-full bg-overlay px-2.5 py-1 text-xs font-medium text-fg-3 transition-colors hover:bg-overlay-strong hover:text-fg focus:outline-2 focus:outline-offset-1 focus:outline-teal-500">
<Icon className="size-3.5 text-fg-muted" />
<span>{value.label}</span>
<ChevronUpDownIcon className="size-3.5 text-fg-muted" />
</ListboxButton>
<ListboxOptions
anchor={{ to: "bottom start", gap: 6 }}
className="z-50 w-52 rounded-lg bg-panel p-1 shadow-2xl shadow-black/60 ring-1 ring-line-strong focus:outline-none"
>
{options.map((opt) => (
<ListboxOption
key={opt.id}
value={opt}
className="group flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-sm text-fg-2 data-focus:bg-overlay data-focus:text-fg data-selected:text-fg"
>
<CheckIcon className="invisible size-4 text-teal-300 group-data-selected:visible" />
<span>{opt.label}</span>
</ListboxOption>
))}
</ListboxOptions>
</div>
</Listbox>
);
}
export default function ComposerChips() {
const [project, setProject] = useState(PROJECTS[0]!);
const [branch, setBranch] = useState(BRANCHES[0]!);
const [model, setModel] = useState(MODELS[0]!);
return (
<div className="flex flex-wrap items-center gap-1.5">
<Chip options={PROJECTS} value={project} onChange={setProject} Icon={FolderIcon} />
<Chip options={BRANCHES} value={branch} onChange={setBranch} Icon={BranchIcon} />
<Chip options={MODELS} value={model} onChange={setModel} Icon={CpuChipIcon} />
</div>
);
}

View file

@ -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 (
<ComposerPrimitive.Root className="mx-auto mb-4 flex w-full max-w-2xl flex-col overflow-hidden rounded-2xl bg-panel-alt/80 shadow-lg shadow-black/30 ring-1 ring-line-strong backdrop-blur-sm focus-within:ring-teal-500/40">
<ComposerPrimitive.Input
rows={1}
autoFocus
placeholder="Ask anything…"
className="block max-h-48 w-full resize-none bg-transparent px-4 pt-4 pb-2 text-sm text-fg placeholder:text-fg-muted focus:outline-none"
/>
<div className="flex items-center justify-between gap-3 px-4 pt-2 pb-3">
<ComposerChips />
<ThreadPrimitive.If running>
<ComposerPrimitive.Cancel asChild>
<button
type="button"
aria-label="Stop"
className="inline-flex size-9 items-center justify-center rounded-full bg-overlay-strong text-fg transition-colors hover:bg-overlay"
>
<StopIcon className="size-4" />
</button>
</ComposerPrimitive.Cancel>
</ThreadPrimitive.If>
<ThreadPrimitive.If running={false}>
<ComposerPrimitive.Send asChild>
<button
type="submit"
aria-label="Send message"
className="inline-flex size-9 items-center justify-center rounded-full bg-teal-500 text-on-primary transition-colors hover:bg-teal-300 disabled:cursor-not-allowed disabled:opacity-50"
>
<ArrowUpIcon className="size-4" />
</button>
</ComposerPrimitive.Send>
</ThreadPrimitive.If>
</div>
</ComposerPrimitive.Root>
);
}

View file

@ -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 (
<ComposerPrimitive.Root className="m-3 flex w-auto items-end gap-2 rounded-2xl bg-panel-alt/80 px-3 py-2 shadow-md shadow-black/30 ring-1 ring-line-strong backdrop-blur-sm focus-within:ring-teal-500/40">
<ComposerPrimitive.Input
rows={1}
autoFocus
placeholder="Ask Fabro…"
className="block max-h-40 min-w-0 flex-1 resize-none bg-transparent py-1 text-sm text-fg placeholder:text-fg-muted focus:outline-none"
/>
<ThreadPrimitive.If running>
<ComposerPrimitive.Cancel asChild>
<button
type="button"
aria-label="Stop"
className="inline-flex size-7 shrink-0 items-center justify-center rounded-full bg-overlay-strong text-fg transition-colors hover:bg-overlay"
>
<StopIcon className="size-3.5" />
</button>
</ComposerPrimitive.Cancel>
</ThreadPrimitive.If>
<ThreadPrimitive.If running={false}>
<ComposerPrimitive.Send asChild>
<button
type="submit"
aria-label="Send message"
className="inline-flex size-7 shrink-0 items-center justify-center rounded-full bg-teal-500 text-on-primary transition-colors hover:bg-teal-300 disabled:cursor-not-allowed disabled:opacity-50"
>
<ArrowUpIcon className="size-3.5" />
</button>
</ComposerPrimitive.Send>
</ThreadPrimitive.If>
</ComposerPrimitive.Root>
);
}

View file

@ -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 (
<div className="my-2 rounded-lg border border-line bg-overlay/70 text-fg-2">
<div className="flex items-center gap-2 border-b border-line px-3 py-2 text-xs font-medium uppercase tracking-wide text-fg-muted">
<WrenchScrewdriverIcon className="size-3.5" />
<span>tool</span>
<code className="rounded bg-overlay-strong px-1.5 py-0.5 font-mono text-[11px] normal-case tracking-normal text-fg">
{toolName}
</code>
</div>
<Section label="arguments">
<pre className="whitespace-pre-wrap break-words font-mono text-[12px] leading-relaxed text-fg-2">
{formatJson(args)}
</pre>
</Section>
{result !== undefined && (
<Section label="result">
<pre className="whitespace-pre-wrap break-words font-mono text-[12px] leading-relaxed text-fg-2">
{formatJson(result)}
</pre>
</Section>
)}
</div>
);
}
function Section({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="border-b border-line last:border-b-0">
<div className="px-3 pt-2 text-[10px] font-medium uppercase tracking-wider text-fg-muted">
{label}
</div>
<div className="px-3 pb-2">{children}</div>
</div>
);
}
function formatJson(value: unknown): string {
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}

View file

@ -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 <pre> 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 <code> 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;
}
}

View file

@ -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<AskFabroContextValue | null>(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 <AskFabroContext.Provider value={value}>{children}</AskFabroContext.Provider>;
}
export function useAskFabro(): AskFabroContextValue {
const value = useContext(AskFabroContext);
if (!value) throw new Error("useAskFabro must be used inside <AskFabroProvider>");
return value;
}

View file

@ -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<void> {
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<ThreadAssistantMessagePart, { type: "tool-call" }> =>
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<CompletionContentPart, { kind: "text" }> =>
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: [] };
});
}

View file

@ -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<T extends (...args: any[]) => void>(\n" +
" fn: T,\n" +
" ms: number,\n" +
"): (...args: Parameters<T>) => void {\n" +
" let handle: ReturnType<typeof setTimeout> | 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]!;
}

View file

@ -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<string, Chat>;
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<string, Chat> = {};
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<ChatsContextValue | null>(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<ChatsContextValue>(
() => ({ state, createChat, peekScriptIndex, advanceScriptIndex }),
[state, createChat, peekScriptIndex, advanceScriptIndex],
);
return <ChatsContext.Provider value={value}>{children}</ChatsContext.Provider>;
}
export function useChatsStore(): ChatsContextValue {
const value = useContext(ChatsContext);
if (!value) throw new Error("useChatsStore must be used inside <ChatsProvider>");
return value;
}

View file

@ -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[];
};

View file

@ -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: () => <Navigate to="/chats/new" replace /> },
{
path: "chats",
Component: ChatsLayout,
children: [
{ path: "new", Component: ChatsNew },
{ path: ":chatId", Component: ChatsDetail },
],
},
{ path: "sample", Component: Sample },
],
},
]);
createRoot(document.getElementById("root")!).render(
<RouterProvider router={router} />,
);

View file

@ -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 (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<p className="text-sm text-fg-muted">That chat doesn't exist.</p>
<button
type="button"
onClick={() => navigate("/chats/new")}
className="mt-3 text-sm font-medium text-teal-300 hover:text-teal-500"
>
Start a new chat
</button>
</div>
</div>
);
}
return <ChatRuntime key={chatId} chatId={chatId} />;
}
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 (
<AssistantRuntimeProvider runtime={runtime}>
<div className="h-full">
<Thread
components={{ Composer: CustomComposer }}
assistantMessage={{
components: { Text: MarkdownText, ToolFallback },
}}
/>
</div>
</AssistantRuntimeProvider>
);
}

View file

@ -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 (
<ChatsProvider>
<div className="fabro-chat relative isolate flex h-full">
<Sidebar />
<div className="min-h-0 flex-1">
<Outlet />
</div>
</div>
</ChatsProvider>
);
}
function Sidebar() {
const { state } = useChatsStore();
const navigate = useNavigate();
return (
<aside className="flex w-64 shrink-0 flex-col border-r border-line bg-panel/40">
<div className="p-3">
<button
type="button"
onClick={() => navigate("/chats/new")}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-overlay px-3 py-2 text-sm font-medium text-fg ring-1 ring-line-strong transition-all hover:bg-overlay-strong hover:ring-line-strong focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500"
>
<PencilSquareIcon className="size-4" />
New chat
</button>
</div>
<nav className="flex-1 overflow-y-auto px-2 pt-2 pb-3">
{state.order.map((id) => {
const chat = state.chats[id]!;
return (
<NavLink
key={id}
to={`/chats/${id}`}
className={({ isActive }) =>
[
"relative block truncate rounded-md px-3 py-2 text-sm transition-colors",
isActive
? "bg-overlay text-fg before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:rounded-full before:bg-teal-500"
: "text-fg-3 hover:bg-overlay/60 hover:text-fg",
].join(" ")
}
title={chat.title}
>
{chat.title || "New chat"}
</NavLink>
);
})}
</nav>
</aside>
);
}

View file

@ -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<HTMLTextAreaElement>(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 (
<div className="flex h-full flex-col items-center px-6 pt-[18vh] pb-10">
<div className="w-full max-w-2xl">
<form
onSubmit={onSubmit}
className="w-full overflow-hidden rounded-2xl bg-panel-alt/80 shadow-2xl shadow-black/40 ring-1 ring-line-strong backdrop-blur-sm transition-all focus-within:ring-teal-500/40"
>
<textarea
ref={textareaRef}
name="prompt"
aria-label="Message"
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
submit(text);
}
}}
placeholder="Ask anything…"
rows={2}
autoFocus
className="block max-h-72 w-full resize-none bg-transparent px-4 pt-4 pb-2 text-base text-fg placeholder:text-fg-muted focus:outline-none"
/>
<div className="flex items-center justify-between gap-3 px-4 pt-2 pb-3">
<ComposerChips />
<button
type="submit"
disabled={!text.trim()}
aria-label="Send message"
className="inline-flex size-9 items-center justify-center rounded-full bg-teal-500 text-on-primary transition-colors hover:bg-teal-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-teal-500"
>
<ArrowUpIcon className="size-4" />
</button>
</div>
</form>
</div>
</div>
);
}

View file

@ -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 (
<div className="flex h-full flex-col">
<header className="flex h-14 shrink-0 items-center gap-4 border-b border-line bg-panel/40 px-6 backdrop-blur-sm">
<div className="flex items-baseline gap-3">
<h1 className="text-base font-semibold text-fg">Runs</h1>
<span className="text-sm text-fg-3">fabro-web · main</span>
</div>
<button
type="button"
onClick={open}
disabled={isOpen}
className="ml-auto inline-flex items-center gap-1.5 rounded-md bg-overlay px-2.5 py-1.5 text-sm font-medium text-fg-2 ring-1 ring-line-strong transition-colors hover:bg-overlay-strong hover:text-fg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-500 disabled:cursor-not-allowed disabled:opacity-50"
>
<SparklesIcon className="size-4 text-teal-300" />
Ask Fabro
</button>
</header>
<main className="min-h-0 flex-1 overflow-y-auto px-6 py-6">
<div className="mx-auto max-w-4xl">
<div className="grid grid-cols-3 gap-4">
<StatCard
Icon={CheckCircleIcon}
label="Succeeded"
value="142"
accent="text-mint"
/>
<StatCard
Icon={ClockIcon}
label="Running"
value="6"
accent="text-amber"
/>
<StatCard
Icon={ChartBarIcon}
label="Avg duration"
value="2m 14s"
accent="text-teal-300"
/>
</div>
<h2 className="mt-10 mb-3 text-sm font-medium text-fg-3">
Recent runs
</h2>
<ul role="list" className="overflow-hidden rounded-lg bg-panel/40 ring-1 ring-line">
{RUN_ROWS.map((row, i) => (
<li
key={row.id}
className={[
"flex items-center gap-4 px-4 py-3 text-sm",
i !== RUN_ROWS.length - 1 && "border-b border-line",
].filter(Boolean).join(" ")}
>
<span className={`inline-block size-2 shrink-0 rounded-full ${row.dot}`} />
<span className="font-mono text-xs text-fg-3">{row.id}</span>
<span className="truncate text-fg-2">{row.title}</span>
<span className="ml-auto shrink-0 text-xs text-fg-muted tabular-nums">
{row.duration}
</span>
<ArrowTopRightOnSquareIcon className="size-4 shrink-0 text-fg-muted" />
</li>
))}
</ul>
</div>
</main>
</div>
);
}
function StatCard({
Icon,
label,
value,
accent,
}: {
Icon: React.ComponentType<{ className?: string }>;
label: string;
value: string;
accent: string;
}) {
return (
<div className="rounded-lg bg-panel/40 px-4 py-4 ring-1 ring-line">
<div className={`flex items-center gap-2 text-xs font-medium uppercase tracking-wider ${accent}`}>
<Icon className="size-3.5" />
{label}
</div>
<div className="mt-2 text-2xl font-semibold text-fg tabular-nums">
{value}
</div>
</div>
);
}
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" },
];

View file

@ -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"]
}

View file

@ -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",
},
});

View file

@ -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 `<Outlet />` 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 `<Thread>` 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 `<Thread>` 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 `<Thread>`. Without `Text: makeMarkdownText()` the renderer outputs plain
`<p>` 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 `<h1 className="text-5xl font-semibold">`
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
`<Thread>` from `@assistant-ui/react-ui` already wraps itself in
`<div class="aui-root aui-thread-root">`. 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<string, Chat>;
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.

View file

@ -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<i32>,
pub exec_output_tail: Option<ExecOutputTail>,
}
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<ExecOutputTail>,
},
#[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<ExecOutputTail> {
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<agent_client_protocol::Error> for AcpError {
fn from(error: agent_client_protocol::Error) -> Self {
Self::Protocol(error)

View file

@ -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};

View file

@ -102,7 +102,7 @@ pub async fn run_acp_turn(request: AcpRunRequest) -> Result<AcpRunResult, AcpErr
return Err(AcpError::Cancelled);
}
Err(AcpError::TimedOut {
stderr: state.stderr_tail().await,
exec_output_tail: state.exec_output_tail().await,
})
}
}
@ -130,6 +130,9 @@ pub async fn run_acp_turn(request: AcpRunRequest) -> Result<AcpRunResult, AcpErr
if let Some(startup_error) = state.take_startup_error().await {
return Err(AcpError::Sandbox(startup_error));
}
if let Some(process_exit) = state.take_process_exit().await {
return Err(AcpError::ProcessExited(process_exit));
}
return Err(map_protocol_error(error));
}
};

View file

@ -9,10 +9,10 @@ use agent_client_protocol::{
Agent, Client, ConnectTo, Error as ProtocolError, Lines, Result as AcpProtocolResult,
};
use fabro_sandbox::{
Error as SandboxError, Result as SandboxResult, Sandbox, StderrCollector, StdioProcessHandle,
StdioProcessTermination,
DEFAULT_EXEC_OUTPUT_TAIL_BYTES, Error as SandboxError, Result as SandboxResult, Sandbox,
StderrCollector, StdioProcessHandle, StdioProcessTermination,
};
use fabro_types::CommandTermination;
use fabro_types::{CommandTermination, ExecOutputTail};
use futures::io::BufReader;
use futures::sink::unfold;
use futures::{AsyncBufReadExt, AsyncWriteExt, Stream};
@ -21,6 +21,7 @@ use tokio::time::timeout;
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
use crate::command::AcpCommand;
use crate::error::AcpProcessExit;
const CLEAN_EXIT_PROTOCOL_GRACE: Duration = Duration::from_millis(500);
@ -29,6 +30,7 @@ pub(crate) struct TransportState {
handle: Arc<TokioMutex<Option<StdioProcessHandle>>>,
stderr: Arc<TokioMutex<Option<StderrCollector>>>,
startup_error: Arc<TokioMutex<Option<SandboxError>>>,
process_exit: Arc<TokioMutex<Option<AcpProcessExit>>>,
}
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<SandboxError> {
self.startup_error.lock().await.take()
}
pub(crate) async fn take_process_exit(&self) -> Option<AcpProcessExit> {
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<ExecOutputTail> {
let stderr = self.stderr_tail().await;
redacted_stderr_tail(&stderr)
}
}
pub(crate) struct SandboxAcpTransport {
@ -156,21 +176,17 @@ impl ConnectTo<Client> 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<ExecOutputTail> {
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")
}

View file

@ -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")
);
}

View file

@ -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");
}

View file

@ -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<String>,
@ -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<WebFetchSummarizer>,
catalog: Arc<Catalog>,
) -> Box<dyn AgentProfile> {
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<Provider> {
fn parse_provider(args: &AgentArgs) -> anyhow::Result<ProviderId> {
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<ProviderId> {
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<dyn CredentialSource> {
@ -275,16 +298,26 @@ fn standalone_llm_source() -> Arc<dyn CredentialSource> {
}
}
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<AgentProfileKind> {
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<McpServerSettings>,
) -> 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<dyn CredentialSource>,
mcp_servers: Vec<McpServerSettings>,
) -> 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<dyn CredentialSource>,
mcp_servers: Vec<McpServerSettings>,
catalog: Arc<Catalog>,
) -> 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<McpServerSettings>,
) -> 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<McpServerSettings>,
catalog: Arc<Catalog>,
) -> 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<dyn AgentProfile> = 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<Catalog> {
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");

View file

@ -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(),
});

View file

@ -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<Turn>,
turns: Vec<Message>,
}
impl History {
pub fn push(&mut self, turn: Turn) {
pub fn from_session_messages(messages: &[SessionMessage]) -> Result<Self, serde_json::Error> {
Ok(Self {
turns: messages
.iter()
.map(Message::from_session_message)
.collect::<Result<Vec<_>, _>>()?,
})
}
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<SessionMessage> {
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<Message> {
pub fn convert_to_messages(&self) -> Vec<LlmMessage> {
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<ContentPart> = 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<Turn>, token_budget: usize) -> Vec<Turn> {
fn extract_recent_user_messages(discarded: Vec<Message>, token_budget: usize) -> Vec<Message> {
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<Turn>, 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")
);
}
}

View file

@ -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(

View file

@ -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<u64> {
let Turn::Assistant { tool_calls, .. } = turn else {
fn extract_signatures_from_assistant(turn: &Message) -> Vec<u64> {
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(),
});

View file

@ -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<Vec<String>, 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

View file

@ -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<Catalog> {
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");
}

Some files were not shown because too many files have changed in this diff Show more