@saadnvd1/agent-os 0.3.698 → 0.3.715

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/README.md +1 -0
  2. package/app/api/bus/groups/route.ts +44 -0
  3. package/app/api/bus/send/route.ts +11 -1
  4. package/app/api/bus/spawn/route.ts +12 -1
  5. package/app/api/bus/wait/route.ts +22 -0
  6. package/app/api/claims/[id]/route.ts +20 -0
  7. package/app/api/claims/check/route.ts +23 -0
  8. package/app/api/claims/check.test.ts +113 -0
  9. package/app/api/claims/exchange/route.ts +42 -0
  10. package/app/api/claims/outside.test.ts +90 -0
  11. package/app/api/claims/route.test.ts +193 -0
  12. package/app/api/claims/route.ts +73 -0
  13. package/app/api/hosts/[id]/route.ts +9 -1
  14. package/app/api/peer/hello/reveal/route.ts +6 -0
  15. package/app/api/peer/hello/route.ts +9 -0
  16. package/app/api/peer/hello/status/route.ts +6 -0
  17. package/app/api/peer/unlink/route.ts +17 -0
  18. package/app/api/peers/[hostId]/audit/route.ts +14 -0
  19. package/app/api/peers/[hostId]/route.ts +20 -0
  20. package/app/api/peers/account/route.ts +9 -0
  21. package/app/api/peers/gate.test.ts +174 -0
  22. package/app/api/peers/link/[id]/confirm/route.ts +27 -0
  23. package/app/api/peers/link/[id]/route.ts +20 -0
  24. package/app/api/peers/link/route.ts +23 -0
  25. package/app/api/peers/pending/[id]/accept/route.ts +31 -0
  26. package/app/api/peers/pending/[id]/decline/route.ts +13 -0
  27. package/app/api/peers/pending/route.test.ts +37 -0
  28. package/app/api/peers/pending/route.ts +14 -0
  29. package/app/api/session-groups/[id]/restore/route.ts +11 -0
  30. package/app/api/session-groups/[id]/route.ts +25 -0
  31. package/app/api/session-groups/order/route.ts +15 -0
  32. package/app/api/session-groups/route.ts +21 -0
  33. package/app/api/sessions/[id]/fork/route.ts +3 -0
  34. package/app/api/sessions/[id]/group/route.ts +18 -0
  35. package/app/api/sessions/[id]/route.ts +14 -4
  36. package/app/api/sessions/[id]/summarize/route.ts +2 -0
  37. package/app/api/sessions/claims.test.ts +122 -0
  38. package/app/api/sessions/group/route.ts +17 -0
  39. package/app/api/sessions/route.ts +31 -13
  40. package/app/page.tsx +7 -0
  41. package/components/Claims/ClaimFiles.tsx +38 -0
  42. package/components/Claims/ClaimRow.tsx +82 -0
  43. package/components/Claims/ClaimsSheet.tsx +176 -0
  44. package/components/Claims/OverlapPill.tsx +31 -0
  45. package/components/Claims/StartClaimsNotice.tsx +70 -0
  46. package/components/Claims/claims-ui.test.ts +161 -0
  47. package/components/Claims/format.ts +57 -0
  48. package/components/Claims/index.ts +3 -0
  49. package/components/Draft/DraftPanel.tsx +40 -2
  50. package/components/Hosts/AccountMachines.tsx +194 -0
  51. package/components/Hosts/HostRow.tsx +195 -45
  52. package/components/Hosts/HostsDialog.tsx +49 -37
  53. package/components/Hosts/IncomingLinkSheet.tsx +246 -0
  54. package/components/Hosts/LinkCodeSheet.tsx +186 -0
  55. package/components/Hosts/PeerActivitySheet.tsx +111 -0
  56. package/components/Hosts/sheet.ts +3 -0
  57. package/components/SessionList/GroupShelves.tsx +195 -0
  58. package/components/SessionList/MoveToGroupMenu.tsx +105 -0
  59. package/components/SessionList/RowSubtitle.tsx +18 -2
  60. package/components/SessionList/SelectionToolbar.tsx +52 -1
  61. package/components/SessionList/SessionRow.tsx +1 -0
  62. package/components/SessionList/SessionRowMenu.tsx +17 -0
  63. package/components/SessionList/SessionShelves.tsx +5 -0
  64. package/components/SessionList/SidebarBody.tsx +6 -0
  65. package/components/SessionList/SidebarContextMenu.test.ts +104 -0
  66. package/components/SessionList/SidebarContextMenu.tsx +59 -0
  67. package/components/SessionList/SidebarHeader.tsx +5 -0
  68. package/components/SessionList/WorkspaceSwitcher.tsx +23 -6
  69. package/components/SessionList/index.tsx +18 -14
  70. package/components/SessionList/useSidebarData.ts +10 -0
  71. package/components/Welcome/ConnectBlock.tsx +1 -1
  72. package/components/ui/notice.tsx +46 -0
  73. package/components/ui/status-pill.tsx +39 -0
  74. package/data/claims/index.ts +2 -0
  75. package/data/claims/keys.ts +5 -0
  76. package/data/claims/queries.ts +47 -0
  77. package/data/peers/index.ts +2 -0
  78. package/data/peers/keys.ts +7 -0
  79. package/data/peers/queries.ts +172 -0
  80. package/data/push/topics.ts +4 -0
  81. package/data/session-groups/index.ts +156 -0
  82. package/data/sessions/index.ts +0 -1
  83. package/data/sessions/pin.ts +2 -6
  84. package/data/sessions/queries.ts +10 -29
  85. package/data/sessions/shelf.ts +2 -6
  86. package/data/tasks/index.ts +2 -0
  87. package/hooks/useSessions.ts +0 -12
  88. package/lib/__fixtures__/peer-rpc.ts +65 -33
  89. package/lib/agents/brief.ts +3 -1
  90. package/lib/agents/spawn.ts +4 -0
  91. package/lib/bus/format.ts +25 -4
  92. package/lib/bus/index.ts +121 -106
  93. package/lib/bus/resolve.ts +16 -2
  94. package/lib/bus/targets.ts +122 -0
  95. package/lib/bus/waits/kick.ts +12 -0
  96. package/lib/bus/waits/lines.ts +69 -0
  97. package/lib/bus/waits/register.ts +83 -0
  98. package/lib/bus/waits/store.ts +196 -0
  99. package/lib/bus/waits/tick.ts +192 -0
  100. package/lib/claims/collectLocal.ts +121 -0
  101. package/lib/claims/describe.ts +41 -0
  102. package/lib/claims/exchange.ts +91 -0
  103. package/lib/claims/goal.ts +45 -0
  104. package/lib/claims/index.ts +10 -0
  105. package/lib/claims/loop.ts +51 -0
  106. package/lib/claims/machine.ts +21 -0
  107. package/lib/claims/mine.ts +43 -0
  108. package/lib/claims/notifyOverlaps.ts +101 -0
  109. package/lib/claims/onMerged.ts +85 -0
  110. package/lib/claims/outside.ts +36 -0
  111. package/lib/claims/overlap.ts +87 -0
  112. package/lib/claims/overlapCache.ts +86 -0
  113. package/lib/claims/overlapText.ts +36 -0
  114. package/lib/claims/peers.ts +56 -0
  115. package/lib/claims/startCheck.ts +100 -0
  116. package/lib/claims/startWarning.ts +29 -0
  117. package/lib/claims/store.ts +366 -0
  118. package/lib/claims/types.ts +102 -0
  119. package/lib/claims/view.ts +80 -0
  120. package/lib/claims/yield.ts +24 -0
  121. package/lib/connect/control-api.ts +108 -2
  122. package/lib/connect/heartbeat.ts +11 -3
  123. package/lib/connect/serve.ts +2 -0
  124. package/lib/db/changes.ts +6 -0
  125. package/lib/db/migrations/20261020T1400_work_claims.ts +35 -0
  126. package/lib/db/migrations/20261020T1515_peer_unlinked.ts +22 -0
  127. package/lib/db/migrations/20261020T1600_bus_waits.ts +35 -0
  128. package/lib/db/migrations/20261020T1700_clean_connect_names.ts +18 -0
  129. package/lib/db/migrations/20261020T1800_session_groups.ts +49 -0
  130. package/lib/db/migrations/20261020T1900_task_queue_after_claim.ts +13 -0
  131. package/lib/db/migrations/20261020T2000_claim_notices.ts +20 -0
  132. package/lib/db/migrations/20261020T2100_claims_via_host.ts +21 -0
  133. package/lib/db/migrations/20261020T2200_work_claims_watch.ts +10 -0
  134. package/lib/db/queries.ts +0 -40
  135. package/lib/db/session-parts/columns.ts +1 -0
  136. package/lib/db/types.ts +20 -8
  137. package/lib/doctor/checks.ts +64 -1
  138. package/lib/done/archive.ts +2 -0
  139. package/lib/git-remote.ts +43 -0
  140. package/lib/hosts/index.ts +10 -1
  141. package/lib/orchestration.ts +2 -0
  142. package/lib/orchestrator/act.ts +54 -4
  143. package/lib/orchestrator/brief.ts +2 -1
  144. package/lib/orchestrator/claim-wait.ts +71 -0
  145. package/lib/orchestrator/conditions.ts +28 -1
  146. package/lib/orchestrator/describe.ts +19 -1
  147. package/lib/orchestrator/facts.ts +22 -0
  148. package/lib/orchestrator/serve.ts +37 -2
  149. package/lib/orchestrator/tool-names.ts +1 -0
  150. package/lib/orchestrator/tool-schemas.ts +31 -1
  151. package/lib/orchestrator/tools.ts +3 -1
  152. package/lib/orchestrator/watcher.ts +19 -1
  153. package/lib/peer/account.ts +270 -0
  154. package/lib/peer/allowlist.ts +1 -1
  155. package/lib/peer/cli.ts +24 -0
  156. package/lib/peer/client.ts +26 -1
  157. package/lib/peer/code.ts +3 -0
  158. package/lib/peer/hello-route.ts +32 -0
  159. package/lib/peer/hello.ts +346 -0
  160. package/lib/peer/link-cli.ts +248 -0
  161. package/lib/peer/links.ts +117 -5
  162. package/lib/peer/machines.ts +84 -0
  163. package/lib/peer/name.ts +32 -0
  164. package/lib/peer/notify.ts +32 -0
  165. package/lib/peer/pairing-out.ts +382 -0
  166. package/lib/peer/pairing.ts +432 -0
  167. package/lib/peer/pin.ts +62 -0
  168. package/lib/peer/reasons.ts +7 -0
  169. package/lib/peer/revoke.ts +75 -0
  170. package/lib/peer/rpc-intake.ts +37 -8
  171. package/lib/peer/rpc-server.ts +72 -6
  172. package/lib/peer/unlink.ts +62 -0
  173. package/lib/peer/unlinked-proof.ts +44 -0
  174. package/lib/projects.ts +5 -2
  175. package/lib/schedules/when.ts +1 -1
  176. package/lib/security/auth.ts +10 -2
  177. package/lib/security/demo.ts +7 -0
  178. package/lib/security/gate.ts +3 -0
  179. package/lib/session-groups/assignSessions.ts +81 -0
  180. package/lib/session-groups/cli.ts +61 -0
  181. package/lib/session-groups/createGroup.ts +28 -0
  182. package/lib/session-groups/index.ts +21 -0
  183. package/lib/session-groups/removeGroup.ts +36 -0
  184. package/lib/session-groups/reorderGroups.ts +31 -0
  185. package/lib/session-groups/resolveGroup.ts +19 -0
  186. package/lib/session-groups/route.ts +32 -0
  187. package/lib/session-groups/store.ts +85 -0
  188. package/lib/session-groups/types.ts +25 -0
  189. package/lib/session-groups/updateGroup.ts +35 -0
  190. package/lib/session-groups/workspace.ts +21 -0
  191. package/lib/sessions/end.ts +3 -0
  192. package/lib/sessions/launch.ts +5 -2
  193. package/lib/sidebar/rollup.ts +1 -0
  194. package/lib/sidebar/row-menu.ts +2 -0
  195. package/lib/sidebar/shelves.ts +90 -15
  196. package/lib/sidebar/visible-order.ts +5 -0
  197. package/lib/tasks/hold-checks.ts +3 -1
  198. package/lib/tasks/holds.ts +3 -0
  199. package/lib/tasks/index.ts +5 -0
  200. package/lib/tasks/queue-rows.ts +8 -1
  201. package/lib/tasks/queue-start.ts +1 -0
  202. package/lib/tasks/queue-types.ts +9 -1
  203. package/lib/tasks/queue.ts +15 -5
  204. package/lib/tasks/schedule-types.ts +1 -0
  205. package/lib/test-temp.ts +19 -3
  206. package/lib/workspaces.ts +11 -3
  207. package/lib/worktree-sweep/testTemp.ts +23 -106
  208. package/package.json +1 -1
  209. package/scripts/agent-os +3 -1
  210. package/scripts/autodeploy +2 -0
  211. package/scripts/autodeploy.test.ts +31 -1
  212. package/scripts/claim-hook.sh +31 -0
  213. package/scripts/claim-hook.test.ts +230 -0
  214. package/scripts/lib/cli-links.sh +36 -0
  215. package/scripts/lib/commands.sh +5 -0
  216. package/scripts/lib/machine.sh +27 -2
  217. package/scripts/lib/machine.test.ts +25 -3
  218. package/scripts/sync-cli-links +7 -0
  219. package/scripts/sync-cli-links.test.ts +69 -0
  220. package/server.ts +6 -0
  221. package/stores/claimsUi.ts +22 -0
  222. package/app/api/groups/[...path]/route.ts +0 -136
  223. package/app/api/groups/route.ts +0 -93
  224. package/data/groups/index.ts +0 -1
  225. package/data/groups/mutations.ts +0 -95
  226. package/hooks/useGroups.ts +0 -37
