@tanstack/ai-solid 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +17 -0
  2. package/dist/index.d.ts +2 -2
  3. package/dist/index.js +2 -2
  4. package/dist/types.d.ts +29 -3
  5. package/dist/use-chat.js +81 -5
  6. package/dist/use-chat.js.map +1 -1
  7. package/dist/use-generate-audio.d.ts +9 -10
  8. package/dist/use-generate-audio.js +4 -11
  9. package/dist/use-generate-audio.js.map +1 -1
  10. package/dist/use-generate-image.d.ts +9 -10
  11. package/dist/use-generate-image.js +4 -11
  12. package/dist/use-generate-image.js.map +1 -1
  13. package/dist/use-generate-speech.d.ts +9 -10
  14. package/dist/use-generate-speech.js +4 -11
  15. package/dist/use-generate-speech.js.map +1 -1
  16. package/dist/use-generate-video.d.ts +50 -4
  17. package/dist/use-generate-video.js +54 -23
  18. package/dist/use-generate-video.js.map +1 -1
  19. package/dist/use-generation.d.ts +59 -6
  20. package/dist/use-generation.js +43 -19
  21. package/dist/use-generation.js.map +1 -1
  22. package/dist/use-summarize.d.ts +9 -10
  23. package/dist/use-summarize.js +4 -11
  24. package/dist/use-summarize.js.map +1 -1
  25. package/dist/use-transcription.d.ts +9 -10
  26. package/dist/use-transcription.js +4 -11
  27. package/dist/use-transcription.js.map +1 -1
  28. package/package.json +6 -6
  29. package/src/index.ts +10 -0
  30. package/src/types.ts +44 -0
  31. package/src/use-chat.ts +123 -6
  32. package/src/use-generate-audio.ts +33 -23
  33. package/src/use-generate-image.ts +33 -23
  34. package/src/use-generate-speech.ts +32 -23
  35. package/src/use-generate-video.ts +120 -25
  36. package/src/use-generation.ts +129 -25
  37. package/src/use-summarize.ts +34 -23
  38. package/src/use-transcription.ts +33 -24
@@ -1,4 +1,9 @@
1
1
  import { useGeneration } from './use-generation'
2
+ import { reconstructAudioResult } from '@tanstack/ai-client'
3
+ import type {
4
+ UseGenerationOptions,
5
+ UseGenerationReturn,
6
+ } from './use-generation'
2
7
  import type { AudioGenerationResult, StreamChunk } from '@tanstack/ai'
