@saadnvd1/agent-os 0.3.700 → 0.3.723

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.
Files changed (306) hide show
  1. package/README.md +2 -0
  2. package/app/api/agent-roles/route.test.ts +67 -0
  3. package/app/api/agent-roles/route.ts +61 -0
  4. package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
  5. package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
  6. package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
  7. package/app/api/analyst/[id]/review/route.ts +22 -0
  8. package/app/api/analyst/[id]/route.ts +44 -0
  9. package/app/api/analyst/route.ts +10 -0
  10. package/app/api/bus/groups/route.ts +44 -0
  11. package/app/api/bus/spawn/route.ts +34 -2
  12. package/app/api/claims/[id]/route.ts +20 -0
  13. package/app/api/claims/check/route.ts +23 -0
  14. package/app/api/claims/check.test.ts +113 -0
  15. package/app/api/claims/exchange/route.ts +42 -0
  16. package/app/api/claims/outside.test.ts +90 -0
  17. package/app/api/claims/route.test.ts +193 -0
  18. package/app/api/claims/route.ts +73 -0
  19. package/app/api/hosts/[id]/route.ts +9 -1
  20. package/app/api/models/availability/route.ts +9 -0
  21. package/app/api/models/choices/route.ts +9 -0
  22. package/app/api/peer/unlink/route.ts +17 -0
  23. package/app/api/peers/[hostId]/audit/route.ts +14 -0
  24. package/app/api/peers/[hostId]/route.ts +20 -0
  25. package/app/api/peers/gate.test.ts +64 -1
  26. package/app/api/peers/pending/route.test.ts +37 -0
  27. package/app/api/peers/pending/route.ts +11 -4
  28. package/app/api/session-groups/[id]/restore/route.ts +11 -0
  29. package/app/api/session-groups/[id]/route.ts +25 -0
  30. package/app/api/session-groups/order/route.ts +15 -0
  31. package/app/api/session-groups/route.ts +21 -0
  32. package/app/api/sessions/[id]/analyst/route.ts +11 -0
  33. package/app/api/sessions/[id]/crew/route.ts +38 -0
  34. package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
  35. package/app/api/sessions/[id]/export/route.test.ts +125 -0
  36. package/app/api/sessions/[id]/export/route.ts +34 -0
  37. package/app/api/sessions/[id]/fork/route.ts +3 -0
  38. package/app/api/sessions/[id]/group/route.ts +18 -0
  39. package/app/api/sessions/[id]/model/route.ts +49 -0
  40. package/app/api/sessions/[id]/route.ts +14 -4
  41. package/app/api/sessions/[id]/summarize/route.ts +2 -0
  42. package/app/api/sessions/claims.test.ts +122 -0
  43. package/app/api/sessions/group/route.ts +17 -0
  44. package/app/api/sessions/route.ts +31 -14
  45. package/app/page.tsx +10 -0
  46. package/components/Chat/Messages.tsx +104 -10
  47. package/components/Claims/ClaimFiles.tsx +38 -0
  48. package/components/Claims/ClaimRow.tsx +82 -0
  49. package/components/Claims/ClaimsSheet.tsx +176 -0
  50. package/components/Claims/OverlapPill.tsx +31 -0
  51. package/components/Claims/StartClaimsNotice.tsx +70 -0
  52. package/components/Claims/claims-ui.test.ts +161 -0
  53. package/components/Claims/format.ts +57 -0
  54. package/components/Claims/index.ts +3 -0
  55. package/components/Draft/DraftChips.tsx +6 -1
  56. package/components/Draft/DraftPanel.tsx +90 -3
  57. package/components/Draft/ModelUnavailableDialog.tsx +78 -0
  58. package/components/Draft/RoleChips.tsx +110 -0
  59. package/components/Export/ExportActions.tsx +136 -0
  60. package/components/Export/ExportSheet.tsx +217 -0
  61. package/components/Export/RangePicker.tsx +144 -0
  62. package/components/Export/Seg.tsx +38 -0
  63. package/components/Export/TurnSelectBar.tsx +45 -0
  64. package/components/Hosts/AccountMachines.tsx +32 -18
  65. package/components/Hosts/HostRow.tsx +155 -14
  66. package/components/Hosts/IncomingLinkSheet.tsx +28 -10
  67. package/components/Hosts/PeerActivitySheet.tsx +111 -0
  68. package/components/Models/AgentRoleCard.tsx +114 -0
  69. package/components/Models/AgentRolesSection.tsx +132 -0
  70. package/components/Models/ModelBadges.tsx +48 -0
  71. package/components/Models/ModelsSection.tsx +4 -2
  72. package/components/Models/RoleModelSelect.tsx +87 -0
  73. package/components/Pane/DesktopTabBar.tsx +2 -0
  74. package/components/Pane/MobileTabBar.tsx +12 -0
  75. package/components/Pane/PaneMenu.tsx +10 -0
  76. package/components/SessionList/GroupShelves.tsx +195 -0
  77. package/components/SessionList/MoveToGroupMenu.tsx +105 -0
  78. package/components/SessionList/RowSubtitle.tsx +7 -1
  79. package/components/SessionList/SelectionToolbar.tsx +52 -1
  80. package/components/SessionList/SessionRow.tsx +19 -7
  81. package/components/SessionList/SessionRowMenu.tsx +23 -0
  82. package/components/SessionList/SessionShelves.tsx +5 -0
  83. package/components/SessionList/SidebarBody.tsx +6 -0
  84. package/components/SessionList/SidebarContextMenu.test.ts +104 -0
  85. package/components/SessionList/SidebarContextMenu.tsx +59 -0
  86. package/components/SessionList/SidebarHeader.tsx +5 -0
  87. package/components/SessionList/WorkspaceSwitcher.tsx +23 -6
  88. package/components/SessionList/index.tsx +18 -14
  89. package/components/SessionList/useSidebarData.ts +10 -0
  90. package/components/Sessions/RolePill.tsx +196 -0
  91. package/components/Settings/SettingsDialog.tsx +9 -0
  92. package/components/Share/ShareDialog.tsx +13 -0
  93. package/components/ui/notice.tsx +46 -0
  94. package/components/ui/status-pill.tsx +39 -0
  95. package/data/analyst/index.ts +3 -0
  96. package/data/analyst/keys.ts +6 -0
  97. package/data/analyst/mutations.ts +116 -0
  98. package/data/analyst/queries.ts +35 -0
  99. package/data/claims/index.ts +2 -0
  100. package/data/claims/keys.ts +5 -0
  101. package/data/claims/queries.ts +47 -0
  102. package/data/exports/index.ts +2 -0
  103. package/data/exports/keys.ts +3 -0
  104. package/data/exports/queries.ts +51 -0
  105. package/data/models/index.ts +98 -0
  106. package/data/peers/keys.ts +1 -0
  107. package/data/peers/queries.ts +31 -0
  108. package/data/push/topics.ts +9 -0
  109. package/data/session-groups/index.ts +156 -0
  110. package/data/sessions/index.ts +0 -1
  111. package/data/sessions/pin.ts +2 -6
  112. package/data/sessions/queries.ts +36 -29
  113. package/data/sessions/shelf.ts +2 -6
  114. package/data/tasks/index.ts +2 -0
  115. package/hooks/useSessions.ts +0 -12
  116. package/lib/__fixtures__/peer-rpc.ts +65 -33
  117. package/lib/agents/brief.ts +1 -0
  118. package/lib/agents/spawn.ts +4 -0
  119. package/lib/analyst/__fixtures__/items.ts +57 -0
  120. package/lib/analyst/__fixtures__/mocks.ts +49 -0
  121. package/lib/analyst/brief.ts +17 -0
  122. package/lib/analyst/buildDigest.ts +204 -0
  123. package/lib/analyst/createLink.ts +98 -0
  124. package/lib/analyst/endLink.ts +31 -0
  125. package/lib/analyst/forward.ts +109 -0
  126. package/lib/analyst/index.ts +16 -0
  127. package/lib/analyst/linkView.ts +38 -0
  128. package/lib/analyst/onTurnEnd.ts +119 -0
  129. package/lib/analyst/parseReview.ts +41 -0
  130. package/lib/analyst/pushDigest.ts +143 -0
  131. package/lib/analyst/resume.ts +35 -0
  132. package/lib/analyst/route.ts +50 -0
  133. package/lib/analyst/signals.ts +121 -0
  134. package/lib/analyst/store.ts +303 -0
  135. package/lib/analyst/types.ts +140 -0
  136. package/lib/bus/condense.ts +5 -1
  137. package/lib/bus/index.ts +13 -0
  138. package/lib/bus/read-scope.ts +12 -0
  139. package/lib/bus/role-alias.ts +70 -0
  140. package/lib/bus/targets.ts +3 -0
  141. package/lib/chat/drivers/opencode-mapper.ts +55 -1
  142. package/lib/chat/drivers/pi-approvals.ts +7 -4
  143. package/lib/chat/drivers/pi-mapper.ts +38 -0
  144. package/lib/chat/drivers/pi-text-tools.ts +104 -0
  145. package/lib/chat/drivers/pi.ts +8 -3
  146. package/lib/chat/drivers/turn-items.ts +36 -11
  147. package/lib/chat/drivers/turn-outcome.ts +108 -0
  148. package/lib/chat/events.ts +8 -0
  149. package/lib/chat/registry.ts +13 -0
  150. package/lib/chat/store.ts +19 -0
  151. package/lib/chat/turns.ts +1 -0
  152. package/lib/chat/worker/extras.ts +4 -2
  153. package/lib/claims/collectLocal.ts +123 -0
  154. package/lib/claims/describe.ts +41 -0
  155. package/lib/claims/exchange.ts +91 -0
  156. package/lib/claims/goal.ts +45 -0
  157. package/lib/claims/index.ts +10 -0
  158. package/lib/claims/loop.ts +51 -0
  159. package/lib/claims/machine.ts +21 -0
  160. package/lib/claims/mine.ts +43 -0
  161. package/lib/claims/notifyOverlaps.ts +101 -0
  162. package/lib/claims/onMerged.ts +85 -0
  163. package/lib/claims/outside.ts +36 -0
  164. package/lib/claims/overlap.ts +87 -0
  165. package/lib/claims/overlapCache.ts +86 -0
  166. package/lib/claims/overlapText.ts +36 -0
  167. package/lib/claims/peers.ts +56 -0
  168. package/lib/claims/startCheck.ts +100 -0
  169. package/lib/claims/startWarning.ts +29 -0
  170. package/lib/claims/store.ts +366 -0
  171. package/lib/claims/types.ts +102 -0
  172. package/lib/claims/view.ts +80 -0
  173. package/lib/claims/yield.ts +24 -0
  174. package/lib/db/changes.ts +12 -0
  175. package/lib/db/migrations/20261020T1400_work_claims.ts +35 -0
  176. package/lib/db/migrations/20261020T1515_peer_unlinked.ts +22 -0
  177. package/lib/db/migrations/20261020T1700_clean_connect_names.ts +18 -0
  178. package/lib/db/migrations/20261020T1800_session_groups.ts +49 -0
  179. package/lib/db/migrations/20261020T1900_task_queue_after_claim.ts +13 -0
  180. package/lib/db/migrations/20261020T2000_claim_notices.ts +20 -0
  181. package/lib/db/migrations/20261020T2100_claims_via_host.ts +21 -0
  182. package/lib/db/migrations/20261020T2200_work_claims_watch.ts +10 -0
  183. package/lib/db/migrations/20261021T1400_session_agent_roles.ts +20 -0
  184. package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
  185. package/lib/db/queries.ts +0 -40
  186. package/lib/db/session-parts/columns.ts +3 -0
  187. package/lib/db/types.ts +19 -8
  188. package/lib/doctor/checks.ts +64 -1
  189. package/lib/done/archive.ts +2 -0
  190. package/lib/drafts.ts +4 -0
  191. package/lib/export/build.ts +470 -0
  192. package/lib/export/compact.ts +156 -0
  193. package/lib/export/estimate.ts +83 -0
  194. package/lib/export/fixtures.ts +45 -0
  195. package/lib/export/html.tsx +195 -0
  196. package/lib/export/json.ts +39 -0
  197. package/lib/export/load.ts +64 -0
  198. package/lib/export/markdown.ts +127 -0
  199. package/lib/export/request.ts +36 -0
  200. package/lib/export/service.ts +142 -0
  201. package/lib/export/summary.ts +86 -0
  202. package/lib/export/turns.ts +63 -0
  203. package/lib/export/view.ts +173 -0
  204. package/lib/git-remote.ts +43 -0
  205. package/lib/hosts/index.ts +10 -1
  206. package/lib/models/agent-binding.ts +102 -0
  207. package/lib/models/agent-roles-view.ts +157 -0
  208. package/lib/models/agent-roles.ts +87 -0
  209. package/lib/models/availability-refs.ts +17 -0
  210. package/lib/models/availability.ts +91 -0
  211. package/lib/models/catalog.ts +156 -0
  212. package/lib/models/config.ts +27 -0
  213. package/lib/models/locality.ts +33 -0
  214. package/lib/models/providers.ts +11 -0
  215. package/lib/models/resolve.ts +50 -12
  216. package/lib/models/roles.ts +12 -0
  217. package/lib/needs-you.ts +1 -1
  218. package/lib/orchestration.ts +2 -0
  219. package/lib/orchestrator/act.ts +54 -4
  220. package/lib/orchestrator/brief.ts +2 -1
  221. package/lib/orchestrator/claim-wait.ts +71 -0
  222. package/lib/orchestrator/conditions.ts +28 -1
  223. package/lib/orchestrator/describe.ts +6 -1
  224. package/lib/orchestrator/facts.ts +12 -2
  225. package/lib/orchestrator/read.ts +5 -1
  226. package/lib/orchestrator/serve.ts +37 -2
  227. package/lib/orchestrator/tool-names.ts +1 -0
  228. package/lib/orchestrator/tool-schemas.ts +31 -1
  229. package/lib/orchestrator/tools.ts +3 -1
  230. package/lib/orchestrator/untrusted.ts +5 -0
  231. package/lib/orchestrator/watcher.ts +19 -1
  232. package/lib/peer/account.ts +74 -2
  233. package/lib/peer/allowlist.ts +1 -1
  234. package/lib/peer/client.ts +26 -1
  235. package/lib/peer/link-cli.ts +19 -4
  236. package/lib/peer/links.ts +117 -5
  237. package/lib/peer/machines.ts +14 -2
  238. package/lib/peer/name.ts +32 -0
  239. package/lib/peer/pairing.ts +34 -5
  240. package/lib/peer/pin.ts +10 -3
  241. package/lib/peer/reasons.ts +7 -0
  242. package/lib/peer/revoke.ts +75 -0
  243. package/lib/peer/rpc-server.ts +53 -5
  244. package/lib/peer/unlink.ts +62 -0
  245. package/lib/peer/unlinked-proof.ts +44 -0
  246. package/lib/projects.ts +6 -2
  247. package/lib/security/demo.ts +7 -0
  248. package/lib/session-artifacts/agentos-files.ts +94 -0
  249. package/lib/session-artifacts/collect.ts +17 -0
  250. package/lib/session-groups/assignSessions.ts +81 -0
  251. package/lib/session-groups/cli.ts +61 -0
  252. package/lib/session-groups/createGroup.ts +28 -0
  253. package/lib/session-groups/index.ts +21 -0
  254. package/lib/session-groups/removeGroup.ts +36 -0
  255. package/lib/session-groups/reorderGroups.ts +31 -0
  256. package/lib/session-groups/resolveGroup.ts +19 -0
  257. package/lib/session-groups/route.ts +32 -0
  258. package/lib/session-groups/store.ts +85 -0
  259. package/lib/session-groups/types.ts +25 -0
  260. package/lib/session-groups/updateGroup.ts +35 -0
  261. package/lib/session-groups/workspace.ts +21 -0
  262. package/lib/sessions/crew.ts +72 -0
  263. package/lib/sessions/launch.ts +54 -11
  264. package/lib/settings-url.ts +1 -0
  265. package/lib/share/markdown.ts +2 -2
  266. package/lib/share/redact.ts +16 -10
  267. package/lib/share/render.tsx +1 -1
  268. package/lib/share/snapshot.ts +2 -2
  269. package/lib/sidebar/rollup.ts +1 -0
  270. package/lib/sidebar/row-menu.ts +8 -1
  271. package/lib/sidebar/shelves.ts +90 -15
  272. package/lib/sidebar/visible-order.ts +5 -0
  273. package/lib/tasks/hold-checks.ts +3 -1
  274. package/lib/tasks/holds.ts +3 -0
  275. package/lib/tasks/index.ts +11 -0
  276. package/lib/tasks/queue-rows.ts +8 -1
  277. package/lib/tasks/queue-start.ts +1 -0
  278. package/lib/tasks/queue-types.ts +9 -1
  279. package/lib/tasks/queue.ts +15 -5
  280. package/lib/tasks/schedule-types.ts +1 -0
  281. package/lib/test-temp.ts +19 -3
  282. package/lib/workspaces.ts +11 -3
  283. package/lib/worktree-deps/caches.ts +3 -2
  284. package/lib/worktree-deps/clone.ts +3 -1
  285. package/lib/worktree-deps/paths.ts +26 -0
  286. package/lib/worktree-sweep/testTemp.ts +23 -106
  287. package/package.json +1 -1
  288. package/scripts/agent-os +1 -1
  289. package/scripts/autodeploy +2 -0
  290. package/scripts/autodeploy.test.ts +31 -1
  291. package/scripts/claim-hook.sh +31 -0
  292. package/scripts/claim-hook.test.ts +230 -0
  293. package/scripts/lib/cli-links.sh +36 -0
  294. package/scripts/lib/commands.sh +3 -0
  295. package/scripts/lib/machine.sh +27 -2
  296. package/scripts/lib/machine.test.ts +25 -3
  297. package/scripts/sync-cli-links +7 -0
  298. package/scripts/sync-cli-links.test.ts +69 -0
  299. package/server.ts +12 -1
  300. package/stores/claimsUi.ts +22 -0
  301. package/stores/exportUi.ts +87 -0
  302. package/app/api/groups/[...path]/route.ts +0 -136
  303. package/app/api/groups/route.ts +0 -93
  304. package/data/groups/index.ts +0 -1
  305. package/data/groups/mutations.ts +0 -95
  306. package/hooks/useGroups.ts +0 -37
