@opengeni/react 0.12.0 → 0.15.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 (94) hide show
  1. package/README.md +20 -14
  2. package/dist/chunk-TOJR776I.js +2280 -0
  3. package/dist/chunk-TOJR776I.js.map +1 -0
  4. package/dist/index.d.ts +1040 -363
  5. package/dist/index.js +10938 -6281
  6. package/dist/index.js.map +1 -1
  7. package/dist/{machines-BeZ3bD3t.d.ts → machines-BpdwuQcD.d.ts} +134 -14
  8. package/dist/machines.d.ts +1 -1
  9. package/dist/machines.js +23 -1
  10. package/package.json +20 -12
  11. package/src/approvals.ts +16 -4
  12. package/src/client.ts +41 -5
  13. package/src/commands/index.ts +1 -7
  14. package/src/commands/registry.ts +46 -12
  15. package/src/components/chat-composer.tsx +417 -69
  16. package/src/components/code-editor.tsx +28 -36
  17. package/src/components/command-palette.tsx +26 -5
  18. package/src/components/desktop-viewer.tsx +29 -16
  19. package/src/components/diff-view.tsx +27 -189
  20. package/src/components/enrollment-consent.tsx +28 -10
  21. package/src/components/enrollment-device-flow.tsx +8 -5
  22. package/src/components/file-browser.tsx +190 -56
  23. package/src/components/fleet-tile.tsx +13 -4
  24. package/src/components/machine-card.tsx +93 -15
  25. package/src/components/machine-dock-bar.tsx +20 -8
  26. package/src/components/machine-health-pill.tsx +68 -0
  27. package/src/components/machine-metrics.tsx +28 -19
  28. package/src/components/machine-status-pill.tsx +26 -5
  29. package/src/components/machines/health.ts +146 -0
  30. package/src/components/machines/machine-detail.tsx +220 -0
  31. package/src/components/machines/metric-history-chart.tsx +298 -0
  32. package/src/components/machines/metric-sparkline.tsx +76 -0
  33. package/src/components/machines/series.ts +113 -0
  34. package/src/components/machines-dashboard.tsx +21 -3
  35. package/src/components/markdown.tsx +39 -9
  36. package/src/components/message-timeline.tsx +633 -109
  37. package/src/components/pierre-diff.tsx +83 -15
  38. package/src/components/pierre-file.tsx +10 -9
  39. package/src/components/queue-surface.tsx +578 -0
  40. package/src/components/sandbox-files.tsx +193 -225
  41. package/src/components/sandbox-terminal.tsx +256 -88
  42. package/src/components/sandbox-workspace.tsx +957 -0
  43. package/src/components/session-status.tsx +22 -2
  44. package/src/components/workbench-changes.tsx +585 -0
  45. package/src/components/workspace-dock.tsx +213 -59
  46. package/src/hooks/internal.ts +5 -2
  47. package/src/hooks/use-available-models.ts +7 -2
  48. package/src/hooks/use-billing-usage.ts +4 -1
  49. package/src/hooks/use-codex-accounts.ts +74 -44
  50. package/src/hooks/use-composer.ts +414 -63
  51. package/src/hooks/use-environments.ts +92 -47
  52. package/src/hooks/use-file-attachments.ts +101 -55
  53. package/src/hooks/use-goal.ts +52 -16
  54. package/src/hooks/use-machine-chip.ts +134 -0
  55. package/src/hooks/use-machines.ts +34 -16
  56. package/src/hooks/use-packs.ts +24 -19
  57. package/src/hooks/use-relay-frame-stream.ts +5 -2
  58. package/src/hooks/use-rigs.ts +335 -0
  59. package/src/hooks/use-sandbox-files.ts +213 -39
  60. package/src/hooks/use-sandbox-git.ts +188 -11
  61. package/src/hooks/use-sandbox-terminal.ts +18 -14
  62. package/src/hooks/use-scheduled-tasks.ts +10 -2
  63. package/src/hooks/use-session-capabilities.ts +77 -20
  64. package/src/hooks/use-session-control.ts +50 -23
  65. package/src/hooks/use-session-events.ts +50 -21
  66. package/src/hooks/use-session-lineage.ts +128 -0
  67. package/src/hooks/use-session.ts +53 -30
  68. package/src/hooks/use-slash-commands.ts +6 -4
  69. package/src/hooks/use-turn-queue.ts +202 -147
  70. package/src/hooks/use-windowed-sections.ts +204 -0
  71. package/src/hooks/use-workspace-capture.ts +233 -0
  72. package/src/hooks/use-workspace-edit.ts +231 -0
  73. package/src/hooks/use-workspace-sessions.ts +48 -5
  74. package/src/hooks/use-workspaces.ts +18 -15
  75. package/src/index.ts +133 -23
  76. package/src/lib/format.ts +3 -3
  77. package/src/lib/xterm-renderer.ts +65 -0
  78. package/src/lib/xterm-theme.ts +224 -18
  79. package/src/machines.ts +29 -3
  80. package/src/provider.tsx +195 -6
  81. package/src/timeline/activity-rail.tsx +213 -54
  82. package/src/timeline/disclosure-context.tsx +12 -2
  83. package/src/timeline/index.ts +21 -2
  84. package/src/timeline/parsers.ts +87 -16
  85. package/src/timeline/projection.ts +412 -87
  86. package/src/timeline/shared.tsx +111 -24
  87. package/src/timeline/tool-diff.tsx +24 -20
  88. package/src/timeline/tool-renderers.tsx +98 -21
  89. package/src/timeline/turn-summary.tsx +76 -23
  90. package/src/timeline/types.ts +96 -12
  91. package/styles/index.css +22 -0
  92. package/styles/tokens.css +2 -2
  93. package/dist/chunk-5TIXRCSI.js +0 -1291
  94. package/dist/chunk-5TIXRCSI.js.map +0 -1
