@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
@@ -0,0 +1,298 @@
1
+ // ----------------------------------------------------------------------------
2
+ // MetricHistoryChart — the per-metric history visual on the machine detail view.
3
+ //
4
+ // A dependency-free SVG line/area chart tuned for the calm dark aesthetic:
5
+ // a soft gradient fill, a smoothed stroke that draws itself in, whisper-quiet
6
+ // gridlines, dashed threshold guides, and a hover crosshair with a mono readout.
7
+ // It degrades honestly — a handful of points render as markers, zero points show
8
+ // a quiet "no samples" note rather than an empty axis.
9
+ //
10
+ // Pure presentational: it takes plotted {t, v} points + a little config and owns
11
+ // no data-fetching. `color` is any CSS color (pass a token var for theme-safety).
12
+ // ----------------------------------------------------------------------------
13
+ import { useLayoutEffect, useRef, useState } from "react";
14
+ import { cn } from "../../lib/cn";
15
+
16
+ export type SeriesPoint = { t: number; v: number | null };
17
+
18
+ export type MetricHistoryChartProps = {
19
+ points: SeriesPoint[];
20
+ /** Fixed ceiling (100 for %) or "auto" to fit the data with headroom. */
21
+ yMax?: number | "auto";
22
+ yMin?: number;
23
+ /** Format a value for the axis + readout (default: rounded + unit). */
24
+ format?: (v: number) => string;
25
+ unit?: string;
26
+ /** Stroke/fill hue — any CSS color; pass a token var to stay theme-safe. */
27
+ color?: string;
28
+ thresholds?: { warn?: number | undefined; crit?: number | undefined } | undefined;
29
+ height?: number;
30
+ /** Human range label for the empty state ("in the last hour"). */
31
+ rangeLabel?: string | undefined;
32
+ className?: string | undefined;
33
+ };
34
+
35
+ const PAD = { top: 10, right: 12, bottom: 18, left: 40 };
36
+
37
+ function niceCeil(v: number): number {
38
+ if (v <= 0) return 1;
39
+ const mag = 10 ** Math.floor(Math.log10(v));
40
+ const n = v / mag;
41
+ const step = n <= 1 ? 1 : n <= 2 ? 2 : n <= 5 ? 5 : 10;
42
+ return step * mag;
43
+ }
44
+
45
+ /** Catmull-Rom → cubic-bezier path so the line is smooth without overshooting. */
46
+ function smoothPath(pts: Array<{ x: number; y: number }>): string {
47
+ if (pts.length === 0) return "";
48
+ if (pts.length === 1) return `M${pts[0]!.x},${pts[0]!.y}`;
49
+ let d = `M${pts[0]!.x},${pts[0]!.y}`;
50
+ for (let i = 0; i < pts.length - 1; i++) {
51
+ const p0 = pts[i - 1] ?? pts[i]!;
52
+ const p1 = pts[i]!;
53
+ const p2 = pts[i + 1]!;
54
+ const p3 = pts[i + 2] ?? p2;
55
+ const cp1x = p1.x + (p2.x - p0.x) / 6;
56
+ const cp1y = p1.y + (p2.y - p0.y) / 6;
57
+ const cp2x = p2.x - (p3.x - p1.x) / 6;
58
+ const cp2y = p2.y - (p3.y - p1.y) / 6;
59
+ d += `C${cp1x},${cp1y} ${cp2x},${cp2y} ${p2.x},${p2.y}`;
60
+ }
61
+ return d;
62
+ }
63
+
64
+ function fmtClock(t: number): string {
65
+ const d = new Date(t);
66
+ return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
67
+ }
68
+
69
+ export function MetricHistoryChart({
70
+ points,
71
+ yMax = "auto",
72
+ yMin = 0,
73
+ format,
74
+ unit = "",
75
+ color = "var(--og-color-accent)",
76
+ thresholds,
77
+ height = 132,
78
+ rangeLabel = "in this range",
79
+ className,
80
+ }: MetricHistoryChartProps) {
81
+ const ref = useRef<HTMLDivElement>(null);
82
+ const [w, setW] = useState(560);
83
+ const [hover, setHover] = useState<number | null>(null);
84
+
85
+ useLayoutEffect(() => {
86
+ const el = ref.current;
87
+ if (!el) return;
88
+ const ro = new ResizeObserver((entries) => {
89
+ const cw = entries[0]?.contentRect.width;
90
+ if (cw && cw > 0) setW(cw);
91
+ });
92
+ ro.observe(el);
93
+ return () => ro.disconnect();
94
+ }, []);
95
+
96
+ const valued = points.filter(
97
+ (p): p is { t: number; v: number } => p.v != null && Number.isFinite(p.v),
98
+ );
99
+ const fmt = format ?? ((v: number) => `${Math.round(v)}${unit}`);
100
+
101
+ const H = height;
102
+ const plotW = Math.max(1, w - PAD.left - PAD.right);
103
+ const plotH = Math.max(1, H - PAD.top - PAD.bottom);
104
+
105
+ const dataMax = valued.length ? Math.max(...valued.map((p) => p.v)) : 1;
106
+ const top = yMax === "auto" ? niceCeil(Math.max(dataMax * 1.15, thresholds?.warn ?? 0, 1)) : yMax;
107
+ const span = Math.max(1e-6, top - yMin);
108
+
109
+ const tMin = valued.length ? valued[0]!.t : 0;
110
+ const tMax = valued.length ? valued[valued.length - 1]!.t : 1;
111
+ const tSpan = Math.max(1, tMax - tMin);
112
+
113
+ const xOf = (t: number) => PAD.left + ((t - tMin) / tSpan) * plotW;
114
+ const yOf = (v: number) =>
115
+ PAD.top + (1 - (Math.min(top, Math.max(yMin, v)) - yMin) / span) * plotH;
116
+
117
+ const pxPts = valued.map((p) => ({ x: xOf(p.t), y: yOf(p.v), t: p.t, v: p.v }));
118
+ const sparse = pxPts.length > 0 && pxPts.length <= 4;
119
+
120
+ const line = smoothPath(pxPts);
121
+ const area =
122
+ pxPts.length > 1
123
+ ? `${line} L${pxPts[pxPts.length - 1]!.x},${PAD.top + plotH} L${pxPts[0]!.x},${PAD.top + plotH} Z`
124
+ : "";
125
+
126
+ // y gridlines at 0 / mid / top
127
+ const yTicks = [yMin, yMin + span / 2, top];
128
+ const gid = `og-mh-${Math.round(top)}-${Math.round(color.length)}`;
129
+
130
+ const hoverPt = hover != null ? pxPts[hover] : null;
131
+
132
+ return (
133
+ <div
134
+ ref={ref}
135
+ className={cn("relative w-full select-none", className)}
136
+ style={{ height: H }}
137
+ data-metric-chart
138
+ >
139
+ {valued.length === 0 ? (
140
+ <div className="absolute inset-0 flex items-center justify-center">
141
+ <span className="text-og-xs text-og-fg-subtle">No samples {rangeLabel}</span>
142
+ </div>
143
+ ) : (
144
+ <svg
145
+ width={w}
146
+ height={H}
147
+ className="overflow-visible"
148
+ role="img"
149
+ onMouseLeave={() => setHover(null)}
150
+ onMouseMove={(e) => {
151
+ const rect = (e.currentTarget as SVGSVGElement).getBoundingClientRect();
152
+ const mx = e.clientX - rect.left;
153
+ let best = 0;
154
+ let bestD = Infinity;
155
+ for (let i = 0; i < pxPts.length; i++) {
156
+ const d = Math.abs(pxPts[i]!.x - mx);
157
+ if (d < bestD) {
158
+ bestD = d;
159
+ best = i;
160
+ }
161
+ }
162
+ setHover(best);
163
+ }}
164
+ >
165
+ <defs>
166
+ <linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
167
+ <stop offset="0%" stopColor={color} stopOpacity="0.26" />
168
+ <stop offset="100%" stopColor={color} stopOpacity="0" />
169
+ </linearGradient>
170
+ </defs>
171
+
172
+ {/* gridlines + y labels */}
173
+ {yTicks.map((v, i) => {
174
+ const y = yOf(v);
175
+ return (
176
+ <g key={`yt-${v}`}>
177
+ <line
178
+ x1={PAD.left}
179
+ x2={w - PAD.right}
180
+ y1={y}
181
+ y2={y}
182
+ stroke="var(--og-color-border)"
183
+ strokeOpacity={i === 0 ? 0.9 : 0.4}
184
+ strokeWidth={1}
185
+ />
186
+ <text
187
+ x={PAD.left - 8}
188
+ y={y + 3}
189
+ textAnchor="end"
190
+ className="font-og-mono"
191
+ fontSize={10}
192
+ fill="var(--og-color-fg-subtle)"
193
+ >
194
+ {fmt(v)}
195
+ </text>
196
+ </g>
197
+ );
198
+ })}
199
+
200
+ {/* threshold guides */}
201
+ {(["warn", "crit"] as const).map((k) => {
202
+ const tv = thresholds?.[k];
203
+ if (tv == null || tv > top) return null;
204
+ const y = yOf(tv);
205
+ const stroke =
206
+ k === "crit" ? "var(--og-color-status-failed)" : "var(--og-color-status-waiting)";
207
+ return (
208
+ <line
209
+ key={k}
210
+ x1={PAD.left}
211
+ x2={w - PAD.right}
212
+ y1={y}
213
+ y2={y}
214
+ stroke={stroke}
215
+ strokeOpacity={0.5}
216
+ strokeWidth={1}
217
+ strokeDasharray="3 4"
218
+ />
219
+ );
220
+ })}
221
+
222
+ {/* x time ticks (first + last) */}
223
+ {valued.length > 1 &&
224
+ [tMin, tMax].map((t, i) => (
225
+ <text
226
+ key={i === 0 ? "x-start" : "x-end"}
227
+ x={i === 0 ? PAD.left : w - PAD.right}
228
+ y={H - 4}
229
+ textAnchor={i === 0 ? "start" : "end"}
230
+ className="font-og-mono"
231
+ fontSize={10}
232
+ fill="var(--og-color-fg-subtle)"
233
+ >
234
+ {fmtClock(t)}
235
+ </text>
236
+ ))}
237
+
238
+ {/* area + line */}
239
+ {area && <path d={area} fill={`url(#${gid})`} />}
240
+ <path
241
+ d={line}
242
+ fill="none"
243
+ stroke={color}
244
+ strokeWidth={1.75}
245
+ strokeLinejoin="round"
246
+ strokeLinecap="round"
247
+ pathLength={1}
248
+ className="og-mh-draw"
249
+ />
250
+
251
+ {/* sparse markers */}
252
+ {sparse &&
253
+ pxPts.map((p) => <circle key={`m-${p.t}`} cx={p.x} cy={p.y} r={2.5} fill={color} />)}
254
+
255
+ {/* hover crosshair */}
256
+ {hoverPt && (
257
+ <g pointerEvents="none">
258
+ <line
259
+ x1={hoverPt.x}
260
+ x2={hoverPt.x}
261
+ y1={PAD.top}
262
+ y2={PAD.top + plotH}
263
+ stroke="var(--og-color-border-strong)"
264
+ strokeWidth={1}
265
+ />
266
+ <circle
267
+ cx={hoverPt.x}
268
+ cy={hoverPt.y}
269
+ r={3.5}
270
+ fill={color}
271
+ stroke="var(--og-color-bg)"
272
+ strokeWidth={1.5}
273
+ />
274
+ </g>
275
+ )}
276
+ </svg>
277
+ )}
278
+
279
+ {/* floating readout */}
280
+ {hoverPt && (
281
+ <div
282
+ className="pointer-events-none absolute z-10 flex flex-col rounded-og-sm border border-og-border-strong bg-og-surface-3/95 px-2 py-1 shadow-og-md backdrop-blur-sm"
283
+ style={{
284
+ left: Math.min(Math.max(hoverPt.x - 30, 0), w - 78),
285
+ top: Math.max(hoverPt.y - 42, 0),
286
+ }}
287
+ >
288
+ <span className="font-og-mono text-og-sm font-medium tabular-nums text-og-fg">
289
+ {fmt(hoverPt.v)}
290
+ </span>
291
+ <span className="font-og-mono text-[10px] tabular-nums text-og-fg-subtle">
292
+ {fmtClock(hoverPt.t)}
293
+ </span>
294
+ </div>
295
+ )}
296
+ </div>
297
+ );
298
+ }
@@ -0,0 +1,76 @@
1
+ // ----------------------------------------------------------------------------
2
+ // MetricSparkline — an axis-less micro-trend for stat tiles and (later) cards.
3
+ // Same smoothing as the full chart, no chrome: a hairline + a faint fill and a
4
+ // dot on the latest point. Fixed viewBox so it scales crisply at any width.
5
+ // ----------------------------------------------------------------------------
6
+ import type { SeriesPoint } from "./metric-history-chart";
7
+
8
+ export type MetricSparklineProps = {
9
+ points: SeriesPoint[];
10
+ color?: string;
11
+ yMax?: number | "auto";
12
+ height?: number;
13
+ className?: string | undefined;
14
+ };
15
+
16
+ const VW = 120;
17
+
18
+ export function MetricSparkline({
19
+ points,
20
+ color = "var(--og-color-accent)",
21
+ yMax = "auto",
22
+ height = 28,
23
+ className,
24
+ }: MetricSparklineProps) {
25
+ const valued = points.filter(
26
+ (p): p is { t: number; v: number } => p.v != null && Number.isFinite(p.v),
27
+ );
28
+ if (valued.length < 2) {
29
+ return <div className={className} style={{ height }} aria-hidden />;
30
+ }
31
+ const vh = height;
32
+ const top = yMax === "auto" ? Math.max(1, ...valued.map((p) => p.v)) * 1.1 : yMax;
33
+ const tMin = valued[0]!.t;
34
+ const tSpan = Math.max(1, valued[valued.length - 1]!.t - tMin);
35
+ const xy = valued.map((p) => ({
36
+ x: ((p.t - tMin) / tSpan) * VW,
37
+ y: vh - (Math.min(top, Math.max(0, p.v)) / Math.max(1e-6, top)) * (vh - 2) - 1,
38
+ }));
39
+ let d = `M${xy[0]!.x},${xy[0]!.y}`;
40
+ for (let i = 0; i < xy.length - 1; i++) {
41
+ const p0 = xy[i - 1] ?? xy[i]!;
42
+ const p1 = xy[i]!;
43
+ const p2 = xy[i + 1]!;
44
+ const p3 = xy[i + 2] ?? p2;
45
+ d += `C${p1.x + (p2.x - p0.x) / 6},${p1.y + (p2.y - p0.y) / 6} ${p2.x - (p3.x - p1.x) / 6},${p2.y - (p3.y - p1.y) / 6} ${p2.x},${p2.y}`;
46
+ }
47
+ const last = xy[xy.length - 1]!;
48
+ const gid = `spark-${Math.round(top)}-${color.length}`;
49
+ return (
50
+ <svg
51
+ viewBox={`0 0 ${VW} ${vh}`}
52
+ preserveAspectRatio="none"
53
+ className={className}
54
+ style={{ height, width: "100%" }}
55
+ aria-hidden
56
+ >
57
+ <defs>
58
+ <linearGradient id={gid} x1="0" y1="0" x2="0" y2="1">
59
+ <stop offset="0%" stopColor={color} stopOpacity="0.22" />
60
+ <stop offset="100%" stopColor={color} stopOpacity="0" />
61
+ </linearGradient>
62
+ </defs>
63
+ <path d={`${d} L${last.x},${vh} L${xy[0]!.x},${vh} Z`} fill={`url(#${gid})`} />
64
+ <path
65
+ d={d}
66
+ fill="none"
67
+ stroke={color}
68
+ strokeWidth={1.25}
69
+ strokeLinejoin="round"
70
+ strokeLinecap="round"
71
+ vectorEffect="non-scaling-stroke"
72
+ />
73
+ <circle cx={last.x} cy={last.y} r={1.6} fill={color} vectorEffect="non-scaling-stroke" />
74
+ </svg>
75
+ );
76
+ }
@@ -0,0 +1,113 @@
1
+ // Shared series helpers: project a MetricSample[] into per-metric {t, v} points,
2
+ // and the metric catalog the detail view + tiles iterate over.
3
+ import type { MetricSample } from "../../types/machines";
4
+ import type { SeriesPoint } from "./metric-history-chart";
5
+
6
+ export type MetricWindow = "15m" | "1h" | "6h" | "24h";
7
+ export const METRIC_WINDOWS: MetricWindow[] = ["15m", "1h", "6h", "24h"];
8
+
9
+ export const WINDOW_LABEL: Record<MetricWindow, string> = {
10
+ "15m": "in the last 15 min",
11
+ "1h": "in the last hour",
12
+ "6h": "in the last 6 hours",
13
+ "24h": "in the last 24 hours",
14
+ };
15
+
16
+ function ptsFrom(samples: MetricSample[], pick: (s: MetricSample) => number | null): SeriesPoint[] {
17
+ return samples.map((s) => ({ t: new Date(s.sampledAt).getTime(), v: pick(s) }));
18
+ }
19
+
20
+ const memPct = (s: MetricSample) =>
21
+ s.memTotalBytes > 0 ? (s.memUsedBytes / s.memTotalBytes) * 100 : null;
22
+ const diskPct = (s: MetricSample) =>
23
+ s.diskTotalBytes > 0 ? (s.diskUsedBytes / s.diskTotalBytes) * 100 : null;
24
+
25
+ export type MetricKey = "cpu" | "mem" | "disk" | "load" | "gpu";
26
+
27
+ export type MetricDef = {
28
+ key: MetricKey;
29
+ title: string;
30
+ unit: string;
31
+ yMax: number | "auto";
32
+ color: string;
33
+ thresholds?: { warn?: number; crit?: number };
34
+ pick: (s: MetricSample) => number | null;
35
+ /** Big current-value string for tiles/badges. */
36
+ current: (s: MetricSample) => string;
37
+ /** Optional sub-line under the current value. */
38
+ sub?: (s: MetricSample) => string | null;
39
+ };
40
+
41
+ const fmtBytes = (b: number): string => {
42
+ if (b < 1024) return `${b} B`;
43
+ const u = ["KB", "MB", "GB", "TB"];
44
+ let v = b / 1024;
45
+ for (const unit of u) {
46
+ if (v < 1024 || unit === "TB") return `${v.toFixed(v < 10 ? 1 : 0)} ${unit}`;
47
+ v /= 1024;
48
+ }
49
+ return `${b} B`;
50
+ };
51
+
52
+ // The catalog. GPU is included but the detail view only renders it when the
53
+ // latest sample actually reports a GPU (gpuUtilPct != null).
54
+ export const METRICS: MetricDef[] = [
55
+ {
56
+ key: "cpu",
57
+ title: "CPU",
58
+ unit: "%",
59
+ yMax: 100,
60
+ color: "var(--og-color-accent)",
61
+ thresholds: { warn: 90, crit: 98 },
62
+ pick: (s) => s.cpuPct,
63
+ current: (s) => `${Math.round(s.cpuPct)}%`,
64
+ sub: (s) => `${s.runQueue} in run queue`,
65
+ },
66
+ {
67
+ key: "mem",
68
+ title: "Memory",
69
+ unit: "%",
70
+ yMax: 100,
71
+ color: "var(--og-color-status-idle)",
72
+ thresholds: { warn: 85, crit: 95 },
73
+ pick: memPct,
74
+ current: (s) => `${Math.round(memPct(s) ?? 0)}%`,
75
+ sub: (s) => `${fmtBytes(s.memUsedBytes)} / ${fmtBytes(s.memTotalBytes)}`,
76
+ },
77
+ {
78
+ key: "disk",
79
+ title: "Disk",
80
+ unit: "%",
81
+ yMax: 100,
82
+ color: "var(--og-color-status-waiting)",
83
+ thresholds: { warn: 90, crit: 96 },
84
+ pick: diskPct,
85
+ current: (s) => `${Math.round(diskPct(s) ?? 0)}%`,
86
+ sub: (s) => `${fmtBytes(s.diskUsedBytes)} / ${fmtBytes(s.diskTotalBytes)}`,
87
+ },
88
+ {
89
+ key: "load",
90
+ title: "Load average",
91
+ unit: "",
92
+ yMax: "auto",
93
+ color: "var(--og-color-accent-strong)",
94
+ pick: (s) => s.load1,
95
+ current: (s) => s.load1.toFixed(2),
96
+ sub: (s) => `5m ${s.load5.toFixed(2)} · 15m ${s.load15.toFixed(2)}`,
97
+ },
98
+ {
99
+ key: "gpu",
100
+ title: "GPU",
101
+ unit: "%",
102
+ yMax: 100,
103
+ color: "var(--og-color-status-running)",
104
+ thresholds: { warn: 90, crit: 98 },
105
+ pick: (s) => s.gpuUtilPct,
106
+ current: (s) => (s.gpuUtilPct == null ? "—" : `${Math.round(s.gpuUtilPct)}%`),
107
+ sub: (s) => (s.gpuMemBytes == null ? null : `${fmtBytes(s.gpuMemBytes)} used`),
108
+ },
109
+ ];
110
+
111
+ export function pointsFor(def: MetricDef, samples: MetricSample[]): SeriesPoint[] {
112
+ return ptsFrom(samples, def.pick);
113
+ }
@@ -1,7 +1,7 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { LaptopIcon, PlusIcon, RefreshCwIcon } from "lucide-react";
3
3
  import { cn } from "../lib/cn";
