@assistant-ui/react 0.15.17 → 0.15.19

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 (169) hide show
  1. package/README.md +8 -2
  2. package/dist/context/providers/ThreadViewportProvider.js +2 -2
  3. package/dist/context/providers/ThreadViewportProvider.js.map +1 -1
  4. package/dist/hooks/useToolCallElapsed.js.map +1 -1
  5. package/dist/index.d.ts +7 -4
  6. package/dist/index.js +5 -3
  7. package/dist/mcp-apps/McpAppRenderer.d.ts +30 -8
  8. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  9. package/dist/mcp-apps/McpAppRenderer.js +91 -47
  10. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  11. package/dist/mcp-apps/app-frame.d.ts.map +1 -1
  12. package/dist/mcp-apps/app-frame.js +57 -33
  13. package/dist/mcp-apps/app-frame.js.map +1 -1
  14. package/dist/mcp-apps/bridge.js +9 -4
  15. package/dist/mcp-apps/bridge.js.map +1 -1
  16. package/dist/mcp-apps/index.d.ts +2 -2
  17. package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
  18. package/dist/primitives/assistantModal/AssistantModalRoot.js +47 -79
  19. package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
  20. package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
  21. package/dist/primitives/composer/ComposerAttachmentDropzone.js +61 -138
  22. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  23. package/dist/primitives/composer/ComposerInput.js +3 -8
  24. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  25. package/dist/primitives/composer/ComposerInputPluginContext.d.ts +1 -2
  26. package/dist/primitives/composer/ComposerInputPluginContext.d.ts.map +1 -1
  27. package/dist/primitives/composer/ComposerInputPluginContext.js +1 -6
  28. package/dist/primitives/composer/ComposerInputPluginContext.js.map +1 -1
  29. package/dist/primitives/composer/trigger/TriggerPopoverAction.js +46 -15
  30. package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
  31. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +40 -10
  32. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
  33. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts +6 -0
  34. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts.map +1 -0
  35. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js +9 -0
  36. package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js.map +1 -0
  37. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
  38. package/dist/primitives/composer/trigger/triggerKeyboardResource.js +53 -61
  39. package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
  40. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
  41. package/dist/primitives/composer/trigger/triggerNavigationResource.js +129 -141
  42. package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
  43. package/dist/primitives/message/MessageParts.js +2 -14
  44. package/dist/primitives/message/MessageParts.js.map +1 -1
  45. package/dist/primitives/message/MessagePartsGrouped.d.ts +3 -3
  46. package/dist/primitives/message/MessagePartsGrouped.js +2 -2
  47. package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
  48. package/dist/primitives/messagePart/MessagePartText.d.ts +2 -0
  49. package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
  50. package/dist/primitives/messagePart/MessagePartText.js +13 -36
  51. package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
  52. package/dist/primitives/reasoning/useScrollLock.js +3 -1
  53. package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
  54. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  55. package/dist/primitives/thread/useThreadViewportAutoScroll.js +3 -3
  56. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  57. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  58. package/dist/sandbox-host/SandboxHost.js +36 -9
  59. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  60. package/dist/unstable/useLiveCompletionAdapter.js +24 -2
  61. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  62. package/dist/unstable/useMentionAdapter.d.ts +9 -3
  63. package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
  64. package/dist/unstable/useMentionAdapter.js +50 -43
  65. package/dist/unstable/useMentionAdapter.js.map +1 -1
  66. package/dist/unstable/useMessageStallDetection.d.ts +4 -5
  67. package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
  68. package/dist/unstable/useMessageStallDetection.js +16 -15
  69. package/dist/unstable/useMessageStallDetection.js.map +1 -1
  70. package/dist/unstable/useModelContextSnapshot.d.ts +34 -0
  71. package/dist/unstable/useModelContextSnapshot.d.ts.map +1 -0
  72. package/dist/unstable/useModelContextSnapshot.js +61 -0
  73. package/dist/unstable/useModelContextSnapshot.js.map +1 -0
  74. package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
  75. package/dist/unstable/useSlashCommandAdapter.js +2 -8
  76. package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
  77. package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts +14 -0
  78. package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts.map +1 -0
  79. package/dist/unstable/webmcp/WebMcpRegistrationResource.js +85 -0
  80. package/dist/unstable/webmcp/WebMcpRegistrationResource.js.map +1 -0
  81. package/dist/unstable/webmcp/convertTools.d.ts +21 -0
  82. package/dist/unstable/webmcp/convertTools.d.ts.map +1 -0
  83. package/dist/unstable/webmcp/convertTools.js +161 -0
  84. package/dist/unstable/webmcp/convertTools.js.map +1 -0
  85. package/dist/unstable/webmcp/useWebMcpProvider.d.ts +21 -0
  86. package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -0
  87. package/dist/unstable/webmcp/useWebMcpProvider.js +106 -0
  88. package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -0
  89. package/dist/unstable/webmcp/webmcp-host.d.ts +39 -0
  90. package/dist/unstable/webmcp/webmcp-host.d.ts.map +1 -0
  91. package/dist/unstable/webmcp/webmcp-host.js +29 -0
  92. package/dist/unstable/webmcp/webmcp-host.js.map +1 -0
  93. package/dist/utils/Primitive.d.ts +10 -2
  94. package/dist/utils/Primitive.d.ts.map +1 -1
  95. package/dist/utils/Primitive.js +25 -6
  96. package/dist/utils/Primitive.js.map +1 -1
  97. package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
  98. package/dist/utils/getSelectionMessageId.js +17 -3
  99. package/dist/utils/getSelectionMessageId.js.map +1 -1
  100. package/package.json +13 -13
  101. package/src/context/providers/ThreadViewportProvider.tsx +2 -2
  102. package/src/hooks/useToolCallElapsed.ts +3 -0
  103. package/src/index.ts +14 -1
  104. package/src/mcp-apps/McpAppRenderer.test.tsx +253 -5
  105. package/src/mcp-apps/McpAppRenderer.tsx +147 -52
  106. package/src/mcp-apps/app-frame.test.tsx +437 -1
  107. package/src/mcp-apps/app-frame.tsx +76 -21
  108. package/src/mcp-apps/bridge.test.ts +98 -0
  109. package/src/mcp-apps/bridge.ts +14 -3
  110. package/src/mcp-apps/index.ts +5 -1
  111. package/src/primitives/assistantModal/AssistantModalRoot.test.tsx +123 -0
  112. package/src/primitives/assistantModal/AssistantModalRoot.tsx +11 -31
  113. package/src/primitives/assistantModal/AssistantModalTrigger.test.tsx +1 -16
  114. package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +43 -0
  115. package/src/primitives/composer/ComposerAttachmentDropzone.tsx +2 -10
  116. package/src/primitives/composer/ComposerInput.test.tsx +22 -0
  117. package/src/primitives/composer/ComposerInput.tsx +5 -9
  118. package/src/primitives/composer/ComposerInputPluginContext.tsx +0 -10
  119. package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +4 -2
  120. package/src/primitives/composer/trigger/TriggerPopoverBehavior.test.tsx +109 -0
  121. package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +4 -2
  122. package/src/primitives/composer/trigger/matchesTriggerItemQuery.test.ts +52 -0
  123. package/src/primitives/composer/trigger/matchesTriggerItemQuery.ts +13 -0
  124. package/src/primitives/composer/trigger/triggerKeyboardResource.ts +13 -7
  125. package/src/primitives/composer/trigger/triggerNavigationResource.ts +8 -13
  126. package/src/primitives/message/MessageParts.tsx +1 -21
  127. package/src/primitives/message/MessagePartsGrouped.test.tsx +93 -0
  128. package/src/primitives/message/MessagePartsGrouped.tsx +6 -6
  129. package/src/primitives/messagePart/MessagePartText.test.tsx +69 -0
  130. package/src/primitives/messagePart/MessagePartText.tsx +22 -9
  131. package/src/primitives/reasoning/useScrollLock.test.tsx +129 -0
  132. package/src/primitives/reasoning/useScrollLock.ts +15 -1
  133. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +38 -1
  134. package/src/primitives/thread/useThreadViewportAutoScroll.ts +7 -6
  135. package/src/sandbox-host/SandboxHost.test.tsx +160 -2
  136. package/src/sandbox-host/SandboxHost.tsx +50 -10
  137. package/src/tests/MessageParts.rendererOptions.test.tsx +117 -0
  138. package/src/tests/toolCallTiming.test.tsx +55 -1
  139. package/src/unstable/useComposerInputHistory.test.tsx +2 -1
  140. package/src/unstable/useLiveCompletionAdapter.test.tsx +202 -1
  141. package/src/unstable/useLiveCompletionAdapter.ts +38 -8
  142. package/src/unstable/useMentionAdapter.test.tsx +383 -0
  143. package/src/unstable/useMentionAdapter.ts +94 -57
  144. package/src/unstable/useMessageStallDetection.ts +26 -19
  145. package/src/unstable/useModelContextSnapshot.test.tsx +132 -0
  146. package/src/unstable/useModelContextSnapshot.ts +67 -0
  147. package/src/unstable/useSlashCommandAdapter.ts +2 -9
  148. package/src/unstable/webmcp/WebMcpRegistrationResource.ts +92 -0
  149. package/src/unstable/webmcp/__tests__/webmcp.fake.tsx +168 -0
  150. package/src/unstable/webmcp/convertTools.test.ts +553 -0
  151. package/src/unstable/webmcp/convertTools.ts +260 -0
  152. package/src/unstable/webmcp/useWebMcpProvider.test.tsx +466 -0
  153. package/src/unstable/webmcp/useWebMcpProvider.ts +148 -0
  154. package/src/unstable/webmcp/webmcp-host.test.ts +142 -0
  155. package/src/unstable/webmcp/webmcp-host.ts +83 -0
  156. package/src/utils/Primitive.test.tsx +46 -1
  157. package/src/utils/Primitive.tsx +37 -7
  158. package/src/utils/getSelectionMessageId.test.ts +176 -1
  159. package/src/utils/getSelectionMessageId.ts +21 -3
  160. package/dist/utils/invokeCallbackSafely.d.ts +0 -5
  161. package/dist/utils/invokeCallbackSafely.d.ts.map +0 -1
  162. package/dist/utils/invokeCallbackSafely.js +0 -15
  163. package/dist/utils/invokeCallbackSafely.js.map +0 -1
  164. package/dist/utils/useToolArgsFieldStatus.d.ts +0 -19
  165. package/dist/utils/useToolArgsFieldStatus.d.ts.map +0 -1
  166. package/dist/utils/useToolArgsFieldStatus.js +0 -23
  167. package/dist/utils/useToolArgsFieldStatus.js.map +0 -1
  168. package/src/utils/invokeCallbackSafely.ts +0 -11
  169. package/src/utils/useToolArgsFieldStatus.ts +0 -18