package/src/provider.tsx CHANGED
@@ -1,16 +1,44 @@
1
- import { createContext, useContext, useMemo, type ReactNode } from "react";
1
+ import {
2
+ OpenGeniApiContractMismatchError,
3
+ OPENGENI_API_CONTRACT_REVISION,
4
+ type StreamConnectionState,
5
+ type WorkspaceControlEvent,
6
+ } from "@opengeni/sdk";
7
+ import {
8
+ createContext,
9
+ useCallback,
10
+ useContext,
11
+ useEffect,
12
+ useMemo,
13
+ useRef,
14
+ useState,
15
+ type ReactNode,
16
+ } from "react";
2
17
  import type { SessionClientLike } from "./client";
3
18
 
4
19
  export type OpenGeniContextValue = {
5
20
  client: SessionClientLike;
6
21
  workspaceId: string;
22
+ workspaceControlEvent: WorkspaceControlEvent | null;
23
+ workspaceControlConnectionState: StreamConnectionState | "idle" | "error";
24
+ registerSessionReconciler: (
25
+ sessionId: string,
26
+ key: string,
27
+ reconcile: () => Promise<void>,
28
+ ) => () => void;
29
+ reconcileSession: (sessionId: string) => Promise<void>;
7
30
  };
8
31
 
9
32
  const OpenGeniContext = createContext<OpenGeniContextValue | null>(null);
33
+ const NOOP_REGISTER_RECONCILER: OpenGeniContextValue["registerSessionReconciler"] = () => () =>
34
+ undefined;
35
+ const NOOP_RECONCILE_SESSION: OpenGeniContextValue["reconcileSession"] = async () => undefined;
36
+ const CONTRACT_RELOAD_STORAGE_PREFIX = "opengeni.reloadForApiContract:";
10
37
 
11
38
  export type OpenGeniProviderProps = {
12
39
  client: SessionClientLike;
13
40
  workspaceId: string;
41
+ onWorkspaceControlEvent?: ((event: WorkspaceControlEvent) => void) | undefined;
14
42
  children?: ReactNode;
15
43
  };
16
44
 
