@opengeni/react 3.7.0-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 (64) 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-DOK4KZF2.js → chunk-5IXTJTK3.js} +1322 -392
  6. package/dist/chunk-5IXTJTK3.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-R55HFTT3.js → chunk-INGB2N5R.js} +4305 -4019
  10. package/dist/chunk-INGB2N5R.js.map +1 -0
  11. package/dist/{chunk-FJBOU2TH.js → chunk-MOWYEBCQ.js} +1728 -12
  12. package/dist/chunk-MOWYEBCQ.js.map +1 -0
  13. package/dist/chunk-PAJU5KKL.js +331 -0
  14. package/dist/chunk-PAJU5KKL.js.map +1 -0
  15. package/dist/{chunk-VTOREEXF.js → chunk-PLYRL5ER.js} +24 -2
  16. package/dist/{chunk-VTOREEXF.js.map → chunk-PLYRL5ER.js.map} +1 -1
  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/session-chrome.d.ts +37 -4
  21. package/dist/components/session-commands-panel.d.ts +6 -0
  22. package/dist/components/session-conversation.d.ts +14 -0
  23. package/dist/components/timeline-anchor.d.ts +21 -0
  24. package/dist/composer.js +2 -3
  25. package/dist/conversation-timeline.d.ts +5 -0
  26. package/dist/index.d.ts +4 -1
  27. package/dist/index.js +716 -914
  28. package/dist/index.js.map +1 -1
  29. package/dist/interaction.js +2 -2
  30. package/dist/lib/format.d.ts +5 -0
  31. package/dist/machines.js +3 -2
  32. package/dist/session-ui.d.ts +3 -0
  33. package/dist/session-ui.js +14 -4
  34. package/dist/session.js +19 -19
  35. package/dist/timeline/types.d.ts +3 -1
  36. package/package.json +2 -2
  37. package/src/components/artifacts/published-html-artifact-frame.tsx +27 -4
  38. package/src/components/machine-input-display.ts +6 -0
  39. package/src/components/message-timeline.tsx +320 -197
  40. package/src/components/session-chrome.tsx +498 -125
  41. package/src/components/session-commands-panel.tsx +113 -0
  42. package/src/components/session-conversation.tsx +141 -0
  43. package/src/components/timeline-anchor.tsx +91 -0
  44. package/src/conversation-timeline.ts +83 -0
  45. package/src/hooks/use-session-background-commands.ts +21 -10
  46. package/src/hooks/use-session-events.ts +36 -14
  47. package/src/hooks/use-session.ts +23 -2
  48. package/src/index.ts +4 -0
  49. package/src/lib/format.ts +32 -0
  50. package/src/session-ui.ts +3 -0
  51. package/src/timeline/projection.ts +6 -12
  52. package/src/timeline/types.ts +4 -0
  53. package/styles/compiled.css +495 -411
  54. package/styles/index.css +23 -0
  55. package/dist/chunk-7EAFGICR.js +0 -302
  56. package/dist/chunk-7EAFGICR.js.map +0 -1
  57. package/dist/chunk-C6WDLHBA.js +0 -2590
  58. package/dist/chunk-C6WDLHBA.js.map +0 -1
  59. package/dist/chunk-DOK4KZF2.js.map +0 -1
  60. package/dist/chunk-FJBOU2TH.js.map +0 -1
  61. package/dist/chunk-R55HFTT3.js.map +0 -1
  62. package/dist/chunk-YVCHAQZY.js.map +0 -1
  63. /package/dist/{browser-viewer-E5DDEFQ4.js.map → browser-viewer-NTD6UIPR.js.map} +0 -0
  64. /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.
@@ -1289,6 +1317,19 @@ export function MessageTimeline({
1289
1317
  // Boundary progress retires the request owner. A late settlement from
1290
1318
  // that completed prefetch cannot mutate the new window's cooldown owner.
1291
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
+ }
1292
1333
  rearmOlderPrefetchAfterLeavingTop(node);
1293
1334
  requestOlderIfUnderfilled(node);
1294
1335
  });
@@ -1459,6 +1500,7 @@ export function MessageTimeline({
1459
1500
  loadingOlder,
1460
1501
  olderBoundaryKey,
1461
1502
  olderPrefetchArmed,
1503
+ olderDemand,
1462
1504
  onLoadOlder,
1463
1505
  requestOlderIfUnderfilled,
1464
1506
  ]);