@@ -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,7 @@
1
+ export const peerKeys = {
2
+ all: ["peers"] as const,
3
+ account: () => [...peerKeys.all, "account"] as const,
4
+ pending: () => [...peerKeys.all, "pending"] as const,
5
+ link: (id: string) => [...peerKeys.all, "link", id] as const,
6
+ audit: (hostId: string) => [...peerKeys.all, "audit", hostId] as const,
7
+ };
@@ -0,0 +1,172 @@
1
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
+ import type { AuditRow } from "@/lib/peer/audit";
3
+ import type { AccountMachines } from "@/lib/peer/machines";
4
+ import type { PendingView } from "@/lib/peer/pairing";
5
+ import type { Outgoing } from "@/lib/peer/pairing-out";
6
+ import { hostKeys } from "../hosts/keys";
7
+ import { peerKeys } from "./keys";
8
+
9
+ async function json<T>(res: Response): Promise<T> {
10
+ const data = await res.json().catch(() => ({}));
11
+ if (!res.ok)
12
+ throw Object.assign(new Error(data.error || "Request failed"), {
13
+ attemptsLeft: data.attemptsLeft as number | undefined,
14
+ });
15
+ return data;
16
+ }
17
+
18
+ const post = (url: string, body?: object) =>
19
+ fetch(url, {
20
+ method: "POST",
21
+ headers: { "Content-Type": "application/json" },
22
+ body: body ? JSON.stringify(body) : undefined,
23
+ });
24
+
25
+ /** The other machines on this Connect account, and the plan. */
26
+ export function useAccountMachines(enabled: boolean) {
27
+ return useQuery({
28
+ queryKey: peerKeys.account(),
29
+ queryFn: async () =>
30
+ json<AccountMachines>(await fetch("/api/peers/account")),
31
+ enabled,
32
+ staleTime: 30_000,
33
+ });
34
+ }
35
+
36
+ export function useRefreshAccount() {
37
+ const queryClient = useQueryClient();
38
+ return useMutation({
39
+ mutationFn: async () =>
40
+ json<AccountMachines>(await fetch("/api/peers/account?refresh=1")),
41
+ onSuccess: (data) => queryClient.setQueryData(peerKeys.account(), data),
42
+ });
43
+ }
44
+
45
+ /** Machines asking to link here: shown on every page until answered. */
46
+ export function usePendingLinks() {
47
+ return useQuery({
48
+ queryKey: peerKeys.pending(),
49
+ queryFn: async () =>
50
+ (
51
+ await json<{ requests: PendingView[] }>(
52
+ await fetch("/api/peers/pending")
53
+ )
54
+ ).requests,
55
+ refetchInterval: 10_000,
56
+ });
57
+ }
58
+
59
+ export function useStartLink() {
60
+ return useMutation({
61
+ mutationFn: async (machineId: string) =>
62
+ (
63
+ await json<{ link: Outgoing }>(
64
+ await post("/api/peers/link", { machineId })
65
+ )
66
+ ).link,
67
+ });
68
+ }
69
+
70
+ /** A link this machine started, followed until it ends. */
71
+ export function useOutgoingLink(id: string | null) {
72
+ const queryClient = useQueryClient();
73
+ return useQuery({
74
+ queryKey: peerKeys.link(id ?? ""),
75
+ queryFn: async () => {
76
+ const { link } = await json<{ link: Outgoing }>(
77
+ await fetch(`/api/peers/link/${id}`)
78
+ );
79
+ if (link.state === "accepted") {
80
+ void queryClient.invalidateQueries({ queryKey: hostKeys.all });
81
+ void queryClient.invalidateQueries({ queryKey: peerKeys.account() });
82
+ }
83
+ return link;
84
+ },
85
+ enabled: !!id,
86
+ refetchInterval: (q) =>
87
+ !q.state.data || q.state.data.state === "pending" ? 1500 : false,
88
+ });
89
+ }
90
+
91
+ /** The code the other machine shows once it took ours: only it pins it. */
92
+ export function useConfirmLink() {
93
+ const queryClient = useQueryClient();
94
+ return useMutation({
95
+ mutationFn: async (o: { id: string; code: string }) =>
96
+ (
97
+ await json<{ link: Outgoing }>(
98
+ await post(`/api/peers/link/${o.id}/confirm`, { code: o.code })
99
+ )
100
+ ).link,
101
+ onSuccess: (link) => {
102
+ queryClient.setQueryData(peerKeys.link(link.id), link);
103
+ void queryClient.invalidateQueries({ queryKey: hostKeys.all });
104
+ void queryClient.invalidateQueries({ queryKey: peerKeys.account() });
105
+ },
106
+ // Three wrong codes end it: show that.
107
+ onError: (_e, o) =>
108
+ void queryClient.invalidateQueries({ queryKey: peerKeys.link(o.id) }),
109
+ });
110
+ }
111
+
112
+ export function useCancelLink() {
113
+ return useMutation({
114
+ mutationFn: async (id: string) =>
115
+ json(await fetch(`/api/peers/link/${id}`, { method: "DELETE" })),
116
+ });
117
+ }
118
+
119
+ export function useAnswerLink() {
120
+ const queryClient = useQueryClient();
121
+ const done = () => {
122
+ void queryClient.invalidateQueries({ queryKey: peerKeys.all });
123
+ void queryClient.invalidateQueries({ queryKey: hostKeys.all });
124
+ };
125
+ return {
126
+ accept: useMutation({
127
+ mutationFn: async (o: { id: string; code: string }) =>
128
+ json<{ hostId: string; name: string; confirmCode: string }>(
129
+ await post(`/api/peers/pending/${o.id}/accept`, { code: o.code })
130
+ ),
131
+ onSuccess: done,
132
+ // A wrong code changes how many tries are left.
133
+ onError: () =>
134
+ void queryClient.invalidateQueries({ queryKey: peerKeys.pending() }),
135
+ }),
136
+ decline: useMutation({
137
+ mutationFn: async (id: string) =>
138
+ json(await post(`/api/peers/pending/${id}/decline`)),
139
+ onSuccess: done,
140
+ }),
141
+ };
142
+ }
143
+
144
+ /** Unlinks a machine linked through Connect, here and (if it answers) there. */
145
+ export function useUnlinkPeer() {
146
+ const queryClient = useQueryClient();
147
+ return useMutation({
148
+ mutationFn: async (hostId: string) =>
149
+ json<{ name: string; told: boolean; removed: boolean }>(
150
+ await fetch(`/api/peers/${hostId}`, { method: "DELETE" })
151
+ ),
152
+ onSettled: () => {
153
+ void queryClient.invalidateQueries({ queryKey: hostKeys.all });
154
+ void queryClient.invalidateQueries({ queryKey: peerKeys.account() });
155
+ },
156
+ });
157
+ }
158
+
159
+ /** Every call to and from a linked machine, newest first. */
160
+ export function usePeerAudit(hostId: string | null) {
161
+ return useQuery({
162
+ queryKey: peerKeys.audit(hostId ?? ""),
163
+ queryFn: async () =>
164
+ (
165
+ await json<{ rows: AuditRow[] }>(
166
+ await fetch(`/api/peers/${hostId}/audit`)
167
+ )
168
+ ).rows,
169
+ enabled: !!hostId,
170
+ refetchInterval: 15_000,
171
+ });
172
+ }
@@ -18,6 +18,7 @@ import { archivedKeys } from "../done";
18
18
  import { awayKeys } from "../away";
