@assistant-ui/react 0.15.25 → 0.15.27

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 (159) hide show
  1. package/dist/hooks/useToolCallElapsed.d.ts.map +1 -1
  2. package/dist/hooks/useToolCallElapsed.js +3 -2
  3. package/dist/hooks/useToolCallElapsed.js.map +1 -1
  4. package/dist/index.d.ts +31 -5
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  7. package/dist/mcp-apps/McpAppRenderer.js +3 -2
  8. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  9. package/dist/mcp-apps/app-frame.d.ts.map +1 -1
  10. package/dist/mcp-apps/app-frame.js +58 -48
  11. package/dist/mcp-apps/app-frame.js.map +1 -1
  12. package/dist/mcp-apps/bridge.d.ts.map +1 -1
  13. package/dist/mcp-apps/bridge.js +28 -3
  14. package/dist/mcp-apps/bridge.js.map +1 -1
  15. package/dist/mcp-apps/csp.d.ts +4 -0
  16. package/dist/mcp-apps/csp.d.ts.map +1 -0
  17. package/dist/mcp-apps/csp.js +93 -0
  18. package/dist/mcp-apps/csp.js.map +1 -0
  19. package/dist/mcp-apps/index.d.ts +1 -1
  20. package/dist/mcp-apps/index.d.ts.map +1 -1
  21. package/dist/mcp-apps/types.d.ts +19 -1
  22. package/dist/mcp-apps/types.d.ts.map +1 -1
  23. package/dist/mcp-apps/types.js.map +1 -1
  24. package/dist/primitives/assistantModal/AssistantModalRoot.d.ts +1 -0
  25. package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
  26. package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
  27. package/dist/primitives/attachment.d.ts +3 -1
  28. package/dist/primitives/attachment.d.ts.map +1 -1
  29. package/dist/primitives/attachment.js.map +1 -1
  30. package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
  31. package/dist/primitives/composer/ComposerAttachmentDropzone.js +14 -12
  32. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  33. package/dist/primitives/composer/ComposerCancel.d.ts +5 -1
  34. package/dist/primitives/composer/ComposerCancel.d.ts.map +1 -1
  35. package/dist/primitives/composer/ComposerCancel.js +14 -1
  36. package/dist/primitives/composer/ComposerCancel.js.map +1 -1
  37. package/dist/primitives/composer/ComposerInput.d.ts +14 -0
  38. package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
  39. package/dist/primitives/composer/ComposerInput.js +4 -3
  40. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  41. package/dist/primitives/composer/ComposerRoot.d.ts.map +1 -1
  42. package/dist/primitives/composer/ComposerRoot.js +24 -19
  43. package/dist/primitives/composer/ComposerRoot.js.map +1 -1
  44. package/dist/primitives/composer/composerInputSelector.d.ts +3 -0
  45. package/dist/primitives/composer/composerInputSelector.d.ts.map +1 -0
  46. package/dist/primitives/composer/composerInputSelector.js +7 -0
  47. package/dist/primitives/composer/composerInputSelector.js.map +1 -0
  48. package/dist/primitives/composer/useComposerCancelWithFocus.d.ts +2 -0
  49. package/dist/primitives/composer/useComposerCancelWithFocus.d.ts.map +1 -0
  50. package/dist/primitives/composer/useComposerCancelWithFocus.js +42 -0
  51. package/dist/primitives/composer/useComposerCancelWithFocus.js.map +1 -0
  52. package/dist/primitives/composer.d.ts +35 -14
  53. package/dist/primitives/composer.d.ts.map +1 -1
  54. package/dist/primitives/composer.js.map +1 -1
  55. package/dist/primitives/message/MessagePartsGrouped.d.ts +8 -1
  56. package/dist/primitives/message/MessagePartsGrouped.d.ts.map +1 -1
  57. package/dist/primitives/message/MessagePartsGrouped.js +58 -278
  58. package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
  59. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.d.ts.map +1 -1
  60. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +5 -2
  61. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
  62. package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
  63. package/dist/primitives/thread/ThreadViewport.js +4 -3
  64. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  65. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +2 -2
  66. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js.map +1 -1
  67. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  68. package/dist/primitives/thread/useThreadViewportAutoScroll.js +90 -21
  69. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  70. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  71. package/dist/sandbox-host/SandboxHost.js +3 -2
  72. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  73. package/dist/unstable/useComposerInput.d.ts +9 -4
  74. package/dist/unstable/useComposerInput.d.ts.map +1 -1
  75. package/dist/unstable/useComposerInput.js +6 -4
  76. package/dist/unstable/useComposerInput.js.map +1 -1
  77. package/dist/unstable/useComposerInputHistory.d.ts +3 -2
  78. package/dist/unstable/useComposerInputHistory.d.ts.map +1 -1
  79. package/dist/unstable/useComposerInputHistory.js +2 -2
  80. package/dist/unstable/useComposerInputHistory.js.map +1 -1
  81. package/dist/unstable/useLiveCompletionAdapter.d.ts +3 -2
  82. package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -1
  83. package/dist/unstable/useLiveCompletionAdapter.js +8 -7
  84. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  85. package/dist/unstable/useMentionAdapter.d.ts +12 -3
  86. package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
  87. package/dist/unstable/useMentionAdapter.js +2 -2
  88. package/dist/unstable/useMentionAdapter.js.map +1 -1
  89. package/dist/unstable/useMessageStallDetection.d.ts +4 -2
  90. package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
  91. package/dist/unstable/useMessageStallDetection.js +2 -2
  92. package/dist/unstable/useMessageStallDetection.js.map +1 -1
  93. package/dist/unstable/useSlashCommandAdapter.d.ts +5 -2
  94. package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
  95. package/dist/unstable/useSlashCommandAdapter.js +5 -4
  96. package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
  97. package/dist/unstable/webmcp/useWebMcpProvider.d.ts +4 -0
  98. package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -1
  99. package/dist/unstable/webmcp/useWebMcpProvider.js +2 -0
  100. package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -1
  101. package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
  102. package/dist/utils/getSelectionMessageId.js +31 -8
  103. package/dist/utils/getSelectionMessageId.js.map +1 -1
  104. package/dist/utils/hooks/useOnResizeContent.d.ts.map +1 -1
  105. package/dist/utils/hooks/useOnResizeContent.js +2 -22
  106. package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
  107. package/dist/utils/useIsomorphicLayoutEffect.d.ts +3 -0
  108. package/dist/utils/useIsomorphicLayoutEffect.d.ts.map +1 -0
  109. package/dist/utils/useIsomorphicLayoutEffect.js +6 -0
  110. package/dist/utils/useIsomorphicLayoutEffect.js.map +1 -0
  111. package/package.json +10 -10
  112. package/src/hooks/useToolCallElapsed.test.tsx +4 -2
  113. package/src/hooks/useToolCallElapsed.ts +3 -2
  114. package/src/index.ts +20 -2
  115. package/src/mcp-apps/McpAppRenderer.test.tsx +7 -2
  116. package/src/mcp-apps/McpAppRenderer.tsx +3 -9
  117. package/src/mcp-apps/app-frame.test.tsx +102 -6
  118. package/src/mcp-apps/app-frame.tsx +7 -10
  119. package/src/mcp-apps/bridge.test.ts +154 -0
  120. package/src/mcp-apps/bridge.ts +51 -9
  121. package/src/mcp-apps/csp.test.ts +112 -0
  122. package/src/mcp-apps/csp.ts +124 -0
  123. package/src/mcp-apps/index.ts +1 -0
  124. package/src/mcp-apps/types.ts +16 -1
  125. package/src/model-context/frame/AssistantFrame.test.ts +45 -0
  126. package/src/model-context/frame/SPEC_AssistantFrame.md +1 -0
  127. package/src/primitives/assistantModal/AssistantModalRoot.tsx +1 -0
  128. package/src/primitives/attachment.ts +4 -1
  129. package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +32 -1
  130. package/src/primitives/composer/ComposerAttachmentDropzone.tsx +14 -12
  131. package/src/primitives/composer/ComposerCancel.focus.test.tsx +307 -0
  132. package/src/primitives/composer/ComposerCancel.ts +8 -1
  133. package/src/primitives/composer/ComposerDisabledThread.test.tsx +121 -0
  134. package/src/primitives/composer/ComposerInput.tsx +19 -1
  135. package/src/primitives/composer/ComposerRoot.tsx +6 -5
  136. package/src/primitives/composer/composerInputSelector.ts +4 -0
  137. package/src/primitives/composer/useComposerCancelWithFocus.ts +57 -0
  138. package/src/primitives/composer.ts +56 -21
  139. package/src/primitives/message/MessagePartsGrouped.test.tsx +106 -2
  140. package/src/primitives/message/MessagePartsGrouped.tsx +36 -134
  141. package/src/primitives/selectionToolbar/SelectionToolbarRoot.test.tsx +20 -12
  142. package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +6 -2
  143. package/src/primitives/thread/ThreadViewport.tsx +3 -3
  144. package/src/primitives/thread/topAnchor/useTopAnchorReserve.ts +2 -2
  145. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +666 -63
  146. package/src/primitives/thread/useThreadViewportAutoScroll.ts +201 -73
  147. package/src/sandbox-host/SandboxHost.tsx +3 -8
  148. package/src/unstable/useComposerInput.ts +9 -4
  149. package/src/unstable/useComposerInputHistory.ts +3 -2
  150. package/src/unstable/useLiveCompletionAdapter.ts +9 -14
  151. package/src/unstable/useMentionAdapter.ts +12 -3
  152. package/src/unstable/useMessageStallDetection.ts +4 -2
  153. package/src/unstable/useSlashCommandAdapter.ts +8 -4
  154. package/src/unstable/webmcp/useWebMcpProvider.ts +4 -0
  155. package/src/utils/getSelectionMessageId.test.ts +231 -0
  156. package/src/utils/getSelectionMessageId.ts +45 -7
  157. package/src/utils/hooks/useOnResizeContent.ts +2 -29
  158. package/src/utils/useIsomorphicLayoutEffect.test.tsx +21 -0
  159. package/src/utils/useIsomorphicLayoutEffect.ts +9 -0
