@opengeni/react 0.3.0 → 0.4.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 (40) hide show
  1. package/dist/index.d.ts +1035 -14
  2. package/dist/index.js +6867 -1884
  3. package/dist/index.js.map +1 -1
  4. package/package.json +65 -2
  5. package/src/client.ts +21 -0
  6. package/src/components/code-editor.tsx +398 -0
  7. package/src/components/desktop-viewer.tsx +647 -0
  8. package/src/components/diff-view.tsx +230 -0
  9. package/src/components/file-browser.tsx +838 -0
  10. package/src/components/message-timeline.tsx +70 -196
  11. package/src/components/pierre-diff.tsx +140 -0
  12. package/src/components/pierre-file.tsx +142 -0
  13. package/src/components/sandbox-files.tsx +509 -0
  14. package/src/components/sandbox-terminal.tsx +425 -0
  15. package/src/components/workspace-dock.tsx +247 -0
  16. package/src/hooks/use-desktop-stream.ts +214 -0
  17. package/src/hooks/use-sandbox-files.ts +670 -0
  18. package/src/hooks/use-sandbox-git.ts +105 -0
  19. package/src/hooks/use-sandbox-terminal.ts +226 -0
  20. package/src/hooks/use-session-capabilities.ts +415 -0
  21. package/src/hooks/use-terminal-stream.ts +207 -0
  22. package/src/index.ts +111 -2
  23. package/src/lib/cn.ts +20 -1
  24. package/src/lib/git-patch.ts +37 -0
  25. package/src/lib/use-theme-type.ts +40 -0
  26. package/src/lib/xterm-theme.ts +34 -0
  27. package/src/timeline/activity-rail.tsx +207 -0
  28. package/src/timeline/disclosure-context.tsx +34 -0
  29. package/src/timeline/index.ts +85 -0
  30. package/src/timeline/parsers.ts +248 -0
  31. package/src/{timeline.ts → timeline/projection.ts} +59 -134
  32. package/src/timeline/registry.ts +96 -0
  33. package/src/timeline/screenshot-lightbox.tsx +152 -0
  34. package/src/timeline/shared.tsx +481 -0
  35. package/src/timeline/tool-diff.tsx +91 -0
  36. package/src/timeline/tool-renderers.tsx +882 -0
  37. package/src/timeline/turn-summary.tsx +125 -0
  38. package/src/timeline/types.ts +131 -0
  39. package/src/types/external.d.ts +7 -0
  40. package/styles/index.css +72 -0
package/src/index.ts CHANGED
@@ -46,6 +46,37 @@ export type { UseBillingUsageOptions, UseBillingUsageResult } from "./hooks/use-
46
46
  export { useAvailableModels } from "./hooks/use-available-models";
47
47
  export type { UseAvailableModelsOptions, UseAvailableModelsResult } from "./hooks/use-available-models";
48
48
 
49
+ // Sandbox surfacing (Phase 5): capability negotiation + terminal/files/diff/desktop
50
+ export { useSessionCapabilities } from "./hooks/use-session-capabilities";
51
+ export type {
52
+ SessionCapabilitiesState,
53
+ UseSessionCapabilitiesOptions,
54
+ UseSessionCapabilitiesResult,
55
+ } from "./hooks/use-session-capabilities";
56
+ export { useDesktopStream } from "./hooks/use-desktop-stream";
57
+ export type { UseDesktopStreamOptions, UseDesktopStreamResult } from "./hooks/use-desktop-stream";
58
+ export { useTerminalStream } from "./hooks/use-terminal-stream";
59
+ export type {
60
+ TerminalStreamStatus,
61
+ UseTerminalStreamOptions,
62
+ UseTerminalStreamResult,
63
+ } from "./hooks/use-terminal-stream";
64
+ export { useSandboxTerminal } from "./hooks/use-sandbox-terminal";
65
+ export type {
66
+ TerminalChunk,
67
+ UseSandboxTerminalOptions,
68
+ UseSandboxTerminalResult,
69
+ } from "./hooks/use-sandbox-terminal";
70
+ export { useSandboxFiles } from "./hooks/use-sandbox-files";
71
+ export type {
72
+ FileTreeNode,
73
+ FileTreeStatus,
74
+ UseSandboxFilesOptions,
75
+ UseSandboxFilesResult,
76
+ } from "./hooks/use-sandbox-files";
77
+ export { useSandboxGit } from "./hooks/use-sandbox-git";
78
+ export type { UseSandboxGitOptions, UseSandboxGitResult } from "./hooks/use-sandbox-git";
79
+
49
80
  // Pending-approvals projection
