@saadnvd1/agent-os 0.3.698 → 0.3.700
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/app/api/bus/send/route.ts +11 -1
- package/app/api/bus/wait/route.ts +22 -0
- package/app/api/peer/hello/reveal/route.ts +6 -0
- package/app/api/peer/hello/route.ts +9 -0
- package/app/api/peer/hello/status/route.ts +6 -0
- package/app/api/peers/account/route.ts +9 -0
- package/app/api/peers/gate.test.ts +111 -0
- package/app/api/peers/link/[id]/confirm/route.ts +27 -0
- package/app/api/peers/link/[id]/route.ts +20 -0
- package/app/api/peers/link/route.ts +23 -0
- package/app/api/peers/pending/[id]/accept/route.ts +31 -0
- package/app/api/peers/pending/[id]/decline/route.ts +13 -0
- package/app/api/peers/pending/route.ts +7 -0
- package/app/api/sessions/route.ts +18 -1
- package/app/page.tsx +2 -0
- package/components/Hosts/AccountMachines.tsx +180 -0
- package/components/Hosts/HostRow.tsx +44 -35
- package/components/Hosts/HostsDialog.tsx +49 -37
- package/components/Hosts/IncomingLinkSheet.tsx +228 -0
- package/components/Hosts/LinkCodeSheet.tsx +186 -0
- package/components/Hosts/sheet.ts +3 -0
- package/components/SessionList/RowSubtitle.tsx +11 -1
- package/components/Welcome/ConnectBlock.tsx +1 -1
- package/data/peers/index.ts +2 -0
- package/data/peers/keys.ts +6 -0
- package/data/peers/queries.ts +141 -0
- package/lib/agents/brief.ts +3 -1
- package/lib/bus/format.ts +25 -4
- package/lib/bus/index.ts +113 -106
- package/lib/bus/resolve.ts +16 -2
- package/lib/bus/targets.ts +122 -0
- package/lib/bus/waits/kick.ts +12 -0
- package/lib/bus/waits/lines.ts +69 -0
- package/lib/bus/waits/register.ts +83 -0
- package/lib/bus/waits/store.ts +196 -0
- package/lib/bus/waits/tick.ts +192 -0
- package/lib/connect/control-api.ts +108 -2
- package/lib/connect/heartbeat.ts +11 -3
- package/lib/connect/serve.ts +2 -0
- package/lib/db/migrations/20261020T1600_bus_waits.ts +35 -0
- package/lib/db/types.ts +4 -0
- package/lib/done/archive.ts +2 -0
- package/lib/orchestrator/describe.ts +13 -0
- package/lib/orchestrator/facts.ts +17 -0
- package/lib/peer/account.ts +198 -0
- package/lib/peer/cli.ts +24 -0
- package/lib/peer/code.ts +3 -0
- package/lib/peer/hello-route.ts +32 -0
- package/lib/peer/hello.ts +346 -0
- package/lib/peer/link-cli.ts +233 -0
- package/lib/peer/machines.ts +72 -0
- package/lib/peer/notify.ts +32 -0
- package/lib/peer/pairing-out.ts +382 -0
- package/lib/peer/pairing.ts +403 -0
- package/lib/peer/pin.ts +55 -0
- package/lib/peer/rpc-intake.ts +37 -8
- package/lib/peer/rpc-server.ts +19 -1
- package/lib/schedules/when.ts +1 -1
- package/lib/security/auth.ts +10 -2
- package/lib/security/gate.ts +3 -0
- package/lib/sessions/end.ts +3 -0
- package/package.json +1 -1
- package/scripts/agent-os +2 -0
- package/scripts/lib/commands.sh +2 -0
- package/server.ts +3 -0
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { Button } from "@/components/ui/button";
|
|
12
12
|
import { Input } from "@/components/ui/input";
|
|
13
13
|
import { useHostsQuery, useCreateHost } from "@/data/hosts";
|
|
14
|
+
import { AccountMachines } from "./AccountMachines";
|
|
14
15
|
import { HostRow } from "./HostRow";
|
|
15
16
|
|
|
16
17
|
interface HostsDialogProps {
|
|
@@ -39,17 +40,19 @@ export function HostsDialog({ open, onClose }: HostsDialogProps) {
|
|
|
39
40
|
|
|
40
41
|
return (
|
|
41
42
|
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
|
42
|
-
<DialogContent className="max-h-[90vh] max-w-lg overflow-y-auto">
|
|
43
|
+
<DialogContent className="max-h-[90vh] max-w-lg overflow-y-auto [&>*]:min-w-0">
|
|
43
44
|
<DialogHeader>
|
|
44
45
|
<DialogTitle>Machines</DialogTitle>
|
|
45
46
|
<DialogDescription>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
under the project whose folder they are in.
|
|
47
|
+
Link your other machines to start tasks on them and move tasks
|
|
48
|
+
between them. Through AgentOS Connect it works over any network.
|
|
49
49
|
</DialogDescription>
|
|
50
50
|
</DialogHeader>
|
|
51
51
|
|
|
52
|
+
<AccountMachines />
|
|
53
|
+
|
|
52
54
|
<div className="space-y-2">
|
|
55
|
+
<p className="text-sm font-medium">Machines here</p>
|
|
53
56
|
{isPending && (
|
|
54
57
|
<div className="bg-muted/40 h-14 animate-pulse rounded-lg" />
|
|
55
58
|
)}
|
|
@@ -58,39 +61,48 @@ export function HostsDialog({ open, onClose }: HostsDialogProps) {
|
|
|
58
61
|
))}
|
|
59
62
|
</div>
|
|
60
63
|
|
|
61
|
-
<
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
64
|
+
<details className="group">
|
|
65
|
+
<summary className="text-muted-foreground flex min-h-11 cursor-pointer items-center text-sm select-none sm:min-h-9">
|
|
66
|
+
Other ways to link (Tailscale or ssh)
|
|
67
|
+
</summary>
|
|
68
|
+
<p className="text-muted-foreground pb-2 text-xs">
|
|
69
|
+
Sessions run in tmux on any machine you can reach with ssh keys.
|
|
70
|
+
Projects pick a machine; tmux sessions already running there show up
|
|
71
|
+
under the project whose folder they are in.
|
|
72
|
+
</p>
|
|
73
|
+
<form onSubmit={handleAdd} className="space-y-2">
|
|
74
|
+
<div className="flex flex-col gap-2 sm:flex-row">
|
|
75
|
+
<Input
|
|
76
|
+
placeholder="Name (e.g. devbox)"
|
|
77
|
+
value={name}
|
|
78
|
+
onChange={(e) => setName(e.target.value)}
|
|
79
|
+
className="h-11 sm:h-9"
|
|
80
|
+
/>
|
|
81
|
+
<Input
|
|
82
|
+
placeholder="ssh target (user@host or alias)"
|
|
83
|
+
value={sshTarget}
|
|
84
|
+
onChange={(e) => setSshTarget(e.target.value)}
|
|
85
|
+
autoCapitalize="none"
|
|
86
|
+
autoCorrect="off"
|
|
87
|
+
spellCheck={false}
|
|
88
|
+
className="h-11 sm:h-9"
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
{createHost.error && (
|
|
92
|
+
<p className="text-sm text-red-400">{createHost.error.message}</p>
|
|
93
|
+
)}
|
|
94
|
+
<div className="flex justify-end">
|
|
95
|
+
<Button
|
|
96
|
+
type="submit"
|
|
97
|
+
disabled={
|
|
98
|
+
!name.trim() || !sshTarget.trim() || createHost.isPending
|
|
99
|
+
}
|
|
100
|
+
>
|
|
101
|
+
Add machine
|
|
102
|
+
</Button>
|
|
103
|
+
</div>
|
|
104
|
+
</form>
|
|
105
|
+
</details>
|
|
94
106
|
</DialogContent>
|
|
95
107
|
</Dialog>
|
|
96
108
|
);
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { CheckCircle2, ShieldAlert, X } from "lucide-react";
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
} from "@/components/ui/dialog";
|
|
12
|
+
import { Button } from "@/components/ui/button";
|
|
13
|
+
import { Input } from "@/components/ui/input";
|
|
14
|
+
import { useAnswerLink, usePendingLinks } from "@/data/peers";
|
|
15
|
+
import { showCode } from "@/lib/peer/code";
|
|
16
|
+
import type { PendingView } from "@/lib/peer/pairing";
|
|
17
|
+
import { relativeTime } from "@/lib/schedules/when";
|
|
18
|
+
import { SHEET } from "./sheet";
|
|
19
|
+
|
|
20
|
+
/** Its Connect id and when it was last online, next to the account's name. */
|
|
21
|
+
function Origin({ request }: { request: PendingView }) {
|
|
22
|
+
const seen = request.lastSeenAt ? Date.parse(request.lastSeenAt) : NaN;
|
|
23
|
+
return (
|
|
24
|
+
<span className="text-muted-foreground font-mono text-xs">
|
|
25
|
+
{request.machineId.slice(0, 8)}
|
|
26
|
+
{Number.isFinite(seen) && ` · seen ${relativeTime(seen)}`}
|
|
27
|
+
</span>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** On the machine being asked: type the code the other one shows. */
|
|
32
|
+
export function IncomingLinkSheet({
|
|
33
|
+
request,
|
|
34
|
+
onClose,
|
|
35
|
+
}: {
|
|
36
|
+
request: PendingView | null;
|
|
37
|
+
onClose: () => void;
|
|
38
|
+
}) {
|
|
39
|
+
const [code, setCode] = useState("");
|
|
40
|
+
const { accept, decline } = useAnswerLink();
|
|
41
|
+
const digits = code.replace(/\D/g, "");
|
|
42
|
+
const busy = accept.isPending || decline.isPending;
|
|
43
|
+
const close = () => {
|
|
44
|
+
setCode("");
|
|
45
|
+
accept.reset();
|
|
46
|
+
onClose();
|
|
47
|
+
};
|
|
48
|
+
const left = (accept.error as { attemptsLeft?: number } | null)?.attemptsLeft;
|
|
49
|
+
const done = accept.data;
|
|
50
|
+
|
|
51
|
+
if (done)
|
|
52
|
+
return (
|
|
53
|
+
<Dialog open={!!request} onOpenChange={(o) => !o && close()}>
|
|
54
|
+
<DialogContent className={SHEET}>
|
|
55
|
+
<DialogHeader>
|
|
56
|
+
<DialogTitle>Finish on {done.name}</DialogTitle>
|
|
57
|
+
<DialogDescription>
|
|
58
|
+
{done.name} is linked here. Type this code on {done.name} so it
|
|
59
|
+
links back to this machine.
|
|
60
|
+
</DialogDescription>
|
|
61
|
+
</DialogHeader>
|
|
62
|
+
<p
|
|
63
|
+
className="text-center font-mono text-4xl font-semibold tracking-[0.2em] tabular-nums sm:text-5xl"
|
|
64
|
+
aria-label={`Code ${done.confirmCode.split("").join(" ")}`}
|
|
65
|
+
data-testid="confirm-code"
|
|
66
|
+
>
|
|
67
|
+
{showCode(done.confirmCode)}
|
|
68
|
+
</p>
|
|
69
|
+
<p className="flex items-center justify-center gap-2 text-sm">
|
|
70
|
+
<CheckCircle2 className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
|
|
71
|
+
Code matched
|
|
72
|
+
</p>
|
|
73
|
+
<div className="flex justify-end">
|
|
74
|
+
<Button className="min-h-11 sm:min-h-9" onClick={close}>
|
|
75
|
+
Done
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
</DialogContent>
|
|
79
|
+
</Dialog>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Dialog open={!!request} onOpenChange={(o) => !o && close()}>
|
|
84
|
+
<DialogContent className={SHEET}>
|
|
85
|
+
<DialogHeader>
|
|
86
|
+
<DialogTitle>Link {request?.name}?</DialogTitle>
|
|
87
|
+
<DialogDescription>
|
|
88
|
+
{request?.name} asked to link with this machine. Type the code it
|
|
89
|
+
shows to allow it.
|
|
90
|
+
</DialogDescription>
|
|
91
|
+
{request && <Origin request={request} />}
|
|
92
|
+
</DialogHeader>
|
|
93
|
+
<div className="flex gap-2 rounded-lg border border-amber-500/40 bg-amber-500/10 p-3 text-sm">
|
|
94
|
+
<ShieldAlert className="mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
95
|
+
<p>
|
|
96
|
+
Linked machines can start agents and run code on this machine. Only
|
|
97
|
+
link one you own.
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
<form
|
|
101
|
+
className="flex flex-col gap-2"
|
|
102
|
+
onSubmit={(e) => {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
if (request && digits.length === 6)
|
|
105
|
+
accept.mutate({ id: request.id, code: digits });
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<Input
|
|
109
|
+
value={code}
|
|
110
|
+
onChange={(e) => setCode(e.target.value.slice(0, 7))}
|
|
111
|
+
inputMode="numeric"
|
|
112
|
+
autoComplete="one-time-code"
|
|
113
|
+
placeholder="6-digit code"
|
|
114
|
+
aria-label="Code shown on the other machine"
|
|
115
|
+
className="h-12 text-center font-mono text-2xl tracking-[0.3em]"
|
|
116
|
+
autoFocus
|
|
117
|
+
/>
|
|
118
|
+
{accept.error && (
|
|
119
|
+
<p className="text-destructive text-sm">
|
|
120
|
+
{accept.error.message}
|
|
121
|
+
{left ? ` ${left} ${left === 1 ? "try" : "tries"} left.` : ""}
|
|
122
|
+
</p>
|
|
123
|
+
)}
|
|
124
|
+
<div className="flex justify-end gap-2">
|
|
125
|
+
<Button
|
|
126
|
+
type="button"
|
|
127
|
+
variant="ghost"
|
|
128
|
+
className="min-h-11 sm:min-h-9"
|
|
129
|
+
disabled={busy}
|
|
130
|
+
onClick={() =>
|
|
131
|
+
request && decline.mutate(request.id, { onSuccess: close })
|
|
132
|
+
}
|
|
133
|
+
>
|
|
134
|
+
Decline
|
|
135
|
+
</Button>
|
|
136
|
+
<Button
|
|
137
|
+
type="submit"
|
|
138
|
+
className="min-h-11 sm:min-h-9"
|
|
139
|
+
disabled={busy || digits.length !== 6 || left === 0}
|
|
140
|
+
>
|
|
141
|
+
Link
|
|
142
|
+
</Button>
|
|
143
|
+
</div>
|
|
144
|
+
</form>
|
|
145
|
+
</DialogContent>
|
|
146
|
+
</Dialog>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A banner on every page while a machine is waiting for its code here. */
|
|
151
|
+
export function IncomingLinks() {
|
|
152
|
+
const { data: requests = [] } = usePendingLinks();
|
|
153
|
+
// Held while open, so a request voided by a wrong code still says why.
|
|
154
|
+
const [open, setOpen] = useState<PendingView | null>(null);
|
|
155
|
+
// Hidden until a request not seen yet arrives; it covers the header on phones.
|
|
156
|
+
const [later, setLater] = useState<string | null>(null);
|
|
157
|
+
const first = requests[0];
|
|
158
|
+
const hidden = later !== null && requests.every((r) => later.includes(r.id));
|
|
159
|
+
return (
|
|
160
|
+
<>
|
|
161
|
+
{first && !open && !hidden && (
|
|
162
|
+
<div
|
|
163
|
+
role="status"
|
|
164
|
+
className="popover-surface fixed top-[max(0.75rem,env(safe-area-inset-top))] left-1/2 z-50 flex w-[min(28rem,calc(100%-1.5rem))] -translate-x-1/2 items-center gap-3 rounded-xl px-3 py-2 shadow-lg"
|
|
165
|
+
>
|
|
166
|
+
<ShieldAlert className="h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
167
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
168
|
+
<p className="truncate text-sm">
|
|
169
|
+
<span className="font-medium">{first.name}</span> wants to link
|
|
170
|
+
{requests.length > 1 ? ` (+${requests.length - 1} more)` : ""}
|
|
171
|
+
</p>
|
|
172
|
+
<Origin request={first} />
|
|
173
|
+
</div>
|
|
174
|
+
<Button
|
|
175
|
+
size="sm"
|
|
176
|
+
className="min-h-11 sm:min-h-8"
|
|
177
|
+
onClick={() => setOpen(first)}
|
|
178
|
+
>
|
|
179
|
+
Review
|
|
180
|
+
</Button>
|
|
181
|
+
<Button
|
|
182
|
+
size="icon"
|
|
183
|
+
variant="ghost"
|
|
184
|
+
aria-label="Later"
|
|
185
|
+
className="h-11 w-11 shrink-0 sm:h-8 sm:w-8"
|
|
186
|
+
onClick={() => setLater(requests.map((r) => r.id).join(" "))}
|
|
187
|
+
>
|
|
188
|
+
<X className="h-4 w-4" />
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
<IncomingLinkSheet request={open} onClose={() => setOpen(null)} />
|
|
193
|
+
</>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** In Machines: the requests waiting here, for after the banner was put off. */
|
|
198
|
+
export function WaitingLinks() {
|
|
199
|
+
const { data: requests = [] } = usePendingLinks();
|
|
200
|
+
const [open, setOpen] = useState<PendingView | null>(null);
|
|
201
|
+
if (requests.length === 0 && !open) return null;
|
|
202
|
+
return (
|
|
203
|
+
<>
|
|
204
|
+
{requests.map((r) => (
|
|
205
|
+
<div
|
|
206
|
+
key={r.id}
|
|
207
|
+
className="flex items-center gap-3 rounded-lg border border-amber-500/40 bg-amber-500/10 px-3 py-2"
|
|
208
|
+
>
|
|
209
|
+
<ShieldAlert className="h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
210
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
211
|
+
<p className="truncate text-sm">
|
|
212
|
+
<span className="font-medium">{r.name}</span> wants to link
|
|
213
|
+
</p>
|
|
214
|
+
<Origin request={r} />
|
|
215
|
+
</div>
|
|
216
|
+
<Button
|
|
217
|
+
size="sm"
|
|
218
|
+
className="min-h-11 sm:min-h-8"
|
|
219
|
+
onClick={() => setOpen(r)}
|
|
220
|
+
>
|
|
221
|
+
Type code
|
|
222
|
+
</Button>
|
|
223
|
+
</div>
|
|
224
|
+
))}
|
|
225
|
+
<IncomingLinkSheet request={open} onClose={() => setOpen(null)} />
|
|
226
|
+
</>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { CheckCircle2, Loader2, XCircle } from "lucide-react";
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
} from "@/components/ui/dialog";
|
|
12
|
+
import { Button } from "@/components/ui/button";
|
|
13
|
+
import { Input } from "@/components/ui/input";
|
|
14
|
+
import { useCancelLink, useConfirmLink, useOutgoingLink } from "@/data/peers";
|
|
15
|
+
import { showCode } from "@/lib/peer/code";
|
|
16
|
+
import { SHEET } from "./sheet";
|
|
17
|
+
|
|
18
|
+
const ENDED: Record<string, string> = {
|
|
19
|
+
declined: "It was declined there, or the code was typed wrong 3 times.",
|
|
20
|
+
expired: "The request expired before the code was typed.",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Once the other machine took our code: type the one it shows now. */
|
|
24
|
+
function ConfirmForm({
|
|
25
|
+
id,
|
|
26
|
+
name,
|
|
27
|
+
onCancel,
|
|
28
|
+
}: {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
onCancel: () => void;
|
|
32
|
+
}) {
|
|
33
|
+
const [code, setCode] = useState("");
|
|
34
|
+
const confirm = useConfirmLink();
|
|
35
|
+
const digits = code.replace(/\D/g, "");
|
|
36
|
+
const left = (confirm.error as { attemptsLeft?: number } | null)
|
|
37
|
+
?.attemptsLeft;
|
|
38
|
+
return (
|
|
39
|
+
<form
|
|
40
|
+
className="flex flex-col gap-2"
|
|
41
|
+
onSubmit={(e) => {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
if (digits.length === 6) confirm.mutate({ id, code: digits });
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<Input
|
|
47
|
+
value={code}
|
|
48
|
+
onChange={(e) => setCode(e.target.value.slice(0, 7))}
|
|
49
|
+
inputMode="numeric"
|
|
50
|
+
autoComplete="one-time-code"
|
|
51
|
+
placeholder="6-digit code"
|
|
52
|
+
aria-label={`Code shown on ${name}`}
|
|
53
|
+
className="h-12 text-center font-mono text-2xl tracking-[0.3em]"
|
|
54
|
+
autoFocus
|
|
55
|
+
/>
|
|
56
|
+
{confirm.error && (
|
|
57
|
+
<p className="text-destructive text-sm">
|
|
58
|
+
{confirm.error.message}
|
|
59
|
+
{left ? ` ${left} ${left === 1 ? "try" : "tries"} left.` : ""}
|
|
60
|
+
</p>
|
|
61
|
+
)}
|
|
62
|
+
<div className="flex justify-end gap-2">
|
|
63
|
+
<Button
|
|
64
|
+
type="button"
|
|
65
|
+
variant="ghost"
|
|
66
|
+
className="min-h-11 sm:min-h-9"
|
|
67
|
+
onClick={onCancel}
|
|
68
|
+
>
|
|
69
|
+
Cancel
|
|
70
|
+
</Button>
|
|
71
|
+
<Button
|
|
72
|
+
type="submit"
|
|
73
|
+
className="min-h-11 sm:min-h-9"
|
|
74
|
+
disabled={confirm.isPending || digits.length !== 6 || left === 0}
|
|
75
|
+
>
|
|
76
|
+
Finish
|
|
77
|
+
</Button>
|
|
78
|
+
</div>
|
|
79
|
+
</form>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** On the machine that asks: the code to type on the other one. */
|
|
84
|
+
export function LinkCodeSheet({
|
|
85
|
+
linkId,
|
|
86
|
+
onClose,
|
|
87
|
+
onRetry,
|
|
88
|
+
}: {
|
|
89
|
+
linkId: string | null;
|
|
90
|
+
onClose: () => void;
|
|
91
|
+
onRetry: () => void;
|
|
92
|
+
}) {
|
|
93
|
+
const { data: link, error } = useOutgoingLink(linkId);
|
|
94
|
+
const cancel = useCancelLink();
|
|
95
|
+
const close = () => {
|
|
96
|
+
if ((link?.state === "pending" || link?.state === "confirm") && linkId)
|
|
97
|
+
cancel.mutate(linkId);
|
|
98
|
+
onClose();
|
|
99
|
+
};
|
|
100
|
+
const name = link?.name ?? "the other machine";
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Dialog open={!!linkId} onOpenChange={(o) => !o && close()}>
|
|
104
|
+
<DialogContent className={SHEET}>
|
|
105
|
+
<DialogHeader>
|
|
106
|
+
<DialogTitle>Link {name}</DialogTitle>
|
|
107
|
+
<DialogDescription>
|
|
108
|
+
{link?.state === "confirm" ? (
|
|
109
|
+
<>
|
|
110
|
+
Last step: {name} shows a code of its own. Typing it here proves
|
|
111
|
+
it's really {name}.
|
|
112
|
+
</>
|
|
113
|
+
) : link?.state === "accepted" ? (
|
|
114
|
+
<>Both machines list each other as linked now.</>
|
|
115
|
+
) : (
|
|
116
|
+
<>
|
|
117
|
+
Open AgentOS on {name} and type this code there. Its banner, the
|
|
118
|
+
Machines screen, or{" "}
|
|
119
|
+
<code className="font-mono">agent-os link accept</code> in its
|
|
120
|
+
terminal all take it.
|
|
121
|
+
</>
|
|
122
|
+
)}
|
|
123
|
+
</DialogDescription>
|
|
124
|
+
</DialogHeader>
|
|
125
|
+
{link?.state === "confirm" && (
|
|
126
|
+
<ConfirmForm id={link.id} name={name} onCancel={close} />
|
|
127
|
+
)}
|
|
128
|
+
{link && link.state !== "confirm" && link.state !== "accepted" && (
|
|
129
|
+
<p
|
|
130
|
+
className="text-center font-mono text-4xl font-semibold tracking-[0.2em] tabular-nums sm:text-5xl"
|
|
131
|
+
aria-label={`Code ${link.code.split("").join(" ")}`}
|
|
132
|
+
data-testid="link-code"
|
|
133
|
+
>
|
|
134
|
+
{showCode(link.code)}
|
|
135
|
+
</p>
|
|
136
|
+
)}
|
|
137
|
+
{link?.state !== "confirm" && (
|
|
138
|
+
<>
|
|
139
|
+
<div className="flex min-h-11 items-center justify-center gap-2 text-sm">
|
|
140
|
+
{error ? (
|
|
141
|
+
<span className="text-destructive">{error.message}</span>
|
|
142
|
+
) : !link || link.state === "pending" ? (
|
|
143
|
+
<>
|
|
144
|
+
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
|
|
145
|
+
<span className="text-muted-foreground">
|
|
146
|
+
Waiting for the code on {name}…
|
|
147
|
+
</span>
|
|
148
|
+
</>
|
|
149
|
+
) : link.state === "accepted" ? (
|
|
150
|
+
<>
|
|
151
|
+
<CheckCircle2 className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
|
|
152
|
+
<span>Linked. Tasks can run on {name} now.</span>
|
|
153
|
+
</>
|
|
154
|
+
) : (
|
|
155
|
+
<>
|
|
156
|
+
<XCircle className="text-destructive h-4 w-4" />
|
|
157
|
+
<span className="text-muted-foreground">
|
|
158
|
+
{ENDED[link.state] ?? link.error}
|
|
159
|
+
</span>
|
|
160
|
+
</>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
<div className="flex justify-end gap-2">
|
|
164
|
+
{link && !["pending", "accepted"].includes(link.state) && (
|
|
165
|
+
<Button
|
|
166
|
+
variant="outline"
|
|
167
|
+
className="min-h-11 sm:min-h-9"
|
|
168
|
+
onClick={onRetry}
|
|
169
|
+
>
|
|
170
|
+
Try again
|
|
171
|
+
</Button>
|
|
172
|
+
)}
|
|
173
|
+
<Button
|
|
174
|
+
variant={link?.state === "accepted" ? "default" : "ghost"}
|
|
175
|
+
className="min-h-11 sm:min-h-9"
|
|
176
|
+
onClick={close}
|
|
177
|
+
>
|
|
178
|
+
{link?.state === "pending" ? "Cancel" : "Done"}
|
|
179
|
+
</Button>
|
|
180
|
+
</div>
|
|
181
|
+
</>
|
|
182
|
+
)}
|
|
183
|
+
</DialogContent>
|
|
184
|
+
</Dialog>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { Repeat, Layers, Pause } from "lucide-react";
|
|
3
|
+
import { Repeat, Layers, Pause, Hourglass } from "lucide-react";
|
|
4
4
|
import { HostBadge } from "@/components/Hosts";
|
|
5
5
|
import { rowGroup } from "@/lib/recipes/chip";
|
|
6
6
|
import type { Session } from "@/lib/db";
|
|
@@ -77,6 +77,16 @@ export function RowSubtitle({
|
|
|
77
77
|
</span>
|
|
78
78
|
</span>
|
|
79
79
|
)}
|
|
80
|
+
{(session.waiting_on || session.waited_by) && (
|
|
81
|
+
<span className="text-muted-foreground flex min-w-0 items-center gap-1 text-xs">
|
|
82
|
+
<Hourglass aria-hidden className="h-3 w-3 shrink-0" />
|
|
83
|
+
<span className="truncate">
|
|
84
|
+
{session.waiting_on
|
|
85
|
+
? `waiting on ${session.waiting_on}`
|
|
86
|
+
: `${session.waited_by} waiting on it`}
|
|
87
|
+
</span>
|
|
88
|
+
</span>
|
|
89
|
+
)}
|
|
80
90
|
{group && (
|
|
81
91
|
<span
|
|
82
92
|
className="text-muted-foreground/70 flex min-w-0 shrink-0 items-center gap-1 truncate text-xs @[14rem]:shrink"
|