@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
@@ -0,0 +1,196 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { toast } from "sonner";
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from "@/components/ui/dialog";
13
+ import {
14
+ AvailabilityDot,
15
+ LocalityBadge,
16
+ } from "@/components/Models/ModelBadges";
17
+ import { RoleModelSelect } from "@/components/Models/RoleModelSelect";
18
+ import { useCrewQuery, useSwitchSessionModel } from "@/data/models";
19
+ import { useLaunchSession } from "@/data/sessions";
20
+ import { usePanes } from "@/contexts/PaneContext";
21
+ import type { Session } from "@/lib/db";
22
+ import {
23
+ AGENT_ROLES,
24
+ AGENT_ROLE_DEFS,
25
+ isAgentRole,
26
+ } from "@/lib/models/agent-roles";
27
+ import { cn } from "@/lib/utils";
28
+ import { uuid } from "@/lib/uuid";
29
+
30
+ const modelName = (ref: string | null) =>
31
+ ref ? ref.split("/").slice(1).join("/") : "model";
32
+
33
+ // The role a running session was started in and the model it runs on, with
34
+ // its crew: the sessions it works with, by @name. Nothing for a session
35
+ // started without a role. `compact` is a phone's small button.
36
+ export function RolePill({
37
+ session,
38
+ paneId,
39
+ compact,
40
+ }: {
41
+ session: Session | null | undefined;
42
+ paneId?: string;
43
+ compact?: boolean;
44
+ }) {
45
+ const [open, setOpen] = useState(false);
46
+ const role = isAgentRole(session?.agent_role) ? session.agent_role : null;
47
+ const crew = useCrewQuery(session?.id, !!role);
48
+ const switchModel = useSwitchSessionModel(session?.id ?? "");
49
+ const launch = useLaunchSession();
50
+ const { attachSession } = usePanes();
51
+ if (!session || !role) return null;
52
+ const me = crew.data?.members.find((m) => m.id === session.id);
53
+ const down = me?.availability === "down";
54
+ const label = AGENT_ROLE_DEFS[role].label;
55
+ const others = crew.data?.members.filter((m) => m.id !== session.id) ?? [];
56
+
57
+ const add = async (next: (typeof AGENT_ROLES)[number]) => {
58
+ try {
59
+ const { session: made } = await launch.mutateAsync({
60
+ id: uuid(),
61
+ projectId: session.project_id,
62
+ agentType: "claude",
63
+ model: "",
64
+ access: "full",
65
+ useWorktree: false,
66
+ baseBranch: null,
67
+ prompt: "",
68
+ agentRole: next,
69
+ crewId: session.crew_id ?? session.id,
70
+ });
71
+ if (paneId) attachSession(paneId, made.id, made.tmux_name ?? "");
72
+ setOpen(false);
73
+ } catch (error) {
74
+ toast.error(error instanceof Error ? error.message : String(error));
75
+ }
76
+ };
77
+
78
+ return (
79
+ <>
80
+ <button
81
+ type="button"
82
+ onClick={(e) => {
83
+ e.stopPropagation();
84
+ setOpen(true);
85
+ }}
86
+ aria-label={`${label}, ${modelName(me?.ref ?? null)}${down ? ", model not answering" : ""}`}
87
+ className={cn(
88
+ "ring-border/70 flex shrink-0 items-center gap-1.5 rounded-full text-xs ring-1 ring-inset",
89
+ down
90
+ ? "bg-amber-500/10 text-amber-600 dark:text-amber-400"
91
+ : "bg-muted/60",
92
+ compact
93
+ ? "-my-1.5 h-11 w-11 justify-center rounded-lg ring-0"
94
+ : "h-7 max-w-64 px-2.5"
95
+ )}
96
+ >
97
+ <span className="bg-accent text-primary grid h-4 w-4 shrink-0 place-items-center rounded text-[10px] font-semibold">
98
+ {label[0]}
99
+ </span>
100
+ {!compact && (
101
+ <>
102
+ <span className="truncate">
103
+ {label}
104
+ {me?.ref ? ` · ${modelName(me.ref)}` : ""}
105
+ </span>
106
+ {me && !down && <LocalityBadge locality={me.locality} />}
107
+ {down && <span className="shrink-0">unreachable</span>}
108
+ </>
109
+ )}
110
+ {compact && down && (
111
+ <span className="bg-destructive absolute h-2 w-2 translate-x-3 -translate-y-3 rounded-full" />
112
+ )}
113
+ </button>
114
+ <Dialog open={open} onOpenChange={setOpen}>
115
+ <DialogContent>
116
+ <DialogHeader>
117
+ <DialogTitle>{label} · role and crew</DialogTitle>
118
+ <DialogDescription>
119
+ {AGENT_ROLE_DEFS[role].purpose}
120
+ </DialogDescription>
121
+ </DialogHeader>
122
+ <div className="bg-muted/40 space-y-2 rounded-xl p-3">
123
+ <div className="flex items-center justify-between gap-2">
124
+ <span className="flex min-w-0 items-center gap-2 text-sm font-medium">
125
+ <AvailabilityDot
126
+ availability={{ state: me?.availability ?? "unknown" }}
127
+ />
128
+ <span className="truncate">{modelName(me?.ref ?? null)}</span>
129
+ </span>
130
+ {me && <LocalityBadge locality={me.locality} />}
131
+ </div>
132
+ {down && (
133
+ <p role="alert" className="text-destructive text-xs">
134
+ This model&apos;s server isn&apos;t answering. Your conversation
135
+ is kept: switch to another model to carry on.
136
+ </p>
137
+ )}
138
+ <RoleModelSelect
139
+ value={me?.ref ?? null}
140
+ inheritLabel="Switch model…"
141
+ disabled={switchModel.isPending}
142
+ onChange={(ref) =>
143
+ ref &&
144
+ switchModel.mutate(ref, {
145
+ onError: (e) => toast.error(e.message),
146
+ onSuccess: () => toast.success("Switched model"),
147
+ })
148
+ }
149
+ />
150
+ </div>
151
+ <div className="space-y-1.5">
152
+ <h3 className="text-muted-foreground text-xs font-medium uppercase">
153
+ Crew
154
+ </h3>
155
+ {others.length === 0 && (
156
+ <p className="text-muted-foreground text-sm">
157
+ Nobody else yet. Add a role to work with this session.
158
+ </p>
159
+ )}
160
+ {others.map((m) => (
161
+ <div key={m.id} className="flex items-center gap-2 text-sm">
162
+ <span className="min-w-0 flex-1">
163
+ <span className="block truncate">{m.name}</span>
164
+ <span className="text-muted-foreground block truncate font-mono text-xs">
165
+ {m.agentRole ? `@${m.agentRole}` : "no role"} ·{" "}
166
+ {modelName(m.ref)}
167
+ </span>
168
+ </span>
169
+ <AvailabilityDot availability={{ state: m.availability }} />
170
+ <LocalityBadge locality={m.locality} />
171
+ </div>
172
+ ))}
173
+ </div>
174
+ <div className="flex flex-wrap gap-2">
175
+ {AGENT_ROLES.filter(
176
+ (r) =>
177
+ r !== role &&
178
+ r !== "supervisor" &&
179
+ !others.some((m) => m.agentRole === r)
180
+ ).map((r) => (
181
+ <Button
182
+ key={r}
183
+ variant="outline"
184
+ className="h-11 md:h-9"
185
+ disabled={launch.isPending}
186
+ onClick={() => void add(r)}
187
+ >
188
+ Add {AGENT_ROLE_DEFS[r].label}
189
+ </Button>
190
+ ))}
191
+ </div>
192
+ </DialogContent>
193
+ </Dialog>
194
+ </>
195
+ );
196
+ }
@@ -19,6 +19,7 @@ import {
19
19
  FlaskConical,
20
20
  RefreshCw,
21
21
  Smartphone,
22
+ Users,
22
23
  X,
23
24
  type LucideIcon,
24
25
  } from "lucide-react";
