@opengeni/react 2.3.0-canary.0 → 2.5.0-canary.1

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 (46) hide show
  1. package/README.md +9 -2
  2. package/dist/{chunk-LS4POUTY.js → chunk-6H6S5V4Z.js} +250 -120
  3. package/dist/chunk-6H6S5V4Z.js.map +1 -0
  4. package/dist/{chunk-SE4W4WQ3.js → chunk-ATQJEWCA.js} +154 -114
  5. package/dist/chunk-ATQJEWCA.js.map +1 -0
  6. package/dist/{chunk-LTCHJ4MS.js → chunk-KSDXER4X.js} +21 -7
  7. package/dist/chunk-KSDXER4X.js.map +1 -0
  8. package/dist/{chunk-RSXJN73N.js → chunk-MXJGO7LE.js} +34 -1
  9. package/dist/chunk-MXJGO7LE.js.map +1 -0
  10. package/dist/{chunk-JOAVA2TW.js → chunk-T7VAYG2I.js} +8 -7
  11. package/dist/{chunk-JOAVA2TW.js.map → chunk-T7VAYG2I.js.map} +1 -1
  12. package/dist/components/composer.d.ts +1 -1
  13. package/dist/components/fleet-tile.d.ts +1 -1
  14. package/dist/components/message-timeline.d.ts +7 -2
  15. package/dist/composer.js +2 -2
  16. package/dist/hooks/use-session-events.d.ts +2 -1
  17. package/dist/hooks/use-session-lineage.d.ts +4 -0
  18. package/dist/hooks/use-session.d.ts +6 -0
  19. package/dist/hooks/use-workspace-sessions.d.ts +6 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +41 -19
  22. package/dist/index.js.map +1 -1
  23. package/dist/older-history.d.ts +29 -0
  24. package/dist/session-ui.d.ts +2 -0
  25. package/dist/session-ui.js +5 -2
  26. package/dist/session.d.ts +2 -0
  27. package/dist/session.js +5 -3
  28. package/package.json +2 -2
  29. package/src/components/composer.tsx +19 -5
  30. package/src/components/fleet-tile.tsx +7 -5
  31. package/src/components/message-timeline.tsx +399 -160
  32. package/src/components/tip-follow.ts +18 -7
  33. package/src/hooks/use-composer.ts +20 -6
  34. package/src/hooks/use-session-events.ts +84 -76
  35. package/src/hooks/use-session-lineage.ts +26 -8
  36. package/src/hooks/use-session.ts +35 -8
  37. package/src/hooks/use-workspace-sessions.ts +38 -13
  38. package/src/index.ts +2 -0
  39. package/src/older-history.ts +69 -0
  40. package/src/session-ui.ts +2 -0
  41. package/src/session.ts +2 -0
  42. package/src/timeline/activity-rail.tsx +1 -0
  43. package/dist/chunk-LS4POUTY.js.map +0 -1
  44. package/dist/chunk-LTCHJ4MS.js.map +0 -1
  45. package/dist/chunk-RSXJN73N.js.map +0 -1
  46. package/dist/chunk-SE4W4WQ3.js.map +0 -1
@@ -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,15 +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");
374
441
  // A collapsed history tail can be shorter than the viewport. In that state
375
442
  // there is no upward scroll range, so reader intent can never arm the top
376
443
  // sentinel. Request one older page per loaded window until history either
377
- // fills the viewport or the host reports that no older rows remain.
378
- const underfillLoadWindowRef = useRef<string | null>(null);
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);
379
453
  const resizeFollowRafRef = useRef<number | null>(null);
380
454
  const firstGroupKey = allGroups[0] ? timelineGroupKey(allGroups[0]) : null;
381
455
  // Content stays invisible until the tip is hard-parked across a short
