@assistant-ui/react 0.15.29 → 0.15.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/context/stores/ThreadViewport.d.ts +8 -0
  2. package/dist/context/stores/ThreadViewport.d.ts.map +1 -1
  3. package/dist/context/stores/ThreadViewport.js +24 -8
  4. package/dist/context/stores/ThreadViewport.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  8. package/dist/primitives/composer/ComposerResume.d.ts +23 -0
  9. package/dist/primitives/composer/ComposerResume.d.ts.map +1 -0
  10. package/dist/primitives/composer/ComposerResume.js +34 -0
  11. package/dist/primitives/composer/ComposerResume.js.map +1 -0
  12. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts +6 -2
  13. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  14. package/dist/primitives/composer/trigger/triggerSelectionResource.js +4 -1
  15. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  16. package/dist/primitives/composer.d.ts +1 -0
  17. package/dist/primitives/composer.d.ts.map +1 -1
  18. package/dist/primitives/composer.js +3 -1
  19. package/dist/primitives/composer.js.map +1 -1
  20. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  21. package/dist/primitives/thread/useThreadViewportAutoScroll.js +32 -292
  22. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  23. package/dist/utils/hooks/useOnResizeContent.js +1 -1
  24. package/package.json +5 -5
  25. package/src/context/stores/ThreadViewport.ts +36 -8
  26. package/src/index.ts +2 -0
  27. package/src/model-context/makeAssistantVisible.test.tsx +25 -37
  28. package/src/primitives/composer/ComposerInput.tsx +1 -1
  29. package/src/primitives/composer/ComposerResume.ts +36 -0
  30. package/src/primitives/composer/trigger/triggerSelectionResource.test.ts +24 -0
  31. package/src/primitives/composer/trigger/triggerSelectionResource.ts +14 -3
  32. package/src/primitives/composer/useAuiState.composer.test.tsx +84 -0
  33. package/src/primitives/composer.ts +1 -0
  34. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +90 -0
  35. package/src/primitives/thread/useThreadViewportAutoScroll.ts +48 -486
  36. package/src/model-context/frame/AssistantFrame.test.ts +0 -400