19
19
  import { bookmarkKeys } from "../bookmarks/keys";
20
20
  import { learningCardsKeys } from "../learning-cards";
21
+ import { claimKeys } from "../claims/keys";
21
22
 
22
23
  // What each pushed topic (a table that changed, lib/db/changes.ts, or a
23
24
  // pushed view) means the browser should refetch. Only queries on screen
@@ -41,6 +42,7 @@ function keysFor(topic: string): QueryKey[] {
41
42
  case "projects":
42
43
  return [projectKeys.all, sessionKeys.all];
43
44
  case "groups":
45
+ case "session_groups":
44
46
  return [sessionKeys.all];
45
47
  case "workspaces":
46
48
  return [workspaceKeys.all, orchestratorKeys.all];
@@ -76,6 +78,8 @@ function keysFor(topic: string): QueryKey[] {
76
78
  return [devServerKeys.all];
77
79
  case "learning_cards":
78
80
  return [learningCardsKeys.all, orchestratorKeys.all];
81
+ case "work_claims":
82
+ return [claimKeys.all, sessionKeys.all];
79
83
  case "chat_bookmarks":
80
84
  return [bookmarkKeys.all];
81
85
  default:
@@ -0,0 +1,156 @@
1
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { toast } from "sonner";
3
+ import type { SessionGroup } from "@/lib/sidebar/shelves";
4
+ import { sessionKeys } from "@/data/sessions/keys";
5
+ import type { SessionsResponse } from "@/data/sessions/queries";
6
+
7
+ async function send<T>(
8
+ url: string,
9
+ method: string,
10
+ body?: unknown
11
+ ): Promise<T> {
12
+ const res = await fetch(url, {
13
+ method,
14
+ headers: { "Content-Type": "application/json" },
15
+ body: body === undefined ? undefined : JSON.stringify(body),
16
+ });
17
+ const data = await res.json().catch(() => ({}));
18
+ if (!res.ok) throw new Error(data.error ?? "Could not change the group");
19
+ return data as T;
20
+ }
21
+
22
+ // Groups ride along in the sessions response, so every edit patches that
23
+ // one cache entry and lets the change push settle the rest.
24
+ function useGroupCache() {
25
+ const queryClient = useQueryClient();
26
+ const key = sessionKeys.list();
27
+ return {
28
+ key,
29
+ refresh: () => queryClient.invalidateQueries({ queryKey: key }),
30
+ async patch(edit: (old: SessionsResponse) => SessionsResponse) {
31
+ await queryClient.cancelQueries({ queryKey: key });
32
+ const previous = queryClient.getQueryData<SessionsResponse>(key);
33
+ queryClient.setQueryData<SessionsResponse>(
34
+ key,
35
+ (old) => old && edit(old)
36
+ );
37
+ return { previous };
38
+ },
39
+ restore(previous?: SessionsResponse) {
40
+ if (previous) queryClient.setQueryData(key, previous);
41
+ },
42
+ };
43
+ }
44
+
45
+ export function useCreateGroup() {
46
+ const cache = useGroupCache();
47
+ return useMutation({
48
+ mutationFn: (v: { name: string; workspaceId: string | null }) =>
49
+ send<{ sessionGroup: SessionGroup }>("/api/session-groups", "POST", v),
50
+ onSuccess: ({ sessionGroup }) =>
51
+ cache.patch((old) => ({
52
+ ...old,
53
+ sessionGroups: [...old.sessionGroups, sessionGroup],
54
+ })),
55
+ onError: (e: Error) => toast.error(e.message),
56
+ onSettled: cache.refresh,
57
+ });
58
+ }
59
+
60
+ export function useUpdateGroup() {
61
+ const cache = useGroupCache();
62
+ return useMutation({
63
+ mutationFn: ({
64
+ id,
65
+ ...patch
66
+ }: {
67
+ id: string;
68
+ name?: string;
69
+ collapsed?: boolean;
70
+ }) => send(`/api/session-groups/${id}`, "PATCH", patch),
71
+ onMutate: ({ id, ...patch }) =>
72
+ cache.patch((old) => ({
73
+ ...old,
74
+ sessionGroups: old.sessionGroups.map((g) =>
75
+ g.id === id ? { ...g, ...patch } : g
76
+ ),
77
+ })),
78
+ onError: (e: Error, _v, ctx) => {
79
+ cache.restore(ctx?.previous);
80
+ toast.error(e.message);
81
+ },
82
+ onSettled: cache.refresh,
83
+ });
84
+ }
85
+
86
+ export function useReorderGroups() {
87
+ const cache = useGroupCache();
88
+ return useMutation({
89
+ mutationFn: (v: { workspaceId: string | null; ids: string[] }) =>
90
+ send("/api/session-groups/order", "POST", v),
91
+ onMutate: ({ ids }) =>
92
+ cache.patch((old) => ({
93
+ ...old,
94
+ sessionGroups: old.sessionGroups.map((g) =>
95
+ ids.includes(g.id) ? { ...g, sort_order: ids.indexOf(g.id) } : g
96
+ ),
97
+ })),
98
+ onError: (e: Error, _v, ctx) => {
99
+ cache.restore(ctx?.previous);
100
+ toast.error(e.message);
101
+ },
102
+ onSettled: cache.refresh,
103
+ });
104
+ }
105
+
106
+ // Deleting is soft: the toast's Undo restores the group and its members.
107
+ export function useDeleteGroup() {
108
+ const cache = useGroupCache();
109
+ return useMutation({
110
+ mutationFn: (g: { id: string; name: string }) =>
111
+ send(`/api/session-groups/${g.id}`, "DELETE"),
112
+ onMutate: (g) =>
113
+ cache.patch((old) => ({
114
+ ...old,
115
+ sessionGroups: old.sessionGroups.filter((x) => x.id !== g.id),
116
+ })),
117
+ onSuccess: (_d, g) =>
118
+ toast(`Deleted group ${g.name}`, {
119
+ description: "Its sessions went back to the shelves.",
120
+ duration: 10_000,
121
+ action: {
122
+ label: "Undo",
123
+ onClick: () =>
124
+ send(`/api/session-groups/${g.id}/restore`, "POST")
125
+ .catch((e: Error) => toast.error(e.message))
126
+ .finally(cache.refresh),
127
+ },
128
+ }),
129
+ onError: (e: Error, _v, ctx) => {
130
+ cache.restore(ctx?.previous);
131
+ toast.error(e.message);
132
+ },
133
+ onSettled: cache.refresh,
134
+ });
135
+ }
136
+
137
+ // One or many sessions into a group, or out of it with groupId null.
138
+ export function useAssignGroup() {
139
+ const cache = useGroupCache();
140
+ return useMutation({
141
+ mutationFn: (v: { ids: string[]; groupId: string | null }) =>
142
+ send("/api/sessions/group", "POST", v),
143
+ onMutate: ({ ids, groupId }) =>
144
+ cache.patch((old) => ({
145
+ ...old,
146
+ sessions: old.sessions.map((s) =>
147
+ ids.includes(s.id) ? { ...s, session_group_id: groupId } : s
148
+ ),
149
+ })),
150
+ onError: (e: Error, _v, ctx) => {
151
+ cache.restore(ctx?.previous);
152
+ toast.error(e.message);
153
+ },
154
+ onSettled: cache.refresh,
155
+ });
156
+ }
@@ -6,7 +6,6 @@ export {
6
6
  useRenameSession,
7
7
  useForkSession,
8
8
  useSummarizeSession,
9
- useMoveSessionToGroup,
10
9
  useMoveSessionToProject,
11
10
  useSessionSetup,
12
11
  } from "./queries";
@@ -1,11 +1,7 @@
1
1
  import { useMutation, useQueryClient } from "@tanstack/react-query";
2
- import type { Session, Group } from "@/lib/db";
2
+ import type { Session } from "@/lib/db";
3
3
  import { sessionKeys } from "./keys";
4
-
5
- interface SessionsResponse {
6
- sessions: Session[];
7
- groups: Group[];
8
- }
4
+ import type { SessionsResponse } from "./queries";
9
5
 
10
6
  export function usePinSession() {
11
7
  const queryClient = useQueryClient();
@@ -1,13 +1,15 @@
1
+ import { toast } from "sonner";
1
2
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
- import type { Session, Group } from "@/lib/db";
3
+ import type { Session } from "@/lib/db";
4
+ import type { SessionGroup } from "@/lib/sidebar/shelves";
3
5
  import type { AgentType } from "@/lib/providers";
4
6
  import type { ChatAccess, ChatFile, ChatImage } from "@/lib/chat/events";
5
7
  import { sessionKeys } from "./keys";
6
8
  import { usePollWhenOffline, usePushConnected } from "../push/connection";
7
9
 
8
- interface SessionsResponse {
10
+ export interface SessionsResponse {
9
11
  sessions: Session[];
10
- groups: Group[];
12
+ sessionGroups: SessionGroup[];
11
13
  }
12
14
 
13
15
  async function fetchSessions(): Promise<SessionsResponse> {
@@ -128,31 +130,6 @@ export function useSummarizeSession() {
128
130
  });
129
131
  }
130
132
 
131
- export function useMoveSessionToGroup() {
132
- const queryClient = useQueryClient();
133
-
134
- return useMutation({
135
- mutationFn: async ({
136
- sessionId,
137
- groupPath,
138
- }: {
139
- sessionId: string;
140
- groupPath: string;
141
- }) => {
142
- const res = await fetch(`/api/sessions/${sessionId}`, {
143
- method: "PATCH",
144
- headers: { "Content-Type": "application/json" },
145
- body: JSON.stringify({ groupPath }),
146
- });
147
- if (!res.ok) throw new Error("Failed to move session");
148
- return res.json();
149
- },
150
- onSuccess: () => {
151
- queryClient.invalidateQueries({ queryKey: sessionKeys.list() });
152
- },
153
- });
154
- }
155
-
156
133
  export function useMoveSessionToProject() {
157
134
  const queryClient = useQueryClient();
158
135
 
@@ -172,7 +149,11 @@ export function useMoveSessionToProject() {
172
149
  if (!res.ok) throw new Error("Failed to move session");
173
150
  return res.json();
174
151
  },
175
- onSuccess: () => {
152
+ onSuccess: (data: { clearedGroup?: string | null }) => {
153
+ if (data.clearedGroup)
154
+ toast(`Removed from ${data.clearedGroup}`, {
155
+ description: "Groups stay inside one workspace.",
156
+ });
176
157
  queryClient.invalidateQueries({ queryKey: sessionKeys.list() });
177
158
  },
178
159
  });
@@ -1,12 +1,8 @@
1
1
  import { useMutation, useQueryClient } from "@tanstack/react-query";
2
- import type { Session, Group } from "@/lib/db";
2
+ import type { Session } from "@/lib/db";
3
3
  import { toSqliteTime } from "@/lib/session-meta";
4
4
  import { sessionKeys } from "./keys";
5
-
6
- interface SessionsResponse {
7
- sessions: Session[];
8
- groups: Group[];
9
- }
5
+ import type { SessionsResponse } from "./queries";
10
6
 
11
7
  async function post(url: string, body: object) {
12
8
  const res = await fetch(url, {
@@ -47,6 +47,8 @@ export function useCreateTask() {
47
47
  priority?: Priority;
48
48
  baseBranch?: string;
49
49
  hostId?: string;
50
+ // Queue behind other work: a task, or "claim:<id>".
51
+ after?: string;
50
52
  view?: "chat" | "terminal";
51
53
  }) =>
52
54
  // Over the workspace's running task limit it's queued instead.
@@ -6,20 +6,17 @@ import {
6
6
  useRenameSession,
7
7
  useForkSession,
8
8
  useSummarizeSession,
9
- useMoveSessionToGroup,
10
9
  useMoveSessionToProject,
11
10
  } from "@/data/sessions";
12
11
 
13
12
  export function useSessions() {
14
13
  const { data, refetch } = useSessionsQuery();
15
14
  const sessions = data?.sessions ?? [];
16
- const groups = data?.groups ?? [];
17
15
 
18
16
  const deleteMutation = useDeleteSession();
19
17
  const renameMutation = useRenameSession();
20
18
  const forkMutation = useForkSession();
21
19
  const summarizeMutation = useSummarizeSession();
22
- const moveToGroupMutation = useMoveSessionToGroup();
23
20
  const moveToProjectMutation = useMoveSessionToProject();
24
21
 
25
22
  const fetchSessions = useCallback(async () => {
@@ -61,13 +58,6 @@ export function useSessions() {
61
58
  [summarizeMutation]
62
59
  );
63
60
 
64
- const moveSessionToGroup = useCallback(
65
- async (sessionId: string, groupPath: string) => {
66
- await moveToGroupMutation.mutateAsync({ sessionId, groupPath });
67
- },
68
- [moveToGroupMutation]
69
- );
70
-
71
61
  const moveSessionToProject = useCallback(
72
62
  async (sessionId: string, projectId: string) => {
73
63
  await moveToProjectMutation.mutateAsync({ sessionId, projectId });
@@ -77,7 +67,6 @@ export function useSessions() {
77
67
 
78
68
  return {
79
69
  sessions,
80
- groups,
81
70
  loaded: data !== undefined,
82
71
  summarizingSessionId: summarizeMutation.isPending
83
72
  ? (summarizeMutation.variables as string)
@@ -88,7 +77,6 @@ export function useSessions() {
88
77
  renameSession,
89
78
  forkSession,
90
79
  summarizeSession,
91
- moveSessionToGroup,
92
80
  moveSessionToProject,
93
81
  };
94
82
  }