From fb90cdea738a34496263a613165afa9a368678a6 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Mon, 24 Aug 2026 16:25:22 +0200 Subject: [PATCH 1/4] feat(ai-client): hold UI snapshots in a TanStack Store atom Framework hooks read getSnapshot instead of copying fields through change callbacks. useChat return shape stays the same. --- .changeset/ai-client-store-snapshot.md | 14 ++ docs/api/ai-client.md | 28 +++ examples/ts-code-mode-web/package.json | 2 +- examples/ts-react-chat/package.json | 2 +- examples/ts-solid-chat/package.json | 2 +- packages/ai-angular/src/inject-chat.ts | 32 +-- .../ai-angular/src/inject-generate-video.ts | 35 ++-- packages/ai-angular/src/inject-generation.ts | 27 ++- packages/ai-client/package.json | 3 +- packages/ai-client/src/audio-recorder.ts | 32 +-- packages/ai-client/src/byok/client.ts | 20 +- packages/ai-client/src/chat-client.ts | 71 ++++++- packages/ai-client/src/generation-client.ts | 28 +++ packages/ai-client/src/generation-types.ts | 23 ++ packages/ai-client/src/index.ts | 3 + packages/ai-client/src/realtime-client.ts | 42 +++- packages/ai-client/src/snapshot-atom.ts | 41 ++++ packages/ai-client/src/types.ts | 21 ++ .../ai-client/src/video-generation-client.ts | 33 +++ packages/ai-client/tests/chat-client.test.ts | 30 +++ .../ai-client/tests/client-snapshots.test.ts | 94 +++++++++ .../ai-client/tests/snapshot-atom.test.ts | 31 +++ packages/ai-octane/src/use-chat.tsrx | 125 +++-------- .../ai-octane/src/use-generate-video.tsrx | 20 +- packages/ai-octane/src/use-generation.tsrx | 16 +- packages/ai-octane/src/use-realtime-chat.tsrx | 30 ++- packages/ai-preact/src/use-chat.ts | 194 +++-------------- packages/ai-react/src/use-chat.ts | 196 ++++-------------- packages/ai-react/src/use-generate-video.ts | 58 ++---- packages/ai-react/src/use-generation.ts | 46 ++-- packages/ai-react/src/use-realtime-chat.ts | 65 ++---- packages/ai-solid/src/use-chat.ts | 91 +++----- packages/ai-solid/src/use-generate-video.ts | 34 ++- packages/ai-solid/src/use-generation.ts | 26 +-- packages/ai-svelte/src/create-chat.svelte.ts | 75 ++----- .../src/create-generate-video.svelte.ts | 49 ++--- .../ai-svelte/src/create-generation.svelte.ts | 39 ++-- .../tests/create-generation-dispose.test.ts | 35 ++++ packages/ai-vue/src/use-chat.ts | 76 ++----- packages/ai-vue/src/use-generate-video.ts | 41 ++-- packages/ai-vue/src/use-generation.ts | 31 +-- pnpm-lock.yaml | 20 +- 42 files changed, 916 insertions(+), 965 deletions(-) create mode 100644 .changeset/ai-client-store-snapshot.md create mode 100644 packages/ai-client/src/snapshot-atom.ts create mode 100644 packages/ai-client/tests/client-snapshots.test.ts create mode 100644 packages/ai-client/tests/snapshot-atom.test.ts create mode 100644 packages/ai-svelte/tests/create-generation-dispose.test.ts diff --git a/.changeset/ai-client-store-snapshot.md b/.changeset/ai-client-store-snapshot.md new file mode 100644 index 0000000000..6115b3dad5 --- /dev/null +++ b/.changeset/ai-client-store-snapshot.md @@ -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. `useChat()` return shape stays the same. + +`ChatClient` adds `subscribeSnapshot` / `getSnapshot`. The live connection method stays `subscribe()`. Generation, video, realtime, audio, and BYOK clients expose the same snapshot pair (`subscribe` / `getSnapshot`) where the name does not collide. diff --git a/docs/api/ai-client.md b/docs/api/ai-client.md index 16456962b5..49239f634c 100644 --- a/docs/api/ai-client.md +++ b/docs/api/ai-client.md @@ -216,6 +216,34 @@ 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 UI snapshot: messages, status, loading, error, queue, run +id, subscription, connection, session generating, and interrupt state. + +`subscribeSnapshot(listener)` calls `listener` after that snapshot changes. 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"), +}); + +const stop = client.subscribeSnapshot(() => { + render(client.getSnapshot()); +}); + +client.attach(); +client.sendMessage("Hello"); +``` + +`GenerationClient`, `VideoGenerationClient`, `RealtimeClient`, `AudioRecorder`, +and `ByokClient` use `subscribe(listener)` / `getSnapshot()` for the same job. +Their `subscribe` name does not collide with a live connection method. + #### `stop()` Stops the current response generation. It aborts the in-flight request. diff --git a/examples/ts-code-mode-web/package.json b/examples/ts-code-mode-web/package.json index 6ffbec8b6e..ed5a5978ee 100644 --- a/examples/ts-code-mode-web/package.json +++ b/examples/ts-code-mode-web/package.json @@ -35,7 +35,7 @@ "@tanstack/react-router": "^1.158.4", "@tanstack/react-start": "^1.159.0", "@tanstack/router-plugin": "^1.158.4", - "@tanstack/store": "^0.8.0", + "@tanstack/store": "^0.11.1", "framer-motion": "^11.15.0", "highlight.js": "^11.11.1", "isolated-vm": "^6.0.2", diff --git a/examples/ts-react-chat/package.json b/examples/ts-react-chat/package.json index 15cd77c5b5..d2bfd2fcf6 100644 --- a/examples/ts-react-chat/package.json +++ b/examples/ts-react-chat/package.json @@ -54,7 +54,7 @@ "@tanstack/react-start": "^1.159.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-plugin": "^1.158.4", - "@tanstack/store": "^0.8.0", + "@tanstack/store": "^0.11.1", "dockerode": "^4.0.2", "highlight.js": "^11.11.1", "lucide-react": "^0.561.0", diff --git a/examples/ts-solid-chat/package.json b/examples/ts-solid-chat/package.json index c00979bd29..56b0356600 100644 --- a/examples/ts-solid-chat/package.json +++ b/examples/ts-solid-chat/package.json @@ -29,7 +29,7 @@ "@tanstack/solid-router-ssr-query": "^1.139.10", "@tanstack/solid-start": "^1.139.10", "@tanstack/solid-store": "^0.8.0", - "@tanstack/store": "^0.8.0", + "@tanstack/store": "^0.11.1", "highlight.js": "^11.11.1", "lucide-solid": "^0.554.0", "solid-js": "^1.9.10", diff --git a/packages/ai-angular/src/inject-chat.ts b/packages/ai-angular/src/inject-chat.ts index 58a4a13502..77847b3823 100644 --- a/packages/ai-angular/src/inject-chat.ts +++ b/packages/ai-angular/src/inject-chat.ts @@ -133,13 +133,7 @@ export function injectChat< onChunk: (chunk: StreamChunk) => options.onChunk?.(chunk), onFinish: (message) => options.onFinish?.(message), onError: (err) => options.onError?.(err), - onRunIdChange: (nextRunId) => runId.set(nextRunId), - // No `onResumeStateChange`: the run identity is surfaced as the `runId` - // signal (via `onRunIdChange`) and pending interrupts arrive through - // `onInterruptStateChange`, so there is nothing left for it to do — and it - // is not a public option here, matching the other framework packages. onInterruptStateChange: (nextInterruptState, context) => { - interruptState.set(nextInterruptState) options.onInterruptStateChange?.(nextInterruptState, context) }, tools: options.tools, @@ -151,19 +145,24 @@ export function injectChat< ...(options.streamProcessor !== undefined && { streamProcessor: options.streamProcessor, }), - onMessagesChange: (m: Array>) => messages.set(m), - onLoadingChange: (v: boolean) => isLoading.set(v), - onStatusChange: (v: ChatClientState) => status.set(v), - onErrorChange: (v: Error | undefined) => error.set(v), - onSubscriptionChange: (v: boolean) => isSubscribed.set(v), - onConnectionStatusChange: (v: ConnectionStatus) => connectionStatus.set(v), - onSessionGeneratingChange: (v: boolean) => sessionGenerating.set(v), ...(options.queue !== undefined && { queue: options.queue }), - onQueueChange: (nextQueue: Array) => queue.set(nextQueue), }) - messages.set(client.getMessages()) - interruptState.set(client.getInterruptState()) + const applySnapshot = () => { + const next = client.getSnapshot() + messages.set(next.messages) + isLoading.set(next.isLoading) + error.set(next.error) + status.set(next.status) + isSubscribed.set(next.isSubscribed) + connectionStatus.set(next.connectionStatus) + sessionGenerating.set(next.sessionGenerating) + queue.set(next.queue) + runId.set(next.runId) + interruptState.set(next.interruptState) + } + applySnapshot() + const unsubscribeSnapshot = client.subscribeSnapshot(applySnapshot) // START TAILING HERE, not in the constructor. A client is idle until something // attaches it, so a client that gets built and thrown away never opens a @@ -217,6 +216,7 @@ export function injectChat< ) destroyRef.onDestroy(() => { + unsubscribeSnapshot() // Release the connection first: the counterpart of the `attach` above. client.detach() if (liveSource?.()) { diff --git a/packages/ai-angular/src/inject-generate-video.ts b/packages/ai-angular/src/inject-generate-video.ts index 3287fbea29..43f19d7afb 100644 --- a/packages/ai-angular/src/inject-generate-video.ts +++ b/packages/ai-angular/src/inject-generate-video.ts @@ -181,27 +181,6 @@ export function injectGenerateVideo( onStatusUpdate: (s: VideoStatusInfo) => { if (!disposed) options.onStatusUpdate?.(s) }, - onResultChange: (r: TOutput | null) => { - if (!disposed) result.set(r) - }, - onLoadingChange: (l: boolean) => { - if (!disposed) isLoading.set(l) - }, - onErrorChange: (e: Error | undefined) => { - if (!disposed) error.set(e) - }, - onStatusChange: (s: GenerationClientState) => { - if (!disposed) status.set(s) - }, - onJobIdChange: (id: string | null) => { - if (!disposed) jobId.set(id) - }, - onVideoStatusChange: (s: VideoStatusInfo | null) => { - if (!disposed) videoStatus.set(s) - }, - onResumeStateChange: (rs: { runId: string } | null) => { - if (!disposed) runId.set(rs?.runId ?? null) - }, } let client: VideoGenerationClient @@ -221,6 +200,19 @@ export function injectGenerateVideo( ) } + const applySnapshot = () => { + const next = client.getSnapshot() + result.set(next.result) + isLoading.set(next.isLoading) + error.set(next.error) + status.set(next.status) + runId.set(next.runId) + jobId.set(next.jobId) + videoStatus.set(next.videoStatus) + } + applySnapshot() + const unsubscribeSnapshot = client.subscribe(applySnapshot) + if (bodySource) { effect( () => { @@ -242,6 +234,7 @@ export function injectGenerateVideo( ) destroyRef.onDestroy(() => { disposed = true + unsubscribeSnapshot() client.dispose() }) diff --git a/packages/ai-angular/src/inject-generation.ts b/packages/ai-angular/src/inject-generation.ts index dfd095e079..a103f38744 100644 --- a/packages/ai-angular/src/inject-generation.ts +++ b/packages/ai-angular/src/inject-generation.ts @@ -205,21 +205,6 @@ export function injectGeneration< onChunk: (c: StreamChunk) => { if (!disposed) options.onChunk?.(c) }, - onResultChange: (r: TOutput | null) => { - if (!disposed) result.set(r) - }, - onLoadingChange: (l: boolean) => { - if (!disposed) isLoading.set(l) - }, - onErrorChange: (e: Error | undefined) => { - if (!disposed) error.set(e) - }, - onStatusChange: (s: GenerationClientState) => { - if (!disposed) status.set(s) - }, - onResumeStateChange: (rs) => { - if (!disposed) runId.set(rs?.runId ?? null) - }, } const persistenceProps = @@ -253,6 +238,17 @@ export function injectGeneration< ) } + const applySnapshot = () => { + const next = client.getSnapshot() + result.set(next.result) + isLoading.set(next.isLoading) + error.set(next.error) + status.set(next.status) + runId.set(next.runId) + } + applySnapshot() + const unsubscribeSnapshot = client.subscribe(applySnapshot) + if (bodySource) { effect( () => { @@ -274,6 +270,7 @@ export function injectGeneration< ) destroyRef.onDestroy(() => { disposed = true + unsubscribeSnapshot() client.dispose() }) diff --git a/packages/ai-client/package.json b/packages/ai-client/package.json index d8112524e9..7c77b16585 100644 --- a/packages/ai-client/package.json +++ b/packages/ai-client/package.json @@ -67,7 +67,8 @@ "dependencies": { "@tanstack/ai": "workspace:^", "@tanstack/ai-event-client": "workspace:^", - "@tanstack/ai-utils": "workspace:^" + "@tanstack/ai-utils": "workspace:^", + "@tanstack/store": "^0.11.1" }, "devDependencies": { "@standard-schema/spec": "^1.1.0", diff --git a/packages/ai-client/src/audio-recorder.ts b/packages/ai-client/src/audio-recorder.ts index 0f2b79737f..7077a876ba 100644 --- a/packages/ai-client/src/audio-recorder.ts +++ b/packages/ai-client/src/audio-recorder.ts @@ -1,5 +1,7 @@ import { arrayBufferToBase64 } from '@tanstack/ai-utils' import type { AudioPart } from '@tanstack/ai/client' +import { createAtom } from './snapshot-atom' +import type { Atom } from './snapshot-atom' /** Lifecycle state of an {@link AudioRecorder}. */ export type AudioRecorderState = 'idle' | 'recording' | 'stopping' @@ -62,13 +64,13 @@ export class AudioRecorder { private stream: MediaStream | null = null private chunks: Array = [] private startedAt = 0 - private _state: AudioRecorderState = 'idle' + private readonly snapshotAtom: Atom = + createAtom('idle') // True while start() is awaiting getUserMedia (state is still 'idle' then). private starting = false // Set by cancel()/teardown during that window so start() releases the // freshly acquired stream instead of beginning a leaked recording. private pendingCancel = false - private readonly listeners = new Set<(state: AudioRecorderState) => void>() private stopResolve: ((recording: AudioRecording) => void) | null = null private stopReject: ((error: Error) => void) | null = null @@ -87,25 +89,25 @@ export class AudioRecorder { } get state(): AudioRecorderState { - return this._state + return this.snapshotAtom.get() } subscribe(cb: (state: AudioRecorderState) => void): () => void { - this.listeners.add(cb) - return () => { - this.listeners.delete(cb) - } + const { unsubscribe } = this.snapshotAtom.subscribe((next) => { + cb(next) + }) + return unsubscribe } + /** Current recorder lifecycle state. */ + getSnapshot = (): AudioRecorderState => this.snapshotAtom.get() + private setState(state: AudioRecorderState): void { - this._state = state - for (const cb of this.listeners) { - cb(state) - } + this.snapshotAtom.set(state) } async start(): Promise { - if (this._state !== 'idle' || this.starting) { + if (this.state !== 'idle' || this.starting) { return } this.starting = true @@ -168,7 +170,7 @@ export class AudioRecorder { } stop(): Promise { - if (this._state !== 'recording' || !this.recorder) { + if (this.state !== 'recording' || !this.recorder) { return Promise.reject( new Error('AudioRecorder.stop() called while not recording'), ) @@ -179,7 +181,7 @@ export class AudioRecorder { // Some browsers/codecs never fire onstop; this watchdog unwedges the // recorder instead of leaking this promise forever. const watchdog = setTimeout(() => { - if (this._state !== 'stopping') { + if (this.state !== 'stopping') { return } // Detach handlers so a late onstop/onerror from the stalled recorder @@ -215,7 +217,7 @@ export class AudioRecorder { this.pendingCancel = true return } - if (this._state === 'idle') { + if (this.state === 'idle') { return } const recorder = this.recorder diff --git a/packages/ai-client/src/byok/client.ts b/packages/ai-client/src/byok/client.ts index 0324349e1e..1808ec996b 100644 --- a/packages/ai-client/src/byok/client.ts +++ b/packages/ai-client/src/byok/client.ts @@ -4,6 +4,8 @@ import { isProviderId, maskKey, } from '@tanstack/ai/byok' +import { createAtom, subscribeAtom } from '../snapshot-atom' +import type { Atom } from '../snapshot-atom' import { memoryStorage } from './storage' import type { ProviderId } from '@tanstack/ai/byok' import type { Keyring, KeyringStorage } from './storage' @@ -85,29 +87,24 @@ export class ByokClient { #prompt: ByokPrompt | null = null #coverageAll = false #coverage: Record = {} - readonly #listeners = new Set<() => void>() - #snapshot: ByokSnapshot #storageError: string | null = null + readonly #snapshotAtom: Atom readonly #ready: Promise constructor(options: DefineByokOptions = {}) { this.storage = options.storage ?? memoryStorage() this.#locked = Boolean(this.storage.unlockable) - this.#snapshot = this.#buildSnapshot() + this.#snapshotAtom = createAtom(this.#buildSnapshot()) this.#ready = this.#hydrate() } /** Resolves when constructor hydration (peek/load) finishes. */ ready = (): Promise => this.#ready - subscribe = (listener: () => void): (() => void) => { - this.#listeners.add(listener) - return () => { - this.#listeners.delete(listener) - } - } + subscribe = (listener: () => void): (() => void) => + subscribeAtom(this.#snapshotAtom, listener) - getSnapshot = (): ByokSnapshot => this.#snapshot + getSnapshot = (): ByokSnapshot => this.#snapshotAtom.get() #buildSnapshot(): ByokSnapshot { return { @@ -350,8 +347,7 @@ export class ByokClient { } #emit(): void { - this.#snapshot = this.#buildSnapshot() - for (const listener of this.#listeners) listener() + this.#snapshotAtom.set(this.#buildSnapshot()) } } diff --git a/packages/ai-client/src/chat-client.ts b/packages/ai-client/src/chat-client.ts index 0d320cdd84..26d5612772 100644 --- a/packages/ai-client/src/chat-client.ts +++ b/packages/ai-client/src/chat-client.ts @@ -52,9 +52,12 @@ import type { ChatDevtoolsBridge, ChatDevtoolsBridgeOptions, } from './devtools' +import { createAtom, patchAtom, subscribeAtom } from './snapshot-atom' +import type { Atom } from './snapshot-atom' import type { BoundInterrupts, ChatClientOptions, + ChatClientSnapshot, ChatClientState, ChatFetcher, ChatInterruptState, @@ -334,6 +337,24 @@ export class ChatClient< private readonly persistor?: ChatPersistor private readonly clearedStreamTracker = new ClearedStreamTracker() private currentRunId: string | null = null + private readonly snapshotAtom: Atom> = + createAtom>({ + messages: [], + status: 'ready', + isLoading: false, + error: undefined, + isSubscribed: false, + connectionStatus: 'disconnected', + sessionGenerating: false, + queue: [], + runId: null, + interruptState: { + interrupts: Object.freeze([]), + pendingInterrupts: Object.freeze([]), + interruptErrors: Object.freeze([]), + resuming: false, + } as ChatInterruptState, + }) // Interrupt-resume tracking: the run/thread of the most recent interrupted // run, so approvals/client-tool results can be sent back. Cleared when the // run terminates. This is STATE (interrupt) resume, not delivery/cursor. @@ -627,6 +648,9 @@ export class ChatClient< events: { onMessagesChange: (messages: Array) => { this.persistor?.notifyMessagesChanged(messages) + this.patchSnapshot({ + messages: messages as Array>, + }) this.callbacksRef.current.onMessagesChange(messages) }, onStreamStart: () => { @@ -834,6 +858,7 @@ export class ChatClient< }, }) + this.snapshotAtom.set(this.readSnapshot()) this.persistor?.hydrateAsync(persistedState) this.rejoinRunId = rejoinRunId @@ -1255,6 +1280,7 @@ export class ChatClient< private setCurrentRunId(runId: string | null): void { if (this.currentRunId === runId) return this.currentRunId = runId + this.patchSnapshot({ runId }) this.callbacksRef.current.onRunIdChange(runId) } @@ -1446,26 +1472,64 @@ export class ChatClient< return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}` } + private readSnapshot(): ChatClientSnapshot { + return { + messages: this.processor.getMessages() as Array>, + status: this.status, + isLoading: this.isLoading, + error: this.error, + isSubscribed: this.isSubscribed, + connectionStatus: this.connectionStatus, + sessionGenerating: this.sessionGenerating, + queue: this.getQueue(), + runId: this.currentRunId, + interruptState: this.interruptManager.getState(), + } + } + + private patchSnapshot( + patch: Partial>, + ): void { + patchAtom(this.snapshotAtom, patch) + } + + /** + * Subscribe to UI snapshot changes. Does not fire with the current value; + * read {@link getSnapshot} first. + * + * Named separately from {@link subscribe} (the live connection loop). + */ + subscribeSnapshot = (listener: () => void): (() => void) => + subscribeAtom(this.snapshotAtom, listener) + + /** Current UI snapshot. */ + getSnapshot = (): ChatClientSnapshot => + this.snapshotAtom.get() + private setIsLoading(isLoading: boolean): void { this.isLoading = isLoading + this.patchSnapshot({ isLoading }) this.callbacksRef.current.onLoadingChange(isLoading) this.events.loadingChanged(isLoading) } private setStatus(status: ChatClientState): void { this.status = status + this.patchSnapshot({ status }) this.callbacksRef.current.onStatusChange(status) this.devtoolsBridge.emitSnapshot() } private setIsSubscribed(isSubscribed: boolean): void { this.isSubscribed = isSubscribed + this.patchSnapshot({ isSubscribed }) this.callbacksRef.current.onSubscriptionChange(isSubscribed) this.devtoolsBridge.emitSnapshot() } private setConnectionStatus(status: ConnectionStatus): void { this.connectionStatus = status + this.patchSnapshot({ connectionStatus: status }) this.callbacksRef.current.onConnectionStatusChange(status) this.devtoolsBridge.emitSnapshot() } @@ -1473,6 +1537,7 @@ export class ChatClient< private setSessionGenerating(isGenerating: boolean): void { if (this.sessionGenerating === isGenerating) return this.sessionGenerating = isGenerating + this.patchSnapshot({ sessionGenerating: isGenerating }) this.callbacksRef.current.onSessionGeneratingChange(isGenerating) this.devtoolsBridge.emitSnapshot() } @@ -1482,6 +1547,7 @@ export class ChatClient< // Capture state before invoking callbacks so a synchronous nested change // cannot pair this publication's source with a later manager snapshot. const interruptState = this.interruptManager.getState() + this.patchSnapshot({ interruptState }) // Persist (or clear) the durable resume snapshot so a full page reload can // rehydrate pending interrupts and rejoin the run. Folded into the same // persistence adapter that stores messages (one record per chat). @@ -1524,6 +1590,7 @@ export class ChatClient< private setError(error: Error | undefined): void { this.error = error + this.patchSnapshot({ error }) this.callbacksRef.current.onErrorChange(error) this.events.errorChanged(error?.message || null) } @@ -2939,7 +3006,9 @@ export class ChatClient< } private emitQueueChange(): void { - this.callbacksRef.current.onQueueChange(this.getQueue()) + const queue = this.getQueue() + this.patchSnapshot({ queue }) + this.callbacksRef.current.onQueueChange(queue) this.devtoolsBridge.emitSnapshot() } diff --git a/packages/ai-client/src/generation-client.ts b/packages/ai-client/src/generation-client.ts index b61634bd9b..c7a36b0456 100644 --- a/packages/ai-client/src/generation-client.ts +++ b/packages/ai-client/src/generation-client.ts @@ -29,8 +29,11 @@ import type { GenerationDevtoolsBridge, GenerationDevtoolsBridgeOptions, } from './devtools' +import { createAtom, patchAtom, subscribeAtom } from './snapshot-atom' +import type { Atom } from './snapshot-atom' import type { GenerationClientOptions, + GenerationClientSnapshot, GenerationClientState, GenerationFetcher, GenerationPersistenceOptions, @@ -134,6 +137,14 @@ export class GenerationClient< private isLoading = false private error: Error | undefined = undefined private status: GenerationClientState = 'idle' + private readonly snapshotAtom: Atom> = + createAtom>({ + result: null, + isLoading: false, + error: undefined, + status: 'idle', + runId: null, + }) private resumeSnapshot: GenerationResumeSnapshot | undefined private lastEmittedResumeState: string | undefined private abortController: AbortController | null = null @@ -551,6 +562,16 @@ export class GenerationClient< // Getters // =========================== + /** + * Subscribe to UI snapshot changes. Does not fire with the current value; + * read {@link getSnapshot} first. + */ + subscribe = (listener: () => void): (() => void) => + subscribeAtom(this.snapshotAtom, listener) + + /** Current UI snapshot. */ + getSnapshot = (): GenerationClientSnapshot => this.snapshotAtom.get() + getResult(): TOutput | null { return this.result } @@ -601,6 +622,7 @@ export class GenerationClient< private setResult(rawResult: TResult | null): void { if (rawResult === null) { this.result = null + patchAtom(this.snapshotAtom, { result: null }) this.callbacksRef.onResultChange?.(null) this.devtoolsBridge.recordResultChange() return @@ -616,6 +638,7 @@ export class GenerationClient< if (transformed !== undefined) { // Non-null, non-undefined → use transformed value this.result = transformed + patchAtom(this.snapshotAtom, { result: this.result }) this.callbacksRef.onResultChange?.(this.result) this.devtoolsBridge.recordResultChange() return @@ -627,24 +650,28 @@ export class GenerationClient< // `TOutput` defaults to `TResult`, so the runtime cast is sound. // oxlint-disable-next-line eslint-js/no-restricted-syntax -- TOutput defaults to TResult when no onResult transform is supplied this.result = rawResult as unknown as TOutput + patchAtom(this.snapshotAtom, { result: this.result }) this.callbacksRef.onResultChange?.(this.result) this.devtoolsBridge.recordResultChange() } private setIsLoading(isLoading: boolean): void { this.isLoading = isLoading + patchAtom(this.snapshotAtom, { isLoading }) this.callbacksRef.onLoadingChange?.(isLoading) this.devtoolsBridge.recordLoadingChange() } private setError(error: Error | undefined): void { this.error = error + patchAtom(this.snapshotAtom, { error }) this.callbacksRef.onErrorChange?.(error) this.devtoolsBridge.recordErrorChange(error) } private setStatus(status: GenerationClientState): void { this.status = status + patchAtom(this.snapshotAtom, { status }) this.callbacksRef.onStatusChange?.(status) this.devtoolsBridge.recordStatusChange(status) } @@ -741,6 +768,7 @@ export class GenerationClient< return } this.lastEmittedResumeState = signature + patchAtom(this.snapshotAtom, { runId: resumeState?.runId ?? null }) this.callbacksRef.onResumeStateChange?.(resumeState) } diff --git a/packages/ai-client/src/generation-types.ts b/packages/ai-client/src/generation-types.ts index bee7670bb0..cb2cf82c01 100644 --- a/packages/ai-client/src/generation-types.ts +++ b/packages/ai-client/src/generation-types.ts @@ -64,6 +64,29 @@ export type InferGenerationOutput = TFn extends ( */ export type GenerationClientState = 'idle' | 'generating' | 'success' | 'error' +/** + * UI snapshot published by {@link GenerationClient.subscribe} / + * {@link GenerationClient.getSnapshot}. + */ +export interface GenerationClientSnapshot { + result: TOutput | null + isLoading: boolean + error: Error | undefined + status: GenerationClientState + runId: string | null +} + +/** + * UI snapshot published by {@link VideoGenerationClient.subscribe} / + * {@link VideoGenerationClient.getSnapshot}. + */ +export interface VideoGenerationClientSnapshot< + TOutput = unknown, +> extends GenerationClientSnapshot { + jobId: string | null + videoStatus: VideoStatusInfo | null +} + /** * Status of a persisted/restored generation run. * diff --git a/packages/ai-client/src/index.ts b/packages/ai-client/src/index.ts index db4c11d2b1..a2b0e4c15a 100644 --- a/packages/ai-client/src/index.ts +++ b/packages/ai-client/src/index.ts @@ -50,6 +50,7 @@ export type { ChatRequestBody, InferChatMessages, InferredClientContext, + ChatClientSnapshot, ChatClientState, ConnectionStatus, ChatFetcher, @@ -70,7 +71,9 @@ export type { export type { InferGenerationOutput, InferGenerationOutputFromReturn, + GenerationClientSnapshot, GenerationClientState, + VideoGenerationClientSnapshot, GenerationPersistenceOptions, GenerationClientOptions, GenerationFetcher, diff --git a/packages/ai-client/src/realtime-client.ts b/packages/ai-client/src/realtime-client.ts index 7808818745..04b57d8088 100644 --- a/packages/ai-client/src/realtime-client.ts +++ b/packages/ai-client/src/realtime-client.ts @@ -1,4 +1,6 @@ import { convertSchemaToJsonSchema } from '@tanstack/ai/client' +import { createAtom, patchAtom, subscribeAtom } from './snapshot-atom' +import type { Atom } from './snapshot-atom' import type { AnyClientTool, AudioVisualization, @@ -49,13 +51,18 @@ export class RealtimeClient { new Set() private unsubscribers: Array<() => void> = [] - private state: RealtimeClientState = { - status: 'idle', - mode: 'idle', - messages: [], - pendingUserTranscript: null, - pendingAssistantTranscript: null, - error: null, + private readonly snapshotAtom: Atom = + createAtom({ + status: 'idle', + mode: 'idle', + messages: [], + pendingUserTranscript: null, + pendingAssistantTranscript: null, + error: null, + }) + + private get state(): RealtimeClientState { + return this.snapshotAtom.get() } constructor(options: RealtimeClientOptions) { @@ -88,7 +95,13 @@ export class RealtimeClient { return } - this.updateState({ status: 'connecting', error: null }) + this.updateState({ + status: 'connecting', + error: null, + messages: [], + pendingUserTranscript: null, + pendingAssistantTranscript: null, + }) try { // Fetch token from server @@ -195,6 +208,7 @@ export class RealtimeClient { return } this.connection.interrupt() + this.updateState({ pendingAssistantTranscript: null }) } // ============================================================================ @@ -331,6 +345,16 @@ export class RealtimeClient { } } + /** + * Subscribe to UI snapshot changes. Does not fire with the current value; + * read {@link getSnapshot} first. + */ + subscribe = (listener: () => void): (() => void) => + subscribeAtom(this.snapshotAtom, listener) + + /** Current UI snapshot. */ + getSnapshot = (): RealtimeClientState => this.snapshotAtom.get() + // ============================================================================ // Cleanup // ============================================================================ @@ -349,7 +373,7 @@ export class RealtimeClient { // ============================================================================ private updateState(updates: Partial): void { - this.state = { ...this.state, ...updates } + patchAtom(this.snapshotAtom, updates) // Notify callbacks for (const callback of this.stateChangeCallbacks) { diff --git a/packages/ai-client/src/snapshot-atom.ts b/packages/ai-client/src/snapshot-atom.ts new file mode 100644 index 0000000000..d1932dbed9 --- /dev/null +++ b/packages/ai-client/src/snapshot-atom.ts @@ -0,0 +1,41 @@ +import { createAtom } from '@tanstack/store' +import type { Atom } from '@tanstack/store' + +export { createAtom } +export type { Atom } + +/** + * Merge a partial update into an object atom. Skips notify when every + * provided field is `Object.is`-equal to the current value. + */ +export function patchAtom( + atom: Atom, + patch: Partial, +): void { + atom.set((prev) => { + let changed = false + const next = { ...prev } + for (const key of Object.keys(patch) as Array) { + if (Object.is(prev[key], patch[key])) continue + next[key] = patch[key] as T[keyof T] + changed = true + } + return changed ? next : prev + }) +} + +/** + * Adapt a Store atom's `{ unsubscribe }` subscription to the + * `() => void` unsubscribe that `useSyncExternalStore` expects. + */ +export function subscribeAtom( + atom: { + subscribe: (listener: (value: T) => void) => { unsubscribe: () => void } + }, + listener: () => void, +): () => void { + const { unsubscribe } = atom.subscribe(() => { + listener() + }) + return unsubscribe +} diff --git a/packages/ai-client/src/types.ts b/packages/ai-client/src/types.ts index 01373cef02..9c6868b837 100644 --- a/packages/ai-client/src/types.ts +++ b/packages/ai-client/src/types.ts @@ -364,6 +364,27 @@ export type ToolResultState = */ export type ChatClientState = 'ready' | 'submitted' | 'streaming' | 'error' +/** + * UI snapshot published by {@link ChatClient.subscribeSnapshot} / + * {@link ChatClient.getSnapshot}. + */ +export interface ChatClientSnapshot< + TTools extends ReadonlyArray = any, + TInterrupts extends ReadonlyArray> = + readonly [], +> { + messages: Array> + status: ChatClientState + isLoading: boolean + error: Error | undefined + isSubscribed: boolean + connectionStatus: ConnectionStatus + sessionGenerating: boolean + queue: Array + runId: string | null + interruptState: ChatInterruptState +} + /** * Connection lifecycle state for the subscription loop. */ diff --git a/packages/ai-client/src/video-generation-client.ts b/packages/ai-client/src/video-generation-client.ts index 7ae11c2fc1..fa9a2cb5f6 100644 --- a/packages/ai-client/src/video-generation-client.ts +++ b/packages/ai-client/src/video-generation-client.ts @@ -29,6 +29,8 @@ import type { VideoDevtoolsBridge, VideoDevtoolsBridgeOptions, } from './devtools' +import { createAtom, patchAtom, subscribeAtom } from './snapshot-atom' +import type { Atom } from './snapshot-atom' import type { GenerationClientState, GenerationFetcher, @@ -39,6 +41,7 @@ import type { VideoGenerateInput, VideoGenerateResult, VideoGenerationClientOptions, + VideoGenerationClientSnapshot, VideoStatusInfo, } from './generation-types' @@ -139,6 +142,16 @@ export class VideoGenerationClient { private isLoading = false private error: Error | undefined = undefined private status: GenerationClientState = 'idle' + private readonly snapshotAtom: Atom> = + createAtom>({ + result: null, + isLoading: false, + error: undefined, + status: 'idle', + runId: null, + jobId: null, + videoStatus: null, + }) private resumeSnapshot: GenerationResumeSnapshot | undefined private abortController: AbortController | null = null private rejoinedRunId: string | undefined @@ -558,6 +571,17 @@ export class VideoGenerationClient { // Getters // =========================== + /** + * Subscribe to UI snapshot changes. Does not fire with the current value; + * read {@link getSnapshot} first. + */ + subscribe = (listener: () => void): (() => void) => + subscribeAtom(this.snapshotAtom, listener) + + /** Current UI snapshot. */ + getSnapshot = (): VideoGenerationClientSnapshot => + this.snapshotAtom.get() + getResult(): TOutput | null { return this.result } @@ -616,6 +640,7 @@ export class VideoGenerationClient { private setResult(rawResult: VideoGenerateResult | null): void { if (rawResult === null) { this.result = null + patchAtom(this.snapshotAtom, { result: null }) this.callbacksRef.onResultChange?.(null) this.devtoolsBridge.recordResultChange() return @@ -638,6 +663,7 @@ export class VideoGenerationClient { if (transformed !== undefined) { // Non-null, non-undefined → use transformed value this.result = transformed + patchAtom(this.snapshotAtom, { result: this.result }) this.callbacksRef.onResultChange?.(this.result) this.devtoolsBridge.recordResultChange() return @@ -650,36 +676,42 @@ export class VideoGenerationClient { // sound. // oxlint-disable-next-line eslint-js/no-restricted-syntax -- TOutput defaults to VideoGenerateResult when no onResult transform is supplied this.result = rawResult as unknown as TOutput + patchAtom(this.snapshotAtom, { result: this.result }) this.callbacksRef.onResultChange?.(this.result) this.devtoolsBridge.recordResultChange() } private setJobId(jobId: string | null): void { this.jobId = jobId + patchAtom(this.snapshotAtom, { jobId }) this.callbacksRef.onJobIdChange?.(jobId) this.devtoolsBridge.recordJobIdChange() } private setVideoStatus(status: VideoStatusInfo | null): void { this.videoStatus = status + patchAtom(this.snapshotAtom, { videoStatus: status }) this.callbacksRef.onVideoStatusChange?.(status) this.devtoolsBridge.recordVideoStatusChange() } private setIsLoading(isLoading: boolean): void { this.isLoading = isLoading + patchAtom(this.snapshotAtom, { isLoading }) this.callbacksRef.onLoadingChange?.(isLoading) this.devtoolsBridge.recordLoadingChange() } private setError(error: Error | undefined): void { this.error = error + patchAtom(this.snapshotAtom, { error }) this.callbacksRef.onErrorChange?.(error) this.devtoolsBridge.recordErrorChange(error) } private setStatus(status: GenerationClientState): void { this.status = status + patchAtom(this.snapshotAtom, { status }) this.callbacksRef.onStatusChange?.(status) this.devtoolsBridge.recordStatusChange(status) } @@ -768,6 +800,7 @@ export class VideoGenerationClient { : {}), } : null + patchAtom(this.snapshotAtom, { runId: resumeState?.runId ?? null }) this.callbacksRef.onResumeStateChange?.(resumeState) } diff --git a/packages/ai-client/tests/chat-client.test.ts b/packages/ai-client/tests/chat-client.test.ts index 67429010b6..2be80b49fb 100644 --- a/packages/ai-client/tests/chat-client.test.ts +++ b/packages/ai-client/tests/chat-client.test.ts @@ -68,6 +68,36 @@ describe('ChatClient', () => { expect(client.getError()).toBeUndefined() }) + it('exposes a UI snapshot that subscribeSnapshot notifies', () => { + const adapter = createMockConnectionAdapter() + const initialMessages: Array = [ + { + id: 'msg-1', + role: 'user', + parts: [{ type: 'text', content: 'Hello' }], + createdAt: new Date(), + }, + ] + const client = new ChatClient({ + connection: adapter, + initialMessages, + }) + + const snapshot = client.getSnapshot() + expect(snapshot.messages).toEqual(initialMessages) + expect(snapshot.status).toBe('ready') + expect(snapshot.isLoading).toBe(false) + expect(client.getSnapshot()).toBe(snapshot) + + const listener = vi.fn() + const stop = client.subscribeSnapshot(listener) + client.setMessagesManually([]) + expect(listener).toHaveBeenCalledTimes(1) + expect(client.getSnapshot().messages).toEqual([]) + expect(client.getSnapshot()).not.toBe(snapshot) + stop() + }) + it('should initialize with provided messages', () => { const adapter = createMockConnectionAdapter() const initialMessages: Array = [ diff --git a/packages/ai-client/tests/client-snapshots.test.ts b/packages/ai-client/tests/client-snapshots.test.ts new file mode 100644 index 0000000000..90efed3e5b --- /dev/null +++ b/packages/ai-client/tests/client-snapshots.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it, vi } from 'vitest' +import { + AudioRecorder, + GenerationClient, + RealtimeClient, + VideoGenerationClient, +} from '../src' +import type { RealtimeAdapter } from '../src' + +describe('client UI snapshots', () => { + it('GenerationClient notifies subscribe and keeps getSnapshot identity until a change', async () => { + const client = new GenerationClient({ + fetcher: async () => ({ id: '1' }), + }) + const snapshot = client.getSnapshot() + expect(snapshot).toEqual({ + result: null, + isLoading: false, + error: undefined, + status: 'idle', + runId: null, + }) + expect(client.getSnapshot()).toBe(snapshot) + + const listener = vi.fn() + const stop = client.subscribe(listener) + await client.generate({ prompt: 'test' }) + + expect(listener).toHaveBeenCalled() + expect(client.getSnapshot().status).toBe('success') + expect(client.getSnapshot().result).toEqual({ id: '1' }) + expect(client.getSnapshot()).not.toBe(snapshot) + stop() + }) + + it('VideoGenerationClient snapshot includes job fields', async () => { + const result = { + jobId: 'job-1', + status: 'completed' as const, + url: 'https://example.com/video.mp4', + } + const client = new VideoGenerationClient({ + fetcher: async () => result, + }) + const snapshot = client.getSnapshot() + expect(snapshot.jobId).toBeNull() + expect(snapshot.videoStatus).toBeNull() + expect(client.getSnapshot()).toBe(snapshot) + + const listener = vi.fn() + const stop = client.subscribe(listener) + await client.generate({ prompt: 'test video' }) + + expect(listener).toHaveBeenCalled() + expect(client.getSnapshot().status).toBe('success') + expect(client.getSnapshot().result).toEqual(result) + expect(client.getSnapshot()).not.toBe(snapshot) + stop() + }) + + it('AudioRecorder getSnapshot matches state', () => { + const recorder = new AudioRecorder() + expect(recorder.getSnapshot()).toBe('idle') + expect(recorder.getSnapshot()).toBe(recorder.state) + }) + + it('RealtimeClient subscribe fires when connect fails', async () => { + const adapter: RealtimeAdapter = { + provider: 'test', + connect: async () => { + throw new Error('unused') + }, + } + const client = new RealtimeClient({ + getToken: async () => { + throw new Error('no token') + }, + adapter, + }) + const snapshot = client.getSnapshot() + expect(snapshot.status).toBe('idle') + expect(client.getSnapshot()).toBe(snapshot) + + const listener = vi.fn() + const stop = client.subscribe(listener) + await expect(client.connect()).rejects.toThrow('no token') + + expect(listener).toHaveBeenCalled() + expect(client.getSnapshot().status).toBe('error') + expect(client.getSnapshot()).not.toBe(snapshot) + stop() + client.destroy() + }) +}) diff --git a/packages/ai-client/tests/snapshot-atom.test.ts b/packages/ai-client/tests/snapshot-atom.test.ts new file mode 100644 index 0000000000..74da6dbdc1 --- /dev/null +++ b/packages/ai-client/tests/snapshot-atom.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it, vi } from 'vitest' +import { createAtom, patchAtom, subscribeAtom } from '../src/snapshot-atom' + +describe('snapshot-atom', () => { + it('patchAtom skips notify when every field is unchanged', () => { + const atom = createAtom({ count: 1, label: 'a' }) + const listener = vi.fn() + const stop = subscribeAtom(atom, listener) + + patchAtom(atom, { count: 1, label: 'a' }) + expect(listener).not.toHaveBeenCalled() + expect(atom.get()).toEqual({ count: 1, label: 'a' }) + + patchAtom(atom, { count: 2 }) + expect(listener).toHaveBeenCalledTimes(1) + expect(atom.get()).toEqual({ count: 2, label: 'a' }) + stop() + }) + + it('subscribeAtom adapts { unsubscribe } to a plain cleanup', () => { + const atom = createAtom(0) + const listener = vi.fn() + const stop = subscribeAtom(atom, listener) + + atom.set(1) + expect(listener).toHaveBeenCalledTimes(1) + stop() + atom.set(2) + expect(listener).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/ai-octane/src/use-chat.tsrx b/packages/ai-octane/src/use-chat.tsrx index f95f7f9fb8..d197d37ba9 100644 --- a/packages/ai-octane/src/use-chat.tsrx +++ b/packages/ai-octane/src/use-chat.tsrx @@ -90,12 +90,6 @@ export function useChat< useRef>(options) optionsRef.current = options - const syncResumeState = useCallback((target: ChatClient | null) => { - if (!target) return - setRunId(target.getCurrentRunId()) - setInterruptState(target.getInterruptState()) - }, []) - // Create ChatClient instance with callbacks to sync state const { client, initialization } = useMemo(() => { const messagesToUse = options.initialMessages || [] @@ -203,69 +197,11 @@ export function useChat< ...(options.streamProcessor !== undefined && { streamProcessor: options.streamProcessor, }), - onMessagesChange: (newMessages: Array>) => { - runOrQueueForActiveInstance(() => { - setMessages(newMessages) - }) - }, - onLoadingChange: (newIsLoading: boolean) => { - runOrQueueForActiveInstance(() => { - const currentInstance = getActiveInstance() - if (!currentInstance) return - setIsLoading(newIsLoading) - syncResumeState(currentInstance) - }) - }, - onErrorChange: (newError: Error | undefined) => { - runOrQueueForActiveInstance(() => { - setError(newError) - }) - }, - onStatusChange: (status: ChatClientState) => { - runOrQueueForActiveInstance(() => { - setStatus(status) - }) - }, - onSubscriptionChange: (nextIsSubscribed: boolean) => { - runOrQueueForActiveInstance(() => { - setIsSubscribed(nextIsSubscribed) - }) - }, - onConnectionStatusChange: (nextStatus: ConnectionStatus) => { - runOrQueueForActiveInstance(() => { - setConnectionStatus(nextStatus) - }) - }, - onSessionGeneratingChange: (isGenerating: boolean) => { - runOrQueueForActiveInstance(() => { - setSessionGenerating(isGenerating) - }) - }, ...(optionsRef.current.queue !== undefined && { queue: optionsRef.current.queue, }), - onQueueChange: (nextQueue: Array) => { - runOrQueueForActiveInstance(() => { - setQueue(nextQueue) - }) - }, - onRunIdChange: (nextRunId) => { - runOrQueueForActiveInstance(() => { - setRunId(nextRunId) - }) - }, - onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => { - runOrQueueForActiveInstance(() => { - setInterruptState((current) => ({ - ...current, - interrupts: nextPendingInterrupts, - pendingInterrupts: nextPendingInterrupts, - })) - }) - }, onInterruptStateChange: (nextInterruptState, context) => { runOrQueueForActiveInstance(() => { - setInterruptState(nextInterruptState) optionsRef.current.onInterruptStateChange?.( nextInterruptState, context, @@ -275,7 +211,7 @@ export function useChat< }) instanceHolder.current = instance return { client: instance, initialization: initializationState } - }, [clientId, syncResumeState]) + }, [clientId]) useEffect(() => { activeClientRef.current = client @@ -296,10 +232,21 @@ export function useChat< }, [client, initialization]) useEffect(() => { - const clientMessages = client.getMessages() - if (clientMessages !== messagesRef.current) { - setMessages(clientMessages) + const applySnapshot = () => { + const next = client.getSnapshot() + setMessages(next.messages) + setIsLoading(next.isLoading) + setError(next.error) + setStatus(next.status) + setIsSubscribed(next.isSubscribed) + setConnectionStatus(next.connectionStatus) + setSessionGenerating(next.sessionGenerating) + setQueue(next.queue) + setRunId(next.runId) + setInterruptState(next.interruptState) } + applySnapshot() + return client.subscribeSnapshot(applySnapshot) }, [client]) // OCTANE DIVERGENCE: upstream captures the initial transport. ChatClient @@ -369,7 +316,6 @@ export function useChat< useEffect(() => { client.attach() client.mountDevtools() - syncResumeState(client) return () => { client.detach() if (optionsRef.current.live) { @@ -382,20 +328,16 @@ export function useChat< activeClientRef.current = null } } - }, [client, syncResumeState]) + }, [client]) const sendMessage = useCallback( async ( content: string | MultimodalContent, sendOptions?: SendMessageOptions, ) => { - try { - await client.sendMessage(content, undefined, sendOptions) - } finally { - syncResumeState(client) - } + await client.sendMessage(content, undefined, sendOptions) }, - [client, syncResumeState], + [client], ) const cancelQueued = useCallback( @@ -407,22 +349,14 @@ export function useChat< const append = useCallback( async (message: ModelMessage | UIMessage) => { - try { - await client.append(message) - } finally { - syncResumeState(client) - } + await client.append(message) }, - [client, syncResumeState], + [client], ) const reload = useCallback(async () => { - try { - await client.reload() - } finally { - syncResumeState(client) - } - }, [client, syncResumeState]) + await client.reload() + }, [client]) const stop = useCallback(() => { client.stop() @@ -430,8 +364,7 @@ export function useChat< const clear = useCallback(() => { client.clear() - syncResumeState(client) - }, [client, syncResumeState]) + }, [client]) const setMessagesManually = useCallback( (newMessages: Array>) => { @@ -456,21 +389,15 @@ export function useChat< const addToolApprovalResponse = useCallback( async (response: { id: string; approved: boolean }) => { await client.addToolApprovalResponse(response) - syncResumeState(client) }, - [client, syncResumeState], + [client], ) const resumeInterrupts = useCallback( async (resumeItems: Array, state?: ChatResumeState) => { - const result = await client.resumeInterrupts( - resumeItems, - state ?? undefined, - ) - syncResumeState(client) - return result + return await client.resumeInterrupts(resumeItems, state ?? undefined) }, - [client, syncResumeState], + [client], ) const resolveInterrupts = useCallback( diff --git a/packages/ai-octane/src/use-generate-video.tsrx b/packages/ai-octane/src/use-generate-video.tsrx index 5772a97326..c18dddc7bf 100644 --- a/packages/ai-octane/src/use-generate-video.tsrx +++ b/packages/ai-octane/src/use-generate-video.tsrx @@ -172,12 +172,6 @@ export function useGenerateVideo( onStatusUpdate: (s: VideoStatusInfo) => { optionsRef.current.onStatusUpdate?.(s) }, - onResultChange: setResult, - onLoadingChange: setIsLoading, - onErrorChange: setError, - onStatusChange: setStatus, - onJobIdChange: setJobId, - onVideoStatusChange: setVideoStatus, } if (opts.connection) { @@ -199,6 +193,20 @@ export function useGenerateVideo( ) }, [clientId]) + useEffect(() => { + const applySnapshot = () => { + const next = client.getSnapshot() + setResult(next.result) + setIsLoading(next.isLoading) + setError(next.error) + setStatus(next.status) + setJobId(next.jobId) + setVideoStatus(next.videoStatus) + } + applySnapshot() + return client.subscribe(applySnapshot) + }, [client]) + // Sync body changes without recreating client useEffect(() => { // Conditional spread: target uses strict-optional `body?: T`. diff --git a/packages/ai-octane/src/use-generation.tsrx b/packages/ai-octane/src/use-generation.tsrx index e3ca19c084..23eb3b0489 100644 --- a/packages/ai-octane/src/use-generation.tsrx +++ b/packages/ai-octane/src/use-generation.tsrx @@ -160,10 +160,6 @@ export function useGeneration< onChunk: (c: StreamChunk) => { optionsRef.current.onChunk?.(c) }, - onResultChange: setResult, - onLoadingChange: setIsLoading, - onErrorChange: setError, - onStatusChange: setStatus, } if (opts.connection) { @@ -185,6 +181,18 @@ export function useGeneration< ) }, [clientId]) + useEffect(() => { + const applySnapshot = () => { + const next = client.getSnapshot() + setResult(next.result) + setIsLoading(next.isLoading) + setError(next.error) + setStatus(next.status) + } + applySnapshot() + return client.subscribe(applySnapshot) + }, [client]) + // Sync body changes without recreating client useEffect(() => { // Conditional spread: target uses strict-optional `body?: T`. diff --git a/packages/ai-octane/src/use-realtime-chat.tsrx b/packages/ai-octane/src/use-realtime-chat.tsrx index b486b8cc12..4ed30a20ee 100644 --- a/packages/ai-octane/src/use-realtime-chat.tsrx +++ b/packages/ai-octane/src/use-realtime-chat.tsrx @@ -124,17 +124,14 @@ export function useRealtimeChat( semanticEagerness: initial.semanticEagerness, }), onStatusChange: (newStatus) => { - setStatus(newStatus) // OCTANE DIVERGENCE: upstream declares this callback but does not // forward status changes beyond its own hook state. optionsRef.current.onStatusChange?.(newStatus) }, onModeChange: (newMode) => { - setMode(newMode) optionsRef.current.onModeChange?.(newMode) }, onMessage: (message) => { - setMessages((prev) => [...prev, message]) optionsRef.current.onMessage?.(message) }, onUsage: (usage) => { @@ -144,31 +141,36 @@ export function useRealtimeChat( optionsRef.current.onGoAway?.(timeLeft) }, onError: (err) => { - setError(err) optionsRef.current.onError?.(err) }, onConnect: () => { - setError(null) optionsRef.current.onConnect?.() }, onDisconnect: () => { optionsRef.current.onDisconnect?.() }, onInterrupted: () => { - setPendingAssistantTranscript(null) optionsRef.current.onInterrupted?.() }, }) - - // Subscribe to state changes for transcripts - clientRef.current.onStateChange((state) => { - setPendingUserTranscript(state.pendingUserTranscript) - setPendingAssistantTranscript(state.pendingAssistantTranscript) - }) } const client = clientRef.current + useEffect(() => { + const applySnapshot = () => { + const next = client.getSnapshot() + setStatus(next.status) + setMode(next.mode) + setMessages(next.messages) + setPendingUserTranscript(next.pendingUserTranscript) + setPendingAssistantTranscript(next.pendingAssistantTranscript) + setError(next.error) + } + applySnapshot() + return client.subscribe(applySnapshot) + }, [client]) + // Audio level animation loop useEffect(() => { function updateLevels() { @@ -200,10 +202,6 @@ export function useRealtimeChat( // Connection methods const connect = useCallback(async () => { - setError(null) - setMessages([]) - setPendingUserTranscript(null) - setPendingAssistantTranscript(null) await client.connect() }, [client]) diff --git a/packages/ai-preact/src/use-chat.ts b/packages/ai-preact/src/use-chat.ts index f8376da1ee..2eaaeb7c62 100644 --- a/packages/ai-preact/src/use-chat.ts +++ b/packages/ai-preact/src/use-chat.ts @@ -1,21 +1,11 @@ import { ChatClient } from '@tanstack/ai-client' import { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools' -import { - useCallback, - useEffect, - useId, - useMemo, - useRef, - useState, -} from 'preact/hooks' +import { useSyncExternalStore } from 'preact/compat' +import { useCallback, useEffect, useId, useMemo, useRef } from 'preact/hooks' import type { - ChatClientState, ResolvableChatInterrupt, - ChatInterruptState, ChatResumeState, - ConnectionStatus, InferredClientContext, - QueuedMessage, SendMessageOptions, } from '@tanstack/ai-client' import type { @@ -32,9 +22,6 @@ import type { UseChatReturn, } from './types' -const EMPTY_INTERRUPTS = Object.freeze([]) -const EMPTY_INTERRUPT_ERRORS = Object.freeze([]) - export function useChat< const TTools extends ReadonlyArray = any, TContext = InferredClientContext, @@ -50,32 +37,6 @@ export function useChat< const hookId = useId() const clientId = options.threadId ?? hookId - const [messages, setMessages] = useState>>( - options.initialMessages || [], - ) - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(undefined) - const [status, setStatus] = useState('ready') - const [isSubscribed, setIsSubscribed] = useState(false) - const [connectionStatus, setConnectionStatus] = - useState('disconnected') - const [sessionGenerating, setSessionGenerating] = useState(false) - const [queue, setQueue] = useState>([]) - const [runId, setRunId] = useState(null) - const [interruptState, setInterruptState] = useState< - ChatInterruptState - >(() => ({ - interrupts: EMPTY_INTERRUPTS, - pendingInterrupts: EMPTY_INTERRUPTS, - interruptErrors: EMPTY_INTERRUPT_ERRORS, - resuming: false, - })) - - // Track current messages in a ref to preserve them when client is recreated - const messagesRef = useRef>>( - options.initialMessages || [], - ) - const isFirstMountRef = useRef(true) const activeClientRef = useRef(null) const cleanupInvalidationRef = useRef | null>( null, @@ -89,19 +50,8 @@ export function useChat< optionsRef.current = options - const syncResumeState = useCallback((target: ChatClient | null) => { - if (!target) return - setRunId(target.getCurrentRunId()) - setInterruptState(target.getInterruptState()) - }, []) - - useEffect(() => { - messagesRef.current = messages - }, [messages]) - const { client, initialization } = useMemo(() => { const messagesToUse = options.initialMessages || [] - isFirstMountRef.current = false // Build options with conditional spreads for fields whose source // type is `T | undefined` but the ChatClient target uses a strict @@ -209,69 +159,11 @@ export function useChat< ...(options.streamProcessor !== undefined && { streamProcessor: options.streamProcessor, }), - onMessagesChange: (newMessages: Array>) => { - runOrQueueForActiveInstance(() => { - setMessages(newMessages) - }) - }, - onLoadingChange: (newIsLoading: boolean) => { - runOrQueueForActiveInstance(() => { - const currentInstance = getActiveInstance() - if (!currentInstance) return - setIsLoading(newIsLoading) - syncResumeState(currentInstance) - }) - }, - onStatusChange: (newStatus: ChatClientState) => { - runOrQueueForActiveInstance(() => { - setStatus(newStatus) - }) - }, - onErrorChange: (newError: Error | undefined) => { - runOrQueueForActiveInstance(() => { - setError(newError) - }) - }, - onSubscriptionChange: (nextIsSubscribed: boolean) => { - runOrQueueForActiveInstance(() => { - setIsSubscribed(nextIsSubscribed) - }) - }, - onConnectionStatusChange: (nextStatus: ConnectionStatus) => { - runOrQueueForActiveInstance(() => { - setConnectionStatus(nextStatus) - }) - }, - onSessionGeneratingChange: (isGenerating: boolean) => { - runOrQueueForActiveInstance(() => { - setSessionGenerating(isGenerating) - }) - }, ...(optionsRef.current.queue !== undefined && { queue: optionsRef.current.queue, }), - onQueueChange: (nextQueue: Array) => { - runOrQueueForActiveInstance(() => { - setQueue(nextQueue) - }) - }, - onRunIdChange: (nextRunId) => { - runOrQueueForActiveInstance(() => { - setRunId(nextRunId) - }) - }, - onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => { - runOrQueueForActiveInstance(() => { - setInterruptState((current) => ({ - ...current, - interrupts: nextPendingInterrupts, - pendingInterrupts: nextPendingInterrupts, - })) - }) - }, onInterruptStateChange: (nextInterruptState, context) => { runOrQueueForActiveInstance(() => { - setInterruptState(nextInterruptState) optionsRef.current.onInterruptStateChange?.( nextInterruptState, context, @@ -281,7 +173,7 @@ export function useChat< }) instanceHolder.current = instance return { client: instance, initialization: initializationState } - }, [clientId, syncResumeState]) + }, [clientId]) useEffect(() => { activeClientRef.current = client @@ -301,12 +193,10 @@ export function useChat< } }, [client, initialization]) - useEffect(() => { - const clientMessages = client.getMessages() - if (clientMessages !== messagesRef.current) { - setMessages(clientMessages) - } - }, [client]) + const snapshot = useSyncExternalStore( + client.subscribeSnapshot, + client.getSnapshot, + ) // Sync body / forwardedProps changes to the client. // Both populate the same wire payload; `forwardedProps` is preferred @@ -368,10 +258,6 @@ export function useChat< cleanupInvalidationRef.current = null } client.mountDevtools() - // Delivery-durability resume is transparent: the resumable SSE connection - // adapter reattaches via the browser's native Last-Event-ID on reconnect. - // We only seed interrupt (state) resume from the client here. - syncResumeState(client) return () => { cleanupInvalidationRef.current = setTimeout(() => { @@ -400,7 +286,7 @@ export function useChat< } cleanupDisposalRef.current = disposal } - }, [client, syncResumeState]) + }, [client]) // All callback options are read through optionsRef at call time, so fresh // closures from each render are picked up without recreating the client. @@ -409,40 +295,28 @@ export function useChat< content: string | MultimodalContent, sendOptions?: SendMessageOptions, ) => { - try { - await client.sendMessage(content, undefined, sendOptions) - } finally { - syncResumeState(client) - } + await client.sendMessage(content, undefined, sendOptions) }, - [client, syncResumeState], + [client], ) const cancelQueued = useCallback( (id: string) => { client.cancelQueued(id) }, - [client, syncResumeState], + [client], ) const append = useCallback( async (message: ModelMessage | UIMessage) => { - try { - await client.append(message) - } finally { - syncResumeState(client) - } + await client.append(message) }, - [client, syncResumeState], + [client], ) const reload = useCallback(async () => { - try { - await client.reload() - } finally { - syncResumeState(client) - } - }, [client, syncResumeState]) + await client.reload() + }, [client]) const stop = useCallback(() => { client.stop() @@ -450,8 +324,7 @@ export function useChat< const clear = useCallback(() => { client.clear() - syncResumeState(client) - }, [client, syncResumeState]) + }, [client]) const setMessagesManually = useCallback( (newMessages: Array>) => { @@ -476,18 +349,15 @@ export function useChat< const addToolApprovalResponse = useCallback( async (response: { id: string; approved: boolean }) => { await client.addToolApprovalResponse(response) - syncResumeState(client) }, - [client, syncResumeState], + [client], ) const resumeInterrupts = useCallback( async (resumeItems: Array, state?: ChatResumeState) => { - const result = await client.resumeInterrupts(resumeItems, state) - syncResumeState(client) - return result + return await client.resumeInterrupts(resumeItems, state) }, - [client, syncResumeState], + [client], ) const resolveInterrupts = useCallback( @@ -521,7 +391,7 @@ export function useChat< [client], ) - const renderedMessages = client.getMessages() + const renderedMessages = snapshot.messages return { messages: renderedMessages, @@ -529,23 +399,23 @@ export function useChat< append, reload, stop, - isLoading, - error, - status, - isSubscribed, - connectionStatus, - sessionGenerating, + isLoading: snapshot.isLoading, + error: snapshot.error, + status: snapshot.status, + isSubscribed: snapshot.isSubscribed, + connectionStatus: snapshot.connectionStatus, + sessionGenerating: snapshot.sessionGenerating, setMessages: setMessagesManually, clear, addToolResult, addToolApprovalResponse, - queue, + queue: snapshot.queue, cancelQueued, - runId, - interrupts: interruptState.interrupts, - pendingInterrupts: interruptState.pendingInterrupts, - interruptErrors: interruptState.interruptErrors, - resuming: interruptState.resuming, + runId: snapshot.runId, + interrupts: snapshot.interruptState.interrupts, + pendingInterrupts: snapshot.interruptState.pendingInterrupts, + interruptErrors: snapshot.interruptState.interruptErrors, + resuming: snapshot.interruptState.resuming, resolveInterrupts, cancelInterrupts, retryInterrupts, diff --git a/packages/ai-react/src/use-chat.ts b/packages/ai-react/src/use-chat.ts index e5edb97d50..c567df72c2 100644 --- a/packages/ai-react/src/use-chat.ts +++ b/packages/ai-react/src/use-chat.ts @@ -1,6 +1,13 @@ import { ChatClient } from '@tanstack/ai-client' import { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools' -import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' +import { + useCallback, + useEffect, + useId, + useMemo, + useRef, + useSyncExternalStore, +} from 'react' import type { AnyClientTool, InferSchemaType, @@ -11,13 +18,9 @@ import type { StreamChunk, } from '@tanstack/ai/client' import type { - ChatClientState, ResolvableChatInterrupt, - ChatInterruptState, ChatResumeState, - ConnectionStatus, InferredClientContext, - QueuedMessage, SendMessageOptions, StructuredOutputPart, } from '@tanstack/ai-client' @@ -30,9 +33,6 @@ import type { UseChatReturn, } from './types' -const EMPTY_INTERRUPTS = Object.freeze([]) -const EMPTY_INTERRUPT_ERRORS = Object.freeze([]) - export function useChat< const TTools extends ReadonlyArray = any, TSchema extends SchemaInput | undefined = undefined, @@ -49,35 +49,9 @@ export function useChat< const hookId = useId() const clientId = options.threadId ?? hookId - const [messages, setMessages] = useState>>( - options.initialMessages || [], - ) - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(undefined) - const [status, setStatus] = useState('ready') - const [isSubscribed, setIsSubscribed] = useState(false) - const [connectionStatus, setConnectionStatus] = - useState('disconnected') - const [sessionGenerating, setSessionGenerating] = useState(false) - const [queue, setQueue] = useState>([]) - const [runId, setRunId] = useState(null) - const [interruptState, setInterruptState] = useState< - ChatInterruptState - >(() => ({ - interrupts: EMPTY_INTERRUPTS, - pendingInterrupts: EMPTY_INTERRUPTS, - interruptErrors: EMPTY_INTERRUPT_ERRORS, - resuming: false, - })) - type Partial = DeepPartial>> type Final = InferSchemaType> - // Track current messages in a ref to preserve them when client is recreated - const messagesRef = useRef>>( - options.initialMessages || [], - ) - const isFirstMountRef = useRef(true) const subscribedRef = useRef(false) const activeClientRef = useRef(null) const cleanupInvalidationRef = useRef | null>( @@ -88,24 +62,14 @@ export function useChat< timeout: ReturnType } | null>(null) - // Update ref synchronously during render so it's always current when useMemo runs. - messagesRef.current = messages - // Track current options in a ref to avoid recreating client when options change const optionsRef = useRef>(options) optionsRef.current = options - const syncResumeState = useCallback((target: ChatClient | null) => { - if (!target) return - setRunId(target.getCurrentRunId()) - setInterruptState(target.getInterruptState()) - }, []) - // Create ChatClient instance with callbacks to sync state const { client, initialization } = useMemo(() => { const messagesToUse = options.initialMessages || [] - isFirstMountRef.current = false // Build options with conditional spreads for fields whose source // type is `T | undefined` but the ChatClient target uses a strict @@ -211,69 +175,11 @@ export function useChat< ...(options.streamProcessor !== undefined && { streamProcessor: options.streamProcessor, }), - onMessagesChange: (newMessages: Array>) => { - runOrQueueForActiveInstance(() => { - setMessages(newMessages) - }) - }, - onLoadingChange: (newIsLoading: boolean) => { - runOrQueueForActiveInstance(() => { - const currentInstance = getActiveInstance() - if (!currentInstance) return - setIsLoading(newIsLoading) - syncResumeState(currentInstance) - }) - }, - onErrorChange: (newError: Error | undefined) => { - runOrQueueForActiveInstance(() => { - setError(newError) - }) - }, - onStatusChange: (status: ChatClientState) => { - runOrQueueForActiveInstance(() => { - setStatus(status) - }) - }, - onSubscriptionChange: (nextIsSubscribed: boolean) => { - runOrQueueForActiveInstance(() => { - setIsSubscribed(nextIsSubscribed) - }) - }, - onConnectionStatusChange: (nextStatus: ConnectionStatus) => { - runOrQueueForActiveInstance(() => { - setConnectionStatus(nextStatus) - }) - }, - onSessionGeneratingChange: (isGenerating: boolean) => { - runOrQueueForActiveInstance(() => { - setSessionGenerating(isGenerating) - }) - }, ...(optionsRef.current.queue !== undefined && { queue: optionsRef.current.queue, }), - onQueueChange: (nextQueue: Array) => { - runOrQueueForActiveInstance(() => { - setQueue(nextQueue) - }) - }, - onRunIdChange: (nextRunId) => { - runOrQueueForActiveInstance(() => { - setRunId(nextRunId) - }) - }, - onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => { - runOrQueueForActiveInstance(() => { - setInterruptState((current) => ({ - ...current, - interrupts: nextPendingInterrupts, - pendingInterrupts: nextPendingInterrupts, - })) - }) - }, onInterruptStateChange: (nextInterruptState, context) => { runOrQueueForActiveInstance(() => { - setInterruptState(nextInterruptState) optionsRef.current.onInterruptStateChange?.( nextInterruptState, context, @@ -283,7 +189,7 @@ export function useChat< }) instanceHolder.current = instance return { client: instance, initialization: initializationState } - }, [clientId, syncResumeState]) + }, [clientId]) useEffect(() => { activeClientRef.current = client @@ -303,12 +209,11 @@ export function useChat< } }, [client, initialization]) - useEffect(() => { - const clientMessages = client.getMessages() - if (clientMessages !== messagesRef.current) { - setMessages(clientMessages) - } - }, [client]) + const snapshot = useSyncExternalStore( + client.subscribeSnapshot, + client.getSnapshot, + client.getSnapshot, + ) // Sync each wire-payload slot in its own effect so an unrelated option // changing doesn't re-run the others. `updateOptions` declares strict-optional @@ -387,10 +292,6 @@ export function useChat< cleanupInvalidationRef.current = null } client.mountDevtools() - // Delivery-durability resume is transparent: the resumable SSE connection - // adapter re-attaches via the browser's native Last-Event-ID on reconnect. - // We only seed interrupt (state) resume from the client here. - syncResumeState(client) return () => { cleanupInvalidationRef.current = setTimeout(() => { @@ -423,47 +324,35 @@ export function useChat< } cleanupDisposalRef.current = disposal } - }, [client, syncResumeState]) + }, [client]) const sendMessage = useCallback( async ( content: string | MultimodalContent, sendOptions?: SendMessageOptions, ) => { - try { - await client.sendMessage(content, undefined, sendOptions) - } finally { - syncResumeState(client) - } + await client.sendMessage(content, undefined, sendOptions) }, - [client, syncResumeState], + [client], ) const cancelQueued = useCallback( (id: string) => { client.cancelQueued(id) }, - [client, syncResumeState], + [client], ) const append = useCallback( async (message: ModelMessage | UIMessage) => { - try { - await client.append(message) - } finally { - syncResumeState(client) - } + await client.append(message) }, - [client, syncResumeState], + [client], ) const reload = useCallback(async () => { - try { - await client.reload() - } finally { - syncResumeState(client) - } - }, [client, syncResumeState]) + await client.reload() + }, [client]) const stop = useCallback(() => { client.stop() @@ -471,8 +360,7 @@ export function useChat< const clear = useCallback(() => { client.clear() - syncResumeState(client) - }, [client, syncResumeState]) + }, [client]) const setMessagesManually = useCallback( (newMessages: Array>) => { @@ -497,21 +385,15 @@ export function useChat< const addToolApprovalResponse = useCallback( async (response: { id: string; approved: boolean }) => { await client.addToolApprovalResponse(response) - syncResumeState(client) }, - [client, syncResumeState], + [client], ) const resumeInterrupts = useCallback( async (resumeItems: Array, state?: ChatResumeState) => { - const result = await client.resumeInterrupts( - resumeItems, - state ?? undefined, - ) - syncResumeState(client) - return result + return await client.resumeInterrupts(resumeItems, state ?? undefined) }, - [client, syncResumeState], + [client], ) const resolveInterrupts = useCallback( @@ -555,7 +437,7 @@ export function useChat< // a stale assistant turn or a system prompt) we deliberately return null // rather than scanning historical assistants — otherwise a `final` from a // previous session would leak into the hook value on first render. - const renderedMessages = client.getMessages() + const renderedMessages = snapshot.messages const activeStructuredPart = useMemo(() => { let lastUserIndex = -1 @@ -600,23 +482,23 @@ export function useChat< append, reload, stop, - isLoading, - error, - status, - isSubscribed, - connectionStatus, - sessionGenerating, + isLoading: snapshot.isLoading, + error: snapshot.error, + status: snapshot.status, + isSubscribed: snapshot.isSubscribed, + connectionStatus: snapshot.connectionStatus, + sessionGenerating: snapshot.sessionGenerating, setMessages: setMessagesManually, clear, addToolResult, addToolApprovalResponse, - queue, + queue: snapshot.queue, cancelQueued, - runId, - interrupts: interruptState.interrupts, - pendingInterrupts: interruptState.pendingInterrupts, - interruptErrors: interruptState.interruptErrors, - resuming: interruptState.resuming, + runId: snapshot.runId, + interrupts: snapshot.interruptState.interrupts, + pendingInterrupts: snapshot.interruptState.pendingInterrupts, + interruptErrors: snapshot.interruptState.interruptErrors, + resuming: snapshot.interruptState.resuming, resolveInterrupts, cancelInterrupts, retryInterrupts, diff --git a/packages/ai-react/src/use-generate-video.ts b/packages/ai-react/src/use-generate-video.ts index 708281fee2..393c17f1ab 100644 --- a/packages/ai-react/src/use-generate-video.ts +++ b/packages/ai-react/src/use-generate-video.ts @@ -1,6 +1,13 @@ import { VideoGenerationClient } from '@tanstack/ai-client' import { createVideoDevtoolsBridge } from '@tanstack/ai-client/devtools' -import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' +import { + useCallback, + useEffect, + useId, + useMemo, + useRef, + useSyncExternalStore, +} from 'react' import type { StreamChunk } from '@tanstack/ai' import type { AIDevtoolsDisplayOptions, @@ -175,14 +182,6 @@ export function useGenerateVideo( // The hook identity is `threadId`. `hookId` is only a React recreation key. const clientIdentity = options.threadId ?? hookId - const [result, setResult] = useState(null) - const [jobId, setJobId] = useState(null) - const [videoStatus, setVideoStatus] = useState(null) - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(undefined) - const [status, setStatus] = useState('idle') - const [runId, setRunId] = useState(null) - const optionsRef = useRef(options) optionsRef.current = options const disposedRef = useRef(false) @@ -235,27 +234,6 @@ export function useGenerateVideo( onStatusUpdate: (s: VideoStatusInfo) => { if (!disposedRef.current) optionsRef.current.onStatusUpdate?.(s) }, - onResultChange: (r: TOutput | null) => { - if (!disposedRef.current) setResult(r) - }, - onLoadingChange: (l: boolean) => { - if (!disposedRef.current) setIsLoading(l) - }, - onErrorChange: (e: Error | undefined) => { - if (!disposedRef.current) setError(e) - }, - onStatusChange: (s: GenerationClientState) => { - if (!disposedRef.current) setStatus(s) - }, - onJobIdChange: (id: string | null) => { - if (!disposedRef.current) setJobId(id) - }, - onVideoStatusChange: (s: VideoStatusInfo | null) => { - if (!disposedRef.current) setVideoStatus(s) - }, - onResumeStateChange: (rs: { runId: string } | null) => { - if (!disposedRef.current) setRunId(rs?.runId ?? null) - }, } const persistenceProps = @@ -289,6 +267,12 @@ export function useGenerateVideo( ) }, [clientIdentity, hookId]) + const snapshot = useSyncExternalStore( + client.subscribe, + client.getSnapshot, + client.getSnapshot, + ) + // Sync body changes without recreating client useEffect(() => { // Conditional spread: target uses strict-optional `body?: T`. @@ -327,14 +311,14 @@ export function useGenerateVideo( return { generate, - result, - jobId, - videoStatus, - isLoading, - error, - status, + result: snapshot.result, + jobId: snapshot.jobId, + videoStatus: snapshot.videoStatus, + isLoading: snapshot.isLoading, + error: snapshot.error, + status: snapshot.status, stop, reset, - runId, + runId: snapshot.runId, } } diff --git a/packages/ai-react/src/use-generation.ts b/packages/ai-react/src/use-generation.ts index 8106f4cf90..6ebe77c942 100644 --- a/packages/ai-react/src/use-generation.ts +++ b/packages/ai-react/src/use-generation.ts @@ -1,6 +1,13 @@ import { GenerationClient } from '@tanstack/ai-client' import { createGenerationDevtoolsBridge } from '@tanstack/ai-client/devtools' -import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' +import { + useCallback, + useEffect, + useId, + useMemo, + useRef, + useSyncExternalStore, +} from 'react' import type { StreamChunk } from '@tanstack/ai' import type { AIDevtoolsDisplayOptions, @@ -174,12 +181,6 @@ export function useGeneration< // The hook identity is `threadId`. `hookId` is only a React recreation key. const clientIdentity = options.threadId ?? hookId - const [result, setResult] = useState(null) - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(undefined) - const [status, setStatus] = useState('idle') - const [runId, setRunId] = useState(null) - const optionsRef = useRef(options) optionsRef.current = options const disposedRef = useRef(false) @@ -226,21 +227,6 @@ export function useGeneration< onChunk: (c: StreamChunk) => { if (!disposedRef.current) optionsRef.current.onChunk?.(c) }, - onResultChange: (r) => { - if (!disposedRef.current) setResult(r) - }, - onLoadingChange: (l) => { - if (!disposedRef.current) setIsLoading(l) - }, - onErrorChange: (e) => { - if (!disposedRef.current) setError(e) - }, - onStatusChange: (s) => { - if (!disposedRef.current) setStatus(s) - }, - onResumeStateChange: (rs) => { - if (!disposedRef.current) setRunId(rs?.runId ?? null) - }, } const persistenceProps = @@ -274,6 +260,12 @@ export function useGeneration< ) }, [clientIdentity, hookId]) + const snapshot = useSyncExternalStore( + client.subscribe, + client.getSnapshot, + client.getSnapshot, + ) + // Sync body changes without recreating client useEffect(() => { // Conditional spread: target uses strict-optional `body?: T`. @@ -312,12 +304,12 @@ export function useGeneration< return { generate, - result, - isLoading, - error, - status, + result: snapshot.result, + isLoading: snapshot.isLoading, + error: snapshot.error, + status: snapshot.status, stop, reset, - runId, + runId: snapshot.runId, } } diff --git a/packages/ai-react/src/use-realtime-chat.ts b/packages/ai-react/src/use-realtime-chat.ts index bddef502c6..9646765285 100644 --- a/packages/ai-react/src/use-realtime-chat.ts +++ b/packages/ai-react/src/use-realtime-chat.ts @@ -1,11 +1,12 @@ -import { useCallback, useEffect, useRef, useState } from 'react' +import { + useCallback, + useEffect, + useRef, + useState, + useSyncExternalStore, +} from 'react' import { RealtimeClient } from '@tanstack/ai-client' -import type { - RealtimeMessage, - RealtimeMode, - RealtimeSessionConfig, - RealtimeStatus, -} from '@tanstack/ai' +import type { RealtimeSessionConfig } from '@tanstack/ai' import type { UseRealtimeChatOptions, UseRealtimeChatReturn, @@ -58,17 +59,6 @@ const emptyTimeDomainData = new Uint8Array(128).fill(128) export function useRealtimeChat( options: UseRealtimeChatOptions, ): UseRealtimeChatReturn { - // State - const [status, setStatus] = useState('idle') - const [mode, setMode] = useState('idle') - const [messages, setMessages] = useState>([]) - const [pendingUserTranscript, setPendingUserTranscript] = useState< - string | null - >(null) - const [pendingAssistantTranscript, setPendingAssistantTranscript] = useState< - string | null - >(null) - const [error, setError] = useState(null) const [inputLevel, setInputLevel] = useState(0) const [outputLevel, setOutputLevel] = useState(0) @@ -113,15 +103,10 @@ export function useRealtimeChat( ...(initial.semanticEagerness !== undefined && { semanticEagerness: initial.semanticEagerness, }), - onStatusChange: (newStatus) => { - setStatus(newStatus) - }, onModeChange: (newMode) => { - setMode(newMode) optionsRef.current.onModeChange?.(newMode) }, onMessage: (message) => { - setMessages((prev) => [...prev, message]) optionsRef.current.onMessage?.(message) }, onUsage(usage) { @@ -131,30 +116,26 @@ export function useRealtimeChat( optionsRef.current.onGoAway?.(timeLeft) }, onError: (err) => { - setError(err) optionsRef.current.onError?.(err) }, onConnect: () => { - setError(null) optionsRef.current.onConnect?.() }, onDisconnect: () => { optionsRef.current.onDisconnect?.() }, onInterrupted: () => { - setPendingAssistantTranscript(null) optionsRef.current.onInterrupted?.() }, }) - - // Subscribe to state changes for transcripts - clientRef.current.onStateChange((state) => { - setPendingUserTranscript(state.pendingUserTranscript) - setPendingAssistantTranscript(state.pendingAssistantTranscript) - }) } const client = clientRef.current + const snapshot = useSyncExternalStore( + client.subscribe, + client.getSnapshot, + client.getSnapshot, + ) // Audio level animation loop useEffect(() => { @@ -166,7 +147,7 @@ export function useRealtimeChat( animationFrameRef.current = requestAnimationFrame(updateLevels) } - if (status === 'connected') { + if (snapshot.status === 'connected') { updateLevels() } @@ -176,7 +157,7 @@ export function useRealtimeChat( animationFrameRef.current = null } } - }, [status]) + }, [snapshot.status]) // Cleanup on unmount useEffect(() => { @@ -187,10 +168,6 @@ export function useRealtimeChat( // Connection methods const connect = useCallback(async () => { - setError(null) - setMessages([]) - setPendingUserTranscript(null) - setPendingAssistantTranscript(null) await client.connect() }, [client]) @@ -258,16 +235,16 @@ export function useRealtimeChat( return { // Connection state - status, - error, + status: snapshot.status, + error: snapshot.error, connect, disconnect, // Conversation state - mode, - messages, - pendingUserTranscript, - pendingAssistantTranscript, + mode: snapshot.mode, + messages: snapshot.messages, + pendingUserTranscript: snapshot.pendingUserTranscript, + pendingAssistantTranscript: snapshot.pendingAssistantTranscript, // Voice control startListening, diff --git a/packages/ai-solid/src/use-chat.ts b/packages/ai-solid/src/use-chat.ts index 6d5737b6b4..19ddf0f85b 100644 --- a/packages/ai-solid/src/use-chat.ts +++ b/packages/ai-solid/src/use-chat.ts @@ -82,11 +82,6 @@ export function useChat< resuming: false, }) - const syncResumeState = () => { - setRunId(client().getCurrentRunId()) - setInterruptState(client().getInterruptState()) - } - // Structured-output `partial` / `final` are derived from `messages` — // specifically from the structured-output part on the latest assistant // message (the one after the most recent user message). Per-turn parts @@ -159,44 +154,8 @@ export function useChat< ...(options.streamProcessor !== undefined && { streamProcessor: options.streamProcessor, }), - onMessagesChange: (newMessages: Array>) => { - setMessages(newMessages) - }, - onLoadingChange: (newIsLoading: boolean) => { - setIsLoading(newIsLoading) - syncResumeState() - }, - onStatusChange: (newStatus: ChatClientState) => { - setStatus(newStatus) - }, - onErrorChange: (newError: Error | undefined) => { - setError(newError) - }, - onSubscriptionChange: (nextIsSubscribed: boolean) => { - setIsSubscribed(nextIsSubscribed) - }, - onConnectionStatusChange: (nextStatus: ConnectionStatus) => { - setConnectionStatus(nextStatus) - }, - onSessionGeneratingChange: (isGenerating: boolean) => { - setSessionGenerating(isGenerating) - }, ...(options.queue !== undefined && { queue: options.queue }), - onQueueChange: (nextQueue: Array) => { - setQueue(nextQueue) - }, - onRunIdChange: (nextRunId) => { - setRunId(nextRunId) - }, - onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => { - setInterruptState((current) => ({ - ...current, - interrupts: nextPendingInterrupts, - pendingInterrupts: nextPendingInterrupts, - })) - }, onInterruptStateChange: (nextInterruptState, context) => { - setInterruptState(nextInterruptState) options.onInterruptStateChange?.(nextInterruptState, context) }, }) @@ -204,8 +163,26 @@ export function useChat< // Connection and other options are captured at creation time }, [clientId]) - setMessages(client().getMessages()) - syncResumeState() + const applySnapshot = (target: ChatClient) => { + const next = target.getSnapshot() + setMessages(next.messages) + setIsLoading(next.isLoading) + setError(next.error) + setStatus(next.status) + setIsSubscribed(next.isSubscribed) + setConnectionStatus(next.connectionStatus) + setSessionGenerating(next.sessionGenerating) + setQueue(next.queue) + setRunId(next.runId) + setInterruptState(next.interruptState) + } + + createEffect(() => { + const target = client() + applySnapshot(target) + const unsubscribe = target.subscribeSnapshot(() => applySnapshot(target)) + onCleanup(unsubscribe) + }) // Sync body / forwardedProps changes to the client. // Both populate the same wire payload; `forwardedProps` is preferred @@ -245,10 +222,6 @@ export function useChat< // connections per origin until the page reloaded. client().attach() client().mountDevtools() - // Delivery-durability resume is transparent: the resumable SSE connection - // adapter reattaches via the browser's native Last-Event-ID on reconnect. - // We only seed interrupt (state) resume from the client here. - syncResumeState() }) // Cleanup on unmount: stop any in-flight requests. @@ -272,27 +245,15 @@ export function useChat< content: string | MultimodalContent, sendOptions?: SendMessageOptions, ) => { - try { - await client().sendMessage(content, undefined, sendOptions) - } finally { - syncResumeState() - } + await client().sendMessage(content, undefined, sendOptions) } const append = async (message: ModelMessage | UIMessage) => { - try { - await client().append(message) - } finally { - syncResumeState() - } + await client().append(message) } const reload = async () => { - try { - await client().reload() - } finally { - syncResumeState() - } + await client().reload() } const stop = () => { @@ -301,7 +262,6 @@ export function useChat< const clear = () => { client().clear() - syncResumeState() } const setMessagesManually = (newMessages: Array>) => { @@ -323,7 +283,6 @@ export function useChat< approved: boolean }) => { await client().addToolApprovalResponse(response) - syncResumeState() } const cancelQueued = (id: string) => client().cancelQueued(id) @@ -332,9 +291,7 @@ export function useChat< resumeItems: Array, state?: ChatResumeState, ) => { - const result = await client().resumeInterrupts(resumeItems, state) - syncResumeState() - return result + return await client().resumeInterrupts(resumeItems, state) } const resolveInterrupts = ( diff --git a/packages/ai-solid/src/use-generate-video.ts b/packages/ai-solid/src/use-generate-video.ts index 42acd27e53..ceba999c4f 100644 --- a/packages/ai-solid/src/use-generate-video.ts +++ b/packages/ai-solid/src/use-generate-video.ts @@ -244,27 +244,6 @@ export function useGenerateVideo( onStatusUpdate: (s: VideoStatusInfo) => { if (!disposed) options.onStatusUpdate?.(s) }, - onResultChange: (r: TOutput | null) => { - if (!disposed) setResult(() => r) - }, - onLoadingChange: (l: boolean) => { - if (!disposed) setIsLoading(l) - }, - onErrorChange: (e: Error | undefined) => { - if (!disposed) setError(e) - }, - onStatusChange: (s: GenerationClientState) => { - if (!disposed) setStatus(s) - }, - onJobIdChange: (id: string | null) => { - if (!disposed) setJobId(id) - }, - onVideoStatusChange: (s: VideoStatusInfo | null) => { - if (!disposed) setVideoStatus(s) - }, - onResumeStateChange: (rs: { runId: string } | null) => { - if (!disposed) setRunId(rs?.runId ?? null) - }, } if (options.connection) { @@ -286,6 +265,19 @@ export function useGenerateVideo( ) }) + const applySnapshot = () => { + const next = client.getSnapshot() + setResult(() => next.result) + setIsLoading(next.isLoading) + setError(next.error) + setStatus(next.status) + setRunId(next.runId) + setJobId(next.jobId) + setVideoStatus(next.videoStatus) + } + applySnapshot() + onCleanup(client.subscribe(applySnapshot)) + // Sync body changes without recreating client createEffect(() => { const currentBody = options.body diff --git a/packages/ai-solid/src/use-generation.ts b/packages/ai-solid/src/use-generation.ts index eedc8eb5c2..ee614f1d8c 100644 --- a/packages/ai-solid/src/use-generation.ts +++ b/packages/ai-solid/src/use-generation.ts @@ -230,21 +230,6 @@ export function useGeneration< onChunk: (c: StreamChunk) => { if (!disposed) options.onChunk?.(c) }, - onResultChange: (r) => { - if (!disposed) setResult(() => r) - }, - onLoadingChange: (l) => { - if (!disposed) setIsLoading(l) - }, - onErrorChange: (e) => { - if (!disposed) setError(e) - }, - onStatusChange: (s) => { - if (!disposed) setStatus(s) - }, - onResumeStateChange: (rs) => { - if (!disposed) setRunId(rs?.runId ?? null) - }, } const persistenceProps = @@ -280,6 +265,17 @@ export function useGeneration< ) }) + const applySnapshot = () => { + const next = client.getSnapshot() + setResult(() => next.result) + setIsLoading(next.isLoading) + setError(next.error) + setStatus(next.status) + setRunId(next.runId) + } + applySnapshot() + onCleanup(client.subscribe(applySnapshot)) + // Sync body changes without recreating client createEffect(() => { const currentBody = options.body diff --git a/packages/ai-svelte/src/create-chat.svelte.ts b/packages/ai-svelte/src/create-chat.svelte.ts index 9e72e21aeb..97cd334617 100644 --- a/packages/ai-svelte/src/create-chat.svelte.ts +++ b/packages/ai-svelte/src/create-chat.svelte.ts @@ -163,48 +163,28 @@ export function createChat< ...(options.streamProcessor !== undefined && { streamProcessor: options.streamProcessor, }), - onMessagesChange: (newMessages: Array>) => { - messages = newMessages - }, - onLoadingChange: (newIsLoading: boolean) => { - isLoading = newIsLoading - syncResumeState() - }, - onStatusChange: (newStatus: ChatClientState) => { - status = newStatus - }, - onErrorChange: (newError: Error | undefined) => { - error = newError - }, - onSubscriptionChange: (nextIsSubscribed: boolean) => { - isSubscribed = nextIsSubscribed - }, - onConnectionStatusChange: (nextStatus: ConnectionStatus) => { - connectionStatus = nextStatus - }, - onSessionGeneratingChange: (isGenerating: boolean) => { - sessionGenerating = isGenerating - }, ...(options.queue !== undefined && { queue: options.queue }), - onQueueChange: (nextQueue: Array) => { - queue = nextQueue - }, - onRunIdChange: (nextRunId) => { - runId = nextRunId - }, onInterruptStateChange: (nextInterruptState, context) => { - interruptState = nextInterruptState options.onInterruptStateChange?.(nextInterruptState, context) }, }) - function syncResumeState() { - runId = client.getCurrentRunId() - interruptState = client.getInterruptState() + function applySnapshot() { + const next = client.getSnapshot() + messages = next.messages + isLoading = next.isLoading + error = next.error + status = next.status + isSubscribed = next.isSubscribed + connectionStatus = next.connectionStatus + sessionGenerating = next.sessionGenerating + queue = next.queue + runId = next.runId + interruptState = next.interruptState } - messages = client.getMessages() - interruptState = client.getInterruptState() + applySnapshot() + const unsubscribeSnapshot = client.subscribeSnapshot(applySnapshot) if (options.live) { client.subscribe() @@ -218,7 +198,6 @@ export function createChat< // Delivery-durability resume is transparent: the resumable SSE // connection adapter reattaches via the browser's native // Last-Event-ID on reconnect. We only seed interrupt (state) resume. - syncResumeState() client.attach() // ONLY THE VIEW ON SCREEN HOLDS A STREAM. `onMount`'s returned function // runs when the component is destroyed, which is the one automatic @@ -230,6 +209,7 @@ export function createChat< // `detach` keeps the transcript and the resume pointer, so re-entering // the view picks the run back up from the durable log. return () => { + unsubscribeSnapshot() client.detach() } }) @@ -247,29 +227,17 @@ export function createChat< content: string | MultimodalContent, sendOptions?: SendMessageOptions, ) => { - try { - await client.sendMessage(content, undefined, sendOptions) - } finally { - syncResumeState() - } + await client.sendMessage(content, undefined, sendOptions) } const cancelQueued = (id: string) => client.cancelQueued(id) const append = async (message: ModelMessage | UIMessage) => { - try { - await client.append(message) - } finally { - syncResumeState() - } + await client.append(message) } const reload = async () => { - try { - await client.reload() - } finally { - syncResumeState() - } + await client.reload() } const stop = () => { @@ -277,12 +245,12 @@ export function createChat< } const dispose = () => { + unsubscribeSnapshot() client.dispose() } const clear = () => { client.clear() - syncResumeState() } const setMessages = (newMessages: Array>) => { @@ -304,16 +272,13 @@ export function createChat< approved: boolean }) => { await client.addToolApprovalResponse(response) - syncResumeState() } const resumeInterrupts = async ( resumeItems: Array, state?: ChatResumeState, ) => { - const result = await client.resumeInterrupts(resumeItems, state) - syncResumeState() - return result + return await client.resumeInterrupts(resumeItems, state) } const resolveInterrupts = ( diff --git a/packages/ai-svelte/src/create-generate-video.svelte.ts b/packages/ai-svelte/src/create-generate-video.svelte.ts index d8b8615bcf..4448245f8e 100644 --- a/packages/ai-svelte/src/create-generate-video.svelte.ts +++ b/packages/ai-svelte/src/create-generate-video.svelte.ts @@ -235,34 +235,6 @@ export function createGenerateVideo( onStatusUpdate: (s: VideoStatusInfo) => { if (!disposed) options.onStatusUpdate?.(s) }, - onResultChange: (r: TOutput | null) => { - if (disposed) return - result = r - }, - onLoadingChange: (l: boolean) => { - if (disposed) return - isLoading = l - }, - onErrorChange: (e: Error | undefined) => { - if (disposed) return - error = e - }, - onStatusChange: (s: GenerationClientState) => { - if (disposed) return - status = s - }, - onJobIdChange: (id: string | null) => { - if (disposed) return - jobId = id - }, - onVideoStatusChange: (s: VideoStatusInfo | null) => { - if (disposed) return - videoStatus = s - }, - onResumeStateChange: (rs: { runId: string } | null) => { - if (disposed) return - runId = rs?.runId ?? null - }, } let client: VideoGenerationClient @@ -283,6 +255,19 @@ export function createGenerateVideo( ) } + const applySnapshot = () => { + const next = client.getSnapshot() + result = next.result + isLoading = next.isLoading + error = next.error + status = next.status + runId = next.runId + jobId = next.jobId + videoStatus = next.videoStatus + } + applySnapshot() + let unsubscribeSnapshot = client.subscribe(applySnapshot) + // Mount devtools only. Generation runs are never auto-started on setup — // persisted state is read-only for display. client.mountDevtools() @@ -297,7 +282,11 @@ export function createGenerateVideo( // frameworks revive via mountDevtools() in their mount effects), so an // explicit generate() after dispose() is the Svelte revive path: bring // the client and the reactive bindings back together. - disposed = false + if (disposed) { + disposed = false + applySnapshot() + unsubscribeSnapshot = client.subscribe(applySnapshot) + } client.mountDevtools() await client.generate(input) } @@ -312,6 +301,8 @@ export function createGenerateVideo( const dispose = () => { disposed = true + unsubscribeSnapshot() + unsubscribeSnapshot = () => {} client.dispose() } diff --git a/packages/ai-svelte/src/create-generation.svelte.ts b/packages/ai-svelte/src/create-generation.svelte.ts index bf99c9dd3b..1c659dd2f8 100644 --- a/packages/ai-svelte/src/create-generation.svelte.ts +++ b/packages/ai-svelte/src/create-generation.svelte.ts @@ -233,26 +233,6 @@ export function createGeneration< onChunk: (c: StreamChunk) => { if (!disposed) options.onChunk?.(c) }, - onResultChange: (r: TOutput | null) => { - if (disposed) return - result = r - }, - onLoadingChange: (l: boolean) => { - if (disposed) return - isLoading = l - }, - onErrorChange: (e: Error | undefined) => { - if (disposed) return - error = e - }, - onStatusChange: (s: GenerationClientState) => { - if (disposed) return - status = s - }, - onResumeStateChange: (rs) => { - if (disposed) return - runId = rs?.runId ?? null - }, } const persistenceProps = @@ -287,6 +267,17 @@ export function createGeneration< ) } + const applySnapshot = () => { + const next = client.getSnapshot() + result = next.result + isLoading = next.isLoading + error = next.error + status = next.status + runId = next.runId + } + applySnapshot() + let unsubscribeSnapshot = client.subscribe(applySnapshot) + // Mount devtools only. Generation runs are never auto-started on setup — // persisted state is read-only for display. client.mountDevtools() @@ -301,7 +292,11 @@ export function createGeneration< // frameworks revive via mountDevtools() in their mount effects), so an // explicit generate() after dispose() is the Svelte revive path: bring // the client and the reactive bindings back together. - disposed = false + if (disposed) { + disposed = false + applySnapshot() + unsubscribeSnapshot = client.subscribe(applySnapshot) + } client.mountDevtools() await client.generate(input) } @@ -316,6 +311,8 @@ export function createGeneration< const dispose = () => { disposed = true + unsubscribeSnapshot() + unsubscribeSnapshot = () => {} client.dispose() } diff --git a/packages/ai-svelte/tests/create-generation-dispose.test.ts b/packages/ai-svelte/tests/create-generation-dispose.test.ts new file mode 100644 index 0000000000..e7ef6fc3d6 --- /dev/null +++ b/packages/ai-svelte/tests/create-generation-dispose.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest' +import { createGeneration } from '../src/create-generation.svelte' +import { createGenerateVideo } from '../src/create-generate-video.svelte' + +describe('Svelte generate() after dispose() rebinds the snapshot', () => { + it('createGeneration updates result after dispose then generate', async () => { + const gen = createGeneration<{ prompt: string }, { id: string }>({ + fetcher: async (input) => ({ id: input.prompt }), + }) + + await gen.generate({ prompt: 'a' }) + expect(gen.result).toEqual({ id: 'a' }) + + gen.dispose() + await gen.generate({ prompt: 'b' }) + expect(gen.result).toEqual({ id: 'b' }) + }) + + it('createGenerateVideo updates result after dispose then generate', async () => { + const video = createGenerateVideo({ + fetcher: async (input) => ({ + jobId: input.prompt, + status: 'completed' as const, + url: `https://example.com/${input.prompt}.mp4`, + }), + }) + + await video.generate({ prompt: 'one' }) + expect(video.result?.jobId).toBe('one') + + video.dispose() + await video.generate({ prompt: 'two' }) + expect(video.result?.jobId).toBe('two') + }) +}) diff --git a/packages/ai-vue/src/use-chat.ts b/packages/ai-vue/src/use-chat.ts index 8e748f1a75..a51299cccd 100644 --- a/packages/ai-vue/src/use-chat.ts +++ b/packages/ai-vue/src/use-chat.ts @@ -149,48 +149,28 @@ export function useChat< ...(options.streamProcessor !== undefined && { streamProcessor: options.streamProcessor, }), - onMessagesChange: (newMessages: Array>) => { - messages.value = newMessages - }, - onLoadingChange: (newIsLoading: boolean) => { - isLoading.value = newIsLoading - syncResumeState() - }, - onStatusChange: (newStatus: ChatClientState) => { - status.value = newStatus - }, - onErrorChange: (newError: Error | undefined) => { - error.value = newError - }, - onSubscriptionChange: (nextIsSubscribed: boolean) => { - isSubscribed.value = nextIsSubscribed - }, - onConnectionStatusChange: (nextStatus: ConnectionStatus) => { - connectionStatus.value = nextStatus - }, - onSessionGeneratingChange: (isGenerating: boolean) => { - sessionGenerating.value = isGenerating - }, ...(options.queue !== undefined && { queue: options.queue }), - onQueueChange: (nextQueue: Array) => { - queue.value = nextQueue - }, - onRunIdChange: (nextRunId) => { - runId.value = nextRunId - }, onInterruptStateChange: (nextInterruptState, context) => { - interruptState.value = nextInterruptState options.onInterruptStateChange?.(nextInterruptState, context) }, }) - function syncResumeState() { - runId.value = client.getCurrentRunId() - interruptState.value = client.getInterruptState() + function applySnapshot() { + const next = client.getSnapshot() + messages.value = next.messages + isLoading.value = next.isLoading + error.value = next.error + status.value = next.status + isSubscribed.value = next.isSubscribed + connectionStatus.value = next.connectionStatus + sessionGenerating.value = next.sessionGenerating + queue.value = next.queue + runId.value = next.runId + interruptState.value = next.interruptState } - messages.value = client.getMessages() - interruptState.value = client.getInterruptState() + applySnapshot() + onScopeDispose(client.subscribeSnapshot(applySnapshot)) // Sync body / forwardedProps changes to the client. // Both populate the same wire payload; `forwardedProps` is preferred @@ -236,10 +216,6 @@ export function useChat< // connections per origin until the page reloaded. client.attach() client.mountDevtools() - // Delivery-durability resume is transparent: the resumable SSE connection - // adapter reattaches via the browser's native Last-Event-ID on reconnect. - // We only seed interrupt (state) resume from the client here. - syncResumeState() }) // Cleanup on unmount: stop any in-flight requests @@ -263,29 +239,17 @@ export function useChat< content: string | MultimodalContent, sendOptions?: SendMessageOptions, ) => { - try { - await client.sendMessage(content, undefined, sendOptions) - } finally { - syncResumeState() - } + await client.sendMessage(content, undefined, sendOptions) } const cancelQueued = (id: string) => client.cancelQueued(id) const append = async (message: ModelMessage | UIMessage) => { - try { - await client.append(message) - } finally { - syncResumeState() - } + await client.append(message) } const reload = async () => { - try { - await client.reload() - } finally { - syncResumeState() - } + await client.reload() } const stop = () => { @@ -294,7 +258,6 @@ export function useChat< const clear = () => { client.clear() - syncResumeState() } const setMessagesManually = (newMessages: Array>) => { @@ -316,16 +279,13 @@ export function useChat< approved: boolean }) => { await client.addToolApprovalResponse(response) - syncResumeState() } const resumeInterrupts = async ( resumeItems: Array, state?: ChatResumeState, ) => { - const result = await client.resumeInterrupts(resumeItems, state) - syncResumeState() - return result + return await client.resumeInterrupts(resumeItems, state) } const interrupts = computed(() => interruptState.value.interrupts) diff --git a/packages/ai-vue/src/use-generate-video.ts b/packages/ai-vue/src/use-generate-video.ts index fb2ac7a2a7..66ae22cd0a 100644 --- a/packages/ai-vue/src/use-generate-video.ts +++ b/packages/ai-vue/src/use-generate-video.ts @@ -231,34 +231,6 @@ export function useGenerateVideo( onStatusUpdate: (s: VideoStatusInfo) => { if (!disposed) options.onStatusUpdate?.(s) }, - onResultChange: (r: TOutput | null) => { - if (disposed) return - result.value = r - }, - onLoadingChange: (l: boolean) => { - if (disposed) return - isLoading.value = l - }, - onErrorChange: (e: Error | undefined) => { - if (disposed) return - error.value = e - }, - onStatusChange: (s: GenerationClientState) => { - if (disposed) return - status.value = s - }, - onJobIdChange: (id: string | null) => { - if (disposed) return - jobId.value = id - }, - onVideoStatusChange: (s: VideoStatusInfo | null) => { - if (disposed) return - videoStatus.value = s - }, - onResumeStateChange: (rs: { runId: string } | null) => { - if (disposed) return - runId.value = rs?.runId ?? null - }, } let client: VideoGenerationClient @@ -279,6 +251,19 @@ export function useGenerateVideo( ) } + const applySnapshot = () => { + const next = client.getSnapshot() + result.value = next.result + isLoading.value = next.isLoading + error.value = next.error + status.value = next.status + runId.value = next.runId + jobId.value = next.jobId + videoStatus.value = next.videoStatus + } + applySnapshot() + onScopeDispose(client.subscribe(applySnapshot)) + // Sync body changes to the client. // Conditional spread: `updateOptions` declares `body?: Record` // (strict optional) and rejects explicit `undefined` under EOPT. diff --git a/packages/ai-vue/src/use-generation.ts b/packages/ai-vue/src/use-generation.ts index cbec8f435f..2095da2d28 100644 --- a/packages/ai-vue/src/use-generation.ts +++ b/packages/ai-vue/src/use-generation.ts @@ -220,26 +220,6 @@ export function useGeneration< onChunk: (c: StreamChunk) => { if (!disposed) options.onChunk?.(c) }, - onResultChange: (r: TOutput | null) => { - if (disposed) return - result.value = r - }, - onLoadingChange: (l: boolean) => { - if (disposed) return - isLoading.value = l - }, - onErrorChange: (e: Error | undefined) => { - if (disposed) return - error.value = e - }, - onStatusChange: (s: GenerationClientState) => { - if (disposed) return - status.value = s - }, - onResumeStateChange: (rs) => { - if (disposed) return - runId.value = rs?.runId ?? null - }, } const persistenceProps = @@ -274,6 +254,17 @@ export function useGeneration< ) } + const applySnapshot = () => { + const next = client.getSnapshot() + result.value = next.result + isLoading.value = next.isLoading + error.value = next.error + status.value = next.status + runId.value = next.runId + } + applySnapshot() + onScopeDispose(client.subscribe(applySnapshot)) + // Sync body changes to the client. // Conditional spread: `updateOptions` declares `body?: Record` // (strict optional) and rejects explicit `undefined` under EOPT. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 827b407241..b9f0217f4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -480,8 +480,8 @@ importers: specifier: ^1.158.4 version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(supports-color@7.2.0)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/store': - specifier: ^0.8.0 - version: 0.8.0 + specifier: ^0.11.1 + version: 0.11.1 framer-motion: specifier: ^11.15.0 version: 11.18.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -779,8 +779,8 @@ importers: specifier: ^1.158.4 version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(supports-color@7.2.0)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/store': - specifier: ^0.8.0 - version: 0.8.0 + specifier: ^0.11.1 + version: 0.11.1 dockerode: specifier: ^4.0.2 version: 4.0.12(supports-color@7.2.0) @@ -1225,8 +1225,8 @@ importers: specifier: ^0.8.0 version: 0.8.0(solid-js@1.9.10) '@tanstack/store': - specifier: ^0.8.0 - version: 0.8.0 + specifier: ^0.11.1 + version: 0.11.1 highlight.js: specifier: ^11.11.1 version: 11.11.1 @@ -1657,6 +1657,9 @@ importers: '@tanstack/ai-utils': specifier: workspace:^ version: link:../ai-utils + '@tanstack/store': + specifier: ^0.11.1 + version: 0.11.1 devDependencies: '@standard-schema/spec': specifier: ^1.1.0 @@ -9448,6 +9451,9 @@ packages: resolution: {integrity: sha512-fQO+O/5xJpli5KlV6pwDz6DtpbqO/0atdVSyVnkemzk0Mej9azm4HXtw+cKkIPtsSplWs4B1EbMtgGMb9ADhSA==} engines: {node: '>=12'} + '@tanstack/store@0.11.1': + resolution: {integrity: sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA==} + '@tanstack/store@0.7.7': resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} @@ -24420,6 +24426,8 @@ snapshots: - xml2js - yaml + '@tanstack/store@0.11.1': {} + '@tanstack/store@0.7.7': {} '@tanstack/store@0.8.0': {} From ba2b0fe765b10b1ad9553ff450ed4265b0ae512e Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Tue, 25 Aug 2026 16:29:53 +0200 Subject: [PATCH 2/4] fix(ai-client): harden observable snapshots --- .changeset/ai-client-store-snapshot.md | 2 +- docs/api/ai-client.md | 9 +- docs/persistence/client-persistence.md | 11 +- examples/sandbox-cloudflare/package.json | 6 +- examples/sandbox-web/package.json | 6 +- examples/sandbox-web/src/routeTree.gen.ts | 26 +- examples/ts-code-mode-web/package.json | 6 +- .../ts-code-mode-web/src/routeTree.gen.ts | 640 +++---- .../routes/_banking-demo/api.banking-demo.ts | 2 +- .../routes/_banking-demo/api.banking-init.ts | 2 +- .../src/routes/_banking-demo/banking-demo.tsx | 2 +- .../_database-demo/api.database-demo.ts | 4 +- .../routes/_database-demo/api.db-snippets.ts | 2 +- .../src/routes/_database-demo/api.judge.ts | 2 +- .../routes/_database-demo/database-demo.tsx | 2 +- .../_execute-prompt/api.execute-prompt.ts | 4 +- .../_execute-prompt/api.realtime-token.ts | 4 +- .../routes/_execute-prompt/execute-prompt.tsx | 2 +- .../src/routes/_reporting/api.invalidate.ts | 2 +- .../src/routes/_reporting/api.report-demo.ts | 2 +- .../src/routes/_reporting/api.report-event.ts | 2 +- .../src/routes/_reporting/api.report-sse.ts | 2 +- .../src/routes/_reporting/api.reports.ts | 2 +- examples/ts-group-chat/package.json | 8 +- examples/ts-react-chat/package.json | 12 +- examples/ts-react-chat/src/routeTree.gen.ts | 1176 ++++++------- examples/ts-react-media/package.json | 6 +- examples/ts-react-media/src/routeTree.gen.ts | 26 +- examples/ts-react-rerank/package.json | 6 +- examples/ts-react-search/package.json | 10 +- examples/ts-react-search/src/routeTree.gen.ts | 152 +- examples/ts-solid-chat/package.json | 10 +- examples/ts-solid-chat/src/routeTree.gen.ts | 6 +- packages/ai-client/src/audio-recorder.ts | 8 +- packages/ai-client/src/byok/client.ts | 25 +- packages/ai-client/src/chat-client.ts | 84 +- packages/ai-client/src/generation-client.ts | 190 +- packages/ai-client/src/realtime-client.ts | 60 +- packages/ai-client/src/snapshot-atom.ts | 23 +- .../ai-client/src/video-generation-client.ts | 246 ++- .../ai-client/tests/audio-recorder.test.ts | 15 + packages/ai-client/tests/byok-client.test.ts | 28 + .../tests/byok-generation-client.test.ts | 58 + .../byok-video-generation-client.test.ts | 58 + packages/ai-client/tests/chat-client.test.ts | 124 ++ .../ai-client/tests/client-snapshots.test.ts | 59 + .../ai-client/tests/generation-client.test.ts | 503 ++++++ .../tests/generation-devtools.test.ts | 178 ++ .../ai-client/tests/snapshot-atom.test.ts | 8 + .../tests/video-generation-client.test.ts | 627 +++++++ packages/ai-react/src/use-chat.ts | 294 ++-- packages/ai-react/tests/use-chat.test.ts | 369 +++- .../tests/create-generation-dispose.test.ts | 16 +- packages/ai-vertex/package.json | 1 - packages/ai-vertex/tests/factories.test.ts | 86 +- pnpm-lock.yaml | 1297 ++++++-------- testing/e2e/package.json | 4 +- testing/e2e/src/components/ImageGenUI.tsx | 14 +- testing/e2e/src/components/VideoGenUI.tsx | 14 +- testing/e2e/src/routeTree.gen.ts | 1536 ++++++++--------- testing/e2e/tests/chat.spec.ts | 21 + .../generic-middleware-interrupts.spec.ts | 18 + testing/e2e/tests/helpers.ts | 36 +- testing/e2e/tests/image-gen.spec.ts | 12 + .../e2e/tests/persistence-durability.spec.ts | 42 + testing/e2e/tests/video-gen.spec.ts | 12 + testing/panel/package.json | 4 +- testing/panel/src/routeTree.gen.ts | 396 ++--- 68 files changed, 5516 insertions(+), 3104 deletions(-) diff --git a/.changeset/ai-client-store-snapshot.md b/.changeset/ai-client-store-snapshot.md index 6115b3dad5..6a54bb2918 100644 --- a/.changeset/ai-client-store-snapshot.md +++ b/.changeset/ai-client-store-snapshot.md @@ -9,6 +9,6 @@ '@tanstack/ai-octane': patch --- -Hold client UI state in a TanStack Store atom. Framework hooks read `getSnapshot()` instead of copying fields through change callbacks. `useChat()` return shape stays the same. +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. `ChatClient` adds `subscribeSnapshot` / `getSnapshot`. The live connection method stays `subscribe()`. Generation, video, realtime, audio, and BYOK clients expose the same snapshot pair (`subscribe` / `getSnapshot`) where the name does not collide. diff --git a/docs/api/ai-client.md b/docs/api/ai-client.md index 49239f634c..1fabcda168 100644 --- a/docs/api/ai-client.md +++ b/docs/api/ai-client.md @@ -232,8 +232,15 @@ const client = new ChatClient({ connection: fetchServerSentEvents("/api/chat"), }); +function logSnapshot() { + const snapshot = client.getSnapshot(); + console.log(snapshot.status, snapshot.messages.length); +} + +logSnapshot(); + const stop = client.subscribeSnapshot(() => { - render(client.getSnapshot()); + logSnapshot(); }); client.attach(); diff --git a/docs/persistence/client-persistence.md b/docs/persistence/client-persistence.md index b38673ea1c..e2d3e79366 100644 --- a/docs/persistence/client-persistence.md +++ b/docs/persistence/client-persistence.md @@ -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 diff --git a/examples/sandbox-cloudflare/package.json b/examples/sandbox-cloudflare/package.json index b6038d8cf8..1375ec6d77 100644 --- a/examples/sandbox-cloudflare/package.json +++ b/examples/sandbox-cloudflare/package.json @@ -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", @@ -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", diff --git a/examples/sandbox-web/package.json b/examples/sandbox-web/package.json index 75f96ab13e..941ff6df61 100644 --- a/examples/sandbox-web/package.json +++ b/examples/sandbox-web/package.json @@ -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", @@ -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", diff --git a/examples/sandbox-web/src/routeTree.gen.ts b/examples/sandbox-web/src/routeTree.gen.ts index 5bc1e9dd01..1e6a913d7e 100644 --- a/examples/sandbox-web/src/routeTree.gen.ts +++ b/examples/sandbox-web/src/routeTree.gen.ts @@ -11,8 +11,8 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' import { Route as ApiRunRouteImport } from './routes/api.run' -import { Route as ApiRunCancelRouteImport } from './routes/api.run.cancel' import { Route as ApiRunActiveRouteImport } from './routes/api.run.active' +import { Route as ApiRunCancelRouteImport } from './routes/api.run.cancel' const IndexRoute = IndexRouteImport.update({ id: '/', @@ -24,16 +24,16 @@ const ApiRunRoute = ApiRunRouteImport.update({ path: '/api/run', getParentRoute: () => rootRouteImport, } as any) -const ApiRunCancelRoute = ApiRunCancelRouteImport.update({ - id: '/cancel', - path: '/cancel', - getParentRoute: () => ApiRunRoute, -} as any) const ApiRunActiveRoute = ApiRunActiveRouteImport.update({ id: '/active', path: '/active', getParentRoute: () => ApiRunRoute, } as any) +const ApiRunCancelRoute = ApiRunCancelRouteImport.update({ + id: '/cancel', + path: '/cancel', + getParentRoute: () => ApiRunRoute, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute @@ -83,13 +83,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiRunRouteImport parentRoute: typeof rootRouteImport } - '/api/run/cancel': { - id: '/api/run/cancel' - path: '/cancel' - fullPath: '/api/run/cancel' - preLoaderRoute: typeof ApiRunCancelRouteImport - parentRoute: typeof ApiRunRoute - } '/api/run/active': { id: '/api/run/active' path: '/active' @@ -97,6 +90,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiRunActiveRouteImport parentRoute: typeof ApiRunRoute } + '/api/run/cancel': { + id: '/api/run/cancel' + path: '/cancel' + fullPath: '/api/run/cancel' + preLoaderRoute: typeof ApiRunCancelRouteImport + parentRoute: typeof ApiRunRoute + } } } diff --git a/examples/ts-code-mode-web/package.json b/examples/ts-code-mode-web/package.json index ed5a5978ee..c829b45cf6 100644 --- a/examples/ts-code-mode-web/package.json +++ b/examples/ts-code-mode-web/package.json @@ -32,9 +32,9 @@ "@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/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", diff --git a/examples/ts-code-mode-web/src/routeTree.gen.ts b/examples/ts-code-mode-web/src/routeTree.gen.ts index e8255fd491..81c372c1ec 100644 --- a/examples/ts-code-mode-web/src/routeTree.gen.ts +++ b/examples/ts-code-mode-web/src/routeTree.gen.ts @@ -9,78 +9,101 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' -import { Route as StructuredOutputRouteRouteImport } from './routes/_structured-output/route' -import { Route as ReportingRouteRouteImport } from './routes/_reporting/route' -import { Route as NpmGithubChatRouteRouteImport } from './routes/_npm-github-chat/route' -import { Route as HomeRouteRouteImport } from './routes/_home/route' -import { Route as ExecutePromptRouteRouteImport } from './routes/_execute-prompt/route' -import { Route as DatabaseDemoRouteRouteImport } from './routes/_database-demo/route' import { Route as BankingDemoRouteRouteImport } from './routes/_banking-demo/route' +import { Route as DatabaseDemoRouteRouteImport } from './routes/_database-demo/route' +import { Route as ExecutePromptRouteRouteImport } from './routes/_execute-prompt/route' +import { Route as HomeRouteRouteImport } from './routes/_home/route' +import { Route as NpmGithubChatRouteRouteImport } from './routes/_npm-github-chat/route' +import { Route as ReportingRouteRouteImport } from './routes/_reporting/route' +import { Route as StructuredOutputRouteRouteImport } from './routes/_structured-output/route' +import { Route as BankingDemoBankingDemoRouteImport } from './routes/_banking-demo/banking-demo' +import { Route as DatabaseDemoDatabaseDemoRouteImport } from './routes/_database-demo/database-demo' +import { Route as ExecutePromptExecutePromptRouteImport } from './routes/_execute-prompt/execute-prompt' import { Route as HomeIndexRouteImport } from './routes/_home/index' -import { Route as ApiRuntimeRouteImport } from './routes/api.runtime' -import { Route as StructuredOutputStructuredOutputRouteImport } from './routes/_structured-output/structured-output' -import { Route as ReportingReportingAgentRouteImport } from './routes/_reporting/reporting-agent' import { Route as NpmGithubChatNpmGithubChatRouteImport } from './routes/_npm-github-chat/npm-github-chat' -import { Route as ExecutePromptExecutePromptRouteImport } from './routes/_execute-prompt/execute-prompt' -import { Route as DatabaseDemoDatabaseDemoRouteImport } from './routes/_database-demo/database-demo' -import { Route as BankingDemoBankingDemoRouteImport } from './routes/_banking-demo/banking-demo' -import { Route as StructuredOutputApiStructuredOutputSnippetsRouteImport } from './routes/_structured-output/api.structured-output-snippets' -import { Route as StructuredOutputApiStructuredOutputRouteImport } from './routes/_structured-output/api.structured-output' -import { Route as ReportingApiReportsRouteImport } from './routes/_reporting/api.reports' -import { Route as ReportingApiReportSseRouteImport } from './routes/_reporting/api.report-sse' -import { Route as ReportingApiReportEventRouteImport } from './routes/_reporting/api.report-event' -import { Route as ReportingApiReportDemoRouteImport } from './routes/_reporting/api.report-demo' -import { Route as ReportingApiInvalidateRouteImport } from './routes/_reporting/api.invalidate' -import { Route as NpmGithubChatApiGeneratePdfRouteImport } from './routes/_npm-github-chat/api.generate-pdf' -import { Route as NpmGithubChatApiCodemodeRouteImport } from './routes/_npm-github-chat/api.codemode' -import { Route as HomeApiSnippetsRouteImport } from './routes/_home/api.snippets' -import { Route as HomeApiProductRegularRouteImport } from './routes/_home/api.product-regular' -import { Route as HomeApiProductCodemodeRouteImport } from './routes/_home/api.product-codemode' -import { Route as ExecutePromptApiRealtimeTokenRouteImport } from './routes/_execute-prompt/api.realtime-token' -import { Route as ExecutePromptApiExecutePromptRouteImport } from './routes/_execute-prompt/api.execute-prompt' -import { Route as DatabaseDemoApiJudgeRouteImport } from './routes/_database-demo/api.judge' -import { Route as DatabaseDemoApiDbSnippetsRouteImport } from './routes/_database-demo/api.db-snippets' -import { Route as DatabaseDemoApiDatabaseDemoRouteImport } from './routes/_database-demo/api.database-demo' -import { Route as BankingDemoApiBankingInitRouteImport } from './routes/_banking-demo/api.banking-init' +import { Route as ReportingReportingAgentRouteImport } from './routes/_reporting/reporting-agent' +import { Route as StructuredOutputStructuredOutputRouteImport } from './routes/_structured-output/structured-output' +import { Route as ApiRuntimeRouteImport } from './routes/api.runtime' import { Route as BankingDemoApiBankingDemoRouteImport } from './routes/_banking-demo/api.banking-demo' +import { Route as BankingDemoApiBankingInitRouteImport } from './routes/_banking-demo/api.banking-init' +import { Route as DatabaseDemoApiDatabaseDemoRouteImport } from './routes/_database-demo/api.database-demo' +import { Route as DatabaseDemoApiDbSnippetsRouteImport } from './routes/_database-demo/api.db-snippets' +import { Route as DatabaseDemoApiJudgeRouteImport } from './routes/_database-demo/api.judge' +import { Route as ExecutePromptApiExecutePromptRouteImport } from './routes/_execute-prompt/api.execute-prompt' +import { Route as ExecutePromptApiRealtimeTokenRouteImport } from './routes/_execute-prompt/api.realtime-token' +import { Route as HomeApiProductCodemodeRouteImport } from './routes/_home/api.product-codemode' +import { Route as HomeApiProductRegularRouteImport } from './routes/_home/api.product-regular' +import { Route as HomeApiSnippetsRouteImport } from './routes/_home/api.snippets' +import { Route as NpmGithubChatApiCodemodeRouteImport } from './routes/_npm-github-chat/api.codemode' +import { Route as NpmGithubChatApiGeneratePdfRouteImport } from './routes/_npm-github-chat/api.generate-pdf' +import { Route as ReportingApiInvalidateRouteImport } from './routes/_reporting/api.invalidate' +import { Route as ReportingApiReportDemoRouteImport } from './routes/_reporting/api.report-demo' +import { Route as ReportingApiReportEventRouteImport } from './routes/_reporting/api.report-event' +import { Route as ReportingApiReportSseRouteImport } from './routes/_reporting/api.report-sse' +import { Route as ReportingApiReportsRouteImport } from './routes/_reporting/api.reports' +import { Route as StructuredOutputApiStructuredOutputRouteImport } from './routes/_structured-output/api.structured-output' +import { Route as StructuredOutputApiStructuredOutputSnippetsRouteImport } from './routes/_structured-output/api.structured-output-snippets' -const StructuredOutputRouteRoute = StructuredOutputRouteRouteImport.update({ - id: '/_structured-output', +const BankingDemoRouteRoute = BankingDemoRouteRouteImport.update({ + id: '/_banking-demo', getParentRoute: () => rootRouteImport, } as any) -const ReportingRouteRoute = ReportingRouteRouteImport.update({ - id: '/_reporting', +const DatabaseDemoRouteRoute = DatabaseDemoRouteRouteImport.update({ + id: '/_database-demo', getParentRoute: () => rootRouteImport, } as any) -const NpmGithubChatRouteRoute = NpmGithubChatRouteRouteImport.update({ - id: '/_npm-github-chat', +const ExecutePromptRouteRoute = ExecutePromptRouteRouteImport.update({ + id: '/_execute-prompt', getParentRoute: () => rootRouteImport, } as any) const HomeRouteRoute = HomeRouteRouteImport.update({ id: '/_home', getParentRoute: () => rootRouteImport, } as any) -const ExecutePromptRouteRoute = ExecutePromptRouteRouteImport.update({ - id: '/_execute-prompt', +const NpmGithubChatRouteRoute = NpmGithubChatRouteRouteImport.update({ + id: '/_npm-github-chat', getParentRoute: () => rootRouteImport, } as any) -const DatabaseDemoRouteRoute = DatabaseDemoRouteRouteImport.update({ - id: '/_database-demo', +const ReportingRouteRoute = ReportingRouteRouteImport.update({ + id: '/_reporting', getParentRoute: () => rootRouteImport, } as any) -const BankingDemoRouteRoute = BankingDemoRouteRouteImport.update({ - id: '/_banking-demo', +const StructuredOutputRouteRoute = StructuredOutputRouteRouteImport.update({ + id: '/_structured-output', getParentRoute: () => rootRouteImport, } as any) +const BankingDemoBankingDemoRoute = BankingDemoBankingDemoRouteImport.update({ + id: '/banking-demo', + path: '/banking-demo', + getParentRoute: () => BankingDemoRouteRoute, +} as any) +const DatabaseDemoDatabaseDemoRoute = + DatabaseDemoDatabaseDemoRouteImport.update({ + id: '/database-demo', + path: '/database-demo', + getParentRoute: () => DatabaseDemoRouteRoute, + } as any) +const ExecutePromptExecutePromptRoute = + ExecutePromptExecutePromptRouteImport.update({ + id: '/execute-prompt', + path: '/execute-prompt', + getParentRoute: () => ExecutePromptRouteRoute, + } as any) const HomeIndexRoute = HomeIndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => HomeRouteRoute, } as any) -const ApiRuntimeRoute = ApiRuntimeRouteImport.update({ - id: '/api/runtime', - path: '/api/runtime', - getParentRoute: () => rootRouteImport, +const NpmGithubChatNpmGithubChatRoute = + NpmGithubChatNpmGithubChatRouteImport.update({ + id: '/npm-github-chat', + path: '/npm-github-chat', + getParentRoute: () => NpmGithubChatRouteRoute, + } as any) +const ReportingReportingAgentRoute = ReportingReportingAgentRouteImport.update({ + id: '/reporting-agent', + path: '/reporting-agent', + getParentRoute: () => ReportingRouteRoute, } as any) const StructuredOutputStructuredOutputRoute = StructuredOutputStructuredOutputRouteImport.update({ @@ -88,54 +111,87 @@ const StructuredOutputStructuredOutputRoute = path: '/structured-output', getParentRoute: () => StructuredOutputRouteRoute, } as any) -const ReportingReportingAgentRoute = ReportingReportingAgentRouteImport.update({ - id: '/reporting-agent', - path: '/reporting-agent', - getParentRoute: () => ReportingRouteRoute, +const ApiRuntimeRoute = ApiRuntimeRouteImport.update({ + id: '/api/runtime', + path: '/api/runtime', + getParentRoute: () => rootRouteImport, } as any) -const NpmGithubChatNpmGithubChatRoute = - NpmGithubChatNpmGithubChatRouteImport.update({ - id: '/npm-github-chat', - path: '/npm-github-chat', - getParentRoute: () => NpmGithubChatRouteRoute, +const BankingDemoApiBankingDemoRoute = + BankingDemoApiBankingDemoRouteImport.update({ + id: '/api/banking-demo', + path: '/api/banking-demo', + getParentRoute: () => BankingDemoRouteRoute, } as any) -const ExecutePromptExecutePromptRoute = - ExecutePromptExecutePromptRouteImport.update({ - id: '/execute-prompt', - path: '/execute-prompt', - getParentRoute: () => ExecutePromptRouteRoute, +const BankingDemoApiBankingInitRoute = + BankingDemoApiBankingInitRouteImport.update({ + id: '/api/banking-init', + path: '/api/banking-init', + getParentRoute: () => BankingDemoRouteRoute, } as any) -const DatabaseDemoDatabaseDemoRoute = - DatabaseDemoDatabaseDemoRouteImport.update({ - id: '/database-demo', - path: '/database-demo', +const DatabaseDemoApiDatabaseDemoRoute = + DatabaseDemoApiDatabaseDemoRouteImport.update({ + id: '/api/database-demo', + path: '/api/database-demo', getParentRoute: () => DatabaseDemoRouteRoute, } as any) -const BankingDemoBankingDemoRoute = BankingDemoBankingDemoRouteImport.update({ - id: '/banking-demo', - path: '/banking-demo', - getParentRoute: () => BankingDemoRouteRoute, +const DatabaseDemoApiDbSnippetsRoute = + DatabaseDemoApiDbSnippetsRouteImport.update({ + id: '/api/db-snippets', + path: '/api/db-snippets', + getParentRoute: () => DatabaseDemoRouteRoute, + } as any) +const DatabaseDemoApiJudgeRoute = DatabaseDemoApiJudgeRouteImport.update({ + id: '/api/judge', + path: '/api/judge', + getParentRoute: () => DatabaseDemoRouteRoute, } as any) -const StructuredOutputApiStructuredOutputSnippetsRoute = - StructuredOutputApiStructuredOutputSnippetsRouteImport.update({ - id: '/api/structured-output-snippets', - path: '/api/structured-output-snippets', - getParentRoute: () => StructuredOutputRouteRoute, +const ExecutePromptApiExecutePromptRoute = + ExecutePromptApiExecutePromptRouteImport.update({ + id: '/api/execute-prompt', + path: '/api/execute-prompt', + getParentRoute: () => ExecutePromptRouteRoute, } as any) -const StructuredOutputApiStructuredOutputRoute = - StructuredOutputApiStructuredOutputRouteImport.update({ - id: '/api/structured-output', - path: '/api/structured-output', - getParentRoute: () => StructuredOutputRouteRoute, +const ExecutePromptApiRealtimeTokenRoute = + ExecutePromptApiRealtimeTokenRouteImport.update({ + id: '/api/realtime-token', + path: '/api/realtime-token', + getParentRoute: () => ExecutePromptRouteRoute, } as any) -const ReportingApiReportsRoute = ReportingApiReportsRouteImport.update({ - id: '/api/reports', - path: '/api/reports', +const HomeApiProductCodemodeRoute = HomeApiProductCodemodeRouteImport.update({ + id: '/api/product-codemode', + path: '/api/product-codemode', + getParentRoute: () => HomeRouteRoute, +} as any) +const HomeApiProductRegularRoute = HomeApiProductRegularRouteImport.update({ + id: '/api/product-regular', + path: '/api/product-regular', + getParentRoute: () => HomeRouteRoute, +} as any) +const HomeApiSnippetsRoute = HomeApiSnippetsRouteImport.update({ + id: '/api/snippets', + path: '/api/snippets', + getParentRoute: () => HomeRouteRoute, +} as any) +const NpmGithubChatApiCodemodeRoute = + NpmGithubChatApiCodemodeRouteImport.update({ + id: '/api/codemode', + path: '/api/codemode', + getParentRoute: () => NpmGithubChatRouteRoute, + } as any) +const NpmGithubChatApiGeneratePdfRoute = + NpmGithubChatApiGeneratePdfRouteImport.update({ + id: '/api/generate-pdf', + path: '/api/generate-pdf', + getParentRoute: () => NpmGithubChatRouteRoute, + } as any) +const ReportingApiInvalidateRoute = ReportingApiInvalidateRouteImport.update({ + id: '/api/invalidate', + path: '/api/invalidate', getParentRoute: () => ReportingRouteRoute, } as any) -const ReportingApiReportSseRoute = ReportingApiReportSseRouteImport.update({ - id: '/api/report-sse', - path: '/api/report-sse', +const ReportingApiReportDemoRoute = ReportingApiReportDemoRouteImport.update({ + id: '/api/report-demo', + path: '/api/report-demo', getParentRoute: () => ReportingRouteRoute, } as any) const ReportingApiReportEventRoute = ReportingApiReportEventRouteImport.update({ @@ -143,83 +199,27 @@ const ReportingApiReportEventRoute = ReportingApiReportEventRouteImport.update({ path: '/api/report-event', getParentRoute: () => ReportingRouteRoute, } as any) -const ReportingApiReportDemoRoute = ReportingApiReportDemoRouteImport.update({ - id: '/api/report-demo', - path: '/api/report-demo', +const ReportingApiReportSseRoute = ReportingApiReportSseRouteImport.update({ + id: '/api/report-sse', + path: '/api/report-sse', getParentRoute: () => ReportingRouteRoute, } as any) -const ReportingApiInvalidateRoute = ReportingApiInvalidateRouteImport.update({ - id: '/api/invalidate', - path: '/api/invalidate', +const ReportingApiReportsRoute = ReportingApiReportsRouteImport.update({ + id: '/api/reports', + path: '/api/reports', getParentRoute: () => ReportingRouteRoute, } as any) -const NpmGithubChatApiGeneratePdfRoute = - NpmGithubChatApiGeneratePdfRouteImport.update({ - id: '/api/generate-pdf', - path: '/api/generate-pdf', - getParentRoute: () => NpmGithubChatRouteRoute, +const StructuredOutputApiStructuredOutputRoute = + StructuredOutputApiStructuredOutputRouteImport.update({ + id: '/api/structured-output', + path: '/api/structured-output', + getParentRoute: () => StructuredOutputRouteRoute, } as any) -const NpmGithubChatApiCodemodeRoute = - NpmGithubChatApiCodemodeRouteImport.update({ - id: '/api/codemode', - path: '/api/codemode', - getParentRoute: () => NpmGithubChatRouteRoute, - } as any) -const HomeApiSnippetsRoute = HomeApiSnippetsRouteImport.update({ - id: '/api/snippets', - path: '/api/snippets', - getParentRoute: () => HomeRouteRoute, -} as any) -const HomeApiProductRegularRoute = HomeApiProductRegularRouteImport.update({ - id: '/api/product-regular', - path: '/api/product-regular', - getParentRoute: () => HomeRouteRoute, -} as any) -const HomeApiProductCodemodeRoute = HomeApiProductCodemodeRouteImport.update({ - id: '/api/product-codemode', - path: '/api/product-codemode', - getParentRoute: () => HomeRouteRoute, -} as any) -const ExecutePromptApiRealtimeTokenRoute = - ExecutePromptApiRealtimeTokenRouteImport.update({ - id: '/api/realtime-token', - path: '/api/realtime-token', - getParentRoute: () => ExecutePromptRouteRoute, - } as any) -const ExecutePromptApiExecutePromptRoute = - ExecutePromptApiExecutePromptRouteImport.update({ - id: '/api/execute-prompt', - path: '/api/execute-prompt', - getParentRoute: () => ExecutePromptRouteRoute, - } as any) -const DatabaseDemoApiJudgeRoute = DatabaseDemoApiJudgeRouteImport.update({ - id: '/api/judge', - path: '/api/judge', - getParentRoute: () => DatabaseDemoRouteRoute, -} as any) -const DatabaseDemoApiDbSnippetsRoute = - DatabaseDemoApiDbSnippetsRouteImport.update({ - id: '/api/db-snippets', - path: '/api/db-snippets', - getParentRoute: () => DatabaseDemoRouteRoute, - } as any) -const DatabaseDemoApiDatabaseDemoRoute = - DatabaseDemoApiDatabaseDemoRouteImport.update({ - id: '/api/database-demo', - path: '/api/database-demo', - getParentRoute: () => DatabaseDemoRouteRoute, - } as any) -const BankingDemoApiBankingInitRoute = - BankingDemoApiBankingInitRouteImport.update({ - id: '/api/banking-init', - path: '/api/banking-init', - getParentRoute: () => BankingDemoRouteRoute, - } as any) -const BankingDemoApiBankingDemoRoute = - BankingDemoApiBankingDemoRouteImport.update({ - id: '/api/banking-demo', - path: '/api/banking-demo', - getParentRoute: () => BankingDemoRouteRoute, +const StructuredOutputApiStructuredOutputSnippetsRoute = + StructuredOutputApiStructuredOutputSnippetsRouteImport.update({ + id: '/api/structured-output-snippets', + path: '/api/structured-output-snippets', + getParentRoute: () => StructuredOutputRouteRoute, } as any) export interface FileRoutesByFullPath { @@ -427,25 +427,25 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/_structured-output': { - id: '/_structured-output' + '/_banking-demo': { + id: '/_banking-demo' path: '' fullPath: '/' - preLoaderRoute: typeof StructuredOutputRouteRouteImport + preLoaderRoute: typeof BankingDemoRouteRouteImport parentRoute: typeof rootRouteImport } - '/_reporting': { - id: '/_reporting' + '/_database-demo': { + id: '/_database-demo' path: '' fullPath: '/' - preLoaderRoute: typeof ReportingRouteRouteImport + preLoaderRoute: typeof DatabaseDemoRouteRouteImport parentRoute: typeof rootRouteImport } - '/_npm-github-chat': { - id: '/_npm-github-chat' + '/_execute-prompt': { + id: '/_execute-prompt' path: '' fullPath: '/' - preLoaderRoute: typeof NpmGithubChatRouteRouteImport + preLoaderRoute: typeof ExecutePromptRouteRouteImport parentRoute: typeof rootRouteImport } '/_home': { @@ -455,27 +455,48 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof HomeRouteRouteImport parentRoute: typeof rootRouteImport } - '/_execute-prompt': { - id: '/_execute-prompt' + '/_npm-github-chat': { + id: '/_npm-github-chat' path: '' fullPath: '/' - preLoaderRoute: typeof ExecutePromptRouteRouteImport + preLoaderRoute: typeof NpmGithubChatRouteRouteImport parentRoute: typeof rootRouteImport } - '/_database-demo': { - id: '/_database-demo' + '/_reporting': { + id: '/_reporting' path: '' fullPath: '/' - preLoaderRoute: typeof DatabaseDemoRouteRouteImport + preLoaderRoute: typeof ReportingRouteRouteImport parentRoute: typeof rootRouteImport } - '/_banking-demo': { - id: '/_banking-demo' + '/_structured-output': { + id: '/_structured-output' path: '' fullPath: '/' - preLoaderRoute: typeof BankingDemoRouteRouteImport + preLoaderRoute: typeof StructuredOutputRouteRouteImport parentRoute: typeof rootRouteImport } + '/_banking-demo/banking-demo': { + id: '/_banking-demo/banking-demo' + path: '/banking-demo' + fullPath: '/banking-demo' + preLoaderRoute: typeof BankingDemoBankingDemoRouteImport + parentRoute: typeof BankingDemoRouteRoute + } + '/_database-demo/database-demo': { + id: '/_database-demo/database-demo' + path: '/database-demo' + fullPath: '/database-demo' + preLoaderRoute: typeof DatabaseDemoDatabaseDemoRouteImport + parentRoute: typeof DatabaseDemoRouteRoute + } + '/_execute-prompt/execute-prompt': { + id: '/_execute-prompt/execute-prompt' + path: '/execute-prompt' + fullPath: '/execute-prompt' + preLoaderRoute: typeof ExecutePromptExecutePromptRouteImport + parentRoute: typeof ExecutePromptRouteRoute + } '/_home/': { id: '/_home/' path: '/' @@ -483,27 +504,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof HomeIndexRouteImport parentRoute: typeof HomeRouteRoute } - '/api/runtime': { - id: '/api/runtime' - path: '/api/runtime' - fullPath: '/api/runtime' - preLoaderRoute: typeof ApiRuntimeRouteImport - parentRoute: typeof rootRouteImport - } - '/_structured-output/structured-output': { - id: '/_structured-output/structured-output' - path: '/structured-output' - fullPath: '/structured-output' - preLoaderRoute: typeof StructuredOutputStructuredOutputRouteImport - parentRoute: typeof StructuredOutputRouteRoute - } - '/_reporting/reporting-agent': { - id: '/_reporting/reporting-agent' - path: '/reporting-agent' - fullPath: '/reporting-agent' - preLoaderRoute: typeof ReportingReportingAgentRouteImport - parentRoute: typeof ReportingRouteRoute - } '/_npm-github-chat/npm-github-chat': { id: '/_npm-github-chat/npm-github-chat' path: '/npm-github-chat' @@ -511,95 +511,81 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof NpmGithubChatNpmGithubChatRouteImport parentRoute: typeof NpmGithubChatRouteRoute } - '/_execute-prompt/execute-prompt': { - id: '/_execute-prompt/execute-prompt' - path: '/execute-prompt' - fullPath: '/execute-prompt' - preLoaderRoute: typeof ExecutePromptExecutePromptRouteImport - parentRoute: typeof ExecutePromptRouteRoute - } - '/_database-demo/database-demo': { - id: '/_database-demo/database-demo' - path: '/database-demo' - fullPath: '/database-demo' - preLoaderRoute: typeof DatabaseDemoDatabaseDemoRouteImport - parentRoute: typeof DatabaseDemoRouteRoute - } - '/_banking-demo/banking-demo': { - id: '/_banking-demo/banking-demo' - path: '/banking-demo' - fullPath: '/banking-demo' - preLoaderRoute: typeof BankingDemoBankingDemoRouteImport - parentRoute: typeof BankingDemoRouteRoute + '/_reporting/reporting-agent': { + id: '/_reporting/reporting-agent' + path: '/reporting-agent' + fullPath: '/reporting-agent' + preLoaderRoute: typeof ReportingReportingAgentRouteImport + parentRoute: typeof ReportingRouteRoute } - '/_structured-output/api/structured-output-snippets': { - id: '/_structured-output/api/structured-output-snippets' - path: '/api/structured-output-snippets' - fullPath: '/api/structured-output-snippets' - preLoaderRoute: typeof StructuredOutputApiStructuredOutputSnippetsRouteImport + '/_structured-output/structured-output': { + id: '/_structured-output/structured-output' + path: '/structured-output' + fullPath: '/structured-output' + preLoaderRoute: typeof StructuredOutputStructuredOutputRouteImport parentRoute: typeof StructuredOutputRouteRoute } - '/_structured-output/api/structured-output': { - id: '/_structured-output/api/structured-output' - path: '/api/structured-output' - fullPath: '/api/structured-output' - preLoaderRoute: typeof StructuredOutputApiStructuredOutputRouteImport - parentRoute: typeof StructuredOutputRouteRoute + '/api/runtime': { + id: '/api/runtime' + path: '/api/runtime' + fullPath: '/api/runtime' + preLoaderRoute: typeof ApiRuntimeRouteImport + parentRoute: typeof rootRouteImport } - '/_reporting/api/reports': { - id: '/_reporting/api/reports' - path: '/api/reports' - fullPath: '/api/reports' - preLoaderRoute: typeof ReportingApiReportsRouteImport - parentRoute: typeof ReportingRouteRoute + '/_banking-demo/api/banking-demo': { + id: '/_banking-demo/api/banking-demo' + path: '/api/banking-demo' + fullPath: '/api/banking-demo' + preLoaderRoute: typeof BankingDemoApiBankingDemoRouteImport + parentRoute: typeof BankingDemoRouteRoute } - '/_reporting/api/report-sse': { - id: '/_reporting/api/report-sse' - path: '/api/report-sse' - fullPath: '/api/report-sse' - preLoaderRoute: typeof ReportingApiReportSseRouteImport - parentRoute: typeof ReportingRouteRoute + '/_banking-demo/api/banking-init': { + id: '/_banking-demo/api/banking-init' + path: '/api/banking-init' + fullPath: '/api/banking-init' + preLoaderRoute: typeof BankingDemoApiBankingInitRouteImport + parentRoute: typeof BankingDemoRouteRoute } - '/_reporting/api/report-event': { - id: '/_reporting/api/report-event' - path: '/api/report-event' - fullPath: '/api/report-event' - preLoaderRoute: typeof ReportingApiReportEventRouteImport - parentRoute: typeof ReportingRouteRoute + '/_database-demo/api/database-demo': { + id: '/_database-demo/api/database-demo' + path: '/api/database-demo' + fullPath: '/api/database-demo' + preLoaderRoute: typeof DatabaseDemoApiDatabaseDemoRouteImport + parentRoute: typeof DatabaseDemoRouteRoute } - '/_reporting/api/report-demo': { - id: '/_reporting/api/report-demo' - path: '/api/report-demo' - fullPath: '/api/report-demo' - preLoaderRoute: typeof ReportingApiReportDemoRouteImport - parentRoute: typeof ReportingRouteRoute + '/_database-demo/api/db-snippets': { + id: '/_database-demo/api/db-snippets' + path: '/api/db-snippets' + fullPath: '/api/db-snippets' + preLoaderRoute: typeof DatabaseDemoApiDbSnippetsRouteImport + parentRoute: typeof DatabaseDemoRouteRoute } - '/_reporting/api/invalidate': { - id: '/_reporting/api/invalidate' - path: '/api/invalidate' - fullPath: '/api/invalidate' - preLoaderRoute: typeof ReportingApiInvalidateRouteImport - parentRoute: typeof ReportingRouteRoute + '/_database-demo/api/judge': { + id: '/_database-demo/api/judge' + path: '/api/judge' + fullPath: '/api/judge' + preLoaderRoute: typeof DatabaseDemoApiJudgeRouteImport + parentRoute: typeof DatabaseDemoRouteRoute } - '/_npm-github-chat/api/generate-pdf': { - id: '/_npm-github-chat/api/generate-pdf' - path: '/api/generate-pdf' - fullPath: '/api/generate-pdf' - preLoaderRoute: typeof NpmGithubChatApiGeneratePdfRouteImport - parentRoute: typeof NpmGithubChatRouteRoute + '/_execute-prompt/api/execute-prompt': { + id: '/_execute-prompt/api/execute-prompt' + path: '/api/execute-prompt' + fullPath: '/api/execute-prompt' + preLoaderRoute: typeof ExecutePromptApiExecutePromptRouteImport + parentRoute: typeof ExecutePromptRouteRoute } - '/_npm-github-chat/api/codemode': { - id: '/_npm-github-chat/api/codemode' - path: '/api/codemode' - fullPath: '/api/codemode' - preLoaderRoute: typeof NpmGithubChatApiCodemodeRouteImport - parentRoute: typeof NpmGithubChatRouteRoute + '/_execute-prompt/api/realtime-token': { + id: '/_execute-prompt/api/realtime-token' + path: '/api/realtime-token' + fullPath: '/api/realtime-token' + preLoaderRoute: typeof ExecutePromptApiRealtimeTokenRouteImport + parentRoute: typeof ExecutePromptRouteRoute } - '/_home/api/snippets': { - id: '/_home/api/snippets' - path: '/api/snippets' - fullPath: '/api/snippets' - preLoaderRoute: typeof HomeApiSnippetsRouteImport + '/_home/api/product-codemode': { + id: '/_home/api/product-codemode' + path: '/api/product-codemode' + fullPath: '/api/product-codemode' + preLoaderRoute: typeof HomeApiProductCodemodeRouteImport parentRoute: typeof HomeRouteRoute } '/_home/api/product-regular': { @@ -609,61 +595,75 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof HomeApiProductRegularRouteImport parentRoute: typeof HomeRouteRoute } - '/_home/api/product-codemode': { - id: '/_home/api/product-codemode' - path: '/api/product-codemode' - fullPath: '/api/product-codemode' - preLoaderRoute: typeof HomeApiProductCodemodeRouteImport + '/_home/api/snippets': { + id: '/_home/api/snippets' + path: '/api/snippets' + fullPath: '/api/snippets' + preLoaderRoute: typeof HomeApiSnippetsRouteImport parentRoute: typeof HomeRouteRoute } - '/_execute-prompt/api/realtime-token': { - id: '/_execute-prompt/api/realtime-token' - path: '/api/realtime-token' - fullPath: '/api/realtime-token' - preLoaderRoute: typeof ExecutePromptApiRealtimeTokenRouteImport - parentRoute: typeof ExecutePromptRouteRoute + '/_npm-github-chat/api/codemode': { + id: '/_npm-github-chat/api/codemode' + path: '/api/codemode' + fullPath: '/api/codemode' + preLoaderRoute: typeof NpmGithubChatApiCodemodeRouteImport + parentRoute: typeof NpmGithubChatRouteRoute } - '/_execute-prompt/api/execute-prompt': { - id: '/_execute-prompt/api/execute-prompt' - path: '/api/execute-prompt' - fullPath: '/api/execute-prompt' - preLoaderRoute: typeof ExecutePromptApiExecutePromptRouteImport - parentRoute: typeof ExecutePromptRouteRoute + '/_npm-github-chat/api/generate-pdf': { + id: '/_npm-github-chat/api/generate-pdf' + path: '/api/generate-pdf' + fullPath: '/api/generate-pdf' + preLoaderRoute: typeof NpmGithubChatApiGeneratePdfRouteImport + parentRoute: typeof NpmGithubChatRouteRoute } - '/_database-demo/api/judge': { - id: '/_database-demo/api/judge' - path: '/api/judge' - fullPath: '/api/judge' - preLoaderRoute: typeof DatabaseDemoApiJudgeRouteImport - parentRoute: typeof DatabaseDemoRouteRoute + '/_reporting/api/invalidate': { + id: '/_reporting/api/invalidate' + path: '/api/invalidate' + fullPath: '/api/invalidate' + preLoaderRoute: typeof ReportingApiInvalidateRouteImport + parentRoute: typeof ReportingRouteRoute } - '/_database-demo/api/db-snippets': { - id: '/_database-demo/api/db-snippets' - path: '/api/db-snippets' - fullPath: '/api/db-snippets' - preLoaderRoute: typeof DatabaseDemoApiDbSnippetsRouteImport - parentRoute: typeof DatabaseDemoRouteRoute + '/_reporting/api/report-demo': { + id: '/_reporting/api/report-demo' + path: '/api/report-demo' + fullPath: '/api/report-demo' + preLoaderRoute: typeof ReportingApiReportDemoRouteImport + parentRoute: typeof ReportingRouteRoute } - '/_database-demo/api/database-demo': { - id: '/_database-demo/api/database-demo' - path: '/api/database-demo' - fullPath: '/api/database-demo' - preLoaderRoute: typeof DatabaseDemoApiDatabaseDemoRouteImport - parentRoute: typeof DatabaseDemoRouteRoute + '/_reporting/api/report-event': { + id: '/_reporting/api/report-event' + path: '/api/report-event' + fullPath: '/api/report-event' + preLoaderRoute: typeof ReportingApiReportEventRouteImport + parentRoute: typeof ReportingRouteRoute } - '/_banking-demo/api/banking-init': { - id: '/_banking-demo/api/banking-init' - path: '/api/banking-init' - fullPath: '/api/banking-init' - preLoaderRoute: typeof BankingDemoApiBankingInitRouteImport - parentRoute: typeof BankingDemoRouteRoute + '/_reporting/api/report-sse': { + id: '/_reporting/api/report-sse' + path: '/api/report-sse' + fullPath: '/api/report-sse' + preLoaderRoute: typeof ReportingApiReportSseRouteImport + parentRoute: typeof ReportingRouteRoute } - '/_banking-demo/api/banking-demo': { - id: '/_banking-demo/api/banking-demo' - path: '/api/banking-demo' - fullPath: '/api/banking-demo' - preLoaderRoute: typeof BankingDemoApiBankingDemoRouteImport - parentRoute: typeof BankingDemoRouteRoute + '/_reporting/api/reports': { + id: '/_reporting/api/reports' + path: '/api/reports' + fullPath: '/api/reports' + preLoaderRoute: typeof ReportingApiReportsRouteImport + parentRoute: typeof ReportingRouteRoute + } + '/_structured-output/api/structured-output': { + id: '/_structured-output/api/structured-output' + path: '/api/structured-output' + fullPath: '/api/structured-output' + preLoaderRoute: typeof StructuredOutputApiStructuredOutputRouteImport + parentRoute: typeof StructuredOutputRouteRoute + } + '/_structured-output/api/structured-output-snippets': { + id: '/_structured-output/api/structured-output-snippets' + path: '/api/structured-output-snippets' + fullPath: '/api/structured-output-snippets' + preLoaderRoute: typeof StructuredOutputApiStructuredOutputSnippetsRouteImport + parentRoute: typeof StructuredOutputRouteRoute } } } diff --git a/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts b/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts index ff14d96999..335115b4be 100644 --- a/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts +++ b/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts @@ -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 }) => { diff --git a/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-init.ts b/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-init.ts index 30630aaa38..1275b6aca7 100644 --- a/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-init.ts +++ b/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-init.ts @@ -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 () => { diff --git a/examples/ts-code-mode-web/src/routes/_banking-demo/banking-demo.tsx b/examples/ts-code-mode-web/src/routes/_banking-demo/banking-demo.tsx index d0ab65e1db..6dd38976e6 100644 --- a/examples/ts-code-mode-web/src/routes/_banking-demo/banking-demo.tsx +++ b/examples/ts-code-mode-web/src/routes/_banking-demo/banking-demo.tsx @@ -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, }) diff --git a/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts b/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts index 10d7a12a85..4258f9ceb3 100644 --- a/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts +++ b/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts @@ -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 }) => { diff --git a/examples/ts-code-mode-web/src/routes/_database-demo/api.db-snippets.ts b/examples/ts-code-mode-web/src/routes/_database-demo/api.db-snippets.ts index a7f0cdcac0..a04274deb9 100644 --- a/examples/ts-code-mode-web/src/routes/_database-demo/api.db-snippets.ts +++ b/examples/ts-code-mode-web/src/routes/_database-demo/api.db-snippets.ts @@ -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 () => { diff --git a/examples/ts-code-mode-web/src/routes/_database-demo/api.judge.ts b/examples/ts-code-mode-web/src/routes/_database-demo/api.judge.ts index 3b31b0a3e3..b231857928 100644 --- a/examples/ts-code-mode-web/src/routes/_database-demo/api.judge.ts +++ b/examples/ts-code-mode-web/src/routes/_database-demo/api.judge.ts @@ -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 }) => { diff --git a/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx b/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx index c4a05fab1e..d4d9c966e2 100644 --- a/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx +++ b/examples/ts-code-mode-web/src/routes/_database-demo/database-demo.tsx @@ -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, }) diff --git a/examples/ts-code-mode-web/src/routes/_execute-prompt/api.execute-prompt.ts b/examples/ts-code-mode-web/src/routes/_execute-prompt/api.execute-prompt.ts index f5c83f3f42..f49ecf5a4f 100644 --- a/examples/ts-code-mode-web/src/routes/_execute-prompt/api.execute-prompt.ts +++ b/examples/ts-code-mode-web/src/routes/_execute-prompt/api.execute-prompt.ts @@ -33,9 +33,7 @@ async function getDriver(): Promise { 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 }) => { diff --git a/examples/ts-code-mode-web/src/routes/_execute-prompt/api.realtime-token.ts b/examples/ts-code-mode-web/src/routes/_execute-prompt/api.realtime-token.ts index 9b4b38cafe..5b7ab96876 100644 --- a/examples/ts-code-mode-web/src/routes/_execute-prompt/api.realtime-token.ts +++ b/examples/ts-code-mode-web/src/routes/_execute-prompt/api.realtime-token.ts @@ -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 () => { diff --git a/examples/ts-code-mode-web/src/routes/_execute-prompt/execute-prompt.tsx b/examples/ts-code-mode-web/src/routes/_execute-prompt/execute-prompt.tsx index e44c07563d..d42bebb877 100644 --- a/examples/ts-code-mode-web/src/routes/_execute-prompt/execute-prompt.tsx +++ b/examples/ts-code-mode-web/src/routes/_execute-prompt/execute-prompt.tsx @@ -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, }) diff --git a/examples/ts-code-mode-web/src/routes/_reporting/api.invalidate.ts b/examples/ts-code-mode-web/src/routes/_reporting/api.invalidate.ts index f7100859e5..f473ebd017 100644 --- a/examples/ts-code-mode-web/src/routes/_reporting/api.invalidate.ts +++ b/examples/ts-code-mode-web/src/routes/_reporting/api.invalidate.ts @@ -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 }) => { diff --git a/examples/ts-code-mode-web/src/routes/_reporting/api.report-demo.ts b/examples/ts-code-mode-web/src/routes/_reporting/api.report-demo.ts index dcf599641b..2ef92fdd4d 100644 --- a/examples/ts-code-mode-web/src/routes/_reporting/api.report-demo.ts +++ b/examples/ts-code-mode-web/src/routes/_reporting/api.report-demo.ts @@ -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 }) => { diff --git a/examples/ts-code-mode-web/src/routes/_reporting/api.report-event.ts b/examples/ts-code-mode-web/src/routes/_reporting/api.report-event.ts index 90bf68b2f5..11d97742ee 100644 --- a/examples/ts-code-mode-web/src/routes/_reporting/api.report-event.ts +++ b/examples/ts-code-mode-web/src/routes/_reporting/api.report-event.ts @@ -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 }) => { diff --git a/examples/ts-code-mode-web/src/routes/_reporting/api.report-sse.ts b/examples/ts-code-mode-web/src/routes/_reporting/api.report-sse.ts index d53bf55080..97a429b400 100644 --- a/examples/ts-code-mode-web/src/routes/_reporting/api.report-sse.ts +++ b/examples/ts-code-mode-web/src/routes/_reporting/api.report-sse.ts @@ -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 }) => { diff --git a/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts b/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts index 4ff06ad390..4a64e80fa1 100644 --- a/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts +++ b/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts @@ -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 }) => { diff --git a/examples/ts-group-chat/package.json b/examples/ts-group-chat/package.json index 8b0e844330..66edb51124 100644 --- a/examples/ts-group-chat/package.json +++ b/examples/ts-group-chat/package.json @@ -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", diff --git a/examples/ts-react-chat/package.json b/examples/ts-react-chat/package.json index d2bfd2fcf6..797ed025b1 100644 --- a/examples/ts-react-chat/package.json +++ b/examples/ts-react-chat/package.json @@ -48,12 +48,12 @@ "@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/react-router": "^1.170.32", + "@tanstack/react-router-devtools": "^1.167.1", + "@tanstack/react-router-ssr-query": "^1.167.1", + "@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", diff --git a/examples/ts-react-chat/src/routeTree.gen.ts b/examples/ts-react-chat/src/routeTree.gen.ts index c4dd562270..4180f5d44f 100644 --- a/examples/ts-react-chat/src/routeTree.gen.ts +++ b/examples/ts-react-chat/src/routeTree.gen.ts @@ -9,127 +9,122 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' -import { Route as WebsocketChatRouteImport } from './routes/websocket-chat' -import { Route as TypesafeToolsRouteImport } from './routes/typesafe-tools' -import { Route as ThreadsRouteImport } from './routes/threads' -import { Route as ServerFnChatRouteImport } from './routes/server-fn-chat' -import { Route as SandboxesDurableRouteImport } from './routes/sandboxes-durable' -import { Route as SandboxesRouteImport } from './routes/sandboxes' -import { Route as ResumableRouteImport } from './routes/resumable' -import { Route as RepoReportRouteImport } from './routes/repo-report' -import { Route as RealtimeRouteImport } from './routes/realtime' -import { Route as QueueingRouteImport } from './routes/queueing' -import { Route as PersistentChatRouteImport } from './routes/persistent-chat' -import { Route as McpDemoRouteImport } from './routes/mcp-demo' -import { Route as McpAppsRouteImport } from './routes/mcp-apps' -import { Route as Issue176ToolResultRouteImport } from './routes/issue-176-tool-result' -import { Route as InterruptsRouteImport } from './routes/interrupts' -import { Route as ImageToolReproRouteImport } from './routes/image-tool-repro' -import { Route as ImageGenRouteImport } from './routes/image-gen' -import { Route as GenericInterruptsRouteImport } from './routes/generic-interrupts' -import { Route as GenerationHooksRouteImport } from './routes/generation-hooks' -import { Route as CapabilityDemoRouteImport } from './routes/capability-demo' -import { Route as AppStudioRouteImport } from './routes/app-studio' import { Route as IndexRouteImport } from './routes/index' -import { Route as GenerationsVideoRouteImport } from './routes/generations.video' -import { Route as GenerationsTranscriptionRouteImport } from './routes/generations.transcription' -import { Route as GenerationsSummarizeRouteImport } from './routes/generations.summarize' -import { Route as GenerationsStructuredOutputRouteImport } from './routes/generations.structured-output' -import { Route as GenerationsStructuredChatRouteImport } from './routes/generations.structured-chat' -import { Route as GenerationsSpeechRouteImport } from './routes/generations.speech' -import { Route as GenerationsPersistentGenerationRouteImport } from './routes/generations.persistent-generation' -import { Route as GenerationsOpenrouterCombinedRouteImport } from './routes/generations.openrouter-combined' -import { Route as GenerationsImageRouteImport } from './routes/generations.image' -import { Route as GenerationsAudioRouteImport } from './routes/generations.audio' -import { Route as ExampleRuntimeContextRouteImport } from './routes/example.runtime-context' -import { Route as ApiTranscribeRouteImport } from './routes/api.transcribe' -import { Route as ApiTanchatRouteImport } from './routes/api.tanchat' -import { Route as ApiSummarizeRouteImport } from './routes/api.summarize' -import { Route as ApiStructuredOutputRouteImport } from './routes/api.structured-output' -import { Route as ApiStructuredChatRouteImport } from './routes/api.structured-chat' -import { Route as ApiSandboxTriageDurableRouteImport } from './routes/api.sandbox-triage-durable' -import { Route as ApiSandboxTriageRouteImport } from './routes/api.sandbox-triage' -import { Route as ApiSandboxRepoReportRouteImport } from './routes/api.sandbox-repo-report' -import { Route as ApiResumableRouteImport } from './routes/api.resumable' -import { Route as ApiPersistentChatRouteImport } from './routes/api.persistent-chat' -import { Route as ApiOpenrouterCombinedRouteImport } from './routes/api.openrouter-combined' -import { Route as ApiMcpStatusRouteImport } from './routes/api.mcp-status' -import { Route as ApiMcpPoolRouteImport } from './routes/api.mcp-pool' -import { Route as ApiMcpManualRouteImport } from './routes/api.mcp-manual' -import { Route as ApiMcpChatRouteImport } from './routes/api.mcp-chat' -import { Route as ApiMcpAppsWeatherServerRouteImport } from './routes/api.mcp-apps-weather-server' -import { Route as ApiMcpAppsShopServerRouteImport } from './routes/api.mcp-apps-shop-server' -import { Route as ApiMcpAppsChatRouteImport } from './routes/api.mcp-apps-chat' -import { Route as ApiMcpAppsCallRouteImport } from './routes/api.mcp-apps-call' -import { Route as ApiInterruptsRouteImport } from './routes/api.interrupts' -import { Route as ApiImageToolReproRouteImport } from './routes/api.image-tool-repro' -import { Route as ApiImageGenRouteImport } from './routes/api.image-gen' -import { Route as ApiGenericInterruptsRouteImport } from './routes/api.generic-interrupts' -import { Route as ApiCapabilityDemoRouteImport } from './routes/api.capability-demo' -import { Route as ApiArtifactsRouteImport } from './routes/api.artifacts' -import { Route as ApiAppStudioForkRouteImport } from './routes/api.app-studio-fork' +import { Route as AppStudioRouteImport } from './routes/app-studio' +import { Route as CapabilityDemoRouteImport } from './routes/capability-demo' +import { Route as GenerationHooksRouteImport } from './routes/generation-hooks' +import { Route as GenericInterruptsRouteImport } from './routes/generic-interrupts' +import { Route as ImageGenRouteImport } from './routes/image-gen' +import { Route as ImageToolReproRouteImport } from './routes/image-tool-repro' +import { Route as InterruptsRouteImport } from './routes/interrupts' +import { Route as Issue176ToolResultRouteImport } from './routes/issue-176-tool-result' +import { Route as McpAppsRouteImport } from './routes/mcp-apps' +import { Route as McpDemoRouteImport } from './routes/mcp-demo' +import { Route as PersistentChatRouteImport } from './routes/persistent-chat' +import { Route as QueueingRouteImport } from './routes/queueing' +import { Route as RealtimeRouteImport } from './routes/realtime' +import { Route as RepoReportRouteImport } from './routes/repo-report' +import { Route as ResumableRouteImport } from './routes/resumable' +import { Route as SandboxesRouteImport } from './routes/sandboxes' +import { Route as SandboxesDurableRouteImport } from './routes/sandboxes-durable' +import { Route as ServerFnChatRouteImport } from './routes/server-fn-chat' +import { Route as ThreadsRouteImport } from './routes/threads' +import { Route as TypesafeToolsRouteImport } from './routes/typesafe-tools' +import { Route as WebsocketChatRouteImport } from './routes/websocket-chat' import { Route as ApiAppStudioRouteImport } from './routes/api.app-studio' +import { Route as ApiAppStudioForkRouteImport } from './routes/api.app-studio-fork' +import { Route as ApiArtifactsRouteImport } from './routes/api.artifacts' +import { Route as ApiCapabilityDemoRouteImport } from './routes/api.capability-demo' +import { Route as ApiGenericInterruptsRouteImport } from './routes/api.generic-interrupts' +import { Route as ApiImageGenRouteImport } from './routes/api.image-gen' +import { Route as ApiImageToolReproRouteImport } from './routes/api.image-tool-repro' +import { Route as ApiInterruptsRouteImport } from './routes/api.interrupts' +import { Route as ApiMcpAppsCallRouteImport } from './routes/api.mcp-apps-call' +import { Route as ApiMcpAppsChatRouteImport } from './routes/api.mcp-apps-chat' +import { Route as ApiMcpAppsShopServerRouteImport } from './routes/api.mcp-apps-shop-server' +import { Route as ApiMcpAppsWeatherServerRouteImport } from './routes/api.mcp-apps-weather-server' +import { Route as ApiMcpChatRouteImport } from './routes/api.mcp-chat' +import { Route as ApiMcpManualRouteImport } from './routes/api.mcp-manual' +import { Route as ApiMcpPoolRouteImport } from './routes/api.mcp-pool' +import { Route as ApiMcpStatusRouteImport } from './routes/api.mcp-status' +import { Route as ApiOpenrouterCombinedRouteImport } from './routes/api.openrouter-combined' +import { Route as ApiPersistentChatRouteImport } from './routes/api.persistent-chat' +import { Route as ApiResumableRouteImport } from './routes/api.resumable' +import { Route as ApiSandboxRepoReportRouteImport } from './routes/api.sandbox-repo-report' +import { Route as ApiSandboxTriageRouteImport } from './routes/api.sandbox-triage' +import { Route as ApiSandboxTriageDurableRouteImport } from './routes/api.sandbox-triage-durable' +import { Route as ApiStructuredChatRouteImport } from './routes/api.structured-chat' +import { Route as ApiStructuredOutputRouteImport } from './routes/api.structured-output' +import { Route as ApiSummarizeRouteImport } from './routes/api.summarize' +import { Route as ApiTanchatRouteImport } from './routes/api.tanchat' +import { Route as ApiTranscribeRouteImport } from './routes/api.transcribe' +import { Route as ExampleRuntimeContextRouteImport } from './routes/example.runtime-context' +import { Route as GenerationsAudioRouteImport } from './routes/generations.audio' +import { Route as GenerationsImageRouteImport } from './routes/generations.image' +import { Route as GenerationsOpenrouterCombinedRouteImport } from './routes/generations.openrouter-combined' +import { Route as GenerationsPersistentGenerationRouteImport } from './routes/generations.persistent-generation' +import { Route as GenerationsSpeechRouteImport } from './routes/generations.speech' +import { Route as GenerationsStructuredChatRouteImport } from './routes/generations.structured-chat' +import { Route as GenerationsStructuredOutputRouteImport } from './routes/generations.structured-output' +import { Route as GenerationsSummarizeRouteImport } from './routes/generations.summarize' +import { Route as GenerationsTranscriptionRouteImport } from './routes/generations.transcription' +import { Route as GenerationsVideoRouteImport } from './routes/generations.video' +import { Route as ApiGenerateAudioRouteImport } from './routes/api.generate.audio' +import { Route as ApiGenerateImageRouteImport } from './routes/api.generate.image' +import { Route as ApiGenerateSpeechRouteImport } from './routes/api.generate.speech' +import { Route as ApiGenerateVideoRouteImport } from './routes/api.generate.video' import { Route as ExampleGuitarsIndexRouteImport } from './routes/example.guitars/index' import { Route as ExampleGuitarsGuitarIdRouteImport } from './routes/example.guitars/$guitarId' -import { Route as ApiGenerateVideoRouteImport } from './routes/api.generate.video' -import { Route as ApiGenerateSpeechRouteImport } from './routes/api.generate.speech' -import { Route as ApiGenerateImageRouteImport } from './routes/api.generate.image' -import { Route as ApiGenerateAudioRouteImport } from './routes/api.generate.audio' import { Route as ApiGenerateImageArtifactRouteImport } from './routes/api.generate.image.artifact' -const WebsocketChatRoute = WebsocketChatRouteImport.update({ - id: '/websocket-chat', - path: '/websocket-chat', - getParentRoute: () => rootRouteImport, -} as any) -const TypesafeToolsRoute = TypesafeToolsRouteImport.update({ - id: '/typesafe-tools', - path: '/typesafe-tools', +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', getParentRoute: () => rootRouteImport, } as any) -const ThreadsRoute = ThreadsRouteImport.update({ - id: '/threads', - path: '/threads', +const AppStudioRoute = AppStudioRouteImport.update({ + id: '/app-studio', + path: '/app-studio', getParentRoute: () => rootRouteImport, } as any) -const ServerFnChatRoute = ServerFnChatRouteImport.update({ - id: '/server-fn-chat', - path: '/server-fn-chat', +const CapabilityDemoRoute = CapabilityDemoRouteImport.update({ + id: '/capability-demo', + path: '/capability-demo', getParentRoute: () => rootRouteImport, } as any) -const SandboxesDurableRoute = SandboxesDurableRouteImport.update({ - id: '/sandboxes-durable', - path: '/sandboxes-durable', +const GenerationHooksRoute = GenerationHooksRouteImport.update({ + id: '/generation-hooks', + path: '/generation-hooks', getParentRoute: () => rootRouteImport, } as any) -const SandboxesRoute = SandboxesRouteImport.update({ - id: '/sandboxes', - path: '/sandboxes', +const GenericInterruptsRoute = GenericInterruptsRouteImport.update({ + id: '/generic-interrupts', + path: '/generic-interrupts', getParentRoute: () => rootRouteImport, } as any) -const ResumableRoute = ResumableRouteImport.update({ - id: '/resumable', - path: '/resumable', +const ImageGenRoute = ImageGenRouteImport.update({ + id: '/image-gen', + path: '/image-gen', getParentRoute: () => rootRouteImport, } as any) -const RepoReportRoute = RepoReportRouteImport.update({ - id: '/repo-report', - path: '/repo-report', +const ImageToolReproRoute = ImageToolReproRouteImport.update({ + id: '/image-tool-repro', + path: '/image-tool-repro', getParentRoute: () => rootRouteImport, } as any) -const RealtimeRoute = RealtimeRouteImport.update({ - id: '/realtime', - path: '/realtime', +const InterruptsRoute = InterruptsRouteImport.update({ + id: '/interrupts', + path: '/interrupts', getParentRoute: () => rootRouteImport, } as any) -const QueueingRoute = QueueingRouteImport.update({ - id: '/queueing', - path: '/queueing', +const Issue176ToolResultRoute = Issue176ToolResultRouteImport.update({ + id: '/issue-176-tool-result', + path: '/issue-176-tool-result', getParentRoute: () => rootRouteImport, } as any) -const PersistentChatRoute = PersistentChatRouteImport.update({ - id: '/persistent-chat', - path: '/persistent-chat', +const McpAppsRoute = McpAppsRouteImport.update({ + id: '/mcp-apps', + path: '/mcp-apps', getParentRoute: () => rootRouteImport, } as any) const McpDemoRoute = McpDemoRouteImport.update({ @@ -137,169 +132,134 @@ const McpDemoRoute = McpDemoRouteImport.update({ path: '/mcp-demo', getParentRoute: () => rootRouteImport, } as any) -const McpAppsRoute = McpAppsRouteImport.update({ - id: '/mcp-apps', - path: '/mcp-apps', +const PersistentChatRoute = PersistentChatRouteImport.update({ + id: '/persistent-chat', + path: '/persistent-chat', getParentRoute: () => rootRouteImport, } as any) -const Issue176ToolResultRoute = Issue176ToolResultRouteImport.update({ - id: '/issue-176-tool-result', - path: '/issue-176-tool-result', +const QueueingRoute = QueueingRouteImport.update({ + id: '/queueing', + path: '/queueing', getParentRoute: () => rootRouteImport, } as any) -const InterruptsRoute = InterruptsRouteImport.update({ - id: '/interrupts', - path: '/interrupts', +const RealtimeRoute = RealtimeRouteImport.update({ + id: '/realtime', + path: '/realtime', getParentRoute: () => rootRouteImport, } as any) -const ImageToolReproRoute = ImageToolReproRouteImport.update({ - id: '/image-tool-repro', - path: '/image-tool-repro', +const RepoReportRoute = RepoReportRouteImport.update({ + id: '/repo-report', + path: '/repo-report', getParentRoute: () => rootRouteImport, } as any) -const ImageGenRoute = ImageGenRouteImport.update({ - id: '/image-gen', - path: '/image-gen', +const ResumableRoute = ResumableRouteImport.update({ + id: '/resumable', + path: '/resumable', getParentRoute: () => rootRouteImport, } as any) -const GenericInterruptsRoute = GenericInterruptsRouteImport.update({ - id: '/generic-interrupts', - path: '/generic-interrupts', +const SandboxesRoute = SandboxesRouteImport.update({ + id: '/sandboxes', + path: '/sandboxes', getParentRoute: () => rootRouteImport, } as any) -const GenerationHooksRoute = GenerationHooksRouteImport.update({ - id: '/generation-hooks', - path: '/generation-hooks', +const SandboxesDurableRoute = SandboxesDurableRouteImport.update({ + id: '/sandboxes-durable', + path: '/sandboxes-durable', getParentRoute: () => rootRouteImport, } as any) -const CapabilityDemoRoute = CapabilityDemoRouteImport.update({ - id: '/capability-demo', - path: '/capability-demo', +const ServerFnChatRoute = ServerFnChatRouteImport.update({ + id: '/server-fn-chat', + path: '/server-fn-chat', getParentRoute: () => rootRouteImport, } as any) -const AppStudioRoute = AppStudioRouteImport.update({ - id: '/app-studio', - path: '/app-studio', +const ThreadsRoute = ThreadsRouteImport.update({ + id: '/threads', + path: '/threads', getParentRoute: () => rootRouteImport, } as any) -const IndexRoute = IndexRouteImport.update({ - id: '/', - path: '/', +const TypesafeToolsRoute = TypesafeToolsRouteImport.update({ + id: '/typesafe-tools', + path: '/typesafe-tools', getParentRoute: () => rootRouteImport, } as any) -const GenerationsVideoRoute = GenerationsVideoRouteImport.update({ - id: '/generations/video', - path: '/generations/video', +const WebsocketChatRoute = WebsocketChatRouteImport.update({ + id: '/websocket-chat', + path: '/websocket-chat', getParentRoute: () => rootRouteImport, } as any) -const GenerationsTranscriptionRoute = - GenerationsTranscriptionRouteImport.update({ - id: '/generations/transcription', - path: '/generations/transcription', - getParentRoute: () => rootRouteImport, - } as any) -const GenerationsSummarizeRoute = GenerationsSummarizeRouteImport.update({ - id: '/generations/summarize', - path: '/generations/summarize', - getParentRoute: () => rootRouteImport, -} as any) -const GenerationsStructuredOutputRoute = - GenerationsStructuredOutputRouteImport.update({ - id: '/generations/structured-output', - path: '/generations/structured-output', - getParentRoute: () => rootRouteImport, - } as any) -const GenerationsStructuredChatRoute = - GenerationsStructuredChatRouteImport.update({ - id: '/generations/structured-chat', - path: '/generations/structured-chat', - getParentRoute: () => rootRouteImport, - } as any) -const GenerationsSpeechRoute = GenerationsSpeechRouteImport.update({ - id: '/generations/speech', - path: '/generations/speech', +const ApiAppStudioRoute = ApiAppStudioRouteImport.update({ + id: '/api/app-studio', + path: '/api/app-studio', getParentRoute: () => rootRouteImport, } as any) -const GenerationsPersistentGenerationRoute = - GenerationsPersistentGenerationRouteImport.update({ - id: '/generations/persistent-generation', - path: '/generations/persistent-generation', - getParentRoute: () => rootRouteImport, - } as any) -const GenerationsOpenrouterCombinedRoute = - GenerationsOpenrouterCombinedRouteImport.update({ - id: '/generations/openrouter-combined', - path: '/generations/openrouter-combined', - getParentRoute: () => rootRouteImport, - } as any) -const GenerationsImageRoute = GenerationsImageRouteImport.update({ - id: '/generations/image', - path: '/generations/image', +const ApiAppStudioForkRoute = ApiAppStudioForkRouteImport.update({ + id: '/api/app-studio-fork', + path: '/api/app-studio-fork', getParentRoute: () => rootRouteImport, } as any) -const GenerationsAudioRoute = GenerationsAudioRouteImport.update({ - id: '/generations/audio', - path: '/generations/audio', +const ApiArtifactsRoute = ApiArtifactsRouteImport.update({ + id: '/api/artifacts', + path: '/api/artifacts', getParentRoute: () => rootRouteImport, } as any) -const ExampleRuntimeContextRoute = ExampleRuntimeContextRouteImport.update({ - id: '/example/runtime-context', - path: '/example/runtime-context', +const ApiCapabilityDemoRoute = ApiCapabilityDemoRouteImport.update({ + id: '/api/capability-demo', + path: '/api/capability-demo', getParentRoute: () => rootRouteImport, } as any) -const ApiTranscribeRoute = ApiTranscribeRouteImport.update({ - id: '/api/transcribe', - path: '/api/transcribe', +const ApiGenericInterruptsRoute = ApiGenericInterruptsRouteImport.update({ + id: '/api/generic-interrupts', + path: '/api/generic-interrupts', getParentRoute: () => rootRouteImport, } as any) -const ApiTanchatRoute = ApiTanchatRouteImport.update({ - id: '/api/tanchat', - path: '/api/tanchat', +const ApiImageGenRoute = ApiImageGenRouteImport.update({ + id: '/api/image-gen', + path: '/api/image-gen', getParentRoute: () => rootRouteImport, } as any) -const ApiSummarizeRoute = ApiSummarizeRouteImport.update({ - id: '/api/summarize', - path: '/api/summarize', +const ApiImageToolReproRoute = ApiImageToolReproRouteImport.update({ + id: '/api/image-tool-repro', + path: '/api/image-tool-repro', getParentRoute: () => rootRouteImport, } as any) -const ApiStructuredOutputRoute = ApiStructuredOutputRouteImport.update({ - id: '/api/structured-output', - path: '/api/structured-output', +const ApiInterruptsRoute = ApiInterruptsRouteImport.update({ + id: '/api/interrupts', + path: '/api/interrupts', getParentRoute: () => rootRouteImport, } as any) -const ApiStructuredChatRoute = ApiStructuredChatRouteImport.update({ - id: '/api/structured-chat', - path: '/api/structured-chat', +const ApiMcpAppsCallRoute = ApiMcpAppsCallRouteImport.update({ + id: '/api/mcp-apps-call', + path: '/api/mcp-apps-call', getParentRoute: () => rootRouteImport, } as any) -const ApiSandboxTriageDurableRoute = ApiSandboxTriageDurableRouteImport.update({ - id: '/api/sandbox-triage-durable', - path: '/api/sandbox-triage-durable', +const ApiMcpAppsChatRoute = ApiMcpAppsChatRouteImport.update({ + id: '/api/mcp-apps-chat', + path: '/api/mcp-apps-chat', getParentRoute: () => rootRouteImport, } as any) -const ApiSandboxTriageRoute = ApiSandboxTriageRouteImport.update({ - id: '/api/sandbox-triage', - path: '/api/sandbox-triage', +const ApiMcpAppsShopServerRoute = ApiMcpAppsShopServerRouteImport.update({ + id: '/api/mcp-apps-shop-server', + path: '/api/mcp-apps-shop-server', getParentRoute: () => rootRouteImport, } as any) -const ApiSandboxRepoReportRoute = ApiSandboxRepoReportRouteImport.update({ - id: '/api/sandbox-repo-report', - path: '/api/sandbox-repo-report', +const ApiMcpAppsWeatherServerRoute = ApiMcpAppsWeatherServerRouteImport.update({ + id: '/api/mcp-apps-weather-server', + path: '/api/mcp-apps-weather-server', getParentRoute: () => rootRouteImport, } as any) -const ApiResumableRoute = ApiResumableRouteImport.update({ - id: '/api/resumable', - path: '/api/resumable', +const ApiMcpChatRoute = ApiMcpChatRouteImport.update({ + id: '/api/mcp-chat', + path: '/api/mcp-chat', getParentRoute: () => rootRouteImport, } as any) -const ApiPersistentChatRoute = ApiPersistentChatRouteImport.update({ - id: '/api/persistent-chat', - path: '/api/persistent-chat', +const ApiMcpManualRoute = ApiMcpManualRouteImport.update({ + id: '/api/mcp-manual', + path: '/api/mcp-manual', getParentRoute: () => rootRouteImport, } as any) -const ApiOpenrouterCombinedRoute = ApiOpenrouterCombinedRouteImport.update({ - id: '/api/openrouter-combined', - path: '/api/openrouter-combined', +const ApiMcpPoolRoute = ApiMcpPoolRouteImport.update({ + id: '/api/mcp-pool', + path: '/api/mcp-pool', getParentRoute: () => rootRouteImport, } as any) const ApiMcpStatusRoute = ApiMcpStatusRouteImport.update({ @@ -307,94 +267,129 @@ const ApiMcpStatusRoute = ApiMcpStatusRouteImport.update({ path: '/api/mcp-status', getParentRoute: () => rootRouteImport, } as any) -const ApiMcpPoolRoute = ApiMcpPoolRouteImport.update({ - id: '/api/mcp-pool', - path: '/api/mcp-pool', +const ApiOpenrouterCombinedRoute = ApiOpenrouterCombinedRouteImport.update({ + id: '/api/openrouter-combined', + path: '/api/openrouter-combined', getParentRoute: () => rootRouteImport, } as any) -const ApiMcpManualRoute = ApiMcpManualRouteImport.update({ - id: '/api/mcp-manual', - path: '/api/mcp-manual', +const ApiPersistentChatRoute = ApiPersistentChatRouteImport.update({ + id: '/api/persistent-chat', + path: '/api/persistent-chat', getParentRoute: () => rootRouteImport, } as any) -const ApiMcpChatRoute = ApiMcpChatRouteImport.update({ - id: '/api/mcp-chat', - path: '/api/mcp-chat', +const ApiResumableRoute = ApiResumableRouteImport.update({ + id: '/api/resumable', + path: '/api/resumable', getParentRoute: () => rootRouteImport, } as any) -const ApiMcpAppsWeatherServerRoute = ApiMcpAppsWeatherServerRouteImport.update({ - id: '/api/mcp-apps-weather-server', - path: '/api/mcp-apps-weather-server', +const ApiSandboxRepoReportRoute = ApiSandboxRepoReportRouteImport.update({ + id: '/api/sandbox-repo-report', + path: '/api/sandbox-repo-report', getParentRoute: () => rootRouteImport, } as any) -const ApiMcpAppsShopServerRoute = ApiMcpAppsShopServerRouteImport.update({ - id: '/api/mcp-apps-shop-server', - path: '/api/mcp-apps-shop-server', +const ApiSandboxTriageRoute = ApiSandboxTriageRouteImport.update({ + id: '/api/sandbox-triage', + path: '/api/sandbox-triage', getParentRoute: () => rootRouteImport, } as any) -const ApiMcpAppsChatRoute = ApiMcpAppsChatRouteImport.update({ - id: '/api/mcp-apps-chat', - path: '/api/mcp-apps-chat', +const ApiSandboxTriageDurableRoute = ApiSandboxTriageDurableRouteImport.update({ + id: '/api/sandbox-triage-durable', + path: '/api/sandbox-triage-durable', getParentRoute: () => rootRouteImport, } as any) -const ApiMcpAppsCallRoute = ApiMcpAppsCallRouteImport.update({ - id: '/api/mcp-apps-call', - path: '/api/mcp-apps-call', +const ApiStructuredChatRoute = ApiStructuredChatRouteImport.update({ + id: '/api/structured-chat', + path: '/api/structured-chat', getParentRoute: () => rootRouteImport, } as any) -const ApiInterruptsRoute = ApiInterruptsRouteImport.update({ - id: '/api/interrupts', - path: '/api/interrupts', +const ApiStructuredOutputRoute = ApiStructuredOutputRouteImport.update({ + id: '/api/structured-output', + path: '/api/structured-output', getParentRoute: () => rootRouteImport, } as any) -const ApiImageToolReproRoute = ApiImageToolReproRouteImport.update({ - id: '/api/image-tool-repro', - path: '/api/image-tool-repro', +const ApiSummarizeRoute = ApiSummarizeRouteImport.update({ + id: '/api/summarize', + path: '/api/summarize', getParentRoute: () => rootRouteImport, } as any) -const ApiImageGenRoute = ApiImageGenRouteImport.update({ - id: '/api/image-gen', - path: '/api/image-gen', +const ApiTanchatRoute = ApiTanchatRouteImport.update({ + id: '/api/tanchat', + path: '/api/tanchat', getParentRoute: () => rootRouteImport, } as any) -const ApiGenericInterruptsRoute = ApiGenericInterruptsRouteImport.update({ - id: '/api/generic-interrupts', - path: '/api/generic-interrupts', +const ApiTranscribeRoute = ApiTranscribeRouteImport.update({ + id: '/api/transcribe', + path: '/api/transcribe', getParentRoute: () => rootRouteImport, } as any) -const ApiCapabilityDemoRoute = ApiCapabilityDemoRouteImport.update({ - id: '/api/capability-demo', - path: '/api/capability-demo', +const ExampleRuntimeContextRoute = ExampleRuntimeContextRouteImport.update({ + id: '/example/runtime-context', + path: '/example/runtime-context', getParentRoute: () => rootRouteImport, } as any) -const ApiArtifactsRoute = ApiArtifactsRouteImport.update({ - id: '/api/artifacts', - path: '/api/artifacts', +const GenerationsAudioRoute = GenerationsAudioRouteImport.update({ + id: '/generations/audio', + path: '/generations/audio', getParentRoute: () => rootRouteImport, } as any) -const ApiAppStudioForkRoute = ApiAppStudioForkRouteImport.update({ - id: '/api/app-studio-fork', - path: '/api/app-studio-fork', +const GenerationsImageRoute = GenerationsImageRouteImport.update({ + id: '/generations/image', + path: '/generations/image', getParentRoute: () => rootRouteImport, } as any) -const ApiAppStudioRoute = ApiAppStudioRouteImport.update({ - id: '/api/app-studio', - path: '/api/app-studio', +const GenerationsOpenrouterCombinedRoute = + GenerationsOpenrouterCombinedRouteImport.update({ + id: '/generations/openrouter-combined', + path: '/generations/openrouter-combined', + getParentRoute: () => rootRouteImport, + } as any) +const GenerationsPersistentGenerationRoute = + GenerationsPersistentGenerationRouteImport.update({ + id: '/generations/persistent-generation', + path: '/generations/persistent-generation', + getParentRoute: () => rootRouteImport, + } as any) +const GenerationsSpeechRoute = GenerationsSpeechRouteImport.update({ + id: '/generations/speech', + path: '/generations/speech', getParentRoute: () => rootRouteImport, } as any) -const ExampleGuitarsIndexRoute = ExampleGuitarsIndexRouteImport.update({ - id: '/example/guitars/', - path: '/example/guitars/', +const GenerationsStructuredChatRoute = + GenerationsStructuredChatRouteImport.update({ + id: '/generations/structured-chat', + path: '/generations/structured-chat', + getParentRoute: () => rootRouteImport, + } as any) +const GenerationsStructuredOutputRoute = + GenerationsStructuredOutputRouteImport.update({ + id: '/generations/structured-output', + path: '/generations/structured-output', + getParentRoute: () => rootRouteImport, + } as any) +const GenerationsSummarizeRoute = GenerationsSummarizeRouteImport.update({ + id: '/generations/summarize', + path: '/generations/summarize', getParentRoute: () => rootRouteImport, } as any) -const ExampleGuitarsGuitarIdRoute = ExampleGuitarsGuitarIdRouteImport.update({ - id: '/example/guitars/$guitarId', - path: '/example/guitars/$guitarId', +const GenerationsTranscriptionRoute = + GenerationsTranscriptionRouteImport.update({ + id: '/generations/transcription', + path: '/generations/transcription', + getParentRoute: () => rootRouteImport, + } as any) +const GenerationsVideoRoute = GenerationsVideoRouteImport.update({ + id: '/generations/video', + path: '/generations/video', getParentRoute: () => rootRouteImport, } as any) -const ApiGenerateVideoRoute = ApiGenerateVideoRouteImport.update({ - id: '/api/generate/video', - path: '/api/generate/video', +const ApiGenerateAudioRoute = ApiGenerateAudioRouteImport.update({ + id: '/api/generate/audio', + path: '/api/generate/audio', + getParentRoute: () => rootRouteImport, +} as any) +const ApiGenerateImageRoute = ApiGenerateImageRouteImport.update({ + id: '/api/generate/image', + path: '/api/generate/image', getParentRoute: () => rootRouteImport, } as any) const ApiGenerateSpeechRoute = ApiGenerateSpeechRouteImport.update({ @@ -402,14 +397,19 @@ const ApiGenerateSpeechRoute = ApiGenerateSpeechRouteImport.update({ path: '/api/generate/speech', getParentRoute: () => rootRouteImport, } as any) -const ApiGenerateImageRoute = ApiGenerateImageRouteImport.update({ - id: '/api/generate/image', - path: '/api/generate/image', +const ApiGenerateVideoRoute = ApiGenerateVideoRouteImport.update({ + id: '/api/generate/video', + path: '/api/generate/video', getParentRoute: () => rootRouteImport, } as any) -const ApiGenerateAudioRoute = ApiGenerateAudioRouteImport.update({ - id: '/api/generate/audio', - path: '/api/generate/audio', +const ExampleGuitarsIndexRoute = ExampleGuitarsIndexRouteImport.update({ + id: '/example/guitars/', + path: '/example/guitars/', + getParentRoute: () => rootRouteImport, +} as any) +const ExampleGuitarsGuitarIdRoute = ExampleGuitarsGuitarIdRouteImport.update({ + id: '/example/guitars/$guitarId', + path: '/example/guitars/$guitarId', getParentRoute: () => rootRouteImport, } as any) const ApiGenerateImageArtifactRoute = @@ -908,74 +908,81 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/websocket-chat': { - id: '/websocket-chat' - path: '/websocket-chat' - fullPath: '/websocket-chat' - preLoaderRoute: typeof WebsocketChatRouteImport + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } - '/typesafe-tools': { - id: '/typesafe-tools' - path: '/typesafe-tools' - fullPath: '/typesafe-tools' - preLoaderRoute: typeof TypesafeToolsRouteImport + '/app-studio': { + id: '/app-studio' + path: '/app-studio' + fullPath: '/app-studio' + preLoaderRoute: typeof AppStudioRouteImport parentRoute: typeof rootRouteImport } - '/threads': { - id: '/threads' - path: '/threads' - fullPath: '/threads' - preLoaderRoute: typeof ThreadsRouteImport + '/capability-demo': { + id: '/capability-demo' + path: '/capability-demo' + fullPath: '/capability-demo' + preLoaderRoute: typeof CapabilityDemoRouteImport parentRoute: typeof rootRouteImport } - '/server-fn-chat': { - id: '/server-fn-chat' - path: '/server-fn-chat' - fullPath: '/server-fn-chat' - preLoaderRoute: typeof ServerFnChatRouteImport + '/generation-hooks': { + id: '/generation-hooks' + path: '/generation-hooks' + fullPath: '/generation-hooks' + preLoaderRoute: typeof GenerationHooksRouteImport parentRoute: typeof rootRouteImport } - '/sandboxes-durable': { - id: '/sandboxes-durable' - path: '/sandboxes-durable' - fullPath: '/sandboxes-durable' - preLoaderRoute: typeof SandboxesDurableRouteImport + '/generic-interrupts': { + id: '/generic-interrupts' + path: '/generic-interrupts' + fullPath: '/generic-interrupts' + preLoaderRoute: typeof GenericInterruptsRouteImport parentRoute: typeof rootRouteImport } - '/sandboxes': { - id: '/sandboxes' - path: '/sandboxes' - fullPath: '/sandboxes' - preLoaderRoute: typeof SandboxesRouteImport + '/image-gen': { + id: '/image-gen' + path: '/image-gen' + fullPath: '/image-gen' + preLoaderRoute: typeof ImageGenRouteImport parentRoute: typeof rootRouteImport } - '/resumable': { - id: '/resumable' - path: '/resumable' - fullPath: '/resumable' - preLoaderRoute: typeof ResumableRouteImport + '/image-tool-repro': { + id: '/image-tool-repro' + path: '/image-tool-repro' + fullPath: '/image-tool-repro' + preLoaderRoute: typeof ImageToolReproRouteImport parentRoute: typeof rootRouteImport } - '/repo-report': { - id: '/repo-report' - path: '/repo-report' - fullPath: '/repo-report' - preLoaderRoute: typeof RepoReportRouteImport + '/interrupts': { + id: '/interrupts' + path: '/interrupts' + fullPath: '/interrupts' + preLoaderRoute: typeof InterruptsRouteImport parentRoute: typeof rootRouteImport } - '/realtime': { - id: '/realtime' - path: '/realtime' - fullPath: '/realtime' - preLoaderRoute: typeof RealtimeRouteImport + '/issue-176-tool-result': { + id: '/issue-176-tool-result' + path: '/issue-176-tool-result' + fullPath: '/issue-176-tool-result' + preLoaderRoute: typeof Issue176ToolResultRouteImport parentRoute: typeof rootRouteImport } - '/queueing': { - id: '/queueing' - path: '/queueing' - fullPath: '/queueing' - preLoaderRoute: typeof QueueingRouteImport + '/mcp-apps': { + id: '/mcp-apps' + path: '/mcp-apps' + fullPath: '/mcp-apps' + preLoaderRoute: typeof McpAppsRouteImport + parentRoute: typeof rootRouteImport + } + '/mcp-demo': { + id: '/mcp-demo' + path: '/mcp-demo' + fullPath: '/mcp-demo' + preLoaderRoute: typeof McpDemoRouteImport parentRoute: typeof rootRouteImport } '/persistent-chat': { @@ -985,200 +992,214 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PersistentChatRouteImport parentRoute: typeof rootRouteImport } - '/mcp-demo': { - id: '/mcp-demo' - path: '/mcp-demo' - fullPath: '/mcp-demo' - preLoaderRoute: typeof McpDemoRouteImport + '/queueing': { + id: '/queueing' + path: '/queueing' + fullPath: '/queueing' + preLoaderRoute: typeof QueueingRouteImport parentRoute: typeof rootRouteImport } - '/mcp-apps': { - id: '/mcp-apps' - path: '/mcp-apps' - fullPath: '/mcp-apps' - preLoaderRoute: typeof McpAppsRouteImport + '/realtime': { + id: '/realtime' + path: '/realtime' + fullPath: '/realtime' + preLoaderRoute: typeof RealtimeRouteImport parentRoute: typeof rootRouteImport } - '/issue-176-tool-result': { - id: '/issue-176-tool-result' - path: '/issue-176-tool-result' - fullPath: '/issue-176-tool-result' - preLoaderRoute: typeof Issue176ToolResultRouteImport + '/repo-report': { + id: '/repo-report' + path: '/repo-report' + fullPath: '/repo-report' + preLoaderRoute: typeof RepoReportRouteImport parentRoute: typeof rootRouteImport } - '/interrupts': { - id: '/interrupts' - path: '/interrupts' - fullPath: '/interrupts' - preLoaderRoute: typeof InterruptsRouteImport + '/resumable': { + id: '/resumable' + path: '/resumable' + fullPath: '/resumable' + preLoaderRoute: typeof ResumableRouteImport parentRoute: typeof rootRouteImport } - '/image-tool-repro': { - id: '/image-tool-repro' - path: '/image-tool-repro' - fullPath: '/image-tool-repro' - preLoaderRoute: typeof ImageToolReproRouteImport + '/sandboxes': { + id: '/sandboxes' + path: '/sandboxes' + fullPath: '/sandboxes' + preLoaderRoute: typeof SandboxesRouteImport parentRoute: typeof rootRouteImport } - '/image-gen': { - id: '/image-gen' - path: '/image-gen' - fullPath: '/image-gen' - preLoaderRoute: typeof ImageGenRouteImport + '/sandboxes-durable': { + id: '/sandboxes-durable' + path: '/sandboxes-durable' + fullPath: '/sandboxes-durable' + preLoaderRoute: typeof SandboxesDurableRouteImport parentRoute: typeof rootRouteImport } - '/generic-interrupts': { - id: '/generic-interrupts' - path: '/generic-interrupts' - fullPath: '/generic-interrupts' - preLoaderRoute: typeof GenericInterruptsRouteImport + '/server-fn-chat': { + id: '/server-fn-chat' + path: '/server-fn-chat' + fullPath: '/server-fn-chat' + preLoaderRoute: typeof ServerFnChatRouteImport parentRoute: typeof rootRouteImport } - '/generation-hooks': { - id: '/generation-hooks' - path: '/generation-hooks' - fullPath: '/generation-hooks' - preLoaderRoute: typeof GenerationHooksRouteImport + '/threads': { + id: '/threads' + path: '/threads' + fullPath: '/threads' + preLoaderRoute: typeof ThreadsRouteImport parentRoute: typeof rootRouteImport } - '/capability-demo': { - id: '/capability-demo' - path: '/capability-demo' - fullPath: '/capability-demo' - preLoaderRoute: typeof CapabilityDemoRouteImport + '/typesafe-tools': { + id: '/typesafe-tools' + path: '/typesafe-tools' + fullPath: '/typesafe-tools' + preLoaderRoute: typeof TypesafeToolsRouteImport parentRoute: typeof rootRouteImport } - '/app-studio': { - id: '/app-studio' - path: '/app-studio' - fullPath: '/app-studio' - preLoaderRoute: typeof AppStudioRouteImport + '/websocket-chat': { + id: '/websocket-chat' + path: '/websocket-chat' + fullPath: '/websocket-chat' + preLoaderRoute: typeof WebsocketChatRouteImport parentRoute: typeof rootRouteImport } - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexRouteImport + '/api/app-studio': { + id: '/api/app-studio' + path: '/api/app-studio' + fullPath: '/api/app-studio' + preLoaderRoute: typeof ApiAppStudioRouteImport parentRoute: typeof rootRouteImport } - '/generations/video': { - id: '/generations/video' - path: '/generations/video' - fullPath: '/generations/video' - preLoaderRoute: typeof GenerationsVideoRouteImport + '/api/app-studio-fork': { + id: '/api/app-studio-fork' + path: '/api/app-studio-fork' + fullPath: '/api/app-studio-fork' + preLoaderRoute: typeof ApiAppStudioForkRouteImport parentRoute: typeof rootRouteImport } - '/generations/transcription': { - id: '/generations/transcription' - path: '/generations/transcription' - fullPath: '/generations/transcription' - preLoaderRoute: typeof GenerationsTranscriptionRouteImport + '/api/artifacts': { + id: '/api/artifacts' + path: '/api/artifacts' + fullPath: '/api/artifacts' + preLoaderRoute: typeof ApiArtifactsRouteImport parentRoute: typeof rootRouteImport } - '/generations/summarize': { - id: '/generations/summarize' - path: '/generations/summarize' - fullPath: '/generations/summarize' - preLoaderRoute: typeof GenerationsSummarizeRouteImport + '/api/capability-demo': { + id: '/api/capability-demo' + path: '/api/capability-demo' + fullPath: '/api/capability-demo' + preLoaderRoute: typeof ApiCapabilityDemoRouteImport parentRoute: typeof rootRouteImport } - '/generations/structured-output': { - id: '/generations/structured-output' - path: '/generations/structured-output' - fullPath: '/generations/structured-output' - preLoaderRoute: typeof GenerationsStructuredOutputRouteImport + '/api/generic-interrupts': { + id: '/api/generic-interrupts' + path: '/api/generic-interrupts' + fullPath: '/api/generic-interrupts' + preLoaderRoute: typeof ApiGenericInterruptsRouteImport parentRoute: typeof rootRouteImport } - '/generations/structured-chat': { - id: '/generations/structured-chat' - path: '/generations/structured-chat' - fullPath: '/generations/structured-chat' - preLoaderRoute: typeof GenerationsStructuredChatRouteImport + '/api/image-gen': { + id: '/api/image-gen' + path: '/api/image-gen' + fullPath: '/api/image-gen' + preLoaderRoute: typeof ApiImageGenRouteImport parentRoute: typeof rootRouteImport } - '/generations/speech': { - id: '/generations/speech' - path: '/generations/speech' - fullPath: '/generations/speech' - preLoaderRoute: typeof GenerationsSpeechRouteImport + '/api/image-tool-repro': { + id: '/api/image-tool-repro' + path: '/api/image-tool-repro' + fullPath: '/api/image-tool-repro' + preLoaderRoute: typeof ApiImageToolReproRouteImport parentRoute: typeof rootRouteImport } - '/generations/persistent-generation': { - id: '/generations/persistent-generation' - path: '/generations/persistent-generation' - fullPath: '/generations/persistent-generation' - preLoaderRoute: typeof GenerationsPersistentGenerationRouteImport + '/api/interrupts': { + id: '/api/interrupts' + path: '/api/interrupts' + fullPath: '/api/interrupts' + preLoaderRoute: typeof ApiInterruptsRouteImport parentRoute: typeof rootRouteImport } - '/generations/openrouter-combined': { - id: '/generations/openrouter-combined' - path: '/generations/openrouter-combined' - fullPath: '/generations/openrouter-combined' - preLoaderRoute: typeof GenerationsOpenrouterCombinedRouteImport + '/api/mcp-apps-call': { + id: '/api/mcp-apps-call' + path: '/api/mcp-apps-call' + fullPath: '/api/mcp-apps-call' + preLoaderRoute: typeof ApiMcpAppsCallRouteImport parentRoute: typeof rootRouteImport } - '/generations/image': { - id: '/generations/image' - path: '/generations/image' - fullPath: '/generations/image' - preLoaderRoute: typeof GenerationsImageRouteImport + '/api/mcp-apps-chat': { + id: '/api/mcp-apps-chat' + path: '/api/mcp-apps-chat' + fullPath: '/api/mcp-apps-chat' + preLoaderRoute: typeof ApiMcpAppsChatRouteImport parentRoute: typeof rootRouteImport } - '/generations/audio': { - id: '/generations/audio' - path: '/generations/audio' - fullPath: '/generations/audio' - preLoaderRoute: typeof GenerationsAudioRouteImport + '/api/mcp-apps-shop-server': { + id: '/api/mcp-apps-shop-server' + path: '/api/mcp-apps-shop-server' + fullPath: '/api/mcp-apps-shop-server' + preLoaderRoute: typeof ApiMcpAppsShopServerRouteImport parentRoute: typeof rootRouteImport } - '/example/runtime-context': { - id: '/example/runtime-context' - path: '/example/runtime-context' - fullPath: '/example/runtime-context' - preLoaderRoute: typeof ExampleRuntimeContextRouteImport + '/api/mcp-apps-weather-server': { + id: '/api/mcp-apps-weather-server' + path: '/api/mcp-apps-weather-server' + fullPath: '/api/mcp-apps-weather-server' + preLoaderRoute: typeof ApiMcpAppsWeatherServerRouteImport parentRoute: typeof rootRouteImport } - '/api/transcribe': { - id: '/api/transcribe' - path: '/api/transcribe' - fullPath: '/api/transcribe' - preLoaderRoute: typeof ApiTranscribeRouteImport + '/api/mcp-chat': { + id: '/api/mcp-chat' + path: '/api/mcp-chat' + fullPath: '/api/mcp-chat' + preLoaderRoute: typeof ApiMcpChatRouteImport parentRoute: typeof rootRouteImport } - '/api/tanchat': { - id: '/api/tanchat' - path: '/api/tanchat' - fullPath: '/api/tanchat' - preLoaderRoute: typeof ApiTanchatRouteImport + '/api/mcp-manual': { + id: '/api/mcp-manual' + path: '/api/mcp-manual' + fullPath: '/api/mcp-manual' + preLoaderRoute: typeof ApiMcpManualRouteImport parentRoute: typeof rootRouteImport } - '/api/summarize': { - id: '/api/summarize' - path: '/api/summarize' - fullPath: '/api/summarize' - preLoaderRoute: typeof ApiSummarizeRouteImport + '/api/mcp-pool': { + id: '/api/mcp-pool' + path: '/api/mcp-pool' + fullPath: '/api/mcp-pool' + preLoaderRoute: typeof ApiMcpPoolRouteImport parentRoute: typeof rootRouteImport } - '/api/structured-output': { - id: '/api/structured-output' - path: '/api/structured-output' - fullPath: '/api/structured-output' - preLoaderRoute: typeof ApiStructuredOutputRouteImport + '/api/mcp-status': { + id: '/api/mcp-status' + path: '/api/mcp-status' + fullPath: '/api/mcp-status' + preLoaderRoute: typeof ApiMcpStatusRouteImport + parentRoute: typeof rootRouteImport + } + '/api/openrouter-combined': { + id: '/api/openrouter-combined' + path: '/api/openrouter-combined' + fullPath: '/api/openrouter-combined' + preLoaderRoute: typeof ApiOpenrouterCombinedRouteImport + parentRoute: typeof rootRouteImport + } + '/api/persistent-chat': { + id: '/api/persistent-chat' + path: '/api/persistent-chat' + fullPath: '/api/persistent-chat' + preLoaderRoute: typeof ApiPersistentChatRouteImport parentRoute: typeof rootRouteImport } - '/api/structured-chat': { - id: '/api/structured-chat' - path: '/api/structured-chat' - fullPath: '/api/structured-chat' - preLoaderRoute: typeof ApiStructuredChatRouteImport + '/api/resumable': { + id: '/api/resumable' + path: '/api/resumable' + fullPath: '/api/resumable' + preLoaderRoute: typeof ApiResumableRouteImport parentRoute: typeof rootRouteImport } - '/api/sandbox-triage-durable': { - id: '/api/sandbox-triage-durable' - path: '/api/sandbox-triage-durable' - fullPath: '/api/sandbox-triage-durable' - preLoaderRoute: typeof ApiSandboxTriageDurableRouteImport + '/api/sandbox-repo-report': { + id: '/api/sandbox-repo-report' + path: '/api/sandbox-repo-report' + fullPath: '/api/sandbox-repo-report' + preLoaderRoute: typeof ApiSandboxRepoReportRouteImport parentRoute: typeof rootRouteImport } '/api/sandbox-triage': { @@ -1188,144 +1209,151 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiSandboxTriageRouteImport parentRoute: typeof rootRouteImport } - '/api/sandbox-repo-report': { - id: '/api/sandbox-repo-report' - path: '/api/sandbox-repo-report' - fullPath: '/api/sandbox-repo-report' - preLoaderRoute: typeof ApiSandboxRepoReportRouteImport + '/api/sandbox-triage-durable': { + id: '/api/sandbox-triage-durable' + path: '/api/sandbox-triage-durable' + fullPath: '/api/sandbox-triage-durable' + preLoaderRoute: typeof ApiSandboxTriageDurableRouteImport parentRoute: typeof rootRouteImport } - '/api/resumable': { - id: '/api/resumable' - path: '/api/resumable' - fullPath: '/api/resumable' - preLoaderRoute: typeof ApiResumableRouteImport + '/api/structured-chat': { + id: '/api/structured-chat' + path: '/api/structured-chat' + fullPath: '/api/structured-chat' + preLoaderRoute: typeof ApiStructuredChatRouteImport parentRoute: typeof rootRouteImport } - '/api/persistent-chat': { - id: '/api/persistent-chat' - path: '/api/persistent-chat' - fullPath: '/api/persistent-chat' - preLoaderRoute: typeof ApiPersistentChatRouteImport + '/api/structured-output': { + id: '/api/structured-output' + path: '/api/structured-output' + fullPath: '/api/structured-output' + preLoaderRoute: typeof ApiStructuredOutputRouteImport parentRoute: typeof rootRouteImport } - '/api/openrouter-combined': { - id: '/api/openrouter-combined' - path: '/api/openrouter-combined' - fullPath: '/api/openrouter-combined' - preLoaderRoute: typeof ApiOpenrouterCombinedRouteImport + '/api/summarize': { + id: '/api/summarize' + path: '/api/summarize' + fullPath: '/api/summarize' + preLoaderRoute: typeof ApiSummarizeRouteImport parentRoute: typeof rootRouteImport } - '/api/mcp-status': { - id: '/api/mcp-status' - path: '/api/mcp-status' - fullPath: '/api/mcp-status' - preLoaderRoute: typeof ApiMcpStatusRouteImport + '/api/tanchat': { + id: '/api/tanchat' + path: '/api/tanchat' + fullPath: '/api/tanchat' + preLoaderRoute: typeof ApiTanchatRouteImport parentRoute: typeof rootRouteImport } - '/api/mcp-pool': { - id: '/api/mcp-pool' - path: '/api/mcp-pool' - fullPath: '/api/mcp-pool' - preLoaderRoute: typeof ApiMcpPoolRouteImport + '/api/transcribe': { + id: '/api/transcribe' + path: '/api/transcribe' + fullPath: '/api/transcribe' + preLoaderRoute: typeof ApiTranscribeRouteImport parentRoute: typeof rootRouteImport } - '/api/mcp-manual': { - id: '/api/mcp-manual' - path: '/api/mcp-manual' - fullPath: '/api/mcp-manual' - preLoaderRoute: typeof ApiMcpManualRouteImport + '/example/runtime-context': { + id: '/example/runtime-context' + path: '/example/runtime-context' + fullPath: '/example/runtime-context' + preLoaderRoute: typeof ExampleRuntimeContextRouteImport parentRoute: typeof rootRouteImport } - '/api/mcp-chat': { - id: '/api/mcp-chat' - path: '/api/mcp-chat' - fullPath: '/api/mcp-chat' - preLoaderRoute: typeof ApiMcpChatRouteImport + '/generations/audio': { + id: '/generations/audio' + path: '/generations/audio' + fullPath: '/generations/audio' + preLoaderRoute: typeof GenerationsAudioRouteImport parentRoute: typeof rootRouteImport } - '/api/mcp-apps-weather-server': { - id: '/api/mcp-apps-weather-server' - path: '/api/mcp-apps-weather-server' - fullPath: '/api/mcp-apps-weather-server' - preLoaderRoute: typeof ApiMcpAppsWeatherServerRouteImport + '/generations/image': { + id: '/generations/image' + path: '/generations/image' + fullPath: '/generations/image' + preLoaderRoute: typeof GenerationsImageRouteImport parentRoute: typeof rootRouteImport } - '/api/mcp-apps-shop-server': { - id: '/api/mcp-apps-shop-server' - path: '/api/mcp-apps-shop-server' - fullPath: '/api/mcp-apps-shop-server' - preLoaderRoute: typeof ApiMcpAppsShopServerRouteImport + '/generations/openrouter-combined': { + id: '/generations/openrouter-combined' + path: '/generations/openrouter-combined' + fullPath: '/generations/openrouter-combined' + preLoaderRoute: typeof GenerationsOpenrouterCombinedRouteImport parentRoute: typeof rootRouteImport } - '/api/mcp-apps-chat': { - id: '/api/mcp-apps-chat' - path: '/api/mcp-apps-chat' - fullPath: '/api/mcp-apps-chat' - preLoaderRoute: typeof ApiMcpAppsChatRouteImport + '/generations/persistent-generation': { + id: '/generations/persistent-generation' + path: '/generations/persistent-generation' + fullPath: '/generations/persistent-generation' + preLoaderRoute: typeof GenerationsPersistentGenerationRouteImport parentRoute: typeof rootRouteImport } - '/api/mcp-apps-call': { - id: '/api/mcp-apps-call' - path: '/api/mcp-apps-call' - fullPath: '/api/mcp-apps-call' - preLoaderRoute: typeof ApiMcpAppsCallRouteImport + '/generations/speech': { + id: '/generations/speech' + path: '/generations/speech' + fullPath: '/generations/speech' + preLoaderRoute: typeof GenerationsSpeechRouteImport parentRoute: typeof rootRouteImport } - '/api/interrupts': { - id: '/api/interrupts' - path: '/api/interrupts' - fullPath: '/api/interrupts' - preLoaderRoute: typeof ApiInterruptsRouteImport + '/generations/structured-chat': { + id: '/generations/structured-chat' + path: '/generations/structured-chat' + fullPath: '/generations/structured-chat' + preLoaderRoute: typeof GenerationsStructuredChatRouteImport parentRoute: typeof rootRouteImport } - '/api/image-tool-repro': { - id: '/api/image-tool-repro' - path: '/api/image-tool-repro' - fullPath: '/api/image-tool-repro' - preLoaderRoute: typeof ApiImageToolReproRouteImport + '/generations/structured-output': { + id: '/generations/structured-output' + path: '/generations/structured-output' + fullPath: '/generations/structured-output' + preLoaderRoute: typeof GenerationsStructuredOutputRouteImport parentRoute: typeof rootRouteImport } - '/api/image-gen': { - id: '/api/image-gen' - path: '/api/image-gen' - fullPath: '/api/image-gen' - preLoaderRoute: typeof ApiImageGenRouteImport + '/generations/summarize': { + id: '/generations/summarize' + path: '/generations/summarize' + fullPath: '/generations/summarize' + preLoaderRoute: typeof GenerationsSummarizeRouteImport parentRoute: typeof rootRouteImport } - '/api/generic-interrupts': { - id: '/api/generic-interrupts' - path: '/api/generic-interrupts' - fullPath: '/api/generic-interrupts' - preLoaderRoute: typeof ApiGenericInterruptsRouteImport + '/generations/transcription': { + id: '/generations/transcription' + path: '/generations/transcription' + fullPath: '/generations/transcription' + preLoaderRoute: typeof GenerationsTranscriptionRouteImport parentRoute: typeof rootRouteImport } - '/api/capability-demo': { - id: '/api/capability-demo' - path: '/api/capability-demo' - fullPath: '/api/capability-demo' - preLoaderRoute: typeof ApiCapabilityDemoRouteImport + '/generations/video': { + id: '/generations/video' + path: '/generations/video' + fullPath: '/generations/video' + preLoaderRoute: typeof GenerationsVideoRouteImport parentRoute: typeof rootRouteImport } - '/api/artifacts': { - id: '/api/artifacts' - path: '/api/artifacts' - fullPath: '/api/artifacts' - preLoaderRoute: typeof ApiArtifactsRouteImport + '/api/generate/audio': { + id: '/api/generate/audio' + path: '/api/generate/audio' + fullPath: '/api/generate/audio' + preLoaderRoute: typeof ApiGenerateAudioRouteImport parentRoute: typeof rootRouteImport } - '/api/app-studio-fork': { - id: '/api/app-studio-fork' - path: '/api/app-studio-fork' - fullPath: '/api/app-studio-fork' - preLoaderRoute: typeof ApiAppStudioForkRouteImport + '/api/generate/image': { + id: '/api/generate/image' + path: '/api/generate/image' + fullPath: '/api/generate/image' + preLoaderRoute: typeof ApiGenerateImageRouteImport parentRoute: typeof rootRouteImport } - '/api/app-studio': { - id: '/api/app-studio' - path: '/api/app-studio' - fullPath: '/api/app-studio' - preLoaderRoute: typeof ApiAppStudioRouteImport + '/api/generate/speech': { + id: '/api/generate/speech' + path: '/api/generate/speech' + fullPath: '/api/generate/speech' + preLoaderRoute: typeof ApiGenerateSpeechRouteImport + parentRoute: typeof rootRouteImport + } + '/api/generate/video': { + id: '/api/generate/video' + path: '/api/generate/video' + fullPath: '/api/generate/video' + preLoaderRoute: typeof ApiGenerateVideoRouteImport parentRoute: typeof rootRouteImport } '/example/guitars/': { @@ -1342,34 +1370,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ExampleGuitarsGuitarIdRouteImport parentRoute: typeof rootRouteImport } - '/api/generate/video': { - id: '/api/generate/video' - path: '/api/generate/video' - fullPath: '/api/generate/video' - preLoaderRoute: typeof ApiGenerateVideoRouteImport - parentRoute: typeof rootRouteImport - } - '/api/generate/speech': { - id: '/api/generate/speech' - path: '/api/generate/speech' - fullPath: '/api/generate/speech' - preLoaderRoute: typeof ApiGenerateSpeechRouteImport - parentRoute: typeof rootRouteImport - } - '/api/generate/image': { - id: '/api/generate/image' - path: '/api/generate/image' - fullPath: '/api/generate/image' - preLoaderRoute: typeof ApiGenerateImageRouteImport - parentRoute: typeof rootRouteImport - } - '/api/generate/audio': { - id: '/api/generate/audio' - path: '/api/generate/audio' - fullPath: '/api/generate/audio' - preLoaderRoute: typeof ApiGenerateAudioRouteImport - parentRoute: typeof rootRouteImport - } '/api/generate/image/artifact': { id: '/api/generate/image/artifact' path: '/artifact' diff --git a/examples/ts-react-media/package.json b/examples/ts-react-media/package.json index 89f5d0b74f..1f832eb91c 100644 --- a/examples/ts-react-media/package.json +++ b/examples/ts-react-media/package.json @@ -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", diff --git a/examples/ts-react-media/src/routeTree.gen.ts b/examples/ts-react-media/src/routeTree.gen.ts index cfbe5387c9..3182bcf153 100644 --- a/examples/ts-react-media/src/routeTree.gen.ts +++ b/examples/ts-react-media/src/routeTree.gen.ts @@ -9,19 +9,19 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' -import { Route as SeedanceRouteImport } from './routes/seedance' import { Route as IndexRouteImport } from './routes/index' +import { Route as SeedanceRouteImport } from './routes/seedance' -const SeedanceRoute = SeedanceRouteImport.update({ - id: '/seedance', - path: '/seedance', - getParentRoute: () => rootRouteImport, -} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => rootRouteImport, } as any) +const SeedanceRoute = SeedanceRouteImport.update({ + id: '/seedance', + path: '/seedance', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute @@ -51,13 +51,6 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/seedance': { - id: '/seedance' - path: '/seedance' - fullPath: '/seedance' - preLoaderRoute: typeof SeedanceRouteImport - parentRoute: typeof rootRouteImport - } '/': { id: '/' path: '/' @@ -65,6 +58,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/seedance': { + id: '/seedance' + path: '/seedance' + fullPath: '/seedance' + preLoaderRoute: typeof SeedanceRouteImport + parentRoute: typeof rootRouteImport + } } } diff --git a/examples/ts-react-rerank/package.json b/examples/ts-react-rerank/package.json index 83785b9b13..045fd110d7 100644 --- a/examples/ts-react-rerank/package.json +++ b/examples/ts-react-rerank/package.json @@ -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", diff --git a/examples/ts-react-search/package.json b/examples/ts-react-search/package.json index 26b915c7df..3ffaa70b71 100644 --- a/examples/ts-react-search/package.json +++ b/examples/ts-react-search/package.json @@ -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.1", + "@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", diff --git a/examples/ts-react-search/src/routeTree.gen.ts b/examples/ts-react-search/src/routeTree.gen.ts index 1ef02fbb58..3fa520b194 100644 --- a/examples/ts-react-search/src/routeTree.gen.ts +++ b/examples/ts-react-search/src/routeTree.gen.ts @@ -9,59 +9,59 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' -import { Route as LayoutRouteRouteImport } from './routes/_layout/route' import { Route as IndexRouteImport } from './routes/index' -import { Route as ApiSettlementsRouteImport } from './routes/api/settlements' -import { Route as ApiSearchRouteImport } from './routes/api/search' -import { Route as ApiOrdersRouteImport } from './routes/api/orders' -import { Route as ApiDisputesRouteImport } from './routes/api/disputes' -import { Route as LayoutSettlementsRouteImport } from './routes/_layout/settlements' -import { Route as LayoutOrdersRouteImport } from './routes/_layout/orders' +import { Route as LayoutRouteRouteImport } from './routes/_layout/route' import { Route as LayoutDisputesRouteImport } from './routes/_layout/disputes' +import { Route as LayoutOrdersRouteImport } from './routes/_layout/orders' +import { Route as LayoutSettlementsRouteImport } from './routes/_layout/settlements' +import { Route as ApiDisputesRouteImport } from './routes/api/disputes' +import { Route as ApiOrdersRouteImport } from './routes/api/orders' +import { Route as ApiSearchRouteImport } from './routes/api/search' +import { Route as ApiSettlementsRouteImport } from './routes/api/settlements' -const LayoutRouteRoute = LayoutRouteRouteImport.update({ - id: '/_layout', - getParentRoute: () => rootRouteImport, -} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', getParentRoute: () => rootRouteImport, } as any) -const ApiSettlementsRoute = ApiSettlementsRouteImport.update({ - id: '/api/settlements', - path: '/api/settlements', +const LayoutRouteRoute = LayoutRouteRouteImport.update({ + id: '/_layout', getParentRoute: () => rootRouteImport, } as any) -const ApiSearchRoute = ApiSearchRouteImport.update({ - id: '/api/search', - path: '/api/search', - getParentRoute: () => rootRouteImport, +const LayoutDisputesRoute = LayoutDisputesRouteImport.update({ + id: '/disputes', + path: '/disputes', + getParentRoute: () => LayoutRouteRoute, } as any) -const ApiOrdersRoute = ApiOrdersRouteImport.update({ - id: '/api/orders', - path: '/api/orders', - getParentRoute: () => rootRouteImport, +const LayoutOrdersRoute = LayoutOrdersRouteImport.update({ + id: '/orders', + path: '/orders', + getParentRoute: () => LayoutRouteRoute, +} as any) +const LayoutSettlementsRoute = LayoutSettlementsRouteImport.update({ + id: '/settlements', + path: '/settlements', + getParentRoute: () => LayoutRouteRoute, } as any) const ApiDisputesRoute = ApiDisputesRouteImport.update({ id: '/api/disputes', path: '/api/disputes', getParentRoute: () => rootRouteImport, } as any) -const LayoutSettlementsRoute = LayoutSettlementsRouteImport.update({ - id: '/settlements', - path: '/settlements', - getParentRoute: () => LayoutRouteRoute, +const ApiOrdersRoute = ApiOrdersRouteImport.update({ + id: '/api/orders', + path: '/api/orders', + getParentRoute: () => rootRouteImport, } as any) -const LayoutOrdersRoute = LayoutOrdersRouteImport.update({ - id: '/orders', - path: '/orders', - getParentRoute: () => LayoutRouteRoute, +const ApiSearchRoute = ApiSearchRouteImport.update({ + id: '/api/search', + path: '/api/search', + getParentRoute: () => rootRouteImport, } as any) -const LayoutDisputesRoute = LayoutDisputesRouteImport.update({ - id: '/disputes', - path: '/disputes', - getParentRoute: () => LayoutRouteRoute, +const ApiSettlementsRoute = ApiSettlementsRouteImport.update({ + id: '/api/settlements', + path: '/api/settlements', + getParentRoute: () => rootRouteImport, } as any) export interface FileRoutesByFullPath { @@ -141,13 +141,6 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/_layout': { - id: '/_layout' - path: '' - fullPath: '/' - preLoaderRoute: typeof LayoutRouteRouteImport - parentRoute: typeof rootRouteImport - } '/': { id: '/' path: '/' @@ -155,26 +148,33 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } - '/api/settlements': { - id: '/api/settlements' - path: '/api/settlements' - fullPath: '/api/settlements' - preLoaderRoute: typeof ApiSettlementsRouteImport + '/_layout': { + id: '/_layout' + path: '' + fullPath: '/' + preLoaderRoute: typeof LayoutRouteRouteImport parentRoute: typeof rootRouteImport } - '/api/search': { - id: '/api/search' - path: '/api/search' - fullPath: '/api/search' - preLoaderRoute: typeof ApiSearchRouteImport - parentRoute: typeof rootRouteImport + '/_layout/disputes': { + id: '/_layout/disputes' + path: '/disputes' + fullPath: '/disputes' + preLoaderRoute: typeof LayoutDisputesRouteImport + parentRoute: typeof LayoutRouteRoute } - '/api/orders': { - id: '/api/orders' - path: '/api/orders' - fullPath: '/api/orders' - preLoaderRoute: typeof ApiOrdersRouteImport - parentRoute: typeof rootRouteImport + '/_layout/orders': { + id: '/_layout/orders' + path: '/orders' + fullPath: '/orders' + preLoaderRoute: typeof LayoutOrdersRouteImport + parentRoute: typeof LayoutRouteRoute + } + '/_layout/settlements': { + id: '/_layout/settlements' + path: '/settlements' + fullPath: '/settlements' + preLoaderRoute: typeof LayoutSettlementsRouteImport + parentRoute: typeof LayoutRouteRoute } '/api/disputes': { id: '/api/disputes' @@ -183,26 +183,26 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiDisputesRouteImport parentRoute: typeof rootRouteImport } - '/_layout/settlements': { - id: '/_layout/settlements' - path: '/settlements' - fullPath: '/settlements' - preLoaderRoute: typeof LayoutSettlementsRouteImport - parentRoute: typeof LayoutRouteRoute + '/api/orders': { + id: '/api/orders' + path: '/api/orders' + fullPath: '/api/orders' + preLoaderRoute: typeof ApiOrdersRouteImport + parentRoute: typeof rootRouteImport } - '/_layout/orders': { - id: '/_layout/orders' - path: '/orders' - fullPath: '/orders' - preLoaderRoute: typeof LayoutOrdersRouteImport - parentRoute: typeof LayoutRouteRoute + '/api/search': { + id: '/api/search' + path: '/api/search' + fullPath: '/api/search' + preLoaderRoute: typeof ApiSearchRouteImport + parentRoute: typeof rootRouteImport } - '/_layout/disputes': { - id: '/_layout/disputes' - path: '/disputes' - fullPath: '/disputes' - preLoaderRoute: typeof LayoutDisputesRouteImport - parentRoute: typeof LayoutRouteRoute + '/api/settlements': { + id: '/api/settlements' + path: '/api/settlements' + fullPath: '/api/settlements' + preLoaderRoute: typeof ApiSettlementsRouteImport + parentRoute: typeof rootRouteImport } } } diff --git a/examples/ts-solid-chat/package.json b/examples/ts-solid-chat/package.json index 56b0356600..98b7f475bb 100644 --- a/examples/ts-solid-chat/package.json +++ b/examples/ts-solid-chat/package.json @@ -21,13 +21,13 @@ "@tanstack/ai-solid": "workspace:*", "@tanstack/ai-solid-ui": "workspace:*", "@tanstack/nitro-v2-vite-plugin": "^1.155.0", - "@tanstack/router-plugin": "^1.158.4", + "@tanstack/router-plugin": "^1.168.35", "@tanstack/solid-ai-devtools": "workspace:*", "@tanstack/solid-devtools": "^0.7.15", - "@tanstack/solid-router": "^1.139.10", - "@tanstack/solid-router-devtools": "^1.139.10", - "@tanstack/solid-router-ssr-query": "^1.139.10", - "@tanstack/solid-start": "^1.139.10", + "@tanstack/solid-router": "^1.170.30", + "@tanstack/solid-router-devtools": "^1.167.1", + "@tanstack/solid-router-ssr-query": "^1.167.1", + "@tanstack/solid-start": "^1.168.47", "@tanstack/solid-store": "^0.8.0", "@tanstack/store": "^0.11.1", "highlight.js": "^11.11.1", diff --git a/examples/ts-solid-chat/src/routeTree.gen.ts b/examples/ts-solid-chat/src/routeTree.gen.ts index f474ce588d..573ae36e46 100644 --- a/examples/ts-solid-chat/src/routeTree.gen.ts +++ b/examples/ts-solid-chat/src/routeTree.gen.ts @@ -39,7 +39,7 @@ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/api/chat': typeof ApiChatRoute '/example/guitars/$guitarId': typeof ExampleGuitarsGuitarIdRoute - '/example/guitars': typeof ExampleGuitarsIndexRoute + '/example/guitars/': typeof ExampleGuitarsIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -60,7 +60,7 @@ export interface FileRouteTypes { | '/' | '/api/chat' | '/example/guitars/$guitarId' - | '/example/guitars' + | '/example/guitars/' fileRoutesByTo: FileRoutesByTo to: '/' | '/api/chat' | '/example/guitars/$guitarId' | '/example/guitars' id: @@ -97,7 +97,7 @@ declare module '@tanstack/solid-router' { '/example/guitars/': { id: '/example/guitars/' path: '/example/guitars' - fullPath: '/example/guitars' + fullPath: '/example/guitars/' preLoaderRoute: typeof ExampleGuitarsIndexRouteImport parentRoute: typeof rootRouteImport } diff --git a/packages/ai-client/src/audio-recorder.ts b/packages/ai-client/src/audio-recorder.ts index 7077a876ba..070ff95f58 100644 --- a/packages/ai-client/src/audio-recorder.ts +++ b/packages/ai-client/src/audio-recorder.ts @@ -66,6 +66,7 @@ export class AudioRecorder { private startedAt = 0 private readonly snapshotAtom: Atom = createAtom('idle') + private readonly listeners = new Set<(state: AudioRecorderState) => void>() // True while start() is awaiting getUserMedia (state is still 'idle' then). private starting = false // Set by cancel()/teardown during that window so start() releases the @@ -93,10 +94,8 @@ export class AudioRecorder { } subscribe(cb: (state: AudioRecorderState) => void): () => void { - const { unsubscribe } = this.snapshotAtom.subscribe((next) => { - cb(next) - }) - return unsubscribe + this.listeners.add(cb) + return () => this.listeners.delete(cb) } /** Current recorder lifecycle state. */ @@ -104,6 +103,7 @@ export class AudioRecorder { private setState(state: AudioRecorderState): void { this.snapshotAtom.set(state) + for (const listener of this.listeners) listener(state) } async start(): Promise { diff --git a/packages/ai-client/src/byok/client.ts b/packages/ai-client/src/byok/client.ts index 1808ec996b..b41d6c264a 100644 --- a/packages/ai-client/src/byok/client.ts +++ b/packages/ai-client/src/byok/client.ts @@ -4,7 +4,7 @@ import { isProviderId, maskKey, } from '@tanstack/ai/byok' -import { createAtom, subscribeAtom } from '../snapshot-atom' +import { createAtom } from '../snapshot-atom' import type { Atom } from '../snapshot-atom' import { memoryStorage } from './storage' import type { ProviderId } from '@tanstack/ai/byok' @@ -89,6 +89,7 @@ export class ByokClient { #coverage: Record = {} #storageError: string | null = null readonly #snapshotAtom: Atom + readonly #listeners = new Set<() => void>() readonly #ready: Promise constructor(options: DefineByokOptions = {}) { @@ -101,18 +102,27 @@ export class ByokClient { /** Resolves when constructor hydration (peek/load) finishes. */ ready = (): Promise => this.#ready - subscribe = (listener: () => void): (() => void) => - subscribeAtom(this.#snapshotAtom, listener) + subscribe = (listener: () => void): (() => void) => { + this.#listeners.add(listener) + return () => this.#listeners.delete(listener) + } getSnapshot = (): ByokSnapshot => this.#snapshotAtom.get() #buildSnapshot(): ByokSnapshot { - return { - status: { ...this.#statuses }, + return Object.freeze({ + status: Object.freeze( + Object.fromEntries( + Object.entries(this.#statuses).map(([key, value]) => [ + key, + value ? Object.freeze({ ...value }) : value, + ]), + ), + ), locked: this.#locked, - prompt: this.#prompt, + prompt: this.#prompt ? Object.freeze({ ...this.#prompt }) : null, storageError: this.#storageError, - } + }) } keys(): Keyring { @@ -348,6 +358,7 @@ export class ByokClient { #emit(): void { this.#snapshotAtom.set(this.#buildSnapshot()) + for (const listener of this.#listeners) listener() } } diff --git a/packages/ai-client/src/chat-client.ts b/packages/ai-client/src/chat-client.ts index 26d5612772..6085b6a4e2 100644 --- a/packages/ai-client/src/chat-client.ts +++ b/packages/ai-client/src/chat-client.ts @@ -649,7 +649,9 @@ export class ChatClient< onMessagesChange: (messages: Array) => { this.persistor?.notifyMessagesChanged(messages) this.patchSnapshot({ - messages: messages as Array>, + messages: this.freezeSnapshotMessages( + messages as Array>, + ), }) this.callbacksRef.current.onMessagesChange(messages) }, @@ -1472,19 +1474,89 @@ export class ChatClient< return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}` } + private freezeSnapshotPart>( + part: TPart, + ): TPart { + return Object.freeze({ + ...part, + ...('source' in part && + typeof part.source === 'object' && + part.source !== null + ? { source: Object.freeze({ ...part.source }) } + : {}), + ...(part.type === 'tool-call' && 'approval' in part && part.approval + ? { approval: Object.freeze({ ...part.approval }) } + : {}), + ...(part.type === 'tool-result' && Array.isArray(part.content) + ? { + content: Object.freeze( + part.content.map((contentPart) => + this.freezeSnapshotPart(contentPart), + ), + ), + } + : {}), + }) as TPart + } + + private freezeSnapshotMessages( + messages: Array>, + ): Array> { + return Object.freeze( + messages.map((message) => + Object.freeze({ + ...message, + parts: Object.freeze( + message.parts.map((part) => this.freezeSnapshotPart(part)), + ), + }), + ), + ) as Array> + } + + private freezeSnapshotQueue( + queue: Array, + ): Array { + return Object.freeze( + queue.map((item) => + Object.freeze({ + ...item, + ...(typeof item.content === 'object' + ? { + content: Object.freeze({ + ...item.content, + ...(Array.isArray(item.content.content) + ? { + content: Object.freeze( + item.content.content.map((part) => + this.freezeSnapshotPart(part), + ), + ), + } + : {}), + }), + } + : {}), + }), + ), + ) as Array + } + private readSnapshot(): ChatClientSnapshot { - return { - messages: this.processor.getMessages() as Array>, + return Object.freeze({ + messages: this.freezeSnapshotMessages( + this.processor.getMessages() as Array>, + ), status: this.status, isLoading: this.isLoading, error: this.error, isSubscribed: this.isSubscribed, connectionStatus: this.connectionStatus, sessionGenerating: this.sessionGenerating, - queue: this.getQueue(), + queue: this.freezeSnapshotQueue(this.getQueue()), runId: this.currentRunId, interruptState: this.interruptManager.getState(), - } + }) } private patchSnapshot( @@ -3007,7 +3079,7 @@ export class ChatClient< private emitQueueChange(): void { const queue = this.getQueue() - this.patchSnapshot({ queue }) + this.patchSnapshot({ queue: this.freezeSnapshotQueue(queue) }) this.callbacksRef.current.onQueueChange(queue) this.devtoolsBridge.emitSnapshot() } diff --git a/packages/ai-client/src/generation-client.ts b/packages/ai-client/src/generation-client.ts index c7a36b0456..a6fed26e1c 100644 --- a/packages/ai-client/src/generation-client.ts +++ b/packages/ai-client/src/generation-client.ts @@ -276,14 +276,19 @@ export class GenerationClient< this.input = input this.progress = null - const runId = this.devtoolsBridge.beginRun(input) - this.setIsLoading(true) - this.setStatus('generating') - this.setError(undefined) - const abortController = new AbortController() this.abortController = abortController const { signal } = abortController + const runId = this.devtoolsBridge.beginRun(input) + patchAtom(this.snapshotAtom, { runId }) + if (!this.ownsRun(abortController)) return + + this.setIsLoading(true, abortController) + if (!this.ownsRun(abortController)) return + this.setStatus('generating', abortController) + if (!this.ownsRun(abortController)) return + this.setError(undefined, abortController) + if (!this.ownsRun(abortController)) return try { let headers: Record | undefined @@ -293,6 +298,7 @@ export class GenerationClient< this.body.provider, ) headers = await prepareResolvedByokHeaders(this.byok, provider) + if (!this.ownsRun(abortController)) return } if (this.fetcher) { @@ -301,19 +307,21 @@ export class GenerationClient< input, headers === undefined ? { signal } : { signal, headers }, ) - if (signal.aborted) return + if (!this.ownsRun(abortController)) return if (result instanceof Response) { // Server function returned SSE Response — parse stream await this.processStream( parseSSEResponse(result, signal), runId, - signal, + abortController, ) } else { this.devtoolsBridge.ensureRunStarted(runId) - this.setResult(result) - this.setStatus('success') - this.completePlainFetcherResumeSnapshot(result) + this.setResult(result, abortController) + if (!this.ownsRun(abortController)) return + this.setStatus('success', abortController) + if (!this.ownsRun(abortController)) return + this.completePlainFetcherResumeSnapshot(result, abortController) } } else if (this.connection) { // Streaming adapter path @@ -324,13 +332,13 @@ export class GenerationClient< signal, this.createRunContext(runId, headers), ) - await this.processStream(stream, runId, signal) + await this.processStream(stream, runId, abortController) } else { throw new Error( 'GenerationClient requires either a connection or fetcher option', ) } - if (!signal.aborted && this.status === 'success') { + if (this.ownsRun(abortController) && this.status === 'success') { // Bump progress to 100 on successful completion so devtools // snapshots reflect the final state. The bridge mirrors this in // the run's recorded progress, but the snapshot reads `progress` @@ -343,17 +351,22 @@ export class GenerationClient< ) } } catch (err: unknown) { - if (signal.aborted) return + if (!this.ownsRun(abortController)) return const error = err instanceof Error ? err : new Error(String(err)) if (error instanceof ByokMissingError) { this.byok?.request(error.provider, 'missing') + if (!this.ownsRun(abortController)) return } if (error instanceof ByokBlockedError && error.reason === 'locked') { this.byok?.request(error.provider, 'locked') + if (!this.ownsRun(abortController)) return } - this.setError(error) - this.setStatus('error') - this.recordResumeSnapshotError(error) + this.setError(error, abortController) + if (!this.ownsRun(abortController)) return + this.setStatus('error', abortController) + if (!this.ownsRun(abortController)) return + this.recordResumeSnapshotError(error, abortController) + if (!this.ownsRun(abortController)) return this.devtoolsBridge.finishRun( this.devtoolsBridge.getActiveRunId() ?? runId, 'run:errored', @@ -363,8 +376,11 @@ export class GenerationClient< this.callbacksRef.onError?.(error) } finally { if (this.abortController === abortController) { - this.abortController = null - this.setIsLoading(false) + patchAtom(this.snapshotAtom, { runId: null }) + if (this.ownsRun(abortController)) { + this.setIsLoading(false, abortController) + if (this.ownsRun(abortController)) this.abortController = null + } } } } @@ -384,8 +400,9 @@ export class GenerationClient< private async processStream( source: AsyncIterable, fallbackRunId: string, - signal: AbortSignal, + abortController: AbortController, ): Promise { + const { signal } = abortController let streamRunId: string | undefined let sawTerminalChunk = false @@ -394,7 +411,9 @@ export class GenerationClient< const chunk = restoreInboundChunk(raw) this.callbacksRef.onChunk?.(chunk) - this.observeResumeSnapshot(chunk) + if (!this.ownsRun(abortController)) return + this.observeResumeSnapshot(chunk, abortController) + if (!this.ownsRun(abortController)) return const chunkRunId = 'runId' in chunk && typeof chunk.runId === 'string' ? chunk.runId @@ -404,19 +423,23 @@ export class GenerationClient< switch (chunk.type) { case 'RUN_STARTED': { streamRunId = chunk.runId + patchAtom(this.snapshotAtom, { runId: chunk.runId }) + if (!this.ownsRun(abortController)) return this.devtoolsBridge.ensureRunStarted(chunk.runId) break } case 'CUSTOM': { this.devtoolsBridge.ensureRunStarted(streamRunId ?? fallbackRunId) if (chunk.name === GENERATION_EVENTS.RESULT) { - this.setResult(chunk.value as TResult) + this.setResult(chunk.value as TResult, abortController) + if (!this.ownsRun(abortController)) return } else if (chunk.name === GENERATION_EVENTS.PROGRESS) { const { progress, message } = chunk.value as { progress: number message?: string } - this.setProgress(progress, message) + this.setProgress(progress, message, abortController) + if (!this.ownsRun(abortController)) return } break } @@ -424,7 +447,8 @@ export class GenerationClient< streamRunId = chunk.runId sawTerminalChunk = true this.devtoolsBridge.ensureRunStarted(chunk.runId) - this.setStatus('success') + this.setStatus('success', abortController) + if (!this.ownsRun(abortController)) return break } case 'RUN_ERROR': { @@ -452,17 +476,29 @@ export class GenerationClient< */ stop(): void { const runId = this.devtoolsBridge.getActiveRunId() + const stoppedStatus = this.status if (this.abortController) { this.abortController.abort() this.abortController = null } - this.setIsLoading(false) - if (this.status === 'generating') { - this.setStatus('idle') - if (runId) { + if (runId) { + if (stoppedStatus === 'success') { + this.devtoolsBridge.finishRun(runId, 'run:completed', 'completed') + } else if (stoppedStatus === 'error') { + this.devtoolsBridge.finishRun( + runId, + 'run:errored', + 'errored', + this.error?.message, + ) + } else { this.devtoolsBridge.finishRun(runId, 'run:cancelled', 'cancelled') } } + patchAtom(this.snapshotAtom, { runId: null }) + if (stoppedStatus === 'generating') { + this.setStatus('idle') + } // A stopped run is no longer resumable. Without this the in-memory // snapshot stays `running`, and a remount's `maybeResumeInFlight` would // rejoin a run the user just cancelled. @@ -474,6 +510,7 @@ export class GenerationClient< } this.notifyResumeSnapshotChanged() } + this.setIsLoading(false) } /** @@ -484,6 +521,7 @@ export class GenerationClient< */ reset(): void { this.stop() + if (this.abortController || this.isLoading) return this.setResult(null) this.input = null this.progress = null @@ -548,7 +586,9 @@ export class GenerationClient< this.abortController.abort() this.abortController = null } - this.setIsLoading(false) + if (this.resumeSnapshot?.status !== 'running') { + patchAtom(this.snapshotAtom, { runId: null }) + } this.devtoolsBridge.dispose() this.devtoolsMounted = false // Re-arm mount hydration + rejoin so a remount resumes from the (preserved) @@ -556,6 +596,7 @@ export class GenerationClient< // `maybeResumeInFlight`, both individually guarded. this.serverHydrationStarted = false this.rejoinedRunId = undefined + this.setIsLoading(false) } // =========================== @@ -619,17 +660,23 @@ export class GenerationClient< // Private state setters // =========================== - private setResult(rawResult: TResult | null): void { + private setResult( + rawResult: TResult | null, + controller?: AbortController, + ): void { if (rawResult === null) { this.result = null patchAtom(this.snapshotAtom, { result: null }) + if (controller && !this.ownsRun(controller)) return this.callbacksRef.onResultChange?.(null) + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordResultChange() return } if (this.callbacksRef.onResult) { const transformed = this.callbacksRef.onResult(rawResult) + if (controller && !this.ownsRun(controller)) return if (transformed === null) { // null return → keep previous result unchanged, just re-emit this.devtoolsBridge.emitState() @@ -639,7 +686,9 @@ export class GenerationClient< // Non-null, non-undefined → use transformed value this.result = transformed patchAtom(this.snapshotAtom, { result: this.result }) + if (controller && !this.ownsRun(controller)) return this.callbacksRef.onResultChange?.(this.result) + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordResultChange() return } @@ -651,32 +700,50 @@ export class GenerationClient< // oxlint-disable-next-line eslint-js/no-restricted-syntax -- TOutput defaults to TResult when no onResult transform is supplied this.result = rawResult as unknown as TOutput patchAtom(this.snapshotAtom, { result: this.result }) + if (controller && !this.ownsRun(controller)) return this.callbacksRef.onResultChange?.(this.result) + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordResultChange() } - private setIsLoading(isLoading: boolean): void { + private setIsLoading(isLoading: boolean, controller?: AbortController): void { this.isLoading = isLoading patchAtom(this.snapshotAtom, { isLoading }) + if (controller && !this.ownsRun(controller)) return this.callbacksRef.onLoadingChange?.(isLoading) + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordLoadingChange() } - private setError(error: Error | undefined): void { + private setError( + error: Error | undefined, + controller?: AbortController, + ): void { this.error = error patchAtom(this.snapshotAtom, { error }) + if (controller && !this.ownsRun(controller)) return this.callbacksRef.onErrorChange?.(error) + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordErrorChange(error) } - private setStatus(status: GenerationClientState): void { + private setStatus( + status: GenerationClientState, + controller?: AbortController, + ): void { this.status = status patchAtom(this.snapshotAtom, { status }) + if (controller && !this.ownsRun(controller)) return this.callbacksRef.onStatusChange?.(status) + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordStatusChange(status) } - private setProgress(value: number, message?: string): void { + private setProgress( + value: number, + message?: string, + controller?: AbortController, + ): void { this.progress = { value, ...(message ? { message } : {}), @@ -686,6 +753,7 @@ export class GenerationClient< } else { this.callbacksRef.onProgress?.(value, message) } + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordProgressChange() } @@ -712,6 +780,10 @@ export class GenerationClient< return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}` } + private ownsRun(controller: AbortController): boolean { + return !controller.signal.aborted && this.abortController === controller + } + private createRunContext( runId: string, headers?: Record, @@ -723,12 +795,15 @@ export class GenerationClient< } } - private observeResumeSnapshot(chunk: StreamChunk): void { + private observeResumeSnapshot( + chunk: StreamChunk, + controller: AbortController, + ): void { this.resumeSnapshot = updateGenerationResumeSnapshot( this.resumeSnapshot, chunk, ) - this.notifyResumeSnapshotChanged() + this.notifyResumeSnapshotChanged(controller) } /** @@ -736,9 +811,10 @@ export class GenerationClient< * The snapshot stays internal (persistence + devtools); the hook consumes * `resumeState`, mirroring the chat client. */ - private notifyResumeSnapshotChanged(): void { + private notifyResumeSnapshotChanged(controller?: AbortController): void { this.callbacksRef.onResumeSnapshotChange?.(this.resumeSnapshot) - this.emitResumeState() + if (controller && !this.ownsRun(controller)) return + this.emitResumeState(controller) } /** @@ -752,7 +828,7 @@ export class GenerationClient< * boundaries and when artifacts land, so skip the notification unless it * materially changed — same gate the persistence writes use. */ - private emitResumeState(): void { + private emitResumeState(controller?: AbortController): void { const snapshot = this.resumeSnapshot const state = snapshot?.resumeState const resumeState: GenerationResumeState | null = state @@ -769,6 +845,7 @@ export class GenerationClient< } this.lastEmittedResumeState = signature patchAtom(this.snapshotAtom, { runId: resumeState?.runId ?? null }) + if (controller && !this.ownsRun(controller)) return this.callbacksRef.onResumeStateChange?.(resumeState) } @@ -897,7 +974,10 @@ export class GenerationClient< * stale `error` from a previous run is intentionally dropped — this run * succeeded. */ - private completePlainFetcherResumeSnapshot(rawResult: unknown): void { + private completePlainFetcherResumeSnapshot( + rawResult: unknown, + controller: AbortController, + ): void { const previous = this.resumeSnapshot const result = createGenerationResultSnapshot(rawResult) this.resumeSnapshot = { @@ -914,7 +994,7 @@ export class GenerationClient< ? { result: { ...previous.result } } : {}), } - this.notifyResumeSnapshotChanged() + this.notifyResumeSnapshotChanged(controller) } /** @@ -923,15 +1003,20 @@ export class GenerationClient< * mark the snapshot `error`, leaving a persisted record that claims the * run is still in flight. */ - private recordResumeSnapshotError(error: Error): void { + private recordResumeSnapshotError( + error: Error, + controller?: AbortController, + ): void { // Surface the failure on the OBSERVABLE fields FIRST: a rejoin (or live // stream) that emits RUN_ERROR has already flipped the snapshot to `error` // via `observeResumeSnapshot`, so the early-return below would otherwise // skip this and leave `status` stuck on `generating` — the run would look // like it is still going forever. The guard avoids a duplicate `error` // emission on the live `generate()` path, which sets the status itself. - if (this.status !== 'error') this.setStatus('error') - this.setError(error) + if (this.status !== 'error') this.setStatus('error', controller) + if (controller && !this.ownsRun(controller)) return + this.setError(error, controller) + if (controller && !this.ownsRun(controller)) return if (this.resumeSnapshot?.status === 'error') return if (!this.resumeSnapshot && !this.serverDriven) return const previous = this.resumeSnapshot @@ -946,7 +1031,7 @@ export class GenerationClient< ...(previous?.result ? { result: { ...previous.result } } : {}), error: { message: error.message }, } - this.notifyResumeSnapshotChanged() + this.notifyResumeSnapshotChanged(controller) } /** @@ -1070,23 +1155,26 @@ export class GenerationClient< this.rejoinedRunId = runId const controller = new AbortController() this.abortController = controller - this.setIsLoading(true) - this.setStatus('generating') + this.setIsLoading(true, controller) + if (!this.ownsRun(controller)) return + this.setStatus('generating', controller) + if (!this.ownsRun(controller)) return void (async () => { try { await this.processStream( joinRun(runId, controller.signal), runId, - controller.signal, + controller, ) } catch (error) { - if (!controller.signal.aborted) { + if (this.ownsRun(controller)) { const failure = error instanceof Error ? error : new Error(String(error)) // Settles `status`/`error` AND rewrites the snapshot to a terminal // `error` with a null `resumeState`, so the next mount does not // rejoin this run again. - this.recordResumeSnapshotError(failure) + this.recordResumeSnapshotError(failure, controller) + if (!this.ownsRun(controller)) return this.callbacksRef.onError?.(failure) } } finally { @@ -1094,8 +1182,8 @@ export class GenerationClient< // fresh `generate()` may have replaced the controller while the tail // was settling, and that live run owns `isLoading` now. if (this.abortController === controller) { - this.abortController = null - this.setIsLoading(false) + this.setIsLoading(false, controller) + if (this.ownsRun(controller)) this.abortController = null } } })() diff --git a/packages/ai-client/src/realtime-client.ts b/packages/ai-client/src/realtime-client.ts index 04b57d8088..a97f420d99 100644 --- a/packages/ai-client/src/realtime-client.ts +++ b/packages/ai-client/src/realtime-client.ts @@ -1,5 +1,5 @@ import { convertSchemaToJsonSchema } from '@tanstack/ai/client' -import { createAtom, patchAtom, subscribeAtom } from './snapshot-atom' +import { createAtom, subscribeAtom } from './snapshot-atom' import type { Atom } from './snapshot-atom' import type { AnyClientTool, @@ -51,19 +51,17 @@ export class RealtimeClient { new Set() private unsubscribers: Array<() => void> = [] - private readonly snapshotAtom: Atom = - createAtom({ - status: 'idle', - mode: 'idle', - messages: [], - pendingUserTranscript: null, - pendingAssistantTranscript: null, - error: null, - }) - - private get state(): RealtimeClientState { - return this.snapshotAtom.get() + private state: RealtimeClientState = { + status: 'idle', + mode: 'idle', + messages: [], + pendingUserTranscript: null, + pendingAssistantTranscript: null, + error: null, } + private readonly snapshotAtom: Atom = createAtom( + this.buildSnapshot(), + ) constructor(options: RealtimeClientOptions) { this.options = { @@ -373,11 +371,18 @@ export class RealtimeClient { // ============================================================================ private updateState(updates: Partial): void { - patchAtom(this.snapshotAtom, updates) + const changed = Object.keys(updates).some((key) => { + const stateKey = key as keyof RealtimeClientState + return !Object.is(this.state[stateKey], updates[stateKey]) + }) + if (!changed) return + this.state = { ...this.state, ...updates } + this.snapshotAtom.set(this.buildSnapshot()) + const snapshot = this.snapshotAtom.get() // Notify callbacks for (const callback of this.stateChangeCallbacks) { - callback(this.state) + callback(snapshot) } // Notify specific callbacks @@ -389,6 +394,31 @@ export class RealtimeClient { } } + private buildSnapshot(): RealtimeClientState { + return { + ...this.state, + messages: Object.freeze( + this.state.messages.map((message) => + Object.freeze({ + ...message, + parts: Object.freeze( + message.parts.map((part) => + Object.freeze({ + ...part, + ...('source' in part && + typeof part.source === 'object' && + part.source !== null + ? { source: Object.freeze({ ...part.source }) } + : {}), + }), + ), + ), + }), + ), + ) as Array, + } + } + private addMessage(message: RealtimeMessage): void { this.updateState({ messages: [...this.state.messages, message], diff --git a/packages/ai-client/src/snapshot-atom.ts b/packages/ai-client/src/snapshot-atom.ts index d1932dbed9..b05b7ff9d2 100644 --- a/packages/ai-client/src/snapshot-atom.ts +++ b/packages/ai-client/src/snapshot-atom.ts @@ -1,9 +1,28 @@ -import { createAtom } from '@tanstack/store' +import { createAtom as createStoreAtom } from '@tanstack/store' import type { Atom } from '@tanstack/store' -export { createAtom } export type { Atom } +function freezeSnapshot(value: T): T { + return typeof value === 'object' && value !== null + ? Object.freeze(value) + : value +} + +export function createAtom(initialValue: T): Atom { + const atom = createStoreAtom(freezeSnapshot(initialValue)) + const set = atom.set.bind(atom) + atom.set = (updater) => + set((previous) => + freezeSnapshot( + typeof updater === 'function' + ? (updater as (value: T) => T)(previous) + : updater, + ), + ) + return atom +} + /** * Merge a partial update into an object atom. Skips notify when every * provided field is `Object.is`-equal to the current value. diff --git a/packages/ai-client/src/video-generation-client.ts b/packages/ai-client/src/video-generation-client.ts index fa9a2cb5f6..eceddf9595 100644 --- a/packages/ai-client/src/video-generation-client.ts +++ b/packages/ai-client/src/video-generation-client.ts @@ -269,16 +269,23 @@ export class VideoGenerationClient { this.input = input this.progress = null - const runId = this.devtoolsBridge.beginRun(input) - this.setIsLoading(true) - this.setStatus('generating') - this.setError(undefined) - this.setJobId(null) - this.setVideoStatus(null) - const abortController = new AbortController() this.abortController = abortController const { signal } = abortController + const runId = this.devtoolsBridge.beginRun(input) + patchAtom(this.snapshotAtom, { runId }) + if (!this.ownsRun(abortController)) return + + this.setIsLoading(true, abortController) + if (!this.ownsRun(abortController)) return + this.setStatus('generating', abortController) + if (!this.ownsRun(abortController)) return + this.setError(undefined, abortController) + if (!this.ownsRun(abortController)) return + this.setJobId(null, abortController) + if (!this.ownsRun(abortController)) return + this.setVideoStatus(null, abortController) + if (!this.ownsRun(abortController)) return try { let headers: Record | undefined @@ -288,10 +295,11 @@ export class VideoGenerationClient { this.body.provider, ) headers = await prepareResolvedByokHeaders(this.byok, provider) + if (!this.ownsRun(abortController)) return } if (this.fetcher) { - await this.generateWithFetcher(input, signal, runId, headers) + await this.generateWithFetcher(input, abortController, runId, headers) } else if (this.connection) { const mergedData = { ...this.body, ...input } const stream = this.connection.connect( @@ -300,13 +308,13 @@ export class VideoGenerationClient { signal, this.createRunContext(runId, headers), ) - await this.processStream(stream, runId, signal) + await this.processStream(stream, runId, abortController) } else { throw new Error( 'VideoGenerationClient requires either a connection or fetcher option', ) } - if (!signal.aborted && this.status === 'success') { + if (this.ownsRun(abortController) && this.status === 'success') { this.devtoolsBridge.finishRun( this.devtoolsBridge.getActiveRunId() ?? runId, 'run:completed', @@ -314,17 +322,22 @@ export class VideoGenerationClient { ) } } catch (err: unknown) { - if (signal.aborted) return + if (!this.ownsRun(abortController)) return const error = err instanceof Error ? err : new Error(String(err)) if (error instanceof ByokMissingError) { this.byok?.request(error.provider, 'missing') + if (!this.ownsRun(abortController)) return } if (error instanceof ByokBlockedError && error.reason === 'locked') { this.byok?.request(error.provider, 'locked') + if (!this.ownsRun(abortController)) return } - this.setError(error) - this.setStatus('error') - this.recordResumeSnapshotError(error) + this.setError(error, abortController) + if (!this.ownsRun(abortController)) return + this.setStatus('error', abortController) + if (!this.ownsRun(abortController)) return + this.recordResumeSnapshotError(error, abortController) + if (!this.ownsRun(abortController)) return this.devtoolsBridge.finishRun( this.devtoolsBridge.getActiveRunId() ?? runId, 'run:errored', @@ -334,8 +347,11 @@ export class VideoGenerationClient { this.callbacksRef.onError?.(error) } finally { if (this.abortController === abortController) { - this.abortController = null - this.setIsLoading(false) + patchAtom(this.snapshotAtom, { runId: null }) + if (this.ownsRun(abortController)) { + this.setIsLoading(false, abortController) + if (this.ownsRun(abortController)) this.abortController = null + } } } } @@ -345,27 +361,34 @@ export class VideoGenerationClient { */ private async generateWithFetcher( input: VideoGenerateInput, - signal: AbortSignal, + abortController: AbortController, runId: string, headers?: Record, ): Promise { if (!this.fetcher) return + const { signal } = abortController // Fetcher returns a completed result directly, or a Response with SSE body const result = await this.fetcher( input, headers === undefined ? { signal } : { signal, headers }, ) - if (signal.aborted) return + if (!this.ownsRun(abortController)) return if (result instanceof Response) { // Server function returned SSE Response — parse stream - await this.processStream(parseSSEResponse(result, signal), runId, signal) + await this.processStream( + parseSSEResponse(result, signal), + runId, + abortController, + ) } else { this.devtoolsBridge.ensureRunStarted(runId) - this.setResult(result) - this.setStatus('success') - this.completePlainFetcherResumeSnapshot(result) + this.setResult(result, abortController) + if (!this.ownsRun(abortController)) return + this.setStatus('success', abortController) + if (!this.ownsRun(abortController)) return + this.completePlainFetcherResumeSnapshot(result, abortController) } } @@ -381,8 +404,9 @@ export class VideoGenerationClient { private async processStream( source: AsyncIterable, fallbackRunId: string, - signal: AbortSignal, + abortController: AbortController, ): Promise { + const { signal } = abortController let streamRunId: string | undefined let sawTerminalChunk = false @@ -391,7 +415,9 @@ export class VideoGenerationClient { const chunk = restoreInboundChunk(raw) this.callbacksRef.onChunk?.(chunk) - this.observeResumeSnapshot(chunk) + if (!this.ownsRun(abortController)) return + this.observeResumeSnapshot(chunk, abortController) + if (!this.ownsRun(abortController)) return const chunkRunId = 'runId' in chunk && typeof chunk.runId === 'string' ? chunk.runId @@ -401,6 +427,8 @@ export class VideoGenerationClient { switch (chunk.type) { case 'RUN_STARTED': { streamRunId = chunk.runId + patchAtom(this.snapshotAtom, { runId: chunk.runId }) + if (!this.ownsRun(abortController)) return this.devtoolsBridge.ensureRunStarted(chunk.runId) break } @@ -408,23 +436,30 @@ export class VideoGenerationClient { this.devtoolsBridge.ensureRunStarted(streamRunId ?? fallbackRunId) if (chunk.name === GENERATION_EVENTS.VIDEO_JOB_CREATED) { const { jobId } = chunk.value as { jobId: string } - this.setJobId(jobId) + this.setJobId(jobId, abortController) + if (!this.ownsRun(abortController)) return this.callbacksRef.onJobCreated?.(jobId) + if (!this.ownsRun(abortController)) return } else if (chunk.name === GENERATION_EVENTS.VIDEO_STATUS) { const statusInfo = chunk.value as VideoStatusInfo - this.setVideoStatus(statusInfo) + this.setVideoStatus(statusInfo, abortController) + if (!this.ownsRun(abortController)) return this.callbacksRef.onStatusUpdate?.(statusInfo) + if (!this.ownsRun(abortController)) return if (statusInfo.progress !== undefined) { - this.setProgress(statusInfo.progress) + this.setProgress(statusInfo.progress, undefined, abortController) + if (!this.ownsRun(abortController)) return } } else if (chunk.name === GENERATION_EVENTS.RESULT) { - this.setResult(chunk.value as VideoGenerateResult) + this.setResult(chunk.value as VideoGenerateResult, abortController) + if (!this.ownsRun(abortController)) return } else if (chunk.name === GENERATION_EVENTS.PROGRESS) { const { progress, message } = chunk.value as { progress: number message?: string } - this.setProgress(progress, message) + this.setProgress(progress, message, abortController) + if (!this.ownsRun(abortController)) return } break } @@ -432,7 +467,8 @@ export class VideoGenerationClient { streamRunId = chunk.runId sawTerminalChunk = true this.devtoolsBridge.ensureRunStarted(chunk.runId) - this.setStatus('success') + this.setStatus('success', abortController) + if (!this.ownsRun(abortController)) return break } case 'RUN_ERROR': { @@ -460,17 +496,29 @@ export class VideoGenerationClient { */ stop(): void { const runId = this.devtoolsBridge.getActiveRunId() + const stoppedStatus = this.status if (this.abortController) { this.abortController.abort() this.abortController = null } - this.setIsLoading(false) - if (this.status === 'generating') { - this.setStatus('idle') - if (runId) { + if (runId) { + if (stoppedStatus === 'success') { + this.devtoolsBridge.finishRun(runId, 'run:completed', 'completed') + } else if (stoppedStatus === 'error') { + this.devtoolsBridge.finishRun( + runId, + 'run:errored', + 'errored', + this.error?.message, + ) + } else { this.devtoolsBridge.finishRun(runId, 'run:cancelled', 'cancelled') } } + patchAtom(this.snapshotAtom, { runId: null }) + if (stoppedStatus === 'generating') { + this.setStatus('idle') + } // A stopped run is no longer resumable. Without this the in-memory // snapshot stays `running`, and a remount's `maybeResumeInFlight` would // rejoin a run the user just cancelled. @@ -482,6 +530,7 @@ export class VideoGenerationClient { } this.notifyResumeSnapshotChanged() } + this.setIsLoading(false) } /** @@ -492,6 +541,7 @@ export class VideoGenerationClient { */ reset(): void { this.stop() + if (this.abortController || this.isLoading) return this.setResult(null) this.input = null this.progress = null @@ -560,11 +610,14 @@ export class VideoGenerationClient { this.abortController.abort() this.abortController = null } - this.setIsLoading(false) + if (this.resumeSnapshot?.status !== 'running') { + patchAtom(this.snapshotAtom, { runId: null }) + } this.devtoolsBridge.dispose() this.devtoolsMounted = false this.serverHydrationStarted = false this.rejoinedRunId = undefined + this.setIsLoading(false) } // =========================== @@ -637,24 +690,33 @@ export class VideoGenerationClient { // Private state setters // =========================== - private setResult(rawResult: VideoGenerateResult | null): void { + private setResult( + rawResult: VideoGenerateResult | null, + controller?: AbortController, + ): void { if (rawResult === null) { this.result = null patchAtom(this.snapshotAtom, { result: null }) + if (controller && !this.ownsRun(controller)) return this.callbacksRef.onResultChange?.(null) + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordResultChange() return } const completedStatus = this.createCompletedVideoStatus(rawResult) if (this.progress?.value !== 100) { - this.setProgress(100, this.progress?.message) + this.setProgress(100, this.progress?.message, controller) + if (controller && !this.ownsRun(controller)) return } - this.setJobId(rawResult.jobId) - this.setVideoStatus(completedStatus) + this.setJobId(rawResult.jobId, controller) + if (controller && !this.ownsRun(controller)) return + this.setVideoStatus(completedStatus, controller) + if (controller && !this.ownsRun(controller)) return if (this.callbacksRef.onResult) { const transformed = this.callbacksRef.onResult(rawResult) + if (controller && !this.ownsRun(controller)) return if (transformed === null) { // null return → keep previous result unchanged, just re-emit this.devtoolsBridge.emitState() @@ -664,7 +726,9 @@ export class VideoGenerationClient { // Non-null, non-undefined → use transformed value this.result = transformed patchAtom(this.snapshotAtom, { result: this.result }) + if (controller && !this.ownsRun(controller)) return this.callbacksRef.onResultChange?.(this.result) + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordResultChange() return } @@ -677,46 +741,75 @@ export class VideoGenerationClient { // oxlint-disable-next-line eslint-js/no-restricted-syntax -- TOutput defaults to VideoGenerateResult when no onResult transform is supplied this.result = rawResult as unknown as TOutput patchAtom(this.snapshotAtom, { result: this.result }) + if (controller && !this.ownsRun(controller)) return this.callbacksRef.onResultChange?.(this.result) + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordResultChange() } - private setJobId(jobId: string | null): void { + private setJobId(jobId: string | null, controller?: AbortController): void { this.jobId = jobId patchAtom(this.snapshotAtom, { jobId }) + if (controller && !this.ownsRun(controller)) return this.callbacksRef.onJobIdChange?.(jobId) + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordJobIdChange() } - private setVideoStatus(status: VideoStatusInfo | null): void { - this.videoStatus = status - patchAtom(this.snapshotAtom, { videoStatus: status }) - this.callbacksRef.onVideoStatusChange?.(status) + private setVideoStatus( + status: VideoStatusInfo | null, + controller?: AbortController, + ): void { + this.videoStatus = status ? { ...status } : null + patchAtom(this.snapshotAtom, { + videoStatus: this.videoStatus + ? Object.freeze({ ...this.videoStatus }) + : null, + }) + if (controller && !this.ownsRun(controller)) return + this.callbacksRef.onVideoStatusChange?.(this.videoStatus) + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordVideoStatusChange() } - private setIsLoading(isLoading: boolean): void { + private setIsLoading(isLoading: boolean, controller?: AbortController): void { this.isLoading = isLoading patchAtom(this.snapshotAtom, { isLoading }) + if (controller && !this.ownsRun(controller)) return this.callbacksRef.onLoadingChange?.(isLoading) + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordLoadingChange() } - private setError(error: Error | undefined): void { + private setError( + error: Error | undefined, + controller?: AbortController, + ): void { this.error = error patchAtom(this.snapshotAtom, { error }) + if (controller && !this.ownsRun(controller)) return this.callbacksRef.onErrorChange?.(error) + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordErrorChange(error) } - private setStatus(status: GenerationClientState): void { + private setStatus( + status: GenerationClientState, + controller?: AbortController, + ): void { this.status = status patchAtom(this.snapshotAtom, { status }) + if (controller && !this.ownsRun(controller)) return this.callbacksRef.onStatusChange?.(status) + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordStatusChange(status) } - private setProgress(value: number, message?: string): void { + private setProgress( + value: number, + message?: string, + controller?: AbortController, + ): void { this.progress = { value, ...(message ? { message } : {}), @@ -726,6 +819,7 @@ export class VideoGenerationClient { } else { this.callbacksRef.onProgress?.(value, message) } + if (controller && !this.ownsRun(controller)) return this.devtoolsBridge.recordProgressChange() } @@ -763,6 +857,10 @@ export class VideoGenerationClient { return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}` } + private ownsRun(controller: AbortController): boolean { + return !controller.signal.aborted && this.abortController === controller + } + private createRunContext( runId: string, headers?: Record, @@ -774,22 +872,26 @@ export class VideoGenerationClient { } } - private observeResumeSnapshot(chunk: StreamChunk): void { + private observeResumeSnapshot( + chunk: StreamChunk, + controller: AbortController, + ): void { this.resumeSnapshot = updateGenerationResumeSnapshot( this.resumeSnapshot, chunk, ) - this.notifyResumeSnapshotChanged() + this.notifyResumeSnapshotChanged(controller) } /** Notify the internal snapshot listener AND emit the public resume state. */ - private notifyResumeSnapshotChanged(): void { + private notifyResumeSnapshotChanged(controller?: AbortController): void { this.callbacksRef.onResumeSnapshotChange?.(this.resumeSnapshot) - this.emitResumeState() + if (controller && !this.ownsRun(controller)) return + this.emitResumeState(controller) } /** Derive the public `resumeState` from the internal snapshot. */ - private emitResumeState(): void { + private emitResumeState(controller?: AbortController): void { const snapshot = this.resumeSnapshot const state = snapshot?.resumeState const resumeState: GenerationResumeState | null = state @@ -801,6 +903,7 @@ export class VideoGenerationClient { } : null patchAtom(this.snapshotAtom, { runId: resumeState?.runId ?? null }) + if (controller && !this.ownsRun(controller)) return this.callbacksRef.onResumeStateChange?.(resumeState) } @@ -914,7 +1017,10 @@ export class VideoGenerationClient { * stale `error` from a previous run is intentionally dropped — this run * succeeded. */ - private completePlainFetcherResumeSnapshot(rawResult: unknown): void { + private completePlainFetcherResumeSnapshot( + rawResult: unknown, + controller: AbortController, + ): void { const previous = this.resumeSnapshot const result = createGenerationResultSnapshot(rawResult) this.resumeSnapshot = { @@ -931,7 +1037,7 @@ export class VideoGenerationClient { ? { result: { ...previous.result } } : {}), } - this.notifyResumeSnapshotChanged() + this.notifyResumeSnapshotChanged(controller) } /** @@ -940,14 +1046,19 @@ export class VideoGenerationClient { * mark the snapshot `error`, leaving a persisted record that claims the * run is still in flight. */ - private recordResumeSnapshotError(error: Error): void { + private recordResumeSnapshotError( + error: Error, + controller?: AbortController, + ): void { // Surface the failure on the observable fields FIRST, unconditionally (see // the note in GenerationClient.recordResumeSnapshotError): a RUN_ERROR // already flipped the snapshot to `error`, so the early-return would else // skip this and leave `status` stuck on `generating`. The guard avoids a // duplicate `error` emission on the live `generate()` path. - if (this.status !== 'error') this.setStatus('error') - this.setError(error) + if (this.status !== 'error') this.setStatus('error', controller) + if (controller && !this.ownsRun(controller)) return + this.setError(error, controller) + if (controller && !this.ownsRun(controller)) return if (this.resumeSnapshot?.status === 'error') return if (!this.resumeSnapshot && !this.serverDriven) return const previous = this.resumeSnapshot @@ -962,7 +1073,7 @@ export class VideoGenerationClient { ...(previous?.result ? { result: { ...previous.result } } : {}), error: { message: error.message }, } - this.notifyResumeSnapshotChanged() + this.notifyResumeSnapshotChanged(controller) } /** @@ -1079,23 +1190,26 @@ export class VideoGenerationClient { this.rejoinedRunId = runId const controller = new AbortController() this.abortController = controller - this.setIsLoading(true) - this.setStatus('generating') + this.setIsLoading(true, controller) + if (!this.ownsRun(controller)) return + this.setStatus('generating', controller) + if (!this.ownsRun(controller)) return void (async () => { try { await this.processStream( joinRun(runId, controller.signal), runId, - controller.signal, + controller, ) } catch (error) { - if (!controller.signal.aborted) { + if (this.ownsRun(controller)) { const failure = error instanceof Error ? error : new Error(String(error)) // Settles `status`/`error` AND rewrites the snapshot to a terminal // `error` with a null `resumeState`, so the next mount does not // rejoin this run again. - this.recordResumeSnapshotError(failure) + this.recordResumeSnapshotError(failure, controller) + if (!this.ownsRun(controller)) return this.callbacksRef.onError?.(failure) } } finally { @@ -1103,8 +1217,8 @@ export class VideoGenerationClient { // fresh `generate()` may have replaced the controller while the tail // was settling, and that live run owns `isLoading` now. if (this.abortController === controller) { - this.abortController = null - this.setIsLoading(false) + this.setIsLoading(false, controller) + if (this.ownsRun(controller)) this.abortController = null } } })() diff --git a/packages/ai-client/tests/audio-recorder.test.ts b/packages/ai-client/tests/audio-recorder.test.ts index 49c773b0cd..387308fb8b 100644 --- a/packages/ai-client/tests/audio-recorder.test.ts +++ b/packages/ai-client/tests/audio-recorder.test.ts @@ -75,6 +75,21 @@ afterEach(() => { }) describe('AudioRecorder', () => { + it('uses Set subscription behavior and reports repeated states', async () => { + const recorder = new AudioRecorder() + const listener = vi.fn() + const first = recorder.subscribe(listener) + recorder.subscribe(listener) + + getUserMedia.mockRejectedValueOnce(new Error('denied')) + await expect(recorder.start()).rejects.toThrow('denied') + expect(listener).toHaveBeenCalledTimes(1) + first() + getUserMedia.mockRejectedValueOnce(new Error('denied')) + await expect(recorder.start()).rejects.toThrow('denied') + expect(listener).toHaveBeenCalledTimes(1) + }) + it('isSupported() is true when media APIs exist', () => { expect(AudioRecorder.isSupported()).toBe(true) }) diff --git a/packages/ai-client/tests/byok-client.test.ts b/packages/ai-client/tests/byok-client.test.ts index b2bab1c253..8851378c86 100644 --- a/packages/ai-client/tests/byok-client.test.ts +++ b/packages/ai-client/tests/byok-client.test.ts @@ -8,6 +8,34 @@ describe('defineByok memory', () => { vi.unstubAllGlobals() }) + it('isolates and freezes nested snapshot state', async () => { + const byok = defineByok() + await byok.update('openai', 'sk-abcdefghij') + const snapshot = byok.getSnapshot() + + expect(Object.isFrozen(snapshot)).toBe(true) + expect(Object.isFrozen(snapshot.status)).toBe(true) + expect(Object.isFrozen(snapshot.status.openai)).toBe(true) + Reflect.set(snapshot.status.openai!, 'masked', 'leak') + expect(byok.getSnapshot().status.openai).toEqual({ + state: 'set', + masked: 'ghij', + }) + }) + + it('uses Set subscription behavior', () => { + const byok = defineByok() + const listener = vi.fn() + const first = byok.subscribe(listener) + byok.subscribe(listener) + + byok.request('openai', 'missing') + expect(listener).toHaveBeenCalledTimes(1) + first() + byok.request('openai', 'missing') + expect(listener).toHaveBeenCalledTimes(1) + }) + it('updates and snapshots without exposing the raw key', async () => { const byok = defineByok({ storage: memoryStorage() }) await byok.update('openai', 'sk-abcdefghij') diff --git a/packages/ai-client/tests/byok-generation-client.test.ts b/packages/ai-client/tests/byok-generation-client.test.ts index 948635c5b2..eeceb405e9 100644 --- a/packages/ai-client/tests/byok-generation-client.test.ts +++ b/packages/ai-client/tests/byok-generation-client.test.ts @@ -87,6 +87,64 @@ describe('GenerationClient byok', () => { expect(client.getError()).toBeInstanceOf(ByokMissingError) }) + it('stops the failed run when the BYOK request subscriber stops', async () => { + const byok = defineByok({ storage: memoryStorage() }) + await byok.update('elevenlabs', ELEVENLABS_KEY) + const onErrorChange = vi.fn() + let client!: GenerationClient<{ prompt: string }, { id: string }> + client = new GenerationClient({ + fetcher: async (): Promise<{ id: string }> => { + throw new ByokMissingError('elevenlabs') + }, + byok, + byokProvider: () => 'elevenlabs', + onErrorChange, + }) + byok.subscribe(() => { + if (byok.getSnapshot().prompt) client.stop() + }) + + await client.generate({ prompt: 'first' }) + + expect(client.getStatus()).toBe('idle') + expect(client.getError()).toBeUndefined() + expect(onErrorChange).not.toHaveBeenCalledWith(expect.any(Error)) + }) + + it('does not let a failed BYOK run overwrite a replacement', async () => { + const byok = defineByok({ storage: memoryStorage() }) + await byok.update('elevenlabs', ELEVENLABS_KEY) + let releaseSecond!: (value: { id: string }) => void + const secondResult = new Promise<{ id: string }>((resolve) => { + releaseSecond = resolve + }) + let secondGenerate: Promise | undefined + let replaced = false + const client = new GenerationClient({ + fetcher: async (input) => { + if (input.prompt === 'first') { + throw new ByokMissingError('elevenlabs') + } + return secondResult + }, + byok, + byokProvider: () => 'elevenlabs', + }) + byok.subscribe(() => { + if (!byok.getSnapshot().prompt || replaced) return + replaced = true + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + }) + + await client.generate({ prompt: 'first' }) + + expect(client.getStatus()).toBe('generating') + expect(client.getError()).toBeUndefined() + releaseSecond({ id: 'second' }) + await secondGenerate + }) + it('requests a missing key when the fetcher returns a byokMissing Response', async () => { const byok = defineByok({ storage: memoryStorage() }) await byok.update('elevenlabs', ELEVENLABS_KEY) diff --git a/packages/ai-client/tests/byok-video-generation-client.test.ts b/packages/ai-client/tests/byok-video-generation-client.test.ts index 166b3eb4d4..2b7633c385 100644 --- a/packages/ai-client/tests/byok-video-generation-client.test.ts +++ b/packages/ai-client/tests/byok-video-generation-client.test.ts @@ -92,6 +92,64 @@ describe('VideoGenerationClient byok', () => { expect(client.getError()).toBeInstanceOf(ByokMissingError) }) + it('stops the failed run when the BYOK request subscriber stops', async () => { + const byok = defineByok({ storage: memoryStorage() }) + await byok.update('elevenlabs', ELEVENLABS_KEY) + const onErrorChange = vi.fn() + let client!: VideoGenerationClient + client = new VideoGenerationClient({ + fetcher: async () => { + throw new ByokMissingError('elevenlabs') + }, + byok, + byokProvider: () => 'elevenlabs', + onErrorChange, + }) + byok.subscribe(() => { + if (byok.getSnapshot().prompt) client.stop() + }) + + await client.generate({ prompt: 'first' }) + + expect(client.getStatus()).toBe('idle') + expect(client.getError()).toBeUndefined() + expect(onErrorChange).not.toHaveBeenCalledWith(expect.any(Error)) + }) + + it('does not let a failed BYOK run overwrite a replacement', async () => { + const byok = defineByok({ storage: memoryStorage() }) + await byok.update('elevenlabs', ELEVENLABS_KEY) + let releaseSecond!: (value: typeof VIDEO_RESULT) => void + const secondResult = new Promise((resolve) => { + releaseSecond = resolve + }) + let secondGenerate: Promise | undefined + let replaced = false + const client = new VideoGenerationClient({ + fetcher: async (input) => { + if (input.prompt === 'first') { + throw new ByokMissingError('elevenlabs') + } + return secondResult + }, + byok, + byokProvider: () => 'elevenlabs', + }) + byok.subscribe(() => { + if (!byok.getSnapshot().prompt || replaced) return + replaced = true + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + }) + + await client.generate({ prompt: 'first' }) + + expect(client.getStatus()).toBe('generating') + expect(client.getError()).toBeUndefined() + releaseSecond(VIDEO_RESULT) + await secondGenerate + }) + it('requests a missing key when the fetcher returns a byokMissing Response', async () => { const byok = defineByok({ storage: memoryStorage() }) await byok.update('elevenlabs', ELEVENLABS_KEY) diff --git a/packages/ai-client/tests/chat-client.test.ts b/packages/ai-client/tests/chat-client.test.ts index 2be80b49fb..e3f8a5cb74 100644 --- a/packages/ai-client/tests/chat-client.test.ts +++ b/packages/ai-client/tests/chat-client.test.ts @@ -98,6 +98,75 @@ describe('ChatClient', () => { stop() }) + it('isolates nested message parts from callers and old snapshots', () => { + const adapter = createMockConnectionAdapter() + const source = { + type: 'url' as const, + value: 'https://example.com/a.png', + } + const client = new ChatClient({ + connection: adapter, + initialMessages: [ + { + id: 'msg-1', + role: 'user', + parts: [{ type: 'image', source }], + createdAt: new Date(), + }, + ], + }) + const oldSnapshot = client.getSnapshot() + + source.value = 'https://example.com/changed.png' + expect(oldSnapshot.messages[0]?.parts[0]).toMatchObject({ + source: { value: 'https://example.com/a.png' }, + }) + const publishedPart = oldSnapshot.messages[0]?.parts[0] + expect(Object.isFrozen(publishedPart)).toBe(true) + if (!publishedPart || !('source' in publishedPart)) { + throw new Error('Expected a message part with a source') + } + expect(Object.isFrozen(publishedPart.source)).toBe(true) + }) + + it('isolates messages published by normal updates', () => { + const client = new ChatClient({ + connection: createMockConnectionAdapter(), + }) + const oldSnapshot = client.getSnapshot() + const source = { + type: 'url' as const, + value: 'https://example.com/updated.png', + } + + client.setMessagesManually([ + { + id: 'updated-1', + role: 'user', + parts: [{ type: 'image', source }], + }, + ]) + + const snapshot = client.getSnapshot() + const part = snapshot.messages[0]?.parts[0] + expect(snapshot).not.toBe(oldSnapshot) + expect(oldSnapshot.messages).toEqual([]) + expect(Object.isFrozen(snapshot.messages)).toBe(true) + expect(Object.isFrozen(snapshot.messages[0])).toBe(true) + expect(Object.isFrozen(snapshot.messages[0]?.parts)).toBe(true) + expect(Object.isFrozen(part)).toBe(true) + expect(part).toMatchObject({ source }) + + if (!part || !('source' in part)) { + throw new Error('Expected a message part with a source') + } + const publishedSource = part.source + expect(Object.isFrozen(publishedSource)).toBe(true) + expect(Reflect.set(publishedSource, 'value', 'changed')).toBe(false) + expect(client.getMessages()[0]?.parts[0]).toMatchObject({ source }) + expect(client.getSnapshot()).toBe(snapshot) + }) + it('should initialize with provided messages', () => { const adapter = createMockConnectionAdapter() const initialMessages: Array = [ @@ -2278,6 +2347,61 @@ describe('ChatClient', () => { { type: 'text', content: 'Second' }, ]) }) + + it('isolates queue entries published by normal updates', async () => { + const client = new ChatClient({ + connection: createMockConnectionAdapter({ + chunks: createTextChunks('Response'), + chunkDelay: 100, + }), + }) + const firstSend = client.sendMessage('First') + const oldSnapshot = client.getSnapshot() + + await client.sendMessage({ + content: [ + { + type: 'image', + source: { + type: 'url', + value: 'https://example.com/queued.png', + }, + }, + ], + }) + + const snapshot = client.getSnapshot() + const queuedContent = snapshot.queue[0]?.content + expect(snapshot).not.toBe(oldSnapshot) + expect(oldSnapshot.queue).toEqual([]) + expect(Object.isFrozen(snapshot.queue)).toBe(true) + expect(Object.isFrozen(snapshot.queue[0])).toBe(true) + expect(typeof queuedContent).toBe('object') + if (typeof queuedContent === 'object') { + expect(Object.isFrozen(queuedContent)).toBe(true) + expect(Object.isFrozen(queuedContent.content)).toBe(true) + const queuedPart = Array.isArray(queuedContent.content) + ? queuedContent.content[0] + : undefined + expect(Object.isFrozen(queuedPart)).toBe(true) + if (!queuedPart || !('source' in queuedPart)) { + throw new Error('Expected a queued content part with a source') + } + const publishedSource = queuedPart.source + expect(Object.isFrozen(publishedSource)).toBe(true) + expect(Reflect.set(publishedSource, 'value', 'changed')).toBe(false) + } + expect(client.getQueue()[0]?.content).toMatchObject({ + content: [ + { + source: { value: 'https://example.com/queued.png' }, + }, + ], + }) + expect(client.getSnapshot()).toBe(snapshot) + + await firstSend + }) }) describe('append', () => { diff --git a/packages/ai-client/tests/client-snapshots.test.ts b/packages/ai-client/tests/client-snapshots.test.ts index 90efed3e5b..bcbf6b8636 100644 --- a/packages/ai-client/tests/client-snapshots.test.ts +++ b/packages/ai-client/tests/client-snapshots.test.ts @@ -8,6 +8,22 @@ import { import type { RealtimeAdapter } from '../src' describe('client UI snapshots', () => { + it('publishes and clears the active generation run id', async () => { + let resolve!: (value: { id: string }) => void + const pending = new Promise<{ id: string }>((done) => { + resolve = done + }) + const client = new GenerationClient({ fetcher: async () => pending }) + + const generating = client.generate({ prompt: 'test' }) + expect(client.getSnapshot().runId).toEqual(expect.any(String)) + expect(client.getSnapshot().runId).not.toBe('') + + resolve({ id: '1' }) + await generating + expect(client.getSnapshot().runId).toBeNull() + }) + it('GenerationClient notifies subscribe and keeps getSnapshot identity until a change', async () => { const client = new GenerationClient({ fetcher: async () => ({ id: '1' }), @@ -55,6 +71,10 @@ describe('client UI snapshots', () => { expect(client.getSnapshot().status).toBe('success') expect(client.getSnapshot().result).toEqual(result) expect(client.getSnapshot()).not.toBe(snapshot) + const completed = client.getSnapshot() + expect(Object.isFrozen(completed.videoStatus)).toBe(true) + Reflect.set(completed.videoStatus!, 'status', 'failed') + expect(client.getVideoStatus()?.status).toBe('completed') stop() }) @@ -91,4 +111,43 @@ describe('client UI snapshots', () => { stop() client.destroy() }) + + it('RealtimeClient does not read connection control from its public snapshot', async () => { + const getToken = vi.fn(async () => { + throw new Error('token attempted') + }) + const client = new RealtimeClient({ + getToken, + adapter: { provider: 'test', connect: vi.fn() }, + }) + + Reflect.set(client.getSnapshot(), 'status', 'connected') + await expect(client.connect()).rejects.toThrow('token attempted') + expect(getToken).toHaveBeenCalledOnce() + }) + + it('RealtimeClient isolates connection control from state change callbacks', async () => { + const getToken = vi.fn(async () => ({ + provider: 'test', + token: 'token', + expiresAt: Date.now() + 120_000, + config: {}, + })) + const connect = vi.fn(async () => { + throw new Error('connect attempted') + }) + const client = new RealtimeClient({ + getToken, + adapter: { provider: 'test', connect }, + }) + client.onStateChange((state) => { + Reflect.set(state, 'status', 'connected') + }) + + await expect(client.connect()).rejects.toThrow('connect attempted') + await expect(client.connect()).rejects.toThrow('connect attempted') + + expect(getToken).toHaveBeenCalledTimes(2) + expect(connect).toHaveBeenCalledTimes(2) + }) }) diff --git a/packages/ai-client/tests/generation-client.test.ts b/packages/ai-client/tests/generation-client.test.ts index 8b941689dd..cf93a55495 100644 --- a/packages/ai-client/tests/generation-client.test.ts +++ b/packages/ai-client/tests/generation-client.test.ts @@ -115,6 +115,7 @@ describe('GenerationClient', () => { expect(onResultChange).toHaveBeenCalledWith(mockResult) expect(client.getResult()).toEqual(mockResult) expect(client.getStatus()).toBe('success') + expect(client.getSnapshot().runId).toBeNull() expect(client.getIsLoading()).toBe(false) }) @@ -274,6 +275,7 @@ describe('GenerationClient', () => { expect(onError).toHaveBeenCalledWith(expect.any(Error)) expect(client.getStatus()).toBe('error') expect(client.getError()?.message).toBe('Generation failed') + expect(client.getSnapshot().runId).toBeNull() }) it('should report progress from CUSTOM progress events', async () => { @@ -408,14 +410,366 @@ describe('GenerationClient', () => { const generatePromise = client.generate({ prompt: 'test' }) expect(client.getIsLoading()).toBe(true) + expect(client.getSnapshot().runId).toEqual(expect.any(String)) client.stop() expect(client.getIsLoading()).toBe(false) expect(client.getStatus()).toBe('idle') + expect(client.getSnapshot().runId).toBeNull() resolvePromise!({ id: '1' }) await generatePromise }) + + it('should not overwrite a run started by the stopped loading callback', async () => { + let releaseFirst!: (value: { id: string }) => void + let releaseSecond!: (value: { id: string }) => void + const firstResult = new Promise<{ id: string }>((resolve) => { + releaseFirst = resolve + }) + const secondResult = new Promise<{ id: string }>((resolve) => { + releaseSecond = resolve + }) + let secondGenerate: Promise | undefined + let secondRunId: string | null = null + let replaced = false + let client!: GenerationClient<{ prompt: string }, { id: string }> + client = new GenerationClient({ + fetcher: async (input) => + input.prompt === 'first' ? firstResult : secondResult, + onLoadingChange: (isLoading) => { + if (isLoading || replaced) return + replaced = true + secondGenerate = client.generate({ prompt: 'second' }) + secondRunId = client.getSnapshot().runId + }, + }) + + const firstGenerate = client.generate({ prompt: 'first' }) + client.stop() + + expect(secondRunId).not.toBeNull() + expect(client.getSnapshot().runId).toBe(secondRunId) + expect(client.getIsLoading()).toBe(true) + expect(client.getStatus()).toBe('generating') + releaseFirst({ id: 'first' }) + releaseSecond({ id: 'second' }) + await Promise.all([firstGenerate, secondGenerate]) + }) + + it('should stop before transport work when loading callback stops', async () => { + const fetcher = vi.fn(async () => ({ id: 'unexpected' })) + let client!: GenerationClient<{ prompt: string }, { id: string }> + client = new GenerationClient({ + fetcher, + onLoadingChange: (isLoading) => { + if (isLoading) client.stop() + }, + }) + + await client.generate({ prompt: 'test' }) + + expect(fetcher).not.toHaveBeenCalled() + expect(client.getStatus()).toBe('idle') + expect(client.getSnapshot().runId).toBeNull() + }) + + it('should not complete resume state after success callback stops', async () => { + let client!: GenerationClient<{ prompt: string }, { id: string }> + client = new GenerationClient({ + fetcher: async () => ({ id: 'result' }), + onStatusChange: (status) => { + if (status === 'success') client.stop() + }, + }) + + await client.generate({ prompt: 'test' }) + + expect(client.getStatus()).toBe('success') + expect(client.getSnapshot().runId).toBeNull() + expect(client.getResumeSnapshot()).toBeUndefined() + }) + + it('should not let initialization continue after loading callback starts a replacement', async () => { + let releaseSecond!: (value: { id: string }) => void + const secondResult = new Promise<{ id: string }>((resolve) => { + releaseSecond = resolve + }) + const prompts: Array = [] + let client!: GenerationClient<{ prompt: string }, { id: string }> + let replaced = false + let secondGenerate: Promise | undefined + client = new GenerationClient({ + fetcher: async (input) => { + prompts.push(input.prompt) + return input.prompt === 'second' ? secondResult : { id: 'first' } + }, + onLoadingChange: (isLoading) => { + if (!isLoading || replaced) return + replaced = true + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + }, + }) + + await client.generate({ prompt: 'first' }) + + expect(prompts).toEqual(['second']) + expect(client.getStatus()).toBe('generating') + releaseSecond({ id: 'second' }) + await secondGenerate + }) + + it('should not let an old result callback complete a replacement run', async () => { + let releaseSecond!: (value: { id: string }) => void + const secondResult = new Promise<{ id: string }>((resolve) => { + releaseSecond = resolve + }) + let client!: GenerationClient<{ prompt: string }, { id: string }> + let secondGenerate: Promise | undefined + let secondRunId: string | null = null + client = new GenerationClient({ + fetcher: async (input) => + input.prompt === 'first' ? { id: 'first' } : secondResult, + onResultChange: (result) => { + if (result?.id !== 'first') return + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + secondRunId = client.getSnapshot().runId + }, + }) + + await client.generate({ prompt: 'first' }) + + expect(secondRunId).not.toBeNull() + expect(client.getSnapshot().runId).toBe(secondRunId) + expect(client.getStatus()).toBe('generating') + expect(client.getResumeSnapshot()).toBeUndefined() + releaseSecond({ id: 'second' }) + await secondGenerate + }) + + it('should not let an old error callback overwrite a replacement run', async () => { + let releaseSecond!: (value: { id: string }) => void + const secondResult = new Promise<{ id: string }>((resolve) => { + releaseSecond = resolve + }) + let client!: GenerationClient<{ prompt: string }, { id: string }> + let secondGenerate: Promise | undefined + let secondRunId: string | null = null + client = new GenerationClient({ + fetcher: async (input) => { + if (input.prompt === 'first') throw new Error('first failed') + return secondResult + }, + onErrorChange: (error) => { + if (error?.message !== 'first failed') return + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + secondRunId = client.getSnapshot().runId + }, + }) + + await client.generate({ prompt: 'first' }) + + expect(secondRunId).not.toBeNull() + expect(client.getSnapshot().runId).toBe(secondRunId) + expect(client.getStatus()).toBe('generating') + releaseSecond({ id: 'second' }) + await secondGenerate + }) + + it('should remain stopped when the error callback stops', async () => { + let client!: GenerationClient<{ prompt: string }, { id: string }> + client = new GenerationClient({ + fetcher: async (): Promise<{ id: string }> => { + throw new Error('failed') + }, + onErrorChange: (error) => { + if (error) client.stop() + }, + }) + + await client.generate({ prompt: 'test' }) + + expect(client.getStatus()).toBe('idle') + expect(client.getSnapshot().runId).toBeNull() + }) + + it('should not let an old error status callback overwrite a replacement run', async () => { + let releaseSecond!: (value: { id: string }) => void + const secondResult = new Promise<{ id: string }>((resolve) => { + releaseSecond = resolve + }) + let client!: GenerationClient<{ prompt: string }, { id: string }> + let secondGenerate: Promise | undefined + let replaced = false + client = new GenerationClient({ + fetcher: async (input) => { + if (input.prompt === 'first') throw new Error('first failed') + return secondResult + }, + onStatusChange: (status) => { + if (status !== 'error' || replaced) return + replaced = true + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + }, + }) + + await client.generate({ prompt: 'first' }) + + expect(client.getStatus()).toBe('generating') + expect(client.getError()).toBeUndefined() + releaseSecond({ id: 'second' }) + await secondGenerate + }) + + it('should stop before transport when the run id subscriber stops', async () => { + const fetcher = vi.fn(async () => ({ id: 'unexpected' })) + const onLoadingChange = vi.fn() + const client = new GenerationClient({ fetcher, onLoadingChange }) + client.subscribe(() => { + if (client.getSnapshot().runId) client.stop() + }) + + await client.generate({ prompt: 'test' }) + + expect(fetcher).not.toHaveBeenCalled() + expect(onLoadingChange).not.toHaveBeenCalledWith(true) + expect(client.getSnapshot().runId).toBeNull() + }) + + it('should not continue an old run after the run id subscriber starts a replacement', async () => { + let releaseSecond!: (value: { id: string }) => void + const secondResult = new Promise<{ id: string }>((resolve) => { + releaseSecond = resolve + }) + const prompts: Array = [] + let secondGenerate: Promise | undefined + let replaced = false + const client = new GenerationClient({ + fetcher: async (input) => { + prompts.push(input.prompt) + return input.prompt === 'second' ? secondResult : { id: 'first' } + }, + }) + client.subscribe(() => { + if (!client.getSnapshot().runId || replaced) return + replaced = true + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + }) + + await client.generate({ prompt: 'first' }) + + expect(prompts).toEqual(['second']) + expect(client.getStatus()).toBe('generating') + releaseSecond({ id: 'second' }) + await secondGenerate + }) + + it('should not call the loading callback after its snapshot subscriber stops', async () => { + const fetcher = vi.fn(async () => ({ id: 'unexpected' })) + const onLoadingChange = vi.fn() + const client = new GenerationClient({ fetcher, onLoadingChange }) + client.subscribe(() => { + if (client.getSnapshot().isLoading) client.stop() + }) + + await client.generate({ prompt: 'test' }) + + expect(fetcher).not.toHaveBeenCalled() + expect(onLoadingChange).not.toHaveBeenCalledWith(true) + }) + + it('should not call the old status callback after its subscriber starts a replacement', async () => { + let releaseSecond!: (value: { id: string }) => void + const secondResult = new Promise<{ id: string }>((resolve) => { + releaseSecond = resolve + }) + const generatingStatuses: Array = [] + let secondGenerate: Promise | undefined + let replaced = false + const client = new GenerationClient({ + fetcher: async (input) => + input.prompt === 'second' ? secondResult : { id: 'first' }, + onStatusChange: (status) => { + if (status === 'generating') generatingStatuses.push(status) + }, + }) + client.subscribe(() => { + if (client.getSnapshot().status !== 'generating' || replaced) return + replaced = true + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + }) + + await client.generate({ prompt: 'first' }) + + expect(generatingStatuses).toEqual(['generating']) + expect(client.getStatus()).toBe('generating') + releaseSecond({ id: 'second' }) + await secondGenerate + }) + + it('should not call the old result callback after its subscriber starts a replacement', async () => { + let releaseSecond!: (value: { id: string }) => void + const secondResult = new Promise<{ id: string }>((resolve) => { + releaseSecond = resolve + }) + const onResultChange = vi.fn() + let secondGenerate: Promise | undefined + let replaced = false + const client = new GenerationClient({ + fetcher: async (input) => + input.prompt === 'second' ? secondResult : { id: 'first' }, + onResultChange, + }) + client.subscribe(() => { + if (client.getResult()?.id !== 'first' || replaced) return + replaced = true + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + }) + + await client.generate({ prompt: 'first' }) + + expect(onResultChange).not.toHaveBeenCalledWith({ id: 'first' }) + expect(client.getStatus()).toBe('generating') + releaseSecond({ id: 'second' }) + await secondGenerate + }) + + it('should not call the old error callback after its subscriber starts a replacement', async () => { + let releaseSecond!: (value: { id: string }) => void + const secondResult = new Promise<{ id: string }>((resolve) => { + releaseSecond = resolve + }) + const onErrorChange = vi.fn() + let secondGenerate: Promise | undefined + let replaced = false + const client = new GenerationClient({ + fetcher: async (input) => { + if (input.prompt === 'first') throw new Error('first failed') + return secondResult + }, + onErrorChange, + }) + client.subscribe(() => { + if (client.getError()?.message !== 'first failed' || replaced) return + replaced = true + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + }) + + await client.generate({ prompt: 'first' }) + + expect(onErrorChange).not.toHaveBeenCalledWith(expect.any(Error)) + expect(client.getStatus()).toBe('generating') + releaseSecond({ id: 'second' }) + await secondGenerate + }) }) describe('reset()', () => { @@ -432,6 +786,82 @@ describe('GenerationClient', () => { expect(client.getResult()).toBeNull() expect(client.getError()).toBeUndefined() expect(client.getStatus()).toBe('idle') + expect(client.getSnapshot().runId).toBeNull() + }) + + it('should not overwrite a run started by the reset loading callback', async () => { + let releaseFirst!: (value: { id: string }) => void + let releaseSecond!: (value: { id: string }) => void + const firstResult = new Promise<{ id: string }>((resolve) => { + releaseFirst = resolve + }) + const secondResult = new Promise<{ id: string }>((resolve) => { + releaseSecond = resolve + }) + let replaceOnStop = false + let secondGenerate: Promise | undefined + let secondRunId: string | null = null + let client!: GenerationClient<{ prompt: string }, { id: string }> + client = new GenerationClient({ + fetcher: async (input) => { + if (input.prompt === 'seed') return { id: 'seed' } + return input.prompt === 'first' ? firstResult : secondResult + }, + onLoadingChange: (isLoading) => { + if (isLoading || !replaceOnStop) return + replaceOnStop = false + secondGenerate = client.generate({ prompt: 'second' }) + secondRunId = client.getSnapshot().runId + }, + }) + await client.generate({ prompt: 'seed' }) + const firstGenerate = client.generate({ prompt: 'first' }) + replaceOnStop = true + + client.reset() + + expect(secondRunId).not.toBeNull() + expect(client.getSnapshot().runId).toBe(secondRunId) + expect(client.getStatus()).toBe('generating') + expect(client.getIsLoading()).toBe(true) + expect(client.getResult()).toEqual({ id: 'seed' }) + releaseFirst({ id: 'first' }) + releaseSecond({ id: 'second' }) + await Promise.all([firstGenerate, secondGenerate]) + }) + }) + + describe('dispose()', () => { + it('should not overwrite a run started by the dispose loading callback', async () => { + const first = createDeferred<{ id: string }>() + const second = createDeferred<{ id: string }>() + let replaceOnDispose = false + let secondGenerate: Promise | undefined + let secondRunId: string | null = null + let client!: GenerationClient<{ prompt: string }, { id: string }> + client = new GenerationClient({ + fetcher: (input) => + input.prompt === 'first' ? first.promise : second.promise, + onLoadingChange: (isLoading) => { + if (isLoading || !replaceOnDispose) return + replaceOnDispose = false + client.mountDevtools() + secondGenerate = client.generate({ prompt: 'second' }) + secondRunId = client.getSnapshot().runId + }, + }) + const firstGenerate = client.generate({ prompt: 'first' }) + replaceOnDispose = true + + client.dispose() + + expect(secondRunId).not.toBeNull() + expect(client.getSnapshot().runId).toBe(secondRunId) + expect(client.getStatus()).toBe('generating') + expect(client.getIsLoading()).toBe(true) + first.resolve({ id: 'first' }) + second.resolve({ id: 'second' }) + await Promise.all([firstGenerate, secondGenerate]) }) }) @@ -560,6 +990,75 @@ describe('GenerationClient', () => { expect(client.getStatus()).toBe('idle') }) + it('should not publish a run id after onChunk stops the run', async () => { + const connection: ConnectConnectionAdapter = { + async *connect() { + yield { + type: EventType.RUN_STARTED as const, + runId: 'run-1', + threadId: 'thread-1', + timestamp: Date.now(), + } + }, + } + let client!: GenerationClient<{ prompt: string }, { id: string }> + client = new GenerationClient({ + connection, + onChunk: (chunk) => { + if (chunk.type === EventType.RUN_STARTED) client.stop() + }, + }) + + await client.generate({ prompt: 'test' }) + + expect(client.getSnapshot().runId).toBeNull() + expect(client.getStatus()).toBe('idle') + }) + + it('should not let an old onChunk continuation overwrite a newer run', async () => { + let connectionCount = 0 + let releaseSecond!: () => void + const secondBlocked = new Promise((resolve) => { + releaseSecond = resolve + }) + const connection: ConnectConnectionAdapter = { + async *connect() { + connectionCount++ + if (connectionCount === 1) { + yield { + type: EventType.RUN_STARTED as const, + runId: 'run-1', + threadId: 'thread-1', + timestamp: Date.now(), + } + return + } + await secondBlocked + }, + } + let client!: GenerationClient<{ prompt: string }, { id: string }> + let secondGenerate: Promise | undefined + let secondLocalRunId: string | null = null + client = new GenerationClient({ + connection, + onChunk: (chunk) => { + if (chunk.type !== EventType.RUN_STARTED) return + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + secondLocalRunId = client.getSnapshot().runId + }, + }) + + await client.generate({ prompt: 'first' }) + + expect(secondLocalRunId).not.toBeNull() + expect(client.getSnapshot().runId).toBe(secondLocalRunId) + expect(client.getIsLoading()).toBe(true) + client.stop() + releaseSecond() + await secondGenerate + }) + it('should not let a stopped run clear the controller for a newer generation', async () => { const firstAborted = createDeferred() const firstCanFinish = createDeferred() @@ -621,12 +1120,14 @@ describe('GenerationClient', () => { await waitForCondition(() => { expect(signals).toHaveLength(2) expect(client.getIsLoading()).toBe(true) + expect(client.getSnapshot().runId).toBe('run-2') }) firstCanFinish.resolve(undefined) await firstGenerate expect(client.getIsLoading()).toBe(true) + expect(client.getSnapshot().runId).toBe('run-2') client.stop() expect(signals[1]?.aborted).toBe(true) @@ -758,12 +1259,14 @@ describe('GenerationClient', () => { await waitForCondition(() => { expect(signals).toHaveLength(2) expect(client.getIsLoading()).toBe(true) + expect(client.getSnapshot().runId).toBe('run-2') }) firstCanFinish.resolve(undefined) await firstGenerate expect(client.getIsLoading()).toBe(true) + expect(client.getSnapshot().runId).toBe('run-2') client.stop() expect(signals[1]?.aborted).toBe(true) diff --git a/packages/ai-client/tests/generation-devtools.test.ts b/packages/ai-client/tests/generation-devtools.test.ts index 133519a42d..ce967ca388 100644 --- a/packages/ai-client/tests/generation-devtools.test.ts +++ b/packages/ai-client/tests/generation-devtools.test.ts @@ -289,6 +289,184 @@ describe('generation client devtools bridge', () => { client.dispose() }) + it('finishes a generation devtools run when a runId subscriber stops it', async () => { + const fetcher = vi.fn(async () => ({ text: 'done' })) + const client = new GenerationClient({ + threadId: 'generation-run-id-stop', + fetcher, + devtools: { hookName: 'useGenerationRunIdStop' }, + }) + const unsubscribe = client.subscribe(() => { + if (client.getSnapshot().runId) client.stop() + }) + + await client.generate({ prompt: 'stop early' }) + + expect(fetcher).not.toHaveBeenCalled() + expect(latestSnapshotState().activeRunId).toBeNull() + unsubscribe() + client.dispose() + }) + + it('finishes a generation devtools run when loading callback stops it', async () => { + const fetcher = vi.fn(async () => ({ text: 'done' })) + let client: GenerationClient<{ prompt: string }, { text: string }> + client = new GenerationClient({ + threadId: 'generation-loading-stop', + fetcher, + devtools: { hookName: 'useGenerationLoadingStop' }, + onLoadingChange: (isLoading) => { + if (isLoading) client.stop() + }, + }) + + await client.generate({ prompt: 'stop while loading' }) + + expect(fetcher).not.toHaveBeenCalled() + expect(latestSnapshotState().activeRunId).toBeNull() + client.dispose() + }) + + it('finishes a completed generation devtools run when success callback stops it', async () => { + let client: GenerationClient<{ prompt: string }, { text: string }> + client = new GenerationClient({ + threadId: 'generation-success-stop', + fetcher: async () => ({ text: 'done' }), + devtools: { hookName: 'useGenerationSuccessStop' }, + onStatusChange: (status) => { + if (status === 'success') client.stop() + }, + }) + + await client.generate({ prompt: 'finish then stop' }) + + expect(latestSnapshotState().activeRunId).toBeNull() + expect(latestGenerationRuns().at(-1)).toEqual( + expect.objectContaining({ status: 'success', result: { text: 'done' } }), + ) + client.dispose() + }) + + it('finishes an errored generation devtools run when error callback stops it', async () => { + let client: GenerationClient<{ prompt: string }, { text: string }> + client = new GenerationClient<{ prompt: string }, { text: string }>({ + threadId: 'generation-error-stop', + devtools: { hookName: 'useGenerationErrorStop' }, + fetcher: async (): Promise<{ text: string }> => { + throw new Error('generation failed') + }, + onStatusChange: (status) => { + if (status === 'error') client.stop() + }, + }) + + await client.generate({ prompt: 'fail then stop' }) + + expect(latestSnapshotState().activeRunId).toBeNull() + expect(latestGenerationRuns().at(-1)).toEqual( + expect.objectContaining({ + status: 'error', + error: 'generation failed', + }), + ) + client.dispose() + }) + + it('finishes a video devtools run when a runId subscriber stops it', async () => { + const fetcher = vi.fn(async () => ({ + jobId: 'video-1', + status: 'completed' as const, + url: 'https://example.com/video-1.mp4', + })) + const client = new VideoGenerationClient({ + threadId: 'video-run-id-stop', + fetcher, + devtools: { hookName: 'useVideoRunIdStop' }, + }) + const unsubscribe = client.subscribe(() => { + if (client.getSnapshot().runId) client.stop() + }) + + await client.generate({ prompt: 'stop early' }) + + expect(fetcher).not.toHaveBeenCalled() + expect(latestSnapshotState().activeRunId).toBeNull() + unsubscribe() + client.dispose() + }) + + it('finishes a video devtools run when loading callback stops it', async () => { + const fetcher = vi.fn(async () => ({ + jobId: 'video-1', + status: 'completed' as const, + url: 'https://example.com/video-1.mp4', + })) + let client: VideoGenerationClient + client = new VideoGenerationClient({ + threadId: 'video-loading-stop', + fetcher, + devtools: { hookName: 'useVideoLoadingStop' }, + onLoadingChange: (isLoading) => { + if (isLoading) client.stop() + }, + }) + + await client.generate({ prompt: 'stop while loading' }) + + expect(fetcher).not.toHaveBeenCalled() + expect(latestSnapshotState().activeRunId).toBeNull() + client.dispose() + }) + + it('finishes a completed video devtools run when success callback stops it', async () => { + let client: VideoGenerationClient + client = new VideoGenerationClient({ + threadId: 'video-success-stop', + fetcher: async () => ({ + jobId: 'video-1', + status: 'completed' as const, + url: 'https://example.com/video-1.mp4', + }), + devtools: { hookName: 'useVideoSuccessStop' }, + onStatusChange: (status) => { + if (status === 'success') client.stop() + }, + }) + + await client.generate({ prompt: 'finish then stop' }) + + expect(latestSnapshotState().activeRunId).toBeNull() + expect(latestGenerationRuns().at(-1)).toEqual( + expect.objectContaining({ + status: 'success', + result: expect.objectContaining({ jobId: 'video-1' }), + }), + ) + client.dispose() + }) + + it('finishes an errored video devtools run when error callback stops it', async () => { + let client: VideoGenerationClient + client = new VideoGenerationClient({ + threadId: 'video-error-stop', + devtools: { hookName: 'useVideoErrorStop' }, + fetcher: async () => { + throw new Error('video failed') + }, + onStatusChange: (status) => { + if (status === 'error') client.stop() + }, + }) + + await client.generate({ prompt: 'fail then stop' }) + + expect(latestSnapshotState().activeRunId).toBeNull() + expect(latestGenerationRuns().at(-1)).toEqual( + expect.objectContaining({ status: 'error', error: 'video failed' }), + ) + client.dispose() + }) + it('includes input, progress, and renderable previews in generation snapshots', async () => { const client = new GenerationClient({ threadId: 'image-hook', diff --git a/packages/ai-client/tests/snapshot-atom.test.ts b/packages/ai-client/tests/snapshot-atom.test.ts index 74da6dbdc1..dbd0742190 100644 --- a/packages/ai-client/tests/snapshot-atom.test.ts +++ b/packages/ai-client/tests/snapshot-atom.test.ts @@ -2,6 +2,14 @@ import { describe, expect, it, vi } from 'vitest' import { createAtom, patchAtom, subscribeAtom } from '../src/snapshot-atom' describe('snapshot-atom', () => { + it('freezes object snapshots and keeps identity without a change', () => { + const atom = createAtom({ count: 1 }) + const snapshot = atom.get() + + expect(Object.isFrozen(snapshot)).toBe(true) + expect(atom.get()).toBe(snapshot) + }) + it('patchAtom skips notify when every field is unchanged', () => { const atom = createAtom({ count: 1, label: 'a' }) const listener = vi.fn() diff --git a/packages/ai-client/tests/video-generation-client.test.ts b/packages/ai-client/tests/video-generation-client.test.ts index be788e4d57..7e8ad13be0 100644 --- a/packages/ai-client/tests/video-generation-client.test.ts +++ b/packages/ai-client/tests/video-generation-client.test.ts @@ -224,6 +224,7 @@ describe('VideoGenerationClient', () => { ) expect(client.getStatus()).toBe('success') expect(client.getJobId()).toBe('job-123') + expect(client.getSnapshot().runId).toBeNull() }) it('should track video status updates', async () => { @@ -314,6 +315,7 @@ describe('VideoGenerationClient', () => { expect(onError).toHaveBeenCalledWith(expect.any(Error)) expect(client.getStatus()).toBe('error') expect(client.getError()?.message).toBe('Video generation failed') + expect(client.getSnapshot().runId).toBeNull() }) it('should report progress from video:status events', async () => { @@ -483,10 +485,12 @@ describe('VideoGenerationClient', () => { const generatePromise = client.generate({ prompt: 'test' }) expect(client.getIsLoading()).toBe(true) + expect(client.getSnapshot().runId).toEqual(expect.any(String)) client.stop() expect(client.getIsLoading()).toBe(false) expect(client.getStatus()).toBe('idle') + expect(client.getSnapshot().runId).toBeNull() resolvePromise!({ jobId: 'job-1', @@ -495,6 +499,454 @@ describe('VideoGenerationClient', () => { }) await generatePromise }) + + it('should not overwrite a run started by the stopped loading callback', async () => { + const firstVideo = { + jobId: 'job-1', + status: 'completed' as const, + url: 'https://example.com/first.mp4', + } + const secondVideo = { + jobId: 'job-2', + status: 'completed' as const, + url: 'https://example.com/second.mp4', + } + let releaseFirst!: (value: typeof firstVideo) => void + let releaseSecond!: (value: typeof secondVideo) => void + const firstResult = new Promise((resolve) => { + releaseFirst = resolve + }) + const secondResult = new Promise((resolve) => { + releaseSecond = resolve + }) + let secondGenerate: Promise | undefined + let secondRunId: string | null = null + let replaced = false + let client!: VideoGenerationClient + client = new VideoGenerationClient({ + fetcher: async (input) => + input.prompt === 'first' ? firstResult : secondResult, + onLoadingChange: (isLoading) => { + if (isLoading || replaced) return + replaced = true + secondGenerate = client.generate({ prompt: 'second' }) + secondRunId = client.getSnapshot().runId + }, + }) + + const firstGenerate = client.generate({ prompt: 'first' }) + client.stop() + + expect(secondRunId).not.toBeNull() + expect(client.getSnapshot().runId).toBe(secondRunId) + expect(client.getIsLoading()).toBe(true) + expect(client.getStatus()).toBe('generating') + releaseFirst(firstVideo) + releaseSecond(secondVideo) + await Promise.all([firstGenerate, secondGenerate]) + }) + + it('should clear the run id when the success status callback stops', async () => { + let client!: VideoGenerationClient + client = new VideoGenerationClient({ + fetcher: async () => ({ + jobId: 'job-1', + status: 'completed', + url: 'https://example.com/video.mp4', + }), + onStatusChange: (status) => { + if (status === 'success') client.stop() + }, + }) + + await client.generate({ prompt: 'test' }) + + expect(client.getStatus()).toBe('success') + expect(client.getSnapshot().runId).toBeNull() + expect(client.getResumeSnapshot()).toBeUndefined() + }) + + it('should remain stopped when the error callback stops', async () => { + let client!: VideoGenerationClient + client = new VideoGenerationClient({ + fetcher: async () => { + throw new Error('failed') + }, + onErrorChange: (error) => { + if (error) client.stop() + }, + }) + + await client.generate({ prompt: 'test' }) + + expect(client.getStatus()).toBe('idle') + expect(client.getSnapshot().runId).toBeNull() + }) + + it('should stop before transport work when loading callback stops', async () => { + const fetcher = vi.fn(async () => ({ + jobId: 'unexpected', + status: 'completed' as const, + url: 'https://example.com/unexpected.mp4', + })) + let client!: VideoGenerationClient + client = new VideoGenerationClient({ + fetcher, + onLoadingChange: (isLoading) => { + if (isLoading) client.stop() + }, + }) + + await client.generate({ prompt: 'test' }) + + expect(fetcher).not.toHaveBeenCalled() + expect(client.getStatus()).toBe('idle') + expect(client.getSnapshot().runId).toBeNull() + }) + + it('should not let initialization continue after loading callback starts a replacement', async () => { + const secondVideo = { + jobId: 'job-2', + status: 'completed' as const, + url: 'https://example.com/second.mp4', + } + let releaseSecond!: (value: typeof secondVideo) => void + const secondResult = new Promise((resolve) => { + releaseSecond = resolve + }) + const prompts: Array = [] + let client!: VideoGenerationClient + let replaced = false + let secondGenerate: Promise | undefined + client = new VideoGenerationClient({ + fetcher: async (input) => { + if (typeof input.prompt === 'string') prompts.push(input.prompt) + return input.prompt === 'second' + ? secondResult + : { + jobId: 'job-1', + status: 'completed', + url: 'https://example.com/first.mp4', + } + }, + onLoadingChange: (isLoading) => { + if (!isLoading || replaced) return + replaced = true + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + }, + }) + + await client.generate({ prompt: 'first' }) + + expect(prompts).toEqual(['second']) + expect(client.getStatus()).toBe('generating') + releaseSecond(secondVideo) + await secondGenerate + }) + + it('should not let an old result callback complete a replacement run', async () => { + const secondVideo = { + jobId: 'job-2', + status: 'completed' as const, + url: 'https://example.com/second.mp4', + } + let releaseSecond!: (value: typeof secondVideo) => void + const secondResult = new Promise((resolve) => { + releaseSecond = resolve + }) + let client!: VideoGenerationClient + let secondGenerate: Promise | undefined + let secondRunId: string | null = null + client = new VideoGenerationClient({ + fetcher: async (input) => + input.prompt === 'first' + ? { + jobId: 'job-1', + status: 'completed', + url: 'https://example.com/first.mp4', + } + : secondResult, + onResultChange: (result) => { + if (result?.jobId !== 'job-1') return + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + secondRunId = client.getSnapshot().runId + }, + }) + + await client.generate({ prompt: 'first' }) + + expect(secondRunId).not.toBeNull() + expect(client.getSnapshot().runId).toBe(secondRunId) + expect(client.getStatus()).toBe('generating') + expect(client.getResumeSnapshot()).toBeUndefined() + releaseSecond(secondVideo) + await secondGenerate + }) + + it('should not let an old error callback overwrite a replacement run', async () => { + const secondVideo = { + jobId: 'job-2', + status: 'completed' as const, + url: 'https://example.com/second.mp4', + } + let releaseSecond!: (value: typeof secondVideo) => void + const secondResult = new Promise((resolve) => { + releaseSecond = resolve + }) + let client!: VideoGenerationClient + let secondGenerate: Promise | undefined + let secondRunId: string | null = null + client = new VideoGenerationClient({ + fetcher: async (input) => { + if (input.prompt === 'first') throw new Error('first failed') + return secondResult + }, + onErrorChange: (error) => { + if (error?.message !== 'first failed') return + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + secondRunId = client.getSnapshot().runId + }, + }) + + await client.generate({ prompt: 'first' }) + + expect(secondRunId).not.toBeNull() + expect(client.getSnapshot().runId).toBe(secondRunId) + expect(client.getStatus()).toBe('generating') + releaseSecond(secondVideo) + await secondGenerate + }) + + it('should not let an old error status callback overwrite a replacement run', async () => { + const secondVideo = { + jobId: 'job-2', + status: 'completed' as const, + url: 'https://example.com/second.mp4', + } + let releaseSecond!: (value: typeof secondVideo) => void + const secondResult = new Promise((resolve) => { + releaseSecond = resolve + }) + let client!: VideoGenerationClient + let secondGenerate: Promise | undefined + let replaced = false + client = new VideoGenerationClient({ + fetcher: async (input) => { + if (input.prompt === 'first') throw new Error('first failed') + return secondResult + }, + onStatusChange: (status) => { + if (status !== 'error' || replaced) return + replaced = true + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + }, + }) + + await client.generate({ prompt: 'first' }) + + expect(client.getStatus()).toBe('generating') + expect(client.getError()).toBeUndefined() + releaseSecond(secondVideo) + await secondGenerate + }) + + it('should stop before transport when the run id subscriber stops', async () => { + const fetcher = vi.fn(async () => ({ + jobId: 'unexpected', + status: 'completed' as const, + url: 'https://example.com/unexpected.mp4', + })) + const onLoadingChange = vi.fn() + const client = new VideoGenerationClient({ fetcher, onLoadingChange }) + client.subscribe(() => { + if (client.getSnapshot().runId) client.stop() + }) + + await client.generate({ prompt: 'test' }) + + expect(fetcher).not.toHaveBeenCalled() + expect(onLoadingChange).not.toHaveBeenCalledWith(true) + expect(client.getSnapshot().runId).toBeNull() + }) + + it('should not continue an old run after the run id subscriber starts a replacement', async () => { + const secondVideo = { + jobId: 'job-2', + status: 'completed' as const, + url: 'https://example.com/second.mp4', + } + let releaseSecond!: (value: typeof secondVideo) => void + const secondResult = new Promise((resolve) => { + releaseSecond = resolve + }) + const prompts: Array = [] + let secondGenerate: Promise | undefined + let replaced = false + const client = new VideoGenerationClient({ + fetcher: async (input) => { + if (typeof input.prompt === 'string') prompts.push(input.prompt) + return input.prompt === 'second' + ? secondResult + : { + jobId: 'job-1', + status: 'completed', + url: 'https://example.com/first.mp4', + } + }, + }) + client.subscribe(() => { + if (!client.getSnapshot().runId || replaced) return + replaced = true + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + }) + + await client.generate({ prompt: 'first' }) + + expect(prompts).toEqual(['second']) + expect(client.getStatus()).toBe('generating') + releaseSecond(secondVideo) + await secondGenerate + }) + + it('should not call the loading callback after its snapshot subscriber stops', async () => { + const fetcher = vi.fn(async () => ({ + jobId: 'unexpected', + status: 'completed' as const, + url: 'https://example.com/unexpected.mp4', + })) + const onLoadingChange = vi.fn() + const client = new VideoGenerationClient({ fetcher, onLoadingChange }) + client.subscribe(() => { + if (client.getSnapshot().isLoading) client.stop() + }) + + await client.generate({ prompt: 'test' }) + + expect(fetcher).not.toHaveBeenCalled() + expect(onLoadingChange).not.toHaveBeenCalledWith(true) + }) + + it('should not call the old status callback after its subscriber starts a replacement', async () => { + const secondVideo = { + jobId: 'job-2', + status: 'completed' as const, + url: 'https://example.com/second.mp4', + } + let releaseSecond!: (value: typeof secondVideo) => void + const secondResult = new Promise((resolve) => { + releaseSecond = resolve + }) + const generatingStatuses: Array = [] + let secondGenerate: Promise | undefined + let replaced = false + const client = new VideoGenerationClient({ + fetcher: async (input) => + input.prompt === 'second' + ? secondResult + : { + jobId: 'job-1', + status: 'completed', + url: 'https://example.com/first.mp4', + }, + onStatusChange: (status) => { + if (status === 'generating') generatingStatuses.push(status) + }, + }) + client.subscribe(() => { + if (client.getSnapshot().status !== 'generating' || replaced) return + replaced = true + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + }) + + await client.generate({ prompt: 'first' }) + + expect(generatingStatuses).toEqual(['generating']) + expect(client.getStatus()).toBe('generating') + releaseSecond(secondVideo) + await secondGenerate + }) + + it('should not call the old result callback after its subscriber starts a replacement', async () => { + const secondVideo = { + jobId: 'job-2', + status: 'completed' as const, + url: 'https://example.com/second.mp4', + } + let releaseSecond!: (value: typeof secondVideo) => void + const secondResult = new Promise((resolve) => { + releaseSecond = resolve + }) + const onResultChange = vi.fn() + let secondGenerate: Promise | undefined + let replaced = false + const client = new VideoGenerationClient({ + fetcher: async (input) => + input.prompt === 'second' + ? secondResult + : { + jobId: 'job-1', + status: 'completed', + url: 'https://example.com/first.mp4', + }, + onResultChange, + }) + client.subscribe(() => { + if (client.getResult()?.jobId !== 'job-1' || replaced) return + replaced = true + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + }) + + await client.generate({ prompt: 'first' }) + + expect(onResultChange).not.toHaveBeenCalledWith( + expect.objectContaining({ jobId: 'job-1' }), + ) + expect(client.getStatus()).toBe('generating') + releaseSecond(secondVideo) + await secondGenerate + }) + + it('should not call the old error callback after its subscriber starts a replacement', async () => { + const secondVideo = { + jobId: 'job-2', + status: 'completed' as const, + url: 'https://example.com/second.mp4', + } + let releaseSecond!: (value: typeof secondVideo) => void + const secondResult = new Promise((resolve) => { + releaseSecond = resolve + }) + const onErrorChange = vi.fn() + let secondGenerate: Promise | undefined + let replaced = false + const client = new VideoGenerationClient({ + fetcher: async (input) => { + if (input.prompt === 'first') throw new Error('first failed') + return secondResult + }, + onErrorChange, + }) + client.subscribe(() => { + if (client.getError()?.message !== 'first failed' || replaced) return + replaced = true + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + }) + + await client.generate({ prompt: 'first' }) + + expect(onErrorChange).not.toHaveBeenCalledWith(expect.any(Error)) + expect(client.getStatus()).toBe('generating') + releaseSecond(secondVideo) + await secondGenerate + }) }) describe('reset()', () => { @@ -561,6 +1013,112 @@ describe('VideoGenerationClient', () => { expect(client.getVideoStatus()).toBeNull() expect(client.getError()).toBeUndefined() expect(client.getStatus()).toBe('idle') + expect(client.getSnapshot().runId).toBeNull() + }) + + it('should not overwrite a run started by the reset loading callback', async () => { + const seedVideo = { + jobId: 'seed', + status: 'completed' as const, + url: 'https://example.com/seed.mp4', + } + const firstVideo = { + jobId: 'job-1', + status: 'completed' as const, + url: 'https://example.com/first.mp4', + } + const secondVideo = { + jobId: 'job-2', + status: 'completed' as const, + url: 'https://example.com/second.mp4', + } + let releaseFirst!: (value: typeof firstVideo) => void + let releaseSecond!: (value: typeof secondVideo) => void + const firstResult = new Promise((resolve) => { + releaseFirst = resolve + }) + const secondResult = new Promise((resolve) => { + releaseSecond = resolve + }) + let replaceOnStop = false + let secondGenerate: Promise | undefined + let secondRunId: string | null = null + let client!: VideoGenerationClient + client = new VideoGenerationClient({ + fetcher: async (input) => { + if (input.prompt === 'seed') return seedVideo + return input.prompt === 'first' ? firstResult : secondResult + }, + onLoadingChange: (isLoading) => { + if (isLoading || !replaceOnStop) return + replaceOnStop = false + secondGenerate = client.generate({ prompt: 'second' }) + secondRunId = client.getSnapshot().runId + }, + }) + await client.generate({ prompt: 'seed' }) + const firstGenerate = client.generate({ prompt: 'first' }) + replaceOnStop = true + + client.reset() + + expect(secondRunId).not.toBeNull() + expect(client.getSnapshot().runId).toBe(secondRunId) + expect(client.getStatus()).toBe('generating') + expect(client.getIsLoading()).toBe(true) + expect(client.getResult()).toEqual(seedVideo) + releaseFirst(firstVideo) + releaseSecond(secondVideo) + await Promise.all([firstGenerate, secondGenerate]) + }) + }) + + describe('dispose()', () => { + it('should not overwrite a run started by the dispose loading callback', async () => { + const firstVideo = { + jobId: 'job-1', + status: 'completed' as const, + url: 'https://example.com/first.mp4', + } + const secondVideo = { + jobId: 'job-2', + status: 'completed' as const, + url: 'https://example.com/second.mp4', + } + let releaseFirst!: (value: typeof firstVideo) => void + let releaseSecond!: (value: typeof secondVideo) => void + const first = new Promise((resolve) => { + releaseFirst = resolve + }) + const second = new Promise((resolve) => { + releaseSecond = resolve + }) + let replaceOnDispose = false + let secondGenerate: Promise | undefined + let secondRunId: string | null = null + let client!: VideoGenerationClient + client = new VideoGenerationClient({ + fetcher: (input) => (input.prompt === 'first' ? first : second), + onLoadingChange: (isLoading) => { + if (isLoading || !replaceOnDispose) return + replaceOnDispose = false + client.mountDevtools() + secondGenerate = client.generate({ prompt: 'second' }) + secondRunId = client.getSnapshot().runId + }, + }) + const firstGenerate = client.generate({ prompt: 'first' }) + replaceOnDispose = true + + client.dispose() + + expect(secondRunId).not.toBeNull() + expect(client.getSnapshot().runId).toBe(secondRunId) + expect(client.getStatus()).toBe('generating') + expect(client.getIsLoading()).toBe(true) + releaseFirst(firstVideo) + releaseSecond(secondVideo) + await Promise.all([firstGenerate, secondGenerate]) }) }) @@ -652,6 +1210,75 @@ describe('VideoGenerationClient', () => { expect(client.getResult()).toBeNull() expect(client.getStatus()).toBe('idle') }) + + it('should not publish a run id after onChunk stops the run', async () => { + const connection: ConnectConnectionAdapter = { + async *connect() { + yield { + type: EventType.RUN_STARTED as const, + runId: 'run-1', + threadId: 'thread-1', + timestamp: Date.now(), + } + }, + } + let client!: VideoGenerationClient + client = new VideoGenerationClient({ + connection, + onChunk: (chunk) => { + if (chunk.type === EventType.RUN_STARTED) client.stop() + }, + }) + + await client.generate({ prompt: 'test' }) + + expect(client.getSnapshot().runId).toBeNull() + expect(client.getStatus()).toBe('idle') + }) + + it('should not let an old onChunk continuation overwrite a newer run', async () => { + let connectionCount = 0 + let releaseSecond!: () => void + const secondBlocked = new Promise((resolve) => { + releaseSecond = resolve + }) + const connection: ConnectConnectionAdapter = { + async *connect() { + connectionCount++ + if (connectionCount === 1) { + yield { + type: EventType.RUN_STARTED as const, + runId: 'run-1', + threadId: 'thread-1', + timestamp: Date.now(), + } + return + } + await secondBlocked + }, + } + let client!: VideoGenerationClient + let secondGenerate: Promise | undefined + let secondLocalRunId: string | null = null + client = new VideoGenerationClient({ + connection, + onChunk: (chunk) => { + if (chunk.type !== EventType.RUN_STARTED) return + client.stop() + secondGenerate = client.generate({ prompt: 'second' }) + secondLocalRunId = client.getSnapshot().runId + }, + }) + + await client.generate({ prompt: 'first' }) + + expect(secondLocalRunId).not.toBeNull() + expect(client.getSnapshot().runId).toBe(secondLocalRunId) + expect(client.getIsLoading()).toBe(true) + client.stop() + releaseSecond() + await secondGenerate + }) }) describe('error wrapping', () => { diff --git a/packages/ai-react/src/use-chat.ts b/packages/ai-react/src/use-chat.ts index c567df72c2..e1cdf0e004 100644 --- a/packages/ai-react/src/use-chat.ts +++ b/packages/ai-react/src/use-chat.ts @@ -18,6 +18,7 @@ import type { StreamChunk, } from '@tanstack/ai/client' import type { + ChatClientPersistence, ResolvableChatInterrupt, ChatResumeState, InferredClientContext, @@ -68,128 +69,180 @@ export function useChat< optionsRef.current = options // Create ChatClient instance with callbacks to sync state - const { client, initialization } = useMemo(() => { - const messagesToUse = options.initialMessages || [] - - // Build options with conditional spreads for fields whose source - // type is `T | undefined` but the ChatClient target uses a strict - // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects - // assigning `undefined` to those, so we omit the key when absent. - const initialOptions = optionsRef.current - const transport = initialOptions.connection - ? { connection: initialOptions.connection } - : { fetcher: initialOptions.fetcher } - - const instanceHolder: { - current: ChatClient | undefined - } = { current: undefined } - const getActiveInstance = () => { - const currentInstance = instanceHolder.current - if (!currentInstance || activeClientRef.current !== currentInstance) { - return undefined + const { client, initialization, serverSnapshot, startPersistenceHydration } = + useMemo(() => { + const messagesToUse = options.initialMessages || [] + + // Build options with conditional spreads for fields whose source + // type is `T | undefined` but the ChatClient target uses a strict + // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects + // assigning `undefined` to those, so we omit the key when absent. + const initialOptions = optionsRef.current + let persistence = initialOptions.persistence + let releasePersistenceHydration = () => {} + if (persistence && typeof persistence === 'object') { + const source = persistence + let hydrationStarted = false + let releaseRead: (() => void) | undefined + let hydrationPromise: + | Promise< + Awaited['getItem']>> + > + | undefined + const gatedPersistence: ChatClientPersistence = { + getItem(id) { + if (!hydrationPromise) { + hydrationPromise = new Promise((resolve, reject) => { + let readStarted = false + releaseRead = () => { + if (readStarted) return + readStarted = true + try { + resolve(source.getItem.call(source, id)) + } catch (error) { + reject(error) + } + } + }) + } + if (hydrationStarted) releaseRead?.() + return hydrationPromise + }, + setItem(id, state) { + return source.setItem.call(source, id, state) + }, + removeItem(id) { + return source.removeItem.call(source, id) + }, + } + persistence = gatedPersistence + releasePersistenceHydration = () => { + if (hydrationStarted) return + hydrationStarted = true + releaseRead?.() + } } - return currentInstance - } - // ChatClient may publish while its constructor is running or while async - // persistence resolves before commit. Preserve those exact notifications - // until this render commits; invoking them here would run state setters and - // user callbacks for a client React may abandon. - const initializationState = { - ready: false, - callbacks: [] as Array<() => void>, - } - const runOrQueueForActiveInstance = (callback: () => void) => { - if (!initializationState.ready) { - initializationState.callbacks.push(callback) - return + const transport = initialOptions.connection + ? { connection: initialOptions.connection } + : { fetcher: initialOptions.fetcher } + + const instanceHolder: { + current: ChatClient | undefined + } = { current: undefined } + const getActiveInstance = () => { + const currentInstance = instanceHolder.current + if (!currentInstance || activeClientRef.current !== currentInstance) { + return undefined + } + return currentInstance } - const currentInstance = instanceHolder.current - if (!currentInstance || activeClientRef.current !== currentInstance) - return - callback() - } - const instance = new ChatClient({ - devtoolsBridgeFactory: createChatDevtoolsBridge, - ...transport, - initialMessages: messagesToUse, - ...(typeof initialOptions.threadId === 'string' && - initialOptions.persistence - ? { - persistence: initialOptions.persistence, - threadId: initialOptions.threadId, - } - : { - ...(initialOptions.threadId !== undefined && { + // ChatClient may publish while its constructor is running or while async + // persistence resolves before commit. Preserve those exact notifications + // until this render commits; invoking them here would run state setters and + // user callbacks for a client React may abandon. + const initializationState = { + ready: false, + callbacks: [] as Array<() => void>, + } + const runOrQueueForActiveInstance = (callback: () => void) => { + if (!initializationState.ready) { + initializationState.callbacks.push(callback) + return + } + const currentInstance = instanceHolder.current + if (!currentInstance || activeClientRef.current !== currentInstance) + return + callback() + } + const instance = new ChatClient({ + devtoolsBridgeFactory: createChatDevtoolsBridge, + ...transport, + initialMessages: messagesToUse, + ...(typeof initialOptions.threadId === 'string' && persistence + ? { + persistence, threadId: initialOptions.threadId, + } + : { + ...(initialOptions.threadId !== undefined && { + threadId: initialOptions.threadId, + }), }), - }), - ...(initialOptions.body !== undefined && { body: initialOptions.body }), - ...(initialOptions.forwardedProps !== undefined && { - forwardedProps: initialOptions.forwardedProps, - }), - ...(initialOptions.byok !== undefined && { byok: initialOptions.byok }), - byokProvider: () => optionsRef.current.byokProvider?.(), - ...(initialOptions.initialResumeSnapshot !== undefined && { - initialResumeSnapshot: initialOptions.initialResumeSnapshot, - }), - ...(initialOptions.context !== undefined && { - context: initialOptions.context, - }), - devtools: { - ...initialOptions.devtools, - framework: 'react', - hookName: 'useChat', - outputKind: initialOptions.outputSchema ? 'structured' : 'chat', - }, - onResponse: (response) => { - // ChatClient awaits this return value. Queuing would drop the promise. - if (!getActiveInstance()) return - return optionsRef.current.onResponse?.(response) - }, - onChunk: (chunk: StreamChunk) => { - runOrQueueForActiveInstance(() => { - optionsRef.current.onChunk?.(chunk) - }) - }, - onFinish: (message: UIMessage) => { - runOrQueueForActiveInstance(() => { - optionsRef.current.onFinish?.(message) - }) - }, - onError: (error: Error) => { - runOrQueueForActiveInstance(() => { - optionsRef.current.onError?.(error) - }) - }, - ...(initialOptions.tools !== undefined && { - tools: initialOptions.tools, - }), - ...(initialOptions.interrupts !== undefined && { - interrupts: initialOptions.interrupts, - }), - onCustomEvent: (eventType, data, context) => { - runOrQueueForActiveInstance(() => { - optionsRef.current.onCustomEvent?.(eventType, data, context) - }) - }, - ...(options.streamProcessor !== undefined && { - streamProcessor: options.streamProcessor, - }), - ...(optionsRef.current.queue !== undefined && { - queue: optionsRef.current.queue, - }), - onInterruptStateChange: (nextInterruptState, context) => { - runOrQueueForActiveInstance(() => { - optionsRef.current.onInterruptStateChange?.( - nextInterruptState, - context, - ) - }) - }, - }) - instanceHolder.current = instance - return { client: instance, initialization: initializationState } - }, [clientId]) + ...(initialOptions.body !== undefined && { body: initialOptions.body }), + ...(initialOptions.forwardedProps !== undefined && { + forwardedProps: initialOptions.forwardedProps, + }), + ...(initialOptions.byok !== undefined && { byok: initialOptions.byok }), + byokProvider: () => optionsRef.current.byokProvider?.(), + ...(initialOptions.initialResumeSnapshot !== undefined && { + initialResumeSnapshot: initialOptions.initialResumeSnapshot, + }), + ...(initialOptions.context !== undefined && { + context: initialOptions.context, + }), + devtools: { + ...initialOptions.devtools, + framework: 'react', + hookName: 'useChat', + outputKind: initialOptions.outputSchema ? 'structured' : 'chat', + }, + onResponse: (response) => { + // ChatClient awaits this return value. Queuing would drop the promise. + if (!getActiveInstance()) return + return optionsRef.current.onResponse?.(response) + }, + onChunk: (chunk: StreamChunk) => { + runOrQueueForActiveInstance(() => { + optionsRef.current.onChunk?.(chunk) + }) + }, + onFinish: (message: UIMessage) => { + runOrQueueForActiveInstance(() => { + optionsRef.current.onFinish?.(message) + }) + }, + onError: (error: Error) => { + runOrQueueForActiveInstance(() => { + optionsRef.current.onError?.(error) + }) + }, + ...(initialOptions.tools !== undefined && { + tools: initialOptions.tools, + }), + ...(initialOptions.interrupts !== undefined && { + interrupts: initialOptions.interrupts, + }), + onCustomEvent: (eventType, data, context) => { + runOrQueueForActiveInstance(() => { + optionsRef.current.onCustomEvent?.(eventType, data, context) + }) + }, + ...(options.streamProcessor !== undefined && { + streamProcessor: options.streamProcessor, + }), + ...(optionsRef.current.queue !== undefined && { + queue: optionsRef.current.queue, + }), + onInterruptStateChange: (nextInterruptState, context) => { + runOrQueueForActiveInstance(() => { + optionsRef.current.onInterruptStateChange?.( + nextInterruptState, + context, + ) + }) + }, + }) + instanceHolder.current = instance + const capturedServerSnapshot = instance.getSnapshot() + return { + client: instance, + initialization: initializationState, + serverSnapshot: capturedServerSnapshot, + startPersistenceHydration: releasePersistenceHydration, + } + }, [clientId]) + + const getServerSnapshot = useCallback(() => serverSnapshot, [serverSnapshot]) useEffect(() => { activeClientRef.current = client @@ -212,7 +265,7 @@ export function useChat< const snapshot = useSyncExternalStore( client.subscribeSnapshot, client.getSnapshot, - client.getSnapshot, + getServerSnapshot, ) // Sync each wire-payload slot in its own effect so an unrelated option @@ -277,10 +330,11 @@ export function useChat< // run is picked straight back up from the durable log. useEffect(() => { client.attach() + startPersistenceHydration() return () => { client.detach() } - }, [client]) + }, [client, startPersistenceHydration]) useEffect(() => { if (cleanupDisposalRef.current?.client === client) { diff --git a/packages/ai-react/tests/use-chat.test.ts b/packages/ai-react/tests/use-chat.test.ts index 3f6d6593f8..04f2fe9f01 100644 --- a/packages/ai-react/tests/use-chat.test.ts +++ b/packages/ai-react/tests/use-chat.test.ts @@ -1,7 +1,13 @@ import { EventType } from '@tanstack/ai' import { ChatClient } from '@tanstack/ai-client' import { act, render, renderHook, waitFor } from '@testing-library/react' -import { StrictMode, Suspense, createElement, useState } from 'react' +import { + StrictMode, + Suspense, + createElement, + useLayoutEffect, + useState, +} from 'react' import { afterEach, describe, expect, it, vi } from 'vitest' import { useChat } from '../src/use-chat' import { @@ -232,7 +238,7 @@ describe('useChat', () => { expect(persistence.getItem).toHaveBeenCalledWith('persisted-chat') }) - it('should forward synchronous persisted interrupt hydration', () => { + it('forwards persisted interrupt hydration from a synchronous store', async () => { const onInterruptStateChange = vi.fn() const persistence = { getItem: vi.fn(() => ({ @@ -254,7 +260,9 @@ describe('useChat', () => { { wrapper: StrictMode }, ) - expect(result.current.interrupts).toHaveLength(2) + await waitFor(() => { + expect(result.current.interrupts).toHaveLength(2) + }) expect(onInterruptStateChange).toHaveBeenCalledOnce() expect(onInterruptStateChange).toHaveBeenCalledWith( expect.objectContaining({ interrupts: result.current.interrupts }), @@ -262,6 +270,360 @@ describe('useChat', () => { ) }) + it('keeps the server message count for the first hydration render', async () => { + const initialMessages: Array = [ + { + id: 'server-message', + role: 'user', + parts: [{ type: 'text', content: 'Server message' }], + createdAt: new Date(), + }, + ] + const persistedMessages: Array = [ + ...initialMessages, + { + id: 'persisted-message', + role: 'assistant', + parts: [{ type: 'text', content: 'Persisted message' }], + createdAt: new Date(), + }, + ] + const persistence = { + getItem: vi.fn(() => persistedMessages), + setItem: vi.fn(), + removeItem: vi.fn(), + } + const renderCounts: Array = [] + const onRecoverableError = vi.fn() + const container = document.createElement('div') + container.innerHTML = '
1
' + document.body.append(container) + + function HydratedChat() { + const chat = useChat({ + connection: createMockConnectionAdapter(), + threadId: 'hydrated-message-chat', + initialMessages, + persistence, + }) + renderCounts.push(chat.messages.length) + return createElement( + 'div', + { 'data-testid': 'message-count' }, + chat.messages.length, + ) + } + + const view = render( + createElement(StrictMode, null, createElement(HydratedChat)), + { container, hydrate: true, onRecoverableError }, + ) + + expect(renderCounts[0]).toBe(1) + await waitFor(() => { + expect(view.getByTestId('message-count').textContent).toBe('2') + }) + expect(persistence.getItem).toHaveBeenCalledOnce() + expect(onRecoverableError).not.toHaveBeenCalled() + }) + + it('keeps server interrupts empty until persistence hydration starts after commit', async () => { + const persistence = { + getItem: vi.fn(() => ({ + messages: [], + resume: createInterruptResumeSnapshot(), + })), + setItem: vi.fn(), + removeItem: vi.fn(), + } + const renderCounts: Array = [] + const hydrationCommitStates: Array = [] + const onRecoverableError = vi.fn() + const container = document.createElement('div') + container.innerHTML = '
0
' + document.body.append(container) + + function HydratedInterrupts() { + let committed = false + useLayoutEffect(() => { + committed = true + }, []) + const chat = useChat({ + connection: createMockConnectionAdapter(), + threadId: 'hydrated-interrupt-chat', + persistence, + onInterruptStateChange: (_state, context) => { + if (context.source === 'hydrate') { + hydrationCommitStates.push(committed) + } + }, + }) + renderCounts.push(chat.interrupts.length) + return createElement( + 'div', + { 'data-testid': 'interrupt-count' }, + chat.interrupts.length, + ) + } + + const view = render( + createElement(StrictMode, null, createElement(HydratedInterrupts)), + { container, hydrate: true, onRecoverableError }, + ) + + expect(renderCounts[0]).toBe(0) + await waitFor(() => { + expect(view.getByTestId('interrupt-count').textContent).toBe('2') + }) + expect(hydrationCommitStates).toEqual([true]) + expect(persistence.getItem).toHaveBeenCalledOnce() + expect(onRecoverableError).not.toHaveBeenCalled() + }) + + it('keeps the initial resume snapshot through hydration before rejoining the stored run', async () => { + const initialResumeSnapshot = createInterruptResumeSnapshot() + const persistence = { + getItem: vi.fn(() => ({ + messages: [], + resume: { + schemaVersion: 2, + resumeState: { + threadId: 'hydrated-resume-chat', + runId: 'stored-run', + }, + }, + })), + setItem: vi.fn(), + removeItem: vi.fn(), + } + const joinRun = vi.fn(async function* (_runId: string) {}) + const connection: ResumableConnectConnectionAdapter = { + connect: async function* () {}, + joinRun, + } + const renderCounts: Array = [] + const onRecoverableError = vi.fn() + const container = document.createElement('div') + container.innerHTML = '
2
' + document.body.append(container) + + function HydratedResume() { + const chat = useChat({ + connection, + threadId: 'hydrated-resume-chat', + initialResumeSnapshot, + persistence, + }) + renderCounts.push(chat.interrupts.length) + return createElement( + 'div', + { 'data-testid': 'interrupt-count' }, + chat.interrupts.length, + ) + } + + const view = render(createElement(HydratedResume), { + container, + hydrate: true, + onRecoverableError, + }) + + expect(renderCounts[0]).toBe(2) + expect(joinRun).not.toHaveBeenCalled() + await waitFor(() => { + expect(view.getByTestId('interrupt-count').textContent).toBe('0') + expect(joinRun).toHaveBeenCalledOnce() + }) + expect(joinRun).toHaveBeenCalledWith('stored-run', expect.anything()) + expect(persistence.getItem).toHaveBeenCalledOnce() + expect(onRecoverableError).not.toHaveBeenCalled() + }) + + it('preserves the persistence adapter receiver for reads, writes, and removal', async () => { + class ReceiverSensitivePersistence implements ChatClientPersistence { + calls: Array = [] + + getItem(id: string) { + this.calls.push(`get:${id}`) + return [] + } + + setItem(id: string) { + this.calls.push(`set:${id}`) + } + + removeItem(id: string) { + this.calls.push(`remove:${id}`) + } + } + + const persistence = new ReceiverSensitivePersistence() + const { result } = renderUseChat({ + connection: createMockConnectionAdapter({ + chunks: createTextChunks('Persisted reply'), + }), + threadId: 'receiver-chat', + persistence, + }) + + await waitFor(() => { + expect(persistence.calls).toContain('get:receiver-chat') + }) + await act(async () => { + await result.current.sendMessage('Persist me') + }) + await waitFor(() => { + expect(persistence.calls).toContain('set:receiver-chat') + }) + const removalsBeforeClear = persistence.calls.filter( + (call) => call === 'remove:receiver-chat', + ).length + act(() => result.current.clear()) + await waitFor(() => { + const removalsAfterClear = persistence.calls.filter( + (call) => call === 'remove:receiver-chat', + ).length + expect(removalsAfterClear).toBeGreaterThan(removalsBeforeClear) + }) + }) + + it('keeps initial state when a synchronous persistence read throws during hydration', async () => { + const initialMessages: Array = [ + { + id: 'initial-message', + role: 'user', + parts: [{ type: 'text', content: 'Initial' }], + createdAt: new Date(), + }, + ] + const persistence = { + getItem: vi.fn(() => { + throw new Error('storage unavailable') + }), + setItem: vi.fn(), + removeItem: vi.fn(), + } + const onInterruptStateChange = vi.fn() + const onRecoverableError = vi.fn() + const container = document.createElement('div') + container.innerHTML = '
1
' + document.body.append(container) + + function HydratedChat() { + const chat = useChat({ + connection: createMockConnectionAdapter(), + threadId: 'throwing-sync-chat', + initialMessages, + persistence, + onInterruptStateChange, + }) + return createElement( + 'div', + { 'data-testid': 'message-count' }, + chat.messages.length, + ) + } + + const view = render(createElement(HydratedChat), { + container, + hydrate: true, + onRecoverableError, + }) + + await waitFor(() => { + expect(persistence.getItem).toHaveBeenCalledOnce() + }) + expect(view.getByTestId('message-count').textContent).toBe('1') + expect(onInterruptStateChange).not.toHaveBeenCalled() + expect(onRecoverableError).not.toHaveBeenCalled() + }) + + it('keeps initial state when an asynchronous persistence read rejects', async () => { + const initialMessages: Array = [ + { + id: 'initial-message', + role: 'user', + parts: [{ type: 'text', content: 'Initial' }], + createdAt: new Date(), + }, + ] + const persistence = { + getItem: vi.fn(async () => { + throw new Error('storage unavailable') + }), + setItem: vi.fn(), + removeItem: vi.fn(), + } + const onInterruptStateChange = vi.fn() + const { result } = renderUseChat({ + connection: createMockConnectionAdapter(), + threadId: 'throwing-async-chat', + initialMessages, + persistence, + onInterruptStateChange, + }) + + await waitFor(() => { + expect(persistence.getItem).toHaveBeenCalledOnce() + }) + expect(result.current.messages).toEqual(initialMessages) + expect(onInterruptStateChange).not.toHaveBeenCalled() + }) + + it('applies asynchronous persistence only after attach and commit', async () => { + const hydration = createDeferred<{ + messages: Array + resume: ReturnType + }>() + let committed = false + const readCommitStates: Array = [] + const hydrationCommitStates: Array = [] + const persistence = { + getItem: vi.fn(() => { + readCommitStates.push(committed) + return hydration.promise + }), + setItem: vi.fn(), + removeItem: vi.fn(), + } + + function useHydratedChat() { + useLayoutEffect(() => { + committed = true + }, []) + const chat = useChat({ + connection: createMockConnectionAdapter(), + threadId: 'async-hydration-chat', + persistence, + onInterruptStateChange: (_state, context) => { + if (context.source === 'hydrate') { + hydrationCommitStates.push(committed) + } + }, + }) + return chat.interrupts.length + } + + const { result } = renderHook(() => useHydratedChat()) + + expect(result.current).toBe(0) + await waitFor(() => { + expect(persistence.getItem).toHaveBeenCalledOnce() + }) + expect(readCommitStates).toEqual([true]) + await act(async () => { + hydration.resolve({ + messages: [], + resume: createInterruptResumeSnapshot(), + }) + await hydration.promise + }) + await waitFor(() => { + expect(result.current).toBe(2) + }) + expect(hydrationCommitStates).toEqual([true]) + }) + it('does not publish async hydration from an abandoned render', async () => { const hydration = createDeferred<{ messages: Array @@ -302,6 +664,7 @@ describe('useChat', () => { await hydration.promise }) + expect(persistence.getItem).not.toHaveBeenCalled() expect(onInterruptStateChange).not.toHaveBeenCalled() }) diff --git a/packages/ai-svelte/tests/create-generation-dispose.test.ts b/packages/ai-svelte/tests/create-generation-dispose.test.ts index e7ef6fc3d6..31920ea009 100644 --- a/packages/ai-svelte/tests/create-generation-dispose.test.ts +++ b/packages/ai-svelte/tests/create-generation-dispose.test.ts @@ -18,11 +18,17 @@ describe('Svelte generate() after dispose() rebinds the snapshot', () => { it('createGenerateVideo updates result after dispose then generate', async () => { const video = createGenerateVideo({ - fetcher: async (input) => ({ - jobId: input.prompt, - status: 'completed' as const, - url: `https://example.com/${input.prompt}.mp4`, - }), + fetcher: async (input) => { + if (typeof input.prompt !== 'string') { + throw new Error('Expected a text prompt') + } + + return { + jobId: input.prompt, + status: 'completed' as const, + url: `https://example.com/${input.prompt}.mp4`, + } + }, }) await video.generate({ prompt: 'one' }) diff --git a/packages/ai-vertex/package.json b/packages/ai-vertex/package.json index 55bf952d85..bdf083f945 100644 --- a/packages/ai-vertex/package.json +++ b/packages/ai-vertex/package.json @@ -58,7 +58,6 @@ "@tanstack/ai": "workspace:^" }, "devDependencies": { - "@google/genai": "^2.10.0", "@tanstack/ai": "workspace:*", "@vitest/coverage-v8": "4.1.10", "vite": "^8.2.1" diff --git a/packages/ai-vertex/tests/factories.test.ts b/packages/ai-vertex/tests/factories.test.ts index 54c9c86b84..c21f6c0d5e 100644 --- a/packages/ai-vertex/tests/factories.test.ts +++ b/packages/ai-vertex/tests/factories.test.ts @@ -11,22 +11,35 @@ import { const mocks = vi.hoisted(() => { return { - constructorSpy: vi.fn<(options: Record) => void>(), + constructorSpy: + vi.fn< + (kind: string, options: Record, model: string) => void + >(), } }) -vi.mock('@google/genai', async (importOriginal) => { - const actual = await importOriginal() +vi.mock('@tanstack/ai-gemini', async (importOriginal) => { + const actual = await importOriginal() + const adapterClass = (kind: string) => { + return class { + readonly name = 'gemini' + readonly model: string - class MockGoogleGenAI { - constructor(options: Record) { - mocks.constructorSpy(options) + constructor(options: Record, model: string) { + this.model = model + mocks.constructorSpy(kind, options, model) + } } } return { ...actual, - GoogleGenAI: MockGoogleGenAI, + GeminiTextAdapter: adapterClass('text'), + GeminiImageAdapter: adapterClass('image'), + GeminiEmbeddingAdapter: adapterClass('embedding'), + GeminiTTSAdapter: adapterClass('speech'), + GeminiAudioAdapter: adapterClass('audio'), + GeminiVideoAdapter: adapterClass('video'), } }) @@ -49,11 +62,15 @@ describe('vertex factories', () => { expect(adapter.name).toBe('gemini') expect(adapter.model).toBe('gemini-3.7-flash') - expect(mocks.constructorSpy).toHaveBeenCalledExactlyOnceWith({ - project: 'my-project', - location: 'europe-west1', - vertexai: true, - }) + expect(mocks.constructorSpy).toHaveBeenCalledExactlyOnceWith( + 'text', + { + project: 'my-project', + location: 'europe-west1', + vertexai: true, + }, + 'gemini-3.7-flash', + ) }) it('builds summarize, image, embedding, speech, audio, and video adapters', () => { @@ -66,13 +83,42 @@ describe('vertex factories', () => { expect(vertexAudio('lyria-3-pro-preview', auth).name).toBe('gemini') expect(vertexVideo('veo-3.1-generate-preview', auth).name).toBe('gemini') - expect(mocks.constructorSpy).toHaveBeenCalledTimes(6) - for (const call of mocks.constructorSpy.mock.calls) { - expect(call[0]).toMatchObject({ - project: 'my-project', - location: 'europe-west1', - vertexai: true, - }) - } + expect(mocks.constructorSpy.mock.calls).toEqual([ + [ + 'text', + { project: 'my-project', location: 'europe-west1', vertexai: true }, + 'gemini-3.7-flash', + ], + [ + 'image', + { project: 'my-project', location: 'europe-west1', vertexai: true }, + 'gemini-3.1-flash-image', + ], + [ + 'embedding', + { project: 'my-project', location: 'europe-west1', vertexai: true }, + 'gemini-embedding-001', + ], + [ + 'speech', + { project: 'my-project', location: 'europe-west1', vertexai: true }, + 'gemini-3.1-flash-tts-preview', + ], + [ + 'audio', + { project: 'my-project', location: 'europe-west1', vertexai: true }, + 'lyria-3-pro-preview', + ], + [ + 'video', + { + project: 'my-project', + location: 'europe-west1', + vertexai: true, + allowUrlFetch: undefined, + }, + 'veo-3.1-generate-preview', + ], + ]) }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9f0217f4e..41cfa800f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,11 +195,11 @@ importers: specifier: workspace:* version: link:../../packages/ai-sandbox-local-process '@tanstack/react-router': - specifier: ^1.158.4 - version: 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.170.32 + version: 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.159.0 - version: 1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.49 + version: 1.168.49(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) lucide-react: specifier: ^0.561.0 version: 0.561.0(react@19.2.3) @@ -235,8 +235,8 @@ importers: specifier: ^4.1.18 version: 4.1.18(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/router-plugin': - specifier: ^1.158.4 - version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(supports-color@7.2.0)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.35 + version: 1.168.35(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@types/node': specifier: ^24.10.1 version: 24.10.3 @@ -283,11 +283,11 @@ importers: specifier: workspace:* version: link:../../packages/ai-sandbox-docker '@tanstack/react-router': - specifier: ^1.158.4 - version: 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.170.32 + version: 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.159.0 - version: 1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.49 + version: 1.168.49(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) lucide-react: specifier: ^0.561.0 version: 0.561.0(react@19.2.3) @@ -320,8 +320,8 @@ importers: specifier: ^4.1.18 version: 4.1.18(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/router-plugin': - specifier: ^1.158.4 - version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(supports-color@7.2.0)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.35 + version: 1.168.35(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@types/node': specifier: ^24.10.1 version: 24.10.3 @@ -471,14 +471,14 @@ importers: specifier: workspace:* version: link:../../packages/ai-react '@tanstack/react-router': - specifier: ^1.158.4 - version: 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.170.32 + version: 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.159.0 - version: 1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.49 + version: 1.168.49(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/router-plugin': - specifier: ^1.158.4 - version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(supports-color@7.2.0)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.35 + version: 1.168.35(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/store': specifier: ^0.11.1 version: 0.11.1 @@ -580,17 +580,17 @@ importers: specifier: ^0.9.10 version: 0.9.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10) '@tanstack/react-router': - specifier: ^1.158.4 - version: 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.170.32 + version: 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-router-devtools': - specifier: ^1.158.4 - version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.159.4)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.167.1 + version: 1.167.1(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.171.27)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.159.0 - version: 1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.49 + version: 1.168.49(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/router-plugin': - specifier: ^1.158.4 - version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(supports-color@7.2.0)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.35 + version: 1.168.35(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) capnweb: specifier: ^0.10.0 version: 0.10.0 @@ -761,23 +761,23 @@ importers: specifier: ^0.9.10 version: 0.9.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10) '@tanstack/react-router': - specifier: ^1.158.4 - version: 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.170.32 + version: 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-router-devtools': - specifier: ^1.158.4 - version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.159.4)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.167.1 + version: 1.167.1(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.171.27)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-router-ssr-query': - specifier: ^1.158.4 - version: 1.159.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.159.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.167.1 + version: 1.167.1(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.171.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.159.0 - version: 1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.49 + version: 1.168.49(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/react-store': - specifier: ^0.8.0 - version: 0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^0.11.1 + version: 0.11.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/router-plugin': - specifier: ^1.158.4 - version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(supports-color@7.2.0)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.35 + version: 1.168.35(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/store': specifier: ^0.11.1 version: 0.11.1 @@ -897,14 +897,14 @@ importers: specifier: workspace:* version: link:../../packages/ai-react '@tanstack/react-router': - specifier: ^1.158.4 - version: 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.170.32 + version: 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.159.0 - version: 1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.49 + version: 1.168.49(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/router-plugin': - specifier: ^1.158.4 - version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(supports-color@7.2.0)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.35 + version: 1.168.35(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) lucide-react: specifier: ^0.561.0 version: 0.561.0(react@19.2.3) @@ -1001,14 +1001,14 @@ importers: specifier: workspace:* version: link:../../packages/ai-openrouter '@tanstack/react-router': - specifier: ^1.158.4 - version: 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.170.32 + version: 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.159.0 - version: 1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.49 + version: 1.168.49(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/router-plugin': - specifier: ^1.158.4 - version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(supports-color@7.2.0)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.35 + version: 1.168.35(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) lucide-react: specifier: ^0.561.0 version: 0.561.0(react@19.2.3) @@ -1074,23 +1074,23 @@ importers: specifier: ^5.90.12 version: 5.90.12(react@19.2.3) '@tanstack/react-router': - specifier: ^1.158.4 - version: 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.170.32 + version: 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-router-devtools': - specifier: ^1.158.4 - version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.159.4)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.167.1 + version: 1.167.1(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.171.27)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-router-ssr-query': - specifier: ^1.158.4 - version: 1.159.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.159.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.167.1 + version: 1.167.1(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.171.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.159.0 - version: 1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.49 + version: 1.168.49(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/router-plugin': - specifier: ^1.158.4 - version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(supports-color@7.2.0)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.35 + version: 1.168.35(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/zod-adapter': specifier: ^1.140.1 - version: 1.166.9(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(zod@4.3.6) + version: 1.166.9(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(zod@4.3.6) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -1201,8 +1201,8 @@ importers: specifier: ^1.155.0 version: 1.155.0(@electric-sql/pglite@0.4.3)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.2.18)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.2.18)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)(rolldown@1.1.5)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/router-plugin': - specifier: ^1.158.4 - version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(supports-color@7.2.0)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.35 + version: 1.168.35(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/solid-ai-devtools': specifier: workspace:* version: link:../../packages/solid-ai-devtools @@ -1210,17 +1210,17 @@ importers: specifier: ^0.7.15 version: 0.7.17(csstype@3.2.3)(solid-js@1.9.10) '@tanstack/solid-router': - specifier: ^1.139.10 - version: 1.141.1(solid-js@1.9.10) + specifier: ^1.170.30 + version: 1.170.30(solid-js@1.9.10) '@tanstack/solid-router-devtools': - specifier: ^1.139.10 - version: 1.141.1(@tanstack/router-core@1.159.4)(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(csstype@3.2.3)(solid-js@1.9.10) + specifier: ^1.167.1 + version: 1.167.1(@tanstack/router-core@1.171.27)(@tanstack/solid-router@1.170.30(solid-js@1.9.10))(csstype@3.2.3)(solid-js@1.9.10) '@tanstack/solid-router-ssr-query': - specifier: ^1.139.10 - version: 1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.159.4)(@tanstack/solid-query@5.90.15(solid-js@1.9.10))(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(eslint@9.39.4(jiti@2.7.0))(solid-js@1.9.10)(supports-color@7.2.0)(typescript@5.9.3) + specifier: ^1.167.1 + version: 1.167.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.171.27)(@tanstack/solid-query@5.90.15(solid-js@1.9.10))(@tanstack/solid-router@1.170.30(solid-js@1.9.10))(solid-js@1.9.10) '@tanstack/solid-start': - specifier: ^1.139.10 - version: 1.141.1(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.6(srvx@0.11.17))(solid-js@1.9.10)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.47 + version: 1.168.47(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(solid-js@1.9.10)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/solid-store': specifier: ^0.8.0 version: 0.8.0(solid-js@1.9.10) @@ -2721,9 +2721,6 @@ importers: specifier: workspace:^ version: link:../ai-gemini devDependencies: - '@google/genai': - specifier: ^2.10.0 - version: 2.10.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)) '@tanstack/ai': specifier: workspace:* version: link:../ai @@ -3011,11 +3008,11 @@ importers: specifier: workspace:* version: link:../../packages/react-ai-devtools '@tanstack/react-router': - specifier: ^1.158.4 - version: 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.170.32 + version: 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.159.0 - version: 1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.49 + version: 1.168.49(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) arktype: specifier: ^2.1.28 version: 2.2.0 @@ -3132,11 +3129,11 @@ importers: specifier: ^0.9.10 version: 0.9.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10) '@tanstack/react-router': - specifier: ^1.158.4 - version: 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.170.32 + version: 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.159.0 - version: 1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^1.168.49 + version: 1.168.49(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/start': specifier: ^1.120.20 version: 1.120.20(340eefabdbae5b0c948fd6f8943277d1) @@ -4066,10 +4063,6 @@ packages: resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} - engines: {node: '>=6.9.0'} - '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} @@ -4086,10 +4079,6 @@ packages: resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.5': - resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} - engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} @@ -5916,9 +5905,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nothing-but/utils@0.17.0': - resolution: {integrity: sha512-TuCHcHLOqDL0SnaAxACfuRHBNRgNJcNn9X0GiH5H3YSDBVquCr3qEIG3FOQAuMyZCbu9w8nk2CHhOsn7IvhIwQ==} - '@nx/nx-darwin-arm64@23.1.1': resolution: {integrity: sha512-Rq/RXLX5uIvJQfb6kuUgEirquT5ARaAgQyNaMZVnOPAL5wuxaDvQag8WX/WUCIgbUKZuGkclJwM7Vlnvtn3bdg==} cpu: [arm64] @@ -8219,9 +8205,6 @@ packages: '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} - '@rolldown/pluginutils@1.0.0-beta.40': - resolution: {integrity: sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w==} - '@rolldown/pluginutils@1.0.0-beta.53': resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} @@ -8671,26 +8654,6 @@ packages: resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} - '@solid-devtools/debugger@0.28.1': - resolution: {integrity: sha512-6qIUI6VYkXoRnL8oF5bvh2KgH71qlJ18hNw/mwSyY6v48eb80ZR48/5PDXufUa3q+MBSuYa1uqTMwLewpay9eg==} - peerDependencies: - solid-js: ^1.9.0 - - '@solid-devtools/logger@0.9.11': - resolution: {integrity: sha512-THbiY1iQlieL6vdgJc4FIsLe7V8a57hod/Thm8zdKrTkWL88UPZjkBBfM+mVNGusd4OCnAN20tIFBhNnuT1Dew==} - peerDependencies: - solid-js: ^1.9.0 - - '@solid-devtools/shared@0.20.0': - resolution: {integrity: sha512-o5TACmUOQsxpzpOKCjbQqGk8wL8PMi+frXG9WNu4Lh3PQVUB6hs95Kl/S8xc++zwcMguUKZJn8h5URUiMOca6Q==} - peerDependencies: - solid-js: ^1.9.0 - - '@solid-primitives/bounds@0.1.3': - resolution: {integrity: sha512-UbiyKMdSPmtijcEDnYLQL3zzaejpwWDAJJ4Gt5P0hgVs6A72piov0GyNw7V2SroH7NZFwxlYS22YmOr8A5xc1Q==} - peerDependencies: - solid-js: ^1.6.12 - '@solid-primitives/event-listener@2.4.3': resolution: {integrity: sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg==} peerDependencies: @@ -8701,11 +8664,6 @@ packages: peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/media@2.3.3': - resolution: {integrity: sha512-hQ4hLOGvfbugQi5Eu1BFWAIJGIAzztq9x0h02xgBGl2l0Jaa3h7tg6bz5tV1NSuNYVGio4rPoa7zVQQLkkx9dA==} - peerDependencies: - solid-js: ^1.6.12 - '@solid-primitives/refs@1.1.2': resolution: {integrity: sha512-K7tf2thy7L+YJjdqXspXOg5xvNEOH8tgEWsp0+1mQk3obHBRD6hEjYZk7p7FlJphSZImS35je3UfmWuD7MhDfg==} peerDependencies: @@ -8721,21 +8679,11 @@ packages: peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/scheduled@1.5.2': - resolution: {integrity: sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA==} - peerDependencies: - solid-js: ^1.6.12 - '@solid-primitives/static-store@0.1.2': resolution: {integrity: sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw==} peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/styles@0.1.2': - resolution: {integrity: sha512-7iX5K+J5b1PRrbgw3Ki92uvU2LgQ0Kd/QMsrAZxDg5dpUBwMyTijZkA3bbs1ikZsT1oQhS41bTyKbjrXeU0Awg==} - peerDependencies: - solid-js: ^1.6.12 - '@solid-primitives/utils@6.3.2': resolution: {integrity: sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ==} peerDependencies: @@ -9027,6 +8975,10 @@ packages: resolution: {integrity: sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA==} engines: {node: '>=12'} + '@tanstack/history@1.162.1': + resolution: {integrity: sha512-DR9t6lfLVdrjgCwpglrR9DR7Ok8/HlXjcOE+goWXF3zyuLUO/ug7vMbSFxTqrQTtbRghJfyhmIZ0S6LhPIy44w==} + engines: {node: '>=20.19'} + '@tanstack/nitro-v2-vite-plugin@1.155.0': resolution: {integrity: sha512-iW9S2c4QxlT+WgKVT+Mw8hzvLqyVSg4UefzX74LX5dfUxbGDahxZItp+07JSD5Ok8ZdTlUHzwLnIsncXh/MNAQ==} engines: {node: '>=22.12'} @@ -9065,21 +9017,21 @@ packages: peerDependencies: react: ^18 || ^19 - '@tanstack/react-router-devtools@1.159.5': - resolution: {integrity: sha512-IIyomu+ypWTxyoYT32mxamVmdTs7ZCGcTbdj7HVvtD3xp1lvo/bwRXj9oERENmb+OAPOaWF2doRYC/pmKjK5vg==} - engines: {node: '>=12'} + '@tanstack/react-router-devtools@1.167.1': + resolution: {integrity: sha512-pjfGrmjj4d7naEPM7oshqFfwBoxDPNo/UxltlHH5ePbHsJ+plBhd+JaAewm1ueYOjZ0js9hckjWWDYXpCrSfKw==} + engines: {node: '>=20.19'} peerDependencies: - '@tanstack/react-router': ^1.159.5 - '@tanstack/router-core': ^1.159.4 + '@tanstack/react-router': ^1.170.19 + '@tanstack/router-core': ^1.171.16 react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' peerDependenciesMeta: '@tanstack/router-core': optional: true - '@tanstack/react-router-ssr-query@1.159.5': - resolution: {integrity: sha512-r/Um9DASjmaTdiyXA3cpooGLrSD1BlcXUNsRz4zAlvD38m0gIFvaykOM2Naztqm9H+2CJkocRZp2/kR69Xcmyg==} - engines: {node: '>=12'} + '@tanstack/react-router-ssr-query@1.167.1': + resolution: {integrity: sha512-W9j5JPnBikyafvuUfykFfHIWod58OAbAAa5leNkXBcoDoocghMmu6w9uZOmUZvAWT7CSvgj5tBUtF7CM2OoHXQ==} + engines: {node: '>=20.19'} peerDependencies: '@tanstack/query-core': '>=5.90.0' '@tanstack/react-query': '>=5.90.0' @@ -9094,9 +9046,9 @@ packages: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-router@1.159.5': - resolution: {integrity: sha512-rVb0MtKzP5c0BkWIoFgWBiRAJHYSU3bhsEHbT0cRdRLmlJiw21Awb6VEjgYq3hJiEhowcKKm6J8AdRD/8oZ5dQ==} - engines: {node: '>=12'} + '@tanstack/react-router@1.170.32': + resolution: {integrity: sha512-SIpxvaTKco100a5ZR3ePmArbhtm3XOx+w1dpGYY9gxHDta4iXSKDdQuhLonwJbIMkVJsU1rwXf0UDHMrF/1snw==} + engines: {node: '>=20.19'} peerDependencies: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' @@ -9108,8 +9060,8 @@ packages: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-start-client@1.159.5': - resolution: {integrity: sha512-Qynx7XWHI1rhVpSUx6P40zazcQVJRhl2fnAcSH0I6vaAxSuZm8lvI37YacXPRu8flvI/ZGlF095arxiyps+A0w==} + '@tanstack/react-start-client@1.168.30': + resolution: {integrity: sha512-qsZuykUl1EF0/rc1bin1RtjFzz07YMOTBzhOstSDzbOVm/WKf1QKFTN+qAZi74xlbXSWNuT2MiFRyg4RKwj8iw==} engines: {node: '>=22.12.0'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -9126,6 +9078,23 @@ packages: resolution: {integrity: sha512-z+4YL6shTtsHjk32yaIemQwgkx6FcqwPBYfeNt7Co2eOpWrvsoo/Fe9869/oIY2sPyhiWDs1rDb3e0qnAy8Cag==} engines: {node: '>=12'} + '@tanstack/react-start-rsc@0.1.48': + resolution: {integrity: sha512-UglRdTMuF3c4dvzL/gh4dMVbMWHsPy8ZgTQdT2qpTlyt1b/3m+R40tBFdsfTgw76VTXivW+qV/ih0PN9000XTw==} + engines: {node: '>=22.12.0'} + peerDependencies: + '@rspack/core': '>=2.0.0-0' + '@vitejs/plugin-rsc': '>=0.5.30' + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + react-server-dom-rspack: '>=0.0.2' + peerDependenciesMeta: + '@rspack/core': + optional: true + '@vitejs/plugin-rsc': + optional: true + react-server-dom-rspack: + optional: true + '@tanstack/react-start-server@1.141.1': resolution: {integrity: sha512-GsZ7De5CCDkKcrimvwJiYJHMFGw7LQRu8IuLvqyzYgQvJPcbkfY5UIQd2IZfgRNFegzb2aYzkAXwv5lxfdu1VQ==} engines: {node: '>=22.12.0'} @@ -9133,20 +9102,35 @@ packages: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-start-server@1.159.5': - resolution: {integrity: sha512-X4/SunwDTEbGkYTfM0gR+79amfk6phAM+2+NlC2s7TX2cD51xE8bUz2a//RxfOh9xg8f0f2CRIO34xTEDHGTfQ==} + '@tanstack/react-start-server@1.167.37': + resolution: {integrity: sha512-cODHpFU8vIm7AdHii9W3NEuwyruNmT5wLDZjRsJLT9Jp+7FACnfJrRbvxnp1ldSv/9mxcHKi/OgwbdHQeMZcAQ==} engines: {node: '>=22.12.0'} peerDependencies: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-start@1.159.5': - resolution: {integrity: sha512-vfnF7eYswAK54ru6Ay08nb0TXVzTBaVRsbbRW7hx2M0chgwtSx+YScYzoixqkccRARQBN8a/CeVq7vNFW8525w==} + '@tanstack/react-start@1.168.49': + resolution: {integrity: sha512-iQb1ZoEHqvZMGLR4G7v3tTtgL06yv/bwxvGP3waVHxVn7bRpyopM44YbOluaGkcJzc13ZTvdLKWYNAN3bxMX/Q==} engines: {node: '>=22.12.0'} peerDependencies: + '@rsbuild/core': ^2.0.0 + '@vitejs/plugin-rsc': '*' react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' vite: '>=7.0.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@vitejs/plugin-rsc': + optional: true + vite: + optional: true + + '@tanstack/react-store@0.11.1': + resolution: {integrity: sha512-HaIGKI3YLmjBYIvy5DFDY23oNaYZIsTZfngey07Uh5iLVJgM3bIGCnZeOFOqzjFld9JHWcaHJnasD/bKoGKwJQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 '@tanstack/react-store@0.8.0': resolution: {integrity: sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==} @@ -9154,6 +9138,12 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/react-store@0.9.3': + resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/router-core@1.131.50': resolution: {integrity: sha512-eojd4JZ5ziUhGEmXZ4CaVX5mQdiTMiz56Sp8ZQ6r7deb55Q+5G4JQDkeuXpI7HMAvzr+4qlsFeLaDRXXjXyOqQ==} engines: {node: '>=12'} @@ -9166,22 +9156,15 @@ packages: resolution: {integrity: sha512-MFzPH39ijNO83qJN3pe7x4iAlhZyqgao3sJIzv3SJ4Pnk12xMnzuDzIAQT/1WV6JolPQEcw0Wr4L5agF8yxoeg==} engines: {node: '>=12'} - '@tanstack/router-devtools-core@1.141.1': - resolution: {integrity: sha512-wD9yRvOk6FI+thiNBplhkGutPIPBlXvWu9ttU/obdFY5oXQj9WYgNS+IO9BEe8Pz5rNEu8zE/oLn4RUGIVdtnw==} - engines: {node: '>=12'} - peerDependencies: - '@tanstack/router-core': ^1.141.1 - csstype: ^3.0.10 - solid-js: '>=1.9.5' - peerDependenciesMeta: - csstype: - optional: true + '@tanstack/router-core@1.171.27': + resolution: {integrity: sha512-wDwSLvoLwIaNcnx9UNcN9Mb7Y8QwCYq1U1RQZwyN186gnkIoIYI2SOxy8VqH1vFigbkHkk4FmwMAQlghPgDK2g==} + engines: {node: '>=20.19'} - '@tanstack/router-devtools-core@1.159.4': - resolution: {integrity: sha512-qMUeIv+6n1mZOcO2raCIbdOeDeMpJEmgm6oMs/nWEG61lYrzJYaCcpBTviAX0nRhSiQSUCX9cHiosUEA0e2HAw==} - engines: {node: '>=12'} + '@tanstack/router-devtools-core@1.168.1': + resolution: {integrity: sha512-qr4voa4cpSMwQvS3867xkU3AB3MtJbTuovKIy+btjJ/Faju6er9w0nDylmD+005Mk/3YKw9/iueZJl2JAB7JOA==} + engines: {node: '>=20.19'} peerDependencies: - '@tanstack/router-core': ^1.159.4 + '@tanstack/router-core': ^1.171.16 csstype: ^3.0.10 peerDependenciesMeta: csstype: @@ -9191,14 +9174,14 @@ packages: resolution: {integrity: sha512-zlMBw5l88GIg3v+378JsfDYq3ejEaJmD3P1R+m0yEPxh0N//Id1FjKNSS7yJbejlK2WGVm9DUG46iBdTDMQM+Q==} engines: {node: '>=12'} - '@tanstack/router-generator@1.141.1': - resolution: {integrity: sha512-21RbVAoIDn7s/n/PKMN6U60d5hCeVADrBH/uN6B/poMT4MVYtJXqISVzkc2RAboVRw6eRdYFeF+YlwA3nF6y3Q==} - engines: {node: '>=12'} - '@tanstack/router-generator@1.159.4': resolution: {integrity: sha512-O8tICQoSuvK6vs3mvBdI3zVLFmYfj/AYDCX0a5msSADP/2S0GsgDDTB5ah731TqYCtjeNriaWz9iqst38cjF/Q==} engines: {node: '>=12'} + '@tanstack/router-generator@1.167.33': + resolution: {integrity: sha512-Z3lCWIPuRUMPmuI8Mm48x/s49TxmHOaFVZ52j1W1QKYrsFHyT6U/h9bqfHJDxfQ8kz7y9q+W1YPKZ15Ee7yuCA==} + engines: {node: '>=20.19'} + '@tanstack/router-plugin@1.131.50': resolution: {integrity: sha512-gdEBPGzx7llQNRnaqfPJ1iaPS3oqB8SlvKRG5l7Fxp4q4yINgkeowFYSKEhPOc9bjoNhGrIHOlvPTPXEzAQXzQ==} engines: {node: '>=12'} @@ -9220,12 +9203,12 @@ packages: webpack: optional: true - '@tanstack/router-plugin@1.141.1': - resolution: {integrity: sha512-SoNXm8QK8cqX1Q4y1AfLU0tZLOt4mB4wkdpBI/Mi3ZEezF8tIrmaenyJ3987cjT9jedTy3VDBab1wN0g8MoOXg==} + '@tanstack/router-plugin@1.159.5': + resolution: {integrity: sha512-i2LR3WRaBOAZ1Uab5QBG9UxZIRJ3V56JVu890NysbuX15rgzRiL5yLAbfenOHdhaHy2+4joX35VICAHuVWy7Og==} engines: {node: '>=12'} peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.141.1 + '@tanstack/react-router': ^1.159.5 vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' vite-plugin-solid: ^2.11.10 webpack: '>=5.92.0' @@ -9241,14 +9224,14 @@ packages: webpack: optional: true - '@tanstack/router-plugin@1.159.5': - resolution: {integrity: sha512-i2LR3WRaBOAZ1Uab5QBG9UxZIRJ3V56JVu890NysbuX15rgzRiL5yLAbfenOHdhaHy2+4joX35VICAHuVWy7Og==} - engines: {node: '>=12'} + '@tanstack/router-plugin@1.168.35': + resolution: {integrity: sha512-foDAZKFqHXae+oFbIgcsSvy2QCVRn7XdS3nhwcRvD+ed6JrKPUP/1lQMsZLJqWycgR1vkZF7gs955KGa0NZQ0w==} + engines: {node: '>=20.19'} peerDependencies: - '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.159.5 - vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' - vite-plugin-solid: ^2.11.10 + '@rsbuild/core': '>=1.0.2 || ^2.0.0' + '@tanstack/react-router': ^1.170.32 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0' + vite-plugin-solid: ^2.11.10 || ^3.0.0-0 webpack: '>=5.92.0' peerDependenciesMeta: '@rsbuild/core': @@ -9262,16 +9245,9 @@ packages: webpack: optional: true - '@tanstack/router-ssr-query-core@1.141.1': - resolution: {integrity: sha512-bkRXUhktifxBewnnphH59E0sGcsUI1NmNqxzCAmXIb93xYgafhjUGGYwfK6FqFBOmCB5isr32exGO3+UMHJr/A==} - engines: {node: '>=12'} - peerDependencies: - '@tanstack/query-core': '>=5.90.0' - '@tanstack/router-core': '>=1.127.0' - - '@tanstack/router-ssr-query-core@1.159.4': - resolution: {integrity: sha512-zqgJaf8iTk2ugrJqvU+b8NnFf3MKcnFaurtlp5tP38lNHQGW4En9UaOBOou/6zYZMlKQBcvqxSGJiwaLIyTK2g==} - engines: {node: '>=12'} + '@tanstack/router-ssr-query-core@1.169.1': + resolution: {integrity: sha512-rngux8s/3mPQzcjLYDLkNU31coYVyCgrVTfpdwqUdY5jIEHqGTXrO73DTkPR1PppwYUeVhmNCgl8TctRcnupjg==} + engines: {node: '>=20.19'} peerDependencies: '@tanstack/query-core': '>=5.90.0' '@tanstack/router-core': '>=1.127.0' @@ -9288,6 +9264,10 @@ packages: resolution: {integrity: sha512-qZ76eaLKU6Ae9iI/mc5zizBX149DXXZkBVVO3/QRIll79uKLJZHQlMKR++2ba7JsciBWz1pgpIBcCJPE9S0LVg==} engines: {node: '>=12'} + '@tanstack/router-utils@1.162.2': + resolution: {integrity: sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ==} + engines: {node: '>=20.19'} + '@tanstack/server-functions-plugin@1.131.2': resolution: {integrity: sha512-hWsaSgEZAVyzHg8+IcJWCEtfI9ZSlNELErfLiGHG9XCHEXMegFWsrESsKHlASzJqef9RsuOLDl+1IMPIskwdDw==} engines: {node: '>=12'} @@ -9307,50 +9287,56 @@ packages: peerDependencies: solid-js: ^1.6.0 - '@tanstack/solid-router-devtools@1.141.1': - resolution: {integrity: sha512-ezm8V1UR47kY/xquIobascexHkbWOcH1YXc8BRvYDKV33NEdeUhMVUqkZmwuojlRYXhF7LEfj2aSwZr0uBfH8A==} - engines: {node: '>=12'} + '@tanstack/solid-router-devtools@1.167.1': + resolution: {integrity: sha512-GZiRbHx7E1vbuv1zGjPl1EmCKbwKZKoCU0TjycBjU6SW0rMt/24SON0MbtMbPlLs3XE28jFi7C5odwE2hryh/Q==} + engines: {node: '>=20.19'} peerDependencies: - '@tanstack/router-core': ^1.141.1 - '@tanstack/solid-router': ^1.141.1 + '@tanstack/router-core': ^1.171.16 + '@tanstack/solid-router': ^1.170.19 solid-js: ^1.9.10 peerDependenciesMeta: '@tanstack/router-core': optional: true - '@tanstack/solid-router-ssr-query@1.141.1': - resolution: {integrity: sha512-jM2RKblxWbrMV2nmZ+L+Y2HvfBZ448tZHXYBP/NrQa2YAHVXCZfVE5ojjB5vyDZHc7ueFAF8+yIvMGnIQQE99g==} - engines: {node: '>=12'} + '@tanstack/solid-router-ssr-query@1.167.1': + resolution: {integrity: sha512-N76V3oz1ZQlTveBTRpLGOTlUtXU5GKOD++IkMnLBtuHEsge9ReluB94QBgVA+ElpQgdeso9uKUs/JsPluAynUA==} + engines: {node: '>=20.19'} peerDependencies: '@tanstack/query-core': '>=5.90.0' '@tanstack/solid-query': '>=5.90.0' '@tanstack/solid-router': '>=1.127.0' solid-js: ^1.9.10 - '@tanstack/solid-router@1.141.1': - resolution: {integrity: sha512-xcWkYdwvTXzQwLd4gI0efq7dUzXlcTyent/UDrGf1K5dDBqlFSAj9c11vNwP0+YvCaPt4U1r8czyMD1Qr3aNGA==} - engines: {node: '>=12'} + '@tanstack/solid-router@1.170.30': + resolution: {integrity: sha512-FozWeln6hx6o2T45gFFdkGQJxvq/LN0EUlzx7+eENy9HJH/thEBSb89KTkQahULzs9bsMOufzeXJJVgB6H9/wA==} + engines: {node: '>=20.19'} peerDependencies: solid-js: ^1.9.10 - '@tanstack/solid-start-client@1.141.1': - resolution: {integrity: sha512-NGc70Jj9DoWFieFX9iQG70fmuTvX7nfTUG5vlboxBzvTuPrIdyeddrQVnBX/kXUdFzLGzez7QlxqyOrm3zouuQ==} + '@tanstack/solid-start-client@1.168.29': + resolution: {integrity: sha512-+F3Z/J1SVxPj2Hs53Wvos2DhIe7cGvlHpwZD9x0+aIWofIlkRSnrGm2wehWZzun1tzgZNnK2L2thkQlXVtuVaw==} engines: {node: '>=22.12.0'} peerDependencies: solid-js: '>=1.0.0' - '@tanstack/solid-start-server@1.141.1': - resolution: {integrity: sha512-vIuw74+6cgBmh1zAqc27dKHd5rU2K4V6nHt+u7vBKUUu/9QIF7PoQD0fj2dWnrjnz7EJKb1W7Gr9rAtOdglgGw==} + '@tanstack/solid-start-server@1.167.36': + resolution: {integrity: sha512-7WMqLxe1Yr+9Q+Ze2dsYrPnousiPmA5LndJtg1MnaIRNecL1NInd5jsiIwNB42qOWTwcGr8aD8LnHH4dcVh/wA==} engines: {node: '>=22.12.0'} peerDependencies: solid-js: ^1.0.0 - '@tanstack/solid-start@1.141.1': - resolution: {integrity: sha512-b7B7UUPn3GANvnvTusiryTr+CUEs+lMMFRRiDiBZXx+Yb1cqtp/UYilEDHdqnx2xUoAhR0FBd4kRcieCV69DVg==} + '@tanstack/solid-start@1.168.47': + resolution: {integrity: sha512-yqXrj5Q1wGh/9Gb/wGp117AavC48tFk29LP/2oGHBJKq4ZATYes8s3yPBRSWlSkqQG9N2yQzjmF+sutXZSZVzw==} engines: {node: '>=22.12.0'} peerDependencies: + '@rsbuild/core': ^2.0.0 solid-js: '>=1.0.0' vite: '>=7.0.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + vite: + optional: true '@tanstack/solid-store@0.8.0': resolution: {integrity: sha512-JwqTedbxyOGw7mfmdGkB0RGgefRCw/tNauc8tlMcaS1mV5wTFT8c1KIB3LgttuHaanMJEBeqQJ7bc/R0WTP1fA==} @@ -9373,6 +9359,10 @@ packages: resolution: {integrity: sha512-9j2i1PRTIGcYAD+509znve0ngK81ZUfbX4XCpoNFMaUUpRHoEPPK5I9+PzLFvL9sNOto67x+WULCUggzX+lEKQ==} engines: {node: '>=22.12.0'} + '@tanstack/start-client-core@1.170.27': + resolution: {integrity: sha512-Ro6ZSM0NgYKDMxM0e8qyU4mBfnld2Zb74BA/9f4i35C0Y3IAA8Zxs/DIPfOo9VRYWp5c5n5Q8dRBn2qn0vtPhQ==} + engines: {node: '>=22.12.0'} + '@tanstack/start-config@1.120.20': resolution: {integrity: sha512-oH/mfTSHV8Qbil74tWicPLW6+kKmT3esXCnDzvrkhi3+N8ZuVUDr01Qpil0Wxf9lLPfM5L6VX03nF4hSU8vljg==} engines: {node: '>=12'} @@ -9385,23 +9375,27 @@ packages: resolution: {integrity: sha512-D69B78L6pcFN5X5PHaydv7CScQcKLzJeEYqs7jpuyyqGQHSUIZUjS955j+Sir8cHhuDIovCe2LmsYHeZfWf3dQ==} engines: {node: '>=22.12.0'} + '@tanstack/start-fn-stubs@1.162.0': + resolution: {integrity: sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ==} + engines: {node: '>=22.12.0'} + '@tanstack/start-plugin-core@1.131.50': resolution: {integrity: sha512-eFvMA0chqLtHbq+8ojp1fXN7AQjhmeoOpQaZaU1d51wb7ugetrn0k3OuHblxtE/O0L4HEC9s4X5zmFJt0vLh0w==} engines: {node: '>=12'} peerDependencies: vite: '>=6.0.0' - '@tanstack/start-plugin-core@1.141.1': - resolution: {integrity: sha512-jXfgKeM4XX7aoP7WF/cJZH52N0ewIYRdP028ItXaSwUMcDyBO+PPTNbSEqTu3zbzLsmTOOyuJEpIrVA0/lmbjA==} - engines: {node: '>=22.12.0'} - peerDependencies: - vite: '>=7.0.0' - - '@tanstack/start-plugin-core@1.159.5': - resolution: {integrity: sha512-QGiBw+L3qu2sUY0Tg9KovxjDSi5kevoANEcq9RLX7iIhLkTjrILN6hnAlXZUzqk5Egaf0aN2yWhwI4HWucMprw==} + '@tanstack/start-plugin-core@1.171.39': + resolution: {integrity: sha512-Zyj6G4MDFLXcHYhPavhewCBo8dsxi3qvPk31zl/QtTzYzOY9rJHDDBGarKsJq20ziChmkGn/sttYqb4vGCxJDA==} engines: {node: '>=22.12.0'} peerDependencies: + '@rsbuild/core': ^2.0.0 vite: '>=7.0.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + vite: + optional: true '@tanstack/start-server-core@1.131.50': resolution: {integrity: sha512-3SWwwhW2GKMhPSaqWRal6Jj1Y9ObfdWEXKFQid1LBuk5xk/Es4bmW68o++MbVgs/GxUxyeZ3TRVqb0c7RG1sog==} @@ -9415,6 +9409,10 @@ packages: resolution: {integrity: sha512-sGpr+iil+pcY3Gglvbnxaj7fCEPTQJv4oF7YA24SVv8YvayLXtBXpF26miJLA+KR9P31dQdPYe1gTjv5zRyvHg==} engines: {node: '>=22.12.0'} + '@tanstack/start-server-core@1.169.31': + resolution: {integrity: sha512-56w8l+Fao01YCrmv0hzNxL3b3FRmqLRGdE11izZNQsW+1CcSYuehAM5khWKABuI1DI/UIBLGV7BwL4Dlg0eHCw==} + engines: {node: '>=22.12.0'} + '@tanstack/start-server-functions-client@1.131.50': resolution: {integrity: sha512-4aM17fFdVAFH6uLPswKJxzrhhIjcCwKqzfTcgY3OnhUKnaZBTQwJA+nUHQCI6IWvEvrcrNVtFTtv13TkDk3YMw==} engines: {node: '>=12'} @@ -9447,6 +9445,10 @@ packages: resolution: {integrity: sha512-iGkmuCIq3PLI4GKOGwgUNHQKZ13YV8LGq62o2hVnyXE64Jm2SP7c5z6D1ndydpk4JwdRzQKlcOFT/1agvS6Nsg==} engines: {node: '>=22.12.0'} + '@tanstack/start-storage-context@1.167.29': + resolution: {integrity: sha512-8qfprC5774XMRDQlMogkfiGpFLiBf0xDG4bMFUbfkSzpCAQwpLbgGY4Zwft22O9rKYq2vUXusKAdVjvJTUEquQ==} + engines: {node: '>=22.12.0'} + '@tanstack/start@1.120.20': resolution: {integrity: sha512-fQO+O/5xJpli5KlV6pwDz6DtpbqO/0atdVSyVnkemzk0Mej9azm4HXtw+cKkIPtsSplWs4B1EbMtgGMb9ADhSA==} engines: {node: '>=12'} @@ -9460,6 +9462,9 @@ packages: '@tanstack/store@0.8.0': resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==} + '@tanstack/store@0.9.3': + resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} + '@tanstack/typedoc-config@0.3.4': resolution: {integrity: sha512-WXJavpiNqumNwdBQTM3wBqbHaULgoZ3cIri7xJinKqX/mOGXgc6ZghfAardD2PydP8tOcPf+xhrd1Qk/am9DMg==} engines: {node: '>=18'} @@ -9468,14 +9473,14 @@ packages: resolution: {integrity: sha512-VEEOxc4mvyu67O+Bl0APtYjwcNRcL9it9B4HKbNgcBTIOEalhk+ufBl4kiqc8WP1sx1+NAaiS+3CcJBhrqaSRg==} engines: {node: '>=12'} - '@tanstack/virtual-file-routes@1.141.0': - resolution: {integrity: sha512-CJrWtr6L9TVzEImm9S7dQINx+xJcYP/aDkIi6gnaWtIgbZs1pnzsE0yJc2noqXZ+yAOqLx3TBGpBEs9tS0P9/A==} - engines: {node: '>=12'} - '@tanstack/virtual-file-routes@1.154.7': resolution: {integrity: sha512-cHHDnewHozgjpI+MIVp9tcib6lYEQK5MyUr0ChHpHFGBl8Xei55rohFK0I0ve/GKoHeioaK42Smd8OixPp6CTg==} engines: {node: '>=12'} + '@tanstack/virtual-file-routes@1.162.0': + resolution: {integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==} + engines: {node: '>=20.19'} + '@tanstack/vite-config@0.6.0': resolution: {integrity: sha512-TtcetFc7aBm9BBclG3/8pM8ZlyzS8yNwi+eScN4gqsB9xnxyOPCbuvvkk//ybfQj8MVEZavQ03EOQxmOJdW8Rw==} engines: {node: '>=18'} @@ -9756,32 +9761,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.49.0': - resolution: {integrity: sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.59.4': resolution: {integrity: sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.49.0': - resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.59.4': resolution: {integrity: sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.49.0': - resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/tsconfig-utils@8.59.4': resolution: {integrity: sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -9795,33 +9784,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.49.0': - resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.59.4': resolution: {integrity: sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.49.0': - resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.59.4': resolution: {integrity: sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.49.0': - resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.59.4': resolution: {integrity: sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -9829,10 +9801,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.49.0': - resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.59.4': resolution: {integrity: sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -11024,6 +10992,9 @@ packages: cookie-es@2.0.1: resolution: {integrity: sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==} + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -11757,13 +11728,6 @@ packages: engines: {node: '>=6.0'} hasBin: true - eslint-plugin-solid@0.14.5: - resolution: {integrity: sha512-nfuYK09ah5aJG/oEN6P1qziy1zLgW4PDWe75VNPi4CEFYk1x2AEqwFeQfEPR7gNn0F2jOeqKhx2E+5oNCOBYWQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 - typescript: '>=4.8.4' - eslint-plugin-unused-imports@4.3.0: resolution: {integrity: sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==} peerDependencies: @@ -12415,6 +12379,16 @@ packages: crossws: optional: true + h3@2.0.1-rc.20: + resolution: {integrity: sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==} + engines: {node: '>=20.11.1'} + hasBin: true + peerDependencies: + crossws: ^0.4.1 + peerDependenciesMeta: + crossws: + optional: true + h3@2.0.1-rc.22: resolution: {integrity: sha512-Esv0DMIuPkCTSWCA0vO73vcTqwzH1wjSrAO1TXNu/K3up1sZHa9EKMapbmxCDYBeymC3fVTk4qxp7ogQWQ+KgA==} engines: {node: '>=20.11.1'} @@ -12540,10 +12514,6 @@ packages: html-link-extractor@1.0.5: resolution: {integrity: sha512-ADd49pudM157uWHwHQPUSX4ssMsvR/yHIswOR5CUfBdK9g9ZYGMhVSE6KZVHJ6kCkR0gH4htsfzU6zECDNVwyw==} - html-tags@3.3.1: - resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} - engines: {node: '>=8'} - html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -12771,10 +12741,6 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} - is-html@2.0.0: - resolution: {integrity: sha512-S+OpgB5i7wzIue/YSE5hg0e5ZYfG3hhpNh9KGl6ayJ38p7ED6wxQLd1TV91xHpcTvw90KMJ9EwN3F/iNflHBVg==} - engines: {node: '>=8'} - is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -13098,9 +13064,6 @@ packages: jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - kebab-case@1.0.2: - resolution: {integrity: sha512-7n6wXq4gNgBELfDCpzKc+mRrZFs7D+wgfF5WRFLNAr4DA/qtr9Js8uOAVAfHhuLMfAcQ0pRKqbpjx+TcJVdE1Q==} - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -13135,9 +13098,6 @@ packages: knitwork@1.3.0: resolution: {integrity: sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==} - known-css-properties@0.30.0: - resolution: {integrity: sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==} - kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} @@ -15310,6 +15270,12 @@ packages: peerDependencies: seroval: ^1.0 + seroval-plugins@1.6.2: + resolution: {integrity: sha512-TfxuUjlbBESzUOWdTkTKqvSmav0ABym+itetDXLK6mDz8SmrpdI30aF8RTXE8Bvq+tH/1yIDkvy3W0lfQb1ipQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + seroval@1.3.2: resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} engines: {node: '>=10'} @@ -15322,6 +15288,10 @@ packages: resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==} engines: {node: '>=10'} + seroval@1.6.2: + resolution: {integrity: sha512-mPT+SD2TrlB6wvte1KkYOYUkubaTbd6pZ/6Kk3C9nxzrHmCZyhxOO7XGAeL7f+yLKZglzGtM9odUVvg/EhO+vQ==} + engines: {node: '>=10'} + serve-placeholder@2.0.2: resolution: {integrity: sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==} @@ -15904,12 +15874,6 @@ packages: ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -16235,6 +16199,39 @@ packages: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} + unplugin@3.3.0: + resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@farmfe/core': '*' + '@rspack/core': '*' + bun-types-no-globals: '*' + esbuild: '*' + rolldown: '*' + rollup: '*' + unloader: '*' + vite: '*' + webpack: '*' + peerDependenciesMeta: + '@farmfe/core': + optional: true + '@rspack/core': + optional: true + bun-types-no-globals: + optional: true + esbuild: + optional: true + rolldown: + optional: true + rollup: + optional: true + unloader: + optional: true + vite: + optional: true + webpack: + optional: true + unrun@0.2.19: resolution: {integrity: sha512-DbwbJ9BvPEb3BeZnIpP9S5tGLO/JIgPQ3JrpMRFIfZMZfMG19f26OlLbC2ml8RRdrI2ZA7z2t+at5tsIHbh6Qw==} engines: {node: '>=20.19.0'} @@ -16635,14 +16632,6 @@ packages: yaml: optional: true - vitefu@1.1.1: - resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 - peerDependenciesMeta: - vite: - optional: true - vitefu@1.1.3: resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} peerDependencies: @@ -17050,6 +17039,9 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zone.js@0.15.1: resolution: {integrity: sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==} @@ -17244,7 +17236,7 @@ snapshots: '@angular/compiler-cli@21.2.17(@angular/compiler@21.2.20)(typescript@5.9.3)': dependencies: '@angular/compiler': 21.2.20 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@jridgewell/sourcemap-codec': 1.5.5 chokidar: 5.0.0 convert-source-map: 1.9.0 @@ -17731,9 +17723,9 @@ snapshots: '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helpers': 7.28.4 '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 + '@babel/template': 7.29.7 '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3(supports-color@7.2.0) @@ -17751,9 +17743,9 @@ snapshots: '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5(supports-color@7.2.0)) '@babel/helpers': 7.28.4 '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 + '@babel/template': 7.29.7 '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3(supports-color@7.2.0) @@ -17763,12 +17755,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/core@7.29.0(supports-color@7.2.0)': + '@babel/core@7.29.0': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.0(supports-color@7.2.0)) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.0) '@babel/helpers': 7.29.7 '@babel/parser': 7.29.7 '@babel/template': 7.29.7 @@ -17806,7 +17798,7 @@ snapshots: '@babel/generator@7.28.5': dependencies: '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -17890,19 +17882,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 - '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.0) - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/traverse': 7.29.7 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -17988,15 +17967,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/helper-module-imports': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 - transitivePeerDependencies: - - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -18006,9 +17976,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.0(supports-color@7.2.0))': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0 '@babel/helper-module-imports': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 '@babel/traverse': 7.29.7 @@ -18074,15 +18044,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/helper-member-expression-to-functions': 7.29.7 - '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/traverse': 7.29.7 - transitivePeerDependencies: - - supports-color - '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -18146,7 +18107,7 @@ snapshots: '@babel/parser@7.29.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@babel/parser@7.29.7': dependencies: @@ -18196,21 +18157,11 @@ snapshots: '@babel/core': 7.28.5(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -18231,11 +18182,6 @@ snapshots: '@babel/core': 7.28.5(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -18346,14 +18292,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -18502,17 +18440,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-typescript@7.28.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-typescript@7.28.5(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -18548,17 +18475,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - '@babel/preset-typescript@7.28.5(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -18581,12 +18497,6 @@ snapshots: '@babel/runtime@7.29.2': {} - '@babel/template@7.27.2': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -18599,8 +18509,8 @@ snapshots: '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.5 - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -18629,11 +18539,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/types@7.28.5': - dependencies: - '@babel/helper-string-parser': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -19702,7 +19607,7 @@ snapshots: '@expo/metro-config@56.0.13(expo@56.0.5)(typescript@5.9.3)': dependencies: '@babel/code-frame': 7.29.0 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/generator': 7.29.1 '@expo/config': 56.0.9(typescript@5.9.3) '@expo/env': 2.3.0 @@ -20537,8 +20442,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nothing-but/utils@0.17.0': {} - '@nx/nx-darwin-arm64@23.1.1': optional: true @@ -22594,8 +22497,6 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rolldown/pluginutils@1.0.0-beta.40': {} - '@rolldown/pluginutils@1.0.0-beta.53': {} '@rolldown/pluginutils@1.0.0-rc.3': {} @@ -22935,48 +22836,6 @@ snapshots: '@smithy/util-buffer-from': 2.2.0 tslib: 2.8.1 - '@solid-devtools/debugger@0.28.1(solid-js@1.9.10)': - dependencies: - '@nothing-but/utils': 0.17.0 - '@solid-devtools/shared': 0.20.0(solid-js@1.9.10) - '@solid-primitives/bounds': 0.1.3(solid-js@1.9.10) - '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.10) - '@solid-primitives/keyboard': 1.3.3(solid-js@1.9.10) - '@solid-primitives/rootless': 1.5.2(solid-js@1.9.10) - '@solid-primitives/scheduled': 1.5.2(solid-js@1.9.10) - '@solid-primitives/static-store': 0.1.2(solid-js@1.9.10) - '@solid-primitives/utils': 6.3.2(solid-js@1.9.10) - solid-js: 1.9.10 - - '@solid-devtools/logger@0.9.11(solid-js@1.9.10)': - dependencies: - '@nothing-but/utils': 0.17.0 - '@solid-devtools/debugger': 0.28.1(solid-js@1.9.10) - '@solid-devtools/shared': 0.20.0(solid-js@1.9.10) - '@solid-primitives/utils': 6.3.2(solid-js@1.9.10) - solid-js: 1.9.10 - - '@solid-devtools/shared@0.20.0(solid-js@1.9.10)': - dependencies: - '@nothing-but/utils': 0.17.0 - '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.10) - '@solid-primitives/media': 2.3.3(solid-js@1.9.10) - '@solid-primitives/refs': 1.1.2(solid-js@1.9.10) - '@solid-primitives/rootless': 1.5.2(solid-js@1.9.10) - '@solid-primitives/scheduled': 1.5.2(solid-js@1.9.10) - '@solid-primitives/static-store': 0.1.2(solid-js@1.9.10) - '@solid-primitives/styles': 0.1.2(solid-js@1.9.10) - '@solid-primitives/utils': 6.3.2(solid-js@1.9.10) - solid-js: 1.9.10 - - '@solid-primitives/bounds@0.1.3(solid-js@1.9.10)': - dependencies: - '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.10) - '@solid-primitives/resize-observer': 2.1.3(solid-js@1.9.10) - '@solid-primitives/static-store': 0.1.2(solid-js@1.9.10) - '@solid-primitives/utils': 6.3.2(solid-js@1.9.10) - solid-js: 1.9.10 - '@solid-primitives/event-listener@2.4.3(solid-js@1.9.10)': dependencies: '@solid-primitives/utils': 6.3.2(solid-js@1.9.10) @@ -22989,14 +22848,6 @@ snapshots: '@solid-primitives/utils': 6.3.2(solid-js@1.9.10) solid-js: 1.9.10 - '@solid-primitives/media@2.3.3(solid-js@1.9.10)': - dependencies: - '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.10) - '@solid-primitives/rootless': 1.5.2(solid-js@1.9.10) - '@solid-primitives/static-store': 0.1.2(solid-js@1.9.10) - '@solid-primitives/utils': 6.3.2(solid-js@1.9.10) - solid-js: 1.9.10 - '@solid-primitives/refs@1.1.2(solid-js@1.9.10)': dependencies: '@solid-primitives/utils': 6.3.2(solid-js@1.9.10) @@ -23015,21 +22866,11 @@ snapshots: '@solid-primitives/utils': 6.3.2(solid-js@1.9.10) solid-js: 1.9.10 - '@solid-primitives/scheduled@1.5.2(solid-js@1.9.10)': - dependencies: - solid-js: 1.9.10 - '@solid-primitives/static-store@0.1.2(solid-js@1.9.10)': dependencies: '@solid-primitives/utils': 6.3.2(solid-js@1.9.10) solid-js: 1.9.10 - '@solid-primitives/styles@0.1.2(solid-js@1.9.10)': - dependencies: - '@solid-primitives/rootless': 1.5.2(solid-js@1.9.10) - '@solid-primitives/utils': 6.3.2(solid-js@1.9.10) - solid-js: 1.9.10 - '@solid-primitives/utils@6.3.2(solid-js@1.9.10)': dependencies: solid-js: 1.9.10 @@ -23361,6 +23202,8 @@ snapshots: '@tanstack/history@1.154.14': {} + '@tanstack/history@1.162.1': {} + '@tanstack/nitro-v2-vite-plugin@1.155.0(@electric-sql/pglite@0.4.3)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)(rolldown@1.2.4)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': dependencies: nitropack: 2.13.1(@electric-sql/pglite@0.4.3)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)(rolldown@1.2.4) @@ -23470,23 +23313,23 @@ snapshots: '@tanstack/query-core': 5.90.12 react: 19.2.3 - '@tanstack/react-router-devtools@1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.159.4)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-router-devtools@1.167.1(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.171.27)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/react-router': 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-devtools-core': 1.159.4(@tanstack/router-core@1.159.4)(csstype@3.2.3) + '@tanstack/react-router': 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-devtools-core': 1.168.1(@tanstack/router-core@1.171.27)(csstype@3.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@tanstack/router-core': 1.159.4 + '@tanstack/router-core': 1.171.27 transitivePeerDependencies: - csstype - '@tanstack/react-router-ssr-query@1.159.5(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.159.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-router-ssr-query@1.167.1(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.171.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/query-core': 5.90.12 '@tanstack/react-query': 5.90.12(react@19.2.3) - '@tanstack/react-router': 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-ssr-query-core': 1.159.4(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.159.4) + '@tanstack/react-router': 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-ssr-query-core': 1.169.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.171.27) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: @@ -23503,16 +23346,14 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/history': 1.154.14 - '@tanstack/react-store': 0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-core': 1.159.4 + '@tanstack/history': 1.162.1 + '@tanstack/react-store': 0.9.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-core': 1.171.27 isbot: 5.1.32 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 '@tanstack/react-start-client@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: @@ -23524,19 +23365,17 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-start-client@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-start-client@1.168.30(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/react-router': 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-core': 1.159.4 - '@tanstack/start-client-core': 1.159.4 + '@tanstack/react-router': 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-core': 1.171.27 + '@tanstack/start-client-core': 1.170.27 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - '@tanstack/react-start-plugin@1.131.50(e7a157c953e155a2765d901b34df38b3)': + '@tanstack/react-start-plugin@1.131.50(5e76e58f58fcdc553da46eb093ac388b)': dependencies: - '@tanstack/start-plugin-core': 1.131.50(1a1cdd8b9c2c533357f243540b1932ef) + '@tanstack/start-plugin-core': 1.131.50(b114ec7757aa79b8f316a176b0457a25) '@vitejs/plugin-react': 4.7.0(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) pathe: 2.0.3 vite: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) @@ -23626,6 +23465,32 @@ snapshots: - xml2js - yaml + '@tanstack/react-start-rsc@0.1.48(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': + dependencies: + '@tanstack/react-router': 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-core': 1.171.27 + '@tanstack/router-utils': 1.162.2 + '@tanstack/start-client-core': 1.170.27 + '@tanstack/start-fn-stubs': 1.162.0 + '@tanstack/start-plugin-core': 1.171.39(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + '@tanstack/start-storage-context': 1.167.29 + pathe: 2.0.3 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + transitivePeerDependencies: + - '@farmfe/core' + - '@rsbuild/core' + - bun-types-no-globals + - crossws + - esbuild + - rolldown + - rollup + - supports-color + - unloader + - vite + - vite-plugin-solid + - webpack + '@tanstack/react-start-server@1.141.1(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/history': 1.141.0 @@ -23638,38 +23503,52 @@ snapshots: transitivePeerDependencies: - crossws - '@tanstack/react-start-server@1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-start-server@1.167.37(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/history': 1.154.14 - '@tanstack/react-router': 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-core': 1.159.4 - '@tanstack/start-client-core': 1.159.4 - '@tanstack/start-server-core': 1.159.4(crossws@0.4.6(srvx@0.11.17)) + '@tanstack/react-router': 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-core': 1.171.27 + '@tanstack/start-server-core': 1.169.31(crossws@0.4.6(srvx@0.11.17)) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - crossws - '@tanstack/react-start@1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': + '@tanstack/react-start@1.168.49(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': dependencies: - '@tanstack/react-router': 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/react-start-client': 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/react-start-server': 1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-utils': 1.158.0 - '@tanstack/start-client-core': 1.159.4 - '@tanstack/start-plugin-core': 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.6(srvx@0.11.17))(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) - '@tanstack/start-server-core': 1.159.4(crossws@0.4.6(srvx@0.11.17)) + '@tanstack/react-router': 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-start-client': 1.168.30(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-start-rsc': 0.1.48(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + '@tanstack/react-start-server': 1.167.37(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-utils': 1.162.2 + '@tanstack/start-client-core': 1.170.27 + '@tanstack/start-plugin-core': 1.171.39(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + '@tanstack/start-server-core': 1.169.31(crossws@0.4.6(srvx@0.11.17)) pathe: 2.0.3 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + optionalDependencies: vite: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) transitivePeerDependencies: - - '@rsbuild/core' + - '@farmfe/core' + - '@rspack/core' + - bun-types-no-globals - crossws + - esbuild + - react-server-dom-rspack + - rolldown + - rollup - supports-color + - unloader - vite-plugin-solid - webpack + '@tanstack/react-store@0.11.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@tanstack/store': 0.11.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + use-sync-external-store: 1.6.0(react@19.2.3) + '@tanstack/react-store@0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/store': 0.8.0 @@ -23677,6 +23556,13 @@ snapshots: react-dom: 19.2.3(react@19.2.3) use-sync-external-store: 1.6.0(react@19.2.3) + '@tanstack/react-store@0.9.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@tanstack/store': 0.9.3 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + use-sync-external-store: 1.6.0(react@19.2.3) + '@tanstack/router-core@1.131.50': dependencies: '@tanstack/history': 1.131.2 @@ -23707,22 +23593,18 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.141.1(@tanstack/router-core@1.159.4)(csstype@3.2.3)(solid-js@1.9.10)': + '@tanstack/router-core@1.171.27': dependencies: - '@tanstack/router-core': 1.159.4 - clsx: 2.1.1 - goober: 2.1.18(csstype@3.2.3) - solid-js: 1.9.10 - tiny-invariant: 1.3.3 - optionalDependencies: - csstype: 3.2.3 + '@tanstack/history': 1.162.1 + cookie-es: 3.1.1 + seroval: 1.6.2 + seroval-plugins: 1.6.2(seroval@1.6.2) - '@tanstack/router-devtools-core@1.159.4(@tanstack/router-core@1.159.4)(csstype@3.2.3)': + '@tanstack/router-devtools-core@1.168.1(@tanstack/router-core@1.171.27)(csstype@3.2.3)': dependencies: - '@tanstack/router-core': 1.159.4 + '@tanstack/router-core': 1.171.27 clsx: 2.1.1 goober: 2.1.18(csstype@3.2.3) - tiny-invariant: 1.3.3 optionalDependencies: csstype: 3.2.3 @@ -23739,11 +23621,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-generator@1.141.1': + '@tanstack/router-generator@1.159.4': dependencies: - '@tanstack/router-core': 1.141.1 - '@tanstack/router-utils': 1.141.0 - '@tanstack/virtual-file-routes': 1.141.0 + '@tanstack/router-core': 1.159.4 + '@tanstack/router-utils': 1.158.0 + '@tanstack/virtual-file-routes': 1.154.7 prettier: 3.7.4 recast: 0.23.11 source-map: 0.7.6 @@ -23752,20 +23634,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-generator@1.159.4': + '@tanstack/router-generator@1.167.33': dependencies: - '@tanstack/router-core': 1.159.4 - '@tanstack/router-utils': 1.158.0 - '@tanstack/virtual-file-routes': 1.154.7 + '@babel/types': 7.29.7 + '@tanstack/router-core': 1.171.27 + '@tanstack/router-utils': 1.162.2 + '@tanstack/virtual-file-routes': 1.162.0 + jiti: 2.7.0 + magic-string: 0.30.21 prettier: 3.7.4 - recast: 0.23.11 - source-map: 0.7.6 - tsx: 4.21.0 - zod: 3.25.76 + zod: 4.4.3 transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.131.50(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': + '@tanstack/router-plugin@1.131.50(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) @@ -23782,13 +23664,13 @@ snapshots: unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-router': 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) vite: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) vite-plugin-solid: 2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.141.1(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': + '@tanstack/router-plugin@1.159.5(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) @@ -23796,52 +23678,49 @@ snapshots: '@babel/template': 7.29.7 '@babel/traverse': 7.29.7 '@babel/types': 7.29.7 - '@tanstack/router-core': 1.141.1 - '@tanstack/router-generator': 1.141.1 - '@tanstack/router-utils': 1.141.0 - '@tanstack/virtual-file-routes': 1.141.0 - babel-dead-code-elimination: 1.0.10 + '@tanstack/router-core': 1.159.4 + '@tanstack/router-generator': 1.159.4 + '@tanstack/router-utils': 1.158.0 + '@tanstack/virtual-file-routes': 1.154.7 chokidar: 3.6.0 unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-router': 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) vite: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) vite-plugin-solid: 2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(supports-color@7.2.0)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': + '@tanstack/router-plugin@1.168.35(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.0) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) + '@babel/core': 7.29.7 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 '@babel/types': 7.29.7 - '@tanstack/router-core': 1.159.4 - '@tanstack/router-generator': 1.159.4 - '@tanstack/router-utils': 1.158.0 - '@tanstack/virtual-file-routes': 1.154.7 - chokidar: 3.6.0 - unplugin: 2.3.11 - zod: 3.25.76 + '@tanstack/router-core': 1.171.27 + '@tanstack/router-generator': 1.167.33 + '@tanstack/router-utils': 1.162.2 + chokidar: 5.0.0 + unplugin: 3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + zod: 4.4.3 optionalDependencies: - '@tanstack/react-router': 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-router': 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) vite: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) vite-plugin-solid: 2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) transitivePeerDependencies: + - '@farmfe/core' + - '@rspack/core' + - bun-types-no-globals + - esbuild + - rolldown + - rollup - supports-color + - unloader - '@tanstack/router-ssr-query-core@1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.159.4)': + '@tanstack/router-ssr-query-core@1.169.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.171.27)': dependencies: '@tanstack/query-core': 5.90.12 - '@tanstack/router-core': 1.159.4 - - '@tanstack/router-ssr-query-core@1.159.4(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.159.4)': - dependencies: - '@tanstack/query-core': 5.90.12 - '@tanstack/router-core': 1.159.4 + '@tanstack/router-core': 1.171.27 '@tanstack/router-utils@1.131.2': dependencies: @@ -23869,7 +23748,20 @@ snapshots: '@tanstack/router-utils@1.158.0': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + ansis: 4.2.0 + babel-dead-code-elimination: 1.0.12 + diff: 8.0.2 + pathe: 2.0.3 + tinyglobby: 0.2.17 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-utils@1.162.2': + dependencies: '@babel/generator': 7.29.7 '@babel/parser': 7.29.7 '@babel/types': 7.29.7 @@ -23927,80 +23819,74 @@ snapshots: '@tanstack/query-core': 5.90.12 solid-js: 1.9.10 - '@tanstack/solid-router-devtools@1.141.1(@tanstack/router-core@1.159.4)(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(csstype@3.2.3)(solid-js@1.9.10)': + '@tanstack/solid-router-devtools@1.167.1(@tanstack/router-core@1.171.27)(@tanstack/solid-router@1.170.30(solid-js@1.9.10))(csstype@3.2.3)(solid-js@1.9.10)': dependencies: - '@tanstack/router-devtools-core': 1.141.1(@tanstack/router-core@1.159.4)(csstype@3.2.3)(solid-js@1.9.10) - '@tanstack/solid-router': 1.141.1(solid-js@1.9.10) + '@tanstack/router-devtools-core': 1.168.1(@tanstack/router-core@1.171.27)(csstype@3.2.3) + '@tanstack/solid-router': 1.170.30(solid-js@1.9.10) solid-js: 1.9.10 optionalDependencies: - '@tanstack/router-core': 1.159.4 + '@tanstack/router-core': 1.171.27 transitivePeerDependencies: - csstype - '@tanstack/solid-router-ssr-query@1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.159.4)(@tanstack/solid-query@5.90.15(solid-js@1.9.10))(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(eslint@9.39.4(jiti@2.7.0))(solid-js@1.9.10)(supports-color@7.2.0)(typescript@5.9.3)': + '@tanstack/solid-router-ssr-query@1.167.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.171.27)(@tanstack/solid-query@5.90.15(solid-js@1.9.10))(@tanstack/solid-router@1.170.30(solid-js@1.9.10))(solid-js@1.9.10)': dependencies: '@tanstack/query-core': 5.90.12 - '@tanstack/router-ssr-query-core': 1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.159.4) + '@tanstack/router-ssr-query-core': 1.169.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.171.27) '@tanstack/solid-query': 5.90.15(solid-js@1.9.10) - '@tanstack/solid-router': 1.141.1(solid-js@1.9.10) - eslint-plugin-solid: 0.14.5(eslint@9.39.4(jiti@2.7.0))(supports-color@7.2.0)(typescript@5.9.3) + '@tanstack/solid-router': 1.170.30(solid-js@1.9.10) solid-js: 1.9.10 transitivePeerDependencies: - '@tanstack/router-core' - - eslint - - supports-color - - typescript - '@tanstack/solid-router@1.141.1(solid-js@1.9.10)': + '@tanstack/solid-router@1.170.30(solid-js@1.9.10)': dependencies: - '@solid-devtools/logger': 0.9.11(solid-js@1.9.10) '@solid-primitives/refs': 1.1.2(solid-js@1.9.10) '@solidjs/meta': 0.29.4(solid-js@1.9.10) - '@tanstack/history': 1.141.0 - '@tanstack/router-core': 1.141.1 - '@tanstack/solid-store': 0.8.0(solid-js@1.9.10) + '@tanstack/history': 1.162.1 + '@tanstack/router-core': 1.171.27 isbot: 5.1.32 solid-js: 1.9.10 - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - '@tanstack/solid-start-client@1.141.1(solid-js@1.9.10)': + '@tanstack/solid-start-client@1.168.29(solid-js@1.9.10)': dependencies: - '@tanstack/router-core': 1.141.1 - '@tanstack/solid-router': 1.141.1(solid-js@1.9.10) - '@tanstack/start-client-core': 1.141.1 + '@tanstack/router-core': 1.171.27 + '@tanstack/solid-router': 1.170.30(solid-js@1.9.10) + '@tanstack/start-client-core': 1.170.27 solid-js: 1.9.10 - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - '@tanstack/solid-start-server@1.141.1(crossws@0.4.6(srvx@0.11.17))(solid-js@1.9.10)': + '@tanstack/solid-start-server@1.167.36(crossws@0.4.6(srvx@0.11.17))(solid-js@1.9.10)': dependencies: - '@solidjs/meta': 0.29.4(solid-js@1.9.10) - '@tanstack/history': 1.141.0 - '@tanstack/router-core': 1.141.1 - '@tanstack/solid-router': 1.141.1(solid-js@1.9.10) - '@tanstack/start-client-core': 1.141.1 - '@tanstack/start-server-core': 1.141.1(crossws@0.4.6(srvx@0.11.17)) + '@tanstack/router-core': 1.171.27 + '@tanstack/solid-router': 1.170.30(solid-js@1.9.10) + '@tanstack/start-server-core': 1.169.31(crossws@0.4.6(srvx@0.11.17)) solid-js: 1.9.10 transitivePeerDependencies: - crossws - '@tanstack/solid-start@1.141.1(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.6(srvx@0.11.17))(solid-js@1.9.10)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': + '@tanstack/solid-start@1.168.47(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(solid-js@1.9.10)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': dependencies: - '@tanstack/solid-router': 1.141.1(solid-js@1.9.10) - '@tanstack/solid-start-client': 1.141.1(solid-js@1.9.10) - '@tanstack/solid-start-server': 1.141.1(crossws@0.4.6(srvx@0.11.17))(solid-js@1.9.10) - '@tanstack/start-client-core': 1.141.1 - '@tanstack/start-plugin-core': 1.141.1(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.6(srvx@0.11.17))(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) - '@tanstack/start-server-core': 1.141.1(crossws@0.4.6(srvx@0.11.17)) + '@tanstack/solid-router': 1.170.30(solid-js@1.9.10) + '@tanstack/solid-start-client': 1.168.29(solid-js@1.9.10) + '@tanstack/solid-start-server': 1.167.36(crossws@0.4.6(srvx@0.11.17))(solid-js@1.9.10) + '@tanstack/start-client-core': 1.170.27 + '@tanstack/start-plugin-core': 1.171.39(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + '@tanstack/start-server-core': 1.169.31(crossws@0.4.6(srvx@0.11.17)) pathe: 2.0.3 solid-js: 1.9.10 + optionalDependencies: vite: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) transitivePeerDependencies: - - '@rsbuild/core' + - '@farmfe/core' + - '@rspack/core' - '@tanstack/react-router' + - bun-types-no-globals - crossws + - esbuild + - rolldown + - rollup - supports-color + - unloader - vite-plugin-solid - webpack @@ -24085,12 +23971,19 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 + '@tanstack/start-client-core@1.170.27': + dependencies: + '@tanstack/router-core': 1.171.27 + '@tanstack/start-fn-stubs': 1.162.0 + '@tanstack/start-storage-context': 1.167.29 + seroval: 1.6.2 + '@tanstack/start-config@1.120.20(340eefabdbae5b0c948fd6f8943277d1)': dependencies: - '@tanstack/react-router': 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/react-start-plugin': 1.131.50(e7a157c953e155a2765d901b34df38b3) + '@tanstack/react-router': 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-start-plugin': 1.131.50(5e76e58f58fcdc553da46eb093ac388b) '@tanstack/router-generator': 1.159.4 - '@tanstack/router-plugin': 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(supports-color@7.2.0)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + '@tanstack/router-plugin': 1.159.5(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/server-functions-plugin': 1.141.0(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/start-server-functions-handler': 1.120.19(crossws@0.4.6(srvx@0.11.17)) '@vitejs/plugin-react': 4.7.0(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) @@ -24153,14 +24046,16 @@ snapshots: '@tanstack/start-fn-stubs@1.154.7': {} - '@tanstack/start-plugin-core@1.131.50(1a1cdd8b9c2c533357f243540b1932ef)': + '@tanstack/start-fn-stubs@1.162.0': {} + + '@tanstack/start-plugin-core@1.131.50(b114ec7757aa79b8f316a176b0457a25)': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.29.7 '@babel/types': 7.29.7 '@tanstack/router-core': 1.131.50 '@tanstack/router-generator': 1.131.50 - '@tanstack/router-plugin': 1.131.50(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + '@tanstack/router-plugin': 1.131.50(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/router-utils': 1.131.2 '@tanstack/server-functions-plugin': 1.131.2(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@tanstack/start-server-core': 1.131.50 @@ -24173,7 +24068,7 @@ snapshots: pathe: 2.0.3 ufo: 1.6.3 vite: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) - vitefu: 1.1.1(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + vitefu: 1.1.3(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) xmlbuilder2: 3.1.1 zod: 3.25.76 transitivePeerDependencies: @@ -24211,65 +24106,41 @@ snapshots: - webpack - xml2js - '@tanstack/start-plugin-core@1.141.1(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.6(srvx@0.11.17))(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/types': 7.29.0 - '@rolldown/pluginutils': 1.0.0-beta.40 - '@tanstack/router-core': 1.141.1 - '@tanstack/router-generator': 1.141.1 - '@tanstack/router-plugin': 1.141.1(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) - '@tanstack/router-utils': 1.141.0 - '@tanstack/server-functions-plugin': 1.141.0(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) - '@tanstack/start-client-core': 1.141.1 - '@tanstack/start-server-core': 1.141.1(crossws@0.4.6(srvx@0.11.17)) - babel-dead-code-elimination: 1.0.10 - cheerio: 1.1.2 - exsolve: 1.0.8 - pathe: 2.0.3 - srvx: 0.8.16 - tinyglobby: 0.2.17 - ufo: 1.6.1 - vite: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) - vitefu: 1.1.1(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) - xmlbuilder2: 4.0.3 - zod: 3.25.76 - transitivePeerDependencies: - - '@rsbuild/core' - - '@tanstack/react-router' - - crossws - - supports-color - - vite-plugin-solid - - webpack - - '@tanstack/start-plugin-core@1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.6(srvx@0.11.17))(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': + '@tanstack/start-plugin-core@1.171.39(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': dependencies: '@babel/code-frame': 7.27.1 - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.7 '@babel/types': 7.29.7 - '@rolldown/pluginutils': 1.0.0-beta.40 - '@tanstack/router-core': 1.159.4 - '@tanstack/router-generator': 1.159.4 - '@tanstack/router-plugin': 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(supports-color@7.2.0)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) - '@tanstack/router-utils': 1.158.0 - '@tanstack/start-client-core': 1.159.4 - '@tanstack/start-server-core': 1.159.4(crossws@0.4.6(srvx@0.11.17)) - cheerio: 1.1.2 + '@tanstack/router-core': 1.171.27 + '@tanstack/router-generator': 1.167.33 + '@tanstack/router-plugin': 1.168.35(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite-plugin-solid@2.11.14(solid-js@1.9.10)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + '@tanstack/router-utils': 1.162.2 + '@tanstack/start-server-core': 1.169.31(crossws@0.4.6(srvx@0.11.17)) exsolve: 1.0.8 + lightningcss: 1.33.0 pathe: 2.0.3 + picomatch: 4.0.5 + seroval: 1.6.2 + source-map: 0.7.6 srvx: 0.11.17 tinyglobby: 0.2.17 ufo: 1.6.3 - vite: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) - vitefu: 1.1.1(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + vitefu: 1.1.3(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) xmlbuilder2: 4.0.3 - zod: 3.25.76 + zod: 4.4.3 + optionalDependencies: + vite: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) transitivePeerDependencies: - - '@rsbuild/core' + - '@farmfe/core' + - '@rspack/core' - '@tanstack/react-router' + - bun-types-no-globals - crossws + - esbuild + - rolldown + - rollup - supports-color + - unloader - vite-plugin-solid - webpack @@ -24309,6 +24180,18 @@ snapshots: transitivePeerDependencies: - crossws + '@tanstack/start-server-core@1.169.31(crossws@0.4.6(srvx@0.11.17))': + dependencies: + '@tanstack/history': 1.162.1 + '@tanstack/router-core': 1.171.27 + '@tanstack/start-client-core': 1.170.27 + '@tanstack/start-storage-context': 1.167.29 + fetchdts: 0.1.7 + h3-v2: h3@2.0.1-rc.20(crossws@0.4.6(srvx@0.11.17)) + seroval: 1.6.2 + transitivePeerDependencies: + - crossws + '@tanstack/start-server-functions-client@1.131.50(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': dependencies: '@tanstack/server-functions-plugin': 1.131.2(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) @@ -24363,6 +24246,10 @@ snapshots: dependencies: '@tanstack/router-core': 1.159.4 + '@tanstack/start-storage-context@1.167.29': + dependencies: + '@tanstack/router-core': 1.171.27 + '@tanstack/start@1.120.20(340eefabdbae5b0c948fd6f8943277d1)': dependencies: '@tanstack/react-start-client': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -24432,6 +24319,8 @@ snapshots: '@tanstack/store@0.8.0': {} + '@tanstack/store@0.9.3': {} + '@tanstack/typedoc-config@0.3.4': dependencies: typedoc: 0.28.20(@typescript/typescript6@6.0.2) @@ -24441,10 +24330,10 @@ snapshots: '@tanstack/virtual-file-routes@1.131.2': {} - '@tanstack/virtual-file-routes@1.141.0': {} - '@tanstack/virtual-file-routes@1.154.7': {} + '@tanstack/virtual-file-routes@1.162.0': {} + '@tanstack/vite-config@0.6.0(@microsoft/api-extractor@7.47.7(@types/node@24.10.3))(esbuild@0.28.1)(rolldown@1.1.5)(rollup@4.60.1)(supports-color@7.2.0)(typescript@7.0.2)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': dependencies: vite: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) @@ -24462,9 +24351,9 @@ snapshots: - typescript - webpack - '@tanstack/zod-adapter@1.166.9(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(zod@4.3.6)': + '@tanstack/zod-adapter@1.166.9(@tanstack/react-router@1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(zod@4.3.6)': dependencies: - '@tanstack/react-router': 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-router': 1.170.32(react-dom@19.2.3(react@19.2.3))(react@19.2.3) zod: 4.3.6 '@testing-library/dom@10.4.1': @@ -24793,15 +24682,6 @@ snapshots: - supports-color optional: true - '@typescript-eslint/project-service@8.49.0(supports-color@7.2.0)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) - '@typescript-eslint/types': 8.59.4 - debug: 4.4.3(supports-color@7.2.0) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/project-service@8.59.4(typescript@7.0.2)': dependencies: '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@7.0.2) @@ -24812,21 +24692,12 @@ snapshots: - supports-color optional: true - '@typescript-eslint/scope-manager@8.49.0': - dependencies: - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/visitor-keys': 8.49.0 - '@typescript-eslint/scope-manager@8.59.4': dependencies: '@typescript-eslint/types': 8.59.4 '@typescript-eslint/visitor-keys': 8.59.4 optional: true - '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.59.4(typescript@7.0.2)': dependencies: typescript: 7.0.2 @@ -24845,25 +24716,8 @@ snapshots: - supports-color optional: true - '@typescript-eslint/types@8.49.0': {} - '@typescript-eslint/types@8.59.4': {} - '@typescript-eslint/typescript-estree@8.49.0(supports-color@7.2.0)(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.49.0(supports-color@7.2.0)(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/visitor-keys': 8.49.0 - debug: 4.4.3(supports-color@7.2.0) - minimatch: 9.0.5 - semver: 7.8.4 - tinyglobby: 0.2.17 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.59.4(typescript@7.0.2)': dependencies: '@typescript-eslint/project-service': 8.59.4(typescript@7.0.2) @@ -24880,17 +24734,6 @@ snapshots: - supports-color optional: true - '@typescript-eslint/utils@8.49.0(eslint@9.39.4(jiti@2.7.0))(supports-color@7.2.0)(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.49.0 - '@typescript-eslint/types': 8.49.0 - '@typescript-eslint/typescript-estree': 8.49.0(supports-color@7.2.0)(typescript@5.9.3) - eslint: 9.39.4(jiti@2.7.0) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@7.0.2)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) @@ -24903,11 +24746,6 @@ snapshots: - supports-color optional: true - '@typescript-eslint/visitor-keys@8.49.0': - dependencies: - '@typescript-eslint/types': 8.49.0 - eslint-visitor-keys: 4.2.1 - '@typescript-eslint/visitor-keys@8.59.4': dependencies: '@typescript-eslint/types': 8.59.4 @@ -25632,15 +25470,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/helper-module-imports': 7.18.6 - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.0) - '@babel/types': 7.29.7 - html-entities: 2.3.3 - parse5: 7.3.0 - babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.29.7): dependencies: '@babel/core': 7.29.7 @@ -25794,13 +25623,6 @@ snapshots: - '@babel/core' - supports-color - babel-preset-solid@1.9.10(@babel/core@7.29.0)(solid-js@1.9.10): - dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - babel-plugin-jsx-dom-expressions: 0.40.3(@babel/core@7.29.0) - optionalDependencies: - solid-js: 1.9.10 - babel-preset-solid@1.9.10(@babel/core@7.29.7)(solid-js@1.9.10): dependencies: '@babel/core': 7.29.7 @@ -26391,6 +26213,8 @@ snapshots: cookie-es@2.0.1: {} + cookie-es@3.1.1: {} + cookie-signature@1.2.2: {} cookie@0.6.0: {} @@ -26967,9 +26791,9 @@ snapshots: esbuild-plugin-solid@0.5.0(esbuild@0.28.1)(solid-js@1.9.10): dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - babel-preset-solid: 1.9.10(@babel/core@7.29.0)(solid-js@1.9.10) + '@babel/core': 7.29.7 + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.7) + babel-preset-solid: 1.9.10(@babel/core@7.29.7)(solid-js@1.9.10) esbuild: 0.28.1 solid-js: 1.9.10 transitivePeerDependencies: @@ -27106,19 +26930,6 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-plugin-solid@0.14.5(eslint@9.39.4(jiti@2.7.0))(supports-color@7.2.0)(typescript@5.9.3): - dependencies: - '@typescript-eslint/utils': 8.49.0(eslint@9.39.4(jiti@2.7.0))(supports-color@7.2.0)(typescript@5.9.3) - eslint: 9.39.4(jiti@2.7.0) - estraverse: 5.3.0 - is-html: 2.0.0 - kebab-case: 1.0.2 - known-css-properties: 0.30.0 - style-to-object: 1.0.14 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@7.0.2))(eslint@9.39.4(jiti@2.7.0))(typescript@7.0.2))(eslint@9.39.4(jiti@2.7.0)): dependencies: eslint: 9.39.4(jiti@2.7.0) @@ -27962,6 +27773,13 @@ snapshots: optionalDependencies: crossws: 0.4.6(srvx@0.11.17) + h3@2.0.1-rc.20(crossws@0.4.6(srvx@0.11.17)): + dependencies: + rou3: 0.8.1 + srvx: 0.11.17 + optionalDependencies: + crossws: 0.4.6(srvx@0.11.17) + h3@2.0.1-rc.22(crossws@0.4.6(srvx@0.11.17)): dependencies: rou3: 0.8.1 @@ -28138,8 +27956,6 @@ snapshots: dependencies: cheerio: 1.1.2 - html-tags@3.3.1: {} - html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} @@ -28368,10 +28184,6 @@ snapshots: is-hexadecimal@2.0.1: {} - is-html@2.0.0: - dependencies: - html-tags: 3.3.1 - is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 @@ -28736,8 +28548,6 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 - kebab-case@1.0.2: {} - keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -28779,8 +28589,6 @@ snapshots: knitwork@1.3.0: {} - known-css-properties@0.30.0: {} - kolorist@1.8.0: {} lan-network@0.2.1: {} @@ -29406,7 +29214,7 @@ snapshots: metro-source-map@0.84.4: dependencies: '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 flow-enums-runtime: 0.0.6 invariant: 2.2.4 metro-symbolicate: 0.84.4 @@ -31814,7 +31622,7 @@ snapshots: dependencies: '@babel/generator': 7.28.5 '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 ast-kit: 2.2.0 birpc: 3.0.0 dts-resolver: 2.1.3(oxc-resolver@11.21.3) @@ -32150,12 +31958,18 @@ snapshots: dependencies: seroval: 1.5.0 + seroval-plugins@1.6.2(seroval@1.6.2): + dependencies: + seroval: 1.6.2 + seroval@1.3.2: {} seroval@1.4.0: {} seroval@1.5.0: {} + seroval@1.6.2: {} + serve-placeholder@2.0.2: dependencies: defu: 6.1.4 @@ -32812,10 +32626,6 @@ snapshots: ts-algebra@2.0.0: {} - ts-api-utils@2.1.0(typescript@5.9.3): - dependencies: - typescript: 5.9.3 - ts-api-utils@2.5.0(typescript@7.0.2): dependencies: typescript: 7.0.2 @@ -33208,6 +33018,17 @@ snapshots: picomatch: 4.0.5 webpack-virtual-modules: 0.6.2 + unplugin@3.3.0(esbuild@0.28.1)(rolldown@1.2.4)(rollup@4.60.1)(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)): + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.5 + webpack-virtual-modules: 0.6.2 + optionalDependencies: + esbuild: 0.28.1 + rolldown: 1.2.4 + rollup: 4.60.1 + vite: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) + unrun@0.2.19(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1): dependencies: rolldown: 1.0.0-beta.53(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) @@ -33571,10 +33392,6 @@ snapshots: tsx: 4.23.12 yaml: 2.9.0 - vitefu@1.1.1(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)): - optionalDependencies: - vite: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) - vitefu@1.1.3(vite@8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)): optionalDependencies: vite: 8.2.1(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) @@ -34001,6 +33818,8 @@ snapshots: zod@4.3.6: {} + zod@4.4.3: {} + zone.js@0.15.1: {} zwitch@2.0.4: {} diff --git a/testing/e2e/package.json b/testing/e2e/package.json index 0d100c5160..59e0b387f1 100644 --- a/testing/e2e/package.json +++ b/testing/e2e/package.json @@ -45,8 +45,8 @@ "@tanstack/devtools-event-bus": "^0.4.1", "@tanstack/nitro-v2-vite-plugin": "^1.155.0", "@tanstack/react-ai-devtools": "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", "arktype": "^2.1.28", "react": "^19.2.3", "react-dom": "^19.2.3", diff --git a/testing/e2e/src/components/ImageGenUI.tsx b/testing/e2e/src/components/ImageGenUI.tsx index 93cec8b7ce..bd59c7d159 100644 --- a/testing/e2e/src/components/ImageGenUI.tsx +++ b/testing/e2e/src/components/ImageGenUI.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useRef, useState } from 'react' import { useGenerateImage, fetchServerSentEvents, @@ -67,8 +67,12 @@ export function ImageGenUI({ } } - const { generate, result, isLoading, error, status } = + const { generate, result, isLoading, error, status, runId } = useGenerateImage(connectionOptions()) + const activeTuple = useRef({ runId: '', status: 'idle', isLoading: false }) + if (runId && status === 'generating' && isLoading) { + activeTuple.current = { runId, status, isLoading } + } const handleGenerate = async () => { if (!imageFile) { @@ -89,6 +93,12 @@ export function ImageGenUI({ return (
+ {activeTuple.current.runId} + {activeTuple.current.status} + + {String(activeTuple.current.isLoading)} + + {runId ?? ''}
{ if (!imageFile) { @@ -92,6 +96,12 @@ export function VideoGenUI({ return (
+ {activeTuple.current.runId} + {activeTuple.current.status} + + {String(activeTuple.current.isLoading)} + + {runId ?? ''}
rootRouteImport, -} as any) -const ToolsTestRoute = ToolsTestRouteImport.update({ - id: '/tools-test', - path: '/tools-test', +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', getParentRoute: () => rootRouteImport, } as any) -const PersistenceDurabilityRoute = PersistenceDurabilityRouteImport.update({ - id: '/persistence-durability', - path: '/persistence-durability', +const ByokRoute = ByokRouteImport.update({ + id: '/byok', + path: '/byok', getParentRoute: () => rootRouteImport, } as any) -const MiddlewareTestRoute = MiddlewareTestRouteImport.update({ - id: '/middleware-test', - path: '/middleware-test', +const ChatClientDefaultBridgeRoute = ChatClientDefaultBridgeRouteImport.update({ + id: '/chat-client-default-bridge', + path: '/chat-client-default-bridge', getParentRoute: () => rootRouteImport, } as any) -const MarkdownCjkRoute = MarkdownCjkRouteImport.update({ - id: '/markdown-cjk', - path: '/markdown-cjk', +const DevtoolsChatRoute = DevtoolsChatRouteImport.update({ + id: '/devtools-chat', + path: '/devtools-chat', getParentRoute: () => rootRouteImport, } as any) -const JoinRunClientToolRoute = JoinRunClientToolRouteImport.update({ - id: '/join-run-client-tool', - path: '/join-run-client-tool', +const DevtoolsGenerationHooksRoute = DevtoolsGenerationHooksRouteImport.update({ + id: '/devtools-generation-hooks', + path: '/devtools-generation-hooks', getParentRoute: () => rootRouteImport, } as any) -const InterruptsTestRoute = InterruptsTestRouteImport.update({ - id: '/interrupts-test', - path: '/interrupts-test', +const DevtoolsMemoryRoute = DevtoolsMemoryRouteImport.update({ + id: '/devtools-memory', + path: '/devtools-memory', getParentRoute: () => rootRouteImport, } as any) -const GenerationPersistenceServerRoute = - GenerationPersistenceServerRouteImport.update({ - id: '/generation-persistence-server', - path: '/generation-persistence-server', - getParentRoute: () => rootRouteImport, - } as any) -const GenerationPersistenceResumeRoute = - GenerationPersistenceResumeRouteImport.update({ - id: '/generation-persistence-resume', - path: '/generation-persistence-resume', - getParentRoute: () => rootRouteImport, - } as any) -const ForeignInterruptRoute = ForeignInterruptRouteImport.update({ - id: '/foreign-interrupt', - path: '/foreign-interrupt', +const DevtoolsRouteARoute = DevtoolsRouteARouteImport.update({ + id: '/devtools-route-a', + path: '/devtools-route-a', getParentRoute: () => rootRouteImport, } as any) -const DevtoolsToolsRoute = DevtoolsToolsRouteImport.update({ - id: '/devtools-tools', - path: '/devtools-tools', +const DevtoolsRouteBRoute = DevtoolsRouteBRouteImport.update({ + id: '/devtools-route-b', + path: '/devtools-route-b', getParentRoute: () => rootRouteImport, } as any) const DevtoolsStructuredRoute = DevtoolsStructuredRouteImport.update({ @@ -156,44 +139,61 @@ const DevtoolsStructuredRoute = DevtoolsStructuredRouteImport.update({ path: '/devtools-structured', getParentRoute: () => rootRouteImport, } as any) -const DevtoolsRouteBRoute = DevtoolsRouteBRouteImport.update({ - id: '/devtools-route-b', - path: '/devtools-route-b', +const DevtoolsToolsRoute = DevtoolsToolsRouteImport.update({ + id: '/devtools-tools', + path: '/devtools-tools', getParentRoute: () => rootRouteImport, } as any) -const DevtoolsRouteARoute = DevtoolsRouteARouteImport.update({ - id: '/devtools-route-a', - path: '/devtools-route-a', +const ForeignInterruptRoute = ForeignInterruptRouteImport.update({ + id: '/foreign-interrupt', + path: '/foreign-interrupt', getParentRoute: () => rootRouteImport, } as any) -const DevtoolsMemoryRoute = DevtoolsMemoryRouteImport.update({ - id: '/devtools-memory', - path: '/devtools-memory', +const GenerationPersistenceResumeRoute = + GenerationPersistenceResumeRouteImport.update({ + id: '/generation-persistence-resume', + path: '/generation-persistence-resume', + getParentRoute: () => rootRouteImport, + } as any) +const GenerationPersistenceServerRoute = + GenerationPersistenceServerRouteImport.update({ + id: '/generation-persistence-server', + path: '/generation-persistence-server', + getParentRoute: () => rootRouteImport, + } as any) +const InterruptsTestRoute = InterruptsTestRouteImport.update({ + id: '/interrupts-test', + path: '/interrupts-test', getParentRoute: () => rootRouteImport, } as any) -const DevtoolsGenerationHooksRoute = DevtoolsGenerationHooksRouteImport.update({ - id: '/devtools-generation-hooks', - path: '/devtools-generation-hooks', +const JoinRunClientToolRoute = JoinRunClientToolRouteImport.update({ + id: '/join-run-client-tool', + path: '/join-run-client-tool', getParentRoute: () => rootRouteImport, } as any) -const DevtoolsChatRoute = DevtoolsChatRouteImport.update({ - id: '/devtools-chat', - path: '/devtools-chat', +const MarkdownCjkRoute = MarkdownCjkRouteImport.update({ + id: '/markdown-cjk', + path: '/markdown-cjk', getParentRoute: () => rootRouteImport, } as any) -const ChatClientDefaultBridgeRoute = ChatClientDefaultBridgeRouteImport.update({ - id: '/chat-client-default-bridge', - path: '/chat-client-default-bridge', +const MiddlewareTestRoute = MiddlewareTestRouteImport.update({ + id: '/middleware-test', + path: '/middleware-test', getParentRoute: () => rootRouteImport, } as any) -const ByokRoute = ByokRouteImport.update({ - id: '/byok', - path: '/byok', +const PersistenceDurabilityRoute = PersistenceDurabilityRouteImport.update({ + id: '/persistence-durability', + path: '/persistence-durability', getParentRoute: () => rootRouteImport, } as any) -const IndexRoute = IndexRouteImport.update({ - id: '/', - path: '/', +const ToolsTestRoute = ToolsTestRouteImport.update({ + id: '/tools-test', + path: '/tools-test', + getParentRoute: () => rootRouteImport, +} as any) +const WebsocketAdapterRoute = WebsocketAdapterRouteImport.update({ + id: '/websocket-adapter', + path: '/websocket-adapter', getParentRoute: () => rootRouteImport, } as any) const ProviderIndexRoute = ProviderIndexRouteImport.update({ @@ -201,176 +201,134 @@ const ProviderIndexRoute = ProviderIndexRouteImport.update({ path: '/$provider/', getParentRoute: () => rootRouteImport, } as any) -const ApiVideoRoute = ApiVideoRouteImport.update({ - id: '/api/video', - path: '/api/video', - getParentRoute: () => rootRouteImport, -} as any) -const ApiTtsRoute = ApiTtsRouteImport.update({ - id: '/api/tts', - path: '/api/tts', +const ProviderFeatureRoute = ProviderFeatureRouteImport.update({ + id: '/$provider/$feature', + path: '/$provider/$feature', getParentRoute: () => rootRouteImport, } as any) -const ApiTranscriptionRoute = ApiTranscriptionRouteImport.update({ - id: '/api/transcription', - path: '/api/transcription', +const ApiAnthropicBugTestRoute = ApiAnthropicBugTestRouteImport.update({ + id: '/api/anthropic-bug-test', + path: '/api/anthropic-bug-test', getParentRoute: () => rootRouteImport, } as any) -const ApiToolsTestRoute = ApiToolsTestRouteImport.update({ - id: '/api/tools-test', - path: '/api/tools-test', +const ApiAnthropicSkillsWireRoute = ApiAnthropicSkillsWireRouteImport.update({ + id: '/api/anthropic-skills-wire', + path: '/api/anthropic-skills-wire', getParentRoute: () => rootRouteImport, } as any) -const ApiToolCallLifecycleWireRoute = - ApiToolCallLifecycleWireRouteImport.update({ - id: '/api/tool-call-lifecycle-wire', - path: '/api/tool-call-lifecycle-wire', +const ApiAnthropicStructuredUsageRoute = + ApiAnthropicStructuredUsageRouteImport.update({ + id: '/api/anthropic-structured-usage', + path: '/api/anthropic-structured-usage', getParentRoute: () => rootRouteImport, } as any) -const ApiSummarizeRoute = ApiSummarizeRouteImport.update({ - id: '/api/summarize', - path: '/api/summarize', +const ApiArktypeToolWireRoute = ApiArktypeToolWireRouteImport.update({ + id: '/api/arktype-tool-wire', + path: '/api/arktype-tool-wire', getParentRoute: () => rootRouteImport, } as any) -const ApiSandboxToolHistoryRoute = ApiSandboxToolHistoryRouteImport.update({ - id: '/api/sandbox-tool-history', - path: '/api/sandbox-tool-history', +const ApiAudioRoute = ApiAudioRouteImport.update({ + id: '/api/audio', + path: '/api/audio', getParentRoute: () => rootRouteImport, } as any) -const ApiSandboxFilePersistenceRoute = - ApiSandboxFilePersistenceRouteImport.update({ - id: '/api/sandbox-file-persistence', - path: '/api/sandbox-file-persistence', - getParentRoute: () => rootRouteImport, - } as any) -const ApiSandboxDurabilityRoute = ApiSandboxDurabilityRouteImport.update({ - id: '/api/sandbox-durability', - path: '/api/sandbox-durability', +const ApiByokChatRoute = ApiByokChatRouteImport.update({ + id: '/api/byok-chat', + path: '/api/byok-chat', getParentRoute: () => rootRouteImport, } as any) -const ApiProviderToolDispatchWireRoute = - ApiProviderToolDispatchWireRouteImport.update({ - id: '/api/provider-tool-dispatch-wire', - path: '/api/provider-tool-dispatch-wire', - getParentRoute: () => rootRouteImport, - } as any) -const ApiPersistenceDurabilityRoute = - ApiPersistenceDurabilityRouteImport.update({ - id: '/api/persistence-durability', - path: '/api/persistence-durability', +const ApiByteplusSeedance1080pWireRoute = + ApiByteplusSeedance1080pWireRouteImport.update({ + id: '/api/byteplus-seedance-1080p-wire', + path: '/api/byteplus-seedance-1080p-wire', getParentRoute: () => rootRouteImport, } as any) -const ApiOtelUsageRoute = ApiOtelUsageRouteImport.update({ - id: '/api/otel-usage', - path: '/api/otel-usage', +const ApiChatRoute = ApiChatRouteImport.update({ + id: '/api/chat', + path: '/api/chat', getParentRoute: () => rootRouteImport, } as any) -const ApiOtelTranscriptionRoute = ApiOtelTranscriptionRouteImport.update({ - id: '/api/otel-transcription', - path: '/api/otel-transcription', +const ApiDevtoolsMemoryRoute = ApiDevtoolsMemoryRouteImport.update({ + id: '/api/devtools-memory', + path: '/api/devtools-memory', getParentRoute: () => rootRouteImport, } as any) -const ApiOtelMediaRoute = ApiOtelMediaRouteImport.update({ - id: '/api/otel-media', - path: '/api/otel-media', +const ApiDurableDeliveryRoute = ApiDurableDeliveryRouteImport.update({ + id: '/api/durable-delivery', + path: '/api/durable-delivery', getParentRoute: () => rootRouteImport, } as any) -const ApiOpenrouterWebToolsWireRoute = - ApiOpenrouterWebToolsWireRouteImport.update({ - id: '/api/openrouter-web-tools-wire', - path: '/api/openrouter-web-tools-wire', - getParentRoute: () => rootRouteImport, - } as any) -const ApiOpenrouterReasoningWireRoute = - ApiOpenrouterReasoningWireRouteImport.update({ - id: '/api/openrouter-reasoning-wire', - path: '/api/openrouter-reasoning-wire', - getParentRoute: () => rootRouteImport, - } as any) -const ApiOpenrouterJsonObjectWireRoute = - ApiOpenrouterJsonObjectWireRouteImport.update({ - id: '/api/openrouter-json-object-wire', - path: '/api/openrouter-json-object-wire', - getParentRoute: () => rootRouteImport, - } as any) -const ApiOpenrouterCostRoute = ApiOpenrouterCostRouteImport.update({ - id: '/api/openrouter-cost', - path: '/api/openrouter-cost', +const ApiDurableTakeoverRoute = ApiDurableTakeoverRouteImport.update({ + id: '/api/durable-takeover', + path: '/api/durable-takeover', getParentRoute: () => rootRouteImport, } as any) -const ApiOpenaiUsageDetailsRoute = ApiOpenaiUsageDetailsRouteImport.update({ - id: '/api/openai-usage-details', - path: '/api/openai-usage-details', +const ApiEmbeddingRoute = ApiEmbeddingRouteImport.update({ + id: '/api/embedding', + path: '/api/embedding', getParentRoute: () => rootRouteImport, } as any) -const ApiOpenaiStrictToolNullWireRoute = - ApiOpenaiStrictToolNullWireRouteImport.update({ - id: '/api/openai-strict-tool-null-wire', - path: '/api/openai-strict-tool-null-wire', - getParentRoute: () => rootRouteImport, - } as any) -const ApiOpenaiShellSkillsWireRoute = - ApiOpenaiShellSkillsWireRouteImport.update({ - id: '/api/openai-shell-skills-wire', - path: '/api/openai-shell-skills-wire', - getParentRoute: () => rootRouteImport, - } as any) -const ApiOpenaiCompletedResponseTextRoute = - ApiOpenaiCompletedResponseTextRouteImport.update({ - id: '/api/openai-completed-response-text', - path: '/api/openai-completed-response-text', - getParentRoute: () => rootRouteImport, - } as any) -const ApiNonStreamingRunErrorRoute = ApiNonStreamingRunErrorRouteImport.update({ - id: '/api/non-streaming-run-error', - path: '/api/non-streaming-run-error', +const ApiForeignInterruptRoute = ApiForeignInterruptRouteImport.update({ + id: '/api/foreign-interrupt', + path: '/api/foreign-interrupt', getParentRoute: () => rootRouteImport, } as any) -const ApiMultimodalToolResultWireRoute = - ApiMultimodalToolResultWireRouteImport.update({ - id: '/api/multimodal-tool-result-wire', - path: '/api/multimodal-tool-result-wire', +const ApiGeminiImageGaModelsRoute = ApiGeminiImageGaModelsRouteImport.update({ + id: '/api/gemini-image-ga-models', + path: '/api/gemini-image-ga-models', + getParentRoute: () => rootRouteImport, +} as any) +const ApiGeminiNativeImageWireRoute = + ApiGeminiNativeImageWireRouteImport.update({ + id: '/api/gemini-native-image-wire', + path: '/api/gemini-native-image-wire', getParentRoute: () => rootRouteImport, } as any) -const ApiMistralStrictToolNullWireRoute = - ApiMistralStrictToolNullWireRouteImport.update({ - id: '/api/mistral-strict-tool-null-wire', - path: '/api/mistral-strict-tool-null-wire', +const ApiGenerationPersistenceResumeRoute = + ApiGenerationPersistenceResumeRouteImport.update({ + id: '/api/generation-persistence-resume', + path: '/api/generation-persistence-resume', getParentRoute: () => rootRouteImport, } as any) -const ApiMiddlewareTestRoute = ApiMiddlewareTestRouteImport.update({ - id: '/api/middleware-test', - path: '/api/middleware-test', +const ApiGenerationPersistenceServerRoute = + ApiGenerationPersistenceServerRouteImport.update({ + id: '/api/generation-persistence-server', + path: '/api/generation-persistence-server', + getParentRoute: () => rootRouteImport, + } as any) +const ApiImageRoute = ApiImageRouteImport.update({ + id: '/api/image', + path: '/api/image', getParentRoute: () => rootRouteImport, } as any) -const ApiMessageIdsRoute = ApiMessageIdsRouteImport.update({ - id: '/api/message-ids', - path: '/api/message-ids', +const ApiInterruptsTestRoute = ApiInterruptsTestRouteImport.update({ + id: '/api/interrupts-test', + path: '/api/interrupts-test', getParentRoute: () => rootRouteImport, } as any) -const ApiMcpTestRoute = ApiMcpTestRouteImport.update({ - id: '/api/mcp-test', - path: '/api/mcp-test', +const ApiJoinRunClientToolRoute = ApiJoinRunClientToolRouteImport.update({ + id: '/api/join-run-client-tool', + path: '/api/join-run-client-tool', getParentRoute: () => rootRouteImport, } as any) -const ApiMcpStatusTestRoute = ApiMcpStatusTestRouteImport.update({ - id: '/api/mcp-status-test', - path: '/api/mcp-status-test', +const ApiLazyToolsWireRoute = ApiLazyToolsWireRouteImport.update({ + id: '/api/lazy-tools-wire', + path: '/api/lazy-tools-wire', getParentRoute: () => rootRouteImport, } as any) -const ApiMcpServerRoute = ApiMcpServerRouteImport.update({ - id: '/api/mcp-server', - path: '/api/mcp-server', +const ApiMaxToolCallsWireRoute = ApiMaxToolCallsWireRouteImport.update({ + id: '/api/max-tool-calls-wire', + path: '/api/max-tool-calls-wire', getParentRoute: () => rootRouteImport, } as any) -const ApiMcpManagedTestRoute = ApiMcpManagedTestRouteImport.update({ - id: '/api/mcp-managed-test', - path: '/api/mcp-managed-test', +const ApiMcpAppsCallRoute = ApiMcpAppsCallRouteImport.update({ + id: '/api/mcp-apps-call', + path: '/api/mcp-apps-call', getParentRoute: () => rootRouteImport, } as any) -const ApiMcpLifecycleTestRoute = ApiMcpLifecycleTestRouteImport.update({ - id: '/api/mcp-lifecycle-test', - path: '/api/mcp-lifecycle-test', +const ApiMcpAppsChatRoute = ApiMcpAppsChatRouteImport.update({ + id: '/api/mcp-apps-chat', + path: '/api/mcp-apps-chat', getParentRoute: () => rootRouteImport, } as any) const ApiMcpAppsServerRoute = ApiMcpAppsServerRouteImport.update({ @@ -378,160 +336,202 @@ const ApiMcpAppsServerRoute = ApiMcpAppsServerRouteImport.update({ path: '/api/mcp-apps-server', getParentRoute: () => rootRouteImport, } as any) -const ApiMcpAppsChatRoute = ApiMcpAppsChatRouteImport.update({ - id: '/api/mcp-apps-chat', - path: '/api/mcp-apps-chat', +const ApiMcpLifecycleTestRoute = ApiMcpLifecycleTestRouteImport.update({ + id: '/api/mcp-lifecycle-test', + path: '/api/mcp-lifecycle-test', getParentRoute: () => rootRouteImport, } as any) -const ApiMcpAppsCallRoute = ApiMcpAppsCallRouteImport.update({ - id: '/api/mcp-apps-call', - path: '/api/mcp-apps-call', +const ApiMcpManagedTestRoute = ApiMcpManagedTestRouteImport.update({ + id: '/api/mcp-managed-test', + path: '/api/mcp-managed-test', + getParentRoute: () => rootRouteImport, +} as any) +const ApiMcpServerRoute = ApiMcpServerRouteImport.update({ + id: '/api/mcp-server', + path: '/api/mcp-server', getParentRoute: () => rootRouteImport, } as any) -const ApiMaxToolCallsWireRoute = ApiMaxToolCallsWireRouteImport.update({ - id: '/api/max-tool-calls-wire', - path: '/api/max-tool-calls-wire', +const ApiMcpStatusTestRoute = ApiMcpStatusTestRouteImport.update({ + id: '/api/mcp-status-test', + path: '/api/mcp-status-test', getParentRoute: () => rootRouteImport, } as any) -const ApiLazyToolsWireRoute = ApiLazyToolsWireRouteImport.update({ - id: '/api/lazy-tools-wire', - path: '/api/lazy-tools-wire', +const ApiMcpTestRoute = ApiMcpTestRouteImport.update({ + id: '/api/mcp-test', + path: '/api/mcp-test', getParentRoute: () => rootRouteImport, } as any) -const ApiJoinRunClientToolRoute = ApiJoinRunClientToolRouteImport.update({ - id: '/api/join-run-client-tool', - path: '/api/join-run-client-tool', +const ApiMessageIdsRoute = ApiMessageIdsRouteImport.update({ + id: '/api/message-ids', + path: '/api/message-ids', getParentRoute: () => rootRouteImport, } as any) -const ApiInterruptsTestRoute = ApiInterruptsTestRouteImport.update({ - id: '/api/interrupts-test', - path: '/api/interrupts-test', +const ApiMiddlewareTestRoute = ApiMiddlewareTestRouteImport.update({ + id: '/api/middleware-test', + path: '/api/middleware-test', getParentRoute: () => rootRouteImport, } as any) -const ApiImageRoute = ApiImageRouteImport.update({ - id: '/api/image', - path: '/api/image', +const ApiMistralStrictToolNullWireRoute = + ApiMistralStrictToolNullWireRouteImport.update({ + id: '/api/mistral-strict-tool-null-wire', + path: '/api/mistral-strict-tool-null-wire', + getParentRoute: () => rootRouteImport, + } as any) +const ApiMultimodalToolResultWireRoute = + ApiMultimodalToolResultWireRouteImport.update({ + id: '/api/multimodal-tool-result-wire', + path: '/api/multimodal-tool-result-wire', + getParentRoute: () => rootRouteImport, + } as any) +const ApiNonStreamingRunErrorRoute = ApiNonStreamingRunErrorRouteImport.update({ + id: '/api/non-streaming-run-error', + path: '/api/non-streaming-run-error', getParentRoute: () => rootRouteImport, } as any) -const ApiGenerationPersistenceServerRoute = - ApiGenerationPersistenceServerRouteImport.update({ - id: '/api/generation-persistence-server', - path: '/api/generation-persistence-server', +const ApiOpenaiCompletedResponseTextRoute = + ApiOpenaiCompletedResponseTextRouteImport.update({ + id: '/api/openai-completed-response-text', + path: '/api/openai-completed-response-text', getParentRoute: () => rootRouteImport, } as any) -const ApiGenerationPersistenceResumeRoute = - ApiGenerationPersistenceResumeRouteImport.update({ - id: '/api/generation-persistence-resume', - path: '/api/generation-persistence-resume', +const ApiOpenaiShellSkillsWireRoute = + ApiOpenaiShellSkillsWireRouteImport.update({ + id: '/api/openai-shell-skills-wire', + path: '/api/openai-shell-skills-wire', getParentRoute: () => rootRouteImport, } as any) -const ApiGeminiNativeImageWireRoute = - ApiGeminiNativeImageWireRouteImport.update({ - id: '/api/gemini-native-image-wire', - path: '/api/gemini-native-image-wire', +const ApiOpenaiStrictToolNullWireRoute = + ApiOpenaiStrictToolNullWireRouteImport.update({ + id: '/api/openai-strict-tool-null-wire', + path: '/api/openai-strict-tool-null-wire', getParentRoute: () => rootRouteImport, } as any) -const ApiGeminiImageGaModelsRoute = ApiGeminiImageGaModelsRouteImport.update({ - id: '/api/gemini-image-ga-models', - path: '/api/gemini-image-ga-models', - getParentRoute: () => rootRouteImport, -} as any) -const ApiForeignInterruptRoute = ApiForeignInterruptRouteImport.update({ - id: '/api/foreign-interrupt', - path: '/api/foreign-interrupt', - getParentRoute: () => rootRouteImport, -} as any) -const ApiEmbeddingRoute = ApiEmbeddingRouteImport.update({ - id: '/api/embedding', - path: '/api/embedding', +const ApiOpenaiUsageDetailsRoute = ApiOpenaiUsageDetailsRouteImport.update({ + id: '/api/openai-usage-details', + path: '/api/openai-usage-details', getParentRoute: () => rootRouteImport, } as any) -const ApiDurableTakeoverRoute = ApiDurableTakeoverRouteImport.update({ - id: '/api/durable-takeover', - path: '/api/durable-takeover', +const ApiOpenrouterCostRoute = ApiOpenrouterCostRouteImport.update({ + id: '/api/openrouter-cost', + path: '/api/openrouter-cost', getParentRoute: () => rootRouteImport, } as any) -const ApiDurableDeliveryRoute = ApiDurableDeliveryRouteImport.update({ - id: '/api/durable-delivery', - path: '/api/durable-delivery', +const ApiOpenrouterJsonObjectWireRoute = + ApiOpenrouterJsonObjectWireRouteImport.update({ + id: '/api/openrouter-json-object-wire', + path: '/api/openrouter-json-object-wire', + getParentRoute: () => rootRouteImport, + } as any) +const ApiOpenrouterReasoningWireRoute = + ApiOpenrouterReasoningWireRouteImport.update({ + id: '/api/openrouter-reasoning-wire', + path: '/api/openrouter-reasoning-wire', + getParentRoute: () => rootRouteImport, + } as any) +const ApiOpenrouterWebToolsWireRoute = + ApiOpenrouterWebToolsWireRouteImport.update({ + id: '/api/openrouter-web-tools-wire', + path: '/api/openrouter-web-tools-wire', + getParentRoute: () => rootRouteImport, + } as any) +const ApiOtelMediaRoute = ApiOtelMediaRouteImport.update({ + id: '/api/otel-media', + path: '/api/otel-media', getParentRoute: () => rootRouteImport, } as any) -const ApiDevtoolsMemoryRoute = ApiDevtoolsMemoryRouteImport.update({ - id: '/api/devtools-memory', - path: '/api/devtools-memory', +const ApiOtelTranscriptionRoute = ApiOtelTranscriptionRouteImport.update({ + id: '/api/otel-transcription', + path: '/api/otel-transcription', getParentRoute: () => rootRouteImport, } as any) -const ApiChatRoute = ApiChatRouteImport.update({ - id: '/api/chat', - path: '/api/chat', +const ApiOtelUsageRoute = ApiOtelUsageRouteImport.update({ + id: '/api/otel-usage', + path: '/api/otel-usage', getParentRoute: () => rootRouteImport, } as any) -const ApiByokChatRoute = ApiByokChatRouteImport.update({ - id: '/api/byok-chat', - path: '/api/byok-chat', +const ApiPersistenceDurabilityRoute = + ApiPersistenceDurabilityRouteImport.update({ + id: '/api/persistence-durability', + path: '/api/persistence-durability', + getParentRoute: () => rootRouteImport, + } as any) +const ApiProviderToolDispatchWireRoute = + ApiProviderToolDispatchWireRouteImport.update({ + id: '/api/provider-tool-dispatch-wire', + path: '/api/provider-tool-dispatch-wire', + getParentRoute: () => rootRouteImport, + } as any) +const ApiSandboxDurabilityRoute = ApiSandboxDurabilityRouteImport.update({ + id: '/api/sandbox-durability', + path: '/api/sandbox-durability', getParentRoute: () => rootRouteImport, } as any) -const ApiByteplusSeedance1080pWireRoute = - ApiByteplusSeedance1080pWireRouteImport.update({ - id: '/api/byteplus-seedance-1080p-wire', - path: '/api/byteplus-seedance-1080p-wire', +const ApiSandboxFilePersistenceRoute = + ApiSandboxFilePersistenceRouteImport.update({ + id: '/api/sandbox-file-persistence', + path: '/api/sandbox-file-persistence', getParentRoute: () => rootRouteImport, } as any) -const ApiAudioRoute = ApiAudioRouteImport.update({ - id: '/api/audio', - path: '/api/audio', +const ApiSandboxToolHistoryRoute = ApiSandboxToolHistoryRouteImport.update({ + id: '/api/sandbox-tool-history', + path: '/api/sandbox-tool-history', getParentRoute: () => rootRouteImport, } as any) -const ApiArktypeToolWireRoute = ApiArktypeToolWireRouteImport.update({ - id: '/api/arktype-tool-wire', - path: '/api/arktype-tool-wire', +const ApiSummarizeRoute = ApiSummarizeRouteImport.update({ + id: '/api/summarize', + path: '/api/summarize', getParentRoute: () => rootRouteImport, } as any) -const ApiAnthropicStructuredUsageRoute = - ApiAnthropicStructuredUsageRouteImport.update({ - id: '/api/anthropic-structured-usage', - path: '/api/anthropic-structured-usage', +const ApiToolCallLifecycleWireRoute = + ApiToolCallLifecycleWireRouteImport.update({ + id: '/api/tool-call-lifecycle-wire', + path: '/api/tool-call-lifecycle-wire', getParentRoute: () => rootRouteImport, } as any) -const ApiAnthropicSkillsWireRoute = ApiAnthropicSkillsWireRouteImport.update({ - id: '/api/anthropic-skills-wire', - path: '/api/anthropic-skills-wire', +const ApiToolsTestRoute = ApiToolsTestRouteImport.update({ + id: '/api/tools-test', + path: '/api/tools-test', getParentRoute: () => rootRouteImport, } as any) -const ApiAnthropicBugTestRoute = ApiAnthropicBugTestRouteImport.update({ - id: '/api/anthropic-bug-test', - path: '/api/anthropic-bug-test', +const ApiTranscriptionRoute = ApiTranscriptionRouteImport.update({ + id: '/api/transcription', + path: '/api/transcription', getParentRoute: () => rootRouteImport, } as any) -const ProviderFeatureRoute = ProviderFeatureRouteImport.update({ - id: '/$provider/$feature', - path: '/$provider/$feature', +const ApiTtsRoute = ApiTtsRouteImport.update({ + id: '/api/tts', + path: '/api/tts', getParentRoute: () => rootRouteImport, } as any) -const ApiVideoStreamRoute = ApiVideoStreamRouteImport.update({ +const ApiVideoRoute = ApiVideoRouteImport.update({ + id: '/api/video', + path: '/api/video', + getParentRoute: () => rootRouteImport, +} as any) +const ApiAudioStreamRoute = ApiAudioStreamRouteImport.update({ id: '/stream', path: '/stream', - getParentRoute: () => ApiVideoRoute, + getParentRoute: () => ApiAudioRoute, } as any) -const ApiTtsStreamRoute = ApiTtsStreamRouteImport.update({ +const ApiImageStreamRoute = ApiImageStreamRouteImport.update({ id: '/stream', path: '/stream', - getParentRoute: () => ApiTtsRoute, + getParentRoute: () => ApiImageRoute, } as any) const ApiTranscriptionStreamRoute = ApiTranscriptionStreamRouteImport.update({ id: '/stream', path: '/stream', getParentRoute: () => ApiTranscriptionRoute, } as any) -const ApiImageStreamRoute = ApiImageStreamRouteImport.update({ +const ApiTtsStreamRoute = ApiTtsStreamRouteImport.update({ id: '/stream', path: '/stream', - getParentRoute: () => ApiImageRoute, + getParentRoute: () => ApiTtsRoute, } as any) -const ApiAudioStreamRoute = ApiAudioStreamRouteImport.update({ +const ApiVideoStreamRoute = ApiVideoStreamRouteImport.update({ id: '/stream', path: '/stream', - getParentRoute: () => ApiAudioRoute, + getParentRoute: () => ApiVideoRoute, } as any) export interface FileRoutesByFullPath { @@ -1138,74 +1138,67 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/websocket-adapter': { - id: '/websocket-adapter' - path: '/websocket-adapter' - fullPath: '/websocket-adapter' - preLoaderRoute: typeof WebsocketAdapterRouteImport - parentRoute: typeof rootRouteImport - } - '/tools-test': { - id: '/tools-test' - path: '/tools-test' - fullPath: '/tools-test' - preLoaderRoute: typeof ToolsTestRouteImport - parentRoute: typeof rootRouteImport - } - '/persistence-durability': { - id: '/persistence-durability' - path: '/persistence-durability' - fullPath: '/persistence-durability' - preLoaderRoute: typeof PersistenceDurabilityRouteImport + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } - '/middleware-test': { - id: '/middleware-test' - path: '/middleware-test' - fullPath: '/middleware-test' - preLoaderRoute: typeof MiddlewareTestRouteImport + '/byok': { + id: '/byok' + path: '/byok' + fullPath: '/byok' + preLoaderRoute: typeof ByokRouteImport parentRoute: typeof rootRouteImport } - '/markdown-cjk': { - id: '/markdown-cjk' - path: '/markdown-cjk' - fullPath: '/markdown-cjk' - preLoaderRoute: typeof MarkdownCjkRouteImport + '/chat-client-default-bridge': { + id: '/chat-client-default-bridge' + path: '/chat-client-default-bridge' + fullPath: '/chat-client-default-bridge' + preLoaderRoute: typeof ChatClientDefaultBridgeRouteImport parentRoute: typeof rootRouteImport } - '/join-run-client-tool': { - id: '/join-run-client-tool' - path: '/join-run-client-tool' - fullPath: '/join-run-client-tool' - preLoaderRoute: typeof JoinRunClientToolRouteImport + '/devtools-chat': { + id: '/devtools-chat' + path: '/devtools-chat' + fullPath: '/devtools-chat' + preLoaderRoute: typeof DevtoolsChatRouteImport parentRoute: typeof rootRouteImport } - '/interrupts-test': { - id: '/interrupts-test' - path: '/interrupts-test' - fullPath: '/interrupts-test' - preLoaderRoute: typeof InterruptsTestRouteImport + '/devtools-generation-hooks': { + id: '/devtools-generation-hooks' + path: '/devtools-generation-hooks' + fullPath: '/devtools-generation-hooks' + preLoaderRoute: typeof DevtoolsGenerationHooksRouteImport parentRoute: typeof rootRouteImport } - '/generation-persistence-server': { - id: '/generation-persistence-server' - path: '/generation-persistence-server' - fullPath: '/generation-persistence-server' - preLoaderRoute: typeof GenerationPersistenceServerRouteImport + '/devtools-memory': { + id: '/devtools-memory' + path: '/devtools-memory' + fullPath: '/devtools-memory' + preLoaderRoute: typeof DevtoolsMemoryRouteImport parentRoute: typeof rootRouteImport } - '/generation-persistence-resume': { - id: '/generation-persistence-resume' - path: '/generation-persistence-resume' - fullPath: '/generation-persistence-resume' - preLoaderRoute: typeof GenerationPersistenceResumeRouteImport + '/devtools-route-a': { + id: '/devtools-route-a' + path: '/devtools-route-a' + fullPath: '/devtools-route-a' + preLoaderRoute: typeof DevtoolsRouteARouteImport parentRoute: typeof rootRouteImport } - '/foreign-interrupt': { - id: '/foreign-interrupt' - path: '/foreign-interrupt' - fullPath: '/foreign-interrupt' - preLoaderRoute: typeof ForeignInterruptRouteImport + '/devtools-route-b': { + id: '/devtools-route-b' + path: '/devtools-route-b' + fullPath: '/devtools-route-b' + preLoaderRoute: typeof DevtoolsRouteBRouteImport + parentRoute: typeof rootRouteImport + } + '/devtools-structured': { + id: '/devtools-structured' + path: '/devtools-structured' + fullPath: '/devtools-structured' + preLoaderRoute: typeof DevtoolsStructuredRouteImport parentRoute: typeof rootRouteImport } '/devtools-tools': { @@ -1215,67 +1208,74 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DevtoolsToolsRouteImport parentRoute: typeof rootRouteImport } - '/devtools-structured': { - id: '/devtools-structured' - path: '/devtools-structured' - fullPath: '/devtools-structured' - preLoaderRoute: typeof DevtoolsStructuredRouteImport + '/foreign-interrupt': { + id: '/foreign-interrupt' + path: '/foreign-interrupt' + fullPath: '/foreign-interrupt' + preLoaderRoute: typeof ForeignInterruptRouteImport parentRoute: typeof rootRouteImport } - '/devtools-route-b': { - id: '/devtools-route-b' - path: '/devtools-route-b' - fullPath: '/devtools-route-b' - preLoaderRoute: typeof DevtoolsRouteBRouteImport + '/generation-persistence-resume': { + id: '/generation-persistence-resume' + path: '/generation-persistence-resume' + fullPath: '/generation-persistence-resume' + preLoaderRoute: typeof GenerationPersistenceResumeRouteImport parentRoute: typeof rootRouteImport } - '/devtools-route-a': { - id: '/devtools-route-a' - path: '/devtools-route-a' - fullPath: '/devtools-route-a' - preLoaderRoute: typeof DevtoolsRouteARouteImport + '/generation-persistence-server': { + id: '/generation-persistence-server' + path: '/generation-persistence-server' + fullPath: '/generation-persistence-server' + preLoaderRoute: typeof GenerationPersistenceServerRouteImport parentRoute: typeof rootRouteImport } - '/devtools-memory': { - id: '/devtools-memory' - path: '/devtools-memory' - fullPath: '/devtools-memory' - preLoaderRoute: typeof DevtoolsMemoryRouteImport + '/interrupts-test': { + id: '/interrupts-test' + path: '/interrupts-test' + fullPath: '/interrupts-test' + preLoaderRoute: typeof InterruptsTestRouteImport parentRoute: typeof rootRouteImport } - '/devtools-generation-hooks': { - id: '/devtools-generation-hooks' - path: '/devtools-generation-hooks' - fullPath: '/devtools-generation-hooks' - preLoaderRoute: typeof DevtoolsGenerationHooksRouteImport + '/join-run-client-tool': { + id: '/join-run-client-tool' + path: '/join-run-client-tool' + fullPath: '/join-run-client-tool' + preLoaderRoute: typeof JoinRunClientToolRouteImport parentRoute: typeof rootRouteImport } - '/devtools-chat': { - id: '/devtools-chat' - path: '/devtools-chat' - fullPath: '/devtools-chat' - preLoaderRoute: typeof DevtoolsChatRouteImport + '/markdown-cjk': { + id: '/markdown-cjk' + path: '/markdown-cjk' + fullPath: '/markdown-cjk' + preLoaderRoute: typeof MarkdownCjkRouteImport parentRoute: typeof rootRouteImport } - '/chat-client-default-bridge': { - id: '/chat-client-default-bridge' - path: '/chat-client-default-bridge' - fullPath: '/chat-client-default-bridge' - preLoaderRoute: typeof ChatClientDefaultBridgeRouteImport + '/middleware-test': { + id: '/middleware-test' + path: '/middleware-test' + fullPath: '/middleware-test' + preLoaderRoute: typeof MiddlewareTestRouteImport parentRoute: typeof rootRouteImport } - '/byok': { - id: '/byok' - path: '/byok' - fullPath: '/byok' - preLoaderRoute: typeof ByokRouteImport + '/persistence-durability': { + id: '/persistence-durability' + path: '/persistence-durability' + fullPath: '/persistence-durability' + preLoaderRoute: typeof PersistenceDurabilityRouteImport parentRoute: typeof rootRouteImport } - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexRouteImport + '/tools-test': { + id: '/tools-test' + path: '/tools-test' + fullPath: '/tools-test' + preLoaderRoute: typeof ToolsTestRouteImport + parentRoute: typeof rootRouteImport + } + '/websocket-adapter': { + id: '/websocket-adapter' + path: '/websocket-adapter' + fullPath: '/websocket-adapter' + preLoaderRoute: typeof WebsocketAdapterRouteImport parentRoute: typeof rootRouteImport } '/$provider/': { @@ -1285,207 +1285,200 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProviderIndexRouteImport parentRoute: typeof rootRouteImport } - '/api/video': { - id: '/api/video' - path: '/api/video' - fullPath: '/api/video' - preLoaderRoute: typeof ApiVideoRouteImport - parentRoute: typeof rootRouteImport - } - '/api/tts': { - id: '/api/tts' - path: '/api/tts' - fullPath: '/api/tts' - preLoaderRoute: typeof ApiTtsRouteImport + '/$provider/$feature': { + id: '/$provider/$feature' + path: '/$provider/$feature' + fullPath: '/$provider/$feature' + preLoaderRoute: typeof ProviderFeatureRouteImport parentRoute: typeof rootRouteImport } - '/api/transcription': { - id: '/api/transcription' - path: '/api/transcription' - fullPath: '/api/transcription' - preLoaderRoute: typeof ApiTranscriptionRouteImport + '/api/anthropic-bug-test': { + id: '/api/anthropic-bug-test' + path: '/api/anthropic-bug-test' + fullPath: '/api/anthropic-bug-test' + preLoaderRoute: typeof ApiAnthropicBugTestRouteImport parentRoute: typeof rootRouteImport } - '/api/tools-test': { - id: '/api/tools-test' - path: '/api/tools-test' - fullPath: '/api/tools-test' - preLoaderRoute: typeof ApiToolsTestRouteImport + '/api/anthropic-skills-wire': { + id: '/api/anthropic-skills-wire' + path: '/api/anthropic-skills-wire' + fullPath: '/api/anthropic-skills-wire' + preLoaderRoute: typeof ApiAnthropicSkillsWireRouteImport parentRoute: typeof rootRouteImport } - '/api/tool-call-lifecycle-wire': { - id: '/api/tool-call-lifecycle-wire' - path: '/api/tool-call-lifecycle-wire' - fullPath: '/api/tool-call-lifecycle-wire' - preLoaderRoute: typeof ApiToolCallLifecycleWireRouteImport + '/api/anthropic-structured-usage': { + id: '/api/anthropic-structured-usage' + path: '/api/anthropic-structured-usage' + fullPath: '/api/anthropic-structured-usage' + preLoaderRoute: typeof ApiAnthropicStructuredUsageRouteImport parentRoute: typeof rootRouteImport } - '/api/summarize': { - id: '/api/summarize' - path: '/api/summarize' - fullPath: '/api/summarize' - preLoaderRoute: typeof ApiSummarizeRouteImport + '/api/arktype-tool-wire': { + id: '/api/arktype-tool-wire' + path: '/api/arktype-tool-wire' + fullPath: '/api/arktype-tool-wire' + preLoaderRoute: typeof ApiArktypeToolWireRouteImport parentRoute: typeof rootRouteImport } - '/api/sandbox-tool-history': { - id: '/api/sandbox-tool-history' - path: '/api/sandbox-tool-history' - fullPath: '/api/sandbox-tool-history' - preLoaderRoute: typeof ApiSandboxToolHistoryRouteImport + '/api/audio': { + id: '/api/audio' + path: '/api/audio' + fullPath: '/api/audio' + preLoaderRoute: typeof ApiAudioRouteImport parentRoute: typeof rootRouteImport } - '/api/sandbox-file-persistence': { - id: '/api/sandbox-file-persistence' - path: '/api/sandbox-file-persistence' - fullPath: '/api/sandbox-file-persistence' - preLoaderRoute: typeof ApiSandboxFilePersistenceRouteImport + '/api/byok-chat': { + id: '/api/byok-chat' + path: '/api/byok-chat' + fullPath: '/api/byok-chat' + preLoaderRoute: typeof ApiByokChatRouteImport parentRoute: typeof rootRouteImport } - '/api/sandbox-durability': { - id: '/api/sandbox-durability' - path: '/api/sandbox-durability' - fullPath: '/api/sandbox-durability' - preLoaderRoute: typeof ApiSandboxDurabilityRouteImport + '/api/byteplus-seedance-1080p-wire': { + id: '/api/byteplus-seedance-1080p-wire' + path: '/api/byteplus-seedance-1080p-wire' + fullPath: '/api/byteplus-seedance-1080p-wire' + preLoaderRoute: typeof ApiByteplusSeedance1080pWireRouteImport parentRoute: typeof rootRouteImport } - '/api/provider-tool-dispatch-wire': { - id: '/api/provider-tool-dispatch-wire' - path: '/api/provider-tool-dispatch-wire' - fullPath: '/api/provider-tool-dispatch-wire' - preLoaderRoute: typeof ApiProviderToolDispatchWireRouteImport + '/api/chat': { + id: '/api/chat' + path: '/api/chat' + fullPath: '/api/chat' + preLoaderRoute: typeof ApiChatRouteImport parentRoute: typeof rootRouteImport } - '/api/persistence-durability': { - id: '/api/persistence-durability' - path: '/api/persistence-durability' - fullPath: '/api/persistence-durability' - preLoaderRoute: typeof ApiPersistenceDurabilityRouteImport + '/api/devtools-memory': { + id: '/api/devtools-memory' + path: '/api/devtools-memory' + fullPath: '/api/devtools-memory' + preLoaderRoute: typeof ApiDevtoolsMemoryRouteImport parentRoute: typeof rootRouteImport } - '/api/otel-usage': { - id: '/api/otel-usage' - path: '/api/otel-usage' - fullPath: '/api/otel-usage' - preLoaderRoute: typeof ApiOtelUsageRouteImport + '/api/durable-delivery': { + id: '/api/durable-delivery' + path: '/api/durable-delivery' + fullPath: '/api/durable-delivery' + preLoaderRoute: typeof ApiDurableDeliveryRouteImport parentRoute: typeof rootRouteImport } - '/api/otel-transcription': { - id: '/api/otel-transcription' - path: '/api/otel-transcription' - fullPath: '/api/otel-transcription' - preLoaderRoute: typeof ApiOtelTranscriptionRouteImport + '/api/durable-takeover': { + id: '/api/durable-takeover' + path: '/api/durable-takeover' + fullPath: '/api/durable-takeover' + preLoaderRoute: typeof ApiDurableTakeoverRouteImport parentRoute: typeof rootRouteImport } - '/api/otel-media': { - id: '/api/otel-media' - path: '/api/otel-media' - fullPath: '/api/otel-media' - preLoaderRoute: typeof ApiOtelMediaRouteImport + '/api/embedding': { + id: '/api/embedding' + path: '/api/embedding' + fullPath: '/api/embedding' + preLoaderRoute: typeof ApiEmbeddingRouteImport parentRoute: typeof rootRouteImport } - '/api/openrouter-web-tools-wire': { - id: '/api/openrouter-web-tools-wire' - path: '/api/openrouter-web-tools-wire' - fullPath: '/api/openrouter-web-tools-wire' - preLoaderRoute: typeof ApiOpenrouterWebToolsWireRouteImport + '/api/foreign-interrupt': { + id: '/api/foreign-interrupt' + path: '/api/foreign-interrupt' + fullPath: '/api/foreign-interrupt' + preLoaderRoute: typeof ApiForeignInterruptRouteImport parentRoute: typeof rootRouteImport } - '/api/openrouter-reasoning-wire': { - id: '/api/openrouter-reasoning-wire' - path: '/api/openrouter-reasoning-wire' - fullPath: '/api/openrouter-reasoning-wire' - preLoaderRoute: typeof ApiOpenrouterReasoningWireRouteImport + '/api/gemini-image-ga-models': { + id: '/api/gemini-image-ga-models' + path: '/api/gemini-image-ga-models' + fullPath: '/api/gemini-image-ga-models' + preLoaderRoute: typeof ApiGeminiImageGaModelsRouteImport parentRoute: typeof rootRouteImport } - '/api/openrouter-json-object-wire': { - id: '/api/openrouter-json-object-wire' - path: '/api/openrouter-json-object-wire' - fullPath: '/api/openrouter-json-object-wire' - preLoaderRoute: typeof ApiOpenrouterJsonObjectWireRouteImport + '/api/gemini-native-image-wire': { + id: '/api/gemini-native-image-wire' + path: '/api/gemini-native-image-wire' + fullPath: '/api/gemini-native-image-wire' + preLoaderRoute: typeof ApiGeminiNativeImageWireRouteImport parentRoute: typeof rootRouteImport } - '/api/openrouter-cost': { - id: '/api/openrouter-cost' - path: '/api/openrouter-cost' - fullPath: '/api/openrouter-cost' - preLoaderRoute: typeof ApiOpenrouterCostRouteImport + '/api/generation-persistence-resume': { + id: '/api/generation-persistence-resume' + path: '/api/generation-persistence-resume' + fullPath: '/api/generation-persistence-resume' + preLoaderRoute: typeof ApiGenerationPersistenceResumeRouteImport parentRoute: typeof rootRouteImport } - '/api/openai-usage-details': { - id: '/api/openai-usage-details' - path: '/api/openai-usage-details' - fullPath: '/api/openai-usage-details' - preLoaderRoute: typeof ApiOpenaiUsageDetailsRouteImport + '/api/generation-persistence-server': { + id: '/api/generation-persistence-server' + path: '/api/generation-persistence-server' + fullPath: '/api/generation-persistence-server' + preLoaderRoute: typeof ApiGenerationPersistenceServerRouteImport parentRoute: typeof rootRouteImport } - '/api/openai-strict-tool-null-wire': { - id: '/api/openai-strict-tool-null-wire' - path: '/api/openai-strict-tool-null-wire' - fullPath: '/api/openai-strict-tool-null-wire' - preLoaderRoute: typeof ApiOpenaiStrictToolNullWireRouteImport + '/api/image': { + id: '/api/image' + path: '/api/image' + fullPath: '/api/image' + preLoaderRoute: typeof ApiImageRouteImport parentRoute: typeof rootRouteImport } - '/api/openai-shell-skills-wire': { - id: '/api/openai-shell-skills-wire' - path: '/api/openai-shell-skills-wire' - fullPath: '/api/openai-shell-skills-wire' - preLoaderRoute: typeof ApiOpenaiShellSkillsWireRouteImport + '/api/interrupts-test': { + id: '/api/interrupts-test' + path: '/api/interrupts-test' + fullPath: '/api/interrupts-test' + preLoaderRoute: typeof ApiInterruptsTestRouteImport parentRoute: typeof rootRouteImport } - '/api/openai-completed-response-text': { - id: '/api/openai-completed-response-text' - path: '/api/openai-completed-response-text' - fullPath: '/api/openai-completed-response-text' - preLoaderRoute: typeof ApiOpenaiCompletedResponseTextRouteImport + '/api/join-run-client-tool': { + id: '/api/join-run-client-tool' + path: '/api/join-run-client-tool' + fullPath: '/api/join-run-client-tool' + preLoaderRoute: typeof ApiJoinRunClientToolRouteImport parentRoute: typeof rootRouteImport } - '/api/non-streaming-run-error': { - id: '/api/non-streaming-run-error' - path: '/api/non-streaming-run-error' - fullPath: '/api/non-streaming-run-error' - preLoaderRoute: typeof ApiNonStreamingRunErrorRouteImport + '/api/lazy-tools-wire': { + id: '/api/lazy-tools-wire' + path: '/api/lazy-tools-wire' + fullPath: '/api/lazy-tools-wire' + preLoaderRoute: typeof ApiLazyToolsWireRouteImport parentRoute: typeof rootRouteImport } - '/api/multimodal-tool-result-wire': { - id: '/api/multimodal-tool-result-wire' - path: '/api/multimodal-tool-result-wire' - fullPath: '/api/multimodal-tool-result-wire' - preLoaderRoute: typeof ApiMultimodalToolResultWireRouteImport + '/api/max-tool-calls-wire': { + id: '/api/max-tool-calls-wire' + path: '/api/max-tool-calls-wire' + fullPath: '/api/max-tool-calls-wire' + preLoaderRoute: typeof ApiMaxToolCallsWireRouteImport parentRoute: typeof rootRouteImport } - '/api/mistral-strict-tool-null-wire': { - id: '/api/mistral-strict-tool-null-wire' - path: '/api/mistral-strict-tool-null-wire' - fullPath: '/api/mistral-strict-tool-null-wire' - preLoaderRoute: typeof ApiMistralStrictToolNullWireRouteImport + '/api/mcp-apps-call': { + id: '/api/mcp-apps-call' + path: '/api/mcp-apps-call' + fullPath: '/api/mcp-apps-call' + preLoaderRoute: typeof ApiMcpAppsCallRouteImport parentRoute: typeof rootRouteImport } - '/api/middleware-test': { - id: '/api/middleware-test' - path: '/api/middleware-test' - fullPath: '/api/middleware-test' - preLoaderRoute: typeof ApiMiddlewareTestRouteImport + '/api/mcp-apps-chat': { + id: '/api/mcp-apps-chat' + path: '/api/mcp-apps-chat' + fullPath: '/api/mcp-apps-chat' + preLoaderRoute: typeof ApiMcpAppsChatRouteImport parentRoute: typeof rootRouteImport } - '/api/message-ids': { - id: '/api/message-ids' - path: '/api/message-ids' - fullPath: '/api/message-ids' - preLoaderRoute: typeof ApiMessageIdsRouteImport + '/api/mcp-apps-server': { + id: '/api/mcp-apps-server' + path: '/api/mcp-apps-server' + fullPath: '/api/mcp-apps-server' + preLoaderRoute: typeof ApiMcpAppsServerRouteImport parentRoute: typeof rootRouteImport } - '/api/mcp-test': { - id: '/api/mcp-test' - path: '/api/mcp-test' - fullPath: '/api/mcp-test' - preLoaderRoute: typeof ApiMcpTestRouteImport + '/api/mcp-lifecycle-test': { + id: '/api/mcp-lifecycle-test' + path: '/api/mcp-lifecycle-test' + fullPath: '/api/mcp-lifecycle-test' + preLoaderRoute: typeof ApiMcpLifecycleTestRouteImport parentRoute: typeof rootRouteImport } - '/api/mcp-status-test': { - id: '/api/mcp-status-test' - path: '/api/mcp-status-test' - fullPath: '/api/mcp-status-test' - preLoaderRoute: typeof ApiMcpStatusTestRouteImport + '/api/mcp-managed-test': { + id: '/api/mcp-managed-test' + path: '/api/mcp-managed-test' + fullPath: '/api/mcp-managed-test' + preLoaderRoute: typeof ApiMcpManagedTestRouteImport parentRoute: typeof rootRouteImport } '/api/mcp-server': { @@ -1495,215 +1488,222 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiMcpServerRouteImport parentRoute: typeof rootRouteImport } - '/api/mcp-managed-test': { - id: '/api/mcp-managed-test' - path: '/api/mcp-managed-test' - fullPath: '/api/mcp-managed-test' - preLoaderRoute: typeof ApiMcpManagedTestRouteImport + '/api/mcp-status-test': { + id: '/api/mcp-status-test' + path: '/api/mcp-status-test' + fullPath: '/api/mcp-status-test' + preLoaderRoute: typeof ApiMcpStatusTestRouteImport parentRoute: typeof rootRouteImport } - '/api/mcp-lifecycle-test': { - id: '/api/mcp-lifecycle-test' - path: '/api/mcp-lifecycle-test' - fullPath: '/api/mcp-lifecycle-test' - preLoaderRoute: typeof ApiMcpLifecycleTestRouteImport + '/api/mcp-test': { + id: '/api/mcp-test' + path: '/api/mcp-test' + fullPath: '/api/mcp-test' + preLoaderRoute: typeof ApiMcpTestRouteImport parentRoute: typeof rootRouteImport } - '/api/mcp-apps-server': { - id: '/api/mcp-apps-server' - path: '/api/mcp-apps-server' - fullPath: '/api/mcp-apps-server' - preLoaderRoute: typeof ApiMcpAppsServerRouteImport + '/api/message-ids': { + id: '/api/message-ids' + path: '/api/message-ids' + fullPath: '/api/message-ids' + preLoaderRoute: typeof ApiMessageIdsRouteImport parentRoute: typeof rootRouteImport } - '/api/mcp-apps-chat': { - id: '/api/mcp-apps-chat' - path: '/api/mcp-apps-chat' - fullPath: '/api/mcp-apps-chat' - preLoaderRoute: typeof ApiMcpAppsChatRouteImport + '/api/middleware-test': { + id: '/api/middleware-test' + path: '/api/middleware-test' + fullPath: '/api/middleware-test' + preLoaderRoute: typeof ApiMiddlewareTestRouteImport parentRoute: typeof rootRouteImport } - '/api/mcp-apps-call': { - id: '/api/mcp-apps-call' - path: '/api/mcp-apps-call' - fullPath: '/api/mcp-apps-call' - preLoaderRoute: typeof ApiMcpAppsCallRouteImport + '/api/mistral-strict-tool-null-wire': { + id: '/api/mistral-strict-tool-null-wire' + path: '/api/mistral-strict-tool-null-wire' + fullPath: '/api/mistral-strict-tool-null-wire' + preLoaderRoute: typeof ApiMistralStrictToolNullWireRouteImport parentRoute: typeof rootRouteImport } - '/api/max-tool-calls-wire': { - id: '/api/max-tool-calls-wire' - path: '/api/max-tool-calls-wire' - fullPath: '/api/max-tool-calls-wire' - preLoaderRoute: typeof ApiMaxToolCallsWireRouteImport + '/api/multimodal-tool-result-wire': { + id: '/api/multimodal-tool-result-wire' + path: '/api/multimodal-tool-result-wire' + fullPath: '/api/multimodal-tool-result-wire' + preLoaderRoute: typeof ApiMultimodalToolResultWireRouteImport parentRoute: typeof rootRouteImport } - '/api/lazy-tools-wire': { - id: '/api/lazy-tools-wire' - path: '/api/lazy-tools-wire' - fullPath: '/api/lazy-tools-wire' - preLoaderRoute: typeof ApiLazyToolsWireRouteImport + '/api/non-streaming-run-error': { + id: '/api/non-streaming-run-error' + path: '/api/non-streaming-run-error' + fullPath: '/api/non-streaming-run-error' + preLoaderRoute: typeof ApiNonStreamingRunErrorRouteImport + parentRoute: typeof rootRouteImport + } + '/api/openai-completed-response-text': { + id: '/api/openai-completed-response-text' + path: '/api/openai-completed-response-text' + fullPath: '/api/openai-completed-response-text' + preLoaderRoute: typeof ApiOpenaiCompletedResponseTextRouteImport parentRoute: typeof rootRouteImport } - '/api/join-run-client-tool': { - id: '/api/join-run-client-tool' - path: '/api/join-run-client-tool' - fullPath: '/api/join-run-client-tool' - preLoaderRoute: typeof ApiJoinRunClientToolRouteImport + '/api/openai-shell-skills-wire': { + id: '/api/openai-shell-skills-wire' + path: '/api/openai-shell-skills-wire' + fullPath: '/api/openai-shell-skills-wire' + preLoaderRoute: typeof ApiOpenaiShellSkillsWireRouteImport parentRoute: typeof rootRouteImport } - '/api/interrupts-test': { - id: '/api/interrupts-test' - path: '/api/interrupts-test' - fullPath: '/api/interrupts-test' - preLoaderRoute: typeof ApiInterruptsTestRouteImport + '/api/openai-strict-tool-null-wire': { + id: '/api/openai-strict-tool-null-wire' + path: '/api/openai-strict-tool-null-wire' + fullPath: '/api/openai-strict-tool-null-wire' + preLoaderRoute: typeof ApiOpenaiStrictToolNullWireRouteImport parentRoute: typeof rootRouteImport } - '/api/image': { - id: '/api/image' - path: '/api/image' - fullPath: '/api/image' - preLoaderRoute: typeof ApiImageRouteImport + '/api/openai-usage-details': { + id: '/api/openai-usage-details' + path: '/api/openai-usage-details' + fullPath: '/api/openai-usage-details' + preLoaderRoute: typeof ApiOpenaiUsageDetailsRouteImport parentRoute: typeof rootRouteImport } - '/api/generation-persistence-server': { - id: '/api/generation-persistence-server' - path: '/api/generation-persistence-server' - fullPath: '/api/generation-persistence-server' - preLoaderRoute: typeof ApiGenerationPersistenceServerRouteImport + '/api/openrouter-cost': { + id: '/api/openrouter-cost' + path: '/api/openrouter-cost' + fullPath: '/api/openrouter-cost' + preLoaderRoute: typeof ApiOpenrouterCostRouteImport parentRoute: typeof rootRouteImport } - '/api/generation-persistence-resume': { - id: '/api/generation-persistence-resume' - path: '/api/generation-persistence-resume' - fullPath: '/api/generation-persistence-resume' - preLoaderRoute: typeof ApiGenerationPersistenceResumeRouteImport + '/api/openrouter-json-object-wire': { + id: '/api/openrouter-json-object-wire' + path: '/api/openrouter-json-object-wire' + fullPath: '/api/openrouter-json-object-wire' + preLoaderRoute: typeof ApiOpenrouterJsonObjectWireRouteImport parentRoute: typeof rootRouteImport } - '/api/gemini-native-image-wire': { - id: '/api/gemini-native-image-wire' - path: '/api/gemini-native-image-wire' - fullPath: '/api/gemini-native-image-wire' - preLoaderRoute: typeof ApiGeminiNativeImageWireRouteImport + '/api/openrouter-reasoning-wire': { + id: '/api/openrouter-reasoning-wire' + path: '/api/openrouter-reasoning-wire' + fullPath: '/api/openrouter-reasoning-wire' + preLoaderRoute: typeof ApiOpenrouterReasoningWireRouteImport parentRoute: typeof rootRouteImport } - '/api/gemini-image-ga-models': { - id: '/api/gemini-image-ga-models' - path: '/api/gemini-image-ga-models' - fullPath: '/api/gemini-image-ga-models' - preLoaderRoute: typeof ApiGeminiImageGaModelsRouteImport + '/api/openrouter-web-tools-wire': { + id: '/api/openrouter-web-tools-wire' + path: '/api/openrouter-web-tools-wire' + fullPath: '/api/openrouter-web-tools-wire' + preLoaderRoute: typeof ApiOpenrouterWebToolsWireRouteImport parentRoute: typeof rootRouteImport } - '/api/foreign-interrupt': { - id: '/api/foreign-interrupt' - path: '/api/foreign-interrupt' - fullPath: '/api/foreign-interrupt' - preLoaderRoute: typeof ApiForeignInterruptRouteImport + '/api/otel-media': { + id: '/api/otel-media' + path: '/api/otel-media' + fullPath: '/api/otel-media' + preLoaderRoute: typeof ApiOtelMediaRouteImport parentRoute: typeof rootRouteImport } - '/api/embedding': { - id: '/api/embedding' - path: '/api/embedding' - fullPath: '/api/embedding' - preLoaderRoute: typeof ApiEmbeddingRouteImport + '/api/otel-transcription': { + id: '/api/otel-transcription' + path: '/api/otel-transcription' + fullPath: '/api/otel-transcription' + preLoaderRoute: typeof ApiOtelTranscriptionRouteImport parentRoute: typeof rootRouteImport } - '/api/durable-takeover': { - id: '/api/durable-takeover' - path: '/api/durable-takeover' - fullPath: '/api/durable-takeover' - preLoaderRoute: typeof ApiDurableTakeoverRouteImport + '/api/otel-usage': { + id: '/api/otel-usage' + path: '/api/otel-usage' + fullPath: '/api/otel-usage' + preLoaderRoute: typeof ApiOtelUsageRouteImport parentRoute: typeof rootRouteImport } - '/api/durable-delivery': { - id: '/api/durable-delivery' - path: '/api/durable-delivery' - fullPath: '/api/durable-delivery' - preLoaderRoute: typeof ApiDurableDeliveryRouteImport + '/api/persistence-durability': { + id: '/api/persistence-durability' + path: '/api/persistence-durability' + fullPath: '/api/persistence-durability' + preLoaderRoute: typeof ApiPersistenceDurabilityRouteImport parentRoute: typeof rootRouteImport } - '/api/devtools-memory': { - id: '/api/devtools-memory' - path: '/api/devtools-memory' - fullPath: '/api/devtools-memory' - preLoaderRoute: typeof ApiDevtoolsMemoryRouteImport + '/api/provider-tool-dispatch-wire': { + id: '/api/provider-tool-dispatch-wire' + path: '/api/provider-tool-dispatch-wire' + fullPath: '/api/provider-tool-dispatch-wire' + preLoaderRoute: typeof ApiProviderToolDispatchWireRouteImport parentRoute: typeof rootRouteImport } - '/api/chat': { - id: '/api/chat' - path: '/api/chat' - fullPath: '/api/chat' - preLoaderRoute: typeof ApiChatRouteImport + '/api/sandbox-durability': { + id: '/api/sandbox-durability' + path: '/api/sandbox-durability' + fullPath: '/api/sandbox-durability' + preLoaderRoute: typeof ApiSandboxDurabilityRouteImport parentRoute: typeof rootRouteImport } - '/api/byok-chat': { - id: '/api/byok-chat' - path: '/api/byok-chat' - fullPath: '/api/byok-chat' - preLoaderRoute: typeof ApiByokChatRouteImport + '/api/sandbox-file-persistence': { + id: '/api/sandbox-file-persistence' + path: '/api/sandbox-file-persistence' + fullPath: '/api/sandbox-file-persistence' + preLoaderRoute: typeof ApiSandboxFilePersistenceRouteImport parentRoute: typeof rootRouteImport } - '/api/byteplus-seedance-1080p-wire': { - id: '/api/byteplus-seedance-1080p-wire' - path: '/api/byteplus-seedance-1080p-wire' - fullPath: '/api/byteplus-seedance-1080p-wire' - preLoaderRoute: typeof ApiByteplusSeedance1080pWireRouteImport + '/api/sandbox-tool-history': { + id: '/api/sandbox-tool-history' + path: '/api/sandbox-tool-history' + fullPath: '/api/sandbox-tool-history' + preLoaderRoute: typeof ApiSandboxToolHistoryRouteImport parentRoute: typeof rootRouteImport } - '/api/audio': { - id: '/api/audio' - path: '/api/audio' - fullPath: '/api/audio' - preLoaderRoute: typeof ApiAudioRouteImport + '/api/summarize': { + id: '/api/summarize' + path: '/api/summarize' + fullPath: '/api/summarize' + preLoaderRoute: typeof ApiSummarizeRouteImport parentRoute: typeof rootRouteImport } - '/api/arktype-tool-wire': { - id: '/api/arktype-tool-wire' - path: '/api/arktype-tool-wire' - fullPath: '/api/arktype-tool-wire' - preLoaderRoute: typeof ApiArktypeToolWireRouteImport + '/api/tool-call-lifecycle-wire': { + id: '/api/tool-call-lifecycle-wire' + path: '/api/tool-call-lifecycle-wire' + fullPath: '/api/tool-call-lifecycle-wire' + preLoaderRoute: typeof ApiToolCallLifecycleWireRouteImport parentRoute: typeof rootRouteImport } - '/api/anthropic-structured-usage': { - id: '/api/anthropic-structured-usage' - path: '/api/anthropic-structured-usage' - fullPath: '/api/anthropic-structured-usage' - preLoaderRoute: typeof ApiAnthropicStructuredUsageRouteImport + '/api/tools-test': { + id: '/api/tools-test' + path: '/api/tools-test' + fullPath: '/api/tools-test' + preLoaderRoute: typeof ApiToolsTestRouteImport parentRoute: typeof rootRouteImport } - '/api/anthropic-skills-wire': { - id: '/api/anthropic-skills-wire' - path: '/api/anthropic-skills-wire' - fullPath: '/api/anthropic-skills-wire' - preLoaderRoute: typeof ApiAnthropicSkillsWireRouteImport + '/api/transcription': { + id: '/api/transcription' + path: '/api/transcription' + fullPath: '/api/transcription' + preLoaderRoute: typeof ApiTranscriptionRouteImport parentRoute: typeof rootRouteImport } - '/api/anthropic-bug-test': { - id: '/api/anthropic-bug-test' - path: '/api/anthropic-bug-test' - fullPath: '/api/anthropic-bug-test' - preLoaderRoute: typeof ApiAnthropicBugTestRouteImport + '/api/tts': { + id: '/api/tts' + path: '/api/tts' + fullPath: '/api/tts' + preLoaderRoute: typeof ApiTtsRouteImport parentRoute: typeof rootRouteImport } - '/$provider/$feature': { - id: '/$provider/$feature' - path: '/$provider/$feature' - fullPath: '/$provider/$feature' - preLoaderRoute: typeof ProviderFeatureRouteImport + '/api/video': { + id: '/api/video' + path: '/api/video' + fullPath: '/api/video' + preLoaderRoute: typeof ApiVideoRouteImport parentRoute: typeof rootRouteImport } - '/api/video/stream': { - id: '/api/video/stream' + '/api/audio/stream': { + id: '/api/audio/stream' path: '/stream' - fullPath: '/api/video/stream' - preLoaderRoute: typeof ApiVideoStreamRouteImport - parentRoute: typeof ApiVideoRoute + fullPath: '/api/audio/stream' + preLoaderRoute: typeof ApiAudioStreamRouteImport + parentRoute: typeof ApiAudioRoute } - '/api/tts/stream': { - id: '/api/tts/stream' + '/api/image/stream': { + id: '/api/image/stream' path: '/stream' - fullPath: '/api/tts/stream' - preLoaderRoute: typeof ApiTtsStreamRouteImport - parentRoute: typeof ApiTtsRoute + fullPath: '/api/image/stream' + preLoaderRoute: typeof ApiImageStreamRouteImport + parentRoute: typeof ApiImageRoute } '/api/transcription/stream': { id: '/api/transcription/stream' @@ -1712,19 +1712,19 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiTranscriptionStreamRouteImport parentRoute: typeof ApiTranscriptionRoute } - '/api/image/stream': { - id: '/api/image/stream' + '/api/tts/stream': { + id: '/api/tts/stream' path: '/stream' - fullPath: '/api/image/stream' - preLoaderRoute: typeof ApiImageStreamRouteImport - parentRoute: typeof ApiImageRoute + fullPath: '/api/tts/stream' + preLoaderRoute: typeof ApiTtsStreamRouteImport + parentRoute: typeof ApiTtsRoute } - '/api/audio/stream': { - id: '/api/audio/stream' + '/api/video/stream': { + id: '/api/video/stream' path: '/stream' - fullPath: '/api/audio/stream' - preLoaderRoute: typeof ApiAudioStreamRouteImport - parentRoute: typeof ApiAudioRoute + fullPath: '/api/video/stream' + preLoaderRoute: typeof ApiVideoStreamRouteImport + parentRoute: typeof ApiVideoRoute } } } diff --git a/testing/e2e/tests/chat.spec.ts b/testing/e2e/tests/chat.spec.ts index c0e5b25663..0e0f5a7b8b 100644 --- a/testing/e2e/tests/chat.spec.ts +++ b/testing/e2e/tests/chat.spec.ts @@ -224,6 +224,26 @@ test.describe('openai chat persistence', () => { 'Fender Stratocaster', ) + const hydrationErrors: Array = [] + page.on('pageerror', (error) => { + if ( + /hydration failed|hydrated but|server rendered html/i.test( + error.message, + ) + ) { + hydrationErrors.push(error.message) + } + }) + page.on('console', (message) => { + const text = message.text() + if ( + message.type() === 'error' && + /hydration failed|hydrated but|server rendered html/i.test(text) + ) { + hydrationErrors.push(text) + } + }) + await page.reload() await expect(page.getByTestId('user-message')).toContainText( @@ -232,6 +252,7 @@ test.describe('openai chat persistence', () => { await expect(page.getByTestId('assistant-message')).toContainText( 'Fender Stratocaster', ) + expect(hydrationErrors).toEqual([]) }) test('clear() removes the persisted conversation so a reload starts empty', async ({ diff --git a/testing/e2e/tests/generic-middleware-interrupts.spec.ts b/testing/e2e/tests/generic-middleware-interrupts.spec.ts index 083e841fd3..d6b65b69bf 100644 --- a/testing/e2e/tests/generic-middleware-interrupts.spec.ts +++ b/testing/e2e/tests/generic-middleware-interrupts.spec.ts @@ -307,6 +307,23 @@ test('restores a pending generic interrupt after reload and resumes it', async ( aimockPort, }) => { await startScenario(page, testId, aimockPort, 'generic-before-model') + const hydrationErrors: Array = [] + page.on('pageerror', (error) => { + if ( + /hydration failed|hydrated but|server rendered html/i.test(error.message) + ) { + hydrationErrors.push(error.message) + } + }) + page.on('console', (message) => { + const text = message.text() + if ( + message.type() === 'error' && + /hydration failed|hydrated but|server rendered html/i.test(text) + ) { + hydrationErrors.push(text) + } + }) await page.reload() await expect(page.getByTestId('generic-review-plan')).toBeVisible() await resolveReview(page) @@ -318,4 +335,5 @@ test('restores a pending generic interrupt after reload and resumes it', async ( expect(capture.resolutions).toEqual([ expect.objectContaining({ status: 'resolved' }), ]) + expect(hydrationErrors).toEqual([]) }) diff --git a/testing/e2e/tests/helpers.ts b/testing/e2e/tests/helpers.ts index de8ddfb72c..8cbc6df980 100644 --- a/testing/e2e/tests/helpers.ts +++ b/testing/e2e/tests/helpers.ts @@ -187,28 +187,32 @@ export async function getTranscriptionResult(page: Page): Promise { export async function fillPrompt(page: Page, text: string) { const input = page.getByTestId('prompt-input') - await input.click() - await input.fill(text) - await input.dispatchEvent('input', { bubbles: true }) - // If fill() didn't trigger React onChange, fall back to pressSequentially const btn = page.getByTestId('generate-button') - if (await btn.isDisabled()) { - await input.clear() - await input.pressSequentially(text, { delay: 30 }) - } + await expect + .poll(async () => { + await input.click() + await input.fill('') + await input.dispatchEvent('input', { bubbles: true }) + await input.fill(text) + await input.dispatchEvent('input', { bubbles: true }) + return btn.isEnabled() + }) + .toBe(true) } export async function fillTextInput(page: Page, text: string) { const input = page.getByTestId('text-input') - await input.click() - await input.fill(text) - await input.dispatchEvent('input', { bubbles: true }) - // If fill() didn't trigger React onChange, fall back to pressSequentially const btn = page.getByTestId('generate-button') - if (await btn.isDisabled()) { - await input.clear() - await input.pressSequentially(text, { delay: 30 }) - } + await expect + .poll(async () => { + await input.click() + await input.fill('') + await input.dispatchEvent('input', { bubbles: true }) + await input.fill(text) + await input.dispatchEvent('input', { bubbles: true }) + return btn.isEnabled() + }) + .toBe(true) } export async function clickGenerate(page: Page) { diff --git a/testing/e2e/tests/image-gen.spec.ts b/testing/e2e/tests/image-gen.spec.ts index 7997996fa5..927b6d77a5 100644 --- a/testing/e2e/tests/image-gen.spec.ts +++ b/testing/e2e/tests/image-gen.spec.ts @@ -20,6 +20,10 @@ for (const provider of providersFor('image-gen')) { await fillPrompt(page, 'a guitar in a music store') await clickGenerate(page) await waitForGenerationComplete(page) + await expect(page.getByTestId('active-run-id')).not.toHaveText('') + await expect(page.getByTestId('active-status')).toHaveText('generating') + await expect(page.getByTestId('active-loading')).toHaveText('true') + await expect(page.getByTestId('final-run-id')).toHaveText('') const images = page.getByTestId('generated-image') await expect(images).toHaveCount(1) }) @@ -35,6 +39,10 @@ for (const provider of providersFor('image-gen')) { await fillPrompt(page, 'a guitar in a music store') await clickGenerate(page) await waitForGenerationComplete(page) + await expect(page.getByTestId('active-run-id')).not.toHaveText('') + await expect(page.getByTestId('active-status')).toHaveText('generating') + await expect(page.getByTestId('active-loading')).toHaveText('true') + await expect(page.getByTestId('final-run-id')).toHaveText('') const images = page.getByTestId('generated-image') await expect(images).toHaveCount(1) }) @@ -50,6 +58,10 @@ for (const provider of providersFor('image-gen')) { await fillPrompt(page, 'a guitar in a music store') await clickGenerate(page) await waitForGenerationComplete(page) + await expect(page.getByTestId('active-run-id')).not.toHaveText('') + await expect(page.getByTestId('active-status')).toHaveText('generating') + await expect(page.getByTestId('active-loading')).toHaveText('true') + await expect(page.getByTestId('final-run-id')).toHaveText('') const images = page.getByTestId('generated-image') await expect(images).toHaveCount(1) }) diff --git a/testing/e2e/tests/persistence-durability.spec.ts b/testing/e2e/tests/persistence-durability.spec.ts index 0c57dcd9ff..a6af750e7f 100644 --- a/testing/e2e/tests/persistence-durability.spec.ts +++ b/testing/e2e/tests/persistence-durability.spec.ts @@ -59,6 +59,26 @@ test.describe('persistence durability (browser refresh)', () => { expect(Array.isArray(record.messages)).toBe(true) expect(record.messages.length).toBeGreaterThanOrEqual(2) + const hydrationErrors: Array = [] + page.on('pageerror', (error) => { + if ( + /hydration failed|hydrated but|server rendered html/i.test( + error.message, + ) + ) { + hydrationErrors.push(error.message) + } + }) + page.on('console', (message) => { + const text = message.text() + if ( + message.type() === 'error' && + /hydration failed|hydrated but|server rendered html/i.test(text) + ) { + hydrationErrors.push(text) + } + }) + // Full reload: the conversation must come back from localStorage alone. await page.reload() @@ -71,6 +91,7 @@ test.describe('persistence durability (browser refresh)', () => { ) // No in-flight run to rejoin after a clean finish — the page settles idle. await expect(page.getByTestId('loading-indicator')).toHaveCount(0) + expect(hydrationErrors).toEqual([]) }) test('a pending interrupt survives a reload (rehydrated from the resume snapshot)', async ({ @@ -98,6 +119,26 @@ test.describe('persistence durability (browser refresh)', () => { const record = JSON.parse(stored!) as { resume?: unknown } expect(record.resume).toBeTruthy() + const hydrationErrors: Array = [] + page.on('pageerror', (error) => { + if ( + /hydration failed|hydrated but|server rendered html/i.test( + error.message, + ) + ) { + hydrationErrors.push(error.message) + } + }) + page.on('console', (message) => { + const text = message.text() + if ( + message.type() === 'error' && + /hydration failed|hydrated but|server rendered html/i.test(text) + ) { + hydrationErrors.push(text) + } + }) + // Full reload: the interrupt must rehydrate from localStorage, not a refetch. await page.reload() @@ -108,6 +149,7 @@ test.describe('persistence durability (browser refresh)', () => { await expect(page.getByTestId('interrupt-confirm-shipment')).toBeVisible() await expect(page.getByTestId('interrupt-kind')).toHaveText('generic') await expect(page.getByTestId('interrupt-source')).toHaveText('hydrate') + expect(hydrationErrors).toEqual([]) }) test('restores a pending interrupt from the SERVER on a fresh load (persistence: true)', async ({ diff --git a/testing/e2e/tests/video-gen.spec.ts b/testing/e2e/tests/video-gen.spec.ts index 841fbccb8e..d7f5da6a62 100644 --- a/testing/e2e/tests/video-gen.spec.ts +++ b/testing/e2e/tests/video-gen.spec.ts @@ -20,6 +20,10 @@ for (const provider of providersFor('video-gen')) { await fillPrompt(page, 'a guitar being played in a store') await clickGenerate(page) await waitForGenerationComplete(page, 60_000) + await expect(page.getByTestId('active-run-id')).not.toHaveText('') + await expect(page.getByTestId('active-status')).toHaveText('generating') + await expect(page.getByTestId('active-loading')).toHaveText('true') + await expect(page.getByTestId('final-run-id')).toHaveText('') const video = page.getByTestId('generated-video') await expect(video).toBeVisible() }) @@ -35,6 +39,10 @@ for (const provider of providersFor('video-gen')) { await fillPrompt(page, 'a guitar being played in a store') await clickGenerate(page) await waitForGenerationComplete(page, 60_000) + await expect(page.getByTestId('active-run-id')).not.toHaveText('') + await expect(page.getByTestId('active-status')).toHaveText('generating') + await expect(page.getByTestId('active-loading')).toHaveText('true') + await expect(page.getByTestId('final-run-id')).toHaveText('') const video = page.getByTestId('generated-video') await expect(video).toBeVisible() }) @@ -50,6 +58,10 @@ for (const provider of providersFor('video-gen')) { await fillPrompt(page, 'a guitar being played in a store') await clickGenerate(page) await waitForGenerationComplete(page, 60_000) + await expect(page.getByTestId('active-run-id')).not.toHaveText('') + await expect(page.getByTestId('active-status')).toHaveText('generating') + await expect(page.getByTestId('active-loading')).toHaveText('true') + await expect(page.getByTestId('final-run-id')).toHaveText('') const video = page.getByTestId('generated-video') await expect(video).toBeVisible() }) diff --git a/testing/panel/package.json b/testing/panel/package.json index 7b1e3d63be..583e07d6de 100644 --- a/testing/panel/package.json +++ b/testing/panel/package.json @@ -27,8 +27,8 @@ "@tanstack/nitro-v2-vite-plugin": "^1.155.0", "@tanstack/react-ai-devtools": "workspace:*", "@tanstack/react-devtools": "^0.9.10", - "@tanstack/react-router": "^1.158.4", - "@tanstack/react-start": "^1.159.0", + "@tanstack/react-router": "^1.170.32", + "@tanstack/react-start": "^1.168.49", "@tanstack/start": "^1.120.20", "highlight.js": "^11.11.1", "lucide-react": "^0.561.0", diff --git a/testing/panel/src/routeTree.gen.ts b/testing/panel/src/routeTree.gen.ts index c9ce6fdaf3..3cea702cd0 100644 --- a/testing/panel/src/routeTree.gen.ts +++ b/testing/panel/src/routeTree.gen.ts @@ -9,54 +9,54 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' -import { Route as VideoRouteImport } from './routes/video' -import { Route as TtsRouteImport } from './routes/tts' -import { Route as TranscriptionRouteImport } from './routes/transcription' -import { Route as SummarizeRouteImport } from './routes/summarize' -import { Route as StructuredRouteImport } from './routes/structured' -import { Route as StreamDebuggerRouteImport } from './routes/stream-debugger' -import { Route as SimulatorRouteImport } from './routes/simulator' -import { Route as MemoryRouteImport } from './routes/memory' -import { Route as ImageRouteImport } from './routes/image' -import { Route as AddonManagerRouteImport } from './routes/addon-manager' import { Route as IndexRouteImport } from './routes/index' -import { Route as ApiVideoRouteImport } from './routes/api.video' -import { Route as ApiTtsRouteImport } from './routes/api.tts' -import { Route as ApiTranscriptionRouteImport } from './routes/api.transcription' -import { Route as ApiSummarizeRouteImport } from './routes/api.summarize' -import { Route as ApiStructuredRouteImport } from './routes/api.structured' -import { Route as ApiSimulatorChatRouteImport } from './routes/api.simulator-chat' -import { Route as ApiMemoryInspectRouteImport } from './routes/api.memory-inspect' -import { Route as ApiMemoryChatRouteImport } from './routes/api.memory-chat' -import { Route as ApiLoadTraceRouteImport } from './routes/api.load-trace' -import { Route as ApiListTracesRouteImport } from './routes/api.list-traces' -import { Route as ApiImageRouteImport } from './routes/api.image' -import { Route as ApiChatRouteImport } from './routes/api.chat' +import { Route as AddonManagerRouteImport } from './routes/addon-manager' +import { Route as ImageRouteImport } from './routes/image' +import { Route as MemoryRouteImport } from './routes/memory' +import { Route as SimulatorRouteImport } from './routes/simulator' +import { Route as StreamDebuggerRouteImport } from './routes/stream-debugger' +import { Route as StructuredRouteImport } from './routes/structured' +import { Route as SummarizeRouteImport } from './routes/summarize' +import { Route as TranscriptionRouteImport } from './routes/transcription' +import { Route as TtsRouteImport } from './routes/tts' +import { Route as VideoRouteImport } from './routes/video' import { Route as ApiAddonChatRouteImport } from './routes/api.addon-chat' +import { Route as ApiChatRouteImport } from './routes/api.chat' +import { Route as ApiImageRouteImport } from './routes/api.image' +import { Route as ApiListTracesRouteImport } from './routes/api.list-traces' +import { Route as ApiLoadTraceRouteImport } from './routes/api.load-trace' +import { Route as ApiMemoryChatRouteImport } from './routes/api.memory-chat' +import { Route as ApiMemoryInspectRouteImport } from './routes/api.memory-inspect' +import { Route as ApiSimulatorChatRouteImport } from './routes/api.simulator-chat' +import { Route as ApiStructuredRouteImport } from './routes/api.structured' +import { Route as ApiSummarizeRouteImport } from './routes/api.summarize' +import { Route as ApiTranscriptionRouteImport } from './routes/api.transcription' +import { Route as ApiTtsRouteImport } from './routes/api.tts' +import { Route as ApiVideoRouteImport } from './routes/api.video' -const VideoRoute = VideoRouteImport.update({ - id: '/video', - path: '/video', +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', getParentRoute: () => rootRouteImport, } as any) -const TtsRoute = TtsRouteImport.update({ - id: '/tts', - path: '/tts', +const AddonManagerRoute = AddonManagerRouteImport.update({ + id: '/addon-manager', + path: '/addon-manager', getParentRoute: () => rootRouteImport, } as any) -const TranscriptionRoute = TranscriptionRouteImport.update({ - id: '/transcription', - path: '/transcription', +const ImageRoute = ImageRouteImport.update({ + id: '/image', + path: '/image', getParentRoute: () => rootRouteImport, } as any) -const SummarizeRoute = SummarizeRouteImport.update({ - id: '/summarize', - path: '/summarize', +const MemoryRoute = MemoryRouteImport.update({ + id: '/memory', + path: '/memory', getParentRoute: () => rootRouteImport, } as any) -const StructuredRoute = StructuredRouteImport.update({ - id: '/structured', - path: '/structured', +const SimulatorRoute = SimulatorRouteImport.update({ + id: '/simulator', + path: '/simulator', getParentRoute: () => rootRouteImport, } as any) const StreamDebuggerRoute = StreamDebuggerRouteImport.update({ @@ -64,59 +64,59 @@ const StreamDebuggerRoute = StreamDebuggerRouteImport.update({ path: '/stream-debugger', getParentRoute: () => rootRouteImport, } as any) -const SimulatorRoute = SimulatorRouteImport.update({ - id: '/simulator', - path: '/simulator', +const StructuredRoute = StructuredRouteImport.update({ + id: '/structured', + path: '/structured', getParentRoute: () => rootRouteImport, } as any) -const MemoryRoute = MemoryRouteImport.update({ - id: '/memory', - path: '/memory', +const SummarizeRoute = SummarizeRouteImport.update({ + id: '/summarize', + path: '/summarize', getParentRoute: () => rootRouteImport, } as any) -const ImageRoute = ImageRouteImport.update({ - id: '/image', - path: '/image', +const TranscriptionRoute = TranscriptionRouteImport.update({ + id: '/transcription', + path: '/transcription', getParentRoute: () => rootRouteImport, } as any) -const AddonManagerRoute = AddonManagerRouteImport.update({ - id: '/addon-manager', - path: '/addon-manager', +const TtsRoute = TtsRouteImport.update({ + id: '/tts', + path: '/tts', getParentRoute: () => rootRouteImport, } as any) -const IndexRoute = IndexRouteImport.update({ - id: '/', - path: '/', +const VideoRoute = VideoRouteImport.update({ + id: '/video', + path: '/video', getParentRoute: () => rootRouteImport, } as any) -const ApiVideoRoute = ApiVideoRouteImport.update({ - id: '/api/video', - path: '/api/video', +const ApiAddonChatRoute = ApiAddonChatRouteImport.update({ + id: '/api/addon-chat', + path: '/api/addon-chat', getParentRoute: () => rootRouteImport, } as any) -const ApiTtsRoute = ApiTtsRouteImport.update({ - id: '/api/tts', - path: '/api/tts', +const ApiChatRoute = ApiChatRouteImport.update({ + id: '/api/chat', + path: '/api/chat', getParentRoute: () => rootRouteImport, } as any) -const ApiTranscriptionRoute = ApiTranscriptionRouteImport.update({ - id: '/api/transcription', - path: '/api/transcription', +const ApiImageRoute = ApiImageRouteImport.update({ + id: '/api/image', + path: '/api/image', getParentRoute: () => rootRouteImport, } as any) -const ApiSummarizeRoute = ApiSummarizeRouteImport.update({ - id: '/api/summarize', - path: '/api/summarize', +const ApiListTracesRoute = ApiListTracesRouteImport.update({ + id: '/api/list-traces', + path: '/api/list-traces', getParentRoute: () => rootRouteImport, } as any) -const ApiStructuredRoute = ApiStructuredRouteImport.update({ - id: '/api/structured', - path: '/api/structured', +const ApiLoadTraceRoute = ApiLoadTraceRouteImport.update({ + id: '/api/load-trace', + path: '/api/load-trace', getParentRoute: () => rootRouteImport, } as any) -const ApiSimulatorChatRoute = ApiSimulatorChatRouteImport.update({ - id: '/api/simulator-chat', - path: '/api/simulator-chat', +const ApiMemoryChatRoute = ApiMemoryChatRouteImport.update({ + id: '/api/memory-chat', + path: '/api/memory-chat', getParentRoute: () => rootRouteImport, } as any) const ApiMemoryInspectRoute = ApiMemoryInspectRouteImport.update({ @@ -124,34 +124,34 @@ const ApiMemoryInspectRoute = ApiMemoryInspectRouteImport.update({ path: '/api/memory-inspect', getParentRoute: () => rootRouteImport, } as any) -const ApiMemoryChatRoute = ApiMemoryChatRouteImport.update({ - id: '/api/memory-chat', - path: '/api/memory-chat', +const ApiSimulatorChatRoute = ApiSimulatorChatRouteImport.update({ + id: '/api/simulator-chat', + path: '/api/simulator-chat', getParentRoute: () => rootRouteImport, } as any) -const ApiLoadTraceRoute = ApiLoadTraceRouteImport.update({ - id: '/api/load-trace', - path: '/api/load-trace', +const ApiStructuredRoute = ApiStructuredRouteImport.update({ + id: '/api/structured', + path: '/api/structured', getParentRoute: () => rootRouteImport, } as any) -const ApiListTracesRoute = ApiListTracesRouteImport.update({ - id: '/api/list-traces', - path: '/api/list-traces', +const ApiSummarizeRoute = ApiSummarizeRouteImport.update({ + id: '/api/summarize', + path: '/api/summarize', getParentRoute: () => rootRouteImport, } as any) -const ApiImageRoute = ApiImageRouteImport.update({ - id: '/api/image', - path: '/api/image', +const ApiTranscriptionRoute = ApiTranscriptionRouteImport.update({ + id: '/api/transcription', + path: '/api/transcription', getParentRoute: () => rootRouteImport, } as any) -const ApiChatRoute = ApiChatRouteImport.update({ - id: '/api/chat', - path: '/api/chat', +const ApiTtsRoute = ApiTtsRouteImport.update({ + id: '/api/tts', + path: '/api/tts', getParentRoute: () => rootRouteImport, } as any) -const ApiAddonChatRoute = ApiAddonChatRouteImport.update({ - id: '/api/addon-chat', - path: '/api/addon-chat', +const ApiVideoRoute = ApiVideoRouteImport.update({ + id: '/api/video', + path: '/api/video', getParentRoute: () => rootRouteImport, } as any) @@ -344,39 +344,39 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/video': { - id: '/video' - path: '/video' - fullPath: '/video' - preLoaderRoute: typeof VideoRouteImport + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } - '/tts': { - id: '/tts' - path: '/tts' - fullPath: '/tts' - preLoaderRoute: typeof TtsRouteImport + '/addon-manager': { + id: '/addon-manager' + path: '/addon-manager' + fullPath: '/addon-manager' + preLoaderRoute: typeof AddonManagerRouteImport parentRoute: typeof rootRouteImport } - '/transcription': { - id: '/transcription' - path: '/transcription' - fullPath: '/transcription' - preLoaderRoute: typeof TranscriptionRouteImport + '/image': { + id: '/image' + path: '/image' + fullPath: '/image' + preLoaderRoute: typeof ImageRouteImport parentRoute: typeof rootRouteImport } - '/summarize': { - id: '/summarize' - path: '/summarize' - fullPath: '/summarize' - preLoaderRoute: typeof SummarizeRouteImport + '/memory': { + id: '/memory' + path: '/memory' + fullPath: '/memory' + preLoaderRoute: typeof MemoryRouteImport parentRoute: typeof rootRouteImport } - '/structured': { - id: '/structured' - path: '/structured' - fullPath: '/structured' - preLoaderRoute: typeof StructuredRouteImport + '/simulator': { + id: '/simulator' + path: '/simulator' + fullPath: '/simulator' + preLoaderRoute: typeof SimulatorRouteImport parentRoute: typeof rootRouteImport } '/stream-debugger': { @@ -386,81 +386,81 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof StreamDebuggerRouteImport parentRoute: typeof rootRouteImport } - '/simulator': { - id: '/simulator' - path: '/simulator' - fullPath: '/simulator' - preLoaderRoute: typeof SimulatorRouteImport + '/structured': { + id: '/structured' + path: '/structured' + fullPath: '/structured' + preLoaderRoute: typeof StructuredRouteImport parentRoute: typeof rootRouteImport } - '/memory': { - id: '/memory' - path: '/memory' - fullPath: '/memory' - preLoaderRoute: typeof MemoryRouteImport + '/summarize': { + id: '/summarize' + path: '/summarize' + fullPath: '/summarize' + preLoaderRoute: typeof SummarizeRouteImport parentRoute: typeof rootRouteImport } - '/image': { - id: '/image' - path: '/image' - fullPath: '/image' - preLoaderRoute: typeof ImageRouteImport + '/transcription': { + id: '/transcription' + path: '/transcription' + fullPath: '/transcription' + preLoaderRoute: typeof TranscriptionRouteImport parentRoute: typeof rootRouteImport } - '/addon-manager': { - id: '/addon-manager' - path: '/addon-manager' - fullPath: '/addon-manager' - preLoaderRoute: typeof AddonManagerRouteImport + '/tts': { + id: '/tts' + path: '/tts' + fullPath: '/tts' + preLoaderRoute: typeof TtsRouteImport parentRoute: typeof rootRouteImport } - '/': { - id: '/' - path: '/' - fullPath: '/' - preLoaderRoute: typeof IndexRouteImport + '/video': { + id: '/video' + path: '/video' + fullPath: '/video' + preLoaderRoute: typeof VideoRouteImport parentRoute: typeof rootRouteImport } - '/api/video': { - id: '/api/video' - path: '/api/video' - fullPath: '/api/video' - preLoaderRoute: typeof ApiVideoRouteImport + '/api/addon-chat': { + id: '/api/addon-chat' + path: '/api/addon-chat' + fullPath: '/api/addon-chat' + preLoaderRoute: typeof ApiAddonChatRouteImport parentRoute: typeof rootRouteImport } - '/api/tts': { - id: '/api/tts' - path: '/api/tts' - fullPath: '/api/tts' - preLoaderRoute: typeof ApiTtsRouteImport + '/api/chat': { + id: '/api/chat' + path: '/api/chat' + fullPath: '/api/chat' + preLoaderRoute: typeof ApiChatRouteImport parentRoute: typeof rootRouteImport } - '/api/transcription': { - id: '/api/transcription' - path: '/api/transcription' - fullPath: '/api/transcription' - preLoaderRoute: typeof ApiTranscriptionRouteImport + '/api/image': { + id: '/api/image' + path: '/api/image' + fullPath: '/api/image' + preLoaderRoute: typeof ApiImageRouteImport parentRoute: typeof rootRouteImport } - '/api/summarize': { - id: '/api/summarize' - path: '/api/summarize' - fullPath: '/api/summarize' - preLoaderRoute: typeof ApiSummarizeRouteImport + '/api/list-traces': { + id: '/api/list-traces' + path: '/api/list-traces' + fullPath: '/api/list-traces' + preLoaderRoute: typeof ApiListTracesRouteImport parentRoute: typeof rootRouteImport } - '/api/structured': { - id: '/api/structured' - path: '/api/structured' - fullPath: '/api/structured' - preLoaderRoute: typeof ApiStructuredRouteImport + '/api/load-trace': { + id: '/api/load-trace' + path: '/api/load-trace' + fullPath: '/api/load-trace' + preLoaderRoute: typeof ApiLoadTraceRouteImport parentRoute: typeof rootRouteImport } - '/api/simulator-chat': { - id: '/api/simulator-chat' - path: '/api/simulator-chat' - fullPath: '/api/simulator-chat' - preLoaderRoute: typeof ApiSimulatorChatRouteImport + '/api/memory-chat': { + id: '/api/memory-chat' + path: '/api/memory-chat' + fullPath: '/api/memory-chat' + preLoaderRoute: typeof ApiMemoryChatRouteImport parentRoute: typeof rootRouteImport } '/api/memory-inspect': { @@ -470,46 +470,46 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiMemoryInspectRouteImport parentRoute: typeof rootRouteImport } - '/api/memory-chat': { - id: '/api/memory-chat' - path: '/api/memory-chat' - fullPath: '/api/memory-chat' - preLoaderRoute: typeof ApiMemoryChatRouteImport + '/api/simulator-chat': { + id: '/api/simulator-chat' + path: '/api/simulator-chat' + fullPath: '/api/simulator-chat' + preLoaderRoute: typeof ApiSimulatorChatRouteImport parentRoute: typeof rootRouteImport } - '/api/load-trace': { - id: '/api/load-trace' - path: '/api/load-trace' - fullPath: '/api/load-trace' - preLoaderRoute: typeof ApiLoadTraceRouteImport + '/api/structured': { + id: '/api/structured' + path: '/api/structured' + fullPath: '/api/structured' + preLoaderRoute: typeof ApiStructuredRouteImport parentRoute: typeof rootRouteImport } - '/api/list-traces': { - id: '/api/list-traces' - path: '/api/list-traces' - fullPath: '/api/list-traces' - preLoaderRoute: typeof ApiListTracesRouteImport + '/api/summarize': { + id: '/api/summarize' + path: '/api/summarize' + fullPath: '/api/summarize' + preLoaderRoute: typeof ApiSummarizeRouteImport parentRoute: typeof rootRouteImport } - '/api/image': { - id: '/api/image' - path: '/api/image' - fullPath: '/api/image' - preLoaderRoute: typeof ApiImageRouteImport + '/api/transcription': { + id: '/api/transcription' + path: '/api/transcription' + fullPath: '/api/transcription' + preLoaderRoute: typeof ApiTranscriptionRouteImport parentRoute: typeof rootRouteImport } - '/api/chat': { - id: '/api/chat' - path: '/api/chat' - fullPath: '/api/chat' - preLoaderRoute: typeof ApiChatRouteImport + '/api/tts': { + id: '/api/tts' + path: '/api/tts' + fullPath: '/api/tts' + preLoaderRoute: typeof ApiTtsRouteImport parentRoute: typeof rootRouteImport } - '/api/addon-chat': { - id: '/api/addon-chat' - path: '/api/addon-chat' - fullPath: '/api/addon-chat' - preLoaderRoute: typeof ApiAddonChatRouteImport + '/api/video': { + id: '/api/video' + path: '/api/video' + fullPath: '/api/video' + preLoaderRoute: typeof ApiVideoRouteImport parentRoute: typeof rootRouteImport } } From cec27e772b23329fe3370441022a436daf881ef0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 21:32:02 +0000 Subject: [PATCH 3/4] ci: apply automated fixes --- scripts/lovable-gateway.models.json | 387 ++++++---------------------- 1 file changed, 82 insertions(+), 305 deletions(-) diff --git a/scripts/lovable-gateway.models.json b/scripts/lovable-gateway.models.json index 8a193376e4..79c6babbc9 100644 --- a/scripts/lovable-gateway.models.json +++ b/scripts/lovable-gateway.models.json @@ -12,15 +12,8 @@ "max_tokens": 65536, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -85,15 +78,8 @@ "max_tokens": 65536, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text", - "image" - ] + "input": ["text", "image", "video"], + "output": ["text", "image"] }, "pricing": { "input": { @@ -150,15 +136,8 @@ "max_tokens": 65536, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -222,12 +201,8 @@ "context_window": 8192, "max_tokens": 16384, "modalities": { - "input": [ - "text" - ], - "output": [ - "audio" - ] + "input": ["text"], + "output": ["audio"] }, "pricing": { "input": { @@ -265,12 +240,8 @@ "max_tokens": 16384, "knowledge": "2025-01", "modalities": { - "input": [ - "text" - ], - "output": [ - "audio" - ] + "input": ["text"], + "output": ["audio"] }, "pricing": { "input": { @@ -308,15 +279,8 @@ "max_tokens": 65536, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -406,12 +370,8 @@ "max_tokens": 16384, "knowledge": "2025-01", "modalities": { - "input": [ - "text" - ], - "output": [ - "audio" - ] + "input": ["text"], + "output": ["audio"] }, "pricing": { "input": { @@ -449,15 +409,8 @@ "max_tokens": 65000, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -522,15 +475,8 @@ "max_tokens": 32768, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text", - "image" - ] + "input": ["text", "image", "video"], + "output": ["text", "image"] }, "pricing": { "input": { @@ -595,15 +541,8 @@ "max_tokens": 32768, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text", - "image" - ] + "input": ["text", "image", "video"], + "output": ["text", "image"] }, "pricing": { "input": { @@ -668,15 +607,8 @@ "max_tokens": 65000, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -740,15 +672,8 @@ "context_window": 65536, "max_tokens": 4096, "modalities": { - "input": [ - "text", - "image", - "video" - ], - "output": [ - "text", - "image" - ] + "input": ["text", "image", "video"], + "output": ["text", "image"] }, "pricing": { "input": { @@ -813,12 +738,8 @@ "max_tokens": 16384, "knowledge": "2025-01", "modalities": { - "input": [ - "text" - ], - "output": [ - "audio" - ] + "input": ["text"], + "output": ["audio"] }, "pricing": { "input": { @@ -856,15 +777,8 @@ "max_tokens": 64000, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -954,15 +868,8 @@ "max_tokens": 64000, "knowledge": "2025-01", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -1027,15 +934,8 @@ "max_tokens": 64000, "knowledge": "2026-03", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -1100,12 +1000,8 @@ "max_tokens": 0, "knowledge": "2025-05", "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] + "input": ["text"], + "output": ["text"] }, "pricing": { "input": { @@ -1133,15 +1029,8 @@ "max_tokens": 0, "knowledge": "2025-11", "modalities": { - "input": [ - "text", - "image", - "audio", - "video" - ], - "output": [ - "text" - ] + "input": ["text", "image", "audio", "video"], + "output": ["text"] }, "pricing": { "input": { @@ -1192,13 +1081,8 @@ "context_window": 0, "max_tokens": 0, "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "video" - ] + "input": ["text", "image"], + "output": ["video"] }, "pricing": { "video_duration": { @@ -1226,13 +1110,8 @@ "context_window": 0, "max_tokens": 0, "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "video" - ] + "input": ["text", "image"], + "output": ["video"] }, "pricing": { "video_duration": { @@ -1260,13 +1139,8 @@ "context_window": 0, "max_tokens": 0, "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "video" - ] + "input": ["text", "image"], + "output": ["video"] }, "pricing": { "video_duration": { @@ -1291,13 +1165,8 @@ "context_window": 16000, "max_tokens": 2000, "modalities": { - "input": [ - "text", - "audio" - ], - "output": [ - "text" - ] + "input": ["text", "audio"], + "output": ["text"] }, "pricing": { "input": { @@ -1342,12 +1211,8 @@ "context_window": 2000, "max_tokens": 0, "modalities": { - "input": [ - "text" - ], - "output": [ - "audio" - ] + "input": ["text"], + "output": ["audio"] }, "pricing": { "input": { @@ -1384,13 +1249,8 @@ "context_window": 16000, "max_tokens": 2000, "modalities": { - "input": [ - "text", - "audio" - ], - "output": [ - "text" - ] + "input": ["text", "audio"], + "output": ["text"] }, "pricing": { "input": { @@ -1436,13 +1296,8 @@ "max_tokens": 128000, "knowledge": "2024-09-30", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -1526,13 +1381,8 @@ "max_tokens": 128000, "knowledge": "2024-05-30", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -1616,13 +1466,8 @@ "max_tokens": 128000, "knowledge": "2024-05-30", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -1671,13 +1516,8 @@ "max_tokens": 128000, "knowledge": "2024-10", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -1761,13 +1601,8 @@ "max_tokens": 128000, "knowledge": "2025-08-31", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -1866,13 +1701,8 @@ "max_tokens": 128000, "knowledge": "2025-08-31", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -1956,13 +1786,8 @@ "max_tokens": 128000, "knowledge": "2025-08-31", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -2011,13 +1836,8 @@ "max_tokens": 128000, "knowledge": "2025-08-31", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -2081,13 +1901,8 @@ "max_tokens": 128000, "knowledge": "2025-12-01", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -2186,13 +2001,8 @@ "max_tokens": 128000, "knowledge": "2025-12-01", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -2256,13 +2066,8 @@ "max_tokens": 128000, "knowledge": "2026-02-16", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -2361,13 +2166,8 @@ "max_tokens": 128000, "knowledge": "2026-02-16", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -2466,13 +2266,8 @@ "max_tokens": 128000, "knowledge": "2026-02-16", "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "text" - ] + "input": ["text", "image"], + "output": ["text"] }, "pricing": { "input": { @@ -2570,13 +2365,8 @@ "context_window": 0, "max_tokens": 0, "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "image" - ] + "input": ["text", "image"], + "output": ["image"] }, "pricing": { "input": { @@ -2621,13 +2411,8 @@ "context_window": 0, "max_tokens": 0, "modalities": { - "input": [ - "text", - "image" - ], - "output": [ - "image" - ] + "input": ["text", "image"], + "output": ["image"] }, "pricing": { "input": { @@ -2672,12 +2457,8 @@ "context_window": 8191, "max_tokens": 0, "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] + "input": ["text"], + "output": ["text"] }, "pricing": { "input": { @@ -2704,12 +2485,8 @@ "context_window": 8191, "max_tokens": 0, "modalities": { - "input": [ - "text" - ], - "output": [ - "text" - ] + "input": ["text"], + "output": ["text"] }, "pricing": { "input": { From 9cb2a630667446e63b7b8ad84651e6209659f52a Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Thu, 27 Aug 2026 12:48:03 +0200 Subject: [PATCH 4/4] fix(ai-solid): apply chat snapshot before first paint createEffect runs after paint, so persisted messages and resume state never showed on the first render. Apply the snapshot during setup after live subscribe, same as Vue and Solid generation hooks. --- packages/ai-solid/src/use-chat.ts | 5 +++++ packages/ai-solid/tests/use-chat.test.ts | 16 +++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/ai-solid/src/use-chat.ts b/packages/ai-solid/src/use-chat.ts index 19ddf0f85b..b5eba10893 100644 --- a/packages/ai-solid/src/use-chat.ts +++ b/packages/ai-solid/src/use-chat.ts @@ -207,6 +207,11 @@ export function useChat< client().unsubscribe() } + // First paint must see constructor hydration (sync store / resume snapshot) + // and the live subscribe above. `createEffect` runs after paint, so the + // subscribe effect cannot be the first apply. + applySnapshot(client()) + createEffect(() => { if (options.live) { client().subscribe() diff --git a/packages/ai-solid/tests/use-chat.test.ts b/packages/ai-solid/tests/use-chat.test.ts index 050654c896..52d182bac7 100644 --- a/packages/ai-solid/tests/use-chat.test.ts +++ b/packages/ai-solid/tests/use-chat.test.ts @@ -153,14 +153,20 @@ describe('useChat', () => { removeItem: vi.fn(), } - const { result } = renderUseChat({ - connection: adapter, - threadId: 'persisted-chat', - persistence: persistence, + let firstMessages: Array | undefined + const rendered = renderHook(() => { + const chat = useChat({ + connection: adapter, + threadId: 'persisted-chat', + persistence, + }) + firstMessages ??= chat.messages() + return chat }) + expect(firstMessages).toEqual(persistedMessages) await waitFor(() => { - expect(result.current.messages).toEqual(persistedMessages) + expect(rendered.result.messages()).toEqual(persistedMessages) }) expect(persistence.getItem).toHaveBeenCalledWith('persisted-chat') })