@saadnvd1/agent-os 0.3.698 → 0.3.715
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 +1 -0
- package/app/api/bus/groups/route.ts +44 -0
- package/app/api/bus/send/route.ts +11 -1
- package/app/api/bus/spawn/route.ts +12 -1
- package/app/api/bus/wait/route.ts +22 -0
- package/app/api/claims/[id]/route.ts +20 -0
- package/app/api/claims/check/route.ts +23 -0
- package/app/api/claims/check.test.ts +113 -0
- package/app/api/claims/exchange/route.ts +42 -0
- package/app/api/claims/outside.test.ts +90 -0
- package/app/api/claims/route.test.ts +193 -0
- package/app/api/claims/route.ts +73 -0
- package/app/api/hosts/[id]/route.ts +9 -1
- 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/peer/unlink/route.ts +17 -0
- package/app/api/peers/[hostId]/audit/route.ts +14 -0
- package/app/api/peers/[hostId]/route.ts +20 -0
- package/app/api/peers/account/route.ts +9 -0
- package/app/api/peers/gate.test.ts +174 -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.test.ts +37 -0
- package/app/api/peers/pending/route.ts +14 -0
- package/app/api/session-groups/[id]/restore/route.ts +11 -0
- package/app/api/session-groups/[id]/route.ts +25 -0
- package/app/api/session-groups/order/route.ts +15 -0
- package/app/api/session-groups/route.ts +21 -0
- package/app/api/sessions/[id]/fork/route.ts +3 -0
- package/app/api/sessions/[id]/group/route.ts +18 -0
- package/app/api/sessions/[id]/route.ts +14 -4
- package/app/api/sessions/[id]/summarize/route.ts +2 -0
- package/app/api/sessions/claims.test.ts +122 -0
- package/app/api/sessions/group/route.ts +17 -0
- package/app/api/sessions/route.ts +31 -13
- package/app/page.tsx +7 -0
- package/components/Claims/ClaimFiles.tsx +38 -0
- package/components/Claims/ClaimRow.tsx +82 -0
- package/components/Claims/ClaimsSheet.tsx +176 -0
- package/components/Claims/OverlapPill.tsx +31 -0
- package/components/Claims/StartClaimsNotice.tsx +70 -0
- package/components/Claims/claims-ui.test.ts +161 -0
- package/components/Claims/format.ts +57 -0
- package/components/Claims/index.ts +3 -0
- package/components/Draft/DraftPanel.tsx +40 -2
- package/components/Hosts/AccountMachines.tsx +194 -0
- package/components/Hosts/HostRow.tsx +195 -45
- package/components/Hosts/HostsDialog.tsx +49 -37
- package/components/Hosts/IncomingLinkSheet.tsx +246 -0
- package/components/Hosts/LinkCodeSheet.tsx +186 -0
- package/components/Hosts/PeerActivitySheet.tsx +111 -0
- package/components/Hosts/sheet.ts +3 -0
- package/components/SessionList/GroupShelves.tsx +195 -0
- package/components/SessionList/MoveToGroupMenu.tsx +105 -0
- package/components/SessionList/RowSubtitle.tsx +18 -2
- package/components/SessionList/SelectionToolbar.tsx +52 -1
- package/components/SessionList/SessionRow.tsx +1 -0
- package/components/SessionList/SessionRowMenu.tsx +17 -0
- package/components/SessionList/SessionShelves.tsx +5 -0
- package/components/SessionList/SidebarBody.tsx +6 -0
- package/components/SessionList/SidebarContextMenu.test.ts +104 -0
- package/components/SessionList/SidebarContextMenu.tsx +59 -0
- package/components/SessionList/SidebarHeader.tsx +5 -0
- package/components/SessionList/WorkspaceSwitcher.tsx +23 -6
- package/components/SessionList/index.tsx +18 -14
- package/components/SessionList/useSidebarData.ts +10 -0
- package/components/Welcome/ConnectBlock.tsx +1 -1
- package/components/ui/notice.tsx +46 -0
- package/components/ui/status-pill.tsx +39 -0
- package/data/claims/index.ts +2 -0
- package/data/claims/keys.ts +5 -0
- package/data/claims/queries.ts +47 -0
- package/data/peers/index.ts +2 -0
- package/data/peers/keys.ts +7 -0
- package/data/peers/queries.ts +172 -0
- package/data/push/topics.ts +4 -0
- package/data/session-groups/index.ts +156 -0
- package/data/sessions/index.ts +0 -1
- package/data/sessions/pin.ts +2 -6
- package/data/sessions/queries.ts +10 -29
- package/data/sessions/shelf.ts +2 -6
- package/data/tasks/index.ts +2 -0
- package/hooks/useSessions.ts +0 -12
- package/lib/__fixtures__/peer-rpc.ts +65 -33
- package/lib/agents/brief.ts +3 -1
- package/lib/agents/spawn.ts +4 -0
- package/lib/bus/format.ts +25 -4
- package/lib/bus/index.ts +121 -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/claims/collectLocal.ts +121 -0
- package/lib/claims/describe.ts +41 -0
- package/lib/claims/exchange.ts +91 -0
- package/lib/claims/goal.ts +45 -0
- package/lib/claims/index.ts +10 -0
- package/lib/claims/loop.ts +51 -0
- package/lib/claims/machine.ts +21 -0
- package/lib/claims/mine.ts +43 -0
- package/lib/claims/notifyOverlaps.ts +101 -0
- package/lib/claims/onMerged.ts +85 -0
- package/lib/claims/outside.ts +36 -0
- package/lib/claims/overlap.ts +87 -0
- package/lib/claims/overlapCache.ts +86 -0
- package/lib/claims/overlapText.ts +36 -0
- package/lib/claims/peers.ts +56 -0
- package/lib/claims/startCheck.ts +100 -0
- package/lib/claims/startWarning.ts +29 -0
- package/lib/claims/store.ts +366 -0
- package/lib/claims/types.ts +102 -0
- package/lib/claims/view.ts +80 -0
- package/lib/claims/yield.ts +24 -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/changes.ts +6 -0
- package/lib/db/migrations/20261020T1400_work_claims.ts +35 -0
- package/lib/db/migrations/20261020T1515_peer_unlinked.ts +22 -0
- package/lib/db/migrations/20261020T1600_bus_waits.ts +35 -0
- package/lib/db/migrations/20261020T1700_clean_connect_names.ts +18 -0
- package/lib/db/migrations/20261020T1800_session_groups.ts +49 -0
- package/lib/db/migrations/20261020T1900_task_queue_after_claim.ts +13 -0
- package/lib/db/migrations/20261020T2000_claim_notices.ts +20 -0
- package/lib/db/migrations/20261020T2100_claims_via_host.ts +21 -0
- package/lib/db/migrations/20261020T2200_work_claims_watch.ts +10 -0
- package/lib/db/queries.ts +0 -40
- package/lib/db/session-parts/columns.ts +1 -0
- package/lib/db/types.ts +20 -8
- package/lib/doctor/checks.ts +64 -1
- package/lib/done/archive.ts +2 -0
- package/lib/git-remote.ts +43 -0
- package/lib/hosts/index.ts +10 -1
- package/lib/orchestration.ts +2 -0
- package/lib/orchestrator/act.ts +54 -4
- package/lib/orchestrator/brief.ts +2 -1
- package/lib/orchestrator/claim-wait.ts +71 -0
- package/lib/orchestrator/conditions.ts +28 -1
- package/lib/orchestrator/describe.ts +19 -1
- package/lib/orchestrator/facts.ts +22 -0
- package/lib/orchestrator/serve.ts +37 -2
- package/lib/orchestrator/tool-names.ts +1 -0
- package/lib/orchestrator/tool-schemas.ts +31 -1
- package/lib/orchestrator/tools.ts +3 -1
- package/lib/orchestrator/watcher.ts +19 -1
- package/lib/peer/account.ts +270 -0
- package/lib/peer/allowlist.ts +1 -1
- package/lib/peer/cli.ts +24 -0
- package/lib/peer/client.ts +26 -1
- 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 +248 -0
- package/lib/peer/links.ts +117 -5
- package/lib/peer/machines.ts +84 -0
- package/lib/peer/name.ts +32 -0
- package/lib/peer/notify.ts +32 -0
- package/lib/peer/pairing-out.ts +382 -0
- package/lib/peer/pairing.ts +432 -0
- package/lib/peer/pin.ts +62 -0
- package/lib/peer/reasons.ts +7 -0
- package/lib/peer/revoke.ts +75 -0
- package/lib/peer/rpc-intake.ts +37 -8
- package/lib/peer/rpc-server.ts +72 -6
- package/lib/peer/unlink.ts +62 -0
- package/lib/peer/unlinked-proof.ts +44 -0
- package/lib/projects.ts +5 -2
- package/lib/schedules/when.ts +1 -1
- package/lib/security/auth.ts +10 -2
- package/lib/security/demo.ts +7 -0
- package/lib/security/gate.ts +3 -0
- package/lib/session-groups/assignSessions.ts +81 -0
- package/lib/session-groups/cli.ts +61 -0
- package/lib/session-groups/createGroup.ts +28 -0
- package/lib/session-groups/index.ts +21 -0
- package/lib/session-groups/removeGroup.ts +36 -0
- package/lib/session-groups/reorderGroups.ts +31 -0
- package/lib/session-groups/resolveGroup.ts +19 -0
- package/lib/session-groups/route.ts +32 -0
- package/lib/session-groups/store.ts +85 -0
- package/lib/session-groups/types.ts +25 -0
- package/lib/session-groups/updateGroup.ts +35 -0
- package/lib/session-groups/workspace.ts +21 -0
- package/lib/sessions/end.ts +3 -0
- package/lib/sessions/launch.ts +5 -2
- package/lib/sidebar/rollup.ts +1 -0
- package/lib/sidebar/row-menu.ts +2 -0
- package/lib/sidebar/shelves.ts +90 -15
- package/lib/sidebar/visible-order.ts +5 -0
- package/lib/tasks/hold-checks.ts +3 -1
- package/lib/tasks/holds.ts +3 -0
- package/lib/tasks/index.ts +5 -0
- package/lib/tasks/queue-rows.ts +8 -1
- package/lib/tasks/queue-start.ts +1 -0
- package/lib/tasks/queue-types.ts +9 -1
- package/lib/tasks/queue.ts +15 -5
- package/lib/tasks/schedule-types.ts +1 -0
- package/lib/test-temp.ts +19 -3
- package/lib/workspaces.ts +11 -3
- package/lib/worktree-sweep/testTemp.ts +23 -106
- package/package.json +1 -1
- package/scripts/agent-os +3 -1
- package/scripts/autodeploy +2 -0
- package/scripts/autodeploy.test.ts +31 -1
- package/scripts/claim-hook.sh +31 -0
- package/scripts/claim-hook.test.ts +230 -0
- package/scripts/lib/cli-links.sh +36 -0
- package/scripts/lib/commands.sh +5 -0
- package/scripts/lib/machine.sh +27 -2
- package/scripts/lib/machine.test.ts +25 -3
- package/scripts/sync-cli-links +7 -0
- package/scripts/sync-cli-links.test.ts +69 -0
- package/server.ts +6 -0
- package/stores/claimsUi.ts +22 -0
- package/app/api/groups/[...path]/route.ts +0 -136
- package/app/api/groups/route.ts +0 -93
- package/data/groups/index.ts +0 -1
- package/data/groups/mutations.ts +0 -95
- package/hooks/useGroups.ts +0 -37
|
@@ -1,16 +1,77 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useState } from "react";
|
|
3
4
|
import { Button } from "@/components/ui/button";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Activity,
|
|
7
|
+
ExternalLink,
|
|
8
|
+
Monitor,
|
|
9
|
+
Server,
|
|
10
|
+
Trash2,
|
|
11
|
+
PlugZap,
|
|
12
|
+
Loader2,
|
|
13
|
+
Link2,
|
|
14
|
+
Unlink,
|
|
15
|
+
} from "lucide-react";
|
|
16
|
+
import { Notice } from "@/components/ui/notice";
|
|
5
17
|
import type { Host } from "@/lib/db";
|
|
6
18
|
import { useDeleteHost, useLinkHost, useTestHost } from "@/data/hosts";
|
|
19
|
+
import { useUnlinkPeer } from "@/data/peers";
|
|
20
|
+
import { NEEDS_PRO, UPGRADE_URL } from "@/lib/peer/reasons";
|
|
7
21
|
import { cn } from "@/lib/utils";
|
|
22
|
+
import { PeerActivitySheet } from "./PeerActivitySheet";
|
|
23
|
+
|
|
24
|
+
function connectLine(host: Host): string {
|
|
25
|
+
if (host.peer_state === "active")
|
|
26
|
+
return "Linked through Connect · tasks run on its AgentOS";
|
|
27
|
+
if (host.peer_state === "suspended") return "Through Connect · suspended";
|
|
28
|
+
if (host.peer_state === "unlinked")
|
|
29
|
+
return `Through Connect · ${host.peer_note ?? "unlinked"}`;
|
|
30
|
+
return "Through Connect · not linked";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Why a Connect link is suspended, with the way out when there is one. */
|
|
34
|
+
function SuspendedNote({ reason }: { reason: string | null | undefined }) {
|
|
35
|
+
const plan = reason === NEEDS_PRO;
|
|
36
|
+
return (
|
|
37
|
+
<Notice
|
|
38
|
+
tone="warning"
|
|
39
|
+
className="mt-2"
|
|
40
|
+
action={
|
|
41
|
+
plan && (
|
|
42
|
+
<Button
|
|
43
|
+
asChild
|
|
44
|
+
variant="outline"
|
|
45
|
+
size="sm"
|
|
46
|
+
className="min-h-11 sm:min-h-8"
|
|
47
|
+
>
|
|
48
|
+
<a href={UPGRADE_URL} target="_blank" rel="noopener noreferrer">
|
|
49
|
+
Upgrade
|
|
50
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
51
|
+
</a>
|
|
52
|
+
</Button>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
{plan
|
|
57
|
+
? "Linking machines needs Pro. Calls both ways are refused until you upgrade; it comes back without linking again."
|
|
58
|
+
: `Suspended: ${reason ?? "no reason given"}. Calls both ways are refused; it comes back on its own if the machine returns to your account.`}
|
|
59
|
+
</Notice>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
8
62
|
|
|
9
63
|
export function HostRow({ host }: { host: Host }) {
|
|
10
64
|
const deleteHost = useDeleteHost();
|
|
11
65
|
const testHost = useTestHost();
|
|
12
66
|
const linkHost = useLinkHost();
|
|
67
|
+
const unlink = useUnlinkPeer();
|
|
68
|
+
const [activity, setActivity] = useState(false);
|
|
69
|
+
const [confirmUnlink, setConfirmUnlink] = useState(false);
|
|
13
70
|
const isLocal = host.id === "local";
|
|
71
|
+
const viaConnect = host.kind === "connect";
|
|
72
|
+
const pinned =
|
|
73
|
+
viaConnect &&
|
|
74
|
+
(host.peer_state === "active" || host.peer_state === "suspended");
|
|
14
75
|
const result = testHost.data;
|
|
15
76
|
const Icon = isLocal ? Monitor : Server;
|
|
16
77
|
|
|
@@ -23,54 +84,90 @@ export function HostRow({ host }: { host: Host }) {
|
|
|
23
84
|
<p className="text-muted-foreground truncate text-xs">
|
|
24
85
|
{isLocal
|
|
25
86
|
? "Where agent-os is running"
|
|
26
|
-
:
|
|
27
|
-
?
|
|
28
|
-
: host.
|
|
87
|
+
: viaConnect
|
|
88
|
+
? connectLine(host)
|
|
89
|
+
: host.linked
|
|
90
|
+
? `${host.ssh_target} · tasks run on its AgentOS`
|
|
91
|
+
: host.ssh_target}
|
|
29
92
|
</p>
|
|
30
93
|
</div>
|
|
31
94
|
{!isLocal && (
|
|
32
95
|
<>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
96
|
+
{!viaConnect && (
|
|
97
|
+
<>
|
|
98
|
+
<Button
|
|
99
|
+
variant="ghost"
|
|
100
|
+
size="icon-sm"
|
|
101
|
+
className="h-11 w-11 sm:h-8 sm:w-8"
|
|
102
|
+
onClick={() => testHost.mutate(host.id)}
|
|
103
|
+
disabled={testHost.isPending}
|
|
104
|
+
aria-label="Test connection"
|
|
105
|
+
>
|
|
106
|
+
{testHost.isPending ? (
|
|
107
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
108
|
+
) : (
|
|
109
|
+
<PlugZap className="h-4 w-4" />
|
|
110
|
+
)}
|
|
111
|
+
</Button>
|
|
112
|
+
<Button
|
|
113
|
+
variant="ghost"
|
|
114
|
+
size="icon-sm"
|
|
115
|
+
className="h-11 w-11 sm:h-8 sm:w-8"
|
|
116
|
+
onClick={() => linkHost.mutate(host.id)}
|
|
117
|
+
disabled={linkHost.isPending}
|
|
118
|
+
aria-label={
|
|
119
|
+
host.linked ? "Link its AgentOS again" : "Link its AgentOS"
|
|
120
|
+
}
|
|
121
|
+
title="Pair with the AgentOS running there, so tasks can run on it"
|
|
122
|
+
>
|
|
123
|
+
{linkHost.isPending ? (
|
|
124
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
125
|
+
) : (
|
|
126
|
+
<Link2 className="h-4 w-4" />
|
|
127
|
+
)}
|
|
128
|
+
</Button>
|
|
129
|
+
</>
|
|
130
|
+
)}
|
|
131
|
+
{viaConnect && host.peer_state && (
|
|
132
|
+
<Button
|
|
133
|
+
variant="ghost"
|
|
134
|
+
size="icon-sm"
|
|
135
|
+
className="h-11 w-11 sm:h-8 sm:w-8"
|
|
136
|
+
onClick={() => setActivity(true)}
|
|
137
|
+
aria-label="Activity"
|
|
138
|
+
title="Every call to and from this machine"
|
|
139
|
+
>
|
|
140
|
+
<Activity className="h-4 w-4" />
|
|
141
|
+
</Button>
|
|
142
|
+
)}
|
|
143
|
+
{pinned ? (
|
|
144
|
+
<Button
|
|
145
|
+
variant="ghost"
|
|
146
|
+
size="icon-sm"
|
|
147
|
+
className="h-11 w-11 sm:h-8 sm:w-8"
|
|
148
|
+
onClick={() => setConfirmUnlink(true)}
|
|
149
|
+
disabled={unlink.isPending || confirmUnlink}
|
|
150
|
+
aria-label="Unlink"
|
|
151
|
+
title="Unlink: it can no longer call this machine, nor this one it"
|
|
152
|
+
>
|
|
153
|
+
{unlink.isPending ? (
|
|
154
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
155
|
+
) : (
|
|
156
|
+
<Unlink className="h-4 w-4" />
|
|
157
|
+
)}
|
|
158
|
+
</Button>
|
|
159
|
+
) : (
|
|
160
|
+
<Button
|
|
161
|
+
variant="ghost"
|
|
162
|
+
size="icon-sm"
|
|
163
|
+
className="h-11 w-11 sm:h-8 sm:w-8"
|
|
164
|
+
onClick={() => deleteHost.mutate(host.id)}
|
|
165
|
+
disabled={deleteHost.isPending}
|
|
166
|
+
aria-label="Remove machine"
|
|
167
|
+
>
|
|
168
|
+
<Trash2 className="h-4 w-4" />
|
|
169
|
+
</Button>
|
|
170
|
+
)}
|
|
74
171
|
</>
|
|
75
172
|
)}
|
|
76
173
|
</div>
|
|
@@ -98,6 +195,59 @@ export function HostRow({ host }: { host: Host }) {
|
|
|
98
195
|
{linkHost.error.message}
|
|
99
196
|
</p>
|
|
100
197
|
)}
|
|
198
|
+
{host.peer_state === "suspended" && (
|
|
199
|
+
<SuspendedNote reason={host.peer_note} />
|
|
200
|
+
)}
|
|
201
|
+
{confirmUnlink && (
|
|
202
|
+
<Notice
|
|
203
|
+
tone="warning"
|
|
204
|
+
className="mt-2"
|
|
205
|
+
action={
|
|
206
|
+
<div className="flex gap-2">
|
|
207
|
+
<Button
|
|
208
|
+
variant="ghost"
|
|
209
|
+
size="sm"
|
|
210
|
+
className="min-h-11 sm:min-h-8"
|
|
211
|
+
onClick={() => setConfirmUnlink(false)}
|
|
212
|
+
>
|
|
213
|
+
Cancel
|
|
214
|
+
</Button>
|
|
215
|
+
<Button
|
|
216
|
+
variant="destructive"
|
|
217
|
+
size="sm"
|
|
218
|
+
className="min-h-11 sm:min-h-8"
|
|
219
|
+
onClick={() => {
|
|
220
|
+
setConfirmUnlink(false);
|
|
221
|
+
unlink.mutate(host.id);
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
Unlink
|
|
225
|
+
</Button>
|
|
226
|
+
</div>
|
|
227
|
+
}
|
|
228
|
+
>
|
|
229
|
+
Unlink {host.name}? It's unlinked on both machines, and linking
|
|
230
|
+
them again needs a fresh pairing with new codes.
|
|
231
|
+
</Notice>
|
|
232
|
+
)}
|
|
233
|
+
{unlink.data && !unlink.data.told && (
|
|
234
|
+
<p className="text-muted-foreground mt-1.5 text-xs">
|
|
235
|
+
Unlinked here. {host.name} couldn't be told now; its next call is
|
|
236
|
+
refused and it will show as unlinked.
|
|
237
|
+
</p>
|
|
238
|
+
)}
|
|
239
|
+
{unlink.error && (
|
|
240
|
+
<p className="text-destructive mt-1.5 text-xs">
|
|
241
|
+
{unlink.error.message}
|
|
242
|
+
</p>
|
|
243
|
+
)}
|
|
244
|
+
{viaConnect && (
|
|
245
|
+
<PeerActivitySheet
|
|
246
|
+
hostId={activity ? host.id : null}
|
|
247
|
+
name={host.name}
|
|
248
|
+
onClose={() => setActivity(false)}
|
|
249
|
+
/>
|
|
250
|
+
)}
|
|
101
251
|
{deleteHost.error && (
|
|
102
252
|
<p className="text-destructive mt-1.5 text-xs">
|
|
103
253
|
{deleteHost.error.message}
|
|
@@ -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,246 @@
|
|
|
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
|
+
const asking = (r: PendingView) =>
|
|
32
|
+
r.state === "accepted" ? "is linked: finish there" : "wants to link";
|
|
33
|
+
|
|
34
|
+
/** On the machine being asked: type the code the other one shows. */
|
|
35
|
+
export function IncomingLinkSheet({
|
|
36
|
+
request,
|
|
37
|
+
onClose,
|
|
38
|
+
}: {
|
|
39
|
+
request: PendingView | null;
|
|
40
|
+
onClose: () => void;
|
|
41
|
+
}) {
|
|
42
|
+
const [code, setCode] = useState("");
|
|
43
|
+
const { accept, decline } = useAnswerLink();
|
|
44
|
+
const digits = code.replace(/\D/g, "");
|
|
45
|
+
const busy = accept.isPending || decline.isPending;
|
|
46
|
+
const close = () => {
|
|
47
|
+
setCode("");
|
|
48
|
+
accept.reset();
|
|
49
|
+
onClose();
|
|
50
|
+
};
|
|
51
|
+
const left = (accept.error as { attemptsLeft?: number } | null)?.attemptsLeft;
|
|
52
|
+
// Held by the server too, so a reload on this machine still shows it.
|
|
53
|
+
const done =
|
|
54
|
+
accept.data ??
|
|
55
|
+
(request?.confirmCode
|
|
56
|
+
? { name: request.name, confirmCode: request.confirmCode }
|
|
57
|
+
: null);
|
|
58
|
+
|
|
59
|
+
if (done)
|
|
60
|
+
return (
|
|
61
|
+
<Dialog open={!!request} onOpenChange={(o) => !o && close()}>
|
|
62
|
+
<DialogContent className={SHEET}>
|
|
63
|
+
<DialogHeader>
|
|
64
|
+
<DialogTitle>Finish on {done.name}</DialogTitle>
|
|
65
|
+
<DialogDescription>
|
|
66
|
+
{done.name} is linked here. Type this code on {done.name} so it
|
|
67
|
+
links back to this machine.
|
|
68
|
+
</DialogDescription>
|
|
69
|
+
</DialogHeader>
|
|
70
|
+
<p
|
|
71
|
+
className="text-center font-mono text-4xl font-semibold tracking-[0.2em] tabular-nums sm:text-5xl"
|
|
72
|
+
aria-label={`Code ${done.confirmCode.split("").join(" ")}`}
|
|
73
|
+
data-testid="confirm-code"
|
|
74
|
+
>
|
|
75
|
+
{showCode(done.confirmCode)}
|
|
76
|
+
</p>
|
|
77
|
+
<p className="flex items-center justify-center gap-2 text-sm">
|
|
78
|
+
<CheckCircle2 className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
|
|
79
|
+
Code matched
|
|
80
|
+
</p>
|
|
81
|
+
<div className="flex justify-end">
|
|
82
|
+
<Button className="min-h-11 sm:min-h-9" onClick={close}>
|
|
83
|
+
Done
|
|
84
|
+
</Button>
|
|
85
|
+
</div>
|
|
86
|
+
</DialogContent>
|
|
87
|
+
</Dialog>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Dialog open={!!request} onOpenChange={(o) => !o && close()}>
|
|
92
|
+
<DialogContent className={SHEET}>
|
|
93
|
+
<DialogHeader>
|
|
94
|
+
<DialogTitle>Link {request?.name}?</DialogTitle>
|
|
95
|
+
<DialogDescription>
|
|
96
|
+
{request?.name} asked to link with this machine. Type the code it
|
|
97
|
+
shows to allow it.
|
|
98
|
+
</DialogDescription>
|
|
99
|
+
{request && <Origin request={request} />}
|
|
100
|
+
</DialogHeader>
|
|
101
|
+
<div className="flex gap-2 rounded-lg border border-amber-500/40 bg-amber-500/10 p-3 text-sm">
|
|
102
|
+
<ShieldAlert className="mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
103
|
+
<p>
|
|
104
|
+
Linked machines can start agents and run code on this machine. Only
|
|
105
|
+
link one you own.
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
<form
|
|
109
|
+
className="flex flex-col gap-2"
|
|
110
|
+
onSubmit={(e) => {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
if (request && digits.length === 6)
|
|
113
|
+
accept.mutate({ id: request.id, code: digits });
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
<Input
|
|
117
|
+
value={code}
|
|
118
|
+
onChange={(e) => setCode(e.target.value.slice(0, 7))}
|
|
119
|
+
inputMode="numeric"
|
|
120
|
+
autoComplete="one-time-code"
|
|
121
|
+
placeholder="6-digit code"
|
|
122
|
+
aria-label="Code shown on the other machine"
|
|
123
|
+
className="h-12 text-center font-mono text-2xl tracking-[0.3em]"
|
|
124
|
+
autoFocus
|
|
125
|
+
/>
|
|
126
|
+
{accept.error && (
|
|
127
|
+
<p className="text-destructive text-sm">
|
|
128
|
+
{accept.error.message}
|
|
129
|
+
{left ? ` ${left} ${left === 1 ? "try" : "tries"} left.` : ""}
|
|
130
|
+
</p>
|
|
131
|
+
)}
|
|
132
|
+
<div className="flex justify-end gap-2">
|
|
133
|
+
<Button
|
|
134
|
+
type="button"
|
|
135
|
+
variant="ghost"
|
|
136
|
+
className="min-h-11 sm:min-h-9"
|
|
137
|
+
disabled={busy}
|
|
138
|
+
onClick={() =>
|
|
139
|
+
request && decline.mutate(request.id, { onSuccess: close })
|
|
140
|
+
}
|
|
141
|
+
>
|
|
142
|
+
Decline
|
|
143
|
+
</Button>
|
|
144
|
+
<Button
|
|
145
|
+
type="submit"
|
|
146
|
+
className="min-h-11 sm:min-h-9"
|
|
147
|
+
disabled={busy || digits.length !== 6 || left === 0}
|
|
148
|
+
>
|
|
149
|
+
Link
|
|
150
|
+
</Button>
|
|
151
|
+
</div>
|
|
152
|
+
</form>
|
|
153
|
+
</DialogContent>
|
|
154
|
+
</Dialog>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A banner on every page while a machine is waiting for its code here. */
|
|
159
|
+
export function IncomingLinks() {
|
|
160
|
+
const { data: requests = [] } = usePendingLinks();
|
|
161
|
+
// Held while open, so a request voided by a wrong code still says why.
|
|
162
|
+
const [open, setOpen] = useState<PendingView | null>(null);
|
|
163
|
+
// Hidden until a request not seen yet arrives; it covers the header on phones.
|
|
164
|
+
const [later, setLater] = useState<string | null>(null);
|
|
165
|
+
const shown = requests.filter((r) => !later?.includes(r.id));
|
|
166
|
+
const first = shown[0];
|
|
167
|
+
const hidden = shown.length === 0;
|
|
168
|
+
return (
|
|
169
|
+
<>
|
|
170
|
+
{first && !open && !hidden && (
|
|
171
|
+
<div
|
|
172
|
+
role="status"
|
|
173
|
+
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"
|
|
174
|
+
>
|
|
175
|
+
<ShieldAlert className="h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
176
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
177
|
+
<p className="truncate text-sm">
|
|
178
|
+
<span className="font-medium">{first.name}</span> {asking(first)}
|
|
179
|
+
{shown.length > 1 ? ` (+${shown.length - 1} more)` : ""}
|
|
180
|
+
</p>
|
|
181
|
+
<Origin request={first} />
|
|
182
|
+
</div>
|
|
183
|
+
<Button
|
|
184
|
+
size="sm"
|
|
185
|
+
className="min-h-11 sm:min-h-8"
|
|
186
|
+
onClick={() => setOpen(first)}
|
|
187
|
+
>
|
|
188
|
+
{first.state === "accepted" ? "Show code" : "Review"}
|
|
189
|
+
</Button>
|
|
190
|
+
<Button
|
|
191
|
+
size="icon"
|
|
192
|
+
variant="ghost"
|
|
193
|
+
aria-label="Later"
|
|
194
|
+
className="h-11 w-11 shrink-0 sm:h-8 sm:w-8"
|
|
195
|
+
onClick={() =>
|
|
196
|
+
setLater(`${later ?? ""} ${shown.map((r) => r.id).join(" ")}`)
|
|
197
|
+
}
|
|
198
|
+
>
|
|
199
|
+
<X className="h-4 w-4" />
|
|
200
|
+
</Button>
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
<IncomingLinkSheet
|
|
204
|
+
request={open}
|
|
205
|
+
onClose={() => {
|
|
206
|
+
// Answered, or put off: either way it stops covering the page.
|
|
207
|
+
if (open) setLater(`${later ?? ""} ${open.id}`);
|
|
208
|
+
setOpen(null);
|
|
209
|
+
}}
|
|
210
|
+
/>
|
|
211
|
+
</>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** In Machines: the requests waiting here, for after the banner was put off. */
|
|
216
|
+
export function WaitingLinks() {
|
|
217
|
+
const { data: requests = [] } = usePendingLinks();
|
|
218
|
+
const [open, setOpen] = useState<PendingView | null>(null);
|
|
219
|
+
if (requests.length === 0 && !open) return null;
|
|
220
|
+
return (
|
|
221
|
+
<>
|
|
222
|
+
{requests.map((r) => (
|
|
223
|
+
<div
|
|
224
|
+
key={r.id}
|
|
225
|
+
className="flex items-center gap-3 rounded-lg border border-amber-500/40 bg-amber-500/10 px-3 py-2"
|
|
226
|
+
>
|
|
227
|
+
<ShieldAlert className="h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
228
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
229
|
+
<p className="truncate text-sm">
|
|
230
|
+
<span className="font-medium">{r.name}</span> {asking(r)}
|
|
231
|
+
</p>
|
|
232
|
+
<Origin request={r} />
|
|
233
|
+
</div>
|
|
234
|
+
<Button
|
|
235
|
+
size="sm"
|
|
236
|
+
className="min-h-11 sm:min-h-8"
|
|
237
|
+
onClick={() => setOpen(r)}
|
|
238
|
+
>
|
|
239
|
+
{r.state === "accepted" ? "Show code" : "Type code"}
|
|
240
|
+
</Button>
|
|
241
|
+
</div>
|
|
242
|
+
))}
|
|
243
|
+
<IncomingLinkSheet request={open} onClose={() => setOpen(null)} />
|
|
244
|
+
</>
|
|
245
|
+
);
|
|
246
|
+
}
|