4
- import type { MachineView } from "../types/machines";
4
+ import type { MachineView, MetricSample } from "../types/machines";
5
5
  import { MachineCard } from "./machine-card";
6
6
 
7
7
  export type MachinesDashboardProps = {
@@ -14,6 +14,12 @@ export type MachinesDashboardProps = {
14
14
  onAttach?: ((machine: MachineView) => void) | undefined;
15
15
  /** The sandbox id currently being attached/swapped to (disables that card). */
16
16
  attachingSandboxId?: string | null | undefined;
17
+ /** Short recent history per machine (keyed by sandboxId) — drives card sparklines. */
18
+ seriesByMachine?: Record<string, MetricSample[]> | undefined;
19
+ /** Open the per-machine telemetry detail (makes each card actionable). */
20
+ onOpenDetail?: ((machine: MachineView) => void) | undefined;
21
+ /** Shared clock so freshness/relative times render consistently across cards. */
22
+ now?: number | undefined;
17
23
  /** Open the enrollment flow (the "Enroll a machine" CTA). */
18
24
  onEnroll?: (() => void) | undefined;
19
25
  onRefresh?: (() => void) | undefined;
@@ -109,6 +115,9 @@ export function MachinesDashboard({
109
115
  error,
110
116
  onAttach,
111
117
  attachingSandboxId,
118
+ seriesByMachine,
119
+ onOpenDetail,
120
+ now,
112
121
  onEnroll,
113
122
  onRefresh,
114
123
  className,
@@ -143,7 +152,10 @@ export function MachinesDashboard({
143
152
  {loading && machines.length === 0 ? (
144
153
  <div data-machines-loading className="grid gap-3 sm:grid-cols-2">
145
154
  {[0, 1].map((i) => (
146
- <div key={i} className="h-36 animate-og-pulse rounded-og-lg border border-og-border bg-og-surface-1" />
155
+ <div
156
+ key={i}
157
+ className="h-36 animate-og-pulse rounded-og-lg border border-og-border bg-og-surface-1"
158
+ />
147
159
  ))}
148
160
  </div>
149
161
  ) : isEmpty ? (
@@ -153,9 +165,15 @@ export function MachinesDashboard({
153
165
  {machines.map((machine) => (
154
166
  <MachineCard
155
167
  key={machine.sandboxId}
156
- machine={{ ...machine, active: machine.active || machine.sandboxId === activeSandboxId }}
168
+ machine={{
169
+ ...machine,
170
+ active: machine.active || machine.sandboxId === activeSandboxId,
171
+ }}
157
172
  onAttach={onAttach}
158
173
  attaching={attachingSandboxId === machine.sandboxId}
174
+ series={seriesByMachine?.[machine.sandboxId]}
175
+ onOpenDetail={onOpenDetail}
176
+ now={now}
159
177
  />
160
178
  ))}
161
179
  </div>
@@ -26,7 +26,10 @@ export type MarkdownProps = {
26
26
 
27
27
  const components: Components = {
28
28
  h1: ({ children, ...props }) => (
29
- <h1 className="mt-5 mb-2.5 text-xl font-semibold tracking-tight text-og-fg first:mt-0" {...props}>
29
+ <h1
30
+ className="mt-5 mb-2.5 text-xl font-semibold tracking-tight text-og-fg first:mt-0"
31
+ {...props}
32
+ >
30
33
  {children}
31
34
  </h1>
32
35
  ),
@@ -36,12 +39,18 @@ const components: Components = {
36
39
  </h2>
37
40
  ),
38
41
  h3: ({ children, ...props }) => (
39
- <h3 className="mt-4 mb-1.5 text-og-md font-semibold tracking-tight text-og-fg first:mt-0" {...props}>
42
+ <h3
43
+ className="mt-4 mb-1.5 text-og-md font-semibold tracking-tight text-og-fg first:mt-0"
44
+ {...props}
45
+ >
40
46
  {children}
41
47
  </h3>
42
48
  ),
43
49
  h4: ({ children, ...props }) => (
44
- <h4 className="mt-4 mb-1.5 text-og-sm font-semibold uppercase tracking-[0.04em] text-og-fg-muted first:mt-0" {...props}>
50
+ <h4
51
+ className="mt-4 mb-1.5 text-og-sm font-semibold uppercase tracking-[0.04em] text-og-fg-muted first:mt-0"
52
+ {...props}
53
+ >
45
54
  {children}
46
55
  </h4>
47
56
  ),
@@ -71,12 +80,18 @@ const components: Components = {
71
80
  </a>
72
81
  ),
73
82
  ul: ({ children, ...props }) => (
74
- <ul className="my-2.5 ml-5 flex list-disc flex-col gap-1 marker:text-og-fg-subtle first:mt-0 last:mb-0" {...props}>
83
+ <ul
84
+ className="my-2.5 ml-5 flex list-disc flex-col gap-1 marker:text-og-fg-subtle first:mt-0 last:mb-0"
85
+ {...props}
86
+ >
75
87
  {children}
76
88
  </ul>
77
89
  ),
78
90
  ol: ({ children, ...props }) => (
79
- <ol className="my-2.5 ml-5 flex list-decimal flex-col gap-1 marker:text-og-fg-subtle first:mt-0 last:mb-0" {...props}>
91
+ <ol
92
+ className="my-2.5 ml-5 flex list-decimal flex-col gap-1 marker:text-og-fg-subtle first:mt-0 last:mb-0"
93
+ {...props}
94
+ >
80
95
  {children}
81
96
  </ol>
82
97
  ),
@@ -84,7 +99,10 @@ const components: Components = {
84
99
  // negative margin pull the checkbox back to the bullet column so it aligns
85
100
  // with the text.
86
101
  li: ({ children, ...props }) => (
87
- <li className="leading-7 marker:text-og-fg-subtle [&>ul]:my-1 [&>ol]:my-1 [&:has(>input)]:list-none [&:has(>input)]:-ml-5" {...props}>
102
+ <li
103
+ className="leading-7 marker:text-og-fg-subtle [&>ul]:my-1 [&>ol]:my-1 [&:has(>input)]:list-none [&:has(>input)]:-ml-5"
104
+ {...props}
105
+ >
88
106
  {children}
89
107
  </li>
90
108
  ),
@@ -142,16 +160,28 @@ const components: Components = {
142
160
  </thead>
143
161
  ),
144
162
  th: ({ children, ...props }) => (
145
- <th className="border-b border-og-border px-3 py-1.5 text-left font-medium text-og-fg" {...props}>
163
+ <th
164
+ className="border-b border-og-border px-3 py-1.5 text-left font-medium text-og-fg"
165
+ {...props}
166
+ >
146
167
  {children}
147
168
  </th>
148
169
  ),
149
170
  td: ({ children, ...props }) => (
150
- <td className="border-b border-og-border px-3 py-1.5 align-top text-og-fg-muted [tr:last-child>&]:border-b-0" {...props}>
171
+ <td
172
+ className="border-b border-og-border px-3 py-1.5 align-top text-og-fg-muted [tr:last-child>&]:border-b-0"
173
+ {...props}
174
+ >
151
175
  {children}
152
176
  </td>
153
177
  ),
154
- img: ({ alt, ...props }) => <img alt={alt ?? ""} className="my-3 max-w-full rounded-og-md border border-og-border" {...props} />,
178
+ img: ({ alt, ...props }) => (
179
+ <img
180
+ alt={alt ?? ""}
181
+ className="my-3 max-w-full rounded-og-md border border-og-border"
182
+ {...props}
183
+ />
184
+ ),
155
185
  };
156
186
 
157
187
  function MarkdownImpl({ children, className }: MarkdownProps) {