@opengeni/react 1.0.1 → 2.3.0-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.
Files changed (174) hide show
  1. package/README.md +33 -10
  2. package/dist/artifacts-spreadsheet.js +1 -1
  3. package/dist/artifacts.js +1 -1
  4. package/dist/browser-viewer-GXUICIAG.js +14 -0
  5. package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
  6. package/dist/chunk-4MDLEHXL.js.map +1 -0
  7. package/dist/chunk-5YK2ZDIG.js +116 -0
  8. package/dist/chunk-5YK2ZDIG.js.map +1 -0
  9. package/dist/chunk-H5YGYNXS.js +329 -0
  10. package/dist/chunk-H5YGYNXS.js.map +1 -0
  11. package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
  12. package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
  13. package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
  14. package/dist/chunk-JOAVA2TW.js.map +1 -0
  15. package/dist/{chunk-Y7OEDUJ5.js → chunk-LS4POUTY.js} +1057 -426
  16. package/dist/chunk-LS4POUTY.js.map +1 -0
  17. package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
  18. package/dist/chunk-LTCHJ4MS.js.map +1 -0
  19. package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
  20. package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
  21. package/dist/chunk-QGBJ6GHG.js +2300 -0
  22. package/dist/chunk-QGBJ6GHG.js.map +1 -0
  23. package/dist/{chunk-6KLX2RH5.js → chunk-RSXJN73N.js} +365 -21
  24. package/dist/chunk-RSXJN73N.js.map +1 -0
  25. package/dist/{chunk-B6WPZSTT.js → chunk-SE4W4WQ3.js} +78 -16
  26. package/dist/chunk-SE4W4WQ3.js.map +1 -0
  27. package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
  28. package/dist/chunk-W6NFJXSA.js.map +1 -0
  29. package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
  30. package/dist/chunk-X4A66EWZ.js.map +1 -0
  31. package/dist/chunk-X4JAFD5R.js +3907 -0
  32. package/dist/chunk-X4JAFD5R.js.map +1 -0
  33. package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
  34. package/dist/chunk-XXWH2JLC.js.map +1 -0
  35. package/dist/client.d.ts +2 -2
  36. package/dist/components/browser-viewer.d.ts +7 -1
  37. package/dist/components/computer-viewer.d.ts +4 -1
  38. package/dist/components/enrollment-consent.d.ts +6 -3
  39. package/dist/components/file-browser.d.ts +7 -1
  40. package/dist/components/human-input-form.d.ts +1 -1
  41. package/dist/components/human-input-surface.d.ts +1 -1
  42. package/dist/components/machine-card.d.ts +4 -1
  43. package/dist/components/machines/machine-detail.d.ts +4 -1
  44. package/dist/components/machines/machine-operation-policy.d.ts +11 -0
  45. package/dist/components/machines-dashboard.d.ts +3 -1
  46. package/dist/components/markdown.d.ts +20 -1
  47. package/dist/components/message-timeline.d.ts +1 -1
  48. package/dist/components/model-policy-picker.d.ts +2 -0
  49. package/dist/components/pierre-file.d.ts +1 -1
  50. package/dist/components/sandbox-files.d.ts +8 -2
  51. package/dist/components/sandbox-workspace.d.ts +33 -3
  52. package/dist/components/session-chrome.d.ts +15 -3
  53. package/dist/components/workspace-dock.d.ts +6 -2
  54. package/dist/composer.js +5 -5
  55. package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
  56. package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
  57. package/dist/hooks/channel-cache.d.ts +23 -0
  58. package/dist/hooks/shared-machines-list.d.ts +14 -0
  59. package/dist/hooks/use-attached-browsers.d.ts +2 -1
  60. package/dist/hooks/use-channels.d.ts +1 -0
  61. package/dist/hooks/use-composer.d.ts +37 -9
  62. package/dist/hooks/use-machine-chip.d.ts +3 -3
  63. package/dist/hooks/use-machines.d.ts +19 -1
  64. package/dist/hooks/use-sandbox-files.d.ts +1 -0
  65. package/dist/hooks/use-session-background-commands.d.ts +14 -0
  66. package/dist/hooks/use-turn-queue.d.ts +12 -0
  67. package/dist/hooks/use-workspace-sessions.d.ts +1 -0
  68. package/dist/index.d.ts +7 -5
  69. package/dist/index.js +1096 -623
  70. package/dist/index.js.map +1 -1
  71. package/dist/interaction.js +29 -181
  72. package/dist/interaction.js.map +1 -1
  73. package/dist/lib/interaction-errors.d.ts +3 -0
  74. package/dist/machines.d.ts +4 -2
  75. package/dist/machines.js +10 -2
  76. package/dist/model-policy.d.ts +1 -1
  77. package/dist/model-policy.js +1 -1
  78. package/dist/platform-activity-row-BJHUHXAT.js +169 -0
  79. package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
  80. package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
  81. package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
  82. package/dist/realtime/realtime-control.d.ts +6 -3
  83. package/dist/realtime.js +36 -14
  84. package/dist/realtime.js.map +1 -1
  85. package/dist/session-ui.d.ts +1 -1
  86. package/dist/session-ui.js +6 -2
  87. package/dist/session.d.ts +2 -2
  88. package/dist/session.js +4 -4
  89. package/dist/timeline/index.d.ts +1 -1
  90. package/dist/timeline/platform-activity-row.d.ts +15 -0
  91. package/dist/timeline/screenshot-lightbox.d.ts +1 -1
  92. package/dist/timeline/shared.d.ts +4 -2
  93. package/dist/timeline/types.d.ts +50 -5
  94. package/dist/types/machines.d.ts +2 -2
  95. package/package.json +3 -3
  96. package/src/client.ts +12 -1
  97. package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
  98. package/src/components/browser-viewer.tsx +361 -58
  99. package/src/components/chat-composer.tsx +1 -1
  100. package/src/components/composer.tsx +6 -6
  101. package/src/components/computer-viewer.tsx +368 -115
  102. package/src/components/enrollment-consent.tsx +64 -6
  103. package/src/components/file-browser.tsx +117 -2
  104. package/src/components/human-input-form.tsx +103 -59
  105. package/src/components/human-input-surface.tsx +1 -1
  106. package/src/components/machine-card.tsx +111 -0
  107. package/src/components/machine-input-display.ts +15 -0
  108. package/src/components/machines/machine-detail.tsx +47 -0
  109. package/src/components/machines/machine-operation-policy.tsx +179 -0
  110. package/src/components/machines-dashboard.tsx +6 -0
  111. package/src/components/markdown.tsx +174 -20
  112. package/src/components/message-timeline.tsx +215 -18
  113. package/src/components/model-policy-picker.tsx +9 -17
  114. package/src/components/pierre-file.tsx +1 -1
  115. package/src/components/queue-surface-implementation.tsx +82 -16
  116. package/src/components/sandbox-files.tsx +135 -19
  117. package/src/components/sandbox-workspace.tsx +204 -92
  118. package/src/components/session-chrome.tsx +415 -208
  119. package/src/components/session-status.tsx +1 -1
  120. package/src/components/user-message-body.tsx +168 -48
  121. package/src/components/workspace-dock.tsx +141 -175
  122. package/src/hooks/channel-cache.ts +104 -0
  123. package/src/hooks/shared-machines-list.ts +239 -0
  124. package/src/hooks/use-attached-browsers.ts +5 -1
  125. package/src/hooks/use-browser-frame-stream.ts +6 -1
  126. package/src/hooks/use-browser-session.ts +6 -4
  127. package/src/hooks/use-browser-sessions.ts +7 -1
  128. package/src/hooks/use-channels.ts +32 -10
  129. package/src/hooks/use-composer.ts +760 -291
  130. package/src/hooks/use-computer-frame-stream.ts +14 -9
  131. package/src/hooks/use-computer-session.ts +16 -14
  132. package/src/hooks/use-computer-sessions.ts +8 -1
  133. package/src/hooks/use-last-started-turn-policy.ts +1 -1
  134. package/src/hooks/use-machine-chip.ts +23 -6
  135. package/src/hooks/use-machines.ts +104 -6
  136. package/src/hooks/use-sandbox-files.ts +195 -140
  137. package/src/hooks/use-sandbox-git.ts +58 -29
  138. package/src/hooks/use-session-background-commands.ts +56 -0
  139. package/src/hooks/use-session-capabilities.ts +18 -0
  140. package/src/hooks/use-turn-queue.ts +111 -12
  141. package/src/hooks/use-workspace-sessions.ts +5 -1
  142. package/src/index.ts +26 -4
  143. package/src/lib/interaction-errors.ts +18 -0
  144. package/src/machines.ts +9 -1
  145. package/src/model-policy.ts +10 -2
  146. package/src/realtime/realtime-control.tsx +53 -17
  147. package/src/session-context.ts +1 -0
  148. package/src/session-ui.ts +6 -1
  149. package/src/session.ts +9 -1
  150. package/src/timeline/activity-rail.tsx +20 -75
  151. package/src/timeline/index.ts +4 -0
  152. package/src/timeline/platform-activity-row.tsx +201 -0
  153. package/src/timeline/projection.ts +529 -25
  154. package/src/timeline/screenshot-lightbox.tsx +48 -14
  155. package/src/timeline/shared.tsx +8 -2
  156. package/src/timeline/tool-renderers.tsx +104 -6
  157. package/src/timeline/turn-summary.tsx +6 -1
  158. package/src/timeline/types.ts +77 -2
  159. package/src/types/machines.ts +10 -0
  160. package/styles/compiled.css +115 -104
  161. package/dist/chunk-5UQPNLIO.js.map +0 -1
  162. package/dist/chunk-6KLX2RH5.js.map +0 -1
  163. package/dist/chunk-B6WPZSTT.js.map +0 -1
  164. package/dist/chunk-HA46CYQK.js.map +0 -1
  165. package/dist/chunk-IGC6A2ZI.js.map +0 -1
  166. package/dist/chunk-LLOLUHVG.js +0 -5937
  167. package/dist/chunk-LLOLUHVG.js.map +0 -1
  168. package/dist/chunk-MKPIAGVL.js.map +0 -1
  169. package/dist/chunk-R3AZOAB5.js.map +0 -1
  170. package/dist/chunk-XBZWOM2K.js.map +0 -1
  171. package/dist/chunk-Y7OEDUJ5.js.map +0 -1
  172. package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
  173. package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
  174. /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
