@opengeni/react 0.11.0 → 0.13.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 +1 -1
- package/dist/{chunk-5C7RGAWA.js → chunk-NFYVQWIB.js} +170 -64
- package/dist/chunk-NFYVQWIB.js.map +1 -0
- package/dist/index.d.ts +791 -145
- package/dist/index.js +5447 -2213
- package/dist/index.js.map +1 -1
- package/dist/{machines-BeZ3bD3t.d.ts → machines-CnlMb7E-.d.ts} +5 -5
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +1 -1
- package/package.json +17 -12
- package/src/approvals.ts +16 -4
- package/src/client.ts +32 -5
- package/src/commands/index.ts +1 -7
- package/src/commands/registry.ts +46 -12
- package/src/components/chat-composer.tsx +135 -39
- 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 +16 -4
- package/src/components/machine-dock-bar.tsx +20 -8
- package/src/components/machine-metrics.tsx +31 -8
- package/src/components/machine-status-pill.tsx +26 -5
- package/src/components/machines-dashboard.tsx +8 -2
- 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/sandbox-files.tsx +110 -227
- package/src/components/sandbox-terminal.tsx +256 -88
- package/src/components/sandbox-workspace.tsx +823 -0
- package/src/components/session-status.tsx +28 -2
- package/src/components/workbench-changes.tsx +541 -0
- package/src/components/workspace-dock.tsx +85 -22
- 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 +57 -36
- 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 +49 -21
- package/src/hooks/use-session-events.ts +48 -20
- package/src/hooks/use-session-lineage.ts +119 -0
- package/src/hooks/use-session.ts +43 -28
- package/src/hooks/use-slash-commands.ts +6 -4
- package/src/hooks/use-turn-queue.ts +74 -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 +128 -23
- package/src/lib/format.ts +44 -6
- package/src/lib/xterm-renderer.ts +65 -0
- package/src/lib/xterm-theme.ts +224 -18
- package/src/machines.ts +13 -3
- package/src/provider.tsx +3 -1
- 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 +44 -10
- package/src/timeline/projection.ts +435 -84
- 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/tokens.css +2 -2
- package/dist/chunk-5C7RGAWA.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>
|
|
@@ -22,7 +22,8 @@ export type MachineCardProps = {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
function KindIcon({ machine }: { machine: MachineView }) {
|
|
25
|
-
if (machine.isSessionGroup)
|
|
25
|
+
if (machine.isSessionGroup)
|
|
26
|
+
return <ServerIcon className="size-4 text-og-fg-subtle" aria-hidden />;
|
|
26
27
|
if (machine.kind === "modal") return <CpuIcon className="size-4 text-og-fg-subtle" aria-hidden />;
|
|
27
28
|
return <LaptopIcon className="size-4 text-og-fg-subtle" aria-hidden />;
|
|
28
29
|
}
|
|
@@ -71,7 +72,9 @@ export function MachineCard({ machine, onAttach, attaching, className }: Machine
|
|
|
71
72
|
) : null}
|
|
72
73
|
</div>
|
|
73
74
|
<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">
|
|
75
|
+
<span className="capitalize">
|
|
76
|
+
{machine.isSessionGroup ? "session sandbox" : machine.kind}
|
|
77
|
+
</span>
|
|
75
78
|
<span aria-hidden>·</span>
|
|
76
79
|
<span className="font-og-mono">
|
|
77
80
|
{machine.os}/{machine.arch}
|
|
@@ -88,7 +91,12 @@ export function MachineCard({ machine, onAttach, attaching, className }: Machine
|
|
|
88
91
|
</div>
|
|
89
92
|
</div>
|
|
90
93
|
</div>
|
|
91
|
-
<MachineStatusPill
|
|
94
|
+
<MachineStatusPill
|
|
95
|
+
state={machine.state}
|
|
96
|
+
sharedSessionCount={machine.sharedSessionCount}
|
|
97
|
+
size="sm"
|
|
98
|
+
className="shrink-0"
|
|
99
|
+
/>
|
|
92
100
|
</div>
|
|
93
101
|
|
|
94
102
|
{shared ? (
|
|
@@ -105,7 +113,11 @@ export function MachineCard({ machine, onAttach, attaching, className }: Machine
|
|
|
105
113
|
|
|
106
114
|
<div className="mt-1 flex items-center justify-between gap-3 text-og-xs text-og-fg-subtle">
|
|
107
115
|
<span>
|
|
108
|
-
{machine.lastSeenAt ?
|
|
116
|
+
{machine.lastSeenAt ? (
|
|
117
|
+
<>Last seen {formatRelativeTime(machine.lastSeenAt)}</>
|
|
118
|
+
) : (
|
|
119
|
+
"Never connected"
|
|
120
|
+
)}
|
|
109
121
|
</span>
|
|
110
122
|
{machine.active ? (
|
|
111
123
|
<span className="text-og-accent">Routing here</span>
|
|
@@ -22,10 +22,9 @@ export type MachineDockBarProps = {
|
|
|
22
22
|
* render IDENTICALLY regardless of backend — this bar is the only backend-aware
|
|
23
23
|
* chrome (the dock-parity contract: selfhosted == Modal for the surfaces).
|
|
24
24
|
*/
|
|
25
|
-
export function MachineDockBar({ name, kind, state,
|
|
25
|
+
export function MachineDockBar({ name, kind, state, className }: MachineDockBarProps) {
|
|
26
26
|
const Icon = kind === "selfhosted" ? LaptopIcon : CpuIcon;
|
|
27
27
|
const stateBadge = MACHINE_STATE_BADGE_META[state];
|
|
28
|
-
const shared = (sharedSessionCount ?? 0) > 1;
|
|
29
28
|
return (
|
|
30
29
|
<div
|
|
31
30
|
data-machine-dock-bar
|
|
@@ -40,13 +39,20 @@ export function MachineDockBar({ name, kind, state, sharedSessionCount, classNam
|
|
|
40
39
|
{stateBadge ? (
|
|
41
40
|
<span
|
|
42
41
|
data-state-badge={state}
|
|
43
|
-
className={cn(
|
|
42
|
+
className={cn(
|
|
43
|
+
"shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium",
|
|
44
|
+
stateBadge.badgeClassName,
|
|
45
|
+
)}
|
|
44
46
|
>
|
|
45
47
|
{stateBadge.label}
|
|
46
48
|
</span>
|
|
47
49
|
) : null}
|
|
48
50
|
</div>
|
|
49
|
-
<ConnectionStatusPill
|
|
51
|
+
<ConnectionStatusPill
|
|
52
|
+
status={connectionStatusForState(state)}
|
|
53
|
+
size="sm"
|
|
54
|
+
className="shrink-0"
|
|
55
|
+
/>
|
|
50
56
|
</div>
|
|
51
57
|
);
|
|
52
58
|
}
|
|
@@ -63,21 +69,27 @@ export type SharedMachineDisclosureProps = {
|
|
|
63
69
|
* dock (and reused on the desktop take-control gate) so the user knows others are
|
|
64
70
|
* driving the same whole-machine lease. Render when `sharedSessionCount > 1`.
|
|
65
71
|
*/
|
|
66
|
-
export function SharedMachineDisclosure({
|
|
72
|
+
export function SharedMachineDisclosure({
|
|
73
|
+
sharedSessionCount,
|
|
74
|
+
density = "compact",
|
|
75
|
+
className,
|
|
76
|
+
}: SharedMachineDisclosureProps) {
|
|
67
77
|
const others = Math.max(0, sharedSessionCount - 1);
|
|
68
78
|
return (
|
|
69
79
|
<div
|
|
70
80
|
data-shared-disclosure
|
|
71
81
|
className={cn(
|
|
72
82
|
"flex items-center gap-1.5 border-og-accent/25 bg-og-accent-soft text-og-fg-muted",
|
|
73
|
-
density === "full"
|
|
83
|
+
density === "full"
|
|
84
|
+
? "rounded-og-md border px-3 py-2 text-[12px]"
|
|
85
|
+
: "border-b px-2.5 py-1 text-[11px]",
|
|
74
86
|
className,
|
|
75
87
|
)}
|
|
76
88
|
>
|
|
77
89
|
<UsersIcon className="size-3.5 shrink-0 text-og-accent" aria-hidden />
|
|
78
90
|
<span>
|
|
79
|
-
Shared — {others === 1 ? "another session is" : `${others} other sessions are`} on this
|
|
80
|
-
terminal, files, and desktop.
|
|
91
|
+
Shared — {others === 1 ? "another session is" : `${others} other sessions are`} on this
|
|
92
|
+
machine. They see the same terminal, files, and desktop.
|
|
81
93
|
</span>
|
|
82
94
|
</div>
|
|
83
95
|
);
|
|
@@ -29,11 +29,17 @@ function Meter({
|
|
|
29
29
|
tone: "ok" | "warn" | "hot";
|
|
30
30
|
}) {
|
|
31
31
|
const fillClass =
|
|
32
|
-
tone === "hot"
|
|
32
|
+
tone === "hot"
|
|
33
|
+
? "bg-og-status-failed"
|
|
34
|
+
: tone === "warn"
|
|
35
|
+
? "bg-og-status-waiting"
|
|
36
|
+
: "bg-og-status-running";
|
|
33
37
|
return (
|
|
34
38
|
<div className="flex min-w-0 flex-col gap-1" data-metric={label.toLowerCase()}>
|
|
35
39
|
<div className="flex items-baseline justify-between gap-2">
|
|
36
|
-
<span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">
|
|
40
|
+
<span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">
|
|
41
|
+
{label}
|
|
42
|
+
</span>
|
|
37
43
|
<span className="font-og-mono text-[11px] tabular-nums text-og-fg-muted">{value}</span>
|
|
38
44
|
</div>
|
|
39
45
|
<div className="h-1 w-full overflow-hidden rounded-full bg-og-surface-2">
|
|
@@ -55,7 +61,11 @@ function toneFor(p: number): "ok" | "warn" | "hot" {
|
|
|
55
61
|
|
|
56
62
|
/** The token text color for a tone (matches the `Meter` fill ramp). */
|
|
57
63
|
function toneTextClass(tone: "ok" | "warn" | "hot"): string {
|
|
58
|
-
return tone === "hot"
|
|
64
|
+
return tone === "hot"
|
|
65
|
+
? "text-og-status-failed"
|
|
66
|
+
: tone === "warn"
|
|
67
|
+
? "text-og-status-waiting"
|
|
68
|
+
: "text-og-status-running";
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
/**
|
|
@@ -86,11 +96,18 @@ function StatTriple({
|
|
|
86
96
|
return (
|
|
87
97
|
<div className="flex items-center justify-between gap-3" data-metric="load">
|
|
88
98
|
<div className="flex min-w-0 flex-col gap-1">
|
|
89
|
-
<span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">
|
|
99
|
+
<span className="text-[10px] font-medium uppercase tracking-wide text-og-fg-subtle">
|
|
100
|
+
Load
|
|
101
|
+
</span>
|
|
90
102
|
<div className="flex items-baseline gap-3">
|
|
91
103
|
{stats.map((s) => (
|
|
92
104
|
<div key={s.label} className="flex items-baseline gap-1">
|
|
93
|
-
<span
|
|
105
|
+
<span
|
|
106
|
+
className={cn(
|
|
107
|
+
"text-[10px] font-medium uppercase tracking-wide",
|
|
108
|
+
toneTextClass(tone),
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
94
111
|
{s.label}
|
|
95
112
|
</span>
|
|
96
113
|
<span className={cn("font-og-mono text-[12px] tabular-nums", toneTextClass(tone))}>
|
|
@@ -128,8 +145,10 @@ export function MachineMetrics({ metrics, density = "compact", className }: Mach
|
|
|
128
145
|
);
|
|
129
146
|
}
|
|
130
147
|
|
|
131
|
-
const memPct =
|
|
132
|
-
|
|
148
|
+
const memPct =
|
|
149
|
+
metrics.memTotalBytes > 0 ? (metrics.memUsedBytes / metrics.memTotalBytes) * 100 : 0;
|
|
150
|
+
const diskPct =
|
|
151
|
+
metrics.diskTotalBytes > 0 ? (metrics.diskUsedBytes / metrics.diskTotalBytes) * 100 : 0;
|
|
133
152
|
const hasGpu = metrics.gpuUtilPct !== null;
|
|
134
153
|
const gpuMemLabel = metrics.gpuMemBytes !== null ? formatBytes(metrics.gpuMemBytes) : null;
|
|
135
154
|
// Memory & disk are honest used/total ratios → full-width meters; stack them in
|
|
@@ -165,7 +184,11 @@ export function MachineMetrics({ metrics, density = "compact", className }: Mach
|
|
|
165
184
|
{hasGpu ? (
|
|
166
185
|
<Meter
|
|
167
186
|
label="GPU"
|
|
168
|
-
value={
|
|
187
|
+
value={
|
|
188
|
+
gpuMemLabel
|
|
189
|
+
? `${metrics.gpuUtilPct!.toFixed(0)}% · ${gpuMemLabel}`
|
|
190
|
+
: `${metrics.gpuUtilPct!.toFixed(0)}%`
|
|
191
|
+
}
|
|
169
192
|
fillPct={metrics.gpuUtilPct!}
|
|
170
193
|
tone={toneFor(metrics.gpuUtilPct!)}
|
|
171
194
|
/>
|