@@ -1651,7 +1693,7 @@ export function MessageTimeline({
1651
1693
  // Pointer-dragged scroll-up away from tip. Layout churn never arms this.
1652
1694
  if (readerArmed && cumulativeReaderUp > TIP_FOLLOW_READER_UP_EPS_PX && !nearBottomPinned) {
1653
1695
  clearReaderIntent();
1654
- releasePinFromReader(node);
1696
+ requestEarlierFromReader();
1655
1697
  rearmOlderPrefetchAfterLeavingTop(node);
1656
1698
  return;
1657
1699
  }
@@ -1676,6 +1718,10 @@ export function MessageTimeline({
1676
1718
  }
1677
1719
 
1678
1720
  syncScrollBaseline(node);
1721
+ if (programmatic) {
1722
+ // Anchor restoration is never permission to resume tip-follow.
1723
+ return;
1724
+ }
1679
1725
  const nearBottom = isNearBottom(node);
1680
1726
 
1681
1727
  // Re-pin only when the reader moved toward/at the tip without a content
@@ -1685,8 +1731,7 @@ export function MessageTimeline({
1685
1731
  // inside PIN_THRESHOLD once the window is tall) and snapped them back.
1686
1732
  const inserted = Math.max(0, nextMaxScroll - previousMaxScroll);
1687
1733
  const towardTip = nextTop - previousTop - inserted;
1688
- const nextPinned =
1689
- !hasNewer && nearBottom && towardTip >= -TIP_FOLLOW_READER_UP_EPS_PX && inserted <= 1;
1734
+ const nextPinned = !hasNewer && nearBottom && towardTip > 0.5 && inserted <= 1;
1690
1735
  if (!nextPinned) {
1691
1736
  stopFollow();
1692
1737
  }
@@ -1759,148 +1804,202 @@ export function MessageTimeline({
1759
1804
  onScroll={onScroll}
1760
1805
  onScrollEnd={onScrollEnd}
1761
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
+ }}
1762
1847
  onPointerDown={onPointerDown}
1763
1848
  onKeyDown={onKeyDown}
1764
1849
  style={groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined}
1765
1850
  className={cn(
1766
1851
  // tabIndex=-1 is programmatic only — never paint a focus ring on
1767
1852
  // 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",
1853
+ "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pt-16 pb-6 sm:px-6 outline-hidden",
1769
1854
  autoFollow && pinned && !hasNewer
1770
1855
  ? "[overflow-anchor:none]"
1771
1856
  : "[overflow-anchor:auto]",
1772
1857
  )}
1773
1858
  >
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}
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"
1805
1970
  />
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
1971
  ) : 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;
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
1863
1982
  }
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>
1983
+ context={timelineGroupEntryContext}
1984
+ />
1985
+ );
1986
+ })}
1987
+ {groups.length > 0 && trailingState ? (
1988
+ <div data-og-timeline-trailing-state="">{trailingState}</div>
1900
1989
  ) : null}
1901
- </motion.div>
1902
- ) : null}
1903
- </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
+
1904
2003
  <AnimatePresence>
1905
2004
  {loadingNewer ? (
1906
2005
  <motion.div
@@ -2805,7 +2904,7 @@ function CompactionRow({ item }: { item: ContextCompactionItem }) {
2805
2904
  ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
2806
2905
  : item.phase === "started"
2807
2906
  ? WAITING_PILL_CLASS
2808
- : "border-og-border bg-og-surface-1 text-og-fg-muted";
2907
+ : NEUTRAL_PILL;
2809
2908
  return (
2810
2909
  <div className={cn(enter && "animate-og-enter", "flex justify-center")}>
2811
2910
  <div
@@ -3301,41 +3400,46 @@ type GoalMeta = { label: string; pill: string; icon: ComponentType<{ className?:
3301
3400
 
3302
3401
  const NEUTRAL_PILL = "border-og-border bg-og-surface-1 text-og-fg-muted";
3303
3402
 
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
- };
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
+ }
3330
3434
 
3331
3435
  /**
3332
3436
  * 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
3437
+ * {@link goalMeta} so all six actions are visually distinguishable while the
3334
3438
  * palette stays restrained — see that table for the per-action rationale.
3335
3439
  */
3336
3440
  function GoalRow({ item }: { item: GoalItem }) {
3337
3441
  const enter = useEntranceAnimation();
3338
- const { label, pill, icon: Icon } = GOAL_META[item.action];
3442
+ const { label, pill, icon: Icon } = goalMeta(item.action);
3339
3443
  return (
3340
3444
  <div className={cn(enter && "animate-og-enter", "flex justify-center")}>
3341
3445
  <span
@@ -3378,7 +3482,11 @@ function MachineInputBatchRow({
3378
3482
 
3379
3483
  return (
3380
3484
  <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="">
3485
+ <details
3486
+ className="group w-full max-w-full"
3487
+ data-og-machine-input-batch=""
3488
+ title={`Received ${formatClockTime(item.occurredAt)}`}
3489
+ >
3382
3490
  <summary className="flex cursor-pointer list-none justify-center [&::-webkit-details-marker]:hidden">
3383
3491
  <span
3384
3492
  className={cn(
@@ -3394,6 +3502,9 @@ function MachineInputBatchRow({
3394
3502
  </span>
3395
3503
  </summary>
3396
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>
3397
3508
  {item.members.map((member) => (
3398
3509
  <MachineInputRow
3399
3510
  key={member.id}
@@ -3521,7 +3632,7 @@ function NoticeRow({ item }: { item: NoticeItem }) {
3521
3632
  ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
3522
3633
  : item.tone === "waiting"
3523
3634
  ? WAITING_PILL_CLASS
3524
- : "border-og-border bg-og-surface-1 text-og-fg-muted";
3635
+ : NEUTRAL_PILL;
3525
3636
  return (
3526
3637
  <div
3527
3638
  className={cn(
@@ -3529,12 +3640,24 @@ function NoticeRow({ item }: { item: NoticeItem }) {
3529
3640
  "flex items-start gap-2.5 rounded-og-md border px-3.5 py-2.5 text-og-menu",
3530
3641
  tone,
3531
3642
  )}
3532
- role="status"
3643
+ role={item.recordedOutcome ? "note" : "status"}
3644
+ data-og-recorded-outcome={item.recordedOutcome ? "wait" : undefined}
3533
3645
  >
3534
3646
  <TriangleAlertIcon
3535
3647
  className={cn("mt-0.5 size-4 shrink-0", item.tone === "cancelled" && "opacity-60")}
3536
3648
  />
3537
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}
3538
3661
  <span className="whitespace-pre-wrap break-words">{item.text}</span>
3539
3662
  {item.details ? (
3540
3663
  <details className="mt-2 text-og-control">