diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2847b057..1f22b5fa5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,6 +50,8 @@ pnpm --filter @openuidev/react-lang build Use the package name that matches the area you are changing. +If you are a coding agent (or working with one), read the `AGENTS.md` in the package you are changing before editing, for example `packages/react-headless/AGENTS.md`. For building apps on the Agent Interface SDK, the self-contained consumer guide is `docs/public/AGENTS.md`, served at `/AGENTS.md` on the docs site. + ## Before Opening a Pull Request Before opening a pull request, please make sure that: diff --git a/docs/app/llms.txt/route.ts b/docs/app/llms.txt/route.ts index cc98a1c3a..2e29fed1d 100644 --- a/docs/app/llms.txt/route.ts +++ b/docs/app/llms.txt/route.ts @@ -6,6 +6,9 @@ export async function GET() { const lines: string[] = []; lines.push("# Documentation"); lines.push(""); + lines.push( + "- [AGENTS.md — complete agent guide for the Agent Interface SDK](/AGENTS.md): self-contained instructions for coding agents", + ); for (const page of source.getPages()) { lines.push(`- [${page.data.title}](${page.url}): ${page.data.description}`); } diff --git a/docs/content/docs/openui-lang/examples/harnesses/pi-agent-harness.mdx b/docs/content/docs/openui-lang/examples/harnesses/pi-agent-harness.mdx index ae362d8ba..e5606d3c8 100644 --- a/docs/content/docs/openui-lang/examples/harnesses/pi-agent-harness.mdx +++ b/docs/content/docs/openui-lang/examples/harnesses/pi-agent-harness.mdx @@ -26,39 +26,32 @@ Its mid-turn activity (reasoning and tool runs) surfaces as cards too. | Piece | File | Role | | --- | --- | --- | -| Frontend | `src/app/page.tsx` | A single `` whose `llm.send` posts to the bridge with `streamProtocol: openAIReadableStreamAdapter()`. Generates the OpenUI Lang system prompt and sends it with each turn. | -| Bridge route | `src/app/api/chat/route.ts` | Drives a pi `AgentSession` and re-emits its events as NDJSON OpenAI chunks (`delta.content` is OpenUI Lang). | -| Session registry | `src/lib/pi-session.ts` | One persistent `AgentSession` per chat thread, keyed by the `x-conversation-id` header. | +| Frontend | `src/app/page.tsx` | A single `` wired to `fetchLLM`, which posts each turn to the bridge and parses the reply with `openAIReadableStreamAdapter()`. | +| Bridge route | `src/app/api/chat/route.ts` | Computes the OpenUI Lang system prompt server-side, drives a pi `AgentSession`, and re-emits its events as NDJSON OpenAI chunks (`delta.content` is OpenUI Lang). | +| Session registry | `src/lib/pi-session.ts` | One persistent `AgentSession` per chat thread, keyed by the `threadId` that `fetchLLM` sends in the request body. | | Agent | `@earendil-works/pi-coding-agent` | The pi coding agent: `read` / `bash` / `edit` / `write` on the workspace you choose at launch. | Everything runs in **one Next.js process**: the App-Router route _is_ the backend. The pi SDK is embedded directly (no separate server), so there is no second service and no CORS. Each chat thread maps to one persistent pi `AgentSession`, so multi-turn context is preserved. ## Connecting the frontend -The client is a single `` (the artifact chat surface with sidebar thread history). It generates the OpenUI Lang system prompt from the component library, sends it with each turn via its `llm.send`, and parses the response with `openAIReadableStreamAdapter()` (NDJSON OpenAI chunks): +The client is a single `` (the artifact chat surface with sidebar thread history) wired to `fetchLLM`. It posts each turn to the bridge — including the thread's stable client-generated id as `threadId` in the body — and parses the response with `openAIReadableStreamAdapter()` (NDJSON OpenAI chunks): ```tsx import { AgentInterface, + fetchLLM, openAIMessageFormat, openAIReadableStreamAdapter, - type ChatLLM, } from "@openuidev/react-ui"; -import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui/genui-lib"; - -const systemPrompt = openuiLibrary.prompt(openuiPromptOptions); - -const llm: ChatLLM = { - // threadId is stable per thread, so each thread maps to its own persistent pi AgentSession. - send: ({ threadId, messages, signal }) => - fetch("/api/chat", { - method: "POST", - headers: { "Content-Type": "application/json", "x-conversation-id": threadId }, - body: JSON.stringify({ systemPrompt, messages: openAIMessageFormat.toApi(messages) }), - signal, - }), - streamProtocol: openAIReadableStreamAdapter(), -}; +import { openuiLibrary } from "@openuidev/react-ui/genui-lib"; + +// threadId is stable per thread, so each thread maps to its own persistent pi AgentSession. +const llm = fetchLLM({ + url: "/api/chat", + streamAdapter: openAIReadableStreamAdapter(), + messageFormat: openAIMessageFormat, +}); ; ``` -The `systemPrompt` generated here is the **same** string the backend injects into pi, so the model's markup always matches the component set the renderer knows. +The client does **not** generate the OpenUI Lang system prompt. The route computes it server-side from the **same** `openuiLibrary` this page renders with, so the model's markup always matches the component set the renderer knows — and the client doesn't have to ship the (large) prompt. A request can still pass `systemPrompt` in the body as an optional override. ## The bridge route -The route keys a persistent `AgentSession` by the `x-conversation-id` header, injects the OpenUI Lang prompt via `appendSystemPrompt`, subscribes to the session's events, and re-emits them as NDJSON OpenAI chunks. Because pi keeps its own transcript, only the newest user turn is sent to `session.prompt()`: +The route keys a persistent `AgentSession` by the `threadId` that `fetchLLM` sends in the request body (an `x-conversation-id` header is kept only as a fallback for older/custom clients), computes the OpenUI Lang prompt server-side (`body.systemPrompt` can override it), injects it via `appendSystemPrompt`, subscribes to the session's events, and re-emits them as NDJSON OpenAI chunks. Because pi keeps its own transcript, only the newest user turn is sent to `session.prompt()`: ```ts // lib/pi-session.ts: one AgentSession per conversation @@ -89,7 +82,12 @@ const { session } = await createAgentSession({ cwd, agentDir, settingsManager, r ``` ```ts -// app/api/chat/route.ts: translate pi events into OpenAI NDJSON +// app/api/chat/route.ts: server-side OpenUI prompt, then pi events into OpenAI NDJSON +const defaultSystemPrompt = openuiLibrary.prompt(openuiPromptOptions); + +const conversationId = + body.threadId || req.headers.get("x-conversation-id") || crypto.randomUUID(); + const unsubscribe = session.subscribe((event) => { if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { enqueue(ndjsonChunk({ content: event.assistantMessageEvent.delta })); @@ -99,7 +97,7 @@ await session.prompt(lastUserText); enqueue(ndjsonChunk({}, "stop")); ``` -The pi SDK is ESM-only, so it is loaded with a native dynamic `import()` and marked as a webpack external in `next.config.ts` (the example runs with `--webpack`). +The pi SDK is ESM-only, so it is loaded with a native dynamic `import()` and marked as a webpack external in `next.config.ts` (the example runs with `--webpack`). The same config also externalizes `@openuidev/react-ui/genui-lib` — scoped to imports issued from the API routes — because the route computes the OpenUI prompt from it server-side and Next's route-handler graph would otherwise reject its client-oriented React imports at build time. ## Thinking states @@ -143,12 +141,12 @@ The pi SDK resolves a model from either an environment API key (`ANTHROPIC_API_K ``` examples/harnesses/pi-agent-harness/ -|- src/app/page.tsx # wired to openAIReadableStreamAdapter() -|- src/app/api/chat/route.ts # pi event stream into NDJSON OpenAI chunks +|- src/app/page.tsx # wired to fetchLLM + openAIReadableStreamAdapter() +|- src/app/api/chat/route.ts # server-side OpenUI prompt; pi event stream into NDJSON OpenAI chunks |- src/lib/pi-session.ts # one persistent pi AgentSession per conversation |- src/library.ts # the OpenUI component library (re-exported) |- scripts/launch.mjs # picks the agent workspace, then starts Next -|- next.config.ts # keeps the ESM-only pi SDK external +|- next.config.ts # keeps the ESM-only pi SDK (and, for API routes, genui-lib) external ``` ## Run the example diff --git a/docs/public/AGENTS.md b/docs/public/AGENTS.md index 0ac348586..29ec0eab1 100644 --- a/docs/public/AGENTS.md +++ b/docs/public/AGENTS.md @@ -20,12 +20,13 @@ Everything imports from `@openuidev/react-ui`. Read the Rules first. `openAIReadableStreamAdapter()` with `openAIMessageFormat`; `agUIAdapter()` with `identityMessageFormat` (the default); `langGraphAdapter()` with `langGraphMessageFormat`. -- **OpenUI Cloud is two planes.** `llm` is a `ChatLLM` whose `send` posts to your - own `/api/chat` route, which proxies to Cloud with `THESYS_API_KEY`. `storage` +- **OpenUI Cloud is two planes.** `llm` is `fetchLLM({ url: "/api/chat", ... })`, + and your own `/api/chat` route proxies to Cloud with `THESYS_API_KEY`. `storage` is `useOpenuiCloudStorage({ token: "/api/frontend-token" })`. `THESYS_API_KEY` is server-side only, never in the browser. - **On Cloud, send only the latest message.** The Responses API replays history - from the conversation: `input: openAIConversationMessageFormat.toApi(messages.slice(-1))`. + from the conversation, so wrap the message format: + `toApi: (msgs) => openAIConversationMessageFormat.toApi(msgs.slice(-1))`. - **For Cloud, the component set, storage hook, and artifacts come from `@openuidev/thesys`** (`chatLibrary`, `useOpenuiCloudStorage`, `artifactRenderers`, `artifactCategories`); the server route uses @@ -66,9 +67,9 @@ import "@openuidev/thesys/styles.css"; import { AgentInterface, + fetchLLM, openAIConversationMessageFormat, openAIResponsesAdapter, - type ChatLLM, } from "@openuidev/react-ui"; import { chatLibrary, @@ -77,20 +78,15 @@ import { artifactCategories, } from "@openuidev/thesys"; -const llm: ChatLLM = { - // Cloud replays history from the conversation, so send only the latest message. - send: ({ threadId, messages, signal }) => - fetch("/api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - threadId, - input: openAIConversationMessageFormat.toApi(messages.slice(-1)), - }), - signal, - }), - streamProtocol: openAIResponsesAdapter(), -}; +const llm = fetchLLM({ + url: "/api/chat", + streamAdapter: openAIResponsesAdapter(), + // Cloud replays history from the conversation, so this wrapper sends only the latest message. + messageFormat: { + toApi: (msgs) => openAIConversationMessageFormat.toApi(msgs.slice(-1)), + fromApi: openAIConversationMessageFormat.fromApi, + }, +}); export default function App() { const storage = useOpenuiCloudStorage({ @@ -116,7 +112,9 @@ export default function App() { import { artifactTool, createResponsesInstructions } from "@openuidev/thesys-server"; export async function POST(req: Request) { - const { threadId, input } = await req.json(); + // fetchLLM POSTs { threadId, runId, messages, tools, context }; here messages + // is the latest message only (the client's messageFormat slices). + const { threadId, messages } = await req.json(); const upstream = await fetch("https://api.thesys.dev/v1/embed/responses", { method: "POST", headers: { @@ -126,7 +124,7 @@ export async function POST(req: Request) { body: JSON.stringify({ model: "openai/gpt-5", conversation: threadId, // Cloud stores and replays the conversation - input, + input: messages, stream: true, store: true, tools: [artifactTool()], // managed slides/report tool @@ -211,12 +209,14 @@ export async function POST(req: Request) { } ``` -`fetchLLM` POSTs `{ threadId, messages: messageFormat.toApi(messages) }` to `url` -and forwards the abort signal. Here `messages` is the **full thread history** (the -SDK loads it from `storage` and holds it client-side), so forward all of it to your -provider. (Contrast Cloud, where you send only the latest because Cloud replays the -conversation.) Your route must **stream** and **close the stream when done** (the -client's `isRunning` flips back to `false` only on close). +`fetchLLM` POSTs an AG-UI `RunAgentInput`-shaped body to `url` — `{ threadId, +runId, messages: messageFormat.toApi(messages), tools: [], context: [] }`, with a +fresh `runId` per send — and forwards the abort signal. Routes may destructure +just `{ messages }`. Here `messages` is the **full thread history** (the SDK loads +it from `storage` and holds it client-side), so forward all of it to your +provider. (Contrast Cloud, where the wrapped message format sends only the latest +because Cloud replays the conversation.) Your route must **stream** and **close +the stream when done** (the client's `isRunning` flips back to `false` only on close). ## `` props @@ -241,8 +241,10 @@ Children are slot overrides (see Customization). ## Backends: `ChatLLM` and adapters -The `llm` is a `ChatLLM`. `fetchLLM` builds one for the common case; for full -control, write the object directly (this is what the Cloud quickstart does). +The `llm` is a `ChatLLM`. `fetchLLM` builds one for anything that POSTs to an +HTTP endpoint and streams the response back (both quickstarts use it). Write the +object directly only for genuinely custom cases — e.g. synthesizing the stream +client-side when there is no HTTP endpoint at all. ```ts interface ChatLLM { diff --git a/examples/fastapi-backend/README.md b/examples/fastapi-backend/README.md index e974990a3..1914c1b9c 100644 --- a/examples/fastapi-backend/README.md +++ b/examples/fastapi-backend/README.md @@ -27,14 +27,17 @@ fastapi-backend/ │ ├── .env.example # Environment template │ ├── pyproject.toml # Python dependencies │ └── app/ -│ └── main.py # Streaming chat endpoint +│ ├── main.py # Streaming chat endpoint +│ └── system_prompt.txt # Generated OpenUI system prompt (see below) └── frontend/ ├── package.json ├── vite.config.js # Vite + proxy to localhost:8000 ├── index.html + ├── scripts/ + │ └── generate-prompt.mjs # Writes backend/app/system_prompt.txt └── src/ ├── main.jsx - ├── App.jsx # Identical to genui-chat-app + ├── App.jsx # fetchLLM + AgentInterface └── index.css ``` @@ -60,7 +63,17 @@ Add your key to `backend/.env`: OPENAI_API_KEY=sk-... ``` -### 2. Start the backend +### 2. Generate the system prompt (first run only) + +The backend reads the OpenUI system prompt from `backend/app/system_prompt.txt` at startup. A generated copy is checked in, so this step is only needed if the file is missing or after upgrading `@openuidev/react-ui`: + +```bash +cd frontend +npm install +npm run generate:prompt +``` + +### 3. Start the backend ```bash cd backend @@ -76,7 +89,7 @@ uvicorn app.main:app --reload The API will be available at `http://localhost:8000`. -### 3. Start the frontend +### 4. Start the frontend ```bash cd frontend @@ -92,13 +105,18 @@ Open [http://localhost:5173](http://localhost:5173). A FastAPI endpoint that: -1. Receives `{ systemPrompt, messages }` as JSON -2. Forwards the conversation to the OpenAI streaming API -3. Yields each chunk as a NDJSON line — the same format the JavaScript SDK's `toReadableStream()` produces +1. Loads the OpenUI system prompt from `app/system_prompt.txt` at startup (fails fast with a pointer to `npm run generate:prompt` if the file is missing) +2. Receives `{ threadId, runId, messages, tools, context }` as JSON — the body `fetchLLM` sends; `systemPrompt` may optionally be included to override the server-side default +3. Forwards the conversation to the OpenAI streaming API +4. Yields each chunk as a NDJSON line — the same format the JavaScript SDK's `toReadableStream()` produces ### `frontend/src/App.jsx` -Identical to the one scaffolded by `npx @openuidev/cli create`. Uses `openAIReadableStreamAdapter()` to parse the NDJSON stream from the backend — no frontend changes were needed to switch from Next.js to FastAPI. +Wires `AgentInterface` to the backend with `fetchLLM({ url: "/api/chat", streamAdapter: openAIReadableStreamAdapter(), messageFormat: openAIMessageFormat })`. The system prompt is owned by the backend, so the client sends only the conversation — no frontend changes were needed to switch from Next.js to FastAPI. + +### `frontend/scripts/generate-prompt.mjs` + +A small Node script that imports `openuiLibrary` and `openuiPromptOptions` from `@openuidev/react-ui/genui-lib` and writes the generated prompt to `backend/app/system_prompt.txt`. Run it via `npm run generate:prompt` after upgrading `@openuidev/react-ui`. ## Learn More diff --git a/examples/fastapi-backend/backend/app/main.py b/examples/fastapi-backend/backend/app/main.py index 27bb9cc1c..45e235834 100644 --- a/examples/fastapi-backend/backend/app/main.py +++ b/examples/fastapi-backend/backend/app/main.py @@ -1,5 +1,6 @@ """Minimal FastAPI backend — streams OpenAI completions as NDJSON.""" import os +from pathlib import Path from dotenv import load_dotenv from fastapi import FastAPI @@ -12,15 +13,24 @@ client = AsyncOpenAI() MODEL = os.environ.get("OPENAI_MODEL", "gpt-5.5") +PROMPT_FILE = Path(__file__).parent / "system_prompt.txt" +if not PROMPT_FILE.is_file(): + raise RuntimeError( + f"Missing {PROMPT_FILE} — generate it by running `npm run generate:prompt` " + "in the frontend/ directory." + ) +DEFAULT_SYSTEM_PROMPT = PROMPT_FILE.read_text(encoding="utf-8") + app = FastAPI() app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) @app.post("/api/chat") async def chat(body: dict): + system_prompt = body.get("systemPrompt") or DEFAULT_SYSTEM_PROMPT response = await client.chat.completions.create( model=MODEL, - messages=[{"role": "system", "content": body["systemPrompt"]}, *body["messages"]], + messages=[{"role": "system", "content": system_prompt}, *body["messages"]], stream=True, ) diff --git a/examples/fastapi-backend/backend/app/system_prompt.txt b/examples/fastapi-backend/backend/app/system_prompt.txt new file mode 100644 index 000000000..8ecb29283 --- /dev/null +++ b/examples/fastapi-backend/backend/app/system_prompt.txt @@ -0,0 +1,213 @@ +You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang. + +## Syntax Rules + +1. Each statement is on its own line: `identifier = Expression` +2. `root` is the entry point — every program must define `root = Stack(...)` +3. Expressions are: strings ("..."), numbers, booleans (true/false), null, arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...) +4. Use references for readability: define `name = ...` on one line, then use `name` later +5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array. +6. Arguments are POSITIONAL (order matters, not names). Write `Stack([children], "row", "l")` NOT `Stack([children], direction: "row", gap: "l")` — colon syntax is NOT supported and silently breaks +7. Optional arguments can be omitted from the end +- Strings use double quotes with backslash escaping + +## Component Signatures + +Arguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming. +Props typed `ActionExpression` accept an Action([@steps...]) expression. See the Action section for available steps (@ToAssistant, @OpenUrl). +Props marked `$binding` accept a `$variable` reference for two-way binding. + +### Layout +Stack(children: any[], direction?: "row" | "column", gap?: "none" | "xs" | "s" | "m" | "l" | "xl" | "2xl", align?: "start" | "center" | "end" | "stretch" | "baseline", justify?: "start" | "center" | "end" | "between" | "around" | "evenly", wrap?: boolean) — Flex container. direction: "row"|"column" (default "column"). gap: "none"|"xs"|"s"|"m"|"l"|"xl"|"2xl" (default "m"). align: "start"|"center"|"end"|"stretch"|"baseline". justify: "start"|"center"|"end"|"between"|"around"|"evenly". +Tabs(items: TabItem[]) — Tabbed container +TabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[]) — value is unique id, trigger is tab label, content is array of components +Accordion(items: AccordionItem[]) — Collapsible sections +AccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[]) — value is unique id, trigger is section title +Steps(items: StepsItem[]) — Step-by-step guide +StepsItem(title: string, details: string) — title and details text for one step +Carousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[][], variant?: "card" | "sunk") — Horizontal scrollable carousel +Separator(orientation?: "horizontal" | "vertical", decorative?: boolean) — Visual divider between content sections +Modal(title: string, open?: $binding, children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[], size?: "sm" | "md" | "lg") — Modal dialog. open is a reactive $boolean binding — set to true to open, X/Escape/backdrop auto-closes. Put Form with buttons inside children. +- For grid-like layouts, use Stack with direction "row" and wrap set to true. +- Prefer justify "start" (or omit justify) with wrap=true for stable columns instead of uneven gutters. +- Use nested Stacks when you need explicit rows/sections. +- Show/hide sections: $editId != "" ? Card([editForm]) : null +- Modal: Modal("Title", $showModal, [content]) — $showModal is boolean, X/Escape auto-closes. Put Form with its own buttons inside children. +- Use Tabs for alternative views (chart types, data sections) — no $variable needed +- Shared filter across Tabs: same $days binding in Query args works across all TabItems + +### Content +Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps | Tabs | Carousel | Stack)[], variant?: "card" | "sunk" | "clear", direction?: "row" | "column", gap?: "none" | "xs" | "s" | "m" | "l" | "xl" | "2xl", align?: "start" | "center" | "end" | "stretch" | "baseline", justify?: "start" | "center" | "end" | "between" | "around" | "evenly", wrap?: boolean) — Styled container. variant: "card" (default, elevated) | "sunk" (recessed) | "clear" (transparent). Always full width. Accepts all Stack flex params (default: direction "column"). Cards flex to share space in row/wrap layouts. +CardHeader(title?: string, subtitle?: string) — Header with optional title and subtitle +TextContent(text: string, size?: "small" | "default" | "large" | "small-heavy" | "large-heavy") — Text block. Supports markdown. Optional size: "small" | "default" | "large" | "small-heavy" | "large-heavy". +MarkDownRenderer(textMarkdown: string, variant?: "clear" | "card" | "sunk") — Renders markdown text with optional container variant +Callout(variant: "info" | "warning" | "error" | "success" | "neutral", title: string, description: string, visible?: $binding) — Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false. +TextCallout(variant?: "neutral" | "info" | "warning" | "success" | "danger", title?: string, description?: string) — Text callout with variant, title, and description +Image(alt: string, src?: string) — Image with alt text and optional URL +ImageBlock(src: string, alt?: string) — Image block with loading state +ImageGallery(images: {src: string, alt?: string, details?: string}[]) — Gallery grid of images with modal preview +CodeBlock(language: string, codeString: string) — Syntax-highlighted code block +- Use Cards to group related KPIs or sections. Stack with direction "row" for side-by-side layouts. +- Success toast: Callout("success", "Saved", "Done.", $showSuccess) — use @Set($showSuccess, true) in save action, auto-dismisses after 3s. For errors: result.status == "error" ? Callout("error", "Failed", result.error) : null +- KPI card: Card([TextContent("Label", "small"), TextContent("" + @Count(@Filter(data.rows, "field", "==", "value")), "large-heavy")]) + +### Tables +Table(columns: Col[]) — Data table — column-oriented. Each Col holds its own data array. +Col(label: string, data: any, type?: "string" | "number" | "action") — Column definition — holds label + data array +- Table is COLUMN-oriented: Table([Col("Label", dataArray), Col("Count", countArray, "number")]). Use array pluck for data: data.rows.fieldName +- Col data can be component arrays for styled cells: Col("Status", @Each(data.rows, "item", Tag(item.status, null, "sm", item.status == "open" ? "success" : "danger"))) +- Row actions: Col("Actions", @Each(data.rows, "t", Button("Edit", Action([@Set($showEdit, true), @Set($editId, t.id)])))) +- Sortable: sorted = @Sort(data.rows, $sortField, "desc"). Bind $sortField to Select. Use sorted.fieldName for Col data +- Searchable: filtered = @Filter(data.rows, "title", "contains", $search). Bind $search to Input +- Chain sort + filter: filtered = @Filter(...) then sorted = @Sort(filtered, ...) — use sorted for both Table and Charts +- Empty state: @Count(data.rows) > 0 ? Table([...]) : TextContent("No data yet") + +### Charts (2D) +BarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", xLabel?: string, yLabel?: string) — Vertical bars; use for comparing values across categories with one or more series +LineChart(labels: string[], series: Series[], variant?: "linear" | "natural" | "step", xLabel?: string, yLabel?: string) — Lines over categories; use for trends and continuous data over time +AreaChart(labels: string[], series: Series[], variant?: "linear" | "natural" | "step", xLabel?: string, yLabel?: string) — Filled area under lines; use for cumulative totals or volume trends over time +RadarChart(labels: string[], series: Series[]) — Spider/web chart; use for comparing multiple variables across one or more entities +HorizontalBarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", xLabel?: string, yLabel?: string) — Horizontal bars; prefer when category labels are long or for ranked lists +Series(category: string, values: number[]) — One data series +- Charts accept column arrays: LineChart(labels, [Series("Name", values)]). Use array pluck: LineChart(data.rows.day, [Series("Views", data.rows.views)]) +- Use Cards to wrap charts with CardHeader for titled sections +- Chart + Table from same source: use @Sort or @Filter result for both LineChart and Table Col data +- Multiple chart views: use Tabs — Tabs([TabItem("line", "Line", [LineChart(...)]), TabItem("bar", "Bar", [BarChart(...)])]) + +### Charts (1D) +PieChart(labels: string[], values: number[], variant?: "pie" | "donut", appearance?: "circular" | "semiCircular") — Circular slices; use plucked arrays: PieChart(data.categories, data.values) +RadialChart(labels: string[], values: number[]) — Radial bars; use plucked arrays: RadialChart(data.categories, data.values) +SingleStackedBarChart(labels: string[], values: number[]) — Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values) +Slice(category: string, value: number) — One slice with label and numeric value +- PieChart and BarChart need NUMBERS, not objects. For list data, use @Count(@Filter(...)) to aggregate: +- PieChart from list: `PieChart(["Low", "Med", "High"], [@Count(@Filter(data.rows, "priority", "==", "low")), @Count(@Filter(data.rows, "priority", "==", "medium")), @Count(@Filter(data.rows, "priority", "==", "high"))], "donut")` +- KPI from count: `TextContent("" + @Count(@Filter(data.rows, "status", "==", "open")), "large-heavy")` + +### Charts (Scatter) +ScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string) — X/Y scatter plot; use for correlations, distributions, and clustering +ScatterSeries(name: string, points: Point[]) — Named dataset +Point(x: number, y: number, z?: number) — Data point with numeric coordinates + +### Forms +Form(name: string, buttons: Buttons, fields?: FormControl[]) — Form container with fields and explicit action buttons +FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup, hint?: string) — Field with label, input component, and optional hint text +Label(text: string) — Text label +Input(name: string, placeholder?: string, type?: "text" | "email" | "password" | "number" | "url", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) +TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) +Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding, size?: "small" | "medium" | "large") +SelectItem(value: string, label: string) — Option for Select +DatePicker(name: string, mode?: "single" | "range", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) +Slider(name: string, variant: "continuous" | "discrete", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) — Numeric slider input; supports continuous and discrete (stepped) variants +CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding>) +CheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean) +RadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) +RadioItem(label: string, description: string, value: string) +SwitchGroup(name: string, items: SwitchItem[], variant?: "clear" | "card" | "sunk", value?: $binding>) — Group of switch toggles +SwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean) — Individual switch toggle +- For Form fields, define EACH FormControl as its own reference — do NOT inline all controls in one array. This allows progressive field-by-field streaming. +- NEVER nest Form inside Form — each Form should be a standalone container. +- Form requires explicit buttons. Always pass a Buttons(...) reference as the third Form argument. +- rules is an optional object: {required: true, email: true, minLength: 8, maxLength: 100} +- Available rules: required, email, min, max, minLength, maxLength, pattern, url, numeric +- The renderer shows error messages automatically — do NOT generate error text in the UI +- Conditional fields: $country == "US" ? stateField : $country == "UK" ? postcodeField : addressField +- Edit form in Modal: Modal("Edit", $showEdit, [Form("edit", Buttons([saveBtn, cancelBtn]), [fields...])]). Save button should include @Set($showEdit, false) to close modal. + +### Buttons +Button(label: string, action?: ActionExpression, variant?: "primary" | "secondary" | "tertiary", type?: "normal" | "destructive", size?: "extra-small" | "small" | "medium" | "large") — Clickable button +Buttons(buttons: Button[], direction?: "row" | "column") — Group of Button components. direction: "row" (default) | "column". +- Toggle in @Each: @Each(rows, "t", Button(t.status == "open" ? "Close" : "Reopen", Action([...]))) + +### Data Display +TagBlock(tags: string[]) — tags is an array of strings +Tag(text: string, icon?: string, size?: "sm" | "md" | "lg", variant?: "neutral" | "info" | "success" | "warning" | "danger") — Styled tag/badge with optional icon and variant +- Color-mapped Tag: Tag(value, null, "sm", value == "high" ? "danger" : value == "medium" ? "warning" : "neutral") + +## Action — Button Behavior + +Action([@steps...]) wires button clicks to operations. Steps are @-prefixed built-in actions. Steps execute in order. +Buttons without an explicit Action prop automatically send their label to the assistant (equivalent to Action([@ToAssistant(label)])). + +Available steps: +- @ToAssistant("message") — Send a message to the assistant (for conversational buttons like "Tell me more", "Explain this") +- @OpenUrl("https://...") — Navigate to a URL + +Example — simple nav: +``` +viewBtn = Button("View", Action([@OpenUrl("https://example.com")])) +``` + +- Action can be assigned to a variable or inlined: Button("Go", onSubmit) and Button("Go", Action([...])) both work + +## Hoisting & Streaming (CRITICAL) + +openui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed. + +During streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in. + +**Recommended statement order for optimal streaming:** +1. `root = Stack(...)` — UI shell appears immediately +2. Component definitions — fill in as they stream +3. Data values — leaf content last + +Always write the root = Stack(...) statement first so the UI shell appears immediately, even before child data has streamed in. + +## Examples + +Example 1 — Table (column-oriented): + +root = Stack([title, tbl]) +title = TextContent("Top Languages", "large-heavy") +tbl = Table([Col("Language", langs), Col("Users (M)", users), Col("Year", years)]) +langs = ["Python", "JavaScript", "Java", "TypeScript", "Go"] +users = [15.7, 14.2, 12.1, 8.5, 5.2] +years = [1991, 1995, 1995, 2012, 2009] + +Example 2 — Bar chart: + +root = Stack([title, chart]) +title = TextContent("Q4 Revenue", "large-heavy") +chart = BarChart(labels, [s1, s2], "grouped") +labels = ["Oct", "Nov", "Dec"] +s1 = Series("Product A", [120, 150, 180]) +s2 = Series("Product B", [90, 110, 140]) + +Example 3 — Form with validation: + +root = Stack([title, form]) +title = TextContent("Contact Us", "large-heavy") +form = Form("contact", btns, [nameField, emailField, countryField, msgField]) +nameField = FormControl("Name", Input("name", "Your name", "text", { required: true, minLength: 2 })) +emailField = FormControl("Email", Input("email", "you@example.com", "email", { required: true, email: true })) +countryField = FormControl("Country", Select("country", countryOpts, "Select...", { required: true })) +msgField = FormControl("Message", TextArea("message", "Tell us more...", 4, { required: true, minLength: 10 })) +countryOpts = [SelectItem("us", "United States"), SelectItem("uk", "United Kingdom"), SelectItem("de", "Germany")] +btns = Buttons([Button("Submit", Action([@ToAssistant("Submit")]), "primary"), Button("Cancel", Action([@ToAssistant("Cancel")]), "secondary")]) + +Example 4 — Tabs with mixed content: + +root = Stack([title, tabs]) +title = TextContent("React vs Vue", "large-heavy") +tabs = Tabs([tabReact, tabVue]) +tabReact = TabItem("react", "React", reactContent) +tabVue = TabItem("vue", "Vue", vueContent) +reactContent = [TextContent("React is a library by Meta for building UIs."), Callout("info", "Note", "React uses JSX syntax.")] +vueContent = [TextContent("Vue is a progressive framework by Evan You."), Callout("success", "Tip", "Vue has a gentle learning curve.")] + +## Important Rules +- When asked about data, generate realistic/plausible data +- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.) + +## Final Verification +Before finishing, walk your output and verify: +1. root = Stack(...) is the FIRST line (for optimal streaming). +2. Every referenced name is defined. Every defined name (other than root) is reachable from root. + +- For grid-like layouts, use Stack with direction "row" and wrap=true. Avoid justify="between" unless you specifically want large gutters. +- For forms, define one FormControl reference per field so controls can stream progressively. +- For forms, always provide the second Form argument with Buttons(...) actions: Form(name, buttons, fields). +- Never nest Form inside Form. +- Use @Reset($var1, $var2) after form submit to restore defaults — not @Set($var, "") +- Multi-query refresh: Action([@Run(mutation), @Run(query1), @Run(query2), @Reset(...)]) +- $variables are reactive: changing via Select or @Set re-evaluates all Queries and expressions referencing them +- Use existing components (Tabs, Accordion, Modal) before inventing ternary show/hide patterns \ No newline at end of file diff --git a/examples/fastapi-backend/frontend/package.json b/examples/fastapi-backend/frontend/package.json index 9160dfafd..ea84b83f3 100644 --- a/examples/fastapi-backend/frontend/package.json +++ b/examples/fastapi-backend/frontend/package.json @@ -5,7 +5,8 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "generate:prompt": "node scripts/generate-prompt.mjs" }, "dependencies": { "@openuidev/react-headless": "workspace:*", diff --git a/examples/fastapi-backend/frontend/scripts/generate-prompt.mjs b/examples/fastapi-backend/frontend/scripts/generate-prompt.mjs new file mode 100644 index 000000000..bc36f8159 --- /dev/null +++ b/examples/fastapi-backend/frontend/scripts/generate-prompt.mjs @@ -0,0 +1,20 @@ +// Writes the OpenUI system prompt to backend/app/system_prompt.txt so the +// FastAPI backend can prepend it server-side. Re-run after upgrading +// @openuidev/react-ui: +// +// npm run generate:prompt +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui/genui-lib"; + +const outFile = resolve( + dirname(fileURLToPath(import.meta.url)), + "../../backend/app/system_prompt.txt", +); + +const prompt = openuiLibrary.prompt(openuiPromptOptions); +await mkdir(dirname(outFile), { recursive: true }); +await writeFile(outFile, prompt, "utf8"); +console.log(`Wrote ${prompt.length} chars to ${outFile}`); diff --git a/examples/fastapi-backend/frontend/src/App.jsx b/examples/fastapi-backend/frontend/src/App.jsx index 6e0328280..965603125 100644 --- a/examples/fastapi-backend/frontend/src/App.jsx +++ b/examples/fastapi-backend/frontend/src/App.jsx @@ -3,32 +3,24 @@ import "@openuidev/react-ui/styles/index.css"; import { AgentInterface, + fetchLLM, openAIMessageFormat, openAIReadableStreamAdapter, } from "@openuidev/react-ui"; -import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui/genui-lib"; +import { openuiLibrary } from "@openuidev/react-ui/genui-lib"; import { useMemo } from "react"; -const systemPrompt = openuiLibrary.prompt(openuiPromptOptions); - export default function App() { - // Storage is AgentInterface's built-in in-memory default (wiped on reload). The - // backend call is unchanged — only the chat surface moved from FullScreen to - // AgentInterface. + // Storage is AgentInterface's built-in in-memory default (wiped on reload). + // The system prompt lives server-side: FastAPI reads backend/app/system_prompt.txt, + // regenerated with `npm run generate:prompt`. const llm = useMemo( - () => ({ - send: ({ messages, signal }) => - fetch("/api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - systemPrompt, - messages: openAIMessageFormat.toApi(messages), - }), - signal, - }), - streamProtocol: openAIReadableStreamAdapter(), - }), + () => + fetchLLM({ + url: "/api/chat", + streamAdapter: openAIReadableStreamAdapter(), + messageFormat: openAIMessageFormat, + }), [], ); diff --git a/examples/form-generator/src/app/page.tsx b/examples/form-generator/src/app/page.tsx index 41ed70788..81deff179 100644 --- a/examples/form-generator/src/app/page.tsx +++ b/examples/form-generator/src/app/page.tsx @@ -3,6 +3,7 @@ import { herouiChatLibrary } from "@/lib/heroui-genui"; import type { Message } from "@openuidev/react-headless"; import { + fetchLLM, openAIAdapter, openAIMessageFormat, processStreamedMessage, @@ -33,6 +34,11 @@ const STARTERS = [ }, ]; +const llm = fetchLLM({ + url: "/api/chat", + streamAdapter: openAIAdapter(), + messageFormat: openAIMessageFormat, +}); export default function Page() { const [instruction, setInstruction] = useState(""); @@ -44,73 +50,72 @@ export default function Page() { const [error, setError] = useState(null); const abortRef = useRef(null); - const handleSubmit = useCallback(async (overrideText?: string) => { - const text = (overrideText ?? instruction).trim(); - if (!text || isStreaming) return; + const handleSubmit = useCallback( + async (overrideText?: string) => { + const text = (overrideText ?? instruction).trim(); + if (!text || isStreaming) return; - setError(null); - setInstruction(""); + setError(null); + setInstruction(""); - let userContent = text; - if (Object.keys(formFieldSnapshot).length > 0) { - userContent = `Current form values (JSON): ${JSON.stringify(formFieldSnapshot)}\n\n${text}`; - } + let userContent = text; + if (Object.keys(formFieldSnapshot).length > 0) { + userContent = `Current form values (JSON): ${JSON.stringify(formFieldSnapshot)}\n\n${text}`; + } - const userMsg: Message = { id: crypto.randomUUID(), role: "user", content: userContent }; - const nextMessages = [...messages, userMsg]; - setMessages(nextMessages); + const userMsg: Message = { id: crypto.randomUUID(), role: "user", content: userContent }; + const nextMessages = [...messages, userMsg]; + setMessages(nextMessages); - const abortController = new AbortController(); - abortRef.current = abortController; - setIsStreaming(true); - setStreamingCode(""); + const abortController = new AbortController(); + abortRef.current = abortController; + setIsStreaming(true); + setStreamingCode(""); - let draftContent = ""; + let draftContent = ""; - try { - const response = await fetch("/api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - messages: openAIMessageFormat.toApi(nextMessages as any), - }), - signal: abortController.signal, - }); + try { + const response = await llm.send({ + threadId: "form", + messages: nextMessages, + signal: abortController.signal, + }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + if (!response.ok) throw new Error(`HTTP ${response.status}`); - const assistantId = crypto.randomUUID(); + const assistantId = crypto.randomUUID(); - const applyContent = (msg: Message) => { - draftContent = typeof msg.content === "string" ? msg.content : ""; - setStreamingCode(draftContent); - }; + const applyContent = (msg: Message) => { + draftContent = typeof msg.content === "string" ? msg.content : ""; + setStreamingCode(draftContent); + }; - await processStreamedMessage({ - response, - adapter: openAIAdapter(), - createMessage: applyContent, - updateMessage: applyContent, - }); + await processStreamedMessage({ + response, + adapter: llm.streamProtocol, + createMessage: applyContent, + updateMessage: applyContent, + }); - const assistantMsg: Message = { - id: assistantId, - role: "assistant", - content: draftContent, - }; - setMessages((prev) => [...prev, assistantMsg]); - setLatestCode(draftContent); - setStreamingCode(null); - } catch (err) { - if ((err as Error).name !== "AbortError") { - setError((err as Error).message ?? "Something went wrong."); + const assistantMsg: Message = { + id: assistantId, + role: "assistant", + content: draftContent, + }; + setMessages((prev) => [...prev, assistantMsg]); + setLatestCode(draftContent); + setStreamingCode(null); + } catch (err) { + if ((err as Error).name !== "AbortError") { + setError((err as Error).message ?? "Something went wrong."); + } + } finally { + setIsStreaming(false); + abortRef.current = null; } - } finally { - setIsStreaming(false); - abortRef.current = null; - } - }, [instruction, isStreaming, messages, formFieldSnapshot]); + }, + [instruction, isStreaming, messages, formFieldSnapshot], + ); const handleReset = useCallback(() => { abortRef.current?.abort(); @@ -149,9 +154,7 @@ export default function Page() {
{/* Title */}
-

