@opengeni/react 0.12.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.
Files changed (85) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-5TIXRCSI.js → chunk-NFYVQWIB.js} +150 -64
  3. package/dist/chunk-NFYVQWIB.js.map +1 -0
  4. package/dist/index.d.ts +760 -142
  5. package/dist/index.js +5345 -2146
  6. package/dist/index.js.map +1 -1
  7. package/dist/{machines-BeZ3bD3t.d.ts → machines-CnlMb7E-.d.ts} +5 -5
  8. package/dist/machines.d.ts +1 -1
  9. package/dist/machines.js +1 -1
  10. package/package.json +17 -12
  11. package/src/approvals.ts +16 -4
  12. package/src/client.ts +32 -5
  13. package/src/commands/index.ts +1 -7
  14. package/src/commands/registry.ts +46 -12
  15. package/src/components/chat-composer.tsx +135 -39
  16. package/src/components/code-editor.tsx +28 -36
  17. package/src/components/command-palette.tsx +26 -5
  18. package/src/components/desktop-viewer.tsx +29 -16
  19. package/src/components/diff-view.tsx +27 -189
  20. package/src/components/enrollment-consent.tsx +28 -10
  21. package/src/components/enrollment-device-flow.tsx +8 -5
  22. package/src/components/file-browser.tsx +190 -56
  23. package/src/components/fleet-tile.tsx +13 -4
  24. package/src/components/machine-card.tsx +16 -4
  25. package/src/components/machine-dock-bar.tsx +20 -8
  26. package/src/components/machine-metrics.tsx +31 -8
  27. package/src/components/machine-status-pill.tsx +26 -5
  28. package/src/components/machines-dashboard.tsx +8 -2
  29. package/src/components/markdown.tsx +39 -9
  30. package/src/components/message-timeline.tsx +633 -109
  31. package/src/components/pierre-diff.tsx +83 -15
  32. package/src/components/pierre-file.tsx +10 -9
  33. package/src/components/sandbox-files.tsx +110 -227
  34. package/src/components/sandbox-terminal.tsx +256 -88
  35. package/src/components/sandbox-workspace.tsx +823 -0
  36. package/src/components/session-status.tsx +28 -2
  37. package/src/components/workbench-changes.tsx +541 -0
  38. package/src/components/workspace-dock.tsx +85 -22
  39. package/src/hooks/internal.ts +5 -2
  40. package/src/hooks/use-available-models.ts +7 -2
  41. package/src/hooks/use-billing-usage.ts +4 -1
  42. package/src/hooks/use-codex-accounts.ts +74 -44
  43. package/src/hooks/use-composer.ts +57 -36
  44. package/src/hooks/use-environments.ts +92 -47
  45. package/src/hooks/use-file-attachments.ts +101 -55
  46. package/src/hooks/use-goal.ts +52 -16
  47. package/src/hooks/use-machine-chip.ts +134 -0
  48. package/src/hooks/use-machines.ts +34 -16
  49. package/src/hooks/use-packs.ts +24 -19
  50. package/src/hooks/use-relay-frame-stream.ts +5 -2
  51. package/src/hooks/use-rigs.ts +335 -0
  52. package/src/hooks/use-sandbox-files.ts +213 -39
  53. package/src/hooks/use-sandbox-git.ts +188 -11
  54. package/src/hooks/use-sandbox-terminal.ts +18 -14
  55. package/src/hooks/use-scheduled-tasks.ts +10 -2
  56. package/src/hooks/use-session-capabilities.ts +77 -20
  57. package/src/hooks/use-session-control.ts +49 -21
  58. package/src/hooks/use-session-events.ts +48 -20
  59. package/src/hooks/use-session-lineage.ts +119 -0
  60. package/src/hooks/use-session.ts +43 -28
  61. package/src/hooks/use-slash-commands.ts +6 -4
  62. package/src/hooks/use-turn-queue.ts +74 -147
  63. package/src/hooks/use-windowed-sections.ts +204 -0
  64. package/src/hooks/use-workspace-capture.ts +233 -0
  65. package/src/hooks/use-workspace-edit.ts +231 -0
  66. package/src/hooks/use-workspace-sessions.ts +48 -5
  67. package/src/hooks/use-workspaces.ts +18 -15
  68. package/src/index.ts +127 -23
  69. package/src/lib/format.ts +3 -3
  70. package/src/lib/xterm-renderer.ts +65 -0
  71. package/src/lib/xterm-theme.ts +224 -18
  72. package/src/machines.ts +13 -3
  73. package/src/provider.tsx +3 -1
  74. package/src/timeline/activity-rail.tsx +213 -54
  75. package/src/timeline/disclosure-context.tsx +12 -2
  76. package/src/timeline/index.ts +21 -2
  77. package/src/timeline/parsers.ts +44 -10
  78. package/src/timeline/projection.ts +388 -85
  79. package/src/timeline/shared.tsx +111 -24
  80. package/src/timeline/tool-diff.tsx +24 -20
  81. package/src/timeline/tool-renderers.tsx +98 -21
  82. package/src/timeline/turn-summary.tsx +76 -23
  83. package/src/timeline/types.ts +96 -12
  84. package/styles/tokens.css +2 -2
  85. 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
