@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.
- package/README.md +33 -10
- package/dist/artifacts-spreadsheet.js +1 -1
- package/dist/artifacts.js +1 -1
- package/dist/browser-viewer-GXUICIAG.js +14 -0
- package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
- package/dist/chunk-4MDLEHXL.js.map +1 -0
- package/dist/chunk-5YK2ZDIG.js +116 -0
- package/dist/chunk-5YK2ZDIG.js.map +1 -0
- package/dist/chunk-H5YGYNXS.js +329 -0
- package/dist/chunk-H5YGYNXS.js.map +1 -0
- package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
- package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
- package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
- package/dist/chunk-JOAVA2TW.js.map +1 -0
- package/dist/{chunk-Y7OEDUJ5.js → chunk-LS4POUTY.js} +1057 -426
- package/dist/chunk-LS4POUTY.js.map +1 -0
- package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
- package/dist/chunk-LTCHJ4MS.js.map +1 -0
- package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
- package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
- package/dist/chunk-QGBJ6GHG.js +2300 -0
- package/dist/chunk-QGBJ6GHG.js.map +1 -0
- package/dist/{chunk-6KLX2RH5.js → chunk-RSXJN73N.js} +365 -21
- package/dist/chunk-RSXJN73N.js.map +1 -0
- package/dist/{chunk-B6WPZSTT.js → chunk-SE4W4WQ3.js} +78 -16
- package/dist/chunk-SE4W4WQ3.js.map +1 -0
- package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
- package/dist/chunk-W6NFJXSA.js.map +1 -0
- package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
- package/dist/chunk-X4A66EWZ.js.map +1 -0
- package/dist/chunk-X4JAFD5R.js +3907 -0
- package/dist/chunk-X4JAFD5R.js.map +1 -0
- package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
- package/dist/chunk-XXWH2JLC.js.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/components/browser-viewer.d.ts +7 -1
- package/dist/components/computer-viewer.d.ts +4 -1
- package/dist/components/enrollment-consent.d.ts +6 -3
- package/dist/components/file-browser.d.ts +7 -1
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +1 -1
- package/dist/components/machine-card.d.ts +4 -1
- package/dist/components/machines/machine-detail.d.ts +4 -1
- package/dist/components/machines/machine-operation-policy.d.ts +11 -0
- package/dist/components/machines-dashboard.d.ts +3 -1
- package/dist/components/markdown.d.ts +20 -1
- package/dist/components/message-timeline.d.ts +1 -1
- package/dist/components/model-policy-picker.d.ts +2 -0
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/sandbox-files.d.ts +8 -2
- package/dist/components/sandbox-workspace.d.ts +33 -3
- package/dist/components/session-chrome.d.ts +15 -3
- package/dist/components/workspace-dock.d.ts +6 -2
- package/dist/composer.js +5 -5
- package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
- package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
- package/dist/hooks/channel-cache.d.ts +23 -0
- package/dist/hooks/shared-machines-list.d.ts +14 -0
- package/dist/hooks/use-attached-browsers.d.ts +2 -1
- package/dist/hooks/use-channels.d.ts +1 -0
- package/dist/hooks/use-composer.d.ts +37 -9
- package/dist/hooks/use-machine-chip.d.ts +3 -3
- package/dist/hooks/use-machines.d.ts +19 -1
- package/dist/hooks/use-sandbox-files.d.ts +1 -0
- package/dist/hooks/use-session-background-commands.d.ts +14 -0
- package/dist/hooks/use-turn-queue.d.ts +12 -0
- package/dist/hooks/use-workspace-sessions.d.ts +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1096 -623
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +29 -181
- package/dist/interaction.js.map +1 -1
- package/dist/lib/interaction-errors.d.ts +3 -0
- package/dist/machines.d.ts +4 -2
- package/dist/machines.js +10 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/platform-activity-row-BJHUHXAT.js +169 -0
- package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
- package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
- package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +6 -3
- package/dist/realtime.js +36 -14
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.d.ts +1 -1
- package/dist/session-ui.js +6 -2
- package/dist/session.d.ts +2 -2
- package/dist/session.js +4 -4
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/platform-activity-row.d.ts +15 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/shared.d.ts +4 -2
- package/dist/timeline/types.d.ts +50 -5
- package/dist/types/machines.d.ts +2 -2
- package/package.json +3 -3
- package/src/client.ts +12 -1
- package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
- package/src/components/browser-viewer.tsx +361 -58
- package/src/components/chat-composer.tsx +1 -1
- package/src/components/composer.tsx +6 -6
- package/src/components/computer-viewer.tsx +368 -115
- package/src/components/enrollment-consent.tsx +64 -6
- package/src/components/file-browser.tsx +117 -2
- package/src/components/human-input-form.tsx +103 -59
- package/src/components/human-input-surface.tsx +1 -1
- package/src/components/machine-card.tsx +111 -0
- package/src/components/machine-input-display.ts +15 -0
- package/src/components/machines/machine-detail.tsx +47 -0
- package/src/components/machines/machine-operation-policy.tsx +179 -0
- package/src/components/machines-dashboard.tsx +6 -0
- package/src/components/markdown.tsx +174 -20
- package/src/components/message-timeline.tsx +215 -18
- package/src/components/model-policy-picker.tsx +9 -17
- package/src/components/pierre-file.tsx +1 -1
- package/src/components/queue-surface-implementation.tsx +82 -16
- package/src/components/sandbox-files.tsx +135 -19
- package/src/components/sandbox-workspace.tsx +204 -92
- package/src/components/session-chrome.tsx +415 -208
- package/src/components/session-status.tsx +1 -1
- package/src/components/user-message-body.tsx +168 -48
- package/src/components/workspace-dock.tsx +141 -175
- package/src/hooks/channel-cache.ts +104 -0
- package/src/hooks/shared-machines-list.ts +239 -0
- package/src/hooks/use-attached-browsers.ts +5 -1
- package/src/hooks/use-browser-frame-stream.ts +6 -1
- package/src/hooks/use-browser-session.ts +6 -4
- package/src/hooks/use-browser-sessions.ts +7 -1
- package/src/hooks/use-channels.ts +32 -10
- package/src/hooks/use-composer.ts +760 -291
- package/src/hooks/use-computer-frame-stream.ts +14 -9
- package/src/hooks/use-computer-session.ts +16 -14
- package/src/hooks/use-computer-sessions.ts +8 -1
- package/src/hooks/use-last-started-turn-policy.ts +1 -1
- package/src/hooks/use-machine-chip.ts +23 -6
- package/src/hooks/use-machines.ts +104 -6
- package/src/hooks/use-sandbox-files.ts +195 -140
- package/src/hooks/use-sandbox-git.ts +58 -29
- package/src/hooks/use-session-background-commands.ts +56 -0
- package/src/hooks/use-session-capabilities.ts +18 -0
- package/src/hooks/use-turn-queue.ts +111 -12
- package/src/hooks/use-workspace-sessions.ts +5 -1
- package/src/index.ts +26 -4
- package/src/lib/interaction-errors.ts +18 -0
- package/src/machines.ts +9 -1
- package/src/model-policy.ts +10 -2
- package/src/realtime/realtime-control.tsx +53 -17
- package/src/session-context.ts +1 -0
- package/src/session-ui.ts +6 -1
- package/src/session.ts +9 -1
- package/src/timeline/activity-rail.tsx +20 -75
- package/src/timeline/index.ts +4 -0
- package/src/timeline/platform-activity-row.tsx +201 -0
- package/src/timeline/projection.ts +529 -25
- package/src/timeline/screenshot-lightbox.tsx +48 -14
- package/src/timeline/shared.tsx +8 -2
- package/src/timeline/tool-renderers.tsx +104 -6
- package/src/timeline/turn-summary.tsx +6 -1
- package/src/timeline/types.ts +77 -2
- package/src/types/machines.ts +10 -0
- package/styles/compiled.css +115 -104
- package/dist/chunk-5UQPNLIO.js.map +0 -1
- package/dist/chunk-6KLX2RH5.js.map +0 -1
- package/dist/chunk-B6WPZSTT.js.map +0 -1
- package/dist/chunk-HA46CYQK.js.map +0 -1
- package/dist/chunk-IGC6A2ZI.js.map +0 -1
- package/dist/chunk-LLOLUHVG.js +0 -5937
- package/dist/chunk-LLOLUHVG.js.map +0 -1
- package/dist/chunk-MKPIAGVL.js.map +0 -1
- package/dist/chunk-R3AZOAB5.js.map +0 -1
- package/dist/chunk-XBZWOM2K.js.map +0 -1
- package/dist/chunk-Y7OEDUJ5.js.map +0 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
- /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, {
|
|
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
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
{
|
|
2426
|
-
<div
|
|
2427
|
-
|
|
2428
|
-
|
|
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-
|
|
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?.
|
|
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
|
|