@saadnvd1/agent-os 0.3.700 → 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 (185) hide show
  1. package/README.md +1 -0
  2. package/app/api/bus/groups/route.ts +44 -0
  3. package/app/api/bus/spawn/route.ts +12 -1
  4. package/app/api/claims/[id]/route.ts +20 -0
  5. package/app/api/claims/check/route.ts +23 -0
  6. package/app/api/claims/check.test.ts +113 -0
  7. package/app/api/claims/exchange/route.ts +42 -0
  8. package/app/api/claims/outside.test.ts +90 -0
  9. package/app/api/claims/route.test.ts +193 -0
  10. package/app/api/claims/route.ts +73 -0
  11. package/app/api/hosts/[id]/route.ts +9 -1
  12. package/app/api/peer/unlink/route.ts +17 -0
  13. package/app/api/peers/[hostId]/audit/route.ts +14 -0
  14. package/app/api/peers/[hostId]/route.ts +20 -0
  15. package/app/api/peers/gate.test.ts +64 -1
  16. package/app/api/peers/pending/route.test.ts +37 -0
  17. package/app/api/peers/pending/route.ts +11 -4
  18. package/app/api/session-groups/[id]/restore/route.ts +11 -0
  19. package/app/api/session-groups/[id]/route.ts +25 -0
  20. package/app/api/session-groups/order/route.ts +15 -0
  21. package/app/api/session-groups/route.ts +21 -0
  22. package/app/api/sessions/[id]/fork/route.ts +3 -0
  23. package/app/api/sessions/[id]/group/route.ts +18 -0
  24. package/app/api/sessions/[id]/route.ts +14 -4
  25. package/app/api/sessions/[id]/summarize/route.ts +2 -0
  26. package/app/api/sessions/claims.test.ts +122 -0
  27. package/app/api/sessions/group/route.ts +17 -0
  28. package/app/api/sessions/route.ts +15 -14
  29. package/app/page.tsx +5 -0
  30. package/components/Claims/ClaimFiles.tsx +38 -0
  31. package/components/Claims/ClaimRow.tsx +82 -0
  32. package/components/Claims/ClaimsSheet.tsx +176 -0
  33. package/components/Claims/OverlapPill.tsx +31 -0
  34. package/components/Claims/StartClaimsNotice.tsx +70 -0
  35. package/components/Claims/claims-ui.test.ts +161 -0
  36. package/components/Claims/format.ts +57 -0
  37. package/components/Claims/index.ts +3 -0
  38. package/components/Draft/DraftPanel.tsx +40 -2
  39. package/components/Hosts/AccountMachines.tsx +32 -18
  40. package/components/Hosts/HostRow.tsx +155 -14
  41. package/components/Hosts/IncomingLinkSheet.tsx +28 -10
  42. package/components/Hosts/PeerActivitySheet.tsx +111 -0
  43. package/components/SessionList/GroupShelves.tsx +195 -0
  44. package/components/SessionList/MoveToGroupMenu.tsx +105 -0
  45. package/components/SessionList/RowSubtitle.tsx +7 -1
  46. package/components/SessionList/SelectionToolbar.tsx +52 -1
  47. package/components/SessionList/SessionRow.tsx +1 -0
  48. package/components/SessionList/SessionRowMenu.tsx +17 -0
  49. package/components/SessionList/SessionShelves.tsx +5 -0
  50. package/components/SessionList/SidebarBody.tsx +6 -0
  51. package/components/SessionList/SidebarContextMenu.test.ts +104 -0
  52. package/components/SessionList/SidebarContextMenu.tsx +59 -0
  53. package/components/SessionList/SidebarHeader.tsx +5 -0
  54. package/components/SessionList/WorkspaceSwitcher.tsx +23 -6
  55. package/components/SessionList/index.tsx +18 -14
  56. package/components/SessionList/useSidebarData.ts +10 -0
  57. package/components/ui/notice.tsx +46 -0
  58. package/components/ui/status-pill.tsx +39 -0
  59. package/data/claims/index.ts +2 -0
  60. package/data/claims/keys.ts +5 -0
  61. package/data/claims/queries.ts +47 -0
  62. package/data/peers/keys.ts +1 -0
  63. package/data/peers/queries.ts +31 -0
  64. package/data/push/topics.ts +4 -0
  65. package/data/session-groups/index.ts +156 -0
  66. package/data/sessions/index.ts +0 -1
  67. package/data/sessions/pin.ts +2 -6
  68. package/data/sessions/queries.ts +10 -29
  69. package/data/sessions/shelf.ts +2 -6
  70. package/data/tasks/index.ts +2 -0
  71. package/hooks/useSessions.ts +0 -12
  72. package/lib/__fixtures__/peer-rpc.ts +65 -33
  73. package/lib/agents/spawn.ts +4 -0
  74. package/lib/bus/index.ts +8 -0
  75. package/lib/claims/collectLocal.ts +121 -0
  76. package/lib/claims/describe.ts +41 -0
  77. package/lib/claims/exchange.ts +91 -0
  78. package/lib/claims/goal.ts +45 -0
  79. package/lib/claims/index.ts +10 -0
  80. package/lib/claims/loop.ts +51 -0
  81. package/lib/claims/machine.ts +21 -0
  82. package/lib/claims/mine.ts +43 -0
  83. package/lib/claims/notifyOverlaps.ts +101 -0
  84. package/lib/claims/onMerged.ts +85 -0
  85. package/lib/claims/outside.ts +36 -0
  86. package/lib/claims/overlap.ts +87 -0
  87. package/lib/claims/overlapCache.ts +86 -0
  88. package/lib/claims/overlapText.ts +36 -0
  89. package/lib/claims/peers.ts +56 -0
  90. package/lib/claims/startCheck.ts +100 -0
  91. package/lib/claims/startWarning.ts +29 -0
  92. package/lib/claims/store.ts +366 -0
  93. package/lib/claims/types.ts +102 -0
  94. package/lib/claims/view.ts +80 -0
  95. package/lib/claims/yield.ts +24 -0
  96. package/lib/db/changes.ts +6 -0
  97. package/lib/db/migrations/20261020T1400_work_claims.ts +35 -0
  98. package/lib/db/migrations/20261020T1515_peer_unlinked.ts +22 -0
  99. package/lib/db/migrations/20261020T1700_clean_connect_names.ts +18 -0
  100. package/lib/db/migrations/20261020T1800_session_groups.ts +49 -0
  101. package/lib/db/migrations/20261020T1900_task_queue_after_claim.ts +13 -0
  102. package/lib/db/migrations/20261020T2000_claim_notices.ts +20 -0
  103. package/lib/db/migrations/20261020T2100_claims_via_host.ts +21 -0
  104. package/lib/db/migrations/20261020T2200_work_claims_watch.ts +10 -0
  105. package/lib/db/queries.ts +0 -40
  106. package/lib/db/session-parts/columns.ts +1 -0
  107. package/lib/db/types.ts +16 -8
  108. package/lib/doctor/checks.ts +64 -1
  109. package/lib/git-remote.ts +43 -0
  110. package/lib/hosts/index.ts +10 -1
  111. package/lib/orchestration.ts +2 -0
  112. package/lib/orchestrator/act.ts +54 -4
  113. package/lib/orchestrator/brief.ts +2 -1
  114. package/lib/orchestrator/claim-wait.ts +71 -0
  115. package/lib/orchestrator/conditions.ts +28 -1
  116. package/lib/orchestrator/describe.ts +6 -1
  117. package/lib/orchestrator/facts.ts +5 -0
  118. package/lib/orchestrator/serve.ts +37 -2
  119. package/lib/orchestrator/tool-names.ts +1 -0
  120. package/lib/orchestrator/tool-schemas.ts +31 -1
  121. package/lib/orchestrator/tools.ts +3 -1
  122. package/lib/orchestrator/watcher.ts +19 -1
  123. package/lib/peer/account.ts +74 -2
  124. package/lib/peer/allowlist.ts +1 -1
  125. package/lib/peer/client.ts +26 -1
  126. package/lib/peer/link-cli.ts +19 -4
  127. package/lib/peer/links.ts +117 -5
  128. package/lib/peer/machines.ts +14 -2
  129. package/lib/peer/name.ts +32 -0
  130. package/lib/peer/pairing.ts +34 -5
  131. package/lib/peer/pin.ts +10 -3
  132. package/lib/peer/reasons.ts +7 -0
  133. package/lib/peer/revoke.ts +75 -0
  134. package/lib/peer/rpc-server.ts +53 -5
  135. package/lib/peer/unlink.ts +62 -0
  136. package/lib/peer/unlinked-proof.ts +44 -0
  137. package/lib/projects.ts +5 -2
  138. package/lib/security/demo.ts +7 -0
  139. package/lib/session-groups/assignSessions.ts +81 -0
  140. package/lib/session-groups/cli.ts +61 -0
  141. package/lib/session-groups/createGroup.ts +28 -0
  142. package/lib/session-groups/index.ts +21 -0
  143. package/lib/session-groups/removeGroup.ts +36 -0
  144. package/lib/session-groups/reorderGroups.ts +31 -0
  145. package/lib/session-groups/resolveGroup.ts +19 -0
  146. package/lib/session-groups/route.ts +32 -0
  147. package/lib/session-groups/store.ts +85 -0
  148. package/lib/session-groups/types.ts +25 -0
  149. package/lib/session-groups/updateGroup.ts +35 -0
  150. package/lib/session-groups/workspace.ts +21 -0
  151. package/lib/sessions/launch.ts +5 -2
  152. package/lib/sidebar/rollup.ts +1 -0
  153. package/lib/sidebar/row-menu.ts +2 -0
  154. package/lib/sidebar/shelves.ts +90 -15
  155. package/lib/sidebar/visible-order.ts +5 -0
  156. package/lib/tasks/hold-checks.ts +3 -1
  157. package/lib/tasks/holds.ts +3 -0
  158. package/lib/tasks/index.ts +5 -0
  159. package/lib/tasks/queue-rows.ts +8 -1
  160. package/lib/tasks/queue-start.ts +1 -0
  161. package/lib/tasks/queue-types.ts +9 -1
  162. package/lib/tasks/queue.ts +15 -5
  163. package/lib/tasks/schedule-types.ts +1 -0
  164. package/lib/test-temp.ts +19 -3
  165. package/lib/workspaces.ts +11 -3
  166. package/lib/worktree-sweep/testTemp.ts +23 -106
  167. package/package.json +1 -1
  168. package/scripts/agent-os +1 -1
  169. package/scripts/autodeploy +2 -0
  170. package/scripts/autodeploy.test.ts +31 -1
  171. package/scripts/claim-hook.sh +31 -0
  172. package/scripts/claim-hook.test.ts +230 -0
  173. package/scripts/lib/cli-links.sh +36 -0
  174. package/scripts/lib/commands.sh +3 -0
  175. package/scripts/lib/machine.sh +27 -2
  176. package/scripts/lib/machine.test.ts +25 -3
  177. package/scripts/sync-cli-links +7 -0
  178. package/scripts/sync-cli-links.test.ts +69 -0
  179. package/server.ts +3 -0
  180. package/stores/claimsUi.ts +22 -0
  181. package/app/api/groups/[...path]/route.ts +0 -136
  182. package/app/api/groups/route.ts +0 -93
  183. package/data/groups/index.ts +0 -1
  184. package/data/groups/mutations.ts +0 -95
  185. package/hooks/useGroups.ts +0 -37
@@ -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,
@@ -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,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
+ }
@@ -3,4 +3,5 @@ export const peerKeys = {
3
3
  account: () => [...peerKeys.all, "account"] as const,
4
4
  pending: () => [...peerKeys.all, "pending"] as const,
5
5
  link: (id: string) => [...peerKeys.all, "link", id] as const,
6
+ audit: (hostId: string) => [...peerKeys.all, "audit", hostId] as const,
6
7
  };
@@ -1,4 +1,5 @@
1
1
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
+ import type { AuditRow } from "@/lib/peer/audit";
2
3
  import type { AccountMachines } from "@/lib/peer/machines";
3
4
  import type { PendingView } from "@/lib/peer/pairing";
4
5
  import type { Outgoing } from "@/lib/peer/pairing-out";
@@ -139,3 +140,33 @@ export function useAnswerLink() {
139
140
  }),
140
141
  };
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
  }