@opengeni/react 0.5.0 → 0.6.1

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.
@@ -0,0 +1,245 @@
1
+ import { type ReactNode, useState } from "react";
2
+ import {
3
+ CircleAlertIcon,
4
+ LaptopIcon,
5
+ MonitorIcon,
6
+ ScreenShareIcon,
7
+ ShieldAlertIcon,
8
+ TerminalIcon,
9
+ UsersIcon,
10
+ } from "lucide-react";
11
+ import { cn } from "../lib/cn";
12
+
13
+ /** The machine identity surfaced to the approver, from the device-flow start. */
14
+ export type EnrollmentConsentMachine = {
15
+ /** Human-friendly name (hostname by default). */
16
+ machineName: string;
17
+ os: string;
18
+ arch: string;
19
+ /** The agent can offer a display (a real screen / Xvfb is available). */
20
+ canOfferDisplay: boolean;
21
+ /** The agent requests screen control (computer-use). */
22
+ requestsScreenControl: boolean;
23
+ };
24
+
25
+ /** The phase of the approve page (drives the rendered panel). */
26
+ export type EnrollmentConsentPhase = "review" | "approving" | "approved" | "denied" | "error";
27
+
28
+ export type EnrollmentConsentProps = {
29
+ /** The short code the user typed / scanned (echoed back for confirmation). */
30
+ userCode: string;
31
+ machine: EnrollmentConsentMachine;
32
+ phase?: EnrollmentConsentPhase | undefined;
33
+ /** Approve with the chosen screen-control consent. */
34
+ onApprove?: ((allowScreenControl: boolean) => void) | undefined;
35
+ onDeny?: (() => void) | undefined;
36
+ /** Error message when phase === "error". */
37
+ errorMessage?: string | undefined;
38
+ className?: string | undefined;
39
+ };
40
+
41
+ function Capability({ icon, title, body }: { icon: ReactNode; title: string; body: string }) {
42
+ return (
43
+ <li className="flex items-start gap-2.5">
44
+ <span className="mt-0.5 shrink-0 text-og-status-failed">{icon}</span>
45
+ <span className="min-w-0">
46
+ <span className="block text-[13px] font-medium text-og-fg">{title}</span>
47
+ <span className="block text-[12px] text-og-fg-muted">{body}</span>
48
+ </span>
49
+ </li>
50
+ );
51
+ }
52
+
53
+ /**
54
+ * The user-facing APPROVE page for the device-flow enrollment: a LOUD,
55
+ * un-minimised whole-machine consent. Approving grants the agent FULL access to
56
+ * this machine — exec, files, terminal — and (when toggled) live screen control.
57
+ * The danger framing is deliberate (§18 hardening: whole-machine = loud consent),
58
+ * never buried. Renders review / approving / approved / denied / error phases.
59
+ */
60
+ export function EnrollmentConsent({
61
+ userCode,
62
+ machine,
63
+ phase = "review",
64
+ onApprove,
65
+ onDeny,
66
+ errorMessage,
67
+ className,
68
+ }: EnrollmentConsentProps) {
69
+ const [allowScreenControl, setAllowScreenControl] = useState(machine.requestsScreenControl);
70
+ const busy = phase === "approving";
71
+
72
+ if (phase === "approved") {
73
+ return (
74
+ <ConsentResult
75
+ className={className}
76
+ tone="ok"
77
+ title="Machine connected"
78
+ body={`${machine.machineName} is now enrolled in this workspace. You can close this page — it will appear in your Machines dashboard.`}
79
+ />
80
+ );
81
+ }
82
+ if (phase === "denied") {
83
+ return (
84
+ <ConsentResult
85
+ className={className}
86
+ tone="muted"
87
+ title="Enrollment denied"
88
+ body={`You declined to connect ${machine.machineName}. The agent has no access to this machine. You can re-run the install one-liner to try again.`}
89
+ />
90
+ );
91
+ }
92
+ if (phase === "error") {
93
+ return (
94
+ <ConsentResult
95
+ className={className}
96
+ tone="danger"
97
+ title="Could not complete enrollment"
98
+ body={errorMessage ?? "The code may have expired. Re-run the install one-liner on the machine for a fresh code."}
99
+ />
100
+ );
101
+ }
102
+
103
+ return (
104
+ <div
105
+ data-enrollment-consent
106
+ className={cn(
107
+ "og-root mx-auto flex w-full max-w-md flex-col gap-5 rounded-og-lg border border-og-status-failed/30 bg-og-surface-1 p-6 shadow-og-md",
108
+ className,
109
+ )}
110
+ >
111
+ <div className="flex items-start gap-3">
112
+ <span className="flex size-9 shrink-0 items-center justify-center rounded-full bg-og-status-failed/15 text-og-status-failed">
113
+ <ShieldAlertIcon className="size-5" aria-hidden />
114
+ </span>
115
+ <div className="min-w-0">
116
+ <h1 className="text-base font-semibold text-og-fg">Give the agent your whole machine?</h1>
117
+ <p className="mt-1 text-[13px] text-og-fg-muted">
118
+ Approving lets the OpenGeni agent run on <span className="font-medium text-og-fg">{machine.machineName}</span> with
119
+ full access. This is your real computer — not a sandbox.
120
+ </p>
121
+ </div>
122
+ </div>
123
+
124
+ <div className="flex flex-wrap items-center gap-2 rounded-og-md border border-og-border bg-og-surface-2 px-3 py-2 text-[12px]">
125
+ <LaptopIcon className="size-4 text-og-fg-subtle" aria-hidden />
126
+ <span className="font-medium text-og-fg">{machine.machineName}</span>
127
+ <span className="font-og-mono text-og-fg-subtle">
128
+ {machine.os}/{machine.arch}
129
+ </span>
130
+ <span aria-hidden className="text-og-fg-subtle">·</span>
131
+ <span className="text-og-fg-subtle">
132
+ code <span className="font-og-mono font-medium text-og-fg">{userCode}</span>
133
+ </span>
134
+ </div>
135
+
136
+ <ul className="flex flex-col gap-3">
137
+ <Capability
138
+ icon={<TerminalIcon className="size-4" aria-hidden />}
139
+ title="Read, write & run anything"
140
+ body="The agent can read and modify any file, run any command, and use your git credentials — as if it were you at the keyboard."
141
+ />
142
+ {machine.canOfferDisplay ? (
143
+ <Capability
144
+ icon={<MonitorIcon className="size-4" aria-hidden />}
145
+ title="See your screen"
146
+ body="The agent can capture this machine's display to watch what it is doing."
147
+ />
148
+ ) : null}
149
+ <Capability
150
+ icon={<UsersIcon className="size-4" aria-hidden />}
151
+ title="Shared while connected"
152
+ body="Any session in this workspace can use this machine while it is online. Disconnect the agent to revoke access instantly."
153
+ />
154
+ </ul>
155
+
156
+ {machine.canOfferDisplay ? (
157
+ <label
158
+ data-screen-control-toggle
159
+ className="flex cursor-pointer items-start gap-3 rounded-og-md border border-og-border bg-og-bg px-3 py-2.5"
160
+ >
161
+ <input
162
+ type="checkbox"
163
+ checked={allowScreenControl}
164
+ disabled={busy}
165
+ onChange={(e) => setAllowScreenControl(e.target.checked)}
166
+ className="mt-0.5 size-4 accent-[var(--og-color-accent)]"
167
+ />
168
+ <span className="min-w-0">
169
+ <span className="flex items-center gap-1.5 text-[13px] font-medium text-og-fg">
170
+ <ScreenShareIcon className="size-3.5 text-og-fg-muted" aria-hidden />
171
+ Also let the agent control my mouse & keyboard
172
+ </span>
173
+ <span className="mt-0.5 block text-[12px] text-og-fg-muted">
174
+ Optional. Enables computer-use — the agent can move the pointer, type, and click on this machine's desktop. Leave
175
+ off to let it only watch.
176
+ </span>
177
+ </span>
178
+ </label>
179
+ ) : (
180
+ <p className="flex items-start gap-2 rounded-og-md border border-og-border bg-og-bg px-3 py-2 text-[12px] text-og-fg-muted">
181
+ <CircleAlertIcon className="mt-px size-3.5 shrink-0 text-og-fg-subtle" aria-hidden />
182
+ This machine has no display, so screen control isn't available — files, terminal, and git only.
183
+ </p>
184
+ )}
185
+
186
+ <div className="flex items-center gap-2">
187
+ <button
188
+ type="button"
189
+ data-deny
190
+ disabled={busy}
191
+ onClick={() => onDeny?.()}
192
+ className="flex-1 rounded-og-sm border border-og-border px-3 py-2 text-sm font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg disabled:opacity-50"
193
+ >
194
+ Cancel
195
+ </button>
196
+ <button
197
+ type="button"
198
+ data-approve
199
+ disabled={busy}
200
+ onClick={() => onApprove?.(allowScreenControl)}
201
+ className="flex-1 rounded-og-sm bg-og-status-failed px-3 py-2 text-sm font-semibold text-white transition-colors hover:opacity-90 disabled:opacity-50"
202
+ >
203
+ {busy ? "Connecting…" : "Grant full access"}
204
+ </button>
205
+ </div>
206
+ </div>
207
+ );
208
+ }
209
+
210
+ function ConsentResult({
211
+ tone,
212
+ title,
213
+ body,
214
+ className,
215
+ }: {
216
+ tone: "ok" | "muted" | "danger";
217
+ title: string;
218
+ body: string;
219
+ className?: string | undefined;
220
+ }) {
221
+ const ring =
222
+ tone === "ok"
223
+ ? "border-og-status-running/30"
224
+ : tone === "danger"
225
+ ? "border-og-status-failed/30"
226
+ : "border-og-border";
227
+ const iconClass =
228
+ tone === "ok" ? "text-og-status-running" : tone === "danger" ? "text-og-status-failed" : "text-og-fg-subtle";
229
+ return (
230
+ <div
231
+ data-enrollment-result={tone}
232
+ className={cn(
233
+ "og-root mx-auto flex w-full max-w-md flex-col items-center gap-3 rounded-og-lg border bg-og-surface-1 p-6 text-center shadow-og-md",
234
+ ring,
235
+ className,
236
+ )}
237
+ >
238
+ <span className={cn("flex size-10 items-center justify-center rounded-full bg-og-surface-2", iconClass)}>
239
+ <LaptopIcon className="size-5" aria-hidden />
240
+ </span>
241
+ <h1 className="text-base font-semibold text-og-fg">{title}</h1>
242
+ <p className="max-w-sm text-[13px] text-og-fg-muted">{body}</p>
243
+ </div>
244
+ );
245
+ }
@@ -0,0 +1,182 @@
1
+ import { type ReactNode } from "react";
2
+ import { CopyIcon, ExternalLinkIcon, LaptopIcon, TerminalIcon } from "lucide-react";
3
+ import { cn } from "../lib/cn";
4
+ import type { MachineState } from "../types/machines";
5
+ import { ConnectionStatusPill } from "./machine-status-pill";
6
+
7
+ /** The poll state of an in-flight device-flow enrollment. */
8
+ export type DeviceFlowPhase = "pending" | "authorized" | "denied" | "expired" | "disabled";
9
+
10
+ export type EnrollmentDeviceFlowProps = {
11
+ /** The short user code the agent printed (shown big for the human to confirm). */
12
+ userCode: string;
13
+ /** Where the user approves — the same-origin device page. */
14
+ verificationUri: string;
15
+ /** The complete URI (code pre-filled) for a one-click / QR open. */
16
+ verificationUriComplete?: string | undefined;
17
+ /** The install one-liner the user ran (echoed for context). Optional. */
18
+ installCommand?: string | undefined;
19
+ phase?: DeviceFlowPhase | undefined;
20
+ /** Seconds remaining before the code expires (drives a quiet countdown). */
21
+ expiresInSeconds?: number | undefined;
22
+ onCopyCode?: (() => void) | undefined;
23
+ onCopyInstall?: (() => void) | undefined;
24
+ onOpenVerification?: (() => void) | undefined;
25
+ className?: string | undefined;
26
+ };
27
+
28
+ const PHASE_TO_STATE: Record<DeviceFlowPhase, MachineState> = {
29
+ pending: "enrolling",
30
+ authorized: "online",
31
+ denied: "offline",
32
+ expired: "offline",
33
+ disabled: "offline",
34
+ };
35
+
36
+ function PhaseLabel({ phase }: { phase: DeviceFlowPhase }): ReactNode {
37
+ const text: Record<DeviceFlowPhase, string> = {
38
+ pending: "Waiting for approval",
39
+ authorized: "Connected",
40
+ denied: "Denied",
41
+ expired: "Code expired",
42
+ disabled: "Enrollment disabled",
43
+ };
44
+ return <span>{text[phase]}</span>;
45
+ }
46
+
47
+ /**
48
+ * The IN-SESSION device-flow panel: the agent (after the install one-liner) prints
49
+ * a short `userCode` + a `verificationUri`; this surfaces them so the user can open
50
+ * the approve page and confirm the code. Polls in the caller; this renders the
51
+ * pending → authorized/denied/expired progression with a connection pill. This is
52
+ * the FIRST step of the IA flow (device-flow → Machines list → attach/swap).
53
+ */
54
+ export function EnrollmentDeviceFlow({
55
+ userCode,
56
+ verificationUri,
57
+ verificationUriComplete,
58
+ installCommand,
59
+ phase = "pending",
60
+ expiresInSeconds,
61
+ onCopyCode,
62
+ onCopyInstall,
63
+ onOpenVerification,
64
+ className,
65
+ }: EnrollmentDeviceFlowProps) {
66
+ const href = verificationUriComplete ?? verificationUri;
67
+ // Before the one-liner runs the caller passes a placeholder (dashes) for the
68
+ // code — surface that as an explicit waiting state rather than dashes that look
69
+ // like a real code to type.
70
+ const codeIsPlaceholder = !/[A-Za-z0-9]/.test(userCode);
71
+ const awaitingCode = phase === "pending" && codeIsPlaceholder;
72
+ return (
73
+ <div
74
+ data-enrollment-device-flow
75
+ className={cn(
76
+ "og-root flex w-full max-w-md flex-col gap-4 rounded-og-lg border border-og-border bg-og-surface-1 p-5 shadow-og-sm",
77
+ className,
78
+ )}
79
+ >
80
+ <div className="flex items-center justify-between gap-3">
81
+ <div className="flex items-center gap-2">
82
+ <span className="flex size-7 items-center justify-center rounded-full bg-og-surface-2 text-og-fg-muted">
83
+ <LaptopIcon className="size-4" aria-hidden />
84
+ </span>
85
+ <h2 className="text-sm font-semibold text-og-fg">Connect a machine</h2>
86
+ </div>
87
+ <ConnectionStatusPill
88
+ status={
89
+ PHASE_TO_STATE[phase] === "online"
90
+ ? "online"
91
+ : PHASE_TO_STATE[phase] === "offline"
92
+ ? "offline"
93
+ : "reconnecting"
94
+ }
95
+ label={(<PhaseLabel phase={phase} />) as unknown as string}
96
+ size="sm"
97
+ />
98
+ </div>
99
+
100
+ {installCommand ? (
101
+ <div className="flex flex-col gap-1.5">
102
+ <span className="text-[11px] font-medium uppercase tracking-wide text-og-fg-subtle">1 · Run on the machine</span>
103
+ <div className="flex items-center justify-between gap-2 overflow-hidden rounded-og-md border border-og-border bg-og-bg px-2.5 py-1.5">
104
+ <code className="flex min-w-0 items-center gap-2 overflow-x-auto font-og-mono text-[12px] text-og-fg">
105
+ <TerminalIcon className="size-3.5 shrink-0 text-og-fg-subtle" aria-hidden />
106
+ {installCommand}
107
+ </code>
108
+ <button
109
+ type="button"
110
+ data-copy-install
111
+ onClick={onCopyInstall}
112
+ title="Copy command"
113
+ className="shrink-0 rounded-og-sm p-1.5 text-og-fg-subtle transition-colors hover:bg-og-surface-2 hover:text-og-fg"
114
+ >
115
+ <CopyIcon className="size-4" aria-hidden />
116
+ </button>
117
+ </div>
118
+ </div>
119
+ ) : null}
120
+
121
+ <div className="flex flex-col gap-1.5">
122
+ <span className="text-[11px] font-medium uppercase tracking-wide text-og-fg-subtle">
123
+ {installCommand ? "2 · Approve the machine" : "Approve the machine"}
124
+ </span>
125
+ <div className="flex items-center justify-between gap-3 rounded-og-md border border-og-border bg-og-bg px-3 py-2.5">
126
+ {awaitingCode ? (
127
+ <span data-user-code className="font-og-mono text-[13px] text-og-fg-subtle">
128
+ Run the command above — your code appears here
129
+ </span>
130
+ ) : (
131
+ <span
132
+ data-user-code
133
+ className="select-all font-og-mono text-2xl font-semibold tracking-[0.25em] text-og-fg"
134
+ >
135
+ {userCode}
136
+ </span>
137
+ )}
138
+ <button
139
+ type="button"
140
+ data-copy-code
141
+ onClick={onCopyCode}
142
+ disabled={awaitingCode}
143
+ title="Copy code"
144
+ className="shrink-0 rounded-og-sm p-1.5 text-og-fg-subtle transition-colors hover:bg-og-surface-2 hover:text-og-fg disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-og-fg-subtle"
145
+ >
146
+ <CopyIcon className="size-4" aria-hidden />
147
+ </button>
148
+ </div>
149
+ </div>
150
+
151
+ <a
152
+ href={href}
153
+ data-open-verification
154
+ target="_blank"
155
+ rel="noreferrer"
156
+ onClick={onOpenVerification}
157
+ className="inline-flex items-center justify-center gap-1.5 rounded-og-sm bg-og-accent px-3 py-2 text-sm font-medium text-og-accent-fg transition-colors hover:bg-og-accent-strong"
158
+ >
159
+ Open approval page
160
+ <ExternalLinkIcon className="size-3.5" aria-hidden />
161
+ </a>
162
+
163
+ <p className="text-center text-[11px] text-og-fg-subtle">
164
+ {awaitingCode ? (
165
+ <>
166
+ The one-liner prints a short code. Open{" "}
167
+ <span className="font-og-mono text-og-fg-muted">{verificationUri}</span> and confirm the code shown there
168
+ matches.
169
+ </>
170
+ ) : (
171
+ <>
172
+ Confirm code <span className="font-og-mono text-og-fg-muted">{userCode}</span> — the one printed by the
173
+ one-liner — at <span className="font-og-mono text-og-fg-muted">{verificationUri}</span>.
174
+ {typeof expiresInSeconds === "number" ? (
175
+ <> Expires in {Math.max(0, Math.round(expiresInSeconds / 60))} min.</>
176
+ ) : null}
177
+ </>
178
+ )}
179
+ </p>
180
+ </div>
181
+ );
182
+ }
@@ -0,0 +1,131 @@
1
+ import {
2
+ CpuIcon,
3
+ LaptopIcon,
4
+ MonitorIcon,
5
+ MonitorOffIcon,
6
+ ServerIcon,
7
+ UsersIcon,
8
+ } from "lucide-react";
9
+ import { cn } from "../lib/cn";
10
+ import { formatRelativeTime } from "../lib/format";
11
+ import type { MachineView } from "../types/machines";
12
+ import { MachineMetrics } from "./machine-metrics";
13
+ import { MachineStatusPill } from "./machine-status-pill";
14
+
15
+ export type MachineCardProps = {
16
+ machine: MachineView;
17
+ /** Attach/swap the session's active sandbox to this machine. */
18
+ onAttach?: ((machine: MachineView) => void) | undefined;
19
+ /** Whether an attach/swap is currently in flight (disables the affordance). */
20
+ attaching?: boolean | undefined;
21
+ className?: string | undefined;
22
+ };
23
+
24
+ function KindIcon({ machine }: { machine: MachineView }) {
25
+ if (machine.isSessionGroup) return <ServerIcon className="size-4 text-og-fg-subtle" aria-hidden />;
26
+ if (machine.kind === "modal") return <CpuIcon className="size-4 text-og-fg-subtle" aria-hidden />;
27
+ return <LaptopIcon className="size-4 text-og-fg-subtle" aria-hidden />;
28
+ }
29
+
30
+ /**
31
+ * One machine in the Machines dashboard: name + kind + OS/arch, the composite
32
+ * connection/state/shared status, latest resource meters, last-seen recency, and
33
+ * an attach/swap affordance. The session's currently-active sandbox carries an
34
+ * accent edge + an "Active" marker (never an attach button — it's already active).
35
+ */
36
+ export function MachineCard({ machine, onAttach, attaching, className }: MachineCardProps) {
37
+ const offline = machine.state === "offline";
38
+ const attachable = !machine.active && !offline && Boolean(onAttach);
39
+ const shared = machine.sharedSessionCount > 1;
40
+
41
+ return (
42
+ <div
43
+ data-machine-card={machine.sandboxId}
44
+ data-active={machine.active ? "true" : "false"}
45
+ className={cn(
46
+ "og-root relative flex flex-col gap-3 overflow-hidden rounded-og-lg border border-og-border",
47
+ "bg-og-surface-1 p-4 shadow-og-sm",
48
+ machine.active && "border-og-accent/40",
49
+ className,
50
+ )}
51
+ >
52
+ {machine.active ? (
53
+ <span aria-hidden className="absolute inset-y-0 left-0 w-0.5 bg-og-accent" />
54
+ ) : null}
55
+
56
+ <div className="flex items-start justify-between gap-3">
57
+ <div className="flex min-w-0 items-start gap-2.5">
58
+ <span className="mt-0.5 shrink-0">
59
+ <KindIcon machine={machine} />
60
+ </span>
61
+ <div className="min-w-0">
62
+ <div className="flex items-center gap-2">
63
+ <span className="truncate text-sm font-medium text-og-fg">{machine.name}</span>
64
+ {machine.active ? (
65
+ <span
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-[10px] font-medium text-og-accent"
68
+ >
69
+ Active
70
+ </span>
71
+ ) : null}
72
+ </div>
73
+ <div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[11px] text-og-fg-subtle">
74
+ <span className="capitalize">{machine.isSessionGroup ? "session sandbox" : machine.kind}</span>
75
+ <span aria-hidden>·</span>
76
+ <span className="font-og-mono">
77
+ {machine.os}/{machine.arch}
78
+ </span>
79
+ <span aria-hidden>·</span>
80
+ <span className="inline-flex items-center gap-1">
81
+ {machine.hasDisplay ? (
82
+ <MonitorIcon className="size-3" aria-hidden />
83
+ ) : (
84
+ <MonitorOffIcon className="size-3" aria-hidden />
85
+ )}
86
+ {machine.hasDisplay ? "display" : "headless"}
87
+ </span>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ <MachineStatusPill state={machine.state} sharedSessionCount={machine.sharedSessionCount} size="sm" className="shrink-0" />
92
+ </div>
93
+
94
+ {shared ? (
95
+ <p
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-[11px] text-og-fg-muted"
98
+ >
99
+ <UsersIcon className="size-3.5 shrink-0 text-og-accent" aria-hidden />
100
+ Shared — {machine.sharedSessionCount} sessions are on this machine.
101
+ </p>
102
+ ) : null}
103
+
104
+ <MachineMetrics metrics={machine.metrics} />
105
+
106
+ <div className="mt-1 flex items-center justify-between gap-3 text-[11px] text-og-fg-subtle">
107
+ <span>
108
+ {machine.lastSeenAt ? <>Last seen {formatRelativeTime(machine.lastSeenAt)}</> : "Never connected"}
109
+ </span>
110
+ {machine.active ? (
111
+ <span className="text-og-accent">Routing here</span>
112
+ ) : attachable ? (
113
+ <button
114
+ type="button"
115
+ data-attach
116
+ disabled={attaching}
117
+ onClick={() => onAttach?.(machine)}
118
+ className={cn(
119
+ "rounded-og-sm border border-og-border px-2.5 py-1 text-[11px] font-medium text-og-fg-muted transition-colors",
120
+ "hover:border-og-border-strong hover:text-og-fg disabled:cursor-not-allowed disabled:opacity-50",
121
+ )}
122
+ >
123
+ {attaching ? "Switching…" : "Attach"}
124
+ </button>
125
+ ) : (
126
+ <span className="text-og-fg-subtle/70">{offline ? "Unavailable" : "—"}</span>
127
+ )}
128
+ </div>
129
+ </div>
130
+ );
131
+ }
@@ -0,0 +1,84 @@
1
+ import { LaptopIcon, CpuIcon, UsersIcon } from "lucide-react";
2
+ import { cn } from "../lib/cn";
3
+ import type { MachineKind, MachineState } from "../types/machines";
4
+ import { ConnectionStatusPill, MACHINE_STATE_BADGE_META } from "./machine-status-pill";
5
+ import { connectionStatusForState } from "../types/machines";
6
+
7
+ export type MachineDockBarProps = {
8
+ /** The active machine's display name. */
9
+ name: string;
10
+ kind: MachineKind;
11
+ state: MachineState;
12
+ /** Live sessions sharing this whole-machine lease (>1 ⇒ shared disclosure). */
13
+ sharedSessionCount?: number | undefined;
14
+ className?: string | undefined;
15
+ };
16
+
17
+ /**
18
+ * A slim bar that surfaces WHICH machine the dock surfaces (Files/Terminal/
19
+ * Desktop) are bound to + its connection status. Mounted above the dock tabs so
20
+ * the user always knows whether they are looking at the Modal box or their own
21
+ * machine, and whether it is online / reconnecting / offline. The surfaces below
22
+ * render IDENTICALLY regardless of backend — this bar is the only backend-aware
23
+ * chrome (the dock-parity contract: selfhosted == Modal for the surfaces).
24
+ */
25
+ export function MachineDockBar({ name, kind, state, sharedSessionCount, className }: MachineDockBarProps) {
26
+ const Icon = kind === "selfhosted" ? LaptopIcon : CpuIcon;
27
+ const stateBadge = MACHINE_STATE_BADGE_META[state];
28
+ const shared = (sharedSessionCount ?? 0) > 1;
29
+ return (
30
+ <div
31
+ data-machine-dock-bar
32
+ className={cn(
33
+ "flex shrink-0 items-center justify-between gap-2 border-b border-og-border bg-og-surface-1 px-2.5 py-1",
34
+ className,
35
+ )}
36
+ >
37
+ <div className="flex min-w-0 items-center gap-1.5">
38
+ <Icon className="size-3.5 shrink-0 text-og-fg-subtle" aria-hidden />
39
+ <span className="truncate text-[11px] font-medium text-og-fg">{name}</span>
40
+ {stateBadge ? (
41
+ <span
42
+ data-state-badge={state}
43
+ className={cn("shrink-0 rounded-full border px-1.5 py-px text-[10px] font-medium", stateBadge.badgeClassName)}
44
+ >
45
+ {stateBadge.label}
46
+ </span>
47
+ ) : null}
48
+ </div>
49
+ <ConnectionStatusPill status={connectionStatusForState(state)} size="sm" className="shrink-0" />
50
+ </div>
51
+ );
52
+ }
53
+
54
+ export type SharedMachineDisclosureProps = {
55
+ sharedSessionCount: number;
56
+ /** Compact (inline strip) vs full (a notice block). */
57
+ density?: "compact" | "full" | undefined;
58
+ className?: string | undefined;
59
+ };
60
+
61
+ /**
62
+ * The "shared — another session is on this machine" disclosure. Surfaced in the
63
+ * dock (and reused on the desktop take-control gate) so the user knows others are
64
+ * driving the same whole-machine lease. Render when `sharedSessionCount > 1`.
65
+ */
66
+ export function SharedMachineDisclosure({ sharedSessionCount, density = "compact", className }: SharedMachineDisclosureProps) {
67
+ const others = Math.max(0, sharedSessionCount - 1);
68
+ return (
69
+ <div
70
+ data-shared-disclosure
71
+ className={cn(
72
+ "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]",
74
+ className,
75
+ )}
76
+ >
77
+ <UsersIcon className="size-3.5 shrink-0 text-og-accent" aria-hidden />
78
+ <span>
79
+ Shared — {others === 1 ? "another session is" : `${others} other sessions are`} on this machine. They see the same
80
+ terminal, files, and desktop.
81
+ </span>
82
+ </div>
83
+ );
84
+ }