@assistant-ui/react 0.15.18 → 0.15.20

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 (113) hide show
  1. package/README.md +7 -1
  2. package/dist/context/providers/ThreadViewportProvider.js +2 -2
  3. package/dist/context/providers/ThreadViewportProvider.js.map +1 -1
  4. package/dist/context/stores/ThreadViewport.d.ts.map +1 -1
  5. package/dist/context/stores/ThreadViewport.js +2 -1
  6. package/dist/context/stores/ThreadViewport.js.map +1 -1
  7. package/dist/hooks/useToolCallElapsed.js.map +1 -1
  8. package/dist/index.d.ts +5 -5
  9. package/dist/index.js +3 -3
  10. package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.d.ts +2 -2
  11. package/dist/legacy-runtime/runtime-cores/external-store/external-message-converter.js +2 -2
  12. package/dist/mcp-apps/McpAppRenderer.js +1 -1
  13. package/dist/mcp-apps/app-frame.d.ts.map +1 -1
  14. package/dist/mcp-apps/app-frame.js +18 -13
  15. package/dist/mcp-apps/app-frame.js.map +1 -1
  16. package/dist/mcp-apps/bridge.js +7 -1
  17. package/dist/mcp-apps/bridge.js.map +1 -1
  18. package/dist/model-context/frame/useAssistantFrameHost.js +20 -2
  19. package/dist/model-context/frame/useAssistantFrameHost.js.map +1 -1
  20. package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
  21. package/dist/primitives/assistantModal/AssistantModalRoot.js +47 -79
  22. package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
  23. package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
  24. package/dist/primitives/composer/ComposerAttachmentDropzone.js +61 -138
  25. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  26. package/dist/primitives/composer/ComposerInput.js +3 -8
  27. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  28. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
  29. package/dist/primitives/composer/trigger/triggerKeyboardResource.js +53 -61
  30. package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
  31. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
  32. package/dist/primitives/composer/trigger/triggerNavigationResource.js +127 -137
  33. package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
  34. package/dist/primitives/message/MessageParts.js +2 -14
  35. package/dist/primitives/message/MessageParts.js.map +1 -1
  36. package/dist/primitives/message/MessagePartsGrouped.d.ts +3 -3
  37. package/dist/primitives/message/MessagePartsGrouped.js +2 -2
  38. package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
  39. package/dist/primitives/messagePart/MessagePartText.d.ts +2 -0
  40. package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
  41. package/dist/primitives/messagePart/MessagePartText.js +13 -36
  42. package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
  43. package/dist/primitives/reasoning/useScrollLock.js +3 -1
  44. package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
  45. package/dist/sandbox-host/SandboxHost.js +23 -5
  46. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  47. package/dist/unstable/useLiveCompletionAdapter.js +24 -2
  48. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  49. package/dist/unstable/useMentionAdapter.js +2 -2
  50. package/dist/unstable/useMentionAdapter.js.map +1 -1
  51. package/dist/unstable/useMessageStallDetection.d.ts +4 -5
  52. package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
  53. package/dist/unstable/useMessageStallDetection.js +16 -15
  54. package/dist/unstable/useMessageStallDetection.js.map +1 -1
  55. package/dist/unstable/webmcp/convertTools.d.ts.map +1 -1
  56. package/dist/unstable/webmcp/convertTools.js +54 -3
  57. package/dist/unstable/webmcp/convertTools.js.map +1 -1
  58. package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -1
  59. package/dist/unstable/webmcp/useWebMcpProvider.js +2 -1
  60. package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -1
  61. package/dist/utils/Primitive.d.ts +10 -2
  62. package/dist/utils/Primitive.d.ts.map +1 -1
  63. package/dist/utils/Primitive.js +25 -6
  64. package/dist/utils/Primitive.js.map +1 -1
  65. package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
  66. package/dist/utils/getSelectionMessageId.js +17 -3
  67. package/dist/utils/getSelectionMessageId.js.map +1 -1
  68. package/package.json +12 -12
  69. package/src/context/providers/ThreadViewportProvider.tsx +2 -2
  70. package/src/context/stores/ThreadViewport.test.ts +29 -0
  71. package/src/context/stores/ThreadViewport.ts +6 -3
  72. package/src/hooks/useToolCallElapsed.ts +3 -0
  73. package/src/index.ts +6 -1
  74. package/src/legacy-runtime/runtime-cores/external-store/external-message-converter.ts +2 -0
  75. package/src/mcp-apps/app-frame.test.tsx +189 -1
  76. package/src/mcp-apps/app-frame.tsx +31 -22
  77. package/src/mcp-apps/bridge.test.ts +98 -0
  78. package/src/mcp-apps/bridge.ts +7 -1
  79. package/src/model-context/frame/useAssistantFrameHost.test.tsx +42 -0
  80. package/src/model-context/frame/useAssistantFrameHost.ts +25 -2
  81. package/src/primitives/assistantModal/AssistantModalRoot.test.tsx +123 -0
  82. package/src/primitives/assistantModal/AssistantModalRoot.tsx +11 -31
  83. package/src/primitives/assistantModal/AssistantModalTrigger.test.tsx +1 -16
  84. package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +43 -0
  85. package/src/primitives/composer/ComposerAttachmentDropzone.tsx +2 -10
  86. package/src/primitives/composer/ComposerInput.test.tsx +22 -0
  87. package/src/primitives/composer/ComposerInput.tsx +5 -9
  88. package/src/primitives/composer/trigger/triggerKeyboardResource.ts +13 -7
  89. package/src/primitives/composer/trigger/triggerNavigationResource.ts +5 -3
  90. package/src/primitives/message/MessageParts.tsx +1 -21
  91. package/src/primitives/message/MessagePartsGrouped.test.tsx +93 -0
  92. package/src/primitives/message/MessagePartsGrouped.tsx +6 -6
  93. package/src/primitives/messagePart/MessagePartText.test.tsx +69 -0
  94. package/src/primitives/messagePart/MessagePartText.tsx +22 -9
  95. package/src/primitives/reasoning/useScrollLock.test.tsx +129 -0
  96. package/src/primitives/reasoning/useScrollLock.ts +15 -1
  97. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +38 -1
  98. package/src/sandbox-host/SandboxHost.test.tsx +40 -0
  99. package/src/sandbox-host/SandboxHost.tsx +24 -4
  100. package/src/tests/MessageParts.rendererOptions.test.tsx +117 -0
  101. package/src/tests/toolCallTiming.test.tsx +55 -1
  102. package/src/unstable/useLiveCompletionAdapter.test.tsx +202 -1
  103. package/src/unstable/useLiveCompletionAdapter.ts +38 -8
  104. package/src/unstable/useMentionAdapter.test.tsx +47 -1
  105. package/src/unstable/useMentionAdapter.ts +2 -2
  106. package/src/unstable/useMessageStallDetection.ts +26 -19
  107. package/src/unstable/webmcp/convertTools.test.ts +181 -21
  108. package/src/unstable/webmcp/convertTools.ts +87 -7
  109. package/src/unstable/webmcp/useWebMcpProvider.ts +2 -4
  110. package/src/utils/Primitive.test.tsx +46 -1
  111. package/src/utils/Primitive.tsx +37 -7
  112. package/src/utils/getSelectionMessageId.test.ts +176 -1
  113. package/src/utils/getSelectionMessageId.ts +21 -3