@@ -18,33 +18,68 @@ export { ComposerPrimitiveQuoteDismiss as QuoteDismiss } from "./composer/Compos
18
18
  export { ComposerPrimitiveQueue as Queue } from "./composer/ComposerQueue";
19
19
 
20
20
  export { ComposerPrimitiveTriggerPopover as TriggerPopover } from "./composer/trigger";
21
- /** @deprecated Use `ComposerPrimitive.TriggerPopover` instead. */
22
- export { ComposerPrimitiveTriggerPopover as Unstable_TriggerPopover } from "./composer/trigger";
21
+ export {
22
+ /** @deprecated Use `ComposerPrimitive.TriggerPopover` instead. */
23
+ ComposerPrimitiveTriggerPopover as Unstable_TriggerPopover,
24
+ } from "./composer/trigger";
23
25
  export { ComposerPrimitiveTriggerPopoverRoot as TriggerPopoverRoot } from "./composer/trigger";
24
- /** @deprecated Use `ComposerPrimitive.TriggerPopoverRoot` instead. */
25
- export { ComposerPrimitiveTriggerPopoverRoot as Unstable_TriggerPopoverRoot } from "./composer/trigger";
26
+ export {
27
+ /** @deprecated Use `ComposerPrimitive.TriggerPopoverRoot` instead. */
28
+ ComposerPrimitiveTriggerPopoverRoot as Unstable_TriggerPopoverRoot,
29
+ } from "./composer/trigger";
26
30
  export { ComposerPrimitiveTriggerPopoverCategories as TriggerPopoverCategories } from "./composer/trigger";
