@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.
- package/README.md +20 -14
- package/dist/chunk-TOJR776I.js +2280 -0
- package/dist/chunk-TOJR776I.js.map +1 -0
- package/dist/index.d.ts +1040 -363
- package/dist/index.js +10938 -6281
- package/dist/index.js.map +1 -1
- package/dist/{machines-BeZ3bD3t.d.ts → machines-BpdwuQcD.d.ts} +134 -14
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +23 -1
- package/package.json +20 -12
- package/src/approvals.ts +16 -4
- package/src/client.ts +41 -5
- package/src/commands/index.ts +1 -7
- package/src/commands/registry.ts +46 -12
- package/src/components/chat-composer.tsx +417 -69
- package/src/components/code-editor.tsx +28 -36
- package/src/components/command-palette.tsx +26 -5
- package/src/components/desktop-viewer.tsx +29 -16
- package/src/components/diff-view.tsx +27 -189
- package/src/components/enrollment-consent.tsx +28 -10
- package/src/components/enrollment-device-flow.tsx +8 -5
- package/src/components/file-browser.tsx +190 -56
- package/src/components/fleet-tile.tsx +13 -4
- package/src/components/machine-card.tsx +93 -15
- package/src/components/machine-dock-bar.tsx +20 -8
- package/src/components/machine-health-pill.tsx +68 -0
- package/src/components/machine-metrics.tsx +28 -19
- package/src/components/machine-status-pill.tsx +26 -5
- 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 +21 -3
- package/src/components/markdown.tsx +39 -9
- package/src/components/message-timeline.tsx +633 -109
- package/src/components/pierre-diff.tsx +83 -15
- package/src/components/pierre-file.tsx +10 -9
- package/src/components/queue-surface.tsx +578 -0
- package/src/components/sandbox-files.tsx +193 -225
- package/src/components/sandbox-terminal.tsx +256 -88
- package/src/components/sandbox-workspace.tsx +957 -0
- package/src/components/session-status.tsx +22 -2
- package/src/components/workbench-changes.tsx +585 -0
- package/src/components/workspace-dock.tsx +213 -59
- package/src/hooks/internal.ts +5 -2
- package/src/hooks/use-available-models.ts +7 -2
- package/src/hooks/use-billing-usage.ts +4 -1
- package/src/hooks/use-codex-accounts.ts +74 -44
- package/src/hooks/use-composer.ts +414 -63
- package/src/hooks/use-environments.ts +92 -47
- package/src/hooks/use-file-attachments.ts +101 -55
- package/src/hooks/use-goal.ts +52 -16
- package/src/hooks/use-machine-chip.ts +134 -0
- package/src/hooks/use-machines.ts +34 -16
- package/src/hooks/use-packs.ts +24 -19
- package/src/hooks/use-relay-frame-stream.ts +5 -2
- package/src/hooks/use-rigs.ts +335 -0
- package/src/hooks/use-sandbox-files.ts +213 -39
- package/src/hooks/use-sandbox-git.ts +188 -11
- package/src/hooks/use-sandbox-terminal.ts +18 -14
- package/src/hooks/use-scheduled-tasks.ts +10 -2
- package/src/hooks/use-session-capabilities.ts +77 -20
- package/src/hooks/use-session-control.ts +50 -23
- package/src/hooks/use-session-events.ts +50 -21
- package/src/hooks/use-session-lineage.ts +128 -0
- package/src/hooks/use-session.ts +53 -30
- package/src/hooks/use-slash-commands.ts +6 -4
- package/src/hooks/use-turn-queue.ts +202 -147
- package/src/hooks/use-windowed-sections.ts +204 -0
- package/src/hooks/use-workspace-capture.ts +233 -0
- package/src/hooks/use-workspace-edit.ts +231 -0
- package/src/hooks/use-workspace-sessions.ts +48 -5
- package/src/hooks/use-workspaces.ts +18 -15
- package/src/index.ts +133 -23
- package/src/lib/format.ts +3 -3
- package/src/lib/xterm-renderer.ts +65 -0
- package/src/lib/xterm-theme.ts +224 -18
- package/src/machines.ts +29 -3
- package/src/provider.tsx +195 -6
- package/src/timeline/activity-rail.tsx +213 -54
- package/src/timeline/disclosure-context.tsx +12 -2
- package/src/timeline/index.ts +21 -2
- package/src/timeline/parsers.ts +87 -16
- package/src/timeline/projection.ts +412 -87
- package/src/timeline/shared.tsx +111 -24
- package/src/timeline/tool-diff.tsx +24 -20
- package/src/timeline/tool-renderers.tsx +98 -21
- package/src/timeline/turn-summary.tsx +76 -23
- package/src/timeline/types.ts +96 -12
- package/styles/index.css +22 -0
- package/styles/tokens.css +2 -2
- package/dist/chunk-5TIXRCSI.js +0 -1291
- package/dist/chunk-5TIXRCSI.js.map +0 -1
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
FilePlusIcon,
|
|
5
5
|
FolderIcon,
|
|
6
6
|
FolderPlusIcon,
|
|
7
|
-
Loader2Icon,
|
|
8
7
|
PencilIcon,
|
|
9
8
|
RefreshCwIcon,
|
|
10
9
|
Trash2Icon,
|
|
@@ -14,10 +13,12 @@ import {
|
|
|
14
13
|
type ReactNode,
|
|
15
14
|
useCallback,
|
|
16
15
|
useEffect,
|
|
16
|
+
useLayoutEffect,
|
|
17
17
|
useMemo,
|
|
18
18
|
useRef,
|
|
19
19
|
useState,
|
|
20
20
|
} from "react";
|
|
21
|
+
import { VList, type VListHandle } from "virtua";
|
|
21
22
|
import { cn } from "../lib/cn";
|
|
22
23
|
import type { FileTreeNode, UseSandboxFilesResult } from "../hooks/use-sandbox-files";
|
|
23
24
|
|
|
@@ -57,6 +58,7 @@ const STATUS_TINT: Record<NonNullable<FileTreeNode["status"]>, string> = {
|
|
|
57
58
|
renamed: "text-og-accent",
|
|
58
59
|
untracked: "text-og-fg-subtle",
|
|
59
60
|
};
|
|
61
|
+
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
60
62
|
|
|
61
63
|
/** Parent dir of a workspace-relative POSIX path ("" for a root entry). */
|
|
62
64
|
function parentOf(path: string): string {
|
|
@@ -69,6 +71,20 @@ function joinPath(parent: string, name: string): string {
|
|
|
69
71
|
return parent ? `${parent}/${name}` : name;
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/** A stable React key for a flat render item. */
|
|
75
|
+
function itemKey(item: RenderItem): string {
|
|
76
|
+
switch (item.type) {
|
|
77
|
+
case "node":
|
|
78
|
+
return item.node.path;
|
|
79
|
+
case "create":
|
|
80
|
+
return `create:${item.parent}`;
|
|
81
|
+
case "skeleton":
|
|
82
|
+
return `skeleton:${item.path}`;
|
|
83
|
+
case "residue":
|
|
84
|
+
return `residue:${item.path}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
/** Walk the tree to find a node by path. */
|
|
73
89
|
function findNode(nodes: FileTreeNode[], path: string): FileTreeNode | undefined {
|
|
74
90
|
for (const node of nodes) {
|
|
@@ -87,6 +103,14 @@ type DraftCreate = { parent: string; kind: "file" | "dir" };
|
|
|
87
103
|
type DraftRename = { path: string };
|
|
88
104
|
type ContextMenuState = { node: FileTreeNode | null; x: number; y: number };
|
|
89
105
|
|
|
106
|
+
/** One virtualized row. Nodes plus the synthetic rows the recursive renderer
|
|
107
|
+
* used to nest inline. */
|
|
108
|
+
type RenderItem =
|
|
109
|
+
| { type: "node"; node: FileTreeNode; depth: number }
|
|
110
|
+
| { type: "create"; parent: string; kind: "file" | "dir"; depth: number }
|
|
111
|
+
| { type: "skeleton"; path: string; depth: number }
|
|
112
|
+
| { type: "residue"; path: string; depth: number };
|
|
113
|
+
|
|
90
114
|
/**
|
|
91
115
|
* The file MANAGER, fed by the FileSystem service via `useSandboxFiles`. This is
|
|
92
116
|
* a first-class editable tree (not a render-only view): lazy-expand with a
|
|
@@ -121,7 +145,24 @@ export function FileBrowser({
|
|
|
121
145
|
const [dragOver, setDragOver] = useState<string | null>(null);
|
|
122
146
|
const [menu, setMenu] = useState<ContextMenuState | null>(null);
|
|
123
147
|
const [busy, setBusy] = useState(false);
|
|
148
|
+
const [coarsePointer, setCoarsePointer] = useState(false);
|
|
124
149
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
150
|
+
const vlistRef = useRef<VListHandle>(null);
|
|
151
|
+
|
|
152
|
+
useBrowserLayoutEffect(() => {
|
|
153
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
154
|
+
const query = window.matchMedia("(pointer: coarse)");
|
|
155
|
+
const update = () => setCoarsePointer(query.matches);
|
|
156
|
+
update();
|
|
157
|
+
if (typeof query.addEventListener === "function") {
|
|
158
|
+
query.addEventListener("change", update);
|
|
159
|
+
return () => query.removeEventListener("change", update);
|
|
160
|
+
}
|
|
161
|
+
if (typeof query.addListener === "function") {
|
|
162
|
+
query.addListener(update);
|
|
163
|
+
return () => query.removeListener(update);
|
|
164
|
+
}
|
|
165
|
+
}, []);
|
|
125
166
|
|
|
126
167
|
// The keyboard cursor: an explicitly-navigated node falls back to the
|
|
127
168
|
// externally-selected file so arrow keys pick up where the preview pane is.
|
|
@@ -169,20 +210,53 @@ export function FileBrowser({
|
|
|
169
210
|
[expanded, expand],
|
|
170
211
|
);
|
|
171
212
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
213
|
+
// Flatten the VISIBLE (expanded) tree into a single ordered render list — the
|
|
214
|
+
// input to the virtualizer. Interleaves the synthetic rows the recursive
|
|
215
|
+
// renderer used to nest (inline create input, lazy-expand skeleton, and the
|
|
216
|
+
// cold residue "contents on machine" row) so every visual row is one list item.
|
|
217
|
+
const cold = result.source !== "live";
|
|
218
|
+
const renderItems = useMemo(() => {
|
|
219
|
+
const items: RenderItem[] = [];
|
|
220
|
+
if (draftCreate && draftCreate.parent === "") {
|
|
221
|
+
items.push({ type: "create", parent: "", kind: draftCreate.kind, depth: 0 });
|
|
222
|
+
}
|
|
175
223
|
const walk = (nodes: FileTreeNode[], depth: number) => {
|
|
176
224
|
for (const node of nodes) {
|
|
177
|
-
|
|
178
|
-
if (node.kind
|
|
225
|
+
items.push({ type: "node", node, depth });
|
|
226
|
+
if (node.kind !== "dir" || !expanded.has(node.path)) continue;
|
|
227
|
+
if (draftCreate && draftCreate.parent === node.path) {
|
|
228
|
+
items.push({
|
|
229
|
+
type: "create",
|
|
230
|
+
parent: node.path,
|
|
231
|
+
kind: draftCreate.kind,
|
|
232
|
+
depth: depth + 1,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
const childless = node.children === undefined || node.children.length === 0;
|
|
236
|
+
const isLoading = loadingPaths.has(node.path) || result.expandingPaths.has(node.path);
|
|
237
|
+
// A collapsed residue dir (node_modules, .git, …) can't list children from
|
|
238
|
+
// the cold capture — offer to open it live rather than a dead skeleton.
|
|
239
|
+
if (node.truncated && childless && cold && !isLoading) {
|
|
240
|
+
items.push({ type: "residue", path: node.path, depth: depth + 1 });
|
|
241
|
+
} else if (node.children && node.children.length > 0) {
|
|
179
242
|
walk(node.children, depth + 1);
|
|
243
|
+
} else if (isLoading && childless) {
|
|
244
|
+
items.push({ type: "skeleton", path: node.path, depth: depth + 1 });
|
|
180
245
|
}
|
|
181
246
|
}
|
|
182
247
|
};
|
|
183
248
|
walk(result.tree, 0);
|
|
184
|
-
return
|
|
185
|
-
}, [result.tree, expanded]);
|
|
249
|
+
return items;
|
|
250
|
+
}, [result.tree, expanded, draftCreate, loadingPaths, result.expandingPaths, cold]);
|
|
251
|
+
|
|
252
|
+
// The visible NODE rows in order — drives keyboard up/down navigation.
|
|
253
|
+
const flatRows = useMemo(
|
|
254
|
+
() =>
|
|
255
|
+
renderItems.flatMap((item) =>
|
|
256
|
+
item.type === "node" ? [{ node: item.node, depth: item.depth }] : [],
|
|
257
|
+
),
|
|
258
|
+
[renderItems],
|
|
259
|
+
);
|
|
186
260
|
|
|
187
261
|
const supportsMutation = editable;
|
|
188
262
|
|
|
@@ -380,7 +454,19 @@ export function FileBrowser({
|
|
|
380
454
|
break;
|
|
381
455
|
}
|
|
382
456
|
},
|
|
383
|
-
[
|
|
457
|
+
[
|
|
458
|
+
draftCreate,
|
|
459
|
+
draftRename,
|
|
460
|
+
flatRows,
|
|
461
|
+
cursor,
|
|
462
|
+
expanded,
|
|
463
|
+
open,
|
|
464
|
+
toggle,
|
|
465
|
+
onSelectFile,
|
|
466
|
+
startRename,
|
|
467
|
+
runDelete,
|
|
468
|
+
supportsMutation,
|
|
469
|
+
],
|
|
384
470
|
);
|
|
385
471
|
|
|
386
472
|
// Close the context menu on any outside interaction.
|
|
@@ -401,10 +487,12 @@ export function FileBrowser({
|
|
|
401
487
|
const showEmpty = !result.loading && result.tree.length === 0 && !draftCreate;
|
|
402
488
|
const showToolbar = supportsMutation || Boolean(result.error) || result.loading;
|
|
403
489
|
|
|
404
|
-
|
|
490
|
+
// Render a SINGLE node row (no recursion — children are separate flat items the
|
|
491
|
+
// virtualizer renders). Returns the row element; the caller assigns the key.
|
|
492
|
+
const renderNodeRow = (node: FileTreeNode, depth: number): ReactNode => {
|
|
405
493
|
const isOpen = expanded.has(node.path);
|
|
406
494
|
if (renderNode) {
|
|
407
|
-
return <div
|
|
495
|
+
return <div>{renderNode(node, depth, isOpen)}</div>;
|
|
408
496
|
}
|
|
409
497
|
const isDir = node.kind === "dir";
|
|
410
498
|
const isSelected = node.path === selectedPath || node.path === active;
|
|
@@ -412,12 +500,10 @@ export function FileBrowser({
|
|
|
412
500
|
const isLoading = loadingPaths.has(node.path) || result.expandingPaths.has(node.path);
|
|
413
501
|
const isRenaming = draftRename?.path === node.path;
|
|
414
502
|
const isDropTarget = dragOver === (isDir ? node.path : parentOf(node.path));
|
|
415
|
-
const showSkeleton = isDir && isOpen && isLoading && (node.children === undefined || node.children.length === 0);
|
|
416
503
|
const dropDir = isDir ? node.path : parentOf(node.path);
|
|
417
504
|
|
|
418
505
|
return (
|
|
419
506
|
<div
|
|
420
|
-
key={node.path}
|
|
421
507
|
role="treeitem"
|
|
422
508
|
aria-expanded={isDir ? isOpen : undefined}
|
|
423
509
|
aria-selected={isCursor || undefined}
|
|
@@ -483,28 +569,34 @@ export function FileBrowser({
|
|
|
483
569
|
setMenu({ node, x: e.clientX, y: e.clientY });
|
|
484
570
|
}}
|
|
485
571
|
className={cn(
|
|
486
|
-
"group flex w-full items-center gap-1 truncate rounded-og-sm px-1 py-0.5 text-left text-og-sm pointer-coarse:min-h-
|
|
572
|
+
"group relative flex min-h-7 w-full items-center gap-1 truncate rounded-og-sm px-1 py-0.5 text-left text-og-sm pointer-coarse:min-h-11",
|
|
487
573
|
"hover:bg-og-surface-2",
|
|
488
574
|
isSelected && "bg-og-surface-2",
|
|
489
575
|
isCursor && "outline outline-1 -outline-offset-1 outline-og-accent",
|
|
490
576
|
isDropTarget && "bg-og-accent-soft ring-1 ring-inset ring-og-accent",
|
|
577
|
+
// A thin progress shimmer on the expanding row — no per-node spinner.
|
|
578
|
+
isLoading &&
|
|
579
|
+
"after:pointer-events-none after:absolute after:inset-x-1 after:bottom-0 after:h-px after:animate-pulse after:rounded-full after:bg-og-accent-soft",
|
|
491
580
|
node.status ? STATUS_TINT[node.status] : undefined,
|
|
492
581
|
)}
|
|
493
582
|
style={{ paddingLeft: `${depth * 12 + 4}px` }}
|
|
494
583
|
>
|
|
495
584
|
{isDir ? (
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
)
|
|
585
|
+
<ChevronRightIcon
|
|
586
|
+
className={cn(
|
|
587
|
+
"size-3 shrink-0 transition-transform",
|
|
588
|
+
isOpen && "rotate-90",
|
|
589
|
+
isLoading && "text-og-accent",
|
|
590
|
+
)}
|
|
591
|
+
/>
|
|
504
592
|
) : (
|
|
505
593
|
<span className="inline-block w-3 shrink-0" />
|
|
506
594
|
)}
|
|
507
|
-
{isDir ?
|
|
595
|
+
{isDir ? (
|
|
596
|
+
<FolderIcon className="size-3.5 shrink-0" />
|
|
597
|
+
) : (
|
|
598
|
+
<FileIcon className="size-3.5 shrink-0" />
|
|
599
|
+
)}
|
|
508
600
|
<span className="truncate">{node.name}</span>
|
|
509
601
|
{supportsMutation && (
|
|
510
602
|
<span
|
|
@@ -525,23 +617,31 @@ export function FileBrowser({
|
|
|
525
617
|
)}
|
|
526
618
|
</button>
|
|
527
619
|
)}
|
|
620
|
+
</div>
|
|
621
|
+
);
|
|
622
|
+
};
|
|
528
623
|
|
|
529
|
-
|
|
530
|
-
|
|
624
|
+
// Render one flat list item (node row or a synthetic create/skeleton/residue row).
|
|
625
|
+
const renderItem = (item: RenderItem): ReactNode => {
|
|
626
|
+
switch (item.type) {
|
|
627
|
+
case "node":
|
|
628
|
+
return renderNodeRow(item.node, item.depth);
|
|
629
|
+
case "create":
|
|
630
|
+
return (
|
|
531
631
|
<InlineInput
|
|
532
|
-
depth={depth
|
|
533
|
-
kind={
|
|
632
|
+
depth={item.depth}
|
|
633
|
+
kind={item.kind}
|
|
534
634
|
initialValue=""
|
|
535
635
|
onCommit={(name) => void commitCreate(name)}
|
|
536
636
|
onCancel={() => setDraftCreate(null)}
|
|
537
637
|
/>
|
|
538
|
-
)
|
|
539
|
-
|
|
540
|
-
|
|
638
|
+
);
|
|
639
|
+
case "skeleton":
|
|
640
|
+
return (
|
|
541
641
|
<div
|
|
542
642
|
role="group"
|
|
543
643
|
aria-hidden
|
|
544
|
-
style={{ paddingLeft: `${
|
|
644
|
+
style={{ paddingLeft: `${item.depth * 12 + 4}px` }}
|
|
545
645
|
className="space-y-1 py-1"
|
|
546
646
|
>
|
|
547
647
|
{[0, 1, 2].map((i) => (
|
|
@@ -552,16 +652,29 @@ export function FileBrowser({
|
|
|
552
652
|
/>
|
|
553
653
|
))}
|
|
554
654
|
</div>
|
|
555
|
-
)
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
655
|
+
);
|
|
656
|
+
case "residue":
|
|
657
|
+
return (
|
|
658
|
+
<div
|
|
659
|
+
style={{ paddingLeft: `${item.depth * 12 + 4}px` }}
|
|
660
|
+
className="flex items-center gap-1.5 py-0.5 pr-1 text-og-xs italic text-og-fg-subtle"
|
|
661
|
+
>
|
|
662
|
+
<span className="inline-block w-3 shrink-0" />
|
|
663
|
+
<span className="min-w-0 truncate">contents on machine — open when live</span>
|
|
664
|
+
</div>
|
|
665
|
+
);
|
|
666
|
+
}
|
|
561
667
|
};
|
|
562
668
|
|
|
669
|
+
// Keep the keyboard cursor's row on screen even though off-screen rows unmount.
|
|
670
|
+
useEffect(() => {
|
|
671
|
+
if (!cursor) return;
|
|
672
|
+
const index = renderItems.findIndex((it) => it.type === "node" && it.node.path === cursor);
|
|
673
|
+
if (index >= 0) vlistRef.current?.scrollToIndex(index);
|
|
674
|
+
}, [cursor, renderItems]);
|
|
675
|
+
|
|
563
676
|
return (
|
|
564
|
-
<div className={cn("flex min-w-0 flex-col", className)}>
|
|
677
|
+
<div className={cn("flex min-h-0 min-w-0 flex-col", className)}>
|
|
565
678
|
{showToolbar && (
|
|
566
679
|
<div className="flex shrink-0 flex-wrap items-center gap-0.5 border-b border-og-border px-1 py-1">
|
|
567
680
|
{supportsMutation ? (
|
|
@@ -595,7 +708,11 @@ export function FileBrowser({
|
|
|
595
708
|
</>
|
|
596
709
|
) : null}
|
|
597
710
|
<span className="ml-auto" />
|
|
598
|
-
<ToolbarButton
|
|
711
|
+
<ToolbarButton
|
|
712
|
+
label="Refresh"
|
|
713
|
+
onClick={() => void result.refresh()}
|
|
714
|
+
disabled={busy || result.loading}
|
|
715
|
+
>
|
|
599
716
|
<RefreshCwIcon className={cn("size-3.5", result.loading && "animate-spin")} />
|
|
600
717
|
</ToolbarButton>
|
|
601
718
|
</div>
|
|
@@ -628,31 +745,27 @@ export function FileBrowser({
|
|
|
628
745
|
: undefined
|
|
629
746
|
}
|
|
630
747
|
className={cn(
|
|
631
|
-
"min-w-0 flex-1
|
|
748
|
+
"flex min-h-0 min-w-0 flex-1 flex-col p-1 outline-none",
|
|
632
749
|
dragOver === "" && "ring-1 ring-inset ring-og-accent",
|
|
633
750
|
)}
|
|
634
751
|
data-opengeni-file-tree
|
|
635
752
|
>
|
|
636
|
-
{/* A root-level create input sits above the rows. */}
|
|
637
|
-
{draftCreate?.parent === "" && (
|
|
638
|
-
<InlineInput
|
|
639
|
-
depth={0}
|
|
640
|
-
kind={draftCreate.kind}
|
|
641
|
-
initialValue=""
|
|
642
|
-
onCommit={(name) => void commitCreate(name)}
|
|
643
|
-
onCancel={() => setDraftCreate(null)}
|
|
644
|
-
/>
|
|
645
|
-
)}
|
|
646
753
|
{showErrorEmpty ? (
|
|
647
754
|
<div className="flex flex-col items-start gap-2 p-2 text-og-sm text-og-fg-subtle">
|
|
648
|
-
<div>
|
|
755
|
+
<div>
|
|
756
|
+
{fallback ??
|
|
757
|
+
`Could not load files: ${result.error?.message ?? "refresh the file list to try again"}`}
|
|
758
|
+
</div>
|
|
649
759
|
<button
|
|
650
760
|
type="button"
|
|
651
761
|
onClick={() => void result.refresh()}
|
|
652
762
|
disabled={result.loading}
|
|
653
763
|
className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2 py-1 text-og-xs font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:min-h-10"
|
|
654
764
|
>
|
|
655
|
-
<RefreshCwIcon
|
|
765
|
+
<RefreshCwIcon
|
|
766
|
+
className={cn("size-3.5", result.loading && "animate-spin")}
|
|
767
|
+
aria-hidden
|
|
768
|
+
/>
|
|
656
769
|
Retry
|
|
657
770
|
</button>
|
|
658
771
|
</div>
|
|
@@ -661,7 +774,21 @@ export function FileBrowser({
|
|
|
661
774
|
{emptyState ?? "This directory is empty"}
|
|
662
775
|
</div>
|
|
663
776
|
) : (
|
|
664
|
-
|
|
777
|
+
// Virtualized: only the rows in/near the viewport mount, so a 3k-node
|
|
778
|
+
// tree stays responsive. The flat item list already carries the
|
|
779
|
+
// expanded subtree + synthetic (create/skeleton/residue) rows.
|
|
780
|
+
<VList
|
|
781
|
+
ref={vlistRef}
|
|
782
|
+
className="min-h-0 flex-1"
|
|
783
|
+
itemSize={coarsePointer ? 44 : 28}
|
|
784
|
+
ssrCount={Math.min(32, renderItems.length)}
|
|
785
|
+
>
|
|
786
|
+
{renderItems.map((item) => (
|
|
787
|
+
<div key={itemKey(item)} className="min-w-0">
|
|
788
|
+
{renderItem(item)}
|
|
789
|
+
</div>
|
|
790
|
+
))}
|
|
791
|
+
</VList>
|
|
665
792
|
)}
|
|
666
793
|
</div>
|
|
667
794
|
|
|
@@ -699,7 +826,7 @@ function ToolbarButton({
|
|
|
699
826
|
onClick={onClick}
|
|
700
827
|
disabled={disabled}
|
|
701
828
|
className={cn(
|
|
702
|
-
"inline-flex items-center justify-center rounded-og-sm p-1 text-og-fg-muted pointer-coarse:min-h-
|
|
829
|
+
"inline-flex size-7 items-center justify-center rounded-og-sm p-1 text-og-fg-muted max-[1023px]:size-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
|
|
703
830
|
"hover:bg-og-surface-2 hover:text-og-fg",
|
|
704
831
|
"disabled:cursor-not-allowed disabled:opacity-40",
|
|
705
832
|
)}
|
|
@@ -746,9 +873,16 @@ function InlineInput({
|
|
|
746
873
|
};
|
|
747
874
|
|
|
748
875
|
return (
|
|
749
|
-
<div
|
|
876
|
+
<div
|
|
877
|
+
className="flex items-center gap-1 px-1 py-0.5"
|
|
878
|
+
style={{ paddingLeft: `${depth * 12 + 4}px` }}
|
|
879
|
+
>
|
|
750
880
|
<span className="inline-block w-3 shrink-0" />
|
|
751
|
-
{kind === "dir" ?
|
|
881
|
+
{kind === "dir" ? (
|
|
882
|
+
<FolderIcon className="size-3.5 shrink-0" />
|
|
883
|
+
) : (
|
|
884
|
+
<FileIcon className="size-3.5 shrink-0" />
|
|
885
|
+
)}
|
|
752
886
|
<input
|
|
753
887
|
ref={ref}
|
|
754
888
|
value={value}
|
|
@@ -47,7 +47,8 @@ export function FleetTile({ session, title, subtitle, onOpen, className }: Fleet
|
|
|
47
47
|
"og-root group relative flex w-full flex-col gap-2.5 overflow-hidden rounded-og-lg border border-og-border",
|
|
48
48
|
"bg-og-surface-1 p-4 text-left shadow-og-sm",
|
|
49
49
|
"transition-[border-color,background-color,box-shadow,transform] duration-200 ease-og-out",
|
|
50
|
-
onOpen &&
|
|
50
|
+
onOpen &&
|
|
51
|
+
"hover:-translate-y-px hover:border-og-border-strong hover:bg-og-surface-2 hover:shadow-og-md",
|
|
51
52
|
"disabled:cursor-default",
|
|
52
53
|
className,
|
|
53
54
|
)}
|
|
@@ -56,14 +57,22 @@ export function FleetTile({ session, title, subtitle, onOpen, className }: Fleet
|
|
|
56
57
|
aria-hidden
|
|
57
58
|
className={cn(
|
|
58
59
|
"absolute inset-y-0 left-0 w-0.5 transition-opacity duration-300",
|
|
59
|
-
running
|
|
60
|
+
running
|
|
61
|
+
? "bg-og-accent opacity-100"
|
|
62
|
+
: needsYou
|
|
63
|
+
? "bg-og-status-waiting opacity-100"
|
|
64
|
+
: "opacity-0",
|
|
60
65
|
)}
|
|
61
66
|
/>
|
|
62
67
|
<span className="flex w-full items-start justify-between gap-3">
|
|
63
|
-
<span className="line-clamp-2 min-w-0 text-og-base font-medium leading-snug text-og-fg">
|
|
68
|
+
<span className="line-clamp-2 min-w-0 text-og-base font-medium leading-snug text-og-fg">
|
|
69
|
+
{title ?? sessionDisplayTitle(session)}
|
|
70
|
+
</span>
|
|
64
71
|
<SessionStatus status={session.status} size="sm" className="mt-px" />
|
|
65
72
|
</span>
|
|
66
|
-
{subtitle ?
|
|
73
|
+
{subtitle ? (
|
|
74
|
+
<span className="line-clamp-1 text-og-sm text-og-fg-muted">{subtitle}</span>
|
|
75
|
+
) : null}
|
|
67
76
|
<span className="mt-auto flex w-full items-center gap-2 text-og-xs text-og-fg-subtle">
|
|
68
77
|
<span className="font-og-mono">{session.id.slice(0, 8)}</span>
|
|
69
78
|
<span aria-hidden>·</span>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ArrowRightIcon,
|
|
2
3
|
CpuIcon,
|
|
3
4
|
LaptopIcon,
|
|
4
5
|
MonitorIcon,
|
|
@@ -8,9 +9,10 @@ import {
|
|
|
8
9
|
} from "lucide-react";
|
|
9
10
|
import { cn } from "../lib/cn";
|
|
10
11
|
import { formatRelativeTime } from "../lib/format";
|
|
11
|
-
import type { MachineView } from "../types/machines";
|
|
12
|
+
import type { MachineView, MetricSample } from "../types/machines";
|
|
13
|
+
import { MachineHealthPill } from "./machine-health-pill";
|
|
12
14
|
import { MachineMetrics } from "./machine-metrics";
|
|
13
|
-
import {
|
|
15
|
+
import { MetricSparkline } from "./machines/metric-sparkline";
|
|
14
16
|
|
|
15
17
|
export type MachineCardProps = {
|
|
16
18
|
machine: MachineView;
|
|
@@ -18,33 +20,68 @@ export type MachineCardProps = {
|
|
|
18
20
|
onAttach?: ((machine: MachineView) => void) | undefined;
|
|
19
21
|
/** Whether an attach/swap is currently in flight (disables the affordance). */
|
|
20
22
|
attaching?: boolean | undefined;
|
|
23
|
+
/** Short recent history (e.g. 15m) — drives the card's CPU trend + preview. */
|
|
24
|
+
series?: MetricSample[] | undefined;
|
|
25
|
+
/** Open the full per-machine telemetry detail. Makes the whole card actionable. */
|
|
26
|
+
onOpenDetail?: ((machine: MachineView) => void) | undefined;
|
|
27
|
+
now?: number | undefined;
|
|
21
28
|
className?: string | undefined;
|
|
22
29
|
};
|
|
23
30
|
|
|
24
31
|
function KindIcon({ machine }: { machine: MachineView }) {
|
|
25
|
-
if (machine.isSessionGroup)
|
|
32
|
+
if (machine.isSessionGroup)
|
|
33
|
+
return <ServerIcon className="size-4 text-og-fg-subtle" aria-hidden />;
|
|
26
34
|
if (machine.kind === "modal") return <CpuIcon className="size-4 text-og-fg-subtle" aria-hidden />;
|
|
27
35
|
return <LaptopIcon className="size-4 text-og-fg-subtle" aria-hidden />;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
/**
|
|
31
|
-
* One machine in the Machines dashboard: name + kind + OS/arch, the
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
39
|
+
* One machine in the Machines dashboard: name + kind + OS/arch, the fused HEALTH
|
|
40
|
+
* verdict, latest resource meters, a CPU trend that previews the history, live
|
|
41
|
+
* freshness, and attach/swap. The whole card opens the telemetry detail when
|
|
42
|
+
* `onOpenDetail` is wired. The session's active sandbox carries an accent edge.
|
|
35
43
|
*/
|
|
36
|
-
export function MachineCard({
|
|
44
|
+
export function MachineCard({
|
|
45
|
+
machine,
|
|
46
|
+
onAttach,
|
|
47
|
+
attaching,
|
|
48
|
+
series,
|
|
49
|
+
onOpenDetail,
|
|
50
|
+
now,
|
|
51
|
+
className,
|
|
52
|
+
}: MachineCardProps) {
|
|
37
53
|
const offline = machine.state === "offline";
|
|
38
54
|
const attachable = !machine.active && !offline && Boolean(onAttach);
|
|
39
55
|
const shared = machine.sharedSessionCount > 1;
|
|
56
|
+
const clockNow = now ?? Date.now();
|
|
57
|
+
const sampledAgo = machine.metrics
|
|
58
|
+
? formatRelativeTime(machine.metrics.sampledAt, new Date(clockNow))
|
|
59
|
+
: null;
|
|
60
|
+
const cpuPts = (series ?? []).map((s) => ({ t: new Date(s.sampledAt).getTime(), v: s.cpuPct }));
|
|
61
|
+
const openable = Boolean(onOpenDetail);
|
|
40
62
|
|
|
41
63
|
return (
|
|
42
64
|
<div
|
|
43
65
|
data-machine-card={machine.sandboxId}
|
|
44
66
|
data-active={machine.active ? "true" : "false"}
|
|
67
|
+
role={openable ? "button" : undefined}
|
|
68
|
+
tabIndex={openable ? 0 : undefined}
|
|
69
|
+
onClick={openable ? () => onOpenDetail?.(machine) : undefined}
|
|
70
|
+
onKeyDown={
|
|
71
|
+
openable
|
|
72
|
+
? (e) => {
|
|
73
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
onOpenDetail?.(machine);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
: undefined
|
|
79
|
+
}
|
|
45
80
|
className={cn(
|
|
46
|
-
"og-root relative flex flex-col gap-3 overflow-hidden rounded-og-lg border border-og-border",
|
|
47
|
-
"bg-og-surface-1 p-4 shadow-og-sm",
|
|
81
|
+
"og-root group relative flex flex-col gap-3 overflow-hidden rounded-og-lg border border-og-border",
|
|
82
|
+
"bg-og-surface-1 p-4 shadow-og-sm transition-colors",
|
|
83
|
+
openable &&
|
|
84
|
+
"cursor-pointer hover:border-og-border-strong focus-visible:border-og-accent/60 focus-visible:outline-none",
|
|
48
85
|
machine.active && "border-og-accent/40",
|
|
49
86
|
className,
|
|
50
87
|
)}
|
|
@@ -71,7 +108,9 @@ export function MachineCard({ machine, onAttach, attaching, className }: Machine
|
|
|
71
108
|
) : null}
|
|
72
109
|
</div>
|
|
73
110
|
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-og-xs text-og-fg-subtle">
|
|
74
|
-
<span className="capitalize">
|
|
111
|
+
<span className="capitalize">
|
|
112
|
+
{machine.isSessionGroup ? "session sandbox" : machine.kind}
|
|
113
|
+
</span>
|
|
75
114
|
<span aria-hidden>·</span>
|
|
76
115
|
<span className="font-og-mono">
|
|
77
116
|
{machine.os}/{machine.arch}
|
|
@@ -88,7 +127,13 @@ export function MachineCard({ machine, onAttach, attaching, className }: Machine
|
|
|
88
127
|
</div>
|
|
89
128
|
</div>
|
|
90
129
|
</div>
|
|
91
|
-
<
|
|
130
|
+
<MachineHealthPill
|
|
131
|
+
state={machine.state}
|
|
132
|
+
metrics={machine.metrics}
|
|
133
|
+
now={clockNow}
|
|
134
|
+
size="sm"
|
|
135
|
+
className="shrink-0"
|
|
136
|
+
/>
|
|
92
137
|
</div>
|
|
93
138
|
|
|
94
139
|
{shared ? (
|
|
@@ -103,9 +148,39 @@ export function MachineCard({ machine, onAttach, attaching, className }: Machine
|
|
|
103
148
|
|
|
104
149
|
<MachineMetrics metrics={machine.metrics} />
|
|
105
150
|
|
|
151
|
+
{/* CPU trend — previews the history and invites opening the detail. */}
|
|
152
|
+
{cpuPts.length > 1 ? (
|
|
153
|
+
<div className="flex flex-col gap-1 rounded-og-md bg-og-surface-2/40 p-2.5">
|
|
154
|
+
<div className="flex items-center justify-between text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">
|
|
155
|
+
<span>CPU · last 15m</span>
|
|
156
|
+
{openable ? (
|
|
157
|
+
<span className="flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
|
158
|
+
Telemetry <ArrowRightIcon className="size-2.5" aria-hidden />
|
|
159
|
+
</span>
|
|
160
|
+
) : null}
|
|
161
|
+
</div>
|
|
162
|
+
<MetricSparkline points={cpuPts} color="var(--og-color-accent)" yMax={100} height={26} />
|
|
163
|
+
</div>
|
|
164
|
+
) : null}
|
|
165
|
+
|
|
106
166
|
<div className="mt-1 flex items-center justify-between gap-3 text-og-xs text-og-fg-subtle">
|
|
107
|
-
<span>
|
|
108
|
-
{
|
|
167
|
+
<span className="inline-flex items-center gap-1.5">
|
|
168
|
+
{sampledAgo ? (
|
|
169
|
+
<>
|
|
170
|
+
<span
|
|
171
|
+
className={cn(
|
|
172
|
+
"size-1.5 rounded-full",
|
|
173
|
+
sampledAgo === "now" ? "bg-og-status-idle" : "bg-og-fg-subtle",
|
|
174
|
+
)}
|
|
175
|
+
aria-hidden
|
|
176
|
+
/>
|
|
177
|
+
{sampledAgo === "now" ? "Live" : `Updated ${sampledAgo} ago`}
|
|
178
|
+
</>
|
|
179
|
+
) : machine.lastSeenAt ? (
|
|
180
|
+
<>Last seen {formatRelativeTime(machine.lastSeenAt, new Date(clockNow))}</>
|
|
181
|
+
) : (
|
|
182
|
+
"Never connected"
|
|
183
|
+
)}
|
|
109
184
|
</span>
|
|
110
185
|
{machine.active ? (
|
|
111
186
|
<span className="text-og-accent">Routing here</span>
|
|
@@ -114,7 +189,10 @@ export function MachineCard({ machine, onAttach, attaching, className }: Machine
|
|
|
114
189
|
type="button"
|
|
115
190
|
data-attach
|
|
116
191
|
disabled={attaching}
|
|
117
|
-
onClick={() =>
|
|
192
|
+
onClick={(e) => {
|
|
193
|
+
e.stopPropagation();
|
|
194
|
+
onAttach?.(machine);
|
|
195
|
+
}}
|
|
118
196
|
className={cn(
|
|
119
197
|
"rounded-og-sm border border-og-border px-2.5 py-1 text-og-xs font-medium text-og-fg-muted transition-colors pointer-coarse:min-h-10",
|
|
120
198
|
"hover:border-og-border-strong hover:text-og-fg disabled:cursor-not-allowed disabled:opacity-50",
|