@assistant-ui/react 0.15.7 → 0.15.9

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 (25) hide show
  1. package/LICENSE +21 -0
  2. package/dist/client/ExternalThread.d.ts +3 -1
  3. package/dist/client/ExternalThread.d.ts.map +1 -1
  4. package/dist/client/ExternalThread.js +524 -335
  5. package/dist/client/ExternalThread.js.map +1 -1
  6. package/dist/legacy-runtime/cloud/auiV0.d.ts +1 -0
  7. package/dist/legacy-runtime/cloud/auiV0.d.ts.map +1 -1
  8. package/dist/legacy-runtime/cloud/auiV0.js +2 -1
  9. package/dist/legacy-runtime/cloud/auiV0.js.map +1 -1
  10. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  11. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +2 -0
  12. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  13. package/dist/unstable/useLiveCompletionAdapter.js +32 -7
  14. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  15. package/dist/utils/useToolArgsFieldStatus.d.ts +2 -2
  16. package/package.json +13 -15
  17. package/src/client/ExternalThread.ts +172 -9
  18. package/src/legacy-runtime/cloud/auiV0.ts +8 -1
  19. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransport.spec.md +1 -0
  20. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx +79 -0
  21. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +2 -0
  22. package/src/tests/external-thread-feedback.test.tsx +204 -0
  23. package/src/tests/external-thread-speech.test.tsx +328 -0
  24. package/src/unstable/useLiveCompletionAdapter.test.tsx +90 -0
  25. package/src/unstable/useLiveCompletionAdapter.ts +32 -7
