@opengeni/react 2.2.0-canary.0 → 2.5.0-canary.0

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 (47) hide show
  1. package/README.md +9 -2
  2. package/dist/{chunk-5RZN3DD5.js → chunk-6H6S5V4Z.js} +280 -113
  3. package/dist/chunk-6H6S5V4Z.js.map +1 -0
  4. package/dist/{chunk-UOLJWT5Y.js → chunk-ATQJEWCA.js} +154 -114
  5. package/dist/chunk-ATQJEWCA.js.map +1 -0
  6. package/dist/{chunk-XSA2QAKP.js → chunk-MXJGO7LE.js} +65 -3
  7. package/dist/chunk-MXJGO7LE.js.map +1 -0
  8. package/dist/{chunk-LTCHJ4MS.js → chunk-NVREJA6V.js} +2 -2
  9. package/dist/{chunk-JOAVA2TW.js → chunk-T7VAYG2I.js} +8 -7
  10. package/dist/{chunk-JOAVA2TW.js.map → chunk-T7VAYG2I.js.map} +1 -1
  11. package/dist/components/fleet-tile.d.ts +1 -1
  12. package/dist/components/message-timeline.d.ts +7 -2
  13. package/dist/composer.js +2 -2
  14. package/dist/hooks/use-session-events.d.ts +2 -1
  15. package/dist/hooks/use-session-lineage.d.ts +4 -0
  16. package/dist/hooks/use-session.d.ts +6 -0
  17. package/dist/hooks/use-workspace-sessions.d.ts +6 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.js +41 -19
  20. package/dist/index.js.map +1 -1
  21. package/dist/older-history.d.ts +29 -0
  22. package/dist/session-ui.d.ts +2 -0
  23. package/dist/session-ui.js +5 -2
  24. package/dist/session.d.ts +2 -0
  25. package/dist/session.js +5 -3
  26. package/dist/timeline/types.d.ts +2 -0
  27. package/package.json +2 -2
  28. package/src/components/fleet-tile.tsx +7 -5
  29. package/src/components/human-input-form.tsx +14 -6
  30. package/src/components/message-timeline.tsx +428 -137
  31. package/src/components/tip-follow.ts +18 -7
  32. package/src/hooks/use-composer.ts +20 -6
  33. package/src/hooks/use-session-events.ts +84 -76
  34. package/src/hooks/use-session-lineage.ts +26 -8
  35. package/src/hooks/use-session.ts +35 -8
  36. package/src/hooks/use-workspace-sessions.ts +38 -13
  37. package/src/index.ts +2 -0
  38. package/src/older-history.ts +69 -0
  39. package/src/session-ui.ts +2 -0
  40. package/src/session.ts +2 -0
  41. package/src/timeline/activity-rail.tsx +1 -0
  42. package/src/timeline/projection.ts +53 -2
  43. package/src/timeline/types.ts +2 -0
  44. package/dist/chunk-5RZN3DD5.js.map +0 -1
  45. package/dist/chunk-UOLJWT5Y.js.map +0 -1
  46. package/dist/chunk-XSA2QAKP.js.map +0 -1
  47. /package/dist/{chunk-LTCHJ4MS.js.map → chunk-NVREJA6V.js.map} +0 -0