3
8
  import type {
4
9
  AIDevtoolsDisplayOptions,
@@ -6,6 +11,7 @@ import type {
6
11
  ConnectConnectionAdapter,
7
12
  GenerationClientState,
8
13
  GenerationFetcher,
14
+ GenerationPersistenceOptions,
9
15
  InferGenerationOutputFromReturn,
10
16
  } from '@tanstack/ai-client'
11
17
  import type { Accessor } from 'solid-js'
@@ -15,12 +21,19 @@ import type { Accessor } from 'solid-js'
15
21
  *
16
22
  * @template TOutput - The transformed output type (defaults to AudioGenerationResult)
17
23
  */
18
- export interface UseGenerateAudioOptions<TOutput = AudioGenerationResult> {
24
+ export interface UseGenerateAudioOptions<
25
+ TOutput = AudioGenerationResult,
26
+ > extends Pick<
27
+ UseGenerationOptions<AudioGenerateInput, AudioGenerationResult, TOutput>,
28
+ 'persistence' | 'threadId' | 'hydrateGeneration' | 'joinRun'
29
+ > {
19
30
  /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */
20
31
  connection?: ConnectConnectionAdapter
21
32
  /** Direct async function for audio generation */
22
33
  fetcher?: GenerationFetcher<AudioGenerateInput, AudioGenerationResult>
23
- /** Unique identifier for this generation instance */
34
+ /**
35
+ * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).
36
+ */
24
37
  id?: string
25
38
  /** Additional body parameters to send with connect-based adapter requests */
26
39
  body?: Record<string, any>
@@ -47,7 +60,9 @@ export interface UseGenerateAudioOptions<TOutput = AudioGenerationResult> {
47
60
  *
48
61
  * @template TOutput - The transformed output type (defaults to AudioGenerationResult)
49
62
  */
50
- export interface UseGenerateAudioReturn<TOutput = AudioGenerationResult> {
63
+ export interface UseGenerateAudioReturn<
64
+ TOutput = AudioGenerationResult,
65
+ > extends Omit<UseGenerationReturn<TOutput>, 'generate'> {
51
66
  /** Trigger audio generation */
52
67
  generate: (input: AudioGenerateInput) => Promise<void>
53
68
  /** The generation result containing audio, or null */
@@ -58,10 +73,6 @@ export interface UseGenerateAudioReturn<TOutput = AudioGenerationResult> {
58
73
  error: Accessor<Error | undefined>
59
74
  /** Current state of the generation */
60
75
  status: Accessor<GenerationClientState>
61
- /** Abort the current generation */
62
- stop: () => void
63
- /** Clear result, error, and return to idle */
64
- reset: () => void
65
76
  }
66
77
 
67
78
  /**
@@ -89,9 +100,12 @@ export interface UseGenerateAudioReturn<TOutput = AudioGenerationResult> {
89
100
  * ```
90
101
  */
91
102
  export function useGenerateAudio<TTransformed = void>(
92
- options: Omit<UseGenerateAudioOptions, 'onResult'> & {
103
+ options: Omit<
104
+ UseGenerateAudioOptions,
105
+ 'onResult' | 'persistence' | 'threadId' | 'id'
106
+ > & {
93
107
  onResult?: (result: AudioGenerationResult) => TTransformed
94
- },
108
+ } & GenerationPersistenceOptions,
95
109
  ): UseGenerateAudioReturn<
96
110
  InferGenerationOutputFromReturn<AudioGenerationResult, TTransformed>
97
111
  > {
@@ -101,19 +115,15 @@ export function useGenerateAudio<TTransformed = void>(
101
115
  hookName: 'useGenerateAudio',
102
116
  outputKind: 'audio' as const,
103
117
  }
104
- const { generate, result, isLoading, error, status, stop, reset } =
105
- useGeneration<AudioGenerateInput, AudioGenerationResult, TTransformed>({
106
- ...options,
107
- devtools,
108
- })
118
+ const generation = useGeneration<
119
+ AudioGenerateInput,
120
+ AudioGenerationResult,
121
+ TTransformed
122
+ >({
123
+ ...options,
124
+ devtools,
125
+ reconstructResult: reconstructAudioResult,
126
+ })
109
127
 
110
- return {
111
- generate: generate as (input: AudioGenerateInput) => Promise<void>,
112
- result,
113
- isLoading,
114
- error,
115
- status,
116
- stop,
117
- reset,
118
- }
128
+ return generation
119
129
  }
@@ -1,10 +1,16 @@
1
1
  import { useGeneration } from './use-generation'
2
+ import { reconstructImageResult } from '@tanstack/ai-client'
3
+ import type {
4
+ UseGenerationOptions,
5
+ UseGenerationReturn,
6
+ } from './use-generation'
2
7
  import type { ImageGenerationResult, StreamChunk } from '@tanstack/ai'
3
8
  import type {
4
9
  AIDevtoolsDisplayOptions,
5
10
  ConnectConnectionAdapter,
6
11
  GenerationClientState,
7
12
  GenerationFetcher,
13
+ GenerationPersistenceOptions,
8
14
  ImageGenerateInput,
9
15
  InferGenerationOutputFromReturn,
10
16
  } from '@tanstack/ai-client'
@@ -15,12 +21,19 @@ import type { Accessor } from 'solid-js'
15
21
  *
16
22
  * @template TOutput - The transformed output type (defaults to ImageGenerationResult)
17
23
  */
18
- export interface UseGenerateImageOptions<TOutput = ImageGenerationResult> {
24
+ export interface UseGenerateImageOptions<
25
+ TOutput = ImageGenerationResult,
26
+ > extends Pick<
27
+ UseGenerationOptions<ImageGenerateInput, ImageGenerationResult, TOutput>,
28
+ 'persistence' | 'threadId' | 'hydrateGeneration' | 'joinRun'
29
+ > {
19
30
  /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */
20
31
  connection?: ConnectConnectionAdapter
21
32
  /** Direct async function for image generation */
22
33
  fetcher?: GenerationFetcher<ImageGenerateInput, ImageGenerationResult>
23
- /** Unique identifier for this generation instance */
34
+ /**
35
+ * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).
36
+ */
24
37
  id?: string
25
38
  /** Additional body parameters to send with connect-based adapter requests */
26
39
  body?: Record<string, any>
@@ -47,7 +60,9 @@ export interface UseGenerateImageOptions<TOutput = ImageGenerationResult> {
47
60
  *
48
61
  * @template TOutput - The transformed output type (defaults to ImageGenerationResult)
49
62
  */
50
- export interface UseGenerateImageReturn<TOutput = ImageGenerationResult> {
63
+ export interface UseGenerateImageReturn<
64
+ TOutput = ImageGenerationResult,
65
+ > extends Omit<UseGenerationReturn<TOutput>, 'generate'> {
51
66
  /** Trigger image generation */
52
67
  generate: (input: ImageGenerateInput) => Promise<void>
53
68
  /** The generation result containing images, or null */
@@ -58,10 +73,6 @@ export interface UseGenerateImageReturn<TOutput = ImageGenerationResult> {
58
73
  error: Accessor<Error | undefined>
59
74
  /** Current state of the generation */
60
75
  status: Accessor<GenerationClientState>
61
- /** Abort the current generation */
62
- stop: () => void
63
- /** Clear result, error, and return to idle */
64
- reset: () => void
65
76
  }
66
77
 
67
78
  /**
@@ -97,9 +108,12 @@ export interface UseGenerateImageReturn<TOutput = ImageGenerationResult> {
97
108
  * ```
98
109
  */
99
110
  export function useGenerateImage<TTransformed = void>(
100
- options: Omit<UseGenerateImageOptions, 'onResult'> & {
111
+ options: Omit<
112
+ UseGenerateImageOptions,
113
+ 'onResult' | 'persistence' | 'threadId' | 'id'
114
+ > & {
101
115
  onResult?: (result: ImageGenerationResult) => TTransformed
102
- },
116
+ } & GenerationPersistenceOptions,
103
117
  ): UseGenerateImageReturn<
104
118
  InferGenerationOutputFromReturn<ImageGenerationResult, TTransformed>
105
119
  > {
@@ -109,19 +123,15 @@ export function useGenerateImage<TTransformed = void>(
109
123
  hookName: 'useGenerateImage',
110
124
  outputKind: 'image' as const,
111
125
  }
112
- const { generate, result, isLoading, error, status, stop, reset } =
113
- useGeneration<ImageGenerateInput, ImageGenerationResult, TTransformed>({
114
- ...options,
115
- devtools,
116
- })
126
+ const generation = useGeneration<
127
+ ImageGenerateInput,
128
+ ImageGenerationResult,
129
+ TTransformed
130
+ >({
131
+ ...options,
132
+ devtools,
133
+ reconstructResult: reconstructImageResult,
134
+ })
117
135
 
118
- return {
119
- generate: generate as (input: ImageGenerateInput) => Promise<void>,
120
- result,
121
- isLoading,
122
- error,
123
- status,
124
- stop,
125
- reset,
126
- }
136
+ return generation
127
137
  }
@@ -1,10 +1,16 @@
1
1
  import { useGeneration } from './use-generation'
2
+ import { reconstructSpeechResult } from '@tanstack/ai-client'
3
+ import type {
4
+ UseGenerationOptions,
5
+ UseGenerationReturn,
6
+ } from './use-generation'
2
7
  import type { StreamChunk, TTSResult } from '@tanstack/ai'
3
8
  import type {
4
9
  AIDevtoolsDisplayOptions,
5
10
  ConnectConnectionAdapter,
6
11
  GenerationClientState,
7
12
  GenerationFetcher,
13
+ GenerationPersistenceOptions,
8
14
  InferGenerationOutputFromReturn,
9
15
  SpeechGenerateInput,
10
16
  } from '@tanstack/ai-client'
@@ -15,12 +21,17 @@ import type { Accessor } from 'solid-js'
15
21
  *
16
22
  * @template TOutput - The transformed output type (defaults to TTSResult)
17
23
  */
18
- export interface UseGenerateSpeechOptions<TOutput = TTSResult> {
24
+ export interface UseGenerateSpeechOptions<TOutput = TTSResult> extends Pick<
25
+ UseGenerationOptions<SpeechGenerateInput, TTSResult, TOutput>,
26
+ 'persistence' | 'threadId' | 'hydrateGeneration' | 'joinRun'
27
+ > {
19
28
  /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */
20
29
  connection?: ConnectConnectionAdapter
21
30
  /** Direct async function for speech generation */
22
31
  fetcher?: GenerationFetcher<SpeechGenerateInput, TTSResult>
23
- /** Unique identifier for this generation instance */
32
+ /**
33
+ * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).
34
+ */
24
35
  id?: string
25
36
  /** Additional body parameters to send with connect-based adapter requests */
26
37
  body?: Record<string, any>
@@ -47,7 +58,10 @@ export interface UseGenerateSpeechOptions<TOutput = TTSResult> {
47
58
  *
48
59
  * @template TOutput - The transformed output type (defaults to TTSResult)
49
60
  */
50
- export interface UseGenerateSpeechReturn<TOutput = TTSResult> {
61
+ export interface UseGenerateSpeechReturn<TOutput = TTSResult> extends Omit<
62
+ UseGenerationReturn<TOutput>,
63
+ 'generate'
64
+ > {
51
65
  /** Trigger speech generation */
52
66
  generate: (input: SpeechGenerateInput) => Promise<void>
53
67
  /** The TTS result containing audio data, or null */
@@ -58,10 +72,6 @@ export interface UseGenerateSpeechReturn<TOutput = TTSResult> {
58
72
  error: Accessor<Error | undefined>
59
73
  /** Current state of the generation */
60
74
  status: Accessor<GenerationClientState>
61
- /** Abort the current generation */
62
- stop: () => void
63
- /** Clear result, error, and return to idle */
64
- reset: () => void
65
75
  }
66
76
 
67
77
  /**
@@ -91,9 +101,12 @@ export interface UseGenerateSpeechReturn<TOutput = TTSResult> {
91
101
  * ```
92
102
  */
93
103
  export function useGenerateSpeech<TTransformed = void>(
94
- options: Omit<UseGenerateSpeechOptions, 'onResult'> & {
104
+ options: Omit<
105
+ UseGenerateSpeechOptions,
106
+ 'onResult' | 'persistence' | 'threadId' | 'id'
107
+ > & {
95
108
  onResult?: (result: TTSResult) => TTransformed
96
- },
109
+ } & GenerationPersistenceOptions,
97
110
  ): UseGenerateSpeechReturn<
98
111
  InferGenerationOutputFromReturn<TTSResult, TTransformed>
99
112
  > {
@@ -103,19 +116,15 @@ export function useGenerateSpeech<TTransformed = void>(
103
116
  hookName: 'useGenerateSpeech',
104
117
  outputKind: 'audio' as const,
105
118
  }
106
- const { generate, result, isLoading, error, status, stop, reset } =
107
- useGeneration<SpeechGenerateInput, TTSResult, TTransformed>({
108
- ...options,
109
- devtools,
110
- })
119
+ const generation = useGeneration<
120
+ SpeechGenerateInput,
121
+ TTSResult,
122
+ TTransformed
123
+ >({
124
+ ...options,
125
+ devtools,
126
+ reconstructResult: reconstructSpeechResult,
127
+ })
111
128
 
112
- return {
113
- generate: generate as (input: SpeechGenerateInput) => Promise<void>,
114
- result,
115
- isLoading,
116
- error,
117
- status,
118
- stop,
119
- reset,
120
- }
129
+ return generation
121
130
  }
@@ -2,11 +2,11 @@ import { VideoGenerationClient } from '@tanstack/ai-client'
2
2
  import { createVideoDevtoolsBridge } from '@tanstack/ai-client/devtools'
3
3
  import {
4
4
  createEffect,
5
- createMemo,
6
5
  createSignal,
7
6
  createUniqueId,
8
7
  onCleanup,
9
8
  onMount,
9
+ untrack,
10
10
  } from 'solid-js'
11
11
  import type { StreamChunk } from '@tanstack/ai'
12
12
  import type {
@@ -14,6 +14,7 @@ import type {
14
14
  ConnectConnectionAdapter,
15
15
  GenerationClientState,
16
16
  GenerationFetcher,
17
+ GenerationPersistenceOptions,
17
18
  InferGenerationOutputFromReturn,
18
19
  VideoGenerateInput,
19
20
  VideoGenerateResult,
@@ -31,12 +32,51 @@ export interface UseGenerateVideoOptions<TOutput = VideoGenerateResult> {
31
32
  connection?: ConnectConnectionAdapter
32
33
  /** Direct async function that returns a completed video result */
33
34
  fetcher?: GenerationFetcher<VideoGenerateInput, VideoGenerateResult>
34
- /** Unique identifier for this generation instance */
35
+ /**
36
+ * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).
37
+ */
35
38
  id?: string
36
39
  /** Additional body parameters to send with connect-based adapter requests */
37
40
  body?: Record<string, any>
38
41
  /** Display options for TanStack AI Devtools. */
39
42
  devtools?: AIDevtoolsDisplayOptions
43
+ /**
44
+ * How this generation persists across reloads.
45
+ * - Omit / `false`: ephemeral, in-memory only.
46
+ * - `true`: server-driven — on mount the client hydrates the last generation
47
+ * for its `threadId` from the server (needs a connection with a
48
+ * `hydrateGeneration` handler) and repaints it; it never auto-starts a run.
49
+ */
50
+ persistence?: boolean
51
+ /**
52
+ * The **scope** this generation belongs to: a stable, app-chosen name for the
53
+ * slot successive runs fill — not a link to a chat conversation.
54
+ *
55
+ * The hook starts empty and produces many runs over its life; each gets its
56
+ * own `runId`, but all belong to one scope. Persistence keys on this, so
57
+ * derive it from your own domain and keep it identical across reloads (e.g.
58
+ * `` `video-${videoId}-start-frame` ``). It is also sent as the AG-UI thread
59
+ * id on the wire, which the protocol requires.
60
+ *
61
+ * **Required whenever `persistence` is set** — an app that cannot name the
62
+ * scope has nothing to restore to. Optional for ephemeral generations, where
63
+ * it falls back to `id` purely to satisfy the wire.
64
+ */
65
+ threadId?: string
66
+ /**
67
+ * Server-driven hydration handler for `persistence: true` when the
68
+ * connection doesn't carry one (e.g. alongside `fetcher`, or a `stream()` /
69
+ * `rpcStream()` adapter built without handlers) — typically a one-line
70
+ * server-function call. The connection's own handler takes precedence.
71
+ */
72
+ hydrateGeneration?: ConnectConnectionAdapter['hydrateGeneration']
73
+ /**
74
+ * Re-attach handler that replays a run still generating to completion on
75
+ * mount, when the connection doesn't carry one. Without it, a restored
76
+ * `running` snapshot surfaces as an (interrupted) error. The connection's
77
+ * own handler takes precedence.
78
+ */
79
+ joinRun?: ConnectConnectionAdapter['joinRun']
40
80
  /**
41
81
  * Callback when video generation completes. Can optionally return a transformed value.
42
82
  *
@@ -81,6 +121,13 @@ export interface UseGenerateVideoReturn<TOutput = VideoGenerateResult> {
81
121
  stop: () => void
82
122
  /** Clear all state and return to idle */
83
123
  reset: () => void
124
+ /**
125
+ * The id of the generation job currently running, or `null` when nothing is in
126
+ * flight. Each call to `generate` is one job with its own id. Pass it to your
127
+ * own endpoint to cancel or poll the provider job — `stop()` only aborts the
128
+ * local stream, it does not stop work already running on the provider.
129
+ */
130
+ runId: Accessor<string | null>
84
131
  }
85
132
 
86
133
  /**
@@ -118,9 +165,12 @@ export interface UseGenerateVideoReturn<TOutput = VideoGenerateResult> {
118
165
  // parameter is typed as `VideoGenerateResult` and `result` narrows to the
119
166
  // transform's return. See issue #848.
120
167
  export function useGenerateVideo<TTransformed = void>(
121
- options: Omit<UseGenerateVideoOptions, 'onResult'> & {
168
+ options: Omit<
169
+ UseGenerateVideoOptions,
170
+ 'onResult' | 'persistence' | 'threadId' | 'id'
171
+ > & {
122
172
  onResult?: (result: VideoGenerateResult) => TTransformed
123
- },
173
+ } & GenerationPersistenceOptions,
124
174
  ): UseGenerateVideoReturn<
125
175
  InferGenerationOutputFromReturn<VideoGenerateResult, TTransformed>
126
176
  > {
@@ -129,7 +179,6 @@ export function useGenerateVideo<TTransformed = void>(
129
179
  TTransformed
130
180
  >
131
181
  const hookId = createUniqueId()
132
- const clientId = options.id || hookId
133
182
 
134
183
  const [result, setResult] = createSignal<TOutput | null>(null)
135
184
  const [jobId, setJobId] = createSignal<string | null>(null)
@@ -139,13 +188,30 @@ export function useGenerateVideo<TTransformed = void>(
139
188
  const [isLoading, setIsLoading] = createSignal(false)
140
189
  const [error, setError] = createSignal<Error | undefined>(undefined)
141
190
  const [status, setStatus] = createSignal<GenerationClientState>('idle')
191
+ const [runId, setRunId] = createSignal<string | null>(null)
192
+ let disposed = false
142
193
 
143
- const client = createMemo(() => {
194
+ // Built once. `untrack` keeps the option reads below from subscribing
195
+ // construction to `options.persistence` / `options.devtools` /
196
+ // `options.body`: a re-run would build a second client
197
+ // and orphan the first (only the live one is disposed on cleanup). Later
198
+ // `options.body` changes are pushed through `updateOptions` instead.
199
+ const client = untrack((): VideoGenerationClient<TOutput> => {
144
200
  // Conditional spread on `body`: VideoGenerationClientOptions.body
145
201
  // is a strict optional; EOPT forbids passing `T | undefined`.
146
202
  const baseOptions = {
147
- id: clientId,
148
203
  body: options.body,
204
+ // Identity: pass `threadId` alone when set (never also pass deprecated `id`).
205
+ ...(options.threadId !== undefined
206
+ ? { threadId: options.threadId }
207
+ : { id: options.id ?? hookId }),
208
+ ...(options.persistence !== undefined && {
209
+ persistence: options.persistence,
210
+ }),
211
+ ...(options.hydrateGeneration !== undefined && {
212
+ hydrateGeneration: options.hydrateGeneration,
213
+ }),
214
+ ...(options.joinRun !== undefined && { joinRun: options.joinRun }),
149
215
  devtoolsBridgeFactory: createVideoDevtoolsBridge,
150
216
  devtools: {
151
217
  ...options.devtools,
@@ -159,17 +225,42 @@ export function useGenerateVideo<TTransformed = void>(
159
225
  onResult: ((r: VideoGenerateResult) => options.onResult?.(r)) as (
160
226
  result: VideoGenerateResult,
161
227
  ) => TOutput | null | void,
162
- onError: (e: Error) => options.onError?.(e),
163
- onProgress: (p: number, m?: string) => options.onProgress?.(p, m),
164
- onChunk: (c: StreamChunk) => options.onChunk?.(c),
165
- onJobCreated: (id: string) => options.onJobCreated?.(id),
166
- onStatusUpdate: (s: VideoStatusInfo) => options.onStatusUpdate?.(s),
167
- onResultChange: setResult,
168
- onLoadingChange: setIsLoading,
169
- onErrorChange: setError,
170
- onStatusChange: setStatus,
171
- onJobIdChange: setJobId,
172
- onVideoStatusChange: setVideoStatus,
228
+ onError: (e: Error) => {
229
+ if (!disposed) options.onError?.(e)
230
+ },
231
+ onProgress: (p: number, m?: string) => {
232
+ if (!disposed) options.onProgress?.(p, m)
233
+ },
234
+ onChunk: (c: StreamChunk) => {
235
+ if (!disposed) options.onChunk?.(c)
236
+ },
237
+ onJobCreated: (id: string) => {
238
+ if (!disposed) options.onJobCreated?.(id)
239
+ },
240
+ onStatusUpdate: (s: VideoStatusInfo) => {
241
+ if (!disposed) options.onStatusUpdate?.(s)
242
+ },
243
+ onResultChange: (r: TOutput | null) => {
244
+ if (!disposed) setResult(() => r)
245
+ },
246
+ onLoadingChange: (l: boolean) => {
247
+ if (!disposed) setIsLoading(l)
248
+ },
249
+ onErrorChange: (e: Error | undefined) => {
250
+ if (!disposed) setError(e)
251
+ },
252
+ onStatusChange: (s: GenerationClientState) => {
253
+ if (!disposed) setStatus(s)
254
+ },
255
+ onJobIdChange: (id: string | null) => {
256
+ if (!disposed) setJobId(id)
257
+ },
258
+ onVideoStatusChange: (s: VideoStatusInfo | null) => {
259
+ if (!disposed) setVideoStatus(s)
260
+ },
261
+ onResumeStateChange: (rs: { runId: string } | null) => {
262
+ if (!disposed) setRunId(rs?.runId ?? null)
263
+ },
173
264
  }
174
265
 
175
266
  if (options.connection) {
@@ -189,35 +280,38 @@ export function useGenerateVideo<TTransformed = void>(
189
280
  throw new Error(
190
281
  'useGenerateVideo requires either a connection or fetcher option',
191
282
  )
192
- }, [clientId])
283
+ })
193
284
 
194
285
  // Sync body changes without recreating client
195
286
  createEffect(() => {
196
287
  const currentBody = options.body
197
- client().updateOptions({
288
+ client.updateOptions({
198
289
  ...(currentBody !== undefined && { body: currentBody }),
199
290
  })
200
291
  })
201
292
 
293
+ // Mount devtools only. Generation runs are never auto-started on mount — a
294
+ // persisted snapshot is hydrated for display, never replayed.
202
295
  onMount(() => {
203
- client().mountDevtools()
296
+ client.mountDevtools()
204
297
  })
205
298
 
206
299
  // Cleanup on unmount: stop any in-flight requests and unregister devtools
207
300
  onCleanup(() => {
208
- client().dispose()
301
+ disposed = true
302
+ client.dispose()
209
303
  })
210
304
 
211
305
  const generate = async (input: VideoGenerateInput) => {
212
- await client().generate(input)
306
+ await client.generate(input)
213
307
  }
214
308
 
215
309
  const stop = () => {
216
- client().stop()
310
+ client.stop()
217
311
  }
218
312
 
219
313
  const reset = () => {
220
- client().reset()
314
+ client.reset()
221
315
  }
222
316
 
223
317
  return {
@@ -230,5 +324,6 @@ export function useGenerateVideo<TTransformed = void>(
230
324
  status,
231
325
  stop,
232
326
  reset,
327
+ runId,
233
328
  }
234
329
  }