@opengeni/react 3.0.0-canary.0 → 3.3.2-canary.0
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/accounts.js +87 -11
- package/dist/accounts.js.map +1 -1
- package/dist/{browser-viewer-AEFHUYZK.js → browser-viewer-MKKFRRRV.js} +3 -3
- package/dist/{chunk-W6NFJXSA.js → chunk-2DZVYHVO.js} +20 -4
- package/dist/chunk-2DZVYHVO.js.map +1 -0
- package/dist/{chunk-YJFGAFBD.js → chunk-3AGBGTRT.js} +226 -95
- package/dist/chunk-3AGBGTRT.js.map +1 -0
- package/dist/{chunk-YFKJ7VZH.js → chunk-7EAFGICR.js} +45 -11
- package/dist/chunk-7EAFGICR.js.map +1 -0
- package/dist/{chunk-MZ2G2XBA.js → chunk-IOLVN74D.js} +31 -4
- package/dist/chunk-IOLVN74D.js.map +1 -0
- package/dist/{chunk-D3UIAJKY.js → chunk-JAQ5K7W6.js} +3 -2
- package/dist/{chunk-D3UIAJKY.js.map → chunk-JAQ5K7W6.js.map} +1 -1
- package/dist/{chunk-LAKLF4PA.js → chunk-VTOREEXF.js} +8 -7
- package/dist/chunk-VTOREEXF.js.map +1 -0
- package/dist/{chunk-RGALCTWO.js → chunk-XYRDDSXF.js} +4 -4
- package/dist/{chunk-HQVCKCLG.js → chunk-YG4QEQF5.js} +354 -255
- package/dist/chunk-YG4QEQF5.js.map +1 -0
- package/dist/{chunk-XXWH2JLC.js → chunk-YVCHAQZY.js} +2 -2
- package/dist/{chunk-OAM4WAAM.js → chunk-ZPBTI3TE.js} +2 -2
- package/dist/components/fleet-tile.d.ts +2 -2
- package/dist/components/model-policy-picker.d.ts +9 -0
- package/dist/components/tip-follow.d.ts +16 -0
- package/dist/composer.js +5 -5
- package/dist/hooks/use-session-events.d.ts +3 -3
- package/dist/index.js +76 -68
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +2 -2
- package/dist/machines.js +2 -2
- package/dist/realtime.js +46 -7
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.js +4 -4
- package/dist/session.js +4 -4
- package/dist/timeline/entrance.d.ts +13 -1
- package/package.json +3 -2
- package/src/accounts.tsx +116 -14
- package/src/components/fleet-tile.tsx +4 -15
- package/src/components/message-timeline.tsx +237 -101
- package/src/components/model-policy-picker.tsx +24 -3
- package/src/components/tip-follow.ts +52 -0
- package/src/components/tooltip.tsx +38 -5
- package/src/hooks/use-available-models.ts +20 -14
- package/src/hooks/use-composer.ts +49 -3
- package/src/hooks/use-goal.ts +31 -7
- package/src/hooks/use-last-started-turn-policy.ts +11 -5
- package/src/hooks/use-session-events.ts +186 -61
- package/src/hooks/use-session-lineage.ts +20 -16
- package/src/hooks/use-session.ts +23 -19
- package/src/hooks/use-workspace-sessions.ts +31 -27
- package/src/lib/format.ts +8 -6
- package/src/realtime/realtime-control.tsx +55 -6
- package/src/timeline/entrance.tsx +28 -3
- package/src/timeline/projection.ts +4 -0
- package/dist/chunk-HQVCKCLG.js.map +0 -1
- package/dist/chunk-LAKLF4PA.js.map +0 -1
- package/dist/chunk-MZ2G2XBA.js.map +0 -1
- package/dist/chunk-W6NFJXSA.js.map +0 -1
- package/dist/chunk-YFKJ7VZH.js.map +0 -1
- package/dist/chunk-YJFGAFBD.js.map +0 -1
- /package/dist/{browser-viewer-AEFHUYZK.js.map → browser-viewer-MKKFRRRV.js.map} +0 -0
- /package/dist/{chunk-RGALCTWO.js.map → chunk-XYRDDSXF.js.map} +0 -0
- /package/dist/{chunk-XXWH2JLC.js.map → chunk-YVCHAQZY.js.map} +0 -0
- /package/dist/{chunk-OAM4WAAM.js.map → chunk-ZPBTI3TE.js.map} +0 -0
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
SessionEvent,
|
|
5
5
|
SessionStatus,
|
|
6
6
|
} from "@opengeni/sdk";
|
|
7
|
+
import { dequal } from "dequal/lite";
|
|
7
8
|
import {
|
|
8
9
|
ArrowDownIcon,
|
|
9
10
|
ArrowRightIcon,
|
|
@@ -57,12 +58,13 @@ import {
|
|
|
57
58
|
createTipFollowState,
|
|
58
59
|
readerScrollUpPx,
|
|
59
60
|
tipFollowCancel,
|
|
60
|
-
tipFollowCompensateShrink,
|
|
61
61
|
tipFollowCompensateViewportShrink,
|
|
62
|
+
tipFollowObserveContentShrink,
|
|
62
63
|
tipFollowStep,
|
|
63
64
|
supportsScrollEndEvent,
|
|
64
65
|
TIP_FOLLOW_READER_UP_EPS_PX,
|
|
65
66
|
TIP_FOLLOW_SHRINK_EPS_PX,
|
|
67
|
+
type TipFollowContentShrinkBaseline,
|
|
66
68
|
type TipFollowState,
|
|
67
69
|
} from "./tip-follow";
|
|
68
70
|
import {
|
|
@@ -433,7 +435,6 @@ export function MessageTimeline({
|
|
|
433
435
|
}
|
|
434
436
|
return sources;
|
|
435
437
|
}, [resolvedItems]);
|
|
436
|
-
const groups = useStableTimelineGroupKeys(allGroups);
|
|
437
438
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
438
439
|
const topSentinelRef = useRef<HTMLDivElement | null>(null);
|
|
439
440
|
const bottomSentinelRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -506,6 +507,8 @@ export function MessageTimeline({
|
|
|
506
507
|
* jumps (Vimium) settle via scrollend while the camera is idle.
|
|
507
508
|
*/
|
|
508
509
|
const readerIntentArmRef = useRef(false);
|
|
510
|
+
/** Gesture-start geometry; cumulative tiny pointer scrolls share one budget. */
|
|
511
|
+
const readerIntentStartRef = useRef<{ scrollTop: number; maxScroll: number } | null>(null);
|
|
509
512
|
/**
|
|
510
513
|
* Count of camera/snap scrollTop writes whose scroll echoes are not yet
|
|
511
514
|
* consumed. A boolean was wrong when the browser coalesced two writes into
|
|
@@ -533,7 +536,7 @@ export function MessageTimeline({
|
|
|
533
536
|
const userMessageDisclosureMemoryRef = useRef<Map<string, boolean>>(new Map());
|
|
534
537
|
const seenActivityIdsRef = useRef<Set<string>>(new Set());
|
|
535
538
|
const firstItemGroupKeyRef = useRef<string | null>(null);
|
|
536
|
-
const
|
|
539
|
+
const firstItemGroupOffsetTopRef = useRef<number | null>(null);
|
|
537
540
|
const firstItemContentTopRef = useRef<number | null>(null);
|
|
538
541
|
const firstItemId = resolvedItems[0]?.id ?? null;
|
|
539
542
|
// Pagination ownership follows the oldest committed input, before host
|
|
@@ -553,6 +556,7 @@ export function MessageTimeline({
|
|
|
553
556
|
previousBulkFirstKeyRef.current !== undefined &&
|
|
554
557
|
previousBulkFirstKeyRef.current !== firstGroupKey;
|
|
555
558
|
const bulkRender = allGroups.length > 0 && (bulkActive || firstKeyChangedForBulk);
|
|
559
|
+
const groups = useStableTimelineGroupKeys(allGroups, !bulkRender);
|
|
556
560
|
|
|
557
561
|
const applyCanSkipTipCatchup = useCallback((value: boolean) => {
|
|
558
562
|
if (canSkipTipCatchupRef.current !== value) {
|
|
@@ -592,6 +596,7 @@ export function MessageTimeline({
|
|
|
592
596
|
// Pure tip-follow camera. Pin intent uses clamp conservation, not timers.
|
|
593
597
|
const followRef = useRef<TipFollowState>(createTipFollowState());
|
|
594
598
|
const followFrameRef = useRef<number | null>(null);
|
|
599
|
+
const contentShrinkBaselineRef = useRef<TipFollowContentShrinkBaseline | null>(null);
|
|
595
600
|
|
|
596
601
|
const syncScrollBaseline = useCallback((node: HTMLElement) => {
|
|
597
602
|
lastScrollTopRef.current = node.scrollTop;
|
|
@@ -628,7 +633,13 @@ export function MessageTimeline({
|
|
|
628
633
|
cancelLeaveFallback();
|
|
629
634
|
}, [cancelLeaveFallback]);
|
|
630
635
|
|
|
636
|
+
const clearReaderIntent = useCallback(() => {
|
|
637
|
+
readerIntentArmRef.current = false;
|
|
638
|
+
readerIntentStartRef.current = null;
|
|
639
|
+
}, []);
|
|
640
|
+
|
|
631
641
|
const stopFollow = useCallback(() => {
|
|
642
|
+
contentShrinkBaselineRef.current = null;
|
|
632
643
|
followRef.current = tipFollowCancel(followRef.current);
|
|
633
644
|
if (followFrameRef.current != null) {
|
|
634
645
|
cancelFrame(followFrameRef.current);
|
|
@@ -646,7 +657,7 @@ export function MessageTimeline({
|
|
|
646
657
|
if (node && maxScrollOf(node) <= 1) {
|
|
647
658
|
return;
|
|
648
659
|
}
|
|
649
|
-
|
|
660
|
+
clearReaderIntent();
|
|
650
661
|
clearPendingReaderLeave();
|
|
651
662
|
stopFollow();
|
|
652
663
|
applyPinned(false);
|
|
@@ -658,7 +669,7 @@ export function MessageTimeline({
|
|
|
658
669
|
setOlderPrefetchArmed(true);
|
|
659
670
|
}
|
|
660
671
|
},
|
|
661
|
-
[autoFollow, applyPinned, clearPendingReaderLeave, stopFollow],
|
|
672
|
+
[autoFollow, applyPinned, clearPendingReaderLeave, clearReaderIntent, stopFollow],
|
|
662
673
|
);
|
|
663
674
|
|
|
664
675
|
/**
|
|
@@ -731,6 +742,7 @@ export function MessageTimeline({
|
|
|
731
742
|
button: number;
|
|
732
743
|
pointerType: string;
|
|
733
744
|
target: EventTarget | null;
|
|
745
|
+
currentTarget: EventTarget | null;
|
|
734
746
|
}) => {
|
|
735
747
|
// Primary button / touch / pen only. Ignore right-click etc.
|
|
736
748
|
if (event.button && event.pointerType === "mouse") {
|
|
@@ -745,7 +757,12 @@ export function MessageTimeline({
|
|
|
745
757
|
return;
|
|
746
758
|
}
|
|
747
759
|
disclosureKeepsUnpinnedRef.current = false;
|
|
760
|
+
const node =
|
|
761
|
+
event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
|
|
748
762
|
readerIntentArmRef.current = true;
|
|
763
|
+
readerIntentStartRef.current = node
|
|
764
|
+
? { scrollTop: node.scrollTop, maxScroll: maxScrollOf(node) }
|
|
765
|
+
: null;
|
|
749
766
|
};
|
|
750
767
|
|
|
751
768
|
const onKeyDown = (event: { key: string; currentTarget: EventTarget | null }) => {
|
|
@@ -769,6 +786,7 @@ export function MessageTimeline({
|
|
|
769
786
|
|
|
770
787
|
const snapToBottom = useCallback(
|
|
771
788
|
(node: HTMLElement) => {
|
|
789
|
+
clearReaderIntent();
|
|
772
790
|
stopFollow();
|
|
773
791
|
cancelLeaveFallback();
|
|
774
792
|
writeScrollTop(node, Math.max(0, node.scrollHeight - node.clientHeight));
|
|
@@ -781,7 +799,14 @@ export function MessageTimeline({
|
|
|
781
799
|
};
|
|
782
800
|
applyCanSkipTipCatchup(false);
|
|
783
801
|
},
|
|
784
|
-
[
|
|
802
|
+
[
|
|
803
|
+
applyCanSkipTipCatchup,
|
|
804
|
+
cancelLeaveFallback,
|
|
805
|
+
clearReaderIntent,
|
|
806
|
+
stopFollow,
|
|
807
|
+
syncScrollBaseline,
|
|
808
|
+
writeScrollTop,
|
|
809
|
+
],
|
|
785
810
|
);
|
|
786
811
|
|
|
787
812
|
const beginUserMessageDisclosureChange = useCallback(
|
|
@@ -842,6 +867,36 @@ export function MessageTimeline({
|
|
|
842
867
|
}),
|
|
843
868
|
[beginUserMessageDisclosureChange],
|
|
844
869
|
);
|
|
870
|
+
const timelineGroupEntryContext = useMemo<TimelineGroupEntryContext>(
|
|
871
|
+
() => ({
|
|
872
|
+
userMessageDisclosureContext,
|
|
873
|
+
behavior: {
|
|
874
|
+
renderMessageText,
|
|
875
|
+
onOpenSession,
|
|
876
|
+
onMemoryClick,
|
|
877
|
+
onReconnect,
|
|
878
|
+
resolveProviderLogo,
|
|
879
|
+
toolRegistry,
|
|
880
|
+
loadRetainedScreenshot,
|
|
881
|
+
loadRetainedArtifact,
|
|
882
|
+
loadVideoArtifactPlayback,
|
|
883
|
+
turnSummary,
|
|
884
|
+
},
|
|
885
|
+
}),
|
|
886
|
+
[
|
|
887
|
+
loadRetainedArtifact,
|
|
888
|
+
loadRetainedScreenshot,
|
|
889
|
+
loadVideoArtifactPlayback,
|
|
890
|
+
onMemoryClick,
|
|
891
|
+
onOpenSession,
|
|
892
|
+
onReconnect,
|
|
893
|
+
renderMessageText,
|
|
894
|
+
resolveProviderLogo,
|
|
895
|
+
toolRegistry,
|
|
896
|
+
turnSummary,
|
|
897
|
+
userMessageDisclosureContext,
|
|
898
|
+
],
|
|
899
|
+
);
|
|
845
900
|
|
|
846
901
|
const requestOlderIfUnderfilled = useCallback(
|
|
847
902
|
(node: HTMLElement, retry?: OlderLoadAttempt) => {
|
|
@@ -919,19 +974,41 @@ export function MessageTimeline({
|
|
|
919
974
|
: typeof performance !== "undefined"
|
|
920
975
|
? performance.now()
|
|
921
976
|
: Date.now();
|
|
922
|
-
|
|
977
|
+
let previousHeight = followRef.current.lastHeight;
|
|
978
|
+
const previousObservedHeight = lastScrollHeightRef.current;
|
|
979
|
+
if (
|
|
980
|
+
contentShrinkBaselineRef.current &&
|
|
981
|
+
previousObservedHeight > 0 &&
|
|
982
|
+
node.scrollHeight > previousObservedHeight
|
|
983
|
+
) {
|
|
984
|
+
// A reversal ends the collapse sequence. If it remains below the held
|
|
985
|
+
// baseline, adopt the recovered height; if it grew beyond the baseline,
|
|
986
|
+
// leave that baseline for tipFollowStep to observe as real growth.
|
|
987
|
+
contentShrinkBaselineRef.current = null;
|
|
988
|
+
if (node.scrollHeight < previousHeight) {
|
|
989
|
+
followRef.current = {
|
|
990
|
+
...followRef.current,
|
|
991
|
+
lastHeight: node.scrollHeight,
|
|
992
|
+
cameraTop: null,
|
|
993
|
+
};
|
|
994
|
+
previousHeight = node.scrollHeight;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
const shrinkObservation = tipFollowObserveContentShrink(
|
|
998
|
+
contentShrinkBaselineRef.current,
|
|
999
|
+
previousHeight,
|
|
1000
|
+
lastScrollTopRef.current,
|
|
1001
|
+
node.scrollHeight,
|
|
1002
|
+
node.clientHeight,
|
|
1003
|
+
);
|
|
1004
|
+
contentShrinkBaselineRef.current = shrinkObservation.baseline;
|
|
923
1005
|
// Settle-collapse: compensate Δh from the pre-shrink baseline (browser
|
|
924
1006
|
// may already have clamped — don't double-subtract). Keep the follow rAF
|
|
925
1007
|
// alive so when collapse ends (or stream resumes) we ease instead of a
|
|
926
1008
|
// hard stop → flick. Do NOT tip-ease on the same frame as a real shrink
|
|
927
1009
|
// (that fight was the top-of-viewport flicker).
|
|
928
|
-
if (
|
|
929
|
-
let nextTop =
|
|
930
|
-
lastScrollTopRef.current,
|
|
931
|
-
previousHeight,
|
|
932
|
-
node.scrollHeight,
|
|
933
|
-
node.clientHeight,
|
|
934
|
-
);
|
|
1010
|
+
if (shrinkObservation.compensatedScrollTop !== null) {
|
|
1011
|
+
let nextTop = shrinkObservation.compensatedScrollTop;
|
|
935
1012
|
// A chrome/composer dock can land on the same frame as a settle-fold
|
|
936
1013
|
// (the "turn blocked" moment). Compensate BOTH in one write — adopting
|
|
937
1014
|
// the shrunk clientHeight below without gluing left the chrome height
|
|
@@ -967,15 +1044,6 @@ export function MessageTimeline({
|
|
|
967
1044
|
}
|
|
968
1045
|
return;
|
|
969
1046
|
}
|
|
970
|
-
// Sub-eps height noise: adopt height without moving the camera. Do NOT
|
|
971
|
-
// adopt clientHeight here — tipFollowStep (next line) owns that baseline
|
|
972
|
-
// and must still see a same-frame viewport shrink to glue it.
|
|
973
|
-
if (previousHeight > 0 && node.scrollHeight < previousHeight) {
|
|
974
|
-
followRef.current = {
|
|
975
|
-
...followRef.current,
|
|
976
|
-
lastHeight: node.scrollHeight,
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
1047
|
const result = tipFollowStep(followRef.current, {
|
|
980
1048
|
scrollTop: node.scrollTop,
|
|
981
1049
|
scrollHeight: node.scrollHeight,
|
|
@@ -1027,7 +1095,7 @@ export function MessageTimeline({
|
|
|
1027
1095
|
}
|
|
1028
1096
|
const previousFirstItemId = previousFirstItemIdRef.current;
|
|
1029
1097
|
const previousFirstItemGroupKey = firstItemGroupKeyRef.current;
|
|
1030
|
-
const
|
|
1098
|
+
const previousFirstItemGroupOffsetTop = firstItemGroupOffsetTopRef.current;
|
|
1031
1099
|
const previousItemContentTop = firstItemContentTopRef.current;
|
|
1032
1100
|
const previousScrollTop = lastScrollTopRef.current;
|
|
1033
1101
|
const previousMaxScroll = lastMaxScrollRef.current;
|
|
@@ -1060,14 +1128,16 @@ export function MessageTimeline({
|
|
|
1060
1128
|
}
|
|
1061
1129
|
}
|
|
1062
1130
|
const anchorKey = previousFirstItemGroupKey;
|
|
1063
|
-
const previousAnchorTop =
|
|
1064
|
-
anchorKey != null ? previousGroupOffsetByKey.get(anchorKey) : undefined;
|
|
1065
1131
|
const anchorEl =
|
|
1066
1132
|
anchorKey != null
|
|
1067
1133
|
? node.querySelector(`[data-og-group-key="${cssEscapeAttribute(anchorKey)}"]`)
|
|
1068
1134
|
: null;
|
|
1069
|
-
if (
|
|
1070
|
-
|
|
1135
|
+
if (
|
|
1136
|
+
delta == null &&
|
|
1137
|
+
anchorEl instanceof HTMLElement &&
|
|
1138
|
+
previousFirstItemGroupOffsetTop != null
|
|
1139
|
+
) {
|
|
1140
|
+
const moved = Math.round(anchorEl.offsetTop - previousFirstItemGroupOffsetTop);
|
|
1071
1141
|
if (moved) {
|
|
1072
1142
|
delta = moved;
|
|
1073
1143
|
}
|
|
@@ -1170,14 +1240,12 @@ export function MessageTimeline({
|
|
|
1170
1240
|
hasNewer ||
|
|
1171
1241
|
firstItemContentTopRef.current == null;
|
|
1172
1242
|
if (needOffsets) {
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
}
|
|
1180
|
-
groupOffsetByKeyRef.current = nextOffsetByKey;
|
|
1243
|
+
const committedFirstGroupKey = firstItemGroupKeyRef.current;
|
|
1244
|
+
const firstGroupEl = committedFirstGroupKey
|
|
1245
|
+
? node.querySelector(`[data-og-group-key="${cssEscapeAttribute(committedFirstGroupKey)}"]`)
|
|
1246
|
+
: null;
|
|
1247
|
+
firstItemGroupOffsetTopRef.current =
|
|
1248
|
+
firstGroupEl instanceof HTMLElement ? firstGroupEl.offsetTop : null;
|
|
1181
1249
|
const firstItemEl = firstItemId
|
|
1182
1250
|
? node.querySelector(`[data-og-item="${cssEscapeAttribute(firstItemId)}"]`)
|
|
1183
1251
|
: null;
|
|
@@ -1279,14 +1347,16 @@ export function MessageTimeline({
|
|
|
1279
1347
|
lastScrollHeightRef.current = 0;
|
|
1280
1348
|
lastClientHeightRef.current = 0;
|
|
1281
1349
|
firstItemGroupKeyRef.current = null;
|
|
1282
|
-
|
|
1350
|
+
firstItemGroupOffsetTopRef.current = null;
|
|
1283
1351
|
firstItemContentTopRef.current = null;
|
|
1284
1352
|
foldMemoryRef.current.clear();
|
|
1285
1353
|
userMessageDisclosureMemoryRef.current.clear();
|
|
1286
1354
|
disclosureKeepsUnpinnedRef.current = false;
|
|
1355
|
+
clearReaderIntent();
|
|
1356
|
+
contentShrinkBaselineRef.current = null;
|
|
1287
1357
|
seenActivityIdsRef.current.clear();
|
|
1288
1358
|
applyPinned(true);
|
|
1289
|
-
}, [allGroups.length, revealed, applyPinned]);
|
|
1359
|
+
}, [allGroups.length, revealed, applyPinned, clearReaderIntent]);
|
|
1290
1360
|
|
|
1291
1361
|
// Parent commits cover the initial/history-loading cases. Disclosure state
|
|
1292
1362
|
// changes are child-local, so the ResizeObserver below owns dynamic collapse
|
|
@@ -1503,6 +1573,16 @@ export function MessageTimeline({
|
|
|
1503
1573
|
const readerUp = readerScrollUpPx(previousTop, nextTop, previousMaxScroll, nextMaxScroll);
|
|
1504
1574
|
const maxFell = nextMaxScroll < previousMaxScroll - 1;
|
|
1505
1575
|
const readerArmed = readerIntentArmRef.current;
|
|
1576
|
+
const readerIntentStart = readerIntentStartRef.current;
|
|
1577
|
+
const cumulativeReaderUp =
|
|
1578
|
+
readerArmed && readerIntentStart
|
|
1579
|
+
? readerScrollUpPx(
|
|
1580
|
+
readerIntentStart.scrollTop,
|
|
1581
|
+
nextTop,
|
|
1582
|
+
readerIntentStart.maxScroll,
|
|
1583
|
+
nextMaxScroll,
|
|
1584
|
+
)
|
|
1585
|
+
: readerUp;
|
|
1506
1586
|
const heightShrunk =
|
|
1507
1587
|
followRef.current.lastHeight > 0 &&
|
|
1508
1588
|
nextHeight < followRef.current.lastHeight - TIP_FOLLOW_SHRINK_EPS_PX;
|
|
@@ -1528,7 +1608,7 @@ export function MessageTimeline({
|
|
|
1528
1608
|
// Fold / composer content shrink: compensate before baseline sync so
|
|
1529
1609
|
// driveFollow still sees the pre-shrink scrollTop (avoid double-subtract).
|
|
1530
1610
|
if (heightShrunk || maxFell || viewportShrunk) {
|
|
1531
|
-
|
|
1611
|
+
clearReaderIntent();
|
|
1532
1612
|
clearPendingReaderLeave();
|
|
1533
1613
|
driveFollow(node);
|
|
1534
1614
|
return;
|
|
@@ -1550,8 +1630,8 @@ export function MessageTimeline({
|
|
|
1550
1630
|
clearPendingReaderLeave();
|
|
1551
1631
|
}
|
|
1552
1632
|
// Pointer-dragged scroll-up away from tip. Layout churn never arms this.
|
|
1553
|
-
if (readerArmed &&
|
|
1554
|
-
|
|
1633
|
+
if (readerArmed && cumulativeReaderUp > TIP_FOLLOW_READER_UP_EPS_PX && !nearBottomPinned) {
|
|
1634
|
+
clearReaderIntent();
|
|
1555
1635
|
releasePinFromReader(node);
|
|
1556
1636
|
rearmOlderPrefetchAfterLeavingTop(node);
|
|
1557
1637
|
return;
|
|
@@ -1633,7 +1713,7 @@ export function MessageTimeline({
|
|
|
1633
1713
|
<FoldMemoryProvider value={foldMemoryRef.current}>
|
|
1634
1714
|
<SeenActivityIdsProvider value={seenActivityIdsRef.current}>
|
|
1635
1715
|
<TimelineComputeLabelProvider value={computeLabel ?? null}>
|
|
1636
|
-
<EntranceAnimationProvider value={
|
|
1716
|
+
<EntranceAnimationProvider value={false}>
|
|
1637
1717
|
<TooltipProvider delayDuration={400}>
|
|
1638
1718
|
<div className={cn("og-root relative flex min-h-0 flex-col", className)}>
|
|
1639
1719
|
{onAnnotate ? (
|
|
@@ -1686,56 +1766,19 @@ export function MessageTimeline({
|
|
|
1686
1766
|
className="pointer-events-none absolute inset-x-0 top-0 h-px"
|
|
1687
1767
|
/>
|
|
1688
1768
|
) : null}
|
|
1689
|
-
{groups.map(({ group, key }, index) => {
|
|
1690
|
-
const next = groups[index + 1]?.group;
|
|
1691
|
-
const contextCompactionCount =
|
|
1692
|
-
group.kind === "turn"
|
|
1693
|
-
? (group.contextCompactionCount ?? 0)
|
|
1694
|
-
: group.kind === "activity" &&
|
|
1695
|
-
next?.kind === "item" &&
|
|
1696
|
-
next.item.kind === "context-compaction" &&
|
|
1697
|
-
next.item.phase === "compacted"
|
|
1698
|
-
? 1
|
|
1699
|
-
: 0;
|
|
1769
|
+
{groups.map(({ group, key, entranceEnabled }, index) => {
|
|
1700
1770
|
return (
|
|
1701
|
-
<
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
loadRetainedArtifact,
|
|
1713
|
-
loadVideoArtifactPlayback,
|
|
1714
|
-
turnSummary,
|
|
1715
|
-
]}
|
|
1716
|
-
>
|
|
1717
|
-
<UserMessageDisclosureProvider value={userMessageDisclosureContext}>
|
|
1718
|
-
<TimelineGroupView
|
|
1719
|
-
group={group}
|
|
1720
|
-
renderMessageText={renderMessageText}
|
|
1721
|
-
onOpenSession={onOpenSession}
|
|
1722
|
-
onMemoryClick={onMemoryClick}
|
|
1723
|
-
onReconnect={onReconnect}
|
|
1724
|
-
resolveProviderLogo={resolveProviderLogo}
|
|
1725
|
-
toolRegistry={toolRegistry}
|
|
1726
|
-
loadRetainedScreenshot={loadRetainedScreenshot}
|
|
1727
|
-
loadRetainedArtifact={loadRetainedArtifact}
|
|
1728
|
-
loadVideoArtifactPlayback={loadVideoArtifactPlayback}
|
|
1729
|
-
turnSummary={turnSummary}
|
|
1730
|
-
foldLiveCluster={isAgentProgress(next)}
|
|
1731
|
-
trailingAgentText={trailingAgentTextAfterTurn(group, next)}
|
|
1732
|
-
contextCompactionCount={
|
|
1733
|
-
contextCompactionCount > 0 ? contextCompactionCount : undefined
|
|
1734
|
-
}
|
|
1735
|
-
/>
|
|
1736
|
-
</UserMessageDisclosureProvider>
|
|
1737
|
-
</TimelineGroupRenderBoundary>
|
|
1738
|
-
</div>
|
|
1771
|
+
<TimelineGroupEntry
|
|
1772
|
+
key={key}
|
|
1773
|
+
groupKey={key}
|
|
1774
|
+
group={group}
|
|
1775
|
+
nextGroup={groups[index + 1]?.group}
|
|
1776
|
+
entranceEnabled={entranceEnabled}
|
|
1777
|
+
liveEntranceEnabled={
|
|
1778
|
+
group.kind === "activity" ? !bulkRender : undefined
|
|
1779
|
+
}
|
|
1780
|
+
context={timelineGroupEntryContext}
|
|
1781
|
+
/>
|
|
1739
1782
|
);
|
|
1740
1783
|
})}
|
|
1741
1784
|
{groups.length > 0 && trailingState ? (
|
|
@@ -1927,8 +1970,20 @@ export function MessageTimeline({
|
|
|
1927
1970
|
type KeyedTimelineGroup = {
|
|
1928
1971
|
group: TimelineGroup;
|
|
1929
1972
|
key: string;
|
|
1973
|
+
entranceEnabled: boolean;
|
|
1930
1974
|
};
|
|
1931
1975
|
|
|
1976
|
+
function timelineGroupsRenderEqual(previous: TimelineGroup, next: TimelineGroup): boolean {
|
|
1977
|
+
try {
|
|
1978
|
+
return dequal(previous, next);
|
|
1979
|
+
} catch {
|
|
1980
|
+
// Consumer-owned `unknown` payloads may be proxies/getters. Equality is an
|
|
1981
|
+
// optimization only; a hostile comparator surface must fall back to the
|
|
1982
|
+
// authoritative new group rather than taking down the timeline render.
|
|
1983
|
+
return false;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1932
1987
|
/**
|
|
1933
1988
|
* Projection can legitimately change a group's content-derived key while
|
|
1934
1989
|
* retaining its existing rows. The common pagination case is an older activity
|
|
@@ -1937,7 +1992,10 @@ type KeyedTimelineGroup = {
|
|
|
1937
1992
|
* committed groups by their durable item IDs so both the React key and the
|
|
1938
1993
|
* progressive-window anchor survive either change.
|
|
1939
1994
|
*/
|
|
1940
|
-
function useStableTimelineGroupKeys(
|
|
1995
|
+
function useStableTimelineGroupKeys(
|
|
1996
|
+
allGroups: TimelineGroup[],
|
|
1997
|
+
entranceEnabled: boolean,
|
|
1998
|
+
): KeyedTimelineGroup[] {
|
|
1941
1999
|
const previousRef = useRef<KeyedTimelineGroup[]>([]);
|
|
1942
2000
|
const keyedGroups = useMemo(() => {
|
|
1943
2001
|
const previousByItemId = new Map<string, KeyedTimelineGroup>();
|
|
@@ -1950,29 +2008,36 @@ function useStableTimelineGroupKeys(allGroups: TimelineGroup[]): KeyedTimelineGr
|
|
|
1950
2008
|
const usedKeys = new Set<string>();
|
|
1951
2009
|
return allGroups.map((group, index) => {
|
|
1952
2010
|
const itemIds = timelineGroupItemIds(group);
|
|
1953
|
-
let
|
|
2011
|
+
let retainedGroup: KeyedTimelineGroup | undefined;
|
|
1954
2012
|
for (const itemId of itemIds) {
|
|
1955
2013
|
const previous = previousByItemId.get(itemId);
|
|
1956
2014
|
// Retain only same-kind matches. Activity → turn wrap must NOT keep the
|
|
1957
2015
|
// activity chip's React key: that reused a collapsed TurnSummary and
|
|
1958
2016
|
// skipped the settle beat (insta-collapse / content flash).
|
|
1959
2017
|
if (previous && previous.group.kind === group.kind && !usedKeys.has(previous.key)) {
|
|
1960
|
-
|
|
2018
|
+
retainedGroup = previous;
|
|
1961
2019
|
break;
|
|
1962
2020
|
}
|
|
1963
2021
|
}
|
|
1964
2022
|
|
|
1965
2023
|
const canonicalKey = timelineGroupKey(group);
|
|
1966
|
-
let key =
|
|
2024
|
+
let key = retainedGroup?.key ?? canonicalKey;
|
|
1967
2025
|
let collision = 0;
|
|
1968
2026
|
while (usedKeys.has(key)) {
|
|
1969
2027
|
key = `${canonicalKey}:${index}:${collision}`;
|
|
1970
2028
|
collision += 1;
|
|
1971
2029
|
}
|
|
1972
2030
|
usedKeys.add(key);
|
|
1973
|
-
return {
|
|
2031
|
+
return {
|
|
2032
|
+
group:
|
|
2033
|
+
retainedGroup && timelineGroupsRenderEqual(retainedGroup.group, group)
|
|
2034
|
+
? retainedGroup.group
|
|
2035
|
+
: group,
|
|
2036
|
+
key,
|
|
2037
|
+
entranceEnabled: retainedGroup?.entranceEnabled ?? entranceEnabled,
|
|
2038
|
+
};
|
|
1974
2039
|
});
|
|
1975
|
-
}, [allGroups]);
|
|
2040
|
+
}, [allGroups, entranceEnabled]);
|
|
1976
2041
|
|
|
1977
2042
|
useLayoutEffect(() => {
|
|
1978
2043
|
previousRef.current = keyedGroups;
|
|
@@ -2065,11 +2130,82 @@ class TimelineGroupRenderBoundary extends Component<
|
|
|
2065
2130
|
}
|
|
2066
2131
|
}
|
|
2067
2132
|
|
|
2133
|
+
type TimelineGroupEntryProps = {
|
|
2134
|
+
groupKey: string;
|
|
2135
|
+
group: TimelineGroup;
|
|
2136
|
+
nextGroup?: TimelineGroup | undefined;
|
|
2137
|
+
entranceEnabled: boolean;
|
|
2138
|
+
liveEntranceEnabled?: boolean | undefined;
|
|
2139
|
+
context: TimelineGroupEntryContext;
|
|
2140
|
+
};
|
|
2141
|
+
|
|
2142
|
+
type TimelineGroupEntryContext = {
|
|
2143
|
+
userMessageDisclosureContext: UserMessageDisclosureContextValue;
|
|
2144
|
+
behavior: TimelineGroupBehaviorProps;
|
|
2145
|
+
};
|
|
2146
|
+
|
|
2147
|
+
type TimelineGroupBehaviorProps = {
|
|
2148
|
+
renderMessageText: MessageTimelineProps["renderMessageText"];
|
|
2149
|
+
onOpenSession: MessageTimelineProps["onOpenSession"];
|
|
2150
|
+
onMemoryClick: MessageTimelineProps["onMemoryClick"];
|
|
2151
|
+
onReconnect: MessageTimelineProps["onReconnect"];
|
|
2152
|
+
resolveProviderLogo: MessageTimelineProps["resolveProviderLogo"];
|
|
2153
|
+
toolRegistry: ToolRegistry;
|
|
2154
|
+
loadRetainedScreenshot: MessageTimelineProps["loadRetainedScreenshot"];
|
|
2155
|
+
loadRetainedArtifact: MessageTimelineProps["loadRetainedArtifact"];
|
|
2156
|
+
loadVideoArtifactPlayback: MessageTimelineProps["loadVideoArtifactPlayback"];
|
|
2157
|
+
turnSummary: MessageTimelineProps["turnSummary"];
|
|
2158
|
+
};
|
|
2159
|
+
|
|
2160
|
+
/**
|
|
2161
|
+
* Keep the complete settled-row shell behind one shallow memo boundary. A
|
|
2162
|
+
* prepend still reconciles the keyed list, but render-equivalent suffix groups
|
|
2163
|
+
* skip their providers, error boundaries, disclosure wrappers, and row trees.
|
|
2164
|
+
*/
|
|
2165
|
+
const TimelineGroupEntry = memo(function TimelineGroupEntry({
|
|
2166
|
+
groupKey,
|
|
2167
|
+
group,
|
|
2168
|
+
nextGroup,
|
|
2169
|
+
entranceEnabled,
|
|
2170
|
+
liveEntranceEnabled,
|
|
2171
|
+
context,
|
|
2172
|
+
}: TimelineGroupEntryProps) {
|
|
2173
|
+
const { behavior, userMessageDisclosureContext } = context;
|
|
2174
|
+
const contextCompactionCount =
|
|
2175
|
+
group.kind === "turn"
|
|
2176
|
+
? (group.contextCompactionCount ?? 0)
|
|
2177
|
+
: group.kind === "activity" &&
|
|
2178
|
+
nextGroup?.kind === "item" &&
|
|
2179
|
+
nextGroup.item.kind === "context-compaction" &&
|
|
2180
|
+
nextGroup.item.phase === "compacted"
|
|
2181
|
+
? 1
|
|
2182
|
+
: 0;
|
|
2183
|
+
return (
|
|
2184
|
+
<div data-og-timeline-group-anchor="" data-og-group-key={groupKey}>
|
|
2185
|
+
<EntranceAnimationProvider value={entranceEnabled} liveValue={liveEntranceEnabled}>
|
|
2186
|
+
<TimelineGroupRenderBoundary resetKeys={[group, behavior]}>
|
|
2187
|
+
<UserMessageDisclosureProvider value={userMessageDisclosureContext}>
|
|
2188
|
+
<TimelineGroupView
|
|
2189
|
+
{...behavior}
|
|
2190
|
+
group={group}
|
|
2191
|
+
foldLiveCluster={isAgentProgress(nextGroup)}
|
|
2192
|
+
trailingAgentText={trailingAgentTextAfterTurn(group, nextGroup)}
|
|
2193
|
+
contextCompactionCount={
|
|
2194
|
+
contextCompactionCount > 0 ? contextCompactionCount : undefined
|
|
2195
|
+
}
|
|
2196
|
+
/>
|
|
2197
|
+
</UserMessageDisclosureProvider>
|
|
2198
|
+
</TimelineGroupRenderBoundary>
|
|
2199
|
+
</EntranceAnimationProvider>
|
|
2200
|
+
</div>
|
|
2201
|
+
);
|
|
2202
|
+
});
|
|
2203
|
+
|
|
2068
2204
|
// The full loaded window stays mounted, so settled history rows must be cheap
|
|
2069
|
-
// on every commit: projection reuses group
|
|
2070
|
-
// memo skips them.
|
|
2071
|
-
// behavior/callback changes are separate props, so ordinary
|
|
2072
|
-
// updates still invalidate immediately.
|
|
2205
|
+
// on every commit: the stable-key projection reuses render-equivalent group
|
|
2206
|
+
// objects, so memo skips them. Changed projection content receives a new group
|
|
2207
|
+
// object, and behavior/callback changes are separate props, so ordinary
|
|
2208
|
+
// streaming and host updates still invalidate immediately.
|
|
2073
2209
|
const TimelineGroupView = memo(function TimelineGroupView({
|
|
2074
2210
|
group,
|
|
2075
2211
|
renderMessageText,
|
|
@@ -13,6 +13,7 @@ import { DropdownMenu } from "radix-ui";
|
|
|
13
13
|
import {
|
|
14
14
|
useEffect,
|
|
15
15
|
useMemo,
|
|
16
|
+
useRef,
|
|
16
17
|
useState,
|
|
17
18
|
type CSSProperties,
|
|
18
19
|
type ReactNode,
|
|
@@ -541,21 +542,41 @@ export function ModelPolicyPickerMenu(
|
|
|
541
542
|
);
|
|
542
543
|
}
|
|
543
544
|
|
|
544
|
-
|
|
545
|
+
/** @internal Shared state seam kept outside the Radix portal for deterministic testing. */
|
|
546
|
+
export function useModelPolicyPickerState(props: ModelPolicyPickerProps) {
|
|
545
547
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(props.defaultOpen ?? false);
|
|
546
548
|
const open = props.open ?? uncontrolledOpen;
|
|
549
|
+
const rows = effectiveRows(props);
|
|
550
|
+
const [nav, setNav] = usePickerNavState(props.sessionKey, rows, props.model);
|
|
551
|
+
const previousControlledOpen = useRef(props.open);
|
|
552
|
+
useEffect(() => {
|
|
553
|
+
const wasOpen = previousControlledOpen.current;
|
|
554
|
+
previousControlledOpen.current = props.open;
|
|
555
|
+
if (props.open === true && wasOpen !== true) {
|
|
556
|
+
setNav(defaultNavState(rows, props.model));
|
|
557
|
+
}
|
|
558
|
+
// Reset only on the controlled closed-to-open edge; ordinary rerenders
|
|
559
|
+
// deliberately preserve the current drill-down page.
|
|
560
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
561
|
+
}, [props.open]);
|
|
547
562
|
const setOpen = (next: boolean) => {
|
|
563
|
+
if (next) {
|
|
564
|
+
setNav(defaultNavState(rows, props.model));
|
|
565
|
+
}
|
|
548
566
|
if (props.open === undefined) setUncontrolledOpen(next);
|
|
549
567
|
props.onOpenChange?.(next);
|
|
550
568
|
};
|
|
569
|
+
return { open, setOpen, rows, nav, setNav };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export function ModelPolicyPicker(props: ModelPolicyPickerProps) {
|
|
551
573
|
const trigger = usePortalTokenSource<HTMLButtonElement>();
|
|
552
574
|
const portalStyle = usePortalTokenStyle(trigger.source);
|
|
553
575
|
const messages = useMemo(
|
|
554
576
|
() => ({ ...defaultModelPolicyPickerMessages, ...props.messages }),
|
|
555
577
|
[props.messages],
|
|
556
578
|
);
|
|
557
|
-
const rows =
|
|
558
|
-
const [nav, setNav] = usePickerNavState(props.sessionKey, rows, props.model);
|
|
579
|
+
const { open, setOpen, rows, nav, setNav } = useModelPolicyPickerState(props);
|
|
559
580
|
const selected = findPickerRow(rows, props.model);
|
|
560
581
|
|
|
561
582
|
if (props.loading) {
|