@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.
Files changed (66) hide show
  1. package/dist/artifacts.d.ts +2 -0
  2. package/dist/artifacts.js +1391 -2
  3. package/dist/artifacts.js.map +1 -1
  4. package/dist/chunk-2TKI3WEG.js +73 -0
  5. package/dist/chunk-2TKI3WEG.js.map +1 -0
  6. package/dist/{chunk-M2HNWZYR.js → chunk-IHQII5WO.js} +69 -18
  7. package/dist/chunk-IHQII5WO.js.map +1 -0
  8. package/dist/{chunk-HUMIRRYI.js → chunk-IYCR4343.js} +19 -19
  9. package/dist/{chunk-HUMIRRYI.js.map → chunk-IYCR4343.js.map} +1 -1
  10. package/dist/{chunk-WS6D5BHH.js → chunk-QXJCNVSD.js} +2268 -2217
  11. package/dist/chunk-QXJCNVSD.js.map +1 -0
  12. package/dist/{chunk-BV5A4OTE.js → chunk-T5FRXMC3.js} +21 -2
  13. package/dist/{chunk-BV5A4OTE.js.map → chunk-T5FRXMC3.js.map} +1 -1
  14. package/dist/{chunk-BRFMGY4L.js → chunk-UGMKPUEM.js} +28 -3
  15. package/dist/chunk-UGMKPUEM.js.map +1 -0
  16. package/dist/components/artifacts/inline-html-document.d.ts +2 -0
  17. package/dist/components/artifacts/published-html-artifact-frame.d.ts +2 -0
  18. package/dist/components/artifacts/visualization-assets.gen.d.ts +2 -0
  19. package/dist/components/markdown.d.ts +13 -1
  20. package/dist/{computer-viewer-7TEZC6Y2.js → computer-viewer-AA4JH2AP.js} +2 -2
  21. package/dist/connect.js +1 -1
  22. package/dist/index.d.ts +2 -1
  23. package/dist/index.js +7 -4
  24. package/dist/index.js.map +1 -1
  25. package/dist/interaction.js +1 -1
  26. package/dist/{platform-activity-row-27LKAWZD.js → platform-activity-row-UW4UVVM4.js} +6 -3
  27. package/dist/platform-activity-row-UW4UVVM4.js.map +1 -0
  28. package/dist/session-ui.js +4 -3
  29. package/dist/session-ui.js.map +1 -1
  30. package/dist/session.js +1 -1
  31. package/dist/sites-ui.d.ts +2 -1
  32. package/dist/sites.js +12 -4
  33. package/dist/sites.js.map +1 -1
  34. package/dist/timeline/activity-rail.d.ts +1 -0
  35. package/dist/timeline/platform-activity-row.d.ts +1 -1
  36. package/dist/timeline/retained-image.d.ts +16 -0
  37. package/dist/timeline/rolling-activity.d.ts +9 -0
  38. package/dist/timeline/shared.d.ts +4 -1
  39. package/dist/timeline/turn-summary.d.ts +4 -1
  40. package/package.json +4 -4
  41. package/src/artifacts.ts +2 -0
  42. package/src/components/artifacts/inline-html-document.ts +24 -0
  43. package/src/components/artifacts/published-html-artifact-frame.tsx +32 -2
  44. package/src/components/artifacts/visualization-assets/base.css +1015 -0
  45. package/src/components/artifacts/visualization-assets/helpers.html +349 -0
  46. package/src/components/artifacts/visualization-assets.gen.ts +3 -0
  47. package/src/components/copy-button.tsx +7 -2
  48. package/src/components/markdown.tsx +111 -34
  49. package/src/components/message-timeline.tsx +48 -3
  50. package/src/hooks/use-computer-session.ts +36 -1
  51. package/src/index.ts +6 -1
  52. package/src/sites-ui.tsx +23 -6
  53. package/src/timeline/activity-rail.tsx +20 -3
  54. package/src/timeline/platform-activity-row.tsx +5 -2
  55. package/src/timeline/retained-image.ts +91 -0
  56. package/src/timeline/rolling-activity.tsx +83 -0
  57. package/src/timeline/shared.tsx +33 -12
  58. package/src/timeline/tool-renderers.tsx +7 -91
  59. package/src/timeline/turn-summary.tsx +21 -15
  60. package/styles/compiled.css +122 -0
  61. package/styles/index.css +49 -0
  62. package/dist/chunk-BRFMGY4L.js.map +0 -1
  63. package/dist/chunk-M2HNWZYR.js.map +0 -1
  64. package/dist/chunk-WS6D5BHH.js.map +0 -1
  65. package/dist/platform-activity-row-27LKAWZD.js.map +0 -1
  66. /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
- group.kind === "turn" ? !!(enter && !insideTurn && !turnDefaultOpen) : liveActivitySettle;
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 useNestedChip = nestClusterChips && activityShouldFold && !containsGeneratedImage;
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={!activityShouldFold || group.outcome === "failed" ? true : undefined}
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 target = frameTarget ?? focusTarget ?? currentObservation?.target ?? null;
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 { MarkdownProps, SandboxFileLocation } from "./components/markdown";
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 versionId = detail.artifact.currentVersion?.id;
46
- const content =
47
- versionId && (detail.artifact.status === "active" || options.includeArchivedContent)
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: detail.artifact.currentVersion!.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 { BodyNote, PayloadBlock, ActivityDisclosure, ToolCallTruncationProvider } from "./shared";
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
- preview: truncate(item.text, 110),
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
+ }
@@ -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
- className={cn(
194
- "min-w-0 shrink truncate text-og-base font-medium",
195
- titleMono && "font-og-mono text-og-sm font-normal",
196
- running && "og-shimmer-text",
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 { useEffect, useRef, useState, type ReactNode } from "react";
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
- <span className="size-1.5 shrink-0 animate-og-pulse rounded-full bg-og-status-running" />
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,