@assistant-ui/react 0.14.14 → 0.14.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 (207) hide show
  1. package/README.md +5 -1
  2. package/dist/client/ExternalThread.d.ts +2 -12
  3. package/dist/client/ExternalThread.d.ts.map +1 -1
  4. package/dist/client/ExternalThread.js +30 -29
  5. package/dist/client/ExternalThread.js.map +1 -1
  6. package/dist/client/InMemoryThreadList.d.ts.map +1 -1
  7. package/dist/client/InMemoryThreadList.js +11 -10
  8. package/dist/client/InMemoryThreadList.js.map +1 -1
  9. package/dist/client/SingleThreadList.d.ts.map +1 -1
  10. package/dist/client/SingleThreadList.js +9 -8
  11. package/dist/client/SingleThreadList.js.map +1 -1
  12. package/dist/context/providers/ThreadViewportProvider.js +1 -1
  13. package/dist/context/providers/ThreadViewportProvider.js.map +1 -1
  14. package/dist/context/react/ThreadViewportContext.js +1 -1
  15. package/dist/context/react/utils/createContextHook.js +1 -1
  16. package/dist/context/react/utils/ensureBinding.js.map +1 -1
  17. package/dist/context/react/utils/useRuntimeState.js +1 -1
  18. package/dist/context/stores/ThreadViewport.js.map +1 -1
  19. package/dist/devtools/DevToolsHooks.js.map +1 -1
  20. package/dist/index.d.ts +4 -4
  21. package/dist/index.js +3 -3
  22. package/dist/legacy-runtime/AssistantRuntimeProvider.js +1 -1
  23. package/dist/legacy-runtime/cloud/auiV0.js +1 -1
  24. package/dist/legacy-runtime/hooks/AssistantContext.js.map +1 -1
  25. package/dist/legacy-runtime/hooks/AttachmentContext.js.map +1 -1
  26. package/dist/legacy-runtime/hooks/ComposerContext.js.map +1 -1
  27. package/dist/legacy-runtime/hooks/MessageContext.js.map +1 -1
  28. package/dist/legacy-runtime/hooks/MessagePartContext.js.map +1 -1
  29. package/dist/legacy-runtime/hooks/ThreadContext.js +1 -1
  30. package/dist/legacy-runtime/hooks/ThreadContext.js.map +1 -1
  31. package/dist/legacy-runtime/hooks/ThreadListItemContext.js.map +1 -1
  32. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js +1 -1
  33. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js +1 -1
  34. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +1 -1
  35. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +1 -1
  36. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  37. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js +1 -1
  38. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js +1 -1
  39. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  40. package/dist/mcp-apps/McpAppRenderer.js +7 -7
  41. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  42. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
  43. package/dist/mcp-apps/McpAppsRemoteHost.js +5 -4
  44. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
  45. package/dist/mcp-apps/app-frame.d.ts +1 -1
  46. package/dist/mcp-apps/app-frame.d.ts.map +1 -1
  47. package/dist/mcp-apps/app-frame.js +82 -104
  48. package/dist/mcp-apps/app-frame.js.map +1 -1
  49. package/dist/mcp-apps/bridge.d.ts +3 -3
  50. package/dist/mcp-apps/bridge.d.ts.map +1 -1
  51. package/dist/mcp-apps/bridge.js +35 -10
  52. package/dist/mcp-apps/bridge.js.map +1 -1
  53. package/dist/mcp-apps/types.d.ts +2 -12
  54. package/dist/mcp-apps/types.d.ts.map +1 -1
  55. package/dist/mcp-apps/types.js.map +1 -1
  56. package/dist/model-context/frame/useAssistantFrameHost.js +1 -1
  57. package/dist/model-context/makeAssistantVisible.js +1 -1
  58. package/dist/model-context/makeAssistantVisible.js.map +1 -1
  59. package/dist/primitives/actionBar/ActionBarCopy.js +1 -1
  60. package/dist/primitives/actionBar/ActionBarExportMarkdown.js +1 -1
  61. package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
  62. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +1 -1
  63. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +1 -1
  64. package/dist/primitives/actionBar/ActionBarInteractionContext.js +1 -1
  65. package/dist/primitives/actionBar/ActionBarRoot.js +1 -1
  66. package/dist/primitives/actionBar/ActionBarStopSpeaking.js +1 -1
  67. package/dist/primitives/actionBarMore/ActionBarMoreContent.js +1 -1
  68. package/dist/primitives/actionBarMore/ActionBarMoreItem.js +1 -1
  69. package/dist/primitives/actionBarMore/ActionBarMoreRoot.js +1 -1
  70. package/dist/primitives/actionBarMore/ActionBarMoreSeparator.js +1 -1
  71. package/dist/primitives/actionBarMore/ActionBarMoreTrigger.js +1 -1
  72. package/dist/primitives/assistantModal/AssistantModalAnchor.js +1 -1
  73. package/dist/primitives/assistantModal/AssistantModalContent.js +1 -1
  74. package/dist/primitives/assistantModal/AssistantModalRoot.js +1 -1
  75. package/dist/primitives/assistantModal/AssistantModalTrigger.js +1 -1
  76. package/dist/primitives/attachment/AttachmentRemove.js +1 -1
  77. package/dist/primitives/attachment/AttachmentRemove.js.map +1 -1
  78. package/dist/primitives/attachment/AttachmentRoot.js +1 -1
  79. package/dist/primitives/attachment/AttachmentThumb.js +1 -1
  80. package/dist/primitives/branchPicker/BranchPickerRoot.js +1 -1
  81. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +1 -1
  82. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js.map +1 -1
  83. package/dist/primitives/chainOfThought/ChainOfThoughtRoot.js +1 -1
  84. package/dist/primitives/composer/ComposerAddAttachment.js +1 -1
  85. package/dist/primitives/composer/ComposerAddAttachment.js.map +1 -1
  86. package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -1
  87. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  88. package/dist/primitives/composer/ComposerDictationTranscript.js +1 -1
  89. package/dist/primitives/composer/ComposerInput.js +1 -1
  90. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  91. package/dist/primitives/composer/ComposerInputPluginContext.js +1 -1
  92. package/dist/primitives/composer/ComposerQuote.js +1 -1
  93. package/dist/primitives/composer/ComposerQuote.js.map +1 -1
  94. package/dist/primitives/composer/ComposerRoot.js +1 -1
  95. package/dist/primitives/composer/ComposerSend.js +1 -1
  96. package/dist/primitives/composer/ComposerStopDictation.js +1 -1
  97. package/dist/primitives/composer/ComposerStopDictation.js.map +1 -1
  98. package/dist/primitives/composer/trigger/TriggerPopover.js +2 -2
  99. package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
  100. package/dist/primitives/composer/trigger/TriggerPopoverAction.js +1 -1
  101. package/dist/primitives/composer/trigger/TriggerPopoverBack.js +1 -1
  102. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js +1 -1
  103. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +1 -1
  104. package/dist/primitives/composer/trigger/TriggerPopoverItems.js +1 -1
  105. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
  106. package/dist/primitives/composer/trigger/TriggerPopoverResource.js +8 -7
  107. package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -1
  108. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +1 -1
  109. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
  110. package/dist/primitives/composer/trigger/triggerDetectionResource.js +5 -4
  111. package/dist/primitives/composer/trigger/triggerDetectionResource.js.map +1 -1
  112. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
  113. package/dist/primitives/composer/trigger/triggerKeyboardResource.js +8 -7
  114. package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
  115. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
  116. package/dist/primitives/composer/trigger/triggerNavigationResource.js +13 -12
  117. package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
  118. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  119. package/dist/primitives/composer/trigger/triggerSelectionResource.js +7 -6
  120. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  121. package/dist/primitives/error/ErrorMessage.js +1 -1
  122. package/dist/primitives/error/ErrorRoot.js +1 -1
  123. package/dist/primitives/message/MessagePartsGrouped.js +1 -1
  124. package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
  125. package/dist/primitives/message/MessageRoot.js +1 -1
  126. package/dist/primitives/message/MessageRoot.js.map +1 -1
  127. package/dist/primitives/messagePart/MessagePartImage.js +1 -1
  128. package/dist/primitives/messagePart/MessagePartText.js +1 -1
  129. package/dist/primitives/queueItem/QueueItemRemove.js +1 -1
  130. package/dist/primitives/queueItem/QueueItemRemove.js.map +1 -1
  131. package/dist/primitives/queueItem/QueueItemSteer.js +1 -1
  132. package/dist/primitives/queueItem/QueueItemSteer.js.map +1 -1
  133. package/dist/primitives/queueItem/QueueItemText.js +1 -1
  134. package/dist/primitives/reasoning/useScrollLock.js +1 -1
  135. package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
  136. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +1 -1
  137. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js.map +1 -1
  138. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +1 -1
  139. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
  140. package/dist/primitives/suggestion/SuggestionDescription.js +1 -1
  141. package/dist/primitives/suggestion/SuggestionTitle.js +1 -1
  142. package/dist/primitives/suggestion/SuggestionTrigger.js +1 -1
  143. package/dist/primitives/suggestion/SuggestionTrigger.js.map +1 -1
  144. package/dist/primitives/thread/ThreadRoot.js +1 -1
  145. package/dist/primitives/thread/ThreadScrollToBottom.js +1 -1
  146. package/dist/primitives/thread/ThreadScrollToBottom.js.map +1 -1
  147. package/dist/primitives/thread/ThreadViewport.js +1 -1
  148. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  149. package/dist/primitives/thread/ThreadViewportFooter.js +1 -1
  150. package/dist/primitives/thread/ThreadViewportFooter.js.map +1 -1
  151. package/dist/primitives/thread/topAnchor/topAnchorTurn.js.map +1 -1
  152. package/dist/primitives/thread/topAnchor/topAnchorUtils.js.map +1 -1
  153. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +1 -1
  154. package/dist/primitives/thread/useThreadViewportAutoScroll.js +1 -1
  155. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  156. package/dist/primitives/threadList/ThreadListNew.js +1 -1
  157. package/dist/primitives/threadList/ThreadListRoot.js +1 -1
  158. package/dist/primitives/threadListItem/ThreadListItemRoot.js +1 -1
  159. package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js +1 -1
  160. package/dist/primitives/threadListItemMore/ThreadListItemMoreItem.js +1 -1
  161. package/dist/primitives/threadListItemMore/ThreadListItemMoreSeparator.js +1 -1
  162. package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js +1 -1
  163. package/dist/sandbox-host/SandboxHost.d.ts +50 -0
  164. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -0
  165. package/dist/sandbox-host/SandboxHost.js +85 -0
  166. package/dist/sandbox-host/SandboxHost.js.map +1 -0
  167. package/dist/unstable/useMentionAdapter.js +1 -1
  168. package/dist/unstable/useMentionAdapter.js.map +1 -1
  169. package/dist/unstable/useSlashCommandAdapter.js +1 -1
  170. package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
  171. package/dist/utils/Primitive.js +1 -1
  172. package/dist/utils/createActionButton.js +1 -1
  173. package/dist/utils/createActionButton.js.map +1 -1
  174. package/dist/utils/hooks/useManagedRef.js +1 -1
  175. package/dist/utils/hooks/useMediaQuery.js +1 -1
  176. package/dist/utils/hooks/useMediaQuery.js.map +1 -1
  177. package/dist/utils/hooks/useOnResizeContent.js +1 -1
  178. package/dist/utils/hooks/useOnScrollToBottom.js +1 -1
  179. package/dist/utils/hooks/useSizeHandle.js +1 -1
  180. package/dist/utils/json/is-json.js.map +1 -1
  181. package/dist/utils/smooth/SmoothContext.js +1 -1
  182. package/dist/utils/smooth/SmoothContext.js.map +1 -1
  183. package/dist/utils/smooth/useSmooth.js +1 -1
  184. package/dist/utils/smooth/useSmooth.js.map +1 -1
  185. package/dist/utils/useToolArgsFieldStatus.d.ts +2 -2
  186. package/dist/utils/useToolArgsFieldStatus.d.ts.map +1 -1
  187. package/package.json +21 -20
  188. package/src/client/ExternalThread.ts +484 -515
  189. package/src/client/InMemoryThreadList.ts +153 -162
  190. package/src/client/SingleThreadList.ts +87 -84
  191. package/src/context/providers/ThreadViewportProvider.tsx +2 -2
  192. package/src/index.ts +8 -1
  193. package/src/mcp-apps/McpAppRenderer.tsx +28 -35
  194. package/src/mcp-apps/McpAppsRemoteHost.ts +25 -24
  195. package/src/mcp-apps/app-frame.tsx +100 -141
  196. package/src/mcp-apps/bridge.test.ts +100 -60
  197. package/src/mcp-apps/bridge.ts +43 -21
  198. package/src/mcp-apps/types.ts +2 -12
  199. package/src/primitives/composer/trigger/TriggerPopover.tsx +1 -1
  200. package/src/primitives/composer/trigger/TriggerPopoverResource.ts +75 -76
  201. package/src/primitives/composer/trigger/triggerDetectionResource.ts +6 -5
  202. package/src/primitives/composer/trigger/triggerKeyboardResource.ts +9 -13
  203. package/src/primitives/composer/trigger/triggerNavigationResource.ts +14 -19
  204. package/src/primitives/composer/trigger/triggerSelectionResource.ts +8 -7
  205. package/src/sandbox-host/SandboxHost.test.tsx +231 -0
  206. package/src/sandbox-host/SandboxHost.tsx +185 -0
  207. package/src/tests/local-runtime-queue.test.tsx +305 -0
