@assistant-ui/react 0.15.14 → 0.15.15

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 (160) hide show
  1. package/dist/assistant-transport.d.ts +32 -0
  2. package/dist/assistant-transport.d.ts.map +1 -0
  3. package/dist/assistant-transport.js +27 -0
  4. package/dist/assistant-transport.js.map +1 -0
  5. package/dist/context/providers/MessageProvider.js +1 -1
  6. package/dist/hooks/useMessageQuote.js +1 -1
  7. package/dist/hooks/useToolCallElapsed.js +1 -1
  8. package/dist/index.d.ts +4 -7
  9. package/dist/index.js +6 -8
  10. package/dist/legacy-runtime/AssistantRuntimeProvider.js +1 -1
  11. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
  12. package/dist/mcp-apps/McpAppsRemoteHost.js +18 -2
  13. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
  14. package/dist/mcp-apps/bridge.js +39 -24
  15. package/dist/mcp-apps/bridge.js.map +1 -1
  16. package/dist/model-context/makeAssistantVisible.js +1 -1
  17. package/dist/primitives/actionBar/ActionBarCopy.js +1 -1
  18. package/dist/primitives/actionBar/ActionBarExportMarkdown.js +1 -1
  19. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +1 -1
  20. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +1 -1
  21. package/dist/primitives/actionBar/ActionBarStopSpeaking.d.ts.map +1 -1
  22. package/dist/primitives/actionBar/ActionBarStopSpeaking.js +24 -37
  23. package/dist/primitives/actionBar/ActionBarStopSpeaking.js.map +1 -1
  24. package/dist/primitives/actionBar/useActionBarFloatStatus.js +1 -1
  25. package/dist/primitives/assistantModal/AssistantModalRoot.js +1 -1
  26. package/dist/primitives/attachment/AttachmentName.js +1 -1
  27. package/dist/primitives/attachment/AttachmentRemove.js +1 -1
  28. package/dist/primitives/attachment/AttachmentThumb.js +1 -1
  29. package/dist/primitives/branchPicker/BranchPickerCount.js +1 -1
  30. package/dist/primitives/branchPicker/BranchPickerNumber.js +1 -1
  31. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +1 -1
  32. package/dist/primitives/composer/ComposerAddAttachment.js +1 -1
  33. package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -1
  34. package/dist/primitives/composer/ComposerDictationTranscript.js +1 -1
  35. package/dist/primitives/composer/ComposerInput.js +2 -2
  36. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  37. package/dist/primitives/composer/ComposerQuote.js +1 -1
  38. package/dist/primitives/composer/ComposerRoot.js +1 -1
  39. package/dist/primitives/composer/ComposerStopDictation.js +1 -1
  40. package/dist/primitives/composer/trigger/TriggerPopover.js +1 -1
  41. package/dist/primitives/composer/useComposerInputState.js +1 -1
  42. package/dist/primitives/message/MessageIf.js +1 -1
  43. package/dist/primitives/message/MessagePartsGrouped.js +1 -1
  44. package/dist/primitives/message/MessageRoot.js +1 -1
  45. package/dist/primitives/queueItem/QueueItemRemove.js +1 -1
  46. package/dist/primitives/queueItem/QueueItemSteer.js +1 -1
  47. package/dist/primitives/queueItem/QueueItemText.js +1 -1
  48. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +1 -1
  49. package/dist/primitives/suggestion/SuggestionDescription.js +1 -1
  50. package/dist/primitives/suggestion/SuggestionTitle.js +1 -1
  51. package/dist/primitives/suggestion/SuggestionTrigger.js +1 -1
  52. package/dist/primitives/thread/ThreadIf.js +1 -1
  53. package/dist/primitives/thread/ThreadRoot.d.ts.map +1 -1
  54. package/dist/primitives/thread/ThreadRoot.js +41 -9
  55. package/dist/primitives/thread/ThreadRoot.js.map +1 -1
  56. package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
  57. package/dist/primitives/thread/ThreadViewport.js +2 -13
  58. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  59. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  60. package/dist/primitives/thread/useThreadViewportAutoScroll.js +23 -8
  61. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  62. package/dist/primitives/threadList/ThreadListNew.js +1 -1
  63. package/dist/sandbox-host/SandboxHost.js +2 -0
  64. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  65. package/dist/unstable/useComposerInput.d.ts +2 -2
  66. package/dist/unstable/useComposerInput.js +1 -1
  67. package/dist/unstable/useComposerInput.js.map +1 -1
  68. package/dist/unstable/useComposerInputHistory.js +1 -1
  69. package/dist/unstable/useComposerInputHistory.js.map +1 -1
  70. package/dist/unstable/useLiveCompletionAdapter.d.ts +5 -0
  71. package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -1
  72. package/dist/unstable/useLiveCompletionAdapter.js +16 -3
  73. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  74. package/dist/unstable/useMentionAdapter.d.ts +1 -1
  75. package/dist/utils/smooth/SmoothContext.js +1 -1
  76. package/dist/utils/useToolArgsFieldStatus.js +1 -1
  77. package/package.json +9 -9
  78. package/src/assistant-transport.ts +94 -0
  79. package/src/index.ts +8 -4
  80. package/src/mcp-apps/McpAppsRemoteHost.test.ts +29 -3
  81. package/src/mcp-apps/McpAppsRemoteHost.ts +42 -7
  82. package/src/mcp-apps/bridge.test.ts +132 -0
  83. package/src/mcp-apps/bridge.ts +54 -30
  84. package/src/primitives/actionBar/ActionBarStopSpeaking.tsx +0 -9
  85. package/src/primitives/composer/ComposerInput.tsx +1 -1
  86. package/src/primitives/thread/ThreadRoot.test.tsx +251 -0
  87. package/src/primitives/thread/ThreadRoot.tsx +30 -0
  88. package/src/primitives/thread/ThreadViewport.tsx +1 -8
  89. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +152 -0
  90. package/src/primitives/thread/useThreadViewportAutoScroll.ts +26 -19
  91. package/src/sandbox-host/SandboxHost.test.tsx +29 -0
  92. package/src/sandbox-host/SandboxHost.tsx +2 -0
  93. package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +24 -3
  94. package/src/tests/RemoteThreadListRuntime.reloadMainThread.test.tsx +1 -2
  95. package/src/tests/augmentations.test.ts +49 -0
  96. package/src/tests/in-memory-thread-list.test.tsx +1 -1
  97. package/src/tests/threadMessageById.test.tsx +61 -1
  98. package/src/unstable/useComposerInput.ts +2 -2
  99. package/src/unstable/useComposerInputHistory.test.tsx +4 -4
  100. package/src/unstable/useComposerInputHistory.ts +1 -1
  101. package/src/unstable/useLiveCompletionAdapter.test.tsx +100 -0
  102. package/src/unstable/useLiveCompletionAdapter.ts +41 -17
  103. package/dist/client/InMemoryThreadList.d.ts +0 -12
  104. package/dist/client/InMemoryThreadList.d.ts.map +0 -1
  105. package/dist/client/InMemoryThreadList.js +0 -374
  106. package/dist/client/InMemoryThreadList.js.map +0 -1
  107. package/dist/legacy-runtime/cloud/auiV0.d.ts +0 -119
  108. package/dist/legacy-runtime/cloud/auiV0.d.ts.map +0 -1
  109. package/dist/legacy-runtime/cloud/auiV0.js +0 -137
  110. package/dist/legacy-runtime/cloud/auiV0.js.map +0 -1
  111. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.d.ts +0 -16
  112. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.d.ts.map +0 -1
  113. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.js +0 -36
  114. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.js.map +0 -1
  115. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts +0 -17
  116. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts.map +0 -1
  117. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js +0 -55
  118. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js.map +0 -1
  119. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts +0 -11
  120. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts.map +0 -1
  121. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js +0 -165
  122. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js.map +0 -1
  123. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts +0 -15
  124. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts.map +0 -1
  125. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +0 -81
  126. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js.map +0 -1
  127. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts +0 -126
  128. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts.map +0 -1
  129. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.js +0 -0
  130. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts +0 -14
  131. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +0 -1
  132. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +0 -290
  133. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +0 -1
  134. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.d.ts +0 -7
  135. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.d.ts.map +0 -1
  136. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js +0 -25
  137. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js.map +0 -1
  138. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.d.ts +0 -7
  139. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.d.ts.map +0 -1
  140. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js +0 -27
  141. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js.map +0 -1
  142. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.d.ts +0 -2
  143. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.js +0 -2
  144. package/src/client/InMemoryThreadList.ts +0 -247
  145. package/src/legacy-runtime/cloud/auiV0.ts +0 -331
  146. package/src/legacy-runtime/cloud/useCloudThreadListRuntime.ts +0 -31
  147. package/src/legacy-runtime/runtime-cores/assistant-transport/commandQueue.ts +0 -68
  148. package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.test.ts +0 -455
  149. package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.ts +0 -172
  150. package/src/legacy-runtime/runtime-cores/assistant-transport/runManager.ts +0 -113
  151. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-delivery.test.tsx +0 -79
  152. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-parentid.test.tsx +0 -97
  153. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-scheduling.test.ts +0 -307
  154. package/src/legacy-runtime/runtime-cores/assistant-transport/types.ts +0 -170
  155. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransport.spec.md +0 -141
  156. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx +0 -585
  157. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +0 -476
  158. package/src/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.ts +0 -20
  159. package/src/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.ts +0 -9
  160. package/src/legacy-runtime/runtime-cores/assistant-transport/utils.ts +0 -1