- // The visible rows in DOM order drives keyboard up/down navigation.
173
- const flatRows = useMemo(() => {
174
- const rows: { node: FileTreeNode; depth: number }[] = [];
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
- rows.push({ node, depth });
178
- if (node.kind === "dir" && expanded.has(node.path) && node.children?.length) {
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 rows;
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
- [draftCreate, draftRename, flatRows, cursor, expanded, open, toggle, onSelectFile, startRename, runDelete, supportsMutation],
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
- const renderRow = (node: FileTreeNode, depth: number): ReactNode => {
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 key={node.path}>{renderNode(node, depth, isOpen)}</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-10",
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
- isLoading ? (
497
- <Loader2Icon
498
- className="size-3 shrink-0 animate-spin text-og-fg-subtle"
499
- aria-label="Loading"
500
- />
501
- ) : (
502
- <ChevronRightIcon className={cn("size-3 shrink-0 transition-transform", isOpen && "rotate-90")} />
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 ? <FolderIcon className="size-3.5 shrink-0" /> : <FileIcon className="size-3.5 shrink-0" />}
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
- {/* Inline create input nested directly under an expanded directory. */}
530
- {draftCreate?.parent === node.path && isDir && isOpen && (
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 + 1}
533
- kind={draftCreate.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
- {showSkeleton && (
638
+ );
639
+ case "skeleton":
640
+ return (
541
641
  <div
542
642
  role="group"
543
643
  aria-hidden
544
- style={{ paddingLeft: `${(depth + 1) * 12 + 4}px` }}
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
- {isDir && isOpen && node.children && node.children.length > 0 && (
557
- <div role="group">{node.children.map((child) => renderRow(child, depth + 1))}</div>
558
- )}
559
- </div>
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 label="Refresh" onClick={() => void result.refresh()} disabled={busy || result.loading}>
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 overflow-auto p-1 outline-none",
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>{fallback ?? `Could not load files: ${result.error?.message ?? "refresh the file list to try again"}`}</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 className={cn("size-3.5", result.loading && "animate-spin")} aria-hidden />
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
- result.tree.map((node) => renderRow(node, 0))
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-10 pointer-coarse:min-w-10",
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 className="flex items-center gap-1 px-1 py-0.5" style={{ paddingLeft: `${depth * 12 + 4}px` }}>
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" ? <FolderIcon className="size-3.5 shrink-0" /> : <FileIcon className="size-3.5 shrink-0" />}
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 && "hover:-translate-y-px hover:border-og-border-strong hover:bg-og-surface-2 hover:shadow-og-md",
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 ? "bg-og-accent opacity-100" : needsYou ? "bg-og-status-waiting opacity-100" : "opacity-0",
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">{title ?? sessionDisplayTitle(session)}</span>
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 ? <span className="line-clamp-1 text-og-sm text-og-fg-muted">{subtitle}</span> : null}
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) return <ServerIcon className="size-4 text-og-fg-subtle" aria-hidden />;
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">{machine.isSessionGroup ? "session sandbox" : machine.kind}</span>
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 state={machine.state} sharedSessionCount={machine.sharedSessionCount} size="sm" className="shrink-0" />
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 ? <>Last seen {formatRelativeTime(machine.lastSeenAt)}</> : "Never connected"}
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, sharedSessionCount, className }: MachineDockBarProps) {
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("shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium", stateBadge.badgeClassName)}
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 status={connectionStatusForState(state)} size="sm" className="shrink-0" />
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({ sharedSessionCount, density = "compact", className }: SharedMachineDisclosureProps) {
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" ? "rounded-og-md border px-3 py-2 text-[12px]" : "border-b px-2.5 py-1 text-[11px]",
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 machine. They see the same
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" ? "bg-og-status-failed" : tone === "warn" ? "bg-og-status-waiting" : "bg-og-status-running";
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">{label}</span>
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" ? "text-og-status-failed" : tone === "warn" ? "text-og-status-waiting" : "text-og-status-running";
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">Load</span>
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 className={cn("text-[10px] font-medium uppercase tracking-wide", toneTextClass(tone))}>
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 = metrics.memTotalBytes > 0 ? (metrics.memUsedBytes / metrics.memTotalBytes) * 100 : 0;
132
- const diskPct = metrics.diskTotalBytes > 0 ? (metrics.diskUsedBytes / metrics.diskTotalBytes) * 100 : 0;
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={gpuMemLabel ? `${metrics.gpuUtilPct!.toFixed(0)}% · ${gpuMemLabel}` : `${metrics.gpuUtilPct!.toFixed(0)}%`}
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
  />