@opengeni/react 0.13.0 → 0.20.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 (113) hide show
  1. package/README.md +129 -23
  2. package/dist/chunk-6BXBGJ3B.js +1956 -0
  3. package/dist/chunk-6BXBGJ3B.js.map +1 -0
  4. package/dist/chunk-6UT5OC3P.js +2133 -0
  5. package/dist/chunk-6UT5OC3P.js.map +1 -0
  6. package/dist/chunk-EBTT3MYN.js +92 -0
  7. package/dist/chunk-EBTT3MYN.js.map +1 -0
  8. package/dist/chunk-HHFA4AFX.js +425 -0
  9. package/dist/chunk-HHFA4AFX.js.map +1 -0
  10. package/dist/chunk-LAZ2A743.js +2393 -0
  11. package/dist/chunk-LAZ2A743.js.map +1 -0
  12. package/dist/chunk-MRSECXRP.js +563 -0
  13. package/dist/chunk-MRSECXRP.js.map +1 -0
  14. package/dist/chunk-TK7G6XLT.js +18 -0
  15. package/dist/chunk-TK7G6XLT.js.map +1 -0
  16. package/dist/composer-DvUaa5ki.d.ts +585 -0
  17. package/dist/composer.d.ts +6 -0
  18. package/dist/composer.js +56 -0
  19. package/dist/composer.js.map +1 -0
  20. package/dist/index.d.ts +642 -1080
  21. package/dist/index.js +7672 -8005
  22. package/dist/index.js.map +1 -1
  23. package/dist/machines.d.ts +412 -3
  24. package/dist/machines.js +25 -1
  25. package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
  26. package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
  27. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
  28. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
  29. package/dist/queue-surface-implementation-NQMV2ML3.js +890 -0
  30. package/dist/queue-surface-implementation-NQMV2ML3.js.map +1 -0
  31. package/dist/session-Gd4iVPYw.d.ts +425 -0
  32. package/dist/session-context-DIFFlXKc.d.ts +52 -0
  33. package/dist/session.d.ts +4 -0
  34. package/dist/session.js +50 -0
  35. package/dist/session.js.map +1 -0
  36. package/dist/use-composer-BCfm4mzX.d.ts +112 -0
  37. package/package.json +23 -4
  38. package/src/approvals.ts +5 -3
  39. package/src/client.ts +50 -1
  40. package/src/components/approval-surface.tsx +198 -0
  41. package/src/components/chat-composer.tsx +104 -751
  42. package/src/components/code-editor.tsx +104 -26
  43. package/src/components/command-palette.tsx +8 -2
  44. package/src/components/composer-transcription-control.tsx +211 -0
  45. package/src/components/composer.tsx +1536 -0
  46. package/src/components/desktop-viewer.tsx +3 -3
  47. package/src/components/enrollment-consent.tsx +2 -2
  48. package/src/components/file-browser.tsx +367 -29
  49. package/src/components/human-input-form.tsx +417 -0
  50. package/src/components/machine-card.tsx +82 -16
  51. package/src/components/machine-health-pill.tsx +68 -0
  52. package/src/components/machine-metrics.tsx +10 -24
  53. package/src/components/machines/health.ts +146 -0
  54. package/src/components/machines/machine-detail.tsx +220 -0
  55. package/src/components/machines/metric-history-chart.tsx +298 -0
  56. package/src/components/machines/metric-sparkline.tsx +76 -0
  57. package/src/components/machines/series.ts +113 -0
  58. package/src/components/machines-dashboard.tsx +16 -4
  59. package/src/components/message-timeline.tsx +15 -7
  60. package/src/components/model-picker.tsx +12 -3
  61. package/src/components/pierre-diff.tsx +32 -6
  62. package/src/components/queue-surface-implementation.tsx +1041 -0
  63. package/src/components/queue-surface-state.tsx +94 -0
  64. package/src/components/queue-surface.tsx +60 -0
  65. package/src/components/sandbox-files.tsx +230 -21
  66. package/src/components/sandbox-terminal.tsx +8 -2
  67. package/src/components/sandbox-workspace.tsx +489 -135
  68. package/src/components/session-status.tsx +0 -6
  69. package/src/components/workbench-changes.tsx +145 -50
  70. package/src/components/workspace-dock.tsx +329 -68
  71. package/src/composer.ts +60 -0
  72. package/src/hooks/internal.ts +115 -34
  73. package/src/hooks/use-composer.ts +635 -74
  74. package/src/hooks/use-human-input.ts +131 -0
  75. package/src/hooks/use-machine-chip.ts +2 -2
  76. package/src/hooks/use-machines.ts +27 -11
  77. package/src/hooks/use-sandbox-files.ts +310 -62
  78. package/src/hooks/use-sandbox-git.ts +154 -40
  79. package/src/hooks/use-sandbox-terminal.ts +28 -6
  80. package/src/hooks/use-session-capabilities.ts +38 -9
  81. package/src/hooks/use-session-control.ts +43 -15
  82. package/src/hooks/use-session-events.ts +523 -48
  83. package/src/hooks/use-session-lineage.ts +15 -6
  84. package/src/hooks/use-session.ts +10 -2
  85. package/src/hooks/use-slash-commands.ts +38 -38
  86. package/src/hooks/use-transcription.ts +757 -0
  87. package/src/hooks/use-turn-queue.ts +273 -77
  88. package/src/hooks/use-windowed-sections.ts +2 -2
  89. package/src/hooks/use-workspace-capture.ts +146 -40
  90. package/src/hooks/use-workspace-edit.ts +50 -14
  91. package/src/hooks/use-workspace-sessions.ts +3 -0
  92. package/src/human-input.ts +72 -0
  93. package/src/index.ts +88 -12
  94. package/src/lib/noto-sans-arabic-loader.ts +21 -0
  95. package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
  96. package/src/lib/use-portal-token-style.ts +51 -0
  97. package/src/lib/use-unicode-fonts.ts +57 -0
  98. package/src/machines.ts +16 -0
  99. package/src/provider.tsx +163 -41
  100. package/src/session-context.ts +135 -0
  101. package/src/session.ts +79 -0
  102. package/src/timeline/parsers.ts +78 -6
  103. package/src/timeline/projection.ts +36 -6
  104. package/src/timeline/tool-renderers.tsx +41 -0
  105. package/src/timeline/turn-summary.tsx +2 -2
  106. package/src/timeline/types.ts +16 -8
  107. package/styles/index.css +78 -0
  108. package/styles/index.d.ts +2 -0
  109. package/styles/tokens.css +11 -6
  110. package/styles/tokens.d.ts +2 -0
  111. package/dist/chunk-NFYVQWIB.js +0 -1377
  112. package/dist/chunk-NFYVQWIB.js.map +0 -1
  113. package/dist/machines-CnlMb7E-.d.ts +0 -329