27
- /** @deprecated Use `ComposerPrimitive.TriggerPopoverCategories` instead. */
28
- export { ComposerPrimitiveTriggerPopoverCategories as Unstable_TriggerPopoverCategories } from "./composer/trigger";
31
+ export {
32
+ /** @deprecated Use `ComposerPrimitive.TriggerPopoverCategories` instead. */
33
+ ComposerPrimitiveTriggerPopoverCategories as Unstable_TriggerPopoverCategories,
34
+ } from "./composer/trigger";
29
35
  export { ComposerPrimitiveTriggerPopoverCategoryItem as TriggerPopoverCategoryItem } from "./composer/trigger";
30
- /** @deprecated Use `ComposerPrimitive.TriggerPopoverCategoryItem` instead. */
31
- export { ComposerPrimitiveTriggerPopoverCategoryItem as Unstable_TriggerPopoverCategoryItem } from "./composer/trigger";
36
+ export {
37
+ /** @deprecated Use `ComposerPrimitive.TriggerPopoverCategoryItem` instead. */
38
+ ComposerPrimitiveTriggerPopoverCategoryItem as Unstable_TriggerPopoverCategoryItem,
39
+ } from "./composer/trigger";
32
40
  export { ComposerPrimitiveTriggerPopoverItems as TriggerPopoverItems } from "./composer/trigger";
33
- /** @deprecated Use `ComposerPrimitive.TriggerPopoverItems` instead. */
34
- export { ComposerPrimitiveTriggerPopoverItems as Unstable_TriggerPopoverItems } from "./composer/trigger";
41
+ export {
42
+ /** @deprecated Use `ComposerPrimitive.TriggerPopoverItems` instead. */
43
+ ComposerPrimitiveTriggerPopoverItems as Unstable_TriggerPopoverItems,
44
+ } from "./composer/trigger";
35
45
  export { ComposerPrimitiveTriggerPopoverItem as TriggerPopoverItem } from "./composer/trigger";
36
- /** @deprecated Use `ComposerPrimitive.TriggerPopoverItem` instead. */
37
- export { ComposerPrimitiveTriggerPopoverItem as Unstable_TriggerPopoverItem } from "./composer/trigger";
46
+ export {
47
+ /** @deprecated Use `ComposerPrimitive.TriggerPopoverItem` instead. */
48
+ ComposerPrimitiveTriggerPopoverItem as Unstable_TriggerPopoverItem,
49
+ } from "./composer/trigger";
38
50
  export { ComposerPrimitiveTriggerPopoverBack as TriggerPopoverBack } from "./composer/trigger";
