@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.
Files changed (71) hide show
  1. package/README.md +88 -2
  2. package/dist/{chunk-7CJOAW3V.js → chunk-EB67J347.js} +1 -135
  3. package/dist/chunk-EB67J347.js.map +1 -0
  4. package/dist/chunk-EKZH6IDG.js +141 -0
  5. package/dist/chunk-EKZH6IDG.js.map +1 -0
  6. package/dist/{chunk-FSPDND3P.js → chunk-JALF5FI3.js} +8 -6
  7. package/dist/{chunk-FSPDND3P.js.map → chunk-JALF5FI3.js.map} +1 -1
  8. package/dist/{chunk-UCZPNXV3.js → chunk-KR2SK5GJ.js} +1292 -711
  9. package/dist/chunk-KR2SK5GJ.js.map +1 -0
  10. package/dist/{chunk-IP22SLO6.js → chunk-OMCFRWHL.js} +28 -441
  11. package/dist/chunk-OMCFRWHL.js.map +1 -0
  12. package/dist/{chunk-HJ4OQVGW.js → chunk-Q2NCKWTK.js} +10 -5
  13. package/dist/chunk-Q2NCKWTK.js.map +1 -0
  14. package/dist/{chunk-ALA3UGWB.js → chunk-SXIQK54Z.js} +8 -6
  15. package/dist/{chunk-ALA3UGWB.js.map → chunk-SXIQK54Z.js.map} +1 -1
  16. package/dist/chunk-WZT5G5OR.js +433 -0
  17. package/dist/chunk-WZT5G5OR.js.map +1 -0
  18. package/dist/client.d.ts +2 -0
  19. package/dist/components/chat-composer.d.ts +5 -1
  20. package/dist/components/human-input-form.d.ts +16 -6
  21. package/dist/components/human-input-surface.d.ts +19 -0
  22. package/dist/components/machine-input-display.d.ts +12 -0
  23. package/dist/components/message-timeline.d.ts +6 -1
  24. package/dist/components/session-chrome.d.ts +1 -0
  25. package/dist/components/tip-follow.d.ts +30 -0
  26. package/dist/composer.js +14 -11
  27. package/dist/hooks/use-composer.d.ts +1 -1
  28. package/dist/index.d.ts +4 -0
  29. package/dist/index.js +2246 -2236
  30. package/dist/index.js.map +1 -1
  31. package/dist/machines.js +4 -3
  32. package/dist/realtime/dropdown-menu.d.ts +16 -0
  33. package/dist/realtime/realtime-control.d.ts +126 -0
  34. package/dist/realtime.d.ts +11 -0
  35. package/dist/realtime.js +1023 -0
  36. package/dist/realtime.js.map +1 -0
  37. package/dist/session-context.d.ts +7 -1
  38. package/dist/session-ui.d.ts +2 -0
  39. package/dist/session-ui.js +5 -3
  40. package/dist/session.js +4 -3
  41. package/dist/timeline/compute-label.d.ts +2 -0
  42. package/dist/timeline/index.d.ts +1 -0
  43. package/package.json +7 -2
  44. package/src/client.ts +13 -0
  45. package/src/components/chat-composer.tsx +11 -2
  46. package/src/components/composer-transcription-control.tsx +2 -2
  47. package/src/components/composer.tsx +14 -5
  48. package/src/components/human-input-form.tsx +506 -186
  49. package/src/components/human-input-surface.tsx +80 -0
  50. package/src/components/machine-input-display.ts +83 -0
  51. package/src/components/message-timeline.tsx +348 -308
  52. package/src/components/model-policy-picker.tsx +8 -3
  53. package/src/components/sandbox-files.tsx +1 -1
  54. package/src/components/session-chrome.tsx +3 -1
  55. package/src/components/tip-follow.ts +127 -6
  56. package/src/hooks/use-composer.ts +16 -13
  57. package/src/index.ts +4 -0
  58. package/src/realtime/dropdown-menu.tsx +123 -0
  59. package/src/realtime/realtime-control.tsx +1178 -0
  60. package/src/realtime.ts +27 -0
  61. package/src/session-context.ts +16 -0
  62. package/src/session-ui.ts +2 -0
  63. package/src/timeline/compute-label.tsx +18 -0
  64. package/src/timeline/index.ts +3 -0
  65. package/src/timeline/tool-renderers.tsx +232 -43
  66. package/styles/index.css +61 -0
  67. package/styles/tokens.css +2 -0
  68. package/dist/chunk-7CJOAW3V.js.map +0 -1
  69. package/dist/chunk-HJ4OQVGW.js.map +0 -1
  70. package/dist/chunk-IP22SLO6.js.map +0 -1
  71. 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
