@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.
- package/README.md +129 -23
- package/dist/chunk-6BXBGJ3B.js +1956 -0
- package/dist/chunk-6BXBGJ3B.js.map +1 -0
- package/dist/chunk-6UT5OC3P.js +2133 -0
- package/dist/chunk-6UT5OC3P.js.map +1 -0
- package/dist/chunk-EBTT3MYN.js +92 -0
- package/dist/chunk-EBTT3MYN.js.map +1 -0
- package/dist/chunk-HHFA4AFX.js +425 -0
- package/dist/chunk-HHFA4AFX.js.map +1 -0
- package/dist/chunk-LAZ2A743.js +2393 -0
- package/dist/chunk-LAZ2A743.js.map +1 -0
- package/dist/chunk-MRSECXRP.js +563 -0
- package/dist/chunk-MRSECXRP.js.map +1 -0
- package/dist/chunk-TK7G6XLT.js +18 -0
- package/dist/chunk-TK7G6XLT.js.map +1 -0
- package/dist/composer-DvUaa5ki.d.ts +585 -0
- package/dist/composer.d.ts +6 -0
- package/dist/composer.js +56 -0
- package/dist/composer.js.map +1 -0
- package/dist/index.d.ts +642 -1080
- package/dist/index.js +7672 -8005
- package/dist/index.js.map +1 -1
- package/dist/machines.d.ts +412 -3
- package/dist/machines.js +25 -1
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
- package/dist/queue-surface-implementation-NQMV2ML3.js +890 -0
- package/dist/queue-surface-implementation-NQMV2ML3.js.map +1 -0
- package/dist/session-Gd4iVPYw.d.ts +425 -0
- package/dist/session-context-DIFFlXKc.d.ts +52 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +50 -0
- package/dist/session.js.map +1 -0
- package/dist/use-composer-BCfm4mzX.d.ts +112 -0
- package/package.json +23 -4
- package/src/approvals.ts +5 -3
- package/src/client.ts +50 -1
- package/src/components/approval-surface.tsx +198 -0
- package/src/components/chat-composer.tsx +104 -751
- package/src/components/code-editor.tsx +104 -26
- package/src/components/command-palette.tsx +8 -2
- package/src/components/composer-transcription-control.tsx +211 -0
- package/src/components/composer.tsx +1536 -0
- package/src/components/desktop-viewer.tsx +3 -3
- package/src/components/enrollment-consent.tsx +2 -2
- package/src/components/file-browser.tsx +367 -29
- package/src/components/human-input-form.tsx +417 -0
- package/src/components/machine-card.tsx +82 -16
- package/src/components/machine-health-pill.tsx +68 -0
- package/src/components/machine-metrics.tsx +10 -24
- package/src/components/machines/health.ts +146 -0
- package/src/components/machines/machine-detail.tsx +220 -0
- package/src/components/machines/metric-history-chart.tsx +298 -0
- package/src/components/machines/metric-sparkline.tsx +76 -0
- package/src/components/machines/series.ts +113 -0
- package/src/components/machines-dashboard.tsx +16 -4
- package/src/components/message-timeline.tsx +15 -7
- package/src/components/model-picker.tsx +12 -3
- package/src/components/pierre-diff.tsx +32 -6
- package/src/components/queue-surface-implementation.tsx +1041 -0
- package/src/components/queue-surface-state.tsx +94 -0
- package/src/components/queue-surface.tsx +60 -0
- package/src/components/sandbox-files.tsx +230 -21
- package/src/components/sandbox-terminal.tsx +8 -2
- package/src/components/sandbox-workspace.tsx +489 -135
- package/src/components/session-status.tsx +0 -6
- package/src/components/workbench-changes.tsx +145 -50
- package/src/components/workspace-dock.tsx +329 -68
- package/src/composer.ts +60 -0
- package/src/hooks/internal.ts +115 -34
- package/src/hooks/use-composer.ts +635 -74
- package/src/hooks/use-human-input.ts +131 -0
- package/src/hooks/use-machine-chip.ts +2 -2
- package/src/hooks/use-machines.ts +27 -11
- package/src/hooks/use-sandbox-files.ts +310 -62
- package/src/hooks/use-sandbox-git.ts +154 -40
- package/src/hooks/use-sandbox-terminal.ts +28 -6
- package/src/hooks/use-session-capabilities.ts +38 -9
- package/src/hooks/use-session-control.ts +43 -15
- package/src/hooks/use-session-events.ts +523 -48
- package/src/hooks/use-session-lineage.ts +15 -6
- package/src/hooks/use-session.ts +10 -2
- package/src/hooks/use-slash-commands.ts +38 -38
- package/src/hooks/use-transcription.ts +757 -0
- package/src/hooks/use-turn-queue.ts +273 -77
- package/src/hooks/use-windowed-sections.ts +2 -2
- package/src/hooks/use-workspace-capture.ts +146 -40
- package/src/hooks/use-workspace-edit.ts +50 -14
- package/src/hooks/use-workspace-sessions.ts +3 -0
- package/src/human-input.ts +72 -0
- package/src/index.ts +88 -12
- package/src/lib/noto-sans-arabic-loader.ts +21 -0
- package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
- package/src/lib/use-portal-token-style.ts +51 -0
- package/src/lib/use-unicode-fonts.ts +57 -0
- package/src/machines.ts +16 -0
- package/src/provider.tsx +163 -41
- package/src/session-context.ts +135 -0
- package/src/session.ts +79 -0
- package/src/timeline/parsers.ts +78 -6
- package/src/timeline/projection.ts +36 -6
- package/src/timeline/tool-renderers.tsx +41 -0
- package/src/timeline/turn-summary.tsx +2 -2
- package/src/timeline/types.ts +16 -8
- package/styles/index.css +78 -0
- package/styles/index.d.ts +2 -0
- package/styles/tokens.css +11 -6
- package/styles/tokens.d.ts +2 -0
- package/dist/chunk-NFYVQWIB.js +0 -1377
- package/dist/chunk-NFYVQWIB.js.map +0 -1
- 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-
|
|
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-
|
|
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-
|
|
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
|
|
1002
|
-
* affordance
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
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">
|
|
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({
|
|
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
|
-
|
|
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=
|
|
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.
|