@opengeni/react 3.5.1-canary.0 → 3.7.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 (87) hide show
  1. package/dist/artifacts.d.ts +1 -1
  2. package/dist/artifacts.js +248 -6
  3. package/dist/artifacts.js.map +1 -1
  4. package/dist/{browser-viewer-E5DDEFQ4.js → browser-viewer-NTD6UIPR.js} +3 -3
  5. package/dist/{chunk-W2RGYBCV.js → chunk-24M4YBCL.js} +4 -4
  6. package/dist/{chunk-WY3MELVJ.js → chunk-5IXTJTK3.js} +1438 -397
  7. package/dist/chunk-5IXTJTK3.js.map +1 -0
  8. package/dist/chunk-5PQJOCX5.js +902 -0
  9. package/dist/chunk-5PQJOCX5.js.map +1 -0
  10. package/dist/{chunk-UNCXXIQR.js → chunk-INGB2N5R.js} +4310 -3982
  11. package/dist/chunk-INGB2N5R.js.map +1 -0
  12. package/dist/{chunk-TJFMMDQU.js → chunk-MOWYEBCQ.js} +1839 -17
  13. package/dist/chunk-MOWYEBCQ.js.map +1 -0
  14. package/dist/chunk-PAJU5KKL.js +331 -0
  15. package/dist/chunk-PAJU5KKL.js.map +1 -0
  16. package/dist/{chunk-VTOREEXF.js → chunk-PLYRL5ER.js} +24 -2
  17. package/dist/{chunk-VTOREEXF.js.map → chunk-PLYRL5ER.js.map} +1 -1
  18. package/dist/{chunk-YVCHAQZY.js → chunk-ZFOFY5ST.js} +27 -349
  19. package/dist/chunk-ZFOFY5ST.js.map +1 -0
  20. package/dist/client.d.ts +1 -1
  21. package/dist/components/artifacts/index.d.ts +1 -1
  22. package/dist/components/artifacts/published-html-artifact-frame.d.ts +46 -3
  23. package/dist/components/composer.d.ts +2 -0
  24. package/dist/components/session-chrome.d.ts +38 -5
  25. package/dist/components/session-commands-panel.d.ts +6 -0
  26. package/dist/components/session-conversation.d.ts +14 -0
  27. package/dist/components/timeline-anchor.d.ts +21 -0
  28. package/dist/composer.js +2 -3
  29. package/dist/conversation-timeline.d.ts +5 -0
  30. package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-YZ3ZKWMM.js} +2 -1
  31. package/dist/fleet-decision-row-YZ3ZKWMM.js.map +1 -0
  32. package/dist/hooks/use-file-attachments.d.ts +6 -0
  33. package/dist/hooks/use-sandbox-files.d.ts +4 -1
  34. package/dist/hooks/use-workspace-edit.d.ts +4 -0
  35. package/dist/index.d.ts +4 -1
  36. package/dist/index.js +823 -965
  37. package/dist/index.js.map +1 -1
  38. package/dist/interaction.js +2 -2
  39. package/dist/lib/format.d.ts +5 -0
  40. package/dist/machines.js +3 -2
  41. package/dist/session-ui.d.ts +3 -0
  42. package/dist/session-ui.js +14 -4
  43. package/dist/session.js +19 -19
  44. package/dist/timeline/registry.d.ts +2 -0
  45. package/dist/timeline/types.d.ts +4 -2
  46. package/package.json +2 -2
  47. package/src/artifacts.ts +3 -0
  48. package/src/client.ts +2 -1
  49. package/src/components/artifacts/index.ts +3 -0
  50. package/src/components/artifacts/published-html-artifact-frame.tsx +307 -4
  51. package/src/components/composer.tsx +89 -25
  52. package/src/components/machine-input-display.ts +6 -0
  53. package/src/components/message-timeline.tsx +351 -204
  54. package/src/components/sandbox-workspace.tsx +11 -1
  55. package/src/components/session-chrome.tsx +501 -128
  56. package/src/components/session-commands-panel.tsx +113 -0
  57. package/src/components/session-conversation.tsx +141 -0
  58. package/src/components/timeline-anchor.tsx +91 -0
  59. package/src/conversation-timeline.ts +83 -0
  60. package/src/hooks/use-file-attachments.ts +34 -0
  61. package/src/hooks/use-sandbox-files.ts +68 -20
  62. package/src/hooks/use-session-background-commands.ts +21 -10
  63. package/src/hooks/use-session-events.ts +56 -39
  64. package/src/hooks/use-session.ts +23 -2
  65. package/src/hooks/use-workspace-edit.ts +23 -5
  66. package/src/index.ts +4 -0
  67. package/src/lib/format.ts +32 -0
  68. package/src/session-ui.ts +3 -0
  69. package/src/timeline/fleet-decision-projection.ts +2 -1
  70. package/src/timeline/fleet-decision-row.tsx +1 -0
  71. package/src/timeline/projection.ts +157 -19
  72. package/src/timeline/registry.ts +15 -3
  73. package/src/timeline/tool-renderers.tsx +117 -1
  74. package/src/timeline/types.ts +12 -1
  75. package/styles/compiled.css +495 -411
  76. package/styles/index.css +23 -0
  77. package/dist/chunk-7EAFGICR.js +0 -302
  78. package/dist/chunk-7EAFGICR.js.map +0 -1
  79. package/dist/chunk-IB27C53Y.js +0 -2582
  80. package/dist/chunk-IB27C53Y.js.map +0 -1
  81. package/dist/chunk-TJFMMDQU.js.map +0 -1
  82. package/dist/chunk-UNCXXIQR.js.map +0 -1
  83. package/dist/chunk-WY3MELVJ.js.map +0 -1
  84. package/dist/chunk-YVCHAQZY.js.map +0 -1
  85. package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
  86. /package/dist/{browser-viewer-E5DDEFQ4.js.map → browser-viewer-NTD6UIPR.js.map} +0 -0
  87. /package/dist/{chunk-W2RGYBCV.js.map → chunk-24M4YBCL.js.map} +0 -0