@@ -1 +1 @@
1
- {"version":3,"file":"composer.js","names":["TriggerMatch","TriggerMatcher","ComposerPrimitiveRoot","Root","ComposerPrimitiveInput","Input","ComposerPrimitiveSend","Send","ComposerPrimitiveCancel","Cancel","ComposerPrimitiveAddAttachment","AddAttachment","ComposerPrimitiveAttachments","Attachments","ComposerPrimitiveAttachmentByIndex","AttachmentByIndex","ComposerPrimitiveAttachmentDropzone","AttachmentDropzone","ComposerPrimitiveDictate","Dictate","ComposerPrimitiveStopDictation","StopDictation","ComposerPrimitiveDictationTranscript","DictationTranscript","ComposerPrimitiveIf","If","ComposerPrimitiveQuote","Quote","ComposerPrimitiveQuoteText","QuoteText","ComposerPrimitiveQuoteDismiss","QuoteDismiss","ComposerPrimitiveQueue","Queue","ComposerPrimitiveTriggerPopover","TriggerPopover","Unstable_TriggerPopover","ComposerPrimitiveTriggerPopoverRoot","TriggerPopoverRoot","Unstable_TriggerPopoverRoot","ComposerPrimitiveTriggerPopoverCategories","TriggerPopoverCategories","Unstable_TriggerPopoverCategories","ComposerPrimitiveTriggerPopoverCategoryItem","TriggerPopoverCategoryItem","Unstable_TriggerPopoverCategoryItem","ComposerPrimitiveTriggerPopoverItems","TriggerPopoverItems","Unstable_TriggerPopoverItems","ComposerPrimitiveTriggerPopoverItem","TriggerPopoverItem","Unstable_TriggerPopoverItem","ComposerPrimitiveTriggerPopoverBack","TriggerPopoverBack","Unstable_TriggerPopoverBack","useTriggerPopoverRootContext","unstable_useTriggerPopoverRootContext","useTriggerPopoverRootContextOptional","unstable_useTriggerPopoverRootContextOptional","useTriggerPopoverScopeContext","unstable_useTriggerPopoverScopeContext","useTriggerPopoverScopeContextOptional","unstable_useTriggerPopoverScopeContextOptional","useTriggerPopoverTriggers","unstable_useTriggerPopoverTriggers","useTriggerPopoverTriggersOptional","unstable_useTriggerPopoverTriggersOptional","RegisteredTrigger","Unstable_RegisteredTrigger","Unstable_TriggerMatcher","Unstable_TriggerMatch"],"sources":["../../src/primitives/composer.ts"],"sourcesContent":["import type { TriggerMatch, TriggerMatcher } from \"./composer/trigger\";\n\nexport { ComposerPrimitiveRoot as Root } from \"./composer/ComposerRoot\";\nexport { ComposerPrimitiveInput as Input } from \"./composer/ComposerInput\";\nexport { ComposerPrimitiveSend as Send } from \"./composer/ComposerSend\";\nexport { ComposerPrimitiveCancel as Cancel } from \"./composer/ComposerCancel\";\nexport { ComposerPrimitiveAddAttachment as AddAttachment } from \"./composer/ComposerAddAttachment\";\nexport { ComposerPrimitiveAttachments as Attachments } from \"./composer/ComposerAttachments\";\nexport { ComposerPrimitiveAttachmentByIndex as AttachmentByIndex } from \"./composer/ComposerAttachments\";\nexport { ComposerPrimitiveAttachmentDropzone as AttachmentDropzone } from \"./composer/ComposerAttachmentDropzone\";\nexport { ComposerPrimitiveDictate as Dictate } from \"./composer/ComposerDictate\";\nexport { ComposerPrimitiveStopDictation as StopDictation } from \"./composer/ComposerStopDictation\";\nexport { ComposerPrimitiveDictationTranscript as DictationTranscript } from \"./composer/ComposerDictationTranscript\";\nexport { ComposerPrimitiveIf as If } from \"./composer/ComposerIf\";\nexport { ComposerPrimitiveQuote as Quote } from \"./composer/ComposerQuote\";\nexport { ComposerPrimitiveQuoteText as QuoteText } from \"./composer/ComposerQuote\";\nexport { ComposerPrimitiveQuoteDismiss as QuoteDismiss } from \"./composer/ComposerQuote\";\nexport { ComposerPrimitiveQueue as Queue } from \"./composer/ComposerQueue\";\n\nexport { ComposerPrimitiveTriggerPopover as TriggerPopover } from \"./composer/trigger\";\nexport {\n /** @deprecated Use `ComposerPrimitive.TriggerPopover` instead. */\n ComposerPrimitiveTriggerPopover as Unstable_TriggerPopover,\n} from \"./composer/trigger\";\nexport { ComposerPrimitiveTriggerPopoverRoot as TriggerPopoverRoot } from \"./composer/trigger\";\nexport {\n /** @deprecated Use `ComposerPrimitive.TriggerPopoverRoot` instead. */\n ComposerPrimitiveTriggerPopoverRoot as Unstable_TriggerPopoverRoot,\n} from \"./composer/trigger\";\nexport { ComposerPrimitiveTriggerPopoverCategories as TriggerPopoverCategories } from \"./composer/trigger\";\nexport {\n /** @deprecated Use `ComposerPrimitive.TriggerPopoverCategories` instead. */\n ComposerPrimitiveTriggerPopoverCategories as Unstable_TriggerPopoverCategories,\n} from \"./composer/trigger\";\nexport { ComposerPrimitiveTriggerPopoverCategoryItem as TriggerPopoverCategoryItem } from \"./composer/trigger\";\nexport {\n /** @deprecated Use `ComposerPrimitive.TriggerPopoverCategoryItem` instead. */\n ComposerPrimitiveTriggerPopoverCategoryItem as Unstable_TriggerPopoverCategoryItem,\n} from \"./composer/trigger\";\nexport { ComposerPrimitiveTriggerPopoverItems as TriggerPopoverItems } from \"./composer/trigger\";\nexport {\n /** @deprecated Use `ComposerPrimitive.TriggerPopoverItems` instead. */\n ComposerPrimitiveTriggerPopoverItems as Unstable_TriggerPopoverItems,\n} from \"./composer/trigger\";\nexport { ComposerPrimitiveTriggerPopoverItem as TriggerPopoverItem } from \"./composer/trigger\";\nexport {\n /** @deprecated Use `ComposerPrimitive.TriggerPopoverItem` instead. */\n ComposerPrimitiveTriggerPopoverItem as Unstable_TriggerPopoverItem,\n} from \"./composer/trigger\";\nexport { ComposerPrimitiveTriggerPopoverBack as TriggerPopoverBack } from \"./composer/trigger\";\nexport {\n /** @deprecated Use `ComposerPrimitive.TriggerPopoverBack` instead. */\n ComposerPrimitiveTriggerPopoverBack as Unstable_TriggerPopoverBack,\n} from \"./composer/trigger\";\nexport {\n /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */\n useTriggerPopoverRootContext as unstable_useTriggerPopoverRootContext,\n} from \"./composer/trigger\";\nexport {\n /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */\n useTriggerPopoverRootContextOptional as unstable_useTriggerPopoverRootContextOptional,\n} from \"./composer/trigger\";\nexport {\n /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */\n useTriggerPopoverScopeContext as unstable_useTriggerPopoverScopeContext,\n} from \"./composer/trigger\";\nexport {\n /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */\n useTriggerPopoverScopeContextOptional as unstable_useTriggerPopoverScopeContextOptional,\n} from \"./composer/trigger\";\nexport {\n /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */\n useTriggerPopoverTriggers as unstable_useTriggerPopoverTriggers,\n} from \"./composer/trigger\";\nexport {\n /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */\n useTriggerPopoverTriggersOptional as unstable_useTriggerPopoverTriggersOptional,\n} from \"./composer/trigger\";\nexport type {\n /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */\n RegisteredTrigger as Unstable_RegisteredTrigger,\n} from \"./composer/trigger\";\nexport type { TriggerMatch, TriggerMatcher } from \"./composer/trigger\";\n/** @deprecated Use `TriggerMatcher` instead. */\nexport type Unstable_TriggerMatcher = TriggerMatcher;\n/** @deprecated Use `TriggerMatch` instead. */\nexport type Unstable_TriggerMatch = TriggerMatch;\n"],"mappings":""}
1
+ {"version":3,"file":"composer.js","names":["TriggerMatch","TriggerMatcher","ComposerPrimitiveRoot","Root","ComposerPrimitiveInput","Input","ComposerPrimitiveSend","Send","ComposerPrimitiveResume","Resume","ComposerPrimitiveCancel","Cancel","ComposerPrimitiveAddAttachment","AddAttachment","ComposerPrimitiveAttachments","Attachments","ComposerPrimitiveAttachmentByIndex","AttachmentByIndex","ComposerPrimitiveAttachmentDropzone","AttachmentDropzone","ComposerPrimitiveDictate","Dictate","ComposerPrimitiveStopDictation","StopDictation","ComposerPrimitiveDictationTranscript","DictationTranscript","ComposerPrimitiveIf","If","ComposerPrimitiveQuote","Quote","ComposerPrimitiveQuoteText","QuoteText","ComposerPrimitiveQuoteDismiss","QuoteDismiss","ComposerPrimitiveQueue","Queue","ComposerPrimitiveTriggerPopover","TriggerPopover","Unstable_TriggerPopover","ComposerPrimitiveTriggerPopoverRoot","TriggerPopoverRoot","Unstable_TriggerPopoverRoot","ComposerPrimitiveTriggerPopoverCategories","TriggerPopoverCategories","Unstable_TriggerPopoverCategories","ComposerPrimitiveTriggerPopoverCategoryItem","TriggerPopoverCategoryItem","Unstable_TriggerPopoverCategoryItem","ComposerPrimitiveTriggerPopoverItems","TriggerPopoverItems","Unstable_TriggerPopoverItems","ComposerPrimitiveTriggerPopoverItem","TriggerPopoverItem","Unstable_TriggerPopoverItem","ComposerPrimitiveTriggerPopoverBack","TriggerPopoverBack","Unstable_TriggerPopoverBack","useTriggerPopoverRootContext","unstable_useTriggerPopoverRootContext","useTriggerPopoverRootContextOptional","unstable_useTriggerPopoverRootContextOptional","useTriggerPopoverScopeContext","unstable_useTriggerPopoverScopeContext","useTriggerPopoverScopeContextOptional","unstable_useTriggerPopoverScopeContextOptional","useTriggerPopoverTriggers","unstable_useTriggerPopoverTriggers","useTriggerPopoverTriggersOptional","unstable_useTriggerPopoverTriggersOptional","RegisteredTrigger","Unstable_RegisteredTrigger","Unstable_TriggerMatcher","Unstable_TriggerMatch"],"sources":["../../src/primitives/composer.ts"],"sourcesContent":["import type { TriggerMatch, TriggerMatcher } from \"./composer/trigger\";\n\nexport { ComposerPrimitiveRoot as Root } from \"./composer/ComposerRoot\";\nexport { ComposerPrimitiveInput as Input } from \"./composer/ComposerInput\";\nexport { ComposerPrimitiveSend as Send } from \"./composer/ComposerSend\";\nexport { ComposerPrimitiveResume as Resume } from \"./composer/ComposerResume\";\nexport { ComposerPrimitiveCancel as Cancel } from \"./composer/ComposerCancel\";\nexport { ComposerPrimitiveAddAttachment as AddAttachment } from \"./composer/ComposerAddAttachment\";\nexport { ComposerPrimitiveAttachments as Attachments } from \"./composer/ComposerAttachments\";\nexport { ComposerPrimitiveAttachmentByIndex as AttachmentByIndex } from \"./composer/ComposerAttachments\";\nexport { ComposerPrimitiveAttachmentDropzone as AttachmentDropzone } from \"./composer/ComposerAttachmentDropzone\";\nexport { ComposerPrimitiveDictate as Dictate } from \"./composer/ComposerDictate\";\nexport { ComposerPrimitiveStopDictation as StopDictation } from \"./composer/ComposerStopDictation\";\nexport { ComposerPrimitiveDictationTranscript as DictationTranscript } from \"./composer/ComposerDictationTranscript\";\nexport { ComposerPrimitiveIf as If } from \"./composer/ComposerIf\";\nexport { ComposerPrimitiveQuote as Quote } from \"./composer/ComposerQuote\";\nexport { ComposerPrimitiveQuoteText as QuoteText } from \"./composer/ComposerQuote\";\nexport { ComposerPrimitiveQuoteDismiss as QuoteDismiss } from \"./composer/ComposerQuote\";\nexport { ComposerPrimitiveQueue as Queue } from \"./composer/ComposerQueue\";\n\nexport { ComposerPrimitiveTriggerPopover as TriggerPopover } from \"./composer/trigger\";\nexport {\n /** @deprecated Use `ComposerPrimitive.TriggerPopover` instead. */\n ComposerPrimitiveTriggerPopover as Unstable_TriggerPopover,\n} from \"./composer/trigger\";\nexport { ComposerPrimitiveTriggerPopoverRoot as TriggerPopoverRoot } from \"./composer/trigger\";\nexport {\n /** @deprecated Use `ComposerPrimitive.TriggerPopoverRoot` instead. */\n ComposerPrimitiveTriggerPopoverRoot as Unstable_TriggerPopoverRoot,\n} from \"./composer/trigger\";\nexport { ComposerPrimitiveTriggerPopoverCategories as TriggerPopoverCategories } from \"./composer/trigger\";\nexport {\n /** @deprecated Use `ComposerPrimitive.TriggerPopoverCategories` instead. */\n ComposerPrimitiveTriggerPopoverCategories as Unstable_TriggerPopoverCategories,\n} from \"./composer/trigger\";\nexport { ComposerPrimitiveTriggerPopoverCategoryItem as TriggerPopoverCategoryItem } from \"./composer/trigger\";\nexport {\n /** @deprecated Use `ComposerPrimitive.TriggerPopoverCategoryItem` instead. */\n ComposerPrimitiveTriggerPopoverCategoryItem as Unstable_TriggerPopoverCategoryItem,\n} from \"./composer/trigger\";\nexport { ComposerPrimitiveTriggerPopoverItems as TriggerPopoverItems } from \"./composer/trigger\";\nexport {\n /** @deprecated Use `ComposerPrimitive.TriggerPopoverItems` instead. */\n ComposerPrimitiveTriggerPopoverItems as Unstable_TriggerPopoverItems,\n} from \"./composer/trigger\";\nexport { ComposerPrimitiveTriggerPopoverItem as TriggerPopoverItem } from \"./composer/trigger\";\nexport {\n /** @deprecated Use `ComposerPrimitive.TriggerPopoverItem` instead. */\n ComposerPrimitiveTriggerPopoverItem as Unstable_TriggerPopoverItem,\n} from \"./composer/trigger\";\nexport { ComposerPrimitiveTriggerPopoverBack as TriggerPopoverBack } from \"./composer/trigger\";\nexport {\n /** @deprecated Use `ComposerPrimitive.TriggerPopoverBack` instead. */\n ComposerPrimitiveTriggerPopoverBack as Unstable_TriggerPopoverBack,\n} from \"./composer/trigger\";\nexport {\n /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */\n useTriggerPopoverRootContext as unstable_useTriggerPopoverRootContext,\n} from \"./composer/trigger\";\nexport {\n /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */\n useTriggerPopoverRootContextOptional as unstable_useTriggerPopoverRootContextOptional,\n} from \"./composer/trigger\";\nexport {\n /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */\n useTriggerPopoverScopeContext as unstable_useTriggerPopoverScopeContext,\n} from \"./composer/trigger\";\nexport {\n /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */\n useTriggerPopoverScopeContextOptional as unstable_useTriggerPopoverScopeContextOptional,\n} from \"./composer/trigger\";\nexport {\n /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */\n useTriggerPopoverTriggers as unstable_useTriggerPopoverTriggers,\n} from \"./composer/trigger\";\nexport {\n /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */\n useTriggerPopoverTriggersOptional as unstable_useTriggerPopoverTriggersOptional,\n} from \"./composer/trigger\";\nexport type {\n /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */\n RegisteredTrigger as Unstable_RegisteredTrigger,\n} from \"./composer/trigger\";\nexport type { TriggerMatch, TriggerMatcher } from \"./composer/trigger\";\n/** @deprecated Use `TriggerMatcher` instead. */\nexport type Unstable_TriggerMatcher = TriggerMatcher;\n/** @deprecated Use `TriggerMatch` instead. */\nexport type Unstable_TriggerMatch = TriggerMatch;\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"useThreadViewportAutoScroll.d.ts","sourceRoot":"","sources":["../../../src/primitives/thread/useThreadViewportAutoScroll.ts"],"names":[],"mappings":"AAGA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,OAAO,CAAC;AAwH9D,yBAAiB,2BAA2B,CAAC;IAC3C,KAAY,OAAO,GAAG;QACpB;;;;;WAKG;QACH,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;QAEjC;;;;WAIG;QACH,wBAAwB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;QAE/C;;;;WAIG;QACH,0BAA0B,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;QAEjD;;;;WAIG;QACH,4BAA4B,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;KACpD,CAAC;CACH;AAED,eAAO,MAAM,2BAA2B,GAAI,QAAQ,SAAS,WAAW,EAAE,qGAKvE,2BAA2B,CAAC,OAAO,KAAG,WAAW,CAAC,QAAQ,CA+Y5D,CAAC"}
1
+ {"version":3,"file":"useThreadViewportAutoScroll.d.ts","sourceRoot":"","sources":["../../../src/primitives/thread/useThreadViewportAutoScroll.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,OAAO,CAAC;AAQf,yBAAiB,2BAA2B,CAAC;IAC3C,KAAY,OAAO,GAAG;QACpB;;;;;WAKG;QACH,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;QAEjC;;;;WAIG;QACH,wBAAwB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;QAE/C;;;;WAIG;QACH,0BAA0B,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;QAEjD;;;;WAIG;QACH,4BAA4B,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;KACpD,CAAC;CACH;AAED,eAAO,MAAM,2BAA2B,GAAI,QAAQ,SAAS,WAAW,EAAE,qGAKvE,2BAA2B,CAAC,OAAO,KAAG,WAAW,CAAC,QAAQ,CAqE5D,CAAC"}
@@ -1,90 +1,12 @@
1
1
  "use client";
2
+ import { getThreadViewportAutoScroll } from "../../context/stores/ThreadViewport.js";
2
3
  import { useThreadViewportStore } from "../../context/react/ThreadViewportContext.js";
3
- import { writableStore } from "../../context/ReadonlyStore.js";
4
4
  import { useIsomorphicLayoutEffect } from "../../utils/useIsomorphicLayoutEffect.js";
5
5
  import { useOnScrollToBottom } from "../../utils/hooks/useOnScrollToBottom.js";
6
6
  import { useManagedRef } from "../../utils/hooks/useManagedRef.js";
7
- import { useOnResizeContent } from "../../utils/hooks/useOnResizeContent.js";
8
- import { useCallback, useRef } from "@assistant-ui/tap/react-shim";
7
+ import { useCallback, useInsertionEffect, useRef } from "@assistant-ui/tap/react-shim";
9
8
  import { useAui, useAuiEvent, useAuiState } from "@assistant-ui/store";
10
- import { useComposedRefs } from "radix-ui/internal";
11
- import { isUserScrollUp, isViewportAtBottom, viewportOverflows } from "@assistant-ui/store/client";
12
9
  //#region src/primitives/thread/useThreadViewportAutoScroll.ts
13
- const SCROLL_KEYS = /* @__PURE__ */ new Set([
14
- " ",
15
- "ArrowUp",
16
- "ArrowDown",
17
- "PageUp",
18
- "PageDown",
19
- "Home",
20
- "End"
21
- ]);
22
- const INTENT_CANCEL_KEYS = /* @__PURE__ */ new Set(["Enter", ...SCROLL_KEYS]);
23
- const TEXT_ENTRY_SELECTOR = [
24
- "textarea",
25
- "select",
26
- "[contenteditable]:not([contenteditable='false'])",
27
- "input:not([type='checkbox']):not([type='radio']):not([type='button']):not([type='submit']):not([type='reset']):not([type='image']):not([type='range']):not([type='file']):not([type='color'])"
28
- ].join(", ");
29
- const MESSAGE_SELECTOR = "[data-message-id]";
30
- const DISCLOSURE_SELECTOR = "[aria-expanded], summary";
31
- const isOpeningDisclosure = (target) => {
32
- const control = target.closest(DISCLOSURE_SELECTOR);
33
- if (!control) return false;
34
- if (control.tagName === "SUMMARY") return control.parentElement?.hasAttribute("open") === false;
35
- return control.getAttribute("aria-expanded") === "false" && (control.getAttribute("aria-haspopup") ?? "false") === "false" && control.getAttribute("role") !== "combobox";
36
- };
37
- const offsetInViewport = (element, viewport) => element.getBoundingClientRect().top - viewport.getBoundingClientRect().top;
38
- const measureFirstMessage = (viewport) => {
39
- const element = viewport.querySelector(MESSAGE_SELECTOR);
40
- const id = element?.dataset["messageId"];
41
- return element && id ? {
42
- id,
43
- offset: offsetInViewport(element, viewport)
44
- } : null;
45
- };
46
- /** The first message that starts in view, else the one spanning its top edge. */
47
- const measureReaderMessage = (viewport) => {
48
- const { top, bottom } = viewport.getBoundingClientRect();
49
- let spanning = null;
50
- for (const element of viewport.querySelectorAll(MESSAGE_SELECTOR)) {
51
- const rect = element.getBoundingClientRect();
52
- if (rect.bottom <= top) continue;
53
- if (rect.top >= bottom) break;
54
- const id = element.dataset["messageId"];
55
- if (!id) continue;
56
- if (rect.top >= top) return {
57
- id,
58
- offset: rect.top - top
59
- };
60
- spanning ??= {
61
- id,
62
- offset: rect.top - top
63
- };
64
- }
65
- return spanning;
66
- };
67
- const findMessage = (viewport, id) => {
68
- for (const element of viewport.querySelectorAll(MESSAGE_SELECTOR)) if (element.dataset["messageId"] === id) return element;
69
- return null;
70
- };
71
- /**
72
- * Scrolls so the message sits at `anchor.offset` again, recording the
73
- * position it scrolls to in `heldScrollTop` before the scroll event can
74
- * report it; false when the message is gone.
75
- */
76
- const keepMessageAt = (viewport, anchor, heldScrollTop) => {
77
- const element = findMessage(viewport, anchor.id);
78
- if (!element) return false;
79
- const delta = offsetInViewport(element, viewport) - anchor.offset;
80
- heldScrollTop.current = viewport.scrollTop + delta;
81
- if (Math.abs(delta) >= 1) viewport.scrollTo({
82
- top: heldScrollTop.current,
83
- behavior: "instant"
84
- });
85
- heldScrollTop.current = viewport.scrollTop;
86
- return true;
87
- };
88
10
  const useThreadViewportAutoScroll = ({ autoScroll, scrollToBottomOnRunStart = true, scrollToBottomOnInitialize = true, scrollToBottomOnThreadSwitch = true }) => {
89
11
  const aui = useAui();
90
12
  const divRef = useRef(null);
@@ -92,175 +14,21 @@ const useThreadViewportAutoScroll = ({ autoScroll, scrollToBottomOnRunStart = tr
92
14
  const firstMessageId = useAuiState((s_0) => s_0.thread.messages[0]?.id);
93
15
  const threadId = useAuiState((s_1) => s_1.threadListItem.id);
94
16
  const isRunning = useAuiState((s_2) => s_2.thread.isRunning);
95
- const initializeScrollRequestedRef = useRef(false);
96
- const scheduledFrameRef = useRef(null);
97
17
  const threadViewportStore = useThreadViewportStore();
98
- if (autoScroll === void 0) autoScroll = threadViewportStore.getState().turnAnchor !== "top";
99
- const lastScrollTop = useRef(0);
100
- const lastScrollHeight = useRef(0);
101
- const lastObservedScrollHeight = useRef(0);
102
- const lastObservedClientHeight = useRef(0);
103
- const firstMessageRef = useRef(null);
104
- const prependAnchorRef = useRef(null);
105
- const heldScrollTopRef = useRef(0);
106
- const scrollingToBottomBehaviorRef = useRef(null);
107
- const followBottomRef = useRef(autoScroll);
108
- const followPausedRef = useRef(false);
109
- const scrolledSincePauseRef = useRef(false);
110
- const previousAutoScrollRef = useRef(autoScroll);
111
- useIsomorphicLayoutEffect(() => {
112
- const previousAutoScroll = previousAutoScrollRef.current;
113
- previousAutoScrollRef.current = autoScroll;
114
- if (previousAutoScroll || !autoScroll) return;
115
- const div = divRef.current;
116
- followBottomRef.current = div !== null && isViewportAtBottom(div);
117
- }, [autoScroll]);
118
- const scrollToBottom = useCallback((behavior) => {
119
- const div_0 = divRef.current;
120
- if (!div_0) return;
121
- followBottomRef.current = true;
122
- followPausedRef.current = false;
123
- scrollingToBottomBehaviorRef.current = behavior;
124
- prependAnchorRef.current = null;
125
- div_0.scrollTo({
126
- top: div_0.scrollHeight,
127
- behavior
128
- });
129
- lastScrollTop.current = div_0.scrollTop;
130
- lastScrollHeight.current = div_0.scrollHeight;
131
- }, []);
132
- const cancelScheduledFrame = useCallback(() => {
133
- if (scheduledFrameRef.current === null) return;
134
- cancelAnimationFrame(scheduledFrameRef.current);
135
- scheduledFrameRef.current = null;
136
- }, []);
137
- const scheduleScrollToBottom = useCallback((behavior_0) => {
138
- scrollingToBottomBehaviorRef.current = behavior_0;
139
- cancelScheduledFrame();
140
- scheduledFrameRef.current = requestAnimationFrame(() => {
141
- scheduledFrameRef.current = null;
142
- scrollToBottom(behavior_0);
143
- });
144
- }, [cancelScheduledFrame, scrollToBottom]);
145
- useIsomorphicLayoutEffect(() => () => cancelScheduledFrame(), [cancelScheduledFrame]);
146
- const hasActiveTopAnchor = useCallback(() => {
147
- const state = threadViewportStore.getState();
148
- return state.turnAnchor === "top" && state.element.viewport === divRef.current && state.element.anchor !== null;
149
- }, [threadViewportStore]);
150
- const onScroll = () => {
151
- const div_1 = divRef.current;
152
- if (!div_1) return;
153
- if (prependAnchorRef.current && Math.abs(div_1.scrollTop - heldScrollTopRef.current) >= 1) prependAnchorRef.current = null;
154
- const isAtBottom = threadViewportStore.getState().isAtBottom;
155
- const newIsAtBottom = isViewportAtBottom(div_1);
156
- if (scrollingToBottomBehaviorRef.current !== null && !newIsAtBottom && lastScrollTop.current < div_1.scrollTop) {} else {
157
- const userScrolledUp = isUserScrollUp({
158
- scrollTop: lastScrollTop.current,
159
- scrollHeight: lastScrollHeight.current
160
- }, div_1);
161
- if (newIsAtBottom) {
162
- if (viewportOverflows(div_1)) {
163
- scrollingToBottomBehaviorRef.current = null;
164
- if (scrolledSincePauseRef.current) followPausedRef.current = false;
165
- }
166
- if (autoScroll) followBottomRef.current = true;
167
- } else if (userScrolledUp) {
168
- cancelScheduledFrame();
169
- scrollingToBottomBehaviorRef.current = null;
170
- followBottomRef.current = false;
171
- }
172
- if ((newIsAtBottom || scrollingToBottomBehaviorRef.current === null) && newIsAtBottom !== isAtBottom) writableStore(threadViewportStore).setState({ isAtBottom: newIsAtBottom });
173
- }
174
- lastScrollTop.current = div_1.scrollTop;
175
- lastScrollHeight.current = div_1.scrollHeight;
176
- firstMessageRef.current = measureFirstMessage(div_1);
177
- };
178
- const onScrollRef = useRef(onScroll);
179
- useIsomorphicLayoutEffect(() => {
180
- onScrollRef.current = onScroll;
181
- });
182
- const handleScroll = useCallback(() => onScrollRef.current(), []);
183
- const onResize = () => {
184
- const div_2 = divRef.current;
185
- if (!div_2) return;
186
- const { scrollHeight, clientHeight } = div_2;
187
- if (scrollHeight === lastObservedScrollHeight.current && clientHeight === lastObservedClientHeight.current) return;
188
- lastObservedScrollHeight.current = scrollHeight;
189
- lastObservedClientHeight.current = clientHeight;
190
- if (scheduledFrameRef.current === null) {
191
- const scrollBehavior = scrollingToBottomBehaviorRef.current;
192
- if (scrollBehavior && hasActiveTopAnchor()) scrollingToBottomBehaviorRef.current = null;
193
- else if (scrollBehavior) scrollToBottom(scrollBehavior);
194
- else if (autoScroll && !(isRunning && hasActiveTopAnchor()) && followBottomRef.current && !followPausedRef.current) scrollToBottom("instant");
195
- else if (prependAnchorRef.current && !keepMessageAt(div_2, prependAnchorRef.current, heldScrollTopRef)) prependAnchorRef.current = null;
196
- }
197
- handleScroll();
198
- };
199
- const onResizeRef = useRef(onResize);
200
- useIsomorphicLayoutEffect(() => {
201
- onResizeRef.current = onResize;
202
- });
203
- const resizeRef = useOnResizeContent(() => onResizeRef.current());
204
- const scrollRef = useManagedRef(useCallback((el) => {
205
- const cancelPendingScrollToBottom = () => {
206
- if (scrollingToBottomBehaviorRef.current === null && scheduledFrameRef.current === null) return;
207
- cancelScheduledFrame();
208
- scrollingToBottomBehaviorRef.current = null;
209
- handleScroll();
18
+ const binding = getThreadViewportAutoScroll(threadViewportStore);
19
+ const { controller } = binding;
20
+ useInsertionEffect(() => {
21
+ binding.getOptions = () => {
22
+ const state = threadViewportStore.getState();
23
+ return {
24
+ autoScroll: autoScroll ?? state.turnAnchor !== "top",
25
+ holdFollow: isRunning && state.turnAnchor === "top" && state.element.viewport === divRef.current && state.element.anchor !== null,
26
+ scrollToBottomOnInitialize,
27
+ scrollToBottomOnRunStart: scrollToBottomOnRunStart && state.turnAnchor !== "top",
28
+ scrollToBottomOnThreadSwitch
29
+ };
210
30
  };
211
- const cancelOnKeyDown = (event) => {
212
- if (!INTENT_CANCEL_KEYS.has(event.key)) return;
213
- if (event.target?.closest?.(TEXT_ENTRY_SELECTOR)) return;
214
- cancelPendingScrollToBottom();
215
- };
216
- const pauseFollowOnExpand = (event_0) => {
217
- const target_0 = event_0.target;
218
- if (!target_0?.closest?.(MESSAGE_SELECTOR) || target_0.closest(TEXT_ENTRY_SELECTOR) || !isOpeningDisclosure(target_0)) return;
219
- followPausedRef.current = true;
220
- scrolledSincePauseRef.current = false;
221
- cancelPendingScrollToBottom();
222
- };
223
- const noteScrollGesture = (event_1) => {
224
- if (event_1 instanceof KeyboardEvent) {
225
- if (!SCROLL_KEYS.has(event_1.key)) return;
226
- if (event_1.target?.closest?.(TEXT_ENTRY_SELECTOR)) return;
227
- } else if (event_1.type === "pointerdown" && event_1.target !== el) return;
228
- scrolledSincePauseRef.current = true;
229
- };
230
- const releasePrependAnchor = () => {
231
- prependAnchorRef.current = null;
232
- };
233
- const gestures = [
234
- "pointerdown",
235
- "wheel",
236
- "touchstart",
237
- "keydown"
238
- ];
239
- const resumeGestures = [
240
- "pointerdown",
241
- "wheel",
242
- "touchmove",
243
- "keydown"
244
- ];
245
- el.addEventListener("scroll", handleScroll);
246
- el.addEventListener("pointerdown", cancelPendingScrollToBottom);
247
- el.addEventListener("wheel", cancelPendingScrollToBottom, { passive: true });
248
- el.addEventListener("touchstart", cancelPendingScrollToBottom, { passive: true });
249
- el.addEventListener("keydown", cancelOnKeyDown);
250
- el.addEventListener("click", pauseFollowOnExpand, { capture: true });
251
- for (const gesture of resumeGestures) el.addEventListener(gesture, noteScrollGesture, { passive: true });
252
- for (const gesture_0 of gestures) el.addEventListener(gesture_0, releasePrependAnchor, { passive: true });
253
- return () => {
254
- el.removeEventListener("scroll", handleScroll);
255
- el.removeEventListener("pointerdown", cancelPendingScrollToBottom);
256
- el.removeEventListener("wheel", cancelPendingScrollToBottom);
257
- el.removeEventListener("touchstart", cancelPendingScrollToBottom);
258
- el.removeEventListener("keydown", cancelOnKeyDown);
259
- el.removeEventListener("click", pauseFollowOnExpand, { capture: true });
260
- for (const gesture_1 of resumeGestures) el.removeEventListener(gesture_1, noteScrollGesture);
261
- for (const gesture_2 of gestures) el.removeEventListener(gesture_2, releasePrependAnchor);
262
- };
263
- }, [cancelScheduledFrame, handleScroll]));
31
+ });
264
32
  const previousRef = useRef({
265
33
  threadId,
266
34
  firstMessageId
@@ -271,62 +39,34 @@ const useThreadViewportAutoScroll = ({ autoScroll, scrollToBottomOnRunStart = tr
271
39
  threadId,
272
40
  firstMessageId
273
41
  };
274
- if (previous.threadId !== threadId) {
275
- prependAnchorRef.current = null;
276
- return;
277
- }
278
- const div_3 = divRef.current;
42
+ if (previous.threadId !== threadId) return;
279
43
  const previousFirstMessageId = previous.firstMessageId;
280
- if (!div_3 || previousFirstMessageId === void 0 || previousFirstMessageId === firstMessageId || autoScroll && followBottomRef.current && !followPausedRef.current || scrollingToBottomBehaviorRef.current !== null) return;
281
- if (!aui.thread.getState().messages.some((message, index) => index > 0 && message.id === previousFirstMessageId)) return;
282
- const anchor = firstMessageRef.current;
283
- if (anchor?.id === previousFirstMessageId && keepMessageAt(div_3, anchor, heldScrollTopRef)) prependAnchorRef.current = measureReaderMessage(div_3) ?? anchor;
284
- else {
285
- const top = div_3.scrollHeight - (lastScrollHeight.current - lastScrollTop.current);
286
- if (Math.abs(div_3.scrollTop - top) >= 1) div_3.scrollTo({
287
- top,
288
- behavior: "instant"
289
- });
290
- }
291
- lastScrollTop.current = div_3.scrollTop;
292
- lastScrollHeight.current = div_3.scrollHeight;
44
+ if (previousFirstMessageId === void 0 || previousFirstMessageId === firstMessageId) return;
45
+ if (aui.thread.getState().messages.some((message, index) => index > 0 && message.id === previousFirstMessageId)) controller.messagesPrepended(previousFirstMessageId);
293
46
  }, [
294
47
  aui,
295
- autoScroll,
48
+ controller,
296
49
  firstMessageId,
297
50
  threadId
298
51
  ]);
299
52
  useIsomorphicLayoutEffect(() => {
300
- if (!scrollToBottomOnInitialize) return;
301
- if (!hasMessages) {
302
- initializeScrollRequestedRef.current = false;
303
- return;
304
- }
305
- if (initializeScrollRequestedRef.current) return;
306
- initializeScrollRequestedRef.current = true;
307
- if (scrollingToBottomBehaviorRef.current !== null) return;
308
- scheduleScrollToBottom("instant");
53
+ controller.setHasMessages(hasMessages);
309
54
  }, [
55
+ controller,
310
56
  hasMessages,
311
- scheduleScrollToBottom,
312
57
  scrollToBottomOnInitialize
313
58
  ]);
314
- useOnScrollToBottom(({ behavior: behavior_1 }) => {
315
- scrollToBottom(behavior_1);
316
- });
317
- useAuiEvent("thread.runStart", () => {
318
- prependAnchorRef.current = null;
319
- followPausedRef.current = false;
320
- if (!scrollToBottomOnRunStart) return;
321
- if (threadViewportStore.getState().turnAnchor === "top") return;
322
- scheduleScrollToBottom("auto");
323
- });
324
- useAuiEvent("threads.selectionChanged", () => {
325
- followPausedRef.current = false;
326
- if (!scrollToBottomOnThreadSwitch) return;
327
- scheduleScrollToBottom("instant");
328
- });
329
- return useComposedRefs(resizeRef, scrollRef, divRef);
59
+ useOnScrollToBottom(({ behavior }) => controller.scrollToBottom(behavior));
60
+ useAuiEvent("thread.runStart", () => controller.runStarted());
61
+ useAuiEvent("threads.selectionChanged", () => controller.threadSwitched());
62
+ return useManagedRef(useCallback((element) => {
63
+ divRef.current = element;
64
+ const detach = controller.attach(element);
65
+ return () => {
66
+ detach();
67
+ divRef.current = null;
68
+ };
69
+ }, [controller]));
330
70
  };
331
71
  //#endregion
332
72
  export { useThreadViewportAutoScroll };
@@ -1 +1 @@
1
- {"version":3,"file":"useThreadViewportAutoScroll.js","names":["useComposedRefs","useCallback","useRef","RefCallback","useAui","useAuiEvent","useAuiState","isUserScrollUp","isViewportAtBottom","viewportOverflows","useOnResizeContent","useOnScrollToBottom","useManagedRef","writableStore","useThreadViewportStore","useIsomorphicLayoutEffect","SCROLL_KEYS","Set","INTENT_CANCEL_KEYS","TEXT_ENTRY_SELECTOR","join","MESSAGE_SELECTOR","DISCLOSURE_SELECTOR","isOpeningDisclosure","target","Element","control","closest","tagName","parentElement","hasAttribute","getAttribute","MessageOffset","id","offset","offsetInViewport","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","followPausedRef","scrolledSincePauseRef","previousAutoScrollRef","previousAutoScroll","div","scrollToBottom","scrollHeight","cancelScheduledFrame","cancelAnimationFrame","scheduleScrollToBottom","requestAnimationFrame","hasActiveTopAnchor","state","onScroll","isAtBottom","newIsAtBottom","isInFlightDownwardScroll","userScrolledUp","shouldUpdate","setState","onScrollRef","handleScroll","onResize","clientHeight","scrollBehavior","onResizeRef","resizeRef","scrollRef","el","cancelPendingScrollToBottom","cancelOnKeyDown","event","KeyboardEvent","has","key","pauseFollowOnExpand","MouseEvent","noteScrollGesture","Event","type","releasePrependAnchor","gestures","const","resumeGestures","addEventListener","passive","capture","gesture","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, 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\";\nimport { useIsomorphicLayoutEffect } from \"../../utils/useIsomorphicLayoutEffect\";\n\nconst SCROLL_KEYS = new Set([\n \" \",\n \"ArrowUp\",\n \"ArrowDown\",\n \"PageUp\",\n \"PageDown\",\n \"Home\",\n \"End\",\n]);\n\n// Enter and Space activate focused controls, while navigation keys can\n// interrupt scrolling without producing a scroll event.\nconst INTENT_CANCEL_KEYS = new Set([\"Enter\", ...SCROLL_KEYS]);\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\nconst DISCLOSURE_SELECTOR = \"[aria-expanded], summary\";\n\n// A popup trigger (menu, dialog, listbox) also carries aria-expanded, but what\n// it opens is portaled away from the thread, so only disclosures count.\nconst isOpeningDisclosure = (target: Element): boolean => {\n const control = target.closest(DISCLOSURE_SELECTOR);\n if (!control) return false;\n if (control.tagName === \"SUMMARY\")\n return control.parentElement?.hasAttribute(\"open\") === false;\n return (\n control.getAttribute(\"aria-expanded\") === \"false\" &&\n (control.getAttribute(\"aria-haspopup\") ?? \"false\") === \"false\" &&\n control.getAttribute(\"role\") !== \"combobox\"\n );\n};\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 // Set by expanding a disclosure; suspends follow without clearing its intent\n // until a scroll gesture reaches an overflowing bottom (content that fits\n // reads as at the bottom), a run starts, the thread changes, or the reader\n // asks for the bottom again.\n const followPausedRef = useRef(false);\n const scrolledSincePauseRef = useRef(false);\n const previousAutoScrollRef = useRef(autoScroll);\n\n useIsomorphicLayoutEffect(() => {\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 followPausedRef.current = false;\n scrollingToBottomBehaviorRef.current = behavior;\n prependAnchorRef.current = null;\n div.scrollTo({ top: div.scrollHeight, behavior });\n lastScrollTop.current = div.scrollTop;\n lastScrollHeight.current = div.scrollHeight;\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 useIsomorphicLayoutEffect(\n () => () => cancelScheduledFrame(),\n [cancelScheduledFrame],\n );\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 onScroll = () => {\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 scrollingToBottomBehaviorRef.current !== null &&\n !newIsAtBottom &&\n 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 if (scrolledSincePauseRef.current) followPausedRef.current = false;\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 const onScrollRef = useRef(onScroll);\n useIsomorphicLayoutEffect(() => {\n onScrollRef.current = onScroll;\n });\n const handleScroll = useCallback(() => onScrollRef.current(), []);\n\n const onResize = () => {\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 // A scheduled frame owns the pending behavior; resize callbacks must not apply it early.\n if (scheduledFrameRef.current === null) {\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 !followPausedRef.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\n handleScroll();\n };\n const onResizeRef = useRef(onResize);\n useIsomorphicLayoutEffect(() => {\n onResizeRef.current = onResize;\n });\n const resizeRef = useOnResizeContent(() => onResizeRef.current());\n\n const scrollRef = useManagedRef<HTMLElement>(\n useCallback(\n (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 // With nothing pending, the last scroll event already published isAtBottom.\n if (\n scrollingToBottomBehaviorRef.current === null &&\n scheduledFrameRef.current === null\n )\n return;\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 handleScroll();\n };\n // The composer renders inside the viewport, so its keystrokes bubble here;\n // cancellation keys only represent a gesture on thread content when they\n // originate outside text entry.\n const cancelOnKeyDown = (event: KeyboardEvent) => {\n if (!INTENT_CANCEL_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 // Expanding a disclosure inside a message, such as a reasoning block or a\n // tool call card, means the reader is inspecting it, so bottom follow\n // pauses until the reader scrolls or asks for the bottom again. Every\n // other interaction, including copying or selecting text, keeps\n // following. Activation is read from click, which keyboard activation also\n // dispatches, in the capture phase so the state is read before any handler\n // on the disclosure flips it or stops the event.\n const pauseFollowOnExpand = (event: MouseEvent) => {\n const target = event.target as Element | null;\n if (\n !target?.closest?.(MESSAGE_SELECTOR) ||\n target.closest(TEXT_ENTRY_SELECTOR) ||\n !isOpeningDisclosure(target)\n )\n return;\n followPausedRef.current = true;\n scrolledSincePauseRef.current = false;\n cancelPendingScrollToBottom();\n };\n const noteScrollGesture = (event: Event) => {\n if (event instanceof KeyboardEvent) {\n if (!SCROLL_KEYS.has(event.key)) return;\n if (\n (event.target as Element | null)?.closest?.(TEXT_ENTRY_SELECTOR)\n )\n return;\n } else if (event.type === \"pointerdown\" && event.target !== el) {\n return;\n }\n scrolledSincePauseRef.current = true;\n };\n const releasePrependAnchor = () => {\n prependAnchorRef.current = null;\n };\n const gestures = [\n \"pointerdown\",\n \"wheel\",\n \"touchstart\",\n \"keydown\",\n ] as const;\n const resumeGestures = [\n \"pointerdown\",\n \"wheel\",\n \"touchmove\",\n \"keydown\",\n ] as const;\n el.addEventListener(\"scroll\", handleScroll);\n el.addEventListener(\"pointerdown\", cancelPendingScrollToBottom);\n el.addEventListener(\"wheel\", cancelPendingScrollToBottom, {\n passive: true,\n });\n el.addEventListener(\"touchstart\", cancelPendingScrollToBottom, {\n passive: true,\n });\n el.addEventListener(\"keydown\", cancelOnKeyDown);\n el.addEventListener(\"click\", pauseFollowOnExpand, { capture: true });\n for (const gesture of resumeGestures) {\n el.addEventListener(gesture, noteScrollGesture, { passive: true });\n }\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(\"wheel\", cancelPendingScrollToBottom);\n el.removeEventListener(\"touchstart\", cancelPendingScrollToBottom);\n el.removeEventListener(\"keydown\", cancelOnKeyDown);\n el.removeEventListener(\"click\", pauseFollowOnExpand, {\n capture: true,\n });\n for (const gesture of resumeGestures) {\n el.removeEventListener(gesture, noteScrollGesture);\n }\n for (const gesture of gestures) {\n el.removeEventListener(gesture, releasePrependAnchor);\n }\n };\n },\n [cancelScheduledFrame, handleScroll],\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 useIsomorphicLayoutEffect(() => {\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 && !followPausedRef.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 useIsomorphicLayoutEffect(() => {\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 followPausedRef.current = false;\n if (!scrollToBottomOnRunStart) return;\n if (threadViewportStore.getState().turnAnchor === \"top\") return;\n scheduleScrollToBottom(\"auto\");\n });\n\n useAuiEvent(\"threads.selectionChanged\", () => {\n followPausedRef.current = false;\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":["useCallback","useInsertionEffect","useRef","RefCallback","useAui","useAuiEvent","useAuiState","useOnScrollToBottom","useManagedRef","useThreadViewportStore","getThreadViewportAutoScroll","useIsomorphicLayoutEffect","useThreadViewportAutoScroll","Options","autoScroll","scrollToBottomOnRunStart","scrollToBottomOnInitialize","scrollToBottomOnThreadSwitch","HTMLElement","TElement","aui","divRef","hasMessages","s","thread","messages","length","firstMessageId","id","threadId","threadListItem","isRunning","threadViewportStore","binding","controller","getOptions","state","getState","turnAnchor","holdFollow","element","viewport","current","anchor","previousRef","previous","previousFirstMessageId","undefined","prepended","some","message","index","messagesPrepended","setHasMessages","behavior","scrollToBottom","runStarted","threadSwitched","detach","attach"],"sources":["../../../src/primitives/thread/useThreadViewportAutoScroll.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useInsertionEffect,\n useRef,\n type RefCallback,\n} from \"react\";\nimport { useAui, useAuiEvent, useAuiState } from \"@assistant-ui/store\";\nimport { useOnScrollToBottom } from \"../../utils/hooks/useOnScrollToBottom\";\nimport { useManagedRef } from \"../../utils/hooks/useManagedRef\";\nimport { useThreadViewportStore } from \"../../context/react/ThreadViewportContext\";\nimport { getThreadViewportAutoScroll } from \"../../context/stores/ThreadViewport\";\nimport { useIsomorphicLayoutEffect } from \"../../utils/useIsomorphicLayoutEffect\";\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 threadViewportStore = useThreadViewportStore();\n const binding = getThreadViewportAutoScroll(threadViewportStore);\n const { controller } = binding;\n\n useInsertionEffect(() => {\n binding.getOptions = () => {\n const state = threadViewportStore.getState();\n return {\n autoScroll: autoScroll ?? state.turnAnchor !== \"top\",\n holdFollow:\n isRunning &&\n state.turnAnchor === \"top\" &&\n state.element.viewport === divRef.current &&\n state.element.anchor !== null,\n scrollToBottomOnInitialize,\n scrollToBottomOnRunStart:\n scrollToBottomOnRunStart && state.turnAnchor !== \"top\",\n scrollToBottomOnThreadSwitch,\n };\n };\n });\n\n const previousRef = useRef({ threadId, firstMessageId });\n useIsomorphicLayoutEffect(() => {\n const previous = previousRef.current;\n previousRef.current = { threadId, firstMessageId };\n if (previous.threadId !== threadId) return;\n const previousFirstMessageId = previous.firstMessageId;\n if (\n previousFirstMessageId === undefined ||\n previousFirstMessageId === firstMessageId\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) controller.messagesPrepended(previousFirstMessageId);\n }, [aui, controller, firstMessageId, threadId]);\n\n useIsomorphicLayoutEffect(() => {\n controller.setHasMessages(hasMessages);\n }, [controller, hasMessages, scrollToBottomOnInitialize]);\n\n useOnScrollToBottom(({ behavior }) => controller.scrollToBottom(behavior));\n useAuiEvent(\"thread.runStart\", () => controller.runStarted());\n useAuiEvent(\"threads.selectionChanged\", () => controller.threadSwitched());\n\n return useManagedRef(\n useCallback(\n (element: TElement) => {\n divRef.current = element;\n const detach = controller.attach(element);\n return () => {\n detach();\n divRef.current = null;\n };\n },\n [controller],\n ),\n );\n};\n"],"mappings":";;;;;;;;;AAgDA,MAAaY,+BAA6D,EACxEE,YACAC,2BAA2B,MAC3BC,6BAA6B,MAC7BC,+BAA+B,WACiC;CAChE,MAAMG,MAAMhB,OAAO;CACnB,MAAMiB,SAASnB,OAAiB,IAAI;CACpC,MAAMoB,cAAchB,aAAaiB,MAAMA,EAAEC,OAAOC,SAASC,SAAS,CAAC;CACnE,MAAMC,iBAAiBrB,aAAaiB,QAAMA,IAAEC,OAAOC,SAAS,EAAE,EAAEG,EAAE;CAClE,MAAMC,WAAWvB,aAAaiB,QAAMA,IAAEO,eAAeF,EAAE;CACvD,MAAMG,YAAYzB,aAAaiB,QAAMA,IAAEC,OAAOO,SAAS;CACvD,MAAMC,sBAAsBvB,uBAAuB;CACnD,MAAMwB,UAAUvB,4BAA4BsB,mBAAmB;CAC/D,MAAM,EAAEE,eAAeD;CAEvBhC,yBAAyB;EACvBgC,QAAQE,mBAAmB;GACzB,MAAMC,QAAQJ,oBAAoBK,SAAS;GAC3C,OAAO;IACLvB,YAAYA,cAAcsB,MAAME,eAAe;IAC/CC,YACER,aACAK,MAAME,eAAe,SACrBF,MAAMI,QAAQC,aAAapB,OAAOqB,WAClCN,MAAMI,QAAQG,WAAW;IAC3B3B;IACAD,0BACEA,4BAA4BqB,MAAME,eAAe;IACnDrB;GACF;EACF;CACF,CAAC;CAED,MAAM2B,cAAc1C,OAAO;EAAE2B;EAAUF;CAAe,CAAC;CACvDhB,gCAAgC;EAC9B,MAAMkC,WAAWD,YAAYF;EAC7BE,YAAYF,UAAU;GAAEb;GAAUF;EAAe;EACjD,IAAIkB,SAAShB,aAAaA,UAAU;EACpC,MAAMiB,yBAAyBD,SAASlB;EACxC,IACEmB,2BAA2BC,KAAAA,KAC3BD,2BAA2BnB,gBAE3B;EAMF,IALkBP,IAAII,OACnBa,SAAS,CAAC,CACVZ,SAASwB,MACPC,SAASC,UAAUA,QAAQ,KAAKD,QAAQtB,OAAOkB,sBAEhDE,GAAWd,WAAWkB,kBAAkBN,sBAAsB;CACpE,GAAG;EAAC1B;EAAKc;EAAYP;EAAgBE;CAAQ,CAAC;CAE9ClB,gCAAgC;EAC9BuB,WAAWmB,eAAe/B,WAAW;CACvC,GAAG;EAACY;EAAYZ;EAAaN;CAA0B,CAAC;CAExDT,qBAAqB,EAAE+C,eAAepB,WAAWqB,eAAeD,QAAQ,CAAC;CACzEjD,YAAY,yBAAyB6B,WAAWsB,WAAW,CAAC;CAC5DnD,YAAY,kCAAkC6B,WAAWuB,eAAe,CAAC;CAEzE,OAAOjD,cACLR,aACGwC,YAAsB;EACrBnB,OAAOqB,UAAUF;EACjB,MAAMkB,SAASxB,WAAWyB,OAAOnB,OAAO;EACxC,aAAa;GACXkB,OAAO;GACPrC,OAAOqB,UAAU;EACnB;CACF,GACA,CAACR,UAAU,CACb,CACF;AACF"}
@@ -1,8 +1,8 @@
1
1
  import { useManagedRef } from "./useManagedRef.js";
2
2
  import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
3
3
  import "@assistant-ui/tap/react-shim";
4
- import { useCallbackRef } from "radix-ui/internal";
5
4
  import { observeContentResize } from "@assistant-ui/store/client";
5
+ import { useCallbackRef } from "radix-ui/internal";
6
6
  //#region src/utils/hooks/useOnResizeContent.ts
7
7
  const useOnResizeContent = (callback) => {
8
8
  const $ = c(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react",
3
- "version": "0.15.29",
3
+ "version": "0.15.30",
4
4
  "description": "Open-source TypeScript/React library for building production-grade AI chat experiences",
5
5
  "keywords": [
6
6
  "ai",
@@ -55,11 +55,11 @@
55
55
  ],
56
56
  "sideEffects": false,
57
57
  "dependencies": {
58
- "@assistant-ui/core": "^0.3.29",
59
- "@assistant-ui/store": "^0.3.21",
60
- "@assistant-ui/tap": "^0.9.23",
58
+ "@assistant-ui/core": "^0.3.30",
59
+ "@assistant-ui/store": "^0.3.22",
60
+ "@assistant-ui/tap": "^0.9.24",
61
61
  "assistant-cloud": "^0.2.7",
62
- "assistant-stream": "^0.3.52",
62
+ "assistant-stream": "^0.3.53",
63
63
  "radix-ui": "^1.7.0",
64
64
  "react-textarea-autosize": "^8.5.9",
65
65
  "safe-content-frame": "^0.0.34",
@@ -3,6 +3,12 @@
3
3
  import { create } from "zustand";
4
4
  import type { Unsubscribe } from "@assistant-ui/core";
5
5
  import { notifyEventListeners } from "@assistant-ui/core/internal";
6
+ import {
7
+ createThreadViewportAutoScroll,
8
+ type ThreadViewportAutoScroll,
9
+ type ThreadViewportAutoScrollOptions,
10
+ } from "@assistant-ui/store/client";
11
+ import type { ReadonlyStore } from "../ReadonlyStore";
6
12
 
7
13
  export type SizeHandle = {
8
14
  /** Update the height */
@@ -137,6 +143,20 @@ export type ThreadViewportStoreOptions = {
137
143
  | undefined;
138
144
  };
139
145
 
146
+ type ViewportAutoScroll = {
147
+ controller: ThreadViewportAutoScroll;
148
+ getOptions: () => ThreadViewportAutoScrollOptions;
149
+ };
150
+
151
+ const viewportAutoScroll = new WeakMap<
152
+ ReadonlyStore<ThreadViewportState>,
153
+ ViewportAutoScroll
154
+ >();
155
+
156
+ export const getThreadViewportAutoScroll = (
157
+ store: ReadonlyStore<ThreadViewportState>,
158
+ ) => viewportAutoScroll.get(store)!;
159
+
140
160
  export const makeThreadViewportStore = (
141
161
  options: ThreadViewportStoreOptions = {},
142
162
  ) => {
@@ -152,14 +172,21 @@ export const makeThreadViewportStore = (
152
172
  },
153
173
  });
154
174
  });
155
- const insetRegistry = createSizeRegistry((total) => {
156
- store.setState({
157
- height: {
158
- ...store.getState().height,
159
- inset: total,
175
+ const autoScroll: ViewportAutoScroll = {
176
+ getOptions: () => ({
177
+ autoScroll: options.turnAnchor !== "top",
178
+ scrollToBottomOnInitialize: true,
179
+ scrollToBottomOnRunStart: options.turnAnchor !== "top",
180
+ scrollToBottomOnThreadSwitch: true,
181
+ }),
182
+ controller: createThreadViewportAutoScroll({
183
+ getOptions: () => autoScroll.getOptions(),
184
+ onAtBottomChange: (isAtBottom) => store.setState({ isAtBottom }),
185
+ onContentInsetChange: (inset) => {
186
+ store.setState({ height: { ...store.getState().height, inset } });
160
187
  },
161
- });
162
- });
188
+ }),
189
+ };
163
190
  const registerElementSlot = (
164
191
  key: "viewport" | "anchor",
165
192
  element: HTMLElement | null,
@@ -217,7 +244,7 @@ export const makeThreadViewportStore = (
217
244
  topAnchorTurn: null,
218
245
 
219
246
  registerViewport: viewportRegistry.register,
220
- registerContentInset: insetRegistry.register,
247
+ registerContentInset: autoScroll.controller.registerContentInset,
221
248
  registerViewportElement: (element) =>
222
249
  registerElementSlot("viewport", element),
223
250
  registerAnchorElement: (element) => registerElementSlot("anchor", element),
@@ -246,5 +273,6 @@ export const makeThreadViewportStore = (
246
273
  },
247
274
  }));
248
275
 
276
+ viewportAutoScroll.set(store, autoScroll);
249
277
  return store;
250
278
  };
package/src/index.ts CHANGED
@@ -274,7 +274,9 @@ export {
274
274
 
275
275
  export type {
276
276
  ModelContext,
277
+ ModelContextOptions,
277
278
  ModelContextProvider,
279
+ SystemPart,
278
280
  LanguageModelConfig,
279
281
  LanguageModelV1CallSettings,
280
282
  } from "@assistant-ui/core";