@@ -34,6 +35,7 @@ import { AuditsPanel } from "@/components/Audits/AuditsPanel";
34
35
  import { HealthPanel } from "@/components/Health/HealthPanel";
35
36
  import { DevicesPanel } from "@/components/Devices";
36
37
  import { GlobalMergeSection } from "@/components/Merge/GlobalMergeSection";
38
+ import { AgentRolesSection } from "@/components/Models/AgentRolesSection";
37
39
  import { ModelsSection } from "@/components/Models/ModelsSection";
38
40
  import { SharedLinksPanel } from "@/components/Shares/SharedLinksPanel";
39
41
  import { SchedulesPanel } from "@/components/Schedules";
@@ -87,6 +89,11 @@ export const SECTION_INFO: Record<
87
89
  hint: "Which model each kind of work runs on, and providers",
88
90
  icon: Cpu,
89
91
  },
92
+ "agent-roles": {
93
+ label: "Agent roles",
94
+ hint: "Developer, Analyst, Reviewer and Supervisor: a model each, local or cloud",
95
+ icon: Users,
96
+ },
90
97
  notifications: {
91
98
  label: "Notifications",
92
99
  hint: "Sound, browser alerts, your phone",
@@ -145,6 +152,8 @@ function SectionBody({
145
152
  return <WorkspaceSettings />;
146
153
  case "models":
147
154
  return <ModelsSection />;
155
+ case "agent-roles":
156
+ return <AgentRolesSection />;
148
157
  case "devices":
149
158
  return <DevicesPanel />;
150
159
  case "shares":
@@ -28,6 +28,7 @@ import {
28
28
  type SharePreview,
29
29
  } from "@/data/shares";
30
30
  import { shareUi, shareUiActions } from "@/stores/shareUi";
31
+ import { exportUiActions } from "@/stores/exportUi";
31
32
  import {
32
33
  DEFAULT_EXPIRY,
33
34
  EXPIRIES,
@@ -271,6 +272,18 @@ function ShareBody({
271
272
  {error && preview && (
272
273
  <p className="text-destructive text-sm">{error}</p>
273
274
  )}
275
+ {kind === "chat" && sessionId && (
276
+ <button
277
+ type="button"
278
+ onClick={() => {
279
+ shareUiActions.close();
280
+ exportUiActions.open(sessionId, target.title);
281
+ }}
282
+ className="text-muted-foreground hover:text-foreground min-h-11 self-start text-xs underline underline-offset-2"
283
+ >
284
+ Export for AI instead…
285
+ </button>
286
+ )}
274
287
  </div>
275
288
  </div>
276
289
 
@@ -0,0 +1,46 @@
1
+ import type { ReactNode } from "react";
2
+ import { AlertTriangle, Info } from "lucide-react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const TONES = {
6
+ info: {
7
+ box: "bg-muted/60",
8
+ icon: <Info className="text-muted-foreground mt-0.5 h-4 w-4 shrink-0" />,
9
+ },
10
+ warning: {
11
+ box: "bg-amber-500/10",
12
+ icon: (
13
+ <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
14
+ ),
15
+ },
16
+ } as const;
17
+
18
+ /** A short message in a tinted box, with an optional action on the right. */
19
+ export function Notice({
20
+ tone = "info",
21
+ action,
22
+ className,
23
+ children,
24
+ }: {
25
+ tone?: keyof typeof TONES;
26
+ action?: ReactNode;
27
+ className?: string;
28
+ children: ReactNode;
29
+ }) {
30
+ return (
31
+ <div
32
+ role="status"
33
+ className={cn(
34
+ "flex flex-col gap-2 rounded-lg p-3 text-sm sm:flex-row sm:items-center",
35
+ TONES[tone].box,
36
+ className
37
+ )}
38
+ >
39
+ <div className="flex min-w-0 flex-1 gap-2">
40
+ {TONES[tone].icon}
41
+ <div className="min-w-0 flex-1">{children}</div>
42
+ </div>
43
+ {action}
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,39 @@
1
+ import type { ComponentProps } from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ const TONES = {
5
+ amber: "bg-amber-500/15 text-amber-700 dark:text-amber-400",
6
+ muted: "bg-muted text-muted-foreground",
7
+ } as const;
8
+
9
+ /** A small coloured pill for a status. A button when it has an onClick. */
10
+ export function StatusPill({
11
+ tone,
12
+ className,
13
+ onClick,
14
+ ...props
15
+ }: Omit<ComponentProps<"span">, "onClick"> & {
16
+ tone: keyof typeof TONES;
17
+ onClick?: () => void;
18
+ }) {
19
+ const classes = cn(
20
+ "inline-flex max-w-full min-w-0 items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-semibold",
21
+ TONES[tone],
22
+ className
23
+ );
24
+ if (!onClick) return <span className={classes} {...props} />;
25
+ return (
26
+ <button
27
+ type="button"
28
+ onClick={(e) => {
29
+ e.stopPropagation();
30
+ onClick();
31
+ }}
32
+ className={cn(
33
+ classes,
34
+ "relative cursor-pointer before:absolute before:-inset-x-1 before:-inset-y-3 hover:brightness-110"
35
+ )}
36
+ {...(props as ComponentProps<"button">)}
37
+ />
38
+ );
39
+ }
@@ -0,0 +1,3 @@
1
+ export { analystKeys } from "./keys";
2
+ export { useAnalystLink, useSessionAnalyst } from "./queries";
3
+ export * from "./mutations";
@@ -0,0 +1,6 @@
1
+ export const analystKeys = {
2
+ all: ["analyst"] as const,
3
+ link: (id: string) => [...analystKeys.all, "link", id] as const,
4
+ ofSession: (sessionId: string) =>
5
+ [...analystKeys.all, "session", sessionId] as const,
6
+ };
@@ -0,0 +1,116 @@
1
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import type { AnalystLinkView, Recommendation } from "@/lib/analyst/types";
3
+ import { sessionKeys } from "../sessions/keys";
4
+ import { analystKeys } from "./keys";
5
+
6
+ async function call<T>(
7
+ url: string,
8
+ method: string,
9
+ body?: unknown
10
+ ): Promise<T> {
11
+ const res = await fetch(url, {
12
+ method,
13
+ headers: { "Content-Type": "application/json" },
14
+ body: body === undefined ? undefined : JSON.stringify(body),
15
+ });
16
+ const data = await res.json().catch(() => ({}));
17
+ if (!res.ok) throw new Error(data.error || "Something went wrong");
18
+ return data as T;
19
+ }
20
+
21
+ const link = (id: string) => `/api/analyst/${encodeURIComponent(id)}`;
22
+ const rec = (id: string, recId: string, action: string) =>
23
+ `${link(id)}/recommendations/${encodeURIComponent(recId)}/${action}`;
24
+
25
+ function useRefresh() {
26
+ const client = useQueryClient();
27
+ return () => {
28
+ void client.invalidateQueries({ queryKey: analystKeys.all });
29
+ };
30
+ }
31
+
32
+ export interface StartAnalystInput {
33
+ coderSessionId?: string;
34
+ newCoder?: {
35
+ projectId: string;
36
+ agentType: string;
37
+ model?: string;
38
+ prompt?: string;
39
+ useWorktree?: boolean;
40
+ };
41
+ analystModel?: string;
42
+ reviewLast?: boolean;
43
+ }
44
+
45
+ export function useStartAnalyst() {
46
+ const client = useQueryClient();
47
+ return useMutation({
48
+ mutationFn: (input: StartAnalystInput) =>
49
+ call<{ link: AnalystLinkView }>("/api/analyst", "POST", input).then(
50
+ (r) => r.link
51
+ ),
52
+ onSuccess: () => {
53
+ void client.invalidateQueries({ queryKey: analystKeys.all });
54
+ void client.invalidateQueries({ queryKey: sessionKeys.all });
55
+ },
56
+ });
57
+ }
58
+
59
+ export function useSendRecommendation(linkId: string) {
60
+ const refresh = useRefresh();
61
+ return useMutation({
62
+ mutationFn: ({ recId, text }: { recId: string; text?: string }) =>
63
+ call<{ recommendation: Recommendation }>(
64
+ rec(linkId, recId, "send"),
65
+ "POST",
66
+ text === undefined ? {} : { text }
67
+ ),
68
+ onSettled: refresh,
69
+ });
70
+ }
71
+
72
+ export function useDismissRecommendation(linkId: string) {
73
+ const refresh = useRefresh();
74
+ return useMutation({
75
+ mutationFn: (recId: string) =>
76
+ call(rec(linkId, recId, "dismiss"), "POST", {}),
77
+ onSettled: refresh,
78
+ });
79
+ }
80
+
81
+ export function useAskAgain(linkId: string) {
82
+ const refresh = useRefresh();
83
+ return useMutation({
84
+ mutationFn: (recId: string) =>
85
+ call(rec(linkId, recId, "again"), "POST", {}),
86
+ onSettled: refresh,
87
+ });
88
+ }
89
+
90
+ export function useReviewNow(linkId: string) {
91
+ const refresh = useRefresh();
92
+ return useMutation({
93
+ mutationFn: () => call(`${link(linkId)}/review`, "POST", {}),
94
+ onSettled: refresh,
95
+ });
96
+ }
97
+
98
+ export function useSetAnalystPaused(linkId: string) {
99
+ const refresh = useRefresh();
100
+ return useMutation({
101
+ mutationFn: (paused: boolean) => call(link(linkId), "PATCH", { paused }),
102
+ onSettled: refresh,
103
+ });
104
+ }
105
+
106
+ export function useEndAnalyst(linkId: string) {
107
+ const client = useQueryClient();
108
+ return useMutation({
109
+ mutationFn: (endAnalyst: boolean) =>
110
+ call(link(linkId), "DELETE", { endAnalyst }),
111
+ onSettled: () => {
112
+ void client.invalidateQueries({ queryKey: analystKeys.all });
113
+ void client.invalidateQueries({ queryKey: sessionKeys.all });
114
+ },
115
+ });
116
+ }
@@ -0,0 +1,35 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import type { AnalystLinkView } from "@/lib/analyst/types";
3
+ import { usePollWhenOffline } from "../push/connection";
4
+ import { analystKeys } from "./keys";
5
+
6
+ async function read(url: string): Promise<AnalystLinkView | null> {
7
+ const res = await fetch(url);
8
+ if (res.status === 404) return null;
9
+ const body = await res.json().catch(() => ({}));
10
+ if (!res.ok) throw new Error(body.error || "Couldn't load the analyst");
11
+ return body.link ?? null;
12
+ }
13
+
14
+ // The pairing a session is in, as coder or analyst, or null. Pushed when a
15
+ // pairing or a card changes; it polls only while the stream is down.
16
+ export function useSessionAnalyst(sessionId: string | undefined) {
17
+ return useQuery({
18
+ queryKey: analystKeys.ofSession(sessionId ?? ""),
19
+ queryFn: () =>
20
+ read(`/api/sessions/${encodeURIComponent(sessionId!)}/analyst`),
21
+ enabled: !!sessionId,
22
+ staleTime: 5000,
23
+ refetchInterval: usePollWhenOffline(15000),
24
+ });
25
+ }
26
+
27
+ export function useAnalystLink(linkId: string | undefined) {
28
+ return useQuery({
29
+ queryKey: analystKeys.link(linkId ?? ""),
30
+ queryFn: () => read(`/api/analyst/${encodeURIComponent(linkId!)}`),
31
+ enabled: !!linkId,
32
+ staleTime: 5000,
33
+ refetchInterval: usePollWhenOffline(15000),
34
+ });
35
+ }
@@ -0,0 +1,2 @@
1
+ export { claimKeys } from "./keys";
2
+ export { useClaims, useCheckStart } from "./queries";
@@ -0,0 +1,5 @@
1
+ export const claimKeys = {
2
+ all: ["claims"] as const,
3
+ list: (project?: string) =>
4
+ [...claimKeys.all, "list", project ?? ""] as const,
5
+ };
@@ -0,0 +1,47 @@
1
+ import { useMutation, useQuery } from "@tanstack/react-query";
2
+ import type { ClaimsView } from "@/lib/claims/view";
3
+ import type { StartWarning } from "@/lib/claims/startWarning";
4
+ import { usePollWhenOffline } from "../push/connection";
5
+ import { claimKeys } from "./keys";
6
+
7
+ // Claims here and on linked machines. Pushed when a claim changes; it polls
8
+ // only while the stream is down.
9
+ export function useClaims(opts: { project?: string; enabled?: boolean } = {}) {
10
+ const { project, enabled = true } = opts;
11
+ return useQuery({
12
+ queryKey: claimKeys.list(project),
13
+ queryFn: async (): Promise<ClaimsView> => {
14
+ const res = await fetch(
15
+ `/api/claims${project ? `?project=${encodeURIComponent(project)}` : ""}`
16
+ );
17
+ if (!res.ok) throw new Error("Failed to load claims");
18
+ return res.json();
19
+ },
20
+ enabled,
21
+ staleTime: 5000,
22
+ refetchInterval: usePollWhenOffline(15000),
23
+ });
24
+ }
25
+
26
+ export interface StartCheckInput {
27
+ projectId: string;
28
+ name?: string;
29
+ prompt: string;
30
+ branch?: string | null;
31
+ }
32
+
33
+ // Who else is on the work a person is about to start. A failure means no
34
+ // warning: it never stops a start.
35
+ export function useCheckStart() {
36
+ return useMutation({
37
+ mutationFn: async (input: StartCheckInput): Promise<StartWarning[]> => {
38
+ const res = await fetch("/api/claims/check", {
39
+ method: "POST",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify(input),
42
+ });
43
+ if (!res.ok) return [];
44
+ return ((await res.json()) as { overlaps: StartWarning[] }).overlaps;
45
+ },
46
+ });
47
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./keys";
2
+ export * from "./queries";
@@ -0,0 +1,3 @@
1
+ export const exportKeys = {
2
+ all: ["exports"] as const,
3
+ };
@@ -0,0 +1,51 @@
1
+ import { useMutation } from "@tanstack/react-query";
2
+ import {
3
+ exportFileName,
4
+ rangeSlug,
5
+ type ExportEstimate,
6
+ type ExportRequest,
7
+ } from "@/lib/export/view";
8
+
9
+ export type {
10
+ ExportEstimate,
11
+ ExportRequest,
12
+ ExportTurnInfo,
13
+ ExportWarning,
14
+ } from "@/lib/export/view";
15
+
16
+ const post = (url: string, body: unknown) =>
17
+ fetch(url, {
18
+ method: "POST",
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify(body),
21
+ });
22
+
23
+ async function failure(res: Response): Promise<never> {
24
+ const data = await res.json().catch(() => ({}));
25
+ throw new Error(data.error || "Export failed");
26
+ }
27
+
28
+ /** What an export would hold: size, turns, warnings. No file text. */
29
+ export function useExportEstimate(sessionId: string) {
30
+ return useMutation({
31
+ mutationFn: async (req: ExportRequest): Promise<ExportEstimate> => {
32
+ const res = await post(`/api/sessions/${sessionId}/export/estimate`, req);
33
+ return res.ok ? res.json() : failure(res);
34
+ },
35
+ });
36
+ }
37
+
38
+ /** The file itself, as the server built it. */
39
+ export async function fetchExport(
40
+ sessionId: string,
41
+ req: ExportRequest,
42
+ title: string,
43
+ sel: { from: number; to: number; of: number } | null
44
+ ): Promise<{ blob: Blob; name: string }> {
45
+ const res = await post(`/api/sessions/${sessionId}/export`, req);
46
+ if (!res.ok) return failure(res);
47
+ return {
48
+ blob: await res.blob(),
49
+ name: exportFileName(title, sel ? rangeSlug(sel) : "all", req.format),
50
+ };
51
+ }