@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
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Check, Folder, Plus, X } from "lucide-react";
|
|
5
|
+
import { useSessionsQuery } from "@/data/sessions";
|
|
6
|
+
import { useAssignGroup, useCreateGroup } from "@/data/session-groups";
|
|
7
|
+
import { Input } from "@/components/ui/input";
|
|
8
|
+
import { Expander, MenuItem, icon, type MenuKind } from "./RowMenuParts";
|
|
9
|
+
|
|
10
|
+
// "Move to group": the groups of the session's workspace with the current
|
|
11
|
+
// one ticked, a way out, and an inline field that makes a new group and
|
|
12
|
+
// files the session into it.
|
|
13
|
+
export function MoveToGroupMenu({
|
|
14
|
+
kind,
|
|
15
|
+
sessionId,
|
|
16
|
+
groupId,
|
|
17
|
+
workspaceId,
|
|
18
|
+
}: {
|
|
19
|
+
kind: MenuKind;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
groupId: string | null | undefined;
|
|
22
|
+
workspaceId: string | null;
|
|
23
|
+
}) {
|
|
24
|
+
const { data } = useSessionsQuery();
|
|
25
|
+
const assign = useAssignGroup();
|
|
26
|
+
const create = useCreateGroup();
|
|
27
|
+
const [naming, setNaming] = useState(false);
|
|
28
|
+
const [name, setName] = useState("");
|
|
29
|
+
const groups = (data?.sessionGroups ?? [])
|
|
30
|
+
.filter((g) => g.workspace_id === workspaceId)
|
|
31
|
+
.sort((a, b) => a.sort_order - b.sort_order);
|
|
32
|
+
|
|
33
|
+
const make = async () => {
|
|
34
|
+
const next = name.trim();
|
|
35
|
+
if (!next) return;
|
|
36
|
+
const { sessionGroup } = await create.mutateAsync({
|
|
37
|
+
name: next,
|
|
38
|
+
workspaceId,
|
|
39
|
+
});
|
|
40
|
+
assign.mutate({ ids: [sessionId], groupId: sessionGroup.id });
|
|
41
|
+
setNaming(false);
|
|
42
|
+
setName("");
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Expander kind={kind} icon={Folder} label="Move to group">
|
|
47
|
+
{groups.map((g) => (
|
|
48
|
+
<MenuItem
|
|
49
|
+
key={g.id}
|
|
50
|
+
kind={kind}
|
|
51
|
+
onClick={() =>
|
|
52
|
+
assign.mutate({
|
|
53
|
+
ids: [sessionId],
|
|
54
|
+
groupId: g.id === groupId ? null : g.id,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
>
|
|
58
|
+
<Check className={`${icon} ${g.id === groupId ? "" : "invisible"}`} />
|
|
59
|
+
<span className="truncate">{g.name}</span>
|
|
60
|
+
</MenuItem>
|
|
61
|
+
))}
|
|
62
|
+
{groupId && (
|
|
63
|
+
<MenuItem
|
|
64
|
+
kind={kind}
|
|
65
|
+
onClick={() => assign.mutate({ ids: [sessionId], groupId: null })}
|
|
66
|
+
>
|
|
67
|
+
<X className={icon} />
|
|
68
|
+
Remove from group
|
|
69
|
+
</MenuItem>
|
|
70
|
+
)}
|
|
71
|
+
{naming ? (
|
|
72
|
+
<div className="px-1.5 py-1">
|
|
73
|
+
<Input
|
|
74
|
+
autoFocus
|
|
75
|
+
value={name}
|
|
76
|
+
maxLength={60}
|
|
77
|
+
placeholder="Group name"
|
|
78
|
+
aria-label="New group name"
|
|
79
|
+
className="h-11 md:h-8"
|
|
80
|
+
onChange={(e) => setName(e.target.value)}
|
|
81
|
+
onKeyDown={(e) => {
|
|
82
|
+
// Keys here are typing, not the menu's typeahead.
|
|
83
|
+
e.stopPropagation();
|
|
84
|
+
if (e.key === "Enter") void make();
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
<p className="text-muted-foreground/70 px-1 pt-1 text-[11px]">
|
|
88
|
+
↵ create
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
) : (
|
|
92
|
+
<MenuItem
|
|
93
|
+
kind={kind}
|
|
94
|
+
onSelect={(e) => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
setNaming(true);
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<Plus className={icon} />
|
|
100
|
+
New group…
|
|
101
|
+
</MenuItem>
|
|
102
|
+
)}
|
|
103
|
+
</Expander>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -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";
|
|
@@ -9,6 +9,7 @@ import { headerParts } from "@/lib/orchestrator/header-line";
|
|
|
9
9
|
import { cn } from "@/lib/utils";
|
|
10
10
|
import { usageLabel, useSessionUsage } from "@/data/load";
|
|
11
11
|
import { useRowContext } from "./RowContext";
|
|
12
|
+
import { OverlapPill } from "@/components/Claims/OverlapPill";
|
|
12
13
|
import { pauseSubtitle } from "@/lib/sidebar/paused";
|
|
13
14
|
|
|
14
15
|
const ATTENTION = (part: string) => part === "paused" || /\basks?$/.test(part);
|
|
@@ -22,11 +23,14 @@ export function RowSubtitle({
|
|
|
22
23
|
detail,
|
|
23
24
|
live,
|
|
24
25
|
hideHost = false,
|
|
26
|
+
groupName,
|
|
25
27
|
}: {
|
|
26
28
|
session: Session;
|
|
27
29
|
detail?: string | null;
|
|
28
30
|
live?: LiveInput;
|
|
29
31
|
hideHost?: boolean;
|
|
32
|
+
// Set on a finished session that keeps its group: Done shows where it goes back to.
|
|
33
|
+
groupName?: string;
|
|
30
34
|
}) {
|
|
31
35
|
const ctx = useRowContext();
|
|
32
36
|
// What its processes use, on a busy session: "3.1 cores · 4.2 GB".
|
|
@@ -63,6 +67,7 @@ export function RowSubtitle({
|
|
|
63
67
|
return (
|
|
64
68
|
<span className="flex min-w-0 items-center gap-1.5">
|
|
65
69
|
<LiveLine {...live} />
|
|
70
|
+
{session.overlap && <OverlapPill overlap={session.overlap} />}
|
|
66
71
|
{pause && (
|
|
67
72
|
<span className="flex min-w-0 items-center gap-1 text-xs text-amber-600 dark:text-amber-400">
|
|
68
73
|
<Pause aria-hidden className="h-3 w-3 shrink-0" />
|
|
@@ -77,6 +82,16 @@ export function RowSubtitle({
|
|
|
77
82
|
</span>
|
|
78
83
|
</span>
|
|
79
84
|
)}
|
|
85
|
+
{(session.waiting_on || session.waited_by) && (
|
|
86
|
+
<span className="text-muted-foreground flex min-w-0 items-center gap-1 text-xs">
|
|
87
|
+
<Hourglass aria-hidden className="h-3 w-3 shrink-0" />
|
|
88
|
+
<span className="truncate">
|
|
89
|
+
{session.waiting_on
|
|
90
|
+
? `waiting on ${session.waiting_on}`
|
|
91
|
+
: `${session.waited_by} waiting on it`}
|
|
92
|
+
</span>
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
80
95
|
{group && (
|
|
81
96
|
<span
|
|
82
97
|
className="text-muted-foreground/70 flex min-w-0 shrink-0 items-center gap-1 truncate text-xs @[14rem]:shrink"
|
|
@@ -92,13 +107,14 @@ export function RowSubtitle({
|
|
|
92
107
|
</span>
|
|
93
108
|
</span>
|
|
94
109
|
)}
|
|
95
|
-
{(project || detail || usage || session.placed_note) && (
|
|
110
|
+
{(project || detail || usage || session.placed_note || groupName) && (
|
|
96
111
|
<span className="text-muted-foreground/70 min-w-0 truncate text-xs">
|
|
97
112
|
{[
|
|
98
113
|
usage && usageLabel(usage),
|
|
99
114
|
detail?.slice(0, 200),
|
|
100
115
|
session.placed_note,
|
|
101
116
|
project,
|
|
117
|
+
groupName,
|
|
102
118
|
]
|
|
103
119
|
.filter(Boolean)
|
|
104
120
|
.join(" · ")}
|