(messageCount: number)
| 84 | * handle; `onRepin` by submit/scroll-to-bottom. |
| 85 | */ |
| 86 | export function useUnseenDivider(messageCount: number): { |
| 87 | /** Index into messages[] where the divider line renders. Cleared on |
| 88 | * sticky-resume (scroll back to bottom) so the "N new" line doesn't |
| 89 | * linger once everything is visible. */ |
| 90 | dividerIndex: number | null; |
| 91 | /** scrollHeight snapshot at first scroll-away — the divider's y-position. |
| 92 | * FullscreenLayout subscribes to ScrollBox and compares viewport bottom |
| 93 | * against this for pillVisible. Ref so writes don't re-render REPL. */ |
| 94 | dividerYRef: RefObject<number | null>; |
| 95 | onScrollAway: (handle: ScrollBoxHandle) => void; |
| 96 | onRepin: () => void; |
| 97 | /** Scroll the handle so the divider line is at the top of the viewport. */ |
| 98 | jumpToNew: (handle: ScrollBoxHandle | null) => void; |
| 99 | /** Shift dividerIndex and dividerYRef when messages are prepended |
| 100 | * (infinite scroll-back). indexDelta = number of messages prepended; |
| 101 | * heightDelta = content height growth in rows. */ |
| 102 | shiftDivider: (indexDelta: number, heightDelta: number) => void; |
| 103 | } { |
| 104 | const [dividerIndex, setDividerIndex] = useState<number | null>(null); |
| 105 | // Ref holds the current count for onScrollAway to snapshot. Written in |
| 106 | // the render body (not useEffect) so wheel events arriving between a |
| 107 | // message-append render and its effect flush don't capture a stale |
| 108 | // count (off-by-one in the baseline). React Compiler bails out here — |
| 109 | // acceptable for a hook instantiated once in REPL. |
| 110 | const countRef = useRef(messageCount); |
| 111 | countRef.current = messageCount; |
| 112 | // scrollHeight snapshot — the divider's y in content coords. Ref-only: |
| 113 | // read synchronously in onScrollAway (setState is batched, can't |
| 114 | // read-then-write in the same callback) AND by FullscreenLayout's |
| 115 | // pillVisible subscription. null = pinned to bottom. |
| 116 | const dividerYRef = useRef<number | null>(null); |
| 117 | const onRepin = useCallback(() => { |
| 118 | // Don't clear dividerYRef here — a trackpad momentum wheel event |
| 119 | // racing in the same stdin batch would see null and re-snapshot, |
| 120 | // overriding the setDividerIndex(null) below. The useEffect below |
| 121 | // clears the ref after React commits the null dividerIndex, so the |
| 122 | // ref stays non-null until the state settles. |
| 123 | setDividerIndex(null); |
| 124 | }, []); |
| 125 | const onScrollAway = useCallback((handle: ScrollBoxHandle) => { |
| 126 | // Nothing below the viewport → nothing to jump to. Covers both: |
| 127 | // • empty/short session: scrollUp calls scrollTo(0) which breaks sticky |
| 128 | // even at scrollTop=0 (wheel-up on fresh session showed the pill) |
| 129 | // • click-to-select at bottom: useDragToScroll.check() calls |
| 130 | // scrollTo(current) to break sticky so streaming content doesn't shift |
| 131 | // under the selection, then onScroll(false, …) — but scrollTop is still |
| 132 | // at max (Sarah Deaton, #claude-code-feedback 2026-03-15) |
| 133 | // pendingDelta: scrollBy accumulates without updating scrollTop. Without |
| 134 | // it, wheeling up from max would see scrollTop==max and suppress the pill. |
| 135 | const max = Math.max(0, handle.getScrollHeight() - handle.getViewportHeight()); |
| 136 | if (handle.getScrollTop() + handle.getPendingDelta() >= max) return; |
| 137 | // Snapshot only on the FIRST scroll-away. onScrollAway fires on EVERY |
| 138 | // scroll action (not just the initial break from sticky) — this guard |
| 139 | // preserves the original baseline so the count doesn't reset on the |
| 140 | // second PageUp. Subsequent calls are ref-only no-ops (no REPL re-render). |
| 141 | if (dividerYRef.current === null) { |
| 142 | dividerYRef.current = handle.getScrollHeight(); |
| 143 | // New scroll-away session → move the divider here (replaces old one) |
no test coverage detected