@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.
- package/dist/artifacts.d.ts +1 -1
- package/dist/artifacts.js +248 -6
- package/dist/artifacts.js.map +1 -1
- package/dist/{browser-viewer-E5DDEFQ4.js → browser-viewer-NTD6UIPR.js} +3 -3
- package/dist/{chunk-W2RGYBCV.js → chunk-24M4YBCL.js} +4 -4
- package/dist/{chunk-WY3MELVJ.js → chunk-5IXTJTK3.js} +1438 -397
- package/dist/chunk-5IXTJTK3.js.map +1 -0
- package/dist/chunk-5PQJOCX5.js +902 -0
- package/dist/chunk-5PQJOCX5.js.map +1 -0
- package/dist/{chunk-UNCXXIQR.js → chunk-INGB2N5R.js} +4310 -3982
- package/dist/chunk-INGB2N5R.js.map +1 -0
- package/dist/{chunk-TJFMMDQU.js → chunk-MOWYEBCQ.js} +1839 -17
- package/dist/chunk-MOWYEBCQ.js.map +1 -0
- package/dist/chunk-PAJU5KKL.js +331 -0
- package/dist/chunk-PAJU5KKL.js.map +1 -0
- package/dist/{chunk-VTOREEXF.js → chunk-PLYRL5ER.js} +24 -2
- package/dist/{chunk-VTOREEXF.js.map → chunk-PLYRL5ER.js.map} +1 -1
- package/dist/{chunk-YVCHAQZY.js → chunk-ZFOFY5ST.js} +27 -349
- package/dist/chunk-ZFOFY5ST.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/components/artifacts/index.d.ts +1 -1
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +46 -3
- package/dist/components/composer.d.ts +2 -0
- package/dist/components/session-chrome.d.ts +38 -5
- package/dist/components/session-commands-panel.d.ts +6 -0
- package/dist/components/session-conversation.d.ts +14 -0
- package/dist/components/timeline-anchor.d.ts +21 -0
- package/dist/composer.js +2 -3
- package/dist/conversation-timeline.d.ts +5 -0
- package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-YZ3ZKWMM.js} +2 -1
- package/dist/fleet-decision-row-YZ3ZKWMM.js.map +1 -0
- package/dist/hooks/use-file-attachments.d.ts +6 -0
- package/dist/hooks/use-sandbox-files.d.ts +4 -1
- package/dist/hooks/use-workspace-edit.d.ts +4 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +823 -965
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +2 -2
- package/dist/lib/format.d.ts +5 -0
- package/dist/machines.js +3 -2
- package/dist/session-ui.d.ts +3 -0
- package/dist/session-ui.js +14 -4
- package/dist/session.js +19 -19
- package/dist/timeline/registry.d.ts +2 -0
- package/dist/timeline/types.d.ts +4 -2
- package/package.json +2 -2
- package/src/artifacts.ts +3 -0
- package/src/client.ts +2 -1
- package/src/components/artifacts/index.ts +3 -0
- package/src/components/artifacts/published-html-artifact-frame.tsx +307 -4
- package/src/components/composer.tsx +89 -25
- package/src/components/machine-input-display.ts +6 -0
- package/src/components/message-timeline.tsx +351 -204
- package/src/components/sandbox-workspace.tsx +11 -1
- package/src/components/session-chrome.tsx +501 -128
- package/src/components/session-commands-panel.tsx +113 -0
- package/src/components/session-conversation.tsx +141 -0
- package/src/components/timeline-anchor.tsx +91 -0
- package/src/conversation-timeline.ts +83 -0
- package/src/hooks/use-file-attachments.ts +34 -0
- package/src/hooks/use-sandbox-files.ts +68 -20
- package/src/hooks/use-session-background-commands.ts +21 -10
- package/src/hooks/use-session-events.ts +56 -39
- package/src/hooks/use-session.ts +23 -2
- package/src/hooks/use-workspace-edit.ts +23 -5
- package/src/index.ts +4 -0
- package/src/lib/format.ts +32 -0
- package/src/session-ui.ts +3 -0
- package/src/timeline/fleet-decision-projection.ts +2 -1
- package/src/timeline/fleet-decision-row.tsx +1 -0
- package/src/timeline/projection.ts +157 -19
- package/src/timeline/registry.ts +15 -3
- package/src/timeline/tool-renderers.tsx +117 -1
- package/src/timeline/types.ts +12 -1
- package/styles/compiled.css +495 -411
- package/styles/index.css +23 -0
- package/dist/chunk-7EAFGICR.js +0 -302
- package/dist/chunk-7EAFGICR.js.map +0 -1
- package/dist/chunk-IB27C53Y.js +0 -2582
- package/dist/chunk-IB27C53Y.js.map +0 -1
- package/dist/chunk-TJFMMDQU.js.map +0 -1
- package/dist/chunk-UNCXXIQR.js.map +0 -1
- package/dist/chunk-WY3MELVJ.js.map +0 -1
- package/dist/chunk-YVCHAQZY.js.map +0 -1
- package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
- /package/dist/{browser-viewer-E5DDEFQ4.js.map → browser-viewer-NTD6UIPR.js.map} +0 -0
- /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
|
-
|
|
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
|
-
(
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
783
|
-
|
|
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
|
|
1107
|
-
|
|
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
|
-
|
|
1112
|
-
!
|
|
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 ||
|
|
1225
|
+
(wasAtLiveTailBeforeCommit || underfillOwned)
|
|
1192
1226
|
) {
|
|
1193
|
-
//
|
|
1194
|
-
//
|
|
1195
|
-
//
|
|
1196
|
-
//
|
|
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
|
-
|
|
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
|
|
1663
|
-
//
|
|
1664
|
-
|
|
1665
|
-
|
|
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
|
|
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
|
-
<
|
|
1751
|
-
{
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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
|
-
{
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
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
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
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
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
label: "Goal
|
|
3301
|
-
pill:
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
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
|
|
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 } =
|
|
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
|
|
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
|
-
:
|
|
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">
|