@@ -6,10 +6,13 @@ import {
6
6
  useMemo,
7
7
  useRef,
8
8
  useState,
9
- type MutableRefObject,
10
9
  type ReactNode,
11
10
  } from "react";
12
- import type { McpAppMetadata } from "@assistant-ui/core";
11
+ import type {
12
+ McpAppMetadata,
13
+ TextMessagePart,
14
+ ToolCallMessagePart,
15
+ } from "@assistant-ui/core";
13
16
  import type {
14
17
  ToolCallMessagePartComponent,
15
18
  ToolCallMessagePartProps,
@@ -30,13 +33,11 @@ import type {
30
33
  import { getMcpAppFromToolPart } from "./utils";
31
34
  import { isRecord } from "@assistant-ui/core/internal";
32
35
 
33
- export type McpAppRendererOptions = {
34
- /**
35
- * Provides the data-plane operations the widget can request
36
- * (`loadResource`, `callTool`, `readResource`, `listResources`). Use
37
- * `McpAppsRemoteHost({ url })` for the default HTTP-route convention.
38
- */
39
- host: ResourceElement<McpAppsHost>;
36
+ /**
37
+ * Options that apply to a single MCP app. `McpAppRenderer` takes them as
38
+ * thread-wide defaults, and its `forPart` resolver overrides them for one part.
39
+ */
40
+ export type McpAppPartOptions = {
40
41
  /** Sandbox + container styling. Passes through to SafeContentFrame. */
41
42
  sandbox?: McpAppSandboxConfig;
42
43
  /**
@@ -51,6 +52,10 @@ export type McpAppRendererOptions = {
51
52
  /**
52
53
  * Optional widget interaction and lifecycle handlers. Data-plane handlers
53
54
  * (`callTool`, `readResource`, `listResources`) always use `host`.
55
+ *
56
+ * Which handlers a widget may call is captured when its frame mounts, so a
57
+ * `forPart` resolver returns the same handler keys for a part on every
58
+ * render even when the implementations change.
54
59
  */
55
60
  handlers?: Omit<
56
61
  McpAppBridgeHandlers,
@@ -64,6 +69,26 @@ export type McpAppRendererOptions = {
64
69
  errorFallback?: ReactNode | ((error: Error) => ReactNode);
65
70
  };
66
71
 
72
+ export type McpAppRendererOptions = McpAppPartOptions & {
73
+ /**
74
+ * Provides the data-plane operations the widget can request
75
+ * (`loadResource`, `callTool`, `readResource`, `listResources`). Use
76
+ * `McpAppsRemoteHost({ url })` for the default HTTP-route convention.
77
+ */
78
+ host: ResourceElement<McpAppsHost>;
79
+ /**
80
+ * Resolves the options for one part. Each returned value replaces the
81
+ * thread-wide option of the same name, so a host can give every MCP app its
82
+ * own display mode and answer `requestDisplayMode` for the part that asked.
83
+ *
84
+ * `handlers` is the exception and merges per key. Its keys are capabilities
85
+ * negotiated with the widget rather than data, so replacing the whole bag to
86
+ * add one handler would withdraw the others from that part's widget with
87
+ * nothing to observe it by.
88
+ */
89
+ forPart?: (part: ToolCallMessagePart) => McpAppPartOptions;
90
+ };
91
+
67
92
  type LoadedResourceState = {
68
93
  host: McpAppsHost;
69
94
  resourceUri: string;
@@ -72,7 +97,31 @@ type LoadedResourceState = {
72
97
  error?: Error;
73
98
  };
74
99
 
75
- type UseHostStore = UseBoundStore<StoreApi<{ host: McpAppsHost }>>;
100
+ type RendererState = {
101
+ host: McpAppsHost;
102
+ options: McpAppRendererOptions;
103
+ };
104
+
105
+ type UseRendererStore = UseBoundStore<StoreApi<RendererState>>;
106
+
107
+ // Callers pass options as an object literal, so the resource hands this a fresh
108
+ // identity on every run; comparing the values keeps an unchanged run from
109
+ // re-rendering every mounted part. `host` is excluded because it is an element
110
+ // rebuilt on every run, and the store carries the resolved host separately.
111
+ const isSameOptions = (
112
+ a: McpAppRendererOptions,
113
+ b: McpAppRendererOptions,
114
+ ): boolean => {
115
+ const aKeys = Object.keys(a).filter((key) => key !== "host");
116
+ const bKeys = Object.keys(b).filter((key) => key !== "host");
117
+ if (aKeys.length !== bKeys.length) return false;
118
+ const aValues = a as Record<string, unknown>;
119
+ const bValues = b as Record<string, unknown>;
120
+ return aKeys.every(
121
+ (key) =>
122
+ Object.hasOwn(bValues, key) && Object.is(aValues[key], bValues[key]),
123
+ );
124
+ };
76
125
 
77
126
  function getInput(part: {
78
127
  status: { type: string };
@@ -92,27 +141,67 @@ const defaultOpenLink = ({ url }: { url: string }) => {
92
141
  window.open(url, "_blank", "noopener,noreferrer");
93
142
  };
94
143
 
95
- function extractSendMessageText(params: unknown): string | undefined {
96
- if (typeof params === "string") return params;
144
+ function extractSendMessageTexts(params: unknown): string[] | undefined {
145
+ if (typeof params === "string") return [params];
97
146
  if (!params || typeof params !== "object") return undefined;
98
147
  const obj = params as Record<string, unknown>;
99
- if (typeof obj["prompt"] === "string") return obj["prompt"];
100
- if (typeof obj["text"] === "string") return obj["text"];
101
- if (typeof obj["message"] === "string") return obj["message"];
148
+ if (typeof obj["prompt"] === "string") return [obj["prompt"]];
149
+ if (typeof obj["text"] === "string") return [obj["text"]];
150
+ if (typeof obj["message"] === "string") return [obj["message"]];
151
+ if (Array.isArray(obj["content"])) {
152
+ return obj["content"]
153
+ .filter(
154
+ (block): block is { type: "text"; text: string } =>
155
+ isRecord(block) &&
156
+ block["type"] === "text" &&
157
+ typeof block["text"] === "string",
158
+ )
159
+ .map((block) => block.text);
160
+ }
102
161
  return undefined;
103
162
  }
104
163
 
164
+ function extractSendMessageContent(
165
+ params: unknown,
166
+ ): { content: TextMessagePart[] } | { reason: string } {
167
+ const texts = extractSendMessageTexts(params);
168
+ if (!texts) return { reason: "unrecognised params shape" };
169
+ const content = texts
170
+ .filter((text) => text !== "")
171
+ .map((text): TextMessagePart => ({ type: "text", text }));
172
+ if (content.length === 0) {
173
+ return { reason: "no text content to append" };
174
+ }
175
+ return { content };
176
+ }
177
+
178
+ function resolvePartOptions(
179
+ options: McpAppRendererOptions,
180
+ part: ToolCallMessagePartProps,
181
+ ): McpAppRendererOptions {
182
+ const overrides = options.forPart?.(part);
183
+ if (!overrides) return options;
184
+ const handlers =
185
+ options.handlers && overrides.handlers
186
+ ? { ...options.handlers, ...overrides.handlers }
187
+ : (overrides.handlers ?? options.handlers);
188
+ return {
189
+ ...options,
190
+ ...overrides,
191
+ ...(handlers === undefined ? {} : { handlers }),
192
+ };
193
+ }
194
+
105
195
  function InlineRenderer({
106
196
  part,
107
- useHostStore,
108
- optionsRef,
197
+ useRendererStore,
109
198
  }: {
110
199
  part: ToolCallMessagePartProps;
111
- useHostStore: UseHostStore;
112
- optionsRef: MutableRefObject<McpAppRendererOptions>;
200
+ useRendererStore: UseRendererStore;
113
201
  }) {
114
- const opts = optionsRef.current;
115
202
  const aui = useAui();
203
+ const rendererOptions = useRendererStore((state) => state.options);
204
+ const opts = resolvePartOptions(rendererOptions, part);
116
205
  const app = getMcpAppFromToolPart(part);
117
206
  const cachedAppRef = useRef<McpAppMetadata | undefined>(undefined);
118
207
  useLayoutEffect(() => {
@@ -122,7 +211,7 @@ function InlineRenderer({
122
211
 
123
212
  const [loadedResource, setLoadedResource] = useState<LoadedResourceState>();
124
213
 
125
- const host = useHostStore((state) => state.host);
214
+ const host = useRendererStore((state) => state.host);
126
215
  const resourceUri = appForRender?.resourceUri;
127
216
  const serverId = appForRender?.serverId;
128
217
  const callerHandlers = opts.handlers;
@@ -133,12 +222,16 @@ function InlineRenderer({
133
222
  const targetUri = resourceUri;
134
223
  const targetServerId = serverId;
135
224
 
136
- targetHost
137
- .loadResource({
138
- uri: targetUri,
139
- ...(targetServerId ? { serverId: targetServerId } : {}),
140
- })
141
- .then((res) => {
225
+ // Host changes are published later in this passive flush. Defer until that
226
+ // publication lands, then verify this effect still owns the current host.
227
+ const loadResource = async () => {
228
+ await Promise.resolve();
229
+ if (cancelled || useRendererStore.getState().host !== targetHost) return;
230
+ try {
231
+ const res = await targetHost.loadResource({
232
+ uri: targetUri,
233
+ ...(targetServerId ? { serverId: targetServerId } : {}),
234
+ });
142
235
  if (!cancelled)
143
236
  setLoadedResource({
144
237
  host: targetHost,
@@ -148,8 +241,7 @@ function InlineRenderer({
148
241
  : {}),
149
242
  resource: res,
150
243
  });
151
- })
152
- .catch((error: unknown) => {
244
+ } catch (error: unknown) {
153
245
  if (!cancelled) {
154
246
  setLoadedResource({
155
247
  host: targetHost,
@@ -160,12 +252,14 @@ function InlineRenderer({
160
252
  error: error instanceof Error ? error : new Error(String(error)),
161
253
  });
162
254
  }
163
- });
255
+ }
256
+ };
257
+ void loadResource();
164
258
 
165
259
  return () => {
166
260
  cancelled = true;
167
261
  };
168
- }, [host, resourceUri, serverId]);
262
+ }, [host, resourceUri, serverId, useRendererStore]);
169
263
 
170
264
  const bridgeHandlers = useMemo<McpAppBridgeHandlers>(
171
265
  () => ({
@@ -174,32 +268,34 @@ function InlineRenderer({
174
268
  sendMessage:
175
269
  callerHandlers?.sendMessage ??
176
270
  ((params) => {
177
- const text = extractSendMessageText(params);
178
- if (!text) return { ok: false, reason: "unrecognised params shape" };
179
- aui.thread.append({ content: [{ type: "text", text }] });
271
+ const result = extractSendMessageContent(params);
272
+ if ("reason" in result) {
273
+ return { isError: true, ok: false, reason: result.reason };
274
+ }
275
+ aui.thread.append({ content: result.content });
180
276
  return { ok: true };
181
277
  }),
182
278
  callTool: (params) =>
183
- useHostStore.getState().host.callTool({
279
+ useRendererStore.getState().host.callTool({
184
280
  ...params,
185
281
  ...(serverId ? { serverId } : {}),
186
282
  }),
187
283
  readResource: (params) =>
188
- useHostStore.getState().host.readResource({
284
+ useRendererStore.getState().host.readResource({
189
285
  ...params,
190
286
  ...(serverId ? { serverId } : {}),
191
287
  }),
192
288
  listResources: (params) => {
193
289
  if (!serverId) {
194
- return useHostStore.getState().host.listResources(params);
290
+ return useRendererStore.getState().host.listResources(params);
195
291
  }
196
- return useHostStore.getState().host.listResources({
292
+ return useRendererStore.getState().host.listResources({
197
293
  ...(isRecord(params) ? params : {}),
198
294
  serverId,
199
295
  });
200
296
  },
201
297
  }),
202
- [aui, callerHandlers, serverId, useHostStore],
298
+ [aui, callerHandlers, serverId, useRendererStore],
203
299
  );
204
300
 
205
301
  const loadedResourceForApp =
@@ -253,27 +349,26 @@ const useMcpAppRenderer = (
253
349
  ): { readonly render: ToolCallMessagePartComponent } => {
254
350
  const host = useResource(options.host);
255
351
 
256
- const optionsRef = useRef<McpAppRendererOptions>(options);
257
- optionsRef.current = options;
258
-
259
- const [useHostStore] = useState(() =>
260
- create<{ host: McpAppsHost }>(() => ({ host })),
352
+ // The rendered component identity has to stay stable, or every part remounts
353
+ // its frame; options reach the parts through the store instead of props.
354
+ const [useRendererStore] = useState(() =>
355
+ create<RendererState>(() => ({ host, options })),
261
356
  );
262
357
  useEffect(() => {
263
- useHostStore.setState({ host });
264
- }, [host, useHostStore]);
358
+ useRendererStore.setState((prev) =>
359
+ prev.host === host && isSameOptions(prev.options, options)
360
+ ? prev
361
+ : { host, options },
362
+ );
363
+ }, [host, options, useRendererStore]);
265
364
 
266
365
  const render = useMemo((): ToolCallMessagePartComponent => {
267
366
  const Render: ToolCallMessagePartComponent = (props) => (
268
- <InlineRenderer
269
- part={props}
270
- useHostStore={useHostStore}
271
- optionsRef={optionsRef}
272
- />
367
+ <InlineRenderer part={props} useRendererStore={useRendererStore} />
273
368
  );
274
369
  Render.displayName = "McpAppRenderer";
275
370
  return Render;
276
- }, [useHostStore]);
371
+ }, [useRendererStore]);
277
372
 
278
373
  return { render };
279
374
  };