@opengeni/react 0.37.0 → 0.40.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/README.md +88 -2
- package/dist/{chunk-7CJOAW3V.js → chunk-EB67J347.js} +1 -135
- package/dist/chunk-EB67J347.js.map +1 -0
- package/dist/chunk-EKZH6IDG.js +141 -0
- package/dist/chunk-EKZH6IDG.js.map +1 -0
- package/dist/{chunk-FSPDND3P.js → chunk-JALF5FI3.js} +8 -6
- package/dist/{chunk-FSPDND3P.js.map → chunk-JALF5FI3.js.map} +1 -1
- package/dist/{chunk-UCZPNXV3.js → chunk-KR2SK5GJ.js} +1292 -711
- package/dist/chunk-KR2SK5GJ.js.map +1 -0
- package/dist/{chunk-IP22SLO6.js → chunk-OMCFRWHL.js} +28 -441
- package/dist/chunk-OMCFRWHL.js.map +1 -0
- package/dist/{chunk-HJ4OQVGW.js → chunk-Q2NCKWTK.js} +10 -5
- package/dist/chunk-Q2NCKWTK.js.map +1 -0
- package/dist/{chunk-ALA3UGWB.js → chunk-SXIQK54Z.js} +8 -6
- package/dist/{chunk-ALA3UGWB.js.map → chunk-SXIQK54Z.js.map} +1 -1
- package/dist/chunk-WZT5G5OR.js +433 -0
- package/dist/chunk-WZT5G5OR.js.map +1 -0
- package/dist/client.d.ts +2 -0
- package/dist/components/chat-composer.d.ts +5 -1
- package/dist/components/human-input-form.d.ts +16 -6
- package/dist/components/human-input-surface.d.ts +19 -0
- package/dist/components/machine-input-display.d.ts +12 -0
- package/dist/components/message-timeline.d.ts +6 -1
- package/dist/components/session-chrome.d.ts +1 -0
- package/dist/components/tip-follow.d.ts +30 -0
- package/dist/composer.js +14 -11
- package/dist/hooks/use-composer.d.ts +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2246 -2236
- package/dist/index.js.map +1 -1
- package/dist/machines.js +4 -3
- package/dist/realtime/dropdown-menu.d.ts +16 -0
- package/dist/realtime/realtime-control.d.ts +126 -0
- package/dist/realtime.d.ts +11 -0
- package/dist/realtime.js +1023 -0
- package/dist/realtime.js.map +1 -0
- package/dist/session-context.d.ts +7 -1
- package/dist/session-ui.d.ts +2 -0
- package/dist/session-ui.js +5 -3
- package/dist/session.js +4 -3
- package/dist/timeline/compute-label.d.ts +2 -0
- package/dist/timeline/index.d.ts +1 -0
- package/package.json +7 -2
- package/src/client.ts +13 -0
- package/src/components/chat-composer.tsx +11 -2
- package/src/components/composer-transcription-control.tsx +2 -2
- package/src/components/composer.tsx +14 -5
- package/src/components/human-input-form.tsx +506 -186
- package/src/components/human-input-surface.tsx +80 -0
- package/src/components/machine-input-display.ts +83 -0
- package/src/components/message-timeline.tsx +348 -308
- package/src/components/model-policy-picker.tsx +8 -3
- package/src/components/sandbox-files.tsx +1 -1
- package/src/components/session-chrome.tsx +3 -1
- package/src/components/tip-follow.ts +127 -6
- package/src/hooks/use-composer.ts +16 -13
- package/src/index.ts +4 -0
- package/src/realtime/dropdown-menu.tsx +123 -0
- package/src/realtime/realtime-control.tsx +1178 -0
- package/src/realtime.ts +27 -0
- package/src/session-context.ts +16 -0
- package/src/session-ui.ts +2 -0
- package/src/timeline/compute-label.tsx +18 -0
- package/src/timeline/index.ts +3 -0
- package/src/timeline/tool-renderers.tsx +232 -43
- package/styles/index.css +61 -0
- package/styles/tokens.css +2 -0
- package/dist/chunk-7CJOAW3V.js.map +0 -1
- package/dist/chunk-HJ4OQVGW.js.map +0 -1
- package/dist/chunk-IP22SLO6.js.map +0 -1
- package/dist/chunk-UCZPNXV3.js.map +0 -1
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
readerScrollUpPx,
|
|
42
42
|
tipFollowCancel,
|
|
43
43
|
tipFollowCompensateShrink,
|
|
44
|
+
tipFollowCompensateViewportShrink,
|
|
44
45
|
tipFollowStep,
|
|
45
46
|
supportsScrollEndEvent,
|
|
46
47
|
TIP_FOLLOW_READER_UP_EPS_PX,
|
|
@@ -74,7 +75,15 @@ import {
|
|
|
74
75
|
useTurnSettleOpen,
|
|
75
76
|
} from "../timeline";
|
|
76
77
|
import { CopyHoverFrame } from "./copy-button";
|
|
78
|
+
import {
|
|
79
|
+
MACHINE_INPUT_META,
|
|
80
|
+
cleanMachineInputSummary,
|
|
81
|
+
machineInputBatchLabel,
|
|
82
|
+
machineInputSummaryIsUseful,
|
|
83
|
+
readableMachineInputSource,
|
|
84
|
+
} from "./machine-input-display";
|
|
77
85
|
import { SESSION_STATUS_META, StatusDot } from "./session-status";
|
|
86
|
+
import { TimelineComputeLabelProvider } from "../timeline/compute-label";
|
|
78
87
|
import { EntranceAnimationProvider, useEntranceAnimation } from "../timeline/entrance";
|
|
79
88
|
import { SeenActivityIdsProvider } from "../timeline/seen-activity-ids";
|
|
80
89
|
import { TooltipProvider } from "./tooltip";
|
|
@@ -123,6 +132,11 @@ export type MessageTimelineProps = {
|
|
|
123
132
|
* `createDefaultToolRegistry({ entries })` to add custom tool renderers.
|
|
124
133
|
*/
|
|
125
134
|
toolRegistry?: ToolRegistry | undefined;
|
|
135
|
+
/**
|
|
136
|
+
* Display name of the session's active compute target (Connected Machine or
|
|
137
|
+
* cloud sandbox). When set, exec_command collapsed previews prefix `on {label}`.
|
|
138
|
+
*/
|
|
139
|
+
computeLabel?: string | null | undefined;
|
|
126
140
|
/** Customize collapsed turn facets for this timeline instance. */
|
|
127
141
|
turnSummary?: TurnSummaryOptions | undefined;
|
|
128
142
|
/** Follow new events when pinned to the bottom. Defaults to true. */
|
|
@@ -260,6 +274,7 @@ export function MessageTimeline({
|
|
|
260
274
|
onReconnect,
|
|
261
275
|
resolveProviderLogo,
|
|
262
276
|
toolRegistry = defaultToolRegistry,
|
|
277
|
+
computeLabel = null,
|
|
263
278
|
turnSummary,
|
|
264
279
|
autoFollow = true,
|
|
265
280
|
hasOlder = false,
|
|
@@ -391,11 +406,18 @@ export function MessageTimeline({
|
|
|
391
406
|
|
|
392
407
|
const writeScrollTop = useCallback((node: HTMLElement, top: number) => {
|
|
393
408
|
const next = Math.max(0, top);
|
|
394
|
-
|
|
409
|
+
const before = node.scrollTop;
|
|
410
|
+
if (before === next) {
|
|
395
411
|
return;
|
|
396
412
|
}
|
|
397
413
|
programmaticScrollRef.current += 1;
|
|
398
414
|
node.scrollTop = next;
|
|
415
|
+
if (node.scrollTop === before) {
|
|
416
|
+
// The engine floored a sub-device-pixel write to a no-op: no scroll echo
|
|
417
|
+
// will ever fire. Counting it would leak the echo count and silently eat
|
|
418
|
+
// a later REAL reader scroll as programmatic.
|
|
419
|
+
programmaticScrollRef.current -= 1;
|
|
420
|
+
}
|
|
399
421
|
}, []);
|
|
400
422
|
|
|
401
423
|
const cancelLeaveFallback = useCallback(() => {
|
|
@@ -545,6 +567,8 @@ export function MessageTimeline({
|
|
|
545
567
|
followRef.current = {
|
|
546
568
|
...followRef.current,
|
|
547
569
|
lastHeight: node.scrollHeight,
|
|
570
|
+
lastClientHeight: node.clientHeight,
|
|
571
|
+
cameraTop: null,
|
|
548
572
|
};
|
|
549
573
|
},
|
|
550
574
|
[cancelLeaveFallback, stopFollow, syncScrollBaseline, writeScrollTop],
|
|
@@ -578,19 +602,35 @@ export function MessageTimeline({
|
|
|
578
602
|
// hard stop → flick. Do NOT tip-ease on the same frame as a real shrink
|
|
579
603
|
// (that fight was the top-of-viewport flicker).
|
|
580
604
|
if (previousHeight > 0 && node.scrollHeight < previousHeight - TIP_FOLLOW_SHRINK_EPS_PX) {
|
|
581
|
-
|
|
605
|
+
let nextTop = tipFollowCompensateShrink(
|
|
582
606
|
lastScrollTopRef.current,
|
|
583
607
|
previousHeight,
|
|
584
608
|
node.scrollHeight,
|
|
585
609
|
node.clientHeight,
|
|
586
610
|
);
|
|
611
|
+
// A chrome/composer dock can land on the same frame as a settle-fold
|
|
612
|
+
// (the "turn blocked" moment). Compensate BOTH in one write — adopting
|
|
613
|
+
// the shrunk clientHeight below without gluing left the chrome height
|
|
614
|
+
// behind as cold tip debt.
|
|
615
|
+
const previousClient = followRef.current.lastClientHeight;
|
|
616
|
+
if (previousClient > 0 && node.clientHeight < previousClient - TIP_FOLLOW_SHRINK_EPS_PX) {
|
|
617
|
+
nextTop = tipFollowCompensateViewportShrink(
|
|
618
|
+
nextTop,
|
|
619
|
+
previousClient,
|
|
620
|
+
node.clientHeight,
|
|
621
|
+
node.scrollHeight,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
587
624
|
writeScrollTop(node, nextTop);
|
|
588
625
|
syncScrollBaseline(node);
|
|
589
626
|
followRef.current = {
|
|
590
627
|
...followRef.current,
|
|
591
628
|
lastHeight: node.scrollHeight,
|
|
629
|
+
lastClientHeight: node.clientHeight,
|
|
592
630
|
running: true,
|
|
593
631
|
lastTs: now,
|
|
632
|
+
// A direct glue write re-based the camera — drop any stale fraction.
|
|
633
|
+
cameraTop: null,
|
|
594
634
|
};
|
|
595
635
|
if (followFrameRef.current === null) {
|
|
596
636
|
followFrameRef.current = requestFrame((frameNow) => {
|
|
@@ -603,7 +643,9 @@ export function MessageTimeline({
|
|
|
603
643
|
}
|
|
604
644
|
return;
|
|
605
645
|
}
|
|
606
|
-
// Sub-eps height noise: adopt height without moving the camera.
|
|
646
|
+
// Sub-eps height noise: adopt height without moving the camera. Do NOT
|
|
647
|
+
// adopt clientHeight here — tipFollowStep (next line) owns that baseline
|
|
648
|
+
// and must still see a same-frame viewport shrink to glue it.
|
|
607
649
|
if (previousHeight > 0 && node.scrollHeight < previousHeight) {
|
|
608
650
|
followRef.current = {
|
|
609
651
|
...followRef.current,
|
|
@@ -1005,20 +1047,29 @@ export function MessageTimeline({
|
|
|
1005
1047
|
}
|
|
1006
1048
|
|
|
1007
1049
|
if (autoFollow && pinnedRef.current && !hasNewer) {
|
|
1008
|
-
// Fold / composer
|
|
1009
|
-
//
|
|
1010
|
-
|
|
1050
|
+
// Fold / composer / SessionChrome: viewport shrink raises maxScroll without
|
|
1051
|
+
// growing content. Must hit tipFollow before we adopt the new clientHeight
|
|
1052
|
+
// (the near-bottom branch used to poison lastClientHeight and skip glue).
|
|
1053
|
+
const previousClient = followRef.current.lastClientHeight;
|
|
1054
|
+
const viewportShrunk =
|
|
1055
|
+
previousClient > 0 && node.clientHeight < previousClient - TIP_FOLLOW_SHRINK_EPS_PX;
|
|
1056
|
+
// Fold / composer content shrink: compensate before baseline sync so
|
|
1057
|
+
// driveFollow still sees the pre-shrink scrollTop (avoid double-subtract).
|
|
1058
|
+
if (heightShrunk || maxFell || viewportShrunk) {
|
|
1011
1059
|
readerIntentArmRef.current = false;
|
|
1012
1060
|
clearPendingReaderLeave();
|
|
1013
1061
|
driveFollow(node);
|
|
1014
1062
|
return;
|
|
1015
1063
|
}
|
|
1016
1064
|
if (programmatic) {
|
|
1065
|
+
// Camera-write echo: consume it, sync the SHELL baselines only. The
|
|
1066
|
+
// camera's growth baselines (lastHeight / lastClientHeight) belong to
|
|
1067
|
+
// tipFollowStep — adopting them here made every echo "consume" growth
|
|
1068
|
+
// that arrived without a commit (motion/Radix height animations of
|
|
1069
|
+
// nested tools, late layout). Echoes fire before rAF callbacks, so the
|
|
1070
|
+
// step saw frameGrowth=0, never heated, and the cold ~42px/s settle
|
|
1071
|
+
// let bursty growth park the tip under the chrome.
|
|
1017
1072
|
syncScrollBaseline(node);
|
|
1018
|
-
followRef.current = {
|
|
1019
|
-
...followRef.current,
|
|
1020
|
-
lastHeight: nextHeight,
|
|
1021
|
-
};
|
|
1022
1073
|
return;
|
|
1023
1074
|
}
|
|
1024
1075
|
syncScrollBaseline(node);
|
|
@@ -1037,6 +1088,8 @@ export function MessageTimeline({
|
|
|
1037
1088
|
}
|
|
1038
1089
|
// Tip grew under a still viewport (no reader-up): ease back to the tip.
|
|
1039
1090
|
// Reader/extension scroll-up in progress: do not yank — scrollend decides.
|
|
1091
|
+
// Near-bottom with tipDebt≈0 stays the quiet path (do not broaden follow
|
|
1092
|
+
// inside PIN_THRESHOLD — that fights small intentional scroll-ups).
|
|
1040
1093
|
if (!nearBottomPinned && readerUp <= TIP_FOLLOW_READER_UP_EPS_PX) {
|
|
1041
1094
|
if (!pendingReaderLeaveRef.current) {
|
|
1042
1095
|
driveFollow(node);
|
|
@@ -1044,12 +1097,11 @@ export function MessageTimeline({
|
|
|
1044
1097
|
} else if (!nearBottomPinned && readerUp > TIP_FOLLOW_READER_UP_EPS_PX) {
|
|
1045
1098
|
pendingReaderLeaveRef.current = true;
|
|
1046
1099
|
scheduleLeaveFallback();
|
|
1047
|
-
} else {
|
|
1048
|
-
followRef.current = {
|
|
1049
|
-
...followRef.current,
|
|
1050
|
-
lastHeight: nextHeight,
|
|
1051
|
-
};
|
|
1052
1100
|
}
|
|
1101
|
+
// Near-bottom reader jiggle: stay quiet, and leave the camera's growth
|
|
1102
|
+
// baselines alone — adopting them here stole the heat of growth the
|
|
1103
|
+
// step had not seen yet (the next driveFollow then settled cold and
|
|
1104
|
+
// parked short inside the pin band).
|
|
1053
1105
|
return;
|
|
1054
1106
|
}
|
|
1055
1107
|
|
|
@@ -1104,258 +1156,260 @@ export function MessageTimeline({
|
|
|
1104
1156
|
<LightboxProvider>
|
|
1105
1157
|
<FoldMemoryProvider value={foldMemoryRef.current}>
|
|
1106
1158
|
<SeenActivityIdsProvider value={seenActivityIdsRef.current}>
|
|
1107
|
-
<
|
|
1108
|
-
<
|
|
1109
|
-
<
|
|
1110
|
-
{
|
|
1159
|
+
<TimelineComputeLabelProvider value={computeLabel ?? null}>
|
|
1160
|
+
<EntranceAnimationProvider value={!bulkRender}>
|
|
1161
|
+
<TooltipProvider delayDuration={400}>
|
|
1162
|
+
<div className={cn("og-root relative flex min-h-0 flex-col", className)}>
|
|
1163
|
+
{/* Pinned: anchoring off so the tip-follow camera owns the motion.
|
|
1111
1164
|
Unpinned: native scroll anchoring holds the reader's place. */}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
) : null}
|
|
1149
|
-
{groups.map(({ group, key }, index) => {
|
|
1150
|
-
const next = groups[index + 1]?.group;
|
|
1151
|
-
const contextCompactionCount =
|
|
1152
|
-
group.kind === "turn"
|
|
1153
|
-
? (group.contextCompactionCount ?? 0)
|
|
1154
|
-
: group.kind === "activity" &&
|
|
1155
|
-
next?.kind === "item" &&
|
|
1156
|
-
next.item.kind === "context-compaction" &&
|
|
1157
|
-
next.item.phase === "compacted"
|
|
1158
|
-
? 1
|
|
1159
|
-
: 0;
|
|
1160
|
-
return (
|
|
1161
|
-
<div key={key} data-og-timeline-group-anchor="" data-og-group-key={key}>
|
|
1162
|
-
<TimelineGroupRenderBoundary
|
|
1163
|
-
resetKeys={[
|
|
1164
|
-
group,
|
|
1165
|
-
renderMessageText,
|
|
1166
|
-
onOpenSession,
|
|
1167
|
-
onMemoryClick,
|
|
1168
|
-
onReconnect,
|
|
1169
|
-
resolveProviderLogo,
|
|
1170
|
-
toolRegistry,
|
|
1171
|
-
turnSummary,
|
|
1172
|
-
]}
|
|
1173
|
-
>
|
|
1174
|
-
<TimelineGroupView
|
|
1175
|
-
group={group}
|
|
1176
|
-
renderMessageText={renderMessageText}
|
|
1177
|
-
onOpenSession={onOpenSession}
|
|
1178
|
-
onMemoryClick={onMemoryClick}
|
|
1179
|
-
onReconnect={onReconnect}
|
|
1180
|
-
resolveProviderLogo={resolveProviderLogo}
|
|
1181
|
-
toolRegistry={toolRegistry}
|
|
1182
|
-
turnSummary={turnSummary}
|
|
1183
|
-
foldLiveCluster={isAgentProgress(next)}
|
|
1184
|
-
trailingAgentText={trailingAgentTextAfterTurn(group, next)}
|
|
1185
|
-
contextCompactionCount={
|
|
1186
|
-
contextCompactionCount > 0 ? contextCompactionCount : undefined
|
|
1187
|
-
}
|
|
1188
|
-
/>
|
|
1189
|
-
</TimelineGroupRenderBoundary>
|
|
1190
|
-
</div>
|
|
1191
|
-
);
|
|
1192
|
-
})}
|
|
1193
|
-
{hasNewer ? (
|
|
1194
|
-
<div
|
|
1195
|
-
ref={bottomSentinelRef}
|
|
1196
|
-
data-og-bottom-sentinel=""
|
|
1197
|
-
data-og-timeline-chrome=""
|
|
1198
|
-
aria-hidden="true"
|
|
1199
|
-
className="h-px w-full"
|
|
1200
|
-
/>
|
|
1201
|
-
) : null}
|
|
1202
|
-
</div>
|
|
1203
|
-
</div>
|
|
1204
|
-
<AnimatePresence>
|
|
1205
|
-
{loadingOlder ||
|
|
1206
|
-
loadingOldest ||
|
|
1207
|
-
(hasOlder && onJumpToStart && olderPrefetchArmed) ? (
|
|
1208
|
-
// Floating over the scroller (not a timeline row) so showing and
|
|
1209
|
-
// hiding it never reflows history under the reader.
|
|
1210
|
-
<motion.div
|
|
1211
|
-
initial={{ opacity: 0, y: -6 }}
|
|
1212
|
-
animate={{ opacity: 1, y: 0 }}
|
|
1213
|
-
exit={{ opacity: 0, y: -6 }}
|
|
1214
|
-
transition={{ duration: 0.15, ease: "easeOut" }}
|
|
1215
|
-
data-og-loading-older=""
|
|
1216
|
-
aria-live="polite"
|
|
1217
|
-
className="absolute inset-x-0 top-3 z-10 flex justify-center gap-2"
|
|
1218
|
-
>
|
|
1219
|
-
{loadingOlder || loadingOldest ? (
|
|
1220
|
-
<span className="pointer-events-none inline-flex items-center rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1 text-xs font-medium shadow-og-md backdrop-blur">
|
|
1221
|
-
<span className="og-shimmer-text">
|
|
1222
|
-
{loadingOldest ? "Jumping to start…" : "Loading earlier activity…"}
|
|
1223
|
-
</span>
|
|
1224
|
-
</span>
|
|
1165
|
+
<div
|
|
1166
|
+
ref={scrollRef}
|
|
1167
|
+
tabIndex={-1}
|
|
1168
|
+
onScroll={onScroll}
|
|
1169
|
+
onScrollEnd={onScrollEnd}
|
|
1170
|
+
onWheel={onWheel}
|
|
1171
|
+
onPointerDown={onPointerDown}
|
|
1172
|
+
onKeyDown={onKeyDown}
|
|
1173
|
+
style={groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined}
|
|
1174
|
+
className={cn(
|
|
1175
|
+
// tabIndex=-1 is programmatic only — never paint a focus ring on
|
|
1176
|
+
// the whole scroller (click + Shift used to flash a blue outline).
|
|
1177
|
+
"min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-6 sm:px-6 outline-none",
|
|
1178
|
+
autoFollow && pinned && !hasNewer
|
|
1179
|
+
? "[overflow-anchor:none]"
|
|
1180
|
+
: "[overflow-anchor:auto]",
|
|
1181
|
+
)}
|
|
1182
|
+
>
|
|
1183
|
+
<div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
|
|
1184
|
+
{groups.length === 0
|
|
1185
|
+
? (emptyState ?? (
|
|
1186
|
+
<p className="py-10 text-center text-sm text-og-fg-subtle">
|
|
1187
|
+
No activity yet.
|
|
1188
|
+
</p>
|
|
1189
|
+
))
|
|
1190
|
+
: null}
|
|
1191
|
+
{hasOlder && olderPrefetchArmed ? (
|
|
1192
|
+
// Overlaid, not a layout row: mounting/unmounting the sentinel
|
|
1193
|
+
// must never shift content (that shift was itself a wobble).
|
|
1194
|
+
<div
|
|
1195
|
+
ref={topSentinelRef}
|
|
1196
|
+
data-og-top-sentinel=""
|
|
1197
|
+
data-og-timeline-chrome=""
|
|
1198
|
+
aria-hidden="true"
|
|
1199
|
+
className="pointer-events-none absolute inset-x-0 top-0 h-px"
|
|
1200
|
+
/>
|
|
1225
1201
|
) : null}
|
|
1226
|
-
{
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1202
|
+
{groups.map(({ group, key }, index) => {
|
|
1203
|
+
const next = groups[index + 1]?.group;
|
|
1204
|
+
const contextCompactionCount =
|
|
1205
|
+
group.kind === "turn"
|
|
1206
|
+
? (group.contextCompactionCount ?? 0)
|
|
1207
|
+
: group.kind === "activity" &&
|
|
1208
|
+
next?.kind === "item" &&
|
|
1209
|
+
next.item.kind === "context-compaction" &&
|
|
1210
|
+
next.item.phase === "compacted"
|
|
1211
|
+
? 1
|
|
1212
|
+
: 0;
|
|
1213
|
+
return (
|
|
1214
|
+
<div key={key} data-og-timeline-group-anchor="" data-og-group-key={key}>
|
|
1215
|
+
<TimelineGroupRenderBoundary
|
|
1216
|
+
resetKeys={[
|
|
1217
|
+
group,
|
|
1218
|
+
renderMessageText,
|
|
1219
|
+
onOpenSession,
|
|
1220
|
+
onMemoryClick,
|
|
1221
|
+
onReconnect,
|
|
1222
|
+
resolveProviderLogo,
|
|
1223
|
+
toolRegistry,
|
|
1224
|
+
turnSummary,
|
|
1225
|
+
]}
|
|
1226
|
+
>
|
|
1227
|
+
<TimelineGroupView
|
|
1228
|
+
group={group}
|
|
1229
|
+
renderMessageText={renderMessageText}
|
|
1230
|
+
onOpenSession={onOpenSession}
|
|
1231
|
+
onMemoryClick={onMemoryClick}
|
|
1232
|
+
onReconnect={onReconnect}
|
|
1233
|
+
resolveProviderLogo={resolveProviderLogo}
|
|
1234
|
+
toolRegistry={toolRegistry}
|
|
1235
|
+
turnSummary={turnSummary}
|
|
1236
|
+
foldLiveCluster={isAgentProgress(next)}
|
|
1237
|
+
trailingAgentText={trailingAgentTextAfterTurn(group, next)}
|
|
1238
|
+
contextCompactionCount={
|
|
1239
|
+
contextCompactionCount > 0 ? contextCompactionCount : undefined
|
|
1245
1240
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1241
|
+
/>
|
|
1242
|
+
</TimelineGroupRenderBoundary>
|
|
1243
|
+
</div>
|
|
1244
|
+
);
|
|
1245
|
+
})}
|
|
1246
|
+
{hasNewer ? (
|
|
1247
|
+
<div
|
|
1248
|
+
ref={bottomSentinelRef}
|
|
1249
|
+
data-og-bottom-sentinel=""
|
|
1250
|
+
data-og-timeline-chrome=""
|
|
1251
|
+
aria-hidden="true"
|
|
1252
|
+
className="h-px w-full"
|
|
1253
|
+
/>
|
|
1254
|
+
) : null}
|
|
1255
|
+
</div>
|
|
1256
|
+
</div>
|
|
1257
|
+
<AnimatePresence>
|
|
1258
|
+
{loadingOlder ||
|
|
1259
|
+
loadingOldest ||
|
|
1260
|
+
(hasOlder && onJumpToStart && olderPrefetchArmed) ? (
|
|
1261
|
+
// Floating over the scroller (not a timeline row) so showing and
|
|
1262
|
+
// hiding it never reflows history under the reader.
|
|
1263
|
+
<motion.div
|
|
1264
|
+
initial={{ opacity: 0, y: -6 }}
|
|
1265
|
+
animate={{ opacity: 1, y: 0 }}
|
|
1266
|
+
exit={{ opacity: 0, y: -6 }}
|
|
1267
|
+
transition={{ duration: 0.15, ease: "easeOut" }}
|
|
1268
|
+
data-og-loading-older=""
|
|
1269
|
+
aria-live="polite"
|
|
1270
|
+
className="absolute inset-x-0 top-3 z-10 flex justify-center gap-2"
|
|
1271
|
+
>
|
|
1272
|
+
{loadingOlder || loadingOldest ? (
|
|
1273
|
+
<span className="pointer-events-none inline-flex items-center rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1 text-xs font-medium shadow-og-md backdrop-blur">
|
|
1274
|
+
<span className="og-shimmer-text">
|
|
1275
|
+
{loadingOldest ? "Jumping to start…" : "Loading earlier activity…"}
|
|
1276
|
+
</span>
|
|
1277
|
+
</span>
|
|
1278
|
+
) : null}
|
|
1279
|
+
{hasOlder && onJumpToStart && !loadingOldest ? (
|
|
1280
|
+
<button
|
|
1281
|
+
type="button"
|
|
1282
|
+
data-og-jump-to-start=""
|
|
1283
|
+
disabled={loadingOlder}
|
|
1284
|
+
onClick={() => {
|
|
1285
|
+
applyPinned(false);
|
|
1286
|
+
pendingJumpToStartRef.current = true;
|
|
1287
|
+
const seq = ++jumpToStartSeqRef.current;
|
|
1288
|
+
const node = scrollRef.current;
|
|
1289
|
+
void Promise.resolve(onJumpToStart()).then(
|
|
1290
|
+
() => {
|
|
1291
|
+
// The commit that swaps in the oldest window consumes
|
|
1292
|
+
// the flag against the new DOM; this write covers the
|
|
1293
|
+
// already-committed order and the no-window-change
|
|
1294
|
+
// case (jumping within the current window).
|
|
1295
|
+
const scroller = scrollRef.current ?? node;
|
|
1296
|
+
if (scroller) {
|
|
1297
|
+
scroller.scrollTop = 0;
|
|
1298
|
+
}
|
|
1299
|
+
// A host may resolve without ever changing the
|
|
1300
|
+
// window (already on the oldest page). Any swap
|
|
1301
|
+
// commit runs its layout effect before the next
|
|
1302
|
+
// frame, so a flag still armed by then is the
|
|
1303
|
+
// no-change case — clear it, or a LATER prepend
|
|
1304
|
+
// would spuriously jump the reader to the top.
|
|
1305
|
+
requestFrame(() => {
|
|
1306
|
+
if (jumpToStartSeqRef.current === seq) {
|
|
1307
|
+
pendingJumpToStartRef.current = false;
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
},
|
|
1311
|
+
() => {
|
|
1253
1312
|
if (jumpToStartSeqRef.current === seq) {
|
|
1254
1313
|
pendingJumpToStartRef.current = false;
|
|
1255
1314
|
}
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
)
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1315
|
+
},
|
|
1316
|
+
);
|
|
1317
|
+
}}
|
|
1318
|
+
className={cn(
|
|
1319
|
+
"inline-flex items-center gap-1.5 rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1.5",
|
|
1320
|
+
"text-xs font-medium text-og-fg shadow-og-md backdrop-blur",
|
|
1321
|
+
"hover:border-og-border-strong disabled:opacity-60",
|
|
1322
|
+
)}
|
|
1323
|
+
>
|
|
1324
|
+
<ArrowUpToLineIcon className="size-3.5" />
|
|
1325
|
+
Jump to start
|
|
1326
|
+
</button>
|
|
1327
|
+
) : null}
|
|
1328
|
+
</motion.div>
|
|
1329
|
+
) : null}
|
|
1330
|
+
</AnimatePresence>
|
|
1331
|
+
<AnimatePresence>
|
|
1332
|
+
{loadingNewer ? (
|
|
1333
|
+
<motion.div
|
|
1334
|
+
initial={{ opacity: 0, y: 6 }}
|
|
1335
|
+
animate={{ opacity: 1, y: 0 }}
|
|
1336
|
+
exit={{ opacity: 0, y: 6 }}
|
|
1337
|
+
transition={{ duration: 0.15, ease: "easeOut" }}
|
|
1338
|
+
data-og-loading-newer=""
|
|
1339
|
+
aria-live="polite"
|
|
1340
|
+
className="pointer-events-none absolute inset-x-0 bottom-14 z-10 flex justify-center"
|
|
1341
|
+
>
|
|
1342
|
+
<span className="inline-flex items-center rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1 text-xs font-medium shadow-og-md backdrop-blur">
|
|
1343
|
+
<span className="og-shimmer-text">Loading later activity…</span>
|
|
1344
|
+
</span>
|
|
1345
|
+
</motion.div>
|
|
1346
|
+
) : null}
|
|
1347
|
+
</AnimatePresence>
|
|
1348
|
+
<AnimatePresence>
|
|
1349
|
+
{((!pinned && autoFollow) || hasNewer) && autoFollow ? (
|
|
1350
|
+
<motion.button
|
|
1351
|
+
type="button"
|
|
1352
|
+
initial={{ opacity: 0, y: 8 }}
|
|
1353
|
+
animate={{ opacity: 1, y: 0 }}
|
|
1354
|
+
exit={{ opacity: 0, y: 8 }}
|
|
1355
|
+
transition={{ duration: 0.15, ease: "easeOut" }}
|
|
1356
|
+
onClick={() => {
|
|
1357
|
+
if (hasNewer) {
|
|
1358
|
+
// Do not pin against the current history page — its bottom
|
|
1359
|
+
// is not the tip. The pin + snap run when the tip window
|
|
1360
|
+
// actually lands (`hasNewer` flips false).
|
|
1361
|
+
wantPinRef.current = true;
|
|
1362
|
+
const node = scrollRef.current;
|
|
1363
|
+
if (onJumpToLatest) {
|
|
1364
|
+
void Promise.resolve(onJumpToLatest()).then(
|
|
1365
|
+
() => {
|
|
1366
|
+
// Covers a host that flipped hasNewer before
|
|
1367
|
+
// resolving; otherwise the tip-window commit
|
|
1368
|
+
// consumes the flag.
|
|
1369
|
+
const current = scrollRef.current;
|
|
1370
|
+
if (current && wantPinRef.current && !hasNewerRef.current) {
|
|
1371
|
+
wantPinRef.current = false;
|
|
1372
|
+
applyPinned(true);
|
|
1373
|
+
snapToBottom(current);
|
|
1374
|
+
}
|
|
1375
|
+
},
|
|
1376
|
+
() => {
|
|
1377
|
+
// The tip reload failed (ordinary network error):
|
|
1378
|
+
// an armed latch would fire a surprise snap when
|
|
1379
|
+
// the reader later pages to the tip themselves.
|
|
1318
1380
|
wantPinRef.current = false;
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
// window so the newer sentinel can page forward; the
|
|
1333
|
-
// latch pins if the tip window eventually lands.
|
|
1381
|
+
},
|
|
1382
|
+
);
|
|
1383
|
+
} else if (node) {
|
|
1384
|
+
// No tip reload available: jump within the in-memory
|
|
1385
|
+
// window so the newer sentinel can page forward; the
|
|
1386
|
+
// latch pins if the tip window eventually lands.
|
|
1387
|
+
snapToBottom(node);
|
|
1388
|
+
}
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
const node = scrollRef.current;
|
|
1392
|
+
if (node) {
|
|
1393
|
+
applyPinned(true);
|
|
1334
1394
|
snapToBottom(node);
|
|
1335
1395
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
</motion.button>
|
|
1354
|
-
) : null}
|
|
1355
|
-
</AnimatePresence>
|
|
1356
|
-
</div>
|
|
1357
|
-
</TooltipProvider>
|
|
1358
|
-
</EntranceAnimationProvider>
|
|
1396
|
+
}}
|
|
1397
|
+
className={cn(
|
|
1398
|
+
"absolute bottom-4 left-1/2 -translate-x-1/2",
|
|
1399
|
+
"inline-flex items-center gap-1.5 rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1.5",
|
|
1400
|
+
"text-xs font-medium text-og-fg shadow-og-md backdrop-blur",
|
|
1401
|
+
"hover:border-og-border-strong",
|
|
1402
|
+
)}
|
|
1403
|
+
>
|
|
1404
|
+
<ArrowDownIcon className="size-3.5" />
|
|
1405
|
+
Jump to latest
|
|
1406
|
+
</motion.button>
|
|
1407
|
+
) : null}
|
|
1408
|
+
</AnimatePresence>
|
|
1409
|
+
</div>
|
|
1410
|
+
</TooltipProvider>
|
|
1411
|
+
</EntranceAnimationProvider>
|
|
1412
|
+
</TimelineComputeLabelProvider>
|
|
1359
1413
|
</SeenActivityIdsProvider>
|
|
1360
1414
|
</FoldMemoryProvider>
|
|
1361
1415
|
</LightboxProvider>
|
|
@@ -2404,52 +2458,49 @@ function GoalRow({ item }: { item: GoalItem }) {
|
|
|
2404
2458
|
);
|
|
2405
2459
|
}
|
|
2406
2460
|
|
|
2407
|
-
const MACHINE_INPUT_META: Record<MachineInputBatchItem["members"][number]["kind"], string> = {
|
|
2408
|
-
scheduled_occurrence: "Scheduled update",
|
|
2409
|
-
goal_continuation: "Goal continued",
|
|
2410
|
-
agent_message: "Agent update",
|
|
2411
|
-
agent_steer_instruction: "Agent direction",
|
|
2412
|
-
child_terminal_result: "Agent finished",
|
|
2413
|
-
};
|
|
2414
|
-
|
|
2415
2461
|
function MachineInputBatchRow({ item }: { item: MachineInputBatchItem }) {
|
|
2416
2462
|
const enter = useEntranceAnimation();
|
|
2417
|
-
const
|
|
2463
|
+
const label = machineInputBatchLabel(item.members);
|
|
2464
|
+
const single = item.members.length === 1 ? item.members[0]! : null;
|
|
2465
|
+
const singleSummary = single ? cleanMachineInputSummary(single.summary) : "";
|
|
2466
|
+
const showCollapsedSummary =
|
|
2467
|
+
single != null && machineInputSummaryIsUseful(single.kind, singleSummary);
|
|
2468
|
+
|
|
2418
2469
|
return (
|
|
2419
|
-
<div
|
|
2420
|
-
className=
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2470
|
+
<div className={cn(enter && "animate-og-enter", "flex flex-col items-center gap-1.5")}>
|
|
2471
|
+
<details className="group w-full max-w-full" data-og-machine-input-batch="">
|
|
2472
|
+
<summary className="flex cursor-pointer list-none justify-center [&::-webkit-details-marker]:hidden">
|
|
2473
|
+
<span
|
|
2474
|
+
className={cn(
|
|
2475
|
+
"inline-flex max-w-full items-center gap-1.5 rounded-full border px-3 py-1 text-og-sm",
|
|
2476
|
+
NEUTRAL_PILL,
|
|
2477
|
+
)}
|
|
2478
|
+
>
|
|
2479
|
+
<ChevronRightIcon
|
|
2480
|
+
aria-hidden
|
|
2481
|
+
className="size-3.5 shrink-0 transition-transform group-open:rotate-90"
|
|
2482
|
+
/>
|
|
2483
|
+
<span className="truncate">{label}</span>
|
|
2484
|
+
</span>
|
|
2485
|
+
</summary>
|
|
2486
|
+
<div className="mx-auto mt-2 w-full max-w-lg space-y-2 border-t border-og-border/50 pt-2">
|
|
2487
|
+
{item.members.map((member) => (
|
|
2488
|
+
<MachineInputRow key={member.id} member={member} />
|
|
2489
|
+
))}
|
|
2428
2490
|
</div>
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
{item.members.length > visible.length && (
|
|
2436
|
-
<details className="mt-2 pl-8 text-xs text-og-fg-muted">
|
|
2437
|
-
<summary className="cursor-pointer select-none hover:text-og-fg">
|
|
2438
|
-
Show {item.members.length - visible.length} more
|
|
2439
|
-
</summary>
|
|
2440
|
-
<div className="mt-2 space-y-2">
|
|
2441
|
-
{item.members.slice(visible.length).map((member) => (
|
|
2442
|
-
<MachineInputRow key={member.id} member={member} />
|
|
2443
|
-
))}
|
|
2444
|
-
</div>
|
|
2445
|
-
</details>
|
|
2446
|
-
)}
|
|
2491
|
+
</details>
|
|
2492
|
+
{showCollapsedSummary ? (
|
|
2493
|
+
<p className="max-w-lg px-3 text-center text-og-xs leading-4 text-og-fg-subtle">
|
|
2494
|
+
{truncate(singleSummary, 160)}
|
|
2495
|
+
</p>
|
|
2496
|
+
) : null}
|
|
2447
2497
|
</div>
|
|
2448
2498
|
);
|
|
2449
2499
|
}
|
|
2450
2500
|
|
|
2451
2501
|
function MachineInputRow({ member }: { member: MachineInputBatchItem["members"][number] }) {
|
|
2452
2502
|
const source = readableMachineInputSource(member.sourceId);
|
|
2503
|
+
const summary = cleanMachineInputSummary(member.summary);
|
|
2453
2504
|
return (
|
|
2454
2505
|
<div className="flex min-w-0 items-start gap-2.5">
|
|
2455
2506
|
<span className="mt-2 size-1.5 shrink-0 rounded-full bg-og-fg-subtle" aria-hidden />
|
|
@@ -2458,27 +2509,16 @@ function MachineInputRow({ member }: { member: MachineInputBatchItem["members"][
|
|
|
2458
2509
|
{MACHINE_INPUT_META[member.kind]}
|
|
2459
2510
|
</span>
|
|
2460
2511
|
{source && <span className="ml-1.5 text-xs text-og-fg-subtle">from {source}</span>}
|
|
2461
|
-
{
|
|
2512
|
+
{summary ? (
|
|
2462
2513
|
<p className="mt-0.5 whitespace-pre-wrap break-words text-sm leading-5 text-og-fg">
|
|
2463
|
-
{truncate(
|
|
2514
|
+
{truncate(summary, 320)}
|
|
2464
2515
|
</p>
|
|
2465
|
-
)}
|
|
2516
|
+
) : null}
|
|
2466
2517
|
</div>
|
|
2467
2518
|
</div>
|
|
2468
2519
|
);
|
|
2469
2520
|
}
|
|
2470
2521
|
|
|
2471
|
-
function readableMachineInputSource(sourceId: string): string | null {
|
|
2472
|
-
const value = sourceId.trim();
|
|
2473
|
-
if (!value || /^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(value)) return null;
|
|
2474
|
-
if (/^(goal|schedule|system):/i.test(value)) return null;
|
|
2475
|
-
return value.replaceAll("_", " ");
|
|
2476
|
-
}
|
|
2477
|
-
|
|
2478
|
-
function cleanMachineInputSummary(summary: string): string {
|
|
2479
|
-
return summary.replace(/^\[[A-Z][A-Z _-]*(?:\s+\d+\/\d+)?\]\s*/, "").trim();
|
|
2480
|
-
}
|
|
2481
|
-
|
|
2482
2522
|
function NoticeRow({ item }: { item: NoticeItem }) {
|
|
2483
2523
|
const enter = useEntranceAnimation();
|
|
2484
2524
|
const tone =
|