- AI Form Generator -

+

AI Form Generator

Describe a form and get a live, interactive preview instantly.

@@ -187,15 +190,14 @@ export default function Page() { {STARTERS.map((s) => ( ))}
@@ -226,9 +228,7 @@ export default function Page() { {/* Message history */}
{userMessages.slice(1).length === 0 && ( -

- Refinements will appear here. -

+

Refinements will appear here.

)} {userMessages.slice(1).map((msg, i) => (
diff --git a/examples/hands-on-table-chat/src/app/api/chat/route.ts b/examples/hands-on-table-chat/src/app/api/chat/route.ts index a88de26d0..537619baf 100644 --- a/examples/hands-on-table-chat/src/app/api/chat/route.ts +++ b/examples/hands-on-table-chat/src/app/api/chat/route.ts @@ -1,13 +1,13 @@ import { readFileSync } from "fs"; -import { join } from "path"; import { NextRequest } from "next/server"; import OpenAI from "openai"; import type { ChatCompletionMessageParam } from "openai/resources/chat/completions.mjs"; -import { tools, setCurrentThreadId } from "./tools"; +import { join } from "path"; +import { setCurrentThreadId, tools } from "./tools"; const generatedPrompt = readFileSync( join(process.cwd(), "src/generated/system-prompt.txt"), - "utf-8" + "utf-8", ); const SPREADSHEET_INSTRUCTIONS = ` @@ -83,7 +83,7 @@ function extractText(msg: any): string { function sseToolCallStart( encoder: TextEncoder, tc: { id: string; function: { name: string } }, - index: number + index: number, ) { return encoder.encode( `data: ${JSON.stringify({ @@ -105,7 +105,7 @@ function sseToolCallStart( finish_reason: null, }, ], - })}\n\n` + })}\n\n`, ); } @@ -113,7 +113,7 @@ function sseToolCallArgs( encoder: TextEncoder, tc: { id: string; function: { arguments: string } }, result: string, - index: number + index: number, ) { let enrichedArgs: string; try { @@ -137,19 +137,20 @@ function sseToolCallArgs( finish_reason: null, }, ], - })}\n\n` + })}\n\n`, ); } export async function POST(req: NextRequest) { - const { messages, threadId } = await req.json(); + const { messages } = await req.json(); - setCurrentThreadId(threadId ?? "default"); + // Single shared table store keyed on "default"; chat threads are ephemeral, so + // we deliberately ignore the client's per-thread id (it would desync the tools + // from the one spreadsheet visible on screen). + setCurrentThreadId("default"); /* eslint-disable @typescript-eslint/no-explicit-any */ - const lastUserMsg = (messages as any[]) - .filter((m: any) => m.role === "user") - .pop(); + const lastUserMsg = (messages as any[]).filter((m: any) => m.role === "user").pop(); if (lastUserMsg) extractText(lastUserMsg); const cleanMessages = (messages as any[]) @@ -212,9 +213,7 @@ export async function POST(req: NextRequest) { runner.on("functionToolCall", (fc: any) => { const id = `tc-${callIdx}`; pendingCalls.push({ id, name: fc.name, arguments: fc.arguments }); - enqueue( - sseToolCallStart(encoder, { id, function: { name: fc.name } }, callIdx) - ); + enqueue(sseToolCallStart(encoder, { id, function: { name: fc.name } }, callIdx)); callIdx++; }); @@ -226,8 +225,8 @@ export async function POST(req: NextRequest) { encoder, { id: tc.id, function: { arguments: tc.arguments } }, result, - resultIdx - ) + resultIdx, + ), ); } resultIdx++; @@ -254,9 +253,7 @@ export async function POST(req: NextRequest) { runner.on("error", (err: any) => { const msg = err instanceof Error ? err.message : "Stream error"; console.error("Chat route error:", msg); - enqueue( - encoder.encode(`data: ${JSON.stringify({ error: msg })}\n\n`) - ); + enqueue(encoder.encode(`data: ${JSON.stringify({ error: msg })}\n\n`)); close(); }); /* eslint-enable @typescript-eslint/no-explicit-any */ diff --git a/examples/hands-on-table-chat/src/app/page.tsx b/examples/hands-on-table-chat/src/app/page.tsx index fc3f3ea24..7d418e52e 100644 --- a/examples/hands-on-table-chat/src/app/page.tsx +++ b/examples/hands-on-table-chat/src/app/page.tsx @@ -1,40 +1,26 @@ "use client"; import { spreadsheetLibrary } from "@/lib/spreadsheet-library"; -import { - AgentInterface, - openAIAdapter, - openAIMessageFormat, - type ChatLLM, -} from "@openuidev/react-ui"; +import { AgentInterface, fetchLLM, openAIAdapter, openAIMessageFormat } from "@openuidev/react-ui"; import { MessageSquare, PanelRightClose } from "lucide-react"; import dynamic from "next/dynamic"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { TableProvider, useTableContext } from "./TableContext"; +import { TableProvider } from "./TableContext"; const PersistentSpreadsheet = dynamic(() => import("./PersistentSpreadsheet"), { ssr: false }); function ChatPanel({ onClose }: { onClose: () => void }) { - const { threadId } = useTableContext(); - // AgentInterface uses its built-in in-memory storage default (wiped on reload). - // The backend call is unchanged — only the chat surface moved from Copilot to - // AgentInterface. - const llm = useMemo( - () => ({ - send: ({ messages, signal }) => - fetch("/api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - messages: openAIMessageFormat.toApi(messages), - threadId, - }), - signal, - }), - streamProtocol: openAIAdapter(), - }), - [threadId], + // fetchLLM sends AgentInterface's per-thread id, but the server ignores it and + // keys the shared table store on "default" (see api/chat/route.ts). + const llm = useMemo( + () => + fetchLLM({ + url: "/api/chat", + streamAdapter: openAIAdapter(), + messageFormat: openAIMessageFormat, + }), + [], ); return ( diff --git a/examples/harnesses/pi-agent-harness/README.md b/examples/harnesses/pi-agent-harness/README.md index 12a4a0522..66a328abf 100644 --- a/examples/harnesses/pi-agent-harness/README.md +++ b/examples/harnesses/pi-agent-harness/README.md @@ -14,8 +14,9 @@ launch — see **Security** below. ``` Browser (src/app/page.tsx) - AgentInterface ──POST /api/chat ({ systemPrompt, messages })──► route.ts (runtime=nodejs) - + openuiLibrary x-conversation-id: │ + AgentInterface ──POST /api/chat ({ threadId, messages, … })──► route.ts (runtime=nodejs) + + fetchLLM │ + + openuiLibrary │ renderer ◄──NDJSON OpenAI chunks (delta.content = OpenUI Lang)─────────┤ ▼ src/lib/pi-session.ts @@ -33,12 +34,14 @@ launch — see **Security** below. - **Transport:** the frontend's `openAIReadableStreamAdapter()` parses **NDJSON** OpenAI `chat.completion.chunk`s (one JSON object per line). The route translates pi's `text_delta` events into `delta.content`, and pi's reasoning + tool executions into `delta.tool_calls`. -- **System prompt:** `page.tsx` generates the OpenUI Lang prompt client-side - (`openuiLibrary.prompt(openuiPromptOptions)`) and sends it in the request body; the route - injects it into Pi via `DefaultResourceLoader({ appendSystemPrompt: [...] })`, so the backend - prompt and the frontend renderer always reference the same component library. -- **Sessions:** each chat thread (a stable id sent as the `x-conversation-id` header) maps to - one persistent Pi `AgentSession`, so multi-turn context is preserved. +- **System prompt:** the route generates the OpenUI Lang prompt server-side + (`openuiLibrary.prompt(openuiPromptOptions)`) and injects it into Pi via + `DefaultResourceLoader({ appendSystemPrompt: [...] })`, so the backend prompt and the frontend + renderer always reference the same component library. A client may send `systemPrompt` in the + request body as an override. +- **Sessions:** each chat thread (a stable id `fetchLLM` sends as `threadId` in the request body; + the legacy `x-conversation-id` header still works as a fallback) maps to one persistent Pi + `AgentSession`, so multi-turn context is preserved. ## Prerequisites diff --git a/examples/harnesses/pi-agent-harness/next.config.ts b/examples/harnesses/pi-agent-harness/next.config.ts index ba0c4e2a8..4051aca0c 100644 --- a/examples/harnesses/pi-agent-harness/next.config.ts +++ b/examples/harnesses/pi-agent-harness/next.config.ts @@ -14,8 +14,8 @@ const nextConfig: NextConfig = { serverExternalPackages: ["@earendil-works/pi-coding-agent"], webpack: (config, { isServer }) => { if (isServer) { - const externalizePi = ( - { request }: { request?: string }, + const externalize = ( + { context, request }: { context?: string; request?: string }, callback: (err?: null, result?: string) => void, ) => { if (request && /^@earendil-works\/pi-coding-agent(\/|$)/.test(request)) { @@ -25,11 +25,26 @@ const nextConfig: NextConfig = { // bundler never sees them once this entry point is external). return callback(null, `import ${request}`); } + // The chat route computes the OpenUI system prompt server-side from + // genui-lib. That module imports React hooks without a "use client" + // directive, which Next's route-handler (react-server) graph rejects at + // build time even though the route only calls the pure + // `openuiLibrary.prompt()`, never a renderer. Loading it as a runtime + // external skips that bundler check. Scoped to imports issued from the + // API routes so the page's SSR graph still bundles genui-lib against + // Next's own React copy. + if ( + request === "@openuidev/react-ui/genui-lib" && + context && + /[\\/]src[\\/]app[\\/]api[\\/]/.test(context) + ) { + return callback(null, `import ${request}`); + } return callback(); }; config.externals = Array.isArray(config.externals) - ? [externalizePi, ...config.externals] - : [externalizePi]; + ? [externalize, ...config.externals] + : [externalize]; } return config; }, diff --git a/examples/harnesses/pi-agent-harness/src/app/api/chat/route.ts b/examples/harnesses/pi-agent-harness/src/app/api/chat/route.ts index 56a04b677..305036769 100644 --- a/examples/harnesses/pi-agent-harness/src/app/api/chat/route.ts +++ b/examples/harnesses/pi-agent-harness/src/app/api/chat/route.ts @@ -1,4 +1,5 @@ import { abortSession, getOrCreateSession } from "@/lib/pi-session"; +import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui/genui-lib"; import type { NextRequest } from "next/server"; // The Pi SDK spawns bash, reads the filesystem, and talks to model providers — @@ -6,7 +7,14 @@ import type { NextRequest } from "next/server"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; +// OpenUI Lang instructions for the same component library the page renders with, +// computed server-side so the client doesn't have to ship the (large) prompt. +const defaultSystemPrompt = openuiLibrary.prompt(openuiPromptOptions); + interface ChatBody { + /** Per-thread id sent by fetchLLM; maps to a persistent pi AgentSession. */ + threadId?: string; + /** Optional override for the server-computed OpenUI system prompt. */ systemPrompt?: string; messages?: Array<{ role?: string; content?: unknown }>; } @@ -60,7 +68,10 @@ function extractText(content: unknown): string { export async function POST(req: NextRequest) { const body = (await req.json().catch(() => ({}))) as ChatBody; - const conversationId = req.headers.get("x-conversation-id") || crypto.randomUUID(); + // fetchLLM sends the thread id in the body; the `x-conversation-id` header is + // kept as a fallback for older/custom clients. + const conversationId = + body.threadId || req.headers.get("x-conversation-id") || crypto.randomUUID(); const cwd = process.env.PI_AGENT_CWD || process.cwd(); // The frontend re-sends the full thread, but Pi keeps its own transcript, so @@ -73,7 +84,7 @@ export async function POST(req: NextRequest) { try { const entry = await getOrCreateSession(conversationId, { cwd, - systemPrompt: body.systemPrompt, + systemPrompt: body.systemPrompt || defaultSystemPrompt, }); session = entry.session; modelFallbackMessage = entry.modelFallbackMessage; diff --git a/examples/harnesses/pi-agent-harness/src/app/page.tsx b/examples/harnesses/pi-agent-harness/src/app/page.tsx index e6ec006d6..76008f958 100644 --- a/examples/harnesses/pi-agent-harness/src/app/page.tsx +++ b/examples/harnesses/pi-agent-harness/src/app/page.tsx @@ -4,38 +4,26 @@ import "@openuidev/react-ui/styles/index.css"; import { AgentInterface, + fetchLLM, openAIMessageFormat, openAIReadableStreamAdapter, - type ChatLLM, } from "@openuidev/react-ui"; -import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui/genui-lib"; +import { openuiLibrary } from "@openuidev/react-ui/genui-lib"; import { useMemo } from "react"; -const systemPrompt = openuiLibrary.prompt(openuiPromptOptions); - export default function Home() { // AgentInterface uses its built-in in-memory storage default (wiped on reload). - // Each new thread gets a stable client-generated id, so the per-thread - // x-conversation-id maps to an isolated pi AgentSession. The backend call is - // unchanged; only the chat surface moved from FullScreen to AgentInterface. - const llm = useMemo( - () => ({ - send: ({ threadId, messages, signal }) => - fetch("/api/chat", { - method: "POST", - headers: { - "Content-Type": "application/json", - // Map each chat thread to its own persistent pi AgentSession. - "x-conversation-id": threadId, - }, - body: JSON.stringify({ - systemPrompt, - messages: openAIMessageFormat.toApi(messages), - }), - signal, - }), - streamProtocol: openAIReadableStreamAdapter(), - }), + // fetchLLM sends each thread's stable client-generated id as `threadId` in the + // request body, which the route maps to an isolated, persistent pi + // AgentSession. The OpenUI system prompt is computed server-side in the route + // from the same `openuiLibrary` this page renders with. + const llm = useMemo( + () => + fetchLLM({ + url: "/api/chat", + streamAdapter: openAIReadableStreamAdapter(), + messageFormat: openAIMessageFormat, + }), [], ); diff --git a/examples/harnesses/pi-agent-harness/src/lib/pi-session.ts b/examples/harnesses/pi-agent-harness/src/lib/pi-session.ts index a800e8ff8..6630df7b0 100644 --- a/examples/harnesses/pi-agent-harness/src/lib/pi-session.ts +++ b/examples/harnesses/pi-agent-harness/src/lib/pi-session.ts @@ -41,7 +41,7 @@ export interface PiSessionEntry { interface GetOrCreateOptions { /** Workspace the coding agent operates in. */ cwd: string; - /** OpenUI Lang system prompt (generated client-side from the component library). */ + /** OpenUI Lang system prompt (generated from the component library in the route). */ systemPrompt?: string; } diff --git a/examples/harnesses/vercel-eve/AGENTS.md b/examples/harnesses/vercel-eve/AGENTS.md index ef84069db..e2cdf6fcd 100644 --- a/examples/harnesses/vercel-eve/AGENTS.md +++ b/examples/harnesses/vercel-eve/AGENTS.md @@ -1,3 +1,7 @@ # eve Agent App This project uses the eve framework. Before writing code, always read the relevant guide in `node_modules/eve/docs/`. + +## OpenUI chat wiring + +The hand-written `ChatLLM` in `src/eve-chat.ts` is an intentional exception to the repo's fetchLLM-first convention: there is no HTTP chat endpoint here. Its `send` drives Eve's native session protocol (`POST /eve/v1/session`, resumable NDJSON event stream) and synthesizes the AG-UI SSE stream client-side from Eve session events, so `fetchLLM` does not apply — do not migrate it. Normal HTTP chat backends should use `fetchLLM` from `@openuidev/react-ui` instead of hand-writing a `ChatLLM`. diff --git a/examples/mastra-chat/src/app/page.tsx b/examples/mastra-chat/src/app/page.tsx index 8571dbdfe..58fd715a1 100644 --- a/examples/mastra-chat/src/app/page.tsx +++ b/examples/mastra-chat/src/app/page.tsx @@ -1,27 +1,22 @@ "use client"; import { useTheme } from "@/hooks/use-system-theme"; -import { AgentInterface, agUIAdapter, type ChatLLM } from "@openuidev/react-ui"; +import { AgentInterface, agUIAdapter, fetchLLM } from "@openuidev/react-ui"; import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib"; import { useMemo } from "react"; export default function Page() { const mode = useTheme(); - // The backend call is unchanged — only the chat surface moved from FullScreen - // to AgentInterface. Storage is omitted, so AgentInterface uses its built-in - // in-memory default (wiped on reload). - const llm = useMemo( - () => ({ - send: ({ messages, threadId, signal }) => - fetch("/api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ messages, threadId }), - signal, - }), - streamProtocol: agUIAdapter(), - }), + // fetchLLM's default message format POSTs { messages, threadId, ... } — + // exactly what the Mastra route expects. Storage is omitted, so + // AgentInterface uses its built-in in-memory default (wiped on reload). + const llm = useMemo( + () => + fetchLLM({ + url: "/api/chat", + streamAdapter: agUIAdapter(), + }), [], ); diff --git a/examples/material-ui-chat/src/app/page.tsx b/examples/material-ui-chat/src/app/page.tsx index 264e23e90..da8a4899e 100644 --- a/examples/material-ui-chat/src/app/page.tsx +++ b/examples/material-ui-chat/src/app/page.tsx @@ -5,12 +5,7 @@ import LightModeIcon from "@mui/icons-material/LightMode"; import Box from "@mui/material/Box"; import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; -import { - AgentInterface, - openAIAdapter, - openAIMessageFormat, - type ChatLLM, -} from "@openuidev/react-ui"; +import { AgentInterface, fetchLLM, openAIAdapter, openAIMessageFormat } from "@openuidev/react-ui"; import { useMemo } from "react"; import { useColorMode } from "@/hooks/use-system-theme"; @@ -19,17 +14,13 @@ import { muiChatLibrary } from "@/lib/mui-genui"; export default function Page() { const { mode, toggle } = useColorMode(); - const llm = useMemo( - () => ({ - send: ({ messages, signal }) => - fetch("/api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ messages: openAIMessageFormat.toApi(messages) }), - signal, - }), - streamProtocol: openAIAdapter(), - }), + const llm = useMemo( + () => + fetchLLM({ + url: "/api/chat", + streamAdapter: openAIAdapter(), + messageFormat: openAIMessageFormat, + }), [], ); diff --git a/examples/openui-chat/src/app/page.tsx b/examples/openui-chat/src/app/page.tsx index f3d7d3433..ad2913f65 100644 --- a/examples/openui-chat/src/app/page.tsx +++ b/examples/openui-chat/src/app/page.tsx @@ -1,32 +1,23 @@ "use client"; import { useTheme } from "@/hooks/use-system-theme"; -import { - AgentInterface, - openAIAdapter, - openAIMessageFormat, - type ChatLLM, -} from "@openuidev/react-ui"; +import { AgentInterface, fetchLLM, openAIAdapter, openAIMessageFormat } from "@openuidev/react-ui"; import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib"; import { useMemo } from "react"; export default function Page() { const mode = useTheme(); - // AgentInterface uses its built-in in-memory storage (wiped on reload). The - // backend call is unchanged — only the chat surface moved from FullScreen to - // AgentInterface. - const llm = useMemo( - () => ({ - send: ({ messages, signal }) => - fetch("/api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ messages: openAIMessageFormat.toApi(messages) }), - signal, - }), - streamProtocol: openAIAdapter(), - }), + // AgentInterface uses its built-in in-memory storage (wiped on reload). + // fetchLLM POSTs the run payload to /api/chat, sending messages in OpenAI + // format and parsing the OpenAI-style SSE response. + const llm = useMemo( + () => + fetchLLM({ + url: "/api/chat", + streamAdapter: openAIAdapter(), + messageFormat: openAIMessageFormat, + }), [], ); diff --git a/examples/react-email/package.json b/examples/react-email/package.json index b5d656cf9..9281e8e5d 100644 --- a/examples/react-email/package.json +++ b/examples/react-email/package.json @@ -11,9 +11,9 @@ }, "dependencies": { "@openuidev/cli": "workspace:*", - "@openuidev/react-email": "0.2.4", - "@openuidev/react-headless": "0.8.2", - "@openuidev/react-lang": "0.2.6", + "@openuidev/react-email": "workspace:*", + "@openuidev/react-headless": "workspace:*", + "@openuidev/react-lang": "workspace:*", "@react-email/components": "^0.0.41", "@react-email/render": "^1.0.6", "lucide-react": "^0.562.0", diff --git a/examples/react-email/src/app/page.tsx b/examples/react-email/src/app/page.tsx index baf037f3d..ffaa9552f 100644 --- a/examples/react-email/src/app/page.tsx +++ b/examples/react-email/src/app/page.tsx @@ -1,22 +1,17 @@ "use client"; -import { useCallback, useEffect, useRef, useState } from "react"; import { ChatProvider, + fetchLLM, openAIAdapter, openAIMessageFormat, useThread, } from "@openuidev/react-headless"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ComposePage } from "@/components/composePage"; import { EmailEditor } from "@/components/emailEditor"; -import { - saveView, - loadView, - saveMessages, - loadMessages, - clearSession, -} from "@/components/session"; +import { clearSession, loadMessages, loadView, saveMessages, saveView } from "@/components/session"; // ── Main App (manages view state) ── @@ -51,7 +46,7 @@ function EmailApp() { saveView("chat"); processMessage({ role: "user", content: message }); }, - [processMessage] + [processMessage], ); const handleNewEmail = useCallback(() => { @@ -70,21 +65,21 @@ function EmailApp() { // ── Page Root ── export default function Page() { + // fetchLLM POSTs the run payload to /api/chat, sending messages in OpenAI + // format and parsing the OpenAI-style SSE response. + const llm = useMemo( + () => + fetchLLM({ + url: "/api/chat", + streamAdapter: openAIAdapter(), + messageFormat: openAIMessageFormat, + }), + [], + ); + return (
- { - return fetch("/api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - messages: openAIMessageFormat.toApi(messages), - }), - signal: abortController.signal, - }); - }} - streamProtocol={openAIAdapter()} - > +
diff --git a/examples/shadcn-chat/src/app/page.tsx b/examples/shadcn-chat/src/app/page.tsx index 51561636b..42bec3125 100644 --- a/examples/shadcn-chat/src/app/page.tsx +++ b/examples/shadcn-chat/src/app/page.tsx @@ -2,30 +2,19 @@ import { useTheme } from "@/hooks/use-system-theme"; import { shadcnChatLibrary } from "@/lib/shadcn-genui"; -import { - AgentInterface, - openAIAdapter, - openAIMessageFormat, - type ChatLLM, -} from "@openuidev/react-ui"; +import { AgentInterface, fetchLLM, openAIAdapter, openAIMessageFormat } from "@openuidev/react-ui"; import { useMemo } from "react"; export default function Page() { const mode = useTheme(); - const llm = useMemo( - () => ({ - send: ({ messages, signal }) => - fetch("/api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - messages: openAIMessageFormat.toApi(messages), - }), - signal, - }), - streamProtocol: openAIAdapter(), - }), + const llm = useMemo( + () => + fetchLLM({ + url: "/api/chat", + streamAdapter: openAIAdapter(), + messageFormat: openAIMessageFormat, + }), [], ); diff --git a/examples/supabase-chat/src/app/page.tsx b/examples/supabase-chat/src/app/page.tsx index d51769e64..f12ffc413 100644 --- a/examples/supabase-chat/src/app/page.tsx +++ b/examples/supabase-chat/src/app/page.tsx @@ -3,10 +3,10 @@ import { createSupabaseBrowser } from "@/lib/supabase/browser"; import { AgentInterface, + fetchLLM, openAIAdapter, openAIMessageFormat, restStorage, - type ChatLLM, } from "@openuidev/react-ui"; import type { RealtimeChannel } from "@supabase/supabase-js"; import { useEffect, useMemo, useState } from "react"; @@ -24,21 +24,16 @@ export default function Page() { () => restStorage({ baseUrl: "/api/threads", messageFormat: openAIMessageFormat }), [], ); - const llm = useMemo( - () => ({ - send: ({ threadId, messages, signal }) => - fetch("/api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - // Convert from OpenUI's internal format to OpenAI chat format - messages: openAIMessageFormat.toApi(messages), - threadId, - }), - signal, - }), - streamProtocol: openAIAdapter(), - }), + // fetchLLM POSTs { threadId, runId, messages, tools, context }; the route + // reads the top-level threadId and messages (OpenAI chat format via + // openAIMessageFormat) and ignores the rest. + const llm = useMemo( + () => + fetchLLM({ + url: "/api/chat", + streamAdapter: openAIAdapter(), + messageFormat: openAIMessageFormat, + }), [], ); diff --git a/packages/openui-cli/src/templates/openui-self-hosted/README.md b/packages/openui-cli/src/templates/openui-self-hosted/README.md index 12f6d78af..512396765 100644 --- a/packages/openui-cli/src/templates/openui-self-hosted/README.md +++ b/packages/openui-cli/src/templates/openui-self-hosted/README.md @@ -16,8 +16,10 @@ bun dev Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -You can start editing the page by modifying `src/app/api/route.ts` and improving your agent -by adding system prompts or tools. +You can start editing the page by modifying `src/app/api/chat/route.ts` and improving your agent +by adding system prompts or tools. The OpenUI system prompt is generated into +`src/generated/system-prompt.txt` before every `dev` and `build` (or on demand with +`npm run generate:prompt`). ## Learn More diff --git a/packages/openui-cli/src/templates/openui-self-hosted/package.json b/packages/openui-cli/src/templates/openui-self-hosted/package.json index 71499dbd6..8892fad7d 100644 --- a/packages/openui-cli/src/templates/openui-self-hosted/package.json +++ b/packages/openui-cli/src/templates/openui-self-hosted/package.json @@ -3,8 +3,9 @@ "version": "0.1.1", "private": true, "scripts": { - "dev": "next dev", - "build": "next build", + "generate:prompt": "node scripts/generate-prompt.mjs", + "dev": "node scripts/generate-prompt.mjs && next dev", + "build": "node scripts/generate-prompt.mjs && next build", "start": "next start", "lint": "eslint" }, diff --git a/packages/openui-cli/src/templates/openui-self-hosted/scripts/generate-prompt.mjs b/packages/openui-cli/src/templates/openui-self-hosted/scripts/generate-prompt.mjs new file mode 100644 index 000000000..471426e74 --- /dev/null +++ b/packages/openui-cli/src/templates/openui-self-hosted/scripts/generate-prompt.mjs @@ -0,0 +1,20 @@ +// Writes the OpenUI system prompt to src/generated/system-prompt.txt so the +// /api/chat route can prepend it server-side. Runs automatically before +// `dev` and `build`; re-run by hand after customizing the component library: +// +// npm run generate:prompt +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui/genui-lib"; + +const outFile = resolve( + dirname(fileURLToPath(import.meta.url)), + "../src/generated/system-prompt.txt", +); + +const prompt = openuiLibrary.prompt(openuiPromptOptions); +await mkdir(dirname(outFile), { recursive: true }); +await writeFile(outFile, prompt, "utf8"); +console.log(`Wrote ${prompt.length} chars to ${outFile}`); diff --git a/packages/openui-cli/src/templates/openui-self-hosted/src/app/api/chat/route.ts b/packages/openui-cli/src/templates/openui-self-hosted/src/app/api/chat/route.ts index 7ee5e8b72..0dc098728 100644 --- a/packages/openui-cli/src/templates/openui-self-hosted/src/app/api/chat/route.ts +++ b/packages/openui-cli/src/templates/openui-self-hosted/src/app/api/chat/route.ts @@ -1,15 +1,24 @@ +import { readFileSync } from "fs"; import { NextRequest } from "next/server"; import OpenAI from "openai"; +import { join } from "path"; const client = new OpenAI(); +// Generated from the OpenUI component library by scripts/generate-prompt.mjs +// (runs before `dev` and `build`); body.systemPrompt (optional) overrides it. +const defaultSystemPrompt = readFileSync( + join(process.cwd(), "src/generated/system-prompt.txt"), + "utf-8", +); + export async function POST(req: NextRequest) { try { const { messages, systemPrompt } = await req.json(); const response = await client.chat.completions.create({ model: "gpt-5.2", - messages: [{ role: "system", content: systemPrompt }, ...messages], + messages: [{ role: "system", content: systemPrompt ?? defaultSystemPrompt }, ...messages], stream: true, }); diff --git a/packages/openui-cli/src/templates/openui-self-hosted/src/app/page.tsx b/packages/openui-cli/src/templates/openui-self-hosted/src/app/page.tsx index 20e22906c..97534b24c 100644 --- a/packages/openui-cli/src/templates/openui-self-hosted/src/app/page.tsx +++ b/packages/openui-cli/src/templates/openui-self-hosted/src/app/page.tsx @@ -3,28 +3,18 @@ import "@openuidev/react-ui/components.css"; import "@openuidev/react-ui/styles/index.css"; import { + fetchLLM, openAIMessageFormat, openAIReadableStreamAdapter, - type ChatLLM, } from "@openuidev/react-headless"; import { AgentInterface } from "@openuidev/react-ui"; -import { openuiLibrary, openuiPromptOptions } from "@openuidev/react-ui/genui-lib"; +import { openuiLibrary } from "@openuidev/react-ui/genui-lib"; -const systemPrompt = openuiLibrary.prompt(openuiPromptOptions); - -const llm: ChatLLM = { - send: async ({ messages, signal }) => - fetch("/api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - systemPrompt, - messages: openAIMessageFormat.toApi(messages), - }), - signal, - }), - streamProtocol: openAIReadableStreamAdapter(), -}; +const llm = fetchLLM({ + url: "/api/chat", + streamAdapter: openAIReadableStreamAdapter(), + messageFormat: openAIMessageFormat, +}); export default function Home() { return ( diff --git a/packages/react-headless/AGENTS.md b/packages/react-headless/AGENTS.md index dcdb9d58a..721424db1 100644 --- a/packages/react-headless/AGENTS.md +++ b/packages/react-headless/AGENTS.md @@ -4,70 +4,115 @@ ```bash # From monorepo root: -pnpm --filter @openuidev/react-headless run build # tsc → dist/ -pnpm --filter @openuidev/react-headless run test # vitest -pnpm --filter @openuidev/react-headless run ci # lint:check + format:check +pnpm --filter @openuidev/react-headless run build # tsdown → dist/ +pnpm --filter @openuidev/react-headless run test # vitest run +pnpm --filter @openuidev/react-headless run typecheck # tsc --noEmit +pnpm --filter @openuidev/react-headless run ci # lint:check + format:check # Or from this directory: pnpm build && pnpm test ``` -Build order: **`react-headless`** → `react-lang` → `react-ui`. This package has no upstream workspace deps (only `@ag-ui/core` from npm), so it can always build independently. +This package has no upstream workspace deps (runtime dep is only `@ag-ui/core`; `react` and `zustand` are peers), so it can always build independently. `react-ui` depends on it (`workspace:^`) — rebuild this package before verifying changes through `react-ui`. ## File Map -| Path | Purpose | -| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| `src/index.ts` | Public API surface — every export consumers see. Check here first when adding/removing exports. | -| `src/store/createChatStore.ts` | Zustand store factory. All state + actions live here. This is the most critical file. | -| `src/store/ChatProvider.tsx` | React provider — thin wrapper that creates the store once via `useState`. | -| `src/store/ChatContext.ts` | React context + `useChatStore()` internal hook. | -| `src/store/hooks.ts` | `useThread()` / `useThreadList()` — selector hooks over the store. | -| `src/store/types.ts` | All store types: `ChatStore`, `ChatProviderProps`, `Thread`, state/action slices. | -| `src/store/__tests__/createChatStore.test.ts` | Comprehensive test suite for the store (thread CRUD, message CRUD, streaming, cancellation, URL-based defaults, messageFormat round-trips). | -| `src/stream/processStreamedMessage.ts` | Consumes `AsyncIterable` and drives message create/update/delete callbacks. | -| `src/stream/adapters/ag-ui.ts` | Default SSE adapter — parses `data: {json}\n` lines. | -| `src/stream/adapters/openai-completions.ts` | Adapter for OpenAI Chat Completions streaming (`ChatCompletionChunk`). | -| `src/stream/adapters/openai-responses.ts` | Adapter for OpenAI Responses API streaming (`ResponseStreamEvent`). | -| `src/stream/adapters/openai-readable-stream.ts` | Adapter for OpenAI SDK's `Stream.toReadableStream()` — parses NDJSON (no SSE prefix) `ChatCompletionChunk` objects. | -| `src/stream/adapters/openai-message-format.ts` | `MessageFormat` for OpenAI Completions (`ChatCompletionMessageParam[]` ↔ AG-UI). | -| `src/stream/adapters/openai-conversation-message-format.ts` | `MessageFormat` for OpenAI Responses/Conversations API (`ResponseInputItem[]` ↔ AG-UI). | -| `src/types/` | Shared types: `message.ts` (re-exports from `@ag-ui/core`), `messageFormat.ts`, `stream.ts`. | -| `src/hooks/useMessage.tsx` | `MessageContext` / `MessageProvider` / `useMessage` — per-message React context used by `react-ui`. | +| Path | Purpose | +| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `src/index.ts` | Public API surface — every export consumers see. Check here first when adding/removing exports. | +| **Adapters — `src/adapters/`** | | +| `src/adapters/types.ts` | Adapter contracts: `ChatLLM`, `ChatStorage`, `ThreadStorage`, `ArtifactStorage`, plus `Artifact`/`ArtifactSummary`/`ArtifactCategory` types. | +| `src/adapters/fetchLLM.ts` | `fetchLLM({ url, streamAdapter, messageFormat?, headers?, fetch? })` → `ChatLLM`. POSTs an AG-UI `RunAgentInput`-shaped body to `url`. | +| `src/adapters/restStorage.ts` | `restStorage({ baseUrl, messageFormat?, headers?, fetch? })` → `ChatStorage` (thread channel only). REST conventions of the old `threadApiUrl`. | +| `src/adapters/_defaultStorage.ts` | In-memory `ChatStorage` used when `` gets no `storage`. Intentionally NOT exported. | +| **Chat store — `src/store/`** | | +| `src/store/createChatStore.ts` | Zustand store factory — thread-list + thread slices, `processMessage()` send path. This is the most critical file. | +| `src/store/ChatProvider.tsx` | React provider — creates the stores/registries once via lazy `useState` and nests six contexts. | +| `src/store/ChatContext.ts` | React context + `useChatStore()` internal hook. | +| `src/store/types.ts` | `ChatStore`, `ChatProviderProps`, `Thread`, `CreateMessage`, state/action slices. | +| `src/store/toolActivity.ts` | `pairToolActivity` / `partialJSONParse` — pairs tool calls with results into `ToolActivity[]` (status: streaming/executing/success/error). | +| **Artifact + detailed-view stores — `src/store/`** | | +| `src/store/createDetailedViewStore.ts` (+ `detailedViewTypes.ts`) | Detailed-view store — one active view id globally, portal target element. | +| `src/store/createThreadContextStore.ts` (+ `threadContextTypes.ts`) | Per-thread artifact registry (`ArtifactEntry` by id/version). Reset when the selected thread changes. | +| `src/store/ArtifactRenderersContext.ts` (+ `artifactRendererTypes.ts`) | Renderer registry — `defineArtifactRenderer`, lookup by `toolName` (live tool calls) or `type` (stored artifacts). | +| `src/store/ArtifactStorageContext.ts` | Context exposing `storage.artifact ?? null`. | +| `src/store/ArtifactCategoriesContext.ts` (+ `artifactCategories.ts`) | Categories context + `defineArtifactCategories` helper. | +| **Streaming — `src/stream/`** | | +| `src/stream/processStreamedMessage.ts` | Consumes `AsyncIterable` and drives message create/update callbacks (rAF-debounced) + tool-executing callbacks. | +| `src/stream/adapters/ag-ui.ts` | `agUIAdapter()` — AG-UI SSE (`data: {AGUIEvent}` lines). The default adapter in `processStreamedMessage`. | +| `src/stream/adapters/openai-completions.ts` | `openAIAdapter()` — OpenAI Chat Completions SSE (`ChatCompletionChunk`). | +| `src/stream/adapters/openai-responses.ts` | `openAIResponsesAdapter()` — OpenAI Responses API SSE (`ResponseStreamEvent`). | +| `src/stream/adapters/openai-readable-stream.ts` | `openAIReadableStreamAdapter()` — NDJSON from the OpenAI SDK's `Stream.toReadableStream()` (no SSE prefix). | +| `src/stream/adapters/langgraph.ts` | `langGraphAdapter(options?)` — LangGraph named-SSE events (`messages`/`updates`/`error`/`end`), incl. `onInterrupt` callback. | +| `src/stream/adapters/_shared/sseLines.ts` | `sseLineIterator` (`@internal`) — cross-chunk SSE line buffering shared by the `data:`-line adapters. | +| `src/stream/formats/openai-message-format.ts` | `openAIMessageFormat` — `ChatCompletionMessageParam[]` ↔ AG-UI. | +| `src/stream/formats/openai-conversation-message-format.ts` | `openAIConversationMessageFormat` — Responses/Conversations API items ↔ AG-UI. | +| `src/stream/formats/langgraph-message-format.ts` | `langGraphMessageFormat` — LangChain-style messages ↔ AG-UI. | +| **Hooks — `src/hooks/`** | | +| `src/hooks/useThread.ts` | `useThread()` / `useThreadList()` — selector hooks over the chat store (`threadSelector` / `threadListSelector` live here). | +| `src/hooks/useMessage.tsx` | `MessageContext` / `MessageProvider` / `useMessage` — per-message React context used by `react-ui`. | +| `src/hooks/useToolActivities.ts` | Memoized `pairToolActivity` → `ToolActivity[]` for a message. | +| `src/hooks/useDetailedView.ts` / `useActiveDetailedView.ts` / `useDetailedViewPortalTarget.ts` | Detailed-view open/close controls, global "any view open?", portal target. | +| `src/hooks/useArtifactList.ts` / `useArtifactRenderer.ts` | Active thread's artifacts grouped by id; renderer-registry lookup by tool name. | +| **Shared types — `src/types/`** | `message.ts` (re-exports from `@ag-ui/core`), `messageFormat.ts` (`MessageFormat` + `identityMessageFormat`), `stream.ts` (`StreamProtocolAdapter`, `AGUIEvent`/`EventType` re-exports). | ## Key Patterns +### ChatProviderProps + +```ts +interface ChatProviderProps { + llm: ChatLLM; // required — drives message sending and stream parsing + storage?: ChatStorage; // default: internal in-memory storage (no persistence) + artifactRenderers?: ReadonlyArray>; // captured at mount + artifactCategories?: ArtifactCategory[]; + children: React.ReactNode; +} +``` + +Typical wiring uses the built-in adapter factories: + +```tsx + +``` + +- `fetchLLM(...).send` POSTs `{ threadId, runId: crypto.randomUUID(), messages: messageFormat.toApi(messages), tools: [], context: [] }` and forwards the store's abort `signal`. `messageFormat` defaults to `identityMessageFormat`. +- `restStorage` reproduces the REST conventions of the removed `threadApiUrl` prop (`GET {base}/get`, `POST {base}/create`, `GET {base}/get/:id`, `PATCH {base}/update/:id`, `DELETE {base}/delete/:id`). Thread channel only — it provides no `storage.artifact`. +- **Stream adapters are factories** — pass `openAIAdapter()`, never the bare `openAIAdapter` function. + +### How streaming works + +`processMessage()` in the store → append optimistic `UserMessage` (and lazily `createThread` via storage when no thread is selected) → `llm.send({ threadId, messages, signal })` → a non-ok `Response` throws → `processStreamedMessage({ response, adapter: llm.streamProtocol, ... })` → `adapter.parse(response)` yields `AGUIEvent`s → one optimistic `AssistantMessage` accumulates text deltas and tool calls (`createMessage` on first event, rAF-debounced `updateMessage` after), `TOOL_CALL_RESULT` events upsert `ToolMessage`s by `toolCallId`, and `TOOL_CALL_END`/`TOOL_CALL_RESULT` drive the `executingToolCallIds` set. Errors become `threadError` unless the run was aborted (`cancelMessage()`). + ### Adding a new store action 1. Add the type to the appropriate slice in `src/store/types.ts` (`ThreadActions` or `ThreadListActions`). 2. Implement it in `createChatStore.ts` inside the `createStore(...)` call. -3. Add it to the selector in `src/store/hooks.ts` (`threadSelector` or `threadListSelector`). -4. Add tests in `src/store/__tests__/createChatStore.test.ts`. +3. Add it to the selector in `src/hooks/useThread.ts` (`threadSelector` or `threadListSelector`). +4. Add tests in `src/store/__tests__/createChatStore.test.ts` (use the `makeStore` helper). 5. If it should be public, export the type from `src/index.ts`. ### Adding a new stream adapter -1. Create `src/stream/adapters/my-adapter.ts` implementing `StreamProtocolAdapter` (must have `async *parse(response): AsyncIterable`). -2. Export it from `src/stream/adapters/index.ts`. -3. Export it from `src/index.ts`. +1. Create `src/stream/adapters/my-adapter.ts` exporting a **factory** `(options?) => StreamProtocolAdapter` (i.e. returns `{ async *parse(response): AsyncIterable }`). +2. For `data:`-line SSE input, use `sseLineIterator` from `src/stream/adapters/_shared/sseLines.ts` so events split across network chunks are not dropped. +3. `src/stream/adapters/index.ts` re-exports with `export *`, so exporting from your file is enough there; also add the named export to `src/index.ts`. +4. Add tests in `src/stream/adapters/__tests__/`. ### Adding a new message format -1. Create a file in `src/stream/adapters/` implementing the `MessageFormat` interface (`toApi` + `fromApi`). -2. Export it from `src/stream/adapters/index.ts` and `src/index.ts`. - -### ChatProviderProps config pattern - -`ChatProviderProps` uses a **discriminated union** so TypeScript enforces "URL string OR custom functions, not both": +1. Create a file in `src/stream/formats/` implementing the `MessageFormat` interface (`toApi` + `fromApi`, both array-to-array). +2. `src/stream/formats/index.ts` re-exports with `export *`; also add the named export to `src/index.ts`. +3. Message formats are consumed by both `fetchLLM` (outbound send) and `restStorage` (`createThread` outbound, `getMessages` inbound). -- `ThreadApiConfig`: provide `threadApiUrl` string **or** individual functions (`fetchThreadList`, `createThread`, etc.) -- `ChatApiConfig`: provide `apiUrl` string **or** a `processMessage` function - -When `threadApiUrl` is given, `createChatStore` generates default REST implementations (`/get`, `/create`, `/delete/:id`, `/update/:id`, `/get/:id`). - -### How streaming works +### Adding a new LLM or storage adapter -`processMessage()` in the store → `fetch(apiUrl)` → `StreamProtocolAdapter.parse(response)` → yields `AGUIEvent`s → `processStreamedMessage()` accumulates text deltas and tool calls into an `AssistantMessage`, calling `createMessage` on first event and `updateMessage` on subsequent events. +1. Create it in `src/adapters/`, returning a `ChatLLM` or `ChatStorage` (contracts in `src/adapters/types.ts`). +2. Export it (value + options type) from `src/adapters/index.ts` — explicit exports here, not `export *` — and from `src/index.ts`. +3. Add tests in `src/adapters/__tests__/`. ### OpenAI adapter types @@ -75,7 +120,24 @@ The `openai` package is a **devDependency** only (for type imports). It is not b ## Testing -Tests use Vitest and mock `fetch` via `vi.stubGlobal`. Streaming tests create `ReadableStream` instances with hand-crafted SSE payloads. Use `flushPromises()` (a `setTimeout(0)` wrapper) to await async store updates. +Tests use Vitest with no jsdom. Conventions: + +- **Store tests inject mocks — no global `fetch` stubbing.** `src/store/__tests__/__helpers/makeStore.ts` builds a chat store with a `vi.fn()`-mocked `ChatStorage` + `ChatLLM`; override any storage method or `send`/`streamProtocol` per test. +- **Stream-pipeline tests** build adapters from in-memory `AGUIEvent` arrays and stub `requestAnimationFrame` (the debouncer needs it). **Protocol-adapter tests** wrap hand-crafted SSE payloads in `new Response(new ReadableStream(...))`. +- **`restStorage` tests** pass a `vi.fn()` through the factory's `fetch` option. +- Await async store updates with `flushPromises()` (a `setTimeout(0)` wrapper defined per suite). + +| Suite | Covers | +| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| `src/store/__tests__/createChatStore.test.ts` | Thread CRUD, message CRUD, `processMessage` streaming, cancellation, select-while-streaming | +| `src/store/__tests__/toolActivity.test.ts` | `pairToolActivity` statuses, partial-JSON args parsing | +| `src/store/__tests__/artifactRendererRegistry.test.ts` | Renderer registry build/lookup, duplicate handling | +| `src/store/__tests__/createDetailedViewStore.test.ts` + `detailedViewThreadSwitch.test.ts` | Detailed-view store; reset on thread switch | +| `src/store/__tests__/createThreadContextStore.test.ts` + `threadContextSwitch.test.ts` | Per-thread artifact registry; reset on thread switch | +| `src/stream/__tests__/processStreamedMessage.test.ts` + `processStreamedMessageToolStatus.test.ts` | Event loop, `TOOL_CALL_RESULT` upserts, executing-status callbacks | +| `src/stream/adapters/__tests__/langgraph.test.ts` | LangGraph SSE → `AGUIEvent` mapping | +| `src/stream/adapters/__tests__/sseLines.test.ts` | Cross-chunk SSE line buffering | +| `src/adapters/__tests__/restStorage.test.ts` | REST endpoint conventions, `messageFormat`/header handling, error propagation | ```bash pnpm test # run all tests @@ -88,4 +150,6 @@ pnpm vitest run --reporter verbose # verbose output - **Store shape** — `ChatStore` is a flat Zustand store. Do not nest slices into sub-objects; hooks rely on the flat structure. - **`identityMessageFormat`** — this is the default no-op format. It must remain `{ toApi: (m) => m, fromApi: (d) => d as Message[] }`. - **`_abortController` / `_nextCursor`** — these are internal fields prefixed with `_`. Do not expose them in hooks or types. -- **`ChatProvider` store creation** — the store is created once via `useState(() => createChatStore(config))`. It intentionally does not react to config changes after mount. +- **`ChatProvider` mount-once creation** — the chat store, detailed-view store, thread-context store, renderer registry, and resolved storage are all created once via lazy `useState` initializers. Swapping `llm`/`storage`/`artifactRenderers` props after mount is intentionally ignored (dev-only warning for `artifactRenderers`). +- **`_defaultStorage` stays unexported** — it is internal to `ChatProvider` (see the comment in `src/adapters/index.ts`). +- **Adapter factories** — stream adapters (`agUIAdapter()`, etc.) are factory functions, not bare objects. Keep the factory signature; consumers already call them. diff --git a/packages/react-headless/README.md b/packages/react-headless/README.md index 61b172c8a..edd724806 100644 --- a/packages/react-headless/README.md +++ b/packages/react-headless/README.md @@ -1,12 +1,12 @@ # @openuidev/react-headless -Headless React state and streaming primitives for OpenUI chat experiences. Bring your own UI; this package handles threads, messages, adapters, and message format conversion. +Headless React state and streaming primitives for OpenUI chat experiences. Bring your own UI; this package handles threads, messages, LLM/storage adapters, and message format conversion. [![npm version](https://img.shields.io/npm/v/@openuidev/react-headless)](https://www.npmjs.com/package/@openuidev/react-headless) [![monthly downloads](https://img.shields.io/npm/dm/@openuidev/react-headless)](https://www.npmjs.com/package/@openuidev/react-headless) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/thesysdev/openui/blob/main/LICENSE) -**Links:** [Package docs](https://openui.com/docs/api-reference/react-headless) | [Chat docs](https://openui.com/docs/chat) | [GitHub repo](https://github.com/thesysdev/openui) +**Links:** [Package docs](https://openui.com/docs/api-reference/react-headless) | [Agent Interface docs](https://openui.com/docs/agent/getting-started/introduction) | [GitHub repo](https://github.com/thesysdev/openui) ## Install @@ -16,65 +16,99 @@ npm install @openuidev/react-headless pnpm add @openuidev/react-headless ``` -**Peer dependencies:** `react >=19.0.0`, `react-dom >=19.0.0`, `zustand ^4.5.5` +**Peer dependencies:** `react ^18.3.1 || ^19.0.0`, `zustand ^4.5.5` ## Overview Use `@openuidev/react-headless` when you want OpenUI's chat behavior without OpenUI's visual components: -- **`ChatProvider`** manages threads, messages, and streaming state through a Zustand store. +- **`ChatProvider`** manages threads, messages, and streaming state through a Zustand store. It is configured with two adapters: a required **`llm`** (how messages are sent and streamed back) and an optional **`storage`** (where threads persist). +- **`fetchLLM`** builds a `ChatLLM` for any HTTP endpoint that returns a streaming response; **`restStorage`** builds a `ChatStorage` over a REST thread API. - **Selector hooks** expose thread and thread-list state without coupling you to a layout. -- **Streaming adapters** parse SSE or SDK responses from OpenAI, AG-UI, or custom backends. -- **Message formats** convert between your API shape and OpenUI's internal AG-UI shape. +- **Streaming adapters** parse SSE or SDK responses from AG-UI, OpenAI, LangGraph, or custom backends. +- **Message formats** convert between your API's wire shape and OpenUI's internal AG-UI shape. ## Quick Start -### URL-based setup +`fetchLLM` is the standard transport. Point it at your streaming endpoint, pick a stream adapter that matches what the endpoint emits, and pass the resulting `ChatLLM` to `ChatProvider`: -The simplest configuration points to your API and lets the provider handle REST calls and streaming automatically: +```tsx +import { agUIAdapter, ChatProvider, fetchLLM } from "@openuidev/react-headless"; + +const llm = fetchLLM({ + url: "/api/chat", + streamAdapter: agUIAdapter(), +}); + +function App() { + return ( + + + + ); +} +``` + +On each send, `fetchLLM` POSTs `{ threadId, runId, messages, tools: [], context: [] }` (an AG-UI `RunAgentInput`-shaped body) to `url` and hands the streaming `Response` to `streamAdapter` for parsing. + +| Option | Description | +| :--- | :--- | +| `url` | Endpoint that accepts the POSTed body and returns a streaming `Response` | +| `streamAdapter` | Stream protocol adapter for the response body, e.g. `agUIAdapter()` — see [Streaming Adapters](#streaming-adapters) | +| `messageFormat` | Optional wire-format conversion for outgoing messages; defaults to identity (canonical AG-UI messages) | +| `headers` | Optional extra headers merged into the request | +| `fetch` | Optional `fetch` override (tests, custom auth wrappers) | + +Without `storage`, threads live in memory and are wiped on reload. + +### Persistence with `restStorage` + +To persist threads, add a `storage` adapter. `restStorage` covers the common REST case: ```tsx -import { ChatProvider } from "@openuidev/react-headless"; +import { agUIAdapter, ChatProvider, fetchLLM, restStorage } from "@openuidev/react-headless"; + +const llm = fetchLLM({ url: "/api/chat", streamAdapter: agUIAdapter() }); +const storage = restStorage({ baseUrl: "/api/threads" }); function App() { return ( - + ); } ``` -### Custom functions +`restStorage` implements the thread channel over these conventions: + +| Operation | Request | +| :--- | :--- | +| List threads | `GET {baseUrl}/get` (paginate with `?cursor=`) | +| Create thread | `POST {baseUrl}/create` | +| Get messages | `GET {baseUrl}/get/{threadId}` | +| Update thread | `PATCH {baseUrl}/update/{threadId}` | +| Delete thread | `DELETE {baseUrl}/delete/{threadId}` | + +Like `fetchLLM`, it accepts optional `messageFormat`, `headers`, and `fetch` options. -For full control, provide your own functions instead of URLs: +### Custom adapters + +When those conventions don't fit your backend, implement the `ChatLLM` (and/or `ChatStorage`) interface directly: ```tsx - { - return fetch("/api/chat", { - method: "POST", - body: JSON.stringify({ threadId, messages }), - signal: abortController.signal, - }); - }} - fetchThreadList={async () => { - const res = await fetch("/api/threads"); - return res.json(); - }} - createThread={async (firstMessage) => { - const res = await fetch("/api/threads", { +import { openAIAdapter, openAIMessageFormat, type ChatLLM } from "@openuidev/react-headless"; + +const llm: ChatLLM = { + send: ({ threadId, messages, signal }) => + fetch("/api/chat", { method: "POST", - body: JSON.stringify({ message: firstMessage }), - }); - return res.json(); - }} -> - - + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ threadId, messages: openAIMessageFormat.toApi(messages) }), + signal, + }), + streamProtocol: openAIAdapter(), +}; ``` ## Hooks @@ -104,7 +138,7 @@ function ChatMessages() { } ``` -**Returns:** `ThreadState & ThreadActions` +**Returns:** `ThreadState & ThreadActions`. Pass an optional selector to subscribe to a slice, e.g. `useThread((s) => s.processMessage)`. | Field | Type | Description | | :--- | :--- | :--- | @@ -112,6 +146,7 @@ function ChatMessages() { | `isRunning` | `boolean` | Whether the model is currently streaming | | `isLoadingMessages` | `boolean` | Whether messages are being fetched | | `threadError` | `Error \| null` | Error from the last operation | +| `executingToolCallIds` | `Set` | Tool calls whose arguments have closed but whose result hasn't arrived yet | | `processMessage(msg)` | `(msg) => Promise` | Send a message and stream the response | | `cancelMessage()` | `() => void` | Abort the current stream | | `appendMessages(...msgs)` | `(...msgs) => void` | Append messages locally | @@ -144,13 +179,14 @@ function ThreadSidebar() { } ``` -**Returns:** `ThreadListState & ThreadListActions` +**Returns:** `ThreadListState & ThreadListActions`. Also accepts an optional selector, like `useThread`. | Field | Type | Description | | :--- | :--- | :--- | | `threads` | `Thread[]` | All loaded threads | | `selectedThreadId` | `string \| null` | Currently selected thread | | `isLoadingThreads` | `boolean` | Whether the thread list is loading | +| `threadListError` | `Error \| null` | Error from the last thread-list operation | | `hasMoreThreads` | `boolean` | Whether more threads can be loaded | | `loadThreads()` | `() => void` | Fetch the thread list | | `loadMoreThreads()` | `() => void` | Load the next page of threads | @@ -173,24 +209,27 @@ function MessageBubble() { } ``` +### Other hooks + +The package also exports hooks for tool calls, artifacts, and detailed views: `useToolActivities`, `useArtifactList`, `useArtifactRenderer`, `useArtifactStorage`, `useDetailedView`, `useActiveDetailedView`, and `useDetailedViewPortalTarget`. See the [hooks reference](https://openui.com/docs/agent/reference/hooks). + ## Streaming Adapters -Adapters transform HTTP responses into the internal event stream. Pass one to `ChatProvider` via `streamProtocol`: +Adapters transform HTTP responses into the internal AG-UI event stream. They are **factories — call them** and pass the result to `fetchLLM` via `streamAdapter` (or set it as `streamProtocol` on a hand-written `ChatLLM`): ```tsx -import { ChatProvider, openAIAdapter } from "@openuidev/react-headless"; +import { fetchLLM, openAIAdapter } from "@openuidev/react-headless"; - - {children} - +const llm = fetchLLM({ url: "/api/chat", streamAdapter: openAIAdapter() }); ``` | Adapter | Description | | :--- | :--- | -| `agUIAdapter` | Default adapter for AG-UI SSE events (`data: {json}\n`) | -| `openAIAdapter` | Parses OpenAI Chat Completions streaming (`ChatCompletionChunk`) | -| `openAIResponsesAdapter` | Parses OpenAI Responses API streaming (`ResponseStreamEvent`) | -| `openAIReadableStreamAdapter` | Parses OpenAI SDK's `Stream.toReadableStream()` NDJSON output | +| `agUIAdapter()` | Parses AG-UI SSE events (`data: {json}\n`) | +| `openAIAdapter()` | Parses OpenAI Chat Completions streaming (`ChatCompletionChunk`) | +| `openAIResponsesAdapter()` | Parses OpenAI Responses API streaming (`ResponseStreamEvent`) | +| `openAIReadableStreamAdapter()` | Parses OpenAI SDK's `Stream.toReadableStream()` NDJSON output | +| `langGraphAdapter(options?)` | Parses LangGraph named SSE events (`messages`, `updates`, `error`); `options.onInterrupt` handles interrupts | ### Custom adapter @@ -208,14 +247,16 @@ const myAdapter: StreamProtocolAdapter = { ## Message Formats -Message formats convert between your API's message shape and the internal AG-UI format. Pass one to `ChatProvider` via `messageFormat`: +Message formats convert between your API's message shape and the internal AG-UI format. Unlike stream adapters they are plain objects, not factories. Pass one to `fetchLLM` or `restStorage` via the `messageFormat` option: ```tsx -import { ChatProvider, openAIMessageFormat } from "@openuidev/react-headless"; +import { fetchLLM, openAIAdapter, openAIMessageFormat } from "@openuidev/react-headless"; - - {children} - +const llm = fetchLLM({ + url: "/api/chat", + streamAdapter: openAIAdapter(), + messageFormat: openAIMessageFormat, +}); ``` | Format | Description | @@ -223,6 +264,7 @@ import { ChatProvider, openAIMessageFormat } from "@openuidev/react-headless"; | `identityMessageFormat` | Default format when messages are already AG-UI shaped | | `openAIMessageFormat` | Converts to/from OpenAI `ChatCompletionMessageParam[]` | | `openAIConversationMessageFormat` | Converts to/from OpenAI Responses API `ResponseInputItem[]` | +| `langGraphMessageFormat` | Converts to/from LangChain/LangGraph message dicts | ### Custom format @@ -242,6 +284,11 @@ const myFormat: MessageFormat = { ```ts import type { ChatProviderProps, + ChatLLM, + ChatStorage, + ThreadStorage, + FetchLLMOptions, + RestStorageOptions, ChatStore, Thread, ThreadState, @@ -266,7 +313,8 @@ import type { ## Documentation - [React Headless API reference](https://openui.com/docs/api-reference/react-headless) -- [Chat guides](https://openui.com/docs/chat) +- [Agent Interface guides](https://openui.com/docs/agent/getting-started/introduction) +- [Adapters and formats reference](https://openui.com/docs/agent/reference/adapters-and-formats) - [Source on GitHub](https://github.com/thesysdev/openui/tree/main/packages/react-headless) ## License diff --git a/packages/react-ui/README.md b/packages/react-ui/README.md index 544875ec7..38e523937 100644 --- a/packages/react-ui/README.md +++ b/packages/react-ui/README.md @@ -6,7 +6,7 @@ React components and a ready-made chat surface for OpenUI. Use the `AgentInterfa [![monthly downloads](https://img.shields.io/npm/dm/@openuidev/react-ui)](https://www.npmjs.com/package/@openuidev/react-ui) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/thesysdev/openui/blob/main/LICENSE) -**Links:** [Package docs](https://openui.com/docs/api-reference/react-ui) | [Chat docs](https://openui.com/docs/chat) | [GitHub repo](https://github.com/thesysdev/openui) +**Links:** [Package docs](https://openui.com/docs/api-reference/react-ui) | [Agent Interface docs](https://openui.com/docs/agent/getting-started/introduction) | [GitHub repo](https://github.com/thesysdev/openui) ## Install @@ -16,12 +16,12 @@ npm install @openuidev/react-ui @openuidev/react-lang @openuidev/react-headless pnpm add @openuidev/react-ui @openuidev/react-lang @openuidev/react-headless ``` -**Peer dependencies:** `react >=19.0.0`, `react-dom >=19.0.0`, `zustand ^4.5.5`, `@openuidev/react-lang`, `@openuidev/react-headless` +**Peer dependencies:** `react ^18.3.1 || ^19.0.0`, `react-dom ^18.0.0 || ^19.0.0`, `zustand ^4.5.5`, `zod ^3.25.0 || ^4.0.0`, `@openuidev/react-lang`, `@openuidev/react-headless` -Don't forget to import the component styles: +Don't forget to import the component styles (once, app-wide): ```ts -import "@openuidev/react-ui/styles/index.css"; +import "@openuidev/react-ui/components.css"; ``` ## Overview @@ -34,28 +34,21 @@ This package provides three layers: ## Quick Start -The fastest way to get a working chat app is `AgentInterface`. Give it an `llm` transport that talks to your backend. Storage is optional — without it, threads live in memory and are wiped on reload: +The fastest way to get a working chat app is `AgentInterface`. Give it an `llm` transport built with `fetchLLM`: point it at your streaming endpoint, pick the stream adapter that matches what the endpoint emits (adapters are factories — call them), and optionally a message format for the wire shape. Storage is optional — without it, threads live in memory and are wiped on reload: ```tsx -import { - AgentInterface, - openAIAdapter, - openAIMessageFormat, - type ChatLLM, -} from "@openuidev/react-ui"; +import "@openuidev/react-ui/components.css"; + +import { AgentInterface, fetchLLM, openAIAdapter, openAIMessageFormat } from "@openuidev/react-ui"; import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib"; -import "@openuidev/react-ui/styles/index.css"; - -const llm: ChatLLM = { - send: ({ messages, signal }) => - fetch("/api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ messages: openAIMessageFormat.toApi(messages) }), - signal, - }), - streamProtocol: openAIAdapter(), -}; + +// POSTs { threadId, runId, messages, tools, context } to /api/chat and parses +// the streamed response — here an OpenAI Chat Completions stream. +const llm = fetchLLM({ + url: "/api/chat", + streamAdapter: openAIAdapter(), + messageFormat: openAIMessageFormat, +}); function App() { return ( @@ -69,20 +62,22 @@ function App() { } ``` +If your endpoint streams AG-UI events, use `agUIAdapter()` and drop `messageFormat` (messages are sent as-is). For backends that don't fit `fetchLLM`'s conventions, implement the `ChatLLM` interface directly — see the [react-headless README](https://github.com/thesysdev/openui/tree/main/packages/react-headless). + ### The chat surface `AgentInterface` is the single chat surface. It adapts its layout responsively and accepts: | Prop | Description | | :----------------- | :------------------------------------------------------------------------------------------- | -| `storage` | Optional persistence adapter for thread history; defaults to in-memory (wiped on reload). Use `restStorage` from `@openuidev/react-ui` to back it with your own REST API | -| `llm` | Chat transport: `{ send({ messages, signal }), streamProtocol }` | +| `storage` | Optional persistence adapter for thread history; defaults to in-memory (wiped on reload). Use `restStorage({ baseUrl })` from `@openuidev/react-ui` to back it with your own REST API | +| `llm` | Chat transport, usually built with `fetchLLM`; any `ChatLLM` (`{ send({ threadId, messages, signal }), streamProtocol }`) works | | `componentLibrary` | OpenUI Lang library used to render assistant messages (e.g. `openuiChatLibrary`) | | `theme` | Theme configuration, e.g. `{ mode: "light" }` | | `agentName` | Name displayed in the header | | `starters` | Conversation-starter prompts shown on the welcome screen | -See the [chat docs](https://openui.com/docs/chat) for full configuration. +See the [AgentInterface props reference](https://openui.com/docs/agent/reference/agentinterface-props) for full configuration. ## Built-in Component Libraries @@ -148,9 +143,11 @@ OpenUI ships its component styles in two variants: | Import | Cascade behavior | | ------------------------------------------------------- | ------------------------------------------------------------------------- | -| `@openuidev/react-ui/styles/index.css` (default) | Unlayered — override via normal CSS specificity, as in 0.11.x and earlier | +| `@openuidev/react-ui/components.css` (default) | Unlayered — override via normal CSS specificity, as in 0.11.x and earlier | | `@openuidev/react-ui/layered/styles/index.css` (opt-in) | Wrapped in `@layer openui` — any unlayered consumer CSS wins | +`@openuidev/react-ui/styles/index.css` is an identical alias of `components.css` — import one or the other, never both. + Need a single component's CSS? Import it per component: `./styles/.css` (unlayered) or `./layered/styles/.css` (layered). With the layered variant, plain CSS overrides OpenUI without `!important` or specificity matching: @@ -215,7 +212,8 @@ import { Charts } from "@openuidev/react-ui/Charts"; | Import path | Description | | :--------------------------------------------- | :--------------------------------------------------- | | `@openuidev/react-ui` | All components and libraries | -| `@openuidev/react-ui/styles/index.css` | Full compiled stylesheet, unlayered (default import) | +| `@openuidev/react-ui/components.css` | Full compiled stylesheet, unlayered (default import) | +| `@openuidev/react-ui/styles/index.css` | Identical alias of `components.css` | | `@openuidev/react-ui/layered/styles/index.css` | Full stylesheet wrapped in `@layer openui` (opt-in) | | `@openuidev/react-ui/defaults.css` | Theme tokens, always unlayered | | `@openuidev/react-ui/genui-lib` | OpenUI Lang libraries and prompt options | @@ -227,7 +225,7 @@ import { Charts } from "@openuidev/react-ui/Charts"; ## Documentation - [React UI API reference](https://openui.com/docs/api-reference/react-ui) -- [Chat guides](https://openui.com/docs/chat) +- [Agent Interface guides](https://openui.com/docs/agent/getting-started/introduction) - [Source on GitHub](https://github.com/thesysdev/openui/tree/main/packages/react-ui) ## License diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 914e86164..4f840246c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -649,7 +649,7 @@ importers: version: 0.0.53 '@ag-ui/mastra': specifier: ^1.0.1 - version: 1.0.2(15ef11d9d782f272943f408ae4ae9137) + version: 1.0.2(0254d4c65bff74a4da757165177341fa) '@mastra/core': specifier: 1.15.0 version: 1.15.0(@cfworker/json-schema@4.1.1)(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@1.0.0)(typebox@1.1.38)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@1.0.0)(typebox@1.1.38)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.1.38)(zod@4.3.6))(@types/json-schema@7.0.15)(openapi-types@12.1.3)(zod@4.3.6) @@ -1165,14 +1165,14 @@ importers: specifier: workspace:* version: link:../../packages/openui-cli '@openuidev/react-email': - specifier: 0.2.4 - version: 0.2.4(@openuidev/react-lang@0.2.6(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(react@19.2.3)(zod@4.3.6))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.6) + specifier: workspace:* + version: link:../../packages/react-email '@openuidev/react-headless': - specifier: 0.8.2 - version: 0.8.2(react@19.2.3)(zustand@4.5.7(@types/react@19.2.14)(react@19.2.3)) + specifier: workspace:* + version: link:../../packages/react-headless '@openuidev/react-lang': - specifier: 0.2.6 - version: 0.2.6(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(react@19.2.3)(zod@4.3.6) + specifier: workspace:* + version: link:../../packages/react-lang '@react-email/components': specifier: ^0.0.41 version: 0.0.41(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -2070,9 +2070,6 @@ packages: '@ag-ui/client@0.0.49': resolution: {integrity: sha512-G6LTsdULw91sqLBeqW6VBqQjdrMJ0d91gtNr7mvaVVEoQX5pM5qVt7cf61ZNh6uv3UIwEG4oLSwH+v9unVzbtg==} - '@ag-ui/core@0.0.45': - resolution: {integrity: sha512-Ccsarxb23TChONOWXDbNBqp1fIbOSMht8g7w6AsSYBTtdOwZ7h7AkjNkr3LSdVv+RbT30JMdSLtieJE0YepNPg==} - '@ag-ui/core@0.0.46': resolution: {integrity: sha512-5/gC9n20ImA10LMFLLYKOowqn2Btrr3UYXWGosmLc1+KJqREI0t35NXnwqoKlw7TWySznF1bpwY6uIvMtO/ZUg==} @@ -4267,105 +4264,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -4790,35 +4771,30 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@mariozechner/clipboard-linux-arm64-musl@0.3.9': resolution: {integrity: sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': resolution: {integrity: sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - libc: [glibc] '@mariozechner/clipboard-linux-x64-gnu@0.3.9': resolution: {integrity: sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@mariozechner/clipboard-linux-x64-musl@0.3.9': resolution: {integrity: sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': resolution: {integrity: sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==} @@ -5170,112 +5146,96 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-gnu@16.1.6': resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-gnu@16.2.6': resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-gnu@16.2.7': resolution: {integrity: sha512-Re6PZtjBDd0aMU+VcZcC/PrIvj4WhrjDYtMhhCVQamWN4L90EVP0pcEOBQD25prSlw7OzNw5QpHLWMilRLsRNw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@15.5.18': resolution: {integrity: sha512-glaCczEWIrHsokFZ3pP08U4BpKxwIdnT+txdOM32OBgpL9Yw4aqx8NejmgtZQZOdstQ5f0L3CasIZudzCuD+nw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-arm64-musl@16.1.6': resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-arm64-musl@16.2.6': resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-arm64-musl@16.2.7': resolution: {integrity: sha512-qyogG9QtBzWxgJfeGBvOEHI3851gTfCF3wLZ5RDLTBJGAmE9p1qDwKCOdrBrvBzRvYDT+gUDp72pzlSEfAXgNA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@15.5.18': resolution: {integrity: sha512-oUfg2EgJmU3R0OCOWiokGFUTvZiPfXtriXiuF3YNxRoROCdgvTedHIzYoeKH34gsZxS/V7mHbfq2hpAHwhH1/A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-gnu@16.1.6': resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-gnu@16.2.6': resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-gnu@16.2.7': resolution: {integrity: sha512-Vhe4ZDuBpmMogrGi5D4R2Kq4JAQlj6+wvgaFYy31zfES0zPmt6TLA+cuYpM/OLrPZjo2MYQTHVqNUSCR6+fDZQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@15.5.18': resolution: {integrity: sha512-JLxSP3KTd9iu/bvUMQxH7RJo9xKSHf55/6RPE4a6FTSZygGn7uvZbCej0AHXydwkggQGSD9UddSjwv6Xz5ESfA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-linux-x64-musl@16.1.6': resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-linux-x64-musl@16.2.6': resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-linux-x64-musl@16.2.7': resolution: {integrity: sha512-srvian89JahFLw1YLBEuhvPJ0DO5lpUeJQMXy4xYo7g628ZlNgXdNkqoxSAv9OYrBfByh6vxISMwW/mRbzCY+g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@15.5.18': resolution: {integrity: sha512-ir1v7enP52K2HNz3tQQvwF+x7VNxBk1ciiZ18WBPvxf4C59IqdfmHPJYK3vH7rSxpuCVw/8C712wTXNAtEp+NA==} @@ -5715,44 +5675,11 @@ packages: resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} engines: {node: '>=14'} - '@openuidev/lang-core@0.2.6': - resolution: {integrity: sha512-a/WkffvnclxSYBGhJxho8mvaVDYCBNw9GBMP5J2tl17mK6ukA6YmSQvwSmlC0UNi3FiLdj6Wk6qJIjESZ8EOUg==} - peerDependencies: - '@modelcontextprotocol/sdk': '>=1.0.0' - zod: ^3.25.0 || ^4.0.0 - peerDependenciesMeta: - '@modelcontextprotocol/sdk': - optional: true - - '@openuidev/react-email@0.2.4': - resolution: {integrity: sha512-n8z4Zv3Df/cXksMwQo9RNL1aeC/M0REQclvp44TGfhiMiCi1mvWlYZ3uX5qzrerIwHBS+m7NUdI6LugCpFeiaQ==} - peerDependencies: - '@openuidev/react-lang': 0.2.6 - react: ^18.3.1 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - zod: ^3.25.0 || ^4.0.0 - - '@openuidev/react-headless@0.8.2': - resolution: {integrity: sha512-o70dZnwVli25JWYFFZBEtXpCVrdJAkQRoW1/La58C6jVvga/Q2KTnz+rXCLUHX/e9iZpHQ9doZZht+GZ9vRfjA==} - peerDependencies: - react: ^18.3.1 || ^19.0.0 - zustand: ^4.5.5 - '@openuidev/react-lang@0.1.3': resolution: {integrity: sha512-+/sVNVOFFzmnAsR8y+5W/la5U485OGXPFNtgY7wu0XdBdKSkbljNUGBA1XiHkzcxSVoATWOSy9eQ/J4UeHSPuQ==} peerDependencies: react: '>=19.0.0' - '@openuidev/react-lang@0.2.6': - resolution: {integrity: sha512-5lfnMUiUyTQli0fPFwQtUhxV0B5T/PJntgZzUb0IXiJSyWm+FNpYOIozJxim0xxmqmuYwmk1geLTNcjzYIS6mA==} - peerDependencies: - '@modelcontextprotocol/sdk': '>=1.0.0' - react: ^18.3.1 || ^19.0.0 - zod: ^3.25.0 || ^4.0.0 - peerDependenciesMeta: - '@modelcontextprotocol/sdk': - optional: true - '@openuidev/thesys-server@0.1.1': resolution: {integrity: sha512-8lPyh01/j7NYKIXO3h/VpDW+dF8Ii/QuwogAmxpVeh33VsPweot5yxqkvw+4TOrsYTXJSGm+DvcGMEzytBKP6A==} @@ -5820,56 +5747,48 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-minify/binding-linux-arm64-musl@0.131.0': resolution: {integrity: sha512-HGzqTov5sAzXyaNfRkQEpl0fRs+PrMYjT8b5jZAw8foQ/qnW+VMWgAr80Q+2j79T5nhXfboSF5SUgB8mcisgHw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-minify/binding-linux-ppc64-gnu@0.131.0': resolution: {integrity: sha512-zpUZ4pmbDBqaZmRYacxeLHUBxA3fs5K7hi1WSXRVMXC4OjWuVcLsNxeavenKF9i0YtP7Q5n2z12Rz7eEnNWoDA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxc-minify/binding-linux-riscv64-gnu@0.131.0': resolution: {integrity: sha512-CYrC4tpW1wolbw/Fox+T0hxW92s1aG/WLi+htkk02JMiCHOWqGQKxUnm37lLiODKR/OwTYht3LB4xNrsS0RtCg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-minify/binding-linux-riscv64-musl@0.131.0': resolution: {integrity: sha512-ZQNur0zujUjNYgjFF4mcNaeEKWuerY9XkaALYtBsHqNetkj55w0ZwCKYfYKLH2JAdyNF2LuS0s7VGgjXP9EvWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [musl] '@oxc-minify/binding-linux-s390x-gnu@0.131.0': resolution: {integrity: sha512-tR8oiFSNpcS1mfGY1N3/Hy6TxP2wr5X9FFdn/y8GarN8ST/JMLY5SUiwPiU35NKiC69CDaAsLHXoIKUxK/r8Pw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-minify/binding-linux-x64-gnu@0.131.0': resolution: {integrity: sha512-KodzbW12zmT/C/w4bGv2aWN7Q5+KVJKbNoAv5hooYeSujj8xSPGWl8pnyj7dJ9nd8j0CVjubEvHQ86rtzV99OA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-minify/binding-linux-x64-musl@0.131.0': resolution: {integrity: sha512-CNG3/hPE6MxdLikfLq5l0aZMvJ3W5AP1aoVjzQ1Itokv5sbfBcW0fp6Srn8mB86CyAqO9e7dbffZVOWBDVkhgw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@oxc-minify/binding-openharmony-arm64@0.131.0': resolution: {integrity: sha512-UyfimTwMLitJ0+5i5fL9M9U4E+DcIQJpGZWbVxxD3Mp9f7CTyQBIHnS68VEGZe+KQL/Y3IIb3AJ7cZB+ICgTVQ==} @@ -5947,56 +5866,48 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-arm64-musl@0.131.0': resolution: {integrity: sha512-vtPiwmfVTAXzaxDKsOXG+LwgRAA7WEnaeHzhS5z0GE89gAK18KSXnly7Z6saXXq6L3dVMyK44uoTI03zKxrpmw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-parser/binding-linux-ppc64-gnu@0.131.0': resolution: {integrity: sha512-8AW8L7w5cGHSdZPcyZX2yR0+GUODsT15rbRjfdD54rv6DMbtuEB19ysLOpKJlRGfH6UNYNpCHaU1uJWgTWf1/w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-riscv64-gnu@0.131.0': resolution: {integrity: sha512-vvpjkjEOUsPcsYf8evE4MO3aGx9+3wodXEBOicGNnOwTuAik8eBONNkgSdhkGsAblQmfVHJyanRnpxglddTXIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-riscv64-musl@0.131.0': resolution: {integrity: sha512-AqmcNC3fClXX+fxQ6VGEN1667xVFiRBkY0CZmDMSiaeFUsv1+UkBPYYi48IUKcA9/ivvoKNRzQl2I4//kT9F/w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [musl] '@oxc-parser/binding-linux-s390x-gnu@0.131.0': resolution: {integrity: sha512-7d3jOMKy7RSQCcDLIci+ySll2FgsOMl/GiRux4q2JNv0zg4EdhFISa9idvrdN/HEUIQQJNg6dmveUeJl2YErGA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-x64-gnu@0.131.0': resolution: {integrity: sha512-JHK/h95qVqVQ+ITER837kcTdwBDFpFaNnOTYGCP0zdUSX/mLKC7tXOoyrTb6vG7iRPwGlcgBil3v2IjYw1FqJA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-x64-musl@0.131.0': resolution: {integrity: sha512-b2BO82O8azXAyf7EUgOPKu145nWypbNyk07HbU09fkzhm9lEA5oPvaN/M8Nlo7tOErVTa2WOgS4QbOnxAPXdDQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@oxc-parser/binding-openharmony-arm64@0.131.0': resolution: {integrity: sha512-GHO9glZaX7LkX/OGfluEPf1yjg+ehiFbUdowbX6uNWOQhmwKWU4m4+nZ9FJkrHNKuxyI1KKertMdGjVKCApKWA==} @@ -6083,56 +5994,48 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-transform/binding-linux-arm64-musl@0.131.0': resolution: {integrity: sha512-cW0Ab1s0sxfiyP1+gdd94f0vUjwGzJF4F3DepF3VnR9nFTGMmFLugwtrBS3DYjTnbugiUH3Fp+16yys1FhNzIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-transform/binding-linux-ppc64-gnu@0.131.0': resolution: {integrity: sha512-wunAU/lzE1nPGKL47uI0g+4Nsv/12xveOXNu4M70xe85kNBm7mQdMpZIeoVYCxtXew0iHxFKJDT6qK5mYFSA3w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxc-transform/binding-linux-riscv64-gnu@0.131.0': resolution: {integrity: sha512-r4sMt4OB4TryDcVWW9KnsXOf/ea7tIGX2QASNrpetzPocsBZqhHIFDbZ8EkBDjmlmWGHg6BgjVx6lLcMXX4Dcw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-transform/binding-linux-riscv64-musl@0.131.0': resolution: {integrity: sha512-/rLVLItsBjKrnZFLiGrwRB3fs0dAjXZLqY7F42omvacFJjZsceQ3481oQX1bBs3RwoDDyDy/9ZkIN7kYIkv5Gw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [musl] '@oxc-transform/binding-linux-s390x-gnu@0.131.0': resolution: {integrity: sha512-fUprJgJauI1A7e7cDgY/Z3mwLVtE3aswB4lvS96KpRNDHrwOh8bnCJOWf+0CYveDQzghDVFiZWVDo56pO4Wr9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-transform/binding-linux-x64-gnu@0.131.0': resolution: {integrity: sha512-XdbvDT1GPNxrTLXSRt4RU2uCH112q3nINTT05DZqTYYcAxaCPImnMoZe2TlBv5j2376Gk+2pcVnJs6xut47aSw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-transform/binding-linux-x64-musl@0.131.0': resolution: {integrity: sha512-Du2CxlBfC98EV3hOAmLVSUgP0JgqM9F47lRv9v43T4sGPcQVOjs9wffUybGUUraG9unmBZ4dgpMAqlCq0k3dGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@oxc-transform/binding-openharmony-arm64@0.131.0': resolution: {integrity: sha512-wTj2FkOgNhgdisnA0a15QQksyj6AH2snmpgYgAtj098i477x5LpHHdqfuk60jsA/QHSjmUc6dm4P88yI5GY4xA==} @@ -6192,42 +6095,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-wasm@2.5.6': resolution: {integrity: sha512-byAiBZ1t3tXQvc8dMD/eoyE7lTXYorhn+6uVW5AC+JGI1KtJC/LvDche5cfUE+qiefH+Ybq0bUCJU0aB1cSHUA==} @@ -8281,126 +8178,108 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-gnu@1.1.2': resolution: {integrity: sha512-gBSUVO0eaWgw1JMjK3gB8BMlX2Mk148s2lTiVT3e9vjVxbl7UDfMWWY8CfIaaqiXuM9fVTMxIpUz6CAo/B6Vlw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-gnu@1.1.3': resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-arm64-musl@1.1.2': resolution: {integrity: sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-arm64-musl@1.1.3': resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-ppc64-gnu@1.1.2': resolution: {integrity: sha512-X/7bVLWelEsbyWDUSXt7zVsTniLLPIY2n1rH58qr78l9i7MNbbxBWD8gI2vRfBWf4NUXJCUuQnfZDsp32LqsfQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-ppc64-gnu@1.1.3': resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.1.2': resolution: {integrity: sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.1.3': resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.1.2': resolution: {integrity: sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.1.3': resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-linux-x64-musl@1.1.2': resolution: {integrity: sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-linux-x64-musl@1.1.3': resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} @@ -8586,79 +8465,66 @@ packages: resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.4': resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.4': resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.4': resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.4': resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.4': resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.4': resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.4': resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.4': resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.4': resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.4': resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.4': resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.4': resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.60.4': resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} @@ -9230,56 +9096,48 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.1': resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-arm64-musl@4.2.2': resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.1': resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-gnu@4.2.2': resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.1': resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-musl@4.2.2': resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.1': resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} @@ -9362,28 +9220,24 @@ packages: engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@takumi-rs/core-linux-arm64-musl@0.68.17': resolution: {integrity: sha512-4CiEF518wDnujF0fjql2XN6uO+OXl0svy0WgAF2656dCx2gJtWscaHytT2rsQ0ZmoFWE0dyWcDW1g/FBVPvuvA==} engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@takumi-rs/core-linux-x64-gnu@0.68.17': resolution: {integrity: sha512-jm8lTe2E6Tfq2b97GJC31TWK1JAEv+MsVbvL9DCLlYcafgYFlMXDUnOkZFMjlrmh0HcFAYDaBkniNDgIQfXqzg==} engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@takumi-rs/core-linux-x64-musl@0.68.17': resolution: {integrity: sha512-nbdzQgC4ywzltDDV1fer1cKswwGE+xXZHdDiacdd7RM5XBng209Bmo3j1iv9dsX+4xXhByzCCGbxdWhhHqVXmw==} engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@takumi-rs/core-win32-arm64-msvc@0.68.17': resolution: {integrity: sha512-kE4F0LRmuhSwiNkFG7dTY9ID8+B7zb97QedyN/IO2fBJmRQDkqCGcip2gloh8YPPhCuKGjCqqqh2L+Tg9PKW7w==} @@ -10104,61 +9958,51 @@ packages: resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.12.2': resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} cpu: [loong64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-loong64-musl@1.12.2': resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} cpu: [loong64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.12.2': resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.12.2': resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-openharmony-arm64@1.12.2': resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} @@ -14455,56 +14299,48 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.31.1: resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} @@ -19420,11 +19256,6 @@ snapshots: uuid: 11.1.1 zod: 3.25.76 - '@ag-ui/core@0.0.45': - dependencies: - rxjs: 7.8.1 - zod: 3.25.76 - '@ag-ui/core@0.0.46': dependencies: rxjs: 7.8.1 @@ -19469,12 +19300,12 @@ snapshots: - react-dom - ws - '@ag-ui/mastra@1.0.2(15ef11d9d782f272943f408ae4ae9137)': + '@ag-ui/mastra@1.0.2(0254d4c65bff74a4da757165177341fa)': dependencies: '@ag-ui/client': 0.0.49 '@ag-ui/core': 0.0.53 '@ai-sdk/ui-utils': 1.2.11(zod@4.3.6) - '@copilotkit/runtime': 0.0.0-mme-ag-ui-0-0-46-20260227141603(5446566d63c77ef66e64c8ef486231a5) + '@copilotkit/runtime': 0.0.0-mme-ag-ui-0-0-46-20260227141603(62d51d1679e8c285c5ff70477ea8396f) '@mastra/client-js': 1.11.2(@cfworker/json-schema@4.1.1)(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@1.0.0)(typebox@1.1.38)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@1.0.0)(typebox@1.1.38)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.1.38)(zod@4.3.6))(@types/json-schema@7.0.15)(openapi-types@12.1.3)(zod@4.3.6) '@mastra/core': 1.15.0(@cfworker/json-schema@4.1.1)(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@1.0.0)(typebox@1.1.38)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@1.0.0)(typebox@1.1.38)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(typebox@1.1.38)(zod@4.3.6))(@types/json-schema@7.0.15)(openapi-types@12.1.3)(zod@4.3.6) rxjs: 7.8.1 @@ -20892,7 +20723,7 @@ snapshots: dependencies: commander: 13.1.0 - '@copilotkit/runtime@0.0.0-mme-ag-ui-0-0-46-20260227141603(5446566d63c77ef66e64c8ef486231a5)': + '@copilotkit/runtime@0.0.0-mme-ag-ui-0-0-46-20260227141603(62d51d1679e8c285c5ff70477ea8396f)': dependencies: '@ag-ui/client': 0.0.46 '@ag-ui/core': 0.0.46 @@ -20901,7 +20732,7 @@ snapshots: '@ai-sdk/openai': 2.0.106(zod@3.25.76) '@copilotkit/shared': 0.0.0-mme-ag-ui-0-0-46-20260227141603(@ag-ui/core@0.0.46)(encoding@0.1.13) '@copilotkitnext/agent': 0.0.0-mme-ag-ui-0-0-46-20260227141603(@cfworker/json-schema@4.1.1) - '@copilotkitnext/runtime': 0.0.0-mme-ag-ui-0-0-46-20260227141603(@ag-ui/client@0.0.46)(@ag-ui/core@0.0.46)(@ag-ui/encoder@0.0.46)(@copilotkitnext/shared@0.0.0-mme-ag-ui-0-0-46-20260227141603) + '@copilotkitnext/runtime': 0.0.0-mme-ag-ui-0-0-46-20260227141603(@ag-ui/client@0.0.46)(@ag-ui/core@0.0.46)(@ag-ui/encoder@0.0.49)(@copilotkitnext/shared@0.0.0-mme-ag-ui-0-0-46-20260227141603) '@graphql-yoga/plugin-defer-stream': 3.21.0(graphql-yoga@5.21.0(graphql@16.14.0))(graphql@16.14.0) '@hono/node-server': 1.19.14(hono@4.12.23) '@langchain/core': 1.2.1(@opentelemetry/api@1.9.1)(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(openai@4.104.0(encoding@0.1.13)(ws@8.21.0)(zod@4.3.6))(ws@8.21.0) @@ -20964,11 +20795,11 @@ snapshots: - '@cfworker/json-schema' - supports-color - '@copilotkitnext/runtime@0.0.0-mme-ag-ui-0-0-46-20260227141603(@ag-ui/client@0.0.46)(@ag-ui/core@0.0.46)(@ag-ui/encoder@0.0.46)(@copilotkitnext/shared@0.0.0-mme-ag-ui-0-0-46-20260227141603)': + '@copilotkitnext/runtime@0.0.0-mme-ag-ui-0-0-46-20260227141603(@ag-ui/client@0.0.46)(@ag-ui/core@0.0.46)(@ag-ui/encoder@0.0.49)(@copilotkitnext/shared@0.0.0-mme-ag-ui-0-0-46-20260227141603)': dependencies: '@ag-ui/client': 0.0.46 '@ag-ui/core': 0.0.46 - '@ag-ui/encoder': 0.0.46 + '@ag-ui/encoder': 0.0.49 '@copilotkitnext/shared': 0.0.0-mme-ag-ui-0-0-46-20260227141603 cors: 2.8.6 express: 4.22.2 @@ -21889,9 +21720,9 @@ snapshots: '@gar/promisify@1.1.3': optional: true - '@google-cloud/opentelemetry-cloud-monitoring-exporter@0.21.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)': + '@google-cloud/opentelemetry-cloud-monitoring-exporter@0.21.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)': dependencies: - '@google-cloud/opentelemetry-resource-util': 3.0.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13) + '@google-cloud/opentelemetry-resource-util': 3.0.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) '@google-cloud/precise-date': 4.0.0 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) @@ -21903,9 +21734,9 @@ snapshots: - encoding - supports-color - '@google-cloud/opentelemetry-cloud-trace-exporter@3.0.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)': + '@google-cloud/opentelemetry-cloud-trace-exporter@3.0.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)': dependencies: - '@google-cloud/opentelemetry-resource-util': 3.0.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13) + '@google-cloud/opentelemetry-resource-util': 3.0.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) '@grpc/grpc-js': 1.14.4 '@grpc/proto-loader': 0.8.1 '@opentelemetry/api': 1.9.0 @@ -21917,7 +21748,7 @@ snapshots: - encoding - supports-color - '@google-cloud/opentelemetry-resource-util@3.0.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(encoding@0.1.13)': + '@google-cloud/opentelemetry-resource-util@3.0.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)': dependencies: '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.0) @@ -21972,8 +21803,8 @@ snapshots: '@google/adk@1.3.0(@bufbuild/protobuf@2.12.0)(@cfworker/json-schema@4.1.1)(@grpc/grpc-js@1.14.4)(@mikro-orm/mariadb@6.6.15(@mikro-orm/core@6.6.15)(pg@8.20.0))(@mikro-orm/mssql@6.6.15(@mikro-orm/core@6.6.15)(mariadb@3.4.5)(pg@8.20.0))(@mikro-orm/mysql@6.6.15(@mikro-orm/core@6.6.15)(@types/node@22.19.19)(mariadb@3.4.5)(pg@8.20.0))(@mikro-orm/postgresql@6.6.15(@mikro-orm/core@6.6.15)(mariadb@3.4.5))(@mikro-orm/sqlite@6.6.15(@mikro-orm/core@6.6.15)(mariadb@3.4.5)(pg@8.20.0))(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)': dependencies: '@a2a-js/sdk': 0.3.14(@bufbuild/protobuf@2.12.0)(@grpc/grpc-js@1.14.4)(express@4.22.2) - '@google-cloud/opentelemetry-cloud-monitoring-exporter': 0.21.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13) - '@google-cloud/opentelemetry-cloud-trace-exporter': 3.0.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13) + '@google-cloud/opentelemetry-cloud-monitoring-exporter': 0.21.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.1))(encoding@0.1.13) + '@google-cloud/opentelemetry-cloud-trace-exporter': 3.0.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.1))(encoding@0.1.13) '@google-cloud/storage': 7.21.0(encoding@0.1.13) '@google-cloud/vertexai': 1.12.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(encoding@0.1.13) '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3)) @@ -23104,7 +22935,7 @@ snapshots: '@mikro-orm/mariadb@6.6.15(@mikro-orm/core@6.6.15)(pg@8.20.0)': dependencies: '@mikro-orm/core': 6.6.15 - '@mikro-orm/knex': 6.6.15(@mikro-orm/core@6.6.15)(mariadb@3.4.5)(pg@8.20.0)(sqlite3@5.1.7) + '@mikro-orm/knex': 6.6.15(@mikro-orm/core@6.6.15)(mariadb@3.4.5)(mysql2@3.20.0(@types/node@22.19.19))(pg@8.20.0) mariadb: 3.4.5 transitivePeerDependencies: - better-sqlite3 @@ -23157,7 +22988,7 @@ snapshots: '@mikro-orm/postgresql@6.6.15(@mikro-orm/core@6.6.15)(mariadb@3.4.5)': dependencies: '@mikro-orm/core': 6.6.15 - '@mikro-orm/knex': 6.6.15(@mikro-orm/core@6.6.15)(mariadb@3.4.5)(pg@8.20.0)(sqlite3@5.1.7) + '@mikro-orm/knex': 6.6.15(@mikro-orm/core@6.6.15)(mariadb@3.4.5)(mysql2@3.20.0(@types/node@22.19.19))(pg@8.20.0) pg: 8.20.0 postgres-array: 3.0.4 postgres-date: 2.1.0 @@ -24308,26 +24139,6 @@ snapshots: '@opentelemetry/semantic-conventions@1.41.1': {} - '@openuidev/lang-core@0.2.6(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(zod@4.3.6)': - dependencies: - zod: 4.3.6 - optionalDependencies: - '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) - - '@openuidev/react-email@0.2.4(@openuidev/react-lang@0.2.6(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(react@19.2.3)(zod@4.3.6))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.3.6)': - dependencies: - '@openuidev/react-lang': 0.2.6(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(react@19.2.3)(zod@4.3.6) - '@react-email/components': 0.0.41(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - zod: 4.3.6 - - '@openuidev/react-headless@0.8.2(react@19.2.3)(zustand@4.5.7(@types/react@19.2.14)(react@19.2.3))': - dependencies: - '@ag-ui/core': 0.0.45 - react: 19.2.3 - zustand: 4.5.7(@types/react@19.2.14)(react@19.2.3) - '@openuidev/react-lang@0.1.3(react@19.2.0)': dependencies: react: 19.2.0 @@ -24338,14 +24149,6 @@ snapshots: react: 19.2.4 zod: 4.4.3 - '@openuidev/react-lang@0.2.6(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(react@19.2.3)(zod@4.3.6)': - dependencies: - '@openuidev/lang-core': 0.2.6(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(zod@4.3.6) - react: 19.2.3 - zod: 4.3.6 - optionalDependencies: - '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) - '@openuidev/thesys-server@0.1.1': {} '@openuidev/thesys@0.2.1(@openuidev/lang-core@packages+lang-core)(@openuidev/react-headless@packages+react-headless)(@openuidev/react-lang@packages+react-lang)(@openuidev/react-ui@packages+react-ui)(@radix-ui/react-tooltip@1.2.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.4.3)(zustand@4.5.7(@types/react@19.2.14)(react@19.2.3))':