Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

101 lines
3.8 KiB
Markdown

# @assistant-ui/next
Next.js integration for assistant-ui: the `withAui()` config wrapper and the
compiler for the `"use generative"` directive. Colocate a tool's **schema**,
**server-only `execute`**, and **client-only `render`** in one file; the compiler
emits a different module per build target so each side only loads what it needs.
See [SPEC.md](./SPEC.md) for the full design.
## Why
`"use client"` is whole-module, so it can't keep a tool's zod schema readable on
the server while keeping its `render` on the client. And a backend `execute`
holds secrets (DB handles, API keys) that must never reach the browser bundle.
`"use generative"` routes each property to the right place.
Every tool **must** declare an `execute`, and you wrap the default export in
`defineToolkit({ ... })` (both are enforced — the compiler errors otherwise). You
don't declare a tool's kind: the compiler **infers** it from the `execute` and
writes a `type` field back into the output.
| how you author the `execute` | kind | where it runs |
| ----------------------------------------- | ---------- | ---------------------------- |
| `execute` with a `"use client"` directive | `frontend` | client |
| `execute` (plain) | `backend` | server (`server-only` guard) |
| `execute: humanTool()` | `human` | — (the UI supplies a result) |
A plain `execute` is server-only by default — you can only run one in the browser
by opting in with `"use client"`, so secrets can't leak by omission.
## Authoring
```tsx
"use generative";
import { z } from "zod";
import { defineToolkit } from "@assistant-ui/react";
import { db } from "@/db"; // server-only
import { Chart } from "@/ui/chart"; // client-only
export default defineToolkit({
weather: {
description: "Show the weather for a city.",
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => db.weather.get(city), // backend → stays on the server
render: (props) => <Chart data={props} />, // stays on the client
},
});
```
The server build keeps `parameters` + `execute` (guarded by `import
"server-only"`, tagged `type: "backend"`) and drops `render` and `@/ui/chart`.
The client build keeps `parameters` + `render` (under `"use client"`) and drops
`execute` and `@/db`. A `frontend` tool marks its `execute` with `"use client"`:
```tsx
execute: async ({ city }) => {
"use client";
return navigator.geolocation /* … runs in the browser, kept client-side */;
},
```
## Wiring into Next.js
Wrap your config. Detection is by the `"use generative"` directive — there is **no
filename convention**; modules without the directive pass through untouched.
```ts
// next.config.ts
import { withAui } from "@assistant-ui/next";
export default withAui({
/* your Next config */
});
```
`withAui` applies the loader to your TS/TSX. To limit how many files it
scans, narrow the globs: `withAui(config, { rules: ["*.generative.tsx"] })`.
Import the module **bare** from both sides — the loader rewrites it into a facade
that resolves to the right build per layer (no query, no per-file config):
```tsx
// a client component → resolves to the client build (schema + render)
import toolkit from "@/lib/chat.generative";
// a route handler (react-server layer) → resolves to the server build (schema + execute)
import toolkit from "@/lib/chat.generative";
```
With the AI SDK, convert the server build to a `ToolSet` (see
`AISDKToolkit` in `@assistant-ui/react-ai-sdk`).
> **Validated on Next 16.2.6 (Turbopack).** Turbopack honors the loader-emitted
> `"use client"`, but compiles one output per resource path — so the server build
> is selected by its own `?generative-env=server` query rather than by build layer.
> Clear `.next` after changing the loader (Turbopack caches loader output).
## License
MIT