@assistant-ui/react 0.15.24 → 0.15.26
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.
- package/dist/hooks/useToolCallElapsed.d.ts +3 -1
- package/dist/hooks/useToolCallElapsed.d.ts.map +1 -1
- package/dist/hooks/useToolCallElapsed.js +8 -8
- package/dist/hooks/useToolCallElapsed.js.map +1 -1
- package/dist/index.d.ts +39 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +3 -2
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/mcp-apps/app-frame.d.ts.map +1 -1
- package/dist/mcp-apps/app-frame.js +56 -46
- package/dist/mcp-apps/app-frame.js.map +1 -1
- package/dist/mcp-apps/csp.d.ts +4 -0
- package/dist/mcp-apps/csp.d.ts.map +1 -0
- package/dist/mcp-apps/csp.js +93 -0
- package/dist/mcp-apps/csp.js.map +1 -0
- package/dist/mcp-apps/types.d.ts +1 -0
- package/dist/mcp-apps/types.d.ts.map +1 -1
- package/dist/mcp-apps/types.js.map +1 -1
- package/dist/model-context/makeAssistantVisible.d.ts +1 -0
- package/dist/model-context/makeAssistantVisible.d.ts.map +1 -1
- package/dist/model-context/makeAssistantVisible.js +31 -12
- package/dist/model-context/makeAssistantVisible.js.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.d.ts +1 -0
- package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
- package/dist/primitives/attachment.d.ts +3 -1
- package/dist/primitives/attachment.d.ts.map +1 -1
- package/dist/primitives/attachment.js.map +1 -1
- package/dist/primitives/composer/ComposerCancel.d.ts +5 -1
- package/dist/primitives/composer/ComposerCancel.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerCancel.js +14 -1
- package/dist/primitives/composer/ComposerCancel.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.d.ts +15 -1
- package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +5 -4
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerRoot.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerRoot.js +24 -19
- package/dist/primitives/composer/ComposerRoot.js.map +1 -1
- package/dist/primitives/composer/composerInputSelector.d.ts +3 -0
- package/dist/primitives/composer/composerInputSelector.d.ts.map +1 -0
- package/dist/primitives/composer/composerInputSelector.js +7 -0
- package/dist/primitives/composer/composerInputSelector.js.map +1 -0
- package/dist/primitives/composer/trigger/TriggerPopover.d.ts +11 -11
- package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopover.js +62 -60
- package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts +8 -8
- package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js +6 -6
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts +3 -3
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts +9 -9
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +7 -7
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts +5 -5
- package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +8 -8
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +8 -8
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
- package/dist/primitives/composer/trigger/index.d.ts +1 -1
- package/dist/primitives/composer/useComposerCancelWithFocus.d.ts +2 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.d.ts.map +1 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.js +42 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.js.map +1 -0
- package/dist/primitives/composer.d.ts +55 -15
- package/dist/primitives/composer.d.ts.map +1 -1
- package/dist/primitives/composer.js +8 -1
- package/dist/primitives/composer.js.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.d.ts +8 -1
- package/dist/primitives/message/MessagePartsGrouped.d.ts.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.js +58 -278
- package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.d.ts.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +5 -2
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
- package/dist/primitives/thread/ThreadLoadEarlier.d.ts +19 -0
- package/dist/primitives/thread/ThreadLoadEarlier.d.ts.map +1 -0
- package/dist/primitives/thread/ThreadLoadEarlier.js +36 -0
- package/dist/primitives/thread/ThreadLoadEarlier.js.map +1 -0
- package/dist/primitives/thread/ThreadMessages.d.ts +1 -1
- package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadMessages.js +2 -2
- package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadViewport.js +4 -3
- package/dist/primitives/thread/ThreadViewport.js.map +1 -1
- package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +2 -2
- package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +195 -17
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/primitives/thread.d.ts +2 -0
- package/dist/primitives/thread.d.ts.map +1 -1
- package/dist/primitives/thread.js +5 -2
- package/dist/primitives/thread.js.map +1 -1
- package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
- package/dist/sandbox-host/SandboxHost.js +9 -3
- package/dist/sandbox-host/SandboxHost.js.map +1 -1
- package/dist/unstable/useComposerInput.d.ts +7 -4
- package/dist/unstable/useComposerInput.d.ts.map +1 -1
- package/dist/unstable/useComposerInput.js +4 -4
- package/dist/unstable/useComposerInput.js.map +1 -1
- package/dist/unstable/useComposerInputHistory.d.ts +3 -2
- package/dist/unstable/useComposerInputHistory.d.ts.map +1 -1
- package/dist/unstable/useComposerInputHistory.js +2 -2
- package/dist/unstable/useComposerInputHistory.js.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.d.ts +3 -2
- package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.js +8 -7
- package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
- package/dist/unstable/useMentionAdapter.d.ts +12 -3
- package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
- package/dist/unstable/useMentionAdapter.js +2 -2
- package/dist/unstable/useMentionAdapter.js.map +1 -1
- package/dist/unstable/useMessageStallDetection.d.ts +4 -2
- package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
- package/dist/unstable/useMessageStallDetection.js +12 -8
- package/dist/unstable/useMessageStallDetection.js.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.d.ts +5 -2
- package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.js +5 -4
- package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts +4 -0
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -1
- package/dist/unstable/webmcp/useWebMcpProvider.js +2 -0
- package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -1
- package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
- package/dist/utils/getSelectionMessageId.js +31 -8
- package/dist/utils/getSelectionMessageId.js.map +1 -1
- package/dist/utils/hooks/useOnResizeContent.d.ts.map +1 -1
- package/dist/utils/hooks/useOnResizeContent.js +2 -22
- package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
- package/dist/utils/smooth/useSmooth.js +6 -6
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/dist/utils/useIsomorphicLayoutEffect.d.ts +3 -0
- package/dist/utils/useIsomorphicLayoutEffect.d.ts.map +1 -0
- package/dist/utils/useIsomorphicLayoutEffect.js +6 -0
- package/dist/utils/useIsomorphicLayoutEffect.js.map +1 -0
- package/package.json +12 -11
- package/src/hooks/useToolCallElapsed.test.tsx +37 -0
- package/src/hooks/useToolCallElapsed.ts +10 -7
- package/src/index.ts +47 -5
- package/src/mcp-apps/McpAppRenderer.tsx +3 -9
- package/src/mcp-apps/app-frame.test.tsx +38 -0
- package/src/mcp-apps/app-frame.tsx +5 -8
- package/src/mcp-apps/csp.test.ts +112 -0
- package/src/mcp-apps/csp.ts +124 -0
- package/src/mcp-apps/types.ts +1 -0
- package/src/model-context/frame/AssistantFrame.test.ts +45 -0
- package/src/model-context/frame/SPEC_AssistantFrame.md +1 -0
- package/src/model-context/makeAssistantVisible.test.tsx +315 -5
- package/src/model-context/makeAssistantVisible.tsx +43 -7
- package/src/primitives/assistantModal/AssistantModalRoot.tsx +1 -0
- package/src/primitives/attachment.ts +4 -1
- package/src/primitives/composer/ComposerCancel.focus.test.tsx +307 -0
- package/src/primitives/composer/ComposerCancel.ts +8 -1
- package/src/primitives/composer/ComposerInput.tsx +20 -2
- package/src/primitives/composer/ComposerRoot.tsx +6 -5
- package/src/primitives/composer/composerInputSelector.ts +4 -0
- package/src/primitives/composer/trigger/TriggerPopover.tsx +11 -11
- package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +10 -13
- package/src/primitives/composer/trigger/TriggerPopoverCategories.tsx +2 -2
- package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +11 -14
- package/src/primitives/composer/trigger/TriggerPopoverExports.test.ts +61 -0
- package/src/primitives/composer/trigger/TriggerPopoverItems.tsx +3 -3
- package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +8 -8
- package/src/primitives/composer/useComposerCancelWithFocus.ts +57 -0
- package/src/primitives/composer.ts +70 -15
- package/src/primitives/message/MessagePartsGrouped.test.tsx +106 -2
- package/src/primitives/message/MessagePartsGrouped.tsx +36 -134
- package/src/primitives/message/MessageRoot.test.tsx +125 -2
- package/src/primitives/selectionToolbar/SelectionToolbarRoot.test.tsx +20 -12
- package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +6 -2
- package/src/primitives/thread/ThreadLoadEarlier.tsx +35 -0
- package/src/primitives/thread/ThreadMessages.tsx +1 -0
- package/src/primitives/thread/ThreadViewport.tsx +3 -3
- package/src/primitives/thread/topAnchor/useTopAnchorReserve.ts +2 -2
- package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +922 -68
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +330 -56
- package/src/primitives/thread.ts +2 -0
- package/src/sandbox-host/SandboxHost.test.tsx +34 -0
- package/src/sandbox-host/SandboxHost.tsx +11 -12
- package/src/tests/toolCallTiming.test.tsx +19 -2
- package/src/unstable/useComposerInput.ts +7 -4
- package/src/unstable/useComposerInputHistory.ts +3 -2
- package/src/unstable/useLiveCompletionAdapter.ts +9 -14
- package/src/unstable/useMentionAdapter.ts +12 -3
- package/src/unstable/useMessageStallDetection.test.tsx +40 -0
- package/src/unstable/useMessageStallDetection.ts +14 -8
- package/src/unstable/useSlashCommandAdapter.ts +8 -4
- package/src/unstable/webmcp/useWebMcpProvider.ts +4 -0
- package/src/utils/getSelectionMessageId.test.ts +231 -0
- package/src/utils/getSelectionMessageId.ts +45 -7
- package/src/utils/hooks/useOnResizeContent.ts +2 -29
- package/src/utils/smooth/useSmooth.test.tsx +16 -5
- package/src/utils/smooth/useSmooth.ts +7 -7
- package/src/utils/useIsomorphicLayoutEffect.test.tsx +21 -0
- package/src/utils/useIsomorphicLayoutEffect.ts +9 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useWebMcpProvider.js","names":["c","_c","useCallback","useEffect","useInsertionEffect","useRef","useState","useSyncExternalStore","useResources","useTapRoot","withKey","useAui","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","getCurrentTool","elements","entries","push","t1","error","has","add","console","warn","unstable_useWebMcpProvider","options","available","currentToolsRef","current","root","WebMcpProviderRoot","getValue","published","setPublished","_c"],"sources":["../../../src/unstable/webmcp/useWebMcpProvider.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useInsertionEffect,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport { useResources, useTapRoot, withKey } from \"@assistant-ui/tap\";\nimport { useAui } 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 host,\n filter,\n tools,\n getCurrentTool,\n}: {\n host: WebMcpHost;\n filter: (name: string, tool: Tool<any, any>) => boolean;\n tools: Record<string, Tool<any, any>>;\n getCurrentTool: (name: string) => Tool<any, any> | undefined;\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({\n host,\n name,\n signature,\n tool,\n getCurrentTool,\n }),\n [host, name, signature, tool, getCurrentTool],\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 const tools = useModelContextSnapshot(\n aui,\n host.available,\n modelContextToolSource,\n );\n const currentToolsRef = useRef(tools);\n useInsertionEffect(() => {\n currentToolsRef.current = tools;\n }, [tools]);\n const getCurrentTool = useCallback(\n (name: string) => currentToolsRef.current[name],\n [],\n );\n\n const root = useTapRoot(function WebMcpProviderRoot() {\n return useWebMcpRegistry({\n host,\n filter,\n tools,\n getCurrentTool,\n });\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":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"useWebMcpProvider.js","names":["c","_c","useCallback","useEffect","useInsertionEffect","useRef","useState","useSyncExternalStore","useResources","useTapRoot","withKey","useAui","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","getCurrentTool","elements","entries","push","t1","error","has","add","console","warn","unstable_useWebMcpProvider","options","available","currentToolsRef","current","root","WebMcpProviderRoot","getValue","published","setPublished","_c"],"sources":["../../../src/unstable/webmcp/useWebMcpProvider.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useInsertionEffect,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport { useResources, useTapRoot, withKey } from \"@assistant-ui/tap\";\nimport { useAui } 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\n/** @deprecated Experimental since 2026-08-29. Not scheduled for removal; the API may change in any release. */\nexport type Unstable_WebMcpProviderOptions = {\n filter?: (name: string, tool: Tool<any, any>) => boolean;\n};\n\n/** @deprecated Experimental since 2026-08-29. Not scheduled for removal; the API may change in any release. */\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 host,\n filter,\n tools,\n getCurrentTool,\n}: {\n host: WebMcpHost;\n filter: (name: string, tool: Tool<any, any>) => boolean;\n tools: Record<string, Tool<any, any>>;\n getCurrentTool: (name: string) => Tool<any, any> | undefined;\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({\n host,\n name,\n signature,\n tool,\n getCurrentTool,\n }),\n [host, name, signature, tool, getCurrentTool],\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 *\n * @deprecated Experimental since 2026-08-29. Not scheduled for removal; the API may change in any release.\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 const tools = useModelContextSnapshot(\n aui,\n host.available,\n modelContextToolSource,\n );\n const currentToolsRef = useRef(tools);\n useInsertionEffect(() => {\n currentToolsRef.current = tools;\n }, [tools]);\n const getCurrentTool = useCallback(\n (name: string) => currentToolsRef.current[name],\n [],\n );\n\n const root = useTapRoot(function WebMcpProviderRoot() {\n return useWebMcpRegistry({\n host,\n filter,\n tools,\n getCurrentTool,\n });\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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getSelectionMessageId.d.ts","sourceRoot":"","sources":["../../src/utils/getSelectionMessageId.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"getSelectionMessageId.d.ts","sourceRoot":"","sources":["../../src/utils/getSelectionMessageId.ts"],"names":[],"mappings":"AAmFA,eAAO,MAAM,qBAAqB,GAChC,WAAW,SAAS,EACpB,OAAO,OAAO,GAAG,IAAI,KACpB,MAAM,GAAG,IA8CX,CAAC"}
|
|
@@ -25,8 +25,25 @@ const findQuoteMarker = (node, messageElement) => {
|
|
|
25
25
|
if (!messageElement.contains(marker)) return null;
|
|
26
26
|
return marker;
|
|
27
27
|
};
|
|
28
|
-
const
|
|
29
|
-
|
|
28
|
+
const normalizeRangeEnd = (range) => {
|
|
29
|
+
if (range.endOffset !== 0 || range.collapsed) return range;
|
|
30
|
+
const walker = range.endContainer.ownerDocument?.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
|
|
31
|
+
if (!walker) return range;
|
|
32
|
+
walker.currentNode = range.endContainer;
|
|
33
|
+
while (walker.previousNode()) {
|
|
34
|
+
const node = walker.currentNode;
|
|
35
|
+
const offset = node instanceof Text ? node.length : node.childNodes.length;
|
|
36
|
+
if (range.comparePoint(node, offset) < 0) return range;
|
|
37
|
+
const marker = (node instanceof Element ? node : node.parentElement)?.closest(QUOTE_SELECTABLE_SELECTOR);
|
|
38
|
+
if (marker && isExcluded(marker) && !node.contains(range.endContainer)) return range;
|
|
39
|
+
if (!(node instanceof Text) || !node.data.trim()) continue;
|
|
40
|
+
const normalized = range.cloneRange();
|
|
41
|
+
normalized.setEnd(node, offset);
|
|
42
|
+
return normalized.collapsed ? range : normalized;
|
|
43
|
+
}
|
|
44
|
+
return range;
|
|
45
|
+
};
|
|
46
|
+
const intersectsExcluded = (scope, ranges) => {
|
|
30
47
|
for (const marker of scope.querySelectorAll(QUOTE_SELECTABLE_SELECTOR)) {
|
|
31
48
|
if (!isExcluded(marker)) continue;
|
|
32
49
|
if (ranges.some((range) => range.intersectsNode(marker))) return true;
|
|
@@ -34,8 +51,17 @@ const intersectsExcluded = (scope, selection) => {
|
|
|
34
51
|
return false;
|
|
35
52
|
};
|
|
36
53
|
const getSelectionMessageId = (selection, root) => {
|
|
37
|
-
|
|
54
|
+
let { anchorNode, focusNode } = selection;
|
|
38
55
|
if (!anchorNode || !focusNode) return null;
|
|
56
|
+
const ranges = Array.from({ length: selection.rangeCount }, (_, i) => selection.getRangeAt(i));
|
|
57
|
+
if (ranges.length === 1) {
|
|
58
|
+
const range = normalizeRangeEnd(ranges[0]);
|
|
59
|
+
if (range !== ranges[0]) {
|
|
60
|
+
ranges[0] = range;
|
|
61
|
+
anchorNode = range.startContainer;
|
|
62
|
+
focusNode = range.endContainer;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
39
65
|
const anchorMessageElement = findMessageElement(anchorNode);
|
|
40
66
|
const focusMessageElement = findMessageElement(focusNode);
|
|
41
67
|
if (!anchorMessageElement || anchorMessageElement !== focusMessageElement) return null;
|
|
@@ -50,11 +76,8 @@ const getSelectionMessageId = (selection, root) => {
|
|
|
50
76
|
if (!anchorMarker || anchorMarker !== focusMarker) return null;
|
|
51
77
|
}
|
|
52
78
|
const scope = anchorMarker ?? anchorMessageElement;
|
|
53
|
-
for (
|
|
54
|
-
|
|
55
|
-
if (!scope.contains(commonAncestorContainer)) return null;
|
|
56
|
-
}
|
|
57
|
-
return intersectsExcluded(scope, selection) ? null : messageId;
|
|
79
|
+
for (const { commonAncestorContainer } of ranges) if (!scope.contains(commonAncestorContainer)) return null;
|
|
80
|
+
return intersectsExcluded(scope, ranges) ? null : messageId;
|
|
58
81
|
};
|
|
59
82
|
//#endregion
|
|
60
83
|
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","
|
|
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","normalizeRangeEnd","range","Range","endOffset","collapsed","walker","endContainer","ownerDocument","createTreeWalker","commonAncestorContainer","NodeFilter","SHOW_ELEMENT","SHOW_TEXT","currentNode","previousNode","offset","Text","length","childNodes","comparePoint","element","data","trim","normalized","cloneRange","setEnd","intersectsExcluded","scope","ranges","some","intersectsNode","getSelectionMessageId","selection","Selection","root","anchorNode","focusNode","Array","from","rangeCount","_","i","getRangeAt","startContainer","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\nconst normalizeRangeEnd = (range: Range): Range => {\n if (range.endOffset !== 0 || range.collapsed) return range;\n\n const walker = range.endContainer.ownerDocument?.createTreeWalker(\n range.commonAncestorContainer,\n NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,\n );\n if (!walker) return range;\n walker.currentNode = range.endContainer;\n\n while (walker.previousNode()) {\n const node = walker.currentNode;\n const offset = node instanceof Text ? node.length : node.childNodes.length;\n if (range.comparePoint(node, offset) < 0) return range;\n const element = node instanceof Element ? node : node.parentElement;\n const marker = element?.closest(QUOTE_SELECTABLE_SELECTOR);\n if (marker && isExcluded(marker) && !node.contains(range.endContainer))\n return range;\n if (!(node instanceof Text) || !node.data.trim()) continue;\n\n const normalized = range.cloneRange();\n normalized.setEnd(node, offset);\n return normalized.collapsed ? range : normalized;\n }\n return range;\n};\n\nconst intersectsExcluded = (\n scope: Element,\n ranges: readonly Range[],\n): boolean => {\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 = (\n selection: Selection,\n root?: Element | null,\n): string | null => {\n let { anchorNode, focusNode } = selection;\n if (!anchorNode || !focusNode) return null;\n\n const ranges = Array.from({ length: selection.rangeCount }, (_, i) =>\n selection.getRangeAt(i),\n );\n if (ranges.length === 1) {\n const range = normalizeRangeEnd(ranges[0]!);\n if (range !== ranges[0]) {\n ranges[0] = range;\n anchorNode = range.startContainer;\n focusNode = range.endContainer;\n }\n }\n\n const anchorMessageElement = findMessageElement(anchorNode);\n const focusMessageElement = findMessageElement(focusNode);\n\n if (!anchorMessageElement || anchorMessageElement !== focusMessageElement) {\n return null;\n }\n if (root && !root.contains(anchorMessageElement)) {\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 (const { commonAncestorContainer } of ranges) {\n if (!scope.contains(commonAncestorContainer)) return null;\n }\n\n return intersectsExcluded(scope, ranges) ? null : messageId;\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useOnResizeContent.d.ts","sourceRoot":"","sources":["../../../src/utils/hooks/useOnResizeContent.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useOnResizeContent.d.ts","sourceRoot":"","sources":["../../../src/utils/hooks/useOnResizeContent.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,kBAAkB,GAAI,UAAU,MAAM,IAAI,qCAStD,CAAC"}
|
|
@@ -2,39 +2,19 @@ import { useManagedRef } from "./useManagedRef.js";
|
|
|
2
2
|
import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
|
|
3
3
|
import "@assistant-ui/tap/react-shim";
|
|
4
4
|
import { useCallbackRef } from "radix-ui/internal";
|
|
5
|
+
import { observeContentResize } from "@assistant-ui/store/client";
|
|
5
6
|
//#region src/utils/hooks/useOnResizeContent.ts
|
|
6
7
|
const useOnResizeContent = (callback) => {
|
|
7
8
|
const $ = c(2);
|
|
8
9
|
const callbackRef = useCallbackRef(callback);
|
|
9
10
|
let t0;
|
|
10
11
|
if ($[0] !== callbackRef) {
|
|
11
|
-
t0 = (el) =>
|
|
12
|
-
const resizeObserver = new ResizeObserver(() => {
|
|
13
|
-
callbackRef();
|
|
14
|
-
});
|
|
15
|
-
const mutationObserver = new MutationObserver((mutations) => {
|
|
16
|
-
if (mutations.some(_temp)) callbackRef();
|
|
17
|
-
});
|
|
18
|
-
resizeObserver.observe(el);
|
|
19
|
-
mutationObserver.observe(el, {
|
|
20
|
-
childList: true,
|
|
21
|
-
subtree: true,
|
|
22
|
-
attributes: true,
|
|
23
|
-
characterData: true
|
|
24
|
-
});
|
|
25
|
-
return () => {
|
|
26
|
-
resizeObserver.disconnect();
|
|
27
|
-
mutationObserver.disconnect();
|
|
28
|
-
};
|
|
29
|
-
};
|
|
12
|
+
t0 = (el) => observeContentResize(el, callbackRef);
|
|
30
13
|
$[0] = callbackRef;
|
|
31
14
|
$[1] = t0;
|
|
32
15
|
} else t0 = $[1];
|
|
33
16
|
return useManagedRef(t0);
|
|
34
17
|
};
|
|
35
|
-
function _temp(m) {
|
|
36
|
-
return m.type !== "attributes" || m.attributeName !== "style";
|
|
37
|
-
}
|
|
38
18
|
//#endregion
|
|
39
19
|
export { useOnResizeContent };
|
|
40
20
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useOnResizeContent.js","names":["useCallbackRef","useCallback","useManagedRef","useOnResizeContent","callback","$","_c","callbackRef","t0","el","
|
|
1
|
+
{"version":3,"file":"useOnResizeContent.js","names":["observeContentResize","useCallbackRef","useCallback","useManagedRef","useOnResizeContent","callback","$","_c","callbackRef","t0","el","refCallback","_c","refCallback"],"sources":["../../../src/utils/hooks/useOnResizeContent.ts"],"sourcesContent":["import { observeContentResize } from \"@assistant-ui/store/client\";\nimport { useCallbackRef } from \"radix-ui/internal\";\nimport { useCallback } from \"react\";\nimport { useManagedRef } from \"./useManagedRef\";\n\nexport const useOnResizeContent = (callback: () => void) => {\n const callbackRef = useCallbackRef(callback);\n\n const refCallback = useCallback(\n (el: HTMLElement) => observeContentResize(el, callbackRef),\n [callbackRef],\n );\n\n return useManagedRef(refCallback);\n};\n"],"mappings":";;;;;;AAKA,MAAaI,sBAAqBC,aAAA;CAAA,MAAAC,IAAAC,EAAA,CAAA;CAChC,MAAAC,cAAoBP,eAAeI,QAAQ;CAAE,IAAAI;CAAA,IAAAH,EAAA,OAAAE,aAAA;EAG3CC,MAAAC,OAAqBV,qBAAqBU,IAAIF,WAAW;EAACF,EAAA,KAAAE;EAAAF,EAAA,KAAAG;CAAA,OAAAA,KAAAH,EAAA;CAE1D,OAEKH,cAAcQ,EAAW;AAAC"}
|
|
@@ -10,8 +10,8 @@ const DEFAULT_DRAIN_MS = 250;
|
|
|
10
10
|
const DEFAULT_MAX_CHAR_INTERVAL_MS = 5;
|
|
11
11
|
var TextStreamAnimator = class {
|
|
12
12
|
animationFrameId = null;
|
|
13
|
-
lastUpdateTime =
|
|
14
|
-
lastCommitTime =
|
|
13
|
+
lastUpdateTime = 0;
|
|
14
|
+
lastCommitTime = -Infinity;
|
|
15
15
|
targetText = "";
|
|
16
16
|
drainMs = DEFAULT_DRAIN_MS;
|
|
17
17
|
maxCharIntervalMs = DEFAULT_MAX_CHAR_INTERVAL_MS;
|
|
@@ -25,7 +25,7 @@ var TextStreamAnimator = class {
|
|
|
25
25
|
}
|
|
26
26
|
start() {
|
|
27
27
|
if (this.animationFrameId !== null) return;
|
|
28
|
-
this.lastUpdateTime =
|
|
28
|
+
this.lastUpdateTime = performance.now();
|
|
29
29
|
this.animate();
|
|
30
30
|
}
|
|
31
31
|
stop() {
|
|
@@ -35,7 +35,7 @@ var TextStreamAnimator = class {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
animate = () => {
|
|
38
|
-
const currentTime =
|
|
38
|
+
const currentTime = performance.now();
|
|
39
39
|
let timeToConsume = currentTime - this.lastUpdateTime;
|
|
40
40
|
const remainingChars = this.targetText.length - this.currentText.length;
|
|
41
41
|
const baseTimePerChar = Math.min(this.maxCharIntervalMs, this.drainMs / remainingChars);
|
|
@@ -119,7 +119,7 @@ const useSmooth = (state, smooth = false) => {
|
|
|
119
119
|
displayedText,
|
|
120
120
|
state.status
|
|
121
121
|
]);
|
|
122
|
-
const [animatorRef] = useState(new TextStreamAnimator(displayedText, setText));
|
|
122
|
+
const [animatorRef] = useState(() => new TextStreamAnimator(displayedText, setText));
|
|
123
123
|
useEffect(() => {
|
|
124
124
|
animatorRef.drainMs = drainMs;
|
|
125
125
|
animatorRef.maxCharIntervalMs = maxCharIntervalMs;
|
|
@@ -144,7 +144,7 @@ const useSmooth = (state, smooth = false) => {
|
|
|
144
144
|
if (state.status.type === "running") {
|
|
145
145
|
animatorRef.currentText = "";
|
|
146
146
|
animatorRef.targetText = text;
|
|
147
|
-
animatorRef.lastCommitTime =
|
|
147
|
+
animatorRef.lastCommitTime = -Infinity;
|
|
148
148
|
animatorRef.start();
|
|
149
149
|
} else {
|
|
150
150
|
animatorRef.currentText = text;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSmooth.js","names":["useEffect","useMemo","useRef","useState","useAui","useAuiState","MessagePartStatus","ReasoningMessagePart","TextMessagePart","MessagePartState","useCallbackRef","useMediaQuery","useSmoothStatusStore","writableStore","SmoothOptions","drainMs","maxCharIntervalMs","maxCharsPerFrame","minCommitMs","DEFAULT_DRAIN_MS","DEFAULT_MAX_CHAR_INTERVAL_MS","TextStreamAnimator","animationFrameId","lastUpdateTime","Date","now","lastCommitTime","targetText","Infinity","currentText","setText","newText","constructor","start","animate","stop","cancelAnimationFrame","currentTime","deltaTime","timeToConsume","remainingChars","length","baseTimePerChar","Math","min","frameLimit","charsToAdd","requestAnimationFrame","slice","isComplete","SMOOTH_STATUS","Object","freeze","type","positiveOr","value","fallback","undefined","useSmooth","state","smooth","text","reduceMotion","options","enabled","displayedText","setDisplayedText","status","aui","part","prevPart","setPrevPart","startsWith","smoothStatusStore","optional","target","setState","animatorRef","animatorPartRef","partChanged","current"],"sources":["../../../src/utils/smooth/useSmooth.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport type {\n MessagePartStatus,\n ReasoningMessagePart,\n TextMessagePart,\n MessagePartState,\n} from \"@assistant-ui/core\";\nimport { useCallbackRef } from \"radix-ui/internal\";\nimport { useMediaQuery } from \"../hooks/useMediaQuery\";\nimport { useSmoothStatusStore } from \"./SmoothContext\";\nimport { writableStore } from \"../../context/ReadonlyStore\";\n\n/**\n * Tuning options for the smooth text streaming animation.\n */\nexport type SmoothOptions = {\n /**\n * Target time in milliseconds to drain the backlog of unrevealed\n * characters. Larger values reveal long backlogs more gradually.\n * @default 250\n */\n drainMs?: number | undefined;\n /**\n * Maximum time in milliseconds between revealed characters, i.e. the\n * slowest reveal rate when the backlog is short.\n * @default 5\n */\n maxCharIntervalMs?: number | undefined;\n /**\n * Maximum number of characters revealed per animation frame.\n * @default Infinity\n */\n maxCharsPerFrame?: number | undefined;\n /**\n * Minimum time in milliseconds between committed updates. The reveal keeps\n * advancing every frame, but the visible text (and the downstream re-render\n * and markdown re-parse it triggers) is committed at most once per interval.\n * The final frame always commits. `0` commits every frame.\n * @default 0\n */\n minCommitMs?: number | undefined;\n};\n\nconst DEFAULT_DRAIN_MS = 250;\nconst DEFAULT_MAX_CHAR_INTERVAL_MS = 5;\n\nclass TextStreamAnimator {\n private animationFrameId: number | null = null;\n private lastUpdateTime: number = Date.now();\n public lastCommitTime: number = 0;\n\n public targetText: string = \"\";\n public drainMs: number = DEFAULT_DRAIN_MS;\n public maxCharIntervalMs: number = DEFAULT_MAX_CHAR_INTERVAL_MS;\n public maxCharsPerFrame: number = Infinity;\n public minCommitMs: number = 0;\n\n public currentText: string;\n private setText: (newText: string) => void;\n\n constructor(currentText: string, setText: (newText: string) => void) {\n this.currentText = currentText;\n this.setText = setText;\n }\n\n start() {\n if (this.animationFrameId !== null) return;\n this.lastUpdateTime = Date.now();\n this.animate();\n }\n\n stop() {\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n }\n\n private animate = () => {\n const currentTime = Date.now();\n const deltaTime = currentTime - this.lastUpdateTime;\n let timeToConsume = deltaTime;\n\n const remainingChars = this.targetText.length - this.currentText.length;\n const baseTimePerChar = Math.min(\n this.maxCharIntervalMs,\n this.drainMs / remainingChars,\n );\n\n const frameLimit = Math.min(remainingChars, this.maxCharsPerFrame);\n let charsToAdd = 0;\n while (timeToConsume >= baseTimePerChar && charsToAdd < frameLimit) {\n charsToAdd++;\n timeToConsume -= baseTimePerChar;\n }\n // A cap-limited frame must not bank its surplus time, or the next\n // frame would burst past the cap.\n if (charsToAdd === frameLimit && frameLimit === this.maxCharsPerFrame) {\n timeToConsume = 0;\n }\n\n if (charsToAdd !== remainingChars) {\n this.animationFrameId = requestAnimationFrame(this.animate);\n } else {\n this.animationFrameId = null;\n }\n if (charsToAdd === 0) return;\n\n this.currentText = this.targetText.slice(\n 0,\n this.currentText.length + charsToAdd,\n );\n this.lastUpdateTime = currentTime - timeToConsume;\n\n const isComplete = charsToAdd === remainingChars;\n if (isComplete || currentTime - this.lastCommitTime >= this.minCommitMs) {\n this.lastCommitTime = currentTime;\n this.setText(this.currentText);\n }\n };\n}\n\nconst SMOOTH_STATUS: MessagePartStatus = Object.freeze({\n type: \"running\",\n});\n\nconst positiveOr = (value: number | undefined, fallback: number): number =>\n value !== undefined && value > 0 ? value : fallback;\n\n/**\n * Animates streamed message part text with a typewriter-style reveal.\n *\n * Takes the current part state and a `smooth` argument: `false` disables,\n * `true` uses the default rate, and a {@link SmoothOptions} object tunes\n * the reveal. Returns the part state with `text` replaced by the revealed\n * prefix and `status` reporting `running` until the reveal catches up.\n * If the source settles before any character has been revealed, the text\n * is committed immediately so a missed animation frame cannot leave an\n * empty bubble.\n *\n * The reveal auto-disables under `prefers-reduced-motion: reduce`,\n * committing the full text immediately; this takes precedence over an\n * explicit `smooth={true}`.\n *\n * @example\n * ```tsx\n * const { text, status } = useSmooth(useMessagePartText(), {\n * drainMs: 500,\n * maxCharsPerFrame: 30,\n * });\n * ```\n */\nexport const useSmooth = (\n state: MessagePartState & (TextMessagePart | ReasoningMessagePart),\n smooth: boolean | SmoothOptions = false,\n): MessagePartState & (TextMessagePart | ReasoningMessagePart) => {\n const { text } = state;\n const reduceMotion = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n const options =\n typeof smooth === \"object\" && smooth !== null ? smooth : undefined;\n const enabled = smooth !== false && smooth !== null && !reduceMotion;\n const drainMs = positiveOr(options?.drainMs, DEFAULT_DRAIN_MS);\n const maxCharIntervalMs = positiveOr(\n options?.maxCharIntervalMs,\n DEFAULT_MAX_CHAR_INTERVAL_MS,\n );\n const maxCharsPerFrame = positiveOr(options?.maxCharsPerFrame, Infinity);\n const minCommitMs = positiveOr(options?.minCommitMs, 0);\n\n const [displayedText, setDisplayedText] = useState(\n state.status.type === \"running\" ? \"\" : text,\n );\n\n // Render-phase resync on part flip or text discontinuity, so the\n // first paint after a thread switch never shows the previous\n // part's text (#4051). `displayedText` is already a prefix of\n // `text` during normal streaming, so use it as the previous-text\n // reference instead of carrying separate state — avoids the\n // double render per streaming token. Read part identity through\n // `useAuiState` so we actually subscribe to its changes instead\n // of relying on a render-time proxy reference that may be stable\n // across thread swaps.\n const aui = useAui();\n const part = useAuiState(() => aui.part);\n const [prevPart, setPrevPart] = useState(part);\n if (part !== prevPart || !text.startsWith(displayedText)) {\n setPrevPart(part);\n setDisplayedText(state.status.type === \"running\" ? \"\" : text);\n }\n\n const smoothStatusStore = useSmoothStatusStore({ optional: true });\n const setText = useCallbackRef((text: string) => {\n setDisplayedText(text);\n if (smoothStatusStore) {\n const target =\n displayedText !== text || state.status.type === \"running\"\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n });\n\n // TODO this is hacky\n useEffect(() => {\n if (smoothStatusStore) {\n const target =\n enabled && (displayedText !== text || state.status.type === \"running\")\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n }, [smoothStatusStore, enabled, text, displayedText, state.status]);\n\n const [animatorRef] = useState<TextStreamAnimator>(\n new TextStreamAnimator(displayedText, setText),\n );\n\n useEffect(() => {\n animatorRef.drainMs = drainMs;\n animatorRef.maxCharIntervalMs = maxCharIntervalMs;\n animatorRef.maxCharsPerFrame = maxCharsPerFrame;\n animatorRef.minCommitMs = minCommitMs;\n }, [animatorRef, drainMs, maxCharIntervalMs, maxCharsPerFrame, minCommitMs]);\n\n const animatorPartRef = useRef(part);\n useEffect(() => {\n if (!enabled) {\n animatorRef.stop();\n return;\n }\n\n const partChanged = animatorPartRef.current !== part;\n animatorPartRef.current = part;\n // A new part whose text shares a prefix with the previous target would\n // keep the stale cursor and flicker without this reset.\n if (partChanged || !text.startsWith(animatorRef.targetText)) {\n if (state.status.type === \"running\") {\n animatorRef.currentText = \"\";\n animatorRef.targetText = text;\n animatorRef.lastCommitTime = 0;\n animatorRef.start();\n } else {\n animatorRef.currentText = text;\n animatorRef.targetText = text;\n animatorRef.stop();\n setText(text);\n }\n return;\n }\n\n animatorRef.targetText = text;\n if (state.status.type !== \"running\") {\n // No character has painted. A pending frame that never runs would\n // leave an empty bubble after settle.\n if (animatorRef.currentText === \"\") {\n animatorRef.currentText = text;\n animatorRef.stop();\n setText(text);\n return;\n }\n animatorRef.start();\n return;\n }\n\n animatorRef.start();\n }, [animatorRef, enabled, text, state.status.type, part, setText]);\n\n useEffect(() => {\n return () => {\n animatorRef.stop();\n };\n }, [animatorRef]);\n\n return useMemo(\n () =>\n enabled\n ? {\n ...state,\n text: displayedText,\n status: text === displayedText ? state.status : SMOOTH_STATUS,\n }\n : state,\n [enabled, displayedText, state, text],\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useSmooth.js","names":["useEffect","useMemo","useRef","useState","useAui","useAuiState","MessagePartStatus","ReasoningMessagePart","TextMessagePart","MessagePartState","useCallbackRef","useMediaQuery","useSmoothStatusStore","writableStore","SmoothOptions","drainMs","maxCharIntervalMs","maxCharsPerFrame","minCommitMs","DEFAULT_DRAIN_MS","DEFAULT_MAX_CHAR_INTERVAL_MS","TextStreamAnimator","animationFrameId","lastUpdateTime","lastCommitTime","Infinity","targetText","currentText","setText","newText","constructor","start","performance","now","animate","stop","cancelAnimationFrame","currentTime","deltaTime","timeToConsume","remainingChars","length","baseTimePerChar","Math","min","frameLimit","charsToAdd","requestAnimationFrame","slice","isComplete","SMOOTH_STATUS","Object","freeze","type","positiveOr","value","fallback","undefined","useSmooth","state","smooth","text","reduceMotion","options","enabled","displayedText","setDisplayedText","status","aui","part","prevPart","setPrevPart","startsWith","smoothStatusStore","optional","target","setState","animatorRef","animatorPartRef","partChanged","current"],"sources":["../../../src/utils/smooth/useSmooth.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport type {\n MessagePartStatus,\n ReasoningMessagePart,\n TextMessagePart,\n MessagePartState,\n} from \"@assistant-ui/core\";\nimport { useCallbackRef } from \"radix-ui/internal\";\nimport { useMediaQuery } from \"../hooks/useMediaQuery\";\nimport { useSmoothStatusStore } from \"./SmoothContext\";\nimport { writableStore } from \"../../context/ReadonlyStore\";\n\n/**\n * Tuning options for the smooth text streaming animation.\n */\nexport type SmoothOptions = {\n /**\n * Target time in milliseconds to drain the backlog of unrevealed\n * characters. Larger values reveal long backlogs more gradually.\n * @default 250\n */\n drainMs?: number | undefined;\n /**\n * Maximum time in milliseconds between revealed characters, i.e. the\n * slowest reveal rate when the backlog is short.\n * @default 5\n */\n maxCharIntervalMs?: number | undefined;\n /**\n * Maximum number of characters revealed per animation frame.\n * @default Infinity\n */\n maxCharsPerFrame?: number | undefined;\n /**\n * Minimum time in milliseconds between committed updates. The reveal keeps\n * advancing every frame, but the visible text (and the downstream re-render\n * and markdown re-parse it triggers) is committed at most once per interval.\n * The final frame always commits. `0` commits every frame.\n * @default 0\n */\n minCommitMs?: number | undefined;\n};\n\nconst DEFAULT_DRAIN_MS = 250;\nconst DEFAULT_MAX_CHAR_INTERVAL_MS = 5;\n\nclass TextStreamAnimator {\n private animationFrameId: number | null = null;\n private lastUpdateTime: number = 0;\n public lastCommitTime: number = -Infinity;\n\n public targetText: string = \"\";\n public drainMs: number = DEFAULT_DRAIN_MS;\n public maxCharIntervalMs: number = DEFAULT_MAX_CHAR_INTERVAL_MS;\n public maxCharsPerFrame: number = Infinity;\n public minCommitMs: number = 0;\n\n public currentText: string;\n private setText: (newText: string) => void;\n\n constructor(currentText: string, setText: (newText: string) => void) {\n this.currentText = currentText;\n this.setText = setText;\n }\n\n start() {\n if (this.animationFrameId !== null) return;\n this.lastUpdateTime = performance.now();\n this.animate();\n }\n\n stop() {\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n }\n\n private animate = () => {\n const currentTime = performance.now();\n const deltaTime = currentTime - this.lastUpdateTime;\n let timeToConsume = deltaTime;\n\n const remainingChars = this.targetText.length - this.currentText.length;\n const baseTimePerChar = Math.min(\n this.maxCharIntervalMs,\n this.drainMs / remainingChars,\n );\n\n const frameLimit = Math.min(remainingChars, this.maxCharsPerFrame);\n let charsToAdd = 0;\n while (timeToConsume >= baseTimePerChar && charsToAdd < frameLimit) {\n charsToAdd++;\n timeToConsume -= baseTimePerChar;\n }\n // A cap-limited frame must not bank its surplus time, or the next\n // frame would burst past the cap.\n if (charsToAdd === frameLimit && frameLimit === this.maxCharsPerFrame) {\n timeToConsume = 0;\n }\n\n if (charsToAdd !== remainingChars) {\n this.animationFrameId = requestAnimationFrame(this.animate);\n } else {\n this.animationFrameId = null;\n }\n if (charsToAdd === 0) return;\n\n this.currentText = this.targetText.slice(\n 0,\n this.currentText.length + charsToAdd,\n );\n this.lastUpdateTime = currentTime - timeToConsume;\n\n const isComplete = charsToAdd === remainingChars;\n if (isComplete || currentTime - this.lastCommitTime >= this.minCommitMs) {\n this.lastCommitTime = currentTime;\n this.setText(this.currentText);\n }\n };\n}\n\nconst SMOOTH_STATUS: MessagePartStatus = Object.freeze({\n type: \"running\",\n});\n\nconst positiveOr = (value: number | undefined, fallback: number): number =>\n value !== undefined && value > 0 ? value : fallback;\n\n/**\n * Animates streamed message part text with a typewriter-style reveal.\n *\n * Takes the current part state and a `smooth` argument: `false` disables,\n * `true` uses the default rate, and a {@link SmoothOptions} object tunes\n * the reveal. Returns the part state with `text` replaced by the revealed\n * prefix and `status` reporting `running` until the reveal catches up.\n * If the source settles before any character has been revealed, the text\n * is committed immediately so a missed animation frame cannot leave an\n * empty bubble.\n *\n * The reveal auto-disables under `prefers-reduced-motion: reduce`,\n * committing the full text immediately; this takes precedence over an\n * explicit `smooth={true}`.\n *\n * @example\n * ```tsx\n * const { text, status } = useSmooth(useMessagePartText(), {\n * drainMs: 500,\n * maxCharsPerFrame: 30,\n * });\n * ```\n */\nexport const useSmooth = (\n state: MessagePartState & (TextMessagePart | ReasoningMessagePart),\n smooth: boolean | SmoothOptions = false,\n): MessagePartState & (TextMessagePart | ReasoningMessagePart) => {\n const { text } = state;\n const reduceMotion = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n const options =\n typeof smooth === \"object\" && smooth !== null ? smooth : undefined;\n const enabled = smooth !== false && smooth !== null && !reduceMotion;\n const drainMs = positiveOr(options?.drainMs, DEFAULT_DRAIN_MS);\n const maxCharIntervalMs = positiveOr(\n options?.maxCharIntervalMs,\n DEFAULT_MAX_CHAR_INTERVAL_MS,\n );\n const maxCharsPerFrame = positiveOr(options?.maxCharsPerFrame, Infinity);\n const minCommitMs = positiveOr(options?.minCommitMs, 0);\n\n const [displayedText, setDisplayedText] = useState(\n state.status.type === \"running\" ? \"\" : text,\n );\n\n // Render-phase resync on part flip or text discontinuity, so the\n // first paint after a thread switch never shows the previous\n // part's text (#4051). `displayedText` is already a prefix of\n // `text` during normal streaming, so use it as the previous-text\n // reference instead of carrying separate state — avoids the\n // double render per streaming token. Read part identity through\n // `useAuiState` so we actually subscribe to its changes instead\n // of relying on a render-time proxy reference that may be stable\n // across thread swaps.\n const aui = useAui();\n const part = useAuiState(() => aui.part);\n const [prevPart, setPrevPart] = useState(part);\n if (part !== prevPart || !text.startsWith(displayedText)) {\n setPrevPart(part);\n setDisplayedText(state.status.type === \"running\" ? \"\" : text);\n }\n\n const smoothStatusStore = useSmoothStatusStore({ optional: true });\n const setText = useCallbackRef((text: string) => {\n setDisplayedText(text);\n if (smoothStatusStore) {\n const target =\n displayedText !== text || state.status.type === \"running\"\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n });\n\n // TODO this is hacky\n useEffect(() => {\n if (smoothStatusStore) {\n const target =\n enabled && (displayedText !== text || state.status.type === \"running\")\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n }, [smoothStatusStore, enabled, text, displayedText, state.status]);\n\n const [animatorRef] = useState(\n () => new TextStreamAnimator(displayedText, setText),\n );\n\n useEffect(() => {\n animatorRef.drainMs = drainMs;\n animatorRef.maxCharIntervalMs = maxCharIntervalMs;\n animatorRef.maxCharsPerFrame = maxCharsPerFrame;\n animatorRef.minCommitMs = minCommitMs;\n }, [animatorRef, drainMs, maxCharIntervalMs, maxCharsPerFrame, minCommitMs]);\n\n const animatorPartRef = useRef(part);\n useEffect(() => {\n if (!enabled) {\n animatorRef.stop();\n return;\n }\n\n const partChanged = animatorPartRef.current !== part;\n animatorPartRef.current = part;\n // A new part whose text shares a prefix with the previous target would\n // keep the stale cursor and flicker without this reset.\n if (partChanged || !text.startsWith(animatorRef.targetText)) {\n if (state.status.type === \"running\") {\n animatorRef.currentText = \"\";\n animatorRef.targetText = text;\n animatorRef.lastCommitTime = -Infinity;\n animatorRef.start();\n } else {\n animatorRef.currentText = text;\n animatorRef.targetText = text;\n animatorRef.stop();\n setText(text);\n }\n return;\n }\n\n animatorRef.targetText = text;\n if (state.status.type !== \"running\") {\n // No character has painted. A pending frame that never runs would\n // leave an empty bubble after settle.\n if (animatorRef.currentText === \"\") {\n animatorRef.currentText = text;\n animatorRef.stop();\n setText(text);\n return;\n }\n animatorRef.start();\n return;\n }\n\n animatorRef.start();\n }, [animatorRef, enabled, text, state.status.type, part, setText]);\n\n useEffect(() => {\n return () => {\n animatorRef.stop();\n };\n }, [animatorRef]);\n\n return useMemo(\n () =>\n enabled\n ? {\n ...state,\n text: displayedText,\n status: text === displayedText ? state.status : SMOOTH_STATUS,\n }\n : state,\n [enabled, displayedText, state, text],\n );\n};\n"],"mappings":";;;;;;;;AA8CA,MAAMmB,mBAAmB;AACzB,MAAMC,+BAA+B;AAErC,IAAMC,qBAAN,MAAyB;CACvB,mBAA0C;CAC1C,iBAAiC;CACjC,iBAAgC;CAEhC,aAA4B;CAC5B,UAAyBF;CACzB,oBAAmCC;CACnC,mBAAkCK;CAClC,cAA6B;CAE7B;CACA;CAEAK,YAAYH,aAAqBC,SAAoC;EACnE,KAAKD,cAAcA;EACnB,KAAKC,UAAUA;CACjB;CAEAG,QAAQ;EACN,IAAI,KAAKT,qBAAqB,MAAM;EACpC,KAAKC,iBAAiBS,YAAYC,IAAI;EACtC,KAAKC,QAAQ;CACf;CAEAC,OAAO;EACL,IAAI,KAAKb,qBAAqB,MAAM;GAClCc,qBAAqB,KAAKd,gBAAgB;GAC1C,KAAKA,mBAAmB;EAC1B;CACF;CAEA,gBAAwB;EACtB,MAAMe,cAAcL,YAAYC,IAAI;EAEpC,IAAIM,gBADcF,cAAc,KAAKd;EAGrC,MAAMiB,iBAAiB,KAAKd,WAAWe,SAAS,KAAKd,YAAYc;EACjE,MAAMC,kBAAkBC,KAAKC,IAC3B,KAAK5B,mBACL,KAAKD,UAAUyB,cACjB;EAEA,MAAMK,aAAaF,KAAKC,IAAIJ,gBAAgB,KAAKvB,gBAAgB;EACjE,IAAI6B,aAAa;EACjB,OAAOP,iBAAiBG,mBAAmBI,aAAaD,YAAY;GAClEC;GACAP,iBAAiBG;EACnB;EAGA,IAAII,eAAeD,cAAcA,eAAe,KAAK5B,kBACnDsB,gBAAgB;EAGlB,IAAIO,eAAeN,gBACjB,KAAKlB,mBAAmByB,sBAAsB,KAAKb,OAAO;OAE1D,KAAKZ,mBAAmB;EAE1B,IAAIwB,eAAe,GAAG;EAEtB,KAAKnB,cAAc,KAAKD,WAAWsB,MACjC,GACA,KAAKrB,YAAYc,SAASK,UAC5B;EACA,KAAKvB,iBAAiBc,cAAcE;EAGpC,IADmBO,eAAeN,kBAChBH,cAAc,KAAKb,kBAAkB,KAAKN,aAAa;GACvE,KAAKM,iBAAiBa;GACtB,KAAKT,QAAQ,KAAKD,WAAW;EAC/B;CACF;AACF;AAEA,MAAMuB,gBAAmCC,OAAOC,OAAO,EACrDC,MAAM,UACR,CAAC;AAED,MAAMC,cAAcC,OAA2BC,aAC7CD,UAAUE,KAAAA,KAAaF,QAAQ,IAAIA,QAAQC;;;;;;;;;;;;;;;;;;;;;;;;AAyB7C,MAAaE,aACXC,OACAC,SAAkC,UAC8B;CAChE,MAAM,EAAEC,SAASF;CACjB,MAAMG,eAAenD,cAAc,kCAAkC;CACrE,MAAMoD,UACJ,OAAOH,WAAW,YAAYA,WAAW,OAAOA,SAASH,KAAAA;CAC3D,MAAMO,UAAUJ,WAAW,SAASA,WAAW,QAAQ,CAACE;CACxD,MAAM/C,UAAUuC,WAAWS,SAAShD,SAASI,gBAAgB;CAC7D,MAAMH,oBAAoBsC,WACxBS,SAAS/C,mBACTI,4BACF;CACA,MAAMH,mBAAmBqC,WAAWS,SAAS9C,kBAAkBQ,QAAQ;CACvE,MAAMP,cAAcoC,WAAWS,SAAS7C,aAAa,CAAC;CAEtD,MAAM,CAAC+C,eAAeC,oBAAoB/D,SACxCwD,MAAMQ,OAAOd,SAAS,YAAY,KAAKQ,IACzC;CAWA,MAAMO,MAAMhE,OAAO;CACnB,MAAMiE,OAAOhE,kBAAkB+D,IAAIC,IAAI;CACvC,MAAM,CAACC,UAAUC,eAAepE,SAASkE,IAAI;CAC7C,IAAIA,SAASC,YAAY,CAACT,KAAKW,WAAWP,aAAa,GAAG;EACxDM,YAAYF,IAAI;EAChBH,iBAAiBP,MAAMQ,OAAOd,SAAS,YAAY,KAAKQ,IAAI;CAC9D;CAEA,MAAMY,oBAAoB7D,qBAAqB,EAAE8D,UAAU,KAAK,CAAC;CACjE,MAAM9C,UAAUlB,gBAAgBmD,WAAiB;EAC/CK,iBAAiBL,MAAI;EACrB,IAAIY,mBAAmB;GACrB,MAAME,SACJV,kBAAkBJ,UAAQF,MAAMQ,OAAOd,SAAS,YAC5CH,gBACAS,MAAMQ;GACZtD,cAAc4D,iBAAiB,CAAC,CAACG,SAASD,QAAQ,IAAI;EACxD;CACF,CAAC;CAGD3E,gBAAgB;EACd,IAAIyE,mBAAmB;GACrB,MAAME,WACJX,YAAYC,kBAAkBJ,QAAQF,MAAMQ,OAAOd,SAAS,aACxDH,gBACAS,MAAMQ;GACZtD,cAAc4D,iBAAiB,CAAC,CAACG,SAASD,UAAQ,IAAI;EACxD;CACF,GAAG;EAACF;EAAmBT;EAASH;EAAMI;EAAeN,MAAMQ;CAAM,CAAC;CAElE,MAAM,CAACU,eAAe1E,eACd,IAAIkB,mBAAmB4C,eAAerC,OAAO,CACrD;CAEA5B,gBAAgB;EACd6E,YAAY9D,UAAUA;EACtB8D,YAAY7D,oBAAoBA;EAChC6D,YAAY5D,mBAAmBA;EAC/B4D,YAAY3D,cAAcA;CAC5B,GAAG;EAAC2D;EAAa9D;EAASC;EAAmBC;EAAkBC;CAAW,CAAC;CAE3E,MAAM4D,kBAAkB5E,OAAOmE,IAAI;CACnCrE,gBAAgB;EACd,IAAI,CAACgE,SAAS;GACZa,YAAY1C,KAAK;GACjB;EACF;EAEA,MAAM4C,cAAcD,gBAAgBE,YAAYX;EAChDS,gBAAgBE,UAAUX;EAG1B,IAAIU,eAAe,CAAClB,KAAKW,WAAWK,YAAYnD,UAAU,GAAG;GAC3D,IAAIiC,MAAMQ,OAAOd,SAAS,WAAW;IACnCwB,YAAYlD,cAAc;IAC1BkD,YAAYnD,aAAamC;IACzBgB,YAAYrD,iBAAiB;IAC7BqD,YAAY9C,MAAM;GACpB,OAAO;IACL8C,YAAYlD,cAAckC;IAC1BgB,YAAYnD,aAAamC;IACzBgB,YAAY1C,KAAK;IACjBP,QAAQiC,IAAI;GACd;GACA;EACF;EAEAgB,YAAYnD,aAAamC;EACzB,IAAIF,MAAMQ,OAAOd,SAAS,WAAW;GAGnC,IAAIwB,YAAYlD,gBAAgB,IAAI;IAClCkD,YAAYlD,cAAckC;IAC1BgB,YAAY1C,KAAK;IACjBP,QAAQiC,IAAI;IACZ;GACF;GACAgB,YAAY9C,MAAM;GAClB;EACF;EAEA8C,YAAY9C,MAAM;CACpB,GAAG;EAAC8C;EAAab;EAASH;EAAMF,MAAMQ,OAAOd;EAAMgB;EAAMzC;CAAO,CAAC;CAEjE5B,gBAAgB;EACd,aAAa;GACX6E,YAAY1C,KAAK;EACnB;CACF,GAAG,CAAC0C,WAAW,CAAC;CAEhB,OAAO5E,cAEH+D,UACI;EACE,GAAGL;EACHE,MAAMI;EACNE,QAAQN,SAASI,gBAAgBN,MAAMQ,SAASjB;CAClD,IACAS,OACN;EAACK;EAASC;EAAeN;EAAOE;CAAI,CACtC;AACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useIsomorphicLayoutEffect.d.ts","sourceRoot":"","sources":["../../src/utils/useIsomorphicLayoutEffect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAmB,MAAM,OAAO,CAAC;AAMnD,eAAO,MAAM,yBAAyB,kBAEzB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect } from "@assistant-ui/tap/react-shim";
|
|
2
|
+
const useIsomorphicLayoutEffect = typeof globalThis.window !== "undefined" ? useLayoutEffect : useEffect;
|
|
3
|
+
//#endregion
|
|
4
|
+
export { useIsomorphicLayoutEffect };
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=useIsomorphicLayoutEffect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useIsomorphicLayoutEffect.js","names":["useEffect","useLayoutEffect","hasWindow","globalThis","window","useIsomorphicLayoutEffect"],"sources":["../../src/utils/useIsomorphicLayoutEffect.ts"],"sourcesContent":["import { useEffect, useLayoutEffect } from \"react\";\n\nconst hasWindow =\n typeof (globalThis as typeof globalThis & { window?: unknown }).window !==\n \"undefined\";\n\nexport const useIsomorphicLayoutEffect = hasWindow\n ? useLayoutEffect\n : useEffect;\n"],"mappings":";AAMA,MAAaK,4BAHX,OAAQF,WAAwDC,WAChE,cAGEH,kBACAD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.26",
|
|
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.
|
|
59
|
-
"@assistant-ui/store": "^0.3.
|
|
60
|
-
"@assistant-ui/tap": "^0.9.
|
|
61
|
-
"assistant-cloud": "^0.2.
|
|
62
|
-
"assistant-stream": "^0.3.
|
|
63
|
-
"radix-ui": "^1.
|
|
58
|
+
"@assistant-ui/core": "^0.3.25",
|
|
59
|
+
"@assistant-ui/store": "^0.3.18",
|
|
60
|
+
"@assistant-ui/tap": "^0.9.22",
|
|
61
|
+
"assistant-cloud": "^0.2.6",
|
|
62
|
+
"assistant-stream": "^0.3.49",
|
|
63
|
+
"radix-ui": "^1.7.0",
|
|
64
64
|
"react-textarea-autosize": "^8.5.9",
|
|
65
|
-
"safe-content-frame": "^0.0.
|
|
65
|
+
"safe-content-frame": "^0.0.33",
|
|
66
66
|
"zod": "^4.6.5",
|
|
67
67
|
"zustand": "^5.0.15"
|
|
68
68
|
},
|
|
@@ -81,15 +81,15 @@
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@assistant-ui/vite": "0.0.
|
|
84
|
+
"@assistant-ui/vite": "0.0.21",
|
|
85
85
|
"@assistant-ui/x-buildutils": "0.0.30",
|
|
86
86
|
"@assistant-ui/x-react18": "0.0.0",
|
|
87
87
|
"@testing-library/react": "^16.3.3",
|
|
88
88
|
"@types/json-schema": "^7.0.15",
|
|
89
|
-
"@types/node": "^26.6.
|
|
89
|
+
"@types/node": "^26.6.4",
|
|
90
90
|
"@types/react": "^19.3.0",
|
|
91
91
|
"@types/react-dom": "^19.3.0",
|
|
92
|
-
"jsdom": "^30.
|
|
92
|
+
"jsdom": "^30.1.2",
|
|
93
93
|
"react": "^19.3.0",
|
|
94
94
|
"react-dom": "^19.3.0",
|
|
95
95
|
"vitest": "^5.0.3"
|
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
"scripts": {
|
|
111
111
|
"build": "aui-build",
|
|
112
112
|
"test": "vitest run",
|
|
113
|
+
"test:coverage": "vitest run --coverage --coverage.reporter=text --coverage.reporter=html --coverage.reporter=json-summary",
|
|
113
114
|
"typecheck": "tsc --noEmit",
|
|
114
115
|
"test:peer-react18": "vitest run --config vitest.peer-react18.config.ts",
|
|
115
116
|
"test:watch": "vitest",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { renderToString } from "react-dom/server";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const { part } = vi.hoisted(() => ({
|
|
5
|
+
part: {
|
|
6
|
+
type: "tool-call" as const,
|
|
7
|
+
status: { type: "running" as const },
|
|
8
|
+
timing: { startedAt: 9000 },
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("@assistant-ui/store", async (importOriginal) => ({
|
|
13
|
+
...(await importOriginal<typeof import("@assistant-ui/store")>()),
|
|
14
|
+
useAuiState: (
|
|
15
|
+
selector: (state: { optional: { part: typeof part } }) => unknown,
|
|
16
|
+
) => selector({ optional: { part } }),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
import { useToolCallElapsed } from "./useToolCallElapsed";
|
|
20
|
+
|
|
21
|
+
describe("useToolCallElapsed", () => {
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("does not read the wall clock during server render", () => {
|
|
27
|
+
const nowSpy = vi.spyOn(Date, "now");
|
|
28
|
+
const errors = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
29
|
+
const Probe = () => <span>{useToolCallElapsed() ?? "none"}</span>;
|
|
30
|
+
|
|
31
|
+
expect(renderToString(<Probe />)).toBe("<span>none</span>");
|
|
32
|
+
expect(nowSpy).not.toHaveBeenCalled();
|
|
33
|
+
expect(errors.mock.calls.flat().join(" ")).not.toMatch(
|
|
34
|
+
/useLayoutEffect does nothing on the server/,
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useState } from "react";
|
|
4
4
|
import { useAuiState } from "@assistant-ui/store";
|
|
5
|
+
import { useIsomorphicLayoutEffect } from "../utils/useIsomorphicLayoutEffect";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Hook that returns the elapsed wall-clock time of the current tool call in
|
|
@@ -10,7 +11,9 @@ import { useAuiState } from "@assistant-ui/store";
|
|
|
10
11
|
* Reads `part.timing`. Returns `undefined` when the part is not a tool call,
|
|
11
12
|
* carries no timing, ended without a recorded completion (the duration is
|
|
12
13
|
* unknown), or when no message part scope is available (so kit components
|
|
13
|
-
* stay renderable standalone, e.g. in docs previews).
|
|
14
|
+
* stay renderable standalone, e.g. in docs previews). A running call also
|
|
15
|
+
* returns `undefined` until the component mounts, so a server render never
|
|
16
|
+
* reads the clock.
|
|
14
17
|
*
|
|
15
18
|
* @example
|
|
16
19
|
* ```tsx
|
|
@@ -32,12 +35,12 @@ export const useToolCallElapsed = (): number | undefined => {
|
|
|
32
35
|
});
|
|
33
36
|
const running =
|
|
34
37
|
timing !== undefined && timing.completedAt === undefined && partRunning;
|
|
35
|
-
const [now, setNow] = useState
|
|
38
|
+
const [now, setNow] = useState<number | undefined>(undefined);
|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
useIsomorphicLayoutEffect(() => {
|
|
38
41
|
if (!running) return undefined;
|
|
39
|
-
// The clock is
|
|
40
|
-
// the
|
|
42
|
+
// The clock is read only after mount so a prerender never reads it; the
|
|
43
|
+
// layout effect still catches the elapsed value up before the first paint.
|
|
41
44
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
42
45
|
setNow(Date.now());
|
|
43
46
|
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
@@ -47,6 +50,6 @@ export const useToolCallElapsed = (): number | undefined => {
|
|
|
47
50
|
if (timing === undefined) return undefined;
|
|
48
51
|
if (timing.completedAt !== undefined)
|
|
49
52
|
return Math.max(0, timing.completedAt - timing.startedAt);
|
|
50
|
-
if (!running) return undefined;
|
|
53
|
+
if (!running || now === undefined) return undefined;
|
|
51
54
|
return Math.max(0, now - timing.startedAt);
|
|
52
55
|
};
|
package/src/index.ts
CHANGED
|
@@ -164,11 +164,17 @@ export { useExternalStoreRuntime } from "./legacy-runtime/runtime-cores/external
|
|
|
164
164
|
export { useExternalStoreSharedOptions } from "@assistant-ui/core/react";
|
|
165
165
|
export {
|
|
166
166
|
useExternalMessageConverter,
|
|
167
|
+
/** @deprecated Experimental since 2025-01-26. Not scheduled for removal; the API may change in any release. */
|
|
167
168
|
convertExternalMessages as unstable_convertExternalMessages,
|
|
169
|
+
/** @deprecated Experimental since 2026-09-15. Not scheduled for removal; the API may change in any release. */
|
|
168
170
|
createExternalMessageConversionCache as unstable_createExternalMessageConversionCache,
|
|
171
|
+
/** @deprecated Experimental since 2026-09-15. Not scheduled for removal; the API may change in any release. */
|
|
169
172
|
type ExternalMessageConversionCache as Unstable_ExternalMessageConversionCache,
|
|
170
173
|
} from "./legacy-runtime/runtime-cores/external-store/external-message-converter";
|
|
171
|
-
export {
|
|
174
|
+
export {
|
|
175
|
+
/** @deprecated Experimental since 2025-02-02. Not scheduled for removal; the API may change in any release. */
|
|
176
|
+
createMessageConverter as unstable_createMessageConverter,
|
|
177
|
+
} from "./legacy-runtime/runtime-cores/external-store/createMessageConverter";
|
|
172
178
|
|
|
173
179
|
// --- local ---
|
|
174
180
|
export type {
|
|
@@ -333,6 +339,11 @@ export * as ThreadListItemMorePrimitive from "./primitives/threadListItemMore";
|
|
|
333
339
|
export * as SelectionToolbarPrimitive from "./primitives/selectionToolbar";
|
|
334
340
|
|
|
335
341
|
export { groupPartByType, type GroupByContext } from "@assistant-ui/core/react";
|
|
342
|
+
export {
|
|
343
|
+
createThreadRowsSelector,
|
|
344
|
+
type ThreadRow,
|
|
345
|
+
type ThreadRowsOptions,
|
|
346
|
+
} from "@assistant-ui/core/react";
|
|
336
347
|
export { unstable_useThreadMessageIds } from "@assistant-ui/core/react";
|
|
337
348
|
export { useMessagePartText } from "./primitives/messagePart/useMessagePartText";
|
|
338
349
|
export { useMessagePartReasoning } from "./primitives/messagePart/useMessagePartReasoning";
|
|
@@ -375,8 +386,12 @@ export type {
|
|
|
375
386
|
ToolApprovalDisplay,
|
|
376
387
|
ToolApprovalOption,
|
|
377
388
|
ToolApprovalOptionKind,
|
|
389
|
+
ToolApprovalAnswer,
|
|
390
|
+
ToolApprovalQuestion,
|
|
391
|
+
ToolApprovalQuestionOption,
|
|
378
392
|
ToolApprovalResponse,
|
|
379
393
|
ToolCallMessagePart,
|
|
394
|
+
MessagePartTiming,
|
|
380
395
|
ToolCallTiming,
|
|
381
396
|
ToolModelContentPart,
|
|
382
397
|
MessageStatus,
|
|
@@ -487,23 +502,47 @@ export type { ToolExecutionStatus } from "./internal";
|
|
|
487
502
|
|
|
488
503
|
// Unstable - trigger popover (unified root for @ mentions, / slash commands, etc.)
|
|
489
504
|
export {
|
|
505
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
490
506
|
useTriggerPopoverRootContext as unstable_useTriggerPopoverRootContext,
|
|
507
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
491
508
|
useTriggerPopoverRootContextOptional as unstable_useTriggerPopoverRootContextOptional,
|
|
509
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
492
510
|
useTriggerPopoverScopeContext as unstable_useTriggerPopoverScopeContext,
|
|
511
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
493
512
|
useTriggerPopoverScopeContextOptional as unstable_useTriggerPopoverScopeContextOptional,
|
|
513
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
494
514
|
useTriggerPopoverTriggers as unstable_useTriggerPopoverTriggers,
|
|
515
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
495
516
|
useTriggerPopoverTriggersOptional as unstable_useTriggerPopoverTriggersOptional,
|
|
517
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
496
518
|
type RegisteredTrigger as Unstable_RegisteredTrigger,
|
|
497
|
-
type TriggerMatch
|
|
498
|
-
type TriggerMatcher
|
|
519
|
+
type TriggerMatch,
|
|
520
|
+
type TriggerMatcher,
|
|
521
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
499
522
|
type TriggerBehavior as Unstable_TriggerBehavior,
|
|
500
523
|
} from "./primitives/composer/trigger";
|
|
524
|
+
import type {
|
|
525
|
+
TriggerMatch,
|
|
526
|
+
TriggerMatcher,
|
|
527
|
+
} from "./primitives/composer/trigger";
|
|
528
|
+
/** @deprecated Use `TriggerMatch` instead. */
|
|
529
|
+
export type Unstable_TriggerMatch = TriggerMatch;
|
|
530
|
+
/** @deprecated Use `TriggerMatcher` instead. */
|
|
531
|
+
export type Unstable_TriggerMatcher = TriggerMatcher;
|
|
501
532
|
export type {
|
|
533
|
+
DirectiveFormatter,
|
|
534
|
+
DirectiveSegment,
|
|
535
|
+
TriggerAdapter,
|
|
536
|
+
TriggerCategory,
|
|
537
|
+
TriggerItem,
|
|
502
538
|
Unstable_DirectiveFormatter,
|
|
503
539
|
Unstable_DirectiveSegment,
|
|
504
540
|
Unstable_TriggerItem,
|
|
505
541
|
} from "@assistant-ui/core";
|
|
506
|
-
export {
|
|
542
|
+
export {
|
|
543
|
+
defaultDirectiveFormatter,
|
|
544
|
+
unstable_defaultDirectiveFormatter,
|
|
545
|
+
} from "@assistant-ui/core";
|
|
507
546
|
|
|
508
547
|
// Unstable - composer input history (terminal-style ArrowUp/ArrowDown recall)
|
|
509
548
|
export {
|
|
@@ -559,7 +598,10 @@ export {
|
|
|
559
598
|
type Unstable_WebMcpProviderOptions,
|
|
560
599
|
type Unstable_WebMcpProviderResult,
|
|
561
600
|
} from "./unstable/webmcp/useWebMcpProvider";
|
|
562
|
-
export {
|
|
601
|
+
export {
|
|
602
|
+
/** @deprecated Experimental since 2026-08-29. Not scheduled for removal; the API may change in any release. */
|
|
603
|
+
defaultWebMcpFilter as unstable_defaultWebMcpFilter,
|
|
604
|
+
} from "./unstable/webmcp/convertTools";
|
|
563
605
|
|
|
564
606
|
// Shared surface carried by every distribution (scripts/check-distribution-barrels.mjs)
|
|
565
607
|
export type {
|