@@ -1 +1 @@
1
- {"version":3,"file":"useAssistantTransportRuntime.js","names":["c","_c","AppendMessage","ReadonlyJSONObject","ReadonlyJSONValue","asAsyncIterableStream","useExternalStoreRuntime","AssistantRuntime","AddToolResultOptions","useMemo","useRef","useState","AssistantMessageAccumulator","DataStreamDecoder","AssistantTransportDecoder","unstable_createInitialMessage","createInitialMessage","toToolsJSONSchema","AssistantTransportOptions","AddMessageCommand","AddToolResultCommand","UserMessagePart","QueuedCommand","AssistantTransportCommand","SendCommandsRequestBody","useCommandQueue","createReplayBoundaryStream","useReplayRenderWait","useRunManager","useConvertedState","ToolExecutionStatus","createRequestHeaders","useRemoteThreadListRuntime","InMemoryThreadListAdapter","useAui","useAuiState","UserExternalState","convertAppendMessageToCommand","message","role","Error","parts","content","attachments","flatMap","a","contentPart","type","push","text","image","length","parentId","sourceId","readResumeState","response","Response","Promise","runId","state","T","status","ok","value","json","symbolAssistantTransportExtras","Symbol","AssistantTransportExtras","sendCommand","command","asAssistantTransportExtras","extras","useAssistantTransportSendCommand","$","aui","t0","thread","getState","transportExtras","useAssistantTransportState","selector","t","s","useAssistantTransportThreadRuntime","options","agentStateRef","initialState","rerender","resumeFlagRef","isReplaying","setIsReplaying","waitForReplayRender","parentIdRef","undefined","commandQueue","onQueue","runManager","schedule","enqueueAppendMessage","console","warn","current","enqueue","startRun","threadId","threadListItem","remoteId","onRun","signal","AbortSignal","isResume","commands","flush","headers","resumeState","resumeStateApi","resumeStateResponse","fetch","method","body","JSON","stringify","retained","queued","bodyValue","context","runtime","getModelContext","requestBody","Record","system","tools","callSettings","config","prepareSendCommandsRequest","resumeApi","api","onResponse","prev","setReplaying","waitForRender","protocol","strict","decoder","err","stream","pipeThrough","initialMessage","unstable_state","throttle","onError","error","markedDelivered","chunk","metadata","markDelivered","onFinish","onCancel","cmds","inTransit","reset","updateState","updater","inTransitCmds","queuedCmds","toolStatuses","setToolStatuses","pendingCommands","converted","converter","isRunning","messages","isLoading","adapters","unstable_enableToolInvocations","onNew","capabilities","edit","onEdit","onReload","cancel","onResume","onAddToolResult","toolOptions","toolCallId","result","toolName","isError","artifact","modelContent","onLoadExternalState","useAssistantTransportRuntime","runtimeHook","RuntimeHook","adapter","allowNesting"],"sources":["../../../../src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport type { AppendMessage } from \"@assistant-ui/core\";\nimport {\n type ReadonlyJSONObject,\n type ReadonlyJSONValue,\n asAsyncIterableStream,\n} from \"assistant-stream/utils\";\nimport { useExternalStoreRuntime } from \"../external-store/useExternalStoreRuntime\";\nimport type { AssistantRuntime } from \"../../runtime/AssistantRuntime\";\nimport type { AddToolResultOptions } from \"@assistant-ui/core\";\nimport { useMemo, useRef, useState } from \"react\";\nimport {\n AssistantMessageAccumulator,\n DataStreamDecoder,\n AssistantTransportDecoder,\n unstable_createInitialMessage as createInitialMessage,\n toToolsJSONSchema,\n} from \"assistant-stream\";\nimport type {\n AssistantTransportOptions,\n AddMessageCommand,\n AddToolResultCommand,\n UserMessagePart,\n QueuedCommand,\n AssistantTransportCommand,\n SendCommandsRequestBody,\n} from \"./types\";\nimport { useCommandQueue } from \"./commandQueue\";\nimport {\n createReplayBoundaryStream,\n useReplayRenderWait,\n} from \"./replayBoundaryStream\";\nimport { useRunManager } from \"./runManager\";\nimport { useConvertedState } from \"./useConvertedState\";\nimport type { ToolExecutionStatus } from \"@assistant-ui/core\";\nimport { createRequestHeaders } from \"@assistant-ui/core\";\nimport { useRemoteThreadListRuntime } from \"../remote-thread-list/useRemoteThreadListRuntime\";\nimport { InMemoryThreadListAdapter } from \"@assistant-ui/core\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport type { UserExternalState } from \"../../../augmentations\";\n\nconst convertAppendMessageToCommand = (\n message: AppendMessage,\n): AddMessageCommand | null => {\n if (message.role !== \"user\")\n throw new Error(\"Only user messages are supported\");\n\n const parts: UserMessagePart[] = [];\n const content = [\n ...message.content,\n ...(message.attachments?.flatMap((a) => a.content) ?? []),\n ];\n for (const contentPart of content) {\n if (contentPart.type === \"text\") {\n parts.push({ type: \"text\", text: contentPart.text });\n } else if (contentPart.type === \"image\") {\n parts.push({ type: \"image\", image: contentPart.image });\n }\n }\n\n if (parts.length === 0) return null;\n\n return {\n type: \"add-message\",\n message: {\n role: \"user\",\n parts,\n },\n parentId: message.parentId,\n sourceId: message.sourceId,\n };\n};\n\nconst readResumeState = async <T>(\n response: Response,\n): Promise<{ runId: string; state: T } | null> => {\n if (response.status === 204) return null;\n if (!response.ok) {\n throw new Error(\n `Resume state request failed with status ${response.status}: ${await response.text()}`,\n );\n }\n\n let value: unknown;\n try {\n value = await response.json();\n } catch {\n throw new Error(\"Resume state response was not valid JSON\");\n }\n\n if (\n typeof value !== \"object\" ||\n value === null ||\n !(\"state\" in value) ||\n !(\"runId\" in value) ||\n typeof value.runId !== \"string\"\n ) {\n throw new Error(\"Resume state response must contain state and runId\");\n }\n\n return { runId: value.runId, state: value.state as T };\n};\n\nconst symbolAssistantTransportExtras = Symbol(\"assistant-transport-extras\");\ntype AssistantTransportExtras = {\n [symbolAssistantTransportExtras]: true;\n sendCommand: (command: AssistantTransportCommand) => void;\n state: UserExternalState;\n};\n\nconst asAssistantTransportExtras = (\n extras: unknown,\n): AssistantTransportExtras => {\n if (\n typeof extras !== \"object\" ||\n extras == null ||\n !(symbolAssistantTransportExtras in extras)\n )\n throw new Error(\n \"This method can only be called when you are using useAssistantTransportRuntime\",\n );\n\n return extras as AssistantTransportExtras;\n};\n\nexport const useAssistantTransportSendCommand = () => {\n const aui = useAui();\n\n return (command: AssistantTransportCommand) => {\n const extras = aui.thread.getState().extras;\n const transportExtras = asAssistantTransportExtras(extras);\n transportExtras.sendCommand(command);\n };\n};\n\nexport function useAssistantTransportState(): UserExternalState;\nexport function useAssistantTransportState<T>(\n selector: (state: UserExternalState) => T,\n): T;\nexport function useAssistantTransportState<T>(\n selector: (state: UserExternalState) => T = (t) => t as T,\n): T | UserExternalState {\n return useAuiState((s) =>\n selector(asAssistantTransportExtras(s.thread.extras).state),\n );\n}\n\nconst useAssistantTransportThreadRuntime = <T>(\n options: AssistantTransportOptions<T>,\n): AssistantRuntime => {\n const agentStateRef = useRef(options.initialState);\n const [, rerender] = useState(0);\n const resumeFlagRef = useRef(false);\n const [isReplaying, setIsReplaying] = useState(false);\n const waitForReplayRender = useReplayRenderWait();\n const parentIdRef = useRef<string | null | undefined>(undefined);\n const commandQueue = useCommandQueue({\n onQueue: () => runManager.schedule(),\n });\n\n const enqueueAppendMessage = (message: AppendMessage) => {\n const command = convertAppendMessageToCommand(message);\n if (!command) {\n console.warn(\n \"[assistant-ui] Skipped add-message command with no supported parts\",\n );\n return;\n }\n parentIdRef.current = message.parentId;\n commandQueue.enqueue(command, {\n schedule: message.startRun ?? message.role === \"user\",\n });\n };\n\n const threadId = useAuiState((s) => s.threadListItem.remoteId);\n\n const runManager = useRunManager({\n onRun: async (signal: AbortSignal) => {\n const isResume = resumeFlagRef.current;\n resumeFlagRef.current = false;\n setIsReplaying(false);\n const commands: QueuedCommand[] = isResume ? [] : commandQueue.flush();\n if (commands.length === 0 && !isResume) return;\n\n // The flushed batch consumes the parentId; read it alongside the flush\n // (before any awaits) so a mid-run append keeps its own value. Resume\n // runs send no commands, so they neither send nor consume it.\n const parentId = isResume ? undefined : parentIdRef.current;\n if (!isResume) parentIdRef.current = undefined;\n\n const headers = await createRequestHeaders(options.headers);\n let resumeState: { runId: string; state: T } | undefined;\n if (isResume && options.resumeStateApi) {\n const resumeStateResponse = await fetch(options.resumeStateApi, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ threadId }),\n signal,\n });\n const retained = await readResumeState<T>(resumeStateResponse);\n if (retained === null) {\n if (commandQueue.state.queued.length > 0) {\n runManager.schedule();\n }\n return;\n }\n resumeState = retained;\n }\n\n const bodyValue =\n typeof options.body === \"function\"\n ? await options.body()\n : options.body;\n const context = runtime.thread.getModelContext();\n\n let requestBody: Record<string, unknown> = {\n commands,\n ...(resumeState === undefined && { state: agentStateRef.current }),\n system: context.system,\n tools: context.tools ? toToolsJSONSchema(context.tools) : undefined,\n threadId,\n ...(parentId !== undefined && {\n parentId,\n }),\n // nested (new format, aligned with AssistantChatTransport)\n callSettings: context.callSettings,\n config: context.config,\n // @deprecated spread at top level — use nested `callSettings`/`config` instead. Will be removed in a future version.\n ...context.callSettings,\n ...context.config,\n ...(bodyValue ?? {}),\n };\n\n if (options.prepareSendCommandsRequest) {\n requestBody = await options.prepareSendCommandsRequest(\n requestBody as SendCommandsRequestBody,\n );\n }\n\n if (resumeState !== undefined) {\n // The server replays a resume from the snapshot it retained for this\n // runId. Body overrides and prepare hooks can neither substitute a\n // state nor drop the ID the server validates against.\n requestBody = { ...requestBody, runId: resumeState.runId };\n delete requestBody[\"state\"];\n }\n\n const response = await fetch(\n isResume ? options.resumeApi! : options.api,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(requestBody),\n signal,\n },\n );\n\n options.onResponse?.(response);\n\n if (!response.ok) {\n throw new Error(`Status ${response.status}: ${await response.text()}`);\n }\n\n if (!response.body) {\n throw new Error(\"Response body is null\");\n }\n\n if (resumeState !== undefined) {\n agentStateRef.current = resumeState.state;\n rerender((prev) => prev + 1);\n }\n\n const body = await createReplayBoundaryStream(response, {\n setReplaying: setIsReplaying,\n waitForRender: waitForReplayRender,\n });\n\n // Select decoder based on protocol option\n const protocol = options.protocol ?? \"data-stream\";\n // Resume replays a best-effort buffer; always reconcile leniently.\n const strict = isResume ? false : (options.strict ?? true);\n const decoder =\n protocol === \"assistant-transport\"\n ? new AssistantTransportDecoder({ strict })\n : new DataStreamDecoder({ strict });\n\n let err: string | undefined;\n const stream = body.pipeThrough(decoder).pipeThrough(\n new AssistantMessageAccumulator({\n initialMessage: createInitialMessage({\n unstable_state:\n (agentStateRef.current as ReadonlyJSONValue) ?? null,\n }),\n throttle: isResume,\n strict,\n onError: (error) => {\n err = error;\n },\n }),\n );\n\n let markedDelivered = false;\n\n for await (const chunk of asAsyncIterableStream(stream)) {\n if (chunk.metadata.unstable_state === agentStateRef.current) continue;\n\n if (!markedDelivered) {\n commandQueue.markDelivered();\n markedDelivered = true;\n }\n\n agentStateRef.current = chunk.metadata.unstable_state as T;\n rerender((prev) => prev + 1);\n }\n\n if (err) {\n throw new Error(err);\n }\n\n // A successful run confirms delivery even when no state-changing\n // chunk was observed.\n if (!markedDelivered) {\n commandQueue.markDelivered();\n }\n\n // commands that coalesced into this resume run must not starve\n if (isResume && commandQueue.state.queued.length > 0) {\n runManager.schedule();\n }\n },\n onFinish: options.onFinish,\n onCancel: () => {\n setIsReplaying(false);\n const cmds = [\n ...commandQueue.state.inTransit,\n ...commandQueue.state.queued,\n ];\n\n commandQueue.reset();\n\n options.onCancel?.({\n commands: cmds,\n updateState: (updater) => {\n agentStateRef.current = updater(agentStateRef.current);\n rerender((prev) => prev + 1);\n },\n });\n },\n onError: async (error) => {\n setIsReplaying(false);\n const inTransitCmds = [...commandQueue.state.inTransit];\n const queuedCmds = [...commandQueue.state.queued];\n\n commandQueue.reset();\n\n try {\n await options.onError?.(error as Error, {\n commands: inTransitCmds,\n updateState: (updater) => {\n agentStateRef.current = updater(agentStateRef.current);\n rerender((prev) => prev + 1);\n },\n });\n } finally {\n options.onCancel?.({\n commands: queuedCmds,\n updateState: (updater) => {\n agentStateRef.current = updater(agentStateRef.current);\n rerender((prev) => prev + 1);\n },\n error: error as Error,\n });\n }\n },\n });\n\n // Tool execution status state\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n\n // Reactive conversion of agent state + connection metadata → UI state\n const pendingCommands = useMemo(\n () => [...commandQueue.state.inTransit, ...commandQueue.state.queued],\n [commandQueue.state],\n );\n const converted = useConvertedState(\n options.converter,\n agentStateRef.current,\n pendingCommands,\n runManager.isRunning,\n toolStatuses,\n );\n\n // Create runtime\n const runtime = useExternalStoreRuntime({\n messages: converted.messages,\n state: converted.state,\n isRunning: converted.isRunning,\n isLoading: isReplaying,\n adapters: options.adapters,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n extras: {\n [symbolAssistantTransportExtras]: true,\n sendCommand: (command: AssistantTransportCommand) => {\n commandQueue.enqueue(command);\n },\n state: agentStateRef.current as UserExternalState,\n } satisfies AssistantTransportExtras,\n onNew: async (message: AppendMessage): Promise<void> =>\n enqueueAppendMessage(message),\n ...(options.capabilities?.edit && {\n onEdit: async (message: AppendMessage): Promise<void> =>\n enqueueAppendMessage(message),\n }),\n ...(commandQueue.state.queued.length > 0 && {\n onReload: async (parentId: string | null) => {\n parentIdRef.current = parentId;\n runManager.schedule();\n },\n }),\n onCancel: async () => {\n runManager.cancel();\n },\n onResume: async () => {\n if (!options.resumeApi)\n throw new Error(\"Must pass resumeApi to options to resume runs\");\n\n resumeFlagRef.current = true;\n runManager.schedule();\n },\n onAddToolResult: async (\n toolOptions: AddToolResultOptions,\n ): Promise<void> => {\n const command: AddToolResultCommand = {\n type: \"add-tool-result\",\n toolCallId: toolOptions.toolCallId,\n result: toolOptions.result as ReadonlyJSONObject,\n toolName: toolOptions.toolName,\n isError: toolOptions.isError,\n ...(toolOptions.artifact && { artifact: toolOptions.artifact }),\n ...(toolOptions.modelContent !== undefined && {\n modelContent: toolOptions.modelContent,\n }),\n };\n\n commandQueue.enqueue(command);\n },\n onLoadExternalState: async (state) => {\n agentStateRef.current = state as T;\n rerender((prev) => prev + 1);\n },\n });\n\n return runtime;\n};\n\n/**\n * @alpha This is an experimental API that is subject to change.\n */\nexport const useAssistantTransportRuntime = <T>(\n options: AssistantTransportOptions<T>,\n): AssistantRuntime => {\n const runtime = useRemoteThreadListRuntime({\n runtimeHook: function RuntimeHook() {\n return useAssistantTransportThreadRuntime(options);\n },\n adapter: new InMemoryThreadListAdapter(),\n allowNesting: true,\n });\n return runtime;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useAssistantTransportRuntime.js","names":["c","_c","AppendMessage","ReadonlyJSONObject","ReadonlyJSONValue","asAsyncIterableStream","useExternalStoreRuntime","AssistantRuntime","AddToolResultOptions","useMemo","useRef","useState","AssistantMessageAccumulator","DataStreamDecoder","AssistantTransportDecoder","unstable_createInitialMessage","createInitialMessage","toToolsJSONSchema","AssistantTransportOptions","AddMessageCommand","AddToolResultCommand","UserMessagePart","QueuedCommand","AssistantTransportCommand","SendCommandsRequestBody","useCommandQueue","createReplayBoundaryStream","useReplayRenderWait","useRunManager","useConvertedState","ToolExecutionStatus","createRequestHeaders","useRemoteThreadListRuntime","InMemoryThreadListAdapter","useAui","useAuiState","UserExternalState","convertAppendMessageToCommand","message","role","Error","parts","content","attachments","flatMap","a","contentPart","type","push","text","image","length","parentId","sourceId","readResumeState","response","Response","Promise","runId","state","T","status","ok","value","json","symbolAssistantTransportExtras","Symbol","AssistantTransportExtras","sendCommand","command","asAssistantTransportExtras","extras","useAssistantTransportSendCommand","$","aui","t0","thread","getState","transportExtras","useAssistantTransportState","selector","t","s","useAssistantTransportThreadRuntime","options","agentStateRef","initialState","rerender","resumeFlagRef","isReplaying","setIsReplaying","waitForReplayRender","parentIdRef","undefined","commandQueue","onQueue","runManager","schedule","enqueueAppendMessage","console","warn","current","enqueue","startRun","threadId","threadListItem","remoteId","onRun","signal","AbortSignal","isResume","commands","flush","headers","resumeState","resumeStateApi","resumeStateResponse","fetch","method","body","JSON","stringify","retained","queued","bodyValue","context","runtime","getModelContext","requestBody","Record","system","tools","callSettings","config","prepareSendCommandsRequest","resumeApi","api","onResponse","prev","setReplaying","waitForRender","protocol","strict","decoder","err","stream","pipeThrough","initialMessage","unstable_state","throttle","onError","error","markedDelivered","chunk","metadata","markDelivered","onFinish","onCancel","cmds","inTransit","reset","updateState","updater","inTransitCmds","queuedCmds","toolStatuses","setToolStatuses","pendingCommands","converted","converter","isRunning","messages","isLoading","adapters","unstable_enableToolInvocations","onNew","capabilities","edit","onEdit","onReload","cancel","onResume","onAddToolResult","toolOptions","toolCallId","result","toolName","isError","artifact","modelContent","onLoadExternalState","useAssistantTransportRuntime","runtimeHook","RuntimeHook","adapter","allowNesting"],"sources":["../../../../src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport type { AppendMessage } from \"@assistant-ui/core\";\nimport {\n type ReadonlyJSONObject,\n type ReadonlyJSONValue,\n asAsyncIterableStream,\n} from \"assistant-stream/utils\";\nimport { useExternalStoreRuntime } from \"../external-store/useExternalStoreRuntime\";\nimport type { AssistantRuntime } from \"../../runtime/AssistantRuntime\";\nimport type { AddToolResultOptions } from \"@assistant-ui/core\";\nimport { useMemo, useRef, useState } from \"react\";\nimport {\n AssistantMessageAccumulator,\n DataStreamDecoder,\n AssistantTransportDecoder,\n unstable_createInitialMessage as createInitialMessage,\n toToolsJSONSchema,\n} from \"assistant-stream\";\nimport type {\n AssistantTransportOptions,\n AddMessageCommand,\n AddToolResultCommand,\n UserMessagePart,\n QueuedCommand,\n AssistantTransportCommand,\n SendCommandsRequestBody,\n} from \"./types\";\nimport { useCommandQueue } from \"./commandQueue\";\nimport {\n createReplayBoundaryStream,\n useReplayRenderWait,\n} from \"./replayBoundaryStream\";\nimport { useRunManager } from \"./runManager\";\nimport { useConvertedState } from \"./useConvertedState\";\nimport type { ToolExecutionStatus } from \"@assistant-ui/core\";\nimport { createRequestHeaders } from \"@assistant-ui/core\";\nimport { useRemoteThreadListRuntime } from \"../remote-thread-list/useRemoteThreadListRuntime\";\nimport { InMemoryThreadListAdapter } from \"@assistant-ui/core\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport type { UserExternalState } from \"../../../augmentations\";\n\nconst convertAppendMessageToCommand = (\n message: AppendMessage,\n): AddMessageCommand | null => {\n if (message.role !== \"user\")\n throw new Error(\"Only user messages are supported\");\n\n const parts: UserMessagePart[] = [];\n const content = [\n ...message.content,\n ...(message.attachments?.flatMap((a) => a.content) ?? []),\n ];\n for (const contentPart of content) {\n if (contentPart.type === \"text\") {\n parts.push({ type: \"text\", text: contentPart.text });\n } else if (contentPart.type === \"image\") {\n parts.push({ type: \"image\", image: contentPart.image });\n }\n }\n\n if (parts.length === 0) return null;\n\n return {\n type: \"add-message\",\n message: {\n role: \"user\",\n parts,\n },\n parentId: message.parentId,\n sourceId: message.sourceId,\n };\n};\n\nconst readResumeState = async <T>(\n response: Response,\n): Promise<{ runId: string; state: T } | null> => {\n if (response.status === 204) return null;\n if (!response.ok) {\n throw new Error(\n `Resume state request failed with status ${response.status}: ${await response.text()}`,\n );\n }\n\n let value: unknown;\n try {\n value = await response.json();\n } catch {\n throw new Error(\"Resume state response was not valid JSON\");\n }\n\n if (\n typeof value !== \"object\" ||\n value === null ||\n !(\"state\" in value) ||\n !(\"runId\" in value) ||\n typeof value.runId !== \"string\"\n ) {\n throw new Error(\"Resume state response must contain state and runId\");\n }\n\n return { runId: value.runId, state: value.state as T };\n};\n\nconst symbolAssistantTransportExtras = Symbol(\"assistant-transport-extras\");\ntype AssistantTransportExtras = {\n [symbolAssistantTransportExtras]: true;\n sendCommand: (command: AssistantTransportCommand) => void;\n state: UserExternalState;\n};\n\nconst asAssistantTransportExtras = (\n extras: unknown,\n): AssistantTransportExtras => {\n if (\n typeof extras !== \"object\" ||\n extras == null ||\n !(symbolAssistantTransportExtras in extras)\n )\n throw new Error(\n \"This method can only be called when you are using useAssistantTransportRuntime\",\n );\n\n return extras as AssistantTransportExtras;\n};\n\nexport const useAssistantTransportSendCommand = () => {\n const aui = useAui();\n\n return (command: AssistantTransportCommand) => {\n const extras = aui.thread.getState().extras;\n const transportExtras = asAssistantTransportExtras(extras);\n transportExtras.sendCommand(command);\n };\n};\n\nexport function useAssistantTransportState(): UserExternalState;\nexport function useAssistantTransportState<T>(\n selector: (state: UserExternalState) => T,\n): T;\nexport function useAssistantTransportState<T>(\n selector: (state: UserExternalState) => T = (t) => t as T,\n): T | UserExternalState {\n return useAuiState((s) =>\n selector(asAssistantTransportExtras(s.thread.extras).state),\n );\n}\n\nconst useAssistantTransportThreadRuntime = <T>(\n options: AssistantTransportOptions<T>,\n): AssistantRuntime => {\n const agentStateRef = useRef(options.initialState);\n const [, rerender] = useState(0);\n const resumeFlagRef = useRef(false);\n const [isReplaying, setIsReplaying] = useState(false);\n const waitForReplayRender = useReplayRenderWait();\n const parentIdRef = useRef<string | null | undefined>(undefined);\n const commandQueue = useCommandQueue({\n onQueue: () => runManager.schedule(),\n });\n\n const enqueueAppendMessage = (message: AppendMessage) => {\n const command = convertAppendMessageToCommand(message);\n if (!command) {\n console.warn(\n \"[assistant-ui] Skipped add-message command with no supported parts\",\n );\n return;\n }\n parentIdRef.current = message.parentId;\n commandQueue.enqueue(command, {\n schedule: message.startRun ?? message.role === \"user\",\n });\n };\n\n const threadId = useAuiState((s) => s.threadListItem.remoteId);\n\n const runManager = useRunManager({\n onRun: async (signal: AbortSignal) => {\n const isResume = resumeFlagRef.current;\n resumeFlagRef.current = false;\n setIsReplaying(false);\n const commands: QueuedCommand[] = isResume ? [] : commandQueue.flush();\n if (commands.length === 0 && !isResume) return;\n\n // The flushed batch consumes the parentId; read it alongside the flush\n // (before any awaits) so a mid-run append keeps its own value. Resume\n // runs send no commands, so they neither send nor consume it.\n const parentId = isResume ? undefined : parentIdRef.current;\n if (!isResume) parentIdRef.current = undefined;\n\n const headers = await createRequestHeaders(options.headers);\n let resumeState: { runId: string; state: T } | undefined;\n if (isResume && options.resumeStateApi) {\n const resumeStateResponse = await fetch(options.resumeStateApi, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ threadId }),\n signal,\n });\n const retained = await readResumeState<T>(resumeStateResponse);\n if (retained === null) {\n if (commandQueue.state.queued.length > 0) {\n runManager.schedule();\n }\n return;\n }\n resumeState = retained;\n }\n\n const bodyValue =\n typeof options.body === \"function\"\n ? await options.body()\n : options.body;\n const context = runtime.thread.getModelContext();\n\n let requestBody: Record<string, unknown> = {\n commands,\n ...(resumeState === undefined && { state: agentStateRef.current }),\n system: context.system,\n tools: context.tools ? toToolsJSONSchema(context.tools) : undefined,\n threadId,\n ...(parentId !== undefined && {\n parentId,\n }),\n // nested (new format, aligned with AssistantChatTransport)\n callSettings: context.callSettings,\n config: context.config,\n // @deprecated spread at top level — use nested `callSettings`/`config` instead. Will be removed in a future version.\n ...context.callSettings,\n ...context.config,\n ...(bodyValue ?? {}),\n };\n\n if (options.prepareSendCommandsRequest) {\n requestBody = await options.prepareSendCommandsRequest(\n requestBody as SendCommandsRequestBody,\n );\n }\n\n if (resumeState !== undefined) {\n // The server replays a resume from the snapshot it retained for this\n // runId. Body overrides and prepare hooks can neither substitute a\n // state nor drop the ID the server validates against.\n requestBody = { ...requestBody, runId: resumeState.runId };\n delete requestBody[\"state\"];\n }\n\n const response = await fetch(\n isResume ? options.resumeApi! : options.api,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(requestBody),\n signal,\n },\n );\n\n options.onResponse?.(response);\n\n if (!response.ok) {\n throw new Error(`Status ${response.status}: ${await response.text()}`);\n }\n\n if (!response.body) {\n throw new Error(\"Response body is null\");\n }\n\n if (resumeState !== undefined) {\n agentStateRef.current = resumeState.state;\n rerender((prev) => prev + 1);\n }\n\n const body = await createReplayBoundaryStream(response, {\n setReplaying: setIsReplaying,\n waitForRender: waitForReplayRender,\n });\n\n // Select decoder based on protocol option\n const protocol = options.protocol ?? \"data-stream\";\n // Resume replays a best-effort buffer; always reconcile leniently.\n const strict = isResume ? false : (options.strict ?? true);\n const decoder =\n protocol === \"assistant-transport\"\n ? new AssistantTransportDecoder({ strict })\n : new DataStreamDecoder({ strict });\n\n let err: string | undefined;\n const stream = body.pipeThrough(decoder).pipeThrough(\n new AssistantMessageAccumulator({\n initialMessage: createInitialMessage({\n unstable_state:\n (agentStateRef.current as ReadonlyJSONValue) ?? null,\n }),\n throttle: isResume,\n strict,\n onError: (error) => {\n err = error;\n },\n }),\n );\n\n let markedDelivered = false;\n\n for await (const chunk of asAsyncIterableStream(stream)) {\n if (chunk.metadata.unstable_state === agentStateRef.current) continue;\n\n if (!markedDelivered) {\n commandQueue.markDelivered();\n markedDelivered = true;\n }\n\n agentStateRef.current = chunk.metadata.unstable_state as T;\n rerender((prev) => prev + 1);\n }\n\n if (err) {\n throw new Error(err);\n }\n\n // A successful run confirms delivery even when no state-changing\n // chunk was observed.\n if (!markedDelivered) {\n commandQueue.markDelivered();\n }\n\n // commands that coalesced into this resume run must not starve\n if (isResume && commandQueue.state.queued.length > 0) {\n runManager.schedule();\n }\n },\n onFinish: options.onFinish,\n onCancel: () => {\n setIsReplaying(false);\n const cmds = [\n ...commandQueue.state.inTransit,\n ...commandQueue.state.queued,\n ];\n\n commandQueue.reset();\n\n options.onCancel?.({\n commands: cmds,\n updateState: (updater) => {\n agentStateRef.current = updater(agentStateRef.current);\n rerender((prev) => prev + 1);\n },\n });\n },\n onError: async (error) => {\n resumeFlagRef.current = false;\n setIsReplaying(false);\n const inTransitCmds = [...commandQueue.state.inTransit];\n const queuedCmds = [...commandQueue.state.queued];\n\n commandQueue.reset();\n\n try {\n await options.onError?.(error as Error, {\n commands: inTransitCmds,\n updateState: (updater) => {\n agentStateRef.current = updater(agentStateRef.current);\n rerender((prev) => prev + 1);\n },\n });\n } finally {\n options.onCancel?.({\n commands: queuedCmds,\n updateState: (updater) => {\n agentStateRef.current = updater(agentStateRef.current);\n rerender((prev) => prev + 1);\n },\n error: error as Error,\n });\n }\n },\n });\n\n // Tool execution status state\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n\n // Reactive conversion of agent state + connection metadata → UI state\n const pendingCommands = useMemo(\n () => [...commandQueue.state.inTransit, ...commandQueue.state.queued],\n [commandQueue.state],\n );\n const converted = useConvertedState(\n options.converter,\n agentStateRef.current,\n pendingCommands,\n runManager.isRunning,\n toolStatuses,\n );\n\n // Create runtime\n const runtime = useExternalStoreRuntime({\n messages: converted.messages,\n state: converted.state,\n isRunning: converted.isRunning,\n isLoading: isReplaying,\n adapters: options.adapters,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n extras: {\n [symbolAssistantTransportExtras]: true,\n sendCommand: (command: AssistantTransportCommand) => {\n commandQueue.enqueue(command);\n },\n state: agentStateRef.current as UserExternalState,\n } satisfies AssistantTransportExtras,\n onNew: async (message: AppendMessage): Promise<void> =>\n enqueueAppendMessage(message),\n ...(options.capabilities?.edit && {\n onEdit: async (message: AppendMessage): Promise<void> =>\n enqueueAppendMessage(message),\n }),\n ...(commandQueue.state.queued.length > 0 && {\n onReload: async (parentId: string | null) => {\n parentIdRef.current = parentId;\n runManager.schedule();\n },\n }),\n onCancel: async () => {\n resumeFlagRef.current = false;\n runManager.cancel();\n },\n onResume: async () => {\n if (!options.resumeApi)\n throw new Error(\"Must pass resumeApi to options to resume runs\");\n\n resumeFlagRef.current = true;\n runManager.schedule();\n },\n onAddToolResult: async (\n toolOptions: AddToolResultOptions,\n ): Promise<void> => {\n const command: AddToolResultCommand = {\n type: \"add-tool-result\",\n toolCallId: toolOptions.toolCallId,\n result: toolOptions.result as ReadonlyJSONObject,\n toolName: toolOptions.toolName,\n isError: toolOptions.isError,\n ...(toolOptions.artifact && { artifact: toolOptions.artifact }),\n ...(toolOptions.modelContent !== undefined && {\n modelContent: toolOptions.modelContent,\n }),\n };\n\n commandQueue.enqueue(command);\n },\n onLoadExternalState: async (state) => {\n agentStateRef.current = state as T;\n rerender((prev) => prev + 1);\n },\n });\n\n return runtime;\n};\n\n/**\n * @alpha This is an experimental API that is subject to change.\n */\nexport const useAssistantTransportRuntime = <T>(\n options: AssistantTransportOptions<T>,\n): AssistantRuntime => {\n const runtime = useRemoteThreadListRuntime({\n runtimeHook: function RuntimeHook() {\n return useAssistantTransportThreadRuntime(options);\n },\n adapter: new InMemoryThreadListAdapter(),\n allowNesting: true,\n });\n return runtime;\n};\n"],"mappings":"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"}
@@ -34,7 +34,8 @@ function unstable_useLiveCompletionAdapter(options) {
34
34
  const { fetcher, debounceMs = 60, enabled = true } = options;
35
35
  const [state, setState] = useState({
36
36
  query: NO_QUERY,
37
- items: []
37
+ items: [],
38
+ failed: false
38
39
  });
39
40
  const [isLoading, setIsLoading] = useState(false);
40
41
  const fetcherRef = useRef(fetcher);
@@ -42,15 +43,28 @@ function unstable_useLiveCompletionAdapter(options) {
42
43
  const timerRef = useRef(null);
43
44
  const tokenRef = useRef(0);
44
45
  const pendingQueryRef = useRef(null);
46
+ const retryableQueryRef = useRef(null);
47
+ const pendingRetryQueryRef = useRef(null);
45
48
  const cancelTimer = useCallback(() => {
46
49
  if (timerRef.current !== null) {
47
50
  clearTimeout(timerRef.current);
48
51
  timerRef.current = null;
49
52
  }
50
53
  }, []);
54
+ const rearmPendingRetry = useCallback(() => {
55
+ const query = pendingRetryQueryRef.current;
56
+ if (query === null) return;
57
+ retryableQueryRef.current = query;
58
+ pendingRetryQueryRef.current = null;
59
+ }, []);
51
60
  const scheduleFetch = useCallback((query) => {
52
61
  if (!enabled) return;
53
62
  if (pendingQueryRef.current === query) return;
63
+ rearmPendingRetry();
64
+ if (retryableQueryRef.current === query) {
65
+ retryableQueryRef.current = null;
66
+ pendingRetryQueryRef.current = query;
67
+ }
54
68
  pendingQueryRef.current = query;
55
69
  cancelTimer();
56
70
  const token = ++tokenRef.current;
@@ -59,16 +73,21 @@ function unstable_useLiveCompletionAdapter(options) {
59
73
  timerRef.current = null;
60
74
  fetcherRef.current(query).then((items) => {
61
75
  if (token !== tokenRef.current) return;
76
+ pendingRetryQueryRef.current = null;
62
77
  setState({
63
78
  query,
64
- items
79
+ items,
80
+ failed: false
65
81
  });
66
82
  setIsLoading(false);
67
83
  }, () => {
68
84
  if (token !== tokenRef.current) return;
85
+ pendingQueryRef.current = null;
86
+ pendingRetryQueryRef.current = null;
69
87
  setState({
70
88
  query,
71
- items: []
89
+ items: [],
90
+ failed: true
72
91
  });
73
92
  setIsLoading(false);
74
93
  });
@@ -76,29 +95,35 @@ function unstable_useLiveCompletionAdapter(options) {
76
95
  }, [
77
96
  enabled,
78
97
  debounceMs,
79
- cancelTimer
98
+ cancelTimer,
99
+ rearmPendingRetry
80
100
  ]);
81
101
  const invalidatePending = useCallback(() => {
102
+ rearmPendingRetry();
82
103
  cancelTimer();
83
104
  pendingQueryRef.current = null;
84
105
  tokenRef.current += 1;
85
106
  setIsLoading(false);
86
- }, [cancelTimer]);
107
+ }, [cancelTimer, rearmPendingRetry]);
87
108
  useEffect(() => {
88
109
  if (enabled) return;
89
110
  invalidatePending();
90
111
  setState((s) => s.query === NO_QUERY ? s : {
91
112
  query: NO_QUERY,
92
- items: []
113
+ items: [],
114
+ failed: false
93
115
  });
94
116
  }, [enabled, invalidatePending]);
95
117
  useEffect(() => cancelTimer, [cancelTimer]);
118
+ useEffect(() => {
119
+ retryableQueryRef.current = state.failed ? state.query : null;
120
+ }, [state]);
96
121
  return {
97
122
  adapter: useMemo(() => ({
98
123
  categories: () => [],
99
124
  categoryItems: () => [],
100
125
  search: (query) => {
101
- if (query !== state.query) queueMicrotask(() => scheduleFetch(query));
126
+ if (query !== state.query || retryableQueryRef.current === query) queueMicrotask(() => scheduleFetch(query));
102
127
  else if (pendingQueryRef.current !== null && pendingQueryRef.current !== query) queueMicrotask(invalidatePending);
103
128
  return state.items;
104
129
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveCompletionAdapter.js","names":["useCallback","useEffect","useMemo","useRef","useState","Unstable_TriggerAdapter","Unstable_TriggerItem","Unstable_UseLiveCompletionAdapterOptions","fetcher","query","Promise","debounceMs","enabled","NO_QUERY","unstable_useLiveCompletionAdapter","options","adapter","isLoading","state","setState","items","setIsLoading","fetcherRef","current","timerRef","ReturnType","setTimeout","tokenRef","pendingQueryRef","cancelTimer","clearTimeout","scheduleFetch","token","then","invalidatePending","s","categories","categoryItems","search","queueMicrotask"],"sources":["../../src/unstable/useLiveCompletionAdapter.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type {\n Unstable_TriggerAdapter,\n Unstable_TriggerItem,\n} from \"@assistant-ui/core\";\n\nexport type Unstable_UseLiveCompletionAdapterOptions = {\n /**\n * Fetches the items for a query from an async source. Called debounced; the\n * resolved items are cached and returned synchronously to the popover on the\n * next render.\n */\n readonly fetcher: (query: string) => Promise<readonly Unstable_TriggerItem[]>;\n /** Debounce applied before a fetch fires, in milliseconds. @default 60 */\n readonly debounceMs?: number | undefined;\n /** When `false`, no fetch is scheduled and the adapter stays empty. @default true */\n readonly enabled?: boolean | undefined;\n};\n\n/** Sentinel that no real query (including the empty string) equals, so the first query always fetches. */\nconst NO_QUERY = \"\\u0000\";\n\n/**\n * @deprecated Under active development and may change without notice.\n *\n * Bridges an async completion source (a server search, a gateway RPC) into the\n * synchronous `Unstable_TriggerAdapter` that `ComposerTriggerPopover` consumes.\n * `search(query)` returns the last fetched items synchronously and schedules a\n * debounced fetch when the query changes; when results arrive the returned\n * `adapter` identity changes, which re-runs the popover's lookup so the fresh\n * items render. This is a search-only adapter (`categories` are empty).\n *\n * `isLoading` is `true` while a fetch is in flight. Pass it to the popover's\n * `isLoading` prop to render a loading state.\n *\n * @example\n * ```tsx\n * const mentions = unstable_useLiveCompletionAdapter({\n * fetcher: (query) => searchUsers(query),\n * });\n *\n * <ComposerTriggerPopover\n * char=\"@\"\n * adapter={mentions.adapter}\n * isLoading={mentions.isLoading}\n * directive={{ onInserted }}\n * />\n * ```\n */\nexport function unstable_useLiveCompletionAdapter(\n options: Unstable_UseLiveCompletionAdapterOptions,\n): { adapter: Unstable_TriggerAdapter; isLoading: boolean } {\n const { fetcher, debounceMs = 60, enabled = true } = options;\n\n const [state, setState] = useState<{\n query: string;\n items: readonly Unstable_TriggerItem[];\n }>({ query: NO_QUERY, items: [] });\n const [isLoading, setIsLoading] = useState(false);\n\n const fetcherRef = useRef(fetcher);\n fetcherRef.current = fetcher;\n\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const tokenRef = useRef(0);\n const pendingQueryRef = useRef<string | null>(null);\n\n const cancelTimer = useCallback(() => {\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n }, []);\n\n const scheduleFetch = useCallback(\n (query: string) => {\n if (!enabled) return;\n if (pendingQueryRef.current === query) return;\n pendingQueryRef.current = query;\n cancelTimer();\n const token = ++tokenRef.current;\n setIsLoading(true);\n timerRef.current = setTimeout(() => {\n timerRef.current = null;\n fetcherRef.current(query).then(\n (items) => {\n if (token !== tokenRef.current) return;\n setState({ query, items });\n setIsLoading(false);\n },\n () => {\n if (token !== tokenRef.current) return;\n setState({ query, items: [] });\n setIsLoading(false);\n },\n );\n }, debounceMs);\n },\n [enabled, debounceMs, cancelTimer],\n );\n\n const invalidatePending = useCallback(() => {\n cancelTimer();\n pendingQueryRef.current = null;\n tokenRef.current += 1;\n setIsLoading(false);\n }, [cancelTimer]);\n\n useEffect(() => {\n if (enabled) return;\n invalidatePending();\n setState((s) =>\n s.query === NO_QUERY ? s : { query: NO_QUERY, items: [] },\n );\n }, [enabled, invalidatePending]);\n\n useEffect(() => cancelTimer, [cancelTimer]);\n\n const adapter = useMemo<Unstable_TriggerAdapter>(\n () => ({\n categories: () => [],\n categoryItems: () => [],\n search: (query: string) => {\n // search() runs inside the popover's render; defer state updates with\n // queueMicrotask so they are not dispatched while another component renders.\n if (query !== state.query) {\n queueMicrotask(() => scheduleFetch(query));\n } else if (\n pendingQueryRef.current !== null &&\n pendingQueryRef.current !== query\n ) {\n // the query returned to a cached value while a fetch for a different\n // query is in flight; drop it so its result cannot overwrite the cache\n queueMicrotask(invalidatePending);\n }\n return state.items;\n },\n }),\n [state, scheduleFetch, invalidatePending],\n );\n\n return { adapter, isLoading };\n}\n"],"mappings":";;;;AAsBA,MAAMa,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BjB,SAAgBC,kCACdC,SAC0D;CAC1D,MAAM,EAAEP,SAASG,aAAa,IAAIC,UAAU,SAASG;CAErD,MAAM,CAACG,OAAOC,YAAYf,SAGvB;EAAEK,OAAOI;EAAUO,OAAO,CAAA;CAAG,CAAC;CACjC,MAAM,CAACH,WAAWI,gBAAgBjB,SAAS,KAAK;CAEhD,MAAMkB,aAAanB,OAAOK,OAAO;CACjCc,WAAWC,UAAUf;CAErB,MAAMgB,WAAWrB,OAA6C,IAAI;CAClE,MAAMwB,WAAWxB,OAAO,CAAC;CACzB,MAAMyB,kBAAkBzB,OAAsB,IAAI;CAElD,MAAM0B,cAAc7B,kBAAkB;EACpC,IAAIwB,SAASD,YAAY,MAAM;GAC7BO,aAAaN,SAASD,OAAO;GAC7BC,SAASD,UAAU;EACrB;CACF,GAAG,CAAA,CAAE;CAEL,MAAMQ,gBAAgB/B,aACnBS,UAAkB;EACjB,IAAI,CAACG,SAAS;EACd,IAAIgB,gBAAgBL,YAAYd,OAAO;EACvCmB,gBAAgBL,UAAUd;EAC1BoB,YAAY;EACZ,MAAMG,QAAQ,EAAEL,SAASJ;EACzBF,aAAa,IAAI;EACjBG,SAASD,UAAUG,iBAAiB;GAClCF,SAASD,UAAU;GACnBD,WAAWC,QAAQd,KAAK,CAAC,CAACwB,MACvBb,UAAU;IACT,IAAIY,UAAUL,SAASJ,SAAS;IAChCJ,SAAS;KAAEV;KAAOW;IAAM,CAAC;IACzBC,aAAa,KAAK;GACpB,SACM;IACJ,IAAIW,UAAUL,SAASJ,SAAS;IAChCJ,SAAS;KAAEV;KAAOW,OAAO,CAAA;IAAG,CAAC;IAC7BC,aAAa,KAAK;GACpB,CACF;EACF,GAAGV,UAAU;CACf,GACA;EAACC;EAASD;EAAYkB;CAAW,CACnC;CAEA,MAAMK,oBAAoBlC,kBAAkB;EAC1C6B,YAAY;EACZD,gBAAgBL,UAAU;EAC1BI,SAASJ,WAAW;EACpBF,aAAa,KAAK;CACpB,GAAG,CAACQ,WAAW,CAAC;CAEhB5B,gBAAgB;EACd,IAAIW,SAAS;EACbsB,kBAAkB;EAClBf,UAAUgB,MACRA,EAAE1B,UAAUI,WAAWsB,IAAI;GAAE1B,OAAOI;GAAUO,OAAO,CAAA;EAAG,CAC1D;CACF,GAAG,CAACR,SAASsB,iBAAiB,CAAC;CAE/BjC,gBAAgB4B,aAAa,CAACA,WAAW,CAAC;CAyB1C,OAAO;EAAEb,SAvBOd,eACP;GACLkC,kBAAkB,CAAA;GAClBC,qBAAqB,CAAA;GACrBC,SAAS7B,UAAkB;IAGzB,IAAIA,UAAUS,MAAMT,OAClB8B,qBAAqBR,cAActB,KAAK,CAAC;SACpC,IACLmB,gBAAgBL,YAAY,QAC5BK,gBAAgBL,YAAYd,OAI5B8B,eAAeL,iBAAiB;IAElC,OAAOhB,MAAME;GACf;EACF,IACA;GAACF;GAAOa;GAAeG;EAAiB,CAGjClB;EAASC;CAAU;AAC9B"}
1
+ {"version":3,"file":"useLiveCompletionAdapter.js","names":["useCallback","useEffect","useMemo","useRef","useState","Unstable_TriggerAdapter","Unstable_TriggerItem","Unstable_UseLiveCompletionAdapterOptions","fetcher","query","Promise","debounceMs","enabled","NO_QUERY","unstable_useLiveCompletionAdapter","options","adapter","isLoading","state","setState","items","failed","setIsLoading","fetcherRef","current","timerRef","ReturnType","setTimeout","tokenRef","pendingQueryRef","retryableQueryRef","pendingRetryQueryRef","cancelTimer","clearTimeout","rearmPendingRetry","scheduleFetch","token","then","invalidatePending","s","categories","categoryItems","search","queueMicrotask"],"sources":["../../src/unstable/useLiveCompletionAdapter.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type {\n Unstable_TriggerAdapter,\n Unstable_TriggerItem,\n} from \"@assistant-ui/core\";\n\nexport type Unstable_UseLiveCompletionAdapterOptions = {\n /**\n * Fetches the items for a query from an async source. Called debounced; the\n * resolved items are cached and returned synchronously to the popover on the\n * next render.\n */\n readonly fetcher: (query: string) => Promise<readonly Unstable_TriggerItem[]>;\n /** Debounce applied before a fetch fires, in milliseconds. @default 60 */\n readonly debounceMs?: number | undefined;\n /** When `false`, no fetch is scheduled and the adapter stays empty. @default true */\n readonly enabled?: boolean | undefined;\n};\n\n/** Sentinel that no real query (including the empty string) equals, so the first query always fetches. */\nconst NO_QUERY = \"\\u0000\";\n\n/**\n * @deprecated Under active development and may change without notice.\n *\n * Bridges an async completion source (a server search, a gateway RPC) into the\n * synchronous `Unstable_TriggerAdapter` that `ComposerTriggerPopover` consumes.\n * `search(query)` returns the last fetched items synchronously and schedules a\n * debounced fetch when the query changes; when results arrive the returned\n * `adapter` identity changes, which re-runs the popover's lookup so the fresh\n * items render. This is a search-only adapter (`categories` are empty).\n *\n * `isLoading` is `true` while a fetch is in flight. Pass it to the popover's\n * `isLoading` prop to render a loading state.\n *\n * @example\n * ```tsx\n * const mentions = unstable_useLiveCompletionAdapter({\n * fetcher: (query) => searchUsers(query),\n * });\n *\n * <ComposerTriggerPopover\n * char=\"@\"\n * adapter={mentions.adapter}\n * isLoading={mentions.isLoading}\n * directive={{ onInserted }}\n * />\n * ```\n */\nexport function unstable_useLiveCompletionAdapter(\n options: Unstable_UseLiveCompletionAdapterOptions,\n): { adapter: Unstable_TriggerAdapter; isLoading: boolean } {\n const { fetcher, debounceMs = 60, enabled = true } = options;\n\n const [state, setState] = useState<{\n query: string;\n items: readonly Unstable_TriggerItem[];\n failed: boolean;\n }>({ query: NO_QUERY, items: [], failed: false });\n const [isLoading, setIsLoading] = useState(false);\n\n const fetcherRef = useRef(fetcher);\n fetcherRef.current = fetcher;\n\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const tokenRef = useRef(0);\n const pendingQueryRef = useRef<string | null>(null);\n const retryableQueryRef = useRef<string | null>(null);\n const pendingRetryQueryRef = useRef<string | null>(null);\n\n const cancelTimer = useCallback(() => {\n if (timerRef.current !== null) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n }, []);\n\n const rearmPendingRetry = useCallback(() => {\n const query = pendingRetryQueryRef.current;\n if (query === null) return;\n retryableQueryRef.current = query;\n pendingRetryQueryRef.current = null;\n }, []);\n\n const scheduleFetch = useCallback(\n (query: string) => {\n if (!enabled) return;\n if (pendingQueryRef.current === query) return;\n rearmPendingRetry();\n if (retryableQueryRef.current === query) {\n retryableQueryRef.current = null;\n pendingRetryQueryRef.current = query;\n }\n pendingQueryRef.current = query;\n cancelTimer();\n const token = ++tokenRef.current;\n setIsLoading(true);\n timerRef.current = setTimeout(() => {\n timerRef.current = null;\n fetcherRef.current(query).then(\n (items) => {\n if (token !== tokenRef.current) return;\n pendingRetryQueryRef.current = null;\n setState({ query, items, failed: false });\n setIsLoading(false);\n },\n () => {\n if (token !== tokenRef.current) return;\n pendingQueryRef.current = null;\n pendingRetryQueryRef.current = null;\n setState({ query, items: [], failed: true });\n setIsLoading(false);\n },\n );\n }, debounceMs);\n },\n [enabled, debounceMs, cancelTimer, rearmPendingRetry],\n );\n\n const invalidatePending = useCallback(() => {\n rearmPendingRetry();\n cancelTimer();\n pendingQueryRef.current = null;\n tokenRef.current += 1;\n setIsLoading(false);\n }, [cancelTimer, rearmPendingRetry]);\n\n useEffect(() => {\n if (enabled) return;\n invalidatePending();\n setState((s) =>\n s.query === NO_QUERY ? s : { query: NO_QUERY, items: [], failed: false },\n );\n }, [enabled, invalidatePending]);\n\n useEffect(() => cancelTimer, [cancelTimer]);\n\n // Arm retries only after the failed state commits. Arming during rejection\n // would let the failure render immediately schedule another request.\n useEffect(() => {\n retryableQueryRef.current = state.failed ? state.query : null;\n }, [state]);\n\n const adapter = useMemo<Unstable_TriggerAdapter>(\n () => ({\n categories: () => [],\n categoryItems: () => [],\n search: (query: string) => {\n // search() runs inside the popover's render; defer state updates with\n // queueMicrotask so they are not dispatched while another component renders.\n if (query !== state.query || retryableQueryRef.current === query) {\n queueMicrotask(() => scheduleFetch(query));\n } else if (\n pendingQueryRef.current !== null &&\n pendingQueryRef.current !== query\n ) {\n // the query returned to a cached value while a fetch for a different\n // query is in flight; drop it so its result cannot overwrite the cache\n queueMicrotask(invalidatePending);\n }\n return state.items;\n },\n }),\n [state, scheduleFetch, invalidatePending],\n );\n\n return { adapter, isLoading };\n}\n"],"mappings":"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"}
@@ -1,7 +1,5 @@
1
1
  //#region src/utils/useToolArgsFieldStatus.d.ts
2
2
  declare const useToolArgsFieldStatus: (fieldPath: (string | number)[]) => {
3
- type: string;
4
- } | {
5
3
  readonly type: "running";
6
4
  } | {
7
5
  readonly type: "complete";
@@ -9,6 +7,8 @@ declare const useToolArgsFieldStatus: (fieldPath: (string | number)[]) => {
9
7
  readonly type: "incomplete";
10
8
  readonly reason: "cancelled" | "length" | "content-filter" | "other" | "error";
11
9
  readonly error?: unknown;
10
+ } | {
11
+ type: string;
12
12
  };
13
13
  //#endregion
14
14
  export { useToolArgsFieldStatus };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react",
3
- "version": "0.15.7",
3
+ "version": "0.15.9",
4
4
  "description": "Open-source TypeScript/React library for building production-grade AI chat experiences",
5
5
  "keywords": [
6
6
  "ai",
@@ -54,16 +54,9 @@
54
54
  "README.md"
55
55
  ],
56
56
  "sideEffects": false,
57
- "scripts": {
58
- "prepack": "cp ../../README.md /tmp/aui-readme-pack.md && mv /tmp/aui-readme-pack.md ./README.md",
59
- "postpack": "rm -f ./README.md && ln -s ../../README.md ./README.md",
60
- "build": "aui-build",
61
- "test": "vitest run",
62
- "test:watch": "vitest"
63
- },
64
57
  "dependencies": {
65
- "@assistant-ui/core": "^0.3.7",
66
- "@assistant-ui/store": "^0.3.5",
58
+ "@assistant-ui/core": "^0.3.8",
59
+ "@assistant-ui/store": "^0.3.7",
67
60
  "@assistant-ui/tap": "^0.9.10",
68
61
  "@radix-ui/primitive": "^1.1.7",
69
62
  "@radix-ui/react-collection": "^1.1.15",
@@ -74,7 +67,7 @@
74
67
  "@radix-ui/react-use-controllable-state": "^1.2.6",
75
68
  "@radix-ui/react-use-escape-keydown": "^1.1.5",
76
69
  "assistant-cloud": "^0.1.38",
77
- "assistant-stream": "^0.3.34",
70
+ "assistant-stream": "^0.3.35",
78
71
  "nanoid": "^6.0.0",
79
72
  "radix-ui": "^1.6.7",
80
73
  "react-textarea-autosize": "^8.5.9",
@@ -97,8 +90,6 @@
97
90
  }
98
91
  },
99
92
  "devDependencies": {
100
- "@assistant-ui/vite": "workspace:*",
101
- "@assistant-ui/x-buildutils": "workspace:*",
102
93
  "@testing-library/react": "^16.3.2",
103
94
  "@types/json-schema": "^7.0.15",
104
95
  "@types/node": "^26.1.1",
@@ -107,7 +98,9 @@
107
98
  "jsdom": "^29.1.1",
108
99
  "react": "^19.2.8",
109
100
  "react-dom": "^19.2.8",
110
- "vitest": "^4.1.10"
101
+ "vitest": "^4.1.10",
102
+ "@assistant-ui/vite": "0.0.11",
103
+ "@assistant-ui/x-buildutils": "0.0.21"
111
104
  },
112
105
  "publishConfig": {
113
106
  "access": "public",
@@ -121,5 +114,10 @@
121
114
  },
122
115
  "bugs": {
123
116
  "url": "https://github.com/assistant-ui/assistant-ui/issues"
117
+ },
118
+ "scripts": {
119
+ "build": "aui-build",
120
+ "test": "vitest run",
121
+ "test:watch": "vitest"
124
122
  }
125
- }
123
+ }
@@ -33,6 +33,9 @@ import type {
33
33
  ExternalThreadQueueAdapter,
34
34
  ExternalThreadBranchAdapter,
35
35
  QueuePlacement,
36
+ FeedbackAdapter,
37
+ SpeechState,
38
+ SpeechSynthesisAdapter,
36
39
  } from "@assistant-ui/core";
37
40
  import { ToolResponse } from "assistant-stream";
38
41
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
@@ -97,6 +100,8 @@ export type ExternalThreadProps = {
97
100
  onResumeToolCall?: ((options: ResumeToolCallOptions) => void) | undefined;
98
101
  onLoadExternalState?: ((state: unknown) => void) | undefined;
99
102
  attachmentAdapter?: AttachmentAdapter | undefined;
103
+ feedbackAdapter?: FeedbackAdapter | undefined;
104
+ speechAdapter?: SpeechSynthesisAdapter | undefined;
100
105
  /** Queue adapter for runtimes that support message queuing and steering. */
101
106
  queue?: ExternalThreadQueueAdapter;
102
107
  /** Branch adapter for runtimes that track sibling variants of messages. */
@@ -119,6 +124,11 @@ type MessageClientProps = {
119
124
  onAddToolResult?: ((options: AddToolResultOptions) => void) | undefined;
120
125
  onResumeToolCall?: ((options: ResumeToolCallOptions) => void) | undefined;
121
126
  attachmentAdapter?: AttachmentAdapter | undefined;
127
+ submittedFeedback: "positive" | "negative" | undefined;
128
+ onSubmitFeedback: (feedback: { type: "positive" | "negative" }) => void;
129
+ speech: SpeechState | undefined;
130
+ onSpeak: () => void;
131
+ onStopSpeaking: () => void;
122
132
  };
123
133
 
124
134
  // Message Client - minimal implementation
@@ -134,6 +144,11 @@ const useMessageClient = ({
134
144
  onAddToolResult,
135
145
  onResumeToolCall,
136
146
  attachmentAdapter,
147
+ submittedFeedback,
148
+ onSubmitFeedback,
149
+ speech,
150
+ onSpeak,
151
+ onStopSpeaking,
137
152
  }: MessageClientProps): ClientOutput<"message"> => {
138
153
  const [isCopied, setIsCopied] = useState(false);
139
154
  const [isHovering, setIsHovering] = useState(false);
@@ -204,14 +219,24 @@ const useMessageClient = ({
204
219
  const branchCount = branchIndex === -1 ? 1 : branchIds.length;
205
220
 
206
221
  const state = useMemo(() => {
222
+ const messageWithFeedback: ExternalThreadMessage =
223
+ submittedFeedback && message.role === "assistant"
224
+ ? {
225
+ ...message,
226
+ metadata: {
227
+ ...message.metadata,
228
+ submittedFeedback: { type: submittedFeedback },
229
+ },
230
+ }
231
+ : message;
207
232
  return {
208
- ...message,
233
+ ...messageWithFeedback,
209
234
  attachments: message.attachments ?? [],
210
235
  parentId,
211
236
  isLast: false, // Will be set by thread
212
237
  branchNumber,
213
238
  branchCount,
214
- speech: undefined,
239
+ speech,
215
240
  parts: partClients.state,
216
241
  isCopied,
217
242
  isHovering,
@@ -228,6 +253,8 @@ const useMessageClient = ({
228
253
  partClients.state,
229
254
  branchNumber,
230
255
  branchCount,
256
+ submittedFeedback,
257
+ speech,
231
258
  ]);
232
259
 
233
260
  return {
@@ -237,9 +264,9 @@ const useMessageClient = ({
237
264
  reload: () => {
238
265
  onReload?.();
239
266
  },
240
- speak: () => {},
241
- stopSpeaking: () => {},
242
- submitFeedback: () => {},
267
+ speak: onSpeak,
268
+ stopSpeaking: onStopSpeaking,
269
+ submitFeedback: onSubmitFeedback,
243
270
  switchToBranch: ({ position, branchId }) => {
244
271
  if (!branches) return;
245
272
  const target =
@@ -721,6 +748,65 @@ const useComposerClientResource = ({
721
748
 
722
749
  const ComposerClientResource = resource(useComposerClientResource);
723
750
 
751
+ const createSpeechController = (
752
+ notify: (speech: SpeechState | undefined) => void,
753
+ ) => {
754
+ let session: { messageId: string; cancel: () => void } | undefined;
755
+
756
+ const clear = () => {
757
+ if (!session) return;
758
+ session.cancel();
759
+ session = undefined;
760
+ notify(undefined);
761
+ };
762
+
763
+ return {
764
+ speak: (
765
+ adapter: SpeechSynthesisAdapter,
766
+ message: ExternalThreadMessage,
767
+ ) => {
768
+ clear();
769
+
770
+ const utterance = adapter.speak(getThreadMessageText(message));
771
+ let unsub: (() => void) | undefined;
772
+ unsub = utterance.subscribe(() => {
773
+ if (utterance.status.type === "ended") {
774
+ unsub?.();
775
+ session = undefined;
776
+ notify(undefined);
777
+ } else {
778
+ notify({ messageId: message.id, status: utterance.status });
779
+ }
780
+ });
781
+
782
+ if (utterance.status.type === "ended") {
783
+ unsub();
784
+ notify(undefined);
785
+ return;
786
+ }
787
+
788
+ session = {
789
+ messageId: message.id,
790
+ cancel: () => {
791
+ unsub!();
792
+ utterance.cancel();
793
+ },
794
+ };
795
+ notify({ messageId: message.id, status: utterance.status });
796
+ },
797
+ stop: () => {
798
+ if (!session) throw new Error("No message is being spoken");
799
+ clear();
800
+ },
801
+ stopMessage: (messageId: string) => {
802
+ if (session?.messageId !== messageId)
803
+ throw new Error("Message is not being spoken");
804
+ clear();
805
+ },
806
+ dispose: clear,
807
+ };
808
+ };
809
+
724
810
  const dedupeMessagesById = (messages: readonly ExternalThreadMessage[]) => {
725
811
  const seenIds = new Set<string>();
726
812
  const deduped: ExternalThreadMessage[] = [];
@@ -756,6 +842,8 @@ const useExternalThread = ({
756
842
  onResumeToolCall,
757
843
  onLoadExternalState,
758
844
  attachmentAdapter,
845
+ feedbackAdapter,
846
+ speechAdapter,
759
847
  queue,
760
848
  branches,
761
849
  onRespondToToolApproval,
@@ -765,6 +853,71 @@ const useExternalThread = ({
765
853
  [messagesProp],
766
854
  );
767
855
 
856
+ // Local entries are optimistic: they apply only while the message's
857
+ // external submittedFeedback still equals the value seen at click time.
858
+ const [submittedFeedback, setSubmittedFeedback] = useState<
859
+ Record<
860
+ string,
861
+ {
862
+ type: "positive" | "negative";
863
+ external: "positive" | "negative" | undefined;
864
+ }
865
+ >
866
+ >({});
867
+
868
+ const feedbackFor = (msg: ExternalThreadMessage) => {
869
+ const entry = submittedFeedback[msg.id];
870
+ return entry && msg.metadata.submittedFeedback?.type === entry.external
871
+ ? entry.type
872
+ : undefined;
873
+ };
874
+
875
+ useEffect(() => {
876
+ setSubmittedFeedback((prev) => {
877
+ const live = Object.entries(prev).filter(([id, entry]) => {
878
+ const msg = messages.find((m) => m.id === id);
879
+ return !!msg && msg.metadata.submittedFeedback?.type === entry.external;
880
+ });
881
+ return live.length === Object.keys(prev).length
882
+ ? prev
883
+ : Object.fromEntries(live);
884
+ });
885
+ }, [messages]);
886
+
887
+ const handleSubmitFeedback = (
888
+ message: ExternalThreadMessage,
889
+ { type }: { type: "positive" | "negative" },
890
+ ) => {
891
+ if (!feedbackAdapter) throw new Error("Feedback adapter not configured");
892
+ feedbackAdapter.submit({ message, type });
893
+
894
+ if (message.role === "assistant") {
895
+ setSubmittedFeedback((prev) => ({
896
+ ...prev,
897
+ [message.id]: {
898
+ type,
899
+ external: message.metadata.submittedFeedback?.type,
900
+ },
901
+ }));
902
+ }
903
+ };
904
+
905
+ const [speechState, setSpeech] = useState<SpeechState | undefined>(undefined);
906
+ const [speechController] = useState(() => createSpeechController(setSpeech));
907
+
908
+ const hasSpeechAdapter = !!speechAdapter;
909
+ const speech = hasSpeechAdapter ? speechState : undefined;
910
+ useEffect(() => {
911
+ if (!hasSpeechAdapter) speechController.dispose();
912
+ }, [hasSpeechAdapter, speechController]);
913
+
914
+ useEffect(() => () => speechController.dispose(), [speechController]);
915
+
916
+ const handleSpeak = (message: ExternalThreadMessage) => {
917
+ if (!speechAdapter) throw new Error("Speech adapter not configured");
918
+ speechController.speak(speechAdapter, message);
919
+ };
920
+
768
921
  const handleReload = (messageId: string) => {
769
922
  const messageIndex = messages.findIndex((m) => m.id === messageId);
770
923
  if (messageIndex === -1) return;
@@ -786,6 +939,11 @@ const useExternalThread = ({
786
939
  onAddToolResult,
787
940
  onResumeToolCall,
788
941
  attachmentAdapter,
942
+ submittedFeedback: feedbackFor(msg),
943
+ onSubmitFeedback: (feedback) => handleSubmitFeedback(msg, feedback),
944
+ speech: speech?.messageId === msg.id ? speech : undefined,
945
+ onSpeak: () => handleSpeak(msg),
946
+ onStopSpeaking: () => speechController.stopMessage(msg.id),
789
947
  };
790
948
  if (onEdit) props.onEdit = onEdit;
791
949
  return withKey(msg.id, MessageClient(props));
@@ -833,6 +991,8 @@ const useExternalThread = ({
833
991
  const hasEdit = !!onEdit;
834
992
  const hasReload = !!onReload;
835
993
  const hasAttachments = !!attachmentAdapter;
994
+ const hasFeedback = !!feedbackAdapter;
995
+ const hasSpeech = !!speechAdapter;
836
996
  const state = useMemo(() => {
837
997
  const messageStates = messageClients.state.map((s, idx, arr) => ({
838
998
  ...s,
@@ -850,9 +1010,9 @@ const useExternalThread = ({
850
1010
  reload: hasReload,
851
1011
  refetchThread: false,
852
1012
  cancel: isRunning,
853
- speech: false,
1013
+ speech: hasSpeech,
854
1014
  attachments: hasAttachments,
855
- feedback: false,
1015
+ feedback: hasFeedback,
856
1016
  voice: false,
857
1017
  switchToBranch: hasBranches,
858
1018
  switchBranchDuringRun: false,
@@ -864,7 +1024,7 @@ const useExternalThread = ({
864
1024
  state: threadState ?? {},
865
1025
  suggestions: [],
866
1026
  extras,
867
- speech: undefined,
1027
+ speech,
868
1028
  voice: undefined,
869
1029
  composer: composerClient.state,
870
1030
  };
@@ -879,6 +1039,9 @@ const useExternalThread = ({
879
1039
  hasEdit,
880
1040
  hasReload,
881
1041
  hasAttachments,
1042
+ hasFeedback,
1043
+ hasSpeech,
1044
+ speech,
882
1045
  messageClients.state,
883
1046
  composerClient.state,
884
1047
  ]);
@@ -945,7 +1108,7 @@ const useExternalThread = ({
945
1108
  }
946
1109
  return messageClients.get(selector);
947
1110
  },
948
- stopSpeaking: () => {},
1111
+ stopSpeaking: speechController.stop,
949
1112
  connectVoice: () => {},
950
1113
  disconnectVoice: () => {},
951
1114
  getVoiceVolume: () => 0,