@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.
- package/README.md +109 -2
- package/dist/chunk-DEW2ZNF2.js +1238 -0
- package/dist/chunk-DEW2ZNF2.js.map +1 -0
- package/dist/index.d.ts +173 -50
- package/dist/index.js +1422 -982
- package/dist/index.js.map +1 -1
- package/dist/machines-BD6h9P_s.d.ts +329 -0
- package/dist/machines.d.ts +4 -0
- package/dist/machines.js +33 -0
- package/dist/machines.js.map +1 -0
- package/package.json +8 -2
- package/src/components/desktop-viewer.tsx +11 -1
- package/src/components/enrollment-consent.tsx +245 -0
- package/src/components/enrollment-device-flow.tsx +182 -0
- package/src/components/machine-card.tsx +131 -0
- package/src/components/machine-dock-bar.tsx +84 -0
- package/src/components/machine-metrics.tsx +184 -0
- package/src/components/machine-status-pill.tsx +157 -0
- package/src/components/machines-dashboard.tsx +151 -0
- package/src/components/message-timeline.tsx +106 -8
- package/src/components/workspace-dock.tsx +44 -10
- package/src/hooks/use-codex-accounts.ts +179 -0
- package/src/hooks/use-desktop-stream.ts +28 -2
- package/src/hooks/use-goal.ts +10 -2
- package/src/hooks/use-machines.ts +157 -0
- package/src/hooks/use-relay-frame-stream.ts +335 -0
- package/src/index.ts +15 -0
- package/src/lib/relay-wire.ts +116 -0
- package/src/machines.ts +50 -0
- package/src/timeline/activity-rail.tsx +13 -7
- package/src/timeline/index.ts +1 -0
- package/src/timeline/projection.ts +214 -16
- package/src/timeline/turn-summary.tsx +32 -10
- package/src/timeline/types.ts +29 -3
- package/src/types/machines.ts +67 -0
|
@@ -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
|
+
}
|