@opengeni/react 0.6.1 → 0.6.3
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/dist/{chunk-DEW2ZNF2.js → chunk-5C7RGAWA.js} +70 -37
- package/dist/chunk-5C7RGAWA.js.map +1 -0
- package/dist/index.d.ts +29 -5
- package/dist/index.js +1061 -618
- package/dist/index.js.map +1 -1
- package/dist/{machines-BD6h9P_s.d.ts → machines-Bv9tG7qZ.d.ts} +1 -1
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +1 -1
- package/package.json +2 -2
- package/src/components/chat-composer.tsx +127 -60
- package/src/components/code-editor.tsx +15 -15
- package/src/components/desktop-viewer.tsx +40 -36
- package/src/components/diff-view.tsx +22 -22
- package/src/components/enrollment-consent.tsx +13 -13
- package/src/components/file-browser.tsx +74 -67
- package/src/components/fleet-tile.tsx +3 -3
- package/src/components/machine-card.tsx +6 -6
- package/src/components/machine-status-pill.tsx +3 -3
- package/src/components/machines-dashboard.tsx +28 -14
- package/src/components/markdown.tsx +6 -6
- package/src/components/message-timeline.tsx +75 -4
- package/src/components/pierre-diff.tsx +9 -10
- package/src/components/pierre-file.tsx +8 -8
- package/src/components/sandbox-files.tsx +37 -40
- package/src/components/sandbox-terminal.tsx +9 -11
- package/src/components/session-status.tsx +2 -2
- package/src/components/workspace-dock.tsx +153 -49
- package/src/hooks/use-file-attachments.ts +45 -15
- package/src/index.ts +2 -2
- package/src/lib/format.ts +32 -0
- package/src/lib/xterm-theme.ts +8 -11
- package/src/timeline/activity-rail.tsx +1 -1
- package/src/timeline/parsers.ts +104 -0
- package/src/timeline/projection.ts +135 -3
- package/src/timeline/screenshot-lightbox.tsx +1 -1
- package/src/timeline/shared.tsx +4 -1
- package/src/timeline/tool-diff.tsx +1 -1
- package/src/timeline/tool-renderers.tsx +46 -7
- package/src/timeline/turn-summary.tsx +21 -3
- package/src/timeline/types.ts +2 -0
- package/styles/tokens.css +8 -0
- package/dist/chunk-DEW2ZNF2.js.map +0 -1
|
@@ -51,11 +51,11 @@ export type FileBrowserProps = {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
const STATUS_TINT: Record<NonNullable<FileTreeNode["status"]>, string> = {
|
|
54
|
-
added: "text-
|
|
55
|
-
modified: "text-
|
|
56
|
-
deleted: "text-
|
|
57
|
-
renamed: "text-
|
|
58
|
-
untracked: "text-
|
|
54
|
+
added: "text-og-status-idle",
|
|
55
|
+
modified: "text-og-status-running",
|
|
56
|
+
deleted: "text-og-status-failed line-through",
|
|
57
|
+
renamed: "text-og-accent",
|
|
58
|
+
untracked: "text-og-fg-subtle",
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
/** Parent dir of a workspace-relative POSIX path ("" for a root entry). */
|
|
@@ -397,15 +397,9 @@ export function FileBrowser({
|
|
|
397
397
|
};
|
|
398
398
|
}, [menu]);
|
|
399
399
|
|
|
400
|
-
|
|
401
|
-
return (
|
|
402
|
-
<div className={cn("p-3 text-xs text-[color:var(--og-color-fg-subtle,var(--color-fg-subtle,#888))]", className)}>
|
|
403
|
-
{fallback ?? `Files unavailable: ${result.error.message}`}
|
|
404
|
-
</div>
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
|
|
400
|
+
const showErrorEmpty = result.error && result.tree.length === 0 && !draftCreate;
|
|
408
401
|
const showEmpty = !result.loading && result.tree.length === 0 && !draftCreate;
|
|
402
|
+
const showToolbar = supportsMutation || Boolean(result.error) || result.loading;
|
|
409
403
|
|
|
410
404
|
const renderRow = (node: FileTreeNode, depth: number): ReactNode => {
|
|
411
405
|
const isOpen = expanded.has(node.path);
|
|
@@ -489,13 +483,11 @@ export function FileBrowser({
|
|
|
489
483
|
setMenu({ node, x: e.clientX, y: e.clientY });
|
|
490
484
|
}}
|
|
491
485
|
className={cn(
|
|
492
|
-
"group flex w-full items-center gap-1 truncate rounded px-1 py-0.5 text-left text-
|
|
493
|
-
"hover:bg-
|
|
494
|
-
isSelected && "bg-
|
|
495
|
-
isCursor &&
|
|
496
|
-
|
|
497
|
-
isDropTarget &&
|
|
498
|
-
"ring-1 ring-inset ring-[color:var(--og-color-accent,var(--color-info,#58a6ff))] bg-[color:var(--og-color-accent-soft,rgba(88,166,255,0.12))]",
|
|
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",
|
|
487
|
+
"hover:bg-og-surface-2",
|
|
488
|
+
isSelected && "bg-og-surface-2",
|
|
489
|
+
isCursor && "outline outline-1 -outline-offset-1 outline-og-accent",
|
|
490
|
+
isDropTarget && "bg-og-accent-soft ring-1 ring-inset ring-og-accent",
|
|
499
491
|
node.status ? STATUS_TINT[node.status] : undefined,
|
|
500
492
|
)}
|
|
501
493
|
style={{ paddingLeft: `${depth * 12 + 4}px` }}
|
|
@@ -503,7 +495,7 @@ export function FileBrowser({
|
|
|
503
495
|
{isDir ? (
|
|
504
496
|
isLoading ? (
|
|
505
497
|
<Loader2Icon
|
|
506
|
-
className="size-3 shrink-0 animate-spin text-
|
|
498
|
+
className="size-3 shrink-0 animate-spin text-og-fg-subtle"
|
|
507
499
|
aria-label="Loading"
|
|
508
500
|
/>
|
|
509
501
|
) : (
|
|
@@ -526,7 +518,7 @@ export function FileBrowser({
|
|
|
526
518
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
527
519
|
setMenu({ node, x: rect.right, y: rect.bottom });
|
|
528
520
|
}}
|
|
529
|
-
className="ml-auto hidden shrink-0 px-1 text-
|
|
521
|
+
className="ml-auto hidden shrink-0 px-1 text-og-fg-subtle hover:text-og-fg group-hover:inline"
|
|
530
522
|
>
|
|
531
523
|
⋯
|
|
532
524
|
</span>
|
|
@@ -555,7 +547,7 @@ export function FileBrowser({
|
|
|
555
547
|
{[0, 1, 2].map((i) => (
|
|
556
548
|
<div
|
|
557
549
|
key={i}
|
|
558
|
-
className="h-2.5 animate-pulse rounded bg-
|
|
550
|
+
className="h-2.5 animate-pulse rounded-og-sm bg-og-surface-2"
|
|
559
551
|
style={{ width: `${70 - i * 12}%` }}
|
|
560
552
|
/>
|
|
561
553
|
))}
|
|
@@ -570,34 +562,38 @@ export function FileBrowser({
|
|
|
570
562
|
|
|
571
563
|
return (
|
|
572
564
|
<div className={cn("flex min-w-0 flex-col", className)}>
|
|
573
|
-
{
|
|
574
|
-
<div className="flex shrink-0 items-center gap-0.5 border-b border-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
565
|
+
{showToolbar && (
|
|
566
|
+
<div className="flex shrink-0 flex-wrap items-center gap-0.5 border-b border-og-border px-1 py-1">
|
|
567
|
+
{supportsMutation ? (
|
|
568
|
+
<>
|
|
569
|
+
<ToolbarButton label="New file" onClick={() => startCreate("file")} disabled={busy}>
|
|
570
|
+
<FilePlusIcon className="size-3.5" />
|
|
571
|
+
</ToolbarButton>
|
|
572
|
+
<ToolbarButton label="New folder" onClick={() => startCreate("dir")} disabled={busy}>
|
|
573
|
+
<FolderPlusIcon className="size-3.5" />
|
|
574
|
+
</ToolbarButton>
|
|
575
|
+
<ToolbarButton
|
|
576
|
+
label="Rename"
|
|
577
|
+
onClick={() => {
|
|
578
|
+
const node = cursor ? findNode(result.tree, cursor) : undefined;
|
|
579
|
+
if (node) startRename(node);
|
|
580
|
+
}}
|
|
581
|
+
disabled={busy || !cursor}
|
|
582
|
+
>
|
|
583
|
+
<PencilIcon className="size-3.5" />
|
|
584
|
+
</ToolbarButton>
|
|
585
|
+
<ToolbarButton
|
|
586
|
+
label="Delete"
|
|
587
|
+
onClick={() => {
|
|
588
|
+
const node = cursor ? findNode(result.tree, cursor) : undefined;
|
|
589
|
+
if (node) void runDelete(node);
|
|
590
|
+
}}
|
|
591
|
+
disabled={busy || !cursor}
|
|
592
|
+
>
|
|
593
|
+
<Trash2Icon className="size-3.5" />
|
|
594
|
+
</ToolbarButton>
|
|
595
|
+
</>
|
|
596
|
+
) : null}
|
|
601
597
|
<span className="ml-auto" />
|
|
602
598
|
<ToolbarButton label="Refresh" onClick={() => void result.refresh()} disabled={busy || result.loading}>
|
|
603
599
|
<RefreshCwIcon className={cn("size-3.5", result.loading && "animate-spin")} />
|
|
@@ -633,7 +629,7 @@ export function FileBrowser({
|
|
|
633
629
|
}
|
|
634
630
|
className={cn(
|
|
635
631
|
"min-w-0 flex-1 overflow-auto p-1 outline-none",
|
|
636
|
-
dragOver === "" && "ring-1 ring-inset ring-
|
|
632
|
+
dragOver === "" && "ring-1 ring-inset ring-og-accent",
|
|
637
633
|
)}
|
|
638
634
|
data-opengeni-file-tree
|
|
639
635
|
>
|
|
@@ -647,9 +643,22 @@ export function FileBrowser({
|
|
|
647
643
|
onCancel={() => setDraftCreate(null)}
|
|
648
644
|
/>
|
|
649
645
|
)}
|
|
650
|
-
{
|
|
651
|
-
<div className="p-2 text-
|
|
652
|
-
{
|
|
646
|
+
{showErrorEmpty ? (
|
|
647
|
+
<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>
|
|
649
|
+
<button
|
|
650
|
+
type="button"
|
|
651
|
+
onClick={() => void result.refresh()}
|
|
652
|
+
disabled={result.loading}
|
|
653
|
+
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
|
+
>
|
|
655
|
+
<RefreshCwIcon className={cn("size-3.5", result.loading && "animate-spin")} aria-hidden />
|
|
656
|
+
Retry
|
|
657
|
+
</button>
|
|
658
|
+
</div>
|
|
659
|
+
) : showEmpty ? (
|
|
660
|
+
<div className="p-2 text-og-sm text-og-fg-subtle">
|
|
661
|
+
{emptyState ?? "This directory is empty"}
|
|
653
662
|
</div>
|
|
654
663
|
) : (
|
|
655
664
|
result.tree.map((node) => renderRow(node, 0))
|
|
@@ -690,8 +699,8 @@ function ToolbarButton({
|
|
|
690
699
|
onClick={onClick}
|
|
691
700
|
disabled={disabled}
|
|
692
701
|
className={cn(
|
|
693
|
-
"inline-flex items-center justify-center rounded p-1 text-
|
|
694
|
-
"hover:bg-
|
|
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",
|
|
703
|
+
"hover:bg-og-surface-2 hover:text-og-fg",
|
|
695
704
|
"disabled:cursor-not-allowed disabled:opacity-40",
|
|
696
705
|
)}
|
|
697
706
|
>
|
|
@@ -759,8 +768,8 @@ function InlineInput({
|
|
|
759
768
|
e.stopPropagation();
|
|
760
769
|
}}
|
|
761
770
|
className={cn(
|
|
762
|
-
"min-w-0 flex-1 rounded border bg-
|
|
763
|
-
"border-
|
|
771
|
+
"min-w-0 flex-1 rounded-og-sm border bg-og-bg px-1 py-0 text-og-sm",
|
|
772
|
+
"border-og-accent text-og-fg",
|
|
764
773
|
"outline-none",
|
|
765
774
|
)}
|
|
766
775
|
/>
|
|
@@ -801,11 +810,9 @@ function ContextMenu({
|
|
|
801
810
|
onClick();
|
|
802
811
|
}}
|
|
803
812
|
className={cn(
|
|
804
|
-
"flex w-full items-center gap-2 px-2.5 py-1 text-left text-
|
|
805
|
-
"hover:bg-
|
|
806
|
-
danger
|
|
807
|
-
? "text-[color:var(--og-color-danger,var(--color-danger,#f85149))]"
|
|
808
|
-
: "text-[color:var(--og-color-fg,var(--color-fg,#e6e6e6))]",
|
|
813
|
+
"flex w-full items-center gap-2 px-2.5 py-1 text-left text-og-sm pointer-coarse:min-h-10",
|
|
814
|
+
"hover:bg-og-surface-2",
|
|
815
|
+
danger ? "text-og-status-failed" : "text-og-fg",
|
|
809
816
|
)}
|
|
810
817
|
>
|
|
811
818
|
{icon}
|
|
@@ -820,15 +827,15 @@ function ContextMenu({
|
|
|
820
827
|
style={{ left, top }}
|
|
821
828
|
className={cn(
|
|
822
829
|
"fixed z-50 min-w-[160px] overflow-hidden rounded-md border py-1 shadow-lg",
|
|
823
|
-
"border-
|
|
824
|
-
"bg-
|
|
830
|
+
"border-og-border",
|
|
831
|
+
"bg-og-surface-1",
|
|
825
832
|
)}
|
|
826
833
|
>
|
|
827
834
|
{isDir && (
|
|
828
835
|
<>
|
|
829
836
|
{item("New file", <FilePlusIcon className="size-3.5" />, onNewFile)}
|
|
830
837
|
{item("New folder", <FolderPlusIcon className="size-3.5" />, onNewFolder)}
|
|
831
|
-
<div className="my-1 h-px bg-
|
|
838
|
+
<div className="my-1 h-px bg-og-border" />
|
|
832
839
|
</>
|
|
833
840
|
)}
|
|
834
841
|
{item("Rename", <PencilIcon className="size-3.5" />, onRename)}
|
|
@@ -60,11 +60,11 @@ export function FleetTile({ session, title, subtitle, onOpen, className }: Fleet
|
|
|
60
60
|
)}
|
|
61
61
|
/>
|
|
62
62
|
<span className="flex w-full items-start justify-between gap-3">
|
|
63
|
-
<span className="line-clamp-2 min-w-0 text-
|
|
63
|
+
<span className="line-clamp-2 min-w-0 text-og-base font-medium leading-snug text-og-fg">{title ?? sessionDisplayTitle(session)}</span>
|
|
64
64
|
<SessionStatus status={session.status} size="sm" className="mt-px" />
|
|
65
65
|
</span>
|
|
66
|
-
{subtitle ? <span className="line-clamp-1 text-
|
|
67
|
-
<span className="mt-auto flex w-full items-center gap-2 text-
|
|
66
|
+
{subtitle ? <span className="line-clamp-1 text-og-sm text-og-fg-muted">{subtitle}</span> : null}
|
|
67
|
+
<span className="mt-auto flex w-full items-center gap-2 text-og-xs text-og-fg-subtle">
|
|
68
68
|
<span className="font-og-mono">{session.id.slice(0, 8)}</span>
|
|
69
69
|
<span aria-hidden>·</span>
|
|
70
70
|
<span className="truncate">{session.model}</span>
|
|
@@ -60,17 +60,17 @@ export function MachineCard({ machine, onAttach, attaching, className }: Machine
|
|
|
60
60
|
</span>
|
|
61
61
|
<div className="min-w-0">
|
|
62
62
|
<div className="flex items-center gap-2">
|
|
63
|
-
<span className="truncate text-
|
|
63
|
+
<span className="truncate text-og-base font-medium text-og-fg">{machine.name}</span>
|
|
64
64
|
{machine.active ? (
|
|
65
65
|
<span
|
|
66
66
|
data-active-marker
|
|
67
|
-
className="shrink-0 rounded-full border border-og-accent/30 bg-og-accent-soft px-1.5 py-px text-
|
|
67
|
+
className="shrink-0 rounded-full border border-og-accent/30 bg-og-accent-soft px-1.5 py-px text-og-xs font-medium text-og-accent"
|
|
68
68
|
>
|
|
69
69
|
Active
|
|
70
70
|
</span>
|
|
71
71
|
) : null}
|
|
72
72
|
</div>
|
|
73
|
-
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-
|
|
73
|
+
<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
74
|
<span className="capitalize">{machine.isSessionGroup ? "session sandbox" : machine.kind}</span>
|
|
75
75
|
<span aria-hidden>·</span>
|
|
76
76
|
<span className="font-og-mono">
|
|
@@ -94,7 +94,7 @@ export function MachineCard({ machine, onAttach, attaching, className }: Machine
|
|
|
94
94
|
{shared ? (
|
|
95
95
|
<p
|
|
96
96
|
data-shared-disclosure
|
|
97
|
-
className="flex items-center gap-1.5 rounded-og-md border border-og-accent/25 bg-og-accent-soft px-2.5 py-1.5 text-
|
|
97
|
+
className="flex items-center gap-1.5 rounded-og-md border border-og-accent/25 bg-og-accent-soft px-2.5 py-1.5 text-og-xs text-og-fg-muted"
|
|
98
98
|
>
|
|
99
99
|
<UsersIcon className="size-3.5 shrink-0 text-og-accent" aria-hidden />
|
|
100
100
|
Shared — {machine.sharedSessionCount} sessions are on this machine.
|
|
@@ -103,7 +103,7 @@ export function MachineCard({ machine, onAttach, attaching, className }: Machine
|
|
|
103
103
|
|
|
104
104
|
<MachineMetrics metrics={machine.metrics} />
|
|
105
105
|
|
|
106
|
-
<div className="mt-1 flex items-center justify-between gap-3 text-
|
|
106
|
+
<div className="mt-1 flex items-center justify-between gap-3 text-og-xs text-og-fg-subtle">
|
|
107
107
|
<span>
|
|
108
108
|
{machine.lastSeenAt ? <>Last seen {formatRelativeTime(machine.lastSeenAt)}</> : "Never connected"}
|
|
109
109
|
</span>
|
|
@@ -116,7 +116,7 @@ export function MachineCard({ machine, onAttach, attaching, className }: Machine
|
|
|
116
116
|
disabled={attaching}
|
|
117
117
|
onClick={() => onAttach?.(machine)}
|
|
118
118
|
className={cn(
|
|
119
|
-
"rounded-og-sm border border-og-border px-2.5 py-1 text-
|
|
119
|
+
"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
120
|
"hover:border-og-border-strong hover:text-og-fg disabled:cursor-not-allowed disabled:opacity-50",
|
|
121
121
|
)}
|
|
122
122
|
>
|
|
@@ -80,7 +80,7 @@ export function ConnectionStatusPill({ status, label, size = "md", className }:
|
|
|
80
80
|
data-connection-status={status}
|
|
81
81
|
className={cn(
|
|
82
82
|
"og-root inline-flex shrink-0 items-center rounded-full border font-medium",
|
|
83
|
-
size === "sm" ? "gap-1 px-1.5 py-px text-
|
|
83
|
+
size === "sm" ? "gap-1 px-1.5 py-px text-og-xs" : "gap-1.5 px-2 py-0.5 text-og-sm",
|
|
84
84
|
meta.badgeClassName,
|
|
85
85
|
className,
|
|
86
86
|
)}
|
|
@@ -132,7 +132,7 @@ export function MachineStatusPill({ state, sharedSessionCount, size = "md", clas
|
|
|
132
132
|
data-state-badge={state}
|
|
133
133
|
className={cn(
|
|
134
134
|
"inline-flex shrink-0 items-center rounded-full border font-medium",
|
|
135
|
-
size === "sm" ? "px-1.5 py-px text-
|
|
135
|
+
size === "sm" ? "px-1.5 py-px text-og-xs" : "px-2 py-0.5 text-og-sm",
|
|
136
136
|
stateBadge.badgeClassName,
|
|
137
137
|
)}
|
|
138
138
|
>
|
|
@@ -145,7 +145,7 @@ export function MachineStatusPill({ state, sharedSessionCount, size = "md", clas
|
|
|
145
145
|
className={cn(
|
|
146
146
|
"inline-flex shrink-0 items-center gap-1 rounded-full border font-medium",
|
|
147
147
|
"text-og-accent border-og-accent/30 bg-og-accent-soft",
|
|
148
|
-
size === "sm" ? "px-1.5 py-px text-
|
|
148
|
+
size === "sm" ? "px-1.5 py-px text-og-xs" : "px-2 py-0.5 text-og-sm",
|
|
149
149
|
)}
|
|
150
150
|
title={`${sharedSessionCount} sessions are on this machine`}
|
|
151
151
|
>
|
|
@@ -30,8 +30,8 @@ function EmptyState({ onEnroll }: { onEnroll?: (() => void) | undefined }) {
|
|
|
30
30
|
<LaptopIcon className="size-5" aria-hidden />
|
|
31
31
|
</span>
|
|
32
32
|
<div className="space-y-1">
|
|
33
|
-
<p className="text-
|
|
34
|
-
<p className="max-w-xs text-
|
|
33
|
+
<p className="text-og-base font-medium text-og-fg">No machines yet</p>
|
|
34
|
+
<p className="max-w-xs text-og-sm text-og-fg-muted">
|
|
35
35
|
Enroll your own computer to run the agent on it — your files, your terminal, your desktop.
|
|
36
36
|
</p>
|
|
37
37
|
</div>
|
|
@@ -40,7 +40,7 @@ function EmptyState({ onEnroll }: { onEnroll?: (() => void) | undefined }) {
|
|
|
40
40
|
type="button"
|
|
41
41
|
data-enroll-cta
|
|
42
42
|
onClick={onEnroll}
|
|
43
|
-
className="inline-flex items-center gap-1.5 rounded-og-sm bg-og-accent px-3 py-1.5 text-
|
|
43
|
+
className="inline-flex items-center gap-1.5 rounded-og-sm bg-og-accent px-3 py-1.5 text-og-sm font-medium text-og-accent-fg transition-colors hover:bg-og-accent-strong pointer-coarse:min-h-10"
|
|
44
44
|
>
|
|
45
45
|
<PlusIcon className="size-3.5" aria-hidden />
|
|
46
46
|
Enroll a machine
|
|
@@ -54,16 +54,18 @@ function Header({
|
|
|
54
54
|
count,
|
|
55
55
|
onEnroll,
|
|
56
56
|
onRefresh,
|
|
57
|
+
loading,
|
|
57
58
|
}: {
|
|
58
59
|
count: number;
|
|
59
60
|
onEnroll?: (() => void) | undefined;
|
|
60
61
|
onRefresh?: (() => void) | undefined;
|
|
62
|
+
loading?: boolean | undefined;
|
|
61
63
|
}): ReactNode {
|
|
62
64
|
return (
|
|
63
65
|
<div className="flex items-center justify-between gap-3">
|
|
64
66
|
<div className="flex items-baseline gap-2">
|
|
65
|
-
<h2 className="text-
|
|
66
|
-
<span className="font-og-mono text-
|
|
67
|
+
<h2 className="text-og-base font-semibold text-og-fg">Machines</h2>
|
|
68
|
+
<span className="font-og-mono text-og-xs text-og-fg-subtle">{count}</span>
|
|
67
69
|
</div>
|
|
68
70
|
<div className="flex items-center gap-1.5">
|
|
69
71
|
{onRefresh ? (
|
|
@@ -74,7 +76,7 @@ function Header({
|
|
|
74
76
|
title="Refresh"
|
|
75
77
|
className="rounded-og-sm p-1.5 text-og-fg-subtle transition-colors hover:bg-og-surface-2 hover:text-og-fg"
|
|
76
78
|
>
|
|
77
|
-
<RefreshCwIcon className="size-3.5" aria-hidden />
|
|
79
|
+
<RefreshCwIcon className={cn("size-3.5", loading && "animate-og-spin")} aria-hidden />
|
|
78
80
|
</button>
|
|
79
81
|
) : null}
|
|
80
82
|
{onEnroll ? (
|
|
@@ -82,10 +84,10 @@ function Header({
|
|
|
82
84
|
type="button"
|
|
83
85
|
data-enroll-cta
|
|
84
86
|
onClick={onEnroll}
|
|
85
|
-
className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2.5 py-1 text-
|
|
87
|
+
className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2.5 py-1 text-og-sm font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg pointer-coarse:min-h-10"
|
|
86
88
|
>
|
|
87
89
|
<PlusIcon className="size-3.5" aria-hidden />
|
|
88
|
-
Enroll
|
|
90
|
+
Enroll machine
|
|
89
91
|
</button>
|
|
90
92
|
) : null}
|
|
91
93
|
</div>
|
|
@@ -95,7 +97,7 @@ function Header({
|
|
|
95
97
|
|
|
96
98
|
/**
|
|
97
99
|
* The workspace Machines dashboard: the fleet of selfhosted enrollments + the
|
|
98
|
-
* session's
|
|
100
|
+
* session's managed sandbox, each with its connection-status pill, state badges,
|
|
99
101
|
* latest metrics, and an attach/swap affordance. Renders the empty state (no
|
|
100
102
|
* machines → enroll CTA), a load error, and the populated grid. The component
|
|
101
103
|
* is purely presentational — feed it `MachinesResponse` data via `useMachines`.
|
|
@@ -115,15 +117,27 @@ export function MachinesDashboard({
|
|
|
115
117
|
|
|
116
118
|
return (
|
|
117
119
|
<section data-machines-dashboard className={cn("og-root flex flex-col gap-3", className)}>
|
|
118
|
-
<Header count={machines.length} onEnroll={onEnroll} onRefresh={onRefresh} />
|
|
120
|
+
<Header count={machines.length} onEnroll={onEnroll} onRefresh={onRefresh} loading={loading} />
|
|
119
121
|
|
|
120
122
|
{error ? (
|
|
121
|
-
<
|
|
123
|
+
<div
|
|
122
124
|
data-machines-error
|
|
123
|
-
className="rounded-og-md border border-og-status-failed/30 bg-og-status-failed/10 px-3 py-2 text-
|
|
125
|
+
className="flex flex-wrap items-center justify-between gap-2 rounded-og-md border border-og-status-failed/30 bg-og-status-failed/10 px-3 py-2 text-og-sm text-og-status-failed"
|
|
124
126
|
>
|
|
125
|
-
Could not load machines: {error.message}
|
|
126
|
-
|
|
127
|
+
<span>Could not load machines: {error.message}</span>
|
|
128
|
+
{onRefresh ? (
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
data-machines-retry
|
|
132
|
+
onClick={onRefresh}
|
|
133
|
+
disabled={loading}
|
|
134
|
+
className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-status-failed/30 px-2 py-1 text-og-xs font-medium transition-colors hover:border-og-status-failed/50 disabled:cursor-not-allowed disabled:opacity-60 pointer-coarse:min-h-10"
|
|
135
|
+
>
|
|
136
|
+
<RefreshCwIcon className={cn("size-3.5", loading && "animate-og-spin")} aria-hidden />
|
|
137
|
+
{loading ? "Retrying…" : "Retry"}
|
|
138
|
+
</button>
|
|
139
|
+
) : null}
|
|
140
|
+
</div>
|
|
127
141
|
) : null}
|
|
128
142
|
|
|
129
143
|
{loading && machines.length === 0 ? (
|
|
@@ -36,12 +36,12 @@ const components: Components = {
|
|
|
36
36
|
</h2>
|
|
37
37
|
),
|
|
38
38
|
h3: ({ children, ...props }) => (
|
|
39
|
-
<h3 className="mt-4 mb-1.5 text-
|
|
39
|
+
<h3 className="mt-4 mb-1.5 text-og-md font-semibold tracking-tight text-og-fg first:mt-0" {...props}>
|
|
40
40
|
{children}
|
|
41
41
|
</h3>
|
|
42
42
|
),
|
|
43
43
|
h4: ({ children, ...props }) => (
|
|
44
|
-
<h4 className="mt-4 mb-1.5 text-sm font-semibold uppercase tracking-[0.04em] text-og-fg-muted first:mt-0" {...props}>
|
|
44
|
+
<h4 className="mt-4 mb-1.5 text-og-sm font-semibold uppercase tracking-[0.04em] text-og-fg-muted first:mt-0" {...props}>
|
|
45
45
|
{children}
|
|
46
46
|
</h4>
|
|
47
47
|
),
|
|
@@ -94,7 +94,7 @@ const components: Components = {
|
|
|
94
94
|
{...props}
|
|
95
95
|
type="checkbox"
|
|
96
96
|
disabled
|
|
97
|
-
className="mr-2 size-3.5 translate-y-[2px] cursor-default
|
|
97
|
+
className="mr-2 size-3.5 translate-y-[2px] cursor-default accent-og-accent align-baseline"
|
|
98
98
|
/>
|
|
99
99
|
) : (
|
|
100
100
|
<input type={type} {...props} />
|
|
@@ -113,7 +113,7 @@ const components: Components = {
|
|
|
113
113
|
// the `pre` renderer), so a `code` reaching here is treated as inline.
|
|
114
114
|
code: ({ children, className: _className, ...props }) => (
|
|
115
115
|
<code
|
|
116
|
-
className="rounded-og-xs border border-og-border bg-og-surface-1 px-1 py-0.5 font-og-mono text-
|
|
116
|
+
className="rounded-og-xs border border-og-border bg-og-surface-1 px-1 py-0.5 font-og-mono text-og-sm text-og-fg"
|
|
117
117
|
{...props}
|
|
118
118
|
>
|
|
119
119
|
{children}
|
|
@@ -123,7 +123,7 @@ const components: Components = {
|
|
|
123
123
|
// visual consistency (bordered, scrollable, mono, surface background).
|
|
124
124
|
pre: ({ children, ...props }) => (
|
|
125
125
|
<pre
|
|
126
|
-
className="my-3 max-h-96 overflow-auto rounded-og-md border border-og-border bg-og-bg/60 p-3 font-og-mono text-
|
|
126
|
+
className="my-3 max-h-96 overflow-auto rounded-og-md border border-og-border bg-og-bg/60 p-3 font-og-mono text-og-sm text-og-fg-muted [&>code]:border-0 [&>code]:bg-transparent [&>code]:p-0 [&>code]:text-inherit first:mt-0 last:mb-0"
|
|
127
127
|
{...props}
|
|
128
128
|
>
|
|
129
129
|
{children}
|
|
@@ -131,7 +131,7 @@ const components: Components = {
|
|
|
131
131
|
),
|
|
132
132
|
table: ({ children, ...props }) => (
|
|
133
133
|
<div className="my-3 max-w-full overflow-x-auto rounded-og-md border border-og-border first:mt-0 last:mb-0">
|
|
134
|
-
<table className="w-full border-collapse text-
|
|
134
|
+
<table className="w-full border-collapse text-og-base" {...props}>
|
|
135
135
|
{children}
|
|
136
136
|
</table>
|
|
137
137
|
</div>
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "lucide-react";
|
|
12
12
|
import type { ComponentType } from "react";
|
|
13
13
|
import { AnimatePresence, motion } from "motion/react";
|
|
14
|
-
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
14
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
15
15
|
import { cn } from "../lib/cn";
|
|
16
16
|
import { formatRelativeTime, truncate } from "../lib/format";
|
|
17
17
|
import { Markdown } from "./markdown";
|
|
@@ -78,10 +78,38 @@ export function MessageTimeline({
|
|
|
78
78
|
|
|
79
79
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
80
80
|
const [pinned, setPinned] = useState(true);
|
|
81
|
+
// Mirror `pinned` into a ref so the ResizeObserver callback (a stable closure)
|
|
82
|
+
// always reads the live value without re-subscribing on every scroll.
|
|
83
|
+
const pinnedRef = useRef(true);
|
|
84
|
+
// The reader's visual anchor: the topmost still-visible timeline element and
|
|
85
|
+
// its offset from the viewport top. Recaptured on scroll and after every
|
|
86
|
+
// height change, it lets us hold the reader's position when content above the
|
|
87
|
+
// viewport expands or collapses (e.g. a turn folds when it settles).
|
|
88
|
+
const anchorRef = useRef<{ el: Element; top: number } | null>(null);
|
|
81
89
|
const lastItem = resolvedItems[resolvedItems.length - 1];
|
|
82
90
|
const streaming = lastItem !== undefined && (lastItem.kind === "agent-message" || lastItem.kind === "reasoning") && lastItem.streaming;
|
|
83
91
|
const working = status === "running" && !streaming;
|
|
84
92
|
|
|
93
|
+
// Snapshot the topmost visible element and where it sits in the viewport, so a
|
|
94
|
+
// later reflow can restore it to the same spot.
|
|
95
|
+
const captureAnchor = useCallback(() => {
|
|
96
|
+
const node = scrollRef.current;
|
|
97
|
+
const inner = node?.firstElementChild;
|
|
98
|
+
if (!node || !inner) {
|
|
99
|
+
anchorRef.current = null;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const containerTop = node.getBoundingClientRect().top;
|
|
103
|
+
for (const child of Array.from(inner.children)) {
|
|
104
|
+
const rect = child.getBoundingClientRect();
|
|
105
|
+
if (rect.bottom > containerTop + 1) {
|
|
106
|
+
anchorRef.current = { el: child, top: rect.top - containerTop };
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
anchorRef.current = null;
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
85
113
|
// Follow the stream while pinned to the bottom; never fight the reader.
|
|
86
114
|
useEffect(() => {
|
|
87
115
|
const node = scrollRef.current;
|
|
@@ -90,12 +118,51 @@ export function MessageTimeline({
|
|
|
90
118
|
}
|
|
91
119
|
}, [resolvedItems, working, autoFollow, pinned]);
|
|
92
120
|
|
|
121
|
+
// Scroll anchoring: when the content reflows (a fold expands/collapses, a
|
|
122
|
+
// stream appends), keep following the bottom if pinned; otherwise pin the
|
|
123
|
+
// reader's anchor in place. A change ABOVE the anchor shifts its viewport
|
|
124
|
+
// offset — we correct scrollTop by that shift so the reader never gets yanked.
|
|
125
|
+
// A change BELOW the anchor (a bottom append while scrolled up) leaves the
|
|
126
|
+
// anchor put, so `diff` is 0 and we leave scrollTop alone.
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const node = scrollRef.current;
|
|
129
|
+
const inner = node?.firstElementChild;
|
|
130
|
+
if (!node || !inner || typeof ResizeObserver === "undefined") {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const observer = new ResizeObserver(() => {
|
|
134
|
+
const current = scrollRef.current;
|
|
135
|
+
if (!current) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (autoFollow && pinnedRef.current) {
|
|
139
|
+
current.scrollTop = current.scrollHeight;
|
|
140
|
+
} else {
|
|
141
|
+
const anchor = anchorRef.current;
|
|
142
|
+
if (anchor && anchor.el.isConnected) {
|
|
143
|
+
const containerTop = current.getBoundingClientRect().top;
|
|
144
|
+
const now = anchor.el.getBoundingClientRect().top - containerTop;
|
|
145
|
+
const diff = now - anchor.top;
|
|
146
|
+
if (diff !== 0) {
|
|
147
|
+
current.scrollTop += diff;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
captureAnchor();
|
|
152
|
+
});
|
|
153
|
+
observer.observe(inner);
|
|
154
|
+
return () => observer.disconnect();
|
|
155
|
+
}, [autoFollow, captureAnchor]);
|
|
156
|
+
|
|
93
157
|
const onScroll = () => {
|
|
94
158
|
const node = scrollRef.current;
|
|
95
159
|
if (!node) {
|
|
96
160
|
return;
|
|
97
161
|
}
|
|
98
|
-
|
|
162
|
+
const nextPinned = node.scrollHeight - node.scrollTop - node.clientHeight < 48;
|
|
163
|
+
pinnedRef.current = nextPinned;
|
|
164
|
+
setPinned(nextPinned);
|
|
165
|
+
captureAnchor();
|
|
99
166
|
};
|
|
100
167
|
|
|
101
168
|
return (
|
|
@@ -135,6 +202,7 @@ export function MessageTimeline({
|
|
|
135
202
|
if (node) {
|
|
136
203
|
node.scrollTo({ top: node.scrollHeight, behavior: "smooth" });
|
|
137
204
|
}
|
|
205
|
+
pinnedRef.current = true;
|
|
138
206
|
setPinned(true);
|
|
139
207
|
}}
|
|
140
208
|
className={cn(
|
|
@@ -287,8 +355,11 @@ function UserMessageRow({
|
|
|
287
355
|
}) {
|
|
288
356
|
return (
|
|
289
357
|
<div className="animate-og-enter flex justify-end">
|
|
290
|
-
<div className="max-w-[85%] min-w-0
|
|
291
|
-
{
|
|
358
|
+
<div className="flex max-w-[85%] min-w-0 flex-col items-end gap-1">
|
|
359
|
+
{item.pending ? <span className="px-1 text-og-xs text-og-fg-subtle">queued</span> : null}
|
|
360
|
+
<div className="w-fit max-w-full min-w-0 rounded-og-lg rounded-br-og-xs border border-og-border bg-og-surface-2 px-4 py-2.5 text-og-md leading-6 text-og-fg">
|
|
361
|
+
{renderMessageText ? renderMessageText(item.text, item) : <Markdown>{item.text}</Markdown>}
|
|
362
|
+
</div>
|
|
292
363
|
</div>
|
|
293
364
|
</div>
|
|
294
365
|
);
|
|
@@ -98,16 +98,15 @@ export function PierreDiff({
|
|
|
98
98
|
|
|
99
99
|
// Pierre renders inside a shadow DOM, so host CSS can't reach it — but it reads
|
|
100
100
|
// a set of `--diffs-*-override` custom properties through the shadow boundary.
|
|
101
|
-
// Pin the diff's own base background to the dock surface
|
|
102
|
-
//
|
|
103
|
-
// hunk-separator slab so the collapsed-context row isn't a heavy gray bar.
|
|
101
|
+
// Pin the diff's own base background to the dock surface and quiet the hunk
|
|
102
|
+
// separator slab so collapsed context rows do not become heavy bars.
|
|
104
103
|
const pierreVars = {
|
|
105
|
-
"--diffs-dark-bg": "var(--og-color-bg
|
|
106
|
-
"--diffs-light-bg": "var(--og-color-bg
|
|
107
|
-
"--diffs-bg-buffer-override": "var(--og-color-surface-1
|
|
108
|
-
"--diffs-bg-separator-override": "var(--og-color-surface-1
|
|
109
|
-
"--diffs-font-size": "
|
|
110
|
-
"--diffs-line-height": "
|
|
104
|
+
"--diffs-dark-bg": "var(--og-color-bg)",
|
|
105
|
+
"--diffs-light-bg": "var(--og-color-bg)",
|
|
106
|
+
"--diffs-bg-buffer-override": "var(--og-color-surface-1)",
|
|
107
|
+
"--diffs-bg-separator-override": "var(--og-color-surface-1)",
|
|
108
|
+
"--diffs-font-size": "var(--og-code-font-size)",
|
|
109
|
+
"--diffs-line-height": "var(--og-code-line-height)",
|
|
111
110
|
} as CSSProperties;
|
|
112
111
|
|
|
113
112
|
return (
|
|
@@ -133,7 +132,7 @@ export function PierreDiff({
|
|
|
133
132
|
|
|
134
133
|
function DiffSkeleton() {
|
|
135
134
|
return (
|
|
136
|
-
<div className="p-3 text-
|
|
135
|
+
<div className="p-3 text-og-sm text-og-fg-subtle">
|
|
137
136
|
Loading diff…
|
|
138
137
|
</div>
|
|
139
138
|
);
|