39
- /** @deprecated Use `ComposerPrimitive.TriggerPopoverBack` instead. */
40
- export { ComposerPrimitiveTriggerPopoverBack as Unstable_TriggerPopoverBack } from "./composer/trigger";
41
- export { useTriggerPopoverRootContext as unstable_useTriggerPopoverRootContext } from "./composer/trigger";
42
- export { useTriggerPopoverRootContextOptional as unstable_useTriggerPopoverRootContextOptional } from "./composer/trigger";
43
- export { useTriggerPopoverScopeContext as unstable_useTriggerPopoverScopeContext } from "./composer/trigger";
44
- export { useTriggerPopoverScopeContextOptional as unstable_useTriggerPopoverScopeContextOptional } from "./composer/trigger";
45
- export { useTriggerPopoverTriggers as unstable_useTriggerPopoverTriggers } from "./composer/trigger";
46
- export { useTriggerPopoverTriggersOptional as unstable_useTriggerPopoverTriggersOptional } from "./composer/trigger";
47
- export type { RegisteredTrigger as Unstable_RegisteredTrigger } from "./composer/trigger";
51
+ export {
52
+ /** @deprecated Use `ComposerPrimitive.TriggerPopoverBack` instead. */
53
+ ComposerPrimitiveTriggerPopoverBack as Unstable_TriggerPopoverBack,
54
+ } from "./composer/trigger";
55
+ export {
56
+ /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
57
+ useTriggerPopoverRootContext as unstable_useTriggerPopoverRootContext,
58
+ } from "./composer/trigger";
59
+ export {
60
+ /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
61
+ useTriggerPopoverRootContextOptional as unstable_useTriggerPopoverRootContextOptional,
62
+ } from "./composer/trigger";
63
+ export {
64
+ /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
65
+ useTriggerPopoverScopeContext as unstable_useTriggerPopoverScopeContext,
66
+ } from "./composer/trigger";
67
+ export {
68
+ /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
69
+ useTriggerPopoverScopeContextOptional as unstable_useTriggerPopoverScopeContextOptional,
70
+ } from "./composer/trigger";
71
+ export {
72
+ /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
73
+ useTriggerPopoverTriggers as unstable_useTriggerPopoverTriggers,
74
+ } from "./composer/trigger";
75
+ export {
76
+ /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
77
+ useTriggerPopoverTriggersOptional as unstable_useTriggerPopoverTriggersOptional,
78
+ } from "./composer/trigger";
79
+ export type {
80
+ /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
81
+ RegisteredTrigger as Unstable_RegisteredTrigger,
82
+ } from "./composer/trigger";
48
83
  export type { TriggerMatch, TriggerMatcher } from "./composer/trigger";
49
84
  /** @deprecated Use `TriggerMatcher` instead. */
50
85
  export type Unstable_TriggerMatcher = TriggerMatcher;
@@ -12,10 +12,12 @@ import { describe, expect, it } from "vitest";
12
12
  import type { ThreadMessageLike } from "@assistant-ui/core";
13
13
  import {
14
14
  AssistantRuntimeProvider,
15
+ Tools,
15
16
  useAssistantDataUI,
16
17
  useExternalStoreRuntime,
17
18
  } from "@assistant-ui/core/react";
18
- import { useAui } from "@assistant-ui/store";
19
+ import { AuiConfig, useAui } from "@assistant-ui/store";
20
+ import { resource } from "@assistant-ui/tap";
19
21
  import { ThreadPrimitiveMessageByIndex } from "../thread/ThreadMessages";