@@ -49,6 +49,12 @@ import {
49
49
  type OlderHistoryLoader,
50
50
  } from "../older-history";
51
51
  import { Markdown } from "./markdown";
52
+ import {
53
+ TimelineBeforeLayout,
54
+ captureTimelineAnchor,
55
+ timelineAnchorCorrection,
56
+ type TimelineAnchor,
57
+ } from "./timeline-anchor";
52
58
  import {
53
59
  UserMessageBody,
54
60
  UserMessageDisclosureProvider,
@@ -418,8 +424,15 @@ export function MessageTimeline({
418
424
  (item) => item.kind !== "auth-needed" || shouldRenderAuthNeeded(item),
419
425
  );
420
426
  }, [items, events, shouldRenderAuthNeeded]);
421
- const sourceItems = items || events;
427
+ // Event-window identity is independent of projected rows (partial messages
428
+ // can acquire a different first-delta id when older text arrives).
429
+ const sourceItems = events ?? items;
422
430
  const olderBoundaryKey = sourceItems?.[0]?.id;
431
+ const previousSourceIdsRef = useRef(new Set<string>());
432
+ const previousSourceBoundaryRef = useRef<string | undefined>(undefined);
433
+ const readingAnchorRef = useRef<TimelineAnchor | null>(null);
434
+ const olderPageBudgetRef = useRef(0);
435
+ const [olderDemand, setOlderDemand] = useState(0);
423
436
  const allGroups = useMemo(() => groupTimeline(resolvedItems), [resolvedItems]);
