@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
@@ -1,5 +1,6 @@
1
- import { ComponentPropsWithoutRef, ComponentRef, ElementType, ForwardRefExoticComponent, ReactElement, RefAttributes } from "react";
1
+ import { ComponentProps, ComponentPropsWithoutRef, ComponentRef, ElementType, ForwardRefExoticComponent, ReactElement, ReactNode, RefAttributes } from "react";
2
2
  import { Primitive as Primitive$1 } from "radix-ui/internal";
3
+ import { Slot } from "radix-ui";
3
4
  //#region src/utils/Primitive.d.ts
4
5
  /**
5
6
  * Thin wrapper around Radix `Primitive` that adds `render` prop support.
@@ -16,6 +17,13 @@ type WithRenderPropProps<T extends ElementType> = ComponentPropsWithoutRef<T> &
16
17
  render?: ReactElement | undefined;
17
18
  };
18
19
  type PrimitiveProps<E extends PrimitiveNode> = WithRenderPropProps<(typeof Primitive$1)[E]>;
20
+ /**
21
+ * Composes a `render` element at a call site that has already computed its props.
22
+ *
23
+ * `withRenderProp` wraps a component; this covers the case where the props are
24
+ * already in hand and the composition happens inline.
25
+ */
26
+ declare function renderSlot(render: ReactElement, children: ReactNode, props: ComponentProps<typeof Slot.Root>): ReactElement;
19
27
  declare function withRenderProp<T extends ElementType>(Component: T): ForwardRefExoticComponent<WithRenderPropProps<T> & RefAttributes<ComponentRef<T>>>;