@@ -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;
@@ -40,7 +46,7 @@ function EmptyState({ onEnroll }: { onEnroll?: (() => void) | undefined }) {
40
46
  type="button"
41
47
  data-enroll-cta
42
48
  onClick={onEnroll}
43
- className="inline-flex items-center gap-1.5 rounded-og-sm bg-og-accent px-3 py-1.5 text-og-sm font-medium text-og-accent-fg transition-colors hover:bg-og-accent-strong pointer-coarse:min-h-10"
49
+ className="inline-flex items-center gap-1.5 rounded-og-sm bg-og-accent px-3 py-1.5 text-og-sm font-medium text-og-accent-fg transition-colors hover:bg-og-accent-strong pointer-coarse:min-h-11"
44
50
  >
45
51
  <PlusIcon className="size-3.5" aria-hidden />
46
52
  Enroll a machine
@@ -84,7 +90,7 @@ function Header({
84
90
  type="button"
85
91
  data-enroll-cta
86
92
  onClick={onEnroll}
87
- className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2.5 py-1 text-og-sm font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg pointer-coarse:min-h-10"
93
+ className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2.5 py-1 text-og-sm font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg pointer-coarse:min-h-11"
88
94
  >
89
95
  <PlusIcon className="size-3.5" aria-hidden />
90
96
  Enroll machine
@@ -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,
@@ -131,7 +140,7 @@ export function MachinesDashboard({
131
140
  data-machines-retry
132
141
  onClick={onRefresh}
133
142
  disabled={loading}
134
- className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-status-failed/30 px-2 py-1 text-og-xs font-medium transition-colors hover:border-og-status-failed/50 disabled:cursor-not-allowed disabled:opacity-60 pointer-coarse:min-h-10"
143
+ className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-status-failed/30 px-2 py-1 text-og-xs font-medium transition-colors hover:border-og-status-failed/50 disabled:cursor-not-allowed disabled:opacity-60 pointer-coarse:min-h-11"
135
144
  >
136
145
  <RefreshCwIcon className={cn("size-3.5", loading && "animate-og-spin")} aria-hidden />
137
146
  {loading ? "Retrying…" : "Retry"}
@@ -162,6 +171,9 @@ export function MachinesDashboard({
162
171
  }}
163
172
  onAttach={onAttach}
164
173
  attaching={attachingSandboxId === machine.sandboxId}
174
+ series={seriesByMachine?.[machine.sandboxId]}
175
+ onOpenDetail={onOpenDetail}
176
+ now={now}
165
177
  />
166
178
  ))}
167
179
  </div>
@@ -998,10 +998,10 @@ function NoticeRow({ item }: { item: NoticeItem }) {
998
998
  }
999
999
 
1000
1000
  /**
1001
- * The inline reconnect card: a lapsed connection surfaces as a calm, tappable
1002
- * affordance — provider logo, one human line, one primary Reconnect button —
1003
- * instead of a raw "linear.app needs to be reconnected." string. The `reason`
1004
- * only shapes the helper copy; no domain or enum code is shown as a label.
1001
+ * The inline connection card: a lapsed connection surfaces as a calm, tappable
1002
+ * reconnect affordance, while an incompatible host-owned endpoint/resource
1003
+ * contract remains informative and non-actionable. The `reason` only shapes
1004
+ * the helper copy; no domain or enum code is shown as a label.
1005
1005
  * `onReconnect` (from the app, which owns the SDK client) runs the flow; without
1006
1006
  * it, a pre-minted authorization link is offered, or the card stays informative.
1007
1007
  */
@@ -1018,6 +1018,8 @@ function AuthNeededRow({
1018
1018
  const [busy, setBusy] = useState(false);
1019
1019
  const [failed, setFailed] = useState(false);
1020
1020
  const provider = providerLabel(item.providerDomain);
1021
+ const unavailable =
1022
+ item.reason === "unsupported_auth" || item.reason === "resource_scope_unavailable";
1021
1023
 
1022
1024
  const start = async () => {
1023
1025
  if (!onReconnect || busy) {
@@ -1045,11 +1047,13 @@ function AuthNeededRow({
1045
1047
  label={provider}
1046
1048
  />
1047
1049
  <div className="min-w-0">
1048
- <p className="truncate text-og-md font-medium text-og-fg">Reconnect {provider}</p>
1050
+ <p className="truncate text-og-md font-medium text-og-fg">
1051
+ {unavailable ? `${provider} tools unavailable` : `Reconnect ${provider}`}
1052
+ </p>
1049
1053
  <p className="truncate text-og-sm text-og-fg-subtle">{authReasonLine(item.reason)}</p>
1050
1054
  </div>
1051
1055
  </div>
1052
- {onReconnect ? (
1056
+ {!unavailable && onReconnect ? (
1053
1057
  <button
1054
1058
  type="button"
1055
1059
  onClick={() => void start()}
@@ -1062,7 +1066,7 @@ function AuthNeededRow({
1062
1066
  <RefreshCwIcon className={cn("size-3.5", busy && "animate-og-spin")} aria-hidden />
1063
1067
  {busy ? "Reconnecting…" : "Reconnect"}
1064
1068
  </button>
1065
- ) : item.authorizationUrl ? (
1069
+ ) : !unavailable && item.authorizationUrl ? (
1066
1070
  <a
1067
1071
  href={item.authorizationUrl}
1068
1072
  rel="noreferrer"
@@ -1160,6 +1164,10 @@ function authReasonLine(reason: AuthNeededItem["reason"]): string {
1160
1164
  case "expired":
1161
1165
  case "refresh_failed":
1162
1166
  return "Its access expired.";
1167
+ case "unsupported_auth":
1168
+ return "This connection cannot authenticate the configured tool endpoint.";
1169
+ case "resource_scope_unavailable":
1170
+ return "This tool endpoint cannot enforce the selected repository access.";
1163
1171
  default:
1164
1172
  return "Its connection needs attention.";
1165
1173
  }
@@ -12,6 +12,8 @@ export type ModelPickerProps = {
12
12
  onChange: (modelId: string) => void;
13
13
  disabled?: boolean | undefined;
14
14
  className?: string | undefined;
15
+ /** Visible-to-assistive-technology label. Defaults to "Model". */
16
+ label?: string | undefined;
15
17
  };
16
18
 
17
19
  /**
@@ -25,7 +27,14 @@ export type ModelPickerProps = {
25
27
  * the send path. Renders nothing when no models are exposed, so a single-model
26
28
  * deployment shows no chrome.
27
29
  */
28
- export function ModelPicker({ models, value, onChange, disabled, className }: ModelPickerProps) {
30
+ export function ModelPicker({
31
+ models,
32
+ value,
33
+ onChange,
34
+ disabled,
35
+ className,
36
+ label = "Model",
37
+ }: ModelPickerProps) {
29
38
  const selectId = useId();
30
39
  // Group by provider, preserving first-seen order for both the providers and
31
40
  // the models within each — the server already orders the list (default model
@@ -50,14 +59,14 @@ export function ModelPicker({ models, value, onChange, disabled, className }: Mo
50
59
  return (
51
60
  <span className={cn("relative inline-flex items-center", className)}>
52
61
  <label htmlFor={selectId} className="sr-only">
53
- Model
62
+ {label}
54
63
  </label>
55
64
  <select
56
65
  id={selectId}
57
66
  value={value ?? ""}
58
67
  onChange={(event) => onChange(event.target.value)}
59
68
  disabled={disabled === true}
60
- aria-label="Model"
69
+ aria-label={label}
61
70
  className={cn(
62
71
  // Sized like the other footer controls; the chevron overlay needs the
63
72
  // right padding so the value never collides with it.