@@ -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 { Monitor, Server, Trash2, PlugZap, Loader2, Link2 } from "lucide-react";
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.linked
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
- <Button
74
- variant="ghost"
75
- size="icon-sm"
76
- className="h-11 w-11 sm:h-8 sm:w-8"
77
- onClick={() => deleteHost.mutate(host.id)}
78
- disabled={deleteHost.isPending}
79
- aria-label={viaConnect ? "Unlink and remove" : "Remove machine"}
80
- >
81
- <Trash2 className="h-4 w-4" />
82
- </Button>
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&apos;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&apos;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
- const done = accept.data;
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 first = requests[0];
158
- const hidden = later !== null && requests.every((r) => later.includes(r.id));
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> wants to link
170
- {requests.length > 1 ? ` (+${requests.length - 1} more)` : ""}
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={() => setLater(requests.map((r) => r.id).join(" "))}
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 request={open} onClose={() => setOpen(null)} />
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> wants to link
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,114 @@
1
+ "use client";
2
+
3
+ import { AlertTriangle } from "lucide-react";
4
+ import type { AgentRoleView } from "@/lib/models/agent-roles-view";
5
+ import { cn } from "@/lib/utils";
6
+ import { RoleModelSelect } from "./RoleModelSelect";
7
+
8
+ const SOURCE: Record<string, string> = {
9
+ task: "Set for the task",
10
+ project: "From this project",
11
+ workspace: "From the workspace",
12
+ settings: "Set in Settings",
13
+ env: "From the server's environment",
14
+ inherited: "Same as Gate review until set here",
15
+ default: "Default",
16
+ };
17
+
18
+ const initial = (label: string) => label.slice(0, 1);
19
+
20
+ // One role: what it does, the model it runs on, where that came from, and
21
+ // whether the model's server answers. A role whose local model is down says so.
22
+ export function AgentRoleCard({
23
+ role,
24
+ scope,
25
+ pending,
26
+ onChange,
27
+ onFallback,
28
+ }: {
29
+ role: AgentRoleView;
30
+ scope: "settings" | "workspace" | "project";
31
+ pending: boolean;
32
+ onChange: (ref: string | null) => void;
33
+ onFallback: (ref: string | null) => void;
34
+ }) {
35
+ const down = role.choice?.availability.state === "down";
36
+ const label = `Default (${role.default.split("/").slice(1).join("/")})`;
37
+ const inheritLabel =
38
+ scope === "settings"
39
+ ? role.modelRole === "advise"
40
+ ? "Same as Gate review"
41
+ : label
42
+ : "Inherit";
43
+ return (
44
+ <li
45
+ className={cn(
46
+ "bg-muted/40 space-y-2 rounded-xl px-3 py-3",
47
+ down && "ring-destructive/50 ring-1"
48
+ )}
49
+ >
50
+ <label htmlFor={`agent-role-${role.id}`} className="flex gap-2.5">
51
+ <span className="bg-accent text-primary grid h-8 w-8 shrink-0 place-items-center rounded-lg text-sm font-semibold">
52
+ {initial(role.label)}
53
+ </span>
54
+ <span className="min-w-0">
55
+ <span className="block text-sm font-medium">{role.label}</span>
56
+ <span className="text-muted-foreground block text-xs">
57
+ {role.purpose}
58
+ </span>
59
+ </span>
60
+ </label>
61
+ <RoleModelSelect
62
+ id={`agent-role-${role.id}`}
63
+ value={role.value}
64
+ inheritLabel={
65
+ role.value === null && role.binding
66
+ ? `${inheritLabel} · ${role.binding.ref.split("/").slice(1).join("/")}`
67
+ : inheritLabel
68
+ }
69
+ disabled={pending}
70
+ onChange={onChange}
71
+ />
72
+ <p className="text-muted-foreground text-xs">
73
+ {role.error
74
+ ? role.error
75
+ : role.binding
76
+ ? `${SOURCE[role.binding.source] ?? role.binding.source}${role.choice?.endpoint ? ` · ${role.choice.endpoint}` : ""}`
77
+ : ""}
78
+ </p>
79
+ {down && (
80
+ <div
81
+ role="alert"
82
+ className="bg-destructive/10 text-foreground ring-destructive/35 flex gap-2 rounded-lg p-2.5 text-xs ring-1"
83
+ >
84
+ <AlertTriangle className="text-destructive mt-0.5 h-3.5 w-3.5 shrink-0" />
85
+ <span>
86
+ <b className="font-medium">
87
+ This model&apos;s server isn&apos;t answering.
88
+ </b>{" "}
89
+ {role.choice?.availability.error
90
+ ? `${role.choice.availability.error}. `
91
+ : ""}
92
+ New {role.label} sessions can&apos;t start until it is back
93
+ {role.fallback ? ", or you start on the fallback" : ""}.
94
+ </span>
95
+ </div>
96
+ )}
97
+ {scope === "settings" && (
98
+ <div className="flex flex-wrap items-center gap-2">
99
+ <span className="text-muted-foreground text-xs">Fallback</span>
100
+ <div className="min-w-44 flex-1">
101
+ <RoleModelSelect
102
+ id={`agent-role-fallback-${role.id}`}
103
+ value={role.fallback}
104
+ inheritLabel="None (don't fall back)"
105
+ claudeOnly
106
+ disabled={pending}
107
+ onChange={onFallback}
108
+ />
109
+ </div>
110
+ </div>
111
+ )}
112
+ </li>
113
+ );
114
+ }
@@ -0,0 +1,132 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useAgentRolesQuery, useSaveAgentRoles } from "@/data/models";
5
+ import { useProjectsQuery } from "@/data/projects/queries";
6
+ import { useWorkspacesQuery } from "@/data/workspaces";
7
+ import type { RoleMap } from "@/lib/models/config";
8
+ import type { RoleScope } from "@/lib/models/agent-roles-view";
9
+ import { cn } from "@/lib/utils";
10
+ import { AgentRoleCard } from "./AgentRoleCard";
11
+
12
+ const SCOPES: { id: RoleScope; label: string }[] = [
13
+ { id: "settings", label: "Default" },
14
+ { id: "workspace", label: "Workspace" },
15
+ { id: "project", label: "Project" },
16
+ ];
17
+
18
+ // Settings > Agent roles: the model each of Developer, Analyst, Reviewer and
19
+ // Supervisor runs on, for everything or for one workspace or project.
20
+ export function AgentRolesSection() {
21
+ const [scope, setScope] = useState<RoleScope>("settings");
22
+ const [target, setTarget] = useState<string | null>(null);
23
+ const workspaces = useWorkspacesQuery();
24
+ const projects = useProjectsQuery();
25
+ const options =
26
+ scope === "workspace"
27
+ ? (workspaces.data ?? []).map((w) => ({ id: w.id, name: w.name }))
28
+ : scope === "project"
29
+ ? (projects.data ?? [])
30
+ .filter((p) => !p.is_uncategorized)
31
+ .map((p) => ({ id: p.id, name: p.name }))
32
+ : [];
33
+ const id = scope === "settings" ? null : (target ?? options[0]?.id ?? null);
34
+ const { data, isPending, isError, error } = useAgentRolesQuery(scope, id);
35
+ const save = useSaveAgentRoles(scope, id);
36
+
37
+ const saved = (key: "value" | "fallback"): RoleMap =>
38
+ Object.fromEntries(
39
+ (data?.roles ?? [])
40
+ .filter((r) => r[key])
41
+ .map((r) => [r.modelRole, r[key] as string])
42
+ );
43
+ const setRole = (modelRole: string, ref: string | null) => {
44
+ const roles: RoleMap = { ...saved("value") };
45
+ if (ref) roles[modelRole as keyof RoleMap] = ref;
46
+ else delete roles[modelRole as keyof RoleMap];
47
+ save.mutate({ roles });
48
+ };
49
+ const setFallback = (modelRole: string, ref: string | null) => {
50
+ const fallbacks: RoleMap = { ...saved("fallback") };
51
+ if (ref) fallbacks[modelRole as keyof RoleMap] = ref;
52
+ else delete fallbacks[modelRole as keyof RoleMap];
53
+ save.mutate({ roles: saved("value"), fallbacks });
54
+ };
55
+
56
+ return (
57
+ <div className="space-y-4">
58
+ <div
59
+ role="tablist"
60
+ className="bg-muted inline-flex max-w-full gap-0.5 overflow-x-auto rounded-xl p-0.5"
61
+ >
62
+ {SCOPES.map((s) => (
63
+ <button
64
+ key={s.id}
65
+ role="tab"
66
+ aria-selected={scope === s.id}
67
+ type="button"
68
+ onClick={() => {
69
+ setScope(s.id);
70
+ setTarget(null);
71
+ }}
72
+ className={cn(
73
+ "h-11 rounded-lg px-3 text-sm whitespace-nowrap md:h-8",
74
+ scope === s.id
75
+ ? "bg-background text-foreground ring-border ring-1"
76
+ : "text-muted-foreground hover:text-foreground"
77
+ )}
78
+ >
79
+ {s.label}
80
+ </button>
81
+ ))}
82
+ </div>
83
+ {scope !== "settings" && (
84
+ <select
85
+ aria-label={scope === "workspace" ? "Workspace" : "Project"}
86
+ value={id ?? ""}
87
+ onChange={(e) => setTarget(e.target.value)}
88
+ className="border-input bg-background h-11 w-full rounded-md border px-3 text-sm md:h-9"
89
+ >
90
+ {options.length === 0 && <option value="">None yet</option>}
91
+ {options.map((o) => (
92
+ <option key={o.id} value={o.id}>
93
+ {o.name}
94
+ </option>
95
+ ))}
96
+ </select>
97
+ )}
98
+ <p className="text-muted-foreground text-xs">
99
+ The model each role runs on. A workspace or project can override these,
100
+ and a session can when it starts. A Pi model (a server you run) runs
101
+ sessions started in the role; tasks and the merge gate stay on Claude
102
+ Code models. Planning, gate review, scope check, helpers and watchers
103
+ are in Models.
104
+ </p>
105
+ {scope !== "settings" && !id ? (
106
+ <p className="text-muted-foreground text-sm">
107
+ There is no {scope} to set roles for yet.
108
+ </p>
109
+ ) : isPending ? (
110
+ <div className="bg-muted/40 h-64 animate-pulse rounded-lg" />
111
+ ) : isError ? (
112
+ <p className="text-destructive text-sm">{error.message}</p>
113
+ ) : data ? (
114
+ <ul className="space-y-2.5">
115
+ {data.roles.map((r) => (
116
+ <AgentRoleCard
117
+ key={r.id}
118
+ role={r}
119
+ scope={scope}
120
+ pending={save.isPending}
121
+ onChange={(ref) => setRole(r.modelRole, ref)}
122
+ onFallback={(ref) => setFallback(r.modelRole, ref)}
123
+ />
124
+ ))}
125
+ </ul>
126
+ ) : null}
127
+ {save.isError && (
128
+ <p className="text-destructive text-xs">{save.error.message}</p>
129
+ )}
130
+ </div>
131
+ );
132
+ }