20
28
  declare const Primitive: {
21
29
  a: ForwardRefExoticComponent<Omit<import("react").ClassAttributes<HTMLAnchorElement> & import("react").AnchorHTMLAttributes<HTMLAnchorElement> & {
@@ -105,5 +113,5 @@ declare const Primitive: {
105
113
  } & RefAttributes<HTMLUListElement>>;
106
114
  };
107
115
  //#endregion
108
- export { Primitive, type PrimitiveProps, type WithRenderPropProps, withRenderProp };
116
+ export { Primitive, type PrimitiveProps, type WithRenderPropProps, renderSlot, withRenderProp };
109
117
  //# sourceMappingURL=Primitive.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Primitive.d.ts","names":[],"sources":["../../src/utils/Primitive.tsx"],"mappings":";;;;;;;;;;;;cA0BM;KAmBD,wBAAwB;KAExB,oBAAoB,UAAU,eACjC,yBAAyB;EACvB,SAAS;;KAGR,eAAe,UAAU,iBAAiB,4BACrC,aAAgB;iBAajB,eAAe,UAAU,aAAa,WAAW,IAwCtC,0BAChB,oBAAoB,KAAK,cAAc,aAAa;cAclD;;;;IAxEO,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA"}
1
+ {"version":3,"file":"Primitive.d.ts","names":[],"sources":["../../src/utils/Primitive.tsx"],"mappings":";;;;;;;;;;;;;cA4BM;KAmBD,wBAAwB;KAExB,oBAAoB,UAAU,eACjC,yBAAyB;EACvB,SAAS;;KAGR,eAAe,UAAU,iBAAiB,4BACrC,aAAgB;;;;;;;iBAoCjB,WACP,QAAQ,cACR,UAAU,WACV,OAAO,sBAAsB,KAAK,QACjC;iBAMM,eAAe,UAAU,aAAa,WAAW,IAmCtC,0BAChB,oBAAoB,KAAK,cAAc,aAAa;cAclD;;;;IApGO,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA;;;;;IAAA,SAAA"}
@@ -2,6 +2,7 @@ import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
2
2
  import { cloneElement, forwardRef, isValidElement } from "@assistant-ui/tap/react-shim";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  import { Primitive as Primitive$1 } from "radix-ui/internal";
5
+ import { Slot } from "radix-ui";
5
6
  //#region src/utils/Primitive.tsx
6
7
  /**
7
8
  * Thin wrapper around Radix `Primitive` that adds `render` prop support.
@@ -31,6 +32,25 @@ const NODES = [
31
32
  "svg",
32
33
  "ul"
33
34
  ];
35
+ /**
36
+ * Composes the children of a `render` element. Outer children win when supplied;
37
+ * the render element's own children are the fallback.
38
+ */
39
+ function composeRenderElement(render, children) {
40
+ return cloneElement(render, void 0, children !== void 0 ? children : render.props.children);
41
+ }
42
+ /**
43
+ * Composes a `render` element at a call site that has already computed its props.
44
+ *
45
+ * `withRenderProp` wraps a component; this covers the case where the props are
46
+ * already in hand and the composition happens inline.
47
+ */
48
+ function renderSlot(render, children, props) {
49
+ return /* @__PURE__ */ jsx(Slot.Root, {
50
+ ...props,
51
+ children: composeRenderElement(render, children)
52
+ });
53
+ }
34
54
  function withRenderProp(Component) {
35
55
  const Wrapped = forwardRef((t0, ref) => {
36
56
  const $ = c(17);
@@ -53,13 +73,12 @@ function withRenderProp(Component) {
53
73
  }
54
74
  const Comp = Component;
55
75
  if (render && isValidElement(render)) {
56
- const renderChildren = children !== void 0 ? children : render.props.children;
57
76
  const t1 = rest;
58
77
  let t2;
59
- if ($[5] !== render || $[6] !== renderChildren) {
60
- t2 = cloneElement(render, void 0, renderChildren);
61
- $[5] = render;
62
- $[6] = renderChildren;
78
+ if ($[5] !== children || $[6] !== render) {
79
+ t2 = composeRenderElement(render, children);
80
+ $[5] = children;
81
+ $[6] = render;
63
82
  $[7] = t2;
64
83
  } else t2 = $[7];
65
84
  let t3;
@@ -108,6 +127,6 @@ const Primitive = NODES.reduce((acc, node) => {
108
127
  return acc;
109
128
  }, {});
110
129
  //#endregion
111
- export { Primitive, withRenderProp };
130
+ export { Primitive, renderSlot, withRenderProp };
112
131
 
113
132
  //# sourceMappingURL=Primitive.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Primitive.js","names":["ComponentPropsWithoutRef","ComponentRef","ElementType","ForwardRefExoticComponent","PropsWithoutRef","ReactElement","ReactNode","RefAttributes","cloneElement","forwardRef","isValidElement","Primitive","RadixPrimitive","NODES","const","PrimitiveNode","WithRenderPropProps","T","render","PrimitiveProps","E","WithRenderPropRuntimeProps","asChild","children","PrimitiveRef","withRenderProp","Component","Wrapped","t0","ref","$","_c","rest","Comp","renderChildren","undefined","props","Record","t1","t2","t3","componentName","displayName","name","createPrimitive","node","RadixComp","reduce","acc","ReturnType","K"],"sources":["../../src/utils/Primitive.tsx"],"sourcesContent":["import {\n type ComponentPropsWithoutRef,\n type ComponentRef,\n type ElementType,\n type ForwardRefExoticComponent,\n type PropsWithoutRef,\n type ReactElement,\n type ReactNode,\n type RefAttributes,\n cloneElement,\n forwardRef,\n isValidElement,\n} from \"react\";\nimport { Primitive as RadixPrimitive } from \"radix-ui/internal\";\n\n/**\n * Thin wrapper around Radix `Primitive` that adds `render` prop support.\n *\n * When `render` is provided, it is converted to the equivalent `asChild` pattern:\n * render={<Comp props />} + children → asChild + <Comp props>{children}</Comp>\n *\n * All prop merging, ref composition, and event handler chaining remain handled\n * by Radix's battle-tested Slot implementation — we add zero custom logic for that.\n */\n\n// Match @radix-ui/react-primitive's full element set\nconst NODES = [\n \"a\",\n \"button\",\n \"div\",\n \"form\",\n \"h2\",\n \"h3\",\n \"img\",\n \"input\",\n \"label\",\n \"li\",\n \"nav\",\n \"ol\",\n \"p\",\n \"select\",\n \"span\",\n \"svg\",\n \"ul\",\n] as const;\ntype PrimitiveNode = (typeof NODES)[number];\n\ntype WithRenderPropProps<T extends ElementType> =\n ComponentPropsWithoutRef<T> & {\n render?: ReactElement | undefined;\n };\n\ntype PrimitiveProps<E extends PrimitiveNode> = WithRenderPropProps<\n (typeof RadixPrimitive)[E]\n>;\n\ntype WithRenderPropRuntimeProps<T extends ElementType> =\n WithRenderPropProps<T> & {\n asChild?: boolean | undefined;\n children?: ReactNode | undefined;\n };\n\ntype PrimitiveRef<E extends PrimitiveNode> = ComponentRef<\n (typeof RadixPrimitive)[E]\n>;\n\nfunction withRenderProp<T extends ElementType>(Component: T) {\n const Wrapped = forwardRef<ComponentRef<T>, WithRenderPropRuntimeProps<T>>(\n (\n {\n render,\n asChild,\n children,\n ...rest\n }: PropsWithoutRef<WithRenderPropRuntimeProps<T>>,\n ref,\n ) => {\n const Comp = Component as any;\n\n if (render && isValidElement(render)) {\n const renderChildren =\n children !== undefined\n ? children\n : ((render.props as Record<string, unknown>).children as ReactNode);\n\n return (\n <Comp {...(rest as any)} asChild ref={ref}>\n {cloneElement(render, undefined, renderChildren)}\n </Comp>\n );\n }\n\n return (\n <Comp {...(rest as any)} asChild={asChild} ref={ref}>\n {children}\n </Comp>\n );\n },\n );\n\n const componentName =\n typeof Component === \"string\"\n ? Component\n : (Component.displayName ?? Component.name ?? \"Component\");\n Wrapped.displayName = componentName;\n\n return Wrapped as ForwardRefExoticComponent<\n WithRenderPropProps<T> & RefAttributes<ComponentRef<T>>\n >;\n}\n\nfunction createPrimitive<E extends PrimitiveNode>(node: E) {\n const RadixComp = RadixPrimitive[node];\n const Component = withRenderProp(RadixComp);\n\n Component.displayName = `Primitive.${node}`;\n return Component as ForwardRefExoticComponent<\n PrimitiveProps<E> & RefAttributes<PrimitiveRef<E>>\n >;\n}\n\nconst Primitive = NODES.reduce(\n (acc, node) => {\n acc[node] = createPrimitive(node);\n return acc;\n },\n {} as {\n [K in PrimitiveNode]: ReturnType<typeof createPrimitive<K>>;\n },\n);\n\nexport { Primitive, withRenderProp };\nexport type { PrimitiveProps, WithRenderPropProps };\n"],"mappings":";;;;;;;;;;;;;;AA0BA,MAAMa,QAAQ;CACZ;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AAAI;AAuBN,SAASY,eAAsCC,WAAc;CAC3D,MAAMC,UAAUlB,YACdmB,IAAAC,QAAA;EAAA,MAAAC,IAAAC,EAAA,EAAA;EAAA,IAAAT;EAAA,IAAAC;EAAA,IAAAL;EAAA,IAAAc;EAAA,IAAAF,EAAA,OAAAF,IAAA;GACE,CAAA,CAAAV,QAAAI,SAAAC,UAAA,GAAAS,QAAAJ;GAKiDE,EAAA,KAAAF;GAAAE,EAAA,KAAAR;GAAAQ,EAAA,KAAAP;GAAAO,EAAA,KAAAZ;GAAAY,EAAA,KAAAE;EAAA,OAAA;GAAAV,UAAAQ,EAAA;GAAAP,WAAAO,EAAA;GAAAZ,SAAAY,EAAA;GAAAE,OAAAF,EAAA;EAAA;EAGjD,MAAAG,OAAaP;EAEb,IAAIR,UAAUR,eAAeQ,MAAM,GAAC;GAClC,MAAAgB,iBACEX,aAAaY,KAAAA,IAAbZ,WAEML,OAAMkB,MAAiCb;GAGlC,MAAAe,KAAAN;GAAW,IAAAO;GAAA,IAAAT,EAAA,OAAAZ,UAAAY,EAAA,OAAAI,gBAAA;IACnBK,KAAA/B,aAAaU,QAAQiB,KAAAA,GAAWD,cAAc;IAACJ,EAAA,KAAAZ;IAAAY,EAAA,KAAAI;IAAAJ,EAAA,KAAAS;GAAA,OAAAA,KAAAT,EAAA;GAAA,IAAAU;GAAA,IAAAV,EAAA,OAAAD,OAAAC,EAAA,OAAAQ,MAAAR,EAAA,QAAAS,IAAA;IADlDC,KAAA,oBAAC,MAAD;KAAK,GAAMF;KAAc,SAAA;KAAaT;KACnCU,UAAAA;IADE,CAAA;IAEET,EAAA,KAAAD;IAAAC,EAAA,KAAAQ;IAAAR,EAAA,MAAAS;IAAAT,EAAA,MAAAU;GAAA,OAAAA,KAAAV,EAAA;GAAA,OAFPU;EAEO;EAKE,MAAAF,KAAAN;EAAW,IAAAO;EAAA,IAAAT,EAAA,QAAAR,WAAAQ,EAAA,QAAAP,YAAAO,EAAA,QAAAD,OAAAC,EAAA,QAAAQ,IAAA;GAAtBC,KAAA,oBAAC,MAAD;IAAK,GAAMD;IAAuBhB;IAAcO;IAC7CN;GADE,CAAA;GAEEO,EAAA,MAAAR;GAAAQ,EAAA,MAAAP;GAAAO,EAAA,MAAAD;GAAAC,EAAA,MAAAQ;GAAAR,EAAA,MAAAS;EAAA,OAAAA,KAAAT,EAAA;EAAA,OAFPS;CAEO,CAGb;CAMAZ,QAAQe,cAHN,OAAOhB,cAAc,WACjBA,YACCA,UAAUgB,eAAehB,UAAUiB,QAAQ;CAGlD,OAAOhB;AAGT;AAEA,SAASiB,gBAAyCC,MAAS;CACzD,MAAMC,YAAYlC,YAAeiC;CACjC,MAAMnB,YAAYD,eAAeqB,SAAS;CAE1CpB,UAAUgB,cAAc,aAAaG;CACrC,OAAOnB;AAGT;AAEA,MAAMf,YAAYE,MAAMkC,QACrBC,KAAKH,SAAS;CACbG,IAAIH,QAAQD,gBAAgBC,IAAI;CAChC,OAAOG;AACT,GACA,CAAC,CAGH"}
1
+ {"version":3,"file":"Primitive.js","names":["ComponentProps","ComponentPropsWithoutRef","ComponentRef","ElementType","ForwardRefExoticComponent","PropsWithoutRef","ReactElement","ReactNode","RefAttributes","cloneElement","forwardRef","isValidElement","Primitive","RadixPrimitive","Slot","NODES","const","PrimitiveNode","WithRenderPropProps","T","render","PrimitiveProps","E","WithRenderPropRuntimeProps","asChild","children","PrimitiveRef","composeRenderElement","undefined","props","renderSlot","Root","withRenderProp","Component","Wrapped","t0","ref","$","_c","rest","Comp","t1","t2","t3","componentName","displayName","name","createPrimitive","node","RadixComp","reduce","acc","ReturnType","K"],"sources":["../../src/utils/Primitive.tsx"],"sourcesContent":["import {\n type ComponentProps,\n type ComponentPropsWithoutRef,\n type ComponentRef,\n type ElementType,\n type ForwardRefExoticComponent,\n type PropsWithoutRef,\n type ReactElement,\n type ReactNode,\n type RefAttributes,\n cloneElement,\n forwardRef,\n isValidElement,\n} from \"react\";\nimport { Primitive as RadixPrimitive } from \"radix-ui/internal\";\nimport { Slot } from \"radix-ui\";\n\n/**\n * Thin wrapper around Radix `Primitive` that adds `render` prop support.\n *\n * When `render` is provided, it is converted to the equivalent `asChild` pattern:\n * render={<Comp props />} + children → asChild + <Comp props>{children}</Comp>\n *\n * All prop merging, ref composition, and event handler chaining remain handled\n * by Radix's battle-tested Slot implementation — we add zero custom logic for that.\n */\n\n// Match @radix-ui/react-primitive's full element set\nconst NODES = [\n \"a\",\n \"button\",\n \"div\",\n \"form\",\n \"h2\",\n \"h3\",\n \"img\",\n \"input\",\n \"label\",\n \"li\",\n \"nav\",\n \"ol\",\n \"p\",\n \"select\",\n \"span\",\n \"svg\",\n \"ul\",\n] as const;\ntype PrimitiveNode = (typeof NODES)[number];\n\ntype WithRenderPropProps<T extends ElementType> =\n ComponentPropsWithoutRef<T> & {\n render?: ReactElement | undefined;\n };\n\ntype PrimitiveProps<E extends PrimitiveNode> = WithRenderPropProps<\n (typeof RadixPrimitive)[E]\n>;\n\ntype WithRenderPropRuntimeProps<T extends ElementType> =\n WithRenderPropProps<T> & {\n asChild?: boolean | undefined;\n children?: ReactNode | undefined;\n };\n\ntype PrimitiveRef<E extends PrimitiveNode> = ComponentRef<\n (typeof RadixPrimitive)[E]\n>;\n\n/**\n * Composes the children of a `render` element. Outer children win when supplied;\n * the render element's own children are the fallback.\n */\nfunction composeRenderElement(\n render: ReactElement,\n children: ReactNode,\n): ReactElement {\n return cloneElement(\n render,\n undefined,\n children !== undefined\n ? children\n : (render.props as { children?: ReactNode }).children,\n );\n}\n\n/**\n * Composes a `render` element at a call site that has already computed its props.\n *\n * `withRenderProp` wraps a component; this covers the case where the props are\n * already in hand and the composition happens inline.\n */\nfunction renderSlot(\n render: ReactElement,\n children: ReactNode,\n props: ComponentProps<typeof Slot.Root>,\n): ReactElement {\n return (\n <Slot.Root {...props}>{composeRenderElement(render, children)}</Slot.Root>\n );\n}\n\nfunction withRenderProp<T extends ElementType>(Component: T) {\n const Wrapped = forwardRef<ComponentRef<T>, WithRenderPropRuntimeProps<T>>(\n (\n {\n render,\n asChild,\n children,\n ...rest\n }: PropsWithoutRef<WithRenderPropRuntimeProps<T>>,\n ref,\n ) => {\n const Comp = Component as any;\n\n if (render && isValidElement(render)) {\n return (\n <Comp {...(rest as any)} asChild ref={ref}>\n {composeRenderElement(render, children)}\n </Comp>\n );\n }\n\n return (\n <Comp {...(rest as any)} asChild={asChild} ref={ref}>\n {children}\n </Comp>\n );\n },\n );\n\n const componentName =\n typeof Component === \"string\"\n ? Component\n : (Component.displayName ?? Component.name ?? \"Component\");\n Wrapped.displayName = componentName;\n\n return Wrapped as ForwardRefExoticComponent<\n WithRenderPropProps<T> & RefAttributes<ComponentRef<T>>\n >;\n}\n\nfunction createPrimitive<E extends PrimitiveNode>(node: E) {\n const RadixComp = RadixPrimitive[node];\n const Component = withRenderProp(RadixComp);\n\n Component.displayName = `Primitive.${node}`;\n return Component as ForwardRefExoticComponent<\n PrimitiveProps<E> & RefAttributes<PrimitiveRef<E>>\n >;\n}\n\nconst Primitive = NODES.reduce(\n (acc, node) => {\n acc[node] = createPrimitive(node);\n return acc;\n },\n {} as {\n [K in PrimitiveNode]: ReturnType<typeof createPrimitive<K>>;\n },\n);\n\nexport { Primitive, renderSlot, withRenderProp };\nexport type { PrimitiveProps, WithRenderPropProps };\n"],"mappings":";;;;;;;;;;;;;;;AA4BA,MAAMe,QAAQ;CACZ;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AAAI;;;;;AA2BN,SAASY,qBACPP,QACAK,UACc;CACd,OAAOhB,aACLW,QACAQ,KAAAA,GACAH,aAAaG,KAAAA,IACTH,WACCL,OAAOS,MAAmCJ,QACjD;AACF;;;;;;;AAQA,SAASK,WACPV,QACAK,UACAI,OACc;CACd,OACE,oBAAC,KAAK,MAAN;EAAW,GAAIA;EAAQF,UAAAA,qBAAqBP,QAAQK,QAAQ;CAAa,CAAA;AAE7E;AAEA,SAASO,eAAsCC,WAAc;CAC3D,MAAMC,UAAUxB,YACdyB,IAAAC,QAAA;EAAA,MAAAC,IAAAC,EAAA,EAAA;EAAA,IAAAd;EAAA,IAAAC;EAAA,IAAAL;EAAA,IAAAmB;EAAA,IAAAF,EAAA,OAAAF,IAAA;GACE,CAAA,CAAAf,QAAAI,SAAAC,UAAA,GAAAc,QAAAJ;GAKiDE,EAAA,KAAAF;GAAAE,EAAA,KAAAb;GAAAa,EAAA,KAAAZ;GAAAY,EAAA,KAAAjB;GAAAiB,EAAA,KAAAE;EAAA,OAAA;GAAAf,UAAAa,EAAA;GAAAZ,WAAAY,EAAA;GAAAjB,SAAAiB,EAAA;GAAAE,OAAAF,EAAA;EAAA;EAGjD,MAAAG,OAAaP;EAEb,IAAIb,UAAUT,eAAeS,MAAM,GAAC;GAErB,MAAAqB,KAAAF;GAAW,IAAAG;GAAA,IAAAL,EAAA,OAAAZ,YAAAY,EAAA,OAAAjB,QAAA;IACnBsB,KAAAf,qBAAqBP,QAAQK,QAAQ;IAACY,EAAA,KAAAZ;IAAAY,EAAA,KAAAjB;IAAAiB,EAAA,KAAAK;GAAA,OAAAA,KAAAL,EAAA;GAAA,IAAAM;GAAA,IAAAN,EAAA,OAAAD,OAAAC,EAAA,OAAAI,MAAAJ,EAAA,QAAAK,IAAA;IADzCC,KAAA,oBAAC,MAAD;KAAK,GAAMF;KAAc,SAAA;KAAaL;KACnCM,UAAAA;IADE,CAAA;IAEEL,EAAA,KAAAD;IAAAC,EAAA,KAAAI;IAAAJ,EAAA,MAAAK;IAAAL,EAAA,MAAAM;GAAA,OAAAA,KAAAN,EAAA;GAAA,OAFPM;EAEO;EAKE,MAAAF,KAAAF;EAAW,IAAAG;EAAA,IAAAL,EAAA,QAAAb,WAAAa,EAAA,QAAAZ,YAAAY,EAAA,QAAAD,OAAAC,EAAA,QAAAI,IAAA;GAAtBC,KAAA,oBAAC,MAAD;IAAK,GAAMD;IAAuBjB;IAAcY;IAC7CX;GADE,CAAA;GAEEY,EAAA,MAAAb;GAAAa,EAAA,MAAAZ;GAAAY,EAAA,MAAAD;GAAAC,EAAA,MAAAI;GAAAJ,EAAA,MAAAK;EAAA,OAAAA,KAAAL,EAAA;EAAA,OAFPK;CAEO,CAGb;CAMAR,QAAQW,cAHN,OAAOZ,cAAc,WACjBA,YACCA,UAAUY,eAAeZ,UAAUa,QAAQ;CAGlD,OAAOZ;AAGT;AAEA,SAASa,gBAAyCC,MAAS;CACzD,MAAMC,YAAYpC,YAAemC;CACjC,MAAMf,YAAYD,eAAeiB,SAAS;CAE1ChB,UAAUY,cAAc,aAAaG;CACrC,OAAOf;AAGT;AAEA,MAAMrB,YAAYG,MAAMmC,QACrBC,KAAKH,SAAS;CACbG,IAAIH,QAAQD,gBAAgBC,IAAI;CAChC,OAAOG;AACT,GACA,CAAC,CAGH"}
@@ -1 +1 @@
1
- {"version":3,"file":"getSelectionMessageId.d.ts","names":[],"sources":["../../src/utils/getSelectionMessageId.ts"],"mappings":";cA6Ca,wBAAyB,WAAW"}
1
+ {"version":3,"file":"getSelectionMessageId.d.ts","names":[],"sources":["../../src/utils/getSelectionMessageId.ts"],"mappings":";cAwDa,wBAAyB,WAAW"}
@@ -25,6 +25,14 @@ const findQuoteMarker = (node, messageElement) => {
25
25
  if (!messageElement.contains(marker)) return null;
26
26
  return marker;
27
27
  };
28
+ const intersectsExcluded = (scope, selection) => {
29
+ const ranges = Array.from({ length: selection.rangeCount }, (_, i) => selection.getRangeAt(i));
30
+ for (const marker of scope.querySelectorAll(QUOTE_SELECTABLE_SELECTOR)) {
31
+ if (!isExcluded(marker)) continue;
32
+ if (ranges.some((range) => range.intersectsNode(marker))) return true;
33
+ }
34
+ return false;
35
+ };
28
36
  const getSelectionMessageId = (selection) => {
29
37
  const { anchorNode, focusNode } = selection;
30
38
  if (!anchorNode || !focusNode) return null;
@@ -37,9 +45,15 @@ const getSelectionMessageId = (selection) => {
37
45
  const focusMarker = findQuoteMarker(focusNode, anchorMessageElement);
38
46
  if (anchorMarker && isExcluded(anchorMarker)) return null;
39
47
  if (focusMarker && isExcluded(focusMarker)) return null;
40
- if (!hasQuoteSelectableRegion(anchorMessageElement)) return messageId;
41
- if (!anchorMarker || anchorMarker !== focusMarker) return null;
42
- return messageId;
48
+ if (hasQuoteSelectableRegion(anchorMessageElement)) {
49
+ if (!anchorMarker || anchorMarker !== focusMarker) return null;
50
+ }
51
+ const scope = anchorMarker ?? anchorMessageElement;
52
+ for (let i = 0; i < selection.rangeCount; i++) {
53
+ const { commonAncestorContainer } = selection.getRangeAt(i);
54
+ if (!scope.contains(commonAncestorContainer)) return null;
55
+ }
56
+ return intersectsExcluded(scope, selection) ? null : messageId;
43
57
  };
44
58
  //#endregion
45
59
  export { getSelectionMessageId };
@@ -1 +1 @@
1
- {"version":3,"file":"getSelectionMessageId.js","names":["QUOTE_SELECTABLE_SELECTOR","getElement","node","Node","HTMLElement","parentElement","findMessageElement","el","id","getAttribute","isExcluded","marker","Element","hasQuoteSelectableRegion","messageElement","matches","querySelectorAll","findQuoteMarker","closest","contains","getSelectionMessageId","selection","Selection","anchorNode","focusNode","anchorMessageElement","focusMessageElement","messageId","anchorMarker","focusMarker"],"sources":["../../src/utils/getSelectionMessageId.ts"],"sourcesContent":["const QUOTE_SELECTABLE_SELECTOR = \"[data-aui-quote-selectable]\";\n\nconst getElement = (node: Node | null): HTMLElement | null => {\n return node instanceof HTMLElement ? node : (node?.parentElement ?? null);\n};\n\nconst findMessageElement = (node: Node | null): HTMLElement | null => {\n let el = node instanceof HTMLElement ? node : (node?.parentElement ?? null);\n while (el) {\n const id = el.getAttribute(\"data-message-id\");\n if (id) return el;\n el = el.parentElement;\n }\n return null;\n};\n\nconst isExcluded = (marker: Element): boolean => {\n return marker.getAttribute(\"data-aui-quote-selectable\") === \"false\";\n};\n\nconst hasQuoteSelectableRegion = (messageElement: HTMLElement) => {\n if (\n messageElement.matches(QUOTE_SELECTABLE_SELECTOR) &&\n !isExcluded(messageElement)\n ) {\n return true;\n }\n for (const marker of messageElement.querySelectorAll(\n QUOTE_SELECTABLE_SELECTOR,\n )) {\n if (!isExcluded(marker)) return true;\n }\n return false;\n};\n\nconst findQuoteMarker = (\n node: Node | null,\n messageElement: HTMLElement,\n): HTMLElement | null => {\n const marker = getElement(node)?.closest(QUOTE_SELECTABLE_SELECTOR);\n if (!(marker instanceof HTMLElement)) return null;\n if (!messageElement.contains(marker)) return null;\n return marker;\n};\n\nexport const getSelectionMessageId = (selection: Selection): string | null => {\n const { anchorNode, focusNode } = selection;\n if (!anchorNode || !focusNode) return null;\n\n const anchorMessageElement = findMessageElement(anchorNode);\n const focusMessageElement = findMessageElement(focusNode);\n\n if (!anchorMessageElement || anchorMessageElement !== focusMessageElement) {\n return null;\n }\n\n const messageId = anchorMessageElement.getAttribute(\"data-message-id\");\n if (!messageId) return null;\n\n const anchorMarker = findQuoteMarker(anchorNode, anchorMessageElement);\n const focusMarker = findQuoteMarker(focusNode, anchorMessageElement);\n\n if (anchorMarker && isExcluded(anchorMarker)) return null;\n if (focusMarker && isExcluded(focusMarker)) return null;\n\n if (!hasQuoteSelectableRegion(anchorMessageElement)) return messageId;\n\n if (!anchorMarker || anchorMarker !== focusMarker) return null;\n\n return messageId;\n};\n"],"mappings":";AAAA,MAAMA,4BAA4B;AAElC,MAAMC,cAAcC,SAA0C;CAC5D,OAAOA,gBAAgBE,cAAcF,OAAQA,MAAMG,iBAAiB;AACtE;AAEA,MAAMC,sBAAsBJ,SAA0C;CACpE,IAAIK,KAAKL,gBAAgBE,cAAcF,OAAQA,MAAMG,iBAAiB;CACtE,OAAOE,IAAI;EAET,IADWA,GAAGE,aAAa,iBACvBD,GAAI,OAAOD;EACfA,KAAKA,GAAGF;CACV;CACA,OAAO;AACT;AAEA,MAAMK,cAAcC,WAA6B;CAC/C,OAAOA,OAAOF,aAAa,2BAA2B,MAAM;AAC9D;AAEA,MAAMI,4BAA4BC,mBAAgC;CAChE,IACEA,eAAeC,QAAQf,yBAAyB,KAChD,CAACU,WAAWI,cAAc,GAE1B,OAAO;CAET,KAAK,MAAMH,UAAUG,eAAeE,iBAClChB,yBACF,GACE,IAAI,CAACU,WAAWC,MAAM,GAAG,OAAO;CAElC,OAAO;AACT;AAEA,MAAMM,mBACJf,MACAY,mBACuB;CACvB,MAAMH,SAASV,WAAWC,IAAI,CAAC,EAAEgB,QAAQlB,yBAAyB;CAClE,IAAI,EAAEW,kBAAkBP,cAAc,OAAO;CAC7C,IAAI,CAACU,eAAeK,SAASR,MAAM,GAAG,OAAO;CAC7C,OAAOA;AACT;AAEA,MAAaS,yBAAyBC,cAAwC;CAC5E,MAAM,EAAEE,YAAYC,cAAcH;CAClC,IAAI,CAACE,cAAc,CAACC,WAAW,OAAO;CAEtC,MAAMC,uBAAuBnB,mBAAmBiB,UAAU;CAC1D,MAAMG,sBAAsBpB,mBAAmBkB,SAAS;CAExD,IAAI,CAACC,wBAAwBA,yBAAyBC,qBACpD,OAAO;CAGT,MAAMC,YAAYF,qBAAqBhB,aAAa,iBAAiB;CACrE,IAAI,CAACkB,WAAW,OAAO;CAEvB,MAAMC,eAAeX,gBAAgBM,YAAYE,oBAAoB;CACrE,MAAMI,cAAcZ,gBAAgBO,WAAWC,oBAAoB;CAEnE,IAAIG,gBAAgBlB,WAAWkB,YAAY,GAAG,OAAO;CACrD,IAAIC,eAAenB,WAAWmB,WAAW,GAAG,OAAO;CAEnD,IAAI,CAAChB,yBAAyBY,oBAAoB,GAAG,OAAOE;CAE5D,IAAI,CAACC,gBAAgBA,iBAAiBC,aAAa,OAAO;CAE1D,OAAOF;AACT"}
1
+ {"version":3,"file":"getSelectionMessageId.js","names":["QUOTE_SELECTABLE_SELECTOR","getElement","node","Node","HTMLElement","parentElement","findMessageElement","el","id","getAttribute","isExcluded","marker","Element","hasQuoteSelectableRegion","messageElement","matches","querySelectorAll","findQuoteMarker","closest","contains","intersectsExcluded","scope","selection","Selection","ranges","Array","from","length","rangeCount","_","i","getRangeAt","some","range","intersectsNode","getSelectionMessageId","anchorNode","focusNode","anchorMessageElement","focusMessageElement","messageId","anchorMarker","focusMarker","commonAncestorContainer"],"sources":["../../src/utils/getSelectionMessageId.ts"],"sourcesContent":["const QUOTE_SELECTABLE_SELECTOR = \"[data-aui-quote-selectable]\";\n\nconst getElement = (node: Node | null): HTMLElement | null => {\n return node instanceof HTMLElement ? node : (node?.parentElement ?? null);\n};\n\nconst findMessageElement = (node: Node | null): HTMLElement | null => {\n let el = node instanceof HTMLElement ? node : (node?.parentElement ?? null);\n while (el) {\n const id = el.getAttribute(\"data-message-id\");\n if (id) return el;\n el = el.parentElement;\n }\n return null;\n};\n\nconst isExcluded = (marker: Element): boolean => {\n return marker.getAttribute(\"data-aui-quote-selectable\") === \"false\";\n};\n\nconst hasQuoteSelectableRegion = (messageElement: HTMLElement) => {\n if (\n messageElement.matches(QUOTE_SELECTABLE_SELECTOR) &&\n !isExcluded(messageElement)\n ) {\n return true;\n }\n for (const marker of messageElement.querySelectorAll(\n QUOTE_SELECTABLE_SELECTOR,\n )) {\n if (!isExcluded(marker)) return true;\n }\n return false;\n};\n\nconst findQuoteMarker = (\n node: Node | null,\n messageElement: HTMLElement,\n): HTMLElement | null => {\n const marker = getElement(node)?.closest(QUOTE_SELECTABLE_SELECTOR);\n if (!(marker instanceof HTMLElement)) return null;\n if (!messageElement.contains(marker)) return null;\n return marker;\n};\n\nconst intersectsExcluded = (scope: Element, selection: Selection): boolean => {\n const ranges = Array.from({ length: selection.rangeCount }, (_, i) =>\n selection.getRangeAt(i),\n );\n for (const marker of scope.querySelectorAll(QUOTE_SELECTABLE_SELECTOR)) {\n if (!isExcluded(marker)) continue;\n if (ranges.some((range) => range.intersectsNode(marker))) return true;\n }\n return false;\n};\n\nexport const getSelectionMessageId = (selection: Selection): string | null => {\n const { anchorNode, focusNode } = selection;\n if (!anchorNode || !focusNode) return null;\n\n const anchorMessageElement = findMessageElement(anchorNode);\n const focusMessageElement = findMessageElement(focusNode);\n\n if (!anchorMessageElement || anchorMessageElement !== focusMessageElement) {\n return null;\n }\n\n const messageId = anchorMessageElement.getAttribute(\"data-message-id\");\n if (!messageId) return null;\n\n const anchorMarker = findQuoteMarker(anchorNode, anchorMessageElement);\n const focusMarker = findQuoteMarker(focusNode, anchorMessageElement);\n\n if (anchorMarker && isExcluded(anchorMarker)) return null;\n if (focusMarker && isExcluded(focusMarker)) return null;\n\n if (hasQuoteSelectableRegion(anchorMessageElement)) {\n if (!anchorMarker || anchorMarker !== focusMarker) return null;\n }\n\n const scope = anchorMarker ?? anchorMessageElement;\n\n for (let i = 0; i < selection.rangeCount; i++) {\n const { commonAncestorContainer } = selection.getRangeAt(i);\n if (!scope.contains(commonAncestorContainer)) return null;\n }\n\n return intersectsExcluded(scope, selection) ? null : messageId;\n};\n"],"mappings":";AAAA,MAAMA,4BAA4B;AAElC,MAAMC,cAAcC,SAA0C;CAC5D,OAAOA,gBAAgBE,cAAcF,OAAQA,MAAMG,iBAAiB;AACtE;AAEA,MAAMC,sBAAsBJ,SAA0C;CACpE,IAAIK,KAAKL,gBAAgBE,cAAcF,OAAQA,MAAMG,iBAAiB;CACtE,OAAOE,IAAI;EAET,IADWA,GAAGE,aAAa,iBACvBD,GAAI,OAAOD;EACfA,KAAKA,GAAGF;CACV;CACA,OAAO;AACT;AAEA,MAAMK,cAAcC,WAA6B;CAC/C,OAAOA,OAAOF,aAAa,2BAA2B,MAAM;AAC9D;AAEA,MAAMI,4BAA4BC,mBAAgC;CAChE,IACEA,eAAeC,QAAQf,yBAAyB,KAChD,CAACU,WAAWI,cAAc,GAE1B,OAAO;CAET,KAAK,MAAMH,UAAUG,eAAeE,iBAClChB,yBACF,GACE,IAAI,CAACU,WAAWC,MAAM,GAAG,OAAO;CAElC,OAAO;AACT;AAEA,MAAMM,mBACJf,MACAY,mBACuB;CACvB,MAAMH,SAASV,WAAWC,IAAI,CAAC,EAAEgB,QAAQlB,yBAAyB;CAClE,IAAI,EAAEW,kBAAkBP,cAAc,OAAO;CAC7C,IAAI,CAACU,eAAeK,SAASR,MAAM,GAAG,OAAO;CAC7C,OAAOA;AACT;AAEA,MAAMS,sBAAsBC,OAAgBC,cAAkC;CAC5E,MAAME,SAASC,MAAMC,KAAK,EAAEC,QAAQL,UAAUM,WAAW,IAAIC,GAAGC,MAC9DR,UAAUS,WAAWD,CAAC,CACxB;CACA,KAAK,MAAMnB,UAAUU,MAAML,iBAAiBhB,yBAAyB,GAAG;EACtE,IAAI,CAACU,WAAWC,MAAM,GAAG;EACzB,IAAIa,OAAOQ,MAAMC,UAAUA,MAAMC,eAAevB,MAAM,CAAC,GAAG,OAAO;CACnE;CACA,OAAO;AACT;AAEA,MAAawB,yBAAyBb,cAAwC;CAC5E,MAAM,EAAEc,YAAYC,cAAcf;CAClC,IAAI,CAACc,cAAc,CAACC,WAAW,OAAO;CAEtC,MAAMC,uBAAuBhC,mBAAmB8B,UAAU;CAC1D,MAAMG,sBAAsBjC,mBAAmB+B,SAAS;CAExD,IAAI,CAACC,wBAAwBA,yBAAyBC,qBACpD,OAAO;CAGT,MAAMC,YAAYF,qBAAqB7B,aAAa,iBAAiB;CACrE,IAAI,CAAC+B,WAAW,OAAO;CAEvB,MAAMC,eAAexB,gBAAgBmB,YAAYE,oBAAoB;CACrE,MAAMI,cAAczB,gBAAgBoB,WAAWC,oBAAoB;CAEnE,IAAIG,gBAAgB/B,WAAW+B,YAAY,GAAG,OAAO;CACrD,IAAIC,eAAehC,WAAWgC,WAAW,GAAG,OAAO;CAEnD,IAAI7B,yBAAyByB,oBAAoB,GAC3C;MAAA,CAACG,gBAAgBA,iBAAiBC,aAAa,OAAO;CAAA;CAG5D,MAAMrB,QAAQoB,gBAAgBH;CAE9B,KAAK,IAAIR,IAAI,GAAGA,IAAIR,UAAUM,YAAYE,KAAK;EAC7C,MAAM,EAAEa,4BAA4BrB,UAAUS,WAAWD,CAAC;EAC1D,IAAI,CAACT,MAAMF,SAASwB,uBAAuB,GAAG,OAAO;CACvD;CAEA,OAAOvB,mBAAmBC,OAAOC,SAAS,IAAI,OAAOkB;AACvD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react",
3
- "version": "0.15.17",
3
+ "version": "0.15.19",
4
4
  "description": "Open-source TypeScript/React library for building production-grade AI chat experiences",
5
5
  "keywords": [
6
6
  "ai",
@@ -55,15 +55,15 @@
55
55
  ],
56
56
  "sideEffects": false,
57
57
  "dependencies": {
58
- "@assistant-ui/core": "^0.3.16",
59
- "@assistant-ui/store": "^0.3.11",
60
- "@assistant-ui/tap": "^0.9.15",
61
- "assistant-cloud": "^0.1.42",
62
- "assistant-stream": "^0.3.40",
58
+ "@assistant-ui/core": "^0.3.18",
59
+ "@assistant-ui/store": "^0.3.13",
60
+ "@assistant-ui/tap": "^0.9.17",
61
+ "assistant-cloud": "^0.2.0",
62
+ "assistant-stream": "^0.3.42",
63
63
  "radix-ui": "^1.6.7",
64
64
  "react-textarea-autosize": "^8.5.9",
65
- "safe-content-frame": "^0.0.28",
66
- "zod": "^4.4.3",
65
+ "safe-content-frame": "^0.0.30",
66
+ "zod": "^4.5.4",
67
67
  "zustand": "^5.0.15"
68
68
  },
69
69
  "peerDependencies": {
@@ -81,13 +81,13 @@
81
81
  }
82
82
  },
83
83
  "devDependencies": {
84
- "@assistant-ui/vite": "0.0.15",
85
- "@assistant-ui/x-buildutils": "0.0.24",
86
- "@testing-library/react": "^16.3.2",
84
+ "@assistant-ui/vite": "0.0.17",
85
+ "@assistant-ui/x-buildutils": "0.0.26",
86
+ "@testing-library/react": "^16.3.3",
87
87
  "@types/json-schema": "^7.0.15",
88
- "@types/node": "^26.2.0",
88
+ "@types/node": "^26.4.1",
89
89
  "@types/react": "^19.2.18",
90
- "@types/react-dom": "^19.2.5",
90
+ "@types/react-dom": "^19.2.7",
91
91
  "jsdom": "^30.0.1",
92
92
  "react": "^19.2.8",
93
93
  "react-dom": "^19.2.8",
@@ -25,8 +25,8 @@ const useThreadViewportStoreValue = (options: ThreadViewportStoreOptions) => {
25
25
 
26
26
  // Forward scrollToBottom from outer viewport to inner viewport
27
27
  useEffect(() => {
28
- return outerViewport?.getState().onScrollToBottom(() => {
29
- store.getState().scrollToBottom();
28
+ return outerViewport?.getState().onScrollToBottom((config) => {
29
+ store.getState().scrollToBottom(config);
30
30
  });
31
31
  }, [outerViewport, store]);
32
32
 
@@ -36,6 +36,9 @@ export const useToolCallElapsed = (): number | undefined => {
36
36
 
37
37
  useEffect(() => {
38
38
  if (!running) return undefined;
39
+ // The clock is an external source; this catches the elapsed value up before
40
+ // the interval takes over.
41
+ // eslint-disable-next-line react-hooks/set-state-in-effect
39
42
  setNow(Date.now());
40
43
  const id = setInterval(() => setNow(Date.now()), 1000);
41
44
  return () => clearInterval(id);
package/src/index.ts CHANGED
@@ -43,8 +43,10 @@ export type {
43
43
  ThreadListItemState,
44
44
  } from "@assistant-ui/core";
45
45
 
46
+ export { toolApprovalAcceptsText } from "@assistant-ui/core";
47
+
46
48
  export { useCloudThreadListRuntime } from "@assistant-ui/core/react";
47
- export { AssistantCloud } from "assistant-cloud";
49
+ export { AssistantCloud, readAnonymousRefreshToken } from "assistant-cloud";
48
50
 
49
51
  // --- adapters/attachment ---
50
52
  export type { AttachmentAdapter } from "@assistant-ui/core";
@@ -360,6 +362,7 @@ export type {
360
362
  GenerativeUISpec,
361
363
  Unstable_AudioMessagePart,
362
364
  RespondToToolApprovalOptions,
365
+ ToolApprovalDisplay,
363
366
  ToolApprovalOption,
364
367
  ToolApprovalOptionKind,
365
368
  ToolApprovalResponse,
@@ -515,6 +518,7 @@ export {
515
518
  getMcpAppFromToolPart,
516
519
  } from "./mcp-apps";
517
520
  export type {
521
+ McpAppPartOptions,
518
522
  McpAppRendererOptions,
519
523
  McpAppMetadata,
520
524
  McpAppResource,
@@ -531,3 +535,12 @@ export type {
531
535
  ToolCallMessagePartMcpMetadata,
532
536
  } from "./mcp-apps";
533
537
  export type { McpAppResourceOutput } from "@assistant-ui/core/react";
538
+ export type { ShimLoadError, ShimLoadErrorCode } from "safe-content-frame";
539
+
540
+ // Unstable - WebMCP provider (exposes frontend tools to a WebMCP-capable browser)
541
+ export {
542
+ unstable_useWebMcpProvider,
543
+ type Unstable_WebMcpProviderOptions,
544
+ type Unstable_WebMcpProviderResult,
545
+ } from "./unstable/webmcp/useWebMcpProvider";
546
+ export { defaultWebMcpFilter as unstable_defaultWebMcpFilter } from "./unstable/webmcp/convertTools";
@@ -9,16 +9,21 @@ import type {
9
9
  import { beforeEach, describe, expect, it, vi } from "vitest";
10
10
  import type {
11
11
  McpAppBridgeHandlers,
12
+ McpAppFrameProps,
13
+ McpAppHostContext,
12
14
  McpAppsHost,
13
15
  McpAppsRemoteHostOptions,
14
16
  } from "./types";
15
17
 
16
- const { framePropsMock } = vi.hoisted(() => ({ framePropsMock: vi.fn() }));
18
+ const { appendMock, framePropsMock } = vi.hoisted(() => ({
19
+ appendMock: vi.fn(),
20
+ framePropsMock: vi.fn(),
21
+ }));
17
22
 
18
23
  vi.mock("@assistant-ui/store", async (importOriginal) => ({
19
24
  ...(await importOriginal()),
20
25
  useAui: () => ({
21
- thread: () => ({ append: vi.fn() }),
26
+ thread: { append: appendMock },
22
27
  }),
23
28
  }));
24
29
 
@@ -43,9 +48,12 @@ const createDeferred = <T,>() => {
43
48
  return { promise, resolve };
44
49
  };
45
50
 
46
- const createPart = (serverId?: string): ToolCallMessagePartProps => ({
51
+ const createPart = (
52
+ serverId?: string,
53
+ toolCallId = "call-1",
54
+ ): ToolCallMessagePartProps => ({
47
55
  type: "tool-call",
48
- toolCallId: "call-1",
56
+ toolCallId,
49
57
  toolName: "search",
50
58
  args: {},
51
59
  argsText: "{}",
@@ -105,6 +113,60 @@ function MemoizedHarness({ host }: { host: McpAppsHost }) {
105
113
  return <MemoizedPart Renderer={renderer.render} />;
106
114
  }
107
115
 
116
+ const MemoizedPartById = memo(function MemoizedPartById({
117
+ Renderer,
118
+ toolCallId,
119
+ }: {
120
+ Renderer: ToolCallMessagePartComponent;
121
+ toolCallId: string;
122
+ }) {
123
+ return <Renderer {...createPart(undefined, toolCallId)} />;
124
+ });
125
+
126
+ function OptionsHarness({
127
+ host,
128
+ hostContext,
129
+ handlers,
130
+ forPart,
131
+ toolCallIds = ["call-1"],
132
+ }: {
133
+ host: McpAppsHost;
134
+ hostContext?: McpAppHostContext;
135
+ handlers?: McpAppRendererOptions["handlers"];
136
+ forPart?: McpAppRendererOptions["forPart"];
137
+ toolCallIds?: string[];
138
+ }) {
139
+ const renderer = useResource(
140
+ McpAppRenderer({
141
+ host: Host({ host }),
142
+ ...(hostContext === undefined ? {} : { hostContext }),
143
+ ...(handlers === undefined ? {} : { handlers }),
144
+ ...(forPart === undefined ? {} : { forPart }),
145
+ }),
146
+ );
147
+ return toolCallIds.map((toolCallId) => (
148
+ <MemoizedPartById
149
+ key={toolCallId}
150
+ Renderer={renderer.render}
151
+ toolCallId={toolCallId}
152
+ />
153
+ ));
154
+ }
155
+
156
+ const framePropsCalls = () =>
157
+ framePropsMock.mock.calls.map(([props]) => props as McpAppFrameProps);
158
+
159
+ const loadingHost = (): McpAppsHost => ({
160
+ loadResource: vi.fn(async ({ uri }) => ({
161
+ uri,
162
+ mimeType: "text/html;profile=mcp-app" as const,
163
+ html: "",
164
+ })),
165
+ callTool: vi.fn(),
166
+ readResource: vi.fn(),
167
+ listResources: vi.fn(),
168
+ });
169
+
108
170
  function RemoteHarness({
109
171
  url,
110
172
  headers,
@@ -127,9 +189,146 @@ function RemoteHarness({
127
189
 
128
190
  describe("McpAppRenderer", () => {
129
191
  beforeEach(() => {
192
+ appendMock.mockReset();
130
193
  framePropsMock.mockReset();
131
194
  });
132
195
 
196
+ it("loads newly mounted parts through the committed host", async () => {
197
+ const hostA = loadingHost();
198
+ const hostB = loadingHost();
199
+ const view = render(
200
+ <OptionsHarness host={hostA} toolCallIds={["call-1"]} />,
201
+ );
202
+ await waitFor(() => expect(hostA.loadResource).toHaveBeenCalledOnce());
203
+ vi.mocked(hostA.loadResource).mockClear();
204
+
205
+ view.rerender(
206
+ <OptionsHarness host={hostB} toolCallIds={["call-1", "call-2"]} />,
207
+ );
208
+ await waitFor(() => expect(hostB.loadResource).toHaveBeenCalled());
209
+
210
+ expect(hostA.loadResource).not.toHaveBeenCalled();
211
+ });
212
+
213
+ it("merges forPart handlers over the thread-wide handlers", async () => {
214
+ const onInitialized = vi.fn();
215
+ render(
216
+ <OptionsHarness
217
+ host={loadingHost()}
218
+ handlers={{ onInitialized }}
219
+ forPart={(part) => ({
220
+ handlers: {
221
+ requestDisplayMode: ({ mode }) => {
222
+ void part;
223
+ return { mode };
224
+ },
225
+ },
226
+ })}
227
+ />,
228
+ );
229
+
230
+ await waitFor(() => expect(framePropsCalls().length).toBeGreaterThan(0));
231
+ const handlers = framePropsCalls().at(-1)?.handlers;
232
+ expect(handlers?.requestDisplayMode).toBeDefined();
233
+ handlers?.onInitialized?.();
234
+ expect(onInitialized).toHaveBeenCalledOnce();
235
+ });
236
+
237
+ it("leaves mounted parts alone when renderer options are unchanged", async () => {
238
+ const host = loadingHost();
239
+ const hostContext: McpAppHostContext = { displayMode: "inline" };
240
+ const view = () => <OptionsHarness host={host} hostContext={hostContext} />;
241
+
242
+ const rendered = render(view());
243
+ await waitFor(() => expect(framePropsCalls().length).toBeGreaterThan(0));
244
+ const rendersBefore = framePropsCalls().length;
245
+
246
+ rendered.rerender(view());
247
+ expect(framePropsCalls().length).toBe(rendersBefore);
248
+ });
249
+
250
+ it("delivers a changed renderer host context to mounted parts", async () => {
251
+ const host = loadingHost();
252
+ const view = render(
253
+ <OptionsHarness host={host} hostContext={{ displayMode: "inline" }} />,
254
+ );
255
+ await waitFor(() =>
256
+ expect(framePropsCalls().at(-1)?.hostContext).toEqual({
257
+ displayMode: "inline",
258
+ }),
259
+ );
260
+
261
+ view.rerender(
262
+ <OptionsHarness
263
+ host={host}
264
+ hostContext={{ displayMode: "fullscreen" }}
265
+ />,
266
+ );
267
+ await waitFor(() =>
268
+ expect(framePropsCalls().at(-1)?.hostContext).toEqual({
269
+ displayMode: "fullscreen",
270
+ }),
271
+ );
272
+ });
273
+
274
+ it("resolves host context per part through forPart", async () => {
275
+ const modes: Record<string, string> = {
276
+ "call-1": "inline",
277
+ "call-2": "fullscreen",
278
+ };
279
+ render(
280
+ <OptionsHarness
281
+ host={loadingHost()}
282
+ hostContext={{ theme: "light" }}
283
+ toolCallIds={["call-1", "call-2"]}
284
+ forPart={(part) => ({
285
+ hostContext: {
286
+ theme: "light",
287
+ displayMode: modes[part.toolCallId] as "inline" | "fullscreen",
288
+ },
289
+ })}
290
+ />,
291
+ );
292
+
293
+ await waitFor(() => expect(framePropsCalls().length).toBeGreaterThan(1));
294
+ const delivered = framePropsCalls().map((props) => props.hostContext);
295
+ expect(delivered).toContainEqual({
296
+ theme: "light",
297
+ displayMode: "inline",
298
+ });
299
+ expect(delivered).toContainEqual({
300
+ theme: "light",
301
+ displayMode: "fullscreen",
302
+ });
303
+ });
304
+
305
+ it("gives forPart handlers the part that owns the frame", async () => {
306
+ const promoted: string[] = [];
307
+ render(
308
+ <OptionsHarness
309
+ host={loadingHost()}
310
+ toolCallIds={["call-1", "call-2"]}
311
+ forPart={(part) => ({
312
+ hostContext: { toolCallId: part.toolCallId },
313
+ handlers: {
314
+ requestDisplayMode: ({ mode }) => {
315
+ promoted.push(`${part.toolCallId}:${mode}`);
316
+ return { mode };
317
+ },
318
+ },
319
+ })}
320
+ />,
321
+ );
322
+
323
+ await waitFor(() => expect(framePropsCalls().length).toBeGreaterThan(1));
324
+ const second = framePropsCalls().find(
325
+ (props) => props.hostContext?.["toolCallId"] === "call-2",
326
+ );
327
+ await second?.handlers?.requestDisplayMode?.({ mode: "fullscreen" });
328
+
329
+ expect(promoted).toEqual(["call-2:fullscreen"]);
330
+ });
331
+
133
332
  it("injects serverId into loadResource and reloads when it changes", async () => {
134
333
  const loadResource = vi.fn(async ({ uri }: { uri: string }) => ({
135
334
  uri,
@@ -250,7 +449,7 @@ describe("McpAppRenderer", () => {
250
449
  framePropsMock.mockClear();
251
450
  view.rerender(<MemoizedHarness host={nextHost} />);
252
451
 
253
- expect(nextHost.loadResource).toHaveBeenCalledTimes(1);
452
+ await waitFor(() => expect(nextHost.loadResource).toHaveBeenCalledTimes(1));
254
453
  expect(framePropsMock).not.toHaveBeenCalled();
255
454
 
256
455
  nextResource.resolve({
@@ -473,6 +672,55 @@ describe("McpAppRenderer", () => {
473
672
  expect(listResources).toHaveBeenCalledWith(params);
474
673
  });
475
674
 
675
+ it("appends MCP Apps message content in the default sendMessage handler", async () => {
676
+ render(<Harness host={loadingHost()} />);
677
+ await waitFor(() => expect(framePropsMock).toHaveBeenCalled());
678
+ const handlers = framePropsMock.mock.lastCall?.[0]
679
+ .handlers as McpAppBridgeHandlers;
680
+
681
+ expect(
682
+ handlers.sendMessage?.({
683
+ role: "user",
684
+ content: [
685
+ { type: "text", text: "Open Grade 5 Math" },
686
+ { type: "image", data: "image", mimeType: "image/png" },
687
+ { type: "text", text: "Period 4" },
688
+ ],
689
+ }),
690
+ ).toEqual({ ok: true });
691
+ expect(appendMock).toHaveBeenCalledWith({
692
+ content: [
693
+ { type: "text", text: "Open Grade 5 Math" },
694
+ { type: "text", text: "Period 4" },
695
+ ],
696
+ });
697
+
698
+ expect(handlers.sendMessage?.({ text: "typed instead" })).toEqual({
699
+ ok: true,
700
+ });
701
+ expect(appendMock).toHaveBeenLastCalledWith({
702
+ content: [{ type: "text", text: "typed instead" }],
703
+ });
704
+
705
+ expect(
706
+ handlers.sendMessage?.({
707
+ role: "user",
708
+ content: [{ type: "image", data: "image", mimeType: "image/png" }],
709
+ }),
710
+ ).toEqual({
711
+ isError: true,
712
+ ok: false,
713
+ reason: "no text content to append",
714
+ });
715
+
716
+ expect(handlers.sendMessage?.({ role: "user" })).toEqual({
717
+ isError: true,
718
+ ok: false,
719
+ reason: "unrecognised params shape",
720
+ });
721
+ expect(appendMock).toHaveBeenCalledTimes(2);
722
+ });
723
+
476
724
  it("uses caller UI handlers and keeps data-plane handlers on the host", async () => {
477
725
  const host: McpAppsHost = {
478
726
  loadResource: vi.fn(async ({ uri }) => ({