@@ -7,7 +7,6 @@ import type {
7
7
  import {
8
8
  ArrowDownIcon,
9
9
  ArrowRightIcon,
10
- ArrowUpToLineIcon,
11
10
  BotIcon,
12
11
  CheckCircle2Icon,
13
12
  CheckIcon,
@@ -43,6 +42,11 @@ import {
43
42
  import { cn } from "../lib/cn";
44
43
  import { formatClockTime, formatRelativeTime, truncate } from "../lib/format";
45
44
  import { prefersReducedMotion } from "../lib/motion";
45
+ import {
46
+ invokeOlderHistoryLoaderWithReceiptCapture,
47
+ type OlderHistoryLoadReceipt,
48
+ type OlderHistoryLoader,
49
+ } from "../older-history";
46
50
  import { Markdown } from "./markdown";
47
51
  import {
48
52
  UserMessageBody,
@@ -181,8 +185,12 @@ export type MessageTimelineProps = {
181
185
  hasOlder?: boolean | undefined;
182
186
  /** An older window is being fetched; shows the quiet top shimmer. */
183
187
  loadingOlder?: boolean | undefined;
184
- /** Called when the reader nears the top and older history should backfill. */
185
- onLoadOlder?: (() => void) | undefined;
188
+ /**
189
+ * Called when older history should backfill. Existing void, synchronous-value,
190
+ * and arbitrary-promise callbacks remain supported. Receipt-aware loaders
191
+ * preserve committed-page direction through wrappers and bounded windows.
192
+ */
193
+ onLoadOlder?: OlderHistoryLoader | undefined;
186
194
  /** Jump to the durable session start (bounded oldest window, no middle). */
187
195
  onJumpToStart?: (() => void | Promise<void>) | undefined;
188
196
  /** True while the oldest window is loading. */
@@ -213,8 +221,9 @@ export type MessageTimelineProps = {
213
221
  *
214
222
  * Scroll invariant (tip-follow camera — see `./tip-follow.ts`):
215
223
  * - Load/remount: hidden until tip is hard-snapped across a short settle; then
216
- * reveal. Live tip: DOM grows immediately; the camera eases down (not
217
- * tip-glued feed-forward that is the one-line yank).
224
+ * reveal. Live tip: DOM growth advances the pinned viewport by the same
225
+ * amount, so rendered content is visible immediately. Only debt that already
226
+ * existed before the growth goes through the camera ease.
218
227
  * - One continuous follow while hot (faster τ when behind); sleeps when cold.
219
228
  * - While pinned, tip-debt from growth/collapse must NEVER unpin — only
220
229
  * wheel/keys/pointer-armed scroll-up, or a settled scrollend away from the
@@ -235,6 +244,63 @@ const PIN_THRESHOLD_PX = 48;
235
244
  */
236
245
  const OLDER_PREFETCH_MARGIN_PX = 400;
237
246
  const OLDER_PREFETCH_ROOT_MARGIN = `${OLDER_PREFETCH_MARGIN_PX}px 0px 0px 0px`;
247
+ const PRIMARY_ACTION_CLASS =
248
+ "inline-flex w-full shrink-0 items-center justify-center gap-1.5 rounded-og-md bg-og-accent px-3 py-1.5 text-og-menu font-medium text-og-accent-fg sm:w-auto";
249
+ const MESSAGE_BUBBLE_CLASS =
250
+ "w-fit max-w-full min-w-0 rounded-og-lg rounded-br-og-xs border border-og-border bg-og-surface-2 px-4 py-2.5 text-og-md leading-6 text-og-fg";
251
+ const WAITING_PILL_CLASS =
252
+ "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting";
253
+ const LOADING_CHIP_CLASS =
254
+ "pointer-events-none inline-flex items-center rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1 text-og-control font-medium shadow-og-md backdrop-blur";
255
+
256
+ // State: 0 underfill retry, 1 prefetch pending, 2 compatibility-settled,
257
+ // 3 explicit no-progress waiting to become an underfill retry.
258
+ // The tuple identity is the exact request ownership fence. The boundary may rebase
259
+ // forward while that request is pending when a bounded live-tail append evicts
260
+ // its former oldest row. First-party loaders mark the exact owner when their
261
+ // older page commits; retained direction is the fallback for other hosts.
262
+ type OlderLoadAttempt = [
263
+ boundary: string | undefined,
264
+ state?: number,
265
+ receipt?: OlderHistoryLoadReceipt,
266
+ ];
267
+
268
+ function invokeOlderLoad(
269
+ load: OlderHistoryLoader,
270
+ noProgress: () => void,
271
+ attempt: OlderLoadAttempt,
272
+ ): 1 | undefined {
273
+ try {
274
+ // Receipt creation is captured synchronously through legacy wrappers such
275
+ // as `() => void loadOlder()`, even when the wrapper discards the return.
276
+ const result = invokeOlderHistoryLoaderWithReceiptCapture(load, (receipt) => {
277
+ attempt[2] = receipt;
278
+ }) as OlderHistoryLoadReceipt | PromiseLike<unknown> | unknown;
279
+ const receipt =
280
+ attempt[2] ??
281
+ (typeof (result as { committed?: unknown } | undefined)?.committed === "boolean"
282
+ ? (result as OlderHistoryLoadReceipt)
283
+ : undefined);
284
+ if (receipt) {
285
+ attempt[2] = receipt;
286
+ void receipt.then(
287
+ (value) => value === false && !receipt.committed && noProgress(),
288
+ noProgress,
289
+ );
290
+ return 1;
291
+ }
292
+ if (typeof (result as PromiseLike<unknown> | undefined)?.then != "function") {
293
+ return;
294
+ }
295
+ void (result as PromiseLike<unknown>).then(
296
+ (value) => value === false && noProgress(),
297
+ noProgress,
298
+ );
299
+ } catch {
300
+ noProgress();
301
+ }
302
+ return 1;
303
+ }
238
304
 
239
305
  /**
240
306
  * Pinned = the viewport bottom is within PIN_THRESHOLD_PX of the content
@@ -245,7 +311,8 @@ const OLDER_PREFETCH_ROOT_MARGIN = `${OLDER_PREFETCH_MARGIN_PX}px 0px 0px 0px`;
245
311
  * cannot scroll at all is always pinned.
246
312
  */
247
313
  function maxScrollOf(node: HTMLElement): number {
248
- return Math.max(0, node.scrollHeight - node.clientHeight);
314
+ // Browser layout guarantees scrollHeight is at least clientHeight.
315
+ return node.scrollHeight - node.clientHeight;
249
316
  }
250
317
 
251
318
  /**
@@ -334,12 +401,16 @@ export function MessageTimeline({
334
401
  className,
335
402
  }: MessageTimelineProps) {
336
403
  const resolvedItems = useMemo(() => {
337
- const projected = items ?? buildTimeline(events ?? []);
404
+ const projectedItems = items ?? buildTimeline(events ?? []);
338
405
  if (!shouldRenderAuthNeeded) {
339
- return projected;
406
+ return projectedItems;
340
407
  }
341
- return projected.filter((item) => item.kind !== "auth-needed" || shouldRenderAuthNeeded(item));
408
+ return projectedItems.filter(
409
+ (item) => item.kind !== "auth-needed" || shouldRenderAuthNeeded(item),
410
+ );
342
411
  }, [items, events, shouldRenderAuthNeeded]);
412
+ const sourceItems = items || events;
413
+ const olderBoundaryKey = sourceItems?.[0]?.id;
343
414
  const allGroups = useMemo(() => groupTimeline(resolvedItems), [resolvedItems]);
344
415
  const annotationSources = useMemo(() => {
345
416
  const sources = new Map<string, TimelineAnnotationSourceDescriptor>();
@@ -367,10 +438,18 @@ export function MessageTimeline({
367
438
  // history forever while the reader sits at the tip. Arm on first scroll-up.
368
439
  const olderPrefetchArmedRef = useRef(false);
369
440
  const [olderPrefetchArmed, setOlderPrefetchArmed] = useState(false);
370
- // One loadOlder per visit to the top band. Re-arm only after the reader
371
- // leaves that band (scrolls down / sentinel exits) never from scrolling
372
- // further toward y=0 (that was the batch-top load loop).
373
- const olderLoadGateRef = useRef<"armed" | "cooling">("armed");
441
+ // A collapsed history tail can be shorter than the viewport. In that state
442
+ // there is no upward scroll range, so reader intent can never arm the top
443
+ // sentinel. Request one older page per loaded window until history either
444
+ // fills the viewport or the host reports that no older rows remain. Each
445
+ // attempt owns the oldest loaded item boundary across BOTH the automatic
446
+ // underfill and reader-driven sentinel paths. Live-tail appends do not
447
+ // advance older pagination or release that exact owner.
448
+ const olderLoadAttemptRef = useRef<OlderLoadAttempt | null>(null);
449
+ const [underfillSettledAttempt, setUnderfillSettledAttempt] = useState<OlderLoadAttempt | null>(
450
+ null,
451
+ );
452
+ const underfillRetryReadyRef = useRef(false);
374
453
  const resizeFollowRafRef = useRef<number | null>(null);
375
454
  const firstGroupKey = allGroups[0] ? timelineGroupKey(allGroups[0]) : null;
376
455
  // Content stays invisible until the tip is hard-parked across a short
@@ -444,9 +523,21 @@ export function MessageTimeline({
444
523
  const foldMemoryRef = useRef<Map<string, FoldRestingState>>(new Map());
445
524
  const userMessageDisclosureMemoryRef = useRef<Map<string, boolean>>(new Map());
446
525
  const seenActivityIdsRef = useRef<Set<string>>(new Set());
447
- const groupKeyByItemIdRef = useRef<Map<string, string>>(new Map());
526
+ const firstItemGroupKeyRef = useRef<string | null>(null);
448
527
  const groupOffsetByKeyRef = useRef<Map<string, number>>(new Map());
528
+ const firstItemContentTopRef = useRef<number | null>(null);
449
529
  const firstItemId = resolvedItems[0]?.id ?? null;
530
+ // Pagination ownership follows the oldest committed input, before host
531
+ // filtering and grouping. A page containing only suppressed auth notices
532
+ // still advances this receipt, while live-tail appends leave it unchanged.
533
+ // Promise fulfillment alone does neither, so delayed prepends stay fenced.
534
+ const underfillRetryReady = (underfillRetryReadyRef.current = !!(
535
+ underfillSettledAttempt &&
536
+ underfillSettledAttempt === olderLoadAttemptRef.current &&
537
+ hasOlder &&
538
+ onLoadOlder &&
539
+ !loadingOlder
540
+ ));
450
541
  // Bulk paints (the initial tail window, a prepended older window — detected
451
542
  // by the first group key changing) must not run per-row entrance animations.
452
543
  const firstKeyChangedForBulk =
@@ -464,6 +555,15 @@ export function MessageTimeline({
464
555
  }
465
556
  }, []);
466
557
 
558
+ const rearmOlderPrefetchAfterLeavingTop = useCallback((node: HTMLElement) => {
559
+ if (node.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
560
+ const state = olderLoadAttemptRef.current?.[1];
561
+ if (state === 2 || state === 3) {
562
+ olderLoadAttemptRef.current = null;
563
+ }
564
+ }
565
+ }, []);
566
+
467
567
  const revealedRef = useRef(revealed);
468
568
  revealedRef.current = revealed;
469
569
 
@@ -495,7 +595,7 @@ export function MessageTimeline({
495
595
  }, []);
496
596
 
497
597
  const cancelLeaveFallback = useCallback(() => {
498
- if (leaveFallbackRafRef.current !== null) {
598
+ if (leaveFallbackRafRef.current != null) {
499
599
  cancelFrame(leaveFallbackRafRef.current);
500
600
  leaveFallbackRafRef.current = null;
501
601
  }
@@ -508,7 +608,7 @@ export function MessageTimeline({
508
608
 
509
609
  const stopFollow = useCallback(() => {
510
610
  followRef.current = tipFollowCancel(followRef.current);
511
- if (followFrameRef.current !== null) {
611
+ if (followFrameRef.current != null) {
512
612
  cancelFrame(followFrameRef.current);
513
613
  followFrameRef.current = null;
514
614
  }
@@ -551,7 +651,7 @@ export function MessageTimeline({
551
651
  if (programmaticScrollRef.current > 0) {
552
652
  return;
553
653
  }
554
- if (followRef.current.running || followFrameRef.current !== null) {
654
+ if (followRef.current.running || followFrameRef.current != null) {
555
655
  return;
556
656
  }
557
657
  if (isNearBottom(node) || maxScrollOf(node) <= 1) {
@@ -559,11 +659,9 @@ export function MessageTimeline({
559
659
  return;
560
660
  }
561
661
  releasePinFromReader(node);
562
- if (olderLoadGateRef.current === "cooling" && node.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
563
- olderLoadGateRef.current = "armed";
564
- }
662
+ rearmOlderPrefetchAfterLeavingTop(node);
565
663
  },
566
- [autoFollow, clearPendingReaderLeave, releasePinFromReader],
664
+ [autoFollow, clearPendingReaderLeave, rearmOlderPrefetchAfterLeavingTop, releasePinFromReader],
567
665
  );
568
666
 
569
667
  const scheduleLeaveFallback = useCallback(() => {
@@ -613,7 +711,7 @@ export function MessageTimeline({
613
711
  target: EventTarget | null;
614
712
  }) => {
615
713
  // Primary button / touch / pen only. Ignore right-click etc.
616
- if (event.button !== 0 && event.pointerType === "mouse") {
714
+ if (event.button && event.pointerType === "mouse") {
617
715
  return;
618
716
  }
619
717
  // Clicks on chips/buttons/links must not arm — their settle collapse
@@ -722,7 +820,62 @@ export function MessageTimeline({
722
820
  [beginUserMessageDisclosureChange],
723
821
  );
724
822
 
725
- const driveFollowRef = useRef<(node: HTMLElement, now?: number) => void>(() => undefined);
823
+ const requestOlderIfUnderfilled = useCallback(
824
+ (node: HTMLElement, retry?: OlderLoadAttempt) => {
825
+ let currentAttempt = olderLoadAttemptRef.current;
826
+ if (retry && (currentAttempt !== retry || retry[0] !== olderBoundaryKey)) {
827
+ // AnimatePresence may retain the exiting button briefly. Its stale
828
+ // handler cannot replace a newer exact owner.
829
+ return;
830
+ }
831
+ const underfilled = maxScrollOf(node) <= 1;
832
+ if (!retry && underfilled && currentAttempt?.[1] === 3) {
833
+ // A receipted prefetch already declined or failed while the viewport
834
+ // was still scrollable. Collapse may later remove the scroll range;
835
+ // promote that exact owner to Retry without issuing another request.
836
+ currentAttempt[1] = 0;
837
+ setUnderfillSettledAttempt(currentAttempt);
838
+ return;
839
+ }
840
+ if (!retry && underfilled && currentAttempt?.[1] === 2) {
841
+ // A settled ordinary prefetch owns only this visit to the top band.
842
+ // If its resulting window cannot scroll, yield to automatic underfill
843
+ // so the reader is not stranded behind a cooldown they cannot exit.
844
+ currentAttempt = olderLoadAttemptRef.current = null;
845
+ }
846
+ // clientHeight=0 is pre-layout/headless, not evidence that the rendered
847
+ // history underfills a real viewport.
848
+ if (
849
+ node.clientHeight <= 1 ||
850
+ (!retry && !underfilled) ||
851
+ !hasOlder ||
852
+ loadingOlder ||
853
+ !onLoadOlder ||
854
+ (currentAttempt && !retry)
855
+ ) {
856
+ return;
857
+ }
858
+ const attempt: OlderLoadAttempt = (olderLoadAttemptRef.current = [olderBoundaryKey]);
859
+ setUnderfillSettledAttempt(null);
860
+ // Every underfill request owns the ordinary sentinel too, even if reader
861
+ // intent had not armed it yet when this short-window request began.
862
+ const noProgress = () => {
863
+ if (scrollRef.current && olderLoadAttemptRef.current === attempt) {
864
+ setUnderfillSettledAttempt(attempt);
865
+ }
866
+ };
867
+ // Legacy fire-and-forget callbacks keep the one-shot behavior. Hosts
868
+ // that return the real promise opt into safe rejection/no-progress retry;
869
+ // exact `false` is the first-party request-not-accepted receipt.
870
+ // All other fulfillment retains this exact owner until its prepend
871
+ // boundary commits; promise settlement alone cannot prove progress.
872
+ invokeOlderLoad(onLoadOlder, noProgress, attempt);
873
+ },
874
+ [hasOlder, loadingOlder, olderBoundaryKey, onLoadOlder],
875
+ );
876
+ const driveFollowRef = useRef<(node: HTMLElement, now?: number) => void>(
877
+ requestOlderIfUnderfilled as (node: HTMLElement, now?: number) => void,
878
+ );
726
879
  const driveFollow = useCallback(
727
880
  (node: HTMLElement, nowMs?: number) => {
728
881
  if (!pinnedRef.current || hasNewerRef.current) {
@@ -780,7 +933,7 @@ export function MessageTimeline({
780
933
  // A direct glue write re-based the camera — drop any stale fraction.
781
934
  cameraTop: null,
782
935
  };
783
- if (followFrameRef.current === null) {
936
+ if (followFrameRef.current == null) {
784
937
  followFrameRef.current = requestFrame((frameNow) => {
785
938
  followFrameRef.current = null;
786
939
  const current = scrollRef.current;
@@ -814,7 +967,7 @@ export function MessageTimeline({
814
967
  syncScrollBaseline(node);
815
968
  if (result.state.running) {
816
969
  cancelLeaveFallback();
817
- if (followFrameRef.current === null) {
970
+ if (followFrameRef.current == null) {
818
971
  followFrameRef.current = requestFrame((frameNow) => {
819
972
  followFrameRef.current = null;
820
973
  const current = scrollRef.current;
@@ -823,7 +976,7 @@ export function MessageTimeline({
823
976
  }
824
977
  });
825
978
  }
826
- } else if (followFrameRef.current !== null) {
979
+ } else if (followFrameRef.current != null) {
827
980
  cancelFrame(followFrameRef.current);
828
981
  followFrameRef.current = null;
829
982
  }
@@ -846,12 +999,65 @@ export function MessageTimeline({
846
999
  return;
847
1000
  }
848
1001
  const previousFirstItemId = previousFirstItemIdRef.current;
849
- const previousGroupKeyByItemId = groupKeyByItemIdRef.current;
1002
+ const previousFirstItemGroupKey = firstItemGroupKeyRef.current;
850
1003
  const previousGroupOffsetByKey = groupOffsetByKeyRef.current;
1004
+ const previousItemContentTop = firstItemContentTopRef.current;
851
1005
  const previousScrollTop = lastScrollTopRef.current;
852
- const firstItemChanged = previousFirstItemId !== null && firstItemId !== previousFirstItemId;
1006
+ const previousMaxScroll = lastMaxScrollRef.current;
1007
+ const wasAtLiveTailBeforeCommit =
1008
+ previousMaxScroll <= 1 ||
1009
+ previousMaxScroll - previousScrollTop < Math.min(PIN_THRESHOLD_PX, previousMaxScroll);
1010
+ const firstItemChanged = !!previousFirstItemId && firstItemId !== previousFirstItemId;
853
1011
  const prepended =
854
1012
  firstItemChanged && resolvedItems.some((item) => item.id === previousFirstItemId);
1013
+ const attempt = olderLoadAttemptRef.current;
1014
+ const committedZeroOverlapOlderReplacement = !!(
1015
+ attempt?.[2]?.committed &&
1016
+ firstItemChanged &&
1017
+ !prepended
1018
+ );
1019
+ const restorePrependAnchor = () => {
1020
+ // Keep the reader on the same retained rows. Prefer the exact first-item
1021
+ // content coordinate (needed when a prepend merges inside one group),
1022
+ // then the retained group offset, then scrollHeight as a final fallback.
1023
+ // If native anchoring already applied the same shift, leave scrollTop.
1024
+ let delta: number | null = null;
1025
+ if (previousFirstItemId && previousItemContentTop != null) {
1026
+ const itemEl = node.querySelector(
1027
+ `[data-og-item="${cssEscapeAttribute(previousFirstItemId)}"]`,
1028
+ );
1029
+ if (itemEl instanceof HTMLElement) {
1030
+ const scrollerTop = node.getBoundingClientRect().top;
1031
+ const currentItemTop = itemEl.getBoundingClientRect().top - scrollerTop + node.scrollTop;
1032
+ delta = Math.round(currentItemTop - previousItemContentTop);
1033
+ }
1034
+ }
1035
+ const anchorKey = previousFirstItemGroupKey;
1036
+ const previousAnchorTop =
1037
+ anchorKey != null ? previousGroupOffsetByKey.get(anchorKey) : undefined;
1038
+ const anchorEl =
1039
+ anchorKey != null
1040
+ ? node.querySelector(`[data-og-group-key="${cssEscapeAttribute(anchorKey)}"]`)
1041
+ : null;
1042
+ if (delta == null && anchorEl instanceof HTMLElement && previousAnchorTop !== undefined) {
1043
+ const moved = Math.round(anchorEl.offsetTop - previousAnchorTop);
1044
+ if (moved) {
1045
+ delta = moved;
1046
+ }
1047
+ }
1048
+ if (delta == null) {
1049
+ const heightDelta = Math.round(node.scrollHeight - previousScrollHeightRef.current);
1050
+ if (heightDelta > 0) {
1051
+ delta = heightDelta;
1052
+ }
1053
+ }
1054
+ if (delta != null) {
1055
+ const expected = previousScrollTop + delta;
1056
+ if (Math.abs(node.scrollTop - expected) > 2) {
1057
+ writeScrollTop(node, expected);
1058
+ }
1059
+ }
1060
+ };
855
1061
  if (pendingJumpToStartRef.current && firstItemChanged) {
856
1062
  // The oldest window landed — jump against the NEW DOM, and skip the
857
1063
  // prepend correction (it would shift the reader away from the top).
@@ -867,6 +1073,42 @@ export function MessageTimeline({
867
1073
  applyPinned(true);
868
1074
  snapToBottom(node);
869
1075
  }
1076
+ } else if (committedZeroOverlapOlderReplacement) {
1077
+ // An oldest-directed bounded page can replace every previously mounted
1078
+ // row. Its receipt is the only causal proof that this is backward
1079
+ // progress rather than live-tail forward eviction. Anchor at the bottom
1080
+ // seam before recording this window's scroll baselines so an unpinned
1081
+ // reader stays adjacent to the history they were reading.
1082
+ clearPendingReaderLeave();
1083
+ stopFollow();
1084
+ writeScrollTop(node, maxScrollOf(node));
1085
+ if (attempt?.[1] === 1 && pinnedRef.current) {
1086
+ applyPinned(false);
1087
+ }
1088
+ } else if (prepended) {
1089
+ if (
1090
+ autoFollow &&
1091
+ pinnedRef.current &&
1092
+ !hasNewer &&
1093
+ !pendingReaderLeaveRef.current &&
1094
+ (wasAtLiveTailBeforeCommit || olderLoadAttemptRef.current)
1095
+ ) {
1096
+ // A pending short-window load owns the prepend even when live growth
1097
+ // has left its still-pinned camera with raw geometry debt. Unrelated
1098
+ // prepends retain the prior-tip fallback so a stale pin after an
1099
+ // extension/programmatic history jump still restores its row anchor.
1100
+ clearPendingReaderLeave();
1101
+ snapToBottom(node);
1102
+ } else {
1103
+ restorePrependAnchor();
1104
+ if (autoFollow && pinnedRef.current && !hasNewer) {
1105
+ // Geometry or a pending extension/programmatic leave proves the
1106
+ // reader was browsing history even if the pin ref has not settled.
1107
+ stopFollow();
1108
+ clearPendingReaderLeave();
1109
+ applyPinned(false);
1110
+ }
1111
+ }
870
1112
  } else if (autoFollow && pinnedRef.current && !hasNewer) {
871
1113
  // Load/remount (still hidden): hard-park. Live tip after reveal: ease.
872
1114
  // Pending unarmed leave: tip *growth* must not yank (Vimium during stream).
@@ -881,66 +1123,25 @@ export function MessageTimeline({
881
1123
  } else {
882
1124
  driveFollow(node);
883
1125
  }
884
- } else if (prepended) {
885
- // Keep the reader on the same retained rows. Prefer the offsetTop delta
886
- // of the group that still holds the previous first item — that stays
887
- // correct when loadOlder also truncates the tip (height delta then lies).
888
- // If native anchoring already applied the same shift, leave scrollTop.
889
- const anchorKey =
890
- previousFirstItemId !== null
891
- ? previousGroupKeyByItemId.get(previousFirstItemId)
892
- : undefined;
893
- const previousAnchorTop =
894
- anchorKey !== undefined ? previousGroupOffsetByKey.get(anchorKey) : undefined;
895
- const anchorEl =
896
- anchorKey !== undefined
897
- ? node.querySelector(`[data-og-group-key="${cssEscapeAttribute(anchorKey)}"]`)
898
- : null;
899
- let delta: number | null = null;
900
- if (anchorEl instanceof HTMLElement && previousAnchorTop !== undefined) {
901
- const moved = Math.round(anchorEl.offsetTop - previousAnchorTop);
902
- if (moved !== 0) {
903
- delta = moved;
904
- }
905
- }
906
- if (delta === null) {
907
- const heightDelta = Math.round(node.scrollHeight - previousScrollHeightRef.current);
908
- if (heightDelta > 0) {
909
- delta = heightDelta;
910
- }
911
- }
912
- if (delta !== null) {
913
- const expected = previousScrollTop + delta;
914
- if (Math.abs(node.scrollTop - expected) > 2) {
915
- writeScrollTop(node, expected);
916
- }
917
- }
918
1126
  }
919
1127
  // After a prepend, if restore left us below the top prefetch band,
920
1128
  // re-arm so a later approach can load again. Still cooling while parked
921
1129
  // inside the band (short pages) — that stops the y=0 load loop.
922
- if (
923
- prepended &&
924
- !pinnedRef.current &&
925
- olderLoadGateRef.current === "cooling" &&
926
- node.scrollTop > OLDER_PREFETCH_MARGIN_PX
927
- ) {
928
- olderLoadGateRef.current = "armed";
1130
+ if (prepended && !pinnedRef.current && node.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
1131
+ rearmOlderPrefetchAfterLeavingTop(node);
929
1132
  }
930
1133
  previousFirstItemIdRef.current = firstItemId;
931
1134
  previousScrollHeightRef.current = node.scrollHeight;
932
1135
  syncScrollBaseline(node);
933
- // Item→group keys are cheap (data only). offsetTop queries are O(groups)
934
- // layout reads skip while pinned at the live tip (every stream token
935
- // used to remeasure the whole timeline; that was the long-run lag).
936
- const nextKeyByItemId = new Map<string, string>();
937
- for (const { group, key } of groups) {
938
- for (const itemId of timelineGroupItemIds(group)) {
939
- nextKeyByItemId.set(itemId, key);
940
- }
941
- }
942
- groupKeyByItemIdRef.current = nextKeyByItemId;
943
- const needOffsets = prepended || firstItemChanged || !pinnedRef.current || Boolean(hasNewer);
1136
+ // offsetTop queries are O(groups); skip while pinned at the live tip
1137
+ // (every stream token used to remeasure the whole timeline).
1138
+ firstItemGroupKeyRef.current = groups[0]?.key ?? null;
1139
+ const needOffsets =
1140
+ prepended ||
1141
+ firstItemChanged ||
1142
+ !pinnedRef.current ||
1143
+ hasNewer ||
1144
+ firstItemContentTopRef.current == null;
944
1145
  if (needOffsets) {
945
1146
  const nextOffsetByKey = new Map<string, number>();
946
1147
  for (const { key } of groups) {
@@ -950,14 +1151,52 @@ export function MessageTimeline({
950
1151
  }
951
1152
  }
952
1153
  groupOffsetByKeyRef.current = nextOffsetByKey;
1154
+ const firstItemEl = firstItemId
1155
+ ? node.querySelector(`[data-og-item="${cssEscapeAttribute(firstItemId)}"]`)
1156
+ : null;
1157
+ firstItemContentTopRef.current =
1158
+ firstItemId && firstItemEl instanceof HTMLElement
1159
+ ? firstItemEl.getBoundingClientRect().top -
1160
+ node.getBoundingClientRect().top +
1161
+ node.scrollTop
1162
+ : null;
1163
+ }
1164
+
1165
+ // Promise settlement is not itself permission to retry. A receipt-marked
1166
+ // accepted page retires its exact owner on this commit even when projection
1167
+ // is empty or merges into the same first item. Without that mark, retain
1168
+ // the compatibility fallback: a retained prior boundary proves prepend,
1169
+ // while a missing prior boundary is forward eviction and merely rebases.
1170
+ if (!attempt) {
1171
+ return;
1172
+ }
1173
+ if (!attempt[2]?.committed && attempt[0] === olderBoundaryKey) {
1174
+ return;
1175
+ }
1176
+ if (
1177
+ !attempt[2]?.committed &&
1178
+ attempt[0] &&
1179
+ !sourceItems?.find((entry) => entry.id === attempt[0])
1180
+ ) {
1181
+ attempt[0] = olderBoundaryKey;
1182
+ return;
1183
+ }
1184
+ if (!attempt[1] || !hasOlder) {
1185
+ setUnderfillSettledAttempt((olderLoadAttemptRef.current = null));
1186
+ return;
953
1187
  }
1188
+ // Boundary progress retires the request owner. A late settlement from
1189
+ // that completed prefetch cannot mutate the new window's cooldown owner.
1190
+ olderLoadAttemptRef.current = [olderBoundaryKey, 2];
1191
+ rearmOlderPrefetchAfterLeavingTop(node);
1192
+ requestOlderIfUnderfilled(node);
954
1193
  });
955
1194
 
956
1195
  // First paint / session remount: keep the scroller hidden, snap to tip for
957
1196
  // two animation frames (late sync layout), then reveal. Does not change the
958
- // tip-follow ease law used once `revealed` is true.
1197
+ // live tip-follow law used once `revealed` is true.
959
1198
  useLayoutEffect(() => {
960
- if (revealed || allGroups.length === 0) {
1199
+ if (revealed || !allGroups.length) {
961
1200
  return;
962
1201
  }
963
1202
  let cancelled = false;
@@ -985,7 +1224,7 @@ export function MessageTimeline({
985
1224
  return () => {
986
1225
  cancelled = true;
987
1226
  cancelFrame(frame1);
988
- if (frame2 !== 0) {
1227
+ if (frame2) {
989
1228
  cancelFrame(frame2);
990
1229
  }
991
1230
  };
@@ -1004,7 +1243,6 @@ export function MessageTimeline({
1004
1243
  olderPrefetchArmedRef.current = false;
1005
1244
  setOlderPrefetchArmed(false);
1006
1245
  }
1007
- olderLoadGateRef.current = "armed";
1008
1246
  wantPinRef.current = false;
1009
1247
  pendingJumpToStartRef.current = false;
1010
1248
  previousFirstItemIdRef.current = null;
@@ -1013,8 +1251,9 @@ export function MessageTimeline({
1013
1251
  lastMaxScrollRef.current = 0;
1014
1252
  lastScrollHeightRef.current = 0;
1015
1253
  lastClientHeightRef.current = 0;
1016
- groupKeyByItemIdRef.current = new Map();
1254
+ firstItemGroupKeyRef.current = null;
1017
1255
  groupOffsetByKeyRef.current = new Map();
1256
+ firstItemContentTopRef.current = null;
1018
1257
  foldMemoryRef.current.clear();
1019
1258
  userMessageDisclosureMemoryRef.current.clear();
1020
1259
  disclosureKeepsUnpinnedRef.current = false;
@@ -1022,6 +1261,16 @@ export function MessageTimeline({
1022
1261
  applyPinned(true);
1023
1262
  }, [allGroups.length, revealed, applyPinned]);
1024
1263
 
1264
+ // Parent commits cover the initial/history-loading cases. Disclosure state
1265
+ // changes are child-local, so the ResizeObserver below owns dynamic collapse
1266
+ // and expansion after mount.
1267
+ useEffect(() => {
1268
+ const node = scrollRef.current;
1269
+ if (node) {
1270
+ requestOlderIfUnderfilled(node);
1271
+ }
1272
+ });
1273
+
1025
1274
  // Clear the bulk-paint marker a frame after it renders, so rows appended
1026
1275
  // live (streams, new turns) animate exactly as before.
1027
1276
  useLayoutEffect(() => {
@@ -1056,21 +1305,60 @@ export function MessageTimeline({
1056
1305
  (entries) => {
1057
1306
  const intersecting = entries.some((entry) => entry.isIntersecting);
1058
1307
  if (!intersecting) {
1059
- // Left the top band next approach may load once.
1060
- olderLoadGateRef.current = "armed";
1308
+ // Left the top band. A settled ordinary prefetch releases its exact
1309
+ // owner here; a still-pending request remains cooling so a quick
1310
+ // leave/re-enter cannot overlap it.
1311
+ const attempt = olderLoadAttemptRef.current;
1312
+ if (attempt?.[1] === 2 || attempt?.[1] === 3) {
1313
+ olderLoadAttemptRef.current = null;
1314
+ }
1061
1315
  return;
1062
1316
  }
1063
- if (olderLoadGateRef.current !== "armed") {
1317
+ // Both automatic underfill and ordinary prefetch use one boundary
1318
+ // owner. In particular, live growth cannot arm a second sentinel load
1319
+ // while the short-window request that preceded it is still pending.
1320
+ if (olderLoadAttemptRef.current) {
1064
1321
  return;
1065
1322
  }
1066
- olderLoadGateRef.current = "cooling";
1067
- onLoadOlder();
1323
+ const attempt: OlderLoadAttempt = (olderLoadAttemptRef.current = [olderBoundaryKey, 1]);
1324
+ const noProgress = () => {
1325
+ if (!scrollRef.current || olderLoadAttemptRef.current !== attempt) {
1326
+ return;
1327
+ }
1328
+ attempt[1] = 3;
1329
+ if (root.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
1330
+ olderLoadAttemptRef.current = null;
1331
+ } else if (maxScrollOf(root) <= 1) {
1332
+ attempt[1] = 0;
1333
+ setUnderfillSettledAttempt(attempt);
1334
+ }
1335
+ };
1336
+ // Preserve legacy fire-and-forget top-band retries: the callback has
1337
+ // synchronously returned, but this visit remains cooling until exit.
1338
+ // Only rejection/throw/exact `false` is a no-progress receipt. Other
1339
+ // fulfillment retains the pending owner until boundary commit;
1340
+ // otherwise a delayed prepend could overlap a same-boundary request.
1341
+ if (
1342
+ !invokeOlderLoad(onLoadOlder, noProgress, attempt) &&
1343
+ olderLoadAttemptRef.current === attempt
1344
+ ) {
1345
+ olderLoadAttemptRef.current[1] = 2;
1346
+ requestOlderIfUnderfilled(root);
1347
+ }
1068
1348
  },
1069
1349
  { root, rootMargin: OLDER_PREFETCH_ROOT_MARGIN },
1070
1350
  );
1071
1351
  observer.observe(target);
1072
1352
  return () => observer.disconnect();
1073
- }, [olderPrefetchArmed, hasOlder, loadingOlder, onLoadOlder, firstGroupKey]);
1353
+ }, [
1354
+ firstGroupKey,
1355
+ hasOlder,
1356
+ loadingOlder,
1357
+ olderBoundaryKey,
1358
+ olderPrefetchArmed,
1359
+ onLoadOlder,
1360
+ requestOlderIfUnderfilled,
1361
+ ]);
1074
1362
 
1075
1363
  // History view: page forward when the reader nears the bottom of the current
1076
1364
  // non-tip window. Does not pull the whole gap — one density-bounded page.
@@ -1110,7 +1398,7 @@ export function MessageTimeline({
1110
1398
  return;
1111
1399
  }
1112
1400
  const observer = new ResizeObserver(() => {
1113
- if (resizeFollowRafRef.current !== null) {
1401
+ if (resizeFollowRafRef.current != null) {
1114
1402
  return;
1115
1403
  }
1116
1404
  resizeFollowRafRef.current = requestFrame(() => {
@@ -1119,6 +1407,7 @@ export function MessageTimeline({
1119
1407
  if (!current) {
1120
1408
  return;
1121
1409
  }
1410
+ requestOlderIfUnderfilled(current);
1122
1411
  if (!autoFollow || !pinnedRef.current || hasNewerRef.current) {
1123
1412
  return;
1124
1413
  }
@@ -1136,12 +1425,12 @@ export function MessageTimeline({
1136
1425
  observer.observe(node);
1137
1426
  return () => {
1138
1427
  observer.disconnect();
1139
- if (resizeFollowRafRef.current !== null) {
1428
+ if (resizeFollowRafRef.current != null) {
1140
1429
  cancelFrame(resizeFollowRafRef.current);
1141
1430
  resizeFollowRafRef.current = null;
1142
1431
  }
1143
1432
  };
1144
- }, [autoFollow, driveFollow, snapToBottom]);
1433
+ }, [autoFollow, driveFollow, requestOlderIfUnderfilled, snapToBottom]);
1145
1434
 
1146
1435
  // Entering a non-tip history window: drop any live pin so the page bottom
1147
1436
  // cannot re-stick follow across loadNewer. Leaving it (the tip window
@@ -1237,12 +1526,11 @@ export function MessageTimeline({
1237
1526
  if (readerArmed && readerUp > TIP_FOLLOW_READER_UP_EPS_PX && !nearBottomPinned) {
1238
1527
  readerIntentArmRef.current = false;
1239
1528
  releasePinFromReader(node);
1240
- if (olderLoadGateRef.current === "cooling" && node.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
1241
- olderLoadGateRef.current = "armed";
1242
- }
1529
+ rearmOlderPrefetchAfterLeavingTop(node);
1243
1530
  return;
1244
1531
  }
1245
- // Tip grew under a still viewport (no reader-up): ease back to the tip.
1532
+ // Tip grew under a still viewport (no reader-up): track the new growth
1533
+ // immediately and ease only any debt that already existed.
1246
1534
  // Reader/extension scroll-up in progress: do not yank — scrollend decides.
1247
1535
  // Near-bottom with tipDebt≈0 stays the quiet path (do not broaden follow
1248
1536
  // inside PIN_THRESHOLD — that fights small intentional scroll-ups).
@@ -1289,9 +1577,7 @@ export function MessageTimeline({
1289
1577
  }
1290
1578
  // Re-arm older prefetch only after leaving the top band (scroll down into
1291
1579
  // content). Never re-arm/load from continued scroll toward y=0.
1292
- if (olderLoadGateRef.current === "cooling" && node.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
1293
- olderLoadGateRef.current = "armed";
1294
- }
1580
+ rearmOlderPrefetchAfterLeavingTop(node);
1295
1581
  };
1296
1582
 
1297
1583
  const onScrollEnd = () => {
@@ -1355,7 +1641,7 @@ export function MessageTimeline({
1355
1641
  )}
1356
1642
  >
1357
1643
  <div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
1358
- {groups.length === 0
1644
+ {!groups.length
1359
1645
  ? (emptyState ?? (
1360
1646
  <p className="py-10 text-center text-og-menu text-og-fg-subtle">
1361
1647
  No activity yet.
@@ -1442,7 +1728,8 @@ export function MessageTimeline({
1442
1728
  <AnimatePresence>
1443
1729
  {loadingOlder ||
1444
1730
  loadingOldest ||
1445
- (hasOlder && onJumpToStart && olderPrefetchArmed) ? (
1731
+ (hasOlder && onJumpToStart && olderPrefetchArmed) ||
1732
+ underfillRetryReady ? (
1446
1733
  // Floating over the scroller (not a timeline row) so showing and
1447
1734
  // hiding it never reflows history under the reader.
1448
1735
  <motion.div
@@ -1452,26 +1739,38 @@ export function MessageTimeline({
1452
1739
  transition={{ duration: 0.15, ease: "easeOut" }}
1453
1740
  data-og-loading-older=""
1454
1741
  aria-live="polite"
1455
- className="absolute inset-x-0 top-3 z-10 flex justify-center gap-2"
1742
+ className="absolute inset-x-0 top-3 z-10 flex justify-center"
1456
1743
  >
1457
1744
  {loadingOlder || loadingOldest ? (
1458
- <span className="pointer-events-none inline-flex items-center rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1 text-og-control font-medium shadow-og-md backdrop-blur">
1745
+ <span className={LOADING_CHIP_CLASS}>
1459
1746
  <span className="og-shimmer-text">
1460
1747
  {loadingOldest ? "Jumping to start…" : "Loading earlier activity…"}
1461
1748
  </span>
1462
1749
  </span>
1463
1750
  ) : null}
1464
- {hasOlder && onJumpToStart && !loadingOldest ? (
1751
+ {hasOlder &&
1752
+ !loadingOlder &&
1753
+ !loadingOldest &&
1754
+ (underfillRetryReady || onJumpToStart) ? (
1465
1755
  <button
1466
1756
  type="button"
1467
- data-og-jump-to-start=""
1468
- disabled={loadingOlder}
1757
+ data-og-retry={underfillRetryReady || undefined}
1758
+ data-og-jump-to-start={!underfillRetryReady || undefined}
1469
1759
  onClick={() => {
1760
+ const node = scrollRef.current;
1761
+ if (underfillRetryReady) {
1762
+ if (node && underfillRetryReadyRef.current) {
1763
+ // AnimatePresence retains this handler during
1764
+ // exit. Current authorization plus the exact
1765
+ // attempt check prevent stale dispatch.
1766
+ requestOlderIfUnderfilled(node, underfillSettledAttempt!);
1767
+ }
1768
+ return;
1769
+ }
1470
1770
  applyPinned(false);
1471
1771
  pendingJumpToStartRef.current = true;
1472
1772
  const seq = ++jumpToStartSeqRef.current;
1473
- const node = scrollRef.current;
1474
- void Promise.resolve(onJumpToStart()).then(
1773
+ void Promise.resolve(onJumpToStart!()).then(
1475
1774
  () => {
1476
1775
  // The commit that swaps in the oldest window consumes
1477
1776
  // the flag against the new DOM; this write covers the
@@ -1500,14 +1799,9 @@ export function MessageTimeline({
1500
1799
  },
1501
1800
  );
1502
1801
  }}
1503
- className={cn(
1504
- "inline-flex items-center gap-1.5 rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1.5",
1505
- "text-og-control font-medium text-og-fg shadow-og-md backdrop-blur",
1506
- "hover:border-og-border-strong disabled:opacity-60",
1507
- )}
1802
+ className="rounded-full border border-og-border px-3 py-1.5 text-og-control"
1508
1803
  >
1509
- <ArrowUpToLineIcon className="size-3.5" />
1510
- Jump to start
1804
+ {underfillRetryReady ? "Retry earlier activity" : "Jump to start"}
1511
1805
  </button>
1512
1806
  ) : null}
1513
1807
  </motion.div>
@@ -1524,7 +1818,7 @@ export function MessageTimeline({
1524
1818
  aria-live="polite"
1525
1819
  className="pointer-events-none absolute inset-x-0 bottom-14 z-10 flex justify-center"
1526
1820
  >
1527
- <span className="inline-flex items-center rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1 text-og-control font-medium shadow-og-md backdrop-blur">
1821
+ <span className={LOADING_CHIP_CLASS}>
1528
1822
  <span className="og-shimmer-text">Loading later activity…</span>
1529
1823
  </span>
1530
1824
  </motion.div>
@@ -1810,8 +2104,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
1810
2104
  // turn that is the second layer — quiet nested chips under the outer turn
1811
2105
  // summary when contiguous activity naturally clusters (≥2 only).
1812
2106
  const activityShouldFold =
1813
- group.kind === "activity" &&
1814
- Boolean(group.outcome || (foldLiveCluster && clusterIsSettled(group)));
2107
+ group.kind === "activity" && !!(group.outcome || (foldLiveCluster && clusterIsSettled(group)));
1815
2108
  const containsGeneratedImage = timelineGroupContainsGeneratedImage(group);
1816
2109
  // Latch live→folded so a top-level shell that was already mounted open can
1817
2110
  // start the settle beat without remounting bare rail → wrapper.
@@ -1832,7 +2125,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
1832
2125
  );
1833
2126
  }
1834
2127
  const settleFold =
1835
- group.kind === "turn" ? Boolean(enter && !insideTurn && !turnDefaultOpen) : liveActivitySettle;
2128
+ group.kind === "turn" ? !!(enter && !insideTurn && !turnDefaultOpen) : liveActivitySettle;
1836
2129
  switch (group.kind) {
1837
2130
  case "activity":
1838
2131
  if (insideTurn) {
@@ -2194,7 +2487,7 @@ export function trailingAgentTextAfterTurn(
2194
2487
  if (next?.kind === "item" && next.item.kind === "agent-message") {
2195
2488
  const turnIds = collectTurnIdsFromGroups(group.groups);
2196
2489
  // Degenerate body with no turnIds: do not guess — safer than lifting wrong.
2197
- if (turnIds.size === 0) {
2490
+ if (!turnIds.size) {
2198
2491
  return undefined;
2199
2492
  }
2200
2493
  if (!next.item.turnId || !turnIds.has(next.item.turnId)) {
@@ -2213,7 +2506,7 @@ function collectTurnCopyText(
2213
2506
  const parts = [collectAgentMessageText(groups), trailingAgentText?.trim() ?? ""].filter(
2214
2507
  (part) => part.length > 0,
2215
2508
  );
2216
- if (parts.length === 0) {
2509
+ if (!parts.length) {
2217
2510
  return undefined;
2218
2511
  }
2219
2512
  return parts.join("\n\n");
@@ -2298,11 +2591,11 @@ function CompactionRow({ item }: { item: ContextCompactionItem }) {
2298
2591
  const trigger =
2299
2592
  item.trigger && item.phase !== "started" ? COMPACTION_TRIGGER_LABEL[item.trigger] : null;
2300
2593
  const before =
2301
- item.estimatedTokensBefore !== null
2594
+ item.estimatedTokensBefore != null
2302
2595
  ? Math.round(item.estimatedTokensBefore).toLocaleString("en-US")
2303
2596
  : null;
2304
2597
  const after =
2305
- item.estimatedTokensAfter !== null
2598
+ item.estimatedTokensAfter != null
2306
2599
  ? Math.round(item.estimatedTokensAfter).toLocaleString("en-US")
2307
2600
  : null;
2308
2601
  const title =
@@ -2323,7 +2616,7 @@ function CompactionRow({ item }: { item: ContextCompactionItem }) {
2323
2616
  item.phase === "skipped" && item.skipReason === "summarization_failed"
2324
2617
  ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
2325
2618
  : item.phase === "started"
2326
- ? "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting"
2619
+ ? WAITING_PILL_CLASS
2327
2620
  : "border-og-border bg-og-surface-1 text-og-fg-muted";
2328
2621
  return (
2329
2622
  <div className={cn(enter && "animate-og-enter", "flex justify-center")}>
@@ -2403,7 +2696,7 @@ function UserMessageRow({
2403
2696
  className="w-fit max-w-full min-w-0"
2404
2697
  trailing={<MessageFooterTime occurredAt={item.occurredAt} />}
2405
2698
  >
2406
- <div className="w-fit max-w-full min-w-0 rounded-og-lg rounded-br-og-xs border border-og-border bg-og-surface-2 px-4 py-2.5 text-og-md leading-6 text-og-fg">
2699
+ <div className={MESSAGE_BUBBLE_CLASS}>
2407
2700
  {item.text ? (
2408
2701
  <div data-og-annotation-source-key={item.annotationSource?.eventId}>
2409
2702
  {renderMessageText ? (
@@ -2525,7 +2818,7 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
2525
2818
  className="w-fit max-w-[90%] min-w-0 sm:max-w-[82%]"
2526
2819
  trailing={<MessageFooterTime occurredAt={item.occurredAt} />}
2527
2820
  >
2528
- <div className="w-fit max-w-full min-w-0 rounded-og-lg rounded-br-og-xs border border-og-border bg-og-surface-2 px-4 py-2.5 text-og-md leading-6 text-og-fg">
2821
+ <div className={MESSAGE_BUBBLE_CLASS}>
2529
2822
  <p className="text-og-xs font-medium text-og-fg-subtle">{settledLabel}</p>
2530
2823
  {item.response.outcome === "answered" ? (
2531
2824
  <div className="mt-1.5 space-y-3">
@@ -2695,9 +2988,7 @@ function WorkerCompletionRow({
2695
2988
  // payloads can carry a leftover pausedReason/rationale from earlier in the
2696
2989
  // worker's life, and a "Worker completed" card must not show a pause section.
2697
2990
  const showPausedReason =
2698
- item.childStatus !== "failed" &&
2699
- item.goalStatus === "paused" &&
2700
- Boolean(item.pausedReason?.trim());
2991
+ item.childStatus !== "failed" && item.goalStatus === "paused" && !!item.pausedReason?.trim();
2701
2992
  const details: { label: string; value: string; muted?: boolean }[] = [
2702
2993
  ...(item.text.trim() ? [{ label: "Report", value: item.text.trim() }] : []),
2703
2994
  ...(item.evidence?.trim()
@@ -2836,14 +3127,14 @@ const GOAL_META: Record<GoalItem["action"], GoalMeta> = {
2836
3127
  },
2837
3128
  paused: {
2838
3129
  label: "Goal paused",
2839
- pill: "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting",
3130
+ pill: WAITING_PILL_CLASS,
2840
3131
  icon: PauseIcon,
2841
3132
  },
2842
3133
  resumed: { label: "Goal resumed", pill: NEUTRAL_PILL, icon: PlayIcon },
2843
3134
  cleared: { label: "Goal cleared", pill: NEUTRAL_PILL, icon: Trash2Icon },
2844
3135
  held: {
2845
3136
  label: "Goal held",
2846
- pill: "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting",
3137
+ pill: WAITING_PILL_CLASS,
2847
3138
  icon: PauseCircleIcon,
2848
3139
  },
2849
3140
  continuation: { label: "Continuing toward the goal", pill: NEUTRAL_PILL, icon: ArrowRightIcon },
@@ -3041,7 +3332,7 @@ function NoticeRow({ item }: { item: NoticeItem }) {
3041
3332
  item.tone === "failed"
3042
3333
  ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
3043
3334
  : item.tone === "waiting"
3044
- ? "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting"
3335
+ ? WAITING_PILL_CLASS
3045
3336
  : "border-og-border bg-og-surface-1 text-og-fg-muted";
3046
3337
  return (
3047
3338
  <div
@@ -3174,7 +3465,7 @@ function AuthNeededRow({
3174
3465
  onClick={() => void start()}
3175
3466
  disabled={busy}
3176
3467
  className={cn(
3177
- "inline-flex w-full shrink-0 items-center justify-center gap-1.5 rounded-og-md bg-og-accent px-3 py-1.5 text-og-menu font-medium text-og-accent-fg sm:w-auto",
3468
+ PRIMARY_ACTION_CLASS,
3178
3469
  "transition-colors hover:bg-og-accent-strong disabled:opacity-70 pointer-coarse:min-h-9",
3179
3470
  )}
3180
3471
  >
@@ -3187,7 +3478,7 @@ function AuthNeededRow({
3187
3478
  rel="noreferrer"
3188
3479
  target="_blank"
3189
3480
  className={cn(
3190
- "inline-flex w-full shrink-0 items-center justify-center gap-1.5 rounded-og-md bg-og-accent px-3 py-1.5 text-og-menu font-medium text-og-accent-fg sm:w-auto",
3481
+ PRIMARY_ACTION_CLASS,
3191
3482
  "transition-colors hover:bg-og-accent-strong pointer-coarse:min-h-9",
3192
3483
  )}
3193
3484
  >
@@ -3250,7 +3541,7 @@ function AuthProviderLogo({ src, label }: { src: string | null; label: string })
3250
3541
  `capabilityMonogram` so the reconnect tile reads like every other logo tile. */
3251
3542
  function monogram(label: string): string {
3252
3543
  const words = label.trim().split(/\s+/).filter(Boolean);
3253
- if (words.length === 0) {
3544
+ if (!words.length) {
3254
3545
  return "?";
3255
3546
  }
3256
3547
  if (words.length === 1) {