@opengeni/react 5.1.0-canary.0 → 5.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.
- package/dist/artifacts.d.ts +2 -0
- package/dist/artifacts.js +1391 -2
- package/dist/artifacts.js.map +1 -1
- package/dist/chunk-2TKI3WEG.js +73 -0
- package/dist/chunk-2TKI3WEG.js.map +1 -0
- package/dist/{chunk-M2HNWZYR.js → chunk-IHQII5WO.js} +69 -18
- package/dist/chunk-IHQII5WO.js.map +1 -0
- package/dist/{chunk-HUMIRRYI.js → chunk-IYCR4343.js} +19 -19
- package/dist/{chunk-HUMIRRYI.js.map → chunk-IYCR4343.js.map} +1 -1
- package/dist/{chunk-WS6D5BHH.js → chunk-QXJCNVSD.js} +2268 -2217
- package/dist/chunk-QXJCNVSD.js.map +1 -0
- package/dist/{chunk-BV5A4OTE.js → chunk-T5FRXMC3.js} +21 -2
- package/dist/{chunk-BV5A4OTE.js.map → chunk-T5FRXMC3.js.map} +1 -1
- package/dist/{chunk-BRFMGY4L.js → chunk-UGMKPUEM.js} +28 -3
- package/dist/chunk-UGMKPUEM.js.map +1 -0
- package/dist/components/artifacts/inline-html-document.d.ts +2 -0
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +2 -0
- package/dist/components/artifacts/visualization-assets.gen.d.ts +2 -0
- package/dist/components/markdown.d.ts +13 -1
- package/dist/{computer-viewer-7TEZC6Y2.js → computer-viewer-AA4JH2AP.js} +2 -2
- package/dist/connect.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +1 -1
- package/dist/{platform-activity-row-27LKAWZD.js → platform-activity-row-UW4UVVM4.js} +6 -3
- package/dist/platform-activity-row-UW4UVVM4.js.map +1 -0
- package/dist/session-ui.js +4 -3
- package/dist/session-ui.js.map +1 -1
- package/dist/session.js +1 -1
- package/dist/sites-ui.d.ts +2 -1
- package/dist/sites.js +12 -4
- package/dist/sites.js.map +1 -1
- package/dist/timeline/activity-rail.d.ts +1 -0
- package/dist/timeline/platform-activity-row.d.ts +1 -1
- package/dist/timeline/retained-image.d.ts +16 -0
- package/dist/timeline/rolling-activity.d.ts +9 -0
- package/dist/timeline/shared.d.ts +4 -1
- package/dist/timeline/turn-summary.d.ts +4 -1
- package/package.json +4 -4
- package/src/artifacts.ts +2 -0
- package/src/components/artifacts/inline-html-document.ts +24 -0
- package/src/components/artifacts/published-html-artifact-frame.tsx +32 -2
- package/src/components/artifacts/visualization-assets/base.css +1015 -0
- package/src/components/artifacts/visualization-assets/helpers.html +349 -0
- package/src/components/artifacts/visualization-assets.gen.ts +3 -0
- package/src/components/copy-button.tsx +7 -2
- package/src/components/markdown.tsx +111 -34
- package/src/components/message-timeline.tsx +48 -3
- package/src/hooks/use-computer-session.ts +36 -1
- package/src/index.ts +6 -1
- package/src/sites-ui.tsx +23 -6
- package/src/timeline/activity-rail.tsx +20 -3
- package/src/timeline/platform-activity-row.tsx +5 -2
- package/src/timeline/retained-image.ts +91 -0
- package/src/timeline/rolling-activity.tsx +83 -0
- package/src/timeline/shared.tsx +33 -12
- package/src/timeline/tool-renderers.tsx +7 -91
- package/src/timeline/turn-summary.tsx +21 -15
- package/styles/compiled.css +122 -0
- package/styles/index.css +49 -0
- package/dist/chunk-BRFMGY4L.js.map +0 -1
- package/dist/chunk-M2HNWZYR.js.map +0 -1
- package/dist/chunk-WS6D5BHH.js.map +0 -1
- package/dist/platform-activity-row-27LKAWZD.js.map +0 -1
- /package/dist/{computer-viewer-7TEZC6Y2.js.map → computer-viewer-AA4JH2AP.js.map} +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RollingActivity } from "../timeline/rolling-activity";
|
|
1
2
|
import { GenieLoadingOptionsContext, type GenieLoadingOptions } from "../timeline/genie-loading";
|
|
2
3
|
import { ChildSessionLink } from "./child-session-link";
|
|
3
4
|
import { useStartupDetails } from "../timeline/startup-preference";
|
|
@@ -2526,6 +2527,12 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2526
2527
|
const startupDetails = useStartupDetails();
|
|
2527
2528
|
const enter = useEntranceAnimation();
|
|
2528
2529
|
const settleChrome = useTurnSettleOpen();
|
|
2530
|
+
const previousSingleActivity = useRef<ActivityItem | undefined>(undefined);
|
|
2531
|
+
useLayoutEffect(() => {
|
|
2532
|
+
const work =
|
|
2533
|
+
group.kind === "activity" ? group.items.filter((item) => item.kind !== "startup-phase") : [];
|
|
2534
|
+
previousSingleActivity.current = work.length === 1 ? work[0] : undefined;
|
|
2535
|
+
}, [group]);
|
|
2529
2536
|
const foldMemory = useFoldMemory();
|
|
2530
2537
|
// Settled (or live-fold) activity clusters get a chip. Inside an expanded
|
|
2531
2538
|
// turn that is the second layer — quiet nested chips under the outer turn
|
|
@@ -2552,7 +2559,8 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2552
2559
|
);
|
|
2553
2560
|
}
|
|
2554
2561
|
const settleFold =
|
|
2555
|
-
|
|
2562
|
+
!turnSummary?.rolling &&
|
|
2563
|
+
(group.kind === "turn" ? !!(enter && !insideTurn && !turnDefaultOpen) : liveActivitySettle);
|
|
2556
2564
|
switch (group.kind) {
|
|
2557
2565
|
case "activity":
|
|
2558
2566
|
// Preparation is one quiet surface, not a fold with eight technical steps.
|
|
@@ -2578,6 +2586,24 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2578
2586
|
/>
|
|
2579
2587
|
);
|
|
2580
2588
|
}
|
|
2589
|
+
if (
|
|
2590
|
+
turnSummary?.rolling &&
|
|
2591
|
+
!startupDetails &&
|
|
2592
|
+
group.items.filter((item) => item.kind !== "startup-phase").length === 1
|
|
2593
|
+
) {
|
|
2594
|
+
return (
|
|
2595
|
+
<ActivityRail
|
|
2596
|
+
items={group.items}
|
|
2597
|
+
startupActive={false}
|
|
2598
|
+
bare
|
|
2599
|
+
toolRegistry={toolRegistry}
|
|
2600
|
+
onOpenSession={onOpenSession}
|
|
2601
|
+
onMemoryClick={onMemoryClick}
|
|
2602
|
+
loadRetainedScreenshot={loadRetainedScreenshot}
|
|
2603
|
+
loadRetainedArtifact={loadRetainedArtifact}
|
|
2604
|
+
/>
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2581
2607
|
if (insideTurn) {
|
|
2582
2608
|
// Nested chips whenever the parent has ≥2 clusters. During outer settle
|
|
2583
2609
|
// chrome they stay force-open so structure is visible and height stays
|
|
@@ -2589,7 +2615,13 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2589
2615
|
// settled closed pre-wrap was showing as a CHIP, so mounting it closed
|
|
2590
2616
|
// is both the stable height and the honest state — force-opening it
|
|
2591
2617
|
// was the "already-collapsed cluster auto-expands at the end" reopen.
|
|
2592
|
-
const
|
|
2618
|
+
const visibleActivity = group.items.filter((item) => item.kind !== "startup-phase");
|
|
2619
|
+
const singleThought =
|
|
2620
|
+
!startupDetails &&
|
|
2621
|
+
visibleActivity.length === 1 &&
|
|
2622
|
+
visibleActivity[0]?.kind === "reasoning";
|
|
2623
|
+
const useNestedChip =
|
|
2624
|
+
nestClusterChips && activityShouldFold && !containsGeneratedImage && !singleThought;
|
|
2593
2625
|
if (!useNestedChip) {
|
|
2594
2626
|
return (
|
|
2595
2627
|
<ActivityRail
|
|
@@ -2634,7 +2666,20 @@ const TimelineGroupView = memo(function TimelineGroupView({
|
|
|
2634
2666
|
items={group.items}
|
|
2635
2667
|
outcome={group.outcome}
|
|
2636
2668
|
failureText={group.failureText}
|
|
2637
|
-
defaultOpen={
|
|
2669
|
+
defaultOpen={
|
|
2670
|
+
group.outcome === "failed" || (!turnSummary?.rolling && !activityShouldFold)
|
|
2671
|
+
? true
|
|
2672
|
+
: undefined
|
|
2673
|
+
}
|
|
2674
|
+
liveHeader={
|
|
2675
|
+
turnSummary?.rolling && !group.outcome && !foldLiveCluster ? (
|
|
2676
|
+
<RollingActivity
|
|
2677
|
+
items={group.items}
|
|
2678
|
+
toolRegistry={toolRegistry}
|
|
2679
|
+
previousItem={previousSingleActivity.current}
|
|
2680
|
+
/>
|
|
2681
|
+
) : undefined
|
|
2682
|
+
}
|
|
2638
2683
|
foldKey={group.id}
|
|
2639
2684
|
facets={turnSummary?.facets}
|
|
2640
2685
|
settleFold={settleFold}
|
|
@@ -117,6 +117,33 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
|
|
|
117
117
|
if (!mountedRef.current || requestRef.current.id !== id) return;
|
|
118
118
|
const targets = sortComputerTargets(targetResponse.targets);
|
|
119
119
|
const selected = chooseTarget(targets, selectedTargetIdRef.current, session.platform);
|
|
120
|
+
// Discovery is useful independently of semantic observation. Publish it
|
|
121
|
+
// now so a slow/unresponsive application cannot block the frame stream
|
|
122
|
+
// or prevent the person from choosing a different window or screen.
|
|
123
|
+
const previous = observationRef.current.observation;
|
|
124
|
+
const retainedObservation =
|
|
125
|
+
selected &&
|
|
126
|
+
previous?.target.id === selected.id &&
|
|
127
|
+
previous.target.controllerGeneration === selected.controllerGeneration &&
|
|
128
|
+
previous.target.targetGeneration === selected.targetGeneration
|
|
129
|
+
? previous
|
|
130
|
+
: null;
|
|
131
|
+
selectedTargetIdRef.current = selected?.id ?? null;
|
|
132
|
+
targetsRef.current = { computerSessionId, targets };
|
|
133
|
+
observationRef.current = { computerSessionId, observation: retainedObservation };
|
|
134
|
+
setState((current) =>
|
|
135
|
+
current.computerSessionId === computerSessionId
|
|
136
|
+
? {
|
|
137
|
+
...current,
|
|
138
|
+
session,
|
|
139
|
+
targets,
|
|
140
|
+
selectedTargetId: selected?.id ?? null,
|
|
141
|
+
observation: retainedObservation,
|
|
142
|
+
loading: false,
|
|
143
|
+
error: null,
|
|
144
|
+
}
|
|
145
|
+
: current,
|
|
146
|
+
);
|
|
120
147
|
const observation = selected
|
|
121
148
|
? await client.observeComputerTarget(workspaceId, computerSessionId, selected.id, {
|
|
122
149
|
signal: controller.signal,
|
|
@@ -303,7 +330,15 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
|
|
|
303
330
|
? (targetsRef.current.targets.find((candidate) => candidate.id === frame.targetId) ??
|
|
304
331
|
(currentObservation?.target.id === frame.targetId ? currentObservation.target : null))
|
|
305
332
|
: null;
|
|
306
|
-
const
|
|
333
|
+
const inputTarget =
|
|
334
|
+
(action.type === "keyboard" || action.type === "clipboard") &&
|
|
335
|
+
targetsRef.current.computerSessionId === computerSessionId
|
|
336
|
+
? targetsRef.current.targets.find(
|
|
337
|
+
(candidate) => candidate.id === selectedTargetIdRef.current,
|
|
338
|
+
)
|
|
339
|
+
: null;
|
|
340
|
+
const target =
|
|
341
|
+
frameTarget ?? focusTarget ?? currentObservation?.target ?? inputTarget ?? null;
|
|
307
342
|
if (!target) throw new Error("The desktop target is not ready for input.");
|
|
308
343
|
if (frame && frame.targetId !== selectedTargetIdRef.current) {
|
|
309
344
|
throw new Error("The displayed desktop frame is no longer selected.");
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { retainedImageId } from "./components/markdown";
|
|
1
2
|
// oxlint-disable-next-line typescript/triple-slash-reference -- package consumers must load the ambient type for this optional untyped peer without emitting a runtime import.
|
|
2
3
|
/// <reference path="./types/external.d.ts" />
|
|
3
4
|
|
|
@@ -520,7 +521,11 @@ export {
|
|
|
520
521
|
sandboxFileLocationFromHref,
|
|
521
522
|
sandboxFilePathFromHref,
|
|
522
523
|
} from "./components/markdown";
|
|
523
|
-
export type {
|
|
524
|
+
export type {
|
|
525
|
+
MarkdownProps,
|
|
526
|
+
MarkdownInteractiveBlock,
|
|
527
|
+
SandboxFileLocation,
|
|
528
|
+
} from "./components/markdown";
|
|
524
529
|
export { CopyButton, CopyHoverFrame } from "./components/copy-button";
|
|
525
530
|
export { copyTextToClipboard, tableElementToTsv } from "./lib/clipboard";
|
|
526
531
|
export { SessionStatus, StatusDot, SESSION_STATUS_META } from "./components/session-status";
|
package/src/sites-ui.tsx
CHANGED
|
@@ -29,10 +29,11 @@ type SiteScope = { client: SiteClient; workspaceId: string; className?: string }
|
|
|
29
29
|
/** Shared native/embedded read boundary: pin content to the observed version,
|
|
30
30
|
* and never accept a response belonging to another workspace or Site. */
|
|
31
31
|
export async function loadSiteSnapshot(
|
|
32
|
-
client: Pick<SiteClient, "getWorkspaceArtifact" | "getWorkspaceArtifactHtml"
|
|
32
|
+
client: Pick<SiteClient, "getWorkspaceArtifact" | "getWorkspaceArtifactHtml"> &
|
|
33
|
+
Partial<Pick<OpenGeniClient, "getWorkspaceArtifactContent">>,
|
|
33
34
|
workspaceId: string,
|
|
34
35
|
siteId: string,
|
|
35
|
-
options: { signal?: AbortSignal; includeArchivedContent?: boolean } = {},
|
|
36
|
+
options: { signal?: AbortSignal; includeArchivedContent?: boolean; versionId?: string } = {},
|
|
36
37
|
): Promise<{
|
|
37
38
|
detail: WorkspaceArtifactDetailResponse;
|
|
38
39
|
content: SiteDisplayContent | null;
|
|
@@ -42,19 +43,35 @@ export async function loadSiteSnapshot(
|
|
|
42
43
|
options.signal?.throwIfAborted();
|
|
43
44
|
if (detail.artifact.id !== siteId || detail.artifact.workspaceId !== workspaceId)
|
|
44
45
|
throw new Error("Site scope mismatch");
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const selectedVersion = options.versionId
|
|
47
|
+
? detail.versions.find((version) => version.id === options.versionId)
|
|
48
|
+
: detail.artifact.currentVersion;
|
|
49
|
+
const versionId = options.versionId ?? selectedVersion?.id;
|
|
50
|
+
let content: SiteDisplayContent | null =
|
|
51
|
+
selectedVersion &&
|
|
52
|
+
versionId &&
|
|
53
|
+
(detail.artifact.status === "active" || options.includeArchivedContent)
|
|
48
54
|
? {
|
|
49
55
|
artifactId: siteId,
|
|
50
56
|
versionId,
|
|
51
|
-
requestedTools:
|
|
57
|
+
requestedTools: selectedVersion!.requestedTools,
|
|
52
58
|
html: await client.getWorkspaceArtifactHtml(workspaceId, siteId, {
|
|
53
59
|
...requestOptions,
|
|
54
60
|
versionId,
|
|
55
61
|
}),
|
|
56
62
|
}
|
|
57
63
|
: null;
|
|
64
|
+
if (
|
|
65
|
+
versionId &&
|
|
66
|
+
!selectedVersion &&
|
|
67
|
+
(detail.artifact.status === "active" || options.includeArchivedContent)
|
|
68
|
+
) {
|
|
69
|
+
if (!client.getWorkspaceArtifactContent) throw new Error("Site version unavailable");
|
|
70
|
+
content = await client.getWorkspaceArtifactContent(workspaceId, siteId, {
|
|
71
|
+
...requestOptions,
|
|
72
|
+
versionId,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
58
75
|
options.signal?.throwIfAborted();
|
|
59
76
|
if (content && (content.artifactId !== siteId || content.versionId !== versionId))
|
|
60
77
|
throw new Error("Site content mismatch");
|
|
@@ -2,7 +2,7 @@ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
|
2
2
|
import { GenieLoading } from "./genie-loading";
|
|
3
3
|
import { useStartupDetails } from "./startup-preference";
|
|
4
4
|
import { ArrowRightIcon, BotIcon, BrainCircuitIcon } from "lucide-react";
|
|
5
|
-
import { lazy, Suspense, useLayoutEffect, useRef, useState } from "react";
|
|
5
|
+
import { lazy, Suspense, useContext, useLayoutEffect, useRef, useState } from "react";
|
|
6
6
|
import { jsx as rowJsx, jsxs as rowJsxs } from "react/jsx-runtime";
|
|
7
7
|
import { Markdown } from "../components/markdown";
|
|
8
8
|
import { cn } from "../lib/cn";
|
|
@@ -11,7 +11,13 @@ import { defaultToolRegistry } from "./tool-renderers";
|
|
|
11
11
|
import { useEntranceAnimation, useEntranceAnimationLive } from "./entrance";
|
|
12
12
|
import type { RetainedArtifactLoader, RetainedScreenshotLoader, ToolRegistry } from "./registry";
|
|
13
13
|
import { useSeenActivityIds } from "./seen-activity-ids";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
BodyNote,
|
|
16
|
+
PayloadBlock,
|
|
17
|
+
ActivityDisclosure,
|
|
18
|
+
CompactActivityContext,
|
|
19
|
+
ToolCallTruncationProvider,
|
|
20
|
+
} from "./shared";
|
|
15
21
|
import { toolDisplayName } from "./tool-display-name";
|
|
16
22
|
import type { ActivityItem, MemoryItem, WorkerItem } from "./types";
|
|
17
23
|
|
|
@@ -214,7 +220,7 @@ function assertNever(item: never): never {
|
|
|
214
220
|
throw new Error(`ActivityRail: unhandled activity item ${JSON.stringify(item)}`);
|
|
215
221
|
}
|
|
216
222
|
|
|
217
|
-
function renderActivity(
|
|
223
|
+
export function renderActivity(
|
|
218
224
|
item: ActivityItem,
|
|
219
225
|
toolRegistry: ToolRegistry,
|
|
220
226
|
onOpenSession: ((sessionId: string) => void) | undefined,
|
|
@@ -380,6 +386,7 @@ function WorkerRow({
|
|
|
380
386
|
item: WorkerItem;
|
|
381
387
|
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
382
388
|
}) {
|
|
389
|
+
const compact = useContext(CompactActivityContext);
|
|
383
390
|
const running = item.status === "running";
|
|
384
391
|
const failed = item.status === "failed";
|
|
385
392
|
const cancelled = item.status === "cancelled";
|
|
@@ -399,6 +406,16 @@ function WorkerRow({
|
|
|
399
406
|
: cancelled
|
|
400
407
|
? "Worker interrupted"
|
|
401
408
|
: "Worker messaged";
|
|
409
|
+
if (compact) {
|
|
410
|
+
return (
|
|
411
|
+
<ActivityDisclosure
|
|
412
|
+
icon={<BotIcon className="size-3.5" />}
|
|
413
|
+
title={title}
|
|
414
|
+
preview={item.prompt}
|
|
415
|
+
running={running}
|
|
416
|
+
/>
|
|
417
|
+
);
|
|
418
|
+
}
|
|
402
419
|
// A worker is a first-class actor but still a STEP on the rail — a borderless
|
|
403
420
|
// row (no card), aligned to its sibling tool rows: the chevron column is an
|
|
404
421
|
// empty spacer (a worker doesn't expand), then the bot glyph, then the title
|
|
@@ -10,7 +10,6 @@ export default function PlatformActivityRow({
|
|
|
10
10
|
t: displayName,
|
|
11
11
|
b: BotIcon,
|
|
12
12
|
m: Markdown,
|
|
13
|
-
r: truncate,
|
|
14
13
|
j,
|
|
15
14
|
s,
|
|
16
15
|
}: {
|
|
@@ -35,7 +34,11 @@ export default function PlatformActivityRow({
|
|
|
35
34
|
children: "Thought",
|
|
36
35
|
}),
|
|
37
36
|
running: item.streaming,
|
|
38
|
-
|
|
37
|
+
compactPreview: item.text.replace(/\*\*|__|`/g, "").replace(/\s+/g, " "),
|
|
38
|
+
preview: j("span", {
|
|
39
|
+
className: "og-reasoning-preview",
|
|
40
|
+
children: j(Markdown, { streaming: item.streaming, children: item.text }),
|
|
41
|
+
}),
|
|
39
42
|
children: j("div", {
|
|
40
43
|
className: "text-og-base leading-6 text-og-fg-muted [&_strong]:text-og-fg-muted",
|
|
41
44
|
children: j(Markdown, { streaming: item.streaming, children: item.text }),
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import type { RetainedArtifactReference } from "@opengeni/sdk";
|
|
3
|
+
import type { ToolRendererProps } from "./registry";
|
|
4
|
+
type RetainedImageState =
|
|
5
|
+
| { kind: "loading" }
|
|
6
|
+
| { kind: "ready"; url: string }
|
|
7
|
+
| { kind: "unavailable"; label: string }
|
|
8
|
+
| { kind: "error"; message: string };
|
|
9
|
+
|
|
10
|
+
export function useRetainedImageObjectUrl(
|
|
11
|
+
artifact: RetainedArtifactReference,
|
|
12
|
+
load: ToolRendererProps["loadRetainedArtifact"],
|
|
13
|
+
): RetainedImageState {
|
|
14
|
+
const [state, setState] = useState<RetainedImageState>({ kind: "loading" });
|
|
15
|
+
// Function outputs are commonly serialized JSON. Parsing them creates a new
|
|
16
|
+
// object on every render, so depend on the immutable wire value rather than
|
|
17
|
+
// object identity; otherwise the loader can refetch after its own setState.
|
|
18
|
+
const artifactValue = retainedArtifactValue(artifact);
|
|
19
|
+
const stableArtifactRef = useRef({ value: artifactValue, artifact });
|
|
20
|
+
if (stableArtifactRef.current.value !== artifactValue) {
|
|
21
|
+
stableArtifactRef.current = { value: artifactValue, artifact };
|
|
22
|
+
}
|
|
23
|
+
const stableArtifact = stableArtifactRef.current.artifact;
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!load) {
|
|
26
|
+
setState({ kind: "unavailable", label: "retrieval is not configured" });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
let objectUrl: string | null = null;
|
|
31
|
+
setState({ kind: "loading" });
|
|
32
|
+
void load(stableArtifact, controller.signal)
|
|
33
|
+
.then((source) => {
|
|
34
|
+
if (controller.signal.aborted) return;
|
|
35
|
+
if (!source) {
|
|
36
|
+
setState({ kind: "unavailable", label: "bytes are unavailable" });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!(source instanceof Uint8Array)) {
|
|
40
|
+
if (!source.url.trim()) {
|
|
41
|
+
setState({ kind: "unavailable", label: "URL is unavailable" });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setState({ kind: "ready", url: source.url });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
objectUrl = URL.createObjectURL(
|
|
48
|
+
new Blob([source as unknown as BlobPart], { type: stableArtifact.contentType }),
|
|
49
|
+
);
|
|
50
|
+
setState({ kind: "ready", url: objectUrl });
|
|
51
|
+
})
|
|
52
|
+
.catch((error: unknown) => {
|
|
53
|
+
if (controller.signal.aborted) return;
|
|
54
|
+
const status =
|
|
55
|
+
error && typeof error === "object" && "status" in error
|
|
56
|
+
? Number((error as { status?: unknown }).status)
|
|
57
|
+
: null;
|
|
58
|
+
setState(
|
|
59
|
+
status === 404
|
|
60
|
+
? { kind: "unavailable", label: "deleted" }
|
|
61
|
+
: status === 410
|
|
62
|
+
? { kind: "unavailable", label: "expired or unavailable" }
|
|
63
|
+
: { kind: "error", message: "retrieval failed" },
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
return () => {
|
|
67
|
+
controller.abort();
|
|
68
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
69
|
+
};
|
|
70
|
+
}, [stableArtifact, load]);
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function retainedArtifactValue(artifact: RetainedArtifactReference): string {
|
|
75
|
+
return [
|
|
76
|
+
artifact.artifactId,
|
|
77
|
+
artifact.kind,
|
|
78
|
+
artifact.contentType,
|
|
79
|
+
artifact.originalBytes,
|
|
80
|
+
artifact.sha256,
|
|
81
|
+
artifact.retainedAt,
|
|
82
|
+
artifact.dimensions?.width ?? "",
|
|
83
|
+
artifact.dimensions?.height ?? "",
|
|
84
|
+
artifact.retention.policy,
|
|
85
|
+
artifact.retention.expiresAt ?? "",
|
|
86
|
+
artifact.retrieval.method,
|
|
87
|
+
artifact.retrieval.path,
|
|
88
|
+
artifact.retrieval.acceptRanges,
|
|
89
|
+
artifact.retrieval.maxRangeBytes,
|
|
90
|
+
].join("\0");
|
|
91
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { renderActivity } from "./activity-rail";
|
|
2
|
+
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
3
|
+
import { WrenchIcon } from "lucide-react";
|
|
4
|
+
import { Suspense, useEffect, useState } from "react";
|
|
5
|
+
import { CompactActivityContext } from "./shared";
|
|
6
|
+
import { defaultToolRegistry } from "./tool-renderers";
|
|
7
|
+
import type { ToolRegistry } from "./registry";
|
|
8
|
+
import { toolDisplayName } from "./tool-display-name";
|
|
9
|
+
import type { ActivityItem } from "./types";
|
|
10
|
+
|
|
11
|
+
/** One stable viewport; updates replace its content without moving the conversation. */
|
|
12
|
+
export function RollingActivity({
|
|
13
|
+
items,
|
|
14
|
+
toolRegistry = defaultToolRegistry,
|
|
15
|
+
previousItem,
|
|
16
|
+
}: {
|
|
17
|
+
items: ActivityItem[];
|
|
18
|
+
/** The standalone item visible immediately before this reel mounted. */
|
|
19
|
+
previousItem?: ActivityItem | undefined;
|
|
20
|
+
toolRegistry?: ToolRegistry;
|
|
21
|
+
}) {
|
|
22
|
+
const reduced = useReducedMotion();
|
|
23
|
+
const [mounted, setMounted] = useState(false);
|
|
24
|
+
useEffect(() => setMounted(true), []);
|
|
25
|
+
const work = items.filter((item) => item.kind !== "startup-phase");
|
|
26
|
+
const active = work.filter((item) =>
|
|
27
|
+
item.kind === "reasoning" ? item.streaming : "status" in item && item.status === "running",
|
|
28
|
+
);
|
|
29
|
+
// Advance with the event order; finishing a parallel tool must not replay an older one.
|
|
30
|
+
const item = !mounted && previousItem ? previousItem : work.at(-1);
|
|
31
|
+
if (!item) return null;
|
|
32
|
+
const earlierCount = Math.max(
|
|
33
|
+
0,
|
|
34
|
+
work.findIndex((entry) => entry.id === item.id),
|
|
35
|
+
);
|
|
36
|
+
const fallback = (
|
|
37
|
+
<span className="og-rolling-label">
|
|
38
|
+
<WrenchIcon className="size-3.5" />
|
|
39
|
+
<span>{item.kind === "tool-call" ? toolDisplayName(item.name) : "Working"}</span>
|
|
40
|
+
</span>
|
|
41
|
+
);
|
|
42
|
+
return (
|
|
43
|
+
<span
|
|
44
|
+
className="og-rolling-status"
|
|
45
|
+
data-running={active.some((entry) => entry.id === item.id) ? "true" : undefined}
|
|
46
|
+
>
|
|
47
|
+
<span className="sr-only">
|
|
48
|
+
{item.kind === "tool-call"
|
|
49
|
+
? toolDisplayName(item.name)
|
|
50
|
+
: item.kind === "reasoning"
|
|
51
|
+
? "Thinking"
|
|
52
|
+
: "Working"}
|
|
53
|
+
</span>
|
|
54
|
+
<span className="og-rolling-window" aria-hidden="true">
|
|
55
|
+
<AnimatePresence initial={false} mode="sync">
|
|
56
|
+
<motion.span
|
|
57
|
+
key={item.id}
|
|
58
|
+
className="og-rolling-face"
|
|
59
|
+
initial={{
|
|
60
|
+
opacity: 0,
|
|
61
|
+
y: reduced ? 0 : 24,
|
|
62
|
+
}}
|
|
63
|
+
animate={{ opacity: 1, y: 0 }}
|
|
64
|
+
exit={{
|
|
65
|
+
opacity: 0,
|
|
66
|
+
y: reduced ? 0 : -24,
|
|
67
|
+
}}
|
|
68
|
+
transition={{ duration: reduced ? 0 : 0.4, ease: [0.22, 1, 0.36, 1] }}
|
|
69
|
+
>
|
|
70
|
+
<CompactActivityContext.Provider value={true}>
|
|
71
|
+
<Suspense fallback={fallback}>
|
|
72
|
+
{renderActivity(item, toolRegistry, undefined, undefined, undefined, undefined)}
|
|
73
|
+
</Suspense>
|
|
74
|
+
</CompactActivityContext.Provider>
|
|
75
|
+
</motion.span>
|
|
76
|
+
</AnimatePresence>
|
|
77
|
+
</span>
|
|
78
|
+
{earlierCount > 0 ? (
|
|
79
|
+
<span className="og-rolling-count">{`+${earlierCount} earlier`}</span>
|
|
80
|
+
) : null}
|
|
81
|
+
</span>
|
|
82
|
+
);
|
|
83
|
+
}
|
package/src/timeline/shared.tsx
CHANGED
|
@@ -55,6 +55,8 @@ export function ToolCallTruncationProvider({
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export type ActivityDisclosureProps = {
|
|
58
|
+
/** Override the rolling preview; null keeps rapidly streaming details out of the header. */
|
|
59
|
+
compactPreview?: ReactNode;
|
|
58
60
|
icon: ReactNode;
|
|
59
61
|
/** Icon tint. Defaults to the muted foreground; renderers pass accent/failed. */
|
|
60
62
|
iconTone?: "accent" | "failed" | "running" | "muted" | undefined;
|
|
@@ -116,6 +118,8 @@ const ICON_TONE: Record<NonNullable<ActivityDisclosureProps["iconTone"]>, string
|
|
|
116
118
|
* inline media, and at most one right-gutter settle chip. Compact by default;
|
|
117
119
|
* the body mounts only when expanded.
|
|
118
120
|
*/
|
|
121
|
+
export const CompactActivityContext = createContext(false);
|
|
122
|
+
|
|
119
123
|
export function ActivityDisclosure({
|
|
120
124
|
icon,
|
|
121
125
|
iconTone: iconToneProp = "muted",
|
|
@@ -123,6 +127,7 @@ export function ActivityDisclosure({
|
|
|
123
127
|
titleMono,
|
|
124
128
|
running,
|
|
125
129
|
preview,
|
|
130
|
+
compactPreview,
|
|
126
131
|
media,
|
|
127
132
|
chip: chipProp,
|
|
128
133
|
failed,
|
|
@@ -150,6 +155,21 @@ export function ActivityDisclosure({
|
|
|
150
155
|
const forcedDefaultOpen = useForcedDefaultOpen();
|
|
151
156
|
const [open, setOpen] = useState(defaultOpen ?? forcedDefaultOpen ?? false);
|
|
152
157
|
const truncation = useContext(ToolCallTruncationContext);
|
|
158
|
+
const compact = useContext(CompactActivityContext);
|
|
159
|
+
if (compact)
|
|
160
|
+
return (
|
|
161
|
+
<span className="og-rolling-label">
|
|
162
|
+
<span className={cn("shrink-0", ICON_TONE[iconTone])}>{icon}</span>
|
|
163
|
+
<span className="og-command-reel">
|
|
164
|
+
<span className="og-reel-title">{title}</span>
|
|
165
|
+
{(compactPreview === undefined ? preview : compactPreview) ? (
|
|
166
|
+
<span className="og-reel-preview text-og-fg-subtle">
|
|
167
|
+
{compactPreview === undefined ? preview : compactPreview}
|
|
168
|
+
</span>
|
|
169
|
+
) : null}
|
|
170
|
+
</span>
|
|
171
|
+
</span>
|
|
172
|
+
);
|
|
153
173
|
const hasBody = expandable && (children != null || truncation != null);
|
|
154
174
|
|
|
155
175
|
// The preview is detail-on-demand: it is suppressed once the row is open so a
|
|
@@ -189,20 +209,21 @@ export function ActivityDisclosure({
|
|
|
189
209
|
<span className="size-3.5 shrink-0" />
|
|
190
210
|
)}
|
|
191
211
|
<span className={cn("shrink-0", ICON_TONE[iconTone])}>{icon}</span>
|
|
192
|
-
<span
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
212
|
+
<span className={cn("og-command-reel", running && "og-command-reel-running")}>
|
|
213
|
+
<span
|
|
214
|
+
className={cn(
|
|
215
|
+
"min-w-0 shrink truncate text-og-base font-medium",
|
|
216
|
+
titleMono && "font-og-mono text-og-sm font-normal",
|
|
217
|
+
)}
|
|
218
|
+
>
|
|
219
|
+
{title}
|
|
220
|
+
</span>
|
|
221
|
+
{previewVisible && !media ? (
|
|
222
|
+
<span className="min-w-0 flex-1 truncate text-og-sm text-og-fg-subtle">{preview}</span>
|
|
223
|
+
) : (
|
|
224
|
+
<span className="flex-1" />
|
|
197
225
|
)}
|
|
198
|
-
>
|
|
199
|
-
{title}
|
|
200
226
|
</span>
|
|
201
|
-
{previewVisible && !media ? (
|
|
202
|
-
<span className="min-w-0 flex-1 truncate text-og-sm text-og-fg-subtle">{preview}</span>
|
|
203
|
-
) : (
|
|
204
|
-
<span className="flex-1" />
|
|
205
|
-
)}
|
|
206
227
|
{/* The right gutter carries at most ONE signal: the media thumbnail, else a
|
|
207
228
|
terse settle chip (hidden once expanded — the body owns the detail). */}
|
|
208
229
|
{media && !open ? (
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useRetainedImageObjectUrl } from "./retained-image";
|
|
1
2
|
import {
|
|
2
3
|
parseSandboxFileArtifactReceipt,
|
|
3
4
|
type GitFileDiff,
|
|
@@ -34,7 +35,7 @@ import {
|
|
|
34
35
|
VideoIcon,
|
|
35
36
|
WrenchIcon,
|
|
36
37
|
} from "lucide-react";
|
|
37
|
-
import {
|
|
38
|
+
import { useContext, useState, type ReactNode } from "react";
|
|
38
39
|
import { formatBytes, stringifyPayload, tryParseJson } from "../lib/format";
|
|
39
40
|
import { useTimelineComputeLabel } from "./compute-label";
|
|
40
41
|
import {
|
|
@@ -71,6 +72,7 @@ import {
|
|
|
71
72
|
TermBlock,
|
|
72
73
|
Thumbnail,
|
|
73
74
|
ActivityDisclosure,
|
|
75
|
+
CompactActivityContext,
|
|
74
76
|
type DisclosureChip,
|
|
75
77
|
} from "./shared";
|
|
76
78
|
import { RawPatch, ToolDiff } from "./tool-diff";
|
|
@@ -96,9 +98,12 @@ const ICON_SIZE = "size-3.5";
|
|
|
96
98
|
* left-to-right.
|
|
97
99
|
*/
|
|
98
100
|
function RunningPreview({ children }: { children: ReactNode }) {
|
|
101
|
+
const compact = useContext(CompactActivityContext);
|
|
99
102
|
return (
|
|
100
103
|
<span className="inline-flex items-center gap-1.5">
|
|
101
|
-
|
|
104
|
+
{!compact && (
|
|
105
|
+
<span className="size-1.5 shrink-0 animate-og-pulse rounded-full bg-og-status-running" />
|
|
106
|
+
)}
|
|
102
107
|
<span className="min-w-0 truncate">{children}</span>
|
|
103
108
|
</span>
|
|
104
109
|
);
|
|
@@ -766,95 +771,6 @@ function ComputerCallRenderer({ item, loadRetainedScreenshot }: ToolRendererProp
|
|
|
766
771
|
);
|
|
767
772
|
}
|
|
768
773
|
|
|
769
|
-
type RetainedImageState =
|
|
770
|
-
| { kind: "loading" }
|
|
771
|
-
| { kind: "ready"; url: string }
|
|
772
|
-
| { kind: "unavailable"; label: string }
|
|
773
|
-
| { kind: "error"; message: string };
|
|
774
|
-
|
|
775
|
-
function useRetainedImageObjectUrl(
|
|
776
|
-
artifact: RetainedArtifactReference,
|
|
777
|
-
load: ToolRendererProps["loadRetainedArtifact"],
|
|
778
|
-
): RetainedImageState {
|
|
779
|
-
const [state, setState] = useState<RetainedImageState>({ kind: "loading" });
|
|
780
|
-
// Function outputs are commonly serialized JSON. Parsing them creates a new
|
|
781
|
-
// object on every render, so depend on the immutable wire value rather than
|
|
782
|
-
// object identity; otherwise the loader can refetch after its own setState.
|
|
783
|
-
const artifactValue = retainedArtifactValue(artifact);
|
|
784
|
-
const stableArtifactRef = useRef({ value: artifactValue, artifact });
|
|
785
|
-
if (stableArtifactRef.current.value !== artifactValue) {
|
|
786
|
-
stableArtifactRef.current = { value: artifactValue, artifact };
|
|
787
|
-
}
|
|
788
|
-
const stableArtifact = stableArtifactRef.current.artifact;
|
|
789
|
-
useEffect(() => {
|
|
790
|
-
if (!load) {
|
|
791
|
-
setState({ kind: "unavailable", label: "retrieval is not configured" });
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
const controller = new AbortController();
|
|
795
|
-
let objectUrl: string | null = null;
|
|
796
|
-
setState({ kind: "loading" });
|
|
797
|
-
void load(stableArtifact, controller.signal)
|
|
798
|
-
.then((source) => {
|
|
799
|
-
if (controller.signal.aborted) return;
|
|
800
|
-
if (!source) {
|
|
801
|
-
setState({ kind: "unavailable", label: "bytes are unavailable" });
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
|
-
if (!(source instanceof Uint8Array)) {
|
|
805
|
-
if (!source.url.trim()) {
|
|
806
|
-
setState({ kind: "unavailable", label: "URL is unavailable" });
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
setState({ kind: "ready", url: source.url });
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
objectUrl = URL.createObjectURL(
|
|
813
|
-
new Blob([source as unknown as BlobPart], { type: stableArtifact.contentType }),
|
|
814
|
-
);
|
|
815
|
-
setState({ kind: "ready", url: objectUrl });
|
|
816
|
-
})
|
|
817
|
-
.catch((error: unknown) => {
|
|
818
|
-
if (controller.signal.aborted) return;
|
|
819
|
-
const status =
|
|
820
|
-
error && typeof error === "object" && "status" in error
|
|
821
|
-
? Number((error as { status?: unknown }).status)
|
|
822
|
-
: null;
|
|
823
|
-
setState(
|
|
824
|
-
status === 404
|
|
825
|
-
? { kind: "unavailable", label: "deleted" }
|
|
826
|
-
: status === 410
|
|
827
|
-
? { kind: "unavailable", label: "expired or unavailable" }
|
|
828
|
-
: { kind: "error", message: "retrieval failed" },
|
|
829
|
-
);
|
|
830
|
-
});
|
|
831
|
-
return () => {
|
|
832
|
-
controller.abort();
|
|
833
|
-
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
834
|
-
};
|
|
835
|
-
}, [stableArtifact, load]);
|
|
836
|
-
return state;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
function retainedArtifactValue(artifact: RetainedArtifactReference): string {
|
|
840
|
-
return [
|
|
841
|
-
artifact.artifactId,
|
|
842
|
-
artifact.kind,
|
|
843
|
-
artifact.contentType,
|
|
844
|
-
artifact.originalBytes,
|
|
845
|
-
artifact.sha256,
|
|
846
|
-
artifact.retainedAt,
|
|
847
|
-
artifact.dimensions?.width ?? "",
|
|
848
|
-
artifact.dimensions?.height ?? "",
|
|
849
|
-
artifact.retention.policy,
|
|
850
|
-
artifact.retention.expiresAt ?? "",
|
|
851
|
-
artifact.retrieval.method,
|
|
852
|
-
artifact.retrieval.path,
|
|
853
|
-
artifact.retrieval.acceptRanges,
|
|
854
|
-
artifact.retrieval.maxRangeBytes,
|
|
855
|
-
].join("\0");
|
|
856
|
-
}
|
|
857
|
-
|
|
858
774
|
function RetainedSessionImageDisclosure({
|
|
859
775
|
artifact,
|
|
860
776
|
load,
|