- if (node.scrollTop === next) {
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
- const nextTop = tipFollowCompensateShrink(
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 shrink: compensate before baseline sync so driveFollow
1009
- // still sees the pre-shrink scrollTop (avoid double-subtract after clamp).
1010
- if (heightShrunk || maxFell) {
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
- <EntranceAnimationProvider value={!bulkRender}>
1108
- <TooltipProvider delayDuration={400}>
1109
- <div className={cn("og-root relative flex min-h-0 flex-col", className)}>
1110
- {/* Pinned: anchoring off so the tip-follow camera owns the motion.
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
- <div
1113
- ref={scrollRef}
1114
- tabIndex={-1}
1115
- onScroll={onScroll}
1116
- onScrollEnd={onScrollEnd}
1117
- onWheel={onWheel}
1118
- onPointerDown={onPointerDown}
1119
- onKeyDown={onKeyDown}
1120
- style={groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined}
1121
- className={cn(
1122
- // tabIndex=-1 is programmatic only — never paint a focus ring on
1123
- // the whole scroller (click + Shift used to flash a blue outline).
1124
- "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-6 sm:px-6 outline-none",
1125
- autoFollow && pinned && !hasNewer
1126
- ? "[overflow-anchor:none]"
1127
- : "[overflow-anchor:auto]",
1128
- )}
1129
- >
1130
- <div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
1131
- {groups.length === 0
1132
- ? (emptyState ?? (
1133
- <p className="py-10 text-center text-sm text-og-fg-subtle">
1134
- No activity yet.
1135
- </p>
1136
- ))
1137
- : null}
1138
- {hasOlder && olderPrefetchArmed ? (
1139
- // Overlaid, not a layout row: mounting/unmounting the sentinel
1140
- // must never shift content (that shift was itself a wobble).
1141
- <div
1142
- ref={topSentinelRef}
1143
- data-og-top-sentinel=""
1144
- data-og-timeline-chrome=""
1145
- aria-hidden="true"
1146
- className="pointer-events-none absolute inset-x-0 top-0 h-px"
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
- {hasOlder && onJumpToStart && !loadingOldest ? (
1227
- <button
1228
- type="button"
1229
- data-og-jump-to-start=""
1230
- disabled={loadingOlder}
1231
- onClick={() => {
1232
- applyPinned(false);
1233
- pendingJumpToStartRef.current = true;
1234
- const seq = ++jumpToStartSeqRef.current;
1235
- const node = scrollRef.current;
1236
- void Promise.resolve(onJumpToStart()).then(
1237
- () => {
1238
- // The commit that swaps in the oldest window consumes
1239
- // the flag against the new DOM; this write covers the
1240
- // already-committed order and the no-window-change
1241
- // case (jumping within the current window).
1242
- const scroller = scrollRef.current ?? node;
1243
- if (scroller) {
1244
- scroller.scrollTop = 0;
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
- // A host may resolve without ever changing the
1247
- // window (already on the oldest page). Any swap
1248
- // commit runs its layout effect before the next
1249
- // frame, so a flag still armed by then is the
1250
- // no-change case — clear it, or a LATER prepend
1251
- // would spuriously jump the reader to the top.
1252
- requestFrame(() => {
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
- if (jumpToStartSeqRef.current === seq) {
1260
- pendingJumpToStartRef.current = false;
1261
- }
1262
- },
1263
- );
1264
- }}
1265
- className={cn(
1266
- "inline-flex items-center gap-1.5 rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1.5",
1267
- "text-xs font-medium text-og-fg shadow-og-md backdrop-blur",
1268
- "hover:border-og-border-strong disabled:opacity-60",
1269
- )}
1270
- >
1271
- <ArrowUpToLineIcon className="size-3.5" />
1272
- Jump to start
1273
- </button>
1274
- ) : null}
1275
- </motion.div>
1276
- ) : null}
1277
- </AnimatePresence>
1278
- <AnimatePresence>
1279
- {loadingNewer ? (
1280
- <motion.div
1281
- initial={{ opacity: 0, y: 6 }}
1282
- animate={{ opacity: 1, y: 0 }}
1283
- exit={{ opacity: 0, y: 6 }}
1284
- transition={{ duration: 0.15, ease: "easeOut" }}
1285
- data-og-loading-newer=""
1286
- aria-live="polite"
1287
- className="pointer-events-none absolute inset-x-0 bottom-14 z-10 flex justify-center"
1288
- >
1289
- <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">
1290
- <span className="og-shimmer-text">Loading later activity…</span>
1291
- </span>
1292
- </motion.div>
1293
- ) : null}
1294
- </AnimatePresence>
1295
- <AnimatePresence>
1296
- {((!pinned && autoFollow) || hasNewer) && autoFollow ? (
1297
- <motion.button
1298
- type="button"
1299
- initial={{ opacity: 0, y: 8 }}
1300
- animate={{ opacity: 1, y: 0 }}
1301
- exit={{ opacity: 0, y: 8 }}
1302
- transition={{ duration: 0.15, ease: "easeOut" }}
1303
- onClick={() => {
1304
- if (hasNewer) {
1305
- // Do not pin against the current history page — its bottom
1306
- // is not the tip. The pin + snap run when the tip window
1307
- // actually lands (`hasNewer` flips false).
1308
- wantPinRef.current = true;
1309
- const node = scrollRef.current;
1310
- if (onJumpToLatest) {
1311
- void Promise.resolve(onJumpToLatest()).then(
1312
- () => {
1313
- // Covers a host that flipped hasNewer before
1314
- // resolving; otherwise the tip-window commit
1315
- // consumes the flag.
1316
- const current = scrollRef.current;
1317
- if (current && wantPinRef.current && !hasNewerRef.current) {
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
- applyPinned(true);
1320
- snapToBottom(current);
1321
- }
1322
- },
1323
- () => {
1324
- // The tip reload failed (ordinary network error):
1325
- // an armed latch would fire a surprise snap when
1326
- // the reader later pages to the tip themselves.
1327
- wantPinRef.current = false;
1328
- },
1329
- );
1330
- } else if (node) {
1331
- // No tip reload available: jump within the in-memory
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
- return;
1337
- }
1338
- const node = scrollRef.current;
1339
- if (node) {
1340
- applyPinned(true);
1341
- snapToBottom(node);
1342
- }
1343
- }}
1344
- className={cn(
1345
- "absolute bottom-4 left-1/2 -translate-x-1/2",
1346
- "inline-flex items-center gap-1.5 rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1.5",
1347
- "text-xs font-medium text-og-fg shadow-og-md backdrop-blur",
1348
- "hover:border-og-border-strong",
1349
- )}
1350
- >
1351
- <ArrowDownIcon className="size-3.5" />
1352
- Jump to latest
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 visible = item.members.slice(0, 3);
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={cn(
2421
- enter && "animate-og-enter",
2422
- "rounded-og-md border border-og-border/70 bg-og-surface-1/55 px-3 py-2.5",
2423
- )}
2424
- >
2425
- {item.members.length > 1 && (
2426
- <div className="mb-2 text-xs font-medium text-og-fg-subtle">
2427
- {item.members.length} updates joined this turn
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
- <div className="space-y-2">
2431
- {visible.map((member) => (
2432
- <MachineInputRow key={member.id} member={member} />
2433
- ))}
2434
- </div>
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
- {member.summary && (
2512
+ {summary ? (
2462
2513
  <p className="mt-0.5 whitespace-pre-wrap break-words text-sm leading-5 text-og-fg">
2463
- {truncate(cleanMachineInputSummary(member.summary), 320)}
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 =