@@ -3,13 +3,18 @@ import {
3
3
  isValidElement,
4
4
  memo,
5
5
  useEffect,
6
+ useMemo,
6
7
  useReducer,
7
8
  useRef,
8
9
  useState,
9
10
  type ComponentPropsWithoutRef,
10
11
  type ReactNode,
11
12
  } from "react";
12
- import ReactMarkdown, { type Components } from "react-markdown";
13
+ import ReactMarkdown, {
14
+ defaultUrlTransform,
15
+ type Components,
16
+ type UrlTransform,
17
+ } from "react-markdown";
13
18
  import remarkGfm from "remark-gfm";
14
19
  import { cn } from "../lib/cn";
15
20
  import { tableElementToTsv } from "../lib/clipboard";
@@ -46,11 +51,22 @@ export type MarkdownProps = {
46
51
  * transition — that blinked the whole document).
47
52
  */
48
53
  streaming?: boolean | undefined;
54
+ /**
55
+ * Open a session-local file link in the host's Files surface. The path is the
56
+ * exact decoded `sandbox:` payload; `line`, when present, is 1-based.
57
+ */
58
+ onSandboxFile?: ((path: string, line?: number) => void | Promise<void>) | undefined;
59
+ };
60
+
61
+ /** Exact target path plus optional 1-based line from a markdown href. */
62
+ export type SandboxFileLocation = {
63
+ path: string;
64
+ line: number | null;
49
65
  };