@@ -449,11 +523,21 @@ export function MessageTimeline({
449
523
  const foldMemoryRef = useRef<Map<string, FoldRestingState>>(new Map());
450
524
  const userMessageDisclosureMemoryRef = useRef<Map<string, boolean>>(new Map());
451
525
  const seenActivityIdsRef = useRef<Set<string>>(new Set());
452
- const groupKeyByItemIdRef = useRef<Map<string, string>>(new Map());
526
+ const firstItemGroupKeyRef = useRef<string | null>(null);
453
527
  const groupOffsetByKeyRef = useRef<Map<string, number>>(new Map());
528
+ const firstItemContentTopRef = useRef<number | null>(null);
454
529
  const firstItemId = resolvedItems[0]?.id ?? null;
455
- const lastItemId = resolvedItems.at(-1)?.id ?? null;
456
- const loadedWindowKey = JSON.stringify([firstItemId, lastItemId, resolvedItems.length]);
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
+ ));
457
541
  // Bulk paints (the initial tail window, a prepended older window — detected
458
542
  // by the first group key changing) must not run per-row entrance animations.
459
543
  const firstKeyChangedForBulk =
@@ -471,6 +555,15 @@ export function MessageTimeline({
471
555
  }
472
556
  }, []);
473
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
+
474
567
  const revealedRef = useRef(revealed);
475
568
  revealedRef.current = revealed;
476
569
 
@@ -502,7 +595,7 @@ export function MessageTimeline({
502
595
  }, []);
503
596
 