@@ -18,9 +46,161 @@ export type OpenGeniProviderProps = {
18
46
  * Supplies the OpenGeni client + workspace to all hooks below it. Hooks also
19
47
  * accept `{ client, workspaceId }` overrides per call for multi-workspace UIs.
20
48
  */
21
- export function OpenGeniProvider({ client, workspaceId, children }: OpenGeniProviderProps) {
22
- const value = useMemo(() => ({ client, workspaceId }), [client, workspaceId]);
23
- return <OpenGeniContext.Provider value={value}>{children}</OpenGeniContext.Provider>;
49
+ export function OpenGeniProvider({
50
+ client,
51
+ workspaceId,
52
+ onWorkspaceControlEvent,
53
+ children,
54
+ }: OpenGeniProviderProps) {
55
+ const [workspaceControlEvent, setWorkspaceControlEvent] = useState<WorkspaceControlEvent | null>(
56
+ null,
57
+ );
58
+ const [workspaceControlConnectionState, setWorkspaceControlConnectionState] = useState<
59
+ StreamConnectionState | "idle" | "error"
60
+ >("idle");
61
+ const [contractMismatch, setContractMismatch] = useState<OpenGeniApiContractMismatchError | null>(
62
+ null,
63
+ );
64
+ const callbackRef = useRef(onWorkspaceControlEvent);
65
+ const reconcilersRef = useRef(new Map<string, Map<string, () => Promise<void>>>());
66
+ callbackRef.current = onWorkspaceControlEvent;
67
+
68
+ const verifyApiContract = useCallback(async (): Promise<void> => {
69
+ try {
70
+ const config = await client.getClientConfig();
71
+ if (config.apiContractRevision !== OPENGENI_API_CONTRACT_REVISION) {
72
+ throw new OpenGeniApiContractMismatchError(
73
+ OPENGENI_API_CONTRACT_REVISION,
74
+ String(config.apiContractRevision || "(missing)"),
75
+ );
76
+ }
77
+ } catch (error) {
78
+ if (error instanceof OpenGeniApiContractMismatchError) {
79
+ setContractMismatch(error);
80
+ reloadForContractMismatchOnce(error);
81
+ }
82
+ throw error;
83
+ }
84
+ }, [client]);
85
+
86
+ const registerSessionReconciler = useMemo(
87
+ () =>
88
+ (sessionId: string, key: string, reconcile: () => Promise<void>): (() => void) => {
89
+ const sessionReconcilers = reconcilersRef.current.get(sessionId) ?? new Map();
90
+ sessionReconcilers.set(key, reconcile);
91
+ reconcilersRef.current.set(sessionId, sessionReconcilers);
92
+ return () => {
93
+ const current = reconcilersRef.current.get(sessionId);
94
+ current?.delete(key);
95
+ if (current?.size === 0) reconcilersRef.current.delete(sessionId);
96
+ };
97
+ },
98
+ [],
99
+ );
100
+ const reconcileSession = useMemo(
101
+ () =>
102
+ async (sessionId: string): Promise<void> => {
103
+ // This read also crosses the exact API-contract handshake before stale
104
+ // state can be presented as live after a deployment.
105
+ await verifyApiContract();
106
+ const callbacks = [...(reconcilersRef.current.get(sessionId)?.values() ?? [])];
107
+ await Promise.all(callbacks.map((reconcile) => reconcile()));
108
+ },
109
+ [verifyApiContract],
110
+ );
111
+
112
+ useEffect(() => {
113
+ const controller = new AbortController();
114
+ setWorkspaceControlEvent(null);
115
+ setWorkspaceControlConnectionState("connecting");
116
+ void (async () => {
117
+ try {
118
+ await verifyApiContract();
119
+ const workspace = await client.getWorkspace(workspaceId);
120
+ const stream = client.streamWorkspaceControlEvents(workspaceId, {
121
+ after: workspace.inferenceControl.revision,
122
+ signal: controller.signal,
123
+ onStateChange: setWorkspaceControlConnectionState,
124
+ });
125
+ for await (const event of stream) {
126
+ if (controller.signal.aborted) return;
127
+ setWorkspaceControlEvent((current) =>
128
+ !current || event.sequence > current.sequence ? event : current,
129
+ );
130
+ callbackRef.current?.(event);
131
+ }
132
+ } catch (error) {
133
+ if (error instanceof OpenGeniApiContractMismatchError) {
134
+ setContractMismatch(error);
135
+ reloadForContractMismatchOnce(error);
136
+ }
137
+ if (!controller.signal.aborted) setWorkspaceControlConnectionState("error");
138
+ }
139
+ })();
140
+ return () => controller.abort();
141
+ }, [client, verifyApiContract, workspaceId]);
142
+
143
+ const value = useMemo(
144
+ () => ({
145
+ client,
146
+ workspaceId,
147
+ workspaceControlEvent,
148
+ workspaceControlConnectionState,
149
+ registerSessionReconciler,
150
+ reconcileSession,
151
+ }),
152
+ [
153
+ client,
154
+ workspaceId,
155
+ workspaceControlEvent,
156
+ workspaceControlConnectionState,
157
+ registerSessionReconciler,
158
+ reconcileSession,
159
+ ],
160
+ );
161
+ return (
162
+ <OpenGeniContext.Provider value={value}>
163
+ {children}
164
+ {contractMismatch ? <ApiContractMismatchScreen mismatch={contractMismatch} /> : null}
165
+ </OpenGeniContext.Provider>
166
+ );
167
+ }
168
+
169
+ function ApiContractMismatchScreen({ mismatch }: { mismatch: OpenGeniApiContractMismatchError }) {
170
+ return (
171
+ <div
172
+ className="og-root fixed inset-0 z-[2147483647] grid place-items-center bg-og-bg/95 p-6 backdrop-blur-sm"
173
+ role="alert"
174
+ aria-live="assertive"
175
+ data-opengeni-api-contract-mismatch
176
+ >
177
+ <div className="w-full max-w-md rounded-xl border border-og-border bg-og-surface p-6 shadow-2xl">
178
+ <p className="text-sm font-semibold text-og-fg">OpenGeni updated</p>
179
+ <p className="mt-2 text-sm leading-6 text-og-muted">
180
+ This tab cannot safely continue with the new server version. Reload it before sending or
181
+ controlling work.
182
+ </p>
183
+ <p className="mt-3 font-mono text-xs text-og-subtle">
184
+ Client {mismatch.expected} · API {mismatch.actual}
185
+ </p>
186
+ <button
187
+ type="button"
188
+ className="mt-5 inline-flex h-9 items-center rounded-md bg-og-fg px-3 text-sm font-medium text-og-bg"
189
+ onClick={() => window.location.reload()}
190
+ >
191
+ Reload now
192
+ </button>
193
+ </div>
194
+ </div>
195
+ );
196
+ }
197
+
198
+ function reloadForContractMismatchOnce(mismatch: OpenGeniApiContractMismatchError): void {
199
+ if (typeof window === "undefined" || typeof sessionStorage === "undefined") return;
200
+ const key = `${CONTRACT_RELOAD_STORAGE_PREFIX}${mismatch.actual}`;
201
+ if (sessionStorage.getItem(key) === OPENGENI_API_CONTRACT_REVISION) return;
202
+ sessionStorage.setItem(key, OPENGENI_API_CONTRACT_REVISION);
203
+ window.setTimeout(() => window.location.reload(), 150);
24
204
  }
25
205
 
26
206
  export type ClientOverride = {
@@ -38,14 +218,23 @@ export function useOpenGeni(override: ClientOverride = {}): OpenGeniContextValue
38
218
  "@opengeni/react: no OpenGeni client/workspace available. Wrap the tree in <OpenGeniProvider> or pass { client, workspaceId } to the hook.",
39
219
  );
40
220
  }
41
- return { client, workspaceId };
221
+ return {
222
+ client,
223
+ workspaceId,
224
+ workspaceControlEvent: context?.workspaceControlEvent ?? null,
225
+ workspaceControlConnectionState: context?.workspaceControlConnectionState ?? "idle",
226
+ registerSessionReconciler: context?.registerSessionReconciler ?? NOOP_REGISTER_RECONCILER,
227
+ reconcileSession: context?.reconcileSession ?? NOOP_RECONCILE_SESSION,
228
+ };
42
229
  }
