@opengeni/react 0.57.0 → 2.2.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 +36 -11
  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-Y7OEDUJ5.js → chunk-5RZN3DD5.js} +1014 -420
  8. package/dist/chunk-5RZN3DD5.js.map +1 -0
  9. package/dist/chunk-5YK2ZDIG.js +116 -0
  10. package/dist/chunk-5YK2ZDIG.js.map +1 -0
  11. package/dist/chunk-H5YGYNXS.js +329 -0
  12. package/dist/chunk-H5YGYNXS.js.map +1 -0
  13. package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
  14. package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
  15. package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
  16. package/dist/chunk-JOAVA2TW.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-B6WPZSTT.js → chunk-UOLJWT5Y.js} +78 -16
  24. package/dist/chunk-UOLJWT5Y.js.map +1 -0
  25. package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
  26. package/dist/chunk-W6NFJXSA.js.map +1 -0
  27. package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
  28. package/dist/chunk-X4A66EWZ.js.map +1 -0
  29. package/dist/chunk-X4JAFD5R.js +3907 -0
  30. package/dist/chunk-X4JAFD5R.js.map +1 -0
  31. package/dist/{chunk-6KLX2RH5.js → chunk-XSA2QAKP.js} +334 -19
  32. package/dist/chunk-XSA2QAKP.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 +10 -3
  83. package/dist/realtime.js +138 -29
  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 +48 -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 +91 -55
  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 +162 -17
  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 +200 -37
  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 +476 -23
  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 +75 -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;
