@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
@@ -106,6 +106,10 @@ export type CodeEditorProps = {
106
106
  initialContents: string;
107
107
  /** Persist the current buffer. Resolves when the write lands; rejects to surface an error. */
108
108
  onSave: (contents: string) => Promise<unknown>;
109
+ /** Fired once when the buffer FIRST diverges from its baseline (the first real
110
+ * keystroke) — the wake-on-edit intent. The host warms the box on this so the
111
+ * eventual save lands fast; merely opening the file for reading never fires it. */
112
+ onEditIntent?: (() => void) | undefined;
109
113
  /** Read-only mode (e.g. a truncated/too-large file shown for reference only). */
110
114
  readOnly?: boolean | undefined;
111
115
  themeType?: "dark" | "light" | undefined;
@@ -132,6 +136,7 @@ export function CodeEditor({
132
136
  path,
133
137
  initialContents,
134
138
  onSave,
139
+ onEditIntent,
135
140
  readOnly = false,
136
141
  themeType = "dark",
137
142
  loading,
@@ -155,18 +160,35 @@ export function CodeEditor({
155
160
  // Reload the buffer when the file identity changes. Guard on initialContents
156
161
  // too so an external refresh of the SAME path (e.g. fs.changed re-read) reseeds
157
162
  // a clean buffer — but only when the user hasn't got unsaved edits in flight.
163
+ // Fire the wake-on-edit intent at most once per opened file — reset on re-seed.
164
+ const editIntentFiredRef = useRef(false);
158
165
  const lastSeed = useRef<{ path: string; contents: string }>({ path, contents: initialContents });
159
166
  useEffect(() => {
160
167
  const seedChanged =
161
168
  lastSeed.current.path !== path || lastSeed.current.contents !== initialContents;
162
169
  if (!seedChanged) return;
163
170
  lastSeed.current = { path, contents: initialContents };
171
+ editIntentFiredRef.current = false;
164
172
  setValue(initialContents);
165
173
  setBaseline(initialContents);
166
174
  setSaveError(null);
167
175
  setSavedTick(false);
168
176
  }, [path, initialContents]);
169
177
 
178
+ // Record a buffer change: the FIRST divergence from the baseline is the edit
179
+ // intent that warms the box (idempotent — latched per opened file).
180
+ const noteEdit = useCallback(
181
+ (next: string) => {
182
+ setValue(next);
183
+ setSavedTick(false);
184
+ if (!editIntentFiredRef.current && next !== baseline) {
185
+ editIntentFiredRef.current = true;
186
+ onEditIntent?.();
187
+ }
188
+ },
189
+ [baseline, onEditIntent],
190
+ );
191
+
170
192
  // Resolve the lazy bundle (editor + keymap/Prec helpers + language grammar)
171
193
  // once, re-resolving the grammar when the file's language class changes.
172
194
  const langKey = useMemo(() => languageForPath(path), [path]);
@@ -257,16 +279,7 @@ export function CodeEditor({
257
279
  if (failed) {
258
280
  return (
259
281
  <div className={cn("flex h-full min-h-0 flex-col", className)}>
260
- {fallback ?? (
261
- <PlainTextarea
262
- value={value}
263
- readOnly={readOnly}
264
- onChange={(next) => {
265
- setValue(next);
266
- setSavedTick(false);
267
- }}
268
- />
269
- )}
282
+ {fallback ?? <PlainTextarea value={value} readOnly={readOnly} onChange={noteEdit} />}
270
283
  </div>
271
284
  );
272
285
  }
@@ -282,11 +295,7 @@ export function CodeEditor({
282
295
  <span
283
296
  className={cn(
284
297
  "size-1.5 shrink-0 rounded-full transition-colors",
285
- readOnly
286
- ? "bg-transparent"
287
- : dirty
288
- ? "bg-og-status-running"
289
- : "bg-og-status-idle",
298
+ readOnly ? "bg-transparent" : dirty ? "bg-og-status-running" : "bg-og-status-idle",
290
299
  )}
291
300
  title={readOnly ? "read-only" : dirty ? "unsaved changes" : "saved"}
292
301
  />
@@ -303,15 +312,9 @@ export function CodeEditor({
303
312
  {saveError.message}
304
313
  </span>
305
314
  )}
306
- {!saveError && savedTick && (
307
- <span className="text-og-xs text-og-status-idle">
308
- Saved
309
- </span>
310
- )}
315
+ {!saveError && savedTick && <span className="text-og-xs text-og-status-idle">Saved</span>}
311
316
  {readOnly ? (
312
- <span className="text-og-xs uppercase tracking-wide text-og-fg-subtle">
313
- Read-only
314
- </span>
317
+ <span className="text-og-xs uppercase tracking-wide text-og-fg-subtle">Read-only</span>
315
318
  ) : (
316
319
  <button
317
320
  type="button"
@@ -348,14 +351,7 @@ export function CodeEditor({
348
351
  extensions={cmExtensions}
349
352
  height="100%"
350
353
  className="og-cm-editor min-h-full text-og-sm"
351
- onChange={
352
- readOnly
353
- ? undefined
354
- : (next: string) => {
355
- setValue(next);
356
- setSavedTick(false);
357
- }
358
- }
354
+ onChange={readOnly ? undefined : noteEdit}
359
355
  />
360
356
  </Suspense>
361
357
  </div>
@@ -390,9 +386,5 @@ function PlainTextarea({
390
386
  }
391
387
 
392
388
  function EditorSkeleton() {
393
- return (
394
- <div className="p-3 text-og-sm text-og-fg-subtle">
395
- Loading editor…
396
- </div>
397
- );
389
+ return <div className="p-3 text-og-sm text-og-fg-subtle">Loading editor…</div>;
398
390
  }
@@ -23,7 +23,15 @@ export type CommandPaletteProps = {
23
23
  * the opengeni#46 design tokens. Full keyboard nav lives in useSlashCommands;
24
24
  * this component is presentational + aria.
25
25
  */
26
- export function CommandPalette({ open, items, highlight, onHighlight, onRun, argHintText, listboxId }: CommandPaletteProps) {
26
+ export function CommandPalette({
27
+ open,
28
+ items,
29
+ highlight,
30
+ onHighlight,
31
+ onRun,
32
+ argHintText,
33
+ listboxId,
34
+ }: CommandPaletteProps) {
27
35
  return (
28
36
  <AnimatePresence>
29
37
  {open ? (
@@ -61,14 +69,25 @@ export function CommandPalette({ open, items, highlight, onHighlight, onRun, arg
61
69
  className={cn(
62
70
  "mx-1 flex cursor-pointer items-center gap-2 rounded-og-md px-2.5 py-1.5",
63
71
  "transition-colors duration-100",
64
- selected ? "bg-og-accent/15 text-og-fg" : "text-og-fg-muted hover:bg-og-surface-3",
72
+ selected
73
+ ? "bg-og-accent/15 text-og-fg"
74
+ : "text-og-fg-muted hover:bg-og-surface-3",
65
75
  )}
66
76
  >
67
77
  <span className="flex min-w-0 items-baseline gap-1.5">
68
- <span className={cn("font-mono text-[13px]", selected ? "text-og-accent" : "text-og-fg")}>
78
+ <span
79
+ className={cn(
80
+ "font-mono text-[13px]",
81
+ selected ? "text-og-accent" : "text-og-fg",
82
+ )}
83
+ >
69
84
  /{command.name}
70
85
  </span>
71
- {hint ? <span className="truncate font-mono text-[11px] text-og-fg-subtle">{hint}</span> : null}
86
+ {hint ? (
87
+ <span className="truncate font-mono text-[11px] text-og-fg-subtle">
88
+ {hint}
89
+ </span>
90
+ ) : null}
72
91
  </span>
73
92
  <span className="ml-auto flex items-center gap-1.5">
74
93
  {command.danger ? (
@@ -76,7 +95,9 @@ export function CommandPalette({ open, items, highlight, onHighlight, onRun, arg
76
95
  danger
77
96
  </span>
78
97
  ) : null}
79
- <span className="truncate text-[12px] text-og-fg-subtle max-sm:hidden">{command.description}</span>
98
+ <span className="truncate text-[12px] text-og-fg-subtle max-sm:hidden">
99
+ {command.description}
100
+ </span>
80
101
  </span>
81
102
  </li>
82
103
  );
@@ -1,4 +1,8 @@
1
- import type { CapabilityUnavailableReason, DesktopRfbFactory, DesktopStreamCapability } from "@opengeni/sdk";
1
+ import type {
2
+ CapabilityUnavailableReason,
3
+ DesktopRfbFactory,
4
+ DesktopStreamCapability,
5
+ } from "@opengeni/sdk";
2
6
  import { LoaderCircleIcon, MonitorIcon, MousePointerClickIcon, WifiOffIcon } from "lucide-react";
3
7
  import { type ReactNode, useEffect, useRef, useState } from "react";
4
8
  import { cn } from "../lib/cn";
@@ -214,8 +218,7 @@ export function DesktopViewer({
214
218
  // Do NOT open a socket while the viewer-cap (429) notice is showing — the slot
215
219
  // is already exhausted, so connecting would only burn a doomed attempt (and in
216
220
  // tests leak an unhandled ws error from the never-resolving tunnel URL).
217
- const connectCapability =
218
- !transportNull && !needsAck && !viewerCapReached ? capability : null;
221
+ const connectCapability = !transportNull && !needsAck && !viewerCapReached ? capability : null;
219
222
  const stream = useDesktopStream({
220
223
  capability: connectCapability,
221
224
  containerRef,
@@ -309,7 +312,14 @@ export function DesktopViewer({
309
312
  if (!hasLiveUrl || connected || stream.error || connectTimeoutMs <= 0) return;
310
313
  const timer = setTimeout(() => setConnectTimedOut(true), connectTimeoutMs);
311
314
  return () => clearTimeout(timer);
312
- }, [hasLiveUrl, connected, connectTimeoutMs, connectCapability?.url, stream.error, reconnectNonce]);
315
+ }, [
316
+ hasLiveUrl,
317
+ connected,
318
+ connectTimeoutMs,
319
+ connectCapability?.url,
320
+ stream.error,
321
+ reconnectNonce,
322
+ ]);
313
323
 
314
324
  const reconnect = () => {
315
325
  setConnectTimedOut(false);
@@ -338,11 +348,15 @@ export function DesktopViewer({
338
348
  case "viewer_cap":
339
349
  overlay =
340
350
  renderViewerCap?.() ??
341
- defaultNotice("Too many viewers", "This session has reached its live-viewer limit. Try again shortly.");
351
+ defaultNotice(
352
+ "Too many viewers",
353
+ "This session has reached its live-viewer limit. Try again shortly.",
354
+ );
342
355
  break;
343
356
  case "unavailable":
344
357
  overlay =
345
- renderUnavailable?.(reason) ?? defaultNotice("Desktop unavailable", unavailableCopy(reason));
358
+ renderUnavailable?.(reason) ??
359
+ defaultNotice("Desktop unavailable", unavailableCopy(reason));
346
360
  break;
347
361
  case "consent":
348
362
  overlay = renderConsentGate ? (
@@ -362,7 +376,11 @@ export function DesktopViewer({
362
376
  );
363
377
  break;
364
378
  case "error":
365
- overlay = defaultNotice("Desktop disconnected", stream.error?.message ?? "The stream dropped.", reconnect);
379
+ overlay = defaultNotice(
380
+ "Desktop disconnected",
381
+ stream.error?.message ?? "The stream dropped.",
382
+ reconnect,
383
+ );
366
384
  break;
367
385
  case "connecting":
368
386
  case "connected":
@@ -511,9 +529,7 @@ function TakeControlCallToAction({
511
529
  <MousePointerClickIcon className="size-5" strokeWidth={2} />
512
530
  </span>
513
531
  <span className="flex flex-col items-start leading-tight">
514
- <span className="text-og-base font-semibold text-og-fg">
515
- Take control
516
- </span>
532
+ <span className="text-og-base font-semibold text-og-fg">Take control</span>
517
533
  <span className="text-og-xs text-og-fg-subtle">
518
534
  {disabled && disabledReason ? disabledReason : "Drive the mouse & keyboard"}
519
535
  </span>
@@ -594,10 +610,7 @@ function unavailableCopy(reason: CapabilityUnavailableReason | null): string {
594
610
  function WarmingNotice({ onRetry }: { onRetry?: (() => void) | undefined }) {
595
611
  return (
596
612
  <div className="flex max-w-sm flex-col items-center gap-3 rounded-og-lg border border-og-border bg-og-bg/90 p-5 text-center text-og-base text-og-fg backdrop-blur-sm">
597
- <LoaderCircleIcon
598
- className="size-7 animate-og-spin text-og-accent"
599
- strokeWidth={1.5}
600
- />
613
+ <LoaderCircleIcon className="size-7 animate-og-spin text-og-accent" strokeWidth={1.5} />
601
614
  <div className="space-y-1">
602
615
  <div className="font-medium">Warming the sandbox…</div>
603
616
  <p className="text-og-sm text-og-fg-subtle">
@@ -622,8 +635,8 @@ function DefaultConsentGate({ shared, onAccept }: { shared: boolean; onAccept: (
622
635
  <div className="max-w-sm rounded-og-lg border border-og-border bg-og-bg p-4 text-center text-og-base text-og-fg">
623
636
  <div className="mb-1 font-medium">Watch the live desktop?</div>
624
637
  <p className="mb-3 text-og-sm text-og-fg-subtle">
625
- The desktop pixel stream is <strong>un-redacted</strong> — it can show secrets the agent prints
626
- on screen.
638
+ The desktop pixel stream is <strong>un-redacted</strong> — it can show secrets the agent
639
+ prints on screen.
627
640
  {shared
628
641
  ? " This sandbox is shared: you'll also see other sessions' agents on the same screen."
629
642
  : ""}
@@ -1,8 +1,14 @@
1
- import type { GitDiffLine, GitFileDiff } from "@opengeni/sdk";
1
+ import type { GitFileDiff } from "@opengeni/sdk";
2
2
  import { type ReactNode } from "react";
3
3
  import { cn } from "../lib/cn";
4
+ import { PierreDiff } from "./pierre-diff";
4
5
 
5
- /** Theme tokens for the diff line backgrounds. */
6
+ /**
7
+ * @deprecated Use {@link PierreDiff} directly. Retained only as a thin,
8
+ * back-compat alias that renders the Pierre (Shiki) renderer with a plain-text
9
+ * degrade. The old hand-rolled hunk renderer was removed in Workbench v2 — the
10
+ * workbench keeps exactly ONE diff renderer. This alias is removed next major.
11
+ */
6
12
  export type DiffTheme = {
7
13
  addBackground?: string;
8
14
  delBackground?: string;
@@ -10,54 +16,39 @@ export type DiffTheme = {
10
16
  metaForeground?: string;
11
17
  };
12
18
 
19
+ /**
20
+ * @deprecated Use {@link PierreDiff}. `theme`/`onSelectFile`/`emptyState` from the
21
+ * removed hand-rolled renderer are accepted for source compatibility and ignored;
22
+ * `fallback` still overrides the plain-text degrade.
23
+ */
13
24
  export type DiffViewProps = {
14
- /** From `useSandboxGit().diff` — the structured per-file hunks. */
15
25
  diff: GitFileDiff[];
16
- /** Rendered instead of the built-in diff (the Pierre-swap escape hatch). */
17
26
  fallback?: ReactNode | undefined;
18
- /** unified | split. Default "unified". */
19
27
  layout?: "unified" | "split" | undefined;
28
+ themeType?: "dark" | "light" | undefined;
29
+ /** @deprecated line-background theming was part of the removed renderer. */
20
30
  theme?: DiffTheme | undefined;
31
+ /** @deprecated the alias no longer renders a clickable file header. */
21
32
  onSelectFile?: ((path: string) => void) | undefined;
22
- /** Distinguishes "no changes" from "no repository mounted". */
23
33
  isRepo?: boolean | undefined;
24
34
  emptyState?: ReactNode | undefined;
25
35
  className?: string | undefined;
26
36
  };
27
37
 
28
- const STATUS_LABEL: Record<GitFileDiff["status"], string> = {
29
- added: "added",
30
- modified: "modified",
31
- deleted: "deleted",
32
- renamed: "renamed",
33
- copied: "copied",
34
- untracked: "untracked",
35
- ignored: "ignored",
36
- conflicted: "conflicted",
37
- typechange: "typechange",
38
- };
39
-
40
38
  /**
41
- * The diff view, fed by the Git service via `useSandboxGit().diff`. This is our
42
- * `PierreDiff` boundary: the built-in renderer delivers the same UX (per-file
43
- * hunks, unified or side-by-side, old/new line numbers, add/del counts), and a
44
- * consumer with Pierre's `@pierre/diffs` installed can swap it via `fallback`
45
- * while keeping the same `GitFileDiff[]` data contract from the hook.
39
+ * @deprecated Use {@link PierreDiff} directly. Thin alias: renders `PierreDiff`
40
+ * (Shiki-highlighted, unified/split) with a plain-text fallback for a host
41
+ * without `@pierre/diffs`. Removed next major.
46
42
  */
47
43
  export function DiffView({
48
44
  diff,
49
45
  fallback,
50
46
  layout = "unified",
51
- theme,
52
- onSelectFile,
47
+ themeType,
53
48
  isRepo = true,
54
49
  emptyState,
55
50
  className,
56
51
  }: DiffViewProps) {
57
- if (fallback !== undefined) {
58
- return <div className={className}>{fallback}</div>;
59
- }
60
-
61
52
  if (diff.length === 0) {
62
53
  return (
63
54
  <div className={cn("p-3 text-og-sm text-og-fg-subtle", className)}>
@@ -65,166 +56,13 @@ export function DiffView({
65
56
  </div>
66
57
  );
67
58
  }
68
-
69
- return (
70
- <div className={cn("min-w-0 overflow-auto", className)} data-opengeni-diff>
71
- {diff.map((file) => (
72
- <FileDiffBlock
73
- key={file.path}
74
- file={file}
75
- layout={layout}
76
- theme={theme}
77
- {...(onSelectFile ? { onSelect: () => onSelectFile(file.path) } : {})}
78
- />
79
- ))}
80
- </div>
81
- );
82
- }
83
-
84
- function FileDiffBlock({
85
- file,
86
- layout,
87
- theme,
88
- onSelect,
89
- }: {
90
- file: GitFileDiff;
91
- layout: "unified" | "split";
92
- theme: DiffTheme | undefined;
93
- onSelect?: (() => void) | undefined;
94
- }) {
95
- return (
96
- <section className="mb-2 overflow-hidden rounded-og-sm border border-og-border">
97
- <header
98
- className={cn(
99
- "flex items-center justify-between gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1 text-og-sm",
100
- onSelect && "cursor-pointer",
101
- )}
102
- onClick={onSelect}
103
- >
104
- <span className="truncate font-og-mono">
105
- {file.oldPath && file.oldPath !== file.path ? `${file.oldPath} → ${file.path}` : file.path}
106
- </span>
107
- <span className="flex shrink-0 items-center gap-2">
108
- <span className="rounded-og-xs bg-og-bg px-1 py-0.5 text-og-xs text-og-fg-subtle">
109
- {STATUS_LABEL[file.status]}
110
- </span>
111
- <span className="text-og-status-idle">+{file.additions}</span>
112
- <span className="text-og-status-failed">−{file.deletions}</span>
113
- </span>
114
- </header>
115
- {file.isBinary ? (
116
- <div className="px-2 py-3 text-og-sm text-og-fg-subtle">Binary file not shown</div>
117
- ) : file.truncated ? (
118
- <div className="px-2 py-3 text-og-sm text-og-fg-subtle">Diff too large — truncated</div>
119
- ) : layout === "split" ? (
120
- <SplitHunks file={file} theme={theme} />
121
- ) : (
122
- <UnifiedHunks file={file} theme={theme} />
123
- )}
124
- </section>
125
- );
126
- }
127
-
128
- function lineBg(type: GitDiffLine["type"], theme: DiffTheme | undefined): string | undefined {
129
- if (type === "add") return theme?.addBackground ?? "var(--og-color-diff-add-bg)";
130
- if (type === "del") return theme?.delBackground ?? "var(--og-color-diff-del-bg)";
131
- if (type === "meta") return undefined;
132
- return theme?.contextBackground;
133
- }
134
-
135
- function marker(type: GitDiffLine["type"]): string {
136
- if (type === "add") return "+";
137
- if (type === "del") return "−";
138
- return " ";
139
- }
140
-
141
- function UnifiedHunks({ file, theme }: { file: GitFileDiff; theme: DiffTheme | undefined }) {
142
- return (
143
- <div className="font-og-mono text-og-xs">
144
- {file.hunks.map((hunk, hi) => (
145
- <div key={`${file.path}-h${hi}`}>
146
- <div
147
- className="px-2 py-0.5 text-og-accent"
148
- style={{ color: theme?.metaForeground }}
149
- >
150
- {hunk.header}
151
- </div>
152
- {hunk.lines.map((line, li) => (
153
- <div
154
- key={`${file.path}-h${hi}-l${li}`}
155
- className="flex"
156
- style={{ backgroundColor: lineBg(line.type, theme) }}
157
- >
158
- <span className="w-10 shrink-0 select-none px-1 text-right text-og-fg-subtle">
159
- {line.oldNo ?? ""}
160
- </span>
161
- <span className="w-10 shrink-0 select-none px-1 text-right text-og-fg-subtle">
162
- {line.newNo ?? ""}
163
- </span>
164
- <span className="w-4 shrink-0 select-none text-center text-og-fg-subtle">
165
- {marker(line.type)}
166
- </span>
167
- <span className="whitespace-pre-wrap break-all px-1">{line.text}</span>
168
- </div>
169
- ))}
170
- </div>
171
- ))}
172
- </div>
173
- );
174
- }
175
-
176
- function SplitHunks({ file, theme }: { file: GitFileDiff; theme: DiffTheme | undefined }) {
177
59
  return (
178
- <div className="font-og-mono text-og-xs">
179
- {file.hunks.map((hunk, hi) => {
180
- // Pair del/add lines side-by-side; context spans both columns.
181
- const left: (GitDiffLine | null)[] = [];
182
- const right: (GitDiffLine | null)[] = [];
183
- for (const line of hunk.lines) {
184
- if (line.type === "context" || line.type === "meta") {
185
- left.push(line);
186
- right.push(line);
187
- } else if (line.type === "del") {
188
- left.push(line);
189
- } else {
190
- right.push(line);
191
- }
192
- }
193
- const rows = Math.max(left.length, right.length);
194
- return (
195
- <div key={`${file.path}-h${hi}`}>
196
- <div className="px-2 py-0.5 text-og-accent" style={{ color: theme?.metaForeground }}>
197
- {hunk.header}
198
- </div>
199
- {Array.from({ length: rows }).map((_, ri) => {
200
- const l = left[ri] ?? null;
201
- const r = right[ri] ?? null;
202
- return (
203
- <div key={`${file.path}-h${hi}-r${ri}`} className="flex">
204
- <span
205
- className="flex w-1/2 min-w-0"
206
- style={{ backgroundColor: l ? lineBg(l.type, theme) : undefined }}
207
- >
208
- <span className="w-10 shrink-0 select-none px-1 text-right text-og-fg-subtle">
209
- {l?.oldNo ?? ""}
210
- </span>
211
- <span className="whitespace-pre-wrap break-all px-1">{l?.text ?? ""}</span>
212
- </span>
213
- <span
214
- className="flex w-1/2 min-w-0 border-l border-og-border"
215
- style={{ backgroundColor: r ? lineBg(r.type, theme) : undefined }}
216
- >
217
- <span className="w-10 shrink-0 select-none px-1 text-right text-og-fg-subtle">
218
- {r?.newNo ?? ""}
219
- </span>
220
- <span className="whitespace-pre-wrap break-all px-1">{r?.text ?? ""}</span>
221
- </span>
222
- </div>
223
- );
224
- })}
225
- </div>
226
- );
227
- })}
228
- </div>
60
+ <PierreDiff
61
+ diff={diff}
62
+ layout={layout}
63
+ themeType={themeType}
64
+ {...(fallback !== undefined ? { fallback } : {})}
65
+ className={className}
66
+ />
229
67
  );
230
68
  }
@@ -95,7 +95,10 @@ export function EnrollmentConsent({
95
95
  className={className}
96
96
  tone="danger"
97
97
  title="Could not complete enrollment"
98
- body={errorMessage ?? "The code may have expired. Re-run the install one-liner on the machine for a fresh code."}
98
+ body={
99
+ errorMessage ??
100
+ "The code may have expired. Re-run the install one-liner on the machine for a fresh code."
101
+ }
99
102
  />
100
103
  );
101
104
  }
@@ -113,10 +116,13 @@ export function EnrollmentConsent({
113
116
  <ShieldAlertIcon className="size-5" aria-hidden />
114
117
  </span>
115
118
  <div className="min-w-0">
116
- <h1 className="text-og-md font-semibold text-og-fg">Give the agent your whole machine?</h1>
119
+ <h1 className="text-og-md font-semibold text-og-fg">
120
+ Give the agent your whole machine?
121
+ </h1>
117
122
  <p className="mt-1 text-og-base text-og-fg-muted">
118
- Approving lets the OpenGeni agent run on <span className="font-medium text-og-fg">{machine.machineName}</span> with
119
- full access. This is your real computer — not a sandbox.
123
+ Approving lets the OpenGeni agent run on{" "}
124
+ <span className="font-medium text-og-fg">{machine.machineName}</span> with full access.
125
+ This is your real computer — not a sandbox.
120
126
  </p>
121
127
  </div>
122
128
  </div>
@@ -127,7 +133,9 @@ export function EnrollmentConsent({
127
133
  <span className="font-og-mono text-og-fg-subtle">
128
134
  {machine.os}/{machine.arch}
129
135
  </span>
130
- <span aria-hidden className="text-og-fg-subtle">·</span>
136
+ <span aria-hidden className="text-og-fg-subtle">
137
+ ·
138
+ </span>
131
139
  <span className="text-og-fg-subtle">
132
140
  code <span className="font-og-mono font-medium text-og-fg">{userCode}</span>
133
141
  </span>
@@ -171,15 +179,16 @@ export function EnrollmentConsent({
171
179
  Also let the agent control my mouse & keyboard
172
180
  </span>
173
181
  <span className="mt-0.5 block text-og-sm text-og-fg-muted">
174
- Optional. Enables computer-use — the agent can move the pointer, type, and click on this machine's desktop. Leave
175
- off to let it only watch.
182
+ Optional. Enables computer-use — the agent can move the pointer, type, and click on
183
+ this machine's desktop. Leave off to let it only watch.
176
184
  </span>
177
185
  </span>
178
186
  </label>
179
187
  ) : (
180
188
  <p className="flex items-start gap-2 rounded-og-md border border-og-border bg-og-bg px-3 py-2 text-og-sm text-og-fg-muted">
181
189
  <CircleAlertIcon className="mt-px size-3.5 shrink-0 text-og-fg-subtle" aria-hidden />
182
- This machine has no display, so screen control isn't available — files, terminal, and git only.
190
+ This machine has no display, so screen control isn't available — files, terminal, and git
191
+ only.
183
192
  </p>
184
193
  )}
185
194
 
@@ -225,7 +234,11 @@ function ConsentResult({
225
234
  ? "border-og-status-failed/30"
226
235
  : "border-og-border";
227
236
  const iconClass =
228
- tone === "ok" ? "text-og-status-running" : tone === "danger" ? "text-og-status-failed" : "text-og-fg-subtle";
237
+ tone === "ok"
238
+ ? "text-og-status-running"
239
+ : tone === "danger"
240
+ ? "text-og-status-failed"
241
+ : "text-og-fg-subtle";
229
242
  return (
230
243
  <div
231
244
  data-enrollment-result={tone}
@@ -235,7 +248,12 @@ function ConsentResult({
235
248
  className,
236
249
  )}
237
250
  >
238
- <span className={cn("flex size-10 items-center justify-center rounded-full bg-og-surface-2", iconClass)}>
251
+ <span
252
+ className={cn(
253
+ "flex size-10 items-center justify-center rounded-full bg-og-surface-2",
254
+ iconClass,
255
+ )}
256
+ >
239
257
  <LaptopIcon className="size-5" aria-hidden />
240
258
  </span>
241
259
  <h1 className="text-og-md font-semibold text-og-fg">{title}</h1>
@@ -99,7 +99,9 @@ export function EnrollmentDeviceFlow({
99
99
 
100
100
  {installCommand ? (
101
101
  <div className="flex flex-col gap-1.5">
102
- <span className="text-[11px] font-medium uppercase tracking-wide text-og-fg-subtle">1 · Run on the machine</span>
102
+ <span className="text-[11px] font-medium uppercase tracking-wide text-og-fg-subtle">
103
+ 1 · Run on the machine
104
+ </span>
103
105
  <div className="flex items-center justify-between gap-2 overflow-hidden rounded-og-md border border-og-border bg-og-bg px-2.5 py-1.5">
104
106
  <code className="flex min-w-0 items-center gap-2 overflow-x-auto font-og-mono text-[12px] text-og-fg">
105
107
  <TerminalIcon className="size-3.5 shrink-0 text-og-fg-subtle" aria-hidden />
@@ -164,13 +166,14 @@ export function EnrollmentDeviceFlow({
164
166
  {awaitingCode ? (
165
167
  <>
166
168
  The one-liner prints a short code. Open{" "}
167
- <span className="font-og-mono text-og-fg-muted">{verificationUri}</span> and confirm the code shown there
168
- matches.
169
+ <span className="font-og-mono text-og-fg-muted">{verificationUri}</span> and confirm the
170
+ code shown there matches.
169
171
  </>
170
172
  ) : (
171
173
  <>
172
- Confirm code <span className="font-og-mono text-og-fg-muted">{userCode}</span> — the one printed by the
173
- one-liner — at <span className="font-og-mono text-og-fg-muted">{verificationUri}</span>.
174
+ Confirm code <span className="font-og-mono text-og-fg-muted">{userCode}</span> — the one
175
+ printed by the one-liner — at{" "}
176
+ <span className="font-og-mono text-og-fg-muted">{verificationUri}</span>.
174
177
  {typeof expiresInSeconds === "number" ? (
175
178
  <> Expires in {Math.max(0, Math.round(expiresInSeconds / 60))} min.</>
176
179
  ) : null}