@opengeni/react 1.0.2-canary.37191796234001 → 1.0.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.
- package/README.md +15 -6
- package/dist/artifacts.js +1 -1
- package/dist/{browser-viewer-GTR5XQ7X.js → browser-viewer-SHYKQRNX.js} +4 -4
- package/dist/{chunk-CR7U7HAR.js → chunk-26ZNDDLZ.js} +3 -3
- package/dist/{chunk-ELCE5SXV.js → chunk-3CTRFMAK.js} +2 -2
- package/dist/{chunk-XOAMRDDK.js → chunk-742CZ3L3.js} +2 -2
- package/dist/{chunk-H2NJQORO.js → chunk-DO544OSV.js} +7 -1
- package/dist/chunk-DO544OSV.js.map +1 -0
- package/dist/{chunk-IQUCRAT7.js → chunk-EESD3PY3.js} +2 -2
- package/dist/{chunk-23J2ZNXT.js → chunk-EN3YTHKR.js} +3 -3
- package/dist/{chunk-JFNEV4LU.js → chunk-GYVRFZDY.js} +3 -3
- package/dist/{chunk-YWFG7ZX7.js → chunk-HDJWDK7W.js} +3 -3
- package/dist/{chunk-NZUI76EM.js → chunk-J6DBS4T3.js} +2 -2
- package/dist/{chunk-SJZDKVW2.js → chunk-JIQYUGVL.js} +4 -4
- package/dist/{chunk-XUW2UWQM.js → chunk-RLUSC2WA.js} +3 -3
- package/dist/{chunk-XUW2UWQM.js.map → chunk-RLUSC2WA.js.map} +1 -1
- package/dist/{chunk-Q5QBEZIQ.js → chunk-RSI7XYXL.js} +2 -2
- package/dist/{chunk-NEXLLTP4.js → chunk-TKOTKRWY.js} +4 -4
- package/dist/{chunk-NEXLLTP4.js.map → chunk-TKOTKRWY.js.map} +1 -1
- package/dist/{chunk-TMJRG4YC.js → chunk-VV5E43LD.js} +432 -180
- package/dist/chunk-VV5E43LD.js.map +1 -0
- package/dist/{chunk-FFA2OVOV.js → chunk-WGXVEJO5.js} +3 -3
- package/dist/{chunk-E3G7SAYZ.js → chunk-YM2DE7BQ.js} +2 -2
- package/dist/components/open-geni-chat.d.ts +6 -3
- package/dist/components/session-conversation.d.ts +6 -3
- package/dist/components/session-proxy-scope.d.ts +21 -0
- package/dist/composer.js +6 -6
- package/dist/{computer-viewer-ENHVAZ2Q.js → computer-viewer-6SRGWRUX.js} +4 -4
- package/dist/index.js +17 -17
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +4 -4
- package/dist/{latest-question-755OW5DS.js → latest-question-HTRIS4KJ.js} +2 -2
- package/dist/lib/host-theme.d.ts +7 -0
- package/dist/machines.js +3 -3
- package/dist/{model-policy-picker-menu-PWRI7R2X.js → model-policy-picker-menu-M54ISBMN.js} +4 -4
- package/dist/model-policy.js +1 -1
- package/dist/realtime.js +3 -3
- package/dist/{sandbox-files-BBC47SLD.js → sandbox-files-GORZOUUJ.js} +4 -4
- package/dist/session-ui.js +9 -9
- package/dist/session.js +5 -5
- package/dist/timeline/turn-summary.d.ts +3 -1
- package/dist/timeline/types.d.ts +6 -0
- package/dist/usage.js +1 -1
- package/package.json +3 -3
- package/src/components/message-timeline.tsx +704 -450
- package/src/components/open-geni-chat.tsx +56 -37
- package/src/components/session-conversation.tsx +87 -73
- package/src/components/session-proxy-scope.tsx +92 -0
- package/src/hooks/use-session-background-commands.ts +2 -2
- package/src/hooks/use-video-artifact-playback.ts +1 -1
- package/src/lib/host-theme.ts +15 -2
- package/src/model-policy.ts +2 -2
- package/src/session-context.ts +3 -3
- package/src/timeline/projection.ts +9 -0
- package/src/timeline/turn-summary.tsx +10 -0
- package/src/timeline/types.ts +6 -0
- package/dist/chunk-H2NJQORO.js.map +0 -1
- package/dist/chunk-TMJRG4YC.js.map +0 -1
- /package/dist/{browser-viewer-GTR5XQ7X.js.map → browser-viewer-SHYKQRNX.js.map} +0 -0
- /package/dist/{chunk-CR7U7HAR.js.map → chunk-26ZNDDLZ.js.map} +0 -0
- /package/dist/{chunk-ELCE5SXV.js.map → chunk-3CTRFMAK.js.map} +0 -0
- /package/dist/{chunk-XOAMRDDK.js.map → chunk-742CZ3L3.js.map} +0 -0
- /package/dist/{chunk-IQUCRAT7.js.map → chunk-EESD3PY3.js.map} +0 -0
- /package/dist/{chunk-23J2ZNXT.js.map → chunk-EN3YTHKR.js.map} +0 -0
- /package/dist/{chunk-JFNEV4LU.js.map → chunk-GYVRFZDY.js.map} +0 -0
- /package/dist/{chunk-YWFG7ZX7.js.map → chunk-HDJWDK7W.js.map} +0 -0
- /package/dist/{chunk-NZUI76EM.js.map → chunk-J6DBS4T3.js.map} +0 -0
- /package/dist/{chunk-SJZDKVW2.js.map → chunk-JIQYUGVL.js.map} +0 -0
- /package/dist/{chunk-Q5QBEZIQ.js.map → chunk-RSI7XYXL.js.map} +0 -0
- /package/dist/{chunk-FFA2OVOV.js.map → chunk-WGXVEJO5.js.map} +0 -0
- /package/dist/{chunk-E3G7SAYZ.js.map → chunk-YM2DE7BQ.js.map} +0 -0
- /package/dist/{computer-viewer-ENHVAZ2Q.js.map → computer-viewer-6SRGWRUX.js.map} +0 -0
- /package/dist/{latest-question-755OW5DS.js.map → latest-question-HTRIS4KJ.js.map} +0 -0
- /package/dist/{model-policy-picker-menu-PWRI7R2X.js.map → model-policy-picker-menu-M54ISBMN.js.map} +0 -0
- /package/dist/{sandbox-files-BBC47SLD.js.map → sandbox-files-GORZOUUJ.js.map} +0 -0
|
@@ -46,9 +46,11 @@ import { Collapsible } from "radix-ui";
|
|
|
46
46
|
import {
|
|
47
47
|
Component,
|
|
48
48
|
Suspense,
|
|
49
|
+
createContext,
|
|
49
50
|
lazy,
|
|
50
51
|
memo,
|
|
51
52
|
useCallback,
|
|
53
|
+
useContext,
|
|
52
54
|
useEffect,
|
|
53
55
|
useLayoutEffect,
|
|
54
56
|
useMemo,
|
|
@@ -371,6 +373,14 @@ const WAITING_PILL_CLASS =
|
|
|
371
373
|
"border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting";
|
|
372
374
|
const LOADING_CHIP_CLASS =
|
|
373
375
|
"pointer-events-none inline-flex items-center rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1 text-og-control font-medium shadow-og-md backdrop-blur";
|
|
376
|
+
/** How long live progress notes take to fold into (or out of) their work row. */
|
|
377
|
+
const LIVE_NOTE_FOLD_MS = 220;
|
|
378
|
+
/** The timeline list's `gap-5`; a folded note gives it back with `-mt-5`. */
|
|
379
|
+
const LIVE_NOTE_GAP_PX = 20;
|
|
380
|
+
/** Longest a bottom-space loan waits for an opening work list to grow into it. */
|
|
381
|
+
const LIVE_NOTE_RESERVE_MS = 700;
|
|
382
|
+
/** Lets a live work disclosure report whether it is open (see `reportLiveFold`). */
|
|
383
|
+
const LiveNoteFoldContext = createContext<((workId: string, open: boolean) => void) | null>(null);
|
|
374
384
|
|
|
375
385
|
// State: 0 underfill retry, 1 prefetch pending, 2 compatibility-settled,
|
|
376
386
|
// 3 explicit no-progress waiting to become an underfill retry.
|
|
@@ -853,6 +863,191 @@ export function MessageTimeline({
|
|
|
853
863
|
// deliberate chip remounts (activity→turn wrap, nested key flips) so a fold
|
|
854
864
|
// that already settled closed — or that the reader closed — never reopens.
|
|
855
865
|
const foldMemoryRef = useRef<Map<string, FoldRestingState>>(new Map());
|
|
866
|
+
// Live work disclosures that are open. While open, that turn's progress
|
|
867
|
+
// notes read inside the disclosure, so their copies above it fold away.
|
|
868
|
+
const [openLiveFolds, setOpenLiveFolds] = useState<ReadonlySet<string>>(() => new Set());
|
|
869
|
+
const openLiveFoldsRef = useRef<ReadonlySet<string>>(openLiveFolds);
|
|
870
|
+
const liveNoteIdsByWorkRef = useRef<ReadonlyMap<string, readonly string[]>>(new Map());
|
|
871
|
+
const liveFoldMotionRef = useRef<{ frame: number | null; finish: () => void } | null>(null);
|
|
872
|
+
// The bottom-space loan currently on the scroller, by owner.
|
|
873
|
+
const liveFoldReserveRef = useRef<object | null>(null);
|
|
874
|
+
// writeScrollTop is declared further down; the fold reaches it through this.
|
|
875
|
+
const writeScrollTopRef = useRef<(node: HTMLElement, top: number) => void>(() => {});
|
|
876
|
+
const reportLiveFold = useCallback((workId: string, open: boolean) => {
|
|
877
|
+
const current = openLiveFoldsRef.current;
|
|
878
|
+
if (current.has(workId) === open) return;
|
|
879
|
+
const next = new Set(current);
|
|
880
|
+
if (open) next.add(workId);
|
|
881
|
+
else next.delete(workId);
|
|
882
|
+
openLiveFoldsRef.current = next;
|
|
883
|
+
liveFoldMotionRef.current?.finish();
|
|
884
|
+
const scroller = scrollRef.current;
|
|
885
|
+
const rows = (liveNoteIdsByWorkRef.current.get(workId) ?? []).flatMap((id) => {
|
|
886
|
+
const row = scroller?.querySelector<HTMLElement>(
|
|
887
|
+
`[data-og-live-note-id="${cssEscapeAttribute(id)}"]`,
|
|
888
|
+
);
|
|
889
|
+
return row ? [row] : [];
|
|
890
|
+
});
|
|
891
|
+
if (!scroller || rows.length === 0) {
|
|
892
|
+
setOpenLiveFolds(next);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const header = scroller.querySelector<HTMLElement>(
|
|
896
|
+
`[data-og-work-id="${cssEscapeAttribute(workId)}"] [data-og-work-header="outer"]`,
|
|
897
|
+
);
|
|
898
|
+
const viewTop = scroller.getBoundingClientRect().top;
|
|
899
|
+
const headerTop = header?.getBoundingClientRect().top;
|
|
900
|
+
if (
|
|
901
|
+
!open &&
|
|
902
|
+
header &&
|
|
903
|
+
headerTop !== undefined &&
|
|
904
|
+
(headerTop < viewTop || headerTop > viewTop + scroller.clientHeight)
|
|
905
|
+
) {
|
|
906
|
+
// Closed from the row's sticky header after reading down the list: the
|
|
907
|
+
// row is no longer where the reader is. Like any big collapse, land on
|
|
908
|
+
// it in one step — the notes return above and the closed row rests at
|
|
909
|
+
// the bottom edge — rather than animating content the reader can't see.
|
|
910
|
+
setOpenLiveFolds(next);
|
|
911
|
+
requestFrame(() =>
|
|
912
|
+
requestFrame(() => {
|
|
913
|
+
const row = header.closest<HTMLElement>("[data-og-timeline-group-anchor]");
|
|
914
|
+
if (!row?.isConnected) return;
|
|
915
|
+
const box = row.getBoundingClientRect();
|
|
916
|
+
const top = scroller.getBoundingClientRect().top;
|
|
917
|
+
const bottom = top + scroller.clientHeight;
|
|
918
|
+
// + the scroller's pb-6, as at the true end.
|
|
919
|
+
if (box.bottom > bottom || box.top < top)
|
|
920
|
+
writeScrollTopRef.current(scroller, scroller.scrollTop + box.bottom - bottom + 24);
|
|
921
|
+
scheduleQuestionNavRef.current();
|
|
922
|
+
}),
|
|
923
|
+
);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
// Opening a live row near the bottom drops its one-line preview while its
|
|
927
|
+
// list grows from nothing; closing shrinks the list before the notes have
|
|
928
|
+
// grown back. For that moment the page is shorter than where the reader
|
|
929
|
+
// is, so the browser would clamp the scroll and everything would drop.
|
|
930
|
+
// This runs before that frame paints: keep the reader's position and lend
|
|
931
|
+
// one screen of space at the bottom (the shortfall keeps changing through
|
|
932
|
+
// the commit) until the content has grown back into it.
|
|
933
|
+
const readerTop = lastScrollTopRef.current;
|
|
934
|
+
const reserve = readerTop > 0.5 ? scroller.clientHeight : 0;
|
|
935
|
+
const loan = {};
|
|
936
|
+
if (reserve > 0) {
|
|
937
|
+
liveFoldReserveRef.current = loan;
|
|
938
|
+
scroller.style.paddingBottom = `calc(1.5rem + ${reserve}px)`;
|
|
939
|
+
if (Math.abs(scroller.scrollTop - readerTop) > 0.5)
|
|
940
|
+
writeScrollTopRef.current(scroller, readerTop);
|
|
941
|
+
}
|
|
942
|
+
const releaseReserve = (deadline: number) => {
|
|
943
|
+
// Only this motion's loan; a newer toggle may have replaced it.
|
|
944
|
+
if (reserve === 0 || liveFoldReserveRef.current !== loan) return;
|
|
945
|
+
const needed = scroller.scrollTop + scroller.clientHeight - (scroller.scrollHeight - reserve);
|
|
946
|
+
if (needed > 0.5 && performance.now() < deadline && scroller.isConnected) {
|
|
947
|
+
requestFrame(() => releaseReserve(deadline));
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
liveFoldReserveRef.current = null;
|
|
951
|
+
scroller.style.removeProperty("padding-bottom");
|
|
952
|
+
};
|
|
953
|
+
// The notes fold into (or out of) the work row in one short motion. The
|
|
954
|
+
// row glides to where it should rest: opening, into the first note's place
|
|
955
|
+
// (or the top edge, when that note was above view) so its list starts just
|
|
956
|
+
// beneath; closing, down below its returning notes but never past the
|
|
957
|
+
// bottom edge. When everything is in view that is exactly what happens on
|
|
958
|
+
// its own; otherwise the scroll is written in the same frame as the
|
|
959
|
+
// heights, so it cannot lag. Native scroll anchoring is suspended for the
|
|
960
|
+
// motion; inline styles pin the current look across the state commit, end
|
|
961
|
+
// exactly at the classes' resting geometry, and are then removed.
|
|
962
|
+
const workRow = header?.closest<HTMLElement>("[data-og-timeline-group-anchor]") ?? null;
|
|
963
|
+
const rowStart = workRow?.getBoundingClientRect().top;
|
|
964
|
+
const viewBottom = viewTop + scroller.clientHeight;
|
|
965
|
+
let landing =
|
|
966
|
+
open && rowStart !== undefined
|
|
967
|
+
? Math.max(rows[0]!.getBoundingClientRect().top, viewTop)
|
|
968
|
+
: null;
|
|
969
|
+
const heights = rows.map((row) => (open ? row.offsetHeight : 0));
|
|
970
|
+
const apply = (progress: number) => {
|
|
971
|
+
const shown = open ? 1 - progress : progress;
|
|
972
|
+
rows.forEach((note, index) => {
|
|
973
|
+
// The list gap travels inside the row as padding (margin cancels it),
|
|
974
|
+
// so a shrinking row never overlaps the one above it mid-motion.
|
|
975
|
+
note.style.marginTop = `${-LIVE_NOTE_GAP_PX}px`;
|
|
976
|
+
note.style.paddingTop = `${LIVE_NOTE_GAP_PX * shown}px`;
|
|
977
|
+
note.style.height = `${(heights[index]! + LIVE_NOTE_GAP_PX) * shown}px`;
|
|
978
|
+
note.style.opacity = String(shown);
|
|
979
|
+
});
|
|
980
|
+
if (workRow && rowStart !== undefined && landing !== null) {
|
|
981
|
+
const drift =
|
|
982
|
+
workRow.getBoundingClientRect().top - (rowStart + (landing - rowStart) * progress);
|
|
983
|
+
if (Math.abs(drift) > 0.5) writeScrollTopRef.current(scroller, scroller.scrollTop + drift);
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
for (const row of rows) {
|
|
987
|
+
row.style.boxSizing = "border-box";
|
|
988
|
+
row.style.overflow = "hidden";
|
|
989
|
+
row.style.overflowAnchor = "none";
|
|
990
|
+
}
|
|
991
|
+
scroller.style.overflowAnchor = "none";
|
|
992
|
+
apply(0);
|
|
993
|
+
const fold: { frame: number | null; finish: () => void } = {
|
|
994
|
+
frame: null,
|
|
995
|
+
finish: () => {
|
|
996
|
+
if (fold.frame != null) cancelFrame(fold.frame);
|
|
997
|
+
for (const row of rows) {
|
|
998
|
+
for (const property of [
|
|
999
|
+
"height",
|
|
1000
|
+
"margin-top",
|
|
1001
|
+
"padding-top",
|
|
1002
|
+
"opacity",
|
|
1003
|
+
"overflow",
|
|
1004
|
+
"overflow-anchor",
|
|
1005
|
+
"box-sizing",
|
|
1006
|
+
])
|
|
1007
|
+
row.style.removeProperty(property);
|
|
1008
|
+
}
|
|
1009
|
+
scroller.style.removeProperty("overflow-anchor");
|
|
1010
|
+
releaseReserve(performance.now() + LIVE_NOTE_RESERVE_MS);
|
|
1011
|
+
// The row may now be pinned to the top without any scroll: re-place
|
|
1012
|
+
// the up arrow (it sits below a pinned work header).
|
|
1013
|
+
scheduleQuestionNavRef.current();
|
|
1014
|
+
if (liveFoldMotionRef.current === fold) liveFoldMotionRef.current = null;
|
|
1015
|
+
},
|
|
1016
|
+
};
|
|
1017
|
+
liveFoldMotionRef.current = fold;
|
|
1018
|
+
const duration = prefersReducedMotion() ? 0 : LIVE_NOTE_FOLD_MS;
|
|
1019
|
+
let began: number | null = null;
|
|
1020
|
+
const step = (now: number) => {
|
|
1021
|
+
fold.frame = null;
|
|
1022
|
+
if (!rows.every((row) => row.isConnected)) return fold.finish();
|
|
1023
|
+
if (!open && began === null) {
|
|
1024
|
+
// Unfolding: the natural heights are known only once the folded
|
|
1025
|
+
// class is gone; the inline styles still hold them at zero.
|
|
1026
|
+
rows.forEach((note, index) => (heights[index] = note.scrollHeight));
|
|
1027
|
+
if (workRow && rowStart !== undefined) {
|
|
1028
|
+
const returning = heights.reduce((sum, height) => sum + height + LIVE_NOTE_GAP_PX, 0);
|
|
1029
|
+
// The closed row is its header plus one preview line (its list is
|
|
1030
|
+
// still collapsing now, so the row's own height is not final yet).
|
|
1031
|
+
const closedRow = 2 * (header?.getBoundingClientRect().height ?? 0);
|
|
1032
|
+
const lowest = viewBottom - closedRow - 24;
|
|
1033
|
+
landing = Math.min(rowStart + returning, Math.max(lowest, rowStart));
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
began ??= now;
|
|
1037
|
+
const linear = duration === 0 ? 1 : Math.min(1, (now - began) / duration);
|
|
1038
|
+
apply(1 - (1 - linear) ** 3);
|
|
1039
|
+
if (linear < 1) fold.frame = requestFrame(step);
|
|
1040
|
+
else fold.finish();
|
|
1041
|
+
};
|
|
1042
|
+
fold.frame = requestFrame(step);
|
|
1043
|
+
setOpenLiveFolds(next);
|
|
1044
|
+
}, []);
|
|
1045
|
+
useEffect(
|
|
1046
|
+
() => () => {
|
|
1047
|
+
liveFoldMotionRef.current?.finish();
|
|
1048
|
+
},
|
|
1049
|
+
[],
|
|
1050
|
+
);
|
|
856
1051
|
const userMessageDisclosureMemoryRef = useRef<Map<string, boolean>>(new Map());
|
|
857
1052
|
const seenActivityIdsRef = useRef<Set<string>>(new Set());
|
|
858
1053
|
const firstItemGroupKeyRef = useRef<string | null>(null);
|
|
@@ -877,6 +1072,20 @@ export function MessageTimeline({
|
|
|
877
1072
|
previousBulkFirstKeyRef.current !== firstGroupKey;
|
|
878
1073
|
const bulkRender = allGroups.length > 0 && (bulkActive || firstKeyChangedForBulk);
|
|
879
1074
|
const groups = useStableTimelineGroupKeys(allGroups, !bulkRender);
|
|
1075
|
+
// Each live progress note → whether its turn's work disclosure is open.
|
|
1076
|
+
const [liveNoteFolds, liveNoteIdsByWork] = useMemo(() => {
|
|
1077
|
+
const folds = new Map<string, boolean>();
|
|
1078
|
+
const byWork = new Map<string, readonly string[]>();
|
|
1079
|
+
for (const { group } of groups) {
|
|
1080
|
+
if (group.kind !== "activity" || !group.work?.liveNoteIds) continue;
|
|
1081
|
+
byWork.set(group.id, group.work.liveNoteIds);
|
|
1082
|
+
for (const id of group.work.liveNoteIds) folds.set(id, openLiveFolds.has(group.id));
|
|
1083
|
+
}
|
|
1084
|
+
return [folds, byWork] as const;
|
|
1085
|
+
}, [groups, openLiveFolds]);
|
|
1086
|
+
useLayoutEffect(() => {
|
|
1087
|
+
liveNoteIdsByWorkRef.current = liveNoteIdsByWork;
|
|
1088
|
+
}, [liveNoteIdsByWork]);
|
|
880
1089
|
const turnsWithOutput = useMemo(
|
|
881
1090
|
() =>
|
|
882
1091
|
new Set(
|
|
@@ -957,6 +1166,7 @@ export function MessageTimeline({
|
|
|
957
1166
|
programmaticScrollRef.current -= 1;
|
|
958
1167
|
}
|
|
959
1168
|
}, []);
|
|
1169
|
+
writeScrollTopRef.current = writeScrollTop;
|
|
960
1170
|
|
|
961
1171
|
const cancelLeaveFallback = useCallback(() => {
|
|
962
1172
|
if (leaveFallbackRafRef.current != null) {
|
|
@@ -2294,439 +2504,452 @@ export function MessageTimeline({
|
|
|
2294
2504
|
>
|
|
2295
2505
|
<LightboxProvider>
|
|
2296
2506
|
<FoldMemoryProvider value={foldMemoryRef.current}>
|
|
2297
|
-
<
|
|
2298
|
-
<
|
|
2299
|
-
<
|
|
2300
|
-
<
|
|
2301
|
-
<
|
|
2302
|
-
<
|
|
2303
|
-
className={cn("og-root relative flex min-h-0 flex-col", className)}
|
|
2304
|
-
// Sticky insets start at the scroller's padded content edge
|
|
2305
|
-
// (pt-16). Subtract exactly that padding so an expanded work
|
|
2306
|
-
// header pins flush to the scrollport. Pinning it lower left a
|
|
2307
|
-
// band of scrolling rows visible above the header, which then
|
|
2308
|
-
// looked like it floated over the middle of the timeline.
|
|
2309
|
-
style={{ "--og-work-header-top": "-4rem" } as CSSProperties}
|
|
2310
|
-
>
|
|
2311
|
-
{onAnnotate ? (
|
|
2312
|
-
<Suspense fallback={null}>
|
|
2313
|
-
<TimelineAnnotationSelection
|
|
2314
|
-
rootRef={scrollRef}
|
|
2315
|
-
sources={annotationSources}
|
|
2316
|
-
onAnnotate={onAnnotate}
|
|
2317
|
-
/>
|
|
2318
|
-
</Suspense>
|
|
2319
|
-
) : null}
|
|
2320
|
-
{/* Pinned: anchoring off so the tip-follow camera owns the motion.
|
|
2321
|
-
Unpinned: native scroll anchoring holds the reader's place. */}
|
|
2507
|
+
<LiveNoteFoldContext.Provider value={reportLiveFold}>
|
|
2508
|
+
<SeenActivityIdsProvider value={seenActivityIdsRef.current}>
|
|
2509
|
+
<TimelineComputeLabelProvider value={computeLabel ?? null}>
|
|
2510
|
+
<EntranceAnimationProvider value={false}>
|
|
2511
|
+
<TooltipProvider delayDuration={400}>
|
|
2512
|
+
<TimelineAnnotationSourceRootContext.Provider value={scrollRef}>
|
|
2322
2513
|
<div
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2514
|
+
className={cn("og-root relative flex min-h-0 flex-col", className)}
|
|
2515
|
+
// Sticky insets start at the scroller's padded content edge
|
|
2516
|
+
// (pt-16). Subtract exactly that padding so an expanded work
|
|
2517
|
+
// header pins flush to the scrollport. Pinning it lower left a
|
|
2518
|
+
// band of scrolling rows visible above the header, which then
|
|
2519
|
+
// looked like it floated over the middle of the timeline.
|
|
2520
|
+
style={{ "--og-work-header-top": "-4rem" } as CSSProperties}
|
|
2521
|
+
>
|
|
2522
|
+
{onAnnotate ? (
|
|
2523
|
+
<Suspense fallback={null}>
|
|
2524
|
+
<TimelineAnnotationSelection
|
|
2525
|
+
rootRef={scrollRef}
|
|
2526
|
+
sources={annotationSources}
|
|
2527
|
+
onAnnotate={onAnnotate}
|
|
2528
|
+
/>
|
|
2529
|
+
</Suspense>
|
|
2530
|
+
) : null}
|
|
2531
|
+
{/* Pinned: anchoring off so the tip-follow camera owns the motion.
|
|
2532
|
+
Unpinned: native scroll anchoring holds the reader's place. */}
|
|
2533
|
+
<div
|
|
2534
|
+
ref={scrollRef}
|
|
2535
|
+
data-og-timeline-scroller=""
|
|
2536
|
+
data-og-bottom-follow={
|
|
2537
|
+
autoFollow && pinned && !hasNewer ? "true" : "false"
|
|
2343
2538
|
}
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
});
|
|
2354
|
-
}}
|
|
2355
|
-
onTouchEnd={() => {
|
|
2356
|
-
touchPositionRef.current = null;
|
|
2357
|
-
}}
|
|
2358
|
-
onTouchCancel={() => {
|
|
2359
|
-
touchPositionRef.current = null;
|
|
2360
|
-
}}
|
|
2361
|
-
onClickCapture={(event) => {
|
|
2362
|
-
const target =
|
|
2363
|
-
event.target instanceof Element
|
|
2364
|
-
? event.target.closest("button[aria-expanded]")
|
|
2539
|
+
tabIndex={-1}
|
|
2540
|
+
onScroll={onScroll}
|
|
2541
|
+
onScrollEnd={onScrollEnd}
|
|
2542
|
+
onWheel={onWheel}
|
|
2543
|
+
onTouchStart={(event) => {
|
|
2544
|
+
stopSettlement();
|
|
2545
|
+
const touch = event.touches.length === 1 ? event.touches[0] : undefined;
|
|
2546
|
+
touchPositionRef.current = touch
|
|
2547
|
+
? { x: touch.clientX, y: touch.clientY }
|
|
2365
2548
|
: null;
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2549
|
+
}}
|
|
2550
|
+
onTouchMove={(event) => {
|
|
2551
|
+
const touch = event.touches[0];
|
|
2552
|
+
const previous = touchPositionRef.current;
|
|
2553
|
+
if (!touch || !previous || event.touches.length !== 1) {
|
|
2554
|
+
touchPositionRef.current = null;
|
|
2555
|
+
return;
|
|
2373
2556
|
}
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2557
|
+
const deltaX = previous.x - touch.clientX;
|
|
2558
|
+
const deltaY = previous.y - touch.clientY;
|
|
2559
|
+
if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 4) return;
|
|
2560
|
+
touchPositionRef.current = { x: touch.clientX, y: touch.clientY };
|
|
2561
|
+
onWheel({
|
|
2562
|
+
deltaX,
|
|
2563
|
+
deltaY,
|
|
2564
|
+
target: event.target,
|
|
2565
|
+
currentTarget: event.currentTarget,
|
|
2566
|
+
});
|
|
2567
|
+
}}
|
|
2568
|
+
onTouchEnd={() => {
|
|
2569
|
+
touchPositionRef.current = null;
|
|
2570
|
+
}}
|
|
2571
|
+
onTouchCancel={() => {
|
|
2572
|
+
touchPositionRef.current = null;
|
|
2573
|
+
}}
|
|
2574
|
+
onClickCapture={(event) => {
|
|
2575
|
+
const target =
|
|
2576
|
+
event.target instanceof Element
|
|
2577
|
+
? event.target.closest("button[aria-expanded]")
|
|
2578
|
+
: null;
|
|
2579
|
+
if (target) {
|
|
2580
|
+
if (target.hasAttribute("data-og-work-header")) {
|
|
2581
|
+
releaseProgress(
|
|
2582
|
+
target
|
|
2583
|
+
.closest("[data-og-group-key]")
|
|
2584
|
+
?.getAttribute("data-og-group-key") ?? null,
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2402
2587
|
releasePinFromReader();
|
|
2588
|
+
disclosureKeepsUnpinnedRef.current = true;
|
|
2589
|
+
}
|
|
2590
|
+
}}
|
|
2591
|
+
onFocusCapture={(event) => {
|
|
2592
|
+
if (readableTurns && event.target !== event.currentTarget) {
|
|
2593
|
+
stopSettlement();
|
|
2594
|
+
releasePinFromReader();
|
|
2595
|
+
disclosureKeepsUnpinnedRef.current = true;
|
|
2403
2596
|
}
|
|
2404
|
-
readingAnchorRef.current =
|
|
2405
|
-
!pinnedRef.current && scrollRef.current
|
|
2406
|
-
? captureTimelineAnchor(scrollRef.current)
|
|
2407
|
-
: null;
|
|
2408
|
-
settlementRef.current =
|
|
2409
|
-
readableTurns &&
|
|
2410
|
-
autoFollow &&
|
|
2411
|
-
pinnedRef.current &&
|
|
2412
|
-
revealedRef.current &&
|
|
2413
|
-
!hasNewer &&
|
|
2414
|
-
!pendingReaderLeaveRef.current &&
|
|
2415
|
-
!disclosureKeepsUnpinnedRef.current &&
|
|
2416
|
-
!prefersReducedMotion() &&
|
|
2417
|
-
scrollRef.current
|
|
2418
|
-
? captureTimelineSettlement(scrollRef.current, groups)
|
|
2419
|
-
: null;
|
|
2420
2597
|
}}
|
|
2598
|
+
onPointerDown={onPointerDown}
|
|
2599
|
+
onKeyDown={onKeyDown}
|
|
2600
|
+
style={
|
|
2601
|
+
groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined
|
|
2602
|
+
}
|
|
2603
|
+
className={cn(
|
|
2604
|
+
// tabIndex=-1 is programmatic only — never paint a focus ring on
|
|
2605
|
+
// the whole scroller (click + Shift used to flash a blue outline).
|
|
2606
|
+
"min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pt-16 pb-6 sm:px-6 outline-hidden",
|
|
2607
|
+
autoFollow && pinned && !hasNewer
|
|
2608
|
+
? "[overflow-anchor:none]"
|
|
2609
|
+
: "[overflow-anchor:auto]",
|
|
2610
|
+
)}
|
|
2421
2611
|
>
|
|
2422
|
-
<
|
|
2423
|
-
|
|
2424
|
-
{
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2612
|
+
<TimelineBeforeLayout
|
|
2613
|
+
capture={() => {
|
|
2614
|
+
if (readableTurns && timelineHasReader(scrollRef.current)) {
|
|
2615
|
+
releasePinFromReader();
|
|
2616
|
+
}
|
|
2617
|
+
readingAnchorRef.current =
|
|
2618
|
+
!pinnedRef.current && scrollRef.current
|
|
2619
|
+
? captureTimelineAnchor(scrollRef.current)
|
|
2620
|
+
: null;
|
|
2621
|
+
settlementRef.current =
|
|
2622
|
+
readableTurns &&
|
|
2623
|
+
autoFollow &&
|
|
2624
|
+
pinnedRef.current &&
|
|
2625
|
+
revealedRef.current &&
|
|
2626
|
+
!hasNewer &&
|
|
2627
|
+
!pendingReaderLeaveRef.current &&
|
|
2628
|
+
!disclosureKeepsUnpinnedRef.current &&
|
|
2629
|
+
!prefersReducedMotion() &&
|
|
2630
|
+
scrollRef.current
|
|
2631
|
+
? captureTimelineSettlement(scrollRef.current, groups)
|
|
2632
|
+
: null;
|
|
2633
|
+
}}
|
|
2634
|
+
>
|
|
2635
|
+
<div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
|
|
2636
|
+
<AnimatePresence>
|
|
2637
|
+
{loadingOlder ||
|
|
2638
|
+
loadingOldest ||
|
|
2639
|
+
(hasOlder && onJumpToStart && olderPrefetchArmed) ||
|
|
2640
|
+
underfillRetryReady ? (
|
|
2641
|
+
// Reserved top gutter: controls scroll with history and cannot
|
|
2642
|
+
// cover a disclosure. Visibility never changes content height.
|
|
2643
|
+
<motion.div
|
|
2644
|
+
initial={{ opacity: 0, y: -6 }}
|
|
2645
|
+
animate={{ opacity: 1, y: 0 }}
|
|
2646
|
+
exit={{ opacity: 0, y: -6 }}
|
|
2647
|
+
transition={{ duration: 0.15, ease: "easeOut" }}
|
|
2648
|
+
data-og-loading-older=""
|
|
2649
|
+
aria-live="polite"
|
|
2650
|
+
className="pointer-events-none absolute inset-x-0 -top-11 z-10 flex justify-center"
|
|
2651
|
+
>
|
|
2652
|
+
{loadingOlder || loadingOldest ? (
|
|
2653
|
+
<span className={LOADING_CHIP_CLASS}>
|
|
2654
|
+
<span className="og-shimmer-text">
|
|
2655
|
+
{loadingOldest
|
|
2656
|
+
? "Jumping to start…"
|
|
2657
|
+
: "Loading earlier activity…"}
|
|
2658
|
+
</span>
|
|
2445
2659
|
</span>
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
);
|
|
2467
|
-
}
|
|
2468
|
-
return;
|
|
2469
|
-
}
|
|
2470
|
-
applyPinned(false);
|
|
2471
|
-
pendingJumpToStartRef.current = true;
|
|
2472
|
-
const seq = ++jumpToStartSeqRef.current;
|
|
2473
|
-
void Promise.resolve(onJumpToStart!()).then(
|
|
2474
|
-
() => {
|
|
2475
|
-
// The commit that swaps in the oldest window consumes
|
|
2476
|
-
// the flag against the new DOM; this write covers the
|
|
2477
|
-
// already-committed order and the no-window-change
|
|
2478
|
-
// case (jumping within the current window).
|
|
2479
|
-
const scroller = scrollRef.current ?? node;
|
|
2480
|
-
if (scroller) {
|
|
2481
|
-
scroller.scrollTop = 0;
|
|
2660
|
+
) : null}
|
|
2661
|
+
{hasOlder &&
|
|
2662
|
+
!loadingOlder &&
|
|
2663
|
+
!loadingOldest &&
|
|
2664
|
+
(underfillRetryReady || onJumpToStart) ? (
|
|
2665
|
+
<button
|
|
2666
|
+
type="button"
|
|
2667
|
+
data-og-retry={underfillRetryReady || undefined}
|
|
2668
|
+
data-og-jump-to-start={!underfillRetryReady || undefined}
|
|
2669
|
+
onClick={() => {
|
|
2670
|
+
const node = scrollRef.current;
|
|
2671
|
+
if (underfillRetryReady) {
|
|
2672
|
+
if (node && underfillRetryReadyRef.current) {
|
|
2673
|
+
// AnimatePresence retains this handler during
|
|
2674
|
+
// exit. Current authorization plus the exact
|
|
2675
|
+
// attempt check prevent stale dispatch.
|
|
2676
|
+
requestOlderIfUnderfilled(
|
|
2677
|
+
node,
|
|
2678
|
+
underfillSettledAttempt!,
|
|
2679
|
+
);
|
|
2482
2680
|
}
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
applyPinned(false);
|
|
2684
|
+
pendingJumpToStartRef.current = true;
|
|
2685
|
+
const seq = ++jumpToStartSeqRef.current;
|
|
2686
|
+
void Promise.resolve(onJumpToStart!()).then(
|
|
2687
|
+
() => {
|
|
2688
|
+
// The commit that swaps in the oldest window consumes
|
|
2689
|
+
// the flag against the new DOM; this write covers the
|
|
2690
|
+
// already-committed order and the no-window-change
|
|
2691
|
+
// case (jumping within the current window).
|
|
2692
|
+
const scroller = scrollRef.current ?? node;
|
|
2693
|
+
if (scroller) {
|
|
2694
|
+
scroller.scrollTop = 0;
|
|
2695
|
+
}
|
|
2696
|
+
// A host may resolve without ever changing the
|
|
2697
|
+
// window (already on the oldest page). Any swap
|
|
2698
|
+
// commit runs its layout effect before the next
|
|
2699
|
+
// frame, so a flag still armed by then is the
|
|
2700
|
+
// no-change case — clear it, or a LATER prepend
|
|
2701
|
+
// would spuriously jump the reader to the top.
|
|
2702
|
+
requestFrame(() => {
|
|
2703
|
+
if (jumpToStartSeqRef.current === seq) {
|
|
2704
|
+
pendingJumpToStartRef.current = false;
|
|
2705
|
+
}
|
|
2706
|
+
});
|
|
2707
|
+
},
|
|
2708
|
+
() => {
|
|
2490
2709
|
if (jumpToStartSeqRef.current === seq) {
|
|
2491
2710
|
pendingJumpToStartRef.current = false;
|
|
2492
2711
|
}
|
|
2493
|
-
}
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2712
|
+
},
|
|
2713
|
+
);
|
|
2714
|
+
}}
|
|
2715
|
+
className="pointer-events-auto rounded-full border border-og-border px-3 py-1.5 text-og-control"
|
|
2716
|
+
>
|
|
2717
|
+
{underfillRetryReady
|
|
2718
|
+
? underfillSettledAttempt?.[2]?.tailPreserved
|
|
2719
|
+
? "Load earlier activity"
|
|
2720
|
+
: "Retry earlier activity"
|
|
2721
|
+
: "Jump to start"}
|
|
2722
|
+
</button>
|
|
2723
|
+
) : null}
|
|
2724
|
+
</motion.div>
|
|
2725
|
+
) : null}
|
|
2726
|
+
</AnimatePresence>
|
|
2727
|
+
{!groups.length
|
|
2728
|
+
? (emptyState ?? (
|
|
2729
|
+
<p className="py-10 text-center text-og-menu text-og-fg-subtle">
|
|
2730
|
+
No activity yet.
|
|
2731
|
+
</p>
|
|
2732
|
+
))
|
|
2733
|
+
: null}
|
|
2734
|
+
{hasOlder && olderPrefetchArmed ? (
|
|
2735
|
+
// Overlaid, not a layout row: mounting/unmounting the sentinel
|
|
2736
|
+
// must never shift content (that shift was itself a wobble).
|
|
2737
|
+
// End at the scroll origin above the pt-16 gutter so the
|
|
2738
|
+
// observer and scrollTop cooldown share the same 400px band.
|
|
2739
|
+
<div
|
|
2740
|
+
ref={topSentinelRef}
|
|
2741
|
+
data-og-top-sentinel=""
|
|
2742
|
+
data-og-timeline-chrome=""
|
|
2743
|
+
aria-hidden="true"
|
|
2744
|
+
className="pointer-events-none absolute inset-x-0 -top-16 h-px -translate-y-full"
|
|
2745
|
+
/>
|
|
2512
2746
|
) : null}
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
// Overlaid, not a layout row: mounting/unmounting the sentinel
|
|
2523
|
-
// must never shift content (that shift was itself a wobble).
|
|
2524
|
-
// End at the scroll origin above the pt-16 gutter so the
|
|
2525
|
-
// observer and scrollTop cooldown share the same 400px band.
|
|
2526
|
-
<div
|
|
2527
|
-
ref={topSentinelRef}
|
|
2528
|
-
data-og-top-sentinel=""
|
|
2529
|
-
data-og-timeline-chrome=""
|
|
2530
|
-
aria-hidden="true"
|
|
2531
|
-
className="pointer-events-none absolute inset-x-0 -top-16 h-px -translate-y-full"
|
|
2532
|
-
/>
|
|
2533
|
-
) : null}
|
|
2534
|
-
{groups.map(({ group, key, entranceEnabled }, index) => {
|
|
2535
|
-
return (
|
|
2536
|
-
<TimelineSearchRevealContext.Provider
|
|
2537
|
-
key={key}
|
|
2538
|
-
value={
|
|
2539
|
-
searchItem &&
|
|
2540
|
-
timelineGroupItemIds(group).includes(searchItem.id)
|
|
2541
|
-
? searchRevealKey
|
|
2542
|
-
: null
|
|
2543
|
-
}
|
|
2544
|
-
>
|
|
2545
|
-
<TimelineGroupEntry
|
|
2546
|
-
groupKey={key}
|
|
2547
|
-
group={group}
|
|
2548
|
-
nextGroup={groups[index + 1]?.group}
|
|
2549
|
-
startupDismissed={
|
|
2550
|
-
group.kind === "activity" &&
|
|
2551
|
-
group.items.some(
|
|
2552
|
-
(item) => item.turnId && turnsWithOutput.has(item.turnId),
|
|
2553
|
-
)
|
|
2747
|
+
{groups.map(({ group, key, entranceEnabled }, index) => {
|
|
2748
|
+
return (
|
|
2749
|
+
<TimelineSearchRevealContext.Provider
|
|
2750
|
+
key={key}
|
|
2751
|
+
value={
|
|
2752
|
+
searchItem &&
|
|
2753
|
+
timelineGroupItemIds(group).includes(searchItem.id)
|
|
2754
|
+
? searchRevealKey
|
|
2755
|
+
: null
|
|
2554
2756
|
}
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2757
|
+
>
|
|
2758
|
+
<TimelineGroupEntry
|
|
2759
|
+
groupKey={key}
|
|
2760
|
+
group={group}
|
|
2761
|
+
nextGroup={groups[index + 1]?.group}
|
|
2762
|
+
startupDismissed={
|
|
2763
|
+
group.kind === "activity" &&
|
|
2764
|
+
group.items.some(
|
|
2765
|
+
(item) => item.turnId && turnsWithOutput.has(item.turnId),
|
|
2766
|
+
)
|
|
2767
|
+
}
|
|
2768
|
+
entranceEnabled={entranceEnabled}
|
|
2769
|
+
liveEntranceEnabled={
|
|
2770
|
+
group.kind === "activity" ? !bulkRender : undefined
|
|
2771
|
+
}
|
|
2772
|
+
noteFolded={
|
|
2773
|
+
group.kind === "item"
|
|
2774
|
+
? liveNoteFolds.get(group.item.id)
|
|
2775
|
+
: undefined
|
|
2776
|
+
}
|
|
2777
|
+
context={timelineGroupEntryContext}
|
|
2778
|
+
/>
|
|
2779
|
+
</TimelineSearchRevealContext.Provider>
|
|
2780
|
+
);
|
|
2781
|
+
})}
|
|
2782
|
+
{groups.length > 0 && trailingState ? (
|
|
2783
|
+
<div data-og-timeline-trailing-state="">{trailingState}</div>
|
|
2784
|
+
) : null}
|
|
2785
|
+
{hasNewer && newerFailure?.key === newerBoundaryKey ? (
|
|
2786
|
+
<div
|
|
2787
|
+
data-og-newer-error=""
|
|
2788
|
+
className="flex flex-col items-center gap-2 px-4 py-3 text-center text-og-menu text-og-fg-muted"
|
|
2583
2789
|
>
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2790
|
+
<p role="status" className="max-w-prose [overflow-wrap:anywhere]">
|
|
2791
|
+
Couldn’t load later activity. {newerFailure.message}
|
|
2792
|
+
</p>
|
|
2793
|
+
<button
|
|
2794
|
+
ref={newerRetryButtonRef}
|
|
2795
|
+
type="button"
|
|
2796
|
+
data-og-retry-newer=""
|
|
2797
|
+
className="min-h-11 rounded-og-md border border-og-border px-3 py-2 text-og-fg hover:bg-og-surface-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-og-accent"
|
|
2798
|
+
aria-disabled={loadingNewer || newerRetryPending}
|
|
2799
|
+
aria-busy={newerRetryPending}
|
|
2800
|
+
onClick={() => requestNewer(true)}
|
|
2801
|
+
>
|
|
2802
|
+
{newerRetryPending
|
|
2803
|
+
? "Retrying later activity…"
|
|
2804
|
+
: "Retry later activity"}
|
|
2805
|
+
</button>
|
|
2806
|
+
</div>
|
|
2807
|
+
) : null}
|
|
2808
|
+
{hasNewer ? (
|
|
2809
|
+
<div
|
|
2810
|
+
ref={bottomSentinelRef}
|
|
2811
|
+
data-og-bottom-sentinel=""
|
|
2812
|
+
data-og-timeline-chrome=""
|
|
2813
|
+
aria-hidden="true"
|
|
2814
|
+
className="h-px w-full"
|
|
2815
|
+
/>
|
|
2816
|
+
) : null}
|
|
2817
|
+
</div>
|
|
2818
|
+
</TimelineBeforeLayout>
|
|
2819
|
+
</div>
|
|
2820
|
+
{draftAnnotations && draftAnnotations.length > 0 ? (
|
|
2821
|
+
<Suspense fallback={null}>
|
|
2822
|
+
<TimelineAnnotationMarkers
|
|
2823
|
+
rootRef={scrollRef}
|
|
2824
|
+
annotations={draftAnnotations}
|
|
2825
|
+
onSelect={onDraftAnnotationSelect}
|
|
2826
|
+
/>
|
|
2827
|
+
</Suspense>
|
|
2828
|
+
) : null}
|
|
2829
|
+
|
|
2830
|
+
<div
|
|
2831
|
+
data-og-timeline-nav="top"
|
|
2832
|
+
className={cn(
|
|
2833
|
+
"pointer-events-none absolute inset-x-0 z-10 flex justify-center",
|
|
2834
|
+
questionNav?.belowHeader ? "top-10 pointer-coarse:top-13" : "top-3",
|
|
2835
|
+
)}
|
|
2836
|
+
>
|
|
2837
|
+
<AnimatePresence>
|
|
2838
|
+
{questionNav ? (
|
|
2839
|
+
<motion.button
|
|
2840
|
+
key="question-nav"
|
|
2841
|
+
{...NAV_FADE}
|
|
2842
|
+
type="button"
|
|
2843
|
+
data-og-question-nav=""
|
|
2844
|
+
data-og-jump-to-question=""
|
|
2845
|
+
title="Back to your message"
|
|
2846
|
+
onClick={() => jumpToQuestion(questionNav.key)}
|
|
2847
|
+
className={NAV_BUTTON_CLASS}
|
|
2848
|
+
>
|
|
2849
|
+
<ArrowUpIcon aria-hidden className="size-4" />
|
|
2850
|
+
<span className="sr-only">Back to your message</span>
|
|
2851
|
+
</motion.button>
|
|
2589
2852
|
) : null}
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2853
|
+
</AnimatePresence>
|
|
2854
|
+
</div>
|
|
2855
|
+
<div
|
|
2856
|
+
data-og-timeline-nav="bottom"
|
|
2857
|
+
className="pointer-events-none absolute inset-x-0 bottom-3 z-10 flex flex-col items-center gap-2"
|
|
2858
|
+
>
|
|
2859
|
+
<AnimatePresence>
|
|
2860
|
+
{loadingNewer ? (
|
|
2861
|
+
<motion.div
|
|
2862
|
+
key="loading-newer"
|
|
2863
|
+
{...NAV_FADE}
|
|
2864
|
+
data-og-loading-newer=""
|
|
2865
|
+
aria-live="polite"
|
|
2866
|
+
className="flex max-w-full justify-center"
|
|
2867
|
+
>
|
|
2868
|
+
<span className={LOADING_CHIP_CLASS}>
|
|
2869
|
+
<span className="og-shimmer-text">Loading later activity…</span>
|
|
2870
|
+
</span>
|
|
2871
|
+
</motion.div>
|
|
2598
2872
|
) : null}
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
aria-live="polite"
|
|
2648
|
-
className="flex max-w-full justify-center"
|
|
2649
|
-
>
|
|
2650
|
-
<span className={LOADING_CHIP_CLASS}>
|
|
2651
|
-
<span className="og-shimmer-text">Loading later activity…</span>
|
|
2652
|
-
</span>
|
|
2653
|
-
</motion.div>
|
|
2654
|
-
) : null}
|
|
2655
|
-
{jumpPillShown ? (
|
|
2656
|
-
<motion.button
|
|
2657
|
-
key="jump-to-latest"
|
|
2658
|
-
{...NAV_FADE}
|
|
2659
|
-
type="button"
|
|
2660
|
-
data-og-jump-to-latest=""
|
|
2661
|
-
title="Jump to latest"
|
|
2662
|
-
onClick={() => {
|
|
2663
|
-
// Returning to the tip explicitly releases reader-owned
|
|
2664
|
-
// prose. Clear only this timeline's selection before the
|
|
2665
|
-
// synchronous ownership check on the next commit.
|
|
2666
|
-
const viewport = scrollRef.current;
|
|
2667
|
-
const selection = viewport?.ownerDocument.getSelection();
|
|
2668
|
-
if (
|
|
2669
|
-
selection &&
|
|
2670
|
-
!selection.isCollapsed &&
|
|
2671
|
-
(viewport?.contains(selection.anchorNode) ||
|
|
2672
|
-
viewport?.contains(selection.focusNode))
|
|
2673
|
-
)
|
|
2674
|
-
selection.removeAllRanges();
|
|
2675
|
-
disclosureKeepsUnpinnedRef.current = false;
|
|
2676
|
-
if (hasNewer) {
|
|
2677
|
-
// Do not pin against the current history page — its bottom
|
|
2678
|
-
// is not the tip. The pin + snap run when the tip window
|
|
2679
|
-
// actually lands (`hasNewer` flips false).
|
|
2680
|
-
wantPinRef.current = true;
|
|
2681
|
-
const node = scrollRef.current;
|
|
2682
|
-
if (onJumpToLatest) {
|
|
2683
|
-
void Promise.resolve(onJumpToLatest()).then(
|
|
2684
|
-
() => {
|
|
2685
|
-
// Covers a host that flipped hasNewer before
|
|
2686
|
-
// resolving; otherwise the tip-window commit
|
|
2687
|
-
// consumes the flag.
|
|
2688
|
-
const current = scrollRef.current;
|
|
2689
|
-
if (current && wantPinRef.current && !hasNewerRef.current) {
|
|
2873
|
+
{jumpPillShown ? (
|
|
2874
|
+
<motion.button
|
|
2875
|
+
key="jump-to-latest"
|
|
2876
|
+
{...NAV_FADE}
|
|
2877
|
+
type="button"
|
|
2878
|
+
data-og-jump-to-latest=""
|
|
2879
|
+
title="Jump to latest"
|
|
2880
|
+
onClick={() => {
|
|
2881
|
+
// Returning to the tip explicitly releases reader-owned
|
|
2882
|
+
// prose. Clear only this timeline's selection before the
|
|
2883
|
+
// synchronous ownership check on the next commit.
|
|
2884
|
+
const viewport = scrollRef.current;
|
|
2885
|
+
const selection = viewport?.ownerDocument.getSelection();
|
|
2886
|
+
if (
|
|
2887
|
+
selection &&
|
|
2888
|
+
!selection.isCollapsed &&
|
|
2889
|
+
(viewport?.contains(selection.anchorNode) ||
|
|
2890
|
+
viewport?.contains(selection.focusNode))
|
|
2891
|
+
)
|
|
2892
|
+
selection.removeAllRanges();
|
|
2893
|
+
disclosureKeepsUnpinnedRef.current = false;
|
|
2894
|
+
if (hasNewer) {
|
|
2895
|
+
// Do not pin against the current history page — its bottom
|
|
2896
|
+
// is not the tip. The pin + snap run when the tip window
|
|
2897
|
+
// actually lands (`hasNewer` flips false).
|
|
2898
|
+
wantPinRef.current = true;
|
|
2899
|
+
const node = scrollRef.current;
|
|
2900
|
+
if (onJumpToLatest) {
|
|
2901
|
+
void Promise.resolve(onJumpToLatest()).then(
|
|
2902
|
+
() => {
|
|
2903
|
+
// Covers a host that flipped hasNewer before
|
|
2904
|
+
// resolving; otherwise the tip-window commit
|
|
2905
|
+
// consumes the flag.
|
|
2906
|
+
const current = scrollRef.current;
|
|
2907
|
+
if (
|
|
2908
|
+
current &&
|
|
2909
|
+
wantPinRef.current &&
|
|
2910
|
+
!hasNewerRef.current
|
|
2911
|
+
) {
|
|
2912
|
+
wantPinRef.current = false;
|
|
2913
|
+
applyPinned(true);
|
|
2914
|
+
snapToBottom(current);
|
|
2915
|
+
}
|
|
2916
|
+
},
|
|
2917
|
+
() => {
|
|
2918
|
+
// The tip reload failed (ordinary network error):
|
|
2919
|
+
// an armed latch would fire a surprise snap when
|
|
2920
|
+
// the reader later pages to the tip themselves.
|
|
2690
2921
|
wantPinRef.current = false;
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
// window so the newer sentinel can page forward; the
|
|
2705
|
-
// latch pins if the tip window eventually lands.
|
|
2922
|
+
},
|
|
2923
|
+
);
|
|
2924
|
+
} else if (node) {
|
|
2925
|
+
// No tip reload available: jump within the in-memory
|
|
2926
|
+
// window so the newer sentinel can page forward; the
|
|
2927
|
+
// latch pins if the tip window eventually lands.
|
|
2928
|
+
snapToBottom(node);
|
|
2929
|
+
}
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2932
|
+
const node = scrollRef.current;
|
|
2933
|
+
if (node) {
|
|
2934
|
+
applyPinned(true);
|
|
2706
2935
|
snapToBottom(node);
|
|
2707
2936
|
}
|
|
2708
|
-
|
|
2709
|
-
}
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
>
|
|
2718
|
-
<ArrowDownIcon aria-hidden className="size-4" />
|
|
2719
|
-
<span className="sr-only">Jump to latest</span>
|
|
2720
|
-
</motion.button>
|
|
2721
|
-
) : null}
|
|
2722
|
-
</AnimatePresence>
|
|
2937
|
+
}}
|
|
2938
|
+
className={NAV_BUTTON_CLASS}
|
|
2939
|
+
>
|
|
2940
|
+
<ArrowDownIcon aria-hidden className="size-4" />
|
|
2941
|
+
<span className="sr-only">Jump to latest</span>
|
|
2942
|
+
</motion.button>
|
|
2943
|
+
) : null}
|
|
2944
|
+
</AnimatePresence>
|
|
2945
|
+
</div>
|
|
2723
2946
|
</div>
|
|
2724
|
-
</
|
|
2725
|
-
</
|
|
2726
|
-
</
|
|
2727
|
-
</
|
|
2728
|
-
</
|
|
2729
|
-
</
|
|
2947
|
+
</TimelineAnnotationSourceRootContext.Provider>
|
|
2948
|
+
</TooltipProvider>
|
|
2949
|
+
</EntranceAnimationProvider>
|
|
2950
|
+
</TimelineComputeLabelProvider>
|
|
2951
|
+
</SeenActivityIdsProvider>
|
|
2952
|
+
</LiveNoteFoldContext.Provider>
|
|
2730
2953
|
</FoldMemoryProvider>
|
|
2731
2954
|
</LightboxProvider>
|
|
2732
2955
|
</AllowancePresentationProvider>
|
|
@@ -2769,7 +2992,11 @@ function useStableTimelineGroupKeys(
|
|
|
2769
2992
|
const previousByItemId = new Map<string, KeyedTimelineGroup>();
|
|
2770
2993
|
for (const previous of previousRef.current) {
|
|
2771
2994
|
for (const itemId of timelineGroupItemIds(previous.group)) {
|
|
2772
|
-
|
|
2995
|
+
// A live progress note is both its own row and listed in its work
|
|
2996
|
+
// history. Its own row owns the id, so the note keeps its identity.
|
|
2997
|
+
if (previous.group.kind === "item" || !previousByItemId.has(itemId)) {
|
|
2998
|
+
previousByItemId.set(itemId, previous);
|
|
2999
|
+
}
|
|
2773
3000
|
}
|
|
2774
3001
|
}
|
|
2775
3002
|
|
|
@@ -2922,6 +3149,11 @@ type TimelineGroupEntryProps = {
|
|
|
2922
3149
|
startupDismissed: boolean;
|
|
2923
3150
|
entranceEnabled: boolean;
|
|
2924
3151
|
liveEntranceEnabled?: boolean | undefined;
|
|
3152
|
+
/**
|
|
3153
|
+
* Present for a live progress note: true while its turn's work disclosure is
|
|
3154
|
+
* open (the note then reads inside it), false while it reads here.
|
|
3155
|
+
*/
|
|
3156
|
+
noteFolded?: boolean | undefined;
|
|
2925
3157
|
context: TimelineGroupEntryContext;
|
|
2926
3158
|
};
|
|
2927
3159
|
|
|
@@ -2960,6 +3192,7 @@ const TimelineGroupEntry = memo(function TimelineGroupEntry({
|
|
|
2960
3192
|
startupDismissed,
|
|
2961
3193
|
entranceEnabled,
|
|
2962
3194
|
liveEntranceEnabled,
|
|
3195
|
+
noteFolded,
|
|
2963
3196
|
context,
|
|
2964
3197
|
}: TimelineGroupEntryProps) {
|
|
2965
3198
|
const reducedMotion = useReducedMotion();
|
|
@@ -2994,6 +3227,17 @@ const TimelineGroupEntry = memo(function TimelineGroupEntry({
|
|
|
2994
3227
|
group.kind === "item" && group.item.kind === "user-message" ? "" : undefined
|
|
2995
3228
|
}
|
|
2996
3229
|
tabIndex={group.kind === "item" && group.item.kind === "user-message" ? -1 : undefined}
|
|
3230
|
+
// A live progress note reads inside its open work row instead; folded,
|
|
3231
|
+
// it takes no space and also gives back the list's gap-5 above it.
|
|
3232
|
+
data-og-live-note={noteFolded === undefined ? undefined : noteFolded ? "folded" : "shown"}
|
|
3233
|
+
data-og-live-note-id={
|
|
3234
|
+
noteFolded !== undefined && group.kind === "item" ? group.item.id : undefined
|
|
3235
|
+
}
|
|
3236
|
+
aria-hidden={noteFolded || undefined}
|
|
3237
|
+
inert={noteFolded || undefined}
|
|
3238
|
+
className={
|
|
3239
|
+
noteFolded ? "-mt-5 h-0 overflow-hidden opacity-0 [overflow-anchor:none]" : undefined
|
|
3240
|
+
}
|
|
2997
3241
|
>
|
|
2998
3242
|
<EntranceAnimationProvider value={entranceEnabled} liveValue={liveEntranceEnabled}>
|
|
2999
3243
|
<TimelineGroupRenderBoundary resetKeys={[group, behavior]}>
|
|
@@ -3111,6 +3355,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
3111
3355
|
previousSingleActivity.current = work.length === 1 ? work[0] : undefined;
|
|
3112
3356
|
}, [group]);
|
|
3113
3357
|
const foldMemory = useFoldMemory();
|
|
3358
|
+
const reportLiveFold = useContext(LiveNoteFoldContext);
|
|
3114
3359
|
// Settled (or live-fold) activity clusters get a chip. Inside an expanded
|
|
3115
3360
|
// turn that is the second layer — quiet nested chips under the outer turn
|
|
3116
3361
|
// summary when contiguous activity naturally clusters (≥2 only).
|
|
@@ -3192,44 +3437,53 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
3192
3437
|
),
|
|
3193
3438
|
};
|
|
3194
3439
|
return (
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
group.
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
{
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3440
|
+
// Marks this work row for the live-note fold (list keys can be carried
|
|
3441
|
+
// over from an earlier row, so they are not the work id).
|
|
3442
|
+
<div className="contents" data-og-work-id={group.id}>
|
|
3443
|
+
<TurnSummary
|
|
3444
|
+
key="work"
|
|
3445
|
+
items={group.items}
|
|
3446
|
+
status={status}
|
|
3447
|
+
outcome={group.outcome}
|
|
3448
|
+
failureText={group.failureText}
|
|
3449
|
+
foldKey={group.id}
|
|
3450
|
+
defaultOpen={
|
|
3451
|
+
containsPresentedImage ||
|
|
3452
|
+
group.outcome === "failed" ||
|
|
3453
|
+
phases.some((item) => item.status === "failed" || item.status === "cancelled")
|
|
3454
|
+
? true
|
|
3455
|
+
: undefined
|
|
3456
|
+
}
|
|
3457
|
+
facets={turnSummary?.facets}
|
|
3458
|
+
contextCompactionCount={compactedLandmarkCount(group.work.details)}
|
|
3459
|
+
onOpenStateChange={
|
|
3460
|
+
group.work.liveNoteIds?.length && reportLiveFold
|
|
3461
|
+
? (open) => reportLiveFold(group.id, open)
|
|
3462
|
+
: undefined
|
|
3463
|
+
}
|
|
3464
|
+
>
|
|
3465
|
+
<TurnRailFrame compact>
|
|
3466
|
+
{renderFoldedGroups(
|
|
3467
|
+
group.work.details,
|
|
3468
|
+
{
|
|
3469
|
+
renderMessageActions,
|
|
3470
|
+
renderMessageText,
|
|
3471
|
+
onOpenSession,
|
|
3472
|
+
onMemoryClick,
|
|
3473
|
+
onReconnect,
|
|
3474
|
+
renderAuthNeeded,
|
|
3475
|
+
resolveProviderLogo,
|
|
3476
|
+
toolRegistry,
|
|
3477
|
+
loadRetainedScreenshot,
|
|
3478
|
+
loadRetainedArtifact,
|
|
3479
|
+
loadVideoArtifactPlayback,
|
|
3480
|
+
turnSummary,
|
|
3481
|
+
},
|
|
3482
|
+
true,
|
|
3483
|
+
)}
|
|
3484
|
+
</TurnRailFrame>
|
|
3485
|
+
</TurnSummary>
|
|
3486
|
+
</div>
|
|
3233
3487
|
);
|
|
3234
3488
|
}
|
|
3235
3489
|
// Preparation is one quiet surface, not a fold with eight technical steps.
|