43
230
 
44
231
  /**
45
232
  * Resolve the client only — for hooks that are not workspace-scoped
46
233
  * (`useWorkspaces`, `useBillingUsage`).
47
234
  */
48
- export function useOpenGeniClient(override: Pick<ClientOverride, "client"> = {}): SessionClientLike {
235
+ export function useOpenGeniClient(
236
+ override: Pick<ClientOverride, "client"> = {},
237
+ ): SessionClientLike {
49
238
  const context = useContext(OpenGeniContext);
50
239
  const client = override.client ?? context?.client;
51
240
  if (!client) {
@@ -1,12 +1,18 @@
1
- import { BotIcon, BrainIcon, SquareTerminalIcon } from "lucide-react";
1
+ import {
2
+ ArrowRightIcon,
3
+ BotIcon,
4
+ BrainCircuitIcon,
5
+ BrainIcon,
6
+ SquareTerminalIcon,
7
+ } from "lucide-react";
2
8
  import { cn } from "../lib/cn";
3
9
  import { truncate } from "../lib/format";
4
10
  import { defaultToolRegistry } from "./tool-renderers";
5
11
  import { useEntranceAnimation } from "./entrance";
6
12
  import type { ToolRegistry } from "./registry";
7
- import { PayloadBlock, ActivityDisclosure } from "./shared";
13
+ import { BodyNote, PayloadBlock, ActivityDisclosure } from "./shared";
8
14
  import { toolDisplayName } from "./projection";
9
- import type { ActivityItem, ReasoningItem, SandboxItem, WorkerItem } from "./types";
15
+ import type { ActivityItem, MemoryItem, ReasoningItem, SandboxItem, WorkerItem } from "./types";
10
16
 
11
17
  /* ----------------------------------------------------------------------------
12
18
  Activity rail
@@ -25,6 +31,12 @@ export type ActivityRailProps = {
25
31
  toolRegistry?: ToolRegistry | undefined;
26
32
  /** Drill into a spawned worker session. */
27
33
  onOpenSession?: ((sessionId: string) => void) | undefined;
34
+ /**
35
+ * Deep-link a memory row to its record in the host's memory pane. Opt-in: the
36
+ * library draws no "View in memory" affordance without a handler (the memory
37
+ * row is then non-interactive rich content). See {@link MessageTimelineProps}.
38
+ */
39
+ onMemoryClick?: ((memoryId: string) => void) | undefined;
28
40
  /** Drop the left rule + indent (used inside a folded turn summary). */
29
41
  bare?: boolean | undefined;
30
42
  className?: string | undefined;
@@ -42,7 +54,14 @@ function familyOf(item: ActivityItem): string {
42
54
  return item.kind;
43
55
  }
44
56
 
45
- export function ActivityRail({ items, toolRegistry = defaultToolRegistry, onOpenSession, bare, className }: ActivityRailProps) {
57
+ export function ActivityRail({
58
+ items,
59
+ toolRegistry = defaultToolRegistry,
60
+ onOpenSession,
61
+ onMemoryClick,
62
+ bare,
63
+ className,
64
+ }: ActivityRailProps) {
46
65
  const enter = useEntranceAnimation();
47
66
  return (
48
67
  <div
@@ -58,7 +77,7 @@ export function ActivityRail({ items, toolRegistry = defaultToolRegistry, onOpen
58
77
  >
59
78
  {items.map((item, index) => {
60
79
  const newFamily = index > 0 && familyOf(item) !== familyOf(items[index - 1]!);
61
- const row = renderActivity(item, toolRegistry, onOpenSession);
80
+ const row = renderActivity(item, toolRegistry, onOpenSession, onMemoryClick);
62
81
  return (
63
82
  <div key={item.id} className={cn(newFamily && "mt-3")}>
64
83
  {row}
@@ -78,6 +97,7 @@ function renderActivity(
78
97
  item: ActivityItem,
79
98
  toolRegistry: ToolRegistry,
80
99
  onOpenSession: ((sessionId: string) => void) | undefined,
100
+ onMemoryClick: ((memoryId: string) => void) | undefined,
81
101
  ) {
82
102
  switch (item.kind) {
83
103
  case "reasoning":
@@ -90,6 +110,8 @@ function renderActivity(
90
110
  return <WorkerRow item={item} onOpenSession={onOpenSession} />;
91
111
  case "sandbox":
92
112
  return <SandboxRow item={item} />;
113
+ case "memory":
114
+ return <MemoryRow item={item} onMemoryClick={onMemoryClick} />;
93
115
  default:
94
116
  return assertNever(item);
95
117
  }
@@ -117,12 +139,131 @@ function ReasoningRow({ item }: { item: ReasoningItem }) {
117
139
  );
118
140
  }
119
141
 
142
+ /**
143
+ * Human labels for the memory kinds, translated at the SDK boundary so a raw
144
+ * enum slug never renders as UI. Kept local to the library (the app has its own
145
+ * `KIND_LABEL`); an unknown kind simply omits the chip rather than showing a slug.
146
+ */
147
+ const MEMORY_KIND_LABEL: Record<string, string> = {
148
+ preference: "Preference",
149
+ semantic: "Fact",
150
+ procedural: "Procedure",
151
+ decision: "Decision",
152
+ episodic: "History",
153
+ };
154
+
155
+ /**
156
+ * A memory write the agent made mid-turn. A calm, NEUTRAL step (a successful save
157
+ * is ordinary progress, never an exceptional state, so no accent/color): a brain-
158
+ * circuit glyph, "Saved to memory" / "Updated memory", a human kind chip, and the
159
+ * memory text. Expanding reveals the full text; a supersede shows the old text
160
+ * struck through above the new one. When the host opts in with `onMemoryClick`,
161
+ * a quiet "View in memory" affordance deep-links to the LIVE record.
162
+ */
163
+ function MemoryRow({
164
+ item,
165
+ onMemoryClick,
166
+ }: {
167
+ item: MemoryItem;
168
+ onMemoryClick?: ((memoryId: string) => void) | undefined;
169
+ }) {
170
+ const corrected = item.variant === "corrected";
171
+ const kindLabel = MEMORY_KIND_LABEL[item.memoryKind];
172
+ // A supersede carries both the old text (`preview`) and the new (`replacementPreview`);
173
+ // an in-place update / archive carries only `preview`.
174
+ const superseded = corrected && Boolean(item.replacementPreview);
175
+ // Link to the LIVE record: a supersede's replacement when present, else the memory itself.
176
+ const targetId = corrected ? (item.replacementMemoryId ?? item.memoryId) : item.memoryId;
177
+ const deepLink = Boolean(onMemoryClick);
178
+ return (
179
+ <ActivityDisclosure
180
+ icon={<BrainCircuitIcon className="size-3.5" />}
181
+ iconTone="muted"
182
+ title={
183
+ <span className="inline-flex min-w-0 items-center gap-2">
184
+ <span className="shrink-0">{corrected ? "Updated memory" : "Saved to memory"}</span>
185
+ {kindLabel ? (
186
+ <span className="shrink-0 rounded-og-xs bg-og-surface-2 px-1.5 py-px text-og-xs font-normal leading-tight text-og-fg-subtle">
187
+ {kindLabel}
188
+ </span>
189
+ ) : null}
190
+ </span>
191
+ }
192
+ preview={superseded ? item.replacementPreview : item.preview}
193
+ >
194
+ {superseded ? (
195
+ // The correction as a before → after: the old memory struck through and
196
+ // dimmed, the new text in the ordinary body weight below it.
197
+ <div className="flex flex-col gap-1.5">
198
+ <p className="whitespace-pre-wrap text-og-sm leading-6 text-og-fg-subtle line-through">
199
+ {item.preview}
200
+ </p>
201
+ <p className="whitespace-pre-wrap text-og-base leading-6 text-og-fg-muted">
202
+ {item.replacementPreview}
203
+ </p>
204
+ </div>
205
+ ) : corrected && item.action === "updated" ? (
206
+ // Edited in place, no replacement record: the memory is still live, so
207
+ // show its current text — NOT the archived treatment.
208
+ <>
209
+ <p className="whitespace-pre-wrap text-og-base leading-6 text-og-fg-muted">
210
+ {item.preview}
211
+ </p>
212
+ <BodyNote tone="muted">Updated in place.</BodyNote>
213
+ </>
214
+ ) : corrected ? (
215
+ // A correction with no replacement (and not an in-place update) archived the record.
216
+ <BodyNote tone="muted">Archived.</BodyNote>
217
+ ) : (
218
+ <p className="whitespace-pre-wrap text-og-base leading-6 text-og-fg-muted">
219
+ {item.preview}
220
+ </p>
221
+ )}
222
+ {item.deduped ? <BodyNote tone="muted">Merged into an existing memory.</BodyNote> : null}
223
+ {deepLink ? (
224
+ <button
225
+ type="button"
226
+ onClick={() => onMemoryClick?.(targetId)}
227
+ className={cn(
228
+ "group/memlink -mx-1 inline-flex w-fit items-center gap-1 rounded-og-sm px-1 py-0.5 text-left text-og-sm text-og-fg-subtle",
229
+ "outline-none transition-colors duration-150 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent",
230
+ )}
231
+ >
232
+ View in memory
233
+ <ArrowRightIcon className="size-3.5 transition-transform duration-150 group-hover/memlink:translate-x-0.5" />
234
+ </button>
235
+ ) : null}
236
+ </ActivityDisclosure>
237
+ );
238
+ }
239
+
240
+ /**
241
+ * A human title for a sandbox operation row. Named platform operations (the lazy
242
+ * `sandbox.provision` first-establish that now runs mid-turn) read as calm,
243
+ * status-aware English instead of a raw op id — the box coming up should look
244
+ * like "Starting sandbox", never an unexplained long-running command. Unnamed /
245
+ * unknown ops fall back to the generic id-to-words {@link toolDisplayName}.
246
+ */
247
+ function sandboxRowTitle(item: SandboxItem): string {
248
+ if (item.name === "sandbox.provision") {
249
+ if (item.status === "failed") return "Sandbox didn’t start";
250
+ if (item.status === "running") return "Starting sandbox";
251
+ if (item.origin === "resumed") return "Sandbox reattached";
252
+ if (item.origin === "restored") return "Sandbox restored";
253
+ if (item.origin === "created") return "Sandbox created";
254
+ return "Sandbox ready";
255
+ }
256
+ return toolDisplayName(item.name);
257
+ }
258
+
120
259
  function SandboxRow({ item }: { item: SandboxItem }) {
121
260
  return (
122
261
  <ActivityDisclosure
123
262
  icon={<SquareTerminalIcon className="size-3.5" />}
124
- iconTone={item.status === "failed" ? "failed" : item.status === "running" ? "running" : "muted"}
125
- title={toolDisplayName(item.name)}
263
+ iconTone={
264
+ item.status === "failed" ? "failed" : item.status === "running" ? "running" : "muted"
265
+ }
266
+ title={sandboxRowTitle(item)}
126
267
  running={item.status === "running"}
127
268
  failed={item.status === "failed"}
128
269
  cancelled={item.status === "cancelled"}
@@ -135,7 +276,13 @@ function SandboxRow({ item }: { item: SandboxItem }) {
135
276
  }
136
277
 
137
278
  /** Spawned/messaged worker sessions get a first-class card, not a tool row. */
138
- function WorkerRow({ item, onOpenSession }: { item: WorkerItem; onOpenSession?: ((sessionId: string) => void) | undefined }) {
279
+ function WorkerRow({
280
+ item,
281
+ onOpenSession,
282
+ }: {
283
+ item: WorkerItem;
284
+ onOpenSession?: ((sessionId: string) => void) | undefined;
285
+ }) {
139
286
  const running = item.status === "running";
140
287
  const failed = item.status === "failed";
141
288
  const cancelled = item.status === "cancelled";
@@ -148,47 +295,49 @@ function WorkerRow({ item, onOpenSession }: { item: WorkerItem; onOpenSession?:
148
295
  : cancelled
149
296
  ? "Worker interrupted"
150
297
  : "Worker spawned"
151
- : item.action === "interrupt"
152
- ? (() => {
153
- const verb = item.mode === "steer" ? "Steering" : "Stopping";
154
- const done = item.mode === "steer" ? "Worker steered" : "Worker stopped";
155
- return running ? `${verb} worker` : failed ? "Worker interrupt failed" : cancelled ? "Worker interrupted" : done;
156
- })()
157
- : running
158
- ? "Messaging worker"
159
- : failed
160
- ? "Worker message failed"
161
- : cancelled
162
- ? "Worker interrupted"
163
- : "Worker messaged";
164
- return (
165
- <div
166
- className={cn(
167
- "my-0.5 flex items-start gap-3 rounded-og-md border bg-og-surface-1 p-3",
168
- failed ? "border-og-status-failed/40" : "border-og-border",
169
- )}
170
- >
171
- <span
172
- className={cn(
173
- "mt-0.5 inline-flex size-7 shrink-0 items-center justify-center",
174
- failed ? "text-og-status-failed" : "text-og-accent",
175
- )}
176
- >
177
- <BotIcon className="size-4" />
298
+ : running
299
+ ? "Messaging worker"
300
+ : failed
301
+ ? "Worker message failed"
302
+ : cancelled
303
+ ? "Worker interrupted"
304
+ : "Worker messaged";
305
+ // A worker is a first-class actor but still a STEP on the rail — a borderless
306
+ // row (no card), aligned to its sibling tool rows: the chevron column is an
307
+ // empty spacer (a worker doesn't expand), then the bot glyph, then the title
308
+ // with a quiet prompt beneath, and one right-gutter affordance.
309
+ //
310
+ // Once the worker's session id is known, the WHOLE row is a clean, clickable
311
+ // deep-link into that child session (a trailing arrow brightens on hover) —
312
+ // the spawn moment itself is the affordance, not a small side button. A
313
+ // still-in-flight spawn (no id yet) or a failed/cancelled worker is inert, so
314
+ // there is never a dead click target. The gutter mirrors the worker-completion
315
+ // card's calm language: red chip on failure, "interrupted" on cancel.
316
+ const sessionId = item.workerSessionId;
317
+ const deepLink = Boolean(sessionId) && Boolean(onOpenSession) && !failed && !cancelled;
318
+ const inner = (
319
+ <>
320
+ <span className="size-3.5 shrink-0" aria-hidden />
321
+ <span className={cn("mt-px shrink-0", failed ? "text-og-status-failed" : "text-og-accent")}>
322
+ <BotIcon className="size-3.5" />
178
323
  </span>
179
324
  <div className="min-w-0 flex-1">
180
325
  {/* In-flight state is carried ONLY by the shimmering title (no detached
181
326
  pulse badge), matching every other running row in the rail. */}
182
- <span className={cn("text-og-base font-medium", running ? "og-shimmer-text" : failed ? "text-og-status-failed" : "text-og-fg")}>
327
+ <span
328
+ className={cn(
329
+ "text-og-base font-medium",
330
+ running ? "og-shimmer-text" : failed ? "text-og-status-failed" : "text-og-fg",
331
+ )}
332
+ >
183
333
  {title}
184
334
  </span>
185
- {item.prompt ? <p className="mt-0.5 truncate text-og-sm text-og-fg-muted">{truncate(item.prompt, 140)}</p> : null}
186
- {item.workerSessionId ? (
187
- <p className="mt-1 font-og-mono text-og-xs text-og-fg-subtle">{item.workerSessionId.slice(0, 8)}</p>
335
+ {item.prompt ? (
336
+ <p className="mt-0.5 truncate text-og-sm text-og-fg-muted">
337
+ {truncate(item.prompt, 140)}
338
+ </p>
188
339
  ) : null}
189
340
  </div>
190
- {/* Right gutter: failed gets a red chip; cancelled gets a calm "interrupted"
191
- chip (no dot, no red); a live complete worker shows an "Open session" button. */}
192
341
  {failed ? (
193
342
  <span className="inline-flex shrink-0 self-center items-center gap-1.5 font-og-mono text-og-xs leading-none text-og-status-failed">
194
343
  <span className="size-1.5 rounded-full bg-og-status-failed" />
@@ -198,19 +347,29 @@ function WorkerRow({ item, onOpenSession }: { item: WorkerItem; onOpenSession?:
198
347
  <span className="og-cancelled-chip shrink-0 self-center font-og-mono text-og-xs leading-none text-og-fg-subtle">
199
348
  interrupted
200
349
  </span>
201
- ) : item.workerSessionId && onOpenSession ? (
202
- <button
203
- type="button"
204
- onClick={() => item.workerSessionId && onOpenSession(item.workerSessionId)}
205
- className={cn(
206
- "shrink-0 self-center rounded-og-sm border border-og-border px-2.5 py-1 text-og-sm font-medium text-og-fg-muted pointer-coarse:py-2",
207
- "outline-none transition-colors duration-150 hover:border-og-border-strong hover:text-og-fg",
208
- "focus-visible:ring-2 focus-visible:ring-og-accent",
209
- )}
210
- >
211
- Open session
212
- </button>
350
+ ) : deepLink ? (
351
+ <ArrowRightIcon
352
+ aria-hidden
353
+ className="mt-0.5 size-3.5 shrink-0 text-og-fg-subtle transition-[transform,color] duration-150 group-hover/worker:translate-x-0.5 group-hover/worker:text-og-fg"
354
+ />
213
355
  ) : null}
214
- </div>
356
+ </>
215
357
  );
358
+
359
+ if (deepLink && sessionId && onOpenSession) {
360
+ return (
361
+ <button
362
+ type="button"
363
+ onClick={() => onOpenSession(sessionId)}
364
+ className={cn(
365
+ "group/worker -mx-1.5 flex w-full items-start gap-2 rounded-og-sm px-1.5 py-1.5 text-left",
366
+ "outline-none transition-colors duration-150 hover:bg-og-surface-1 focus-visible:ring-2 focus-visible:ring-og-accent",
367
+ "pointer-coarse:py-2.5",
368
+ )}
369
+ >
370
+ {inner}
371
+ </button>
372
+ );
373
+ }
374
+ return <div className="flex items-start gap-2 px-1.5 py-1.5">{inner}</div>;
216
375
  }
@@ -21,8 +21,18 @@ const DisclosureDefaultsContext = createContext<boolean | undefined>(undefined);
21
21
  * Seed the initial open state of every timeline collapsible below this node.
22
22
  * Intended for screenshot/test instrumentation only; absent by default.
23
23
  */
24
- export function DisclosureDefaultsProvider({ defaultOpen, children }: { defaultOpen: boolean; children: ReactNode }) {
25
- return <DisclosureDefaultsContext.Provider value={defaultOpen}>{children}</DisclosureDefaultsContext.Provider>;
24
+ export function DisclosureDefaultsProvider({
25
+ defaultOpen,
26
+ children,
27
+ }: {
28
+ defaultOpen: boolean;
29
+ children: ReactNode;
30
+ }) {
31
+ return (
32
+ <DisclosureDefaultsContext.Provider value={defaultOpen}>
33
+ {children}
34
+ </DisclosureDefaultsContext.Provider>
35
+ );
26
36
  }
27
37
 
28
38
  /**
@@ -16,13 +16,22 @@
16
16
  -------------------------------------------------------------------------- */
17
17
 
18
18
  // projection
19
- export { buildTimeline, creditExhaustedFromEvents, extractSessionRef, groupTimeline, sessionStatusFromEvents, toolDisplayName } from "./projection";
19
+ export {
20
+ buildTimeline,
21
+ creditExhaustedFromEvents,
22
+ extractSessionRef,
23
+ groupTimeline,
24
+ sessionStatusFromEvents,
25
+ toolDisplayName,
26
+ } from "./projection";
20
27
 
21
28
  // item types
22
29
  export type {
23
30
  ActivityItem,
24
31
  AgentMessageItem,
32
+ AuthNeededItem,
25
33
  GoalItem,
34
+ MemoryItem,
26
35
  NoticeItem,
27
36
  ReasoningItem,
28
37
  SandboxItem,
@@ -32,6 +41,7 @@ export type {
32
41
  TurnEndItem,
33
42
  ToolCallItem,
34
43
  UserMessageItem,
44
+ WorkerCompletionItem,
35
45
  WorkerItem,
36
46
  } from "./types";
37
47
 
@@ -53,7 +63,16 @@ export { ActivityRail } from "./activity-rail";
53
63
  export type { ActivityRailProps } from "./activity-rail";
54
64
 
55
65
  // shared primitives (extension authors compose these)
56
- export { ActivityDisclosure, BodyNote, MediaEmpty, MediaSkeleton, PayloadBlock, ScreenshotFigure, TermBlock, Thumbnail } from "./shared";
66
+ export {
67
+ ActivityDisclosure,
68
+ BodyNote,
69
+ MediaEmpty,
70
+ MediaSkeleton,
71
+ PayloadBlock,
72
+ ScreenshotFigure,
73
+ TermBlock,
74
+ Thumbnail,
75
+ } from "./shared";
57
76
  export type { ActivityDisclosureProps, DisclosureChip } from "./shared";
58
77
 
59
78
  // screenshot lightbox