@opengeni/react 3.7.0-canary.0 → 3.7.0-canary.2

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 (67) hide show
  1. package/dist/artifacts.js +24 -5
  2. package/dist/artifacts.js.map +1 -1
  3. package/dist/{browser-viewer-E5DDEFQ4.js → browser-viewer-NTD6UIPR.js} +3 -3
  4. package/dist/{chunk-W2RGYBCV.js → chunk-24M4YBCL.js} +4 -4
  5. package/dist/{chunk-R55HFTT3.js → chunk-3IY6UZI6.js} +4306 -4020
  6. package/dist/chunk-3IY6UZI6.js.map +1 -0
  7. package/dist/chunk-5PQJOCX5.js +902 -0
  8. package/dist/chunk-5PQJOCX5.js.map +1 -0
  9. package/dist/{chunk-FJBOU2TH.js → chunk-MOWYEBCQ.js} +1728 -12
  10. package/dist/chunk-MOWYEBCQ.js.map +1 -0
  11. package/dist/chunk-PAJU5KKL.js +331 -0
  12. package/dist/chunk-PAJU5KKL.js.map +1 -0
  13. package/dist/{chunk-VTOREEXF.js → chunk-PLYRL5ER.js} +24 -2
  14. package/dist/{chunk-VTOREEXF.js.map → chunk-PLYRL5ER.js.map} +1 -1
  15. package/dist/{chunk-DOK4KZF2.js → chunk-WZIMM3GX.js} +1536 -539
  16. package/dist/chunk-WZIMM3GX.js.map +1 -0
  17. package/dist/{chunk-YVCHAQZY.js → chunk-ZFOFY5ST.js} +27 -349
  18. package/dist/chunk-ZFOFY5ST.js.map +1 -0
  19. package/dist/components/artifacts/published-html-artifact-frame.d.ts +3 -2
  20. package/dist/components/child-session-link.d.ts +5 -0
  21. package/dist/components/session-chrome.d.ts +43 -6
  22. package/dist/components/session-commands-panel.d.ts +6 -0
  23. package/dist/components/session-conversation.d.ts +14 -0
  24. package/dist/components/timeline-anchor.d.ts +21 -0
  25. package/dist/composer.js +2 -3
  26. package/dist/conversation-timeline.d.ts +5 -0
  27. package/dist/index.d.ts +4 -1
  28. package/dist/index.js +716 -914
  29. package/dist/index.js.map +1 -1
  30. package/dist/interaction.js +2 -2
  31. package/dist/lib/format.d.ts +5 -0
  32. package/dist/machines.js +3 -2
  33. package/dist/session-ui.d.ts +3 -0
  34. package/dist/session-ui.js +14 -4
  35. package/dist/session.js +19 -19
  36. package/dist/timeline/types.d.ts +3 -1
  37. package/package.json +2 -2
  38. package/src/components/artifacts/published-html-artifact-frame.tsx +27 -4
  39. package/src/components/child-session-link.tsx +27 -0
  40. package/src/components/composer.tsx +1 -1
  41. package/src/components/machine-input-display.ts +8 -2
  42. package/src/components/message-timeline.tsx +336 -198
  43. package/src/components/session-chrome.tsx +572 -161
  44. package/src/components/session-commands-panel.tsx +113 -0
  45. package/src/components/session-conversation.tsx +141 -0
  46. package/src/components/timeline-anchor.tsx +91 -0
  47. package/src/conversation-timeline.ts +83 -0
  48. package/src/hooks/use-session-background-commands.ts +21 -10
  49. package/src/hooks/use-session-events.ts +36 -14
  50. package/src/hooks/use-session.ts +23 -2
  51. package/src/index.ts +4 -0
  52. package/src/lib/format.ts +32 -0
  53. package/src/session-ui.ts +3 -0
  54. package/src/timeline/projection.ts +6 -12
  55. package/src/timeline/types.ts +4 -0
  56. package/styles/compiled.css +513 -410
  57. package/styles/index.css +23 -0
  58. package/dist/chunk-7EAFGICR.js +0 -302
  59. package/dist/chunk-7EAFGICR.js.map +0 -1
  60. package/dist/chunk-C6WDLHBA.js +0 -2590
  61. package/dist/chunk-C6WDLHBA.js.map +0 -1
  62. package/dist/chunk-DOK4KZF2.js.map +0 -1
  63. package/dist/chunk-FJBOU2TH.js.map +0 -1
  64. package/dist/chunk-R55HFTT3.js.map +0 -1
  65. package/dist/chunk-YVCHAQZY.js.map +0 -1
  66. /package/dist/{browser-viewer-E5DDEFQ4.js.map → browser-viewer-NTD6UIPR.js.map} +0 -0
  67. /package/dist/{chunk-W2RGYBCV.js.map → chunk-24M4YBCL.js.map} +0 -0
