{"_id":"@zuilib/ai","_rev":"3-084f8bbc3e878ca9da19c8c317676bc9","name":"@zuilib/ai","dist-tags":{"latest":"0.6.0"},"versions":{"0.4.0":{"name":"@zuilib/ai","version":"0.4.0","keywords":["design-system","ai","llm","streaming","tool-calls","react","tailwindcss","headlessui"],"_id":"@zuilib/ai@0.4.0","maintainers":[{"name":"mahuzedada","email":"chatis@afrointelligence.com"}],"dist":{"shasum":"6213f2424358df23bf83c316efb5748825fded4e","tarball":"https://registry.npmjs.org/@zuilib/ai/-/ai-0.4.0.tgz","fileCount":55,"integrity":"sha512-6gIpKdcowW4T/twieERudvbRGQZ/fl2JhmrxwRAF4ckvIOvTabzMaEUmDZi3CYp9ZT2OQAgjwrIXE1uMYho+0g==","signatures":[{"sig":"MEQCIFWoIEis5i/8AQGCvGqAWPJZZsHto0d37kYDa1aKRvEGAiAA9aVcvV1P9pTr/lTI0Jp1DpTY8W8DmYEX0Te5EKEv4A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":662926},"type":"module","_from":"file:zuilib-ai-0.4.0.tgz","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./icons":{"types":"./dist/icons.d.ts","import":"./dist/icons.js"},"./thread":{"types":"./dist/thread.d.ts","import":"./dist/thread.js"},"./citation":{"types":"./dist/citation.d.ts","import":"./dist/citation.js"},"./reasoning":{"types":"./dist/reasoning.d.ts","import":"./dist/reasoning.js"},"./safe-href":{"types":"./dist/safe-href.d.ts","import":"./dist/safe-href.js"},"./use-thread":{"types":"./dist/use-thread.d.ts","import":"./dist/use-thread.js"},"./consume-run":{"types":"./dist/consume-run.d.ts","import":"./dist/consume-run.js"},"./diff-engine":{"types":"./dist/diff-engine.d.ts","import":"./dist/diff-engine.js"},"./diff-review":{"types":"./dist/diff-review.d.ts","import":"./dist/diff-review.js"},"./adapters/sse":{"types":"./dist/adapters/sse.d.ts","import":"./dist/adapters/sse.js"},"./package.json":"./package.json","./prompt-input":{"types":"./dist/prompt-input.d.ts","import":"./dist/prompt-input.js"},"./tailwind.css":"./dist/styles/tailwind.css","./use-approval":{"types":"./dist/use-approval.d.ts","import":"./dist/use-approval.js"},"./approval-card":{"types":"./dist/approval-card.d.ts","import":"./dist/approval-card.js"},"./message-parts":{"types":"./dist/message-parts.d.ts","import":"./dist/message-parts.js"},"./stream-events":{"types":"./dist/stream-events.d.ts","import":"./dist/stream-events.js"},"./use-tool-call":{"types":"./dist/use-tool-call.d.ts","import":"./dist/use-tool-call.js"},"./assistant-dock":{"types":"./dist/assistant-dock.d.ts","import":"./dist/assistant-dock.js"},"./thread-reducer":{"types":"./dist/thread-reducer.d.ts","import":"./dist/thread-reducer.js"},"./tool-call-card":{"types":"./dist/tool-call-card.d.ts","import":"./dist/tool-call-card.js"},"./adapters/ai-sdk":{"types":"./dist/adapters/ai-sdk.d.ts","import":"./dist/adapters/ai-sdk.js"},"./markdown-parser":{"types":"./dist/markdown-parser.d.ts","import":"./dist/markdown-parser.js"},"./adapters/anthropic":{"types":"./dist/adapters/anthropic.d.ts","import":"./dist/adapters/anthropic.js"},"./streaming-markdown":{"types":"./dist/streaming-markdown.d.ts","import":"./dist/streaming-markdown.js"},"./use-diff-decisions":{"types":"./dist/use-diff-decisions.d.ts","import":"./dist/use-diff-decisions.js"},"./structured-output-form":{"types":"./dist/structured-output-form.d.ts","import":"./dist/structured-output-form.js"}},"scripts":{"test":"node scripts/check-logical.mjs && vitest run","build":"tsup","typecheck":"tsc --noEmit -p tsconfig.json","check:logical":"node scripts/check-logical.mjs"},"_npmUser":{"name":"mahuzedada","email":"chatis@afrointelligence.com"},"_resolved":"/private/var/folders/mn/n3l7wn4s7bzbxsb8xcdwyvyc0000gn/T/2839b50599fcf97eb5317e8484325b6f/zuilib-ai-0.4.0.tgz","_integrity":"sha512-6gIpKdcowW4T/twieERudvbRGQZ/fl2JhmrxwRAF4ckvIOvTabzMaEUmDZi3CYp9ZT2OQAgjwrIXE1uMYho+0g==","_npmVersion":"11.13.0","description":"ZUI UI for AI-assisted enterprise apps: assistant dock, thread, streaming markdown, tool calls, approvals, diff review, structured output, citations, provider adapters","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"24.17.0","dependencies":{"@zuilib/components":"^0.2.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","jsdom":"^30.0.1","react":"^18.2.0","vitest":"^4.1.11","axe-core":"^4.13.0","react-dom":"^18.2.0","typescript":"^6.0.2","@types/react":"^18.0.0","@zuilib/tokens":"^0.2.0","@types/react-dom":"^18.0.0","@headlessui/react":"^2.2.9","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.3","@testing-library/jest-dom":"^7.0.1","@testing-library/user-event":"^14.6.6"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0","tailwindcss":"^4.0.0","@zuilib/tokens":"^0.2.0","@headlessui/react":"^2.2.0"},"peerDependenciesMeta":{"tailwindcss":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/ai_0.4.0_1788297855825_0.8652108946700514","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@zuilib/ai","version":"0.5.0","keywords":["design-system","ai","llm","streaming","tool-calls","react","tailwindcss","base-ui"],"_id":"@zuilib/ai@0.5.0","maintainers":[{"name":"mahuzedada","email":"chatis@afrointelligence.com"}],"dist":{"shasum":"4eb89028749567480b46486a606752da9b8f3704","tarball":"https://registry.npmjs.org/@zuilib/ai/-/ai-0.5.0.tgz","fileCount":55,"integrity":"sha512-rmCJdT5lcxjdKJM989LlIqomy6oFXM1OKUJhVscyOlOHcKMDdjwCSiJl1SStyw+zSRlo76byxQuky04CXYaEww==","signatures":[{"sig":"MEUCIEyjLJ1z1W3teI8rhnymJj/+nctR1oMWQSGYqd+OpRGPAiEAtzKxlT48J8OVdbUccaA90RETjBZ6VXq3q9v0iYiD9V4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":684041},"type":"module","_from":"file:zuilib-ai-0.5.0.tgz","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./icons":{"types":"./dist/icons.d.ts","import":"./dist/icons.js"},"./thread":{"types":"./dist/thread.d.ts","import":"./dist/thread.js"},"./citation":{"types":"./dist/citation.d.ts","import":"./dist/citation.js"},"./reasoning":{"types":"./dist/reasoning.d.ts","import":"./dist/reasoning.js"},"./safe-href":{"types":"./dist/safe-href.d.ts","import":"./dist/safe-href.js"},"./use-thread":{"types":"./dist/use-thread.d.ts","import":"./dist/use-thread.js"},"./consume-run":{"types":"./dist/consume-run.d.ts","import":"./dist/consume-run.js"},"./diff-engine":{"types":"./dist/diff-engine.d.ts","import":"./dist/diff-engine.js"},"./diff-review":{"types":"./dist/diff-review.d.ts","import":"./dist/diff-review.js"},"./adapters/sse":{"types":"./dist/adapters/sse.d.ts","import":"./dist/adapters/sse.js"},"./package.json":"./package.json","./prompt-input":{"types":"./dist/prompt-input.d.ts","import":"./dist/prompt-input.js"},"./tailwind.css":"./dist/styles/tailwind.css","./use-approval":{"types":"./dist/use-approval.d.ts","import":"./dist/use-approval.js"},"./approval-card":{"types":"./dist/approval-card.d.ts","import":"./dist/approval-card.js"},"./message-parts":{"types":"./dist/message-parts.d.ts","import":"./dist/message-parts.js"},"./stream-events":{"types":"./dist/stream-events.d.ts","import":"./dist/stream-events.js"},"./use-tool-call":{"types":"./dist/use-tool-call.d.ts","import":"./dist/use-tool-call.js"},"./assistant-dock":{"types":"./dist/assistant-dock.d.ts","import":"./dist/assistant-dock.js"},"./thread-reducer":{"types":"./dist/thread-reducer.d.ts","import":"./dist/thread-reducer.js"},"./tool-call-card":{"types":"./dist/tool-call-card.d.ts","import":"./dist/tool-call-card.js"},"./adapters/ai-sdk":{"types":"./dist/adapters/ai-sdk.d.ts","import":"./dist/adapters/ai-sdk.js"},"./markdown-parser":{"types":"./dist/markdown-parser.d.ts","import":"./dist/markdown-parser.js"},"./adapters/anthropic":{"types":"./dist/adapters/anthropic.d.ts","import":"./dist/adapters/anthropic.js"},"./streaming-markdown":{"types":"./dist/streaming-markdown.d.ts","import":"./dist/streaming-markdown.js"},"./use-diff-decisions":{"types":"./dist/use-diff-decisions.d.ts","import":"./dist/use-diff-decisions.js"},"./structured-output-form":{"types":"./dist/structured-output-form.d.ts","import":"./dist/structured-output-form.js"}},"scripts":{"test":"node scripts/check-logical.mjs && vitest run","build":"tsup","typecheck":"tsc --noEmit -p tsconfig.json","check:logical":"node scripts/check-logical.mjs"},"_npmUser":{"name":"mahuzedada","email":"chatis@afrointelligence.com"},"_resolved":"/private/var/folders/mn/n3l7wn4s7bzbxsb8xcdwyvyc0000gn/T/2c0d17c1667559e368290b9912aea2e1/zuilib-ai-0.5.0.tgz","_integrity":"sha512-rmCJdT5lcxjdKJM989LlIqomy6oFXM1OKUJhVscyOlOHcKMDdjwCSiJl1SStyw+zSRlo76byxQuky04CXYaEww==","_npmVersion":"11.13.0","description":"ZUI UI for AI-assisted enterprise apps: assistant dock, thread, streaming markdown, tool calls, approvals, diff review, structured output, citations, provider adapters","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"24.17.0","dependencies":{"@base-ui/react":"^1.8.0","@zuilib/primitives":"^0.3.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","jsdom":"^30.0.1","react":"^18.2.0","vitest":"^4.1.11","axe-core":"^4.13.0","react-dom":"^18.2.0","typescript":"^6.0.2","@types/react":"^18.0.0","@types/react-dom":"^18.0.0","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.3","@testing-library/jest-dom":"^7.0.1","@testing-library/user-event":"^14.6.6"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0","tailwindcss":"^4.0.0"},"peerDependenciesMeta":{"tailwindcss":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/ai_0.5.0_1788759574778_0.17471400252048808","host":"s3://npm-registry-packages-npm-production"}},"0.6.0":{"_id":"@zuilib/ai@0.6.0","dist":{"shasum":"60645d71ee9d6779b1371899eaf268ae7f475529","tarball":"https://registry.npmjs.org/@zuilib/ai/-/ai-0.6.0.tgz","fileCount":56,"integrity":"sha512-eCA53xtgIj4l/26pVVKqXmMZNnT+1SNXPEd1LFtfw8crV9g/6HDqsCMcJ7oXy3N3Srwrn+JS1QlPpU8qIvzslQ==","signatures":[{"sig":"MEQCICyYW64imwqI+u/wBfIwOZMlFUh3i6xv3D59blnH5GK2AiAMB806bboxp+2ndzKR36RYujkDwWpKBnQoigcOof5V5w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCqSXvB9U1p0PRw6rElusqCXnkhOtvtnmMTfNhlVTa/agIhAO3CP7DTaRBU8DLyAfcbfAUR0ECnQ9p3KN1Y+Ov7Zb9I"}],"unpackedSize":688148},"name":"@zuilib/ai","type":"module","_from":"file:zuilib-ai-0.6.0.tgz","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./icons":{"types":"./dist/icons.d.ts","import":"./dist/icons.js"},"./thread":{"types":"./dist/thread.d.ts","import":"./dist/thread.js"},"./citation":{"types":"./dist/citation.d.ts","import":"./dist/citation.js"},"./reasoning":{"types":"./dist/reasoning.d.ts","import":"./dist/reasoning.js"},"./safe-href":{"types":"./dist/safe-href.d.ts","import":"./dist/safe-href.js"},"./use-thread":{"types":"./dist/use-thread.d.ts","import":"./dist/use-thread.js"},"./consume-run":{"types":"./dist/consume-run.d.ts","import":"./dist/consume-run.js"},"./diff-engine":{"types":"./dist/diff-engine.d.ts","import":"./dist/diff-engine.js"},"./diff-review":{"types":"./dist/diff-review.d.ts","import":"./dist/diff-review.js"},"./adapters/sse":{"types":"./dist/adapters/sse.d.ts","import":"./dist/adapters/sse.js"},"./package.json":"./package.json","./prompt-input":{"types":"./dist/prompt-input.d.ts","import":"./dist/prompt-input.js"},"./tailwind.css":"./dist/styles/tailwind.css","./use-approval":{"types":"./dist/use-approval.d.ts","import":"./dist/use-approval.js"},"./approval-card":{"types":"./dist/approval-card.d.ts","import":"./dist/approval-card.js"},"./message-parts":{"types":"./dist/message-parts.d.ts","import":"./dist/message-parts.js"},"./stream-events":{"types":"./dist/stream-events.d.ts","import":"./dist/stream-events.js"},"./use-tool-call":{"types":"./dist/use-tool-call.d.ts","import":"./dist/use-tool-call.js"},"./assistant-dock":{"types":"./dist/assistant-dock.d.ts","import":"./dist/assistant-dock.js"},"./thread-reducer":{"types":"./dist/thread-reducer.d.ts","import":"./dist/thread-reducer.js"},"./tool-call-card":{"types":"./dist/tool-call-card.d.ts","import":"./dist/tool-call-card.js"},"./adapters/ai-sdk":{"types":"./dist/adapters/ai-sdk.d.ts","import":"./dist/adapters/ai-sdk.js"},"./markdown-parser":{"types":"./dist/markdown-parser.d.ts","import":"./dist/markdown-parser.js"},"./adapters/anthropic":{"types":"./dist/adapters/anthropic.d.ts","import":"./dist/adapters/anthropic.js"},"./streaming-markdown":{"types":"./dist/streaming-markdown.d.ts","import":"./dist/streaming-markdown.js"},"./use-diff-decisions":{"types":"./dist/use-diff-decisions.d.ts","import":"./dist/use-diff-decisions.js"},"./structured-output-form":{"types":"./dist/structured-output-form.d.ts","import":"./dist/structured-output-form.js"}},"license":"MIT","scripts":{"test":"node scripts/check-logical.mjs && vitest run","build":"tsup","typecheck":"tsc --noEmit -p tsconfig.json","check:logical":"node scripts/check-logical.mjs"},"version":"0.6.0","_npmUser":{"name":"mahuzedada","email":"chatis@afrointelligence.com"},"keywords":["design-system","ai","llm","streaming","tool-calls","react","tailwindcss","base-ui"],"_resolved":"/private/var/folders/mn/n3l7wn4s7bzbxsb8xcdwyvyc0000gn/T/ab472b8c0bae1045813562223dce260a/zuilib-ai-0.6.0.tgz","_integrity":"sha512-eCA53xtgIj4l/26pVVKqXmMZNnT+1SNXPEd1LFtfw8crV9g/6HDqsCMcJ7oXy3N3Srwrn+JS1QlPpU8qIvzslQ==","_npmVersion":"11.13.0","description":"ZUI UI for AI-assisted enterprise apps: assistant dock, thread, streaming markdown, tool calls, approvals, diff review, structured output, citations, provider adapters","directories":{},"maintainers":[{"name":"mahuzedada","email":"chatis@afrointelligence.com"}],"sideEffects":["**/*.css"],"_nodeVersion":"24.17.0","dependencies":{"@base-ui/react":"^1.8.0","@zuilib/primitives":"^0.4.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","jsdom":"^30.0.1","react":"^18.2.0","vitest":"^4.1.11","axe-core":"^4.13.0","react-dom":"^18.2.0","typescript":"^6.0.2","@types/react":"^18.0.0","@types/react-dom":"^18.0.0","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.3","@testing-library/jest-dom":"^7.0.1","@testing-library/user-event":"^14.6.6"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0","tailwindcss":"^4.0.0"},"peerDependenciesMeta":{"tailwindcss":{"optional":true}},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ai_0.6.0_1789965064899_0.7593804891782725"}}},"time":{"created":"2026-09-01T21:24:15.607Z","modified":"2026-09-21T04:31:05.211Z","0.4.0":"2026-09-01T21:24:16.062Z","0.5.0":"2026-09-07T05:39:34.925Z","0.6.0":"2026-09-21T04:31:05.000Z"},"keywords":["design-system","ai","llm","streaming","tool-calls","react","tailwindcss","base-ui"],"description":"ZUI UI for AI-assisted enterprise apps: assistant dock, thread, streaming markdown, tool calls, approvals, diff review, structured output, citations, provider adapters","maintainers":[{"name":"mahuzedada","email":"chatis@afrointelligence.com"}],"readme":"# @zuilib/ai\n\nThe UI that AI-assisted enterprise apps need, on ZUI tokens and\n`@zuilib/primitives`. Provider-agnostic: no SDK, no network. Stream events\nin, wire callbacks out.\n\n| Import | What |\n| --- | --- |\n| `@zuilib/ai/assistant-dock` | Floating, context-aware assistant dock (not a chatbot); runs suggestions as tasks, streams events into them |\n| `@zuilib/ai/thread` | `Thread`: a list of `Message`s, every part rendered by its card |\n| `@zuilib/ai/streaming-markdown` | Markdown that grows as tokens arrive, with `useStreamingText` and citation chips |\n| `@zuilib/ai/tool-call-card` | One tool invocation: args, status, result, elapsed time |\n| `@zuilib/ai/approval-card` | Propose-then-confirm with a risk badge and a confirmation step |\n| `@zuilib/ai/diff-review` | Unified or split line diff with per-hunk accept / reject; texts, hunks or a patch |\n| `@zuilib/ai/structured-output-form` | Model output as editable ZUI form controls |\n| `@zuilib/ai/citation` | Inline `[1]` chips and a source list |\n| `@zuilib/ai/prompt-input` | The chat input: auto-growing textarea, Enter-to-send, toolbar slot, send / stop button |\n| `@zuilib/ai/reasoning` | \"Working on it\" status |\n| `@zuilib/ai/use-thread` · `/use-tool-call` · `/use-approval` · `/use-diff-decisions` · `/consume-run` | The headless hooks and the run driver |\n| `@zuilib/ai/stream-events` | The `AssistantEvent` stream model (types) |\n| `@zuilib/ai/adapters/ai-sdk` · `/anthropic` · `/sse` | Provider streams and messages mapped to events (types only, no SDK) |\n| `@zuilib/ai/markdown-parser` · `/diff-engine` · `/thread-reducer` · `/safe-href` · `/icons` | The pure helpers |\n\n```css\n@import \"tailwindcss\";\n@import \"@zuilib/primitives/tailwind.css\";\n@import \"@zuilib/ai/tailwind.css\";\n```\n\n## Import rules\n\nDefault exports, one subpath per component (`import ToolCallCard from '@zuilib/ai/tool-call-card'`); the barrel `@zuilib/ai` re-exports the components, hooks and pure helpers by name. **Adapters are not on the barrel**: a host that talks to the AI SDK, Anthropic or an SSE gateway imports the matching `@zuilib/ai/adapters/*` subpath, so the root surface stays provider-free. React and React DOM are the required peers; Tailwind is optional and needed only for the Tailwind entry. Base UI, the ZUI primitives and tokens install transitively. The adapters and the pure helpers have no React import, so a server component or a store can use them.\n\n## How it composes\n\n```\nprovider stream ──adapter──▶ AssistantEvent ──useThread / AssistantDock──▶ Message.parts ──Thread──▶ cards\n```\n\n- **`AssistantEvent`** (`@zuilib/ai/stream-events`) is the streaming model: `{type:'text', delta}`, `{type:'tool-call', id, toolName, args?, status?}`, `{type:'tool-result', callId, result?, error?, elapsedMs?}`, `{type:'approval', id, title, description?, risk?, confirm?, preview?, expiresAt?, respond?}`, `{type:'diff', id, original?, modified?, hunks?, patch?, ...}`, `{type:'citation', sources}`, `{type:'custom', kind, id?, data?}`, `{type:'error', message}`, `{type:'done', summary?}`. `ASSISTANT_EVENT_TYPES` is the runtime list of the types, exhaustiveness-checked against the union.\n- **`MessagePart`** (`@zuilib/ai/message-parts`) is the settled model: the same shapes, folded by `applyEvent` (`@zuilib/ai/thread-reducer`) into `Message.parts`.\n- **`Thread`** maps parts to cards; **`AssistantDock`** does the same inside each task.\n- **Adapters** turn an AI SDK stream, an Anthropic Messages stream or a generic `text/event-stream` into `AssistantEvent`s.\n- **Custom parts** are the extension point: a `custom` event carries an open `kind` and a `data` payload, `applyEvent` folds it into a `CustomPart` (an event with the same `id` replaces the part in place), and a `PartRenderers` entry under that `kind` renders it — in `Thread` directly or through the dock's `renderers` prop. A `kind` with no renderer is skipped (or handed to `renderUnknownPart`), so an old client survives a newer stream. The built-in type keys (`text`, `tool-call`, `tool-result`, `approval`, `diff`, `citation` — exported as `BUILTIN_PART_TYPES`) are reserved: a `custom` part whose `kind` matches one never falls through to the built-in card (its shape would not match) and is treated as unknown instead.\n\nAn async generator can pause on an approval: the human's decision comes back as the value of the `yield`.\n\n```tsx\nasync function* run({ signal }: { signal: AbortSignal }): AsyncGenerator<AssistantEvent, void, ApprovalOutcome | undefined> {\n  yield { type: 'text', delta: 'Drafting…' }\n  const outcome = yield { type: 'approval', id: 'send', title: 'Send 12 emails', risk: 'high' }\n  if (outcome !== 'approved') return yield { type: 'done', summary: { summary: 'Nothing sent.' } }\n  yield { type: 'done', summary: { summary: <b>Sent.</b>, apply: { label: 'Open thread', run: openThread } } }\n}\n```\n\n## Component API\n\nEvery component is a `forwardRef` client component (`'use client'`), default-exported (with a named export alongside) from its own subpath, styled only through the `@zuilib/tokens` names, built from `@zuilib/primitives` primitives, using logical CSS properties (`ps-`, `me-`, `start-`, `text-start`; `pnpm check:logical` enforces it), and carrying a `data-slot` on its root and every part. None of them calls a model.\n\n### AssistantDock — `@zuilib/ai/assistant-dock`\n\n```tsx\nconst suggestions: AssistantSuggestion[] = [\n  { id: 'outreach', label: 'Draft outreach for 2 selected', run: async ({ signal }) => {\n    const draft = await api.draftOutreach(ids, { signal })\n    return { summary: draft.summary, apply: { label: 'Queue for approval', run: () => queue(draft) } }\n  } },\n  { id: 'explain', label: 'Explain the change', run: ({ signal }) => fromSSE(fetch('/api/explain', { signal })) },\n]\n<AssistantDock open={open} onOpenChange={setOpen} shortcut=\"⌘J\" shortcutKey=\"j\" header={{ title: 'Accounts', subtitle: '2 selected' }} suggestions={suggestions} onAsk={(text, { signal }) => api.ask(text, { signal })} />\n```\n\nA task runner, not a chatbot: a spark trigger in a corner and a panel with the header badge, three to five suggestions, the tasks it has run and an optional free-text input. A `run` returns a summary, a promise of one, or an async iterable of `AssistantEvent`s; the task card shows the streamed parts (text through `StreamingMarkdown`, tool calls through `ToolCallCard`, approvals through `ApprovalCard`, diffs through `DiffReview`, citations through `CitationList`) and, once done, the summary and an **Apply** button. An `approval` event pauses the run until the human decides on the card; the decision resumes the iterator (as the `yield`'s value, and through the event's `respond` callback).\n\n| Prop | Type | Default |\n|------|------|---------|\n| `open` / `defaultOpen` / `onOpenChange` | `boolean` / `boolean` / `(open) => void` | controlled when `open` is given; `defaultOpen` (default `false`) seeds the uncontrolled dock |\n| `header` | `{ title: string; subtitle?: string }` | required, the header badge |\n| `suggestions` | `AssistantSuggestion[]` | required: `{ id, label, description?, run: (options: { signal }) => RunResult }` |\n| `onAsk` | `(text: string, options: { signal }) => RunResult` | without it there is no input |\n| `shortcut` / `shortcutKey` | `string` | the tooltip label (`'⌘J'`) and the key bound with ⌘ / Ctrl on the document to toggle |\n| `position` | `'bottom-right' \\| 'bottom-left' \\| 'top-right' \\| 'top-left'` | `'bottom-right'` |\n| `strategy` | `'fixed' \\| 'absolute'` | `'fixed'` pins the dock to the viewport; `'absolute'` positions it inside the nearest `relative` ancestor (keeps a subtree theme, a demo frame) |\n| `width` / `height` | `number \\| string` | `20rem` / content (body capped at 26rem); a number is pixels |\n| `resizable` | `boolean` | `false`; a drag handle on the panel's free corner, arrow keys resize by 16px (min 240 x 200) |\n| `dismissOnOutsideClick` | `boolean` | `false`; close on a pointer press outside the panel and trigger. A press in a popup opened from the dock (a portaled listbox, menu or popover reached through its button's `aria-controls`) counts as inside |\n| `modal` | `boolean` | `false`; the panel is a `role=\"dialog\"`, Tab cycles inside it and Escape closes it from anywhere on the page. Not `aria-modal`: the panel renders in place and the page behind it stays in the accessibility tree |\n| `label` / `suggestionsLabel` / `placeholder` | `string` | `'Assistant'` / `'Suggested here'` / `'Ask about what’s on screen…'` |\n| `maxTasks` | `number` | `20`; the scrollback keeps this many tasks, newest first; past it the oldest finished task is dropped, never a running one |\n| `footer` | `ReactNode` | under the input (a tool-use switch, a model picker) |\n| `renderers` | `PartRenderers` | part renderers for every task's thread: override a built-in card, or render `custom` parts by their `kind` |\n| `onTasksChange` | `(tasks: AssistantTask[]) => void` | after every change |\n| `id` | `string` | base for the ids: the root is `id`, the panel `${id}-panel`, the trigger `${id}-trigger` |\n| `className` / `panelClassName` / `triggerClassName` | `string` | panel layer / panel / trigger button |\n\n`RunSummary` is `{ summary: ReactNode; apply?: { label: string; run: () => void } }`. `RunResult` is `RunSummary | AsyncIterable<AssistantEvent> | Promise<either>` (`RunOutput` is the non-promise half). `AssistantTask` is `{ id: string, label, status: 'running' | 'done' | 'error' | 'stopped', parts: MessagePart[], result?, error? }`. Every `run` / `onAsk` receives `{ signal }` (`RunOptions`), aborted when the human presses **Stop** on the task or the dock unmounts (closing the panel leaves a task running); Stop also returns a streaming iterator, and the parts that arrived stay on the card. A rejected run, or an `error` event, is a failed task showing the message. Failed and stopped tasks keep a **Retry** button that calls the same `run` with a fresh signal. Focus moves into the panel on open (the input, else the first suggestion, else the panel); Escape in the panel closes it and returns focus to the trigger, except in a text field inside a task (an approval's type-to-confirm input keeps Escape); Enter in the input submits.\n\nPhones: below `sm` (640px) the panel is a sheet across the bottom of the viewport (the top, for `top-*` positions) — full width, capped at `85dvh`, padded past the safe-area inset, with a close button (`assistant-dock-close`) in its header, since the sheet covers the trigger. `width`, `height` and `resizable` apply from `sm` up; the trigger clears the home indicator and keeps a 44px target on touch screens.\n\nParts: `AssistantDock.Trigger` and `AssistantDock.Panel` read the root's context and throw outside `<AssistantDock>`; `AssistantDock.Task` (`{ task, onStop?, onRetry?, onApprovalDecision?, onDecisionsChange?, renderers?, className? }`) is a plain presentational card reusable in your own thread. Slots: `assistant-dock` (root, `data-position`, `data-strategy`, `data-open`, `data-modal`), `assistant-dock-trigger-layer`, `assistant-dock-trigger`, `assistant-dock-panel-layer`, `assistant-dock-panel` (`data-resizable`), `assistant-dock-resize`, `assistant-dock-header`, `assistant-dock-title`, `assistant-dock-header-badge`, `assistant-dock-close` (phones only), `assistant-dock-body`, `assistant-dock-suggestions`, `assistant-dock-suggestion`, `assistant-dock-tasks`, `assistant-dock-task` (`data-status`), `assistant-dock-task-label`, `assistant-dock-task-status`, `assistant-dock-task-parts`, `assistant-dock-task-stop`, `assistant-dock-task-result`, `assistant-dock-task-apply`, `assistant-dock-task-error`, `assistant-dock-task-retry`, `assistant-dock-input`, `assistant-dock-footer`.\n\n### Thread — `@zuilib/ai/thread`\n\n```tsx\nconst thread = useThread()\n<Thread messages={thread.messages} onApprovalDecision={(id, outcome) => thread.decideApproval(id, outcome)} onDecisionsChange={(id, decisions) => thread.decideDiff(id, decisions)} />\n<Thread messages={messages} renderers={{ text: MyMarkdown }} />\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `messages` | `Message[]` | required |\n| `renderers` | `PartRenderers` | overrides of `DEFAULT_PART_RENDERERS` by part type; any other key renders `custom` parts of that `kind` |\n| `renderUnknownPart` | `(part: CustomPart) => ReactNode` | rendered for a `custom` part whose `kind` has no renderer; without it the part is skipped |\n| `onApprovalDecision` | `(id, outcome: 'approved' \\| 'rejected', message) => void` | an approval part was decided |\n| `onDecisionsChange` | `(id, decisions, accepted: string, message) => void` | a diff part's decisions changed |\n| `roleLabels` | `Partial<Record<role, string>>` | `You` / `Assistant` / `System` / `Tool`, the article labels |\n| `emptyState` | `ReactNode` | shown when there are no messages |\n| `children` | `(items: { id, node }[]) => ReactNode` | render your own list (a virtualiser) from the already-wrapped messages |\n| `messageClassName` / `className` | `string` | every message / the root |\n\nThe default map: `text` → `StreamingMarkdown`, `tool-call` → `ToolCallCard` (fed by its `tool-result`, which renders nothing itself), `approval` → `ApprovalCard`, `diff` → `DiffReview`, `citation` → `CitationList`. A `custom` part is looked up by its `kind` instead of its type, so `renderers={{ chart: ChartCard }}` adds a part type without touching the union; with no renderer for the `kind` the part is skipped, or handed to `renderUnknownPart`. The built-in type keys are reserved (`BUILTIN_PART_TYPES`): a `custom` part whose `kind` matches one is treated as unknown, never rendered by the built-in card. The root is `role=\"log\"`; each message is an `<article>` (`data-role`) wrapping every part in `thread-part` (`data-type`, and `data-kind` on a `custom` part). Messages are keyed by `id` and memoised (`Thread.Message`), parts by a stable internal key (the part's id, else type and index), so a stream re-renders only the message it appends to; a custom renderer reads the callbacks with `useThreadCallbacks()`. Slots: `thread`, `thread-empty`, `thread-message`, `thread-part`.\n\n### Hooks — `@zuilib/ai/use-thread` and friends\n\n- `useThread({ initialMessages? })` → `{ messages, streaming, error, appendUserMessage(text), startAssistantMessage(), applyEvent(event), consume(run, { signal? }), decideApproval(id, outcome), decideDiff(id, decisions), reset(), dispatch }`: a reducer over `AssistantEvent`s (`threadReducer` from `@zuilib/ai/thread-reducer`). `consume` opens an assistant message and drives a run through `consumeRun`; an `approval` event waits for `decideApproval`.\n- `consumeRun(run, apply, { signal?, onEvent?, waitForApproval })` (`@zuilib/ai/consume-run`) drives any `RunResult` and resolves with the `done` summary. Abort returns the iterator and resolves, even while an approval is still undecided; an `error` event returns the iterator too (so an SSE reader is released) and then rejects.\n- `useToolCall(run)` (`@zuilib/ai/use-tool-call`) → `{ status, args, result, error, elapsedMs, start(args), abort(), reset() }` for a client-side tool, props-compatible with `ToolCallCard`. `abort()` cancels the run in flight and returns the status to `queued` (keeping `args`); `reset()` also clears the args and result.\n- `useApproval({ onApprove?, onReject? })` (`@zuilib/ai/use-approval`) → `{ decision, busy, error, approve(), reject(), reset() }`.\n- `useDiffDecisions(hunks, original?, initial?)` (`@zuilib/ai/use-diff-decisions`) → `{ decisions, accept(i), reject(i), acceptAll(), rejectAll(), reset(), undecided, accepted, complete }`.\n\n### Adapters — `@zuilib/ai/adapters/*`\n\nTypes only and duck-typed: nothing is imported from `ai` or `@anthropic-ai/sdk`. Subpath-only — none of these reach the root barrel.\n\n- `ai-sdk`: `fromAISDKUIMessage(message)` / `fromAISDKUIMessages` map a `UIMessage` (v5 parts; v4 `content` and `tool-invocation` tolerated) to a ZUI `Message`; `fromAISDKStream(chunks)` maps UI message stream chunks (`text-delta`, `tool-input-start`, `tool-input-available`, `tool-output-available`, `tool-output-error`, `source-url`, `error`, `finish`) or v4 protocol lines (`0:\"…\"`, `9:{…}`, `a:{…}`, `3:\"…\"`, `d:{…}`) to events; `createAISDKMapper()` is the stateful chunk → events function; `parseUIMessageStreamLine` the v4 line parser. Sources are gathered into one `citation` event before `done`.\n- `anthropic`: `fromAnthropicStream(events)` maps `content_block_start` / `content_block_delta` (`text_delta`, `input_json_delta`) / `content_block_stop` / `message_stop` / `error`; a `tool_use` block is a queued `tool-call` at start and a running one with parsed `args` at stop. `anthropicToolResult(toolUseId, result, { error?, elapsedMs? })` is the event to push after your app ran the tool. `fromAnthropicMessage(message)` maps a finished `Message` (or a user turn with `tool_result` blocks). `createAnthropicMapper()` is the stateful mapper.\n- `sse`: `parseSSE(source)` reads a `Response`, a `ReadableStream<Uint8Array>` or an iterable of strings / bytes into `{ event?, data, id?, retry? }` messages; `fromSSE(source, map?)` maps them to events. `defaultSSEMap`: `[DONE]` → `done`; an `error` event → `error`; JSON with a known `type` (the `ASSISTANT_EVENT_TYPES` list from `@zuilib/ai/stream-events`, checked against the union so a new variant cannot silently drop here) is the event as is — `custom` included, so a server streams `{\"type\":\"custom\",\"kind\":\"chart\",\"data\":…}` with no custom map; JSON `delta` / `text` / `content` strings, a JSON string, or plain text under no event / `text` / `message` → a text delta; anything else is skipped.\n\n### StreamingMarkdown — `@zuilib/ai/streaming-markdown`\n\n```tsx\n<StreamingMarkdown content={answerSoFar} streaming={!done} />\n<StreamingMarkdown content={full} charactersPerSecond={160} sources={sources} />\nconst { text, done } = useStreamingText(source, { enabled: true, charactersPerSecond: 80 })\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `content` | `string` | required, the markdown so far; append as tokens arrive |\n| `streaming` | `boolean` | `false`; caret after the last block, `aria-busy`, `data-streaming` |\n| `charactersPerSecond` | `number` | reveal `content` at this many characters per second (a demo, smoothing a chunky stream); the caret stays until the reveal catches up |\n| `onLinkClick` | `(event, href: string) => void` | called with the sanitised href; `preventDefault()` to route yourself |\n| `linkComponent` | `ComponentType<{ href, className, 'data-slot', children }>` | renders every link (a router `Link`) instead of `<a target=\"_blank\">` |\n| `sources` | `CitationSource[]` | `[n]` / `[^n]` markers render as `Citation` chips over `sources[n - 1]`; a marker with no source stays text; without the prop markers are plain text |\n| `citationAnchor` | `PopoverAnchor \\| false` | `'top'`; `false` renders the popover in place |\n| `announce` | `boolean` | `true`; a visually hidden `role=\"status\"` region receives each completed sentence, then `completeLabel` |\n| `announceDelay` | `number` | `400` ms a completed sentence waits before it is announced |\n| `completeLabel` | `string` | `'Response complete'` |\n\nRenders ATX and setext headings, paragraphs, bold / italic / inline code, links, images (`<img loading=\"lazy\">`; an unsafe `src` renders the alt text), fenced and indented code (`data-language`), nested ordered / unordered / task lists (an item's nested content is `ListItem.blocks`), tables with column alignment, blockquotes and rules; no raw HTML. The parser (`parseMarkdown(source, { partial, citations })`, `parseMarkdownIncremental`, `parseInline(source, { citations })`, `inlineToText` from `@zuilib/ai/markdown-parser`, pure) tolerates an unfinished document: with `partial` (set while busy) a lone `| header |` row is already a table and an unmatched trailing `**` / `` ` `` / `[` / `![` is plain text; once the stream ends a header row without its separator is a paragraph, as in GFM. Blocks are keyed by character offset and parsed incrementally from the last committed parse (read in render, written in an effect), so settled blocks keep their DOM.\n\nThe document is never a live region: announcing a growing tree re-reads it. Instead the status region speaks each sentence (`.`, `!`, `?` before whitespace, or a line break) once, debounced, as plain text. `useStreamingText` runs one interval for the life of the hook (per `enabled` / `charactersPerSecond`) and reads the latest source through a ref, so a growing source never restarts it; a source that is not an extension of the previous one restarts the reveal.\n\nLinks are untrusted model output: `safeHref` (`@zuilib/ai/safe-href`) allows `http`, `https`, `mailto`, `tel` and relative / `#anchor` targets; any other scheme (`javascript:`, `data:`, `file:`, protocol-relative `//host`) renders the link text as plain text with no `href` (`streaming-markdown-link-text`); the same filter applies to image `src`. A safe link is `<a target=\"_blank\" rel=\"noreferrer noopener\">`.\n\nSlots: `streaming-markdown` (root), `-heading`, `-paragraph`, `-code`, `-pre`, `-list`, `-list-item`, `-table-wrap`, `-table`, `-blockquote`, `-rule`, `-link`, `-link-text`, `-image`, `-image-text`, `-status`, `-caret`; citation chips carry the `citation` slots.\n\n### ToolCallCard — `@zuilib/ai/tool-call-card`\n\n```tsx\n<ToolCallCard name=\"crm.search\" status=\"success\" args={{ query: 'vantage' }} result={{ id: 'ACC-1051' }} elapsedMs={340} />\n<ToolCallCard name=\"crm.update\" status=\"error\" args={{ id: 'ACC-1051' }} error=\"403: write access requires approval\" />\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `name` | `string` | required |\n| `description` | `ReactNode` | one line under the name |\n| `args` | `unknown` | a flat object of primitives renders as key / value rows, anything else as pretty JSON |\n| `status` | `'queued' \\| 'running' \\| 'success' \\| 'error'` | required; badge + icon; `running` sets `aria-busy` |\n| `result` | `unknown` | string / number as text, element as is, else JSON; shown on `success`, or while `running` as a partial |\n| `maxResultLength` | `number` | `20000` characters of a text / JSON result before a **Show all** toggle; the result section carries `data-truncated` while cut |\n| `error` | `ReactNode` | shown on `error` |\n| `elapsedMs` | `number` | milliseconds, formatted `340ms` / `1.2s` |\n| `defaultOpen` | `boolean` | open on `error` only; a later transition to `error` also opens the card unless the human has toggled it or `defaultOpen` is set |\n| `open` / `onOpenChange` | `boolean` / `(open) => void` | controlled |\n| `statusLabels` | `Partial<Record<status, string>>` | `Queued` / `Running` / `Done` / `Failed` |\n\nThe header is a disclosure `<button>` (`aria-expanded`; `aria-controls` while the body exists). A closed body is unmounted, not hidden, so nothing inside it can take focus or be read. `ToolCallValue({ value, slot })` and `formatDuration(ms)` are exported. Slots: `tool-call-card` (root, `data-status`, `data-state`), `-header`, `-status-icon`, `-name`, `-description`, `-status`, `-duration`, `-chevron`, `-body`, `-args`, `-args-value`, `-result` (`data-truncated`), `-result-value`, `-result-toggle`, `-error`.\n\n### ApprovalCard — `@zuilib/ai/approval-card`\n\n```tsx\n<ApprovalCard title=\"Email the Vantage sponsor\" description=\"A short note before the renewal.\" risk=\"medium\" preview={<p>{draft}</p>} onApprove={send} onReject={dismiss} onEdit={openEditor} />\n<ApprovalCard title=\"Roll back notifier\" risk=\"high\" expiresAt={Date.now() + 600_000} onApprove={run} onReject={skip} />\n<ApprovalCard title=\"Delete the workspace\" risk=\"high\" confirm=\"type\" confirmationPhrase=\"DELETE\" onApprove={purge} onReject={skip} />\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `title` | `ReactNode` | required |\n| `description` / `preview` | `ReactNode` | the proposal: a draft, a diff, a table |\n| `risk` | `'low' \\| 'medium' \\| 'high'` | `'low'`; badge, border tint; `high` puts the approve button in the danger tone and defaults `confirm` to `'double'` |\n| `confirm` | `'none' \\| 'double' \\| 'type' \\| 'hold'` | `'double'` for `high`, else `'none'` |\n| `confirmationPhrase` / `holdMs` | `string` / `number` | `'APPROVE'` / `1200` ms |\n| `onApprove` / `onReject` | `() => void \\| Promise<unknown>` | required; a returned promise holds the card `busy` until it settles |\n| `onEdit` | `() => void` | adds an Edit button |\n| `busy` | `boolean` | `false`; approve spins, the others lock, keys ignored |\n| `decision` | `'undecided' \\| 'approved' \\| 'rejected' \\| 'expired'` | `'undecided'`; a decided card adds the outcome badge, locks its buttons and the keys, sets `data-decision` |\n| `expiresAt` | `Date \\| number \\| string` | past it the card shows as `expired` on its own |\n| `labels` | `Partial<ApprovalCardLabels>` | every text on the card, one key at a time: `approve`, `reject`, `edit`, `confirm` (the armed button), `busy` (announced with the spinner), `riskLow` / `riskMedium` / `riskHigh`, `approved` / `rejected` / `expired`. Defaults in `DEFAULT_APPROVAL_CARD_LABELS` |\n\nConfirmation: `double` arms the approve button on the first activation (its label becomes `labels.confirm`, `data-armed`; Escape, blur or five seconds disarm it) and approves on the second; `type` shows a field (`approval-card-confirm-input`) and enables Approve only when it holds `confirmationPhrase` (Enter in the field approves); `hold` approves only after the button was held (pointer or Space) for `holdMs`, with a progress bar (`approval-card-hold`) while held. High risk never approves on a single Enter.\n\nKeyboard: the root is a focusable `role=\"group\"` labelled by its title and described by the description and the visually hidden hint. With focus anywhere inside it, Enter activates approve and Escape rejects, except: a text field (`input`, `textarea`, `select`, contenteditable) and a nested popup (`dialog`, `menu`, `listbox`, `combobox`) keep both keys; a button keeps Enter for itself and Escape still rejects. A rejecting Escape is `preventDefault`ed and stopped, so an enclosing dialog does not close on it. Slots: `approval-card` (root, `data-risk`, `data-confirm`, `data-armed`, `data-busy`, `data-decision`), `-header`, `-title`, `-description`, `-risk`, `-decision`, `-preview`, `-confirm-input`, `-actions`, `-edit`, `-reject`, `-approve` (`data-armed`, `data-holding`), `-hold`, `-hint`.\n\n### DiffReview — `@zuilib/ai/diff-review`\n\n```tsx\n<DiffReview original={document} modified={proposal} onDecisionsChange={(decisions, text) => setAccepted(text)} />\n<DiffReview mode=\"split\" original={before} modified={after} originalLabel=\"Current\" modifiedLabel=\"Assistant\" />\n<DiffReview patch={unifiedDiffFromServer} onAcceptHunk={apply} />\n<DiffReview original={a} modified={b} computeDiff={(request) => diffWorker.run(request)} />\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `original` / `modified` | `string` | the two texts |\n| `hunks` | `DiffHunk[]` | precomputed hunks (`diffHunks`, or your own); wins over `patch` and the texts |\n| `patch` | `string` | a unified diff, parsed with `parsePatch` |\n| `computeDiff` | `(request: DiffComputeRequest) => Promise<DiffHunksResult>` | computes the diff asynchronously (typically a Web Worker behind a promise); the card is `data-loading` / `aria-busy` until it resolves |\n| `mode` | `'unified' \\| 'split'` | `'unified'` |\n| `context` | `number` | `3` equal lines around each change |\n| `originalLabel` / `modifiedLabel` | `string` | `'Original'` / `'Proposed'` |\n| `decisions` | `Record<number, 'accepted' \\| 'rejected'>` | by hunk index; controlled. Omit it and the component keeps the decisions itself |\n| `onDecisionsChange` | `(decisions, accepted: string) => void` | every change, controlled or not, with the text the decisions produce |\n| `onAcceptHunk` / `onRejectHunk` | `(index: number, hunk: DiffHunk) => void` | per-hunk buttons appear when either, or `onDecisionsChange`, is given |\n| `onAcceptAll` / `onRejectAll` | `() => void` | header buttons appear when either, or `onDecisionsChange`, is given |\n| `maxLines` | `number` | `5000` lines per side handled by the exact LCS table; beyond it the Myers walk runs |\n| `hardMaxLines` | `number` | `200000` lines per side beyond which the diff is one replace hunk and `truncatedNotice` shows |\n| `truncatedNotice` | `ReactNode` | `'This diff is too large…'` |\n| `lineNumbers` | `boolean` | `true` |\n\nWithout `computeDiff`, the diff runs on deferred inputs (`useDeferredValue`): an urgent update (typing into the texts) renders with the previous hunks first and the recomputation follows; the root carries `data-loading` meanwhile. The accepted text is `applyDecisions(original, hunks, decisions)` when `original` is known and `applyHunkDecisions(hunks, decisions)` (the hunks' own lines) for `hunks` / `patch`.\n\nThe diff helpers are re-exported here and from `@zuilib/ai/diff-engine`: `diffLines(a, b, options)`, `diffHunks(a, b, context, options)`, the `…WithInfo` variants returning `{ lines | hunks, truncated, algorithm: 'lcs' | 'myers' | 'replace' }`, `myersDiff(aLines, bLines)` (linear-space Myers O(ND): a minimal edit script in O(N + M) memory, an explicit work stack, removals listed before additions within a run), `parsePatch(text)`, `toHunks`, `toSplitRows`, `applyDecisions`, `applyHunkDecisions`, `DEFAULT_MAX_DIFF_LINES`, `DEFAULT_HARD_MAX_DIFF_LINES`. In a container narrower than 384px (Tailwind's `@sm` container breakpoint: a phone column, an assistant dock) `mode=\"split\"` renders the unified layout instead; `data-mode` reports the layout actually shown. Slots: `diff-review` (root, `data-mode`, `data-algorithm`, `data-loading`), `-header`, `-summary`, `-actions`, `-accept-all`, `-reject-all`, `-truncated`, `-empty`, `-hunk` (`data-decision`), `-hunk-header`, `-hunk-decision`, `-hunk-accept`, `-hunk-reject`, `-table`, `-row`, `-cell` (`data-type`).\n\n### StructuredOutputForm — `@zuilib/ai/structured-output-form`\n\n```tsx\nconst fields: StructuredField[] = [\n  { name: 'company', label: 'Company', type: 'string', required: true },\n  { name: 'seats', label: 'Seats', type: 'number' },\n  { name: 'term', label: 'Term', type: 'enum', options: ['12', '24', '36'] },\n  { name: 'enterprise', label: 'Enterprise plan', type: 'boolean' },\n]\n<StructuredOutputForm fields={fields} value={value} onValueChange={setValue} errors={errors} onSubmit={save} />\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `fields` | `StructuredField[]` | required: `{ name, label, type, options?, description?, required?, placeholder? }`; `type` is `'string' \\| 'number' \\| 'boolean' \\| 'enum'` or any name a `renderField` handles; enum options are strings or `{ value, label }` |\n| `value` | `Record<string, string \\| number \\| boolean \\| null \\| undefined>` | required, controlled |\n| `onValueChange` | `(value, changedField: string) => void` | required: the next whole value and the field that changed |\n| `onSubmit` | `(value) => void` | |\n| `generated` | `string[]` | every field with a value on mount: the fields wearing the Generated badge until edited |\n| `onFieldChange` | `(name: string) => void` | first edit of a generated field |\n| `errors` | `Partial<Record<string, ReactNode>>` | validation messages by field name, rendered under the control as a `FieldError`; the field is marked invalid through `Field` |\n| `renderField` | `(props: StructuredFieldControlProps) => ReactNode \\| undefined` | called for every field first: return a control (`{ field, value, onValueChange, invalid, generated }`) to take the field over — a date picker, a currency input — inside the standard `Field`, or `undefined` to keep the built-in |\n| `submitLabel` / `generatedLabel` | `string` | `'Use these values'` / `'Generated'` |\n| `footer` | `ReactNode` | before the submit button |\n| `showSubmit` | `boolean` | `true` |\n\nEach field is the matching control (`Input`, `NumberInput`, `Switch`, `NativeSelect`) inside a `Field`, so labels, descriptions, ids and `aria-describedby` are wired; an unknown `type` with no `renderField` falls back to the text input, so a schema from a newer producer stays editable. Slots: `structured-output-form` (root `<form>`), `-field` (`data-type`, `data-generated`), `-generated`, `-error`, `-footer`, `-submit`.\n\n### Citation — `@zuilib/ai/citation`\n\n```tsx\n<p>At-risk MRR fell 8.5% <Citation index={1} source={sources[0]} />.</p>\n<CitationList sources={sources} />\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `index` | `number` | required, the number in the chip |\n| `source` | `CitationSource` | required: `{ id?, title, url?, snippet?, origin? }` |\n| `anchor` | `PopoverAnchor \\| false` | `'top'`; `false` renders the panel in place under the chip (keeps a subtree theme; an anchored panel portals to `<body>`) |\n| `className` | `string` | merged onto the chip |\n\nThe chip is a real button named \"Source n: title\" opening a small `Popover` with the title (a link, `target=\"_blank\"`, when `url` is given), origin and snippet. The root is a `<span>` and the panel holds no block elements, so it is phrasing content and sits inside a `<p>` without a hydration warning. `CitationList` (`Citation.List`) takes `sources: CitationSource[]`, `title` (`'Sources'`) and `className`. Slots: `citation` (root), `citation-chip`, `citation-panel`, `citation-index`, `citation-title`, `citation-origin`, `citation-snippet`, `citation-list`, `citation-list-title`, `citation-list-items`, `citation-list-item`.\n\n### Reasoning — `@zuilib/ai/reasoning`\n\n```tsx\n<Reasoning detail=\"Comparing usage against last quarter\" />\n```\n\n| Prop | Type | Default |\n|------|------|---------|\n| `label` | `ReactNode` | `'Working on it'` |\n| `detail` | `ReactNode` | what the model is doing right now, under the label |\n| `size` | `'sm' \\| 'md'` | `'md'` |\n| `icon` | `ReactNode` | the spark; replaces it |\n\nA `role=\"status\"` `aria-live=\"polite\"` region with a pulsing spark and three bouncing dots on the tokens keyframes, still under `prefers-reduced-motion`. Slots: `reasoning` (root, `data-size`), `-icon`, `-text`, `-label`, `-detail`, `-dots`, `-dot`.\n\n### Message model — `@zuilib/ai/message-parts`\n\nPure types shared by the widgets: `Message` is `{ id, role: 'user' | 'assistant' | 'system' | 'tool', createdAt?, parts: MessagePart[] }` and `MessagePart` is a discriminated union whose members carry the props of the card that renders them: `TextPart` `{ type: 'text', text, streaming? }` → `StreamingMarkdown`; `ToolCallPart` `{ type: 'tool-call', id, toolName, args?, status, description? }` and `ToolResultPart` `{ type: 'tool-result', callId, result?, error?, elapsedMs? }` → `ToolCallCard`; `CitationPart` `{ type: 'citation', sources }` → `CitationList`; `ApprovalPart` `{ type: 'approval', id, title, description?, risk?, confirm?, preview?, decision?, expiresAt? }` → `ApprovalCard`; `DiffPart` `{ type: 'diff', id, original?, modified?, hunks?, patch?, originalLabel?, modifiedLabel?, decisions? }` → `DiffReview`; `CustomPart` `{ type: 'custom', kind, id?, data? }` → the `PartRenderers` entry registered under `kind` (the open variant: the literal `type` keeps the union discriminated, `kind` carries the extension). `@zuilib/ai/thread-reducer` holds the pure folding: `applyEvent(parts, event)`, `finalizeStreamingText`, `decideApproval`, `decideDiff`, `toolResultFor`, `threadReducer`, `createMessageId` and `EMPTY_THREAD`.\n\nIcons (`SparkIcon`, `ToolIcon`, `PencilIcon`, `ExternalLinkIcon`, `SendIcon`, `StopIcon`, `PaperclipIcon`) are at `@zuilib/ai/icons`.\n","readmeFilename":"README.md","license":"MIT"}