feat(ui): group createChatHook options and pass layout slots as components - #1282
Conversation
`layout` now receives `Messages`, `Interrupts`, and `Input`; `message` receives `Parts`. `renderMessages`, `renderInterrupts`, `renderInput`, and `renderParts` are gone — this API ships first in this PR, so there is nothing released to migrate. React and Solid only. Vue and Svelte compose through slots and snippets and already had empty `LayoutProps`. `Messages` and `Interrupts` were already factory-closure declarations, so passing them directly is strictly more stable than the `useCallback` thunks they replace. `Parts` was the hard part: it closes over the message, so the naive version builds a new component per render and React remounts every part on each stream chunk. It now reads a message-scoped context that `MessageView` provides, which makes it one component for the factory's lifetime. `create-ui-stability.test.tsx` pins that: the same `Parts` identity must survive a re-render with a new message object. `Input` is on the layout props only when the config registers an `input`, so rendering one you never registered is a compile error rather than a silent no-op. This is inferred from a `TInput` parameter on the `input` property; a self-referential config constraint was tried first and silently collapses to `false`. Two inference caveats, both found by making the examples compile: - Referencing the factory's own result inside its config (`UI.useChatContext()`) is circular and blocks inference. The examples and the e2e route now use `createChatHookContexts()`, already the documented cycle-breaker. - An inline `function` expression for `input` also blocks it. Arrows and named references infer fine, so the examples hoist the component and both UI docs call the trap out. The default falls back to a union rather than `undefined` so that when inference does degrade, `input` stays assignable and only `Input` goes missing — a false negative instead of a rejected config. Two doc snippets rendered an input they never registered; the new types caught them. `pnpm test:ci` and `pnpm test:dts` pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The conditional `Input` type hides the prop when no `input` is registered, but that inference degrades on some config shapes, and the runtime had no component to render in that case — React threw "Element type is invalid" on an undefined element. Always supply a component and warn once instead, matching the existing "Missing tools.<name> component" warning. The floor is now consistent: you find out at build time where inference works, and on the first dev render where it doesn't. The inference trigger turned out to be narrower than the previous commit claimed. It is source order, not the `function` keyword: an inline `function` expression for `input` infers fine as long as it is listed before `layout`. Hoisting the component out of the config bought nothing — the config literal is evaluated once at module scope, so an inline component is already a stable reference — so the examples and the e2e route go back to inline named components, ordered `input` first. Both UI docs now describe the ordering rule rather than telling people to hoist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`chatComponents` is gone. Registration is now flat on the factory call,
one group per kind:
createChatHook({
options,
context: { chatContext, partContext, interruptContext },
components: { input, layout, message },
toolsComponents: { ... },
interruptsComponents: { tools: { ... }, generic: { ... } },
partsComponents: { ... },
})
`createChatUI` takes the same shape as its second argument, so both
entry points register widgets identically. All four adapters (React,
Solid, Vue, Svelte) move together. Vue and Svelte keep their flat
internal runtime shape; the new config is mapped onto it at the factory
boundary, so nothing downstream of that changes.
`partsComponents` is plural, unlike the original sketch — the registries
are `parts`/`tools`/`interrupts` and the maps hold many entries;
`PartProps` and `ChatUIPartKey` stay singular because they describe one
part.
This also removes the `Input` inference fragility rather than
documenting around it. `layout` and `input` are now siblings inside
`components`, so TypeScript resolves that inner literal as a unit and
fixes `TInput` before contextually typing `layout`. Declaration order no
longer matters and an inline `function` expression works, both of which
broke inference when these keys sat at the config's top level. The
ordering caveat is out of both UI docs, and a type test pins the case
that used to fail so a future flattening cannot regress it silently.
Per instruction, no migration guidance was added anywhere; the existing
`docs/migration/create-ui.md` page covers the older package move only
and has been updated to the new shape rather than extended.
`pnpm test:ci` and `pnpm test:dts` pass across all 86 projects.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`createChatHook` and `createChatUI` have never been published, so teaching them as something to migrate to was wrong. I checked before cutting: `@tanstack/ai-react-ui@0.8.22` (and Solid 0.7.21, Vue 0.2.40) export only the older `Chat` / `ChatMessages` / `ChatInput` compound components, with no `createChatHook` anywhere. So the page keeps the one migration that is real — swapping `@tanstack/ai-*-ui` for the framework `/ui` subpath, which those published packages do need — and loses the Before/After, Steps, and Gotchas sections that walked through adopting the new factory. The new API is documented in the UI guides as a new API. The `Chat` component's `@deprecated` notices pointed at that page for `createChatHook` guidance, so they now point at the matching UI guide. The `docs/api/*` pages no longer advertise the migration page as reading for the typed chat UI. The `*-ui` shim packages still link there, which is still exactly what that page covers. Also brings this branch in line with repo rules I had been working without, having only picked up the worktree's CLAUDE.md late: - Removed the em dashes the docs skill forbids from text added here. - Bumped `updatedAt` in `docs/config.json` for the five pages with real content changes. The four `docs/api/*` pages only lost a stale link, which the rule classes as a factual fix that must not bump. - Rewrote the `create-chat-hook` changeset to describe the shipping option groups instead of the old `chatComponents` shape, and aligned `typed-headless-chat-ui` with the new names. Since none of this has released, these describe the API rather than a change to it. `pnpm test:ci` and `pnpm test:dts` pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`CLAUDE.md` requires E2E coverage for a behavior change and this stack had none. The behavior worth guarding is the reason `Parts` reads a message-scoped context instead of closing over the message: a `Parts` rebuilt per render makes React remount every part on each stream chunk, throwing away local state in tool and part widgets. The purchase tool widget on `/headless-ui` now stamps a per-mount sequence into `data-mount-seq`, and the spec asserts it stays at 1 across the tool-call chunks and again across the resume stream after approval. Verified the test actually catches the regression rather than just passing: reverting `Parts` to a per-render closure and rebuilding takes the widget to 7 mounts, and the assertion fails on the first check. Full suite: 638 passed, 10 failed. Both failing specs (`multi-turn-structured` across providers, and the flaky `durable-takeover`) fail identically on the PR base `ad5c6c28d` with none of this stack's changes present, verified in a scratch worktree. They run against `examples/ts-react-chat`, which uses `useChat` and never touches the `/ui` surface. `multi-turn-structured` is a fixture-playback problem: turn 3 receives turn 2's recipe. `pnpm test:ci` and `pnpm test:dts` pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The framework guides open with one example that registers tools, interrupts, structured output, input and parts at once. A reader wiring their first chat box has to skip past all of it. These five pages each do one thing, in order: 1. `basic-chat` — layout, message, text part. No tools. 2. `format-a-tool` — one tool, branching on `part.state`. 3. `tool-approval` — `needsApproval`, inline or in the list. 4. `custom-interrupt` — `defineInterrupt` with your own schemas. 5. `request-context` — `forwardedProps` from screen to route. Two gaps these close: there was no starting point smaller than the full example, and nothing documented sending per-request context from the chat UI. `docs/advanced/runtime-context.md` covers only the server half, so the new page shows both and says plainly that `forwardedProps` is client-controlled and must not be trusted for identity. React only. The Solid, Vue and Svelte guides already point at the React page for the full map, and each now links here too. Named `recipes`, not `examples`: `scripts/verify-links.ts` treats any path containing `/examples/` as a directory link for the site's app examples, so `docs/ui/examples/*.md` reported every internal link broken. While writing these I checked the API instead of trusting the existing prose, and corrected two things in my own drafts: the terminal tool state is `complete`, not `output-available`, and `ToolCallPart` has no `errorMessage` field. I also confirmed the grouped `ToolProps` snippet is really type-checked by breaking it on purpose and watching kiira fail. All 1183 doc snippets typecheck. `pnpm test:ci` and `pnpm test:dts` pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
View your CI Pipeline Execution ↗ for commit 3310d68
☁️ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-acp
@tanstack/ai-angular
@tanstack/ai-anthropic
@tanstack/ai-bedrock
@tanstack/ai-byteplus
@tanstack/ai-claude-code
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-snippets
@tanstack/ai-codex
@tanstack/ai-cohere
@tanstack/ai-compaction
@tanstack/ai-devtools-core
@tanstack/ai-durable-stream
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-grok-build
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-daytona
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-isolate-quickjs-bun
@tanstack/ai-llmgateway
@tanstack/ai-lovable
@tanstack/ai-mcp
@tanstack/ai-memory
@tanstack/ai-mistral
@tanstack/ai-octane
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-opencode
@tanstack/ai-openrouter
@tanstack/ai-perplexity
@tanstack/ai-persistence
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-sandbox
@tanstack/ai-sandbox-cloudflare
@tanstack/ai-sandbox-daytona
@tanstack/ai-sandbox-docker
@tanstack/ai-sandbox-local-process
@tanstack/ai-sandbox-sprites
@tanstack/ai-sandbox-upstash-box
@tanstack/ai-sandbox-vercel
@tanstack/ai-skills
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-utils
@tanstack/ai-vercel-gateway
@tanstack/ai-vertex
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
@tanstack/svelte-ai-devtools
commit: |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Layout receives a stable Queue component that maps pending sends. Each item has cancelQueued bound. Register the row with components.queue and QueueProps.
* feat(ui): add createChatHook and move Chat UI onto /ui (#1263) * feat(client): add createChatHook bound useChat factory * feat(ui): align createChatUI with Form and Table factories * feat(ui): move chat UI onto framework /ui subpaths Fold the *-ui packages into @tanstack/ai-react/ui, @tanstack/ai-solid/ui, @tanstack/ai-vue/ui, and @tanstack/ai-svelte/ui. createChatHook({ options, chatComponents }) returns useAppChat (Svelte: createAppChat). useAppChat mixes AppChat onto the instance so screens render <chat.AppChat />. * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * feat(ui): deprecate ai-*-ui packages in favor of /ui Restore @tanstack/ai-react-ui, ai-solid-ui, ai-vue-ui, and ai-svelte-ui as thin re-exports of the framework /ui subpath until 1.0.0. Document createChatHook, chat.AppChat, and the import path change. * ci: apply automated fixes * chore(ui): drop unpublished @tanstack/ai-svelte-ui That package never shipped on npm. Svelte chat UI lives on @tanstack/ai-svelte/ui. Keep deprecated shims only for the published react, solid, and vue UI packages. * ci: apply automated fixes * fix(ui): green up the CI suite for createChatHook Nine tasks were failing on this branch. Fixes, in order of substance: - `ai-vue`/`ai-solid` ship their `/ui` subpath as source (Vue SFCs and Solid JSX must be compiled by the consumer), so `create-ui` is type-checked against the *consumer's* tsconfig. Both referenced a bare `process`, which broke any consumer without `@types/node` (this is what failed `ts-vue-chat`). Declare `process` locally, matching the `src/env.d.ts` shape the devtools packages already use, so the literal `process.env.NODE_ENV` bundlers substitute stays intact and the branch is still constant-folded in production. - `ts-react-ui-chatbot` imported `useChatContext` from `@/chat/ui-context`, which only exports the raw contexts. It comes off the factory in `./ui-components`, the way `layout.tsx` already imports it. - Docs: `docs/ui/react.md`'s widget-picking snippet referenced `UI` and `message` that no fence defined, and `p.getWeather` for a widget the example factory never registered. Grouped it with the factory fence and registered `getWeather`/`text`, so the snippet now type-checks against the real API instead of only looking right. - Docs: `docs/ui/vue.md`'s `StatusLine` and `ui` formed an inference cycle. Declare `StatusLine` after `ui` and annotate it; `h(StatusLine)` only runs at render time, so the runtime order is unchanged. - Split the mixed value/type imports in `ai-react`/`ai-solid` `create-ui` and drop the inline `import()` type in the Solid test (oxlint). - Drop devDeps the deprecation commit orphaned in the `ai-*-ui` packages, and alphabetize `ai-vue`'s (knip, sherif). `pnpm test:ci` and `pnpm test:dts` both pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(ui): group createChatHook options and pass layout slots as components (#1282) * feat(ui)!: hand layout and message components, not render thunks `layout` now receives `Messages`, `Interrupts`, and `Input`; `message` receives `Parts`. `renderMessages`, `renderInterrupts`, `renderInput`, and `renderParts` are gone — this API ships first in this PR, so there is nothing released to migrate. React and Solid only. Vue and Svelte compose through slots and snippets and already had empty `LayoutProps`. `Messages` and `Interrupts` were already factory-closure declarations, so passing them directly is strictly more stable than the `useCallback` thunks they replace. `Parts` was the hard part: it closes over the message, so the naive version builds a new component per render and React remounts every part on each stream chunk. It now reads a message-scoped context that `MessageView` provides, which makes it one component for the factory's lifetime. `create-ui-stability.test.tsx` pins that: the same `Parts` identity must survive a re-render with a new message object. `Input` is on the layout props only when the config registers an `input`, so rendering one you never registered is a compile error rather than a silent no-op. This is inferred from a `TInput` parameter on the `input` property; a self-referential config constraint was tried first and silently collapses to `false`. Two inference caveats, both found by making the examples compile: - Referencing the factory's own result inside its config (`UI.useChatContext()`) is circular and blocks inference. The examples and the e2e route now use `createChatHookContexts()`, already the documented cycle-breaker. - An inline `function` expression for `input` also blocks it. Arrows and named references infer fine, so the examples hoist the component and both UI docs call the trap out. The default falls back to a union rather than `undefined` so that when inference does degrade, `input` stays assignable and only `Input` goes missing — a false negative instead of a rejected config. Two doc snippets rendered an input they never registered; the new types caught them. `pnpm test:ci` and `pnpm test:dts` pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ui): warn instead of crashing on an unregistered Input The conditional `Input` type hides the prop when no `input` is registered, but that inference degrades on some config shapes, and the runtime had no component to render in that case — React threw "Element type is invalid" on an undefined element. Always supply a component and warn once instead, matching the existing "Missing tools.<name> component" warning. The floor is now consistent: you find out at build time where inference works, and on the first dev render where it doesn't. The inference trigger turned out to be narrower than the previous commit claimed. It is source order, not the `function` keyword: an inline `function` expression for `input` infers fine as long as it is listed before `layout`. Hoisting the component out of the config bought nothing — the config literal is evaluated once at module scope, so an inline component is already a stable reference — so the examples and the e2e route go back to inline named components, ordered `input` first. Both UI docs now describe the ordering rule rather than telling people to hoist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(ui)!: split createChatHook options into named component groups `chatComponents` is gone. Registration is now flat on the factory call, one group per kind: createChatHook({ options, context: { chatContext, partContext, interruptContext }, components: { input, layout, message }, toolsComponents: { ... }, interruptsComponents: { tools: { ... }, generic: { ... } }, partsComponents: { ... }, }) `createChatUI` takes the same shape as its second argument, so both entry points register widgets identically. All four adapters (React, Solid, Vue, Svelte) move together. Vue and Svelte keep their flat internal runtime shape; the new config is mapped onto it at the factory boundary, so nothing downstream of that changes. `partsComponents` is plural, unlike the original sketch — the registries are `parts`/`tools`/`interrupts` and the maps hold many entries; `PartProps` and `ChatUIPartKey` stay singular because they describe one part. This also removes the `Input` inference fragility rather than documenting around it. `layout` and `input` are now siblings inside `components`, so TypeScript resolves that inner literal as a unit and fixes `TInput` before contextually typing `layout`. Declaration order no longer matters and an inline `function` expression works, both of which broke inference when these keys sat at the config's top level. The ordering caveat is out of both UI docs, and a type test pins the case that used to fail so a future flattening cannot regress it silently. Per instruction, no migration guidance was added anywhere; the existing `docs/migration/create-ui.md` page covers the older package move only and has been updated to the new shape rather than extended. `pnpm test:ci` and `pnpm test:dts` pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: drop migration guidance for the unreleased chat UI API `createChatHook` and `createChatUI` have never been published, so teaching them as something to migrate to was wrong. I checked before cutting: `@tanstack/ai-react-ui@0.8.22` (and Solid 0.7.21, Vue 0.2.40) export only the older `Chat` / `ChatMessages` / `ChatInput` compound components, with no `createChatHook` anywhere. So the page keeps the one migration that is real — swapping `@tanstack/ai-*-ui` for the framework `/ui` subpath, which those published packages do need — and loses the Before/After, Steps, and Gotchas sections that walked through adopting the new factory. The new API is documented in the UI guides as a new API. The `Chat` component's `@deprecated` notices pointed at that page for `createChatHook` guidance, so they now point at the matching UI guide. The `docs/api/*` pages no longer advertise the migration page as reading for the typed chat UI. The `*-ui` shim packages still link there, which is still exactly what that page covers. Also brings this branch in line with repo rules I had been working without, having only picked up the worktree's CLAUDE.md late: - Removed the em dashes the docs skill forbids from text added here. - Bumped `updatedAt` in `docs/config.json` for the five pages with real content changes. The four `docs/api/*` pages only lost a stale link, which the rule classes as a factual fix that must not bump. - Rewrote the `create-chat-hook` changeset to describe the shipping option groups instead of the old `chatComponents` shape, and aligned `typed-headless-chat-ui` with the new names. Since none of this has released, these describe the API rather than a change to it. `pnpm test:ci` and `pnpm test:dts` pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(e2e): pin the Parts subtree against remounting mid-stream `CLAUDE.md` requires E2E coverage for a behavior change and this stack had none. The behavior worth guarding is the reason `Parts` reads a message-scoped context instead of closing over the message: a `Parts` rebuilt per render makes React remount every part on each stream chunk, throwing away local state in tool and part widgets. The purchase tool widget on `/headless-ui` now stamps a per-mount sequence into `data-mount-seq`, and the spec asserts it stays at 1 across the tool-call chunks and again across the resume stream after approval. Verified the test actually catches the regression rather than just passing: reverting `Parts` to a per-render closure and rebuilding takes the widget to 7 mounts, and the assertion fails on the first check. Full suite: 638 passed, 10 failed. Both failing specs (`multi-turn-structured` across providers, and the flaky `durable-takeover`) fail identically on the PR base `ad5c6c28d` with none of this stack's changes present, verified in a scratch worktree. They run against `examples/ts-react-chat`, which uses `useChat` and never touches the `/ui` surface. `multi-turn-structured` is a fixture-playback problem: turn 3 receives turn 2's recipe. `pnpm test:ci` and `pnpm test:dts` pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(ui): add five chat UI recipes The framework guides open with one example that registers tools, interrupts, structured output, input and parts at once. A reader wiring their first chat box has to skip past all of it. These five pages each do one thing, in order: 1. `basic-chat` — layout, message, text part. No tools. 2. `format-a-tool` — one tool, branching on `part.state`. 3. `tool-approval` — `needsApproval`, inline or in the list. 4. `custom-interrupt` — `defineInterrupt` with your own schemas. 5. `request-context` — `forwardedProps` from screen to route. Two gaps these close: there was no starting point smaller than the full example, and nothing documented sending per-request context from the chat UI. `docs/advanced/runtime-context.md` covers only the server half, so the new page shows both and says plainly that `forwardedProps` is client-controlled and must not be trusted for identity. React only. The Solid, Vue and Svelte guides already point at the React page for the full map, and each now links here too. Named `recipes`, not `examples`: `scripts/verify-links.ts` treats any path containing `/examples/` as a directory link for the site's app examples, so `docs/ui/examples/*.md` reported every internal link broken. While writing these I checked the API instead of trusting the existing prose, and corrected two things in my own drafts: the terminal tool state is `complete`, not `output-available`, and `ToolCallPart` has no `errorMessage` field. I also confirmed the grouped `ToolProps` snippet is really type-checked by breaking it on purpose and watching kiira fail. All 1183 doc snippets typecheck. `pnpm test:ci` and `pnpm test:dts` pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: apply automated fixes * feat(ui): add Queue layout slot and QueueProps Layout receives a stable Queue component that maps pending sends. Each item has cancelQueued bound. Register the row with components.queue and QueueProps. * ci: apply automated fixes --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Alem Tuzlak <t.zlak@hotmail.com> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jack Herrington <jack.herrington@netlify.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Jack Herrington <jherr@pobox.com>
* feat(ui): add typed headless createUI adapters * docs(ui): document typed props, context, and interrupt placement Teach ToolProps, RegisteredInterruptProps, and UI.useChat. Show tool approvals inline in the tool or in the interrupt list. Put registered generic interrupts under interrupts.generic next to fallback. Unbound interrupts use fallback. Check interrupt.kind when the copy must differ. * feat(ui): pin InterruptProps and add the typed chatbot example InterruptProps takes a tool name or a registered interrupt id. A tool approval renders in the tool when interrupts.tools omits that name. examples/ts-react-ui-chatbot shows parts, list vs in-tool approval, BYOK, and transcription. * fix(ui): export createChatUI and type-check Vue and Svelte examples Rename the factory to createChatUI. Register every guitar tool in the Vue and Svelte examples so defineComponents type-checks. Point the UI docs at gpt-5.6 and @tanstack/ai-svelte-ui 0.2.0. * feat(ui): move createChatUI onto framework /ui subpaths Chat UI now lives at @tanstack/ai-react/ui, @tanstack/ai-solid/ui, @tanstack/ai-vue/ui, and @tanstack/ai-svelte/ui. The published @tanstack/ai-react-ui, @tanstack/ai-solid-ui, and @tanstack/ai-vue-ui packages re-export those subpaths until 1.0.0. @tanstack/ai-svelte-ui was never published, so that package is gone. * feat(ui): add createChatHook and move Chat UI onto /ui (#1275) * feat(ui): add createChatHook and move Chat UI onto /ui (#1263) * feat(client): add createChatHook bound useChat factory * feat(ui): align createChatUI with Form and Table factories * feat(ui): move chat UI onto framework /ui subpaths Fold the *-ui packages into @tanstack/ai-react/ui, @tanstack/ai-solid/ui, @tanstack/ai-vue/ui, and @tanstack/ai-svelte/ui. createChatHook({ options, chatComponents }) returns useAppChat (Svelte: createAppChat). useAppChat mixes AppChat onto the instance so screens render <chat.AppChat />. * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * feat(ui): deprecate ai-*-ui packages in favor of /ui Restore @tanstack/ai-react-ui, ai-solid-ui, ai-vue-ui, and ai-svelte-ui as thin re-exports of the framework /ui subpath until 1.0.0. Document createChatHook, chat.AppChat, and the import path change. * ci: apply automated fixes * chore(ui): drop unpublished @tanstack/ai-svelte-ui That package never shipped on npm. Svelte chat UI lives on @tanstack/ai-svelte/ui. Keep deprecated shims only for the published react, solid, and vue UI packages. * ci: apply automated fixes * fix(ui): green up the CI suite for createChatHook Nine tasks were failing on this branch. Fixes, in order of substance: - `ai-vue`/`ai-solid` ship their `/ui` subpath as source (Vue SFCs and Solid JSX must be compiled by the consumer), so `create-ui` is type-checked against the *consumer's* tsconfig. Both referenced a bare `process`, which broke any consumer without `@types/node` (this is what failed `ts-vue-chat`). Declare `process` locally, matching the `src/env.d.ts` shape the devtools packages already use, so the literal `process.env.NODE_ENV` bundlers substitute stays intact and the branch is still constant-folded in production. - `ts-react-ui-chatbot` imported `useChatContext` from `@/chat/ui-context`, which only exports the raw contexts. It comes off the factory in `./ui-components`, the way `layout.tsx` already imports it. - Docs: `docs/ui/react.md`'s widget-picking snippet referenced `UI` and `message` that no fence defined, and `p.getWeather` for a widget the example factory never registered. Grouped it with the factory fence and registered `getWeather`/`text`, so the snippet now type-checks against the real API instead of only looking right. - Docs: `docs/ui/vue.md`'s `StatusLine` and `ui` formed an inference cycle. Declare `StatusLine` after `ui` and annotate it; `h(StatusLine)` only runs at render time, so the runtime order is unchanged. - Split the mixed value/type imports in `ai-react`/`ai-solid` `create-ui` and drop the inline `import()` type in the Solid test (oxlint). - Drop devDeps the deprecation commit orphaned in the `ai-*-ui` packages, and alphabetize `ai-vue`'s (knip, sherif). `pnpm test:ci` and `pnpm test:dts` both pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(ui): group createChatHook options and pass layout slots as components (#1282) * feat(ui)!: hand layout and message components, not render thunks `layout` now receives `Messages`, `Interrupts`, and `Input`; `message` receives `Parts`. `renderMessages`, `renderInterrupts`, `renderInput`, and `renderParts` are gone — this API ships first in this PR, so there is nothing released to migrate. React and Solid only. Vue and Svelte compose through slots and snippets and already had empty `LayoutProps`. `Messages` and `Interrupts` were already factory-closure declarations, so passing them directly is strictly more stable than the `useCallback` thunks they replace. `Parts` was the hard part: it closes over the message, so the naive version builds a new component per render and React remounts every part on each stream chunk. It now reads a message-scoped context that `MessageView` provides, which makes it one component for the factory's lifetime. `create-ui-stability.test.tsx` pins that: the same `Parts` identity must survive a re-render with a new message object. `Input` is on the layout props only when the config registers an `input`, so rendering one you never registered is a compile error rather than a silent no-op. This is inferred from a `TInput` parameter on the `input` property; a self-referential config constraint was tried first and silently collapses to `false`. Two inference caveats, both found by making the examples compile: - Referencing the factory's own result inside its config (`UI.useChatContext()`) is circular and blocks inference. The examples and the e2e route now use `createChatHookContexts()`, already the documented cycle-breaker. - An inline `function` expression for `input` also blocks it. Arrows and named references infer fine, so the examples hoist the component and both UI docs call the trap out. The default falls back to a union rather than `undefined` so that when inference does degrade, `input` stays assignable and only `Input` goes missing — a false negative instead of a rejected config. Two doc snippets rendered an input they never registered; the new types caught them. `pnpm test:ci` and `pnpm test:dts` pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ui): warn instead of crashing on an unregistered Input The conditional `Input` type hides the prop when no `input` is registered, but that inference degrades on some config shapes, and the runtime had no component to render in that case — React threw "Element type is invalid" on an undefined element. Always supply a component and warn once instead, matching the existing "Missing tools.<name> component" warning. The floor is now consistent: you find out at build time where inference works, and on the first dev render where it doesn't. The inference trigger turned out to be narrower than the previous commit claimed. It is source order, not the `function` keyword: an inline `function` expression for `input` infers fine as long as it is listed before `layout`. Hoisting the component out of the config bought nothing — the config literal is evaluated once at module scope, so an inline component is already a stable reference — so the examples and the e2e route go back to inline named components, ordered `input` first. Both UI docs now describe the ordering rule rather than telling people to hoist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(ui)!: split createChatHook options into named component groups `chatComponents` is gone. Registration is now flat on the factory call, one group per kind: createChatHook({ options, context: { chatContext, partContext, interruptContext }, components: { input, layout, message }, toolsComponents: { ... }, interruptsComponents: { tools: { ... }, generic: { ... } }, partsComponents: { ... }, }) `createChatUI` takes the same shape as its second argument, so both entry points register widgets identically. All four adapters (React, Solid, Vue, Svelte) move together. Vue and Svelte keep their flat internal runtime shape; the new config is mapped onto it at the factory boundary, so nothing downstream of that changes. `partsComponents` is plural, unlike the original sketch — the registries are `parts`/`tools`/`interrupts` and the maps hold many entries; `PartProps` and `ChatUIPartKey` stay singular because they describe one part. This also removes the `Input` inference fragility rather than documenting around it. `layout` and `input` are now siblings inside `components`, so TypeScript resolves that inner literal as a unit and fixes `TInput` before contextually typing `layout`. Declaration order no longer matters and an inline `function` expression works, both of which broke inference when these keys sat at the config's top level. The ordering caveat is out of both UI docs, and a type test pins the case that used to fail so a future flattening cannot regress it silently. Per instruction, no migration guidance was added anywhere; the existing `docs/migration/create-ui.md` page covers the older package move only and has been updated to the new shape rather than extended. `pnpm test:ci` and `pnpm test:dts` pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: drop migration guidance for the unreleased chat UI API `createChatHook` and `createChatUI` have never been published, so teaching them as something to migrate to was wrong. I checked before cutting: `@tanstack/ai-react-ui@0.8.22` (and Solid 0.7.21, Vue 0.2.40) export only the older `Chat` / `ChatMessages` / `ChatInput` compound components, with no `createChatHook` anywhere. So the page keeps the one migration that is real — swapping `@tanstack/ai-*-ui` for the framework `/ui` subpath, which those published packages do need — and loses the Before/After, Steps, and Gotchas sections that walked through adopting the new factory. The new API is documented in the UI guides as a new API. The `Chat` component's `@deprecated` notices pointed at that page for `createChatHook` guidance, so they now point at the matching UI guide. The `docs/api/*` pages no longer advertise the migration page as reading for the typed chat UI. The `*-ui` shim packages still link there, which is still exactly what that page covers. Also brings this branch in line with repo rules I had been working without, having only picked up the worktree's CLAUDE.md late: - Removed the em dashes the docs skill forbids from text added here. - Bumped `updatedAt` in `docs/config.json` for the five pages with real content changes. The four `docs/api/*` pages only lost a stale link, which the rule classes as a factual fix that must not bump. - Rewrote the `create-chat-hook` changeset to describe the shipping option groups instead of the old `chatComponents` shape, and aligned `typed-headless-chat-ui` with the new names. Since none of this has released, these describe the API rather than a change to it. `pnpm test:ci` and `pnpm test:dts` pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(e2e): pin the Parts subtree against remounting mid-stream `CLAUDE.md` requires E2E coverage for a behavior change and this stack had none. The behavior worth guarding is the reason `Parts` reads a message-scoped context instead of closing over the message: a `Parts` rebuilt per render makes React remount every part on each stream chunk, throwing away local state in tool and part widgets. The purchase tool widget on `/headless-ui` now stamps a per-mount sequence into `data-mount-seq`, and the spec asserts it stays at 1 across the tool-call chunks and again across the resume stream after approval. Verified the test actually catches the regression rather than just passing: reverting `Parts` to a per-render closure and rebuilding takes the widget to 7 mounts, and the assertion fails on the first check. Full suite: 638 passed, 10 failed. Both failing specs (`multi-turn-structured` across providers, and the flaky `durable-takeover`) fail identically on the PR base `ad5c6c28d` with none of this stack's changes present, verified in a scratch worktree. They run against `examples/ts-react-chat`, which uses `useChat` and never touches the `/ui` surface. `multi-turn-structured` is a fixture-playback problem: turn 3 receives turn 2's recipe. `pnpm test:ci` and `pnpm test:dts` pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(ui): add five chat UI recipes The framework guides open with one example that registers tools, interrupts, structured output, input and parts at once. A reader wiring their first chat box has to skip past all of it. These five pages each do one thing, in order: 1. `basic-chat` — layout, message, text part. No tools. 2. `format-a-tool` — one tool, branching on `part.state`. 3. `tool-approval` — `needsApproval`, inline or in the list. 4. `custom-interrupt` — `defineInterrupt` with your own schemas. 5. `request-context` — `forwardedProps` from screen to route. Two gaps these close: there was no starting point smaller than the full example, and nothing documented sending per-request context from the chat UI. `docs/advanced/runtime-context.md` covers only the server half, so the new page shows both and says plainly that `forwardedProps` is client-controlled and must not be trusted for identity. React only. The Solid, Vue and Svelte guides already point at the React page for the full map, and each now links here too. Named `recipes`, not `examples`: `scripts/verify-links.ts` treats any path containing `/examples/` as a directory link for the site's app examples, so `docs/ui/examples/*.md` reported every internal link broken. While writing these I checked the API instead of trusting the existing prose, and corrected two things in my own drafts: the terminal tool state is `complete`, not `output-available`, and `ToolCallPart` has no `errorMessage` field. I also confirmed the grouped `ToolProps` snippet is really type-checked by breaking it on purpose and watching kiira fail. All 1183 doc snippets typecheck. `pnpm test:ci` and `pnpm test:dts` pass across all 86 projects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: apply automated fixes * feat(ui): add Queue layout slot and QueueProps Layout receives a stable Queue component that maps pending sends. Each item has cancelQueued bound. Register the row with components.queue and QueueProps. * ci: apply automated fixes --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Alem Tuzlak <t.zlak@hotmail.com> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jack Herrington <jack.herrington@netlify.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Jack Herrington <jherr@pobox.com> * fix(sandbox): do not leave completed runs marked detached * feat(ui): add createChatHook on Preact, Octane, and Angular /ui * ci: apply automated fixes * fix(angular): compile Chat UI as an ng-packagr secondary entry ng-packagr compiled src/ui twice and crashed with referencedFiles. Move the /ui sources to ui/ next to src. Import the primary package by name. Point export types at the generated d.ts file. Type the Preact and Octane doc snippets so kiira can check them. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jack Herrington <jack.herrington@netlify.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Jack Herrington <jherr@pobox.com>
Registering chat UI components now reads as named groups instead of one
chatComponentsbag, andlayoutreceivesMessages,Interrupts, andInputas components rather thanrenderX()thunks.messagereceivesPartsthe same way. Five new recipe pages take a reader from a plain chat box up to per-request context.Stacked on #1275. Review that one first.
🎯 Changes
Option groups.
createChatHookandcreateChatUItakecontext,components,toolsComponents,interruptsComponents, andpartsComponents.chatComponentsis gone.Components, not thunks.
layoutgetsMessages/Interrupts/Input;messagegetsParts.renderMessages,renderInterrupts,renderInput, andrenderPartsare gone.Partsis the one that needed care. It closes over the message, so building it per render makes React remount every part on each stream chunk, which throws away local state in tool and part widgets. It now reads a message-scoped context, so the kit hands out one stable component.Typed
Input.Inputis on the layout props only when the config registers aninput, so rendering one you never registered fails to compile. When that inference cannot run, the runtime warns once instead of crashing on an undefined element.Docs. Five recipes under
docs/ui/recipes/. Migration guidance forcreateChatHookis removed, because that API has never shipped: the published@tanstack/ai-react-ui@0.8.22exports only the olderChat/ChatMessagescomponents. The page keeps the real migration, which is the@tanstack/ai-*-uito/uipackage swap.All four adapters move together: React, Solid, Vue, Svelte.
✅ Checklist
pnpm run test:pr, or these tests do not apply to this pull request.docs/for this change, or this change is not user-facing.pnpm changeset), or this PR does not change a published package.🚀 Release Impact
Existing changesets are rewritten rather than added to.
createChatHookhas not released, so they describe the shipping API instead of a change to it.Testing
Commands run.
pnpm test:prpasses.pnpm test:ciandpnpm test:dtspass across all 86 projects.pnpm --filter @tanstack/ai-e2e test:e2e: 638 passed, 10 failed. Both failing specs fail the same way on this PR's base with none of these changes applied, checked in a scratch worktree. They run againstexamples/ts-react-chat, which never imports/ui.multi-turn-structuredis a fixture problem: turn 3 receives turn 2's recipe.durable-takeoverpasses on re-run.Manual test.
pnpm install && pnpm build:allcd examples/ts-react-ui-chatbot && pnpm devlayoutwith component props.docs/ui/recipes/basic-chat.mdand copy the client snippet into a new app. It runs as written.How this PR makes testing easy.
testing/e2e/tests/headless-ui.spec.tsasserts thePartssubtree is not remounted mid-stream, using adata-mount-seqstamp on the tool widget. RevertingPartsto a per-render closure takes that to 7 mounts and the test fails.create-ui-types.test.tsxpin the conditionalInput, including the ordering case that used to break inference silently.Risk / rollback
Low. Nothing here has been published, so no installed version changes behaviour. The blast radius is the
/uisurface plus the docs.To roll back, revert this PR. #1275 stands on its own.
Public API change
Before
After