({
config,
setMessages,
scrollRef,
onPrepend,
}: Props)
| 70 | * feature('KAIROS') gate, so build-time elimination is handled there. |
| 71 | */ |
| 72 | export function useAssistantHistory({ |
| 73 | config, |
| 74 | setMessages, |
| 75 | scrollRef, |
| 76 | onPrepend, |
| 77 | }: Props): Result { |
| 78 | const enabled = config?.viewerOnly === true |
| 79 | |
| 80 | // Cursor state: ref-only (no re-render on cursor change). `null` = no |
| 81 | // older pages. `undefined` = initial page not fetched yet. |
| 82 | const cursorRef = useRef<string | null | undefined>(undefined) |
| 83 | const ctxRef = useRef<HistoryAuthCtx | null>(null) |
| 84 | const inflightRef = useRef(false) |
| 85 | |
| 86 | // Scroll-anchor: snapshot height + prepended count before setMessages; |
| 87 | // compensate in useLayoutEffect after React commits. getFreshScrollHeight |
| 88 | // reads Yoga directly so the value is correct post-commit. |
| 89 | const anchorRef = useRef<{ beforeHeight: number; count: number } | null>(null) |
| 90 | |
| 91 | // Fill-viewport chaining: after the initial page commits, if content doesn't |
| 92 | // fill the viewport yet, load another page. Self-chains via the layout effect |
| 93 | // until filled or the budget runs out. Budget set once on initial load; user |
| 94 | // scroll-ups don't need it (maybeLoadOlder re-fires on next wheel event). |
| 95 | const fillBudgetRef = useRef(0) |
| 96 | |
| 97 | // Stable sentinel UUID — reused across swaps so virtual-scroll treats it |
| 98 | // as one item (text-only mutation, not remove+insert). |
| 99 | const sentinelUuidRef = useRef(randomUUID()) |
| 100 | |
| 101 | function mkSentinel(text: string): SystemInformationalMessage { |
| 102 | return { |
| 103 | type: 'system', |
| 104 | subtype: 'informational', |
| 105 | content: text, |
| 106 | isMeta: false, |
| 107 | timestamp: new Date().toISOString(), |
| 108 | uuid: sentinelUuidRef.current, |
| 109 | level: 'info', |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | /** Prepend a page at the front, with scroll-anchor snapshot for non-initial. |
| 114 | * Replaces the sentinel (always at index 0 when present) in-place. */ |
| 115 | const prepend = useCallback( |
| 116 | (page: HistoryPage, isInitial: boolean) => { |
| 117 | const msgs = pageToMessages(page) |
| 118 | cursorRef.current = page.hasMore ? page.firstId : null |
| 119 | |
| 120 | if (!isInitial) { |
| 121 | const s = scrollRef.current |
| 122 | anchorRef.current = s |
| 123 | ? { beforeHeight: s.getFreshScrollHeight(), count: msgs.length } |
| 124 | : null |
| 125 | } |
| 126 | |
| 127 | const sentinel = page.hasMore ? null : mkSentinel(SENTINEL_START) |
| 128 | setMessages(prev => { |
| 129 | // Drop existing sentinel (index 0, known stable UUID — O(1)). |
no test coverage detected