@tanstack/ai-solid 0.17.0 → 0.18.2

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.
package/README.md CHANGED
@@ -407,7 +407,7 @@ export const Route = createFileRoute('/api/chat')({
407
407
  return toServerSentEventsResponse(
408
408
  chat({
409
409
  adapter: anthropicText(),
410
- model: 'claude-sonnet-4-20250514',
410
+ model: 'claude-sonnet-4-6',
411
411
  messages,
412
412
  tools, // Tools with execute functions
413
413
  }),
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Accessor } from "solid-js";
2
- import { AIDevtoolsDisplayOptions, BoundInterrupts, ChatClientOptions, ChatClientState, ChatInterrupt, ChatInterruptState, ChatRequestBody, ChatResumeState, ClientContextOptionFromTools, ConnectionStatus, DistributedOmit, InferredClientContext, MultimodalContent, QueueConfig, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, UIMessage, WhenBusy } from "@tanstack/ai-client";
3
- import { AnyClientTool, InferSchemaType, ModelMessage, RunAgentResumeItem, SchemaInput } from "@tanstack/ai";
2
+ import { AIDevtoolsDisplayOptions, BoundInterrupts, ChatClientOptions, ChatClientState, ChatInterruptState, ChatRequestBody, ChatResumeState, ClientContextOptionFromTools, ConnectionStatus, DistributedOmit, InferredClientContext, MultimodalContent, QueueConfig, QueueOption, QueueStrategy, QueuedMessage, ResolvableChatInterrupt, SendMessageOptions, UIMessage, WhenBusy } from "@tanstack/ai-client";
3
+ import { AnyClientTool, InferSchemaType, InterruptDefinition, ModelMessage, RunAgentResumeItem, SchemaInput } from "@tanstack/ai";
4
4
 
5
5
  //#region src/types.d.ts
6
6
 
@@ -31,7 +31,7 @@ type DeepPartial<T> = T extends ReadonlyArray<infer U> ? Array<DeepPartial<U>> :
31
31
  * Note: Connection and body changes will recreate the ChatClient instance.
32
32
  * To update these options, remount the component or use a key prop.
33
33
  */
34
- type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>> = DistributedOmit<ChatClientOptions<TTools, TContext>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange' | 'onQueueChange' | 'onResumeStateChange' | 'onRunIdChange' | 'context' | 'devtools'> & {
34
+ type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []> = DistributedOmit<ChatClientOptions<TTools, TContext, TInterrupts>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange' | 'onQueueChange' | 'onResumeStateChange' | 'onRunIdChange' | 'context' | 'devtools'> & {
35
35
  /** Display options for TanStack AI Devtools. */
36
36
  devtools?: AIDevtoolsDisplayOptions;
37
37
  live?: boolean;
@@ -45,7 +45,7 @@ type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema e
45
45
  * Discriminated return shape: when `outputSchema` is supplied, the hook adds
46
46
  * typed `partial` / `final` accessors; otherwise the return is unchanged.
47
47
  */
48
- type UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = BaseUseChatReturn<TTools, TSchema extends SchemaInput ? InferSchemaType<TSchema> : unknown> & (TSchema extends SchemaInput ? {
48
+ type UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []> = BaseUseChatReturn<TTools, TSchema extends SchemaInput ? InferSchemaType<TSchema> : unknown, TInterrupts> & (TSchema extends SchemaInput ? {
49
49
  /**
50
50
  * Live progressively-parsed structured output. Derived from the
51
51
  * latest assistant message's structured-output part.
@@ -57,7 +57,7 @@ type UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema ex
57
57
  */
58
58
  final: Accessor<InferSchemaType<TSchema> | null>;
59
59
  } : Record<never, never>);
60
- interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TData = unknown> {
60
+ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TData = unknown, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []> {
61
61
  /**
62
62
  * Current messages in the conversation. When `outputSchema` is supplied,
63
63
  * `messages()[i].parts.find(p => p.type === 'structured-output')` is typed
@@ -114,14 +114,14 @@ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, T
114
114
  * it, correlate a log line).
115
115
  */
116
116
  runId: Accessor<string | null>;
117
- interrupts: Accessor<BoundInterrupts<TTools>>;
117
+ interrupts: Accessor<BoundInterrupts<TTools, TInterrupts>>;
118
118
  /** @deprecated Use `interrupts`. */
119
- pendingInterrupts: Accessor<BoundInterrupts<TTools>>;
120
- interruptErrors: Accessor<ChatInterruptState<TTools>['interruptErrors']>;
119
+ pendingInterrupts: Accessor<BoundInterrupts<TTools, TInterrupts>>;
120
+ interruptErrors: Accessor<ChatInterruptState<TTools, TInterrupts>['interruptErrors']>;
121
121
  resuming: Accessor<boolean>;
122
122
  resolveInterrupts: {
123
123
  (approved: boolean): void;
124
- (resolver: (interrupt: ChatInterrupt<TTools>) => undefined): void;
124
+ (resolver: (interrupt: ResolvableChatInterrupt<TTools, TInterrupts>) => undefined): void;
125
125
  };
126
126
  cancelInterrupts: () => void;
127
127
  retryInterrupts: () => void;
@@ -30,7 +30,9 @@ interface UseAudioRecorderReturn<TOutput> {
30
30
  * failure (and `stop()` rejects with `Recording cancelled` if `cancel()` runs
31
31
  * while a stop is in flight, e.g. on unmount) — handle one channel, not both.
32
32
  */
33
- declare function useAudioRecorder<TOnComplete extends (recording: AudioRecording) => unknown>(options: UseAudioRecorderOptions<TOnComplete>): UseAudioRecorderReturn<InferAudioRecordingOutput<TOnComplete>>;
33
+ declare function useAudioRecorder<TOnComplete extends (recording: AudioRecording) => unknown>(options: UseAudioRecorderOptions<TOnComplete> & {
34
+ onComplete: TOnComplete;
35
+ }): UseAudioRecorderReturn<InferAudioRecordingOutput<TOnComplete>>;
34
36
  declare function useAudioRecorder(options?: UseAudioRecorderOptions<undefined>): UseAudioRecorderReturn<AudioRecording>;
35
37
  //#endregion
36
38
  export { UseAudioRecorderOptions, UseAudioRecorderReturn, useAudioRecorder };
@@ -1 +1 @@
1
- {"version":3,"file":"use-audio-recorder.js","names":[],"sources":["../src/use-audio-recorder.ts"],"sourcesContent":["import { createSignal, onCleanup } from 'solid-js'\nimport { AudioRecorder } from '@tanstack/ai-client'\nimport type {\n AudioRecorderOptions,\n AudioRecording,\n InferAudioRecordingOutput,\n} from '@tanstack/ai-client'\n\nexport type UseAudioRecorderOptions<TOnComplete> = AudioRecorderOptions & {\n /**\n * Optional transform applied to the recording when `stop()` resolves. Its\n * (awaited) return value becomes `recording` and the resolved value of\n * `stop()`. Return nothing to keep the raw `AudioRecording`.\n */\n onComplete?: TOnComplete\n}\n\nexport interface UseAudioRecorderReturn<TOutput> {\n /** Solid accessor: latest recording (transformed if `onComplete` provided), or null. */\n recording: () => TOutput | null\n /** Solid accessor: true while actively capturing audio. */\n isRecording: () => boolean\n /** Whether the browser supports recording. */\n isSupported: boolean\n start: () => Promise<void>\n /** Stop and resolve with the completed recording (transformed if `onComplete` provided). */\n stop: () => Promise<TOutput>\n /** Discard the in-progress recording and release the mic. */\n cancel: () => void\n}\n\n/**\n * Solid hook for recording an audio message. The resolved recording carries\n * `.part` (for `useChat.sendMessage`) and `.base64` (for generation hooks).\n *\n * Errors are delivered via `onError`. `start()` and `stop()` also reject on\n * failure (and `stop()` rejects with `Recording cancelled` if `cancel()` runs\n * while a stop is in flight, e.g. on unmount) — handle one channel, not both.\n */\nexport function useAudioRecorder<\n TOnComplete extends (recording: AudioRecording) => unknown,\n>(\n options: UseAudioRecorderOptions<TOnComplete>,\n): UseAudioRecorderReturn<InferAudioRecordingOutput<TOnComplete>>\nexport function useAudioRecorder(\n options?: UseAudioRecorderOptions<undefined>,\n): UseAudioRecorderReturn<AudioRecording>\nexport function useAudioRecorder(\n options: UseAudioRecorderOptions<(recording: AudioRecording) => unknown> = {},\n): UseAudioRecorderReturn<unknown> {\n const recorder = new AudioRecorder({\n ...(options.audio !== undefined && { audio: options.audio }),\n ...(options.mimeType !== undefined && { mimeType: options.mimeType }),\n ...(options.onError !== undefined && { onError: options.onError }),\n })\n const [isRecording, setIsRecording] = createSignal(false)\n const [recording, setRecording] = createSignal<unknown>(null)\n\n const unsubscribe = recorder.subscribe((state) => {\n setIsRecording(state === 'recording')\n })\n\n onCleanup(() => {\n unsubscribe()\n recorder.cancel()\n })\n\n const stop = async (): Promise<unknown> => {\n const rawRecording = await recorder.stop()\n const transformed = await options.onComplete?.(rawRecording)\n // Only `undefined` (returning nothing) keeps the raw recording; a returned\n // null is a real value, matching the inferred output type.\n const output = transformed === undefined ? rawRecording : transformed\n // Store via updater so a function-valued transform result isn't invoked.\n setRecording(() => output)\n return output\n }\n\n return {\n recording,\n isRecording,\n isSupported: AudioRecorder.isSupported(),\n start: () => recorder.start(),\n stop,\n cancel: () => recorder.cancel(),\n }\n}\n"],"mappings":";;;;AA+CA,SAAgB,iBACd,UAA2E,EAAE,EAC5C;CACjC,MAAM,WAAW,IAAI,cAAc;EACjC,GAAI,QAAQ,UAAU,UAAa,EAAE,OAAO,QAAQ,OAAO;EAC3D,GAAI,QAAQ,aAAa,UAAa,EAAE,UAAU,QAAQ,UAAU;EACpE,GAAI,QAAQ,YAAY,UAAa,EAAE,SAAS,QAAQ,SAAS;EAClE,CAAC;CACF,MAAM,CAAC,aAAa,kBAAkB,aAAa,MAAM;CACzD,MAAM,CAAC,WAAW,gBAAgB,aAAsB,KAAK;CAE7D,MAAM,cAAc,SAAS,WAAW,UAAU;AAChD,iBAAe,UAAU,YAAY;GACrC;AAEF,iBAAgB;AACd,eAAa;AACb,WAAS,QAAQ;GACjB;CAEF,MAAM,OAAO,YAA8B;EACzC,MAAM,eAAe,MAAM,SAAS,MAAM;EAC1C,MAAM,cAAc,MAAM,QAAQ,aAAa,aAAa;EAG5D,MAAM,SAAS,gBAAgB,SAAY,eAAe;AAE1D,qBAAmB,OAAO;AAC1B,SAAO;;AAGT,QAAO;EACL;EACA;EACA,aAAa,cAAc,aAAa;EACxC,aAAa,SAAS,OAAO;EAC7B;EACA,cAAc,SAAS,QAAQ;EAChC"}
1
+ {"version":3,"file":"use-audio-recorder.js","names":[],"sources":["../src/use-audio-recorder.ts"],"sourcesContent":["import { createSignal, onCleanup } from 'solid-js'\nimport { AudioRecorder } from '@tanstack/ai-client'\nimport type {\n AudioRecorderOptions,\n AudioRecording,\n InferAudioRecordingOutput,\n} from '@tanstack/ai-client'\n\nexport type UseAudioRecorderOptions<TOnComplete> = AudioRecorderOptions & {\n /**\n * Optional transform applied to the recording when `stop()` resolves. Its\n * (awaited) return value becomes `recording` and the resolved value of\n * `stop()`. Return nothing to keep the raw `AudioRecording`.\n */\n onComplete?: TOnComplete\n}\n\nexport interface UseAudioRecorderReturn<TOutput> {\n /** Solid accessor: latest recording (transformed if `onComplete` provided), or null. */\n recording: () => TOutput | null\n /** Solid accessor: true while actively capturing audio. */\n isRecording: () => boolean\n /** Whether the browser supports recording. */\n isSupported: boolean\n start: () => Promise<void>\n /** Stop and resolve with the completed recording (transformed if `onComplete` provided). */\n stop: () => Promise<TOutput>\n /** Discard the in-progress recording and release the mic. */\n cancel: () => void\n}\n\n/**\n * Solid hook for recording an audio message. The resolved recording carries\n * `.part` (for `useChat.sendMessage`) and `.base64` (for generation hooks).\n *\n * Errors are delivered via `onError`. `start()` and `stop()` also reject on\n * failure (and `stop()` rejects with `Recording cancelled` if `cancel()` runs\n * while a stop is in flight, e.g. on unmount) — handle one channel, not both.\n */\n// The transforming overload requires `onComplete`. Without that constraint an\n// options object carrying only unrelated keys (`useAudioRecorder({ onError })`)\n// still matches it, `TOnComplete` infers as `unknown`, and `recording`/`stop()`\n// collapse to `unknown` — so passing any option would silently cost you the\n// `AudioRecording` type. Requiring it here sends those calls to the second\n// overload instead (issue #1001).\nexport function useAudioRecorder<\n TOnComplete extends (recording: AudioRecording) => unknown,\n>(\n options: UseAudioRecorderOptions<TOnComplete> & { onComplete: TOnComplete },\n): UseAudioRecorderReturn<InferAudioRecordingOutput<TOnComplete>>\nexport function useAudioRecorder(\n options?: UseAudioRecorderOptions<undefined>,\n): UseAudioRecorderReturn<AudioRecording>\nexport function useAudioRecorder(\n options: UseAudioRecorderOptions<(recording: AudioRecording) => unknown> = {},\n): UseAudioRecorderReturn<unknown> {\n const recorder = new AudioRecorder({\n ...(options.audio !== undefined && { audio: options.audio }),\n ...(options.mimeType !== undefined && { mimeType: options.mimeType }),\n ...(options.onError !== undefined && { onError: options.onError }),\n })\n const [isRecording, setIsRecording] = createSignal(false)\n const [recording, setRecording] = createSignal<unknown>(null)\n\n const unsubscribe = recorder.subscribe((state) => {\n setIsRecording(state === 'recording')\n })\n\n onCleanup(() => {\n unsubscribe()\n recorder.cancel()\n })\n\n const stop = async (): Promise<unknown> => {\n const rawRecording = await recorder.stop()\n const transformed = await options.onComplete?.(rawRecording)\n // Only `undefined` (returning nothing) keeps the raw recording; a returned\n // null is a real value, matching the inferred output type.\n const output = transformed === undefined ? rawRecording : transformed\n // Store via updater so a function-valued transform result isn't invoked.\n setRecording(() => output)\n return output\n }\n\n return {\n recording,\n isRecording,\n isSupported: AudioRecorder.isSupported(),\n start: () => recorder.start(),\n stop,\n cancel: () => recorder.cancel(),\n }\n}\n"],"mappings":";;;;AAqDA,SAAgB,iBACd,UAA2E,EAAE,EAC5C;CACjC,MAAM,WAAW,IAAI,cAAc;EACjC,GAAI,QAAQ,UAAU,UAAa,EAAE,OAAO,QAAQ,OAAO;EAC3D,GAAI,QAAQ,aAAa,UAAa,EAAE,UAAU,QAAQ,UAAU;EACpE,GAAI,QAAQ,YAAY,UAAa,EAAE,SAAS,QAAQ,SAAS;EAClE,CAAC;CACF,MAAM,CAAC,aAAa,kBAAkB,aAAa,MAAM;CACzD,MAAM,CAAC,WAAW,gBAAgB,aAAsB,KAAK;CAE7D,MAAM,cAAc,SAAS,WAAW,UAAU;AAChD,iBAAe,UAAU,YAAY;GACrC;AAEF,iBAAgB;AACd,eAAa;AACb,WAAS,QAAQ;GACjB;CAEF,MAAM,OAAO,YAA8B;EACzC,MAAM,eAAe,MAAM,SAAS,MAAM;EAC1C,MAAM,cAAc,MAAM,QAAQ,aAAa,aAAa;EAG5D,MAAM,SAAS,gBAAgB,SAAY,eAAe;AAE1D,qBAAmB,OAAO;AAC1B,SAAO;;AAGT,QAAO;EACL;EACA;EACA,aAAa,cAAc,aAAa;EACxC,aAAa,SAAS,OAAO;EAC7B;EACA,cAAc,SAAS,QAAQ;EAChC"}
@@ -1,9 +1,9 @@
1
1
  import { UseChatOptions, UseChatReturn } from "./types.js";
2
2
  import { InferredClientContext } from "@tanstack/ai-client";
3
- import { AnyClientTool, SchemaInput } from "@tanstack/ai";
3
+ import { AnyClientTool, InterruptDefinition, SchemaInput } from "@tanstack/ai";
4
4
 
5
5
  //#region src/use-chat.d.ts
6
- declare function useChat<const TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>>(options?: UseChatOptions<TTools, TSchema, TContext>): UseChatReturn<TTools, TSchema>;
6
+ declare function useChat<const TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined, TContext = InferredClientContext<TTools>, const TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []>(options?: UseChatOptions<TTools, TSchema, TContext, TInterrupts>): UseChatReturn<TTools, TSchema, TInterrupts>;
7
7
  //#endregion
8
8
  export { useChat };
9
9
  //# sourceMappingURL=use-chat.d.ts.map
package/dist/use-chat.js CHANGED
@@ -57,6 +57,7 @@ function useChat(options = {}) {
57
57
  options.onError?.(err);
58
58
  },
59
59
  tools: options.tools,
60
+ ...options.interrupts !== void 0 && { interrupts: options.interrupts },
60
61
  onCustomEvent: (eventType, data, context) => options.onCustomEvent?.(eventType, data, context),
61
62
  ...options.streamProcessor !== void 0 && { streamProcessor: options.streamProcessor },
62
63
  onMessagesChange: (newMessages) => {
@@ -95,9 +96,9 @@ function useChat(options = {}) {
95
96
  pendingInterrupts: nextPendingInterrupts
96
97
  }));
97
98
  },
98
- onInterruptStateChange: (nextInterruptState) => {
99
+ onInterruptStateChange: (nextInterruptState, context) => {
99
100
  setInterruptState(nextInterruptState);
100
- options.onInterruptStateChange?.(nextInterruptState);
101
+ options.onInterruptStateChange?.(nextInterruptState, context);
101
102
  }
102
103
  });
103
104
  }, [clientId]);
@@ -1 +1 @@
1
- {"version":3,"file":"use-chat.js","names":[],"sources":["../src/use-chat.ts"],"sourcesContent":["import {\n createEffect,\n createMemo,\n createSignal,\n createUniqueId,\n onCleanup,\n onMount,\n} from 'solid-js'\n\nimport { ChatClient } from '@tanstack/ai-client'\nimport { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'\nimport type {\n ChatClientState,\n ChatInterrupt,\n ChatInterruptState,\n ChatResumeState,\n ConnectionStatus,\n InferredClientContext,\n QueuedMessage,\n SendMessageOptions,\n StructuredOutputPart,\n} from '@tanstack/ai-client'\nimport type {\n AnyClientTool,\n InferSchemaType,\n ModelMessage,\n RunAgentResumeItem,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai'\nimport type {\n DeepPartial,\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nconst EMPTY_INTERRUPTS = Object.freeze([])\nconst EMPTY_INTERRUPT_ERRORS = Object.freeze([])\n\nexport function useChat<\n const TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n TContext = InferredClientContext<TTools>,\n>(\n options: UseChatOptions<TTools, TSchema, TContext> = {} as UseChatOptions<\n TTools,\n TSchema,\n TContext\n >,\n): UseChatReturn<TTools, TSchema> {\n // The hook's identity is its `threadId`. Reload with the same `threadId`\n // restores the same conversation. `hookId` is only a recreation key when no\n // `threadId` is given. It is never sent on the wire.\n const hookId = createUniqueId()\n const clientId = options.threadId ?? hookId\n\n const [messages, setMessages] = createSignal<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = createSignal(false)\n const [error, setError] = createSignal<Error | undefined>(undefined)\n const [status, setStatus] = createSignal<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = createSignal(false)\n const [connectionStatus, setConnectionStatus] =\n createSignal<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = createSignal(false)\n const [queue, setQueue] = createSignal<Array<QueuedMessage>>([])\n const [runId, setRunId] = createSignal<string | null>(null)\n const [interruptState, setInterruptState] = createSignal<\n ChatInterruptState<TTools>\n >({\n interrupts: EMPTY_INTERRUPTS,\n pendingInterrupts: EMPTY_INTERRUPTS,\n interruptErrors: EMPTY_INTERRUPT_ERRORS,\n resuming: false,\n })\n\n const syncResumeState = () => {\n setRunId(client().getCurrentRunId())\n setInterruptState(client().getInterruptState())\n }\n\n // Structured-output `partial` / `final` are derived from `messages` —\n // specifically from the structured-output part on the latest assistant\n // message (the one after the most recent user message). Per-turn parts\n // keep history coherent without a separate reset signal.\n type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\n\n // Create ChatClient instance with callbacks to sync state.\n // Every user-provided callback is wrapped so the LATEST `options.xxx` value\n // is read at call time. Direct assignment would freeze the callback to the\n // reference we saw at creation; the wrapper lets reactive `options` or\n // in-place mutations propagate. When the user clears a callback (sets it to\n // undefined), `?.` no-ops.\n const client = createMemo(() => {\n // Build options with conditional spreads for fields whose source\n // type is `T | undefined` but the ChatClient target uses a strict\n // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects\n // assigning `undefined` to those, so we omit the key when absent.\n const transport = options.connection\n ? { connection: options.connection }\n : { fetcher: options.fetcher }\n return new ChatClient<TTools, TContext>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\n ...(options.initialMessages !== undefined && {\n initialMessages: options.initialMessages,\n }),\n ...(typeof options.threadId === 'string' && options.persistence\n ? {\n persistence: options.persistence,\n threadId: options.threadId,\n }\n : {\n ...(options.threadId !== undefined && {\n threadId: options.threadId,\n }),\n }),\n ...(options.initialResumeSnapshot !== undefined && {\n initialResumeSnapshot: options.initialResumeSnapshot,\n }),\n body: options.body,\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n ...(options.context !== undefined && { context: options.context }),\n devtools: {\n ...options.devtools,\n framework: 'solid',\n hookName: 'useChat',\n outputKind: options.outputSchema ? 'structured' : 'chat',\n },\n onResponse: (response) => options.onResponse?.(response),\n onChunk: (chunk: StreamChunk) => {\n options.onChunk?.(chunk)\n },\n onFinish: (message) => {\n options.onFinish?.(message)\n },\n onError: (err) => {\n options.onError?.(err)\n },\n tools: options.tools,\n onCustomEvent: (eventType, data, context) =>\n options.onCustomEvent?.(eventType, data, context),\n ...(options.streamProcessor !== undefined && {\n streamProcessor: options.streamProcessor,\n }),\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n setIsLoading(newIsLoading)\n syncResumeState()\n },\n onStatusChange: (newStatus: ChatClientState) => {\n setStatus(newStatus)\n },\n onErrorChange: (newError: Error | undefined) => {\n setError(newError)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n setSessionGenerating(isGenerating)\n },\n ...(options.queue !== undefined && { queue: options.queue }),\n onQueueChange: (nextQueue: Array<QueuedMessage>) => {\n setQueue(nextQueue)\n },\n onRunIdChange: (nextRunId) => {\n setRunId(nextRunId)\n },\n onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => {\n setInterruptState((current) => ({\n ...current,\n interrupts: nextPendingInterrupts,\n pendingInterrupts: nextPendingInterrupts,\n }))\n },\n onInterruptStateChange: (nextInterruptState) => {\n setInterruptState(nextInterruptState)\n options.onInterruptStateChange?.(nextInterruptState)\n },\n })\n // Only recreate when clientId changes\n // Connection and other options are captured at creation time\n }, [clientId])\n\n setMessages(client().getMessages())\n syncResumeState()\n\n // Sync body / forwardedProps changes to the client.\n // Both populate the same wire payload; `forwardedProps` is preferred\n // and `body` is deprecated but still supported.\n createEffect(() => {\n // Conditional spread: `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT.\n client().updateOptions({\n ...(options.body !== undefined && { body: options.body }),\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n context: options.context,\n ...(options.queue !== undefined && { queue: options.queue }),\n })\n })\n\n // Apply initial live mode immediately on hook creation.\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n\n createEffect(() => {\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n })\n\n onMount(() => {\n // START TAILING HERE, not in the constructor. A client is idle until a view\n // attaches it, so a client that gets built and thrown away never opens a\n // connection — an unreachable stream would hold one of the browser's ~6\n // connections per origin until the page reloaded.\n client().attach()\n client().mountDevtools()\n // Delivery-durability resume is transparent: the resumable SSE connection\n // adapter reattaches via the browser's native Last-Event-ID on reconnect.\n // We only seed interrupt (state) resume from the client here.\n syncResumeState()\n })\n\n // Cleanup on unmount: stop any in-flight requests.\n onCleanup(() => {\n // Release the connection first: `detach` is the counterpart of the `attach`\n // above, and it keeps the transcript and resume pointer so a later mount can\n // pick the run back up from the durable log.\n client().detach()\n if (options.live) {\n client().unsubscribe()\n } else {\n client().stop()\n }\n client().dispose()\n })\n\n // Callback options are read through `options.xxx` at call time, so reactive\n // or mutated options propagate without recreating the client.\n\n const sendMessage = async (\n content: string | MultimodalContent,\n sendOptions?: SendMessageOptions,\n ) => {\n try {\n await client().sendMessage(content, undefined, sendOptions)\n } finally {\n syncResumeState()\n }\n }\n\n const append = async (message: ModelMessage | UIMessage<TTools>) => {\n try {\n await client().append(message)\n } finally {\n syncResumeState()\n }\n }\n\n const reload = async () => {\n try {\n await client().reload()\n } finally {\n syncResumeState()\n }\n }\n\n const stop = () => {\n client().stop()\n }\n\n const clear = () => {\n client().clear()\n syncResumeState()\n }\n\n const setMessagesManually = (newMessages: Array<UIMessage<TTools>>) => {\n client().setMessagesManually(newMessages)\n }\n\n const addToolResult = async (result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }) => {\n await client().addToolResult(result)\n }\n\n const addToolApprovalResponse = async (response: {\n id: string\n approved: boolean\n }) => {\n await client().addToolApprovalResponse(response)\n syncResumeState()\n }\n\n const cancelQueued = (id: string) => client().cancelQueued(id)\n\n const resumeInterrupts = async (\n resumeItems: Array<RunAgentResumeItem>,\n state?: ChatResumeState,\n ) => {\n const result = await client().resumeInterrupts(resumeItems, state)\n syncResumeState()\n return result\n }\n\n const resolveInterrupts = (\n resolution: boolean | ((interrupt: ChatInterrupt<TTools>) => undefined),\n ) => {\n if (typeof resolution === 'boolean') {\n client().resolveInterrupts(resolution)\n } else {\n client().resolveInterrupts(resolution)\n }\n }\n\n const cancelInterrupts = () => {\n client().cancelInterrupts()\n }\n\n const retryInterrupts = () => {\n client().retryInterrupts()\n }\n\n const resumeInterruptsUnsafe = (\n resumeItems: Array<RunAgentResumeItem>,\n state?: ChatResumeState,\n ) => client().resumeInterruptsUnsafe(resumeItems, state)\n\n const interrupts = () => interruptState().interrupts\n const pendingInterrupts = () => interruptState().pendingInterrupts\n const interruptErrors = () => interruptState().interruptErrors\n const resuming = () => interruptState().resuming\n\n // The \"active\" structured-output part is on the assistant message after\n // the latest user message. When no user message exists yet, return null\n // rather than scanning history — otherwise a stale `final` from\n // `initialMessages` would leak into the value on first render.\n const activeStructuredPart = createMemo<StructuredOutputPart | null>(() => {\n const list = messages()\n let lastUserIndex = -1\n for (let i = list.length - 1; i >= 0; i--) {\n if (list[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1) return null\n for (let i = list.length - 1; i > lastUserIndex; i--) {\n const m = list[i]\n if (m?.role !== 'assistant') continue\n const part = m.parts.find(\n (p): p is StructuredOutputPart => p.type === 'structured-output',\n )\n if (part) return part\n }\n return null\n })\n\n const partial = createMemo<Partial>(() => {\n const part = activeStructuredPart()\n if (!part) return {} as Partial\n const v = part.partial ?? part.data\n return (v ?? {}) as Partial\n })\n\n const final = createMemo<Final | null>(() => {\n const part = activeStructuredPart()\n if (!part || part.status !== 'complete') return null\n return part.data as Final\n })\n\n // partial / final are runtime-tracked unconditionally; the conditional\n // return type hides them when no `outputSchema` is supplied.\n // oxlint-disable-next-line eslint-js/no-restricted-syntax -- primitive return shape diverges from generic UseChatReturn<TTools, TSchema>; TS can't structurally narrow the conditional partial/final fields\n return {\n messages,\n queue,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n status,\n isSubscribed,\n connectionStatus,\n sessionGenerating,\n setMessages: setMessagesManually,\n clear,\n addToolResult,\n addToolApprovalResponse,\n cancelQueued,\n runId,\n interrupts,\n pendingInterrupts,\n interruptErrors,\n resuming,\n resolveInterrupts,\n cancelInterrupts,\n retryInterrupts,\n resumeInterruptsUnsafe,\n resumeInterrupts,\n partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema>\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-chat.js","names":[],"sources":["../src/use-chat.ts"],"sourcesContent":["import {\n createEffect,\n createMemo,\n createSignal,\n createUniqueId,\n onCleanup,\n onMount,\n} from 'solid-js'\n\nimport { ChatClient } from '@tanstack/ai-client'\nimport { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'\nimport type {\n ChatClientState,\n ResolvableChatInterrupt,\n ChatInterruptState,\n ChatResumeState,\n ConnectionStatus,\n InferredClientContext,\n QueuedMessage,\n SendMessageOptions,\n StructuredOutputPart,\n} from '@tanstack/ai-client'\nimport type {\n AnyClientTool,\n InterruptDefinition,\n InferSchemaType,\n ModelMessage,\n RunAgentResumeItem,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai'\nimport type {\n DeepPartial,\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nconst EMPTY_INTERRUPTS = Object.freeze([])\nconst EMPTY_INTERRUPT_ERRORS = Object.freeze([])\n\nexport function useChat<\n const TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n TContext = InferredClientContext<TTools>,\n const TInterrupts extends ReadonlyArray<\n InterruptDefinition<any, any, any, any>\n > = readonly [],\n>(\n options: UseChatOptions<\n TTools,\n TSchema,\n TContext,\n TInterrupts\n > = {} as UseChatOptions<TTools, TSchema, TContext, TInterrupts>,\n): UseChatReturn<TTools, TSchema, TInterrupts> {\n // The hook's identity is its `threadId`. Reload with the same `threadId`\n // restores the same conversation. `hookId` is only a recreation key when no\n // `threadId` is given. It is never sent on the wire.\n const hookId = createUniqueId()\n const clientId = options.threadId ?? hookId\n\n const [messages, setMessages] = createSignal<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = createSignal(false)\n const [error, setError] = createSignal<Error | undefined>(undefined)\n const [status, setStatus] = createSignal<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = createSignal(false)\n const [connectionStatus, setConnectionStatus] =\n createSignal<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = createSignal(false)\n const [queue, setQueue] = createSignal<Array<QueuedMessage>>([])\n const [runId, setRunId] = createSignal<string | null>(null)\n const [interruptState, setInterruptState] = createSignal<\n ChatInterruptState<TTools, TInterrupts>\n >({\n interrupts: EMPTY_INTERRUPTS,\n pendingInterrupts: EMPTY_INTERRUPTS,\n interruptErrors: EMPTY_INTERRUPT_ERRORS,\n resuming: false,\n })\n\n const syncResumeState = () => {\n setRunId(client().getCurrentRunId())\n setInterruptState(client().getInterruptState())\n }\n\n // Structured-output `partial` / `final` are derived from `messages` —\n // specifically from the structured-output part on the latest assistant\n // message (the one after the most recent user message). Per-turn parts\n // keep history coherent without a separate reset signal.\n type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\n\n // Create ChatClient instance with callbacks to sync state.\n // Every user-provided callback is wrapped so the LATEST `options.xxx` value\n // is read at call time. Direct assignment would freeze the callback to the\n // reference we saw at creation; the wrapper lets reactive `options` or\n // in-place mutations propagate. When the user clears a callback (sets it to\n // undefined), `?.` no-ops.\n const client = createMemo(() => {\n // Build options with conditional spreads for fields whose source\n // type is `T | undefined` but the ChatClient target uses a strict\n // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects\n // assigning `undefined` to those, so we omit the key when absent.\n const transport = options.connection\n ? { connection: options.connection }\n : { fetcher: options.fetcher }\n return new ChatClient<TTools, TContext, TInterrupts>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\n ...(options.initialMessages !== undefined && {\n initialMessages: options.initialMessages,\n }),\n ...(typeof options.threadId === 'string' && options.persistence\n ? {\n persistence: options.persistence,\n threadId: options.threadId,\n }\n : {\n ...(options.threadId !== undefined && {\n threadId: options.threadId,\n }),\n }),\n ...(options.initialResumeSnapshot !== undefined && {\n initialResumeSnapshot: options.initialResumeSnapshot,\n }),\n body: options.body,\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n ...(options.context !== undefined && { context: options.context }),\n devtools: {\n ...options.devtools,\n framework: 'solid',\n hookName: 'useChat',\n outputKind: options.outputSchema ? 'structured' : 'chat',\n },\n onResponse: (response) => options.onResponse?.(response),\n onChunk: (chunk: StreamChunk) => {\n options.onChunk?.(chunk)\n },\n onFinish: (message) => {\n options.onFinish?.(message)\n },\n onError: (err) => {\n options.onError?.(err)\n },\n tools: options.tools,\n ...(options.interrupts !== undefined && {\n interrupts: options.interrupts,\n }),\n onCustomEvent: (eventType, data, context) =>\n options.onCustomEvent?.(eventType, data, context),\n ...(options.streamProcessor !== undefined && {\n streamProcessor: options.streamProcessor,\n }),\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n setIsLoading(newIsLoading)\n syncResumeState()\n },\n onStatusChange: (newStatus: ChatClientState) => {\n setStatus(newStatus)\n },\n onErrorChange: (newError: Error | undefined) => {\n setError(newError)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n setSessionGenerating(isGenerating)\n },\n ...(options.queue !== undefined && { queue: options.queue }),\n onQueueChange: (nextQueue: Array<QueuedMessage>) => {\n setQueue(nextQueue)\n },\n onRunIdChange: (nextRunId) => {\n setRunId(nextRunId)\n },\n onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => {\n setInterruptState((current) => ({\n ...current,\n interrupts: nextPendingInterrupts,\n pendingInterrupts: nextPendingInterrupts,\n }))\n },\n onInterruptStateChange: (nextInterruptState, context) => {\n setInterruptState(nextInterruptState)\n options.onInterruptStateChange?.(nextInterruptState, context)\n },\n })\n // Only recreate when clientId changes\n // Connection and other options are captured at creation time\n }, [clientId])\n\n setMessages(client().getMessages())\n syncResumeState()\n\n // Sync body / forwardedProps changes to the client.\n // Both populate the same wire payload; `forwardedProps` is preferred\n // and `body` is deprecated but still supported.\n createEffect(() => {\n // Conditional spread: `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT.\n client().updateOptions({\n ...(options.body !== undefined && { body: options.body }),\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n context: options.context,\n ...(options.queue !== undefined && { queue: options.queue }),\n })\n })\n\n // Apply initial live mode immediately on hook creation.\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n\n createEffect(() => {\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n })\n\n onMount(() => {\n // START TAILING HERE, not in the constructor. A client is idle until a view\n // attaches it, so a client that gets built and thrown away never opens a\n // connection — an unreachable stream would hold one of the browser's ~6\n // connections per origin until the page reloaded.\n client().attach()\n client().mountDevtools()\n // Delivery-durability resume is transparent: the resumable SSE connection\n // adapter reattaches via the browser's native Last-Event-ID on reconnect.\n // We only seed interrupt (state) resume from the client here.\n syncResumeState()\n })\n\n // Cleanup on unmount: stop any in-flight requests.\n onCleanup(() => {\n // Release the connection first: `detach` is the counterpart of the `attach`\n // above, and it keeps the transcript and resume pointer so a later mount can\n // pick the run back up from the durable log.\n client().detach()\n if (options.live) {\n client().unsubscribe()\n } else {\n client().stop()\n }\n client().dispose()\n })\n\n // Callback options are read through `options.xxx` at call time, so reactive\n // or mutated options propagate without recreating the client.\n\n const sendMessage = async (\n content: string | MultimodalContent,\n sendOptions?: SendMessageOptions,\n ) => {\n try {\n await client().sendMessage(content, undefined, sendOptions)\n } finally {\n syncResumeState()\n }\n }\n\n const append = async (message: ModelMessage | UIMessage<TTools>) => {\n try {\n await client().append(message)\n } finally {\n syncResumeState()\n }\n }\n\n const reload = async () => {\n try {\n await client().reload()\n } finally {\n syncResumeState()\n }\n }\n\n const stop = () => {\n client().stop()\n }\n\n const clear = () => {\n client().clear()\n syncResumeState()\n }\n\n const setMessagesManually = (newMessages: Array<UIMessage<TTools>>) => {\n client().setMessagesManually(newMessages)\n }\n\n const addToolResult = async (result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }) => {\n await client().addToolResult(result)\n }\n\n const addToolApprovalResponse = async (response: {\n id: string\n approved: boolean\n }) => {\n await client().addToolApprovalResponse(response)\n syncResumeState()\n }\n\n const cancelQueued = (id: string) => client().cancelQueued(id)\n\n const resumeInterrupts = async (\n resumeItems: Array<RunAgentResumeItem>,\n state?: ChatResumeState,\n ) => {\n const result = await client().resumeInterrupts(resumeItems, state)\n syncResumeState()\n return result\n }\n\n const resolveInterrupts = (\n resolution:\n | boolean\n | ((\n interrupt: ResolvableChatInterrupt<TTools, TInterrupts>,\n ) => undefined),\n ) => {\n if (typeof resolution === 'boolean') {\n client().resolveInterrupts(resolution)\n } else {\n client().resolveInterrupts(resolution)\n }\n }\n\n const cancelInterrupts = () => {\n client().cancelInterrupts()\n }\n\n const retryInterrupts = () => {\n client().retryInterrupts()\n }\n\n const resumeInterruptsUnsafe = (\n resumeItems: Array<RunAgentResumeItem>,\n state?: ChatResumeState,\n ) => client().resumeInterruptsUnsafe(resumeItems, state)\n\n const interrupts = () => interruptState().interrupts\n const pendingInterrupts = () => interruptState().pendingInterrupts\n const interruptErrors = () => interruptState().interruptErrors\n const resuming = () => interruptState().resuming\n\n // The \"active\" structured-output part is on the assistant message after\n // the latest user message. When no user message exists yet, return null\n // rather than scanning history — otherwise a stale `final` from\n // `initialMessages` would leak into the value on first render.\n const activeStructuredPart = createMemo<StructuredOutputPart | null>(() => {\n const list = messages()\n let lastUserIndex = -1\n for (let i = list.length - 1; i >= 0; i--) {\n if (list[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1) return null\n for (let i = list.length - 1; i > lastUserIndex; i--) {\n const m = list[i]\n if (m?.role !== 'assistant') continue\n const part = m.parts.find(\n (p): p is StructuredOutputPart => p.type === 'structured-output',\n )\n if (part) return part\n }\n return null\n })\n\n const partial = createMemo<Partial>(() => {\n const part = activeStructuredPart()\n if (!part) return {} as Partial\n const v = part.partial ?? part.data\n return (v ?? {}) as Partial\n })\n\n const final = createMemo<Final | null>(() => {\n const part = activeStructuredPart()\n if (!part || part.status !== 'complete') return null\n return part.data as Final\n })\n\n // partial / final are runtime-tracked unconditionally; the conditional\n // return type hides them when no `outputSchema` is supplied.\n // oxlint-disable-next-line eslint-js/no-restricted-syntax -- primitive return shape diverges from generic UseChatReturn<TTools, TSchema>; TS can't structurally narrow the conditional partial/final fields\n return {\n messages,\n queue,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n status,\n isSubscribed,\n connectionStatus,\n sessionGenerating,\n setMessages: setMessagesManually,\n clear,\n addToolResult,\n addToolApprovalResponse,\n cancelQueued,\n runId,\n interrupts,\n pendingInterrupts,\n interruptErrors,\n resuming,\n resolveInterrupts,\n cancelInterrupts,\n retryInterrupts,\n resumeInterruptsUnsafe,\n resumeInterrupts,\n partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema, TInterrupts>\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-solid",
3
- "version": "0.17.0",
3
+ "version": "0.18.2",
4
4
  "description": "Solid hooks for TanStack AI streaming chat, structured outputs, and media generation.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -45,11 +45,11 @@
45
45
  "media-generation"
46
46
  ],
47
47
  "dependencies": {
48
- "@tanstack/ai-client": "^0.24.0"
48
+ "@tanstack/ai-client": "^0.25.2"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "solid-js": ">=1.9.10",
52
- "@tanstack/ai": "^0.46.0"
52
+ "@tanstack/ai": "^0.47.3"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@solidjs/testing-library": "^0.8.10",
@@ -61,7 +61,7 @@
61
61
  "tsdown": "^0.17.0-beta.6",
62
62
  "typescript": "5.9.3",
63
63
  "vitest": "^4.1.10",
64
- "@tanstack/ai": "0.46.0"
64
+ "@tanstack/ai": "0.47.3"
65
65
  },
66
66
  "scripts": {
67
67
  "clean": "premove ./build ./dist",
package/src/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  AnyClientTool,
3
+ InterruptDefinition,
3
4
  InferSchemaType,
4
5
  ModelMessage,
5
6
  RunAgentResumeItem,
@@ -10,7 +11,7 @@ import type {
10
11
  BoundInterrupts,
11
12
  ChatClientOptions,
12
13
  ChatClientState,
13
- ChatInterrupt,
14
+ ResolvableChatInterrupt,
14
15
  ChatInterruptState,
15
16
  ChatRequestBody,
16
17
  ChatResumeState,
@@ -79,8 +80,10 @@ export type UseChatOptions<
79
80
  TTools extends ReadonlyArray<AnyClientTool> = any,
80
81
  TSchema extends SchemaInput | undefined = undefined,
81
82
  TContext = InferredClientContext<TTools>,
83
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =
84
+ readonly [],
82
85
  > = DistributedOmit<
83
- ChatClientOptions<TTools, TContext>,
86
+ ChatClientOptions<TTools, TContext, TInterrupts>,
84
87
  | 'onMessagesChange'
85
88
  | 'onLoadingChange'
86
89
  | 'onErrorChange'
@@ -111,9 +114,12 @@ export type UseChatOptions<
111
114
  export type UseChatReturn<
112
115
  TTools extends ReadonlyArray<AnyClientTool> = any,
113
116
  TSchema extends SchemaInput | undefined = undefined,
117
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =
118
+ readonly [],
114
119
  > = BaseUseChatReturn<
115
120
  TTools,
116
- TSchema extends SchemaInput ? InferSchemaType<TSchema> : unknown
121
+ TSchema extends SchemaInput ? InferSchemaType<TSchema> : unknown,
122
+ TInterrupts
117
123
  > &
118
124
  (TSchema extends SchemaInput
119
125
  ? {
@@ -133,6 +139,8 @@ export type UseChatReturn<
133
139
  interface BaseUseChatReturn<
134
140
  TTools extends ReadonlyArray<AnyClientTool> = any,
135
141
  TData = unknown,
142
+ TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =
143
+ readonly [],
136
144
  > {
137
145
  /**
138
146
  * Current messages in the conversation. When `outputSchema` is supplied,
@@ -200,14 +208,20 @@ interface BaseUseChatReturn<
200
208
  * it, correlate a log line).
201
209
  */
202
210
  runId: Accessor<string | null>
203
- interrupts: Accessor<BoundInterrupts<TTools>>
211
+ interrupts: Accessor<BoundInterrupts<TTools, TInterrupts>>
204
212
  /** @deprecated Use `interrupts`. */
205
- pendingInterrupts: Accessor<BoundInterrupts<TTools>>
206
- interruptErrors: Accessor<ChatInterruptState<TTools>['interruptErrors']>
213
+ pendingInterrupts: Accessor<BoundInterrupts<TTools, TInterrupts>>
214
+ interruptErrors: Accessor<
215
+ ChatInterruptState<TTools, TInterrupts>['interruptErrors']
216
+ >
207
217
  resuming: Accessor<boolean>
208
218
  resolveInterrupts: {
209
219
  (approved: boolean): void
210
- (resolver: (interrupt: ChatInterrupt<TTools>) => undefined): void
220
+ (
221
+ resolver: (
222
+ interrupt: ResolvableChatInterrupt<TTools, TInterrupts>,
223
+ ) => undefined,
224
+ ): void
211
225
  }
212
226
  cancelInterrupts: () => void
213
227
  retryInterrupts: () => void
@@ -37,10 +37,16 @@ export interface UseAudioRecorderReturn<TOutput> {
37
37
  * failure (and `stop()` rejects with `Recording cancelled` if `cancel()` runs
38
38
  * while a stop is in flight, e.g. on unmount) — handle one channel, not both.
39
39
  */
40
+ // The transforming overload requires `onComplete`. Without that constraint an
41
+ // options object carrying only unrelated keys (`useAudioRecorder({ onError })`)
42
+ // still matches it, `TOnComplete` infers as `unknown`, and `recording`/`stop()`
43
+ // collapse to `unknown` — so passing any option would silently cost you the
44
+ // `AudioRecording` type. Requiring it here sends those calls to the second
45
+ // overload instead (issue #1001).
40
46
  export function useAudioRecorder<
41
47
  TOnComplete extends (recording: AudioRecording) => unknown,
42
48
  >(
43
- options: UseAudioRecorderOptions<TOnComplete>,
49
+ options: UseAudioRecorderOptions<TOnComplete> & { onComplete: TOnComplete },
44
50
  ): UseAudioRecorderReturn<InferAudioRecordingOutput<TOnComplete>>
45
51
  export function useAudioRecorder(
46
52
  options?: UseAudioRecorderOptions<undefined>,
package/src/use-chat.ts CHANGED
@@ -11,7 +11,7 @@ import { ChatClient } from '@tanstack/ai-client'
11
11
  import { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'
12
12
  import type {
13
13
  ChatClientState,
14
- ChatInterrupt,
14
+ ResolvableChatInterrupt,
15
15
  ChatInterruptState,
16
16
  ChatResumeState,
17
17
  ConnectionStatus,
@@ -22,6 +22,7 @@ import type {
22
22
  } from '@tanstack/ai-client'
23
23
  import type {
24
24
  AnyClientTool,
25
+ InterruptDefinition,
25
26
  InferSchemaType,
26
27
  ModelMessage,
27
28
  RunAgentResumeItem,
@@ -43,13 +44,17 @@ export function useChat<
43
44
  const TTools extends ReadonlyArray<AnyClientTool> = any,
44
45
  TSchema extends SchemaInput | undefined = undefined,
45
46
  TContext = InferredClientContext<TTools>,
47
+ const TInterrupts extends ReadonlyArray<
48
+ InterruptDefinition<any, any, any, any>
49
+ > = readonly [],
46
50
  >(
47
- options: UseChatOptions<TTools, TSchema, TContext> = {} as UseChatOptions<
51
+ options: UseChatOptions<
48
52
  TTools,
49
53
  TSchema,
50
- TContext
51
- >,
52
- ): UseChatReturn<TTools, TSchema> {
54
+ TContext,
55
+ TInterrupts
56
+ > = {} as UseChatOptions<TTools, TSchema, TContext, TInterrupts>,
57
+ ): UseChatReturn<TTools, TSchema, TInterrupts> {
53
58
  // The hook's identity is its `threadId`. Reload with the same `threadId`
54
59
  // restores the same conversation. `hookId` is only a recreation key when no
55
60
  // `threadId` is given. It is never sent on the wire.
@@ -69,7 +74,7 @@ export function useChat<
69
74
  const [queue, setQueue] = createSignal<Array<QueuedMessage>>([])
70
75
  const [runId, setRunId] = createSignal<string | null>(null)
71
76
  const [interruptState, setInterruptState] = createSignal<
72
- ChatInterruptState<TTools>
77
+ ChatInterruptState<TTools, TInterrupts>
73
78
  >({
74
79
  interrupts: EMPTY_INTERRUPTS,
75
80
  pendingInterrupts: EMPTY_INTERRUPTS,
@@ -103,7 +108,7 @@ export function useChat<
103
108
  const transport = options.connection
104
109
  ? { connection: options.connection }
105
110
  : { fetcher: options.fetcher }
106
- return new ChatClient<TTools, TContext>({
111
+ return new ChatClient<TTools, TContext, TInterrupts>({
107
112
  devtoolsBridgeFactory: createChatDevtoolsBridge,
108
113
  ...transport,
109
114
  ...(options.initialMessages !== undefined && {
@@ -144,6 +149,9 @@ export function useChat<
144
149
  options.onError?.(err)
145
150
  },
146
151
  tools: options.tools,
152
+ ...(options.interrupts !== undefined && {
153
+ interrupts: options.interrupts,
154
+ }),
147
155
  onCustomEvent: (eventType, data, context) =>
148
156
  options.onCustomEvent?.(eventType, data, context),
149
157
  ...(options.streamProcessor !== undefined && {
@@ -185,9 +193,9 @@ export function useChat<
185
193
  pendingInterrupts: nextPendingInterrupts,
186
194
  }))
187
195
  },
188
- onInterruptStateChange: (nextInterruptState) => {
196
+ onInterruptStateChange: (nextInterruptState, context) => {
189
197
  setInterruptState(nextInterruptState)
190
- options.onInterruptStateChange?.(nextInterruptState)
198
+ options.onInterruptStateChange?.(nextInterruptState, context)
191
199
  },
192
200
  })
193
201
  // Only recreate when clientId changes
@@ -328,7 +336,11 @@ export function useChat<
328
336
  }
329
337
 
330
338
  const resolveInterrupts = (
331
- resolution: boolean | ((interrupt: ChatInterrupt<TTools>) => undefined),
339
+ resolution:
340
+ | boolean
341
+ | ((
342
+ interrupt: ResolvableChatInterrupt<TTools, TInterrupts>,
343
+ ) => undefined),
332
344
  ) => {
333
345
  if (typeof resolution === 'boolean') {
334
346
  client().resolveInterrupts(resolution)
@@ -426,5 +438,5 @@ export function useChat<
426
438
  resumeInterrupts,
427
439
  partial,
428
440
  final,
429
- } as unknown as UseChatReturn<TTools, TSchema>
441
+ } as unknown as UseChatReturn<TTools, TSchema, TInterrupts>
430
442
  }