@assistant-ui/react 0.15.24 → 0.15.25
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 +7 -8
- package/dist/hooks/useToolCallElapsed.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- 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/composer/ComposerInput.d.ts +1 -1
- package/dist/primitives/composer/ComposerInput.js +1 -1
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- 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.d.ts +20 -1
- 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/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/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +111 -2
- 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 +6 -1
- package/dist/sandbox-host/SandboxHost.js.map +1 -1
- package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
- package/dist/unstable/useMessageStallDetection.js +10 -6
- package/dist/unstable/useMessageStallDetection.js.map +1 -1
- package/dist/utils/smooth/useSmooth.js +6 -6
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/package.json +5 -4
- package/src/hooks/useToolCallElapsed.test.tsx +35 -0
- package/src/hooks/useToolCallElapsed.ts +9 -7
- package/src/index.ts +28 -3
- package/src/model-context/makeAssistantVisible.test.tsx +315 -5
- package/src/model-context/makeAssistantVisible.tsx +43 -7
- package/src/primitives/composer/ComposerInput.tsx +1 -1
- 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.ts +21 -1
- package/src/primitives/message/MessageRoot.test.tsx +125 -2
- package/src/primitives/thread/ThreadLoadEarlier.tsx +35 -0
- package/src/primitives/thread/ThreadMessages.tsx +1 -0
- package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +256 -5
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +147 -1
- package/src/primitives/thread.ts +2 -0
- package/src/sandbox-host/SandboxHost.test.tsx +34 -0
- package/src/sandbox-host/SandboxHost.tsx +8 -4
- package/src/tests/toolCallTiming.test.tsx +19 -2
- package/src/unstable/useMessageStallDetection.test.tsx +40 -0
- package/src/unstable/useMessageStallDetection.ts +10 -6
- package/src/utils/smooth/useSmooth.test.tsx +16 -5
- package/src/utils/smooth/useSmooth.ts +7 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useThreadViewportAutoScroll.js","names":["useComposedRefs","useCallback","useLayoutEffect","useRef","RefCallback","useAuiEvent","useAuiState","isUserScrollUp","isViewportAtBottom","viewportOverflows","useOnResizeContent","useOnScrollToBottom","useManagedRef","writableStore","useThreadViewportStore","ACTIVATION_KEYS","Set","TEXT_ENTRY_SELECTOR","join","useThreadViewportAutoScroll","Options","autoScroll","scrollToBottomOnRunStart","scrollToBottomOnInitialize","scrollToBottomOnThreadSwitch","HTMLElement","TElement","divRef","hasMessages","s","thread","messages","length","isRunning","initializeScrollRequestedRef","scheduledFrameRef","threadViewportStore","undefined","getState","turnAnchor","lastScrollTop","lastScrollHeight","lastObservedScrollHeight","lastObservedClientHeight","scrollingToBottomBehaviorRef","ScrollBehavior","followBottomRef","previousAutoScrollRef","previousAutoScroll","current","div","scrollToBottom","behavior","scrollTo","top","scrollHeight","cancelScheduledFrame","cancelAnimationFrame","scheduleScrollToBottom","requestAnimationFrame","hasActiveTopAnchor","state","element","viewport","anchor","handleScroll","isAtBottom","newIsAtBottom","isInFlightDownwardScroll","scrollTop","userScrolledUp","shouldUpdate","setState","resizeRef","clientHeight","scrollBehavior","scrollRef","el","cancelPendingScrollToBottom","cancelOnKeyDown","event","KeyboardEvent","has","key","target","Element","closest","addEventListener","removeEventListener","autoScrollRef"],"sources":["../../../src/primitives/thread/useThreadViewportAutoScroll.ts"],"sourcesContent":["\"use client\";\n\nimport { useComposedRefs } from \"radix-ui/internal\";\nimport { useCallback, useLayoutEffect, useRef, type RefCallback } from \"react\";\nimport { useAuiEvent, useAuiState } from \"@assistant-ui/store\";\nimport {\n isUserScrollUp,\n isViewportAtBottom,\n viewportOverflows,\n} from \"@assistant-ui/store/client\";\nimport { useOnResizeContent } from \"../../utils/hooks/useOnResizeContent\";\nimport { useOnScrollToBottom } from \"../../utils/hooks/useOnScrollToBottom\";\nimport { useManagedRef } from \"../../utils/hooks/useManagedRef\";\nimport { writableStore } from \"../../context/ReadonlyStore\";\nimport { useThreadViewportStore } from \"../../context/react/ThreadViewportContext\";\n\n// Enter and Space activate a focused control, which is how a collapsible tool\n// call expands without a pointer event ever firing. No other key changes thread\n// content: the keys that scroll the viewport reach handleScroll, which already\n// clears the intent when the user scrolls up.\nconst ACTIVATION_KEYS = new Set([\"Enter\", \" \"]);\n\n// A control that consumes the activation key itself instead of acting on thread\n// content. `contenteditable=\"false\"` marks a non-editable island inside an\n// editable tree, so it is excluded the way ComposerRoot already excludes it;\n// the input types left out are the ones a key activates rather than fills.\nconst TEXT_ENTRY_SELECTOR = [\n \"textarea\",\n \"select\",\n \"[contenteditable]:not([contenteditable='false'])\",\n \"input:not([type='checkbox']):not([type='radio']):not([type='button'])\" +\n \":not([type='submit']):not([type='reset']):not([type='image'])\" +\n \":not([type='range']):not([type='file']):not([type='color'])\",\n].join(\", \");\n\nexport namespace useThreadViewportAutoScroll {\n export type Options = {\n /**\n * Whether to automatically scroll to the bottom when new messages are added.\n * When enabled, the viewport will automatically scroll to show the latest content.\n *\n * Default false if `turnAnchor` is \"top\", otherwise defaults to true.\n */\n autoScroll?: boolean | undefined;\n\n /**\n * Whether to scroll to bottom when a new run starts.\n *\n * Defaults to true.\n */\n scrollToBottomOnRunStart?: boolean | undefined;\n\n /**\n * Whether to scroll to bottom when messages first appear in the thread.\n *\n * Defaults to true.\n */\n scrollToBottomOnInitialize?: boolean | undefined;\n\n /**\n * Whether to scroll to bottom when switching to a different thread.\n *\n * Defaults to true.\n */\n scrollToBottomOnThreadSwitch?: boolean | undefined;\n };\n}\n\nexport const useThreadViewportAutoScroll = <TElement extends HTMLElement>({\n autoScroll,\n scrollToBottomOnRunStart = true,\n scrollToBottomOnInitialize = true,\n scrollToBottomOnThreadSwitch = true,\n}: useThreadViewportAutoScroll.Options): RefCallback<TElement> => {\n const divRef = useRef<TElement>(null);\n const hasMessages = useAuiState((s) => s.thread.messages.length > 0);\n const isRunning = useAuiState((s) => s.thread.isRunning);\n const initializeScrollRequestedRef = useRef(false);\n const scheduledFrameRef = useRef<number | null>(null);\n\n const threadViewportStore = useThreadViewportStore();\n if (autoScroll === undefined) {\n autoScroll = threadViewportStore.getState().turnAnchor !== \"top\";\n }\n\n const lastScrollTop = useRef<number>(0);\n const lastScrollHeight = useRef<number>(0);\n const lastObservedScrollHeight = useRef<number>(0);\n const lastObservedClientHeight = useRef<number>(0);\n\n // Pending bottom-scroll intent. Planted by initialize/run-start/switch/button\n // triggers, cleared when handleScroll confirms we reached bottom, or when the\n // user actively scrolls up while content size is stable.\n const scrollingToBottomBehaviorRef = useRef<ScrollBehavior | null>(null);\n const followBottomRef = useRef(autoScroll);\n const previousAutoScrollRef = useRef(autoScroll);\n\n useLayoutEffect(() => {\n const previousAutoScroll = previousAutoScrollRef.current;\n previousAutoScrollRef.current = autoScroll;\n if (previousAutoScroll || !autoScroll) return;\n\n const div = divRef.current;\n followBottomRef.current = div !== null && isViewportAtBottom(div);\n }, [autoScroll]);\n\n const scrollToBottom = useCallback((behavior: ScrollBehavior) => {\n const div = divRef.current;\n if (!div) return;\n\n followBottomRef.current = true;\n scrollingToBottomBehaviorRef.current = behavior;\n div.scrollTo({ top: div.scrollHeight, behavior });\n }, []);\n\n const cancelScheduledFrame = useCallback(() => {\n if (scheduledFrameRef.current === null) return;\n cancelAnimationFrame(scheduledFrameRef.current);\n scheduledFrameRef.current = null;\n }, []);\n\n const scheduleScrollToBottom = useCallback(\n (behavior: ScrollBehavior) => {\n scrollingToBottomBehaviorRef.current = behavior;\n cancelScheduledFrame();\n scheduledFrameRef.current = requestAnimationFrame(() => {\n scheduledFrameRef.current = null;\n scrollToBottom(behavior);\n });\n },\n [cancelScheduledFrame, scrollToBottom],\n );\n\n useLayoutEffect(() => () => cancelScheduledFrame(), [cancelScheduledFrame]);\n\n const hasActiveTopAnchor = useCallback(() => {\n const state = threadViewportStore.getState();\n return (\n state.turnAnchor === \"top\" &&\n state.element.viewport === divRef.current &&\n state.element.anchor !== null\n );\n }, [threadViewportStore]);\n\n const handleScroll = () => {\n const div = divRef.current;\n if (!div) return;\n\n const isAtBottom = threadViewportStore.getState().isAtBottom;\n const newIsAtBottom = isViewportAtBottom(div);\n\n const isInFlightDownwardScroll =\n !newIsAtBottom && lastScrollTop.current < div.scrollTop;\n if (isInFlightDownwardScroll) {\n // no-op: a smooth scroll-to-bottom fires many midpoint scroll events\n // before landing, don't flicker isAtBottom or clear intent mid-animation\n } else {\n const userScrolledUp = isUserScrollUp(\n {\n scrollTop: lastScrollTop.current,\n scrollHeight: lastScrollHeight.current,\n },\n div,\n );\n\n if (newIsAtBottom) {\n // newIsAtBottom is ambiguous when the viewport doesn't overflow —\n // keep intent alive until content can actually scroll\n if (viewportOverflows(div)) {\n scrollingToBottomBehaviorRef.current = null;\n }\n if (autoScroll) followBottomRef.current = true;\n } else if (userScrolledUp) {\n cancelScheduledFrame();\n scrollingToBottomBehaviorRef.current = null;\n followBottomRef.current = false;\n }\n\n const shouldUpdate =\n newIsAtBottom || scrollingToBottomBehaviorRef.current === null;\n\n if (shouldUpdate && newIsAtBottom !== isAtBottom) {\n writableStore(threadViewportStore).setState({\n isAtBottom: newIsAtBottom,\n });\n }\n }\n\n lastScrollTop.current = div.scrollTop;\n lastScrollHeight.current = div.scrollHeight;\n };\n\n const resizeRef = useOnResizeContent(() => {\n const div = divRef.current;\n if (!div) return;\n\n const { scrollHeight, clientHeight } = div;\n if (\n scrollHeight === lastObservedScrollHeight.current &&\n clientHeight === lastObservedClientHeight.current\n ) {\n return;\n }\n lastObservedScrollHeight.current = scrollHeight;\n lastObservedClientHeight.current = clientHeight;\n\n const scrollBehavior = scrollingToBottomBehaviorRef.current;\n if (scrollBehavior && hasActiveTopAnchor()) {\n // Let the top-anchor reserve own scrolling while a run starts to avoid a bottom-scroll race.\n scrollingToBottomBehaviorRef.current = null;\n } else if (scrollBehavior) {\n scrollToBottom(scrollBehavior);\n } else if (\n autoScroll &&\n !(isRunning && hasActiveTopAnchor()) &&\n followBottomRef.current\n ) {\n scrollToBottom(\"instant\");\n }\n\n handleScroll();\n });\n\n const scrollRef = useManagedRef<HTMLElement>((el) => {\n // A user gesture invalidates pending bottom-scroll intent; otherwise an\n // intent kept alive by a non-overflowing thread (see handleScroll) hijacks\n // the next content growth, e.g. expanding a collapsible tool call. Keyboard\n // activation reaches that same content without ever emitting a pointer\n // event, so it has to cancel the intent too.\n const cancelPendingScrollToBottom = () => {\n // A scheduled frame re-plants the intent when it runs, so clearing the\n // ref alone leaves the gesture undone.\n cancelScheduledFrame();\n scrollingToBottomBehaviorRef.current = null;\n };\n // The composer renders inside the viewport, so its keystrokes bubble here;\n // only an activation key aimed at something other than a text field is a\n // gesture on thread content.\n const cancelOnKeyDown = (event: KeyboardEvent) => {\n if (!ACTIVATION_KEYS.has(event.key)) return;\n const target = event.target as Element | null;\n if (target?.closest?.(TEXT_ENTRY_SELECTOR)) return;\n cancelPendingScrollToBottom();\n };\n el.addEventListener(\"scroll\", handleScroll);\n el.addEventListener(\"pointerdown\", cancelPendingScrollToBottom);\n el.addEventListener(\"keydown\", cancelOnKeyDown);\n return () => {\n el.removeEventListener(\"scroll\", handleScroll);\n el.removeEventListener(\"pointerdown\", cancelPendingScrollToBottom);\n el.removeEventListener(\"keydown\", cancelOnKeyDown);\n };\n });\n\n useLayoutEffect(() => {\n if (!scrollToBottomOnInitialize) return;\n if (!hasMessages) {\n initializeScrollRequestedRef.current = false;\n return;\n }\n if (initializeScrollRequestedRef.current) return;\n\n initializeScrollRequestedRef.current = true;\n // defer to an in-flight run (e.g. first message on a new thread) that\n // already planted intent — otherwise we'd downgrade its \"auto\" to \"instant\"\n if (scrollingToBottomBehaviorRef.current !== null) return;\n scheduleScrollToBottom(\"instant\");\n }, [hasMessages, scheduleScrollToBottom, scrollToBottomOnInitialize]);\n\n useOnScrollToBottom(({ behavior }) => {\n scrollToBottom(behavior);\n });\n\n useAuiEvent(\"thread.runStart\", () => {\n if (!scrollToBottomOnRunStart) return;\n if (threadViewportStore.getState().turnAnchor === \"top\") return;\n scheduleScrollToBottom(\"auto\");\n });\n\n useAuiEvent(\"threads.selectionChanged\", () => {\n if (!scrollToBottomOnThreadSwitch) return;\n scheduleScrollToBottom(\"instant\");\n });\n\n const autoScrollRef = useComposedRefs<TElement>(resizeRef, scrollRef, divRef);\n return autoScrollRef as RefCallback<TElement>;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useThreadViewportAutoScroll.js","names":["useComposedRefs","useCallback","useLayoutEffect","useRef","RefCallback","useAui","useAuiEvent","useAuiState","isUserScrollUp","isViewportAtBottom","viewportOverflows","useOnResizeContent","useOnScrollToBottom","useManagedRef","writableStore","useThreadViewportStore","ACTIVATION_KEYS","Set","TEXT_ENTRY_SELECTOR","join","MESSAGE_SELECTOR","MessageOffset","id","offset","offsetInViewport","element","Element","viewport","getBoundingClientRect","top","measureFirstMessage","HTMLElement","querySelector","dataset","measureReaderMessage","bottom","spanning","querySelectorAll","rect","findMessage","keepMessageAt","anchor","heldScrollTop","current","delta","scrollTop","Math","abs","scrollTo","behavior","useThreadViewportAutoScroll","Options","autoScroll","scrollToBottomOnRunStart","scrollToBottomOnInitialize","scrollToBottomOnThreadSwitch","TElement","aui","divRef","hasMessages","s","thread","messages","length","firstMessageId","threadId","threadListItem","isRunning","initializeScrollRequestedRef","scheduledFrameRef","threadViewportStore","undefined","getState","turnAnchor","lastScrollTop","lastScrollHeight","lastObservedScrollHeight","lastObservedClientHeight","firstMessageRef","prependAnchorRef","heldScrollTopRef","scrollingToBottomBehaviorRef","ScrollBehavior","followBottomRef","previousAutoScrollRef","previousAutoScroll","div","scrollToBottom","scrollHeight","cancelScheduledFrame","cancelAnimationFrame","scheduleScrollToBottom","requestAnimationFrame","hasActiveTopAnchor","state","handleScroll","isAtBottom","newIsAtBottom","isInFlightDownwardScroll","userScrolledUp","shouldUpdate","setState","resizeRef","clientHeight","scrollBehavior","scrollRef","el","cancelPendingScrollToBottom","cancelOnKeyDown","event","KeyboardEvent","has","key","target","closest","releasePrependAnchor","gestures","const","addEventListener","gesture","passive","removeEventListener","previousRef","previous","previousFirstMessageId","prepended","some","message","index","autoScrollRef"],"sources":["../../../src/primitives/thread/useThreadViewportAutoScroll.ts"],"sourcesContent":["\"use client\";\n\nimport { useComposedRefs } from \"radix-ui/internal\";\nimport { useCallback, useLayoutEffect, useRef, type RefCallback } from \"react\";\nimport { useAui, useAuiEvent, useAuiState } from \"@assistant-ui/store\";\nimport {\n isUserScrollUp,\n isViewportAtBottom,\n viewportOverflows,\n} from \"@assistant-ui/store/client\";\nimport { useOnResizeContent } from \"../../utils/hooks/useOnResizeContent\";\nimport { useOnScrollToBottom } from \"../../utils/hooks/useOnScrollToBottom\";\nimport { useManagedRef } from \"../../utils/hooks/useManagedRef\";\nimport { writableStore } from \"../../context/ReadonlyStore\";\nimport { useThreadViewportStore } from \"../../context/react/ThreadViewportContext\";\n\n// Enter and Space activate a focused control, which is how a collapsible tool\n// call expands without a pointer event ever firing. No other key changes thread\n// content: the keys that scroll the viewport reach handleScroll, which already\n// clears the intent when the user scrolls up.\nconst ACTIVATION_KEYS = new Set([\"Enter\", \" \"]);\n\n// A control that consumes the activation key itself instead of acting on thread\n// content. `contenteditable=\"false\"` marks a non-editable island inside an\n// editable tree, so it is excluded the way ComposerRoot already excludes it;\n// the input types left out are the ones a key activates rather than fills.\nconst TEXT_ENTRY_SELECTOR = [\n \"textarea\",\n \"select\",\n \"[contenteditable]:not([contenteditable='false'])\",\n \"input:not([type='checkbox']):not([type='radio']):not([type='button'])\" +\n \":not([type='submit']):not([type='reset']):not([type='image'])\" +\n \":not([type='range']):not([type='file']):not([type='color'])\",\n].join(\", \");\n\nconst MESSAGE_SELECTOR = \"[data-message-id]\";\n\ntype MessageOffset = { readonly id: string; readonly offset: number };\n\nconst offsetInViewport = (element: Element, viewport: Element) =>\n element.getBoundingClientRect().top - viewport.getBoundingClientRect().top;\n\nconst measureFirstMessage = (viewport: HTMLElement): MessageOffset | null => {\n const element = viewport.querySelector<HTMLElement>(MESSAGE_SELECTOR);\n const id = element?.dataset[\"messageId\"];\n return element && id\n ? { id, offset: offsetInViewport(element, viewport) }\n : null;\n};\n\n/** The first message that starts in view, else the one spanning its top edge. */\nconst measureReaderMessage = (viewport: HTMLElement): MessageOffset | null => {\n const { top, bottom } = viewport.getBoundingClientRect();\n let spanning: MessageOffset | null = null;\n for (const element of viewport.querySelectorAll<HTMLElement>(\n MESSAGE_SELECTOR,\n )) {\n const rect = element.getBoundingClientRect();\n if (rect.bottom <= top) continue;\n if (rect.top >= bottom) break;\n const id = element.dataset[\"messageId\"];\n if (!id) continue;\n if (rect.top >= top) return { id, offset: rect.top - top };\n spanning ??= { id, offset: rect.top - top };\n }\n return spanning;\n};\n\nconst findMessage = (viewport: HTMLElement, id: string) => {\n for (const element of viewport.querySelectorAll<HTMLElement>(\n MESSAGE_SELECTOR,\n )) {\n if (element.dataset[\"messageId\"] === id) return element;\n }\n return null;\n};\n\n/**\n * Scrolls so the message sits at `anchor.offset` again, recording the\n * position it scrolls to in `heldScrollTop` before the scroll event can\n * report it; false when the message is gone.\n */\nconst keepMessageAt = (\n viewport: HTMLElement,\n anchor: MessageOffset,\n heldScrollTop: { current: number },\n) => {\n const element = findMessage(viewport, anchor.id);\n if (!element) return false;\n const delta = offsetInViewport(element, viewport) - anchor.offset;\n heldScrollTop.current = viewport.scrollTop + delta;\n if (Math.abs(delta) >= 1) {\n viewport.scrollTo({ top: heldScrollTop.current, behavior: \"instant\" });\n }\n heldScrollTop.current = viewport.scrollTop;\n return true;\n};\n\nexport namespace useThreadViewportAutoScroll {\n export type Options = {\n /**\n * Whether to automatically scroll to the bottom when new messages are added.\n * When enabled, the viewport will automatically scroll to show the latest content.\n *\n * Default false if `turnAnchor` is \"top\", otherwise defaults to true.\n */\n autoScroll?: boolean | undefined;\n\n /**\n * Whether to scroll to bottom when a new run starts.\n *\n * Defaults to true.\n */\n scrollToBottomOnRunStart?: boolean | undefined;\n\n /**\n * Whether to scroll to bottom when messages first appear in the thread.\n *\n * Defaults to true.\n */\n scrollToBottomOnInitialize?: boolean | undefined;\n\n /**\n * Whether to scroll to bottom when switching to a different thread.\n *\n * Defaults to true.\n */\n scrollToBottomOnThreadSwitch?: boolean | undefined;\n };\n}\n\nexport const useThreadViewportAutoScroll = <TElement extends HTMLElement>({\n autoScroll,\n scrollToBottomOnRunStart = true,\n scrollToBottomOnInitialize = true,\n scrollToBottomOnThreadSwitch = true,\n}: useThreadViewportAutoScroll.Options): RefCallback<TElement> => {\n const aui = useAui();\n const divRef = useRef<TElement>(null);\n const hasMessages = useAuiState((s) => s.thread.messages.length > 0);\n const firstMessageId = useAuiState((s) => s.thread.messages[0]?.id);\n const threadId = useAuiState((s) => s.threadListItem.id);\n const isRunning = useAuiState((s) => s.thread.isRunning);\n const initializeScrollRequestedRef = useRef(false);\n const scheduledFrameRef = useRef<number | null>(null);\n\n const threadViewportStore = useThreadViewportStore();\n if (autoScroll === undefined) {\n autoScroll = threadViewportStore.getState().turnAnchor !== \"top\";\n }\n\n const lastScrollTop = useRef<number>(0);\n const lastScrollHeight = useRef<number>(0);\n const lastObservedScrollHeight = useRef<number>(0);\n const lastObservedClientHeight = useRef<number>(0);\n const firstMessageRef = useRef<MessageOffset | null>(null);\n // The row the reader sees when earlier messages land, held until a gesture\n // or a scroll this hook did not make, so content above it that settles\n // afterwards (lazy layout, late media) does not move it on engines without\n // native scroll anchoring.\n const prependAnchorRef = useRef<MessageOffset | null>(null);\n const heldScrollTopRef = useRef(0);\n\n // Pending bottom-scroll intent. Planted by initialize/run-start/switch/button\n // triggers, cleared when handleScroll confirms we reached bottom, or when the\n // user actively scrolls up while content size is stable.\n const scrollingToBottomBehaviorRef = useRef<ScrollBehavior | null>(null);\n const followBottomRef = useRef(autoScroll);\n const previousAutoScrollRef = useRef(autoScroll);\n\n useLayoutEffect(() => {\n const previousAutoScroll = previousAutoScrollRef.current;\n previousAutoScrollRef.current = autoScroll;\n if (previousAutoScroll || !autoScroll) return;\n\n const div = divRef.current;\n followBottomRef.current = div !== null && isViewportAtBottom(div);\n }, [autoScroll]);\n\n const scrollToBottom = useCallback((behavior: ScrollBehavior) => {\n const div = divRef.current;\n if (!div) return;\n\n followBottomRef.current = true;\n scrollingToBottomBehaviorRef.current = behavior;\n prependAnchorRef.current = null;\n div.scrollTo({ top: div.scrollHeight, behavior });\n }, []);\n\n const cancelScheduledFrame = useCallback(() => {\n if (scheduledFrameRef.current === null) return;\n cancelAnimationFrame(scheduledFrameRef.current);\n scheduledFrameRef.current = null;\n }, []);\n\n const scheduleScrollToBottom = useCallback(\n (behavior: ScrollBehavior) => {\n scrollingToBottomBehaviorRef.current = behavior;\n cancelScheduledFrame();\n scheduledFrameRef.current = requestAnimationFrame(() => {\n scheduledFrameRef.current = null;\n scrollToBottom(behavior);\n });\n },\n [cancelScheduledFrame, scrollToBottom],\n );\n\n useLayoutEffect(() => () => cancelScheduledFrame(), [cancelScheduledFrame]);\n\n const hasActiveTopAnchor = useCallback(() => {\n const state = threadViewportStore.getState();\n return (\n state.turnAnchor === \"top\" &&\n state.element.viewport === divRef.current &&\n state.element.anchor !== null\n );\n }, [threadViewportStore]);\n\n const handleScroll = () => {\n const div = divRef.current;\n if (!div) return;\n\n if (\n prependAnchorRef.current &&\n Math.abs(div.scrollTop - heldScrollTopRef.current) >= 1\n ) {\n prependAnchorRef.current = null;\n }\n\n const isAtBottom = threadViewportStore.getState().isAtBottom;\n const newIsAtBottom = isViewportAtBottom(div);\n\n const isInFlightDownwardScroll =\n !newIsAtBottom && lastScrollTop.current < div.scrollTop;\n if (isInFlightDownwardScroll) {\n // no-op: a smooth scroll-to-bottom fires many midpoint scroll events\n // before landing, don't flicker isAtBottom or clear intent mid-animation\n } else {\n const userScrolledUp = isUserScrollUp(\n {\n scrollTop: lastScrollTop.current,\n scrollHeight: lastScrollHeight.current,\n },\n div,\n );\n\n if (newIsAtBottom) {\n // newIsAtBottom is ambiguous when the viewport doesn't overflow —\n // keep intent alive until content can actually scroll\n if (viewportOverflows(div)) {\n scrollingToBottomBehaviorRef.current = null;\n }\n if (autoScroll) followBottomRef.current = true;\n } else if (userScrolledUp) {\n cancelScheduledFrame();\n scrollingToBottomBehaviorRef.current = null;\n followBottomRef.current = false;\n }\n\n const shouldUpdate =\n newIsAtBottom || scrollingToBottomBehaviorRef.current === null;\n\n if (shouldUpdate && newIsAtBottom !== isAtBottom) {\n writableStore(threadViewportStore).setState({\n isAtBottom: newIsAtBottom,\n });\n }\n }\n\n lastScrollTop.current = div.scrollTop;\n lastScrollHeight.current = div.scrollHeight;\n firstMessageRef.current = measureFirstMessage(div);\n };\n\n const resizeRef = useOnResizeContent(() => {\n const div = divRef.current;\n if (!div) return;\n\n const { scrollHeight, clientHeight } = div;\n if (\n scrollHeight === lastObservedScrollHeight.current &&\n clientHeight === lastObservedClientHeight.current\n ) {\n return;\n }\n lastObservedScrollHeight.current = scrollHeight;\n lastObservedClientHeight.current = clientHeight;\n\n const scrollBehavior = scrollingToBottomBehaviorRef.current;\n if (scrollBehavior && hasActiveTopAnchor()) {\n // Let the top-anchor reserve own scrolling while a run starts to avoid a bottom-scroll race.\n scrollingToBottomBehaviorRef.current = null;\n } else if (scrollBehavior) {\n scrollToBottom(scrollBehavior);\n } else if (\n autoScroll &&\n !(isRunning && hasActiveTopAnchor()) &&\n followBottomRef.current\n ) {\n scrollToBottom(\"instant\");\n } else if (\n prependAnchorRef.current &&\n !keepMessageAt(div, prependAnchorRef.current, heldScrollTopRef)\n ) {\n prependAnchorRef.current = null;\n }\n\n handleScroll();\n });\n\n const scrollRef = useManagedRef<HTMLElement>((el) => {\n // A user gesture invalidates pending bottom-scroll intent; otherwise an\n // intent kept alive by a non-overflowing thread (see handleScroll) hijacks\n // the next content growth, e.g. expanding a collapsible tool call. Keyboard\n // activation reaches that same content without ever emitting a pointer\n // event, so it has to cancel the intent too.\n const cancelPendingScrollToBottom = () => {\n // A scheduled frame re-plants the intent when it runs, so clearing the\n // ref alone leaves the gesture undone.\n cancelScheduledFrame();\n scrollingToBottomBehaviorRef.current = null;\n };\n // The composer renders inside the viewport, so its keystrokes bubble here;\n // only an activation key aimed at something other than a text field is a\n // gesture on thread content.\n const cancelOnKeyDown = (event: KeyboardEvent) => {\n if (!ACTIVATION_KEYS.has(event.key)) return;\n const target = event.target as Element | null;\n if (target?.closest?.(TEXT_ENTRY_SELECTOR)) return;\n cancelPendingScrollToBottom();\n };\n const releasePrependAnchor = () => {\n prependAnchorRef.current = null;\n };\n const gestures = [\"pointerdown\", \"wheel\", \"touchstart\", \"keydown\"] as const;\n el.addEventListener(\"scroll\", handleScroll);\n el.addEventListener(\"pointerdown\", cancelPendingScrollToBottom);\n el.addEventListener(\"keydown\", cancelOnKeyDown);\n for (const gesture of gestures) {\n el.addEventListener(gesture, releasePrependAnchor, { passive: true });\n }\n return () => {\n el.removeEventListener(\"scroll\", handleScroll);\n el.removeEventListener(\"pointerdown\", cancelPendingScrollToBottom);\n el.removeEventListener(\"keydown\", cancelOnKeyDown);\n for (const gesture of gestures) {\n el.removeEventListener(gesture, releasePrependAnchor);\n }\n };\n });\n\n // Earlier messages prepended above the reader put the old first message back\n // where the reader saw it, so growth below it in the same commit (a\n // streaming reply) is not mistaken for the page; where the browser already\n // anchored the scroll, this is a no-op.\n const previousRef = useRef({ threadId, firstMessageId });\n useLayoutEffect(() => {\n const previous = previousRef.current;\n previousRef.current = { threadId, firstMessageId };\n if (previous.threadId !== threadId) {\n prependAnchorRef.current = null;\n return;\n }\n const div = divRef.current;\n const previousFirstMessageId = previous.firstMessageId;\n if (\n !div ||\n previousFirstMessageId === undefined ||\n previousFirstMessageId === firstMessageId ||\n (autoScroll && followBottomRef.current) ||\n scrollingToBottomBehaviorRef.current !== null\n )\n return;\n const prepended = aui.thread\n .getState()\n .messages.some(\n (message, index) => index > 0 && message.id === previousFirstMessageId,\n );\n if (!prepended) return;\n\n const anchor = firstMessageRef.current;\n if (\n anchor?.id === previousFirstMessageId &&\n keepMessageAt(div, anchor, heldScrollTopRef)\n ) {\n prependAnchorRef.current = measureReaderMessage(div) ?? anchor;\n } else {\n // Message roots that render no `data-message-id` leave only the total\n // growth to go on, which counts any growth below the reader too.\n const top =\n div.scrollHeight - (lastScrollHeight.current - lastScrollTop.current);\n if (Math.abs(div.scrollTop - top) >= 1) {\n div.scrollTo({ top, behavior: \"instant\" });\n }\n }\n lastScrollTop.current = div.scrollTop;\n lastScrollHeight.current = div.scrollHeight;\n }, [aui, autoScroll, firstMessageId, threadId]);\n\n useLayoutEffect(() => {\n if (!scrollToBottomOnInitialize) return;\n if (!hasMessages) {\n initializeScrollRequestedRef.current = false;\n return;\n }\n if (initializeScrollRequestedRef.current) return;\n\n initializeScrollRequestedRef.current = true;\n // defer to an in-flight run (e.g. first message on a new thread) that\n // already planted intent — otherwise we'd downgrade its \"auto\" to \"instant\"\n if (scrollingToBottomBehaviorRef.current !== null) return;\n scheduleScrollToBottom(\"instant\");\n }, [hasMessages, scheduleScrollToBottom, scrollToBottomOnInitialize]);\n\n useOnScrollToBottom(({ behavior }) => {\n scrollToBottom(behavior);\n });\n\n useAuiEvent(\"thread.runStart\", () => {\n prependAnchorRef.current = null;\n if (!scrollToBottomOnRunStart) return;\n if (threadViewportStore.getState().turnAnchor === \"top\") return;\n scheduleScrollToBottom(\"auto\");\n });\n\n useAuiEvent(\"threads.selectionChanged\", () => {\n if (!scrollToBottomOnThreadSwitch) return;\n scheduleScrollToBottom(\"instant\");\n });\n\n const autoScrollRef = useComposedRefs<TElement>(resizeRef, scrollRef, divRef);\n return autoScrollRef as RefCallback<TElement>;\n};\n"],"mappings":"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"}
|
|
@@ -7,7 +7,9 @@ export { ThreadPrimitiveViewportFooter as ViewportFooter } from "./thread/Thread
|
|
|
7
7
|
export { ThreadPrimitiveMessages as Messages } from "./thread/ThreadMessages.js";
|
|
8
8
|
export { ThreadPrimitiveMessageByIndex as MessageByIndex } from "./thread/ThreadMessages.js";
|
|
9
9
|
export { ThreadPrimitiveUnstable_MessageById as Unstable_MessageById } from "./thread/ThreadMessages.js";
|
|
10
|
+
export { ThreadPrimitiveRow as Row } from "./thread/ThreadMessages.js";
|
|
10
11
|
export { ThreadPrimitiveScrollToBottom as ScrollToBottom } from "./thread/ThreadScrollToBottom.js";
|
|
12
|
+
export { ThreadPrimitiveLoadEarlier as LoadEarlier } from "./thread/ThreadLoadEarlier.js";
|
|
11
13
|
export { ThreadPrimitiveSuggestion as Suggestion } from "./thread/ThreadSuggestion.js";
|
|
12
14
|
export { ThreadPrimitiveSuggestions as Suggestions, ThreadPrimitiveSuggestionByIndex as SuggestionByIndex, } from "./thread/ThreadSuggestions.js";
|
|
13
15
|
//# sourceMappingURL=thread.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thread.d.ts","sourceRoot":"","sources":["../../src/primitives/thread.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,IAAI,IAAI,EAAE,+BAA4B;AAClE,OAAO,EAAE,oBAAoB,IAAI,KAAK,EAAE,gCAA6B;AACrE,OAAO,EAAE,iBAAiB,IAAI,EAAE,EAAE,6BAA0B;AAC5D,OAAO,EAAE,uBAAuB,IAAI,QAAQ,EAAE,mCAAgC;AAC9E,OAAO,EAAE,+BAA+B,IAAI,gBAAgB,EAAE,uDAAoD;AAClH,OAAO,EAAE,6BAA6B,IAAI,cAAc,EAAE,yCAAsC;AAChG,OAAO,EAAE,uBAAuB,IAAI,QAAQ,EAAE,mCAAgC;AAC9E,OAAO,EAAE,6BAA6B,IAAI,cAAc,EAAE,mCAAgC;AAC1F,OAAO,EAAE,mCAAmC,IAAI,oBAAoB,EAAE,mCAAgC;AACtG,OAAO,EAAE,6BAA6B,IAAI,cAAc,EAAE,yCAAsC;AAChG,OAAO,EAAE,yBAAyB,IAAI,UAAU,EAAE,qCAAkC;AACpF,OAAO,EACL,0BAA0B,IAAI,WAAW,EACzC,gCAAgC,IAAI,iBAAiB,GACtD,sCAAmC"}
|
|
1
|
+
{"version":3,"file":"thread.d.ts","sourceRoot":"","sources":["../../src/primitives/thread.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,IAAI,IAAI,EAAE,+BAA4B;AAClE,OAAO,EAAE,oBAAoB,IAAI,KAAK,EAAE,gCAA6B;AACrE,OAAO,EAAE,iBAAiB,IAAI,EAAE,EAAE,6BAA0B;AAC5D,OAAO,EAAE,uBAAuB,IAAI,QAAQ,EAAE,mCAAgC;AAC9E,OAAO,EAAE,+BAA+B,IAAI,gBAAgB,EAAE,uDAAoD;AAClH,OAAO,EAAE,6BAA6B,IAAI,cAAc,EAAE,yCAAsC;AAChG,OAAO,EAAE,uBAAuB,IAAI,QAAQ,EAAE,mCAAgC;AAC9E,OAAO,EAAE,6BAA6B,IAAI,cAAc,EAAE,mCAAgC;AAC1F,OAAO,EAAE,mCAAmC,IAAI,oBAAoB,EAAE,mCAAgC;AACtG,OAAO,EAAE,kBAAkB,IAAI,GAAG,EAAE,mCAAgC;AACpE,OAAO,EAAE,6BAA6B,IAAI,cAAc,EAAE,yCAAsC;AAChG,OAAO,EAAE,0BAA0B,IAAI,WAAW,EAAE,sCAAmC;AACvF,OAAO,EAAE,yBAAyB,IAAI,UAAU,EAAE,qCAAkC;AACpF,OAAO,EACL,0BAA0B,IAAI,WAAW,EACzC,gCAAgC,IAAI,iBAAiB,GACtD,sCAAmC"}
|
|
@@ -5,17 +5,20 @@ import { ThreadPrimitiveEmpty } from "./thread/ThreadEmpty.js";
|
|
|
5
5
|
import { ThreadPrimitiveIf } from "./thread/ThreadIf.js";
|
|
6
6
|
import { ThreadPrimitiveViewport } from "./thread/ThreadViewport.js";
|
|
7
7
|
import { ThreadPrimitiveViewportFooter } from "./thread/ThreadViewportFooter.js";
|
|
8
|
-
import { ThreadPrimitiveMessageByIndex, ThreadPrimitiveMessages, ThreadPrimitiveUnstable_MessageById } from "./thread/ThreadMessages.js";
|
|
8
|
+
import { ThreadPrimitiveMessageByIndex, ThreadPrimitiveMessages, ThreadPrimitiveRow, ThreadPrimitiveUnstable_MessageById } from "./thread/ThreadMessages.js";
|
|
9
9
|
import { ThreadPrimitiveScrollToBottom } from "./thread/ThreadScrollToBottom.js";
|
|
10
|
+
import { ThreadPrimitiveLoadEarlier } from "./thread/ThreadLoadEarlier.js";
|
|
10
11
|
import { ThreadPrimitiveSuggestion } from "./thread/ThreadSuggestion.js";
|
|
11
12
|
import { ThreadPrimitiveSuggestionByIndex, ThreadPrimitiveSuggestions } from "./thread/ThreadSuggestions.js";
|
|
12
13
|
//#region src/primitives/thread.ts
|
|
13
14
|
var thread_exports = /* @__PURE__ */ __exportAll({
|
|
14
15
|
Empty: () => ThreadPrimitiveEmpty,
|
|
15
16
|
If: () => ThreadPrimitiveIf,
|
|
17
|
+
LoadEarlier: () => ThreadPrimitiveLoadEarlier,
|
|
16
18
|
MessageByIndex: () => ThreadPrimitiveMessageByIndex,
|
|
17
19
|
Messages: () => ThreadPrimitiveMessages,
|
|
18
20
|
Root: () => ThreadPrimitiveRoot,
|
|
21
|
+
Row: () => ThreadPrimitiveRow,
|
|
19
22
|
ScrollToBottom: () => ThreadPrimitiveScrollToBottom,
|
|
20
23
|
Suggestion: () => ThreadPrimitiveSuggestion,
|
|
21
24
|
SuggestionByIndex: () => ThreadPrimitiveSuggestionByIndex,
|
|
@@ -26,6 +29,6 @@ var thread_exports = /* @__PURE__ */ __exportAll({
|
|
|
26
29
|
ViewportProvider: () => ThreadPrimitiveViewportProvider
|
|
27
30
|
});
|
|
28
31
|
//#endregion
|
|
29
|
-
export { ThreadPrimitiveEmpty as Empty, ThreadPrimitiveIf as If, ThreadPrimitiveMessageByIndex as MessageByIndex, ThreadPrimitiveMessages as Messages, ThreadPrimitiveRoot as Root, ThreadPrimitiveScrollToBottom as ScrollToBottom, ThreadPrimitiveSuggestion as Suggestion, ThreadPrimitiveSuggestionByIndex as SuggestionByIndex, ThreadPrimitiveSuggestions as Suggestions, ThreadPrimitiveUnstable_MessageById as Unstable_MessageById, ThreadPrimitiveViewport as Viewport, ThreadPrimitiveViewportFooter as ViewportFooter, ThreadPrimitiveViewportProvider as ViewportProvider, thread_exports };
|
|
32
|
+
export { ThreadPrimitiveEmpty as Empty, ThreadPrimitiveIf as If, ThreadPrimitiveLoadEarlier as LoadEarlier, ThreadPrimitiveMessageByIndex as MessageByIndex, ThreadPrimitiveMessages as Messages, ThreadPrimitiveRoot as Root, ThreadPrimitiveRow as Row, ThreadPrimitiveScrollToBottom as ScrollToBottom, ThreadPrimitiveSuggestion as Suggestion, ThreadPrimitiveSuggestionByIndex as SuggestionByIndex, ThreadPrimitiveSuggestions as Suggestions, ThreadPrimitiveUnstable_MessageById as Unstable_MessageById, ThreadPrimitiveViewport as Viewport, ThreadPrimitiveViewportFooter as ViewportFooter, ThreadPrimitiveViewportProvider as ViewportProvider, thread_exports };
|
|
30
33
|
|
|
31
34
|
//# sourceMappingURL=thread.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thread.js","names":["ThreadPrimitiveRoot","Root","ThreadPrimitiveEmpty","Empty","ThreadPrimitiveIf","If","ThreadPrimitiveViewport","Viewport","ThreadPrimitiveViewportProvider","ViewportProvider","ThreadPrimitiveViewportFooter","ViewportFooter","ThreadPrimitiveMessages","Messages","ThreadPrimitiveMessageByIndex","MessageByIndex","ThreadPrimitiveUnstable_MessageById","Unstable_MessageById","ThreadPrimitiveScrollToBottom","ScrollToBottom","ThreadPrimitiveSuggestion","Suggestion","ThreadPrimitiveSuggestions","Suggestions","ThreadPrimitiveSuggestionByIndex","SuggestionByIndex"],"sources":["../../src/primitives/thread.ts"],"sourcesContent":["export { ThreadPrimitiveRoot as Root } from \"./thread/ThreadRoot\";\nexport { ThreadPrimitiveEmpty as Empty } from \"./thread/ThreadEmpty\";\nexport { ThreadPrimitiveIf as If } from \"./thread/ThreadIf\";\nexport { ThreadPrimitiveViewport as Viewport } from \"./thread/ThreadViewport\";\nexport { ThreadPrimitiveViewportProvider as ViewportProvider } from \"../context/providers/ThreadViewportProvider\";\nexport { ThreadPrimitiveViewportFooter as ViewportFooter } from \"./thread/ThreadViewportFooter\";\nexport { ThreadPrimitiveMessages as Messages } from \"./thread/ThreadMessages\";\nexport { ThreadPrimitiveMessageByIndex as MessageByIndex } from \"./thread/ThreadMessages\";\nexport { ThreadPrimitiveUnstable_MessageById as Unstable_MessageById } from \"./thread/ThreadMessages\";\nexport { ThreadPrimitiveScrollToBottom as ScrollToBottom } from \"./thread/ThreadScrollToBottom\";\nexport { ThreadPrimitiveSuggestion as Suggestion } from \"./thread/ThreadSuggestion\";\nexport {\n ThreadPrimitiveSuggestions as Suggestions,\n ThreadPrimitiveSuggestionByIndex as SuggestionByIndex,\n} from \"./thread/ThreadSuggestions\";\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"thread.js","names":["ThreadPrimitiveRoot","Root","ThreadPrimitiveEmpty","Empty","ThreadPrimitiveIf","If","ThreadPrimitiveViewport","Viewport","ThreadPrimitiveViewportProvider","ViewportProvider","ThreadPrimitiveViewportFooter","ViewportFooter","ThreadPrimitiveMessages","Messages","ThreadPrimitiveMessageByIndex","MessageByIndex","ThreadPrimitiveUnstable_MessageById","Unstable_MessageById","ThreadPrimitiveRow","Row","ThreadPrimitiveScrollToBottom","ScrollToBottom","ThreadPrimitiveLoadEarlier","LoadEarlier","ThreadPrimitiveSuggestion","Suggestion","ThreadPrimitiveSuggestions","Suggestions","ThreadPrimitiveSuggestionByIndex","SuggestionByIndex"],"sources":["../../src/primitives/thread.ts"],"sourcesContent":["export { ThreadPrimitiveRoot as Root } from \"./thread/ThreadRoot\";\nexport { ThreadPrimitiveEmpty as Empty } from \"./thread/ThreadEmpty\";\nexport { ThreadPrimitiveIf as If } from \"./thread/ThreadIf\";\nexport { ThreadPrimitiveViewport as Viewport } from \"./thread/ThreadViewport\";\nexport { ThreadPrimitiveViewportProvider as ViewportProvider } from \"../context/providers/ThreadViewportProvider\";\nexport { ThreadPrimitiveViewportFooter as ViewportFooter } from \"./thread/ThreadViewportFooter\";\nexport { ThreadPrimitiveMessages as Messages } from \"./thread/ThreadMessages\";\nexport { ThreadPrimitiveMessageByIndex as MessageByIndex } from \"./thread/ThreadMessages\";\nexport { ThreadPrimitiveUnstable_MessageById as Unstable_MessageById } from \"./thread/ThreadMessages\";\nexport { ThreadPrimitiveRow as Row } from \"./thread/ThreadMessages\";\nexport { ThreadPrimitiveScrollToBottom as ScrollToBottom } from \"./thread/ThreadScrollToBottom\";\nexport { ThreadPrimitiveLoadEarlier as LoadEarlier } from \"./thread/ThreadLoadEarlier\";\nexport { ThreadPrimitiveSuggestion as Suggestion } from \"./thread/ThreadSuggestion\";\nexport {\n ThreadPrimitiveSuggestions as Suggestions,\n ThreadPrimitiveSuggestionByIndex as SuggestionByIndex,\n} from \"./thread/ThreadSuggestions\";\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SandboxHost.d.ts","sourceRoot":"","sources":["../../src/sandbox-host/SandboxHost.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAKnB,MAAM,OAAO,CAAC;AACf,OAAO,EAEL,KAAK,aAAa,EAElB,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAC;AAO5B,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,aAAa,EACb,QAAQ,GAAG,QAAQ,GAAG,aAAa,CACpC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,YAAY,EAAE,CACZ,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,cAAc,KACjB,aAAa,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/C,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;CACjE,CAAC;AAEF,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE;IAAE,MAAM,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACnD,OAAO,CAIT;AASD,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,UAAU,EACV,OAAO,EACP,SAA8B,EAC9B,YAAY,EACZ,OAAO,EACP,cAAc,GACf,EAAE,gBAAgB,+
|
|
1
|
+
{"version":3,"file":"SandboxHost.d.ts","sourceRoot":"","sources":["../../src/sandbox-host/SandboxHost.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAKnB,MAAM,OAAO,CAAC;AACf,OAAO,EAEL,KAAK,aAAa,EAElB,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAC;AAO5B,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,aAAa,EACb,QAAQ,GAAG,QAAQ,GAAG,aAAa,CACpC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,YAAY,EAAE,CACZ,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,cAAc,KACjB,aAAa,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/C,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;CACjE,CAAC;AAEF,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE;IAAE,MAAM,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACnD,OAAO,CAIT;AASD,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,UAAU,EACV,OAAO,EACP,SAA8B,EAC9B,YAAY,EACZ,OAAO,EACP,cAAc,GACf,EAAE,gBAAgB,+BAkKlB"}
|
|
@@ -69,6 +69,7 @@ function SandboxHost(t0) {
|
|
|
69
69
|
let frame = null;
|
|
70
70
|
let bridge = null;
|
|
71
71
|
let onMessage = null;
|
|
72
|
+
const renderController = new AbortController();
|
|
72
73
|
const { content: liveContent, sandbox: sb } = liveRef.current;
|
|
73
74
|
const reportError = (err) => {
|
|
74
75
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -80,7 +81,10 @@ function SandboxHost(t0) {
|
|
|
80
81
|
...sb?.enableBrowserCaching !== void 0 && { enableBrowserCaching: sb.enableBrowserCaching },
|
|
81
82
|
...sb?.salt !== void 0 && { salt: sb.salt }
|
|
82
83
|
});
|
|
83
|
-
const renderOpts =
|
|
84
|
+
const renderOpts = {
|
|
85
|
+
signal: renderController.signal,
|
|
86
|
+
...sb?.unsafeDocumentWrite !== void 0 && { unsafeDocumentWrite: sb.unsafeDocumentWrite }
|
|
87
|
+
};
|
|
84
88
|
scf.renderHtml(liveContent.html, container, renderOpts).then((rendered) => {
|
|
85
89
|
if (cancelled) {
|
|
86
90
|
rendered.dispose();
|
|
@@ -134,6 +138,7 @@ function SandboxHost(t0) {
|
|
|
134
138
|
const bridgeToDispose = bridge;
|
|
135
139
|
bridge = null;
|
|
136
140
|
if (bridgeToDispose) runCleanup(() => bridgeToDispose.dispose());
|
|
141
|
+
runCleanup(() => renderController.abort());
|
|
137
142
|
const frameToDispose = frame;
|
|
138
143
|
frame = null;
|
|
139
144
|
if (frameToDispose) runCleanup(() => frameToDispose.dispose());
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SandboxHost.js","names":["c","_c","CSSProperties","useEffect","useLayoutEffect","useRef","useState","isShimLoadError","RenderedFrame","SafeContentFrame","SandboxOption","invokeUserCallback","DEFAULT_PRODUCT","DEFAULT_MAX_HEIGHT","LOAD_TIMEOUT_MS","SandboxHostConfig","sandbox","useShadowDom","enableBrowserCaching","salt","product","className","style","unsafeDocumentWrite","SandboxHostFrame","Pick","SandboxHostApi","setHeight","height","SandboxBridge","onMessage","event","MessageEvent","dispose","SandboxContent","html","SandboxHostProps","content","contentKey","maxHeight","createBridge","frame","host","onError","error","Error","containerProps","Record","isSandboxFrameMessage","iframe","HTMLIFrameElement","origin","source","contentWindow","LiveSnapshot","SandboxHost","t0","$","t1","undefined","containerRef","contentHeight","setContentHeight","t2","liveRef","t3","t4","current","t5","Symbol","for","container","cancelled","bridge","liveContent","sb","reportError","err","String","bind","scf","renderOpts","renderHtml","then","rendered","hostApi","Number","isFinite","sendMessage","window","addEventListener","fullyLoadedPromiseWithTimeout","catch","err_0","code","err_1","cleanupFailed","cleanupError","runCleanup","cleanup","t6","console","listener","removeEventListener","bridgeToDispose","frameToDispose","resolvedHeight","Math","min","t7","mergedStyle","t8","t9","_c"],"sources":["../../src/sandbox-host/SandboxHost.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type CSSProperties,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport {\n isShimLoadError,\n type RenderedFrame,\n SafeContentFrame,\n type SandboxOption,\n} from \"safe-content-frame\";\nimport { invokeUserCallback } from \"@assistant-ui/core/internal\";\n\nconst DEFAULT_PRODUCT = \"assistant-ui-sandbox\";\nconst DEFAULT_MAX_HEIGHT = 800;\nconst LOAD_TIMEOUT_MS = 10_000;\n\nexport type SandboxHostConfig = {\n sandbox?: SandboxOption[];\n useShadowDom?: boolean;\n enableBrowserCaching?: boolean;\n salt?: string;\n product?: string;\n className?: string;\n style?: CSSProperties;\n unsafeDocumentWrite?: boolean;\n};\n\nexport type SandboxHostFrame = Pick<\n RenderedFrame,\n \"iframe\" | \"origin\" | \"sendMessage\"\n>;\n\nexport type SandboxHostApi = {\n setHeight: (height: number) => void;\n};\n\nexport type SandboxBridge = {\n onMessage: (event: MessageEvent) => void;\n dispose: () => void;\n};\n\nexport type SandboxContent = { html: string };\n\nexport type SandboxHostProps = {\n content: SandboxContent;\n contentKey: string;\n sandbox?: SandboxHostConfig | undefined;\n maxHeight?: number | undefined;\n createBridge: (\n frame: SandboxHostFrame,\n host: SandboxHostApi,\n ) => SandboxBridge;\n onError?: ((error: Error) => void) | undefined;\n containerProps?: Record<string, string | undefined> | undefined;\n};\n\nexport function isSandboxFrameMessage(\n event: MessageEvent,\n frame: { iframe: HTMLIFrameElement; origin: string },\n): boolean {\n return (\n event.source === frame.iframe.contentWindow && event.origin === frame.origin\n );\n}\n\ntype LiveSnapshot = {\n content: SandboxContent;\n sandbox: SandboxHostConfig | undefined;\n createBridge: SandboxHostProps[\"createBridge\"];\n onError: SandboxHostProps[\"onError\"];\n};\n\nexport function SandboxHost({\n content,\n contentKey,\n sandbox,\n maxHeight = DEFAULT_MAX_HEIGHT,\n createBridge,\n onError,\n containerProps,\n}: SandboxHostProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [contentHeight, setContentHeight] = useState<number | undefined>(\n undefined,\n );\n\n const liveRef = useRef<LiveSnapshot>({\n content,\n sandbox,\n createBridge,\n onError,\n });\n useLayoutEffect(() => {\n liveRef.current = { content, sandbox, createBridge, onError };\n }, [content, sandbox, createBridge, onError]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n let cancelled = false;\n let frame: RenderedFrame | null = null;\n let bridge: SandboxBridge | null = null;\n let onMessage: ((event: MessageEvent) => void) | null = null;\n\n const { content: liveContent, sandbox: sb } = liveRef.current;\n\n const reportError = (err: unknown) => {\n const error = err instanceof Error ? err : new Error(String(err));\n invokeUserCallback(\n \"assistant-ui\",\n \"SandboxHost onError\",\n liveRef.current.onError?.bind(liveRef.current),\n error,\n );\n };\n\n const scf = new SafeContentFrame(sb?.product ?? DEFAULT_PRODUCT, {\n ...(sb?.sandbox !== undefined && { sandbox: sb.sandbox }),\n ...(sb?.useShadowDom !== undefined && { useShadowDom: sb.useShadowDom }),\n ...(sb?.enableBrowserCaching !== undefined && {\n enableBrowserCaching: sb.enableBrowserCaching,\n }),\n ...(sb?.salt !== undefined && { salt: sb.salt }),\n });\n\n const renderOpts =\n sb?.unsafeDocumentWrite !== undefined\n ? { unsafeDocumentWrite: sb.unsafeDocumentWrite }\n : undefined;\n\n scf\n .renderHtml(liveContent.html, container, renderOpts)\n .then((rendered) => {\n if (cancelled) {\n rendered.dispose();\n return;\n }\n frame = rendered;\n\n const hostApi: SandboxHostApi = {\n setHeight: (height) => {\n if (\n typeof height === \"number\" &&\n Number.isFinite(height) &&\n height > 0\n ) {\n setContentHeight(height);\n }\n },\n };\n\n bridge = liveRef.current.createBridge(\n {\n iframe: rendered.iframe,\n origin: rendered.origin,\n sendMessage: rendered.sendMessage,\n },\n hostApi,\n );\n\n // Single owner of the window listener; the cross-origin guard runs\n // here so the bridge only sees frame-validated messages.\n onMessage = (event) => {\n if (!isSandboxFrameMessage(event, rendered)) return;\n bridge?.onMessage(event);\n };\n window.addEventListener(\"message\", onMessage);\n\n // renderHtml resolves at iframe load, which a shim that was never\n // served also reaches, so a completed render is only known once the\n // frame says so. render-timeout is excluded because the shim is\n // running and the guest may still paint, and reporting it would leave\n // a host that renders an error state showing one over a frame that\n // recovers.\n rendered.fullyLoadedPromiseWithTimeout(LOAD_TIMEOUT_MS).catch((err) => {\n if (cancelled) return;\n if (isShimLoadError(err) && err.code === \"render-timeout\") return;\n reportError(err);\n });\n })\n .catch((err) => {\n if (cancelled) return;\n frame?.dispose();\n frame = null;\n reportError(err);\n });\n\n return () => {\n cancelled = true;\n let cleanupFailed = false;\n let cleanupError: unknown;\n const runCleanup = (cleanup: () => void) => {\n try {\n cleanup();\n } catch (error) {\n if (cleanupFailed) {\n console.error(error);\n } else {\n cleanupFailed = true;\n cleanupError = error;\n }\n }\n };\n\n if (onMessage) {\n const listener = onMessage;\n onMessage = null;\n runCleanup(() => window.removeEventListener(\"message\", listener));\n }\n const bridgeToDispose = bridge;\n bridge = null;\n if (bridgeToDispose) runCleanup(() => bridgeToDispose.dispose());\n const frameToDispose = frame;\n frame = null;\n if (frameToDispose) runCleanup(() => frameToDispose.dispose());\n runCleanup(() => setContentHeight(undefined));\n\n if (cleanupFailed) throw cleanupError;\n };\n // oxlint-disable-next-line react/exhaustive-deps -- re-init only on contentKey change; live values flow through liveRef\n }, [contentKey]);\n\n const resolvedHeight =\n contentHeight != null ? Math.min(contentHeight, maxHeight) : undefined;\n const mergedStyle =\n resolvedHeight != null\n ? { ...sandbox?.style, height: resolvedHeight }\n : sandbox?.style;\n\n return (\n <div\n {...containerProps}\n ref={containerRef}\n className={sandbox?.className}\n style={mergedStyle}\n />\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SandboxHost.js","names":["c","_c","CSSProperties","useEffect","useLayoutEffect","useRef","useState","isShimLoadError","RenderedFrame","SafeContentFrame","SandboxOption","invokeUserCallback","DEFAULT_PRODUCT","DEFAULT_MAX_HEIGHT","LOAD_TIMEOUT_MS","SandboxHostConfig","sandbox","useShadowDom","enableBrowserCaching","salt","product","className","style","unsafeDocumentWrite","SandboxHostFrame","Pick","SandboxHostApi","setHeight","height","SandboxBridge","onMessage","event","MessageEvent","dispose","SandboxContent","html","SandboxHostProps","content","contentKey","maxHeight","createBridge","frame","host","onError","error","Error","containerProps","Record","isSandboxFrameMessage","iframe","HTMLIFrameElement","origin","source","contentWindow","LiveSnapshot","SandboxHost","t0","$","t1","undefined","containerRef","contentHeight","setContentHeight","t2","liveRef","t3","t4","current","t5","Symbol","for","container","cancelled","bridge","renderController","AbortController","liveContent","sb","reportError","err","String","bind","scf","renderOpts","signal","renderHtml","then","rendered","hostApi","Number","isFinite","sendMessage","window","addEventListener","fullyLoadedPromiseWithTimeout","catch","err_0","code","err_1","cleanupFailed","cleanupError","runCleanup","cleanup","t6","console","listener","removeEventListener","bridgeToDispose","abort","frameToDispose","resolvedHeight","Math","min","t7","mergedStyle","t8","t9","_c"],"sources":["../../src/sandbox-host/SandboxHost.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type CSSProperties,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport {\n isShimLoadError,\n type RenderedFrame,\n SafeContentFrame,\n type SandboxOption,\n} from \"safe-content-frame\";\nimport { invokeUserCallback } from \"@assistant-ui/core/internal\";\n\nconst DEFAULT_PRODUCT = \"assistant-ui-sandbox\";\nconst DEFAULT_MAX_HEIGHT = 800;\nconst LOAD_TIMEOUT_MS = 10_000;\n\nexport type SandboxHostConfig = {\n sandbox?: SandboxOption[];\n useShadowDom?: boolean;\n enableBrowserCaching?: boolean;\n salt?: string;\n product?: string;\n className?: string;\n style?: CSSProperties;\n unsafeDocumentWrite?: boolean;\n};\n\nexport type SandboxHostFrame = Pick<\n RenderedFrame,\n \"iframe\" | \"origin\" | \"sendMessage\"\n>;\n\nexport type SandboxHostApi = {\n setHeight: (height: number) => void;\n};\n\nexport type SandboxBridge = {\n onMessage: (event: MessageEvent) => void;\n dispose: () => void;\n};\n\nexport type SandboxContent = { html: string };\n\nexport type SandboxHostProps = {\n content: SandboxContent;\n contentKey: string;\n sandbox?: SandboxHostConfig | undefined;\n maxHeight?: number | undefined;\n createBridge: (\n frame: SandboxHostFrame,\n host: SandboxHostApi,\n ) => SandboxBridge;\n onError?: ((error: Error) => void) | undefined;\n containerProps?: Record<string, string | undefined> | undefined;\n};\n\nexport function isSandboxFrameMessage(\n event: MessageEvent,\n frame: { iframe: HTMLIFrameElement; origin: string },\n): boolean {\n return (\n event.source === frame.iframe.contentWindow && event.origin === frame.origin\n );\n}\n\ntype LiveSnapshot = {\n content: SandboxContent;\n sandbox: SandboxHostConfig | undefined;\n createBridge: SandboxHostProps[\"createBridge\"];\n onError: SandboxHostProps[\"onError\"];\n};\n\nexport function SandboxHost({\n content,\n contentKey,\n sandbox,\n maxHeight = DEFAULT_MAX_HEIGHT,\n createBridge,\n onError,\n containerProps,\n}: SandboxHostProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [contentHeight, setContentHeight] = useState<number | undefined>(\n undefined,\n );\n\n const liveRef = useRef<LiveSnapshot>({\n content,\n sandbox,\n createBridge,\n onError,\n });\n useLayoutEffect(() => {\n liveRef.current = { content, sandbox, createBridge, onError };\n }, [content, sandbox, createBridge, onError]);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n let cancelled = false;\n let frame: RenderedFrame | null = null;\n let bridge: SandboxBridge | null = null;\n let onMessage: ((event: MessageEvent) => void) | null = null;\n const renderController = new AbortController();\n\n const { content: liveContent, sandbox: sb } = liveRef.current;\n\n const reportError = (err: unknown) => {\n const error = err instanceof Error ? err : new Error(String(err));\n invokeUserCallback(\n \"assistant-ui\",\n \"SandboxHost onError\",\n liveRef.current.onError?.bind(liveRef.current),\n error,\n );\n };\n\n const scf = new SafeContentFrame(sb?.product ?? DEFAULT_PRODUCT, {\n ...(sb?.sandbox !== undefined && { sandbox: sb.sandbox }),\n ...(sb?.useShadowDom !== undefined && { useShadowDom: sb.useShadowDom }),\n ...(sb?.enableBrowserCaching !== undefined && {\n enableBrowserCaching: sb.enableBrowserCaching,\n }),\n ...(sb?.salt !== undefined && { salt: sb.salt }),\n });\n\n const renderOpts = {\n signal: renderController.signal,\n ...(sb?.unsafeDocumentWrite !== undefined && {\n unsafeDocumentWrite: sb.unsafeDocumentWrite,\n }),\n };\n\n scf\n .renderHtml(liveContent.html, container, renderOpts)\n .then((rendered) => {\n if (cancelled) {\n rendered.dispose();\n return;\n }\n frame = rendered;\n\n const hostApi: SandboxHostApi = {\n setHeight: (height) => {\n if (\n typeof height === \"number\" &&\n Number.isFinite(height) &&\n height > 0\n ) {\n setContentHeight(height);\n }\n },\n };\n\n bridge = liveRef.current.createBridge(\n {\n iframe: rendered.iframe,\n origin: rendered.origin,\n sendMessage: rendered.sendMessage,\n },\n hostApi,\n );\n\n // Single owner of the window listener; the cross-origin guard runs\n // here so the bridge only sees frame-validated messages.\n onMessage = (event) => {\n if (!isSandboxFrameMessage(event, rendered)) return;\n bridge?.onMessage(event);\n };\n window.addEventListener(\"message\", onMessage);\n\n // renderHtml resolves at iframe load, which a shim that was never\n // served also reaches, so a completed render is only known once the\n // frame says so. render-timeout is excluded because the shim is\n // running and the guest may still paint, and reporting it would leave\n // a host that renders an error state showing one over a frame that\n // recovers.\n rendered.fullyLoadedPromiseWithTimeout(LOAD_TIMEOUT_MS).catch((err) => {\n if (cancelled) return;\n if (isShimLoadError(err) && err.code === \"render-timeout\") return;\n reportError(err);\n });\n })\n .catch((err) => {\n if (cancelled) return;\n frame?.dispose();\n frame = null;\n reportError(err);\n });\n\n return () => {\n cancelled = true;\n let cleanupFailed = false;\n let cleanupError: unknown;\n const runCleanup = (cleanup: () => void) => {\n try {\n cleanup();\n } catch (error) {\n if (cleanupFailed) {\n console.error(error);\n } else {\n cleanupFailed = true;\n cleanupError = error;\n }\n }\n };\n\n if (onMessage) {\n const listener = onMessage;\n onMessage = null;\n runCleanup(() => window.removeEventListener(\"message\", listener));\n }\n const bridgeToDispose = bridge;\n bridge = null;\n if (bridgeToDispose) runCleanup(() => bridgeToDispose.dispose());\n runCleanup(() => renderController.abort());\n const frameToDispose = frame;\n frame = null;\n if (frameToDispose) runCleanup(() => frameToDispose.dispose());\n runCleanup(() => setContentHeight(undefined));\n\n if (cleanupFailed) throw cleanupError;\n };\n // oxlint-disable-next-line react/exhaustive-deps -- re-init only on contentKey change; live values flow through liveRef\n }, [contentKey]);\n\n const resolvedHeight =\n contentHeight != null ? Math.min(contentHeight, maxHeight) : undefined;\n const mergedStyle =\n resolvedHeight != null\n ? { ...sandbox?.style, height: resolvedHeight }\n : sandbox?.style;\n\n return (\n <div\n {...containerProps}\n ref={containerRef}\n className={sandbox?.className}\n style={mergedStyle}\n />\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useMessageStallDetection.d.ts","sourceRoot":"","sources":["../../src/unstable/useMessageStallDetection.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,qCAAqC,GAAG;IAClD;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,oGAAoG;IACpG,OAAO,EAAE,OAAO,CAAC;IACjB,kFAAkF;IAClF,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,iCAAiC,CAC/C,OAAO,CAAC,EAAE,qCAAqC,GAC9C,8BAA8B,
|
|
1
|
+
{"version":3,"file":"useMessageStallDetection.d.ts","sourceRoot":"","sources":["../../src/unstable/useMessageStallDetection.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,qCAAqC,GAAG;IAClD;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,oGAAoG;IACpG,OAAO,EAAE,OAAO,CAAC;IACjB,kFAAkF;IAClF,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,iCAAiC,CAC/C,OAAO,CAAC,EAAE,qCAAqC,GAC9C,8BAA8B,CAkEhC"}
|
|
@@ -25,9 +25,9 @@ function unstable_useMessageStallDetection(options) {
|
|
|
25
25
|
return values;
|
|
26
26
|
}));
|
|
27
27
|
const running = activity[0] === true;
|
|
28
|
-
const lastActivityRef = useRef(
|
|
28
|
+
const lastActivityRef = useRef(0);
|
|
29
29
|
const [stalled, setStalled] = useState(false);
|
|
30
|
-
const [,
|
|
30
|
+
const [now, setNow] = useState(0);
|
|
31
31
|
useEffect(() => {
|
|
32
32
|
if (!running) return void 0;
|
|
33
33
|
lastActivityRef.current = Date.now();
|
|
@@ -37,13 +37,17 @@ function unstable_useMessageStallDetection(options) {
|
|
|
37
37
|
setStalled(false);
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
|
+
const stall = () => {
|
|
41
|
+
setNow(Date.now());
|
|
42
|
+
setStalled(true);
|
|
43
|
+
};
|
|
40
44
|
const sinceActivity = Date.now() - lastActivityRef.current;
|
|
41
45
|
if (sinceActivity >= thresholdMs) {
|
|
42
|
-
|
|
46
|
+
stall();
|
|
43
47
|
return;
|
|
44
48
|
}
|
|
45
49
|
setStalled(false);
|
|
46
|
-
const id = setTimeout(
|
|
50
|
+
const id = setTimeout(stall, thresholdMs - sinceActivity);
|
|
47
51
|
return () => clearTimeout(id);
|
|
48
52
|
}, [
|
|
49
53
|
running,
|
|
@@ -52,7 +56,7 @@ function unstable_useMessageStallDetection(options) {
|
|
|
52
56
|
]);
|
|
53
57
|
useEffect(() => {
|
|
54
58
|
if (!stalled) return void 0;
|
|
55
|
-
const id = setInterval(() =>
|
|
59
|
+
const id = setInterval(() => setNow(Date.now()), 1e3);
|
|
56
60
|
return () => clearInterval(id);
|
|
57
61
|
}, [stalled]);
|
|
58
62
|
if (!stalled) return {
|
|
@@ -61,7 +65,7 @@ function unstable_useMessageStallDetection(options) {
|
|
|
61
65
|
};
|
|
62
66
|
return {
|
|
63
67
|
stalled: true,
|
|
64
|
-
stalledForMs: Math.max(0,
|
|
68
|
+
stalledForMs: Math.max(0, now - lastActivityRef.current)
|
|
65
69
|
};
|
|
66
70
|
}
|
|
67
71
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useMessageStallDetection.js","names":["useEffect","useRef","useState","useAuiState","useShallowSelector","Unstable_MessageStallDetectionOptions","thresholdMs","Unstable_MessageStallDetection","stalled","stalledForMs","unstable_useMessageStallDetection","options","activity","s","running","message","status","type","values","content","length","part","push","text","argsText","result","undefined","lastActivityRef","
|
|
1
|
+
{"version":3,"file":"useMessageStallDetection.js","names":["useEffect","useRef","useState","useAuiState","useShallowSelector","Unstable_MessageStallDetectionOptions","thresholdMs","Unstable_MessageStallDetection","stalled","stalledForMs","unstable_useMessageStallDetection","options","activity","s","running","message","status","type","values","content","length","part","push","text","argsText","result","undefined","lastActivityRef","setStalled","now","setNow","current","Date","stall","sinceActivity","id","setTimeout","clearTimeout","setInterval","clearInterval","Math","max"],"sources":["../../src/unstable/useMessageStallDetection.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { useAuiState } from \"@assistant-ui/store\";\nimport { useShallowSelector } from \"@assistant-ui/store/internal\";\n\nexport type Unstable_MessageStallDetectionOptions = {\n /**\n * Milliseconds of unchanged message content before the message counts as\n * stalled.\n * @default 2000\n */\n thresholdMs?: number | undefined;\n};\n\nexport type Unstable_MessageStallDetection = {\n /** True while the message is running and its content has not changed for at least `thresholdMs`. */\n stalled: boolean;\n /** Milliseconds since the last observed content change. `0` while not stalled. */\n stalledForMs: number;\n};\n\n/**\n * @deprecated Under active development and might change without notice.\n *\n * Detects mid-run output stalls on the current message: while the message is\n * running, watches its text, reasoning, and tool-argument values plus tool-result\n * availability and reports a stall once they stop changing for `thresholdMs`.\n * Useful for re-surfacing a \"still working\" indicator during tool think-time or\n * provider stalls, after the first tokens have already streamed.\n *\n * Must be used inside a message scope.\n */\nexport function unstable_useMessageStallDetection(\n options?: Unstable_MessageStallDetectionOptions,\n): Unstable_MessageStallDetection {\n const thresholdMs = options?.thresholdMs ?? 2000;\n\n const activity = useAuiState(\n useShallowSelector((s) => {\n const running = s.message.status?.type === \"running\";\n if (!running) return [false];\n\n const values: unknown[] = [true, s.message.content.length];\n\n for (const part of s.message.content) {\n if (part.type === \"text\" || part.type === \"reasoning\") {\n values.push(part.type, part.text);\n } else if (part.type === \"tool-call\") {\n values.push(part.type, part.argsText, part.result !== undefined);\n } else {\n values.push(part.type);\n }\n }\n return values;\n }),\n );\n\n const running = activity[0] === true;\n const lastActivityRef = useRef(0);\n const [stalled, setStalled] = useState(false);\n const [now, setNow] = useState(0);\n\n useEffect(() => {\n if (!running) return undefined;\n lastActivityRef.current = Date.now();\n return undefined;\n }, [running, activity]);\n\n useEffect(() => {\n if (!running) {\n setStalled(false);\n return undefined;\n }\n\n const stall = () => {\n setNow(Date.now());\n setStalled(true);\n };\n const sinceActivity = Date.now() - lastActivityRef.current;\n if (sinceActivity >= thresholdMs) {\n stall();\n return undefined;\n }\n\n setStalled(false);\n const id = setTimeout(stall, thresholdMs - sinceActivity);\n return () => clearTimeout(id);\n }, [running, activity, thresholdMs]);\n\n useEffect(() => {\n if (!stalled) return undefined;\n const id = setInterval(() => setNow(Date.now()), 1000);\n return () => clearInterval(id);\n }, [stalled]);\n\n if (!stalled) return { stalled: false, stalledForMs: 0 };\n return {\n stalled: true,\n stalledForMs: Math.max(0, now - lastActivityRef.current),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiCA,SAAgBU,kCACdC,SACgC;CAChC,MAAML,cAAcK,SAASL,eAAe;CAE5C,MAAMM,WAAWT,YACfC,oBAAoBS,MAAM;EAExB,IAAI,EADYA,EAAEE,QAAQC,QAAQC,SAAS,YAC7B,OAAO,CAAC,KAAK;EAE3B,MAAMC,SAAoB,CAAC,MAAML,EAAEE,QAAQI,QAAQC,MAAM;EAEzD,KAAK,MAAMC,QAAQR,EAAEE,QAAQI,SAC3B,IAAIE,KAAKJ,SAAS,UAAUI,KAAKJ,SAAS,aACxCC,OAAOI,KAAKD,KAAKJ,MAAMI,KAAKE,IAAI;OAC3B,IAAIF,KAAKJ,SAAS,aACvBC,OAAOI,KAAKD,KAAKJ,MAAMI,KAAKG,UAAUH,KAAKI,WAAWC,KAAAA,CAAS;OAE/DR,OAAOI,KAAKD,KAAKJ,IAAI;EAGzB,OAAOC;CACT,CAAC,CACH;CAEA,MAAMJ,UAAUF,SAAS,OAAO;CAChC,MAAMe,kBAAkB1B,OAAO,CAAC;CAChC,MAAM,CAACO,SAASoB,cAAc1B,SAAS,KAAK;CAC5C,MAAM,CAAC2B,KAAKC,UAAU5B,SAAS,CAAC;CAEhCF,gBAAgB;EACd,IAAI,CAACc,SAAS,OAAOY,KAAAA;EACrBC,gBAAgBI,UAAUC,KAAKH,IAAI;CAErC,GAAG,CAACf,SAASF,QAAQ,CAAC;CAEtBZ,gBAAgB;EACd,IAAI,CAACc,SAAS;GACZc,WAAW,KAAK;GAChB;EACF;EAEA,MAAMK,cAAc;GAClBH,OAAOE,KAAKH,IAAI,CAAC;GACjBD,WAAW,IAAI;EACjB;EACA,MAAMM,gBAAgBF,KAAKH,IAAI,IAAIF,gBAAgBI;EACnD,IAAIG,iBAAiB5B,aAAa;GAChC2B,MAAM;GACN;EACF;EAEAL,WAAW,KAAK;EAChB,MAAMO,KAAKC,WAAWH,OAAO3B,cAAc4B,aAAa;EACxD,aAAaG,aAAaF,EAAE;CAC9B,GAAG;EAACrB;EAASF;EAAUN;CAAW,CAAC;CAEnCN,gBAAgB;EACd,IAAI,CAACQ,SAAS,OAAOkB,KAAAA;EACrB,MAAMS,KAAKG,kBAAkBR,OAAOE,KAAKH,IAAI,CAAC,GAAG,GAAI;EACrD,aAAaU,cAAcJ,EAAE;CAC/B,GAAG,CAAC3B,OAAO,CAAC;CAEZ,IAAI,CAACA,SAAS,OAAO;EAAEA,SAAS;EAAOC,cAAc;CAAE;CACvD,OAAO;EACLD,SAAS;EACTC,cAAc+B,KAAKC,IAAI,GAAGZ,MAAMF,gBAAgBI,OAAO;CACzD;AACF"}
|
|
@@ -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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.25",
|
|
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.
|
|
58
|
+
"@assistant-ui/core": "^0.3.24",
|
|
59
59
|
"@assistant-ui/store": "^0.3.17",
|
|
60
60
|
"@assistant-ui/tap": "^0.9.21",
|
|
61
61
|
"assistant-cloud": "^0.2.5",
|
|
62
|
-
"assistant-stream": "^0.3.
|
|
62
|
+
"assistant-stream": "^0.3.48",
|
|
63
63
|
"radix-ui": "^1.6.7",
|
|
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
|
},
|
|
@@ -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,35 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { renderToString } from "react-dom/server";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
const { part } = vi.hoisted(() => ({
|
|
7
|
+
part: {
|
|
8
|
+
type: "tool-call" as const,
|
|
9
|
+
status: { type: "running" as const },
|
|
10
|
+
timing: { startedAt: 9000 },
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("@assistant-ui/store", async (importOriginal) => ({
|
|
15
|
+
...(await importOriginal<typeof import("@assistant-ui/store")>()),
|
|
16
|
+
useAuiState: (
|
|
17
|
+
selector: (state: { optional: { part: typeof part } }) => unknown,
|
|
18
|
+
) => selector({ optional: { part } }),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import { useToolCallElapsed } from "./useToolCallElapsed";
|
|
22
|
+
|
|
23
|
+
describe("useToolCallElapsed", () => {
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("does not read the wall clock during server render", () => {
|
|
29
|
+
const nowSpy = vi.spyOn(Date, "now");
|
|
30
|
+
const Probe = () => <span>{useToolCallElapsed() ?? "none"}</span>;
|
|
31
|
+
|
|
32
|
+
expect(renderToString(<Probe />)).toBe("<span>none</span>");
|
|
33
|
+
expect(nowSpy).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useLayoutEffect, useState } from "react";
|
|
4
4
|
import { useAuiState } from "@assistant-ui/store";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -10,7 +10,9 @@ import { useAuiState } from "@assistant-ui/store";
|
|
|
10
10
|
* Reads `part.timing`. Returns `undefined` when the part is not a tool call,
|
|
11
11
|
* carries no timing, ended without a recorded completion (the duration is
|
|
12
12
|
* unknown), or when no message part scope is available (so kit components
|
|
13
|
-
* stay renderable standalone, e.g. in docs previews).
|
|
13
|
+
* stay renderable standalone, e.g. in docs previews). A running call also
|
|
14
|
+
* returns `undefined` until the component mounts, so a server render never
|
|
15
|
+
* reads the clock.
|
|
14
16
|
*
|
|
15
17
|
* @example
|
|
16
18
|
* ```tsx
|
|
@@ -32,12 +34,12 @@ export const useToolCallElapsed = (): number | undefined => {
|
|
|
32
34
|
});
|
|
33
35
|
const running =
|
|
34
36
|
timing !== undefined && timing.completedAt === undefined && partRunning;
|
|
35
|
-
const [now, setNow] = useState
|
|
37
|
+
const [now, setNow] = useState<number | undefined>(undefined);
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
useLayoutEffect(() => {
|
|
38
40
|
if (!running) return undefined;
|
|
39
|
-
// The clock is
|
|
40
|
-
// the
|
|
41
|
+
// The clock is read only after mount so a prerender never reads it; the
|
|
42
|
+
// layout effect still catches the elapsed value up before the first paint.
|
|
41
43
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
42
44
|
setNow(Date.now());
|
|
43
45
|
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
@@ -47,6 +49,6 @@ export const useToolCallElapsed = (): number | undefined => {
|
|
|
47
49
|
if (timing === undefined) return undefined;
|
|
48
50
|
if (timing.completedAt !== undefined)
|
|
49
51
|
return Math.max(0, timing.completedAt - timing.startedAt);
|
|
50
|
-
if (!running) return undefined;
|
|
52
|
+
if (!running || now === undefined) return undefined;
|
|
51
53
|
return Math.max(0, now - timing.startedAt);
|
|
52
54
|
};
|