@@ -375,7 +377,9 @@ export function MessageTimeline({
375
377
  // post-commit settle (two rAFs). That absorbs sync late layout while hidden
376
378
  // so load/remount does not ease into the tip — live tip-follow is unchanged
377
379
  // once revealed. A flash of the window's TOP is still structurally impossible.
378
- const [revealed, setRevealed] = useState(false);
380
+ // The accepted-create handoff uses the reserved local id "c" so its known
381
+ // first message is visible immediately; loaded histories still park first.
382
+ const [revealed, setRevealed] = useState(resolvedItems[0]?.id === "c");
379
383
  // Mirror `pinned` into a ref, written ONLY by applyPinned, so the
380
384
  // ResizeObserver rAF (a stable closure) reads the live value and a snap can
381
385
  // never race a just-unpinned reader across a pending React commit.
@@ -2031,7 +2035,9 @@ function useLiveSettleFold(folded: boolean): boolean {
2031
2035
  function timelineGroupKey(group: TimelineGroup): string {
2032
2036
  switch (group.kind) {
2033
2037
  case "item":
2034
- return group.item.id;
2038
+ return group.item.kind === "user-message" && group.item.reconciliationKey
2039
+ ? group.item.reconciliationKey
2040
+ : group.item.id;
2035
2041
  case "activity":
2036
2042
  return group.id;
2037
2043
  case "turn":
@@ -2249,6 +2255,8 @@ export function TimelineRow({
2249
2255
  switch (item.kind) {
2250
2256
  case "user-message":
2251
2257
  return <UserMessageRow item={item} renderMessageText={renderMessageText} />;
2258
+ case "human-input":
2259
+ return <HumanInputConversationRow item={item} />;
2252
2260
  case "agent-message":
2253
2261
  return <AgentMessageRow item={item} renderMessageText={renderMessageText} />;
2254
2262
  case "worker-completion":
@@ -2380,14 +2388,7 @@ function UserMessageRow({
2380
2388
  | undefined;
2381
2389
  }) {
2382
2390
  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;
2391
+ const deliveryFailed = item.delivery?.state === "failed";
2391
2392
  return (
2392
2393
  <div className={cn(enter && "animate-og-enter", "flex justify-end")}>
2393
2394
  <div className="flex max-w-[85%] min-w-0 flex-col items-end gap-1">
@@ -2422,19 +2423,25 @@ function UserMessageRow({
2422
2423
  ) : null}
2423
2424
  </div>
2424
2425
  </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 ? (
2426
+ {deliveryFailed ? (
2427
+ <div
2428
+ role="status"
2429
+ className="flex max-w-full items-center gap-2 px-1 text-og-xs text-og-status-failed"
2430
+ >
2431
+ <span className="inline-flex items-center gap-1" title={item.delivery?.error}>
2432
+ <TriangleAlertIcon className="size-3.5 shrink-0" aria-hidden="true" />
2433
+ <span>Message not sent</span>
2434
+ </span>
2435
+ {item.delivery?.onRetry ? (
2429
2436
  <button
2430
2437
  type="button"
2431
- className="font-medium text-og-fg underline decoration-og-border underline-offset-2 hover:text-og-fg-strong"
2438
+ className="font-medium text-og-status-failed underline decoration-og-status-failed/50 underline-offset-2 hover:text-og-fg"
2432
2439
  onClick={item.delivery.onRetry}
2433
2440
  >
2434
2441
  Retry
2435
2442
  </button>
2436
2443
  ) : null}
2437
- {item.delivery?.state === "failed" && item.delivery.onRemove ? (
2444
+ {item.delivery?.onRemove ? (
2438
2445
  <button
2439
2446
  type="button"
2440
2447
  className="font-medium text-og-fg-subtle underline decoration-og-border underline-offset-2 hover:text-og-fg"
@@ -2450,6 +2457,139 @@ function UserMessageRow({
2450
2457
  );
2451
2458
  }
2452
2459
 
2460
+ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
2461
+ const enter = useEntranceAnimation();
2462
+ const multipleQuestions = Math.max(item.questions.length, item.answers.length) > 1;
2463
+ const questionNumberById = new Map(
2464
+ item.questions.map((question, index) => [question.id, index + 1]),
2465
+ );
2466
+ const settledLabel =
2467
+ item.response.outcome === "answered"
2468
+ ? "You answered"
2469
+ : item.response.outcome === "skipped"
2470
+ ? "Skipped"
2471
+ : item.response.outcome === "expired"
2472
+ ? "Expired"
2473
+ : "Cancelled";
2474
+ const copyText = humanInputConversationCopyText(item, settledLabel);
2475
+
2476
+ return (
2477
+ <div
2478
+ className={cn(enter && "animate-og-enter", "flex w-full flex-col gap-2.5")}
2479
+ data-human-input-history={item.requestId}
2480
+ >
2481
+ <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%]">
2482
+ <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">
2483
+ <MessageCircleQuestionIcon aria-hidden="true" className="size-4" />
2484
+ </span>
2485
+ <div className="min-w-0 flex-1">
2486
+ <p className="text-og-xs font-medium text-og-fg-subtle">Agent asked</p>
2487
+ <div className="mt-1.5 space-y-3">
2488
+ {item.questions.length > 0 ? (
2489
+ item.questions.map((question, index) => (
2490
+ <div key={question.id}>
2491
+ {question.label ? (
2492
+ <p className="text-og-sm font-semibold text-og-fg">
2493
+ {multipleQuestions ? (
2494
+ <span className="mr-1.5 tabular-nums text-og-fg-muted">{index + 1}.</span>
2495
+ ) : null}
2496
+ {multipleQuestions ? " " : null}
2497
+ {question.label}
2498
+ </p>
2499
+ ) : null}
2500
+ <p
2501
+ className={cn(
2502
+ "text-og-md leading-6 text-og-fg",
2503
+ question.label && "mt-0.5 text-og-sm text-og-fg-muted",
2504
+ )}
2505
+ >
2506
+ {!question.label && multipleQuestions ? (
2507
+ <span className="mr-1.5 tabular-nums text-og-fg-muted">{index + 1}.</span>
2508
+ ) : null}
2509
+ {!question.label && multipleQuestions ? " " : null}
2510
+ {question.prompt}
2511
+ </p>
2512
+ </div>
2513
+ ))
2514
+ ) : (
2515
+ <p className="text-og-sm text-og-fg-muted">The agent requested structured input.</p>
2516
+ )}
2517
+ </div>
2518
+ </div>
2519
+ </div>
2520
+
2521
+ <div className="flex justify-end">
2522
+ <CopyHoverFrame
2523
+ copyText={copyText}
2524
+ label="Copy answer"
2525
+ className="w-fit max-w-[90%] min-w-0 sm:max-w-[82%]"
2526
+ trailing={<MessageFooterTime occurredAt={item.occurredAt} />}
2527
+ >
2528
+ <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">
2529
+ <p className="text-og-xs font-medium text-og-fg-subtle">{settledLabel}</p>
2530
+ {item.response.outcome === "answered" ? (
2531
+ <div className="mt-1.5 space-y-3">
2532
+ {item.answers.length > 0 ? (
2533
+ item.answers.map((answer, answerIndex) => (
2534
+ <div key={answer.questionId}>
2535
+ {multipleQuestions ? (
2536
+ <p className="text-og-sm font-semibold text-og-fg">
2537
+ <span className="mr-1.5 tabular-nums text-og-fg-muted">
2538
+ {questionNumberById.get(answer.questionId) ?? answerIndex + 1}.
2539
+ </span>{" "}
2540
+ {answer.label}
2541
+ </p>
2542
+ ) : null}
2543
+ <div className={cn("text-og-md text-og-fg", multipleQuestions && "mt-0.5")}>
2544
+ {answer.values.length > 1 ? (
2545
+ <ul className="list-disc space-y-0.5 pl-5">
2546
+ {answer.values.map((value) => (
2547
+ <li key={`${answer.questionId}-${value}`}>{value}</li>
2548
+ ))}
2549
+ </ul>
2550
+ ) : (
2551
+ <p>{answer.values[0] || "Answered"}</p>
2552
+ )}
2553
+ </div>
2554
+ </div>
2555
+ ))
2556
+ ) : (
2557
+ <p>Answered</p>
2558
+ )}
2559
+ </div>
2560
+ ) : null}
2561
+ </div>
2562
+ </CopyHoverFrame>
2563
+ </div>
2564
+ </div>
2565
+ );
2566
+ }
2567
+
2568
+ function humanInputConversationCopyText(item: HumanInputItem, settledLabel: string): string {
2569
+ const questions = item.questions
2570
+ .map(
2571
+ (question, index) =>
2572
+ `${item.questions.length > 1 ? `${index + 1}. ` : ""}${question.label || "Question"}: ${question.prompt}`,
2573
+ )
2574
+ .join("\n\n");
2575
+ const questionNumberById = new Map(
2576
+ item.questions.map((question, index) => [question.id, index + 1]),
2577
+ );
2578
+ const answers = item.answers
2579
+ .map(
2580
+ (answer, index) =>
2581
+ `${
2582
+ item.questions.length > 1
2583
+ ? `${questionNumberById.get(answer.questionId) ?? index + 1}. `
2584
+ : ""
2585
+ }${answer.label}: ${answer.values.join(", ") || "Answered"}`,
2586
+ )
2587
+ .join("\n\n");
2588
+ return [questions, `${settledLabel}${answers ? `\n\n${answers}` : ""}`]
2589
+ .filter(Boolean)
2590
+ .join("\n\n");
2591
+ }
2592
+
2453
2593
  function AgentMessageRow({
2454
2594
  item,
2455
2595
  renderMessageText,
@@ -2701,6 +2841,11 @@ const GOAL_META: Record<GoalItem["action"], GoalMeta> = {
2701
2841
  },
2702
2842
  resumed: { label: "Goal resumed", pill: NEUTRAL_PILL, icon: PlayIcon },
2703
2843
  cleared: { label: "Goal cleared", pill: NEUTRAL_PILL, icon: Trash2Icon },
2844
+ held: {
2845
+ label: "Goal held",
2846
+ pill: "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting",
2847
+ icon: PauseCircleIcon,
2848
+ },
2704
2849
  continuation: { label: "Continuing toward the goal", pill: NEUTRAL_PILL, icon: ArrowRightIcon },
2705
2850
  };
2706
2851
 
@@ -4,6 +4,7 @@ import {
4
4
  ChevronDownIcon,
5
5
  ChevronLeftIcon,
6
6
  ChevronRightIcon,
7
+ Globe2Icon,
7
8
  KeyRoundIcon,
8
9
  ZapIcon,
9
10
  } from "lucide-react";
@@ -20,7 +21,6 @@ import {
20
21
  import { cn } from "../lib/cn";
21
22
  import { usePortalTokenSource, usePortalTokenStyle } from "../lib/use-portal-token-style";
22
23
  import {
23
- coerceReasoningEffortForModel,
24
24
  effortOptionsForModel,
25
25
  findPickerRow,
26
26
  groupPickerRowsByBillingClass,
@@ -64,6 +64,7 @@ export const defaultModelPolicyPickerMessages: ModelPolicyPickerMessages = {
64
64
  codexOnly: "Codex-only session",
65
65
  billingHints: {
66
66
  opengeni_credits: "Will use credits",
67
+ external: "Provider terms and limits apply",
67
68
  codex_subscription: "ChatGPT / Codex plan",
68
69
  supergrok_subscription: "SuperGrok / xAI plan",
69
70
  byok: "Billed to your AI Gateway",
@@ -92,6 +93,8 @@ export type ModelPolicyPickerProps = {
92
93
  sessionKey?: string | undefined;
93
94
  /** Prefer bottom on new-chat surfaces and top for bottom-docked composers. */
94
95
  menuSide?: "top" | "bottom" | undefined;
96
+ /** Hide latency controls on surfaces whose saved policy does not include latency. */
97
+ allowLatencyMode?: boolean | undefined;
95
98
  /** Classes for the portalled menu surface. Prefer --og-* tokens for theming. */
96
99
  contentClassName?: string | undefined;
97
100
  /** Inline styles for the portalled menu, applied after inherited --og-* tokens. */
@@ -151,6 +154,7 @@ export function BillingClassMark(props: {
151
154
  }) {
152
155
  const labels: Record<PickerBillingClass, string> = {
153
156
  opengeni_credits: "OpenGeni",
157
+ external: "External provider",
154
158
  codex_subscription: "Codex",
155
159
  supergrok_subscription: "SuperGrok",
156
160
  byok: "Bring your own key",
@@ -173,6 +177,8 @@ export function BillingClassMark(props: {
173
177
  >
174
178
  {props.billingClass === "opengeni_credits" ? (
175
179
  <OpenGeniMark className={mark} />
180
+ ) : props.billingClass === "external" ? (
181
+ <Globe2Icon className={mark} aria-hidden />
176
182
  ) : props.billingClass === "codex_subscription" ? (
177
183
  <ChatGptMark className={mark} />
178
184
  ) : props.billingClass === "supergrok_subscription" ? (
@@ -354,17 +360,6 @@ export function ModelPolicyPickerMenu(
354
360
  const focusModel =
355
361
  effectiveNav.modelId === null ? undefined : findPickerRow(rows, effectiveNav.modelId);
356
362
  const selectedRow = findPickerRow(rows, props.model);
357
- const { latencyMode, loading, onLatencyModeChange } = props;
358
-
359
- useEffect(() => {
360
- if (loading || !selectedRow?.selectable) return;
361
- const supportsCurrentMode = runnableLatencyModesForModel(selectedRow.catalog).includes(
362
- latencyMode,
363
- );
364
- if (!supportsCurrentMode && latencyMode !== "standard") {
365
- onLatencyModeChange("standard");
366
- }
367
- }, [latencyMode, loading, onLatencyModeChange, selectedRow]);
368
363
 
369
364
  const go = (next: PickerNavState, nextDirection: 1 | -1) => {
370
365
  setDirection(nextDirection);
@@ -374,7 +369,7 @@ export function ModelPolicyPickerMenu(
374
369
  const selectEffort = (row: ClientPickerModelRow, effort: ReasoningEffort) => {
375
370
  if (!row.selectable) return;
376
371
  if (row.id !== props.model) props.onModelChange(row.id);
377
- props.onEffortChange(coerceReasoningEffortForModel(row.catalog, effort));
372
+ props.onEffortChange(effort);
378
373
  };
379
374
 
380
375
  const pageKey =
@@ -445,7 +440,7 @@ export function ModelPolicyPickerMenu(
445
440
  icon={<BillingClassMark billingClass={focusModel.billingClass} />}
446
441
  onBack={() => go({ level: "models", rail: focusModel.billingClass, modelId: null }, -1)}
447
442
  trailing={
448
- supportsFast ? (
443
+ supportsFast && props.allowLatencyMode !== false ? (
449
444
  <button
450
445
  type="button"
451
446
  disabled={!focusModel.selectable}
@@ -460,9 +455,6 @@ export function ModelPolicyPickerMenu(
460
455
  onClick={() => {
461
456
  if (!activeModel) {
462
457
  props.onModelChange(focusModel.id);
463
- props.onEffortChange(
464
- coerceReasoningEffortForModel(focusModel.catalog, props.effort),
465
- );
466
458
  }
467
459
  props.onLatencyModeChange(
468
460
  activeModel && props.latencyMode === "fast" ? "standard" : "fast",
@@ -25,7 +25,7 @@ type FileComponent = ComponentType<{
25
25
  }>;
26
26
 
27
27
  export type PierreFileProps = {
28
- /** Workspace-relative path (used for the header + language inference). */
28
+ /** Target path (used for the header + language inference). */
29
29
  path: string;
30
30
  /** The decoded text contents. */
31
31
  contents: string;