@@ -64,6 +64,47 @@ const toMcpContent = async (result, options) => {
64
64
  } : { content };
65
65
  };
66
66
  const isStandardSchema = (schema) => typeof schema === "object" && schema !== null && "~standard" in schema && schema["~standard"].version === 1;
67
+ const TOOL_ABORTED = Symbol("assistant-ui.webmcp-tool-aborted");
68
+ const isThenable = (value) => typeof value?.then === "function";
69
+ const raceWithAbort = async (value, abortSignal) => {
70
+ let onAbort;
71
+ const abortPromise = new Promise((resolve) => {
72
+ onAbort = () => resolve(TOOL_ABORTED);
73
+ if (abortSignal.aborted) onAbort();
74
+ else abortSignal.addEventListener("abort", onAbort, { once: true });
75
+ });
76
+ try {
77
+ return await Promise.race([abortPromise, value]);
78
+ } finally {
79
+ abortSignal.removeEventListener("abort", onAbort);
80
+ }
81
+ };
82
+ const combineAbortSignals = (callerSignal, lifecycleSignal) => {
83
+ const controller = new AbortController();
84
+ const teardown = [];
85
+ const cleanup = () => {
86
+ while (teardown.length) teardown.pop()();
87
+ };
88
+ const abort = (reason) => {
89
+ cleanup();
90
+ controller.abort(reason);
91
+ };
92
+ const listen = (signal) => {
93
+ const onAbort = () => abort(signal.reason);
94
+ signal.addEventListener("abort", onAbort, { once: true });
95
+ teardown.push(() => signal.removeEventListener("abort", onAbort));
96
+ };
97
+ if (callerSignal.aborted) abort(callerSignal.reason);
98
+ else if (lifecycleSignal.aborted) abort(lifecycleSignal.reason);
99
+ else {
100
+ listen(callerSignal);
101
+ listen(lifecycleSignal);
102
+ }
103
+ return {
104
+ signal: controller.signal,
105
+ cleanup
106
+ };
107
+ };
67
108
  const toWebMcpTool = (name, getTool, lifecycleSignal) => ({
68
109
  name,
69
110
  description: getTool().description ?? "",
@@ -73,13 +114,21 @@ const toWebMcpTool = (name, getTool, lifecycleSignal) => ({
73
114
  const tool = getTool();
74
115
  const args = rawArgs ?? {};
75
116
  const toolCallId = generateId();
117
+ let cleanup;
76
118
  try {
77
119
  const callerSignal = context?.signal;
78
- const abortSignal = !callerSignal ? lifecycleSignal : !lifecycleSignal ? callerSignal : AbortSignal.any([callerSignal, lifecycleSignal]);
120
+ let abortSignal;
121
+ if (callerSignal && lifecycleSignal) {
122
+ const combined = combineAbortSignals(callerSignal, lifecycleSignal);
123
+ abortSignal = combined.signal;
124
+ cleanup = combined.cleanup;
125
+ } else abortSignal = callerSignal ?? lifecycleSignal;
126
+ if (abortSignal?.aborted) return errorResult("Tool execution was cancelled.");
79
127
  let executeFn = tool.execute;
80
128
  if (isStandardSchema(tool.parameters)) {
81
- let validation = tool.parameters["~standard"].validate(args);
82
- validation = await validation;
129
+ const result = tool.parameters["~standard"].validate(args);
130
+ const validation = isThenable(result) ? abortSignal ? await raceWithAbort(result, abortSignal) : await result : result;
131
+ if (validation === TOOL_ABORTED) return errorResult("Tool execution was cancelled.");
83
132
  if (validation.issues) {
84
133
  const issues = validation.issues;
85
134
  executeFn = tool.experimental_onSchemaValidationError ?? (() => {
@@ -101,6 +150,8 @@ const toWebMcpTool = (name, getTool, lifecycleSignal) => ({
101
150
  });
102
151
  } catch (e) {
103
152
  return errorResult(e instanceof Error ? e.message : String(e));
153
+ } finally {
154
+ cleanup?.();
104
155
  }
105
156
  }
106
157
  });
@@ -1 +1 @@
1
- {"version":3,"file":"convertTools.js","names":["ToolResponse","toJSONSchema","Tool","ToolModelContentPart","generateId","WebMcpCallToolResult","WebMcpContent","WebMcpToolDescriptor","defaultWebMcpFilter","_name","tool","type","undefined","execute","disabled","toWebMcpInputSchema","parameters","properties","textContent","text","errorResult","message","isError","content","toText","value","toString","JSON","stringify","String","primitiveContent","mapModelContentPart","part","mediaType","startsWith","data","mimeType","toMcpContent","result","options","toolCallId","args","Record","Promise","response","toResponse","modelContent","map","toModelOutput","parts","input","output","e","console","warn","StandardSchemaLike","version","validate","issues","isStandardSchema","schema","toWebMcpTool","name","getTool","lifecycleSignal","AbortSignal","description","inputSchema","rawArgs","context","aborted","callerSignal","signal","abortSignal","any","executeFn","validation","experimental_onSchemaValidationError","Error","AbortController","human","reject"],"sources":["../../../src/unstable/webmcp/convertTools.ts"],"sourcesContent":["import {\n ToolResponse,\n toJSONSchema,\n type Tool,\n type ToolModelContentPart,\n} from \"assistant-stream\";\nimport { generateId } from \"@assistant-ui/core/internal\";\nimport type {\n WebMcpCallToolResult,\n WebMcpContent,\n WebMcpToolDescriptor,\n} from \"./webmcp-host\";\n\n/**\n * The predicate the WebMCP provider uses when no `filter` is passed: an enabled\n * frontend tool with a client-side `execute`. A tool authored without a `type`\n * is included, because `execute` is what distinguishes the deprecated\n * type-less form from a backend or human tool. A `filter` replaces this, so\n * pass it through to narrow the default set rather than widen it.\n */\nexport const defaultWebMcpFilter = (\n _name: string,\n tool: Tool<any, any>,\n): boolean =>\n (tool.type === \"frontend\" || tool.type === undefined) &&\n !!tool.execute &&\n !tool.disabled;\n\nexport const toWebMcpInputSchema = (tool: Tool<any, any>): unknown =>\n tool.parameters\n ? toJSONSchema(tool.parameters)\n : { type: \"object\", properties: {} };\n\nconst textContent = (text: string): WebMcpContent => ({ type: \"text\", text });\n\nconst errorResult = (message: string): WebMcpCallToolResult => ({\n isError: true,\n content: [textContent(message)],\n});\n\n// bigint throws in JSON.stringify and symbol serializes to undefined, but both\n// have a faithful string form. A value that cannot be serialized at all still\n// throws through to the error result.\nconst toText = (value: unknown): string => {\n if (typeof value === \"string\") return value;\n if (typeof value === \"bigint\" || typeof value === \"symbol\") {\n return value.toString();\n }\n return JSON.stringify(value) ?? String(value);\n};\n\nconst primitiveContent = (value: unknown): WebMcpContent[] => [\n textContent(toText(value)),\n];\n\nconst mapModelContentPart = (part: ToolModelContentPart): WebMcpContent => {\n if (part.type === \"text\") {\n return textContent(part.text ?? \"\");\n }\n if (part.type === \"file\") {\n if (\n typeof part.mediaType === \"string\" &&\n part.mediaType.startsWith(\"image/\")\n ) {\n return { type: \"image\", data: part.data ?? \"\", mimeType: part.mediaType };\n }\n return textContent(part.data ?? \"\");\n }\n return textContent(toText(part));\n};\n\nexport const toMcpContent = async (\n result: unknown,\n options: {\n tool: Tool<any, any>;\n toolCallId: string;\n args: Record<string, unknown>;\n },\n): Promise<WebMcpCallToolResult> => {\n const response = ToolResponse.toResponse(result);\n if (response.modelContent) {\n const content = response.modelContent.map(mapModelContentPart);\n return response.isError ? { isError: true, content } : { content };\n }\n if (!response.isError && options.tool.toModelOutput) {\n try {\n const parts = await options.tool.toModelOutput({\n toolCallId: options.toolCallId,\n input: options.args,\n output: response.result,\n });\n return { content: parts.map(mapModelContentPart) };\n } catch (e) {\n console.warn(\n \"[assistant-ui] toModelOutput threw; falling back to default projection.\",\n e,\n );\n }\n }\n const content = primitiveContent(response.result);\n return response.isError ? { isError: true, content } : { content };\n};\n\ntype StandardSchemaLike = {\n \"~standard\": {\n version: number;\n validate: (\n value: unknown,\n ) =>\n | { issues?: readonly unknown[] | undefined }\n | Promise<{ issues?: readonly unknown[] | undefined }>;\n };\n};\n\nconst isStandardSchema = (schema: unknown): schema is StandardSchemaLike =>\n typeof schema === \"object\" &&\n schema !== null &&\n \"~standard\" in schema &&\n (schema as StandardSchemaLike)[\"~standard\"].version === 1;\n\nexport const toWebMcpTool = (\n name: string,\n getTool: () => Tool<any, any>,\n lifecycleSignal?: AbortSignal,\n): WebMcpToolDescriptor => ({\n name,\n description: getTool().description ?? \"\",\n inputSchema: toWebMcpInputSchema(getTool()),\n execute: async (rawArgs, context) => {\n if (lifecycleSignal?.aborted) {\n return errorResult(`Tool \"${name}\" is no longer registered`);\n }\n const tool = getTool();\n const args = (rawArgs ?? {}) as Record<string, unknown>;\n const toolCallId = generateId();\n try {\n const callerSignal = context?.signal;\n const abortSignal = !callerSignal\n ? lifecycleSignal\n : !lifecycleSignal\n ? callerSignal\n : AbortSignal.any([callerSignal, lifecycleSignal]);\n let executeFn = tool.execute;\n if (isStandardSchema(tool.parameters)) {\n let validation = tool.parameters[\"~standard\"].validate(args);\n validation = await validation;\n if (validation.issues) {\n const issues = validation.issues;\n executeFn =\n tool.experimental_onSchemaValidationError ??\n (() => {\n throw new Error(\n `Function parameter validation failed. ${JSON.stringify(issues)}`,\n );\n });\n }\n }\n\n if (abortSignal?.aborted) {\n return errorResult(\"Tool execution was cancelled.\");\n }\n\n if (!executeFn) {\n return errorResult(`Tool \"${name}\" has no client-side implementation.`);\n }\n\n const result = await executeFn(args, {\n toolCallId,\n abortSignal: abortSignal ?? new AbortController().signal,\n human: () =>\n Promise.reject(\n new Error(\"human input not supported in WebMCP context\"),\n ),\n });\n return await toMcpContent(result, { tool, toolCallId, args });\n } catch (e) {\n return errorResult(e instanceof Error ? e.message : String(e));\n }\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"convertTools.js","names":["ToolResponse","toJSONSchema","Tool","ToolModelContentPart","generateId","WebMcpCallToolResult","WebMcpContent","WebMcpToolDescriptor","defaultWebMcpFilter","_name","tool","type","undefined","execute","disabled","toWebMcpInputSchema","parameters","properties","textContent","text","errorResult","message","isError","content","toText","value","toString","JSON","stringify","String","primitiveContent","mapModelContentPart","part","mediaType","startsWith","data","mimeType","toMcpContent","result","options","toolCallId","args","Record","Promise","response","toResponse","modelContent","map","toModelOutput","parts","input","output","e","console","warn","StandardSchemaLike","version","validate","issues","isStandardSchema","schema","TOOL_ABORTED","Symbol","isThenable","T","PromiseLike","then","raceWithAbort","abortSignal","AbortSignal","onAbort","abortPromise","resolve","aborted","addEventListener","once","race","removeEventListener","combineAbortSignals","callerSignal","lifecycleSignal","signal","cleanup","controller","AbortController","teardown","length","pop","abort","reason","listen","push","toWebMcpTool","name","getTool","description","inputSchema","rawArgs","context","combined","executeFn","validation","experimental_onSchemaValidationError","Error","human","reject"],"sources":["../../../src/unstable/webmcp/convertTools.ts"],"sourcesContent":["import {\n ToolResponse,\n toJSONSchema,\n type Tool,\n type ToolModelContentPart,\n} from \"assistant-stream\";\nimport { generateId } from \"@assistant-ui/core/internal\";\nimport type {\n WebMcpCallToolResult,\n WebMcpContent,\n WebMcpToolDescriptor,\n} from \"./webmcp-host\";\n\n/**\n * The predicate the WebMCP provider uses when no `filter` is passed: an enabled\n * frontend tool with a client-side `execute`. A tool authored without a `type`\n * is included, because `execute` is what distinguishes the deprecated\n * type-less form from a backend or human tool. A `filter` replaces this, so\n * pass it through to narrow the default set rather than widen it.\n */\nexport const defaultWebMcpFilter = (\n _name: string,\n tool: Tool<any, any>,\n): boolean =>\n (tool.type === \"frontend\" || tool.type === undefined) &&\n !!tool.execute &&\n !tool.disabled;\n\nexport const toWebMcpInputSchema = (tool: Tool<any, any>): unknown =>\n tool.parameters\n ? toJSONSchema(tool.parameters)\n : { type: \"object\", properties: {} };\n\nconst textContent = (text: string): WebMcpContent => ({ type: \"text\", text });\n\nconst errorResult = (message: string): WebMcpCallToolResult => ({\n isError: true,\n content: [textContent(message)],\n});\n\n// bigint throws in JSON.stringify and symbol serializes to undefined, but both\n// have a faithful string form. A value that cannot be serialized at all still\n// throws through to the error result.\nconst toText = (value: unknown): string => {\n if (typeof value === \"string\") return value;\n if (typeof value === \"bigint\" || typeof value === \"symbol\") {\n return value.toString();\n }\n return JSON.stringify(value) ?? String(value);\n};\n\nconst primitiveContent = (value: unknown): WebMcpContent[] => [\n textContent(toText(value)),\n];\n\nconst mapModelContentPart = (part: ToolModelContentPart): WebMcpContent => {\n if (part.type === \"text\") {\n return textContent(part.text ?? \"\");\n }\n if (part.type === \"file\") {\n if (\n typeof part.mediaType === \"string\" &&\n part.mediaType.startsWith(\"image/\")\n ) {\n return { type: \"image\", data: part.data ?? \"\", mimeType: part.mediaType };\n }\n return textContent(part.data ?? \"\");\n }\n return textContent(toText(part));\n};\n\nexport const toMcpContent = async (\n result: unknown,\n options: {\n tool: Tool<any, any>;\n toolCallId: string;\n args: Record<string, unknown>;\n },\n): Promise<WebMcpCallToolResult> => {\n const response = ToolResponse.toResponse(result);\n if (response.modelContent) {\n const content = response.modelContent.map(mapModelContentPart);\n return response.isError ? { isError: true, content } : { content };\n }\n if (!response.isError && options.tool.toModelOutput) {\n try {\n const parts = await options.tool.toModelOutput({\n toolCallId: options.toolCallId,\n input: options.args,\n output: response.result,\n });\n return { content: parts.map(mapModelContentPart) };\n } catch (e) {\n console.warn(\n \"[assistant-ui] toModelOutput threw; falling back to default projection.\",\n e,\n );\n }\n }\n const content = primitiveContent(response.result);\n return response.isError ? { isError: true, content } : { content };\n};\n\ntype StandardSchemaLike = {\n \"~standard\": {\n version: number;\n validate: (\n value: unknown,\n ) =>\n | { issues?: readonly unknown[] | undefined }\n | Promise<{ issues?: readonly unknown[] | undefined }>;\n };\n};\n\nconst isStandardSchema = (schema: unknown): schema is StandardSchemaLike =>\n typeof schema === \"object\" &&\n schema !== null &&\n \"~standard\" in schema &&\n (schema as StandardSchemaLike)[\"~standard\"].version === 1;\n\nconst TOOL_ABORTED = Symbol(\"assistant-ui.webmcp-tool-aborted\");\n\nconst isThenable = <T>(value: T | PromiseLike<T>): value is PromiseLike<T> =>\n typeof (value as PromiseLike<T> | null | undefined)?.then === \"function\";\n\nconst raceWithAbort = async <T>(\n value: PromiseLike<T>,\n abortSignal: AbortSignal,\n): Promise<T | typeof TOOL_ABORTED> => {\n let onAbort!: () => void;\n const abortPromise = new Promise<typeof TOOL_ABORTED>((resolve) => {\n onAbort = () => resolve(TOOL_ABORTED);\n if (abortSignal.aborted) {\n onAbort();\n } else {\n abortSignal.addEventListener(\"abort\", onAbort, { once: true });\n }\n });\n\n try {\n // Unlike assistant-stream's helper, cancellation wins when validation aborts and rejects synchronously.\n return await Promise.race([abortPromise, value]);\n } finally {\n abortSignal.removeEventListener(\"abort\", onAbort);\n }\n};\n\n// AbortSignal.any sits above the browserslist floor and rejects any input that\n// is not a native AbortSignal, which a navigator.modelContext polyfill's signal\n// is not. The merged signal tracks its inputs only until cleanup runs, where\n// the single-signal path hands the caller the lifecycle signal itself.\nconst combineAbortSignals = (\n callerSignal: AbortSignal,\n lifecycleSignal: AbortSignal,\n): { signal: AbortSignal; cleanup: () => void } => {\n const controller = new AbortController();\n const teardown: (() => void)[] = [];\n const cleanup = () => {\n while (teardown.length) teardown.pop()!();\n };\n const abort = (reason: unknown) => {\n cleanup();\n controller.abort(reason);\n };\n const listen = (signal: AbortSignal) => {\n const onAbort = () => abort(signal.reason);\n signal.addEventListener(\"abort\", onAbort, { once: true });\n teardown.push(() => signal.removeEventListener(\"abort\", onAbort));\n };\n\n if (callerSignal.aborted) {\n abort(callerSignal.reason);\n } else if (lifecycleSignal.aborted) {\n abort(lifecycleSignal.reason);\n } else {\n listen(callerSignal);\n listen(lifecycleSignal);\n }\n\n return { signal: controller.signal, cleanup };\n};\n\nexport const toWebMcpTool = (\n name: string,\n getTool: () => Tool<any, any>,\n lifecycleSignal?: AbortSignal,\n): WebMcpToolDescriptor => ({\n name,\n description: getTool().description ?? \"\",\n inputSchema: toWebMcpInputSchema(getTool()),\n execute: async (rawArgs, context) => {\n if (lifecycleSignal?.aborted) {\n return errorResult(`Tool \"${name}\" is no longer registered`);\n }\n const tool = getTool();\n const args = (rawArgs ?? {}) as Record<string, unknown>;\n const toolCallId = generateId();\n let cleanup: (() => void) | undefined;\n try {\n const callerSignal = context?.signal;\n let abortSignal: AbortSignal | undefined;\n if (callerSignal && lifecycleSignal) {\n const combined = combineAbortSignals(callerSignal, lifecycleSignal);\n abortSignal = combined.signal;\n cleanup = combined.cleanup;\n } else {\n abortSignal = callerSignal ?? lifecycleSignal;\n }\n\n if (abortSignal?.aborted) {\n return errorResult(\"Tool execution was cancelled.\");\n }\n\n let executeFn = tool.execute;\n if (isStandardSchema(tool.parameters)) {\n const result = tool.parameters[\"~standard\"].validate(args);\n const validation = isThenable(result)\n ? abortSignal\n ? await raceWithAbort(result, abortSignal)\n : await result\n : result;\n if (validation === TOOL_ABORTED) {\n return errorResult(\"Tool execution was cancelled.\");\n }\n if (validation.issues) {\n const issues = validation.issues;\n executeFn =\n tool.experimental_onSchemaValidationError ??\n (() => {\n throw new Error(\n `Function parameter validation failed. ${JSON.stringify(issues)}`,\n );\n });\n }\n }\n\n if (abortSignal?.aborted) {\n return errorResult(\"Tool execution was cancelled.\");\n }\n\n if (!executeFn) {\n return errorResult(`Tool \"${name}\" has no client-side implementation.`);\n }\n\n const result = await executeFn(args, {\n toolCallId,\n abortSignal: abortSignal ?? new AbortController().signal,\n human: () =>\n Promise.reject(\n new Error(\"human input not supported in WebMCP context\"),\n ),\n });\n return await toMcpContent(result, { tool, toolCallId, args });\n } catch (e) {\n return errorResult(e instanceof Error ? e.message : String(e));\n } finally {\n cleanup?.();\n }\n },\n});\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useWebMcpProvider.d.ts","names":[],"sources":["../../../src/unstable/webmcp/useWebMcpProvider.ts"],"mappings":";;KAcY;EACV,UAAU,cAAc,MAAM;;KAGpB;EACV;EACA;;;;;;;;;;cAwGW,6BACX,UAAS,mCACR"}
1
+ {"version":3,"file":"useWebMcpProvider.d.ts","names":[],"sources":["../../../src/unstable/webmcp/useWebMcpProvider.ts"],"mappings":";;KAeY;EACV,UAAU,cAAc,MAAM;;KAGpB;EACV;EACA;;;;;;;;;;cAqGW,6BACX,UAAS,mCACR"}
@@ -7,6 +7,7 @@ import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
7
7
  import { useAui } from "@assistant-ui/store";
8
8
  import { useEffect, useState, useSyncExternalStore } from "@assistant-ui/tap/react-shim";
9
9
  import { useResources, useTapRoot, withKey } from "@assistant-ui/tap";
10
+ import { shallowEqual } from "@assistant-ui/store/internal";
10
11
  //#region src/unstable/webmcp/useWebMcpProvider.ts
11
12
  const EMPTY_NAMES = Object.freeze([]);
12
13
  const EMPTY_TOOLS = Object.freeze({});
@@ -32,7 +33,7 @@ const useStableNames = (names) => {
32
33
  const [cell] = useState(() => ({ names: EMPTY_NAMES }));
33
34
  const next = names.filter((name) => name !== null).sort();
34
35
  const previous = cell.names;
35
- if (previous.length === next.length && previous.every((name_0, index) => name_0 === next[index])) return previous;
36
+ if (shallowEqual(previous, next)) return previous;
36
37
  cell.names = next;
37
38
  return next;
38
39
  };
@@ -1 +1 @@
1
- {"version":3,"file":"useWebMcpProvider.js","names":["c","_c","useEffect","useState","useSyncExternalStore","useResources","useTapRoot","withKey","useAui","AssistantClient","Tool","getDefaultWebMcpHost","WebMcpHost","defaultWebMcpFilter","toWebMcpInputSchema","WebMcpRegistrationResource","useModelContextSnapshot","ModelContextSnapshotSource","Unstable_WebMcpProviderOptions","filter","name","tool","Unstable_WebMcpProviderResult","status","registeredToolNames","EMPTY_NAMES","Object","freeze","EMPTY_TOOLS","Record","warned","WeakSet","signatures","WeakMap","description","signature","signatureOf","cached","get","JSON","stringify","set","NO_SUBSCRIPTION","modelContextToolSource","empty","read","aui","modelContext","getModelContext","tools","subscribe","onChange","useStableNames","names","cell","next","sort","previous","length","every","index","useWebMcpRegistry","t0","$","host","available","elements","entries","push","t1","error","has","add","console","warn","unstable_useWebMcpProvider","options","root","WebMcpProviderRoot","getValue","published","setPublished"],"sources":["../../../src/unstable/webmcp/useWebMcpProvider.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState, useSyncExternalStore } from \"react\";\nimport { useResources, useTapRoot, withKey } from \"@assistant-ui/tap\";\nimport { useAui, type AssistantClient } from \"@assistant-ui/store\";\nimport type { Tool } from \"assistant-stream\";\nimport { getDefaultWebMcpHost, type WebMcpHost } from \"./webmcp-host\";\nimport { defaultWebMcpFilter, toWebMcpInputSchema } from \"./convertTools\";\nimport { WebMcpRegistrationResource } from \"./WebMcpRegistrationResource\";\nimport {\n useModelContextSnapshot,\n type ModelContextSnapshotSource,\n} from \"../useModelContextSnapshot\";\n\nexport type Unstable_WebMcpProviderOptions = {\n filter?: (name: string, tool: Tool<any, any>) => boolean;\n};\n\nexport type Unstable_WebMcpProviderResult = {\n status: \"unsupported\" | \"active\";\n registeredToolNames: readonly string[];\n};\n\nconst EMPTY_NAMES: readonly string[] = Object.freeze([]);\nconst EMPTY_TOOLS: Record<string, Tool<any, any>> = Object.freeze({});\n\n// The description is re-read on every sync so mutating it in place is\n// observed; the schema is converted only when the tool object itself changes.\n// Keyed on the tool so a schema or filter that throws warns once instead of\n// on every model-context notify.\nconst warned = new WeakSet<Tool<any, any>>();\n\nconst signatures = new WeakMap<\n Tool<any, any>,\n { description: string | undefined; signature: string }\n>();\n\nconst signatureOf = (tool: Tool<any, any>) => {\n const cached = signatures.get(tool);\n if (cached && cached.description === tool.description)\n return cached.signature;\n const signature = `${tool.description ?? \"\"}\\u0000${JSON.stringify(\n toWebMcpInputSchema(tool),\n )}`;\n signatures.set(tool, { description: tool.description, signature });\n return signature;\n};\n\nconst NO_SUBSCRIPTION = () => {};\n\n// No `isEqual`: a caller republishing an unchanged tool set is how it asks for\n// a re-sync, which is what re-attempts a tool whose filter has stopped throwing\n// and what picks up a description edited in place on a stable tool object.\nconst modelContextToolSource: ModelContextSnapshotSource<\n Record<string, Tool<any, any>>\n> = {\n empty: EMPTY_TOOLS,\n read: (aui) => aui.modelContext.getModelContext().tools ?? EMPTY_TOOLS,\n subscribe: (aui, onChange) =>\n aui.modelContext.subscribe?.(onChange) ?? NO_SUBSCRIPTION,\n};\n\nconst useStableNames = (names: readonly (string | null)[]) => {\n const [cell] = useState(() => ({ names: EMPTY_NAMES }));\n const next = names.filter((name): name is string => name !== null).sort();\n const previous = cell.names;\n if (\n previous.length === next.length &&\n previous.every((name, index) => name === next[index])\n ) {\n return previous;\n }\n cell.names = next;\n return next;\n};\n\nconst useWebMcpRegistry = ({\n aui,\n host,\n filter,\n}: {\n aui: AssistantClient;\n host: WebMcpHost;\n filter: (name: string, tool: Tool<any, any>) => boolean;\n}) => {\n const tools = useModelContextSnapshot(\n aui,\n host.available,\n modelContextToolSource,\n );\n\n const elements = [];\n for (const [name, tool] of Object.entries(tools)) {\n try {\n if (!filter(name, tool)) continue;\n const signature = signatureOf(tool);\n elements.push(\n withKey(\n name,\n WebMcpRegistrationResource({ host, name, signature, tool }),\n [host, name, signature, tool],\n ),\n );\n } catch (error) {\n if (warned.has(tool)) continue;\n warned.add(tool);\n console.warn(\n `[assistant-ui] Skipping WebMCP registration for tool \"${name}\": filter or schema conversion failed.`,\n error,\n );\n }\n }\n\n return useStableNames(useResources(elements));\n};\n\n/**\n * Publishes the frontend tools in the model context to a WebMCP-capable\n * browser, so the user's own browser agent can call them.\n *\n * Returns `status: \"unsupported\"` when the page exposes no\n * `document.modelContext` (or `navigator.modelContext`), and the sorted names\n * of the tools currently registered with the host.\n */\nexport const unstable_useWebMcpProvider = (\n options: Unstable_WebMcpProviderOptions = {},\n): Unstable_WebMcpProviderResult => {\n const aui = useAui();\n const [host] = useState(getDefaultWebMcpHost);\n const filter = options.filter ?? defaultWebMcpFilter;\n\n const root = useTapRoot(function WebMcpProviderRoot() {\n return useWebMcpRegistry({ aui, host, filter });\n });\n const registeredToolNames = useSyncExternalStore(\n root.subscribe,\n root.getValue,\n () => EMPTY_NAMES,\n );\n\n const [published, setPublished] = useState(false);\n useEffect(() => {\n if (host.available) setPublished(true);\n }, [host]);\n\n return {\n status: published ? \"active\" : \"unsupported\",\n registeredToolNames,\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useWebMcpProvider.js","names":["c","_c","useEffect","useState","useSyncExternalStore","useResources","useTapRoot","withKey","useAui","AssistantClient","Tool","getDefaultWebMcpHost","WebMcpHost","defaultWebMcpFilter","toWebMcpInputSchema","WebMcpRegistrationResource","shallowEqual","useModelContextSnapshot","ModelContextSnapshotSource","Unstable_WebMcpProviderOptions","filter","name","tool","Unstable_WebMcpProviderResult","status","registeredToolNames","EMPTY_NAMES","Object","freeze","EMPTY_TOOLS","Record","warned","WeakSet","signatures","WeakMap","description","signature","signatureOf","cached","get","JSON","stringify","set","NO_SUBSCRIPTION","modelContextToolSource","empty","read","aui","modelContext","getModelContext","tools","subscribe","onChange","useStableNames","names","cell","next","sort","previous","useWebMcpRegistry","t0","$","host","available","elements","entries","push","t1","error","has","add","console","warn","unstable_useWebMcpProvider","options","root","WebMcpProviderRoot","getValue","published","setPublished"],"sources":["../../../src/unstable/webmcp/useWebMcpProvider.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState, useSyncExternalStore } from \"react\";\nimport { useResources, useTapRoot, withKey } from \"@assistant-ui/tap\";\nimport { useAui, type AssistantClient } from \"@assistant-ui/store\";\nimport type { Tool } from \"assistant-stream\";\nimport { getDefaultWebMcpHost, type WebMcpHost } from \"./webmcp-host\";\nimport { defaultWebMcpFilter, toWebMcpInputSchema } from \"./convertTools\";\nimport { WebMcpRegistrationResource } from \"./WebMcpRegistrationResource\";\nimport { shallowEqual } from \"@assistant-ui/store/internal\";\nimport {\n useModelContextSnapshot,\n type ModelContextSnapshotSource,\n} from \"../useModelContextSnapshot\";\n\nexport type Unstable_WebMcpProviderOptions = {\n filter?: (name: string, tool: Tool<any, any>) => boolean;\n};\n\nexport type Unstable_WebMcpProviderResult = {\n status: \"unsupported\" | \"active\";\n registeredToolNames: readonly string[];\n};\n\nconst EMPTY_NAMES: readonly string[] = Object.freeze([]);\nconst EMPTY_TOOLS: Record<string, Tool<any, any>> = Object.freeze({});\n\n// The description is re-read on every sync so mutating it in place is\n// observed; the schema is converted only when the tool object itself changes.\n// Keyed on the tool so a schema or filter that throws warns once instead of\n// on every model-context notify.\nconst warned = new WeakSet<Tool<any, any>>();\n\nconst signatures = new WeakMap<\n Tool<any, any>,\n { description: string | undefined; signature: string }\n>();\n\nconst signatureOf = (tool: Tool<any, any>) => {\n const cached = signatures.get(tool);\n if (cached && cached.description === tool.description)\n return cached.signature;\n const signature = `${tool.description ?? \"\"}\\u0000${JSON.stringify(\n toWebMcpInputSchema(tool),\n )}`;\n signatures.set(tool, { description: tool.description, signature });\n return signature;\n};\n\nconst NO_SUBSCRIPTION = () => {};\n\n// No `isEqual`: a caller republishing an unchanged tool set is how it asks for\n// a re-sync, which is what re-attempts a tool whose filter has stopped throwing\n// and what picks up a description edited in place on a stable tool object.\nconst modelContextToolSource: ModelContextSnapshotSource<\n Record<string, Tool<any, any>>\n> = {\n empty: EMPTY_TOOLS,\n read: (aui) => aui.modelContext.getModelContext().tools ?? EMPTY_TOOLS,\n subscribe: (aui, onChange) =>\n aui.modelContext.subscribe?.(onChange) ?? NO_SUBSCRIPTION,\n};\n\nconst useStableNames = (names: readonly (string | null)[]) => {\n const [cell] = useState(() => ({ names: EMPTY_NAMES }));\n const next = names.filter((name): name is string => name !== null).sort();\n const previous = cell.names;\n if (shallowEqual(previous, next)) {\n return previous;\n }\n cell.names = next;\n return next;\n};\n\nconst useWebMcpRegistry = ({\n aui,\n host,\n filter,\n}: {\n aui: AssistantClient;\n host: WebMcpHost;\n filter: (name: string, tool: Tool<any, any>) => boolean;\n}) => {\n const tools = useModelContextSnapshot(\n aui,\n host.available,\n modelContextToolSource,\n );\n\n const elements = [];\n for (const [name, tool] of Object.entries(tools)) {\n try {\n if (!filter(name, tool)) continue;\n const signature = signatureOf(tool);\n elements.push(\n withKey(\n name,\n WebMcpRegistrationResource({ host, name, signature, tool }),\n [host, name, signature, tool],\n ),\n );\n } catch (error) {\n if (warned.has(tool)) continue;\n warned.add(tool);\n console.warn(\n `[assistant-ui] Skipping WebMCP registration for tool \"${name}\": filter or schema conversion failed.`,\n error,\n );\n }\n }\n\n return useStableNames(useResources(elements));\n};\n\n/**\n * Publishes the frontend tools in the model context to a WebMCP-capable\n * browser, so the user's own browser agent can call them.\n *\n * Returns `status: \"unsupported\"` when the page exposes no\n * `document.modelContext` (or `navigator.modelContext`), and the sorted names\n * of the tools currently registered with the host.\n */\nexport const unstable_useWebMcpProvider = (\n options: Unstable_WebMcpProviderOptions = {},\n): Unstable_WebMcpProviderResult => {\n const aui = useAui();\n const [host] = useState(getDefaultWebMcpHost);\n const filter = options.filter ?? defaultWebMcpFilter;\n\n const root = useTapRoot(function WebMcpProviderRoot() {\n return useWebMcpRegistry({ aui, host, filter });\n });\n const registeredToolNames = useSyncExternalStore(\n root.subscribe,\n root.getValue,\n () => EMPTY_NAMES,\n );\n\n const [published, setPublished] = useState(false);\n useEffect(() => {\n if (host.available) setPublished(true);\n }, [host]);\n\n return {\n status: published ? \"active\" : \"unsupported\",\n registeredToolNames,\n };\n};\n"],"mappings":"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"}
@@ -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<SVGSVGElement>>;
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.18",
3
+ "version": "0.15.20",
4
4
  "description": "Open-source TypeScript/React library for building production-grade AI chat experiences",
5
5
  "keywords": [
6
6
  "ai",
@@ -55,14 +55,14 @@
55
55
  ],
56
56
  "sideEffects": false,
57
57
  "dependencies": {
58
- "@assistant-ui/core": "^0.3.17",
59
- "@assistant-ui/store": "^0.3.12",
60
- "@assistant-ui/tap": "^0.9.16",
61
- "assistant-cloud": "^0.1.43",
62
- "assistant-stream": "^0.3.41",
58
+ "@assistant-ui/core": "^0.3.19",
59
+ "@assistant-ui/store": "^0.3.13",
60
+ "@assistant-ui/tap": "^0.9.17",
61
+ "assistant-cloud": "^0.2.1",
62
+ "assistant-stream": "^0.3.43",
63
63
  "radix-ui": "^1.6.7",
64
64
  "react-textarea-autosize": "^8.5.9",
65
- "safe-content-frame": "^0.0.29",
65
+ "safe-content-frame": "^0.0.30",
66
66
  "zod": "^4.5.4",
67
67
  "zustand": "^5.0.15"
68
68
  },
@@ -81,17 +81,17 @@
81
81
  }
82
82
  },
83
83
  "devDependencies": {
84
- "@assistant-ui/vite": "0.0.16",
85
- "@assistant-ui/x-buildutils": "0.0.25",
84
+ "@assistant-ui/vite": "0.0.17",
85
+ "@assistant-ui/x-buildutils": "0.0.27",
86
86
  "@testing-library/react": "^16.3.3",
87
87
  "@types/json-schema": "^7.0.15",
88
- "@types/node": "^26.4.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",
94
- "vitest": "^4.1.11"
94
+ "vitest": "^5.0.0"
95
95
  },
96
96
  "publishConfig": {
97
97
  "access": "public",
@@ -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
 
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it, onTestFinished, vi } from "vitest";
2
+ import { makeThreadViewportStore } from "./ThreadViewport";
3
+
4
+ describe("makeThreadViewportStore", () => {
5
+ it("notifies every scroll listener when one throws", () => {
6
+ const store = makeThreadViewportStore();
7
+ const listenerError = new Error("scroll listener failed");
8
+ const consoleError = vi
9
+ .spyOn(console, "error")
10
+ .mockImplementation(() => {});
11
+ onTestFinished(() => consoleError.mockRestore());
12
+ const laterListener = vi.fn();
13
+
14
+ store.getState().onScrollToBottom(() => {
15
+ throw listenerError;
16
+ });
17
+ store.getState().onScrollToBottom((config) => {
18
+ config.behavior = "smooth";
19
+ });
20
+ store.getState().onScrollToBottom(laterListener);
21
+
22
+ expect(() => store.getState().scrollToBottom()).not.toThrow();
23
+ expect(laterListener).toHaveBeenCalledWith({ behavior: "auto" });
24
+ expect(consoleError).toHaveBeenCalledWith(
25
+ expect.stringContaining("Thread viewport"),
26
+ listenerError,
27
+ );
28
+ });
29
+ });
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { create } from "zustand";
4
4
  import type { Unsubscribe } from "@assistant-ui/core";
5
+ import { notifyEventListeners } from "@assistant-ui/core/internal";
5
6
 
6
7
  export type SizeHandle = {
7
8
  /** Update the height */
@@ -184,9 +185,11 @@ export const makeThreadViewportStore = (
184
185
  const store = create<ThreadViewportState>(() => ({
185
186
  isAtBottom: true,
186
187
  scrollToBottom: ({ behavior = "auto" } = {}) => {
187
- for (const listener of scrollToBottomListeners) {
188
- listener({ behavior });
189
- }
188
+ notifyEventListeners(
189
+ scrollToBottomListeners,
190
+ () => ({ behavior }),
191
+ "Thread viewport",
192
+ );
190
193
  },
191
194
  onScrollToBottom: (callback) => {
192
195
  scrollToBottomListeners.add(callback);
@@ -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
@@ -46,7 +46,7 @@ export type {
46
46
  export { toolApprovalAcceptsText } from "@assistant-ui/core";
47
47
 
48
48
  export { useCloudThreadListRuntime } from "@assistant-ui/core/react";
49
- export { AssistantCloud } from "assistant-cloud";
49
+ export { AssistantCloud, readAnonymousRefreshToken } from "assistant-cloud";
50
50
 
51
51
  // --- adapters/attachment ---
52
52
  export type { AttachmentAdapter } from "@assistant-ui/core";
@@ -159,6 +159,8 @@ export { useExternalStoreSharedOptions } from "@assistant-ui/core/react";
159
159
  export {
160
160
  useExternalMessageConverter,
161
161
  convertExternalMessages as unstable_convertExternalMessages,
162
+ createExternalMessageConversionCache as unstable_createExternalMessageConversionCache,
163
+ type ExternalMessageConversionCache as Unstable_ExternalMessageConversionCache,
162
164
  } from "./legacy-runtime/runtime-cores/external-store/external-message-converter";
163
165
  export { createMessageConverter as unstable_createMessageConverter } from "./legacy-runtime/runtime-cores/external-store/createMessageConverter";
164
166
 
@@ -278,6 +280,8 @@ export { tool } from "@assistant-ui/core";
278
280
  export { Suggestions, type SuggestionConfig } from "@assistant-ui/core/store";
279
281
  export type {
280
282
  QueueItemState,
283
+ TaskState,
284
+ TaskMethods,
281
285
  QueueItemMethods,
282
286
  } from "@assistant-ui/core/store";
283
287
  export type { ComposerSendOptions } from "@assistant-ui/core/store";
@@ -374,6 +378,7 @@ export type {
374
378
  MessagePartStreamStatus,
375
379
  ToolCallMessagePartStatus,
376
380
  MessageTiming,
381
+ MessageModality,
377
382
  ThreadUserMessagePart,
378
383
  ThreadAssistantMessagePart,
379
384
  ThreadSystemMessage,
@@ -2,4 +2,6 @@
2
2
  export {
3
3
  useExternalMessageConverter,
4
4
  convertExternalMessages,
5
+ createExternalMessageConversionCache,
6
+ type ExternalMessageConversionCache,
5
7
  } from "@assistant-ui/core/react";