50
66
 
51
67
  /* --- element renderers (themed to og-* tokens) ------------------------------ */
52
68
 
53
- const components: Components = {
69
+ const baseComponents: Components = {
54
70
  h1: ({ children, ...props }) => (
55
71
  <h1
56
72
  className="mt-5 mb-2.5 text-xl font-semibold tracking-tight text-og-fg first:mt-0"
@@ -95,16 +111,6 @@ const components: Components = {
95
111
  {children}
96
112
  </em>
97
113
  ),
98
- a: ({ children, ...props }) => (
99
- <a
100
- className="break-words font-medium text-og-accent-strong underline-offset-2 hover:underline"
101
- target="_blank"
102
- rel="noreferrer noopener"
103
- {...props}
104
- >
105
- {children}
106
- </a>
107
- ),
108
114
  ul: ({ children, ...props }) => (
109
115
  <ul
110
116
  className="my-2.5 ml-5 flex list-disc flex-col gap-1 marker:text-og-fg-subtle first:mt-0 last:mb-0"
@@ -184,15 +190,161 @@ const components: Components = {
184
190
  {children}
185
191
  </td>
186
192
  ),
187
- img: ({ alt, ...props }) => (
188
- <img
189
- alt={alt ?? ""}
190
- className="my-3 max-w-full rounded-og-md border border-og-border"
191
- {...props}
192
- />
193
- ),
193
+ img: ({ alt, src, ...props }) =>
194
+ src ? (
195
+ <img
196
+ alt={alt ?? ""}
197
+ src={src}
198
+ className="my-3 max-w-full rounded-og-md border border-og-border"
199
+ {...props}
200
+ />
201
+ ) : alt ? (
202
+ <span className="text-og-fg-subtle" aria-disabled="true">
203
+ {alt}
204
+ </span>
205
+ ) : null,
194
206
  };
195
207
 
208
+ const MARKDOWN_LINK_CLASS =
209
+ "break-words font-medium text-og-accent-strong underline-offset-2 hover:underline";
210
+
211
+ function markdownComponents(onSandboxFile: MarkdownProps["onSandboxFile"]): Components {
212
+ return {
213
+ ...baseComponents,
214
+ a: ({ children, href, ...props }) => {
215
+ const location = sandboxFileLocationFromHref(href);
216
+ if (location !== null) {
217
+ return (
218
+ <SandboxMarkdownLink location={location} onSandboxFile={onSandboxFile}>
219
+ {children}
220
+ </SandboxMarkdownLink>
221
+ );
222
+ }
223
+ if (isSandboxHref(href) || !href) {
224
+ return (
225
+ <span
226
+ className="break-words font-medium text-og-fg-subtle underline decoration-dotted underline-offset-2"
227
+ aria-disabled="true"
228
+ title={
229
+ isSandboxHref(href)
230
+ ? "This sandbox file reference is invalid"
231
+ : "This link is unavailable"
232
+ }
233
+ >
234
+ {children}
235
+ </span>
236
+ );
237
+ }
238
+ return (
239
+ <a
240
+ className={MARKDOWN_LINK_CLASS}
241
+ target="_blank"
242
+ rel="noreferrer noopener"
243
+ href={href}
244
+ {...props}
245
+ >
246
+ {children}
247
+ </a>
248
+ );
249
+ },
250
+ };
251
+ }
252
+
253
+ function SandboxMarkdownLink({
254
+ location,
255
+ onSandboxFile,
256
+ children,
257
+ }: {
258
+ location: SandboxFileLocation;
259
+ onSandboxFile: MarkdownProps["onSandboxFile"];
260
+ children: ReactNode;
261
+ }) {
262
+ const [state, setState] = useState<"idle" | "loading" | "error">("idle");
263
+ const { path, line } = location;
264
+ if (!onSandboxFile) {
265
+ return (
266
+ <span
267
+ className="break-words font-medium text-og-fg-subtle underline decoration-dotted underline-offset-2"
268
+ aria-disabled="true"
269
+ title="This sandbox file requires a session-aware handler"
270
+ >
271
+ {children}
272
+ </span>
273
+ );
274
+ }
275
+ const idleTitle = line !== null ? `Open ${path} at line ${line}` : `Open ${path}`;
276
+ return (
277
+ <button
278
+ type="button"
279
+ className={cn(
280
+ MARKDOWN_LINK_CLASS,
281
+ "inline-flex min-h-7 cursor-pointer items-center disabled:cursor-wait disabled:opacity-70 pointer-coarse:min-h-11",
282
+ )}
283
+ disabled={state === "loading"}
284
+ aria-busy={state === "loading"}
285
+ title={state === "error" ? "Couldn't open this file. Select to retry." : idleTitle}
286
+ onClick={() => {
287
+ setState("loading");
288
+ void Promise.resolve(onSandboxFile(path, line ?? undefined)).then(
289
+ () => setState("idle"),
290
+ () => setState("error"),
291
+ );
292
+ }}
293
+ >
294
+ {children}
295
+ {state === "loading" ? " (opening…)" : state === "error" ? " (retry)" : null}
296
+ </button>
297
+ );
298
+ }
299
+
300
+ const SANDBOX_SCHEME = "sandbox:";
301
+ const LEGACY_WORKSPACE_PREFIX = "/workspace/";
302
+ const TRAILING_LINE = /:([0-9]+)$/u;
303
+ const TRAILING_INVALID_LINE = /:-(?:[0-9]+)$|:[0-9]+-[0-9]+$/u;
304
+ const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/u;
305
+
306
+ function isSandboxHref(href: string | undefined): boolean {
307
+ return href?.startsWith(SANDBOX_SCHEME) ?? false;
308
+ }
309
+
310
+ /**
311
+ * Parse an OpenGeni `sandbox:` application href (plus the historical bare
312
+ * `/workspace/...` form). The decoded path is intentionally opaque here:
313
+ * target selection, path policy, and filesystem authority belong to the
314
+ * session-aware host and its FileSystem boundary.
315
+ */
316
+ export function sandboxFileLocationFromHref(href: string | undefined): SandboxFileLocation | null {
317
+ if (!href) return null;
318
+ const encodedLocation = isSandboxHref(href)
319
+ ? href.slice(SANDBOX_SCHEME.length)
320
+ : href.startsWith(LEGACY_WORKSPACE_PREFIX)
321
+ ? href
322
+ : null;
323
+ if (!encodedLocation || encodedLocation.includes("?") || encodedLocation.includes("#")) {
324
+ return null;
325
+ }
326
+ if (TRAILING_INVALID_LINE.test(encodedLocation)) return null;
327
+ const lineMatch = TRAILING_LINE.exec(encodedLocation);
328
+ const encodedPath = lineMatch ? encodedLocation.slice(0, -lineMatch[0].length) : encodedLocation;
329
+ const line = lineMatch?.[1] ? Number(lineMatch[1]) : null;
330
+ if (line !== null && (!Number.isSafeInteger(line) || line < 1)) return null;
331
+ if (!encodedPath) return null;
332
+ try {
333
+ const path = decodeURIComponent(encodedPath);
334
+ return path && !CONTROL_CHARACTER.test(path) ? { path, line } : null;
335
+ } catch {
336
+ return null;
337
+ }
338
+ }
339
+
340
+ /** Return the exact decoded path for a supported sandbox-link href. */
341
+ export function sandboxFilePathFromHref(href: string | undefined): string | null {
342
+ return sandboxFileLocationFromHref(href)?.path ?? null;
343
+ }
344
+
345
+ const markdownUrlTransform: UrlTransform = (url, key, node) =>
346
+ key === "href" && node.tagName === "a" && isSandboxHref(url) ? url : defaultUrlTransform(url);
347
+
196
348
  /** How long after the stream ends the reveal pipeline stays for trailing animations. */
197
349
  /** Trailing ink window after stream end — keep ≥ {@link INK_FADE_MS}. */
198
350
  const REVEAL_LINGER_MS = 480 * MOTION_INSPECT_SCALE;
@@ -284,7 +436,7 @@ function MarkdownTable({ children, className, ...props }: ComponentPropsWithoutR
284
436
  );
285
437
  }
286
438
 
287
- function MarkdownImpl({ children, className, streaming = false }: MarkdownProps) {
439
+ function MarkdownImpl({ children, className, streaming = false, onSandboxFile }: MarkdownProps) {
288
440
  // Tip-ink engine for THIS body: created on the first streaming render, kept
289
441
  // through a short linger after the stream ends (so the last age window can
290
442
  // finish), then dropped so settled bodies pay zero cost. Observing during
@@ -373,6 +525,7 @@ function MarkdownImpl({ children, className, streaming = false }: MarkdownProps)
373
525
  // fences don't snap to final GFM one commit before the crystallize morph.
374
526
  // Reveal identity still tracks the true source (`children`).
375
527
  const parseText = streaming || revealActive ? softenStreamingMarkdown(children) : children;
528
+ const components = useMemo(() => markdownComponents(onSandboxFile), [onSandboxFile]);
376
529
 
377
530
  // `min-w-0` lets the prose shrink inside flex parents (message bubbles) so
378
531
  // long links and code blocks wrap/scroll instead of forcing overflow.
@@ -390,6 +543,7 @@ function MarkdownImpl({ children, className, streaming = false }: MarkdownProps)
390
543
  remarkPlugins={[remarkGfm]}
391
544
  rehypePlugins={rehypePlugins}
392
545
  components={components}
546
+ urlTransform={markdownUrlTransform}
393
547
  >
394
548
  {parseText}
395
549
  </ReactMarkdown>
@@ -14,6 +14,7 @@ import {
14
14
  ChevronRightIcon,
15
15
  PauseCircleIcon,
16
16
  PauseIcon,
17
+ MessageCircleQuestionIcon,
17
18
  PencilLineIcon,
18
19
  PlayIcon,
19
20
  RefreshCwIcon,
@@ -71,6 +72,7 @@ import {
71
72
  type AuthNeededItem,
72
73
  type ContextCompactionItem,
73
74
  type GoalItem,
75
+ type HumanInputItem,
74
76
  type MachineInputBatchItem,
75
77
  type NoticeItem,
76
78
  type TimelineGroup,
@@ -160,7 +162,7 @@ export type MessageTimelineProps = {
160
162
  toolRegistry?: ToolRegistry | undefined;
161
163
  /** Resolve opaque retained screenshot receipts through the authenticated host SDK. */
162
164
  loadRetainedScreenshot?: RetainedScreenshotLoader | undefined;
163
- /** Resolve permanent generated-image receipts through the authenticated host SDK. */
165
+ /** Resolve permanent workspace image/file receipts through the authenticated host SDK. */
164
166
  loadRetainedArtifact?: RetainedArtifactLoader | undefined;
165
167
  /** Mint short-lived native playback sources for retained generated videos. */
166
168
  loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
@@ -369,13 +371,20 @@ export function MessageTimeline({
369
371
  // leaves that band (scrolls down / sentinel exits) — never from scrolling
370
372
  // further toward y=0 (that was the batch-top load loop).
371
373
  const olderLoadGateRef = useRef<"armed" | "cooling">("armed");
374
+ // A collapsed history tail can be shorter than the viewport. In that state
375
+ // there is no upward scroll range, so reader intent can never arm the top
376
+ // sentinel. Request one older page per loaded window until history either
377
+ // fills the viewport or the host reports that no older rows remain.
378
+ const underfillLoadWindowRef = useRef<string | null>(null);
372
379
  const resizeFollowRafRef = useRef<number | null>(null);
373
380
  const firstGroupKey = allGroups[0] ? timelineGroupKey(allGroups[0]) : null;
374
381
  // Content stays invisible until the tip is hard-parked across a short
375
382
  // post-commit settle (two rAFs). That absorbs sync late layout while hidden
376
383
  // so load/remount does not ease into the tip — live tip-follow is unchanged
377
384
  // once revealed. A flash of the window's TOP is still structurally impossible.
378
- const [revealed, setRevealed] = useState(false);
385
+ // The accepted-create handoff uses the reserved local id "c" so its known
386
+ // first message is visible immediately; loaded histories still park first.
387
+ const [revealed, setRevealed] = useState(resolvedItems[0]?.id === "c");
379
388
  // Mirror `pinned` into a ref, written ONLY by applyPinned, so the
380
389
  // ResizeObserver rAF (a stable closure) reads the live value and a snap can
381
390
  // never race a just-unpinned reader across a pending React commit.
@@ -443,6 +452,8 @@ export function MessageTimeline({
443
452
  const groupKeyByItemIdRef = useRef<Map<string, string>>(new Map());
444
453
  const groupOffsetByKeyRef = useRef<Map<string, number>>(new Map());
445
454
  const firstItemId = resolvedItems[0]?.id ?? null;
455
+ const lastItemId = resolvedItems.at(-1)?.id ?? null;
456
+ const loadedWindowKey = JSON.stringify([firstItemId, lastItemId, resolvedItems.length]);
446
457
  // Bulk paints (the initial tail window, a prepended older window — detected
447
458
  // by the first group key changing) must not run per-row entrance animations.
448
459
  const firstKeyChangedForBulk =
@@ -718,6 +729,39 @@ export function MessageTimeline({
718
729
  [beginUserMessageDisclosureChange],
719
730
  );
720
731
 
732
+ const requestOlderIfUnderfilled = useCallback(
733
+ (node: HTMLElement) => {
734
+ if (
735
+ underfillLoadWindowRef.current !== null &&
736
+ underfillLoadWindowRef.current !== loadedWindowKey
737
+ ) {
738
+ // The requested page landed. Ordinary sentinel prefetch may own the
739
+ // next approach to the top once this window has a usable scroll range.
740
+ olderLoadGateRef.current = "armed";
741
+ }
742
+ // clientHeight=0 is pre-layout/headless, not evidence that the rendered
743
+ // history underfills a real viewport.
744
+ if (
745
+ node.clientHeight <= 1 ||
746
+ maxScrollOf(node) > 1 ||
747
+ !hasOlder ||
748
+ loadingOlder ||
749
+ !onLoadOlder ||
750
+ underfillLoadWindowRef.current === loadedWindowKey
751
+ ) {
752
+ return;
753
+ }
754
+ underfillLoadWindowRef.current = loadedWindowKey;
755
+ // If the reader had already armed ordinary prefetch, do not let its
756
+ // sentinel duplicate this request while the same short page is loading.
757
+ if (olderPrefetchArmedRef.current) {
758
+ olderLoadGateRef.current = "cooling";
759
+ }
760
+ onLoadOlder();
761
+ },
762
+ [hasOlder, loadedWindowKey, loadingOlder, onLoadOlder],
763
+ );
764
+
721
765
  const driveFollowRef = useRef<(node: HTMLElement, now?: number) => void>(() => undefined);
722
766
  const driveFollow = useCallback(
723
767
  (node: HTMLElement, nowMs?: number) => {
@@ -1014,10 +1058,21 @@ export function MessageTimeline({
1014
1058
  foldMemoryRef.current.clear();
1015
1059
  userMessageDisclosureMemoryRef.current.clear();
1016
1060
  disclosureKeepsUnpinnedRef.current = false;
1061
+ underfillLoadWindowRef.current = null;
1017
1062
  seenActivityIdsRef.current.clear();
1018
1063
  applyPinned(true);
1019
1064
  }, [allGroups.length, revealed, applyPinned]);
1020
1065
 
1066
+ // Parent commits cover the initial/history-loading cases. Disclosure state
1067
+ // changes are child-local, so the ResizeObserver below owns dynamic collapse
1068
+ // and expansion after mount.
1069
+ useEffect(() => {
1070
+ const node = scrollRef.current;
1071
+ if (node) {
1072
+ requestOlderIfUnderfilled(node);
1073
+ }
1074
+ });
1075
+
1021
1076
  // Clear the bulk-paint marker a frame after it renders, so rows appended
1022
1077
  // live (streams, new turns) animate exactly as before.
1023
1078
  useLayoutEffect(() => {
@@ -1115,6 +1170,7 @@ export function MessageTimeline({
1115
1170
  if (!current) {
1116
1171
  return;
1117
1172
  }
1173
+ requestOlderIfUnderfilled(current);
1118
1174
  if (!autoFollow || !pinnedRef.current || hasNewerRef.current) {
1119
1175
  return;
1120
1176
  }
@@ -1137,7 +1193,7 @@ export function MessageTimeline({
1137
1193
  resizeFollowRafRef.current = null;
1138
1194
  }
1139
1195
  };
1140
- }, [autoFollow, driveFollow, snapToBottom]);
1196
+ }, [autoFollow, driveFollow, requestOlderIfUnderfilled, snapToBottom]);
1141
1197
 
1142
1198
  // Entering a non-tip history window: drop any live pin so the page bottom
1143
1199
  // cannot re-stick follow across loadNewer. Leaving it (the tip window
@@ -2031,7 +2087,9 @@ function useLiveSettleFold(folded: boolean): boolean {
2031
2087
  function timelineGroupKey(group: TimelineGroup): string {
2032
2088
  switch (group.kind) {
2033
2089
  case "item":
2034
- return group.item.id;
2090
+ return group.item.kind === "user-message" && group.item.reconciliationKey
2091
+ ? group.item.reconciliationKey
2092
+ : group.item.id;
2035
2093
  case "activity":
2036
2094
  return group.id;
2037
2095
  case "turn":
@@ -2249,6 +2307,8 @@ export function TimelineRow({
2249
2307
  switch (item.kind) {
2250
2308
  case "user-message":
2251
2309
  return <UserMessageRow item={item} renderMessageText={renderMessageText} />;
2310
+ case "human-input":
2311
+ return <HumanInputConversationRow item={item} />;
2252
2312
  case "agent-message":
2253
2313
  return <AgentMessageRow item={item} renderMessageText={renderMessageText} />;
2254
2314
  case "worker-completion":
@@ -2380,14 +2440,7 @@ function UserMessageRow({
2380
2440
  | undefined;
2381
2441
  }) {
2382
2442
  const enter = useEntranceAnimation();
2383
- const deliveryLabel =
2384
- item.delivery?.state === "sending"
2385
- ? "Sending…"
2386
- : item.delivery?.state === "queued"
2387
- ? "Queued"
2388
- : item.delivery?.state === "failed"
2389
- ? "Not sent"
2390
- : null;
2443
+ const deliveryFailed = item.delivery?.state === "failed";
2391
2444
  return (
2392
2445
  <div className={cn(enter && "animate-og-enter", "flex justify-end")}>
2393
2446
  <div className="flex max-w-[85%] min-w-0 flex-col items-end gap-1">
@@ -2422,19 +2475,25 @@ function UserMessageRow({
2422
2475
  ) : null}
2423
2476
  </div>
2424
2477
  </CopyHoverFrame>
2425
- {deliveryLabel ? (
2426
- <div className="flex max-w-full items-center gap-2 px-1 text-og-xs text-og-fg-subtle">
2427
- <span title={item.delivery?.error}>{deliveryLabel}</span>
2428
- {item.delivery?.state === "failed" && item.delivery.onRetry ? (
2478
+ {deliveryFailed ? (
2479
+ <div
2480
+ role="status"
2481
+ className="flex max-w-full items-center gap-2 px-1 text-og-xs text-og-status-failed"
2482
+ >
2483
+ <span className="inline-flex items-center gap-1" title={item.delivery?.error}>
2484
+ <TriangleAlertIcon className="size-3.5 shrink-0" aria-hidden="true" />
2485
+ <span>Message not sent</span>
2486
+ </span>
2487
+ {item.delivery?.onRetry ? (
2429
2488
  <button
2430
2489
  type="button"
2431
- className="font-medium text-og-fg underline decoration-og-border underline-offset-2 hover:text-og-fg-strong"
2490
+ className="font-medium text-og-status-failed underline decoration-og-status-failed/50 underline-offset-2 hover:text-og-fg"
2432
2491
  onClick={item.delivery.onRetry}
2433
2492
  >
2434
2493
  Retry
2435
2494
  </button>
2436
2495
  ) : null}
2437
- {item.delivery?.state === "failed" && item.delivery.onRemove ? (
2496
+ {item.delivery?.onRemove ? (
2438
2497
  <button
2439
2498
  type="button"
2440
2499
  className="font-medium text-og-fg-subtle underline decoration-og-border underline-offset-2 hover:text-og-fg"
@@ -2450,6 +2509,139 @@ function UserMessageRow({
2450
2509
  );
2451
2510
  }
2452
2511
 
2512
+ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
2513
+ const enter = useEntranceAnimation();
2514
+ const multipleQuestions = Math.max(item.questions.length, item.answers.length) > 1;
2515
+ const questionNumberById = new Map(
2516
+ item.questions.map((question, index) => [question.id, index + 1]),
2517
+ );
2518
+ const settledLabel =
2519
+ item.response.outcome === "answered"
2520
+ ? "You answered"
2521
+ : item.response.outcome === "skipped"
2522
+ ? "Skipped"
2523
+ : item.response.outcome === "expired"
2524
+ ? "Expired"
2525
+ : "Cancelled";
2526
+ const copyText = humanInputConversationCopyText(item, settledLabel);
2527
+
2528
+ return (
2529
+ <div
2530
+ className={cn(enter && "animate-og-enter", "flex w-full flex-col gap-2.5")}
2531
+ data-human-input-history={item.requestId}
2532
+ >
2533
+ <div className="flex max-w-[90%] items-start gap-3 rounded-og-lg rounded-bl-og-xs border border-og-border bg-og-surface-1 px-3.5 py-3 sm:max-w-[82%]">
2534
+ <span className="mt-0.5 inline-flex size-8 shrink-0 items-center justify-center rounded-og-md bg-og-status-waiting/10 text-og-status-waiting">
2535
+ <MessageCircleQuestionIcon aria-hidden="true" className="size-4" />
2536
+ </span>
2537
+ <div className="min-w-0 flex-1">
2538
+ <p className="text-og-xs font-medium text-og-fg-subtle">Agent asked</p>
2539
+ <div className="mt-1.5 space-y-3">
2540
+ {item.questions.length > 0 ? (
2541
+ item.questions.map((question, index) => (
2542
+ <div key={question.id}>
2543
+ {question.label ? (
2544
+ <p className="text-og-sm font-semibold text-og-fg">
2545
+ {multipleQuestions ? (
2546
+ <span className="mr-1.5 tabular-nums text-og-fg-muted">{index + 1}.</span>
2547
+ ) : null}
2548
+ {multipleQuestions ? " " : null}
2549
+ {question.label}
2550
+ </p>
2551
+ ) : null}
2552
+ <p
2553
+ className={cn(
2554
+ "text-og-md leading-6 text-og-fg",
2555
+ question.label && "mt-0.5 text-og-sm text-og-fg-muted",
2556
+ )}
2557
+ >
2558
+ {!question.label && multipleQuestions ? (
2559
+ <span className="mr-1.5 tabular-nums text-og-fg-muted">{index + 1}.</span>
2560
+ ) : null}
2561
+ {!question.label && multipleQuestions ? " " : null}
2562
+ {question.prompt}
2563
+ </p>
2564
+ </div>
2565
+ ))
2566
+ ) : (
2567
+ <p className="text-og-sm text-og-fg-muted">The agent requested structured input.</p>
2568
+ )}
2569
+ </div>
2570
+ </div>
2571
+ </div>
2572
+
2573
+ <div className="flex justify-end">
2574
+ <CopyHoverFrame
2575
+ copyText={copyText}
2576
+ label="Copy answer"
2577
+ className="w-fit max-w-[90%] min-w-0 sm:max-w-[82%]"
2578
+ trailing={<MessageFooterTime occurredAt={item.occurredAt} />}
2579
+ >
2580
+ <div className="w-fit max-w-full min-w-0 rounded-og-lg rounded-br-og-xs border border-og-border bg-og-surface-2 px-4 py-2.5 text-og-md leading-6 text-og-fg">
2581
+ <p className="text-og-xs font-medium text-og-fg-subtle">{settledLabel}</p>
2582
+ {item.response.outcome === "answered" ? (
2583
+ <div className="mt-1.5 space-y-3">
2584
+ {item.answers.length > 0 ? (
2585
+ item.answers.map((answer, answerIndex) => (
2586
+ <div key={answer.questionId}>
2587
+ {multipleQuestions ? (
2588
+ <p className="text-og-sm font-semibold text-og-fg">
2589
+ <span className="mr-1.5 tabular-nums text-og-fg-muted">
2590
+ {questionNumberById.get(answer.questionId) ?? answerIndex + 1}.
2591
+ </span>{" "}
2592
+ {answer.label}
2593
+ </p>
2594
+ ) : null}
2595
+ <div className={cn("text-og-md text-og-fg", multipleQuestions && "mt-0.5")}>
2596
+ {answer.values.length > 1 ? (
2597
+ <ul className="list-disc space-y-0.5 pl-5">
2598
+ {answer.values.map((value) => (
2599
+ <li key={`${answer.questionId}-${value}`}>{value}</li>
2600
+ ))}
2601
+ </ul>
2602
+ ) : (
2603
+ <p>{answer.values[0] || "Answered"}</p>
2604
+ )}
2605
+ </div>
2606
+ </div>
2607
+ ))
2608
+ ) : (
2609
+ <p>Answered</p>
2610
+ )}
2611
+ </div>
2612
+ ) : null}
2613
+ </div>
2614
+ </CopyHoverFrame>
2615
+ </div>
2616
+ </div>
2617
+ );
2618
+ }
2619
+
2620
+ function humanInputConversationCopyText(item: HumanInputItem, settledLabel: string): string {
2621
+ const questions = item.questions
2622
+ .map(
2623
+ (question, index) =>
2624
+ `${item.questions.length > 1 ? `${index + 1}. ` : ""}${question.label || "Question"}: ${question.prompt}`,
2625
+ )
2626
+ .join("\n\n");
2627
+ const questionNumberById = new Map(
2628
+ item.questions.map((question, index) => [question.id, index + 1]),
2629
+ );
2630
+ const answers = item.answers
2631
+ .map(
2632
+ (answer, index) =>
2633
+ `${
2634
+ item.questions.length > 1
2635
+ ? `${questionNumberById.get(answer.questionId) ?? index + 1}. `
2636
+ : ""
2637
+ }${answer.label}: ${answer.values.join(", ") || "Answered"}`,
2638
+ )
2639
+ .join("\n\n");
2640
+ return [questions, `${settledLabel}${answers ? `\n\n${answers}` : ""}`]
2641
+ .filter(Boolean)
2642
+ .join("\n\n");
2643
+ }
2644
+
2453
2645
  function AgentMessageRow({
2454
2646
  item,
2455
2647
  renderMessageText,
@@ -2701,6 +2893,11 @@ const GOAL_META: Record<GoalItem["action"], GoalMeta> = {
2701
2893
  },
2702
2894
  resumed: { label: "Goal resumed", pill: NEUTRAL_PILL, icon: PlayIcon },
2703
2895
  cleared: { label: "Goal cleared", pill: NEUTRAL_PILL, icon: Trash2Icon },
2896
+ held: {
2897
+ label: "Goal held",
2898
+ pill: "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting",
2899
+ icon: PauseCircleIcon,
2900
+ },
2704
2901
  continuation: { label: "Continuing toward the goal", pill: NEUTRAL_PILL, icon: ArrowRightIcon },
2705
2902
  };
2706
2903