Skip to content

feat: configurable client-side message queue for useChat - #900

Merged
AlemTuzlak merged 27 commits into
mainfrom
feat/client-message-queue
Jul 17, 2026
Merged

feat: configurable client-side message queue for useChat#900
AlemTuzlak merged 27 commits into
mainfrom
feat/client-message-queue

Conversation

@AlemTuzlak

@AlemTuzlakAlemTuzlak commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

What & why

Today sendMessagesilently drops any message sent while a stream is in flight. This adds a first-class, observable, configurable message queue: messages sent mid-stream are queued by default, rendered separately, auto-sent when the stream settles, and cancellable before they go out.

const{ messages, queue, sendMessage, cancelQueued }=useChat({connection: fetchServerSentEvents('/api/chat'),queue: {whenBusy: 'queue',// 'queue' | 'drop' | 'interrupt'drain: 'fifo',// 'fifo' | 'batch'maxSize: 5,onOverflow: 'reject',// 'reject' | 'drop-oldest'},})// shorthand: queue: 'interrupt' | escape hatch: queue: (ctx) => ({ action })sendMessage('urgent',{whenBusy: 'interrupt'})// per-send overridequeue.map((q)=><Pendingkey={q.id}onCancel={()=>cancelQueued(q.id)}/>)

Behavior

  • whenBusyqueue (hold + auto-send), drop (ignore, the old behavior), interrupt (abort current stream, send now).
  • drainfifo (one at a time, in order) or batch (merge all queued into one send: strings joined by \n, multimodal content concatenated).
  • maxSize / onOverflow — cap the queue; reject the incoming send or evict the oldest.
  • Accepts a WhenBusy string shorthand or a QueueStrategy function for full control.
  • Queue flushes on stop() / clear() / unsubscribe() and on stream error (never strands or mis-orders queued sends).
  • Queued items live in their own array — never mixed into messages, never sent to the wire until drained.

⚠️ Behavior change

Sends while streaming are now queued by default (previously dropped). Opt back into the old behavior with queue: 'drop'. Flagged as a minor bump in the changeset.

Surface

  • @tanstack/ai-client — core: queue option, QueuedMessage, getQueue(), cancelQueued(), onQueueChange, FIFO/batch drain, lifecycle + error flush, devtools snapshot.
  • @tanstack/ai-react / -solid / -vue / -svelte / -preact — expose queue + cancelQueued, per-send { whenBusy } override; ai-vue-ui forwards automatically.

Tests & docs

  • Unit tests in ai-client (each whenBusy mode, FIFO order, batch merge incl. multimodal, maxSize/overflow, cancel, flush-on-stop/clear/error) + framework hook tests updated for queue-by-default.
  • E2E (testing/e2e/tests/queue.spec.ts): send → queue-while-streaming → cancel one → assert only the uncancelled message is delivered, in order.
  • Docs: new "Queueing messages" section in docs/chat/streaming.md; chat-experience agent skill updated.
  • pnpm test:pr green across all 50 projects.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Default queueing for sendMessage while a stream is active, with configurable whenBusy (queue/drop/interrupt) and drain/overflow strategies.
    • Added queued-message controls: exposed queue state, cancelQueued(id), and per-send { whenBusy } overrides across chat hooks/components.
    • Extended devtools snapshots and demo UI to display and cancel pending items.
  • Bug Fixes
    • Busy sends are no longer silently dropped; queued items drain automatically, with flush on stop/error/clear/reload.
  • Documentation
    • Added “Queueing Messages” guidance, including drain vs flush and queue strategy behavior.
  • Tests
    • Added/updated unit and Playwright coverage for queuing, cancellation, ordering, drain/batch merging, and edge cases.

@github-actions

github-actionsBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

7 package(s) bumped directly, 3 bumped as dependents.

🟥 Major bumps

PackageVersionReason
@tanstack/ai-react-ui0.8.14 → 1.0.0Dependent
@tanstack/ai-solid-ui0.7.13 → 1.0.0Dependent

🟨 Minor bumps

PackageVersionReason
@tanstack/ai-angular0.2.4 → 0.3.0Changeset
@tanstack/ai-client0.21.0 → 0.22.0Changeset
@tanstack/ai-preact0.10.4 → 0.11.0Changeset
@tanstack/ai-react0.17.0 → 0.18.0Changeset
@tanstack/ai-solid0.14.4 → 0.15.0Changeset
@tanstack/ai-svelte0.14.4 → 0.15.0Changeset
@tanstack/ai-vue0.14.4 → 0.15.0Changeset

🟩 Patch bumps

PackageVersionReason
@tanstack/ai-vue-ui0.2.32 → 0.2.33Dependent

@nx-cloud

nx-cloudBot commented Jul 6, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 6c767cd

CommandStatusDurationResult
nx affected --targets=test:sherif,test:knip,tes...✅ Succeeded5sView ↗

☁️ Nx Cloud last updated this comment at 2026-07-17 07:39:37 UTC

@coderabbitai

coderabbitaiBot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds default queueing for sendMessage calls during active streams, configurable busy handling, queue draining and cancellation, and queue state exposure. The API is integrated across framework adapters, with documentation, examples, and end-to-end coverage.

Changes

Message Queueing Feature

Layer / File(s)Summary
Queue contracts and public exports
packages/ai-client/src/{types,index,devtools}.ts, packages/ai-*/src/{types,index}.ts, packages/ai-angular/src/{types,index}.ts, .changeset/message-queue.md
Adds queue configuration, queued-message, strategy, and per-send types; exposes queue state and controls through public package types and documents the API.
ChatClient queue engine
packages/ai-client/src/chat-client.ts
Implements normalization, busy-send decisions, enqueueing, overflow handling, FIFO/batch draining, cancellation, flushing, lifecycle integration, callbacks, and devtools snapshots.
ChatClient queue validation
packages/ai-client/tests/chat-client-queue.test.ts, packages/ai-client/tests/chat-client.test.ts
Tests queue policies, normalization, cancellation, draining, batching, overflow, interruption, flushing, and ordered concurrent delivery.
Framework adapter queue integration
packages/ai-preact/*, packages/ai-react/*, packages/ai-solid/*, packages/ai-svelte/*, packages/ai-vue/*, packages/ai-angular/*
Threads queue configuration, reactive queue state, cancelQueued, and per-send whenBusy options through framework APIs.
Framework concurrency coverage
packages/ai-*/tests/use-chat.test.ts
Updates adapter tests to expect queued sends to drain automatically in order.
Documentation and queueing example
docs/chat/streaming.md, docs/config.json, packages/ai/skills/.../SKILL.md, examples/ts-react-chat/src/...
Documents queue behavior and adds a routed example comparing FIFO, interrupt, and batch strategies.
E2E queue UI and spec
testing/e2e/fixtures/queue/*, testing/e2e/src/..., testing/e2e/tests/queue.spec.ts
Adds queue rendering and cancellation controls and verifies queued sends, cancellation, FIFO draining, and transcript ordering.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • TanStack/ai#632: Both changes modify devtools snapshot and bridge-related queue/state plumbing.
  • TanStack/ai#762: Both changes extend Angular injectChat integration and its reactive chat API.

Suggested reviewers:tombeckenham

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check nameStatusExplanationResolution
Description check⚠️ WarningThe description is detailed but does not follow the repository template and omits the required Checklist and Release Impact sections.Rewrite it to match the template with separate 🎯 Changes, ✅ Checklist, and 🚀 Release Impact sections, including the required checkboxes.
Docstring Coverage⚠️ WarningDocstring coverage is 37.50% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main change: adding a configurable client-side message queue for useChat.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/client-message-queue

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-newBot commented Jul 6, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@900

@tanstack/ai-acp

npm i https://pkg.pr.new/@tanstack/ai-acp@900

@tanstack/ai-angular

npm i https://pkg.pr.new/@tanstack/ai-angular@900

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@900

@tanstack/ai-bedrock

npm i https://pkg.pr.new/@tanstack/ai-bedrock@900

@tanstack/ai-claude-code

npm i https://pkg.pr.new/@tanstack/ai-claude-code@900

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@900

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@900

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@900

@tanstack/ai-codex

npm i https://pkg.pr.new/@tanstack/ai-codex@900

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@900

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@900

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@900

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@900

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@900

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@900

@tanstack/ai-grok-build

npm i https://pkg.pr.new/@tanstack/ai-grok-build@900

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@900

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@900

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@900

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@900

@tanstack/ai-mcp

npm i https://pkg.pr.new/@tanstack/ai-mcp@900

@tanstack/ai-mistral

npm i https://pkg.pr.new/@tanstack/ai-mistral@900

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@900

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@900

@tanstack/ai-opencode

npm i https://pkg.pr.new/@tanstack/ai-opencode@900

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@900

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@900

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@900

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@900

@tanstack/ai-sandbox

npm i https://pkg.pr.new/@tanstack/ai-sandbox@900

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-sandbox-cloudflare@900

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/@tanstack/ai-sandbox-daytona@900

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/@tanstack/ai-sandbox-docker@900

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/@tanstack/ai-sandbox-local-process@900

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/@tanstack/ai-sandbox-sprites@900

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/@tanstack/ai-sandbox-vercel@900

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@900

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@900

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@900

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@900

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@900

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@900

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@900

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@900

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@900

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@900

commit: 6c767cd

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
packages/ai-solid/src/use-chat.ts (1)

141-144: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Same queue-sync gap as the React adapter.

options.queue is only captured once when the client is created; there's no createEffect re-applying it via client().updateOptions(...) the way body/forwardedProps/context are synced at Line 152-165. See the parallel comment on packages/ai-react/src/use-chat.ts (Line 162-168) — same underlying question of whether queue is meant to be static-per-session or live-updatable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-solid/src/use-chat.ts` around lines 141 - 144, The chat queue is
only read once when the client is created in useChat, so later changes to
options.queue are never synced into the live client. Add a createEffect
alongside the existing body/forwardedProps/context syncing that calls
client().updateOptions(...) whenever options.queue changes, using the same
pattern as the other reactive option updates in useChat so the queue stays in
sync if it is meant to be live-updatable.
🧹 Nitpick comments (3)
packages/ai-preact/src/use-chat.ts (1)

157-163: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

queue config isn't kept in sync after mount.

Unlike body/forwardedProps/context (synced via the updateOptions effect below), queue is only read once at client construction (useMemo keyed on [clientId]). If a consumer changes options.queue (e.g. toggling whenBusy) after mount without changing id, the change is silently ignored. This mirrors the existing behavior for other one-time options like tools/persistence, so it's consistent with current design, but worth confirming it's intentional for a config surface consumers may want to adjust dynamically (e.g., per-message whenBusy overrides already exist for that case).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-preact/src/use-chat.ts` around lines 157 - 163, The `queue`
option in `useChat` is only applied when the client is created in the `useMemo`
block, so later changes to `options.queue` are ignored after mount. Update the
`updateOptions` path in `use-chat.ts` (the effect that already keeps `body`,
`forwardedProps`, and `context` in sync) to also propagate `queue` when
`optionsRef.current.queue` changes, using the same `activeClientRef.current ===
instance` guard as `onQueueChange`. If `queue` is meant to stay one-time like
`tools`/`persistence`, make that behavior explicit in the option handling and
docs.
packages/ai-client/src/chat-client.ts (1)

803-846: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the new sendOptions parameter.

The extensive JSDoc/example block for sendMessage wasn't updated to mention the new third parameter (sendOptions.whenBusy), even though it's a public API addition central to this PR.

📝 Suggested doc addition
 * `@param` body - Optional body parameters to merge with the client's base body for this request.
* Uses shallow merge with per-message body taking priority.
+ * `@param` sendOptions - Optional per-call overrides, e.g. `{ whenBusy: 'interrupt' }` to+ * override the configured queue policy for this one send.
*
* `@example`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-client/src/chat-client.ts` around lines 803 - 846, The public
JSDoc for sendMessage is missing the new third parameter, so update the
documentation block for sendMessage to describe sendOptions and its whenBusy
option alongside content and body. Add a brief `@param` entry and, if helpful, a
small example showing how sendOptions.whenBusy is used, so the docs match the
new signature in chat-client.ts.
packages/ai-client/tests/chat-client-queue.test.ts (1)

71-328: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for interrupt, per-call whenBusy override, and maxSize/onOverflow.

This suite covers default queueing, drop, FIFO/batch drain, and flush-on-stop/error well, but three documented behaviors have zero test coverage:

  • whenBusy: 'interrupt' (abort + send now)
  • per-call sendOptions.whenBusy override on sendMessage
  • maxSize with onOverflow: 'reject' vs 'drop-oldest'

Adding tests for the overflow behavior in particular would surface the maxSize: 0 boundary issue flagged in chat-client.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-client/tests/chat-client-queue.test.ts` around lines 71 - 328,
Add tests in ChatClient message queue coverage for the missing documented
behaviors: verify `whenBusy: 'interrupt'` aborts the in-flight send and allows
the new message to send immediately, verify `sendMessage` honors a per-call
`sendOptions.whenBusy` override over the client default, and verify `maxSize`
with `onOverflow: 'reject'` and `'drop-oldest'` behaves correctly. Use the
existing `ChatClient`, `sendMessage`, `getQueue`, `getMessages`, and the held
connection helpers as reference points, and add a boundary case for `maxSize: 0`
to cover the overflow path mentioned in `chat-client.ts`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/ai-client/src/chat-client.ts`:
- Around line 905-923: The enqueueMessage queue limit check currently lets one
item through when queueConfig.maxSize is 0 and onOverflow is not 'reject',
because it shifts an empty queue and then still pushes a new message. Update
enqueueMessage to treat maxSize: 0 as a hard cap in the same way as other
full-queue cases, using the existing messageQueue, queueConfig, and onOverflow
logic so no item is enqueued when the limit is zero.
- Around line 882-923: The queued item id is generated twice, so the
`pending.id` seen by `QueueStrategy` does not match the actual entry stored by
`enqueueMessage`. Generate one queued id in `decideWhenBusy`, pass it through to
`enqueueMessage`, and use that same id when building the `pending` context and
when pushing the message so `getQueue()`, `onQueueChange`, and
`cancelQueued(id)` all refer to the same item.
In `@packages/ai-svelte/src/types.ts`:
- Around line 149-152: The `queue` property in the chat state interface is
missing `readonly`, unlike the other reactive getters and the Svelte/Vue
equivalents. Update the `queue` declaration in `types.ts` to be readonly
alongside `messages`, `isLoading`, `error`, `status`, `isSubscribed`,
`connectionStatus`, and `sessionGenerating`, so consumers know it is getter-only
and cannot be assigned to directly.
---
Duplicate comments:
In `@packages/ai-solid/src/use-chat.ts`:
- Around line 141-144: The chat queue is only read once when the client is
created in useChat, so later changes to options.queue are never synced into the
live client. Add a createEffect alongside the existing
body/forwardedProps/context syncing that calls client().updateOptions(...)
whenever options.queue changes, using the same pattern as the other reactive
option updates in useChat so the queue stays in sync if it is meant to be
live-updatable.
---
Nitpick comments:
In `@packages/ai-client/src/chat-client.ts`:
- Around line 803-846: The public JSDoc for sendMessage is missing the new third
parameter, so update the documentation block for sendMessage to describe
sendOptions and its whenBusy option alongside content and body. Add a brief
`@param` entry and, if helpful, a small example showing how sendOptions.whenBusy
is used, so the docs match the new signature in chat-client.ts.
In `@packages/ai-client/tests/chat-client-queue.test.ts`:
- Around line 71-328: Add tests in ChatClient message queue coverage for the
missing documented behaviors: verify `whenBusy: 'interrupt'` aborts the
in-flight send and allows the new message to send immediately, verify
`sendMessage` honors a per-call `sendOptions.whenBusy` override over the client
default, and verify `maxSize` with `onOverflow: 'reject'` and `'drop-oldest'`
behaves correctly. Use the existing `ChatClient`, `sendMessage`, `getQueue`,
`getMessages`, and the held connection helpers as reference points, and add a
boundary case for `maxSize: 0` to cover the overflow path mentioned in
`chat-client.ts`.
In `@packages/ai-preact/src/use-chat.ts`:
- Around line 157-163: The `queue` option in `useChat` is only applied when the
client is created in the `useMemo` block, so later changes to `options.queue`
are ignored after mount. Update the `updateOptions` path in `use-chat.ts` (the
effect that already keeps `body`, `forwardedProps`, and `context` in sync) to
also propagate `queue` when `optionsRef.current.queue` changes, using the same
`activeClientRef.current === instance` guard as `onQueueChange`. If `queue` is
meant to stay one-time like `tools`/`persistence`, make that behavior explicit
in the option handling and docs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 384b81cf-49eb-43ca-a54f-13b3dc63cefd

📥 Commits

Reviewing files that changed from the base of the PR and between 405c9d4 and 6aa7207.

📒 Files selected for processing (28)
  • .changeset/message-queue.md
  • docs/chat/streaming.md
  • docs/config.json
  • packages/ai-client/src/chat-client.ts
  • packages/ai-client/src/devtools.ts
  • packages/ai-client/src/index.ts
  • packages/ai-client/src/types.ts
  • packages/ai-client/tests/chat-client-queue.test.ts
  • packages/ai-client/tests/chat-client.test.ts
  • packages/ai-preact/src/types.ts
  • packages/ai-preact/src/use-chat.ts
  • packages/ai-preact/tests/use-chat.test.ts
  • packages/ai-react/src/types.ts
  • packages/ai-react/src/use-chat.ts
  • packages/ai-react/tests/use-chat.test.ts
  • packages/ai-solid/src/types.ts
  • packages/ai-solid/src/use-chat.ts
  • packages/ai-solid/tests/use-chat.test.ts
  • packages/ai-svelte/src/create-chat.svelte.ts
  • packages/ai-svelte/src/types.ts
  • packages/ai-vue/src/types.ts
  • packages/ai-vue/src/use-chat.ts
  • packages/ai-vue/tests/use-chat.test.ts
  • packages/ai/skills/ai-core/chat-experience/SKILL.md
  • testing/e2e/fixtures/queue/basic.json
  • testing/e2e/src/components/ChatUI.tsx
  • testing/e2e/src/routes/$provider/$feature.tsx
  • testing/e2e/tests/queue.spec.ts

Comment threadpackages/ai-client/src/chat-client.ts
Comment threadpackages/ai-client/src/chat-client.ts
Comment threadpackages/ai-svelte/src/types.ts Outdated
Comment on lines +149 to +152
/**
* Pending messages queued while a stream is in flight.
*/
queue: Array<QueuedMessage>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Mark queue as readonly for consistency.

Every other reactive getter in this interface (messages, isLoading, error, status, isSubscribed, connectionStatus, sessionGenerating) is declared readonly, and the Svelte implementation only exposes queue via a getter (no setter). The Vue equivalent is also wrapped in Readonly<...>. Missing readonly here lets consumers believe chat.queue = [...] is a valid assignment.

♻️ Proposed fix
 /**
* Pending messages queued while a stream is in flight.
*/
- queue: Array<QueuedMessage>+ readonly queue: Array<QueuedMessage>
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
*Pendingmessagesqueuedwhileastreamisinflight.
*/
queue: Array<QueuedMessage>
/**
*Pendingmessagesqueuedwhileastreamisinflight.
*/
readonly queue: Array<QueuedMessage>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-svelte/src/types.ts` around lines 149 - 152, The `queue` property
in the chat state interface is missing `readonly`, unlike the other reactive
getters and the Svelte/Vue equivalents. Update the `queue` declaration in
`types.ts` to be readonly alongside `messages`, `isLoading`, `error`, `status`,
`isSubscribed`, `connectionStatus`, and `sessionGenerating`, so consumers know
it is getter-only and cannot be assigned to directly.

AlemTuzlakand others added 24 commits July 17, 2026 08:32
…unsubscribe
Wires the queue drain into streamResponse's settle path so queued sends
actually go out (fifo one-at-a-time, or batch merged with newlines), and
flushes any pending queue on stop()/clear()/unsubscribe() so callers don't
get a surprise send after tearing down. Also surfaces the queue on the
devtools snapshot.
A RUN_ERROR or thrown non-abort error left streamCompletedSuccessfully
false, so the finally block's queue handling (guarded by that flag) was
skipped entirely. Queued messages were stranded until a later direct
sendMessage sent first and drained the stale queued item afterward,
inverting message order. Flush (not drain) the queue on the non-success
settle path, matching stop()'s existing behavior.
Mirrors the ai-react implementation (17bb417): ChatClient's queue-by-default
behavior is now wired through the Preact useChat hook via onQueueChange,
with cancelQueued and per-send whenBusy passthrough. Updates the two tests
that previously asserted a mid-stream second sendMessage was dropped to
instead assert both messages land in order.
The framework packages re-exported QueuedMessage/WhenBusy/QueueConfig/
QueueStrategy/QueueOption from their internal types module but never
forwarded them through the public index entry, so importing them from
e.g. @tanstack/ai-react failed at the package boundary.
Side-by-side panels (queue/fifo, interrupt, queue/batch) driven by a
shared composer, showcasing how each queue strategy handles a message
sent while a stream is in flight, with live queue rendering + cancel.
FIFO was stranding user messages when concurrent sendMessage calls both
passed the isLoading check (or when drain re-entered sendMessage and
re-enqueued items). Claim the client with sendInFlight, walk the queue in
a drain loop via deliverMessage, and stop the loop cleanly on interrupt.
Also: stable strategy pending ids, maxSize validation, Angular injectChat
parity, reload flushes queue, continuation errors still drain, queue
option live-updates, docs/skill accuracy, and unit coverage for the new
policy branches.
@tombeckenham
tombeckenhamforce-pushed the feat/client-message-queue branch from 7401f07 to 83d5407CompareJuly 17, 2026 05:49
Clear @typescript-eslint/no-unnecessary-condition findings in drainQueue
and continuation handling, drop an unnecessary type assertion in queue
tests, and fix import order / redundant narrowing in the queueing example.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/ts-react-chat/src/routes/queueing.tsx`:
- Around line 6-7: Reorder the imports from '`@/lib/model-selection`' in
queueing.tsx so the type-only ModelOption import appears before the value import
of DEFAULT_MODEL_OPTION and MODEL_OPTIONS, satisfying the configured
import/order rule.
In `@packages/ai-client/src/chat-client.ts`:
- Around line 1655-1669: In the queue-draining loop around deliverMessage,
retain the runtime checks of this.isLoading and this.stopMessageQueueDrain
before and after the await. Add narrowly scoped ESLint suppressions for the
static-analysis warnings on those asynchronous guard conditions, rather than
removing or restructuring the guards.
- Around line 1286-1290: Remove the unnecessary isLoading condition around
drainQueue in the continuation failure catch block, and invoke this.drainQueue()
directly because the stream has already settled there.
- Around line 906-919: Update streamResponse() so the queued-message drain runs
again after sendInFlight is reset to false, including the completion path shown
around deliverMessage(). Ensure the follow-up drain occurs after releasing the
send claim so sends enqueued during the completion window are processed.
- Around line 1645-1652: Update the batch-draining branch in the queue drain
method to continue processing until messageQueue is empty, including messages
enqueued during deliverMessage. Preserve the existing merge and delivery
behavior for each batch, but replace the one-shot return flow with a loop or
equivalent continuation that emits queue changes and drains all pending batches.
In `@packages/ai-client/tests/chat-client-queue.test.ts`:
- Around line 1-6: Move the chat-client queue unit test from the tests directory
to sit alongside chat-client.ts, preserving its existing coverage and updating
relative imports such as ChatClient, normalizeQueueOption, createTextChunks, and
connection adapter types for the new location.
- Around line 521-525: Replace the tautological assertion in the rapid-send test
around client.getQueue() with an assertion that verifies the expected queued
message count while the first user message is in flight. Ensure the check
detects sends bypassing the queue and preserves the intended one-in-flight,
remaining-messages-queued behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a3e23dc9-8eb6-4aea-8a73-784ccf39bff8

📥 Commits

Reviewing files that changed from the base of the PR and between 7401f07 and 83d5407.

📒 Files selected for processing (15)
  • .changeset/message-queue.md
  • docs/chat/streaming.md
  • docs/config.json
  • examples/ts-react-chat/src/components/Header.tsx
  • examples/ts-react-chat/src/routeTree.gen.ts
  • examples/ts-react-chat/src/routes/queueing.tsx
  • packages/ai-angular/src/index.ts
  • packages/ai-angular/src/inject-chat.ts
  • packages/ai-angular/src/types.ts
  • packages/ai-client/src/chat-client.ts
  • packages/ai-client/src/devtools.ts
  • packages/ai-client/src/index.ts
  • packages/ai-client/src/types.ts
  • packages/ai-client/tests/chat-client-queue.test.ts
  • packages/ai-client/tests/chat-client.test.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • examples/ts-react-chat/src/components/Header.tsx
  • packages/ai-client/src/index.ts
  • .changeset/message-queue.md
  • packages/ai-client/tests/chat-client.test.ts
  • packages/ai-client/src/devtools.ts
  • docs/chat/streaming.md
  • packages/ai-client/src/types.ts
  • examples/ts-react-chat/src/routeTree.gen.ts

Comment threadexamples/ts-react-chat/src/routes/queueing.tsx Outdated
Comment threadpackages/ai-client/src/chat-client.ts Outdated
Comment threadpackages/ai-client/src/chat-client.ts Outdated
Comment threadpackages/ai-client/src/chat-client.ts
Comment threadpackages/ai-client/src/chat-client.ts
Comment on lines +1 to +6
import { describe, expect, it, vi } from 'vitest'
import { EventType } from '@tanstack/ai/client'
import { ChatClient, normalizeQueueOption } from '../src/chat-client'
import { createTextChunks } from './test-utils'
import type { ConnectConnectionAdapter } from '../src/connection-adapters'
import type { StreamChunk } from '@tanstack/ai/client'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Move this unit test alongside chat-client.ts.

This new test belongs beside packages/ai-client/src/chat-client.ts, with its relative imports adjusted.

As per coding guidelines, *.test.ts files must be placed alongside the source they cover.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-client/tests/chat-client-queue.test.ts` around lines 1 - 6, Move
the chat-client queue unit test from the tests directory to sit alongside
chat-client.ts, preserving its existing coverage and updating relative imports
such as ChatClient, normalizeQueueOption, createTextChunks, and connection
adapter types for the new location.

Source: Coding guidelines

Comment threadpackages/ai-client/tests/chat-client-queue.test.ts Outdated
tombeckenhamand others added 2 commits July 17, 2026 16:51
Loop batch drain so mid-stream enqueues are not stranded, harden
deliverMessage with a claim that hands off to isLoading, unify
QueueStrategy on WhenBusy + busyReason, and re-export SendMessageOptions.
Docs now say drain only on successful settle; tests cover body, mid-drain
error/interrupt, and batch re-enqueue.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/ai-client/tests/chat-client-queue.test.ts`:
- Around line 786-842: Update the “interrupt during FIFO drain” test around the
second connection attempt to track its cancellation, such as by observing the
async connection’s abort signal or cancellation callback. After sending “urgent”
with whenBusy: 'interrupt', assert that the active “second” request was aborted
while preserving the existing ordering and queued “third” assertions.
In `@packages/ai/skills/ai-core/chat-experience/SKILL.md`:
- Around line 445-448: Clarify the “Drain vs flush” documentation to distinguish
interrupt-triggered stream aborts from other active-generation aborts: preserve
queued messages across `interrupt`, while discarding them for stream errors,
non-interrupt aborts, `stop()`, `clear()`, `unsubscribe()`, and `reload()`.
Retain the existing rule that only a successful settle auto-sends queued
messages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 98982007-a845-456f-8888-a73fef7ae114

📥 Commits

Reviewing files that changed from the base of the PR and between dd080a9 and 6c767cd.

📒 Files selected for processing (24)
  • docs/chat/streaming.md
  • packages/ai-angular/src/index.ts
  • packages/ai-angular/src/inject-chat.ts
  • packages/ai-angular/src/types.ts
  • packages/ai-client/src/chat-client.ts
  • packages/ai-client/src/index.ts
  • packages/ai-client/src/types.ts
  • packages/ai-client/tests/chat-client-queue.test.ts
  • packages/ai-preact/src/index.ts
  • packages/ai-preact/src/types.ts
  • packages/ai-preact/src/use-chat.ts
  • packages/ai-react/src/index.ts
  • packages/ai-react/src/types.ts
  • packages/ai-react/src/use-chat.ts
  • packages/ai-solid/src/index.ts
  • packages/ai-solid/src/types.ts
  • packages/ai-solid/src/use-chat.ts
  • packages/ai-svelte/src/create-chat.svelte.ts
  • packages/ai-svelte/src/index.ts
  • packages/ai-svelte/src/types.ts
  • packages/ai-vue/src/index.ts
  • packages/ai-vue/src/types.ts
  • packages/ai-vue/src/use-chat.ts
  • packages/ai/skills/ai-core/chat-experience/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (22)
  • packages/ai-svelte/src/index.ts
  • packages/ai-client/src/index.ts
  • packages/ai-preact/src/index.ts
  • packages/ai-angular/src/index.ts
  • packages/ai-solid/src/index.ts
  • packages/ai-react/src/index.ts
  • packages/ai-solid/src/types.ts
  • packages/ai-vue/src/index.ts
  • docs/chat/streaming.md
  • packages/ai-vue/src/types.ts
  • packages/ai-preact/src/types.ts
  • packages/ai-angular/src/types.ts
  • packages/ai-preact/src/use-chat.ts
  • packages/ai-react/src/use-chat.ts
  • packages/ai-react/src/types.ts
  • packages/ai-svelte/src/types.ts
  • packages/ai-svelte/src/create-chat.svelte.ts
  • packages/ai-solid/src/use-chat.ts
  • packages/ai-client/src/types.ts
  • packages/ai-vue/src/use-chat.ts
  • packages/ai-angular/src/inject-chat.ts
  • packages/ai-client/src/chat-client.ts

Comment on lines +786 to +842
it('interrupt during FIFO drain keeps remaining items for after the interrupt', async () => {
const deferred1 = createDeferred<void>()
const deferred2 = createDeferred<void>()
let call = 0
const connection: ConnectConnectionAdapter = {
async *connect() {
call += 1
if (call === 1) {
await deferred1.promise
} else if (call === 2) {
await deferred2.promise
}
yield* createTextChunks('done', `msg-${call}`)
},
}
const client = new ChatClient({ connection })

const firstSend = client.sendMessage('first')
await vi.waitFor(() => {
expect(client.getIsLoading()).toBe(true)
})
await client.sendMessage('second')
await client.sendMessage('third')

deferred1.resolve()
// Wait until FIFO drain has started streaming "second".
await vi.waitFor(() => {
const users = client.getMessages().filter((m) => m.role === 'user')
expect(users.map((m) => m.parts[0])).toEqual([
{ type: 'text', content: 'first' },
{ type: 'text', content: 'second' },
])
expect(client.getIsLoading()).toBe(true)
})
// "third" should still be queued while "second" streams.
expect(client.getQueue().map((m) => m.content)).toEqual(['third'])

const interruptSend = client.sendMessage('urgent', undefined, {
whenBusy: 'interrupt',
})
// Interrupt does not flush remaining queue.
expect(client.getQueue().map((m) => m.content)).toEqual(['third'])

deferred2.resolve()
await Promise.all([firstSend, interruptSend])
await vi.waitFor(() => {
expect(client.getQueue()).toEqual([])
})

const users = client.getMessages().filter((m) => m.role === 'user')
expect(users.map((m) => m.parts[0])).toEqual([
{ type: 'text', content: 'first' },
{ type: 'text', content: 'second' },
{ type: 'text', content: 'urgent' },
{ type: 'text', content: 'third' },
])
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that the active FIFO drain is actually aborted.

This verifies ordering and queue retention, but never checks that "second" receives cancellation.

Proposed assertion
 const deferred2 = createDeferred<void>()
let call = 0
+ let secondSignal: AbortSignal | undefined
const connection: ConnectConnectionAdapter = {
- async *connect() {+ async *connect(_messages, _data, abortSignal) {
call += 1
if (call === 1) {
await deferred1.promise
} else if (call === 2) {
+ secondSignal = abortSignal
await deferred2.promise
}
@@
const interruptSend = client.sendMessage('urgent', undefined, {
whenBusy: 'interrupt',
})
+ await vi.waitFor(() => {+ expect(secondSignal?.aborted).toBe(true)+ })
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('interrupt during FIFO drain keeps remaining items for after the interrupt',async()=>{
constdeferred1=createDeferred<void>()
constdeferred2=createDeferred<void>()
letcall=0
constconnection: ConnectConnectionAdapter={
async*connect(){
call+=1
if(call===1){
awaitdeferred1.promise
}elseif(call===2){
awaitdeferred2.promise
}
yield*createTextChunks('done',`msg-${call}`)
},
}
constclient=newChatClient({ connection })
constfirstSend=client.sendMessage('first')
awaitvi.waitFor(()=>{
expect(client.getIsLoading()).toBe(true)
})
awaitclient.sendMessage('second')
awaitclient.sendMessage('third')
deferred1.resolve()
// Wait until FIFO drain has started streaming "second".
awaitvi.waitFor(()=>{
constusers=client.getMessages().filter((m)=>m.role==='user')
expect(users.map((m)=>m.parts[0])).toEqual([
{type: 'text',content: 'first'},
{type: 'text',content: 'second'},
])
expect(client.getIsLoading()).toBe(true)
})
// "third" should still be queued while "second" streams.
expect(client.getQueue().map((m)=>m.content)).toEqual(['third'])
constinterruptSend=client.sendMessage('urgent',undefined,{
whenBusy: 'interrupt',
})
// Interrupt does not flush remaining queue.
expect(client.getQueue().map((m)=>m.content)).toEqual(['third'])
deferred2.resolve()
awaitPromise.all([firstSend,interruptSend])
awaitvi.waitFor(()=>{
expect(client.getQueue()).toEqual([])
})
constusers=client.getMessages().filter((m)=>m.role==='user')
expect(users.map((m)=>m.parts[0])).toEqual([
{type: 'text',content: 'first'},
{type: 'text',content: 'second'},
{type: 'text',content: 'urgent'},
{type: 'text',content: 'third'},
])
})
it('interrupt during FIFO drain keeps remaining items for after the interrupt',async()=>{
constdeferred1=createDeferred<void>()
constdeferred2=createDeferred<void>()
letcall=0
letsecondSignal: AbortSignal|undefined
constconnection: ConnectConnectionAdapter={
async*connect(_messages,_data,abortSignal){
call+=1
if(call===1){
awaitdeferred1.promise
}elseif(call===2){
secondSignal=abortSignal
awaitdeferred2.promise
}
yield*createTextChunks('done',`msg-${call}`)
},
}
constclient=newChatClient({ connection })
constfirstSend=client.sendMessage('first')
awaitvi.waitFor(()=>{
expect(client.getIsLoading()).toBe(true)
})
awaitclient.sendMessage('second')
awaitclient.sendMessage('third')
deferred1.resolve()
// Wait until FIFO drain has started streaming "second".
awaitvi.waitFor(()=>{
constusers=client.getMessages().filter((m)=>m.role==='user')
expect(users.map((m)=>m.parts[0])).toEqual([
{type: 'text',content: 'first'},
{type: 'text',content: 'second'},
])
expect(client.getIsLoading()).toBe(true)
})
// "third" should still be queued while "second" streams.
expect(client.getQueue().map((m)=>m.content)).toEqual(['third'])
constinterruptSend=client.sendMessage('urgent',undefined,{
whenBusy: 'interrupt',
})
awaitvi.waitFor(()=>{
expect(secondSignal?.aborted).toBe(true)
})
// Interrupt does not flush remaining queue.
expect(client.getQueue().map((m)=>m.content)).toEqual(['third'])
deferred2.resolve()
awaitPromise.all([firstSend,interruptSend])
awaitvi.waitFor(()=>{
expect(client.getQueue()).toEqual([])
})
constusers=client.getMessages().filter((m)=>m.role==='user')
expect(users.map((m)=>m.parts[0])).toEqual([
{type: 'text',content: 'first'},
{type: 'text',content: 'second'},
{type: 'text',content: 'urgent'},
{type: 'text',content: 'third'},
])
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-client/tests/chat-client-queue.test.ts` around lines 786 - 842,
Update the “interrupt during FIFO drain” test around the second connection
attempt to track its cancellation, such as by observing the async connection’s
abort signal or cancellation callback. After sending “urgent” with whenBusy:
'interrupt', assert that the active “second” request was aborted while
preserving the existing ordering and queued “third” assertions.

Comment on lines +445 to +448
**Drain vs flush:** queued messages auto-send only after a **successful**
settle. They are **discarded** on stream error/abort of the active
generation, `stop()`, `clear()`, `unsubscribe()`, and `reload()`.
`interrupt` does not flush.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clarify the abort exception for interrupts.

“Abort of the active generation” contradicts “interrupt does not flush,” because interrupting aborts the current stream while preserving queued items. Explicitly distinguish interrupt-triggered aborts from aborts that discard the queue.

🧰 Tools
🪛 SkillSpector (2.3.11)

[warning] 372: [MP2] Context Window Stuffing: Skill attempts to fill the context window with filler content, displacing legitimate instructions and safety constraints. This can degrade agent performance or bypass safety boundaries.

Remediation: Implement context-window management that detects and rejects padding or stuffing attempts. Prioritize system instructions over user-injected content.

(Memory Poisoning (MP2))


[warning] 372: [MP2] Context Window Stuffing: Skill attempts to fill the context window with filler content, displacing legitimate instructions and safety constraints. This can degrade agent performance or bypass safety boundaries.

Remediation: Implement context-window management that detects and rejects padding or stuffing attempts. Prioritize system instructions over user-injected content.

(Memory Poisoning (MP2))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai/skills/ai-core/chat-experience/SKILL.md` around lines 445 - 448,
Clarify the “Drain vs flush” documentation to distinguish interrupt-triggered
stream aborts from other active-generation aborts: preserve queued messages
across `interrupt`, while discarding them for stream errors, non-interrupt
aborts, `stop()`, `clear()`, `unsubscribe()`, and `reload()`. Retain the
existing rule that only a successful settle auto-sends queued messages.

@tombeckenham
tombeckenham self-requested a review July 17, 2026 07:20

@tombeckenhamtombeckenham left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed a couple of things with the fifo queue. It now works nicely.

@AlemTuzlak
AlemTuzlak merged commit 35946e3 into mainJul 17, 2026
12 of 13 checks passed
@AlemTuzlak
AlemTuzlak deleted the feat/client-message-queue branch July 17, 2026 08:06
@github-actionsgithub-actionsBot mentioned this pull request Jul 17, 2026
tombeckenham pushed a commit that referenced this pull request Aug 17, 2026
Adds `@tanstack/ai-octane`, an Octane binding for TanStack AI. This is a
port of `@octanejs/tanstack-ai@0.0.11`, which lived in the octanejs/octane
repo as a temporary stopgap; the code moves here essentially unchanged
apart from the rename.
Covers the `@tanstack/ai-react` hook surface (useChat, useRealtimeChat,
useMcpAppBridge, useGeneration, useGenerateImage/Audio/Speech/Video,
useTranscription, useSummarize, useAudioRecorder) plus the 30
`@tanstack/ai-client` convenience re-exports, reusing `@tanstack/ai` and
`@tanstack/ai-client` unchanged. SSR through `octane/server` is tested.
Packaging: like Svelte packages shipping `.svelte`, this one publishes
uncompiled source. The hook modules are `.tsrx`, compiled by the
consumer's Octane plugin, so there is no `dist`/`build` target and no
publint `test:build`; `octane` is a required peer. The `.tsrx.d.ts`
companions are checked declaration emits, so `tsc` still verifies the
full public generic surface.
Deviations from a byte-for-byte move, all recorded in status.json:
- Conformance runs on happy-dom rather than jsdom. This repo pins jsdom
^27, whose `Blob` has no `arrayBuffer()` (Octane pins ^29, which does)
and the useAudioRecorder cases need it. Bumping jsdom for one package
would break sherif's cross-package version consistency.
- Two useChat cases asserted that a second concurrent sendMessage is
dropped. #900 made client-side queueing the default, so both now
assert queue-and-deliver-in-order, mirroring the current ai-react
tests. The hook needed no change — queueing lives in ChatClient.
- The differential parity test stays in the Octane repo: it byte-compares
streamed output against real @tanstack/ai-react, which would make a
sibling workspace package a pinned test dependency.
- Unused vendored test helpers were dropped rather than carried as dead
code (knip).
- The generated .tsrx.d.ts companions are excluded from lint; a
regeneration would undo any hand-fix.
Known gap: the port is baselined against @tanstack/ai-react 0.17.0 while
this repo is at 0.18.1. The interrupts overhaul (#970) and server
persistence / browser-refresh durability (#984) are not yet reflected in
the Octane hooks, and `typetests/` is therefore not wired into CI — its 4
errors are confined to one tool-input-inference case that fails
identically against @tanstack/ai-react, so it is not a port defect.
status.json lists the exact type-surface delta. Parity catch-up is
follow-up work.
pnpm-workspace.yaml excludes octane@0.1.17 and
@octanejs/testing-library@0.1.14 from minimumReleaseAge: Octane publishes
the pair in lockstep (testing-library pins an exact octane peer) and
releases roughly daily, so the newest pair is essentially always inside
the 24h window.
143 tests pass (142 conformance + 1 SSR), no skips or todos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tombeckenham pushed a commit that referenced this pull request Aug 20, 2026
Adds `@tanstack/ai-octane`, an Octane binding for TanStack AI. This is a
port of `@octanejs/tanstack-ai@0.0.11`, which lived in the octanejs/octane
repo as a temporary stopgap; the code moves here essentially unchanged
apart from the rename.
Covers the `@tanstack/ai-react` hook surface (useChat, useRealtimeChat,
useMcpAppBridge, useGeneration, useGenerateImage/Audio/Speech/Video,
useTranscription, useSummarize, useAudioRecorder) plus the 30
`@tanstack/ai-client` convenience re-exports, reusing `@tanstack/ai` and
`@tanstack/ai-client` unchanged. SSR through `octane/server` is tested.
Packaging: like Svelte packages shipping `.svelte`, this one publishes
uncompiled source. The hook modules are `.tsrx`, compiled by the
consumer's Octane plugin, so there is no `dist`/`build` target and no
publint `test:build`; `octane` is a required peer. The `.tsrx.d.ts`
companions are checked declaration emits, so `tsc` still verifies the
full public generic surface.
Deviations from a byte-for-byte move, all recorded in status.json:
- Conformance runs on happy-dom rather than jsdom. This repo pins jsdom
^27, whose `Blob` has no `arrayBuffer()` (Octane pins ^29, which does)
and the useAudioRecorder cases need it. Bumping jsdom for one package
would break sherif's cross-package version consistency.
- Two useChat cases asserted that a second concurrent sendMessage is
dropped. #900 made client-side queueing the default, so both now
assert queue-and-deliver-in-order, mirroring the current ai-react
tests. The hook needed no change — queueing lives in ChatClient.
- The differential parity test stays in the Octane repo: it byte-compares
streamed output against real @tanstack/ai-react, which would make a
sibling workspace package a pinned test dependency.
- Unused vendored test helpers were dropped rather than carried as dead
code (knip).
- The generated .tsrx.d.ts companions are excluded from lint; a
regeneration would undo any hand-fix.
Known gap: the port is baselined against @tanstack/ai-react 0.17.0 while
this repo is at 0.18.1. The interrupts overhaul (#970) and server
persistence / browser-refresh durability (#984) are not yet reflected in
the Octane hooks, and `typetests/` is therefore not wired into CI — its 4
errors are confined to one tool-input-inference case that fails
identically against @tanstack/ai-react, so it is not a port defect.
status.json lists the exact type-surface delta. Parity catch-up is
follow-up work.
pnpm-workspace.yaml excludes octane@0.1.17 and
@octanejs/testing-library@0.1.14 from minimumReleaseAge: Octane publishes
the pair in lockstep (testing-library pins an exact octane peer) and
releases roughly daily, so the newest pair is essentially always inside
the 24h window.
143 tests pass (142 conformance + 1 SSR), no skips or todos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tombeckenham pushed a commit that referenced this pull request Aug 20, 2026
Adds `@tanstack/ai-octane`, an Octane binding for TanStack AI. This is a
port of `@octanejs/tanstack-ai@0.0.11`, which lived in the octanejs/octane
repo as a temporary stopgap; the code moves here essentially unchanged
apart from the rename.
Covers the `@tanstack/ai-react` hook surface (useChat, useRealtimeChat,
useMcpAppBridge, useGeneration, useGenerateImage/Audio/Speech/Video,
useTranscription, useSummarize, useAudioRecorder) plus the 30
`@tanstack/ai-client` convenience re-exports, reusing `@tanstack/ai` and
`@tanstack/ai-client` unchanged. SSR through `octane/server` is tested.
Packaging: like Svelte packages shipping `.svelte`, this one publishes
uncompiled source. The hook modules are `.tsrx`, compiled by the
consumer's Octane plugin, so there is no `dist`/`build` target and no
publint `test:build`; `octane` is a required peer. The `.tsrx.d.ts`
companions are checked declaration emits, so `tsc` still verifies the
full public generic surface.
Deviations from a byte-for-byte move, all recorded in status.json:
- Conformance runs on happy-dom rather than jsdom. This repo pins jsdom
^27, whose `Blob` has no `arrayBuffer()` (Octane pins ^29, which does)
and the useAudioRecorder cases need it. Bumping jsdom for one package
would break sherif's cross-package version consistency.
- Two useChat cases asserted that a second concurrent sendMessage is
dropped. #900 made client-side queueing the default, so both now
assert queue-and-deliver-in-order, mirroring the current ai-react
tests. The hook needed no change — queueing lives in ChatClient.
- The differential parity test stays in the Octane repo: it byte-compares
streamed output against real @tanstack/ai-react, which would make a
sibling workspace package a pinned test dependency.
- Unused vendored test helpers were dropped rather than carried as dead
code (knip).
- The generated .tsrx.d.ts companions are excluded from lint; a
regeneration would undo any hand-fix.
Known gap: the port is baselined against @tanstack/ai-react 0.17.0 while
this repo is at 0.18.1. The interrupts overhaul (#970) and server
persistence / browser-refresh durability (#984) are not yet reflected in
the Octane hooks, and `typetests/` is therefore not wired into CI — its 4
errors are confined to one tool-input-inference case that fails
identically against @tanstack/ai-react, so it is not a port defect.
status.json lists the exact type-surface delta. Parity catch-up is
follow-up work.
pnpm-workspace.yaml excludes octane@0.1.17 and
@octanejs/testing-library@0.1.14 from minimumReleaseAge: Octane publishes
the pair in lockstep (testing-library pins an exact octane peer) and
releases roughly daily, so the newest pair is essentially always inside
the 24h window.
143 tests pass (142 conformance + 1 SSR), no skips or todos.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@AlemTuzlak@tombeckenham