20
22
  import {
21
23
  type MessagePrimitiveUnstable_PartsGrouped,
@@ -48,6 +50,11 @@ const partsMessage =
48
50
  const Named = () => <b>named</b>;
49
51
  const Fallback = () => <i>fallback</i>;
50
52
  const GlobalFallback = () => <b>global-fallback</b>;
53
+ const Mcp = () => <b>mcp</b>;
54
+ const McpApp = resource(function McpApp() {
55
+ return { render: Mcp };
56
+ });
57
+ const mcpConfig = AuiConfig({ tools: Tools({ mcpApp: McpApp() }) });
51
58
 
52
59
  const RegisterFallbackDataUI: FC<{ render: typeof GlobalFallback }> = ({
53
60
  render,
@@ -69,11 +76,13 @@ const Example = ({
69
76
  content,
70
77
  Message: MessageComponent = Message,
71
78
  extra,
79
+ config,
72
80
  isRunning = false,
73
81
  }: {
74
82
  content: ThreadMessageLike["content"];
75
83
  Message?: FC;
76
84
  extra?: ReactNode;
85
+ config?: AuiConfig;
77
86
  isRunning?: boolean;
78
87
  }) => {
79
88
  const messages: ThreadMessageLike[] = [
@@ -86,7 +95,10 @@ const Example = ({
86
95
  onNew: async () => {},
87
96
  });
88
97
  return (
89
- <AssistantRuntimeProvider runtime={runtime}>
98
+ <AssistantRuntimeProvider
99
+ runtime={runtime}
100
+ {...(config === undefined ? {} : { config })}
101
+ >
90
102
  {extra}
91
103
  <ThreadPrimitiveMessageByIndex
92
104
  index={0}
@@ -439,6 +451,17 @@ describe("MessagePrimitive.Unstable_PartsGroupedByParentId", () => {
439
451
  },
440
452
  );
441
453
 
454
+ it("falls back to the default text component for an undefined slot", () => {
455
+ render(
456
+ <Example
457
+ content={[{ type: "text", text: "hello" }]}
458
+ Message={partsMessage({ Text: undefined })}
459
+ />,
460
+ );
461
+
462
+ expect(screen.getByText("hello")).toBeTruthy();
463
+ });
464
+
442
465
  it("renders tool and data UIs registered under an inherited name", () => {
443
466
  const { container } = render(
444
467
  <Example
@@ -461,6 +484,87 @@ describe("MessagePrimitive.Unstable_PartsGroupedByParentId", () => {
461
484
  expect(container.innerHTML).toBe("<b>named</b><b>named</b>");
462
485
  });
463
486
 
487
+ it("renders tools.mcpApp for a tool call with a ui:// resource", () => {
488
+ const { container } = render(
489
+ <Example
490
+ content={[
491
+ {
492
+ type: "tool-call",
493
+ toolCallId: "call",
494
+ toolName: "show_chart",
495
+ args: {},
496
+ mcp: { app: { resourceUri: "ui://chart" } },
497
+ },
498
+ ]}
499
+ Message={partsMessage({ tools: { Fallback } })}
500
+ config={mcpConfig}
501
+ />,
502
+ );
503
+
504
+ expect(container.innerHTML).toBe("<b>mcp</b>");
505
+ });
506
+
507
+ it("prefers an inline by_name component over tools.mcpApp", () => {
508
+ const { container } = render(
509
+ <Example
510
+ content={[
511
+ {
512
+ type: "tool-call",
513
+ toolCallId: "call",
514
+ toolName: "show_chart",
515
+ args: {},
516
+ mcp: { app: { resourceUri: "ui://chart" } },
517
+ },
518
+ ]}
519
+ Message={partsMessage({
520
+ tools: { by_name: { show_chart: Named }, Fallback },
521
+ })}
522
+ config={mcpConfig}
523
+ />,
524
+ );
525
+
526
+ expect(container.innerHTML).toBe("<b>named</b>");
527
+ });
528
+
529
+ it("uses inline Fallback when the tool call has no ui:// resource", () => {
530
+ const { container } = render(
531
+ <Example
532
+ content={[
533
+ {
534
+ type: "tool-call",
535
+ toolCallId: "call",
536
+ toolName: "show_chart",
537
+ args: {},
538
+ },
539
+ ]}
540
+ Message={partsMessage({ tools: { Fallback } })}
541
+ config={mcpConfig}
542
+ />,
543
+ );
544
+
545
+ expect(container.innerHTML).toBe("<i>fallback</i>");
546
+ });
547
+
548
+ it("uses inline Fallback when the resource URI is not an MCP App URI", () => {
549
+ const { container } = render(
550
+ <Example
551
+ content={[
552
+ {
553
+ type: "tool-call",
554
+ toolCallId: "call",
555
+ toolName: "show_chart",
556
+ args: {},
557
+ mcp: { app: { resourceUri: "https://example.com/chart" } },
558
+ },
559
+ ]}
560
+ Message={partsMessage({ tools: { Fallback } })}
561
+ config={mcpConfig}
562
+ />,
563
+ );
564
+
565
+ expect(container.innerHTML).toBe("<i>fallback</i>");
566
+ });
567
+
464
568
  it("uses dataRenderers.fallbacks[0] before inline data.Fallback", async () => {
465
569
  const { container } = render(
466
570
  <Example
@@ -7,23 +7,23 @@ import {
7
7
  type PropsWithChildren,
8
8
  useMemo,
9
9
  } from "react";
10
- import { useAuiState, useAui } from "@assistant-ui/store";
10
+ import { useAuiState } from "@assistant-ui/store";
11
11
  import { PartByIndexProvider } from "../../context/providers/PartByIndexProvider";
12
12
  import { TextMessagePartProvider } from "../../context/providers/TextMessagePartProvider";
13
13
  import { MessagePartPrimitiveText } from "../messagePart/MessagePartText";
14
14
  import { MessagePartPrimitiveImage } from "../messagePart/MessagePartImage";
15
- import type {
16
- Unstable_AudioMessagePartComponent,
17
- DataMessagePartComponent,
18
- DataMessagePartProps,
19
- EmptyMessagePartComponent,
20
- TextMessagePartComponent,
21
- ImageMessagePartComponent,
22
- SourceMessagePartComponent,
23
- ToolCallMessagePartComponent,
24
- ToolCallMessagePartProps,
25
- FileMessagePartComponent,
26
- ReasoningMessagePartComponent,
15
+ import {
16
+ MessagePartComponent,
17
+ type Unstable_AudioMessagePartComponent,
18
+ type DataMessagePartComponent,
19
+ type EmptyMessagePartComponent,
20
+ type TextMessagePartComponent,
21
+ type ImageMessagePartComponent,
22
+ type SourceMessagePartComponent,
23
+ type ToolCallMessagePartComponent,
24
+ type ToolCallMessagePartProps,
25
+ type FileMessagePartComponent,
26
+ type ReasoningMessagePartComponent,
27
27
  } from "@assistant-ui/core/react";
28
28
  import { MessagePartPrimitiveInProgress } from "../messagePart/MessagePartInProgress";
29
29
  import type { MessagePartStatus } from "@assistant-ui/core";
@@ -86,6 +86,13 @@ const useMessagePartsGrouped = (
86
86
  return { groups, partKeys: getMessagePartKeys(parts) };
87
87
  };
88
88
 
89
+ /**
90
+ * @deprecated Prefer `<MessagePrimitive.GroupedParts>` for adjacent
91
+ * grouping — it dispatches all rendering through one `switch (part.type)`
92
+ * and supports nested group paths. Keep this primitive only for
93
+ * non-adjacent clustering (e.g., gathering parts with the same parent-id
94
+ * across the message).
95
+ */
89
96
  export namespace MessagePrimitiveUnstable_PartsGrouped {
90
97
  export type Props = {
91
98
  /**
@@ -231,34 +238,6 @@ export namespace MessagePrimitiveUnstable_PartsGrouped {
231
238
  };
232
239
  }
233
240
 
234
- const ToolUIDisplay = ({
235
- Fallback,
236
- ...props
237
- }: {
238
- Fallback: ToolCallMessagePartComponent | undefined;
239
- } & ToolCallMessagePartProps) => {
240
- const Render = useAuiState(
241
- (s) => s.tools.toolUIs[props.toolName]?.[0]?.render ?? Fallback,
242
- );
243
- if (!Render) return null;
244
- return <Render {...props} />;
245
- };
246
-
247
- const DataUIDisplay = ({
248
- Fallback,
249
- ...props
250
- }: {
251
- Fallback: DataMessagePartComponent | undefined;
252
- } & DataMessagePartProps) => {
253
- const Render = useAuiState((s) => {
254
- const named = s.dataRenderers.renderers[props.name]?.[0];
255
- if (named) return named;
256
- return s.dataRenderers.fallbacks[0] ?? Fallback;
257
- });
258
- if (!Render) return null;
259
- return <Render {...props} />;
260
- };
261
-
262
241
  const defaultComponents = {
263
242
  Text: () => (
264
243
  <p style={{ whiteSpace: "pre-line" }}>
@@ -280,104 +259,17 @@ type MessagePartComponentProps = {
280
259
  components: MessagePrimitiveUnstable_PartsGrouped.Props["components"];
281
260
  };
282
261
 
283
- const MessagePartComponent: FC<MessagePartComponentProps> = ({
284
- components: {
285
- Text = defaultComponents.Text,
286
- Reasoning = defaultComponents.Reasoning,
287
- Image = defaultComponents.Image,
288
- Source = defaultComponents.Source,
289
- File = defaultComponents.File,
290
- Unstable_Audio: Audio = defaultComponents.Unstable_Audio,
291
- tools = {},
292
- data,
293
- } = {},
294
- }) => {
295
- const aui = useAui();
296
- const part = useAuiState((s) => s.part);
297
-
298
- const type = part.type;
299
- if (type === "tool-call") {
300
- const addResult = aui.part.addToolResult;
301
- const resume = aui.part.resumeToolCall;
302
- const respondToApproval = aui.part.respondToToolApproval;
303
- const unstable_recordInteraction = aui.part.unstable_recordInteraction;
304
- if ("Override" in tools)
305
- return (
306
- <tools.Override
307
- {...part}
308
- addResult={addResult}
309
- resume={resume}
310
- respondToApproval={respondToApproval}
311
- {...(unstable_recordInteraction && { unstable_recordInteraction })}
312
- />
313
- );
314
- const Tool =
315
- (tools.by_name && Object.hasOwn(tools.by_name, part.toolName)
316
- ? tools.by_name[part.toolName]
317
- : undefined) ?? tools.Fallback;
318
- return (
319
- <ToolUIDisplay
320
- {...part}
321
- Fallback={Tool}
322
- addResult={addResult}
323
- resume={resume}
324
- respondToApproval={respondToApproval}
325
- {...(unstable_recordInteraction && { unstable_recordInteraction })}
326
- />
327
- );
328
- }
329
-
330
- if (part.status?.type === "requires-action")
331
- throw new Error("Encountered unexpected requires-action status");
332
-
333
- switch (type) {
334
- case "text":
335
- return <Text {...part} />;
336
-
337
- case "reasoning":
338
- return <Reasoning {...part} />;
339
-
340
- case "source":
341
- return <Source {...part} />;
342
-
343
- case "image":
344
- return <Image {...part} />;
345
-
346
- case "file":
347
- return <File {...part} />;
348
-
349
- case "audio":
350
- return <Audio {...part} />;
351
-
352
- case "data": {
353
- const Data =
354
- (data?.by_name && Object.hasOwn(data.by_name, part.name)
355
- ? data.by_name[part.name]
356
- : undefined) ?? data?.Fallback;
357
- return <DataUIDisplay {...part} Fallback={Data} />;
358
- }
359
-
360
- default:
361
- console.warn(`Unknown message part type: ${type}`);
362
- return null;
363
- }
364
- };
365
-
366
262
  type MessagePartProps = {
367
263
  partIndex: number;
368
264
  components: MessagePrimitiveUnstable_PartsGrouped.Props["components"];
369
265
  };
370
266
 
371
- const MessagePartImpl: FC<MessagePartProps> = ({ partIndex, components }) => {
372
- return (
267
+ const MessagePart = memo(
268
+ ({ partIndex, components }: MessagePartProps) => (
373
269
  <PartByIndexProvider index={partIndex}>
374
270
  <MessagePartComponent components={components} />
375
271
  </PartByIndexProvider>
376
- );
377
- };
378
-
379
- const MessagePart = memo(
380
- MessagePartImpl,
272
+ ),
381
273
  (prev, next) =>
382
274
  prev.partIndex === next.partIndex &&
383
275
  prev.components?.Text === next.components?.Text &&
@@ -387,8 +279,7 @@ const MessagePart = memo(
387
279
  prev.components?.File === next.components?.File &&
388
280
  prev.components?.Unstable_Audio === next.components?.Unstable_Audio &&
389
281
  prev.components?.tools === next.components?.tools &&
390
- prev.components?.data === next.components?.data &&
391
- prev.components?.Group === next.components?.Group,
282
+ prev.components?.data === next.components?.data,
392
283
  );
393
284
 
394
285
  const EmptyPartFallback: FC<{
@@ -473,6 +364,17 @@ export const MessagePrimitiveUnstable_PartsGrouped: FC<
473
364
  return <EmptyParts components={components} />;
474
365
  }
475
366
 
367
+ const leafComponents = {
368
+ ...components,
369
+ Text: components?.Text ?? defaultComponents.Text,
370
+ Reasoning: components?.Reasoning ?? defaultComponents.Reasoning,
371
+ Image: components?.Image ?? defaultComponents.Image,
372
+ Source: components?.Source ?? defaultComponents.Source,
373
+ File: components?.File ?? defaultComponents.File,
374
+ Unstable_Audio:
375
+ components?.Unstable_Audio ?? defaultComponents.Unstable_Audio,
376
+ };
377
+
476
378
  return messageGroups.map((group, groupIndex) => {
477
379
  const GroupComponent = components?.Group ?? defaultComponents.Group;
478
380
  const identity = partKeys[group.indices[0]!];
@@ -491,7 +393,7 @@ export const MessagePrimitiveUnstable_PartsGrouped: FC<
491
393
  <MessagePart
492
394
  key={partKeys[partIndex]}
493
395
  partIndex={partIndex}
494
- components={components}
396
+ components={leafComponents}
495
397
  />
496
398
  ))}
497
399
  </GroupComponent>
@@ -1,6 +1,6 @@
1
1
  /** @vitest-environment jsdom */
2
2
  import type { MouseEvent, ReactNode } from "react";
3
- import { fireEvent, render } from "@testing-library/react";
3
+ import { act, fireEvent, render } from "@testing-library/react";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
  import type * as StoreModule from "@assistant-ui/store";
6
6
  import { ThreadPrimitiveRoot } from "../thread/ThreadRoot";
@@ -96,14 +96,6 @@ describe("SelectionToolbarPrimitiveRoot onMouseDown composition", () => {
96
96
  });
97
97
 
98
98
  describe("SelectionToolbarPrimitiveRoot selection changes", () => {
99
- it("opens from a selectionchange event without a mouse or key event", () => {
100
- render(<SelectionToolbarPrimitiveRoot data-testid="toolbar" />);
101
-
102
- fireEvent(document, new Event("selectionchange"));
103
-
104
- expect(document.querySelector('[data-testid="toolbar"]')).not.toBeNull();
105
- });
106
-
107
99
  it("closes an open toolbar when the selection collapses", () => {
108
100
  render(<SelectionToolbarPrimitiveRoot data-testid="toolbar" />);
109
101
  fireEvent(document, new Event("selectionchange"));
@@ -282,17 +274,33 @@ describe("SelectionToolbarPrimitiveRoot frame cleanup", () => {
282
274
  expect(frames).toHaveLength(2);
283
275
  });
284
276
 
285
- it("recovers when the window blurs during a drag", () => {
277
+ it("shows the toolbar when the window blurs during a drag", () => {
286
278
  const { frames } = deferFrames();
287
- render(<SelectionToolbarPrimitiveRoot />);
279
+ render(<SelectionToolbarPrimitiveRoot data-testid="toolbar" />);
288
280
 
289
281
  fireEvent.mouseDown(document);
290
282
  fireEvent(document, new Event("selectionchange"));
291
283
  expect(frames).toHaveLength(0);
292
284
 
293
285
  fireEvent.blur(window);
294
- fireEvent(document, new Event("selectionchange"));
295
286
  expect(frames).toHaveLength(1);
287
+
288
+ act(() => frames[0]?.(0));
289
+
290
+ expect(document.querySelector('[data-testid="toolbar"]')).not.toBeNull();
291
+ });
292
+
293
+ it("does not reopen a toolbar hidden by scroll on an unrelated window blur", () => {
294
+ render(<SelectionToolbarPrimitiveRoot data-testid="toolbar" />);
295
+
296
+ fireEvent(document, new Event("selectionchange"));
297
+ expect(document.querySelector('[data-testid="toolbar"]')).not.toBeNull();
298
+
299
+ fireEvent.scroll(document);
300
+ expect(document.querySelector('[data-testid="toolbar"]')).toBeNull();
301
+
302
+ fireEvent.blur(window);
303
+ expect(document.querySelector('[data-testid="toolbar"]')).toBeNull();
296
304
  });
297
305
 
298
306
  it("recovers when a context menu consumes the mouseup of a press", () => {
@@ -134,12 +134,16 @@ export const SelectionToolbarPrimitiveRoot = forwardRef<
134
134
  isMouseDragging = false;
135
135
  };
136
136
 
137
+ const handleWindowBlur = () => {
138
+ if (isMouseDragging) handleMouseUp();
139
+ };
140
+
137
141
  document.addEventListener("mousedown", handleMouseDown, true);
138
142
  document.addEventListener("mouseup", handleMouseUp, true);
139
143
  document.addEventListener("dragend", handleMouseUp, true);
140
144
  // A context menu can consume the mouseup of the press that opened it.
141
145
  document.addEventListener("contextmenu", handleMouseCancel, true);
142
- window.addEventListener("blur", handleMouseCancel);
146
+ window.addEventListener("blur", handleWindowBlur);
143
147
  document.addEventListener("selectionchange", handleSelectionChange);
144
148
  document.addEventListener("scroll", handleScroll, true);
145
149
 
@@ -149,7 +153,7 @@ export const SelectionToolbarPrimitiveRoot = forwardRef<
149
153
  document.removeEventListener("mouseup", handleMouseUp, true);
150
154
  document.removeEventListener("dragend", handleMouseUp, true);
151
155
  document.removeEventListener("contextmenu", handleMouseCancel, true);
152
- window.removeEventListener("blur", handleMouseCancel);
156
+ window.removeEventListener("blur", handleWindowBlur);
153
157
  document.removeEventListener("selectionchange", handleSelectionChange);
154
158
  document.removeEventListener("scroll", handleScroll, true);
155
159
  };
@@ -7,7 +7,6 @@ import {
7
7
  forwardRef,
8
8
  type ComponentPropsWithoutRef,
9
9
  useCallback,
10
- useLayoutEffect,
11
10
  useMemo,
12
11
  } from "react";
13
12
  import { useAuiState } from "@assistant-ui/store";
@@ -20,6 +19,7 @@ import {
20
19
  useThreadViewportStore,
21
20
  } from "../../context/react/ThreadViewportContext";
22
21
  import { useTopAnchorReserve } from "./topAnchor/useTopAnchorReserve";
22
+ import { useIsomorphicLayoutEffect } from "../../utils/useIsomorphicLayoutEffect";
23
23
  import {
24
24
  getActiveTopAnchorAnchorId,
25
25
  getActiveTopAnchorTargetId,
@@ -124,13 +124,13 @@ const useTopAnchorTurn = (enabled: boolean) => {
124
124
  isTopAnchorTurnValid(topAnchorTurn, s.thread.messages),
125
125
  );
126
126
 
127
- useLayoutEffect(() => {
127
+ useIsomorphicLayoutEffect(() => {
128
128
  if (!topAnchorTurn || topAnchorTurnIsValid) return;
129
129
 
130
130
  threadViewportStore.getState().setTopAnchorTurn(null);
131
131
  }, [threadViewportStore, topAnchorTurn, topAnchorTurnIsValid]);
132
132
 
133
- useLayoutEffect(() => {
133
+ useIsomorphicLayoutEffect(() => {
134
134
  if (!activeTurn) return;
135
135
 
136
136
  const state = threadViewportStore.getState();
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useLayoutEffect } from "react";
3
+ import { useIsomorphicLayoutEffect } from "../../../utils/useIsomorphicLayoutEffect";
4
4
  import { useThreadViewportStore } from "../../../context/react/ThreadViewportContext";
5
5
  import { mountTopAnchorReserve } from "./mountTopAnchorReserve";
6
6
 
@@ -12,7 +12,7 @@ import { mountTopAnchorReserve } from "./mountTopAnchorReserve";
12
12
  export const useTopAnchorReserve = (enabled: boolean) => {
13
13
  const threadViewportStore = useThreadViewportStore();
14
14
 
15
- useLayoutEffect(() => {
15
+ useIsomorphicLayoutEffect(() => {
16
16
  if (!enabled) return;
17
17
  return mountTopAnchorReserve(threadViewportStore);
18
18
  }, [enabled, threadViewportStore]);