424
437
  const annotationSources = useMemo(() => {
425
438
  const sources = new Map<string, TimelineAnnotationSourceDescriptor>();
@@ -648,29 +661,22 @@ export function MessageTimeline({
648
661
  }, []);
649
662
 
650
663
  /** Reader left the tip — wheel, keyboard, pointer-armed scroll-up, or scrollend. */
651
- const releasePinFromReader = useCallback(
652
- (node?: HTMLElement | null) => {
653
- if (!autoFollow || !pinnedRef.current || hasNewerRef.current) {
654
- return;
655
- }
656
- // Unscrollable window: unpin strands Jump-to-latest with no way back.
657
- if (node && maxScrollOf(node) <= 1) {
658
- return;
659
- }
660
- clearReaderIntent();
661
- clearPendingReaderLeave();
662
- stopFollow();
663
- applyPinned(false);
664
- if (wantPinRef.current) {
665
- wantPinRef.current = false;
666
- }
667
- if (!olderPrefetchArmedRef.current) {
668
- olderPrefetchArmedRef.current = true;
669
- setOlderPrefetchArmed(true);
670
- }
671
- },
672
- [autoFollow, applyPinned, clearPendingReaderLeave, clearReaderIntent, stopFollow],
673
- );
664
+ const releasePinFromReader = useCallback(() => {
665
+ if (!autoFollow || !pinnedRef.current || hasNewerRef.current) {
666
+ return;
667
+ }
668
+ clearReaderIntent();
669
+ clearPendingReaderLeave();
670
+ stopFollow();
671
+ applyPinned(false);
672
+ if (wantPinRef.current) {
673
+ wantPinRef.current = false;
674
+ }
675
+ if (!olderPrefetchArmedRef.current) {
676
+ olderPrefetchArmedRef.current = true;
677
+ setOlderPrefetchArmed(true);
678
+ }
679
+ }, [autoFollow, applyPinned, clearPendingReaderLeave, clearReaderIntent, stopFollow]);
674
680
 
675
681
  /**
676
682
  * Settled away from the tip while the camera is idle — Vimium / unfocused
@@ -691,7 +697,7 @@ export function MessageTimeline({
691
697
  clearPendingReaderLeave();
692
698
  return;
693
699
  }
694
- releasePinFromReader(node);
700
+ releasePinFromReader();
695
701
  rearmOlderPrefetchAfterLeavingTop(node);
696
702
  },
697
703
  [autoFollow, clearPendingReaderLeave, rearmOlderPrefetchAfterLeavingTop, releasePinFromReader],
@@ -712,6 +718,19 @@ export function MessageTimeline({
712
718
  });
713
719
  }, [cancelLeaveFallback, releasePinAfterScrollSettled]);
714
720
 
721
+ const requestEarlierFromReader = () => {
722
+ releasePinFromReader();
723
+ wantPinRef.current = false;
724
+ // A stationary upward gesture is still demand. Successful short/folded
725
+ // pages may never create enough range to leave the prefetch band.
726
+ olderPageBudgetRef.current = 8;
727
+ if (olderLoadAttemptRef.current?.[1] === 2) {
728
+ olderLoadAttemptRef.current = null;
729
+ setOlderDemand((value) => value + 1);
730
+ }
731
+ };
732
+ const touchPositionRef = useRef<{ x: number; y: number } | null>(null);
733
+
715
734
  const onWheel = (event: {
716
735
  deltaY: number;
717
736
  deltaX: number;
@@ -729,12 +748,11 @@ export function MessageTimeline({
729
748
  return;
730
749
  }
731
750
  disclosureKeepsUnpinnedRef.current = false;
751
+ programmaticScrollRef.current = 0;
732
752
  if (event.deltaY >= 0) {
733
753
  return;
734
754
  }
735
- const node =
736
- event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
737
- releasePinFromReader(node);
755
+ requestEarlierFromReader();
738
756
  };
739
757
 
740
758
  /** Touch / stylus / mouse drag on the scroller — explicit leave (not layout). */
@@ -779,9 +797,8 @@ export function MessageTimeline({
779
797
  if (event.key !== "ArrowUp" && event.key !== "PageUp" && event.key !== "Home") {
780
798
  return;
781
799
  }
782
- const node =
783
- event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
784
- releasePinFromReader(node);
800
+ programmaticScrollRef.current = 0;
801
+ requestEarlierFromReader();
785
802
  };
786
803
 
787
804
  const snapToBottom = useCallback(
@@ -1103,15 +1120,26 @@ export function MessageTimeline({
1103
1120
  previousMaxScroll <= 1 ||
1104
1121
  previousMaxScroll - previousScrollTop < Math.min(PIN_THRESHOLD_PX, previousMaxScroll);
1105
1122
  const firstItemChanged = !!previousFirstItemId && firstItemId !== previousFirstItemId;
1106
- const prepended =
1107
- firstItemChanged && resolvedItems.some((item) => item.id === previousFirstItemId);
1123
+ const sourceChanged = olderBoundaryKey !== previousSourceBoundaryRef.current;
1124
+ const retainedSource =
1125
+ sourceItems?.some((item) => previousSourceIdsRef.current.has(item.id)) ?? false;
1126
+ const prepended = sourceChanged && retainedSource;
1127
+ previousSourceBoundaryRef.current = olderBoundaryKey;
1128
+ previousSourceIdsRef.current = new Set(sourceItems?.map((item) => item.id));
1129
+ const readingAnchor = readingAnchorRef.current;
1130
+ readingAnchorRef.current = null;
1108
1131
  const attempt = olderLoadAttemptRef.current;
1109
1132
  const committedZeroOverlapOlderReplacement = !!(
1110
1133
  attempt?.[2]?.committed &&
1111
- firstItemChanged &&
1112
- !prepended
1134
+ sourceChanged &&
1135
+ !retainedSource
1113
1136
  );
1114
1137
  const restorePrependAnchor = () => {
1138
+ const correction = readingAnchor && timelineAnchorCorrection(node, readingAnchor);
1139
+ if (correction != null) {
1140
+ if (Math.abs(correction) > 1) writeScrollTop(node, node.scrollTop + correction);
1141
+ return;
1142
+ }
1115
1143
  // Keep the reader on the same retained rows. Prefer the exact first-item
1116
1144
  // content coordinate (needed when a prepend merges inside one group),
1117
1145
  // then the retained group offset, then scrollHeight as a final fallback.
@@ -1183,17 +1211,23 @@ export function MessageTimeline({
1183
1211
  applyPinned(false);
1184
1212
  }
1185
1213
  } else if (prepended) {
1214
+ const olderAttemptState = olderLoadAttemptRef.current?.[1];
1215
+ const underfillOwned =
1216
+ !!olderLoadAttemptRef.current &&
1217
+ olderAttemptState !== 1 &&
1218
+ olderAttemptState !== 2 &&
1219
+ olderAttemptState !== 3;
1186
1220
  if (
1187
1221
  autoFollow &&
1188
1222
  pinnedRef.current &&
1189
1223
  !hasNewer &&
1190
1224
  !pendingReaderLeaveRef.current &&
1191
- (wasAtLiveTailBeforeCommit || olderLoadAttemptRef.current)
1225
+ (wasAtLiveTailBeforeCommit || underfillOwned)
1192
1226
  ) {
1193
- // A pending short-window load owns the prepend even when live growth
1194
- // has left its still-pinned camera with raw geometry debt. Unrelated
1195
- // prepends retain the prior-tip fallback so a stale pin after an
1196
- // extension/programmatic history jump still restores its row anchor.
1227
+ // Still following the live tip: underfill, or a prefetch the reader
1228
+ // started and then returned from. Park at the new tip. A stale pin
1229
+ // while they are actually up in a short window (gap is not inside
1230
+ // PIN_THRESHOLD of maxScroll, so wasAtLiveTail is false) restores.
1197
1231
  clearPendingReaderLeave();
1198
1232
  snapToBottom(node);
1199
1233
  } else {
@@ -1283,6 +1317,19 @@ export function MessageTimeline({
1283
1317
  // Boundary progress retires the request owner. A late settlement from
1284
1318
  // that completed prefetch cannot mutate the new window's cooldown owner.
1285
1319
  olderLoadAttemptRef.current = [olderBoundaryKey, 2];
1320
+ // Bound automatic work, but let continued upward input replenish demand.
1321
+ // Only a committed first-party receipt proves that another page is safe.
1322
+ if (
1323
+ attempt[2]?.committed &&
1324
+ !pinnedRef.current &&
1325
+ hasOlder &&
1326
+ node.scrollTop <= OLDER_PREFETCH_MARGIN_PX &&
1327
+ olderPageBudgetRef.current > 0
1328
+ ) {
1329
+ olderPageBudgetRef.current -= 1;
1330
+ olderLoadAttemptRef.current = null;
1331
+ setOlderDemand((value) => value + 1);
1332
+ }
1286
1333
  rearmOlderPrefetchAfterLeavingTop(node);
1287
1334
  requestOlderIfUnderfilled(node);
1288
1335
  });
@@ -1453,6 +1500,7 @@ export function MessageTimeline({
1453
1500
  loadingOlder,
1454
1501
  olderBoundaryKey,
1455
1502
  olderPrefetchArmed,
1503
+ olderDemand,
1456
1504
  onLoadOlder,
1457
1505
  requestOlderIfUnderfilled,
1458
1506
  ]);
@@ -1598,6 +1646,19 @@ export function MessageTimeline({
1598
1646
  return;
1599
1647
  }
1600
1648
 
1649
+ if (programmatic && !pinnedRef.current) {
1650
+ // Restore/camera writes while reading history are not a return to the tip.
1651
+ // Still expire a pending Jump-to-latest latch: the in-window jump itself
1652
+ // is a programmatic snap, and a later reader scroll-away can arrive
1653
+ // before that echo is consumed. Skipping this left the latch armed and
1654
+ // snapped the reader when hasNewer later flipped false.
1655
+ syncScrollBaseline(node);
1656
+ if (wantPinRef.current && !isNearBottom(node)) {
1657
+ wantPinRef.current = false;
1658
+ }
1659
+ return;
1660
+ }
1661
+
1601
1662
  if (autoFollow && pinnedRef.current && !hasNewer) {
1602
1663
  // Fold / composer / SessionChrome: viewport shrink raises maxScroll without
1603
1664
  // growing content. Must hit tipFollow before we adopt the new clientHeight
@@ -1632,7 +1693,7 @@ export function MessageTimeline({
1632
1693
  // Pointer-dragged scroll-up away from tip. Layout churn never arms this.
1633
1694
  if (readerArmed && cumulativeReaderUp > TIP_FOLLOW_READER_UP_EPS_PX && !nearBottomPinned) {
1634
1695
  clearReaderIntent();
1635
- releasePinFromReader(node);
1696
+ requestEarlierFromReader();
1636
1697
  rearmOlderPrefetchAfterLeavingTop(node);
1637
1698
  return;
1638
1699
  }
@@ -1657,12 +1718,20 @@ export function MessageTimeline({
1657
1718
  }
1658
1719
 
1659
1720
  syncScrollBaseline(node);
1721
+ if (programmatic) {
1722
+ // Anchor restoration is never permission to resume tip-follow.
1723
+ return;
1724
+ }
1660
1725
  const nearBottom = isNearBottom(node);
1661
1726
 
1662
- // Re-pin only when the reader moved toward/at the tip not when a fold
1663
- // clamp dragged scrollTop down onto nearBottom.
1664
- const nextPinned =
1665
- !hasNewer && nearBottom && nextTop >= previousTop - TIP_FOLLOW_READER_UP_EPS_PX;
1727
+ // Re-pin only when the reader moved toward/at the tip without a content
1728
+ // insertion. Prepend restore and overflow-anchor raise scrollTop by
1729
+ // roughly the same amount as maxScroll; treating that as a scroll-down
1730
+ // re-pinned a compact-tail history reader (their preserved gap falls
1731
+ // inside PIN_THRESHOLD once the window is tall) and snapped them back.
1732
+ const inserted = Math.max(0, nextMaxScroll - previousMaxScroll);
1733
+ const towardTip = nextTop - previousTop - inserted;
1734
+ const nextPinned = !hasNewer && nearBottom && towardTip > 0.5 && inserted <= 1;
1666
1735
  if (!nextPinned) {
1667
1736
  stopFollow();
1668
1737
  }
@@ -1735,148 +1804,202 @@ export function MessageTimeline({
1735
1804
  onScroll={onScroll}
1736
1805
  onScrollEnd={onScrollEnd}
1737
1806
  onWheel={onWheel}
1807
+ onTouchStart={(event) => {
1808
+ const touch = event.touches.length === 1 ? event.touches[0] : undefined;
1809
+ touchPositionRef.current = touch
1810
+ ? { x: touch.clientX, y: touch.clientY }
1811
+ : null;
1812
+ }}
1813
+ onTouchMove={(event) => {
1814
+ const touch = event.touches[0];
1815
+ const previous = touchPositionRef.current;
1816
+ if (!touch || !previous || event.touches.length !== 1) {
1817
+ touchPositionRef.current = null;
1818
+ return;
1819
+ }
1820
+ const deltaX = previous.x - touch.clientX;
1821
+ const deltaY = previous.y - touch.clientY;
1822
+ if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 4) return;
1823
+ touchPositionRef.current = { x: touch.clientX, y: touch.clientY };
1824
+ onWheel({
1825
+ deltaX,
1826
+ deltaY,
1827
+ target: event.target,
1828
+ currentTarget: event.currentTarget,
1829
+ });
1830
+ }}
1831
+ onTouchEnd={() => {
1832
+ touchPositionRef.current = null;
1833
+ }}
1834
+ onTouchCancel={() => {
1835
+ touchPositionRef.current = null;
1836
+ }}
1837
+ onClickCapture={(event) => {
1838
+ const target =
1839
+ event.target instanceof Element
1840
+ ? event.target.closest("button[aria-expanded]")
1841
+ : null;
1842
+ if (target) {
1843
+ releasePinFromReader();
1844
+ disclosureKeepsUnpinnedRef.current = true;
1845
+ }
1846
+ }}
1738
1847
  onPointerDown={onPointerDown}
1739
1848
  onKeyDown={onKeyDown}
1740
1849
  style={groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined}
1741
1850
  className={cn(
1742
1851
  // tabIndex=-1 is programmatic only — never paint a focus ring on
1743
1852
  // the whole scroller (click + Shift used to flash a blue outline).
1744
- "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-6 sm:px-6 outline-hidden",
1853
+ "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pt-16 pb-6 sm:px-6 outline-hidden",
1745
1854
  autoFollow && pinned && !hasNewer
1746
1855
  ? "[overflow-anchor:none]"
1747
1856
  : "[overflow-anchor:auto]",
1748
1857
  )}
1749
1858
  >
1750
- <div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
1751
- {!groups.length
1752
- ? (emptyState ?? (
1753
- <p className="py-10 text-center text-og-menu text-og-fg-subtle">
1754
- No activity yet.
1755
- </p>
1756
- ))
1757
- : null}
1758
- {hasOlder && olderPrefetchArmed ? (
1759
- // Overlaid, not a layout row: mounting/unmounting the sentinel
1760
- // must never shift content (that shift was itself a wobble).
1761
- <div
1762
- ref={topSentinelRef}
1763
- data-og-top-sentinel=""
1764
- data-og-timeline-chrome=""
1765
- aria-hidden="true"
1766
- className="pointer-events-none absolute inset-x-0 top-0 h-px"
1767
- />
1768
- ) : null}
1769
- {groups.map(({ group, key, entranceEnabled }, index) => {
1770
- return (
1771
- <TimelineGroupEntry
1772
- key={key}
1773
- groupKey={key}
1774
- group={group}
1775
- nextGroup={groups[index + 1]?.group}
1776
- entranceEnabled={entranceEnabled}
1777
- liveEntranceEnabled={
1778
- group.kind === "activity" ? !bulkRender : undefined
1779
- }
1780
- context={timelineGroupEntryContext}
1859
+ <TimelineBeforeLayout
1860
+ capture={() => {
1861
+ readingAnchorRef.current =
1862
+ !pinnedRef.current && scrollRef.current
1863
+ ? captureTimelineAnchor(scrollRef.current)
1864
+ : null;
1865
+ }}
1866
+ >
1867
+ <div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
1868
+ <AnimatePresence>
1869
+ {loadingOlder ||
1870
+ loadingOldest ||
1871
+ (hasOlder && onJumpToStart && olderPrefetchArmed) ||
1872
+ underfillRetryReady ? (
1873
+ // Reserved top gutter: controls scroll with history and cannot
1874
+ // cover a disclosure. Visibility never changes content height.
1875
+ <motion.div
1876
+ initial={{ opacity: 0, y: -6 }}
1877
+ animate={{ opacity: 1, y: 0 }}
1878
+ exit={{ opacity: 0, y: -6 }}
1879
+ transition={{ duration: 0.15, ease: "easeOut" }}
1880
+ data-og-loading-older=""
1881
+ aria-live="polite"
1882
+ className="pointer-events-none absolute inset-x-0 -top-11 z-10 flex justify-center"
1883
+ >
1884
+ {loadingOlder || loadingOldest ? (
1885
+ <span className={LOADING_CHIP_CLASS}>
1886
+ <span className="og-shimmer-text">
1887
+ {loadingOldest
1888
+ ? "Jumping to start…"
1889
+ : "Loading earlier activity…"}
1890
+ </span>
1891
+ </span>
1892
+ ) : null}
1893
+ {hasOlder &&
1894
+ !loadingOlder &&
1895
+ !loadingOldest &&
1896
+ (underfillRetryReady || onJumpToStart) ? (
1897
+ <button
1898
+ type="button"
1899
+ data-og-retry={underfillRetryReady || undefined}
1900
+ data-og-jump-to-start={!underfillRetryReady || undefined}
1901
+ onClick={() => {
1902
+ const node = scrollRef.current;
1903
+ if (underfillRetryReady) {
1904
+ if (node && underfillRetryReadyRef.current) {
1905
+ // AnimatePresence retains this handler during
1906
+ // exit. Current authorization plus the exact
1907
+ // attempt check prevent stale dispatch.
1908
+ requestOlderIfUnderfilled(node, underfillSettledAttempt!);
1909
+ }
1910
+ return;
1911
+ }
1912
+ applyPinned(false);
1913
+ pendingJumpToStartRef.current = true;
1914
+ const seq = ++jumpToStartSeqRef.current;
1915
+ void Promise.resolve(onJumpToStart!()).then(
1916
+ () => {
1917
+ // The commit that swaps in the oldest window consumes
1918
+ // the flag against the new DOM; this write covers the
1919
+ // already-committed order and the no-window-change
1920
+ // case (jumping within the current window).
1921
+ const scroller = scrollRef.current ?? node;
1922
+ if (scroller) {
1923
+ scroller.scrollTop = 0;
1924
+ }
1925
+ // A host may resolve without ever changing the
1926
+ // window (already on the oldest page). Any swap
1927
+ // commit runs its layout effect before the next
1928
+ // frame, so a flag still armed by then is the
1929
+ // no-change case — clear it, or a LATER prepend
1930
+ // would spuriously jump the reader to the top.
1931
+ requestFrame(() => {
1932
+ if (jumpToStartSeqRef.current === seq) {
1933
+ pendingJumpToStartRef.current = false;
1934
+ }
1935
+ });
1936
+ },
1937
+ () => {
1938
+ if (jumpToStartSeqRef.current === seq) {
1939
+ pendingJumpToStartRef.current = false;
1940
+ }
1941
+ },
1942
+ );
1943
+ }}
1944
+ className="pointer-events-auto rounded-full border border-og-border px-3 py-1.5 text-og-control"
1945
+ >
1946
+ {underfillRetryReady ? "Retry earlier activity" : "Jump to start"}
1947
+ </button>
1948
+ ) : null}
1949
+ </motion.div>
1950
+ ) : null}
1951
+ </AnimatePresence>
1952
+ {!groups.length
1953
+ ? (emptyState ?? (
1954
+ <p className="py-10 text-center text-og-menu text-og-fg-subtle">
1955
+ No activity yet.
1956
+ </p>
1957
+ ))
1958
+ : null}
1959
+ {hasOlder && olderPrefetchArmed ? (
1960
+ // Overlaid, not a layout row: mounting/unmounting the sentinel
1961
+ // must never shift content (that shift was itself a wobble).
1962
+ // End at the scroll origin above the pt-16 gutter so the
1963
+ // observer and scrollTop cooldown share the same 400px band.
1964
+ <div
1965
+ ref={topSentinelRef}
1966
+ data-og-top-sentinel=""
1967
+ data-og-timeline-chrome=""
1968
+ aria-hidden="true"
1969
+ className="pointer-events-none absolute inset-x-0 -top-16 h-px -translate-y-full"
1781
1970
  />
1782
- );
1783
- })}
1784
- {groups.length > 0 && trailingState ? (
1785
- <div data-og-timeline-trailing-state="">{trailingState}</div>
1786
- ) : null}
1787
- {hasNewer ? (
1788
- <div
1789
- ref={bottomSentinelRef}
1790
- data-og-bottom-sentinel=""
1791
- data-og-timeline-chrome=""
1792
- aria-hidden="true"
1793
- className="h-px w-full"
1794
- />
1795
- ) : null}
1796
- </div>
1797
- </div>
1798
- <AnimatePresence>
1799
- {loadingOlder ||
1800
- loadingOldest ||
1801
- (hasOlder && onJumpToStart && olderPrefetchArmed) ||
1802
- underfillRetryReady ? (
1803
- // Floating over the scroller (not a timeline row) so showing and
1804
- // hiding it never reflows history under the reader.
1805
- <motion.div
1806
- initial={{ opacity: 0, y: -6 }}
1807
- animate={{ opacity: 1, y: 0 }}
1808
- exit={{ opacity: 0, y: -6 }}
1809
- transition={{ duration: 0.15, ease: "easeOut" }}
1810
- data-og-loading-older=""
1811
- aria-live="polite"
1812
- className="absolute inset-x-0 top-3 z-10 flex justify-center"
1813
- >
1814
- {loadingOlder || loadingOldest ? (
1815
- <span className={LOADING_CHIP_CLASS}>
1816
- <span className="og-shimmer-text">
1817
- {loadingOldest ? "Jumping to start…" : "Loading earlier activity…"}
1818
- </span>
1819
- </span>
1820
1971
  ) : null}
1821
- {hasOlder &&
1822
- !loadingOlder &&
1823
- !loadingOldest &&
1824
- (underfillRetryReady || onJumpToStart) ? (
1825
- <button
1826
- type="button"
1827
- data-og-retry={underfillRetryReady || undefined}
1828
- data-og-jump-to-start={!underfillRetryReady || undefined}
1829
- onClick={() => {
1830
- const node = scrollRef.current;
1831
- if (underfillRetryReady) {
1832
- if (node && underfillRetryReadyRef.current) {
1833
- // AnimatePresence retains this handler during
1834
- // exit. Current authorization plus the exact
1835
- // attempt check prevent stale dispatch.
1836
- requestOlderIfUnderfilled(node, underfillSettledAttempt!);
1837
- }
1838
- return;
1972
+ {groups.map(({ group, key, entranceEnabled }, index) => {
1973
+ return (
1974
+ <TimelineGroupEntry
1975
+ key={key}
1976
+ groupKey={key}
1977
+ group={group}
1978
+ nextGroup={groups[index + 1]?.group}
1979
+ entranceEnabled={entranceEnabled}
1980
+ liveEntranceEnabled={
1981
+ group.kind === "activity" ? !bulkRender : undefined
1839
1982
  }
1840
- applyPinned(false);
1841
- pendingJumpToStartRef.current = true;
1842
- const seq = ++jumpToStartSeqRef.current;
1843
- void Promise.resolve(onJumpToStart!()).then(
1844
- () => {
1845
- // The commit that swaps in the oldest window consumes
1846
- // the flag against the new DOM; this write covers the
1847
- // already-committed order and the no-window-change
1848
- // case (jumping within the current window).
1849
- const scroller = scrollRef.current ?? node;
1850
- if (scroller) {
1851
- scroller.scrollTop = 0;
1852
- }
1853
- // A host may resolve without ever changing the
1854
- // window (already on the oldest page). Any swap
1855
- // commit runs its layout effect before the next
1856
- // frame, so a flag still armed by then is the
1857
- // no-change case — clear it, or a LATER prepend
1858
- // would spuriously jump the reader to the top.
1859
- requestFrame(() => {
1860
- if (jumpToStartSeqRef.current === seq) {
1861
- pendingJumpToStartRef.current = false;
1862
- }
1863
- });
1864
- },
1865
- () => {
1866
- if (jumpToStartSeqRef.current === seq) {
1867
- pendingJumpToStartRef.current = false;
1868
- }
1869
- },
1870
- );
1871
- }}
1872
- className="rounded-full border border-og-border px-3 py-1.5 text-og-control"
1873
- >
1874
- {underfillRetryReady ? "Retry earlier activity" : "Jump to start"}
1875
- </button>
1983
+ context={timelineGroupEntryContext}
1984
+ />
1985
+ );
1986
+ })}
1987
+ {groups.length > 0 && trailingState ? (
1988
+ <div data-og-timeline-trailing-state="">{trailingState}</div>
1876
1989
  ) : null}
1877
- </motion.div>
1878
- ) : null}
1879
- </AnimatePresence>
1990
+ {hasNewer ? (
1991
+ <div
1992
+ ref={bottomSentinelRef}
1993
+ data-og-bottom-sentinel=""
1994
+ data-og-timeline-chrome=""
1995
+ aria-hidden="true"
1996
+ className="h-px w-full"
1997
+ />
1998
+ ) : null}
1999
+ </div>
2000
+ </TimelineBeforeLayout>
2001
+ </div>
2002
+
1880
2003
  <AnimatePresence>
1881
2004
  {loadingNewer ? (
1882
2005
  <motion.div
@@ -2781,7 +2904,7 @@ function CompactionRow({ item }: { item: ContextCompactionItem }) {
2781
2904
  ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
2782
2905
  : item.phase === "started"
2783
2906
  ? WAITING_PILL_CLASS
2784
- : "border-og-border bg-og-surface-1 text-og-fg-muted";
2907
+ : NEUTRAL_PILL;
2785
2908
  return (
2786
2909
  <div className={cn(enter && "animate-og-enter", "flex justify-center")}>
2787
2910
  <div
@@ -3277,41 +3400,46 @@ type GoalMeta = { label: string; pill: string; icon: ComponentType<{ className?:
3277
3400
 
3278
3401
  const NEUTRAL_PILL = "border-og-border bg-og-surface-1 text-og-fg-muted";
3279
3402
 
3280
- const GOAL_META: Record<GoalItem["action"], GoalMeta> = {
3281
- set: {
3282
- label: "Goal set",
3283
- pill: "border-og-accent/30 bg-og-accent/10 text-og-accent",
3284
- icon: TargetIcon,
3285
- },
3286
- updated: { label: "Goal updated", pill: NEUTRAL_PILL, icon: PencilLineIcon },
3287
- completed: {
3288
- label: "Goal completed",
3289
- pill: "border-og-status-idle/30 bg-og-status-idle/10 text-og-status-idle",
3290
- icon: CheckIcon,
3291
- },
3292
- paused: {
3293
- label: "Goal paused",
3294
- pill: WAITING_PILL_CLASS,
3295
- icon: PauseIcon,
3296
- },
3297
- resumed: { label: "Goal resumed", pill: NEUTRAL_PILL, icon: PlayIcon },
3298
- cleared: { label: "Goal cleared", pill: NEUTRAL_PILL, icon: Trash2Icon },
3299
- held: {
3300
- label: "Goal held",
3301
- pill: WAITING_PILL_CLASS,
3302
- icon: PauseCircleIcon,
3303
- },
3304
- continuation: { label: "Continuing toward the goal", pill: NEUTRAL_PILL, icon: ArrowRightIcon },
3305
- };
3403
+ // Shared production chunks can be cyclic. Resolve icon bindings during render,
3404
+ // after their modules initialize, rather than permanently capturing undefined.
3405
+ function goalMeta(action: GoalItem["action"]): GoalMeta {
3406
+ const metadata: Record<GoalItem["action"], GoalMeta> = {
3407
+ set: {
3408
+ label: "Goal set",
3409
+ pill: "border-og-accent/30 bg-og-accent/10 text-og-accent",
3410
+ icon: TargetIcon,
3411
+ },
3412
+ updated: { label: "Goal updated", pill: NEUTRAL_PILL, icon: PencilLineIcon },
3413
+ completed: {
3414
+ label: "Goal completed",
3415
+ pill: "border-og-status-idle/30 bg-og-status-idle/10 text-og-status-idle",
3416
+ icon: CheckIcon,
3417
+ },
3418
+ paused: {
3419
+ label: "Goal paused",
3420
+ pill: WAITING_PILL_CLASS,
3421
+ icon: PauseIcon,
3422
+ },
3423
+ resumed: { label: "Goal resumed", pill: NEUTRAL_PILL, icon: PlayIcon },
3424
+ cleared: { label: "Goal cleared", pill: NEUTRAL_PILL, icon: Trash2Icon },
3425
+ held: {
3426
+ label: "Goal held",
3427
+ pill: WAITING_PILL_CLASS,
3428
+ icon: PauseCircleIcon,
3429
+ },
3430
+ continuation: { label: "Continuing toward the goal", pill: NEUTRAL_PILL, icon: ArrowRightIcon },
3431
+ };
3432
+ return metadata[action];
3433
+ }
3306
3434
 
3307
3435
  /**
3308
3436
  * A goal landmark pill. Resolves its label, accent/tone, and glyph from
3309
- * {@link GOAL_META} so all six actions are visually distinguishable while the
3437
+ * {@link goalMeta} so all six actions are visually distinguishable while the
3310
3438
  * palette stays restrained — see that table for the per-action rationale.
3311
3439
  */
3312
3440
  function GoalRow({ item }: { item: GoalItem }) {
3313
3441
  const enter = useEntranceAnimation();
3314
- const { label, pill, icon: Icon } = GOAL_META[item.action];
3442
+ const { label, pill, icon: Icon } = goalMeta(item.action);
3315
3443
  return (
3316
3444
  <div className={cn(enter && "animate-og-enter", "flex justify-center")}>
3317
3445
  <span
@@ -3354,7 +3482,11 @@ function MachineInputBatchRow({
3354
3482
 
3355
3483
  return (
3356
3484
  <div className={cn(enter && "animate-og-enter", "flex flex-col items-center gap-1.5")}>
3357
- <details className="group w-full max-w-full" data-og-machine-input-batch="">
3485
+ <details
3486
+ className="group w-full max-w-full"
3487
+ data-og-machine-input-batch=""
3488
+ title={`Received ${formatClockTime(item.occurredAt)}`}
3489
+ >
3358
3490
  <summary className="flex cursor-pointer list-none justify-center [&::-webkit-details-marker]:hidden">
3359
3491
  <span
3360
3492
  className={cn(
@@ -3370,6 +3502,9 @@ function MachineInputBatchRow({
3370
3502
  </span>
3371
3503
  </summary>
3372
3504
  <div className="mx-auto mt-2 w-full max-w-lg space-y-2 border-t border-og-border/50 pt-2">
3505
+ <p className="text-og-xs text-og-fg-subtle">
3506
+ Received <time dateTime={item.occurredAt}>{formatClockTime(item.occurredAt)}</time>
3507
+ </p>
3373
3508
  {item.members.map((member) => (
3374
3509
  <MachineInputRow
3375
3510
  key={member.id}
@@ -3497,7 +3632,7 @@ function NoticeRow({ item }: { item: NoticeItem }) {
3497
3632
  ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
3498
3633
  : item.tone === "waiting"
3499
3634
  ? WAITING_PILL_CLASS
3500
- : "border-og-border bg-og-surface-1 text-og-fg-muted";
3635
+ : NEUTRAL_PILL;
3501
3636
  return (
3502
3637
  <div
3503
3638
  className={cn(
@@ -3505,12 +3640,24 @@ function NoticeRow({ item }: { item: NoticeItem }) {
3505
3640
  "flex items-start gap-2.5 rounded-og-md border px-3.5 py-2.5 text-og-menu",
3506
3641
  tone,
3507
3642
  )}
3508
- role="status"
3643
+ role={item.recordedOutcome ? "note" : "status"}
3644
+ data-og-recorded-outcome={item.recordedOutcome ? "wait" : undefined}
3509
3645
  >
3510
3646
  <TriangleAlertIcon
3511
3647
  className={cn("mt-0.5 size-4 shrink-0", item.tone === "cancelled" && "opacity-60")}
3512
3648
  />
3513
3649
  <div className="min-w-0 flex-1">
3650
+ {item.recordedOutcome ? (
3651
+ <p className="mb-1 text-og-control font-medium">
3652
+ Wait recorded{" "}
3653
+ <time dateTime={item.occurredAt}>
3654
+ {new Date(item.occurredAt).toLocaleString(undefined, {
3655
+ dateStyle: "medium",
3656
+ timeStyle: "short",
3657
+ })}
3658
+ </time>
3659
+ </p>
3660
+ ) : null}
3514
3661
  <span className="whitespace-pre-wrap break-words">{item.text}</span>
3515
3662
  {item.details ? (
3516
3663
  <details className="mt-2 text-og-control">