@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
@@ -22,10 +22,9 @@ export type MachineDockBarProps = {
22
22
  * render IDENTICALLY regardless of backend — this bar is the only backend-aware
23
23
  * chrome (the dock-parity contract: selfhosted == Modal for the surfaces).
24
24
  */
25
- export function MachineDockBar({ name, kind, state, sharedSessionCount, className }: MachineDockBarProps) {
25
+ export function MachineDockBar({ name, kind, state, className }: MachineDockBarProps) {
26
26
  const Icon = kind === "selfhosted" ? LaptopIcon : CpuIcon;
27
27
  const stateBadge = MACHINE_STATE_BADGE_META[state];
28
- const shared = (sharedSessionCount ?? 0) > 1;
29
28
  return (
30
29
  <div
31
30
  data-machine-dock-bar
@@ -40,13 +39,20 @@ export function MachineDockBar({ name, kind, state, sharedSessionCount, classNam
40
39
  {stateBadge ? (
41
40
  <span
42
41
  data-state-badge={state}
43
- className={cn("shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium", stateBadge.badgeClassName)}
42
+ className={cn(
43
+ "shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium",
44
+ stateBadge.badgeClassName,
45
+ )}
44
46
  >
45
47
  {stateBadge.label}
46
48
  </span>
47
49
  ) : null}
48
50
  </div>
49
- <ConnectionStatusPill status={connectionStatusForState(state)} size="sm" className="shrink-0" />
51
+ <ConnectionStatusPill
52
+ status={connectionStatusForState(state)}
53
+ size="sm"
54
+ className="shrink-0"
55
+ />
50
56
  </div>
51
57
  );
52
58
  }
@@ -63,21 +69,27 @@ export type SharedMachineDisclosureProps = {
63
69
  * dock (and reused on the desktop take-control gate) so the user knows others are
64
70
  * driving the same whole-machine lease. Render when `sharedSessionCount > 1`.
65
71
  */
66
- export function SharedMachineDisclosure({ sharedSessionCount, density = "compact", className }: SharedMachineDisclosureProps) {
72
+ export function SharedMachineDisclosure({
73
+ sharedSessionCount,
74
+ density = "compact",
75
+ className,
76
+ }: SharedMachineDisclosureProps) {
67
77
  const others = Math.max(0, sharedSessionCount - 1);
68
78
  return (
69
79
  <div
70
80
  data-shared-disclosure
71
81
  className={cn(
72
82
  "flex items-center gap-1.5 border-og-accent/25 bg-og-accent-soft text-og-fg-muted",
73
- density === "full" ? "rounded-og-md border px-3 py-2 text-[12px]" : "border-b px-2.5 py-1 text-[11px]",
83
+ density === "full"
84
+ ? "rounded-og-md border px-3 py-2 text-[12px]"
85
+ : "border-b px-2.5 py-1 text-[11px]",
74
86
  className,
75
87
  )}
76
88
  >
77
89
  <UsersIcon className="size-3.5 shrink-0 text-og-accent" aria-hidden />
78
90
  <span>
79
- Shared — {others === 1 ? "another session is" : `${others} other sessions are`} on this machine. They see the same
80
- terminal, files, and desktop.
91
+ Shared — {others === 1 ? "another session is" : `${others} other sessions are`} on this
92
+ machine. They see the same terminal, files, and desktop.
81
93
  </span>
82
94
  </div>
83
95
  );
@@ -0,0 +1,68 @@
1
+ // The card's primary status: the fused HEALTH verdict (dot + label), which
2
+ // subsumes plain connection state (offline/reconnecting fall out of it) and adds
3
+ // resource-pressure + staleness. Capability limitations (consent / no display /
4
+ // enrolling) are orthogonal to reachability, so they ride alongside as their own
5
+ // quiet chip — reusing the existing badge meta so the two surfaces never drift.
6
+ import { cn } from "../lib/cn";
7
+ import type { MachineState, MetricSample } from "../types/machines";
8
+ import { deriveHealth, HEALTH_TOKEN, healthPulses } from "./machines/health";
9
+ import { MACHINE_STATE_BADGE_META } from "./machine-status-pill";
10
+
11
+ export type MachineHealthPillProps = {
12
+ state: MachineState;
13
+ metrics: MetricSample | null;
14
+ now?: number | undefined;
15
+ size?: "sm" | "md" | undefined;
16
+ className?: string | undefined;
17
+ };
18
+
19
+ export function MachineHealthPill({
20
+ state,
21
+ metrics,
22
+ now,
23
+ size = "sm",
24
+ className,
25
+ }: MachineHealthPillProps) {
26
+ const health = deriveHealth(state, metrics, now ?? Date.now());
27
+ const tone = HEALTH_TOKEN[health.level];
28
+ const badge = MACHINE_STATE_BADGE_META[state];
29
+ const pad = size === "md" ? "px-2.5 py-1 text-og-sm" : "px-2 py-0.5 text-og-xs";
30
+
31
+ return (
32
+ <div className={cn("flex shrink-0 items-center gap-1.5", className)}>
33
+ {badge ? (
34
+ <span
35
+ className={cn(
36
+ "rounded-full border px-2 py-0.5 text-og-xs font-medium",
37
+ badge.badgeClassName,
38
+ )}
39
+ >
40
+ {badge.label}
41
+ </span>
42
+ ) : null}
43
+ <span
44
+ className={cn(
45
+ "inline-flex items-center gap-1.5 rounded-full border font-medium",
46
+ pad,
47
+ tone.soft,
48
+ tone.text,
49
+ )}
50
+ data-health={health.level}
51
+ title={health.reason}
52
+ >
53
+ <span className="relative flex size-2">
54
+ {healthPulses(health.level) && (
55
+ <span
56
+ className={cn(
57
+ "absolute inline-flex size-full animate-og-pulse rounded-full opacity-60",
58
+ tone.dot,
59
+ )}
60
+ />
61
+ )}
62
+ <span className={cn("relative inline-flex size-2 rounded-full", tone.dot)} />
63
+ </span>
64
+ {health.label}
65
+ </span>
66
+ </div>
67
+ );
68
+ }
@@ -28,12 +28,20 @@ function Meter({
28
28
  fillPct: number;
29
29
  tone: "ok" | "warn" | "hot";
30
30
  }) {
31
+ // Traffic-light ramp, aligned with the fused health verdict: calm resources
32
+ // read green (idle), pressure ambers (running), near-the-wall reds (failed).
31
33
  const fillClass =
32
- tone === "hot" ? "bg-og-status-failed" : tone === "warn" ? "bg-og-status-waiting" : "bg-og-status-running";
34
+ tone === "hot"
35
+ ? "bg-og-status-failed"
36
+ : tone === "warn"
37
+ ? "bg-og-status-running"
38
+ : "bg-og-status-idle";
33
39
  return (
34
40
  <div className="flex min-w-0 flex-col gap-1" data-metric={label.toLowerCase()}>
35
41
  <div className="flex items-baseline justify-between gap-2">
36
- <span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">{label}</span>
42
+ <span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">
43
+ {label}
44
+ </span>
37
45
  <span className="font-og-mono text-[11px] tabular-nums text-og-fg-muted">{value}</span>
38
46
  </div>
39
47
  <div className="h-1 w-full overflow-hidden rounded-full bg-og-surface-2">
@@ -53,16 +61,12 @@ function toneFor(p: number): "ok" | "warn" | "hot" {
53
61
  return "ok";
54
62
  }
55
63
 
56
- /** The token text color for a tone (matches the `Meter` fill ramp). */
57
- function toneTextClass(tone: "ok" | "warn" | "hot"): string {
58
- return tone === "hot" ? "text-og-status-failed" : tone === "warn" ? "text-og-status-waiting" : "text-og-status-running";
59
- }
60
-
61
64
  /**
62
65
  * Load average as three labeled mini-stats (1m / 5m / 15m) — an honest triple of
63
- * numbers, NOT a fake gauge. The whole triple is tinted by `load1`'s tone (load
64
- * isn't a 0..100 ratio, so we tone it but never draw a bar). Run queue rides
65
- * alongside as a quiet chip when there's pending work.
66
+ * numbers, NOT a fake gauge. Load is not a 0..100 ratio and we don't know the
67
+ * core count, so a raw load of 2 or 8 is not inherently "bad" we render it
68
+ * NEUTRAL rather than falsely alarming. The real saturation signal is the run
69
+ * queue, which rides alongside as a quiet chip when there's pending work.
66
70
  */
67
71
  function StatTriple({
68
72
  load1,
@@ -75,9 +79,6 @@ function StatTriple({
75
79
  load15: number;
76
80
  runQueue: number;
77
81
  }) {
78
- // Load isn't a percent — tone by load1 against a nominal single-core ceiling
79
- // (≥0.9/core ≈ saturated). Used only to tint the labels/values, never a bar.
80
- const tone = toneFor(load1 * 100);
81
82
  const stats: Array<{ label: string; value: number }> = [
82
83
  { label: "1m", value: load1 },
83
84
  { label: "5m", value: load5 },
@@ -86,14 +87,16 @@ function StatTriple({
86
87
  return (
87
88
  <div className="flex items-center justify-between gap-3" data-metric="load">
88
89
  <div className="flex min-w-0 flex-col gap-1">
89
- <span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">Load</span>
90
+ <span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">
91
+ Load
92
+ </span>
90
93
  <div className="flex items-baseline gap-3">
91
94
  {stats.map((s) => (
92
95
  <div key={s.label} className="flex items-baseline gap-1">
93
- <span className={cn("text-[10px] font-medium uppercase tracking-wide", toneTextClass(tone))}>
96
+ <span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">
94
97
  {s.label}
95
98
  </span>
96
- <span className={cn("font-og-mono text-[12px] tabular-nums", toneTextClass(tone))}>
99
+ <span className="font-og-mono text-[12px] tabular-nums text-og-fg">
97
100
  {s.value.toFixed(2)}
98
101
  </span>
99
102
  </div>
@@ -128,8 +131,10 @@ export function MachineMetrics({ metrics, density = "compact", className }: Mach
128
131
  );
129
132
  }
130
133
 
131
- const memPct = metrics.memTotalBytes > 0 ? (metrics.memUsedBytes / metrics.memTotalBytes) * 100 : 0;
132
- const diskPct = metrics.diskTotalBytes > 0 ? (metrics.diskUsedBytes / metrics.diskTotalBytes) * 100 : 0;
134
+ const memPct =
135
+ metrics.memTotalBytes > 0 ? (metrics.memUsedBytes / metrics.memTotalBytes) * 100 : 0;
136
+ const diskPct =
137
+ metrics.diskTotalBytes > 0 ? (metrics.diskUsedBytes / metrics.diskTotalBytes) * 100 : 0;
133
138
  const hasGpu = metrics.gpuUtilPct !== null;
134
139
  const gpuMemLabel = metrics.gpuMemBytes !== null ? formatBytes(metrics.gpuMemBytes) : null;
135
140
  // Memory & disk are honest used/total ratios → full-width meters; stack them in
@@ -165,7 +170,11 @@ export function MachineMetrics({ metrics, density = "compact", className }: Mach
165
170
  {hasGpu ? (
166
171
  <Meter
167
172
  label="GPU"
168
- value={gpuMemLabel ? `${metrics.gpuUtilPct!.toFixed(0)}% · ${gpuMemLabel}` : `${metrics.gpuUtilPct!.toFixed(0)}%`}
173
+ value={
174
+ gpuMemLabel
175
+ ? `${metrics.gpuUtilPct!.toFixed(0)}% · ${gpuMemLabel}`
176
+ : `${metrics.gpuUtilPct!.toFixed(0)}%`
177
+ }
169
178
  fillPct={metrics.gpuUtilPct!}
170
179
  tone={toneFor(metrics.gpuUtilPct!)}
171
180
  />
@@ -73,7 +73,12 @@ export type ConnectionStatusPillProps = {
73
73
  * the Machines dashboard, the dock header, and the session timeline. Reconnecting
74
74
  * breathes (the resiliency-as-headline blip), online/offline hold still.
75
75
  */
76
- export function ConnectionStatusPill({ status, label, size = "md", className }: ConnectionStatusPillProps) {
76
+ export function ConnectionStatusPill({
77
+ status,
78
+ label,
79
+ size = "md",
80
+ className,
81
+ }: ConnectionStatusPillProps) {
77
82
  const meta = CONNECTION_STATUS_META[status];
78
83
  return (
79
84
  <span
@@ -100,8 +105,16 @@ export type ConnectionDotProps = {
100
105
  export function ConnectionDot({ status, className }: ConnectionDotProps) {
101
106
  const meta = CONNECTION_STATUS_META[status];
102
107
  return (
103
- <span className={cn("relative inline-flex size-1.5 shrink-0 rounded-full", meta.dotClassName, className)}>
104
- {meta.pulse ? <span className={cn("absolute inset-0 animate-og-pulse rounded-full", meta.dotClassName)} /> : null}
108
+ <span
109
+ className={cn(
110
+ "relative inline-flex size-1.5 shrink-0 rounded-full",
111
+ meta.dotClassName,
112
+ className,
113
+ )}
114
+ >
115
+ {meta.pulse ? (
116
+ <span className={cn("absolute inset-0 animate-og-pulse rounded-full", meta.dotClassName)} />
117
+ ) : null}
105
118
  </span>
106
119
  );
107
120
  }
@@ -121,11 +134,19 @@ export type MachineStatusPillProps = {
121
134
  * dashboard row, the dock header, and the timeline all reuse so the status
122
135
  * language is identical everywhere.
123
136
  */
124
- export function MachineStatusPill({ state, sharedSessionCount, size = "md", className }: MachineStatusPillProps) {
137
+ export function MachineStatusPill({
138
+ state,
139
+ sharedSessionCount,
140
+ size = "md",
141
+ className,
142
+ }: MachineStatusPillProps) {
125
143
  const stateBadge = MACHINE_STATE_BADGE_META[state];
126
144
  const shared = (sharedSessionCount ?? 0) > 1;
127
145
  return (
128
- <span className={cn("og-root inline-flex flex-wrap items-center gap-1", className)} data-machine-state={state}>
146
+ <span
147
+ className={cn("og-root inline-flex flex-wrap items-center gap-1", className)}
148
+ data-machine-state={state}
149
+ >
129
150
  <ConnectionStatusPill status={connectionStatusForState(state)} size={size} />
130
151
  {stateBadge ? (
131
152
  <span
@@ -0,0 +1,146 @@
1
+ // ----------------------------------------------------------------------------
2
+ // Machine health — the single derived signal.
3
+ //
4
+ // The dashboard already shows connection state (a pill) and per-resource meters
5
+ // (independently tinted). What it lacks is ONE verdict that fuses the three
6
+ // things that actually make a machine "in trouble":
7
+ //
8
+ // 1. reachability — is the control plane still hearing from it?
9
+ // 2. resource load — is a resource (mem/disk/cpu) near the wall?
10
+ // 3. freshness — is the latest sample recent, or has telemetry gone quiet?
11
+ //
12
+ // This module is a PURE function from a machine's state + latest sample + clock
13
+ // to a `HealthVerdict`. No React, no tokens — the UI maps `level` to a color.
14
+ // Keeping it pure means it is trivially testable and reused by cards, the detail
15
+ // hero, and any future fleet-summary count.
16
+ // ----------------------------------------------------------------------------
17
+ import type { MachineState, MetricSample } from "../../types/machines";
18
+
19
+ export type HealthLevel = "healthy" | "degraded" | "critical" | "offline" | "unknown";
20
+
21
+ export type HealthVerdict = {
22
+ level: HealthLevel;
23
+ /** Title-case label for the pill ("Healthy", "Under load", "Critical", …). */
24
+ label: string;
25
+ /** One short human clause naming the dominant cause ("Memory 94%", "No display"…). */
26
+ reason: string;
27
+ /** The latest sample is older than we'd expect from a live agent (~5s cadence). */
28
+ stale: boolean;
29
+ };
30
+
31
+ // Resource pressure thresholds. `warn` mirrors the existing meter ramp (>=70
32
+ // tints), `crit` is the "near the wall" line. Disk is stricter than CPU/mem
33
+ // because a full disk is a hard failure, whereas transient CPU/mem spikes are
34
+ // normal on a working machine.
35
+ const PRESSURE = {
36
+ cpu: { warn: 90, crit: 98 },
37
+ mem: { warn: 85, crit: 95 },
38
+ disk: { warn: 90, crit: 96 },
39
+ } as const;
40
+
41
+ // A live agent samples ~every 5s and the series downsamples to ~1/min. We treat
42
+ // a latest sample older than 45s as "telemetry has gone quiet" — enough slack to
43
+ // not flap on a single missed beat, tight enough to notice a wedged sampler.
44
+ const STALE_AFTER_MS = 45_000;
45
+
46
+ function pct(used: number, total: number): number {
47
+ if (!Number.isFinite(used) || !Number.isFinite(total) || total <= 0) return 0;
48
+ return (used / total) * 100;
49
+ }
50
+
51
+ /** Fold a machine's reachability + latest sample + wall-clock into one verdict. */
52
+ export function deriveHealth(
53
+ state: MachineState,
54
+ metrics: MetricSample | null,
55
+ now: number = Date.now(),
56
+ ): HealthVerdict {
57
+ // Reachability dominates: an offline machine has no meaningful resource story.
58
+ if (state === "offline") {
59
+ return { level: "offline", label: "Offline", reason: "Not reachable", stale: true };
60
+ }
61
+ if (state === "enrolling") {
62
+ return { level: "unknown", label: "Enrolling", reason: "Completing enrollment", stale: false };
63
+ }
64
+
65
+ const stale = metrics ? now - new Date(metrics.sampledAt).getTime() > STALE_AFTER_MS : true;
66
+
67
+ // Reconnecting, or online-but-quiet: reachable yet we can't trust the numbers.
68
+ if (state === "reconnecting") {
69
+ return { level: "degraded", label: "Reconnecting", reason: "Re-establishing link", stale };
70
+ }
71
+ if (!metrics) {
72
+ return { level: "unknown", label: "Online", reason: "Awaiting first sample", stale };
73
+ }
74
+ if (stale) {
75
+ return { level: "degraded", label: "Telemetry stale", reason: "No recent sample", stale: true };
76
+ }
77
+
78
+ // Reachable + fresh: the verdict is now the worst resource pressure. Evaluate
79
+ // each resource, keep the most severe, and name it in the reason.
80
+ const memPct = pct(metrics.memUsedBytes, metrics.memTotalBytes);
81
+ const diskPct = pct(metrics.diskUsedBytes, metrics.diskTotalBytes);
82
+ const cpu = metrics.cpuPct;
83
+
84
+ const pressures: Array<{ level: HealthLevel; reason: string; sev: number }> = [
85
+ resolve("Disk", diskPct, PRESSURE.disk),
86
+ resolve("Memory", memPct, PRESSURE.mem),
87
+ resolve("CPU", cpu, PRESSURE.cpu),
88
+ ];
89
+ const worst = pressures.reduce((a, b) => (b.sev > a.sev ? b : a));
90
+
91
+ if (worst.sev === 2) return { level: "critical", label: "Critical", reason: worst.reason, stale };
92
+ if (worst.sev === 1)
93
+ return { level: "degraded", label: "Under load", reason: worst.reason, stale };
94
+ return { level: "healthy", label: "Healthy", reason: "All systems nominal", stale };
95
+ }
96
+
97
+ function resolve(
98
+ name: string,
99
+ value: number,
100
+ t: { warn: number; crit: number },
101
+ ): { level: HealthLevel; reason: string; sev: number } {
102
+ const rounded = Math.round(value);
103
+ if (value >= t.crit) return { level: "critical", reason: `${name} ${rounded}%`, sev: 2 };
104
+ if (value >= t.warn) return { level: "degraded", reason: `${name} ${rounded}%`, sev: 1 };
105
+ return { level: "healthy", reason: `${name} ${rounded}%`, sev: 0 };
106
+ }
107
+
108
+ // --- UI mapping helpers (color/token names live here so every surface agrees) --
109
+
110
+ /**
111
+ * The token color ramp for a health level. Green/amber/red/grey is the
112
+ * universally-legible traffic model; we reserve it for the HEALTH verdict
113
+ * specifically (per-resource meters keep their own ramp in `machine-metrics`).
114
+ */
115
+ export const HEALTH_TOKEN: Record<HealthLevel, { text: string; dot: string; soft: string }> = {
116
+ healthy: {
117
+ text: "text-og-status-idle",
118
+ dot: "bg-og-status-idle",
119
+ soft: "bg-og-status-idle/10 border-og-status-idle/25",
120
+ },
121
+ degraded: {
122
+ text: "text-og-status-running",
123
+ dot: "bg-og-status-running",
124
+ soft: "bg-og-status-running/10 border-og-status-running/25",
125
+ },
126
+ critical: {
127
+ text: "text-og-status-failed",
128
+ dot: "bg-og-status-failed",
129
+ soft: "bg-og-status-failed/10 border-og-status-failed/30",
130
+ },
131
+ offline: {
132
+ text: "text-og-fg-subtle",
133
+ dot: "bg-og-fg-subtle",
134
+ soft: "bg-og-surface-2 border-og-border",
135
+ },
136
+ unknown: {
137
+ text: "text-og-fg-muted",
138
+ dot: "bg-og-fg-muted",
139
+ soft: "bg-og-surface-2 border-og-border",
140
+ },
141
+ };
142
+
143
+ /** Live/transient levels breathe; settled levels hold still. */
144
+ export function healthPulses(level: HealthLevel): boolean {
145
+ return level === "degraded" || level === "critical";
146
+ }
@@ -0,0 +1,220 @@
1
+ // ----------------------------------------------------------------------------
2
+ // MachineDetail — the marquee per-machine telemetry view.
3
+ //
4
+ // Opens from a machine card. Answers, at a glance and then in depth: is this
5
+ // machine healthy right now (one fused verdict), what are its live numbers, and
6
+ // how have they moved over the chosen window. Built entirely on the existing
7
+ // `og-*` token system — no new visual language, just the missing depth.
8
+ // ----------------------------------------------------------------------------
9
+ import { ArrowLeftIcon, CpuIcon, LaptopIcon, RadioIcon, ServerIcon } from "lucide-react";
10
+ import { cn } from "../../lib/cn";
11
+ import { formatRelativeTime } from "../../lib/format";
12
+ import type { MachineView, MetricSample } from "../../types/machines";
13
+ import { deriveHealth, HEALTH_TOKEN, healthPulses } from "./health";
14
+ import { MetricHistoryChart } from "./metric-history-chart";
15
+ import { MetricSparkline } from "./metric-sparkline";
16
+ import { METRICS, METRIC_WINDOWS, pointsFor, WINDOW_LABEL, type MetricWindow } from "./series";
17
+
18
+ export type MachineDetailProps = {
19
+ machine: MachineView;
20
+ /** Downsampled history for the active window (from `fetchSeries`). */
21
+ series: MetricSample[];
22
+ window: MetricWindow;
23
+ onWindowChange: (w: MetricWindow) => void;
24
+ loadingSeries?: boolean | undefined;
25
+ onBack?: (() => void) | undefined;
26
+ now?: number | undefined;
27
+ className?: string | undefined;
28
+ };
29
+
30
+ function KindIcon({ kind, session }: { kind: string; session: boolean }) {
31
+ if (session) return <ServerIcon className="size-4 text-og-fg-subtle" aria-hidden />;
32
+ if (kind === "modal") return <CpuIcon className="size-4 text-og-fg-subtle" aria-hidden />;
33
+ return <LaptopIcon className="size-4 text-og-fg-subtle" aria-hidden />;
34
+ }
35
+
36
+ export function MachineDetail({
37
+ machine,
38
+ series,
39
+ window,
40
+ onWindowChange,
41
+ loadingSeries,
42
+ onBack,
43
+ now = Date.now(),
44
+ className,
45
+ }: MachineDetailProps) {
46
+ const health = deriveHealth(machine.state, machine.metrics, now);
47
+ const tone = HEALTH_TOKEN[health.level];
48
+ const latest = machine.metrics;
49
+ const sampledAgo = latest ? formatRelativeTime(latest.sampledAt, new Date(now)) : null;
50
+
51
+ // Only render metrics that have a value (GPU is dropped on headless boxes).
52
+ const visible = METRICS.filter((m) => !(m.key === "gpu" && latest?.gpuUtilPct == null));
53
+ // The tile grid tracks the metric count exactly so it never leaves a dead cell.
54
+ const tileCols =
55
+ visible.length >= 5
56
+ ? "grid-cols-2 sm:grid-cols-3 lg:grid-cols-5"
57
+ : "grid-cols-2 sm:grid-cols-4";
58
+
59
+ return (
60
+ <div className={cn("og-root mx-auto flex w-full max-w-5xl flex-col gap-5", className)}>
61
+ {/* ── top bar ─────────────────────────────────────────────────────── */}
62
+ <div className="flex items-center justify-between gap-3">
63
+ <div className="flex min-w-0 items-center gap-2.5">
64
+ {onBack && (
65
+ <button
66
+ type="button"
67
+ onClick={onBack}
68
+ className="flex size-7 shrink-0 items-center justify-center rounded-og-sm border border-og-border bg-og-surface-1 text-og-fg-muted transition-colors hover:bg-og-surface-2 hover:text-og-fg"
69
+ aria-label="Back to machines"
70
+ >
71
+ <ArrowLeftIcon className="size-3.5" aria-hidden />
72
+ </button>
73
+ )}
74
+ <KindIcon kind={machine.kind} session={machine.isSessionGroup} />
75
+ <h1 className="truncate text-og-md font-semibold text-og-fg">{machine.name}</h1>
76
+ <span className="hidden shrink-0 rounded-og-sm border border-og-border bg-og-surface-1 px-1.5 py-0.5 font-og-mono text-og-xs text-og-fg-subtle sm:inline">
77
+ {machine.os} · {machine.arch}
78
+ </span>
79
+ </div>
80
+
81
+ {/* range selector */}
82
+ <div className="flex shrink-0 items-center gap-0.5 rounded-og-md border border-og-border bg-og-surface-1 p-0.5">
83
+ {METRIC_WINDOWS.map((w) => (
84
+ <button
85
+ key={w}
86
+ type="button"
87
+ onClick={() => onWindowChange(w)}
88
+ data-active={w === window}
89
+ className={cn(
90
+ "rounded-og-sm px-2 py-1 font-og-mono text-og-xs transition-colors",
91
+ w === window
92
+ ? "bg-og-surface-3 text-og-fg shadow-og-sm"
93
+ : "text-og-fg-subtle hover:text-og-fg-muted",
94
+ )}
95
+ >
96
+ {w}
97
+ </button>
98
+ ))}
99
+ </div>
100
+ </div>
101
+
102
+ {/* ── health hero ─────────────────────────────────────────────────── */}
103
+ <div
104
+ className={cn(
105
+ "relative flex flex-col gap-4 overflow-hidden rounded-og-lg border p-5 shadow-og-sm",
106
+ tone.soft,
107
+ )}
108
+ >
109
+ <span aria-hidden className={cn("absolute inset-y-0 left-0 w-1", tone.dot)} />
110
+ <div className="flex flex-wrap items-center justify-between gap-3 pl-1">
111
+ <div className="flex items-center gap-3">
112
+ <span className="relative flex size-3">
113
+ {healthPulses(health.level) && (
114
+ <span
115
+ className={cn(
116
+ "absolute inline-flex size-full animate-og-pulse rounded-full opacity-60",
117
+ tone.dot,
118
+ )}
119
+ />
120
+ )}
121
+ <span className={cn("relative inline-flex size-3 rounded-full", tone.dot)} />
122
+ </span>
123
+ <div className="flex flex-col">
124
+ <span className={cn("text-og-md font-semibold", tone.text)}>{health.label}</span>
125
+ <span className="text-og-sm text-og-fg-muted">{health.reason}</span>
126
+ </div>
127
+ </div>
128
+ <div className="flex items-start gap-5 pr-1">
129
+ {sampledAgo && (
130
+ <div className="flex flex-col items-end gap-0.5">
131
+ <span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">
132
+ Sampled
133
+ </span>
134
+ <span className="flex items-center gap-1 font-og-mono text-og-sm tabular-nums text-og-fg">
135
+ <RadioIcon
136
+ className={cn("size-3", health.stale ? "text-og-fg-subtle" : tone.text)}
137
+ aria-hidden
138
+ />
139
+ {sampledAgo === "now" ? "live" : `${sampledAgo} ago`}
140
+ </span>
141
+ </div>
142
+ )}
143
+ {machine.lastSeenAt && (
144
+ <div className="hidden flex-col items-end gap-0.5 sm:flex">
145
+ <span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">
146
+ Last seen
147
+ </span>
148
+ <span className="font-og-mono text-og-sm tabular-nums text-og-fg-muted">
149
+ {formatRelativeTime(machine.lastSeenAt, new Date(now))}
150
+ </span>
151
+ </div>
152
+ )}
153
+ </div>
154
+ </div>
155
+
156
+ {/* current-stat tiles */}
157
+ {latest && (
158
+ <div
159
+ className={cn(
160
+ "grid gap-px overflow-hidden rounded-og-md border border-og-border bg-og-border",
161
+ tileCols,
162
+ )}
163
+ >
164
+ {visible.map((m) => {
165
+ const pts = pointsFor(m, series);
166
+ return (
167
+ <div key={m.key} className="flex flex-col gap-1.5 bg-og-surface-1 p-3">
168
+ <span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">
169
+ {m.title}
170
+ </span>
171
+ <span className="font-og-mono text-og-md font-medium tabular-nums text-og-fg">
172
+ {m.current(latest)}
173
+ </span>
174
+ <MetricSparkline points={pts} color={m.color} yMax={m.yMax} height={22} />
175
+ {m.sub && (
176
+ <span className="truncate font-og-mono text-[10px] text-og-fg-subtle">
177
+ {m.sub(latest)}
178
+ </span>
179
+ )}
180
+ </div>
181
+ );
182
+ })}
183
+ </div>
184
+ )}
185
+ </div>
186
+
187
+ {/* ── history charts ──────────────────────────────────────────────── */}
188
+ <div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
189
+ {visible.map((m) => {
190
+ const pts = pointsFor(m, series);
191
+ return (
192
+ <div
193
+ key={m.key}
194
+ className="flex flex-col gap-3 rounded-og-lg border border-og-border bg-og-surface-1 p-4 shadow-og-sm"
195
+ >
196
+ <div className="flex items-baseline justify-between">
197
+ <span className="text-og-sm font-medium text-og-fg">{m.title}</span>
198
+ {latest && (
199
+ <span className="font-og-mono text-og-sm tabular-nums text-og-fg-muted">
200
+ {m.current(latest)}
201
+ </span>
202
+ )}
203
+ </div>
204
+ <MetricHistoryChart
205
+ points={pts}
206
+ yMax={m.yMax}
207
+ unit={m.unit}
208
+ color={m.color}
209
+ thresholds={m.thresholds}
210
+ rangeLabel={WINDOW_LABEL[window]}
211
+ height={128}
212
+ className={cn(loadingSeries && "opacity-50 transition-opacity")}
213
+ />
214
+ </div>
215
+ );
216
+ })}
217
+ </div>
218
+ </div>
219
+ );
220
+ }