504
597
  const cancelLeaveFallback = useCallback(() => {
505
- if (leaveFallbackRafRef.current !== null) {
598
+ if (leaveFallbackRafRef.current != null) {
506
599
  cancelFrame(leaveFallbackRafRef.current);
507
600
  leaveFallbackRafRef.current = null;
508
601
  }
@@ -515,7 +608,7 @@ export function MessageTimeline({
515
608
 
516
609
  const stopFollow = useCallback(() => {
517
610
  followRef.current = tipFollowCancel(followRef.current);
518
- if (followFrameRef.current !== null) {
611
+ if (followFrameRef.current != null) {
519
612
  cancelFrame(followFrameRef.current);
520
613
  followFrameRef.current = null;
521
614
  }
@@ -558,7 +651,7 @@ export function MessageTimeline({
558
651
  if (programmaticScrollRef.current > 0) {
559
652
  return;
560
653
  }
561
- if (followRef.current.running || followFrameRef.current !== null) {
654
+ if (followRef.current.running || followFrameRef.current != null) {
562
655
  return;
563
656
  }
564
657
  if (isNearBottom(node) || maxScrollOf(node) <= 1) {
@@ -566,11 +659,9 @@ export function MessageTimeline({
566
659
  return;
567
660
  }
568
661
  releasePinFromReader(node);
569
- if (olderLoadGateRef.current === "cooling" && node.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
570
- olderLoadGateRef.current = "armed";
571
- }
662
+ rearmOlderPrefetchAfterLeavingTop(node);
572
663
  },
573
- [autoFollow, clearPendingReaderLeave, releasePinFromReader],
664
+ [autoFollow, clearPendingReaderLeave, rearmOlderPrefetchAfterLeavingTop, releasePinFromReader],
574
665
  );
575
666
 
576
667
  const scheduleLeaveFallback = useCallback(() => {
@@ -620,7 +711,7 @@ export function MessageTimeline({
620
711
  target: EventTarget | null;
621
712
  }) => {
622
713
  // Primary button / touch / pen only. Ignore right-click etc.
623
- if (event.button !== 0 && event.pointerType === "mouse") {
714
+ if (event.button && event.pointerType === "mouse") {
624
715
  return;
625
716
  }
626
717
  // Clicks on chips/buttons/links must not arm — their settle collapse
@@ -730,39 +821,61 @@ export function MessageTimeline({
730
821
  );
731
822
 
732
823
  const requestOlderIfUnderfilled = useCallback(
733
- (node: HTMLElement) => {
734
- if (
735
- underfillLoadWindowRef.current !== null &&
736
- underfillLoadWindowRef.current !== loadedWindowKey
737
- ) {
738
- // The requested page landed. Ordinary sentinel prefetch may own the
739
- // next approach to the top once this window has a usable scroll range.
740
- olderLoadGateRef.current = "armed";
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;
741
845
  }
742
846
  // clientHeight=0 is pre-layout/headless, not evidence that the rendered
743
847
  // history underfills a real viewport.
744
848
  if (
745
849
  node.clientHeight <= 1 ||
746
- maxScrollOf(node) > 1 ||
850
+ (!retry && !underfilled) ||
747
851
  !hasOlder ||
748
852
  loadingOlder ||
749
853
  !onLoadOlder ||
750
- underfillLoadWindowRef.current === loadedWindowKey
854
+ (currentAttempt && !retry)
751
855
  ) {
752
856
  return;
753
857
  }
754
- underfillLoadWindowRef.current = loadedWindowKey;
755
- // If the reader had already armed ordinary prefetch, do not let its
756
- // sentinel duplicate this request while the same short page is loading.
757
- if (olderPrefetchArmedRef.current) {
758
- olderLoadGateRef.current = "cooling";
759
- }
760
- onLoadOlder();
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);
761
873
  },
762
- [hasOlder, loadedWindowKey, loadingOlder, onLoadOlder],
874
+ [hasOlder, loadingOlder, olderBoundaryKey, onLoadOlder],
875
+ );
876
+ const driveFollowRef = useRef<(node: HTMLElement, now?: number) => void>(
877
+ requestOlderIfUnderfilled as (node: HTMLElement, now?: number) => void,
763
878
  );
764
-
765
- const driveFollowRef = useRef<(node: HTMLElement, now?: number) => void>(() => undefined);
766
879
  const driveFollow = useCallback(
767
880
  (node: HTMLElement, nowMs?: number) => {
768
881
  if (!pinnedRef.current || hasNewerRef.current) {
@@ -820,7 +933,7 @@ export function MessageTimeline({
820
933
  // A direct glue write re-based the camera — drop any stale fraction.
821
934
  cameraTop: null,
822
935
  };
823
- if (followFrameRef.current === null) {
936
+ if (followFrameRef.current == null) {
824
937
  followFrameRef.current = requestFrame((frameNow) => {
825
938
  followFrameRef.current = null;
826
939
  const current = scrollRef.current;
@@ -854,7 +967,7 @@ export function MessageTimeline({
854
967
  syncScrollBaseline(node);
855
968
  if (result.state.running) {
856
969
  cancelLeaveFallback();
857
- if (followFrameRef.current === null) {
970
+ if (followFrameRef.current == null) {
858
971
  followFrameRef.current = requestFrame((frameNow) => {
859
972
  followFrameRef.current = null;
860
973
  const current = scrollRef.current;
@@ -863,7 +976,7 @@ export function MessageTimeline({
863
976
  }
864
977
  });
865
978
  }
866
- } else if (followFrameRef.current !== null) {
979
+ } else if (followFrameRef.current != null) {
867
980
  cancelFrame(followFrameRef.current);
868
981
  followFrameRef.current = null;
869
982
  }
@@ -886,12 +999,65 @@ export function MessageTimeline({
886
999
  return;
887
1000
  }
888
1001
  const previousFirstItemId = previousFirstItemIdRef.current;
889
- const previousGroupKeyByItemId = groupKeyByItemIdRef.current;
1002
+ const previousFirstItemGroupKey = firstItemGroupKeyRef.current;
890
1003
  const previousGroupOffsetByKey = groupOffsetByKeyRef.current;
1004
+ const previousItemContentTop = firstItemContentTopRef.current;
891
1005
  const previousScrollTop = lastScrollTopRef.current;
892
- 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;
893
1011
  const prepended =
894
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
+ };
895
1061
  if (pendingJumpToStartRef.current && firstItemChanged) {
896
1062
  // The oldest window landed — jump against the NEW DOM, and skip the
897
1063
  // prepend correction (it would shift the reader away from the top).
@@ -907,6 +1073,42 @@ export function MessageTimeline({
907
1073
  applyPinned(true);
908
1074
  snapToBottom(node);
909
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
+ }
910
1112
  } else if (autoFollow && pinnedRef.current && !hasNewer) {
911
1113
  // Load/remount (still hidden): hard-park. Live tip after reveal: ease.
912
1114
  // Pending unarmed leave: tip *growth* must not yank (Vimium during stream).
@@ -921,66 +1123,25 @@ export function MessageTimeline({
921
1123
  } else {
922
1124
  driveFollow(node);
923
1125
  }
924
- } else if (prepended) {
925
- // Keep the reader on the same retained rows. Prefer the offsetTop delta
926
- // of the group that still holds the previous first item — that stays
927
- // correct when loadOlder also truncates the tip (height delta then lies).
928
- // If native anchoring already applied the same shift, leave scrollTop.
929
- const anchorKey =
930
- previousFirstItemId !== null
931
- ? previousGroupKeyByItemId.get(previousFirstItemId)
932
- : undefined;
933
- const previousAnchorTop =
934
- anchorKey !== undefined ? previousGroupOffsetByKey.get(anchorKey) : undefined;
935
- const anchorEl =
936
- anchorKey !== undefined
937
- ? node.querySelector(`[data-og-group-key="${cssEscapeAttribute(anchorKey)}"]`)
938
- : null;
939
- let delta: number | null = null;
940
- if (anchorEl instanceof HTMLElement && previousAnchorTop !== undefined) {
941
- const moved = Math.round(anchorEl.offsetTop - previousAnchorTop);
942
- if (moved !== 0) {
943
- delta = moved;
944
- }
945
- }
946
- if (delta === null) {
947
- const heightDelta = Math.round(node.scrollHeight - previousScrollHeightRef.current);
948
- if (heightDelta > 0) {
949
- delta = heightDelta;
950
- }
951
- }
952
- if (delta !== null) {
953
- const expected = previousScrollTop + delta;
954
- if (Math.abs(node.scrollTop - expected) > 2) {
955
- writeScrollTop(node, expected);
956
- }
957
- }
958
1126
  }
959
1127
  // After a prepend, if restore left us below the top prefetch band,
960
1128
  // re-arm so a later approach can load again. Still cooling while parked
961
1129
  // inside the band (short pages) — that stops the y=0 load loop.
962
- if (
963
- prepended &&
964
- !pinnedRef.current &&
965
- olderLoadGateRef.current === "cooling" &&
966
- node.scrollTop > OLDER_PREFETCH_MARGIN_PX
967
- ) {
968
- olderLoadGateRef.current = "armed";
1130
+ if (prepended && !pinnedRef.current && node.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
1131
+ rearmOlderPrefetchAfterLeavingTop(node);
969
1132
  }
970
1133
  previousFirstItemIdRef.current = firstItemId;
971
1134
  previousScrollHeightRef.current = node.scrollHeight;
972
1135
  syncScrollBaseline(node);
973
- // Item→group keys are cheap (data only). offsetTop queries are O(groups)
974
- // layout reads skip while pinned at the live tip (every stream token
975
- // used to remeasure the whole timeline; that was the long-run lag).
976
- const nextKeyByItemId = new Map<string, string>();
977
- for (const { group, key } of groups) {
978
- for (const itemId of timelineGroupItemIds(group)) {
979
- nextKeyByItemId.set(itemId, key);
980
- }
981
- }
982
- groupKeyByItemIdRef.current = nextKeyByItemId;
983
- 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;
984
1145
  if (needOffsets) {
985
1146
  const nextOffsetByKey = new Map<string, number>();
986
1147
  for (const { key } of groups) {
@@ -990,14 +1151,52 @@ export function MessageTimeline({
990
1151
  }
991
1152
  }
992
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;
993
1183
  }
1184
+ if (!attempt[1] || !hasOlder) {
1185
+ setUnderfillSettledAttempt((olderLoadAttemptRef.current = null));
1186
+ return;
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);
994
1193
  });
995
1194
 
996
1195
  // First paint / session remount: keep the scroller hidden, snap to tip for
997
1196
  // two animation frames (late sync layout), then reveal. Does not change the
998
- // tip-follow ease law used once `revealed` is true.
1197
+ // live tip-follow law used once `revealed` is true.
999
1198
  useLayoutEffect(() => {
1000
- if (revealed || allGroups.length === 0) {
1199
+ if (revealed || !allGroups.length) {
1001
1200
  return;
1002
1201
  }
1003
1202
  let cancelled = false;
@@ -1025,7 +1224,7 @@ export function MessageTimeline({
1025
1224
  return () => {
1026
1225
  cancelled = true;
1027
1226
  cancelFrame(frame1);
1028
- if (frame2 !== 0) {
1227
+ if (frame2) {
1029
1228
  cancelFrame(frame2);
1030
1229
  }
1031
1230
  };
@@ -1044,7 +1243,6 @@ export function MessageTimeline({
1044
1243
  olderPrefetchArmedRef.current = false;
1045
1244
  setOlderPrefetchArmed(false);
1046
1245
  }
1047
- olderLoadGateRef.current = "armed";
1048
1246
  wantPinRef.current = false;
1049
1247
  pendingJumpToStartRef.current = false;
1050
1248
  previousFirstItemIdRef.current = null;
@@ -1053,12 +1251,12 @@ export function MessageTimeline({
1053
1251
  lastMaxScrollRef.current = 0;
1054
1252
  lastScrollHeightRef.current = 0;
1055
1253
  lastClientHeightRef.current = 0;
1056
- groupKeyByItemIdRef.current = new Map();
1254
+ firstItemGroupKeyRef.current = null;
1057
1255
  groupOffsetByKeyRef.current = new Map();
1256
+ firstItemContentTopRef.current = null;
1058
1257
  foldMemoryRef.current.clear();
1059
1258
  userMessageDisclosureMemoryRef.current.clear();
1060
1259
  disclosureKeepsUnpinnedRef.current = false;
1061
- underfillLoadWindowRef.current = null;
1062
1260
  seenActivityIdsRef.current.clear();
1063
1261
  applyPinned(true);
1064
1262
  }, [allGroups.length, revealed, applyPinned]);
@@ -1107,21 +1305,60 @@ export function MessageTimeline({
1107
1305
  (entries) => {
1108
1306
  const intersecting = entries.some((entry) => entry.isIntersecting);
1109
1307
  if (!intersecting) {
1110
- // Left the top band next approach may load once.
1111
- 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
+ }
1112
1315
  return;
1113
1316
  }
1114
- 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) {
1115
1321
  return;
1116
1322
  }
1117
- olderLoadGateRef.current = "cooling";
1118
- 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
+ }
1119
1348
  },
1120
1349
  { root, rootMargin: OLDER_PREFETCH_ROOT_MARGIN },
1121
1350
  );
1122
1351
  observer.observe(target);
1123
1352
  return () => observer.disconnect();
1124
- }, [olderPrefetchArmed, hasOlder, loadingOlder, onLoadOlder, firstGroupKey]);
1353
+ }, [
1354
+ firstGroupKey,
1355
+ hasOlder,
1356
+ loadingOlder,
1357
+ olderBoundaryKey,
1358
+ olderPrefetchArmed,
1359
+ onLoadOlder,
1360
+ requestOlderIfUnderfilled,
1361
+ ]);
1125
1362
 
1126
1363
  // History view: page forward when the reader nears the bottom of the current
1127
1364
  // non-tip window. Does not pull the whole gap — one density-bounded page.
@@ -1161,7 +1398,7 @@ export function MessageTimeline({
1161
1398
  return;
1162
1399
  }
1163
1400
  const observer = new ResizeObserver(() => {
1164
- if (resizeFollowRafRef.current !== null) {
1401
+ if (resizeFollowRafRef.current != null) {
1165
1402
  return;
1166
1403
  }
1167
1404
  resizeFollowRafRef.current = requestFrame(() => {
@@ -1188,7 +1425,7 @@ export function MessageTimeline({
1188
1425
  observer.observe(node);
1189
1426
  return () => {
1190
1427
  observer.disconnect();
1191
- if (resizeFollowRafRef.current !== null) {
1428
+ if (resizeFollowRafRef.current != null) {
1192
1429
  cancelFrame(resizeFollowRafRef.current);
1193
1430
  resizeFollowRafRef.current = null;
1194
1431
  }
@@ -1289,12 +1526,11 @@ export function MessageTimeline({
1289
1526
  if (readerArmed && readerUp > TIP_FOLLOW_READER_UP_EPS_PX && !nearBottomPinned) {
1290
1527
  readerIntentArmRef.current = false;
1291
1528
  releasePinFromReader(node);
1292
- if (olderLoadGateRef.current === "cooling" && node.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
1293
- olderLoadGateRef.current = "armed";
1294
- }
1529
+ rearmOlderPrefetchAfterLeavingTop(node);
1295
1530
  return;
1296
1531
  }
1297
- // 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.
1298
1534
  // Reader/extension scroll-up in progress: do not yank — scrollend decides.
1299
1535
  // Near-bottom with tipDebt≈0 stays the quiet path (do not broaden follow
1300
1536
  // inside PIN_THRESHOLD — that fights small intentional scroll-ups).
@@ -1341,9 +1577,7 @@ export function MessageTimeline({
1341
1577
  }
1342
1578
  // Re-arm older prefetch only after leaving the top band (scroll down into
1343
1579
  // content). Never re-arm/load from continued scroll toward y=0.
1344
- if (olderLoadGateRef.current === "cooling" && node.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
1345
- olderLoadGateRef.current = "armed";
1346
- }
1580
+ rearmOlderPrefetchAfterLeavingTop(node);
1347
1581
  };
1348
1582
 
1349
1583
  const onScrollEnd = () => {
@@ -1407,7 +1641,7 @@ export function MessageTimeline({
1407
1641
  )}
1408
1642
  >
1409
1643
  <div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
1410
- {groups.length === 0
1644
+ {!groups.length
1411
1645
  ? (emptyState ?? (
1412
1646
  <p className="py-10 text-center text-og-menu text-og-fg-subtle">
1413
1647
  No activity yet.
@@ -1494,7 +1728,8 @@ export function MessageTimeline({
1494
1728
  <AnimatePresence>
1495
1729
  {loadingOlder ||
1496
1730
  loadingOldest ||
1497
- (hasOlder && onJumpToStart && olderPrefetchArmed) ? (
1731
+ (hasOlder && onJumpToStart && olderPrefetchArmed) ||
1732
+ underfillRetryReady ? (
1498
1733
  // Floating over the scroller (not a timeline row) so showing and
1499
1734
  // hiding it never reflows history under the reader.
1500
1735
  <motion.div
@@ -1504,26 +1739,38 @@ export function MessageTimeline({
1504
1739
  transition={{ duration: 0.15, ease: "easeOut" }}
1505
1740
  data-og-loading-older=""
1506
1741
  aria-live="polite"
1507
- 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"
1508
1743
  >
1509
1744
  {loadingOlder || loadingOldest ? (
1510
- <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}>
1511
1746
  <span className="og-shimmer-text">
1512
1747
  {loadingOldest ? "Jumping to start…" : "Loading earlier activity…"}
1513
1748
  </span>
1514
1749
  </span>
1515
1750
  ) : null}
1516
- {hasOlder && onJumpToStart && !loadingOldest ? (
1751
+ {hasOlder &&
1752
+ !loadingOlder &&
1753
+ !loadingOldest &&
1754
+ (underfillRetryReady || onJumpToStart) ? (
1517
1755
  <button
1518
1756
  type="button"
1519
- data-og-jump-to-start=""
1520
- disabled={loadingOlder}
1757
+ data-og-retry={underfillRetryReady || undefined}
1758
+ data-og-jump-to-start={!underfillRetryReady || undefined}
1521
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
+ }
1522
1770
  applyPinned(false);
1523
1771
  pendingJumpToStartRef.current = true;
1524
1772
  const seq = ++jumpToStartSeqRef.current;
1525
- const node = scrollRef.current;
1526
- void Promise.resolve(onJumpToStart()).then(
1773
+ void Promise.resolve(onJumpToStart!()).then(
1527
1774
  () => {
1528
1775
  // The commit that swaps in the oldest window consumes
1529
1776
  // the flag against the new DOM; this write covers the
@@ -1552,14 +1799,9 @@ export function MessageTimeline({
1552
1799
  },
1553
1800
  );
1554
1801
  }}
1555
- className={cn(
1556
- "inline-flex items-center gap-1.5 rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1.5",
1557
- "text-og-control font-medium text-og-fg shadow-og-md backdrop-blur",
1558
- "hover:border-og-border-strong disabled:opacity-60",
1559
- )}
1802
+ className="rounded-full border border-og-border px-3 py-1.5 text-og-control"
1560
1803
  >
1561
- <ArrowUpToLineIcon className="size-3.5" />
1562
- Jump to start
1804
+ {underfillRetryReady ? "Retry earlier activity" : "Jump to start"}
1563
1805
  </button>
1564
1806
  ) : null}
1565
1807
  </motion.div>
@@ -1576,7 +1818,7 @@ export function MessageTimeline({
1576
1818
  aria-live="polite"
1577
1819
  className="pointer-events-none absolute inset-x-0 bottom-14 z-10 flex justify-center"
1578
1820
  >
1579
- <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}>
1580
1822
  <span className="og-shimmer-text">Loading later activity…</span>
1581
1823
  </span>
1582
1824
  </motion.div>
@@ -1862,8 +2104,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
1862
2104
  // turn that is the second layer — quiet nested chips under the outer turn
1863
2105
  // summary when contiguous activity naturally clusters (≥2 only).
1864
2106
  const activityShouldFold =
1865
- group.kind === "activity" &&
1866
- Boolean(group.outcome || (foldLiveCluster && clusterIsSettled(group)));
2107
+ group.kind === "activity" && !!(group.outcome || (foldLiveCluster && clusterIsSettled(group)));
1867
2108
  const containsGeneratedImage = timelineGroupContainsGeneratedImage(group);
1868
2109
  // Latch live→folded so a top-level shell that was already mounted open can
1869
2110
  // start the settle beat without remounting bare rail → wrapper.
@@ -1884,7 +2125,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
1884
2125
  );
1885
2126
  }
1886
2127
  const settleFold =
1887
- group.kind === "turn" ? Boolean(enter && !insideTurn && !turnDefaultOpen) : liveActivitySettle;
2128
+ group.kind === "turn" ? !!(enter && !insideTurn && !turnDefaultOpen) : liveActivitySettle;
1888
2129
  switch (group.kind) {
1889
2130
  case "activity":
1890
2131
  if (insideTurn) {
@@ -2246,7 +2487,7 @@ export function trailingAgentTextAfterTurn(
2246
2487
  if (next?.kind === "item" && next.item.kind === "agent-message") {
2247
2488
  const turnIds = collectTurnIdsFromGroups(group.groups);
2248
2489
  // Degenerate body with no turnIds: do not guess — safer than lifting wrong.
2249
- if (turnIds.size === 0) {
2490
+ if (!turnIds.size) {
2250
2491
  return undefined;
2251
2492
  }
2252
2493
  if (!next.item.turnId || !turnIds.has(next.item.turnId)) {
@@ -2265,7 +2506,7 @@ function collectTurnCopyText(
2265
2506
  const parts = [collectAgentMessageText(groups), trailingAgentText?.trim() ?? ""].filter(
2266
2507
  (part) => part.length > 0,
2267
2508
  );
2268
- if (parts.length === 0) {
2509
+ if (!parts.length) {
2269
2510
  return undefined;
2270
2511
  }
2271
2512
  return parts.join("\n\n");
@@ -2350,11 +2591,11 @@ function CompactionRow({ item }: { item: ContextCompactionItem }) {
2350
2591
  const trigger =
2351
2592
  item.trigger && item.phase !== "started" ? COMPACTION_TRIGGER_LABEL[item.trigger] : null;
2352
2593
  const before =
2353
- item.estimatedTokensBefore !== null
2594
+ item.estimatedTokensBefore != null
2354
2595
  ? Math.round(item.estimatedTokensBefore).toLocaleString("en-US")
2355
2596
  : null;
2356
2597
  const after =
2357
- item.estimatedTokensAfter !== null
2598
+ item.estimatedTokensAfter != null
2358
2599
  ? Math.round(item.estimatedTokensAfter).toLocaleString("en-US")
2359
2600
  : null;
2360
2601
  const title =
@@ -2375,7 +2616,7 @@ function CompactionRow({ item }: { item: ContextCompactionItem }) {
2375
2616
  item.phase === "skipped" && item.skipReason === "summarization_failed"
2376
2617
  ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
2377
2618
  : item.phase === "started"
2378
- ? "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting"
2619
+ ? WAITING_PILL_CLASS
2379
2620
  : "border-og-border bg-og-surface-1 text-og-fg-muted";
2380
2621
  return (
2381
2622
  <div className={cn(enter && "animate-og-enter", "flex justify-center")}>
@@ -2455,7 +2696,7 @@ function UserMessageRow({
2455
2696
  className="w-fit max-w-full min-w-0"
2456
2697
  trailing={<MessageFooterTime occurredAt={item.occurredAt} />}
2457
2698
  >
2458
- <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}>
2459
2700
  {item.text ? (
2460
2701
  <div data-og-annotation-source-key={item.annotationSource?.eventId}>
2461
2702
  {renderMessageText ? (
@@ -2577,7 +2818,7 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
2577
2818
  className="w-fit max-w-[90%] min-w-0 sm:max-w-[82%]"
2578
2819
  trailing={<MessageFooterTime occurredAt={item.occurredAt} />}
2579
2820
  >
2580
- <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}>
2581
2822
  <p className="text-og-xs font-medium text-og-fg-subtle">{settledLabel}</p>
2582
2823
  {item.response.outcome === "answered" ? (
2583
2824
  <div className="mt-1.5 space-y-3">
@@ -2747,9 +2988,7 @@ function WorkerCompletionRow({
2747
2988
  // payloads can carry a leftover pausedReason/rationale from earlier in the
2748
2989
  // worker's life, and a "Worker completed" card must not show a pause section.
2749
2990
  const showPausedReason =
2750
- item.childStatus !== "failed" &&
2751
- item.goalStatus === "paused" &&
2752
- Boolean(item.pausedReason?.trim());
2991
+ item.childStatus !== "failed" && item.goalStatus === "paused" && !!item.pausedReason?.trim();
2753
2992
  const details: { label: string; value: string; muted?: boolean }[] = [
2754
2993
  ...(item.text.trim() ? [{ label: "Report", value: item.text.trim() }] : []),
2755
2994
  ...(item.evidence?.trim()
@@ -2888,14 +3127,14 @@ const GOAL_META: Record<GoalItem["action"], GoalMeta> = {
2888
3127
  },
2889
3128
  paused: {
2890
3129
  label: "Goal paused",
2891
- pill: "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting",
3130
+ pill: WAITING_PILL_CLASS,
2892
3131
  icon: PauseIcon,
2893
3132
  },
2894
3133
  resumed: { label: "Goal resumed", pill: NEUTRAL_PILL, icon: PlayIcon },
2895
3134
  cleared: { label: "Goal cleared", pill: NEUTRAL_PILL, icon: Trash2Icon },
2896
3135
  held: {
2897
3136
  label: "Goal held",
2898
- pill: "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting",
3137
+ pill: WAITING_PILL_CLASS,
2899
3138
  icon: PauseCircleIcon,
2900
3139
  },
2901
3140
  continuation: { label: "Continuing toward the goal", pill: NEUTRAL_PILL, icon: ArrowRightIcon },
@@ -3093,7 +3332,7 @@ function NoticeRow({ item }: { item: NoticeItem }) {
3093
3332
  item.tone === "failed"
3094
3333
  ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
3095
3334
  : item.tone === "waiting"
3096
- ? "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting"
3335
+ ? WAITING_PILL_CLASS
3097
3336
  : "border-og-border bg-og-surface-1 text-og-fg-muted";
3098
3337
  return (
3099
3338
  <div
@@ -3226,7 +3465,7 @@ function AuthNeededRow({
3226
3465
  onClick={() => void start()}
3227
3466
  disabled={busy}
3228
3467
  className={cn(
3229
- "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,
3230
3469
  "transition-colors hover:bg-og-accent-strong disabled:opacity-70 pointer-coarse:min-h-9",
3231
3470
  )}
3232
3471
  >
@@ -3239,7 +3478,7 @@ function AuthNeededRow({
3239
3478
  rel="noreferrer"
3240
3479
  target="_blank"
3241
3480
  className={cn(
3242
- "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,
3243
3482
  "transition-colors hover:bg-og-accent-strong pointer-coarse:min-h-9",
3244
3483
  )}
3245
3484
  >
@@ -3302,7 +3541,7 @@ function AuthProviderLogo({ src, label }: { src: string | null; label: string })
3302
3541
  `capabilityMonogram` so the reconnect tile reads like every other logo tile. */
3303
3542
  function monogram(label: string): string {
3304
3543
  const words = label.trim().split(/\s+/).filter(Boolean);
3305
- if (words.length === 0) {
3544
+ if (!words.length) {
3306
3545
  return "?";
3307
3546
  }
3308
3547
  if (words.length === 1) {