50
81
  export { approvalsFromRequiresAction, projectPendingApprovals } from "./approvals";
51
82
  export type { PendingApproval } from "./approvals";
@@ -53,13 +84,13 @@ export type { PendingApproval } from "./approvals";
53
84
  // Timeline projection
54
85
  export {
55
86
  buildTimeline,
56
- compactPayloadPreview,
57
87
  extractSessionRef,
58
88
  groupTimeline,
59
89
  sessionStatusFromEvents,
60
90
  toolDisplayName,
61
91
  } from "./timeline";
62
92
  export type {
93
+ ActivityItem,
63
94
  AgentMessageItem,
64
95
  GoalItem,
65
96
  NoticeItem,
@@ -73,6 +104,60 @@ export type {
73
104
  WorkerItem,
74
105
  } from "./timeline";
75
106
 
107
+ // Tool-renderer registry + the per-tool renderers (the timeline's extension API)
108
+ export { createDefaultToolRegistry, createToolRegistry, defaultToolRegistry, rawTypeOf } from "./timeline";
109
+ export type {
110
+ CreateToolRegistryOptions,
111
+ ToolRegistry,
112
+ ToolRegistryEntry,
113
+ ToolRenderer,
114
+ ToolRendererProps,
115
+ } from "./timeline";
116
+
117
+ // Timeline rendering primitives + the screenshot lightbox (compose custom renderers)
118
+ export {
119
+ ActivityDisclosure,
120
+ ActivityRail,
121
+ BodyNote,
122
+ DisclosureDefaultsProvider,
123
+ LightboxProvider,
124
+ MediaEmpty,
125
+ MediaSkeleton,
126
+ PayloadBlock,
127
+ ScreenshotFigure,
128
+ TermBlock,
129
+ Thumbnail,
130
+ TurnSummary,
131
+ useLightbox,
132
+ useLightboxOptional,
133
+ } from "./timeline";
134
+ export type {
135
+ ActivityDisclosureProps,
136
+ ActivityRailProps,
137
+ DisclosureChip,
138
+ TurnOutcome,
139
+ TurnSummaryProps,
140
+ } from "./timeline";
141
+
142
+ // Pure provider-shape parsers (exec banner, V4A diff, secret redaction, …)
143
+ export {
144
+ applyPatchOps,
145
+ controlCaret,
146
+ execTruncated,
147
+ isApplyPatch,
148
+ isExecSessionLostBanner,
149
+ looksBinary,
150
+ parseExecBannerSessionId,
151
+ parseToolArgs,
152
+ redactSecrets,
153
+ sandboxCommandExitCode,
154
+ stripExecBanner,
155
+ tailPeek,
156
+ unwrapMcpOutput,
157
+ v4aToGitFileDiff,
158
+ } from "./timeline";
159
+ export type { ApplyPatchOperation } from "./timeline";
160
+
76
161
  // Slash-command palette (registry + UI + hook)
77
162
  export {
78
163
  argHint,
@@ -107,7 +192,7 @@ export { ChatComposer } from "./components/chat-composer";
107
192
  export type { ChatComposerProps } from "./components/chat-composer";
108
193
  export { ModelPicker } from "./components/model-picker";
109
194
  export type { ModelPickerProps } from "./components/model-picker";
110
- export { MessageTimeline } from "./components/message-timeline";
195
+ export { MessageTimeline, TimelineRow } from "./components/message-timeline";
111
196
  export type { MessageTimelineProps } from "./components/message-timeline";
112
197
  export { Markdown } from "./components/markdown";
113
198
  export type { MarkdownProps } from "./components/markdown";
@@ -116,6 +201,30 @@ export type { SessionStatusProps, StatusDotProps, SessionStatusMeta } from "./co
116
201
  export { FleetTile, sessionDisplayTitle } from "./components/fleet-tile";
117
202
  export type { FleetTileProps } from "./components/fleet-tile";
118
203
 
204
+ // Sandbox surfacing components (Phase 5)
205
+ export { SandboxTerminal } from "./components/sandbox-terminal";
206
+ export type { SandboxTerminalProps, XtermTheme } from "./components/sandbox-terminal";
207
+ export { FileBrowser } from "./components/file-browser";
208
+ export type { FileBrowserProps } from "./components/file-browser";
209
+ export { DiffView } from "./components/diff-view";
210
+ export type { DiffViewProps, DiffTheme } from "./components/diff-view";
211
+ export { PierreDiff } from "./components/pierre-diff";
212
+ export type { PierreDiffProps } from "./components/pierre-diff";
213
+ export { PierreFile } from "./components/pierre-file";
214
+ export type { PierreFileProps } from "./components/pierre-file";
215
+ export { CodeEditor, languageForPath } from "./components/code-editor";
216
+ export type { CodeEditorProps } from "./components/code-editor";
217
+ export { SandboxFiles } from "./components/sandbox-files";
218
+ export type { SandboxFilesProps } from "./components/sandbox-files";
219
+ export { DesktopViewer } from "./components/desktop-viewer";
220
+ export type { DesktopViewerProps } from "./components/desktop-viewer";
221
+ export { WorkspaceDock } from "./components/workspace-dock";
222
+ export type { WorkspaceDockProps, WorkspaceTab } from "./components/workspace-dock";
223
+
224
+ // Sandbox helpers
225
+ export { gitFileDiffToPatch } from "./lib/git-patch";
226
+ export { xtermThemeFromTokens } from "./lib/xterm-theme";
227
+
119
228
  // Utilities
120
229
  export { cn } from "./lib/cn";
121
230
  export { formatBytes, formatRelativeTime, stringifyPayload, truncate, tryParseJson } from "./lib/format";
package/src/lib/cn.ts CHANGED
@@ -1,5 +1,24 @@
1
1
  import { clsx, type ClassValue } from "clsx";
2
- import { twMerge } from "tailwind-merge";
2
+ import { extendTailwindMerge } from "tailwind-merge";
3
+
4
+ /* ----------------------------------------------------------------------------
5
+ tailwind-merge, taught our design tokens.
6
+
7
+ Our type scale and color ramp share the `og-` prefix: `text-og-base` is a
8
+ FONT SIZE, `text-og-fg-muted` is a COLOR. Stock tailwind-merge can't tell
9
+ them apart — it lumps every `text-og-*` into one `font-size` group and, when
10
+ two land in the same class list, drops the earlier one. That silently ate the
11
+ size off rows like `text-og-base text-og-fg-muted`, leaving titles at the
12
+ browser-default 16px. Register the custom font-size scale so the size and the
13
+ color live in different conflict groups and both survive a merge.
14
+ -------------------------------------------------------------------------- */
15
+ const twMerge = extendTailwindMerge({
16
+ extend: {
17
+ classGroups: {
18
+ "font-size": [{ text: ["og-xs", "og-sm", "og-base", "og-md"] }],
19
+ },
20
+ },
21
+ });
3
22
 
4
23
  /** Merge class names with Tailwind-aware conflict resolution. */
5
24
  export function cn(...inputs: ClassValue[]): string {
@@ -0,0 +1,37 @@
1
+ import type { GitFileDiff } from "@opengeni/sdk";
2
+
3
+ /**
4
+ * Reconstruct a unified-diff patch string for a single `GitFileDiff` so it can
5
+ * be fed to a generic patch renderer (e.g. Pierre's `PatchDiff`). The hook
6
+ * already carries per-line old/new numbers and a hunk header, so we emit a
7
+ * conventional `--- / +++ / @@` patch the parser understands.
8
+ */
9
+ export function gitFileDiffToPatch(file: GitFileDiff): string {
10
+ const oldPath = file.oldPath ?? file.path;
11
+ const newPath = file.path;
12
+ const lines: string[] = [];
13
+ lines.push(`diff --git a/${oldPath} b/${newPath}`);
14
+ if (file.status === "deleted") {
15
+ lines.push(`--- a/${oldPath}`);
16
+ lines.push(`+++ /dev/null`);
17
+ } else if (file.status === "added" || file.status === "untracked") {
18
+ lines.push(`--- /dev/null`);
19
+ lines.push(`+++ b/${newPath}`);
20
+ } else {
21
+ lines.push(`--- a/${oldPath}`);
22
+ lines.push(`+++ b/${newPath}`);
23
+ }
24
+ for (const hunk of file.hunks) {
25
+ const header =
26
+ hunk.header && hunk.header.startsWith("@@")
27
+ ? hunk.header
28
+ : `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
29
+ lines.push(header);
30
+ for (const line of hunk.lines) {
31
+ if (line.type === "meta") continue;
32
+ const prefix = line.type === "add" ? "+" : line.type === "del" ? "-" : " ";
33
+ lines.push(`${prefix}${line.text}`);
34
+ }
35
+ }
36
+ return lines.join("\n") + "\n";
37
+ }
@@ -0,0 +1,40 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ /* ----------------------------------------------------------------------------
4
+ useThemeType
5
+
6
+ Resolve the effective dark/light theme for surfaces (the Pierre/Shiki diff)
7
+ that render outside the reach of host CSS — they need the theme as a value,
8
+ not a cascade. An explicit prop always wins; otherwise read the host's
9
+ `data-og-theme` attribute (set on `<html>` or any ancestor by the same opt-in
10
+ the tokens use) and default to dark, the first-class theme. A MutationObserver
11
+ keeps it live across runtime theme flips.
12
+
13
+ One detector, shared by every diff surface (the Files tab and the timeline),
14
+ so the two can never drift onto different themes.
15
+ -------------------------------------------------------------------------- */
16
+
17
+ /**
18
+ * Resolve the diff theme. An explicit `forced` value wins; otherwise auto-detect
19
+ * from the host `data-og-theme` (defaulting to dark) and track live flips.
20
+ */
21
+ export function useThemeType(forced: "dark" | "light" | undefined): "dark" | "light" {
22
+ const [detected, setDetected] = useState<"dark" | "light">("dark");
23
+ useEffect(() => {
24
+ if (forced || typeof document === "undefined") return;
25
+ const read = () => {
26
+ const el = document.querySelector("[data-og-theme]");
27
+ const value = el?.getAttribute("data-og-theme");
28
+ setDetected(value === "light" ? "light" : "dark");
29
+ };
30
+ read();
31
+ const observer = new MutationObserver(read);
32
+ observer.observe(document.documentElement, {
33
+ attributes: true,
34
+ attributeFilter: ["data-og-theme"],
35
+ subtree: true,
36
+ });
37
+ return () => observer.disconnect();
38
+ }, [forced]);
39
+ return forced ?? detected;
40
+ }
@@ -0,0 +1,34 @@
1
+ import type { XtermTheme } from "../components/sandbox-terminal";
2
+
3
+ /**
4
+ * Derive an xterm `ITheme` subset from the live OKLCH `--og-*` token system (or
5
+ * the app's `--color-*` aliases). Reads the COMPUTED values so xterm — which
6
+ * paints into a canvas and can't consume CSS vars — gets concrete colors. Call
7
+ * on mount and re-derive on a `data-og-theme` flip.
8
+ *
9
+ * SSR-safe: returns undefined off the DOM (the caller keeps xterm's defaults).
10
+ */
11
+ export function xtermThemeFromTokens(root?: HTMLElement | null): XtermTheme | undefined {
12
+ if (typeof window === "undefined" || typeof getComputedStyle === "undefined") return undefined;
13
+ const el = root ?? document.documentElement;
14
+ const style = getComputedStyle(el);
15
+ const read = (names: string[]): string | undefined => {
16
+ for (const name of names) {
17
+ const value = style.getPropertyValue(name).trim();
18
+ if (value) return value;
19
+ }
20
+ return undefined;
21
+ };
22
+ const bg = read(["--og-color-bg", "--color-bg"]);
23
+ const fg = read(["--og-color-fg", "--color-fg"]);
24
+ const accent = read(["--og-color-accent", "--color-brand", "--color-accent"]);
25
+ const theme: XtermTheme = {};
26
+ if (bg) theme.background = bg;
27
+ if (fg) theme.foreground = fg;
28
+ if (accent) {
29
+ theme.cursor = accent;
30
+ theme.selectionBackground = accent;
31
+ }
32
+ if (bg) theme.cursorAccent = bg;
33
+ return Object.keys(theme).length > 0 ? theme : undefined;
34
+ }
@@ -0,0 +1,207 @@
1
+ import { BotIcon, BrainIcon, SquareTerminalIcon } from "lucide-react";
2
+ import { cn } from "../lib/cn";
3
+ import { truncate } from "../lib/format";
4
+ import { defaultToolRegistry } from "./tool-renderers";
5
+ import type { ToolRegistry } from "./registry";
6
+ import { PayloadBlock, ActivityDisclosure } from "./shared";
7
+ import { toolDisplayName } from "./projection";
8
+ import type { ActivityItem, ReasoningItem, SandboxItem, WorkerItem } from "./types";
9
+
10
+ /* ----------------------------------------------------------------------------
11
+ Activity rail
12
+
13
+ Renders a run of clustered activity items (reasoning, tool calls, workers,
14
+ sandbox ops) as the left-bordered column between chat messages. Tool calls
15
+ resolve through the renderer registry; everything else has a first-class row.
16
+
17
+ Shared by `MessageTimeline` and the component demo so both draw the exact
18
+ same rail — no divergence.
19
+ -------------------------------------------------------------------------- */
20
+
21
+ export type ActivityRailProps = {
22
+ items: ActivityItem[];
23
+ /** Renderer registry for tool calls. Defaults to {@link defaultToolRegistry}. */
24
+ toolRegistry?: ToolRegistry | undefined;
25
+ /** Drill into a spawned worker session. */
26
+ onOpenSession?: ((sessionId: string) => void) | undefined;
27
+ /** Drop the left rule + indent (used inside a folded turn summary). */
28
+ bare?: boolean | undefined;
29
+ className?: string | undefined;
30
+ };
31
+
32
+ /**
33
+ * The "family" a row belongs to, for light intra-rail grouping. Consecutive
34
+ * rows of the same family sit tight; a family change gets a little extra top
35
+ * margin so a long run reads as clusters rather than one undifferentiated wall.
36
+ */
37
+ function familyOf(item: ActivityItem): string {
38
+ if (item.kind === "tool-call") {
39
+ return item.name === "exec_command" || item.name === "write_stdin" ? "terminal" : item.name;
40
+ }
41
+ return item.kind;
42
+ }
43
+
44
+ export function ActivityRail({ items, toolRegistry = defaultToolRegistry, onOpenSession, bare, className }: ActivityRailProps) {
45
+ return (
46
+ <div
47
+ className={cn(
48
+ // Rows sit TIGHT by default (gap-0.5) so a same-family run reads as one
49
+ // calm cluster; a family change opens real breathing room (mt-3) below,
50
+ // so a long rail reads as a few clusters, not a metronome of rows.
51
+ "flex flex-col gap-0.5",
52
+ !bare && "animate-og-enter border-l-2 border-og-border pl-3 sm:pl-4",
53
+ className,
54
+ )}
55
+ >
56
+ {items.map((item, index) => {
57
+ const newFamily = index > 0 && familyOf(item) !== familyOf(items[index - 1]!);
58
+ const row = renderActivity(item, toolRegistry, onOpenSession);
59
+ return (
60
+ <div key={item.id} className={cn(newFamily && "mt-3")}>
61
+ {row}
62
+ </div>
63
+ );
64
+ })}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ /** A never-reachable guard: adding an `ActivityItem` kind is now a compile error. */
70
+ function assertNever(item: never): never {
71
+ throw new Error(`ActivityRail: unhandled activity item ${JSON.stringify(item)}`);
72
+ }
73
+
74
+ function renderActivity(
75
+ item: ActivityItem,
76
+ toolRegistry: ToolRegistry,
77
+ onOpenSession: ((sessionId: string) => void) | undefined,
78
+ ) {
79
+ switch (item.kind) {
80
+ case "reasoning":
81
+ return <ReasoningRow item={item} />;
82
+ case "tool-call": {
83
+ const Renderer = toolRegistry.resolve(item);
84
+ return <Renderer item={item} />;
85
+ }
86
+ case "worker":
87
+ return <WorkerRow item={item} onOpenSession={onOpenSession} />;
88
+ case "sandbox":
89
+ return <SandboxRow item={item} />;
90
+ default:
91
+ return assertNever(item);
92
+ }
93
+ }
94
+
95
+ function ReasoningRow({ item }: { item: ReasoningItem }) {
96
+ // Reasoning recedes: a dimmer, lighter-weight title so action rows lead and
97
+ // thought rows sit a half-step back in the hierarchy.
98
+ return (
99
+ <ActivityDisclosure
100
+ icon={<BrainIcon className="size-3.5" />}
101
+ iconTone="muted"
102
+ title={
103
+ item.streaming ? (
104
+ "Thinking"
105
+ ) : (
106
+ <span className="font-normal italic text-og-fg-subtle">Thought</span>
107
+ )
108
+ }
109
+ running={item.streaming}
110
+ preview={truncate(item.text, 110)}
111
+ >
112
+ <p className="whitespace-pre-wrap text-og-base leading-6 text-og-fg-muted">{item.text}</p>
113
+ </ActivityDisclosure>
114
+ );
115
+ }
116
+
117
+ function SandboxRow({ item }: { item: SandboxItem }) {
118
+ return (
119
+ <ActivityDisclosure
120
+ icon={<SquareTerminalIcon className="size-3.5" />}
121
+ iconTone={item.status === "failed" ? "failed" : item.status === "running" ? "running" : "muted"}
122
+ title={toolDisplayName(item.name)}
123
+ running={item.status === "running"}
124
+ failed={item.status === "failed"}
125
+ cancelled={item.status === "cancelled"}
126
+ preview={item.command ?? undefined}
127
+ >
128
+ {item.command ? <PayloadBlock label="Command" value={item.command} /> : null}
129
+ {item.output ? <PayloadBlock label="Output" value={item.output} /> : null}
130
+ </ActivityDisclosure>
131
+ );
132
+ }
133
+
134
+ /** Spawned/messaged worker sessions get a first-class card, not a tool row. */
135
+ function WorkerRow({ item, onOpenSession }: { item: WorkerItem; onOpenSession?: ((sessionId: string) => void) | undefined }) {
136
+ const running = item.status === "running";
137
+ const failed = item.status === "failed";
138
+ const cancelled = item.status === "cancelled";
139
+ const title =
140
+ item.action === "spawn"
141
+ ? running
142
+ ? "Spawning worker"
143
+ : failed
144
+ ? "Worker spawn failed"
145
+ : cancelled
146
+ ? "Worker interrupted"
147
+ : "Worker spawned"
148
+ : running
149
+ ? "Messaging worker"
150
+ : failed
151
+ ? "Worker message failed"
152
+ : cancelled
153
+ ? "Worker interrupted"
154
+ : "Worker messaged";
155
+ return (
156
+ <div
157
+ className={cn(
158
+ "my-0.5 flex items-start gap-3 rounded-og-md border bg-og-surface-1 p-3",
159
+ failed ? "border-og-status-failed/40" : "border-og-border",
160
+ )}
161
+ >
162
+ <span
163
+ className={cn(
164
+ "mt-0.5 inline-flex size-7 shrink-0 items-center justify-center",
165
+ failed ? "text-og-status-failed" : "text-og-accent",
166
+ )}
167
+ >
168
+ <BotIcon className="size-4" />
169
+ </span>
170
+ <div className="min-w-0 flex-1">
171
+ {/* In-flight state is carried ONLY by the shimmering title (no detached
172
+ pulse badge), matching every other running row in the rail. */}
173
+ <span className={cn("text-og-base font-medium", running ? "og-shimmer-text" : failed ? "text-og-status-failed" : "text-og-fg")}>
174
+ {title}
175
+ </span>
176
+ {item.prompt ? <p className="mt-0.5 truncate text-og-sm text-og-fg-muted">{truncate(item.prompt, 140)}</p> : null}
177
+ {item.workerSessionId ? (
178
+ <p className="mt-1 font-og-mono text-og-xs text-og-fg-subtle">{item.workerSessionId.slice(0, 8)}</p>
179
+ ) : null}
180
+ </div>
181
+ {/* Right gutter: failed gets a red chip; cancelled gets a calm "interrupted"
182
+ chip (no dot, no red); a live complete worker shows an "Open session" button. */}
183
+ {failed ? (
184
+ <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">
185
+ <span className="size-1.5 rounded-full bg-og-status-failed" />
186
+ failed
187
+ </span>
188
+ ) : cancelled ? (
189
+ <span className="og-cancelled-chip shrink-0 self-center font-og-mono text-og-xs leading-none text-og-fg-subtle">
190
+ interrupted
191
+ </span>
192
+ ) : item.workerSessionId && onOpenSession ? (
193
+ <button
194
+ type="button"
195
+ onClick={() => item.workerSessionId && onOpenSession(item.workerSessionId)}
196
+ className={cn(
197
+ "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",
198
+ "outline-none transition-colors duration-150 hover:border-og-border-strong hover:text-og-fg",
199
+ "focus-visible:ring-2 focus-visible:ring-og-accent",
200
+ )}
201
+ >
202
+ Open session
203
+ </button>
204
+ ) : null}
205
+ </div>
206
+ );
207
+ }
@@ -0,0 +1,34 @@
1
+ import { createContext, useContext, type ReactNode } from "react";
2
+
3
+ /* ----------------------------------------------------------------------------
4
+ Disclosure defaults context
5
+
6
+ A tiny, opt-in context that lets an ancestor seed the INITIAL open state of
7
+ every collapsible in the timeline (ActivityDisclosure rows and TurnSummary
8
+ chips). Its sole intended use is deterministic screenshot capture: a tool can
9
+ force every card open so a headless render shows expanded bodies.
10
+
11
+ It is fully inert in normal app usage. With no provider, the hook returns
12
+ `undefined`, every collapsible keeps its own author-chosen default, and there
13
+ is zero change to how the components look or animate. Mounting the provider
14
+ only changes the SEED of the initial `open` state — Radix still owns the
15
+ open/close transition, so animations are untouched.
16
+ -------------------------------------------------------------------------- */
17
+
18
+ const DisclosureDefaultsContext = createContext<boolean | undefined>(undefined);
19
+
20
+ /**
21
+ * Seed the initial open state of every timeline collapsible below this node.
22
+ * Intended for screenshot/test instrumentation only; absent by default.
23
+ */
24
+ export function DisclosureDefaultsProvider({ defaultOpen, children }: { defaultOpen: boolean; children: ReactNode }) {
25
+ return <DisclosureDefaultsContext.Provider value={defaultOpen}>{children}</DisclosureDefaultsContext.Provider>;
26
+ }
27
+
28
+ /**
29
+ * The forced initial-open seed from an ancestor {@link DisclosureDefaultsProvider},
30
+ * or `undefined` when none is mounted (the inert, app-default case).
31
+ */
32
+ export function useForcedDefaultOpen(): boolean | undefined {
33
+ return useContext(DisclosureDefaultsContext);
34
+ }
@@ -0,0 +1,85 @@
1
+ /* ----------------------------------------------------------------------------
2
+ @opengeni/react — timeline module
3
+
4
+ The session timeline's data + rendering layer:
5
+ - projection.ts raw SessionEvents -> renderable TimelineItems (pure)
6
+ - types.ts the projected item shapes (the data contract)
7
+ - registry.ts the extensible, typed tool-renderer registry
8
+ - tool-renderers per-tool React renderers + the default registry
9
+ - parsers.ts pure provider-shape parsers (exec banner, V4A diff, …)
10
+ - shared.tsx the restraint primitives (ActivityDisclosure, TermBlock, …)
11
+ - tool-diff.tsx V4A GitFileDiff -> the real DiffView/PierreDiff stack
12
+ - screenshot-lightbox.tsx the app-level screenshot lightbox
13
+
14
+ This barrel is the module's public surface. The components here are the
15
+ single source of truth used by both the live app and the component demo.
16
+ -------------------------------------------------------------------------- */
17
+
18
+ // projection
19
+ export { buildTimeline, extractSessionRef, groupTimeline, sessionStatusFromEvents, toolDisplayName } from "./projection";
20
+
21
+ // item types
22
+ export type {
23
+ ActivityItem,
24
+ AgentMessageItem,
25
+ GoalItem,
26
+ NoticeItem,
27
+ ReasoningItem,
28
+ SandboxItem,
29
+ SessionStatusItem,
30
+ TimelineGroup,
31
+ TimelineItem,
32
+ ToolCallItem,
33
+ UserMessageItem,
34
+ WorkerItem,
35
+ } from "./types";
36
+
37
+ // renderer registry
38
+ export { createToolRegistry, rawTypeOf } from "./registry";
39
+ export type {
40
+ CreateToolRegistryOptions,
41
+ ToolRegistry,
42
+ ToolRegistryEntry,
43
+ ToolRenderer,
44
+ ToolRendererProps,
45
+ } from "./registry";
46
+
47
+ // default renderers + registry
48
+ export { createDefaultToolRegistry, defaultToolRegistry } from "./tool-renderers";
49
+
50
+ // the activity rail (the clustered reasoning/tool/worker/sandbox column)
51
+ export { ActivityRail } from "./activity-rail";
52
+ export type { ActivityRailProps } from "./activity-rail";
53
+
54
+ // shared primitives (extension authors compose these)
55
+ export { ActivityDisclosure, BodyNote, MediaEmpty, MediaSkeleton, PayloadBlock, ScreenshotFigure, TermBlock, Thumbnail } from "./shared";
56
+ export type { ActivityDisclosureProps, DisclosureChip } from "./shared";
57
+
58
+ // screenshot lightbox
59
+ export { LightboxProvider, useLightbox, useLightboxOptional } from "./screenshot-lightbox";
60
+
61
+ // disclosure defaults (opt-in initial-open seed; for screenshot/test instrumentation)
62
+ export { DisclosureDefaultsProvider, useForcedDefaultOpen } from "./disclosure-context";
63
+
64
+ // turn-collapse summary chip
65
+ export { TurnSummary } from "./turn-summary";
66
+ export type { TurnOutcome, TurnSummaryProps } from "./turn-summary";
67
+
68
+ // parsers (pure, reusable by custom renderers)
69
+ export {
70
+ applyPatchOps,
71
+ controlCaret,
72
+ execTruncated,
73
+ isApplyPatch,
74
+ isExecSessionLostBanner,
75
+ looksBinary,
76
+ parseExecBannerSessionId,
77
+ parseToolArgs,
78
+ redactSecrets,
79
+ sandboxCommandExitCode,
80
+ stripExecBanner,
81
+ tailPeek,
82
+ unwrapMcpOutput,
83
+ v4aToGitFileDiff,
84
+ } from "./parsers";
85
+ export type { ApplyPatchOperation } from "./parsers";