@@ -1,3 +1,4 @@
1
+ import { ChildSessionLink } from "./child-session-link";
1
2
  import type {
2
3
  DraftTimelineAnnotation,
3
4
  MediaGenerationResult,
@@ -49,6 +50,12 @@ import {
49
50
  type OlderHistoryLoader,
50
51
  } from "../older-history";
51
52
  import { Markdown } from "./markdown";
53
+ import {
54
+ TimelineBeforeLayout,
55
+ captureTimelineAnchor,
56
+ timelineAnchorCorrection,
57
+ type TimelineAnchor,
58
+ } from "./timeline-anchor";
52
59
  import {
53
60
  UserMessageBody,
54
61
  UserMessageDisclosureProvider,
@@ -418,8 +425,15 @@ export function MessageTimeline({
418
425
  (item) => item.kind !== "auth-needed" || shouldRenderAuthNeeded(item),
419
426
  );
420
427
  }, [items, events, shouldRenderAuthNeeded]);
421
- const sourceItems = items || events;
428
+ // Event-window identity is independent of projected rows (partial messages
429
+ // can acquire a different first-delta id when older text arrives).
430
+ const sourceItems = events ?? items;
422
431
  const olderBoundaryKey = sourceItems?.[0]?.id;
432
+ const previousSourceIdsRef = useRef(new Set<string>());
433
+ const previousSourceBoundaryRef = useRef<string | undefined>(undefined);
434
+ const readingAnchorRef = useRef<TimelineAnchor | null>(null);
435
+ const olderPageBudgetRef = useRef(0);
436
+ const [olderDemand, setOlderDemand] = useState(0);
423
437
  const allGroups = useMemo(() => groupTimeline(resolvedItems), [resolvedItems]);
424
438
  const annotationSources = useMemo(() => {
425
439
  const sources = new Map<string, TimelineAnnotationSourceDescriptor>();
@@ -648,29 +662,22 @@ export function MessageTimeline({
648
662
  }, []);
649
663
 
650
664
  /** 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
- );
665
+ const releasePinFromReader = useCallback(() => {
666
+ if (!autoFollow || !pinnedRef.current || hasNewerRef.current) {
667
+ return;
668
+ }
669
+ clearReaderIntent();
670
+ clearPendingReaderLeave();
671
+ stopFollow();
672
+ applyPinned(false);
673
+ if (wantPinRef.current) {
674
+ wantPinRef.current = false;
675
+ }
676
+ if (!olderPrefetchArmedRef.current) {
677
+ olderPrefetchArmedRef.current = true;
678
+ setOlderPrefetchArmed(true);
679
+ }
680
+ }, [autoFollow, applyPinned, clearPendingReaderLeave, clearReaderIntent, stopFollow]);
674
681
 
675
682
  /**
676
683
  * Settled away from the tip while the camera is idle — Vimium / unfocused
@@ -691,7 +698,7 @@ export function MessageTimeline({
691
698
  clearPendingReaderLeave();
692
699
  return;
693
700
  }
694
- releasePinFromReader(node);
701
+ releasePinFromReader();
695
702
  rearmOlderPrefetchAfterLeavingTop(node);
696
703
  },
697
704
  [autoFollow, clearPendingReaderLeave, rearmOlderPrefetchAfterLeavingTop, releasePinFromReader],
@@ -712,6 +719,19 @@ export function MessageTimeline({
712
719
  });
713
720
  }, [cancelLeaveFallback, releasePinAfterScrollSettled]);
714
721
 
722
+ const requestEarlierFromReader = () => {
723
+ releasePinFromReader();
724
+ wantPinRef.current = false;
725
+ // A stationary upward gesture is still demand. Successful short/folded
726
+ // pages may never create enough range to leave the prefetch band.
727
+ olderPageBudgetRef.current = 8;
728
+ if (olderLoadAttemptRef.current?.[1] === 2) {
729
+ olderLoadAttemptRef.current = null;
730
+ setOlderDemand((value) => value + 1);
731
+ }
732
+ };
733
+ const touchPositionRef = useRef<{ x: number; y: number } | null>(null);
734
+
715
735
  const onWheel = (event: {
716
736
  deltaY: number;
717
737
  deltaX: number;
@@ -729,12 +749,11 @@ export function MessageTimeline({
729
749
  return;
730
750
  }
731
751
  disclosureKeepsUnpinnedRef.current = false;
752
+ programmaticScrollRef.current = 0;
732
753
  if (event.deltaY >= 0) {
733
754
  return;
734
755
  }
735
- const node =
736
- event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
737
- releasePinFromReader(node);
756
+ requestEarlierFromReader();
738
757
  };
739
758
 
740
759
  /** Touch / stylus / mouse drag on the scroller — explicit leave (not layout). */
@@ -779,9 +798,8 @@ export function MessageTimeline({
779
798
  if (event.key !== "ArrowUp" && event.key !== "PageUp" && event.key !== "Home") {
780
799
  return;
781
800
  }
782
- const node =
783
- event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
784
- releasePinFromReader(node);
801
+ programmaticScrollRef.current = 0;
802
+ requestEarlierFromReader();
785
803
  };
786
804
 
787
805
  const snapToBottom = useCallback(
@@ -1103,15 +1121,26 @@ export function MessageTimeline({
1103
1121
  previousMaxScroll <= 1 ||
1104
1122
  previousMaxScroll - previousScrollTop < Math.min(PIN_THRESHOLD_PX, previousMaxScroll);
1105
1123
  const firstItemChanged = !!previousFirstItemId && firstItemId !== previousFirstItemId;
1106
- const prepended =
1107
- firstItemChanged && resolvedItems.some((item) => item.id === previousFirstItemId);
1124
+ const sourceChanged = olderBoundaryKey !== previousSourceBoundaryRef.current;
1125
+ const retainedSource =
1126
+ sourceItems?.some((item) => previousSourceIdsRef.current.has(item.id)) ?? false;
1127
+ const prepended = sourceChanged && retainedSource;
1128
+ previousSourceBoundaryRef.current = olderBoundaryKey;
1129
+ previousSourceIdsRef.current = new Set(sourceItems?.map((item) => item.id));
1130
+ const readingAnchor = readingAnchorRef.current;
1131
+ readingAnchorRef.current = null;
1108
1132
  const attempt = olderLoadAttemptRef.current;
1109
1133
  const committedZeroOverlapOlderReplacement = !!(
1110
1134
  attempt?.[2]?.committed &&
1111
- firstItemChanged &&
1112
- !prepended
1135
+ sourceChanged &&
1136
+ !retainedSource
1113
1137
  );
1114
1138
  const restorePrependAnchor = () => {
1139
+ const correction = readingAnchor && timelineAnchorCorrection(node, readingAnchor);
1140
+ if (correction != null) {
1141
+ if (Math.abs(correction) > 1) writeScrollTop(node, node.scrollTop + correction);
1142
+ return;
1143
+ }
1115
1144
  // Keep the reader on the same retained rows. Prefer the exact first-item
1116
1145
  // content coordinate (needed when a prepend merges inside one group),
1117
1146
  // then the retained group offset, then scrollHeight as a final fallback.
@@ -1289,6 +1318,19 @@ export function MessageTimeline({
1289
1318
  // Boundary progress retires the request owner. A late settlement from
1290
1319
  // that completed prefetch cannot mutate the new window's cooldown owner.
1291
1320
  olderLoadAttemptRef.current = [olderBoundaryKey, 2];
1321
+ // Bound automatic work, but let continued upward input replenish demand.
1322
+ // Only a committed first-party receipt proves that another page is safe.
1323
+ if (
1324
+ attempt[2]?.committed &&
1325
+ !pinnedRef.current &&
1326
+ hasOlder &&
1327
+ node.scrollTop <= OLDER_PREFETCH_MARGIN_PX &&
1328
+ olderPageBudgetRef.current > 0
1329
+ ) {
1330
+ olderPageBudgetRef.current -= 1;
1331
+ olderLoadAttemptRef.current = null;
1332
+ setOlderDemand((value) => value + 1);
1333
+ }
1292
1334
  rearmOlderPrefetchAfterLeavingTop(node);
1293
1335
  requestOlderIfUnderfilled(node);
1294
1336
  });
@@ -1459,6 +1501,7 @@ export function MessageTimeline({
1459
1501
  loadingOlder,
1460
1502
  olderBoundaryKey,
1461
1503
  olderPrefetchArmed,
1504
+ olderDemand,
1462
1505
  onLoadOlder,
1463
1506
  requestOlderIfUnderfilled,
1464
1507
  ]);
@@ -1651,7 +1694,7 @@ export function MessageTimeline({
1651
1694
  // Pointer-dragged scroll-up away from tip. Layout churn never arms this.
1652
1695
  if (readerArmed && cumulativeReaderUp > TIP_FOLLOW_READER_UP_EPS_PX && !nearBottomPinned) {
1653
1696
  clearReaderIntent();
1654
- releasePinFromReader(node);
1697
+ requestEarlierFromReader();
1655
1698
  rearmOlderPrefetchAfterLeavingTop(node);
1656
1699
  return;
1657
1700
  }
@@ -1676,6 +1719,10 @@ export function MessageTimeline({
1676
1719
  }
1677
1720
 
1678
1721
  syncScrollBaseline(node);
1722
+ if (programmatic) {
1723
+ // Anchor restoration is never permission to resume tip-follow.
1724
+ return;
1725
+ }
1679
1726
  const nearBottom = isNearBottom(node);
1680
1727
 
1681
1728
  // Re-pin only when the reader moved toward/at the tip without a content
@@ -1685,8 +1732,7 @@ export function MessageTimeline({
1685
1732
  // inside PIN_THRESHOLD once the window is tall) and snapped them back.
1686
1733
  const inserted = Math.max(0, nextMaxScroll - previousMaxScroll);
1687
1734
  const towardTip = nextTop - previousTop - inserted;
1688
- const nextPinned =
1689
- !hasNewer && nearBottom && towardTip >= -TIP_FOLLOW_READER_UP_EPS_PX && inserted <= 1;
1735
+ const nextPinned = !hasNewer && nearBottom && towardTip > 0.5 && inserted <= 1;
1690
1736
  if (!nextPinned) {
1691
1737
  stopFollow();
1692
1738
  }
@@ -1759,148 +1805,202 @@ export function MessageTimeline({
1759
1805
  onScroll={onScroll}
1760
1806
  onScrollEnd={onScrollEnd}
1761
1807
  onWheel={onWheel}
1808
+ onTouchStart={(event) => {
1809
+ const touch = event.touches.length === 1 ? event.touches[0] : undefined;
1810
+ touchPositionRef.current = touch
1811
+ ? { x: touch.clientX, y: touch.clientY }
1812
+ : null;
1813
+ }}
1814
+ onTouchMove={(event) => {
1815
+ const touch = event.touches[0];
1816
+ const previous = touchPositionRef.current;
1817
+ if (!touch || !previous || event.touches.length !== 1) {
1818
+ touchPositionRef.current = null;
1819
+ return;
1820
+ }
1821
+ const deltaX = previous.x - touch.clientX;
1822
+ const deltaY = previous.y - touch.clientY;
1823
+ if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 4) return;
1824
+ touchPositionRef.current = { x: touch.clientX, y: touch.clientY };
1825
+ onWheel({
1826
+ deltaX,
1827
+ deltaY,
1828
+ target: event.target,
1829
+ currentTarget: event.currentTarget,
1830
+ });
1831
+ }}
1832
+ onTouchEnd={() => {
1833
+ touchPositionRef.current = null;
1834
+ }}
1835
+ onTouchCancel={() => {
1836
+ touchPositionRef.current = null;
1837
+ }}
1838
+ onClickCapture={(event) => {
1839
+ const target =
1840
+ event.target instanceof Element
1841
+ ? event.target.closest("button[aria-expanded]")
1842
+ : null;
1843
+ if (target) {
1844
+ releasePinFromReader();
1845
+ disclosureKeepsUnpinnedRef.current = true;
1846
+ }
1847
+ }}
1762
1848
  onPointerDown={onPointerDown}
1763
1849
  onKeyDown={onKeyDown}
1764
1850
  style={groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined}
1765
1851
  className={cn(
1766
1852
  // tabIndex=-1 is programmatic only — never paint a focus ring on
1767
1853
  // the whole scroller (click + Shift used to flash a blue outline).
1768
- "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-6 sm:px-6 outline-hidden",
1854
+ "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pt-16 pb-6 sm:px-6 outline-hidden",
1769
1855
  autoFollow && pinned && !hasNewer
1770
1856
  ? "[overflow-anchor:none]"
1771
1857
  : "[overflow-anchor:auto]",
1772
1858
  )}
1773
1859
  >
1774
- <div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
1775
- {!groups.length
1776
- ? (emptyState ?? (
1777
- <p className="py-10 text-center text-og-menu text-og-fg-subtle">
1778
- No activity yet.
1779
- </p>
1780
- ))
1781
- : null}
1782
- {hasOlder && olderPrefetchArmed ? (
1783
- // Overlaid, not a layout row: mounting/unmounting the sentinel
1784
- // must never shift content (that shift was itself a wobble).
1785
- <div
1786
- ref={topSentinelRef}
1787
- data-og-top-sentinel=""
1788
- data-og-timeline-chrome=""
1789
- aria-hidden="true"
1790
- className="pointer-events-none absolute inset-x-0 top-0 h-px"
1791
- />
1792
- ) : null}
1793
- {groups.map(({ group, key, entranceEnabled }, index) => {
1794
- return (
1795
- <TimelineGroupEntry
1796
- key={key}
1797
- groupKey={key}
1798
- group={group}
1799
- nextGroup={groups[index + 1]?.group}
1800
- entranceEnabled={entranceEnabled}
1801
- liveEntranceEnabled={
1802
- group.kind === "activity" ? !bulkRender : undefined
1803
- }
1804
- context={timelineGroupEntryContext}
1860
+ <TimelineBeforeLayout
1861
+ capture={() => {
1862
+ readingAnchorRef.current =
1863
+ !pinnedRef.current && scrollRef.current
1864
+ ? captureTimelineAnchor(scrollRef.current)
1865
+ : null;
1866
+ }}
1867
+ >
1868
+ <div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
1869
+ <AnimatePresence>
1870
+ {loadingOlder ||
1871
+ loadingOldest ||
1872
+ (hasOlder && onJumpToStart && olderPrefetchArmed) ||
1873
+ underfillRetryReady ? (
1874
+ // Reserved top gutter: controls scroll with history and cannot
1875
+ // cover a disclosure. Visibility never changes content height.
1876
+ <motion.div
1877
+ initial={{ opacity: 0, y: -6 }}
1878
+ animate={{ opacity: 1, y: 0 }}
1879
+ exit={{ opacity: 0, y: -6 }}
1880
+ transition={{ duration: 0.15, ease: "easeOut" }}
1881
+ data-og-loading-older=""
1882
+ aria-live="polite"
1883
+ className="pointer-events-none absolute inset-x-0 -top-11 z-10 flex justify-center"
1884
+ >
1885
+ {loadingOlder || loadingOldest ? (
1886
+ <span className={LOADING_CHIP_CLASS}>
1887
+ <span className="og-shimmer-text">
1888
+ {loadingOldest
1889
+ ? "Jumping to start…"
1890
+ : "Loading earlier activity…"}
1891
+ </span>
1892
+ </span>
1893
+ ) : null}
1894
+ {hasOlder &&
1895
+ !loadingOlder &&
1896
+ !loadingOldest &&
1897
+ (underfillRetryReady || onJumpToStart) ? (
1898
+ <button
1899
+ type="button"
1900
+ data-og-retry={underfillRetryReady || undefined}
1901
+ data-og-jump-to-start={!underfillRetryReady || undefined}
1902
+ onClick={() => {
1903
+ const node = scrollRef.current;
1904
+ if (underfillRetryReady) {
1905
+ if (node && underfillRetryReadyRef.current) {
1906
+ // AnimatePresence retains this handler during
1907
+ // exit. Current authorization plus the exact
1908
+ // attempt check prevent stale dispatch.
1909
+ requestOlderIfUnderfilled(node, underfillSettledAttempt!);
1910
+ }
1911
+ return;
1912
+ }
1913
+ applyPinned(false);
1914
+ pendingJumpToStartRef.current = true;
1915
+ const seq = ++jumpToStartSeqRef.current;
1916
+ void Promise.resolve(onJumpToStart!()).then(
1917
+ () => {
1918
+ // The commit that swaps in the oldest window consumes
1919
+ // the flag against the new DOM; this write covers the
1920
+ // already-committed order and the no-window-change
1921
+ // case (jumping within the current window).
1922
+ const scroller = scrollRef.current ?? node;
1923
+ if (scroller) {
1924
+ scroller.scrollTop = 0;
1925
+ }
1926
+ // A host may resolve without ever changing the
1927
+ // window (already on the oldest page). Any swap
1928
+ // commit runs its layout effect before the next
1929
+ // frame, so a flag still armed by then is the
1930
+ // no-change case — clear it, or a LATER prepend
1931
+ // would spuriously jump the reader to the top.
1932
+ requestFrame(() => {
1933
+ if (jumpToStartSeqRef.current === seq) {
1934
+ pendingJumpToStartRef.current = false;
1935
+ }
1936
+ });
1937
+ },
1938
+ () => {
1939
+ if (jumpToStartSeqRef.current === seq) {
1940
+ pendingJumpToStartRef.current = false;
1941
+ }
1942
+ },
1943
+ );
1944
+ }}
1945
+ className="pointer-events-auto rounded-full border border-og-border px-3 py-1.5 text-og-control"
1946
+ >
1947
+ {underfillRetryReady ? "Retry earlier activity" : "Jump to start"}
1948
+ </button>
1949
+ ) : null}
1950
+ </motion.div>
1951
+ ) : null}
1952
+ </AnimatePresence>
1953
+ {!groups.length
1954
+ ? (emptyState ?? (
1955
+ <p className="py-10 text-center text-og-menu text-og-fg-subtle">
1956
+ No activity yet.
1957
+ </p>
1958
+ ))
1959
+ : null}
1960
+ {hasOlder && olderPrefetchArmed ? (
1961
+ // Overlaid, not a layout row: mounting/unmounting the sentinel
1962
+ // must never shift content (that shift was itself a wobble).
1963
+ // End at the scroll origin above the pt-16 gutter so the
1964
+ // observer and scrollTop cooldown share the same 400px band.
1965
+ <div
1966
+ ref={topSentinelRef}
1967
+ data-og-top-sentinel=""
1968
+ data-og-timeline-chrome=""
1969
+ aria-hidden="true"
1970
+ className="pointer-events-none absolute inset-x-0 -top-16 h-px -translate-y-full"
1805
1971
  />
1806
- );
1807
- })}
1808
- {groups.length > 0 && trailingState ? (
1809
- <div data-og-timeline-trailing-state="">{trailingState}</div>
1810
- ) : null}
1811
- {hasNewer ? (
1812
- <div
1813
- ref={bottomSentinelRef}
1814
- data-og-bottom-sentinel=""
1815
- data-og-timeline-chrome=""
1816
- aria-hidden="true"
1817
- className="h-px w-full"
1818
- />
1819
- ) : null}
1820
- </div>
1821
- </div>
1822
- <AnimatePresence>
1823
- {loadingOlder ||
1824
- loadingOldest ||
1825
- (hasOlder && onJumpToStart && olderPrefetchArmed) ||
1826
- underfillRetryReady ? (
1827
- // Floating over the scroller (not a timeline row) so showing and
1828
- // hiding it never reflows history under the reader.
1829
- <motion.div
1830
- initial={{ opacity: 0, y: -6 }}
1831
- animate={{ opacity: 1, y: 0 }}
1832
- exit={{ opacity: 0, y: -6 }}
1833
- transition={{ duration: 0.15, ease: "easeOut" }}
1834
- data-og-loading-older=""
1835
- aria-live="polite"
1836
- className="absolute inset-x-0 top-3 z-10 flex justify-center"
1837
- >
1838
- {loadingOlder || loadingOldest ? (
1839
- <span className={LOADING_CHIP_CLASS}>
1840
- <span className="og-shimmer-text">
1841
- {loadingOldest ? "Jumping to start…" : "Loading earlier activity…"}
1842
- </span>
1843
- </span>
1844
1972
  ) : null}
1845
- {hasOlder &&
1846
- !loadingOlder &&
1847
- !loadingOldest &&
1848
- (underfillRetryReady || onJumpToStart) ? (
1849
- <button
1850
- type="button"
1851
- data-og-retry={underfillRetryReady || undefined}
1852
- data-og-jump-to-start={!underfillRetryReady || undefined}
1853
- onClick={() => {
1854
- const node = scrollRef.current;
1855
- if (underfillRetryReady) {
1856
- if (node && underfillRetryReadyRef.current) {
1857
- // AnimatePresence retains this handler during
1858
- // exit. Current authorization plus the exact
1859
- // attempt check prevent stale dispatch.
1860
- requestOlderIfUnderfilled(node, underfillSettledAttempt!);
1861
- }
1862
- return;
1973
+ {groups.map(({ group, key, entranceEnabled }, index) => {
1974
+ return (
1975
+ <TimelineGroupEntry
1976
+ key={key}
1977
+ groupKey={key}
1978
+ group={group}
1979
+ nextGroup={groups[index + 1]?.group}
1980
+ entranceEnabled={entranceEnabled}
1981
+ liveEntranceEnabled={
1982
+ group.kind === "activity" ? !bulkRender : undefined
1863
1983
  }
1864
- applyPinned(false);
1865
- pendingJumpToStartRef.current = true;
1866
- const seq = ++jumpToStartSeqRef.current;
1867
- void Promise.resolve(onJumpToStart!()).then(
1868
- () => {
1869
- // The commit that swaps in the oldest window consumes
1870
- // the flag against the new DOM; this write covers the
1871
- // already-committed order and the no-window-change
1872
- // case (jumping within the current window).
1873
- const scroller = scrollRef.current ?? node;
1874
- if (scroller) {
1875
- scroller.scrollTop = 0;
1876
- }
1877
- // A host may resolve without ever changing the
1878
- // window (already on the oldest page). Any swap
1879
- // commit runs its layout effect before the next
1880
- // frame, so a flag still armed by then is the
1881
- // no-change case — clear it, or a LATER prepend
1882
- // would spuriously jump the reader to the top.
1883
- requestFrame(() => {
1884
- if (jumpToStartSeqRef.current === seq) {
1885
- pendingJumpToStartRef.current = false;
1886
- }
1887
- });
1888
- },
1889
- () => {
1890
- if (jumpToStartSeqRef.current === seq) {
1891
- pendingJumpToStartRef.current = false;
1892
- }
1893
- },
1894
- );
1895
- }}
1896
- className="rounded-full border border-og-border px-3 py-1.5 text-og-control"
1897
- >
1898
- {underfillRetryReady ? "Retry earlier activity" : "Jump to start"}
1899
- </button>
1984
+ context={timelineGroupEntryContext}
1985
+ />
1986
+ );
1987
+ })}
1988
+ {groups.length > 0 && trailingState ? (
1989
+ <div data-og-timeline-trailing-state="">{trailingState}</div>
1900
1990
  ) : null}
1901
- </motion.div>
1902
- ) : null}
1903
- </AnimatePresence>
1991
+ {hasNewer ? (
1992
+ <div
1993
+ ref={bottomSentinelRef}
1994
+ data-og-bottom-sentinel=""
1995
+ data-og-timeline-chrome=""
1996
+ aria-hidden="true"
1997
+ className="h-px w-full"
1998
+ />
1999
+ ) : null}
2000
+ </div>
2001
+ </TimelineBeforeLayout>
2002
+ </div>
2003
+
1904
2004
  <AnimatePresence>
1905
2005
  {loadingNewer ? (
1906
2006
  <motion.div
@@ -2748,7 +2848,11 @@ export function TimelineRow({
2748
2848
  return <GoalRow item={item} />;
2749
2849
  case "machine-input-batch":
2750
2850
  return (
2751
- <MachineInputBatchRow item={item} loadVideoArtifactPlayback={loadVideoArtifactPlayback} />
2851
+ <MachineInputBatchRow
2852
+ item={item}
2853
+ onOpenSession={onOpenSession}
2854
+ loadVideoArtifactPlayback={loadVideoArtifactPlayback}
2855
+ />
2752
2856
  );
2753
2857
  case "notice":
2754
2858
  return <NoticeRow item={item} />;
@@ -2805,7 +2909,7 @@ function CompactionRow({ item }: { item: ContextCompactionItem }) {
2805
2909
  ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
2806
2910
  : item.phase === "started"
2807
2911
  ? WAITING_PILL_CLASS
2808
- : "border-og-border bg-og-surface-1 text-og-fg-muted";
2912
+ : NEUTRAL_PILL;
2809
2913
  return (
2810
2914
  <div className={cn(enter && "animate-og-enter", "flex justify-center")}>
2811
2915
  <div
@@ -3301,41 +3405,46 @@ type GoalMeta = { label: string; pill: string; icon: ComponentType<{ className?:
3301
3405
 
3302
3406
  const NEUTRAL_PILL = "border-og-border bg-og-surface-1 text-og-fg-muted";
3303
3407
 
3304
- const GOAL_META: Record<GoalItem["action"], GoalMeta> = {
3305
- set: {
3306
- label: "Goal set",
3307
- pill: "border-og-accent/30 bg-og-accent/10 text-og-accent",
3308
- icon: TargetIcon,
3309
- },
3310
- updated: { label: "Goal updated", pill: NEUTRAL_PILL, icon: PencilLineIcon },
3311
- completed: {
3312
- label: "Goal completed",
3313
- pill: "border-og-status-idle/30 bg-og-status-idle/10 text-og-status-idle",
3314
- icon: CheckIcon,
3315
- },
3316
- paused: {
3317
- label: "Goal paused",
3318
- pill: WAITING_PILL_CLASS,
3319
- icon: PauseIcon,
3320
- },
3321
- resumed: { label: "Goal resumed", pill: NEUTRAL_PILL, icon: PlayIcon },
3322
- cleared: { label: "Goal cleared", pill: NEUTRAL_PILL, icon: Trash2Icon },
3323
- held: {
3324
- label: "Goal held",
3325
- pill: WAITING_PILL_CLASS,
3326
- icon: PauseCircleIcon,
3327
- },
3328
- continuation: { label: "Continuing toward the goal", pill: NEUTRAL_PILL, icon: ArrowRightIcon },
3329
- };
3408
+ // Shared production chunks can be cyclic. Resolve icon bindings during render,
3409
+ // after their modules initialize, rather than permanently capturing undefined.
3410
+ function goalMeta(action: GoalItem["action"]): GoalMeta {
3411
+ const metadata: Record<GoalItem["action"], GoalMeta> = {
3412
+ set: {
3413
+ label: "Goal set",
3414
+ pill: "border-og-accent/30 bg-og-accent/10 text-og-accent",
3415
+ icon: TargetIcon,
3416
+ },
3417
+ updated: { label: "Goal updated", pill: NEUTRAL_PILL, icon: PencilLineIcon },
3418
+ completed: {
3419
+ label: "Goal completed",
3420
+ pill: "border-og-status-idle/30 bg-og-status-idle/10 text-og-status-idle",
3421
+ icon: CheckIcon,
3422
+ },
3423
+ paused: {
3424
+ label: "Goal paused",
3425
+ pill: WAITING_PILL_CLASS,
3426
+ icon: PauseIcon,
3427
+ },
3428
+ resumed: { label: "Goal resumed", pill: NEUTRAL_PILL, icon: PlayIcon },
3429
+ cleared: { label: "Goal cleared", pill: NEUTRAL_PILL, icon: Trash2Icon },
3430
+ held: {
3431
+ label: "Goal held",
3432
+ pill: WAITING_PILL_CLASS,
3433
+ icon: PauseCircleIcon,
3434
+ },
3435
+ continuation: { label: "Continuing toward the goal", pill: NEUTRAL_PILL, icon: ArrowRightIcon },
3436
+ };
3437
+ return metadata[action];
3438
+ }
3330
3439
 
3331
3440
  /**
3332
3441
  * A goal landmark pill. Resolves its label, accent/tone, and glyph from
3333
- * {@link GOAL_META} so all six actions are visually distinguishable while the
3442
+ * {@link goalMeta} so all six actions are visually distinguishable while the
3334
3443
  * palette stays restrained — see that table for the per-action rationale.
3335
3444
  */
3336
3445
  function GoalRow({ item }: { item: GoalItem }) {
3337
3446
  const enter = useEntranceAnimation();
3338
- const { label, pill, icon: Icon } = GOAL_META[item.action];
3447
+ const { label, pill, icon: Icon } = goalMeta(item.action);
3339
3448
  return (
3340
3449
  <div className={cn(enter && "animate-og-enter", "flex justify-center")}>
3341
3450
  <span
@@ -3356,9 +3465,11 @@ function GoalRow({ item }: { item: GoalItem }) {
3356
3465
 
3357
3466
  function MachineInputBatchRow({
3358
3467
  item,
3468
+ onOpenSession,
3359
3469
  loadVideoArtifactPlayback,
3360
3470
  }: {
3361
3471
  item: MachineInputBatchItem;
3472
+ onOpenSession?: ((sessionId: string) => void) | undefined;
3362
3473
  loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
3363
3474
  }) {
3364
3475
  const enter = useEntranceAnimation();
@@ -3378,7 +3489,11 @@ function MachineInputBatchRow({
3378
3489
 
3379
3490
  return (
3380
3491
  <div className={cn(enter && "animate-og-enter", "flex flex-col items-center gap-1.5")}>
3381
- <details className="group w-full max-w-full" data-og-machine-input-batch="">
3492
+ <details
3493
+ className="group w-full max-w-full"
3494
+ data-og-machine-input-batch=""
3495
+ title={`Received ${formatClockTime(item.occurredAt)}`}
3496
+ >
3382
3497
  <summary className="flex cursor-pointer list-none justify-center [&::-webkit-details-marker]:hidden">
3383
3498
  <span
3384
3499
  className={cn(
@@ -3394,10 +3509,14 @@ function MachineInputBatchRow({
3394
3509
  </span>
3395
3510
  </summary>
3396
3511
  <div className="mx-auto mt-2 w-full max-w-lg space-y-2 border-t border-og-border/50 pt-2">
3512
+ <p className="text-og-xs text-og-fg-subtle">
3513
+ Received <time dateTime={item.occurredAt}>{formatClockTime(item.occurredAt)}</time>
3514
+ </p>
3397
3515
  {item.members.map((member) => (
3398
3516
  <MachineInputRow
3399
3517
  key={member.id}
3400
3518
  member={member}
3519
+ onOpenSession={onOpenSession}
3401
3520
  loadVideoArtifactPlayback={loadVideoArtifactPlayback}
3402
3521
  />
3403
3522
  ))}
@@ -3414,9 +3533,11 @@ function MachineInputBatchRow({
3414
3533
 
3415
3534
  function MachineInputRow({
3416
3535
  member,
3536
+ onOpenSession,
3417
3537
  loadVideoArtifactPlayback,
3418
3538
  }: {
3419
3539
  member: MachineInputBatchItem["members"][number];
3540
+ onOpenSession?: ((sessionId: string) => void) | undefined;
3420
3541
  loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
3421
3542
  }) {
3422
3543
  if (member.kind === "media_generation_result" && member.result) {
@@ -3443,6 +3564,11 @@ function MachineInputRow({
3443
3564
  {truncate(summary, 320)}
3444
3565
  </p>
3445
3566
  ) : null}
3567
+ <ChildSessionLink
3568
+ kind={member.kind}
3569
+ sourceId={member.sourceId}
3570
+ onOpenSession={onOpenSession}
3571
+ />
3446
3572
  </div>
3447
3573
  </div>
3448
3574
  );
@@ -3521,7 +3647,7 @@ function NoticeRow({ item }: { item: NoticeItem }) {
3521
3647
  ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
3522
3648
  : item.tone === "waiting"
3523
3649
  ? WAITING_PILL_CLASS
3524
- : "border-og-border bg-og-surface-1 text-og-fg-muted";
3650
+ : NEUTRAL_PILL;
3525
3651
  return (
3526
3652
  <div
3527
3653
  className={cn(
@@ -3529,12 +3655,24 @@ function NoticeRow({ item }: { item: NoticeItem }) {
3529
3655
  "flex items-start gap-2.5 rounded-og-md border px-3.5 py-2.5 text-og-menu",
3530
3656
  tone,
3531
3657
  )}
3532
- role="status"
3658
+ role={item.recordedOutcome ? "note" : "status"}
3659
+ data-og-recorded-outcome={item.recordedOutcome ? "wait" : undefined}
3533
3660
  >
3534
3661
  <TriangleAlertIcon
3535
3662
  className={cn("mt-0.5 size-4 shrink-0", item.tone === "cancelled" && "opacity-60")}
3536
3663
  />
3537
3664
  <div className="min-w-0 flex-1">
3665
+ {item.recordedOutcome ? (
3666
+ <p className="mb-1 text-og-control font-medium">
3667
+ Wait recorded{" "}
3668
+ <time dateTime={item.occurredAt}>
3669
+ {new Date(item.occurredAt).toLocaleString(undefined, {
3670
+ dateStyle: "medium",
3671
+ timeStyle: "short",
3672
+ })}
3673
+ </time>
3674
+ </p>
3675
+ ) : null}
3538
3676
  <span className="whitespace-pre-wrap break-words">{item.text}</span>
3539
3677
  {item.details ? (
3540
3678
  <details className="mt-2 text-og-control">