@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,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { AGENT_ROLE_DEFS, isAgentRole } from "@/lib/models/agent-roles";
3
4
  import { isFork } from "@/lib/sessions/fork";
4
5
  import { memo, useState } from "react";
5
6
  import { useSnapshot } from "valtio";
@@ -126,19 +127,30 @@ export const SessionRow = memo(function SessionRow({
126
127
  }}
127
128
  />
128
129
  ) : (
129
- <span
130
- className={cn(
131
- "truncate text-sm",
132
- muted && !active ? "text-muted-foreground" : "text-foreground",
133
- active && "font-medium"
130
+ <span className="flex min-w-0 items-center gap-1.5">
131
+ {isAgentRole(session.agent_role) && (
132
+ <span
133
+ title={AGENT_ROLE_DEFS[session.agent_role].label}
134
+ className="bg-accent text-primary grid h-4 w-4 shrink-0 place-items-center rounded text-[10px] font-semibold"
135
+ >
136
+ {AGENT_ROLE_DEFS[session.agent_role].label[0]}
137
+ </span>
134
138
  )}
135
- >
136
- {session.name}
139
+ <span
140
+ className={cn(
141
+ "truncate text-sm",
142
+ muted && !active ? "text-muted-foreground" : "text-foreground",
143
+ active && "font-medium"
144
+ )}
145
+ >
146
+ {session.name}
147
+ </span>
137
148
  </span>
138
149
  )}
139
150
  <RowSubtitle
140
151
  session={session}
141
152
  hideHost={inMachineShelf}
153
+ groupName={row.groupName}
142
154
  detail={row.status?.detail}
143
155
  live={
144
156
  row.need
@@ -15,6 +15,7 @@ import {
15
15
  KanbanSquare,
16
16
  Link,
17
17
  Share2,
18
+ Download,
18
19
  Loader2,
19
20
  Pencil,
20
21
  Pin,
@@ -49,6 +50,7 @@ import type { SidebarRow } from "@/lib/sidebar/shelves";
49
50
  import { useRowContext } from "./RowContext";
50
51
  import { MoveMenuItems } from "@/components/Tasks/MoveMenuItems";
51
52
  import { shareUiActions } from "@/stores/shareUi";
53
+ import { exportUiActions } from "@/stores/exportUi";
52
54
  import { copySessionLink } from "@/hooks/useCopyToClipboard";
53
55
  import { useLinkedHostNames } from "@/data/hosts";
54
56
  import { remoteBlocks, remoteNote } from "@/lib/hosts/remote-menu";
@@ -61,6 +63,7 @@ import {
61
63
  type RowMenuItem,
62
64
  } from "@/lib/sidebar/row-menu";
63
65
  import { Expander, MenuItem, PARTS, icon, type MenuKind } from "./RowMenuParts";
66
+ import { MoveToGroupMenu } from "./MoveToGroupMenu";
64
67
 
65
68
  const GROUP_ICON: Record<RowMenuGroup, typeof Copy> = {
66
69
  open: ExternalLink,
@@ -105,6 +108,12 @@ export function SessionRowMenu({
105
108
  ctx.projects.find((p) => p.id === session.project_id)?.workspace_id ??
106
109
  null;
107
110
 
111
+ // The server files by the project's workspace first.
112
+ const groupWorkspaceId =
113
+ ctx.projects.find((p) => p.id === session.project_id)?.workspace_id ??
114
+ session.workspace_id ??
115
+ null;
116
+
108
117
  const plan = rowMenu({
109
118
  session,
110
119
  row,
@@ -189,6 +198,10 @@ export function SessionRowMenu({
189
198
  title: session.name,
190
199
  })
191
200
  );
201
+ case "export":
202
+ return item("Export for AI…", Download, () =>
203
+ exportUiActions.open(session.id, session.name)
204
+ );
192
205
  case "fork":
193
206
  return item("Fork session", Copy, () => ctx.onFork(session.id));
194
207
  case "fresh-start":
@@ -222,6 +235,16 @@ export function SessionRowMenu({
222
235
  {projects}
223
236
  </Expander>
224
237
  );
238
+ case "move-group":
239
+ return (
240
+ <MoveToGroupMenu
241
+ key={it}
242
+ kind={kind}
243
+ sessionId={session.id}
244
+ groupId={session.session_group_id}
245
+ workspaceId={groupWorkspaceId}
246
+ />
247
+ );
225
248
  case "check-ins":
226
249
  return item("Schedule check-ins", Clock, () =>
227
250
  schedulesUiActions.openDraft(workspaceId!, {
@@ -14,6 +14,7 @@ import { sidebarUiActions } from "@/stores/sidebarUi";
14
14
  import { cn } from "@/lib/utils";
15
15
  import { usePendingCardSessions } from "@/data/learning-cards";
16
16
  import { SessionRow } from "./SessionRow";
17
+ import { GroupShelves } from "./GroupShelves";
17
18
  import { OrchestratorPinRow, OrchestratorStartRow } from "./OrchestratorPinRow";
18
19
  import { QueuedRow } from "./QueuedRow";
19
20
  import type { QueuedRow as QueuedRowData } from "@/lib/sidebar/queued";
@@ -117,6 +118,7 @@ export function SessionShelves({
117
118
  doneCollapsed,
118
119
  snoozedCollapsed,
119
120
  donePages,
121
+ workspaceNames,
120
122
  }: {
121
123
  shelves: Shelves;
122
124
  queued: QueuedRowData[];
@@ -124,6 +126,7 @@ export function SessionShelves({
124
126
  doneCollapsed: boolean;
125
127
  snoozedCollapsed: boolean;
126
128
  donePages: number;
129
+ workspaceNames?: Map<string, string>;
127
130
  }) {
128
131
  const folds = { doneCollapsed, snoozedCollapsed, donePages };
129
132
  const shown = shelves.done.slice(0, doneLimit(donePages));
@@ -142,6 +145,7 @@ export function SessionShelves({
142
145
  shelves.orchestrators.length +
143
146
  toStart.length +
144
147
  shelves.pinned.length +
148
+ shelves.groups.length +
145
149
  shelves.needsYou.length +
146
150
  shelves.working.length +
147
151
  shelves.inReview.length +
@@ -167,6 +171,7 @@ export function SessionShelves({
167
171
  ))}
168
172
  </Shelf>
169
173
  )}
174
+ <GroupShelves groups={shelves.groups} workspaceNames={workspaceNames} />
170
175
  {shelves.needsYou.length > 0 && (
171
176
  <Shelf label="Needs you" count={shelves.needsYou.length} accent>
172
177
  {shelves.needsYou.map((r) => (
@@ -63,6 +63,7 @@ export function SidebarBody({
63
63
  const count =
64
64
  shelves.orchestrators.length +
65
65
  shelves.pinned.length +
66
+ shelves.groups.length +
66
67
  shelves.needsYou.length +
67
68
  shelves.working.length +
68
69
  shelves.inReview.length +
@@ -125,6 +126,11 @@ export function SidebarBody({
125
126
  doneCollapsed={ui.doneCollapsed}
126
127
  snoozedCollapsed={ui.snoozedCollapsed}
127
128
  donePages={ui.donePages}
129
+ workspaceNames={
130
+ data.workspace
131
+ ? undefined
132
+ : new Map(data.workspaces.map((w) => [w.id, w.name]))
133
+ }
128
134
  />
129
135
  <MachineShelves groups={data.machines} />
130
136
  <SidebarExtras
@@ -0,0 +1,104 @@
1
+ import "@/components/Chat/composer/test-dom";
2
+ import { act, createElement } from "react";
3
+ import { createRoot, type Root } from "react-dom/client";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ const mutate = vi.fn();
7
+ vi.mock("@/data/session-groups", () => ({
8
+ useCreateGroup: () => ({ mutate }),
9
+ }));
10
+
11
+ // Radix menus need a real browser; render the item inline.
12
+ vi.mock("@/components/ui/context-menu", async () => {
13
+ const { createElement: h } = await import("react");
14
+ const part = (p: { children?: unknown }) =>
15
+ h("div", null, p.children as never);
16
+ return {
17
+ ContextMenu: part,
18
+ ContextMenuTrigger: part,
19
+ ContextMenuContent: part,
20
+ ContextMenuItem: (p: { children?: unknown; onSelect?: () => void }) =>
21
+ h("button", { onClick: p.onSelect }, p.children as never),
22
+ };
23
+ });
24
+
25
+ vi.mock("@/components/ui/dialog", async () => {
26
+ const { createElement: h } = await import("react");
27
+ const part = (p: { children?: unknown; open?: boolean }) =>
28
+ p.open === false ? null : h("div", null, p.children as never);
29
+ return {
30
+ Dialog: part,
31
+ DialogContent: part,
32
+ DialogHeader: part,
33
+ DialogTitle: part,
34
+ };
35
+ });
36
+
37
+ import { SidebarContextMenu, isEditableTarget } from "./SidebarContextMenu";
38
+
39
+ (
40
+ globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
41
+ ).IS_REACT_ACT_ENVIRONMENT = true;
42
+
43
+ let root: Root;
44
+ let host: HTMLElement;
45
+ beforeEach(() => {
46
+ mutate.mockClear();
47
+ host = document.createElement("div");
48
+ document.body.append(host);
49
+ root = createRoot(host);
50
+ act(() =>
51
+ root.render(createElement(SidebarContextMenu, { workspaceId: "w1" }, "x"))
52
+ );
53
+ });
54
+ afterEach(() => {
55
+ act(() => root.unmount());
56
+ host.remove();
57
+ document.body.innerHTML = "";
58
+ });
59
+
60
+ const openDialog = () =>
61
+ act(() => {
62
+ host.querySelector("button")!.click();
63
+ });
64
+ const type = (value: string) => {
65
+ const input = document.body.querySelector("input")!;
66
+ act(() => {
67
+ Object.getOwnPropertyDescriptor(
68
+ HTMLInputElement.prototype,
69
+ "value"
70
+ )!.set!.call(input, value);
71
+ input.dispatchEvent(new window.Event("input", { bubbles: true }));
72
+ });
73
+ };
74
+ const submit = () =>
75
+ act(() => {
76
+ document.body
77
+ .querySelector("form")!
78
+ .dispatchEvent(
79
+ new window.Event("submit", { bubbles: true, cancelable: true })
80
+ );
81
+ });
82
+
83
+ describe("SidebarContextMenu", () => {
84
+ it("creates a group in the workspace from the typed name", () => {
85
+ openDialog();
86
+ type(" Docs ");
87
+ submit();
88
+ expect(mutate).toHaveBeenCalledWith({ name: "Docs", workspaceId: "w1" });
89
+ });
90
+
91
+ it("does nothing for an empty name", () => {
92
+ openDialog();
93
+ type(" ");
94
+ submit();
95
+ expect(mutate).not.toHaveBeenCalled();
96
+ });
97
+
98
+ it("leaves text fields to the browser's menu", () => {
99
+ const input = document.createElement("input");
100
+ const div = document.createElement("div");
101
+ expect(isEditableTarget(input)).toBe(true);
102
+ expect(isEditableTarget(div)).toBe(false);
103
+ });
104
+ });
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import { useState, type MouseEvent, type ReactNode } from "react";
4
+ import { FolderPlus } from "lucide-react";
5
+ import * as CM from "@/components/ui/context-menu";
6
+ import { WorkspaceNameDialog } from "@/components/Workspaces";
7
+ import { useCreateGroup } from "@/data/session-groups";
8
+
9
+ export function isEditableTarget(target: EventTarget | null): boolean {
10
+ return (
11
+ target instanceof HTMLElement &&
12
+ !!target.closest("input, textarea, select, [contenteditable='true']")
13
+ );
14
+ }
15
+
16
+ // Right-click anywhere in the session list (empty space or a group header)
17
+ // offers "New group". Rows keep their own menu, which wins because it has
18
+ // already handled the event; text fields keep the browser's.
19
+ export function SidebarContextMenu({
20
+ workspaceId,
21
+ children,
22
+ }: {
23
+ workspaceId: string | null;
24
+ children?: ReactNode;
25
+ }) {
26
+ const createGroup = useCreateGroup();
27
+ const [naming, setNaming] = useState(false);
28
+
29
+ return (
30
+ <>
31
+ <CM.ContextMenu>
32
+ <CM.ContextMenuTrigger
33
+ asChild
34
+ onContextMenuCapture={(e: MouseEvent) => {
35
+ if (isEditableTarget(e.target)) e.stopPropagation();
36
+ }}
37
+ >
38
+ {children}
39
+ </CM.ContextMenuTrigger>
40
+ <CM.ContextMenuContent className="w-44">
41
+ <CM.ContextMenuItem
42
+ className="min-h-11 md:min-h-8"
43
+ onSelect={() => setNaming(true)}
44
+ >
45
+ <FolderPlus className="mr-2 h-3.5 w-3.5" />
46
+ New group
47
+ </CM.ContextMenuItem>
48
+ </CM.ContextMenuContent>
49
+ </CM.ContextMenu>
50
+ <WorkspaceNameDialog
51
+ open={naming}
52
+ title="New group"
53
+ submitLabel="Create"
54
+ onSubmit={(name) => createGroup.mutate({ name, workspaceId })}
55
+ onClose={() => setNaming(false)}
56
+ />
57
+ </>
58
+ );
59
+ }
@@ -15,6 +15,7 @@ import {
15
15
  MoreHorizontal,
16
16
  Pin,
17
17
  PinOff,
18
+ Network,
18
19
  Plus,
19
20
  Server,
20
21
  Settings,
@@ -30,6 +31,7 @@ import {
30
31
  import { Button } from "@/components/ui/button";
31
32
  import type { Workspace } from "@/lib/db";
32
33
  import { archivedUiActions } from "@/stores/archivedUi";
34
+ import { claimsUiActions } from "@/stores/claimsUi";
33
35
  import { bookmarksUiActions } from "@/stores/bookmarksUi";
34
36
  import { inboxUiActions } from "@/stores/inboxUi";
35
37
  import { newDraft } from "@/stores/drafts";
@@ -134,6 +136,9 @@ export function SidebarHeader(props: SidebarHeaderProps) {
134
136
  }),
135
137
  separator(),
136
138
  menuItem("Machines", props.onManageHosts, { icon: Server }),
139
+ menuItem("Work across machines", () => claimsUiActions.open(), {
140
+ icon: Network,
141
+ }),
137
142
  menuItem("Settings", () => settingsUiActions.open(), {
138
143
  icon: Settings,
139
144
  }),
@@ -8,6 +8,7 @@ import {
8
8
  ChevronDown,
9
9
  CircleCheck,
10
10
  Clock,
11
+ Folder,
11
12
  FolderPlus,
12
13
  Gauge,
13
14
  LayoutGrid,
@@ -32,6 +33,7 @@ import { orchestratorOpenActions } from "@/stores/orchestratorOpen";
32
33
  import { sidebarUiActions } from "@/stores/sidebarUi";
33
34
  import { schedulesUiActions } from "@/stores/schedulesUi";
34
35
  import { recipesUiActions } from "@/stores/recipesUi";
36
+ import { useCreateGroup } from "@/data/session-groups";
35
37
  import { useAddExistingProject } from "@/components/Projects/AddProject/useAddExistingProject";
36
38
 
37
39
  const icon = "mr-2 h-3.5 w-3.5";
@@ -49,7 +51,8 @@ export function WorkspaceSwitcher({
49
51
  const update = useUpdateWorkspace();
50
52
  const remove = useDeleteWorkspace();
51
53
  const addExisting = useAddExistingProject();
52
- const [dialog, setDialog] = useState<"new" | "rename" | null>(null);
54
+ const createGroup = useCreateGroup();
55
+ const [dialog, setDialog] = useState<"new" | "rename" | "group" | null>(null);
53
56
  const name = current?.name ?? "All workspaces";
54
57
 
55
58
  return (
@@ -89,6 +92,12 @@ export function WorkspaceSwitcher({
89
92
  <Plus className={icon} />
90
93
  New workspace
91
94
  </DM.DropdownMenuItem>
95
+ {current && (
96
+ <DM.DropdownMenuItem onClick={() => setDialog("group")}>
97
+ <Folder className={icon} />
98
+ New group
99
+ </DM.DropdownMenuItem>
100
+ )}
92
101
  {!current && (
93
102
  <>
94
103
  <DM.DropdownMenuItem onClick={() => recipesUiActions.open(null)}>
@@ -175,14 +184,22 @@ export function WorkspaceSwitcher({
175
184
  </DM.DropdownMenuContent>
176
185
  </DM.DropdownMenu>
177
186
  <WorkspaceNameDialog
178
- open={dialog === "new" || dialog === "rename"}
179
- title={dialog === "rename" ? "Rename workspace" : "New workspace"}
187
+ open={dialog !== null}
188
+ title={
189
+ dialog === "rename"
190
+ ? "Rename workspace"
191
+ : dialog === "group"
192
+ ? "New group"
193
+ : "New workspace"
194
+ }
180
195
  initialName={dialog === "rename" ? current?.name : ""}
181
196
  submitLabel={dialog === "rename" ? "Rename" : "Create"}
182
197
  onSubmit={(name) =>
183
- dialog === "rename" && current
184
- ? update.mutate({ id: current.id, name })
185
- : create.mutate(name)
198
+ dialog === "group"
199
+ ? createGroup.mutate({ name, workspaceId: current?.id ?? null })
200
+ : dialog === "rename" && current
201
+ ? update.mutate({ id: current.id, name })
202
+ : create.mutate(name)
186
203
  }
187
204
  onClose={() => setDialog(null)}
188
205
  />
@@ -21,6 +21,7 @@ import { SidebarSearch } from "./SidebarSearch";
21
21
  import { ProjectFilter } from "./ProjectFilter";
22
22
  import { SelectionToolbar } from "./SelectionToolbar";
23
23
  import { SidebarBody } from "./SidebarBody";
24
+ import { SidebarContextMenu } from "./SidebarContextMenu";
24
25
  import { SidebarDialogs, type SidebarDialog } from "./SidebarDialogs";
25
26
  import type { SessionListProps } from "./SessionList.types";
26
27
 
@@ -193,20 +194,23 @@ export function SessionList({
193
194
  )}
194
195
 
195
196
  <ScrollArea className="w-full flex-1">
196
- <div className="max-w-full px-1.5 pb-6">
197
- <RowProvider value={rowContext}>
198
- <SidebarBody
199
- data={data}
200
- onNewProject={addProject}
201
- devServerHandlers={{
202
- onStop: mutations.handleStopDevServer,
203
- onRestart: mutations.handleRestartDevServer,
204
- onRemove: mutations.handleRemoveDevServer,
205
- onViewLogs: (serverId) => setDialog({ kind: "logs", serverId }),
206
- }}
207
- />
208
- </RowProvider>
209
- </div>
197
+ <SidebarContextMenu workspaceId={data.workspace?.id ?? null}>
198
+ <div className="min-h-[60vh] max-w-full px-1.5 pb-6">
199
+ <RowProvider value={rowContext}>
200
+ <SidebarBody
201
+ data={data}
202
+ onNewProject={addProject}
203
+ devServerHandlers={{
204
+ onStop: mutations.handleStopDevServer,
205
+ onRestart: mutations.handleRestartDevServer,
206
+ onRemove: mutations.handleRemoveDevServer,
207
+ onViewLogs: (serverId) =>
208
+ setDialog({ kind: "logs", serverId }),
209
+ }}
210
+ />
211
+ </RowProvider>
212
+ </div>
213
+ </SidebarContextMenu>
210
214
  </ScrollArea>
211
215
 
212
216
  <SidebarDialogs
@@ -3,9 +3,11 @@
3
3
  import { useCallback, useMemo } from "react";
4
4
  import { useSnapshot } from "valtio";
5
5
  import type { Session } from "@/lib/db";
6
+ import type { SessionGroup } from "@/lib/sidebar/shelves";
6
7
  import {
7
8
  buildShelves,
8
9
  inWorkspace,
10
+ orderGroups,
9
11
  projectsInWorkspace,
10
12
  } from "@/lib/sidebar/shelves";
11
13
  import type { TaskState } from "@/lib/tasks/state";
@@ -25,6 +27,7 @@ import type { SessionStatus } from "./SessionList.types";
25
27
 
26
28
  // One empty list for queries still loading, so memos below hold.
27
29
  const NONE: never[] = [];
30
+ const NONE_GROUPS: SessionGroup[] = [];
28
31
 
29
32
  // Everything the flat list needs: the current workspace's sessions, sorted
30
33
  // onto shelves by the remembered filters.
@@ -55,6 +58,7 @@ export function useSidebarData(
55
58
  () => sessionsQuery.data?.sessions ?? [],
56
59
  [sessionsQuery.data]
57
60
  );
61
+ const allGroups = sessionsQuery.data?.sessionGroups ?? NONE_GROUPS;
58
62
  // Keyed by its contents: the hosts query hands back a new object each time.
59
63
  const linkedKey = JSON.stringify(useLinkedHostNames());
60
64
  const linked = useMemo(
@@ -102,6 +106,10 @@ export function useSidebarData(
102
106
  ),
103
107
  projectId: project?.id ?? null,
104
108
  currentOrchestrators,
109
+ groups: orderGroups(
110
+ allGroups.filter((g) => !workspace || g.workspace_id === workspace.id),
111
+ workspaces.map((w) => w.id)
112
+ ),
105
113
  };
106
114
  // The rollups count the whole list; the filter only narrows what shows.
107
115
  const all = buildShelves(input);
@@ -120,6 +128,7 @@ export function useSidebarData(
120
128
  now,
121
129
  linked,
122
130
  sessions,
131
+ allGroups,
123
132
  projects,
124
133
  workspaces,
125
134
  tasks,
@@ -235,6 +244,7 @@ export function useSidebarData(
235
244
  return {
236
245
  ui,
237
246
  shelves,
247
+ groups: allGroups,
238
248
  queued,
239
249
  toStart,
240
250
  machines,