Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/ai-client-store-snapshot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@tanstack/ai-client': minor
'@tanstack/ai-react': patch
'@tanstack/ai-solid': patch
'@tanstack/ai-vue': patch
'@tanstack/ai-svelte': patch
'@tanstack/ai-angular': patch
'@tanstack/ai-preact': patch
'@tanstack/ai-octane': patch
---

Hold client UI state in a TanStack Store atom. Framework hooks read `getSnapshot()` instead of copying fields through change callbacks. React `useChat()` first commits `initialMessages` and `initialResumeSnapshot`, then applies browser persistence after attach. This timing applies to client-only rendering and keeps the server snapshot stable during SSR hydration. The `useChat()` return shape stays the same. Frozen snapshot `messages` and `queue` arrays throw on in-place `push` in every adapter. Use `setMessages`.

`ChatClient` adds `subscribeSnapshot` / `getSnapshot`. The live connection method stays `subscribe()`. Generation, video, realtime, and BYOK clients expose `subscribe` / `getSnapshot`. `AudioRecorder.subscribe` still passes the new state value.
41 changes: 41 additions & 0 deletions docs/api/ai-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,47 @@ Stop tailing and drop the connection. Keeps messages, the run pointer and the ru
id, so a later `attach()` continues where it left off. See
[Lifecycle](#lifecycle-attach-and-detach).

#### `getSnapshot()`

Returns the current frozen `ChatClientSnapshot`. The field names are
`messages`, `status`, `isLoading`, `error`, `queue`, `runId`, `isSubscribed`,
`connectionStatus`, `sessionGenerating`, and `interruptState`. Nested
`messages` and `queue` entries are copies. In-place `messages.push` throws.
Use `setMessages`.

`subscribeSnapshot(listener)` returns an unsubscribe function. It does not
fire with the current value. Read `getSnapshot()` first. This is not
`subscribe()`, which starts the live connection loop.

```typescript
import { ChatClient, fetchServerSentEvents } from "@tanstack/ai-client";

const client = new ChatClient({
connection: fetchServerSentEvents("/api/chat"),
});

function logSnapshot() {
const snapshot = client.getSnapshot();
console.log(snapshot.status, snapshot.messages.length);
}

logSnapshot();

const unsubscribe = client.subscribeSnapshot(() => {
logSnapshot();
});

client.attach();
client.sendMessage("Hello");
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

`GenerationClient`, `VideoGenerationClient`, `RealtimeClient`, and
`ByokClient` use `subscribe(listener)` / `getSnapshot()`. The listener takes
no argument. Call `getSnapshot()` inside it.

`AudioRecorder.subscribe` still passes the new state:
`subscribe((state) => ...)`. `getSnapshot()` returns that same state value.

#### `stop()`

Stops the current response generation. It aborts the in-flight request.
Expand Down
2 changes: 1 addition & 1 deletion docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@
"label": "Client Persistence",
"to": "persistence/client-persistence",
"addedAt": "2026-08-04",
"updatedAt": "2026-08-20"
"updatedAt": "2026-08-24"
},
{
"label": "Generation Persistence",
Expand Down
11 changes: 7 additions & 4 deletions docs/persistence/client-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,13 @@ to pick one and how it differs from the `runId` the hook reports.
The client stores one record per `threadId`, the transcript plus a small resume
pointer. On the next load `useChat` reads it and:

- **Repaints the transcript** from storage with no network. Sync adapters
(`localStorage` / `sessionStorage`) hydrate during construction; IndexedDB
hydrates asynchronously after the database opens (so the first paint may be
empty for a tick).
- **Repaints the transcript** from storage with no network. In React, the first
committed render uses `initialMessages` and `initialResumeSnapshot`. After the
hook attaches, it applies browser persistence from either a synchronous or an
asynchronous adapter. This timing also prevents SSR hydration mismatches. A
direct `ChatClient` and non-React framework wrappers still apply synchronous
stores during construction. IndexedDB always hydrates asynchronously after
the database opens.
- **Rehydrates a pending interrupt**, so an approval prompt comes back exactly as
it was.
- **Rejoins an in-flight run**, if a reply was still streaming when the page
Expand Down
6 changes: 3 additions & 3 deletions examples/sandbox-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"@tanstack/ai-sandbox": "workspace:*",
"@tanstack/ai-sandbox-cloudflare": "workspace:*",
"@tanstack/ai-sandbox-local-process": "workspace:*",
"@tanstack/react-router": "^1.158.4",
"@tanstack/react-start": "^1.159.0",
"@tanstack/react-router": "^1.170.32",
"@tanstack/react-start": "^1.168.49",
"lucide-react": "^0.561.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
Expand All @@ -37,7 +37,7 @@
"devDependencies": {
"@cloudflare/vite-plugin": "^1.42.1",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/router-plugin": "^1.158.4",
"@tanstack/router-plugin": "^1.168.35",
"@types/node": "^24.10.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
Expand Down
6 changes: 3 additions & 3 deletions examples/sandbox-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"@tanstack/ai-react": "workspace:*",
"@tanstack/ai-sandbox": "workspace:*",
"@tanstack/ai-sandbox-docker": "workspace:*",
"@tanstack/react-router": "^1.158.4",
"@tanstack/react-start": "^1.159.0",
"@tanstack/react-router": "^1.170.32",
"@tanstack/react-start": "^1.168.49",
"lucide-react": "^0.561.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
Expand All @@ -31,7 +31,7 @@
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
"@tanstack/router-plugin": "^1.158.4",
"@tanstack/router-plugin": "^1.168.35",
"@types/node": "^24.10.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
Expand Down
8 changes: 4 additions & 4 deletions examples/ts-code-mode-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@
"@tanstack/ai-ollama": "workspace:*",
"@tanstack/ai-openai": "workspace:*",
"@tanstack/ai-react": "workspace:*",
"@tanstack/react-router": "^1.158.4",
"@tanstack/react-start": "^1.159.0",
"@tanstack/router-plugin": "^1.158.4",
"@tanstack/store": "^0.8.0",
"@tanstack/react-router": "^1.170.32",
"@tanstack/react-start": "^1.168.49",
"@tanstack/router-plugin": "^1.168.35",
"@tanstack/store": "^0.11.1",
"framer-motion": "^11.15.0",
"highlight.js": "^11.11.1",
"isolated-vm": "^6.0.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ async function getCodeModeTools() {
return codeModeCache
}

export const Route = createFileRoute('/_banking-demo/api/banking-demo' as any)({
export const Route = createFileRoute('/_banking-demo/api/banking-demo')({
server: {
handlers: {
POST: async ({ request }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { getMockBalances } from '@/lib/reports/create-handler-bindings'

const DASHBOARD_ID = 'dashboard'

export const Route = createFileRoute('/_banking-demo/api/banking-init' as any)({
export const Route = createFileRoute('/_banking-demo/api/banking-init')({
server: {
handlers: {
POST: async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import type {
UIUpdate,
} from '@/lib/reports/types'

export const Route = createFileRoute('/_banking-demo/banking-demo' as any)({
export const Route = createFileRoute('/_banking-demo/banking-demo')({
component: BankingDemoPage,
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,7 @@ function wrapWithTimingEvents(
})()
}

export const Route = createFileRoute(
'/_database-demo/api/database-demo' as any,
)({
export const Route = createFileRoute('/_database-demo/api/database-demo')({
server: {
handlers: {
POST: async ({ request }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const snippetStorage = createFileSnippetStorage({
trustStrategy,
})

export const Route = createFileRoute('/_database-demo/api/db-snippets' as any)({
export const Route = createFileRoute('/_database-demo/api/db-snippets')({
server: {
handlers: {
GET: async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Rate the candidate on two dimensions, each on a scale from 1 to 10 (10 = best):

Provide a two sentence summary of your evaluation.`

export const Route = createFileRoute('/_database-demo/api/judge' as any)({
export const Route = createFileRoute('/_database-demo/api/judge')({
server: {
handlers: {
POST: async ({ request }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import ChatInput from '@/components/ChatInput'
import { formatDuration } from '@/lib/efficiency'
import { toolResultContentToString } from '@/lib/tool-result-content'

export const Route = createFileRoute('/_database-demo/database-demo' as any)({
export const Route = createFileRoute('/_database-demo/database-demo')({
component: DatabaseDemoPage,
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ async function getDriver(): Promise<IsolateDriver> {
return cachedDriver
}

export const Route = createFileRoute(
'/_execute-prompt/api/execute-prompt' as any,
)({
export const Route = createFileRoute('/_execute-prompt/api/execute-prompt')({
server: {
handlers: {
POST: async ({ request }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { createFileRoute } from '@tanstack/react-router'
import { realtimeToken } from '@tanstack/ai'
import { openaiRealtimeToken } from '@tanstack/ai-openai'

export const Route = createFileRoute(
'/_execute-prompt/api/realtime-token' as any,
)({
export const Route = createFileRoute('/_execute-prompt/api/realtime-token')({
server: {
handlers: {
POST: async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import type {
CodeExecution,
} from '@/lib/execute-prompt-realtime-tools'

export const Route = createFileRoute('/_execute-prompt/execute-prompt' as any)({
export const Route = createFileRoute('/_execute-prompt/execute-prompt')({
component: DashboardDemoPage,
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getReportState, getSSEController } from '@/lib/reports/report-storage'
import { refreshComponent } from '@/lib/reports/refresh-component'
import { evaluateWatchersForSignals } from '@/lib/reports/evaluate-watchers'

export const Route = createFileRoute('/_reporting/api/invalidate' as any)({
export const Route = createFileRoute('/_reporting/api/invalidate')({
server: {
handlers: {
POST: async ({ request }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
simulateDeposit,
} from '@/lib/reports/create-handler-bindings'

export const Route = createFileRoute('/_reporting/api/report-demo' as any)({
export const Route = createFileRoute('/_reporting/api/report-demo')({
server: {
handlers: {
POST: async ({ request }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { refreshComponent } from '@/lib/reports/refresh-component'
import { evaluateWatchersForSignals } from '@/lib/reports/evaluate-watchers'

export const Route = createFileRoute('/_reporting/api/report-event' as any)({
export const Route = createFileRoute('/_reporting/api/report-event')({
server: {
handlers: {
POST: async ({ request }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
getSSEController,
} from '@/lib/reports/report-storage'

export const Route = createFileRoute('/_reporting/api/report-sse' as any)({
export const Route = createFileRoute('/_reporting/api/report-sse')({
server: {
handlers: {
GET: async ({ request }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async function getCodeModeTools() {
return codeModeCache
}

export const Route = createFileRoute('/_reporting/api/reports' as any)({
export const Route = createFileRoute('/_reporting/api/reports')({
server: {
handlers: {
POST: async ({ request }) => {
Expand Down
8 changes: 4 additions & 4 deletions examples/ts-group-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
"@tanstack/ai": "workspace:*",
"@tanstack/ai-anthropic": "workspace:*",
"@tanstack/react-devtools": "^0.9.10",
"@tanstack/react-router": "^1.158.4",
"@tanstack/react-router-devtools": "^1.158.4",
"@tanstack/react-start": "^1.159.0",
"@tanstack/router-plugin": "^1.158.4",
"@tanstack/react-router": "^1.170.32",
"@tanstack/react-router-devtools": "^1.167.1",
"@tanstack/react-start": "^1.168.49",
"@tanstack/router-plugin": "^1.168.35",
"capnweb": "^0.10.0",
"nitro": "3.0.260610-beta",
"react": "^19.2.3",
Expand Down
14 changes: 7 additions & 7 deletions examples/ts-react-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@
"@tanstack/ai-sandbox-local-process": "workspace:*",
"@tanstack/ai-sandbox-vercel": "workspace:*",
"@tanstack/react-devtools": "^0.9.10",
"@tanstack/react-router": "^1.158.4",
"@tanstack/react-router-devtools": "^1.158.4",
"@tanstack/react-router-ssr-query": "^1.158.4",
"@tanstack/react-start": "^1.159.0",
"@tanstack/react-store": "^0.8.0",
"@tanstack/router-plugin": "^1.158.4",
"@tanstack/store": "^0.8.0",
"@tanstack/react-router": "^1.170.32",
"@tanstack/react-router-devtools": "^1.167.1",
"@tanstack/react-router-ssr-query": "^1.167.2",
"@tanstack/react-start": "^1.168.49",
"@tanstack/react-store": "^0.11.1",
"@tanstack/router-plugin": "^1.168.35",
"@tanstack/store": "^0.11.1",
"dockerode": "^4.0.2",
"highlight.js": "^11.11.1",
"lucide-react": "^0.561.0",
Expand Down
6 changes: 3 additions & 3 deletions examples/ts-react-media/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
"@tanstack/ai-grok": "workspace:*",
"@tanstack/ai-openrouter": "workspace:*",
"@tanstack/ai-react": "workspace:*",
"@tanstack/react-router": "^1.158.4",
"@tanstack/react-start": "^1.159.0",
"@tanstack/router-plugin": "^1.158.4",
"@tanstack/react-router": "^1.170.32",
"@tanstack/react-start": "^1.168.49",
"@tanstack/router-plugin": "^1.168.35",
"lucide-react": "^0.561.0",
"nitro": "3.0.260610-beta",
"react": "^19.2.3",
Expand Down
6 changes: 3 additions & 3 deletions examples/ts-react-rerank/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
"@tanstack/ai": "workspace:*",
"@tanstack/ai-cohere": "workspace:*",
"@tanstack/ai-openrouter": "workspace:*",
"@tanstack/react-router": "^1.158.4",
"@tanstack/react-start": "^1.159.0",
"@tanstack/router-plugin": "^1.158.4",
"@tanstack/react-router": "^1.170.32",
"@tanstack/react-start": "^1.168.49",
"@tanstack/router-plugin": "^1.168.35",
"lucide-react": "^0.561.0",
"nitro": "3.0.260610-beta",
"react": "^19.2.3",
Expand Down
10 changes: 5 additions & 5 deletions examples/ts-react-search/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
"@tanstack/react-db": "^0.1.55",
"@tanstack/react-devtools": "^0.9.10",
"@tanstack/react-query": "^5.90.12",
"@tanstack/react-router": "^1.158.4",
"@tanstack/react-router-devtools": "^1.158.4",
"@tanstack/react-router-ssr-query": "^1.158.4",
"@tanstack/react-start": "^1.159.0",
"@tanstack/router-plugin": "^1.158.4",
"@tanstack/react-router": "^1.170.32",
"@tanstack/react-router-devtools": "^1.167.1",
"@tanstack/react-router-ssr-query": "^1.167.2",
"@tanstack/react-start": "^1.168.49",
"@tanstack/router-plugin": "^1.168.35",
"@tanstack/zod-adapter": "^1.140.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand Down
8 changes: 4 additions & 4 deletions examples/ts-react-ui-chatbot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
"@tanstack/ai-openai": "workspace:*",
"@tanstack/ai-react": "workspace:*",
"@tanstack/react-devtools": "^0.9.10",
"@tanstack/react-router": "^1.158.4",
"@tanstack/react-router-devtools": "^1.158.4",
"@tanstack/react-start": "^1.159.0",
"@tanstack/router-plugin": "^1.158.4",
"@tanstack/react-router": "^1.170.32",
"@tanstack/react-router-devtools": "^1.167.1",
"@tanstack/react-start": "^1.168.49",
"@tanstack/router-plugin": "^1.168.35",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.561.0",
Expand Down
Loading
Loading