@@ -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","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
+ {"version":3,"file":"useLiveCompletionAdapter.js","names":["useCallback","useEffect","useLayoutEffect","useMemo","useRef","useState","Unstable_TriggerAdapter","Unstable_TriggerItem","Unstable_UseLiveCompletionAdapterOptions","fetcher","query","Promise","cacheKey","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","resolve","then","invalidatePending","cacheKeyRef","s","categories","categoryItems","search","queueMicrotask"],"sources":["../../src/unstable/useLiveCompletionAdapter.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} 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 /**\n * Identifies the fetcher's data source. Change this when switching accounts,\n * workspaces, or another boundary that should invalidate cached results.\n */\n readonly cacheKey?: string | number | undefined;\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, cacheKey, 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 Promise.resolve()\n .then(() => fetcherRef.current(query))\n .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 const cacheKeyRef = useRef(cacheKey);\n useLayoutEffect(() => {\n if (cacheKeyRef.current === cacheKey) return;\n cacheKeyRef.current = cacheKey;\n invalidatePending();\n retryableQueryRef.current = null;\n pendingRetryQueryRef.current = null;\n setState({ query: NO_QUERY, items: [], failed: false });\n }, [cacheKey, invalidatePending]);\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,6 +1,6 @@
1
1
  import { Unstable_DirectiveFormatter, Unstable_TriggerAdapter, Unstable_TriggerItem } from "@assistant-ui/core";
2
- import { ReadonlyJSONObject } from "assistant-stream/utils";
3
2
  import { FC } from "react";
3
+ import { ReadonlyJSONObject } from "assistant-stream/utils";
4
4
  //#region src/unstable/useMentionAdapter.d.ts
5
5
  /** Icon component shape consumed by `ComposerTriggerPopover`'s `iconMap`. */
6
6
  type Unstable_IconComponent = FC<{
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { createContextStoreHook } from "../../context/react/utils/createContextStoreHook.js";
3
- import { useAui } from "@assistant-ui/store";
4
3
  import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
4
+ import { useAui } from "@assistant-ui/store";
5
5
  import { createContext, forwardRef, useContext, useState } from "@assistant-ui/tap/react-shim";
6
6
  import { create } from "zustand";
7
7
  import { jsx } from "react/jsx-runtime";
@@ -1,5 +1,5 @@
1
- import { useAuiState } from "@assistant-ui/store";
2
1
  import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
2
+ import { useAuiState } from "@assistant-ui/store";
3
3
  import { getPartialJsonObjectFieldState } from "assistant-stream/utils";
4
4
  //#region src/utils/useToolArgsFieldStatus.ts
5
5
  const COMPLETE_STATUS = { type: "complete" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react",
3
- "version": "0.15.14",
3
+ "version": "0.15.15",
4
4
  "description": "Open-source TypeScript/React library for building production-grade AI chat experiences",
5
5
  "keywords": [
6
6
  "ai",
@@ -55,9 +55,9 @@
55
55
  ],
56
56
  "sideEffects": false,
57
57
  "dependencies": {
58
- "@assistant-ui/core": "^0.3.13",
59
- "@assistant-ui/store": "^0.3.9",
60
- "@assistant-ui/tap": "^0.9.12",
58
+ "@assistant-ui/core": "^0.3.14",
59
+ "@assistant-ui/store": "^0.3.10",
60
+ "@assistant-ui/tap": "^0.9.13",
61
61
  "@radix-ui/primitive": "^1.1.7",
62
62
  "@radix-ui/react-collection": "^1.1.15",
63
63
  "@radix-ui/react-compose-refs": "^1.1.5",
@@ -66,8 +66,8 @@
66
66
  "@radix-ui/react-use-callback-ref": "^1.1.4",
67
67
  "@radix-ui/react-use-controllable-state": "^1.2.6",
68
68
  "@radix-ui/react-use-escape-keydown": "^1.1.5",
69
- "assistant-cloud": "^0.1.40",
70
- "assistant-stream": "^0.3.37",
69
+ "assistant-cloud": "^0.1.41",
70
+ "assistant-stream": "^0.3.38",
71
71
  "radix-ui": "^1.6.7",
72
72
  "react-textarea-autosize": "^8.5.9",
73
73
  "safe-content-frame": "^0.0.27",
@@ -91,15 +91,15 @@
91
91
  "devDependencies": {
92
92
  "@testing-library/react": "^16.3.2",
93
93
  "@types/json-schema": "^7.0.15",
94
- "@types/node": "^26.1.2",
94
+ "@types/node": "^26.2.0",
95
95
  "@types/react": "^19.2.18",
96
96
  "@types/react-dom": "^19.2.4",
97
97
  "jsdom": "^30.0.1",
98
98
  "react": "^19.2.8",
99
99
  "react-dom": "^19.2.8",
100
100
  "vitest": "^4.1.10",
101
- "@assistant-ui/vite": "0.0.12",
102
- "@assistant-ui/x-buildutils": "0.0.22"
101
+ "@assistant-ui/vite": "0.0.14",
102
+ "@assistant-ui/x-buildutils": "0.0.23"
103
103
  },
104
104
  "publishConfig": {
105
105
  "access": "public",
@@ -0,0 +1,94 @@
1
+ "use client";
2
+
3
+ import type {
4
+ AssistantRuntime,
5
+ UserCommands as CoreUserCommands,
6
+ UserExternalState as CoreUserExternalState,
7
+ } from "@assistant-ui/core";
8
+ import {
9
+ useAssistantTransportRuntime as useCoreAssistantTransportRuntime,
10
+ useAssistantTransportSendCommand as useCoreAssistantTransportSendCommand,
11
+ useAssistantTransportState as useCoreAssistantTransportState,
12
+ } from "@assistant-ui/core/react";
13
+ import type {
14
+ AssistantTransportCommand as CoreAssistantTransportCommand,
15
+ AssistantTransportConnectionMetadata as CoreAssistantTransportConnectionMetadata,
16
+ AssistantTransportOptions as CoreAssistantTransportOptions,
17
+ AssistantTransportProtocol,
18
+ SendCommandsRequestBody as CoreSendCommandsRequestBody,
19
+ } from "@assistant-ui/core/react";
20
+ import type { UserCommands, UserExternalState } from "./augmentations";
21
+
22
+ export type { AssistantTransportProtocol };
23
+
24
+ export type SendCommandsRequestBody = Omit<
25
+ CoreSendCommandsRequestBody,
26
+ "commands"
27
+ > & {
28
+ commands: AssistantTransportCommand[];
29
+ };
30
+
31
+ export type AssistantTransportCommand =
32
+ | Exclude<CoreAssistantTransportCommand, CoreUserCommands>
33
+ | UserCommands;
34
+
35
+ export type AssistantTransportConnectionMetadata = Omit<
36
+ CoreAssistantTransportConnectionMetadata,
37
+ "pendingCommands"
38
+ > & {
39
+ pendingCommands: AssistantTransportCommand[];
40
+ };
41
+
42
+ export type AssistantTransportStateConverter<T> = (
43
+ state: T,
44
+ connectionMetadata: AssistantTransportConnectionMetadata,
45
+ ) => ReturnType<CoreAssistantTransportOptions<T>["converter"]>;
46
+
47
+ export type AssistantTransportOptions<T> = Omit<
48
+ CoreAssistantTransportOptions<T>,
49
+ "converter" | "prepareSendCommandsRequest" | "onError" | "onCancel"
50
+ > & {
51
+ converter: AssistantTransportStateConverter<T>;
52
+ prepareSendCommandsRequest?: (
53
+ body: SendCommandsRequestBody,
54
+ ) => Record<string, unknown> | Promise<Record<string, unknown>>;
55
+ onError?: (
56
+ error: Error,
57
+ params: {
58
+ commands: AssistantTransportCommand[];
59
+ updateState: (updater: (state: T) => T) => void;
60
+ },
61
+ ) => void | Promise<void>;
62
+ onCancel?: (params: {
63
+ commands: AssistantTransportCommand[];
64
+ updateState: (updater: (state: T) => T) => void;
65
+ error?: Error;
66
+ }) => void;
67
+ };
68
+
69
+ export const useAssistantTransportRuntime = <T>(
70
+ options: AssistantTransportOptions<T>,
71
+ ): AssistantRuntime => {
72
+ return useCoreAssistantTransportRuntime(
73
+ options as CoreAssistantTransportOptions<T>,
74
+ );
75
+ };
76
+
77
+ export const useAssistantTransportSendCommand = () => {
78
+ const send = useCoreAssistantTransportSendCommand();
79
+ return (command: AssistantTransportCommand) => {
80
+ send(command as CoreAssistantTransportCommand);
81
+ };
82
+ };
83
+
84
+ export function useAssistantTransportState(): UserExternalState;
85
+ export function useAssistantTransportState<T>(
86
+ selector: (state: UserExternalState) => T,
87
+ ): T;
88
+ export function useAssistantTransportState<T>(
89
+ selector: (state: UserExternalState) => T = (state) => state as T,
90
+ ): T | UserExternalState {
91
+ return useCoreAssistantTransportState(
92
+ selector as (state: CoreUserExternalState) => T,
93
+ );
94
+ }
package/src/index.ts CHANGED
@@ -43,7 +43,7 @@ export type {
43
43
  ThreadListItemState,
44
44
  } from "@assistant-ui/core";
45
45
 
46
- export { useCloudThreadListRuntime } from "./legacy-runtime/cloud/useCloudThreadListRuntime";
46
+ export { useCloudThreadListRuntime } from "@assistant-ui/core/react";
47
47
  export { AssistantCloud } from "assistant-cloud";
48
48
 
49
49
  // --- adapters/attachment ---
@@ -107,13 +107,13 @@ export {
107
107
  useAssistantTransportRuntime,
108
108
  useAssistantTransportSendCommand,
109
109
  useAssistantTransportState,
110
- } from "./legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime";
110
+ } from "./assistant-transport";
111
111
  export type {
112
112
  AssistantTransportConnectionMetadata,
113
113
  AssistantTransportCommand,
114
114
  AssistantTransportProtocol,
115
115
  SendCommandsRequestBody,
116
- } from "./legacy-runtime/runtime-cores/assistant-transport/types";
116
+ } from "./assistant-transport";
117
117
 
118
118
  // --- core ---
119
119
  export type {
@@ -430,7 +430,11 @@ export {
430
430
  export {
431
431
  InMemoryThreadList,
432
432
  type InMemoryThreadListProps,
433
- } from "./client/InMemoryThreadList";
433
+ } from "@assistant-ui/core/store";
434
+ export {
435
+ RemoteThreadList,
436
+ type RemoteThreadListProps,
437
+ } from "@assistant-ui/core/store";
434
438
  export { SingleThreadList } from "@assistant-ui/core/store";
435
439
 
436
440
  export * as INTERNAL from "./internal";
@@ -41,9 +41,15 @@ describe("McpAppsRemoteHost", () => {
41
41
  });
42
42
 
43
43
  it("posts serverId params verbatim for tool calls and resource loads", async () => {
44
- const fetch = vi.fn(async () =>
45
- Response.json({ content: [{ type: "text", text: "ok" }] }),
46
- ) as unknown as typeof globalThis.fetch;
44
+ const fetch = vi.fn(async (_input, init) => {
45
+ const request = JSON.parse(String(init?.body)) as { method: string };
46
+ return request.method === "mcp-apps/read-resource"
47
+ ? Response.json({
48
+ uri: "ui://example/search",
49
+ html: "<main>Search</main>",
50
+ })
51
+ : Response.json({ content: [{ type: "text", text: "ok" }] });
52
+ }) as unknown as typeof globalThis.fetch;
47
53
  const root = mount(fetch);
48
54
 
49
55
  try {
@@ -123,4 +129,24 @@ describe("McpAppsRemoteHost", () => {
123
129
  root.unmount();
124
130
  }
125
131
  });
132
+
133
+ it.each([{}, { uri: " ", html: "" }])(
134
+ "rejects malformed successful resource responses",
135
+ async (response) => {
136
+ const fetch = vi.fn(async () =>
137
+ Response.json(response),
138
+ ) as unknown as typeof globalThis.fetch;
139
+ const root = mount(fetch);
140
+
141
+ try {
142
+ await expect(
143
+ root.getValue().loadResource({ uri: "ui://example/search" }),
144
+ ).rejects.toThrow(
145
+ /Invalid MCP App host response "mcp-apps\/read-resource" from "\/api\/mcp-apps"/,
146
+ );
147
+ } finally {
148
+ root.unmount();
149
+ }
150
+ },
151
+ );
126
152
  });
@@ -39,6 +39,33 @@ const readErrorBody = async (res: Response): Promise<string | undefined> => {
39
39
  }
40
40
  };
41
41
 
42
+ const invalidMcpAppResource = (options: McpAppsRemoteHostOptions): never => {
43
+ throw new Error(
44
+ `Invalid MCP App host response "mcp-apps/read-resource" from "${options.url}": expected a resource with non-empty string "uri" and "html" fields`,
45
+ );
46
+ };
47
+
48
+ const parseMcpAppResource = (
49
+ value: unknown,
50
+ options: McpAppsRemoteHostOptions,
51
+ ): McpAppResource => {
52
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
53
+ return invalidMcpAppResource(options);
54
+ }
55
+
56
+ const resource = value as Record<string, unknown>;
57
+ if (
58
+ typeof resource.uri !== "string" ||
59
+ resource.uri.trim() === "" ||
60
+ typeof resource.html !== "string" ||
61
+ resource.html.trim() === ""
62
+ ) {
63
+ return invalidMcpAppResource(options);
64
+ }
65
+
66
+ return value as McpAppResource;
67
+ };
68
+
42
69
  async function postToHost(
43
70
  options: McpAppsRemoteHostOptions,
44
71
  method: string,
@@ -63,7 +90,14 @@ async function postToHost(
63
90
  `MCP App host request "${method}" to "${options.url}" failed with ${status}${body ? `: ${body}` : ""}`,
64
91
  );
65
92
  }
66
- return res.json();
93
+ try {
94
+ return await res.json();
95
+ } catch (cause) {
96
+ throw new Error(
97
+ `Invalid MCP App host response "${method}" from "${options.url}": expected valid JSON`,
98
+ { cause },
99
+ );
100
+ }
67
101
  }
68
102
 
69
103
  /**
@@ -91,12 +125,13 @@ const useMcpAppsRemoteHost = (
91
125
  };
92
126
  };
93
127
  return {
94
- loadResource: (params) =>
95
- postToHost(
96
- getCurrentOptions(),
97
- "mcp-apps/read-resource",
98
- params,
99
- ) as Promise<McpAppResource>,
128
+ loadResource: async (params) => {
129
+ const options = getCurrentOptions();
130
+ return parseMcpAppResource(
131
+ await postToHost(options, "mcp-apps/read-resource", params),
132
+ options,
133
+ );
134
+ },
100
135
  callTool: (params) =>
101
136
  postToHost(getCurrentOptions(), "tools/call", params),
102
137
  readResource: (params) =>
@@ -124,6 +124,53 @@ describe("createMcpAppBridge", () => {
124
124
  bridge.dispose();
125
125
  });
126
126
 
127
+ it.each([
128
+ [
129
+ "throws",
130
+ () => {
131
+ throw new Error("error callback failed");
132
+ },
133
+ ],
134
+ ["rejects", () => Promise.reject(new Error("error callback failed"))],
135
+ ])(
136
+ "returns an error response when onError %s",
137
+ async (_behavior, onErrorCallback) => {
138
+ const { frame, captured } = makeFrame();
139
+ const callTool = vi.fn().mockRejectedValue(new Error("tool failed"));
140
+ const consoleError = vi
141
+ .spyOn(console, "error")
142
+ .mockImplementation(() => {});
143
+ const onError = vi.fn(onErrorCallback);
144
+ const bridge = createMcpAppBridge({
145
+ frame,
146
+ handlers: { callTool, onError },
147
+ });
148
+
149
+ deliver(bridge, {
150
+ jsonrpc: "2.0",
151
+ id: 8,
152
+ method: "tools/call",
153
+ params: { name: "search" },
154
+ });
155
+ await flush();
156
+
157
+ expect(onError).toHaveBeenCalledWith(new Error("tool failed"));
158
+ expect(captured).toEqual([
159
+ {
160
+ jsonrpc: "2.0",
161
+ id: 8,
162
+ error: { code: -32603, message: "tool failed" },
163
+ },
164
+ ]);
165
+ expect(consoleError).toHaveBeenCalledWith(
166
+ "[assistant-ui] MCP App onError callback threw an error",
167
+ new Error("error callback failed"),
168
+ );
169
+ consoleError.mockRestore();
170
+ bridge.dispose();
171
+ },
172
+ );
173
+
127
174
  it("rejects tools/call for disallowed tool with -32602", async () => {
128
175
  const { frame, captured } = makeFrame();
129
176
  const callTool = vi.fn();
@@ -244,6 +291,60 @@ describe("createMcpAppBridge", () => {
244
291
  bridge.dispose();
245
292
  });
246
293
 
294
+ it("isolates throwing notification handlers and reports their errors", () => {
295
+ const { frame, captured } = makeFrame();
296
+ const initializedError = new Error("initialized callback failed");
297
+ const sizeChangeError = new Error("size change callback failed");
298
+ const teardownError = new Error("teardown callback failed");
299
+ const onError = vi.fn();
300
+ const bridge = createMcpAppBridge({
301
+ frame,
302
+ handlers: {
303
+ onInitialized: () => {
304
+ throw initializedError;
305
+ },
306
+ onSizeChange: () => {
307
+ throw sizeChangeError;
308
+ },
309
+ onLog: () => {
310
+ throw "log callback failed";
311
+ },
312
+ onRequestTeardown: () => {
313
+ throw teardownError;
314
+ },
315
+ onError,
316
+ },
317
+ });
318
+
319
+ expect(() => {
320
+ deliver(bridge, {
321
+ jsonrpc: "2.0",
322
+ method: "notifications/initialized",
323
+ });
324
+ deliver(bridge, {
325
+ jsonrpc: "2.0",
326
+ method: "notifications/size_changed",
327
+ });
328
+ deliver(bridge, {
329
+ jsonrpc: "2.0",
330
+ method: "notifications/log",
331
+ });
332
+ deliver(bridge, {
333
+ jsonrpc: "2.0",
334
+ method: "notifications/request_teardown",
335
+ });
336
+ }).not.toThrow();
337
+
338
+ expect(onError.mock.calls.map(([error]) => error)).toEqual([
339
+ initializedError,
340
+ sizeChangeError,
341
+ new Error("log callback failed"),
342
+ teardownError,
343
+ ]);
344
+ expect(captured).toEqual([]);
345
+ bridge.dispose();
346
+ });
347
+
247
348
  it("notifyToolInput / notifyToolResult / notifyHostContextChanged post both legacy and 2026-01-26 notifications", () => {
248
349
  const { frame, captured } = makeFrame();
249
350
  const bridge = createMcpAppBridge({ frame });
@@ -428,4 +529,35 @@ describe("createMcpAppBridge", () => {
428
529
  expect(onRequestTeardown).toHaveBeenCalledWith({ reason: "done" });
429
530
  bridge.dispose();
430
531
  });
532
+
533
+ it("isolates a throwing onError from widget error notifications", () => {
534
+ const { frame } = makeFrame();
535
+ const callbackError = new Error("error callback failed");
536
+ const consoleError = vi
537
+ .spyOn(console, "error")
538
+ .mockImplementation(() => {});
539
+ const onError = vi.fn(() => {
540
+ throw callbackError;
541
+ });
542
+ const bridge = createMcpAppBridge({
543
+ frame,
544
+ handlers: { callTool: vi.fn(), onError },
545
+ });
546
+
547
+ expect(() =>
548
+ deliver(bridge, {
549
+ jsonrpc: "2.0",
550
+ method: "notifications/error",
551
+ params: { message: "kaboom" },
552
+ }),
553
+ ).not.toThrow();
554
+
555
+ expect(onError).toHaveBeenCalledWith(new Error("kaboom"));
556
+ expect(consoleError).toHaveBeenCalledWith(
557
+ "[assistant-ui] MCP App onError callback threw an error",
558
+ callbackError,
559
+ );
560
+ consoleError.mockRestore();
561
+ bridge.dispose();
562
+ });
431
563
  });
@@ -123,6 +123,23 @@ export function createMcpAppBridge(
123
123
  });
124
124
  };
125
125
 
126
+ const reportErrorCallbackFailure = (error: unknown) => {
127
+ console.error(
128
+ "[assistant-ui] MCP App onError callback threw an error",
129
+ error,
130
+ );
131
+ };
132
+
133
+ const reportError = (error: Error) => {
134
+ try {
135
+ void Promise.resolve(handlers.onError?.(error)).catch(
136
+ reportErrorCallbackFailure,
137
+ );
138
+ } catch (callbackError) {
139
+ reportErrorCallbackFailure(callbackError);
140
+ }
141
+ };
142
+
126
143
  const handleRequest = async (req: McpAppJsonRpcRequest) => {
127
144
  try {
128
145
  const params = req.params;
@@ -376,42 +393,49 @@ export function createMcpAppBridge(
376
393
  }
377
394
  } catch (err) {
378
395
  const error = err instanceof Error ? err : new Error(String(err));
379
- handlers.onError?.(error);
396
+ reportError(error);
380
397
  errorResponse(req.id, JSONRPC_ERROR.internalError, error.message);
381
398
  }
382
399
  };
383
400
 
384
401
  const handleNotification = (note: McpAppJsonRpcNotification) => {
385
- switch (normalizeMethod(note.method)) {
386
- case "notifications/initialized": {
387
- handlers.onInitialized?.();
388
- return;
389
- }
390
- case "notifications/size_changed": {
391
- const p = (note.params ?? {}) as { width?: number; height?: number };
392
- handlers.onSizeChange?.({
393
- ...(typeof p.width === "number" ? { width: p.width } : {}),
394
- ...(typeof p.height === "number" ? { height: p.height } : {}),
395
- });
396
- return;
397
- }
398
- case "notifications/log": {
399
- handlers.onLog?.(note.params);
400
- return;
401
- }
402
- case "notifications/request_teardown": {
403
- handlers.onRequestTeardown?.(note.params);
404
- return;
405
- }
406
- case "notifications/error": {
407
- const p = (note.params ?? {}) as { message?: string };
408
- handlers.onError?.(
409
- new Error(typeof p.message === "string" ? p.message : "Widget error"),
410
- );
411
- return;
402
+ try {
403
+ switch (normalizeMethod(note.method)) {
404
+ case "notifications/initialized": {
405
+ handlers.onInitialized?.();
406
+ return;
407
+ }
408
+ case "notifications/size_changed": {
409
+ const p = (note.params ?? {}) as { width?: number; height?: number };
410
+ handlers.onSizeChange?.({
411
+ ...(typeof p.width === "number" ? { width: p.width } : {}),
412
+ ...(typeof p.height === "number" ? { height: p.height } : {}),
413
+ });
414
+ return;
415
+ }
416
+ case "notifications/log": {
417
+ handlers.onLog?.(note.params);
418
+ return;
419
+ }
420
+ case "notifications/request_teardown": {
421
+ handlers.onRequestTeardown?.(note.params);
422
+ return;
423
+ }
424
+ case "notifications/error": {
425
+ const p = (note.params ?? {}) as { message?: string };
426
+ reportError(
427
+ new Error(
428
+ typeof p.message === "string" ? p.message : "Widget error",
429
+ ),
430
+ );
431
+ return;
432
+ }
433
+ default:
434
+ return;
412
435
  }
413
- default:
414
- return;
436
+ } catch (err) {
437
+ const error = err instanceof Error ? err : new Error(String(err));
438
+ reportError(error);
415
439
  }
416
440
  };
417
441
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { forwardRef } from "react";
4
4
  import type { ActionButtonProps } from "../../utils/createActionButton";
5
- import { useEscapeKeydown } from "@radix-ui/react-use-escape-keydown";
6
5
  import { Primitive } from "../../utils/Primitive";
7
6
  import { composeEventHandlers } from "@radix-ui/primitive";
8
7
  import { useActionBarStopSpeaking as useActionBarStopSpeakingBehavior } from "@assistant-ui/core/react";
@@ -24,14 +23,6 @@ export const ActionBarPrimitiveStopSpeaking = forwardRef<
24
23
  >((props, ref) => {
25
24
  const callback = useActionBarStopSpeaking();
26
25
 
27
- // TODO this stops working if the user is not hovering over an older message
28
- useEscapeKeydown((e) => {
29
- if (callback) {
30
- e.preventDefault();
31
- callback();
32
- }
33
- });
34
-
35
26
  return (
36
27
  <Primitive.button
37
28
  type="button"
@@ -346,7 +346,7 @@ export const ComposerPrimitiveInput = forwardRef<
346
346
  )
347
347
  return undefined;
348
348
 
349
- return aui.on("threadListItem.switchedTo", focus);
349
+ return aui.on("threads.selectionChanged", focus);
350
350
  }, [unstable_focusOnThreadSwitched, focus, aui]);
351
351
 
352
352
  useEffect(() => {