@saadnvd1/agent-os 0.3.700 → 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/spawn/route.ts +12 -1
- 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/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/gate.test.ts +64 -1
- package/app/api/peers/pending/route.test.ts +37 -0
- package/app/api/peers/pending/route.ts +11 -4
- 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 +15 -14
- package/app/page.tsx +5 -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 +32 -18
- package/components/Hosts/HostRow.tsx +155 -14
- package/components/Hosts/IncomingLinkSheet.tsx +28 -10
- package/components/Hosts/PeerActivitySheet.tsx +111 -0
- package/components/SessionList/GroupShelves.tsx +195 -0
- package/components/SessionList/MoveToGroupMenu.tsx +105 -0
- package/components/SessionList/RowSubtitle.tsx +7 -1
- 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/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/keys.ts +1 -0
- package/data/peers/queries.ts +31 -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/spawn.ts +4 -0
- package/lib/bus/index.ts +8 -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/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/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 +16 -8
- package/lib/doctor/checks.ts +64 -1
- 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 +6 -1
- package/lib/orchestrator/facts.ts +5 -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 +74 -2
- package/lib/peer/allowlist.ts +1 -1
- package/lib/peer/client.ts +26 -1
- package/lib/peer/link-cli.ts +19 -4
- package/lib/peer/links.ts +117 -5
- package/lib/peer/machines.ts +14 -2
- package/lib/peer/name.ts +32 -0
- package/lib/peer/pairing.ts +34 -5
- package/lib/peer/pin.ts +10 -3
- package/lib/peer/reasons.ts +7 -0
- package/lib/peer/revoke.ts +75 -0
- package/lib/peer/rpc-server.ts +53 -5
- package/lib/peer/unlink.ts +62 -0
- package/lib/peer/unlinked-proof.ts +44 -0
- package/lib/projects.ts +5 -2
- package/lib/security/demo.ts +7 -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/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 +1 -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 +3 -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 +3 -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
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { ExternalLink, Link2, Loader2, RefreshCw } from "lucide-react";
|
|
5
5
|
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Notice } from "@/components/ui/notice";
|
|
6
7
|
import { LinkForm } from "@/components/Welcome/ConnectBlock";
|
|
7
8
|
import {
|
|
8
9
|
useAccountMachines,
|
|
9
10
|
useRefreshAccount,
|
|
10
11
|
useStartLink,
|
|
11
12
|
} from "@/data/peers";
|
|
12
|
-
import {
|
|
13
|
+
import { UPGRADE_URL } from "@/lib/peer/reasons";
|
|
13
14
|
import type { AccountMachine } from "@/lib/peer/machines";
|
|
14
15
|
import { cn } from "@/lib/utils";
|
|
15
16
|
import { WaitingLinks } from "./IncomingLinkSheet";
|
|
@@ -57,7 +58,12 @@ function MachineRow({
|
|
|
57
58
|
Linked
|
|
58
59
|
</span>
|
|
59
60
|
) : machine.linkState === "suspended" ? (
|
|
60
|
-
<span
|
|
61
|
+
<span
|
|
62
|
+
className="text-xs text-amber-700 dark:text-amber-400"
|
|
63
|
+
title={machine.suspendedReason ?? undefined}
|
|
64
|
+
>
|
|
65
|
+
Suspended
|
|
66
|
+
</span>
|
|
61
67
|
) : (
|
|
62
68
|
<Button
|
|
63
69
|
variant="outline"
|
|
@@ -122,23 +128,31 @@ export function AccountMachines() {
|
|
|
122
128
|
/>
|
|
123
129
|
</Button>
|
|
124
130
|
</div>
|
|
131
|
+
{data.uncheckedSince && (
|
|
132
|
+
<Notice tone="warning">
|
|
133
|
+
Couldn't check your account since{" "}
|
|
134
|
+
{new Date(data.uncheckedSince).toLocaleString([], {
|
|
135
|
+
dateStyle: "medium",
|
|
136
|
+
timeStyle: "short",
|
|
137
|
+
})}
|
|
138
|
+
. Machines already linked keep working; new links wait until it's
|
|
139
|
+
back.
|
|
140
|
+
</Notice>
|
|
141
|
+
)}
|
|
125
142
|
{data.entitlements && !entitled && (
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
</a>
|
|
140
|
-
</Button>
|
|
141
|
-
</div>
|
|
143
|
+
<Notice
|
|
144
|
+
action={
|
|
145
|
+
<Button asChild variant="outline" className="min-h-11 sm:min-h-9">
|
|
146
|
+
<a href={UPGRADE_URL} target="_blank" rel="noopener noreferrer">
|
|
147
|
+
Upgrade
|
|
148
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
149
|
+
</a>
|
|
150
|
+
</Button>
|
|
151
|
+
}
|
|
152
|
+
>
|
|
153
|
+
Link your machines with Pro: tasks move between them over any network,
|
|
154
|
+
end-to-end encrypted.
|
|
155
|
+
</Notice>
|
|
142
156
|
)}
|
|
143
157
|
{entitled && <WaitingLinks />}
|
|
144
158
|
{data.error && (
|
|
@@ -1,17 +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";
|
|
14
71
|
const viaConnect = host.kind === "connect";
|
|
72
|
+
const pinned =
|
|
73
|
+
viaConnect &&
|
|
74
|
+
(host.peer_state === "active" || host.peer_state === "suspended");
|
|
15
75
|
const result = testHost.data;
|
|
16
76
|
const Icon = isLocal ? Monitor : Server;
|
|
17
77
|
|
|
@@ -25,9 +85,7 @@ export function HostRow({ host }: { host: Host }) {
|
|
|
25
85
|
{isLocal
|
|
26
86
|
? "Where agent-os is running"
|
|
27
87
|
: viaConnect
|
|
28
|
-
? host
|
|
29
|
-
? "Linked through Connect · tasks run on its AgentOS"
|
|
30
|
-
: "Through Connect · not linked"
|
|
88
|
+
? connectLine(host)
|
|
31
89
|
: host.linked
|
|
32
90
|
? `${host.ssh_target} · tasks run on its AgentOS`
|
|
33
91
|
: host.ssh_target}
|
|
@@ -70,16 +128,46 @@ export function HostRow({ host }: { host: Host }) {
|
|
|
70
128
|
</Button>
|
|
71
129
|
</>
|
|
72
130
|
)}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
)}
|
|
83
171
|
</>
|
|
84
172
|
)}
|
|
85
173
|
</div>
|
|
@@ -107,6 +195,59 @@ export function HostRow({ host }: { host: Host }) {
|
|
|
107
195
|
{linkHost.error.message}
|
|
108
196
|
</p>
|
|
109
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
|
+
)}
|
|
110
251
|
{deleteHost.error && (
|
|
111
252
|
<p className="text-destructive mt-1.5 text-xs">
|
|
112
253
|
{deleteHost.error.message}
|
|
@@ -28,6 +28,9 @@ function Origin({ request }: { request: PendingView }) {
|
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const asking = (r: PendingView) =>
|
|
32
|
+
r.state === "accepted" ? "is linked: finish there" : "wants to link";
|
|
33
|
+
|
|
31
34
|
/** On the machine being asked: type the code the other one shows. */
|
|
32
35
|
export function IncomingLinkSheet({
|
|
33
36
|
request,
|
|
@@ -46,7 +49,12 @@ export function IncomingLinkSheet({
|
|
|
46
49
|
onClose();
|
|
47
50
|
};
|
|
48
51
|
const left = (accept.error as { attemptsLeft?: number } | null)?.attemptsLeft;
|
|
49
|
-
|
|
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);
|
|
50
58
|
|
|
51
59
|
if (done)
|
|
52
60
|
return (
|
|
@@ -154,8 +162,9 @@ export function IncomingLinks() {
|
|
|
154
162
|
const [open, setOpen] = useState<PendingView | null>(null);
|
|
155
163
|
// Hidden until a request not seen yet arrives; it covers the header on phones.
|
|
156
164
|
const [later, setLater] = useState<string | null>(null);
|
|
157
|
-
const
|
|
158
|
-
const
|
|
165
|
+
const shown = requests.filter((r) => !later?.includes(r.id));
|
|
166
|
+
const first = shown[0];
|
|
167
|
+
const hidden = shown.length === 0;
|
|
159
168
|
return (
|
|
160
169
|
<>
|
|
161
170
|
{first && !open && !hidden && (
|
|
@@ -166,8 +175,8 @@ export function IncomingLinks() {
|
|
|
166
175
|
<ShieldAlert className="h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
167
176
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
168
177
|
<p className="truncate text-sm">
|
|
169
|
-
<span className="font-medium">{first.name}</span>
|
|
170
|
-
{
|
|
178
|
+
<span className="font-medium">{first.name}</span> {asking(first)}
|
|
179
|
+
{shown.length > 1 ? ` (+${shown.length - 1} more)` : ""}
|
|
171
180
|
</p>
|
|
172
181
|
<Origin request={first} />
|
|
173
182
|
</div>
|
|
@@ -176,20 +185,29 @@ export function IncomingLinks() {
|
|
|
176
185
|
className="min-h-11 sm:min-h-8"
|
|
177
186
|
onClick={() => setOpen(first)}
|
|
178
187
|
>
|
|
179
|
-
Review
|
|
188
|
+
{first.state === "accepted" ? "Show code" : "Review"}
|
|
180
189
|
</Button>
|
|
181
190
|
<Button
|
|
182
191
|
size="icon"
|
|
183
192
|
variant="ghost"
|
|
184
193
|
aria-label="Later"
|
|
185
194
|
className="h-11 w-11 shrink-0 sm:h-8 sm:w-8"
|
|
186
|
-
onClick={() =>
|
|
195
|
+
onClick={() =>
|
|
196
|
+
setLater(`${later ?? ""} ${shown.map((r) => r.id).join(" ")}`)
|
|
197
|
+
}
|
|
187
198
|
>
|
|
188
199
|
<X className="h-4 w-4" />
|
|
189
200
|
</Button>
|
|
190
201
|
</div>
|
|
191
202
|
)}
|
|
192
|
-
<IncomingLinkSheet
|
|
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
|
+
/>
|
|
193
211
|
</>
|
|
194
212
|
);
|
|
195
213
|
}
|
|
@@ -209,7 +227,7 @@ export function WaitingLinks() {
|
|
|
209
227
|
<ShieldAlert className="h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
210
228
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
211
229
|
<p className="truncate text-sm">
|
|
212
|
-
<span className="font-medium">{r.name}</span>
|
|
230
|
+
<span className="font-medium">{r.name}</span> {asking(r)}
|
|
213
231
|
</p>
|
|
214
232
|
<Origin request={r} />
|
|
215
233
|
</div>
|
|
@@ -218,7 +236,7 @@ export function WaitingLinks() {
|
|
|
218
236
|
className="min-h-11 sm:min-h-8"
|
|
219
237
|
onClick={() => setOpen(r)}
|
|
220
238
|
>
|
|
221
|
-
Type code
|
|
239
|
+
{r.state === "accepted" ? "Show code" : "Type code"}
|
|
222
240
|
</Button>
|
|
223
241
|
</div>
|
|
224
242
|
))}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowDownLeft, ArrowUpRight } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
} from "@/components/ui/dialog";
|
|
11
|
+
import { usePeerAudit } from "@/data/peers";
|
|
12
|
+
import type { AuditRow, Outcome } from "@/lib/peer/audit";
|
|
13
|
+
import { relativeTime } from "@/lib/schedules/when";
|
|
14
|
+
import { cn } from "@/lib/utils";
|
|
15
|
+
import { SHEET } from "./sheet";
|
|
16
|
+
|
|
17
|
+
const REFUSED: Record<Exclude<Outcome, "ok">, string> = {
|
|
18
|
+
denied_route: "route not open to it",
|
|
19
|
+
bad_envelope: "malformed",
|
|
20
|
+
too_large: "too large",
|
|
21
|
+
too_slow: "too slow",
|
|
22
|
+
busy: "busy",
|
|
23
|
+
bad_sig: "bad signature",
|
|
24
|
+
stale: "clocks differ",
|
|
25
|
+
replay: "replayed",
|
|
26
|
+
unknown_peer: "not linked",
|
|
27
|
+
suspended: "suspended",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function Row({ row }: { row: AuditRow }) {
|
|
31
|
+
const Arrow = row.direction === "in" ? ArrowDownLeft : ArrowUpRight;
|
|
32
|
+
const refused = row.outcome !== "ok";
|
|
33
|
+
return (
|
|
34
|
+
<li className="flex items-start gap-2 py-2">
|
|
35
|
+
<Arrow
|
|
36
|
+
className="text-muted-foreground mt-0.5 h-4 w-4 shrink-0"
|
|
37
|
+
aria-label={row.direction === "in" ? "Called here" : "Called there"}
|
|
38
|
+
/>
|
|
39
|
+
<div className="min-w-0 flex-1">
|
|
40
|
+
<p className="truncate font-mono text-xs">
|
|
41
|
+
{row.method} {row.route}
|
|
42
|
+
</p>
|
|
43
|
+
<p className="text-muted-foreground text-xs">
|
|
44
|
+
{relativeTime(row.at)}
|
|
45
|
+
{row.count > 1 && ` · ${row.count} calls this hour`}
|
|
46
|
+
{` · ${Math.round(row.ms)} ms`}
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
<span
|
|
50
|
+
className={cn(
|
|
51
|
+
"shrink-0 rounded-md px-1.5 py-0.5 text-xs tabular-nums",
|
|
52
|
+
refused
|
|
53
|
+
? "bg-destructive/10 text-destructive"
|
|
54
|
+
: row.status >= 400
|
|
55
|
+
? "bg-amber-500/10 text-amber-700 dark:text-amber-400"
|
|
56
|
+
: "bg-muted text-muted-foreground"
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
{refused ? REFUSED[row.outcome as Exclude<Outcome, "ok">] : row.status}
|
|
60
|
+
</span>
|
|
61
|
+
</li>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Machines → a linked machine → Activity: every call, both ways. */
|
|
66
|
+
export function PeerActivitySheet({
|
|
67
|
+
hostId,
|
|
68
|
+
name,
|
|
69
|
+
onClose,
|
|
70
|
+
}: {
|
|
71
|
+
hostId: string | null;
|
|
72
|
+
name: string;
|
|
73
|
+
onClose: () => void;
|
|
74
|
+
}) {
|
|
75
|
+
const { data: rows, isPending, error } = usePeerAudit(hostId);
|
|
76
|
+
return (
|
|
77
|
+
<Dialog open={!!hostId} onOpenChange={(o) => !o && onClose()}>
|
|
78
|
+
<DialogContent className={cn(SHEET, "max-h-[85vh] [&>*]:min-w-0")}>
|
|
79
|
+
<DialogHeader>
|
|
80
|
+
<DialogTitle>Activity with {name}</DialogTitle>
|
|
81
|
+
<DialogDescription>
|
|
82
|
+
Calls between this machine and {name} over the last 30 days. Reads
|
|
83
|
+
repeated within an hour are counted on one line.
|
|
84
|
+
</DialogDescription>
|
|
85
|
+
</DialogHeader>
|
|
86
|
+
<div className="-mx-1 max-h-[60vh] overflow-y-auto px-1">
|
|
87
|
+
{isPending && hostId ? (
|
|
88
|
+
<div className="space-y-2" aria-label="Loading">
|
|
89
|
+
{[0, 1, 2].map((i) => (
|
|
90
|
+
<div
|
|
91
|
+
key={i}
|
|
92
|
+
className="bg-muted/40 h-10 animate-pulse rounded-md"
|
|
93
|
+
/>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
) : error ? (
|
|
97
|
+
<p className="text-destructive text-sm">{error.message}</p>
|
|
98
|
+
) : !rows?.length ? (
|
|
99
|
+
<p className="text-muted-foreground text-sm">No calls yet.</p>
|
|
100
|
+
) : (
|
|
101
|
+
<ul className="space-y-0.5">
|
|
102
|
+
{rows.map((r, i) => (
|
|
103
|
+
<Row key={`${r.at}-${i}`} row={r} />
|
|
104
|
+
))}
|
|
105
|
+
</ul>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</DialogContent>
|
|
109
|
+
</Dialog>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { ChevronDown, Folder, MoreHorizontal } from "lucide-react";
|
|
5
|
+
import type { GroupShelf } from "@/lib/sidebar/shelves";
|
|
6
|
+
import {
|
|
7
|
+
useDeleteGroup,
|
|
8
|
+
useReorderGroups,
|
|
9
|
+
useUpdateGroup,
|
|
10
|
+
} from "@/data/session-groups";
|
|
11
|
+
import { Input } from "@/components/ui/input";
|
|
12
|
+
import {
|
|
13
|
+
DropdownMenu,
|
|
14
|
+
DropdownMenuContent,
|
|
15
|
+
DropdownMenuItem,
|
|
16
|
+
DropdownMenuSeparator,
|
|
17
|
+
DropdownMenuTrigger,
|
|
18
|
+
} from "@/components/ui/dropdown-menu";
|
|
19
|
+
import { cn } from "@/lib/utils";
|
|
20
|
+
import { SessionRow } from "./SessionRow";
|
|
21
|
+
|
|
22
|
+
function GroupHeader({
|
|
23
|
+
shelf,
|
|
24
|
+
siblings,
|
|
25
|
+
workspaceName,
|
|
26
|
+
}: {
|
|
27
|
+
shelf: GroupShelf;
|
|
28
|
+
siblings: GroupShelf[];
|
|
29
|
+
workspaceName?: string;
|
|
30
|
+
}) {
|
|
31
|
+
const { group, rows, rollup } = shelf;
|
|
32
|
+
const update = useUpdateGroup();
|
|
33
|
+
const reorder = useReorderGroups();
|
|
34
|
+
const remove = useDeleteGroup();
|
|
35
|
+
const [renaming, setRenaming] = useState(false);
|
|
36
|
+
const [name, setName] = useState(group.name);
|
|
37
|
+
const at = siblings.findIndex((s) => s.group.id === group.id);
|
|
38
|
+
const move = (by: -1 | 1) => {
|
|
39
|
+
const ids = siblings.map((s) => s.group.id);
|
|
40
|
+
ids.splice(at + by, 0, ids.splice(at, 1)[0]);
|
|
41
|
+
reorder.mutate({ workspaceId: group.workspace_id, ids });
|
|
42
|
+
};
|
|
43
|
+
const commit = () => {
|
|
44
|
+
const next = name.trim();
|
|
45
|
+
setRenaming(false);
|
|
46
|
+
if (next && next !== group.name)
|
|
47
|
+
update.mutate({ id: group.id, name: next });
|
|
48
|
+
else setName(group.name);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (renaming)
|
|
52
|
+
return (
|
|
53
|
+
<div className="px-2.5 pt-3.5 pb-1.5">
|
|
54
|
+
<Input
|
|
55
|
+
autoFocus
|
|
56
|
+
value={name}
|
|
57
|
+
maxLength={60}
|
|
58
|
+
aria-label="Group name"
|
|
59
|
+
className="h-11 md:h-8"
|
|
60
|
+
onChange={(e) => setName(e.target.value)}
|
|
61
|
+
onBlur={commit}
|
|
62
|
+
onKeyDown={(e) => {
|
|
63
|
+
if (e.key === "Enter") commit();
|
|
64
|
+
if (e.key === "Escape") {
|
|
65
|
+
setName(group.name);
|
|
66
|
+
setRenaming(false);
|
|
67
|
+
}
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="text-muted-foreground/70 group/header flex items-center pr-1 pl-2.5">
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={() =>
|
|
78
|
+
update.mutate({ id: group.id, collapsed: !group.collapsed })
|
|
79
|
+
}
|
|
80
|
+
aria-expanded={!group.collapsed}
|
|
81
|
+
className="hover:text-foreground flex min-h-11 min-w-0 flex-1 items-center gap-1 pt-2 text-left text-[11px] font-medium tracking-[0.08em] uppercase md:min-h-8"
|
|
82
|
+
>
|
|
83
|
+
<Folder aria-hidden className="h-3 w-3 shrink-0" />
|
|
84
|
+
<span className="truncate">{group.name}</span>
|
|
85
|
+
<span className="shrink-0">· {rows.length}</span>
|
|
86
|
+
{rollup.needsYou > 0 && (
|
|
87
|
+
<span className="shrink-0 text-amber-600 normal-case dark:text-amber-400">
|
|
88
|
+
· {rollup.needsYou} needs you
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
{workspaceName && (
|
|
92
|
+
<span className="bg-muted text-muted-foreground ml-1 truncate rounded px-1.5 py-0.5 text-[10px] normal-case">
|
|
93
|
+
{workspaceName}
|
|
94
|
+
</span>
|
|
95
|
+
)}
|
|
96
|
+
<ChevronDown
|
|
97
|
+
aria-hidden
|
|
98
|
+
className={cn(
|
|
99
|
+
"h-3 w-3 shrink-0 transition-transform",
|
|
100
|
+
group.collapsed && "-rotate-90"
|
|
101
|
+
)}
|
|
102
|
+
/>
|
|
103
|
+
</button>
|
|
104
|
+
<DropdownMenu>
|
|
105
|
+
<DropdownMenuTrigger asChild>
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
aria-label={`${group.name} options`}
|
|
109
|
+
className="hover:bg-muted flex h-11 w-11 shrink-0 items-center justify-center rounded-md md:h-8 md:w-8 md:opacity-0 md:group-hover/header:opacity-100 md:focus-visible:opacity-100 md:data-[state=open]:opacity-100 [@media(hover:none)]:md:h-11 [@media(hover:none)]:md:w-11 [@media(hover:none)]:md:opacity-100"
|
|
110
|
+
>
|
|
111
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
112
|
+
</button>
|
|
113
|
+
</DropdownMenuTrigger>
|
|
114
|
+
<DropdownMenuContent align="end" className="w-44">
|
|
115
|
+
<DropdownMenuItem
|
|
116
|
+
className="min-h-11 md:min-h-8"
|
|
117
|
+
onSelect={() => setRenaming(true)}
|
|
118
|
+
>
|
|
119
|
+
Rename
|
|
120
|
+
</DropdownMenuItem>
|
|
121
|
+
<DropdownMenuItem
|
|
122
|
+
className="min-h-11 md:min-h-8"
|
|
123
|
+
disabled={at <= 0}
|
|
124
|
+
onSelect={() => move(-1)}
|
|
125
|
+
>
|
|
126
|
+
Move up
|
|
127
|
+
</DropdownMenuItem>
|
|
128
|
+
<DropdownMenuItem
|
|
129
|
+
className="min-h-11 md:min-h-8"
|
|
130
|
+
disabled={at < 0 || at >= siblings.length - 1}
|
|
131
|
+
onSelect={() => move(1)}
|
|
132
|
+
>
|
|
133
|
+
Move down
|
|
134
|
+
</DropdownMenuItem>
|
|
135
|
+
<DropdownMenuItem
|
|
136
|
+
className="min-h-11 md:min-h-8"
|
|
137
|
+
onSelect={() =>
|
|
138
|
+
update.mutate({ id: group.id, collapsed: !group.collapsed })
|
|
139
|
+
}
|
|
140
|
+
>
|
|
141
|
+
{group.collapsed ? "Unfold" : "Fold"}
|
|
142
|
+
</DropdownMenuItem>
|
|
143
|
+
<DropdownMenuSeparator />
|
|
144
|
+
<DropdownMenuItem
|
|
145
|
+
variant="destructive"
|
|
146
|
+
className="min-h-11 md:min-h-8"
|
|
147
|
+
onSelect={() => remove.mutate({ id: group.id, name: group.name })}
|
|
148
|
+
>
|
|
149
|
+
Delete group
|
|
150
|
+
</DropdownMenuItem>
|
|
151
|
+
</DropdownMenuContent>
|
|
152
|
+
</DropdownMenu>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The user's groups, in order, between Pinned and the status shelves. A
|
|
158
|
+
// folded group keeps its header and counts; an auto-settled member is muted.
|
|
159
|
+
export function GroupShelves({
|
|
160
|
+
groups,
|
|
161
|
+
workspaceNames,
|
|
162
|
+
}: {
|
|
163
|
+
groups: GroupShelf[];
|
|
164
|
+
// Set when more than one workspace's groups show, to tag each header.
|
|
165
|
+
workspaceNames?: Map<string, string>;
|
|
166
|
+
}) {
|
|
167
|
+
return groups.map((shelf) => {
|
|
168
|
+
const { group } = shelf;
|
|
169
|
+
const siblings = groups.filter(
|
|
170
|
+
(g) => g.group.workspace_id === group.workspace_id
|
|
171
|
+
);
|
|
172
|
+
const empty = !group.collapsed && shelf.rows.length === 0;
|
|
173
|
+
return (
|
|
174
|
+
<section key={group.id} aria-label={group.name}>
|
|
175
|
+
<GroupHeader
|
|
176
|
+
shelf={shelf}
|
|
177
|
+
siblings={siblings}
|
|
178
|
+
workspaceName={
|
|
179
|
+
workspaceNames && group.workspace_id
|
|
180
|
+
? workspaceNames.get(group.workspace_id)
|
|
181
|
+
: undefined
|
|
182
|
+
}
|
|
183
|
+
/>
|
|
184
|
+
{empty && (
|
|
185
|
+
<p className="text-muted-foreground/70 px-2.5 pb-2 text-xs">
|
|
186
|
+
Nothing here yet · move a session in from its ⋯ menu
|
|
187
|
+
</p>
|
|
188
|
+
)}
|
|
189
|
+
{(group.collapsed ? [] : shelf.rows).map((r) => (
|
|
190
|
+
<SessionRow key={r.session.id} row={r} muted={r.settled === "auto"} />
|
|
191
|
+
))}
|
|
192
|
+
</section>
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
}
|