@@ -1,4 +1,5 @@
1
- import { resource, tapConst, tapRef } from "@assistant-ui/tap";
1
+ import { useMemo, useRef } from "react";
2
+ import { resource } from "@assistant-ui/tap";
2
3
  import type {
3
4
  McpAppResource,
4
5
  McpAppsHost,
@@ -33,27 +34,27 @@ async function postToHost(
33
34
  * params }`, using the method names expected by the assistant-ui MCP Apps
34
35
  * guide.
35
36
  */
36
- export const McpAppsRemoteHost = resource(
37
- (options: McpAppsRemoteHostOptions): McpAppsHost => {
38
- const optionsRef = tapRef(options);
39
- optionsRef.current = options;
37
+ export const McpAppsRemoteHost = resource(function McpAppsRemoteHost(
38
+ options: McpAppsRemoteHostOptions,
39
+ ): McpAppsHost {
40
+ const optionsRef = useRef(options);
41
+ optionsRef.current = options;
40
42
 
41
- return tapConst(
42
- (): McpAppsHost => ({
43
- loadResource: (params) =>
44
- postToHost(
45
- optionsRef.current,
46
- "mcp-apps/read-resource",
47
- params,
48
- ) as Promise<McpAppResource>,
49
- callTool: (params) =>
50
- postToHost(optionsRef.current, "tools/call", params),
51
- readResource: (params) =>
52
- postToHost(optionsRef.current, "resources/read", params),
53
- listResources: (params) =>
54
- postToHost(optionsRef.current, "resources/list", params),
55
- }),
56
- [],
57
- );
58
- },
59
- );
43
+ return useMemo(
44
+ (): McpAppsHost => ({
45
+ loadResource: (params) =>
46
+ postToHost(
47
+ optionsRef.current,
48
+ "mcp-apps/read-resource",
49
+ params,
50
+ ) as Promise<McpAppResource>,
51
+ callTool: (params) =>
52
+ postToHost(optionsRef.current, "tools/call", params),
53
+ readResource: (params) =>
54
+ postToHost(optionsRef.current, "resources/read", params),
55
+ listResources: (params) =>
56
+ postToHost(optionsRef.current, "resources/list", params),
57
+ }),
58
+ [],
59
+ );
60
+ });
@@ -1,8 +1,13 @@
1
1
  "use client";
2
2
 
3
- import { type MutableRefObject, useEffect, useRef, useState } from "react";
4
- import { type RenderedFrame, SafeContentFrame } from "safe-content-frame";
3
+ import { type MutableRefObject, useEffect, useRef } from "react";
5
4
  import { type McpAppBridge, createMcpAppBridge } from "./bridge";
5
+ import {
6
+ SandboxHost,
7
+ type SandboxBridge,
8
+ type SandboxHostApi,
9
+ type SandboxHostFrame,
10
+ } from "../sandbox-host/SandboxHost";
6
11
  import type {
7
12
  McpAppBridgeHandlers,
8
13
  McpAppFrameProps,
@@ -31,7 +36,7 @@ function useBridgeNotify<T>(
31
36
  }
32
37
  notify(bridgeRef.current, value);
33
38
  lastSentRef.current = value;
34
- // oxlint-disable-next-line tap-hooks/exhaustive-deps -- refs are stable; notify is assumed stable; re-run only when value changes
39
+ // oxlint-disable-next-line react/exhaustive-deps -- refs are stable; notify is assumed stable; re-run only when value changes
35
40
  }, [value]);
36
41
  }
37
42
 
@@ -100,10 +105,6 @@ export function McpAppFrame({
100
105
  hostContext,
101
106
  maxHeight = DEFAULT_MAX_HEIGHT,
102
107
  }: McpAppFrameProps) {
103
- const containerRef = useRef<HTMLDivElement>(null);
104
- const [contentHeight, setContentHeight] = useState<number | undefined>(
105
- undefined,
106
- );
107
108
  const bridgeRef = useRef<McpAppBridge | null>(null);
108
109
  const lastSentInputRef = useRef<unknown>(undefined);
109
110
  const lastSentOutputRef = useRef<unknown>(undefined);
@@ -129,133 +130,93 @@ export function McpAppFrame({
129
130
  output,
130
131
  };
131
132
 
132
- const resourceUri = resource.uri;
133
+ const createBridge = (
134
+ frame: SandboxHostFrame,
135
+ host: SandboxHostApi,
136
+ ): SandboxBridge => {
137
+ const current = liveRef.current;
138
+ let initTimeoutId: ReturnType<typeof setTimeout> | null = null;
133
139
 
134
- useEffect(() => {
135
- const container = containerRef.current;
136
- if (!container) return;
140
+ const flushPending = () => {
141
+ if (widgetReadyRef.current) return;
142
+ widgetReadyRef.current = true;
143
+ const b = bridgeRef.current;
144
+ if (!b) return;
145
+ if (pendingInputRef.current !== undefined) {
146
+ b.notifyToolInput(pendingInputRef.current);
147
+ lastSentInputRef.current = pendingInputRef.current;
148
+ pendingInputRef.current = undefined;
149
+ }
150
+ if (pendingOutputRef.current !== undefined) {
151
+ b.notifyToolResult(pendingOutputRef.current);
152
+ lastSentOutputRef.current = pendingOutputRef.current;
153
+ pendingOutputRef.current = undefined;
154
+ }
155
+ if (pendingHostContextRef.current !== undefined) {
156
+ b.notifyHostContextChanged(pendingHostContextRef.current);
157
+ lastSentHostContextRef.current = pendingHostContextRef.current;
158
+ pendingHostContextRef.current = undefined;
159
+ }
160
+ };
137
161
 
138
- let cancelled = false;
139
- let initTimeoutId: ReturnType<typeof setTimeout> | null = null;
140
- let frame: RenderedFrame | null = null;
141
- const sb = sandbox;
142
- const html = resource.html;
162
+ const liveHandlers = buildLiveHandlers(current.handlers, liveRef);
163
+ const liveOnInitialized = liveHandlers.onInitialized;
164
+ const wrappedHandlers: McpAppBridgeHandlers = {
165
+ ...liveHandlers,
166
+ onInitialized: () => {
167
+ if (initTimeoutId !== null) {
168
+ clearTimeout(initTimeoutId);
169
+ initTimeoutId = null;
170
+ }
171
+ flushPending();
172
+ liveOnInitialized?.();
173
+ },
174
+ onSizeChange: (p) => {
175
+ if (p.height != null) host.setHeight(p.height);
176
+ liveHandlers.onSizeChange?.(p);
177
+ },
178
+ };
179
+
180
+ // Safety net: if the widget never sends notifications/initialized (broken
181
+ // or non-spec-compliant), flush the queue anyway so the host doesn't
182
+ // appear hung.
183
+ initTimeoutId = setTimeout(() => {
184
+ initTimeoutId = null;
185
+ flushPending();
186
+ }, INIT_TIMEOUT_MS);
143
187
 
144
- const scf = new SafeContentFrame(sb?.product ?? DEFAULT_PRODUCT, {
145
- ...(sb?.sandbox !== undefined && { sandbox: sb.sandbox }),
146
- ...(sb?.useShadowDom !== undefined && { useShadowDom: sb.useShadowDom }),
147
- ...(sb?.enableBrowserCaching !== undefined && {
148
- enableBrowserCaching: sb.enableBrowserCaching,
149
- }),
150
- ...(sb?.salt !== undefined && { salt: sb.salt }),
188
+ const bridge = createMcpAppBridge({
189
+ frame,
190
+ handlers: wrappedHandlers,
191
+ hostInfo: current.hostInfo,
192
+ hostContext: current.hostContext,
151
193
  });
194
+ bridgeRef.current = bridge;
152
195
 
153
- const renderOpts =
154
- sb?.unsafeDocumentWrite !== undefined
155
- ? { unsafeDocumentWrite: sb.unsafeDocumentWrite }
156
- : undefined;
196
+ if (current.input !== undefined) pendingInputRef.current = current.input;
197
+ if (current.output !== undefined) pendingOutputRef.current = current.output;
198
+ // hostContext is delivered inside the ui/initialize response; subsequent
199
+ // changes flow through useBridgeNotify's pending path.
157
200
 
158
- scf
159
- .renderHtml(html, container, renderOpts)
160
- .then((rendered) => {
161
- if (cancelled) {
162
- rendered.dispose();
163
- return;
164
- }
165
- frame = rendered;
166
- const current = liveRef.current;
167
- const liveHandlers = buildLiveHandlers(current.handlers, liveRef);
168
- const liveOnInitialized = liveHandlers.onInitialized;
169
- const flushPending = () => {
170
- if (widgetReadyRef.current) return;
171
- widgetReadyRef.current = true;
172
- const b = bridgeRef.current;
173
- if (!b) return;
174
- if (pendingInputRef.current !== undefined) {
175
- b.notifyToolInput(pendingInputRef.current);
176
- lastSentInputRef.current = pendingInputRef.current;
177
- pendingInputRef.current = undefined;
178
- }
179
- if (pendingOutputRef.current !== undefined) {
180
- b.notifyToolResult(pendingOutputRef.current);
181
- lastSentOutputRef.current = pendingOutputRef.current;
182
- pendingOutputRef.current = undefined;
183
- }
184
- if (pendingHostContextRef.current !== undefined) {
185
- b.notifyHostContextChanged(pendingHostContextRef.current);
186
- lastSentHostContextRef.current = pendingHostContextRef.current;
187
- pendingHostContextRef.current = undefined;
188
- }
189
- };
190
- const wrappedHandlers: McpAppBridgeHandlers = {
191
- ...liveHandlers,
192
- onInitialized: () => {
193
- if (initTimeoutId !== null) {
194
- clearTimeout(initTimeoutId);
195
- initTimeoutId = null;
196
- }
197
- flushPending();
198
- liveOnInitialized?.();
199
- },
200
- onSizeChange: (p) => {
201
- if (
202
- typeof p.height === "number" &&
203
- Number.isFinite(p.height) &&
204
- p.height > 0
205
- ) {
206
- setContentHeight(p.height);
207
- }
208
- liveHandlers.onSizeChange?.(p);
209
- },
210
- };
211
- // Safety net: if the widget never sends notifications/initialized
212
- // (broken or non-spec-compliant), flush the queue anyway so the host
213
- // doesn't appear hung.
214
- initTimeoutId = setTimeout(() => {
201
+ return {
202
+ onMessage: bridge.onMessage,
203
+ dispose: () => {
204
+ if (initTimeoutId !== null) {
205
+ clearTimeout(initTimeoutId);
215
206
  initTimeoutId = null;
216
- flushPending();
217
- }, INIT_TIMEOUT_MS);
218
- bridgeRef.current = createMcpAppBridge({
219
- frame: rendered,
220
- handlers: wrappedHandlers,
221
- hostInfo: current.hostInfo,
222
- hostContext: current.hostContext,
223
- });
224
-
225
- if (current.input !== undefined)
226
- pendingInputRef.current = current.input;
227
- if (current.output !== undefined)
228
- pendingOutputRef.current = current.output;
229
- // hostContext is delivered inside the ui/initialize response; subsequent
230
- // changes flow through useBridgeNotify's pending path.
231
- })
232
- .catch((err) => {
233
- liveRef.current.handlers?.onError?.(
234
- err instanceof Error ? err : new Error(String(err)),
235
- );
236
- });
237
-
238
- return () => {
239
- cancelled = true;
240
- if (initTimeoutId !== null) {
241
- clearTimeout(initTimeoutId);
242
- initTimeoutId = null;
243
- }
244
- bridgeRef.current?.dispose();
245
- bridgeRef.current = null;
246
- frame?.dispose();
247
- frame = null;
248
- lastSentInputRef.current = undefined;
249
- lastSentOutputRef.current = undefined;
250
- lastSentHostContextRef.current = undefined;
251
- widgetReadyRef.current = false;
252
- pendingInputRef.current = undefined;
253
- pendingOutputRef.current = undefined;
254
- pendingHostContextRef.current = undefined;
255
- setContentHeight(undefined);
207
+ }
208
+ bridge.dispose();
209
+ bridgeRef.current = null;
210
+ lastSentInputRef.current = undefined;
211
+ lastSentOutputRef.current = undefined;
212
+ lastSentHostContextRef.current = undefined;
213
+ widgetReadyRef.current = false;
214
+ pendingInputRef.current = undefined;
215
+ pendingOutputRef.current = undefined;
216
+ pendingHostContextRef.current = undefined;
217
+ },
256
218
  };
257
- // oxlint-disable-next-line tap-hooks/exhaustive-deps -- re-mount only on resource URI change; live values flow through liveRef
258
- }, [resourceUri]);
219
+ };
259
220
 
260
221
  useBridgeNotify(
261
222
  input,
@@ -282,22 +243,20 @@ export function McpAppFrame({
282
243
  (b, v) => b.notifyHostContextChanged(v),
283
244
  );
284
245
 
285
- const resolvedHeight =
286
- contentHeight != null ? Math.min(contentHeight, maxHeight) : undefined;
287
- const mergedStyle =
288
- resolvedHeight != null
289
- ? { ...sandbox?.style, height: resolvedHeight }
290
- : sandbox?.style;
291
-
292
246
  return (
293
- <div
294
- ref={containerRef}
295
- className={sandbox?.className}
296
- style={mergedStyle}
297
- data-mcp-app-resource={app.resourceUri}
298
- data-mcp-app-prefers-border={
299
- resource.meta?.prefersBorder ? "" : undefined
300
- }
247
+ <SandboxHost
248
+ content={{ html: resource.html }}
249
+ contentKey={resource.uri}
250
+ sandbox={{ ...sandbox, product: sandbox?.product ?? DEFAULT_PRODUCT }}
251
+ maxHeight={maxHeight}
252
+ createBridge={createBridge}
253
+ onError={(err) => liveRef.current.handlers?.onError?.(err)}
254
+ containerProps={{
255
+ "data-mcp-app-resource": app.resourceUri,
256
+ "data-mcp-app-prefers-border": resource.meta?.prefersBorder
257
+ ? ""
258
+ : undefined,
259
+ }}
301
260
  />
302
261
  );
303
262
  }
@@ -1,6 +1,10 @@
1
1
  // @vitest-environment jsdom
2
2
  import { describe, expect, it, vi } from "vitest";
3
- import { createMcpAppBridge, type McpAppBridgeFrame } from "./bridge";
3
+ import {
4
+ createMcpAppBridge,
5
+ type McpAppBridge,
6
+ type McpAppBridgeFrame,
7
+ } from "./bridge";
4
8
  import type {
5
9
  McpAppJsonRpcMessage,
6
10
  McpAppJsonRpcRequest,
@@ -24,13 +28,8 @@ function makeFrame() {
24
28
  return { frame, captured };
25
29
  }
26
30
 
27
- function dispatch(frame: McpAppBridgeFrame, message: McpAppJsonRpcMessage) {
28
- const event = new MessageEvent("message", {
29
- data: message,
30
- origin: frame.origin,
31
- source: frame.iframe.contentWindow,
32
- });
33
- window.dispatchEvent(event);
31
+ function deliver(bridge: McpAppBridge, message: McpAppJsonRpcMessage) {
32
+ bridge.onMessage(new MessageEvent("message", { data: message }));
34
33
  }
35
34
 
36
35
  async function flush() {
@@ -55,7 +54,7 @@ describe("createMcpAppBridge", () => {
55
54
  id: 1,
56
55
  method: "ui/initialize",
57
56
  };
58
- dispatch(frame, req);
57
+ deliver(bridge, req);
59
58
  await flush();
60
59
 
61
60
  expect(captured).toHaveLength(1);
@@ -69,6 +68,34 @@ describe("createMcpAppBridge", () => {
69
68
  expect(result["capabilities"]["ui"]["sendMessage"]).toBe(true);
70
69
  expect(result["capabilities"]["ui"]["openLink"]).toBe(false);
71
70
 
71
+ expect(result["hostInfo"]).toEqual({ name: "test-host", version: "9.9.9" });
72
+ expect(result["hostCapabilities"]).toEqual({
73
+ serverTools: {},
74
+ message: { text: {} },
75
+ });
76
+
77
+ bridge.dispose();
78
+ });
79
+
80
+ it("echoes the requested protocolVersion in the ui/initialize result", async () => {
81
+ const { frame, captured } = makeFrame();
82
+ const bridge = createMcpAppBridge({ frame });
83
+
84
+ deliver(bridge, {
85
+ jsonrpc: "2.0",
86
+ id: 1,
87
+ method: "ui/initialize",
88
+ params: { protocolVersion: "2026-01-26" },
89
+ });
90
+ await flush();
91
+
92
+ const result = (captured[0] as McpAppJsonRpcResponse).result as Record<
93
+ string,
94
+ any
95
+ >;
96
+ expect(result["protocolVersion"]).toBe("2026-01-26");
97
+ expect(result["hostCapabilities"]).toEqual({});
98
+
72
99
  bridge.dispose();
73
100
  });
74
101
 
@@ -77,7 +104,7 @@ describe("createMcpAppBridge", () => {
77
104
  const callTool = vi.fn().mockResolvedValue({ ok: true });
78
105
  const bridge = createMcpAppBridge({ frame, handlers: { callTool } });
79
106
 
80
- dispatch(frame, {
107
+ deliver(bridge, {
81
108
  jsonrpc: "2.0",
82
109
  id: 7,
83
110
  method: "tools/call",
@@ -105,7 +132,7 @@ describe("createMcpAppBridge", () => {
105
132
  handlers: { callTool, allowedTools: ["search"] },
106
133
  });
107
134
 
108
- dispatch(frame, {
135
+ deliver(bridge, {
109
136
  jsonrpc: "2.0",
110
137
  id: 2,
111
138
  method: "tools/call",
@@ -123,7 +150,7 @@ describe("createMcpAppBridge", () => {
123
150
  const { frame, captured } = makeFrame();
124
151
  const bridge = createMcpAppBridge({ frame });
125
152
 
126
- dispatch(frame, {
153
+ deliver(bridge, {
127
154
  jsonrpc: "2.0",
128
155
  id: 3,
129
156
  method: "tools/call",
@@ -141,7 +168,7 @@ describe("createMcpAppBridge", () => {
141
168
  const callTool = vi.fn();
142
169
  const bridge = createMcpAppBridge({ frame, handlers: { callTool } });
143
170
 
144
- dispatch(frame, {
171
+ deliver(bridge, {
145
172
  jsonrpc: "2.0",
146
173
  id: 11,
147
174
  method: "tools/call",
@@ -162,7 +189,7 @@ describe("createMcpAppBridge", () => {
162
189
  handlers: { requestDisplayMode },
163
190
  });
164
191
 
165
- dispatch(frame, {
192
+ deliver(bridge, {
166
193
  jsonrpc: "2.0",
167
194
  id: 13,
168
195
  method: "requestDisplayMode",
@@ -180,7 +207,7 @@ describe("createMcpAppBridge", () => {
180
207
  const openLink = vi.fn();
181
208
  const bridge = createMcpAppBridge({ frame, handlers: { openLink } });
182
209
 
183
- dispatch(frame, {
210
+ deliver(bridge, {
184
211
  jsonrpc: "2.0",
185
212
  id: 12,
186
213
  method: "openLink",
@@ -202,12 +229,12 @@ describe("createMcpAppBridge", () => {
202
229
  handlers: { onSizeChange, onInitialized },
203
230
  });
204
231
 
205
- dispatch(frame, {
232
+ deliver(bridge, {
206
233
  jsonrpc: "2.0",
207
234
  method: "notifications/size_changed",
208
235
  params: { width: 320, height: 240 },
209
236
  });
210
- dispatch(frame, {
237
+ deliver(bridge, {
211
238
  jsonrpc: "2.0",
212
239
  method: "notifications/initialized",
213
240
  });
@@ -217,7 +244,7 @@ describe("createMcpAppBridge", () => {
217
244
  bridge.dispose();
218
245
  });
219
246
 
220
- it("notifyToolInput / notifyToolResult / notifyHostContextChanged post correct notifications", () => {
247
+ it("notifyToolInput / notifyToolResult / notifyHostContextChanged post both legacy and 2026-01-26 notifications", () => {
221
248
  const { frame, captured } = makeFrame();
222
249
  const bridge = createMcpAppBridge({ frame });
223
250
 
@@ -231,16 +258,62 @@ describe("createMcpAppBridge", () => {
231
258
  method: "notifications/tools/call/input",
232
259
  params: { input: { a: 1 } },
233
260
  },
261
+ {
262
+ jsonrpc: "2.0",
263
+ method: "ui/notifications/tool-input",
264
+ params: { arguments: { a: 1 } },
265
+ },
234
266
  {
235
267
  jsonrpc: "2.0",
236
268
  method: "notifications/tools/call/result",
237
269
  params: { result: { ok: 1 } },
238
270
  },
271
+ {
272
+ jsonrpc: "2.0",
273
+ method: "ui/notifications/tool-result",
274
+ params: { ok: 1 },
275
+ },
239
276
  {
240
277
  jsonrpc: "2.0",
241
278
  method: "notifications/host_context/changed",
242
279
  params: { theme: "light" },
243
280
  },
281
+ {
282
+ jsonrpc: "2.0",
283
+ method: "ui/notifications/host-context-changed",
284
+ params: { theme: "light" },
285
+ },
286
+ ]);
287
+ bridge.dispose();
288
+ });
289
+
290
+ it("wraps non-object and array tool results in a valid content block for the spec dialect", () => {
291
+ const { frame, captured } = makeFrame();
292
+ const bridge = createMcpAppBridge({ frame });
293
+
294
+ bridge.notifyToolResult("done");
295
+ bridge.notifyToolResult([1, 2]);
296
+ bridge.notifyToolInput(null);
297
+
298
+ const spec = captured.filter((c) =>
299
+ (c as { method?: string }).method?.startsWith("ui/notifications/"),
300
+ );
301
+ expect(spec).toEqual([
302
+ {
303
+ jsonrpc: "2.0",
304
+ method: "ui/notifications/tool-result",
305
+ params: { content: [{ type: "text", text: "done" }] },
306
+ },
307
+ {
308
+ jsonrpc: "2.0",
309
+ method: "ui/notifications/tool-result",
310
+ params: { content: [{ type: "text", text: "1,2" }] },
311
+ },
312
+ {
313
+ jsonrpc: "2.0",
314
+ method: "ui/notifications/tool-input",
315
+ params: {},
316
+ },
244
317
  ]);
245
318
  bridge.dispose();
246
319
  });
@@ -254,13 +327,13 @@ describe("createMcpAppBridge", () => {
254
327
  handlers: { readResource, listResources },
255
328
  });
256
329
 
257
- dispatch(frame, {
330
+ deliver(bridge, {
258
331
  jsonrpc: "2.0",
259
332
  id: 20,
260
333
  method: "resources/read",
261
334
  params: { uri: "ui://app/x" },
262
335
  });
263
- dispatch(frame, {
336
+ deliver(bridge, {
264
337
  jsonrpc: "2.0",
265
338
  id: 21,
266
339
  method: "resources/list",
@@ -279,13 +352,13 @@ describe("createMcpAppBridge", () => {
279
352
  const { frame, captured } = makeFrame();
280
353
  const bridge = createMcpAppBridge({ frame });
281
354
 
282
- dispatch(frame, {
355
+ deliver(bridge, {
283
356
  jsonrpc: "2.0",
284
357
  id: 22,
285
358
  method: "resources/read",
286
359
  params: { uri: "ui://x" },
287
360
  });
288
- dispatch(frame, { jsonrpc: "2.0", id: 23, method: "resources/list" });
361
+ deliver(bridge, { jsonrpc: "2.0", id: 23, method: "resources/list" });
289
362
  await flush();
290
363
 
291
364
  expect((captured[0] as McpAppJsonRpcResponse).error?.code).toBe(-32601);
@@ -302,13 +375,13 @@ describe("createMcpAppBridge", () => {
302
375
  handlers: { sendMessage, updateModelContext },
303
376
  });
304
377
 
305
- dispatch(frame, {
378
+ deliver(bridge, {
306
379
  jsonrpc: "2.0",
307
380
  id: 30,
308
381
  method: "sendMessage",
309
382
  params: { text: "hi" },
310
383
  });
311
- dispatch(frame, {
384
+ deliver(bridge, {
312
385
  jsonrpc: "2.0",
313
386
  id: 31,
314
387
  method: "updateModelContext",
@@ -334,17 +407,17 @@ describe("createMcpAppBridge", () => {
334
407
  handlers: { onLog, onError, onRequestTeardown },
335
408
  });
336
409
 
337
- dispatch(frame, {
410
+ deliver(bridge, {
338
411
  jsonrpc: "2.0",
339
412
  method: "notifications/log",
340
413
  params: { level: "info", message: "hello" },
341
414
  });
342
- dispatch(frame, {
415
+ deliver(bridge, {
343
416
  jsonrpc: "2.0",
344
417
  method: "notifications/error",
345
418
  params: { message: "kaboom" },
346
419
  });
347
- dispatch(frame, {
420
+ deliver(bridge, {
348
421
  jsonrpc: "2.0",
349
422
  method: "notifications/request_teardown",
350
423
  params: { reason: "done" },
@@ -355,37 +428,4 @@ describe("createMcpAppBridge", () => {
355
428
  expect(onRequestTeardown).toHaveBeenCalledWith({ reason: "done" });
356
429
  bridge.dispose();
357
430
  });
358
-
359
- it("ignores messages from wrong origin or wrong source", async () => {
360
- const { frame, captured } = makeFrame();
361
- const callTool = vi.fn();
362
- const bridge = createMcpAppBridge({ frame, handlers: { callTool } });
363
-
364
- const msg: McpAppJsonRpcMessage = {
365
- jsonrpc: "2.0",
366
- id: 1,
367
- method: "tools/call",
368
- params: { name: "search" },
369
- };
370
-
371
- window.dispatchEvent(
372
- new MessageEvent("message", {
373
- data: msg,
374
- origin: "https://attacker.example",
375
- source: frame.iframe.contentWindow,
376
- }),
377
- );
378
- window.dispatchEvent(
379
- new MessageEvent("message", {
380
- data: msg,
381
- origin: frame.origin,
382
- source: window,
383
- }),
384
- );
385
- await flush();
386
-
387
- expect(callTool).not.toHaveBeenCalled();
388
- expect(captured).toHaveLength(0);
389
- bridge.dispose();
390
- });
391
431
  });