@saadnvd1/agent-os 0.3.700 → 0.3.723

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (306) hide show
  1. package/README.md +2 -0
  2. package/app/api/agent-roles/route.test.ts +67 -0
  3. package/app/api/agent-roles/route.ts +61 -0
  4. package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
  5. package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
  6. package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
  7. package/app/api/analyst/[id]/review/route.ts +22 -0
  8. package/app/api/analyst/[id]/route.ts +44 -0
  9. package/app/api/analyst/route.ts +10 -0
  10. package/app/api/bus/groups/route.ts +44 -0
  11. package/app/api/bus/spawn/route.ts +34 -2
  12. package/app/api/claims/[id]/route.ts +20 -0
  13. package/app/api/claims/check/route.ts +23 -0
  14. package/app/api/claims/check.test.ts +113 -0
  15. package/app/api/claims/exchange/route.ts +42 -0
  16. package/app/api/claims/outside.test.ts +90 -0
  17. package/app/api/claims/route.test.ts +193 -0
  18. package/app/api/claims/route.ts +73 -0
  19. package/app/api/hosts/[id]/route.ts +9 -1
  20. package/app/api/models/availability/route.ts +9 -0
  21. package/app/api/models/choices/route.ts +9 -0
  22. package/app/api/peer/unlink/route.ts +17 -0
  23. package/app/api/peers/[hostId]/audit/route.ts +14 -0
  24. package/app/api/peers/[hostId]/route.ts +20 -0
  25. package/app/api/peers/gate.test.ts +64 -1
  26. package/app/api/peers/pending/route.test.ts +37 -0
  27. package/app/api/peers/pending/route.ts +11 -4
  28. package/app/api/session-groups/[id]/restore/route.ts +11 -0
  29. package/app/api/session-groups/[id]/route.ts +25 -0
  30. package/app/api/session-groups/order/route.ts +15 -0
  31. package/app/api/session-groups/route.ts +21 -0
  32. package/app/api/sessions/[id]/analyst/route.ts +11 -0
  33. package/app/api/sessions/[id]/crew/route.ts +38 -0
  34. package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
  35. package/app/api/sessions/[id]/export/route.test.ts +125 -0
  36. package/app/api/sessions/[id]/export/route.ts +34 -0
  37. package/app/api/sessions/[id]/fork/route.ts +3 -0
  38. package/app/api/sessions/[id]/group/route.ts +18 -0
  39. package/app/api/sessions/[id]/model/route.ts +49 -0
  40. package/app/api/sessions/[id]/route.ts +14 -4
  41. package/app/api/sessions/[id]/summarize/route.ts +2 -0
  42. package/app/api/sessions/claims.test.ts +122 -0
  43. package/app/api/sessions/group/route.ts +17 -0
  44. package/app/api/sessions/route.ts +31 -14
  45. package/app/page.tsx +10 -0
  46. package/components/Chat/Messages.tsx +104 -10
  47. package/components/Claims/ClaimFiles.tsx +38 -0
  48. package/components/Claims/ClaimRow.tsx +82 -0
  49. package/components/Claims/ClaimsSheet.tsx +176 -0
  50. package/components/Claims/OverlapPill.tsx +31 -0
  51. package/components/Claims/StartClaimsNotice.tsx +70 -0
  52. package/components/Claims/claims-ui.test.ts +161 -0
  53. package/components/Claims/format.ts +57 -0
  54. package/components/Claims/index.ts +3 -0
  55. package/components/Draft/DraftChips.tsx +6 -1
  56. package/components/Draft/DraftPanel.tsx +90 -3
  57. package/components/Draft/ModelUnavailableDialog.tsx +78 -0
  58. package/components/Draft/RoleChips.tsx +110 -0
  59. package/components/Export/ExportActions.tsx +136 -0
  60. package/components/Export/ExportSheet.tsx +217 -0
  61. package/components/Export/RangePicker.tsx +144 -0
  62. package/components/Export/Seg.tsx +38 -0
  63. package/components/Export/TurnSelectBar.tsx +45 -0
  64. package/components/Hosts/AccountMachines.tsx +32 -18
  65. package/components/Hosts/HostRow.tsx +155 -14
  66. package/components/Hosts/IncomingLinkSheet.tsx +28 -10
  67. package/components/Hosts/PeerActivitySheet.tsx +111 -0
  68. package/components/Models/AgentRoleCard.tsx +114 -0
  69. package/components/Models/AgentRolesSection.tsx +132 -0
  70. package/components/Models/ModelBadges.tsx +48 -0
  71. package/components/Models/ModelsSection.tsx +4 -2
  72. package/components/Models/RoleModelSelect.tsx +87 -0
  73. package/components/Pane/DesktopTabBar.tsx +2 -0
  74. package/components/Pane/MobileTabBar.tsx +12 -0
  75. package/components/Pane/PaneMenu.tsx +10 -0
  76. package/components/SessionList/GroupShelves.tsx +195 -0
  77. package/components/SessionList/MoveToGroupMenu.tsx +105 -0
  78. package/components/SessionList/RowSubtitle.tsx +7 -1
  79. package/components/SessionList/SelectionToolbar.tsx +52 -1
  80. package/components/SessionList/SessionRow.tsx +19 -7
  81. package/components/SessionList/SessionRowMenu.tsx +23 -0
  82. package/components/SessionList/SessionShelves.tsx +5 -0
  83. package/components/SessionList/SidebarBody.tsx +6 -0
  84. package/components/SessionList/SidebarContextMenu.test.ts +104 -0
  85. package/components/SessionList/SidebarContextMenu.tsx +59 -0
  86. package/components/SessionList/SidebarHeader.tsx +5 -0
  87. package/components/SessionList/WorkspaceSwitcher.tsx +23 -6
  88. package/components/SessionList/index.tsx +18 -14
  89. package/components/SessionList/useSidebarData.ts +10 -0
  90. package/components/Sessions/RolePill.tsx +196 -0
  91. package/components/Settings/SettingsDialog.tsx +9 -0
  92. package/components/Share/ShareDialog.tsx +13 -0
  93. package/components/ui/notice.tsx +46 -0
  94. package/components/ui/status-pill.tsx +39 -0
  95. package/data/analyst/index.ts +3 -0
  96. package/data/analyst/keys.ts +6 -0
  97. package/data/analyst/mutations.ts +116 -0
  98. package/data/analyst/queries.ts +35 -0
  99. package/data/claims/index.ts +2 -0
  100. package/data/claims/keys.ts +5 -0
  101. package/data/claims/queries.ts +47 -0
  102. package/data/exports/index.ts +2 -0
  103. package/data/exports/keys.ts +3 -0
  104. package/data/exports/queries.ts +51 -0
  105. package/data/models/index.ts +98 -0
  106. package/data/peers/keys.ts +1 -0
  107. package/data/peers/queries.ts +31 -0
  108. package/data/push/topics.ts +9 -0
  109. package/data/session-groups/index.ts +156 -0
  110. package/data/sessions/index.ts +0 -1
  111. package/data/sessions/pin.ts +2 -6
  112. package/data/sessions/queries.ts +36 -29
  113. package/data/sessions/shelf.ts +2 -6
  114. package/data/tasks/index.ts +2 -0
  115. package/hooks/useSessions.ts +0 -12
  116. package/lib/__fixtures__/peer-rpc.ts +65 -33
  117. package/lib/agents/brief.ts +1 -0
  118. package/lib/agents/spawn.ts +4 -0
  119. package/lib/analyst/__fixtures__/items.ts +57 -0
  120. package/lib/analyst/__fixtures__/mocks.ts +49 -0
  121. package/lib/analyst/brief.ts +17 -0
  122. package/lib/analyst/buildDigest.ts +204 -0
  123. package/lib/analyst/createLink.ts +98 -0
  124. package/lib/analyst/endLink.ts +31 -0
  125. package/lib/analyst/forward.ts +109 -0
  126. package/lib/analyst/index.ts +16 -0
  127. package/lib/analyst/linkView.ts +38 -0
  128. package/lib/analyst/onTurnEnd.ts +119 -0
  129. package/lib/analyst/parseReview.ts +41 -0
  130. package/lib/analyst/pushDigest.ts +143 -0
  131. package/lib/analyst/resume.ts +35 -0
  132. package/lib/analyst/route.ts +50 -0
  133. package/lib/analyst/signals.ts +121 -0
  134. package/lib/analyst/store.ts +303 -0
  135. package/lib/analyst/types.ts +140 -0
  136. package/lib/bus/condense.ts +5 -1
  137. package/lib/bus/index.ts +13 -0
  138. package/lib/bus/read-scope.ts +12 -0
  139. package/lib/bus/role-alias.ts +70 -0
  140. package/lib/bus/targets.ts +3 -0
  141. package/lib/chat/drivers/opencode-mapper.ts +55 -1
  142. package/lib/chat/drivers/pi-approvals.ts +7 -4
  143. package/lib/chat/drivers/pi-mapper.ts +38 -0
  144. package/lib/chat/drivers/pi-text-tools.ts +104 -0
  145. package/lib/chat/drivers/pi.ts +8 -3
  146. package/lib/chat/drivers/turn-items.ts +36 -11
  147. package/lib/chat/drivers/turn-outcome.ts +108 -0
  148. package/lib/chat/events.ts +8 -0
  149. package/lib/chat/registry.ts +13 -0
  150. package/lib/chat/store.ts +19 -0
  151. package/lib/chat/turns.ts +1 -0
  152. package/lib/chat/worker/extras.ts +4 -2
  153. package/lib/claims/collectLocal.ts +123 -0
  154. package/lib/claims/describe.ts +41 -0
  155. package/lib/claims/exchange.ts +91 -0
  156. package/lib/claims/goal.ts +45 -0
  157. package/lib/claims/index.ts +10 -0
  158. package/lib/claims/loop.ts +51 -0
  159. package/lib/claims/machine.ts +21 -0
  160. package/lib/claims/mine.ts +43 -0
  161. package/lib/claims/notifyOverlaps.ts +101 -0
  162. package/lib/claims/onMerged.ts +85 -0
  163. package/lib/claims/outside.ts +36 -0
  164. package/lib/claims/overlap.ts +87 -0
  165. package/lib/claims/overlapCache.ts +86 -0
  166. package/lib/claims/overlapText.ts +36 -0
  167. package/lib/claims/peers.ts +56 -0
  168. package/lib/claims/startCheck.ts +100 -0
  169. package/lib/claims/startWarning.ts +29 -0
  170. package/lib/claims/store.ts +366 -0
  171. package/lib/claims/types.ts +102 -0
  172. package/lib/claims/view.ts +80 -0
  173. package/lib/claims/yield.ts +24 -0
  174. package/lib/db/changes.ts +12 -0
  175. package/lib/db/migrations/20261020T1400_work_claims.ts +35 -0
  176. package/lib/db/migrations/20261020T1515_peer_unlinked.ts +22 -0
  177. package/lib/db/migrations/20261020T1700_clean_connect_names.ts +18 -0
  178. package/lib/db/migrations/20261020T1800_session_groups.ts +49 -0
  179. package/lib/db/migrations/20261020T1900_task_queue_after_claim.ts +13 -0
  180. package/lib/db/migrations/20261020T2000_claim_notices.ts +20 -0
  181. package/lib/db/migrations/20261020T2100_claims_via_host.ts +21 -0
  182. package/lib/db/migrations/20261020T2200_work_claims_watch.ts +10 -0
  183. package/lib/db/migrations/20261021T1400_session_agent_roles.ts +20 -0
  184. package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
  185. package/lib/db/queries.ts +0 -40
  186. package/lib/db/session-parts/columns.ts +3 -0
  187. package/lib/db/types.ts +19 -8
  188. package/lib/doctor/checks.ts +64 -1
  189. package/lib/done/archive.ts +2 -0
  190. package/lib/drafts.ts +4 -0
  191. package/lib/export/build.ts +470 -0
  192. package/lib/export/compact.ts +156 -0
  193. package/lib/export/estimate.ts +83 -0
  194. package/lib/export/fixtures.ts +45 -0
  195. package/lib/export/html.tsx +195 -0
  196. package/lib/export/json.ts +39 -0
  197. package/lib/export/load.ts +64 -0
  198. package/lib/export/markdown.ts +127 -0
  199. package/lib/export/request.ts +36 -0
  200. package/lib/export/service.ts +142 -0
  201. package/lib/export/summary.ts +86 -0
  202. package/lib/export/turns.ts +63 -0
  203. package/lib/export/view.ts +173 -0
  204. package/lib/git-remote.ts +43 -0
  205. package/lib/hosts/index.ts +10 -1
  206. package/lib/models/agent-binding.ts +102 -0
  207. package/lib/models/agent-roles-view.ts +157 -0
  208. package/lib/models/agent-roles.ts +87 -0
  209. package/lib/models/availability-refs.ts +17 -0
  210. package/lib/models/availability.ts +91 -0
  211. package/lib/models/catalog.ts +156 -0
  212. package/lib/models/config.ts +27 -0
  213. package/lib/models/locality.ts +33 -0
  214. package/lib/models/providers.ts +11 -0
  215. package/lib/models/resolve.ts +50 -12
  216. package/lib/models/roles.ts +12 -0
  217. package/lib/needs-you.ts +1 -1
  218. package/lib/orchestration.ts +2 -0
  219. package/lib/orchestrator/act.ts +54 -4
  220. package/lib/orchestrator/brief.ts +2 -1
  221. package/lib/orchestrator/claim-wait.ts +71 -0
  222. package/lib/orchestrator/conditions.ts +28 -1
  223. package/lib/orchestrator/describe.ts +6 -1
  224. package/lib/orchestrator/facts.ts +12 -2
  225. package/lib/orchestrator/read.ts +5 -1
  226. package/lib/orchestrator/serve.ts +37 -2
  227. package/lib/orchestrator/tool-names.ts +1 -0
  228. package/lib/orchestrator/tool-schemas.ts +31 -1
  229. package/lib/orchestrator/tools.ts +3 -1
  230. package/lib/orchestrator/untrusted.ts +5 -0
  231. package/lib/orchestrator/watcher.ts +19 -1
  232. package/lib/peer/account.ts +74 -2
  233. package/lib/peer/allowlist.ts +1 -1
  234. package/lib/peer/client.ts +26 -1
  235. package/lib/peer/link-cli.ts +19 -4
  236. package/lib/peer/links.ts +117 -5
  237. package/lib/peer/machines.ts +14 -2
  238. package/lib/peer/name.ts +32 -0
  239. package/lib/peer/pairing.ts +34 -5
  240. package/lib/peer/pin.ts +10 -3
  241. package/lib/peer/reasons.ts +7 -0
  242. package/lib/peer/revoke.ts +75 -0
  243. package/lib/peer/rpc-server.ts +53 -5
  244. package/lib/peer/unlink.ts +62 -0
  245. package/lib/peer/unlinked-proof.ts +44 -0
  246. package/lib/projects.ts +6 -2
  247. package/lib/security/demo.ts +7 -0
  248. package/lib/session-artifacts/agentos-files.ts +94 -0
  249. package/lib/session-artifacts/collect.ts +17 -0
  250. package/lib/session-groups/assignSessions.ts +81 -0
  251. package/lib/session-groups/cli.ts +61 -0
  252. package/lib/session-groups/createGroup.ts +28 -0
  253. package/lib/session-groups/index.ts +21 -0
  254. package/lib/session-groups/removeGroup.ts +36 -0
  255. package/lib/session-groups/reorderGroups.ts +31 -0
  256. package/lib/session-groups/resolveGroup.ts +19 -0
  257. package/lib/session-groups/route.ts +32 -0
  258. package/lib/session-groups/store.ts +85 -0
  259. package/lib/session-groups/types.ts +25 -0
  260. package/lib/session-groups/updateGroup.ts +35 -0
  261. package/lib/session-groups/workspace.ts +21 -0
  262. package/lib/sessions/crew.ts +72 -0
  263. package/lib/sessions/launch.ts +54 -11
  264. package/lib/settings-url.ts +1 -0
  265. package/lib/share/markdown.ts +2 -2
  266. package/lib/share/redact.ts +16 -10
  267. package/lib/share/render.tsx +1 -1
  268. package/lib/share/snapshot.ts +2 -2
  269. package/lib/sidebar/rollup.ts +1 -0
  270. package/lib/sidebar/row-menu.ts +8 -1
  271. package/lib/sidebar/shelves.ts +90 -15
  272. package/lib/sidebar/visible-order.ts +5 -0
  273. package/lib/tasks/hold-checks.ts +3 -1
  274. package/lib/tasks/holds.ts +3 -0
  275. package/lib/tasks/index.ts +11 -0
  276. package/lib/tasks/queue-rows.ts +8 -1
  277. package/lib/tasks/queue-start.ts +1 -0
  278. package/lib/tasks/queue-types.ts +9 -1
  279. package/lib/tasks/queue.ts +15 -5
  280. package/lib/tasks/schedule-types.ts +1 -0
  281. package/lib/test-temp.ts +19 -3
  282. package/lib/workspaces.ts +11 -3
  283. package/lib/worktree-deps/caches.ts +3 -2
  284. package/lib/worktree-deps/clone.ts +3 -1
  285. package/lib/worktree-deps/paths.ts +26 -0
  286. package/lib/worktree-sweep/testTemp.ts +23 -106
  287. package/package.json +1 -1
  288. package/scripts/agent-os +1 -1
  289. package/scripts/autodeploy +2 -0
  290. package/scripts/autodeploy.test.ts +31 -1
  291. package/scripts/claim-hook.sh +31 -0
  292. package/scripts/claim-hook.test.ts +230 -0
  293. package/scripts/lib/cli-links.sh +36 -0
  294. package/scripts/lib/commands.sh +3 -0
  295. package/scripts/lib/machine.sh +27 -2
  296. package/scripts/lib/machine.test.ts +25 -3
  297. package/scripts/sync-cli-links +7 -0
  298. package/scripts/sync-cli-links.test.ts +69 -0
  299. package/server.ts +12 -1
  300. package/stores/claimsUi.ts +22 -0
  301. package/stores/exportUi.ts +87 -0
  302. package/app/api/groups/[...path]/route.ts +0 -136
  303. package/app/api/groups/route.ts +0 -93
  304. package/data/groups/index.ts +0 -1
  305. package/data/groups/mutations.ts +0 -95
  306. package/hooks/useGroups.ts +0 -37
@@ -0,0 +1,69 @@
1
+ import { execFileSync } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { tempDir } from "@/lib/test-temp";
6
+
7
+ const script = path.resolve(__dirname, "sync-cli-links");
8
+
9
+ function setup() {
10
+ const root = tempDir("aos-synclinks-");
11
+ const home = path.join(root, "home");
12
+ const bin = path.join(root, "bin");
13
+ fs.mkdirSync(home);
14
+ fs.mkdirSync(bin);
15
+ const clone = (name: string) => {
16
+ const dir = path.join(root, name);
17
+ fs.mkdirSync(path.join(dir, "bin"), { recursive: true });
18
+ fs.mkdirSync(path.join(dir, "scripts"), { recursive: true });
19
+ fs.writeFileSync(path.join(dir, "bin/aos"), "");
20
+ fs.writeFileSync(path.join(dir, "scripts/agent-os"), "");
21
+ return dir;
22
+ };
23
+ const live = clone("live");
24
+ const old = clone("old");
25
+ fs.writeFileSync(
26
+ path.join(home, "live-server.json"),
27
+ JSON.stringify({ cwd: live })
28
+ );
29
+ const run = (...args: string[]) =>
30
+ execFileSync(script, args, {
31
+ encoding: "utf-8",
32
+ env: { ...process.env, AOS_BIN_DIR: bin, AGENT_OS_HOME: home },
33
+ });
34
+ return { bin, home, live, old, run };
35
+ }
36
+
37
+ describe("sync-cli-links", () => {
38
+ it("repoints a link into another clone and logs it", () => {
39
+ const t = setup();
40
+ fs.symlinkSync(path.join(t.old, "bin/aos"), path.join(t.bin, "aos"));
41
+ const out = t.run();
42
+ expect(fs.readlinkSync(path.join(t.bin, "aos"))).toBe(
43
+ path.join(t.live, "bin/aos")
44
+ );
45
+ expect(out).toContain("repointed");
46
+ expect(out).toContain(t.old);
47
+ expect(t.run()).toBe("");
48
+ });
49
+
50
+ it("leaves absent links alone unless asked to create them", () => {
51
+ const t = setup();
52
+ t.run();
53
+ expect(fs.existsSync(path.join(t.bin, "aos"))).toBe(false);
54
+ t.run("create");
55
+ expect(fs.readlinkSync(path.join(t.bin, "aos"))).toBe(
56
+ path.join(t.live, "bin/aos")
57
+ );
58
+ expect(fs.readlinkSync(path.join(t.bin, "agent-os"))).toBe(
59
+ path.join(t.live, "scripts/agent-os")
60
+ );
61
+ });
62
+
63
+ it("never replaces a real file", () => {
64
+ const t = setup();
65
+ fs.writeFileSync(path.join(t.bin, "aos"), "mine");
66
+ t.run("create");
67
+ expect(fs.readFileSync(path.join(t.bin, "aos"), "utf-8")).toBe("mine");
68
+ });
69
+ });
package/server.ts CHANGED
@@ -6,6 +6,8 @@ import next from "next";
6
6
  import { WebSocketServer, WebSocket } from "ws";
7
7
  import { ensureBusBrief } from "./lib/agents/launch";
8
8
  import { reattachChats } from "./lib/chat/runner";
9
+ import { setTurnEndHook } from "./lib/chat/registry";
10
+ import { analystTurnEndHook, resumeAnalystLinks } from "./lib/analyst";
9
11
  import { serveChatSocket } from "./lib/chat/socket";
10
12
  import { startStackWatcher } from "./lib/stacks";
11
13
  import { startPauseWatcher } from "./lib/tasks/pause";
@@ -21,6 +23,7 @@ import { realDeps, schedulesEnabled, startScheduler } from "./lib/schedules";
21
23
  import { startSettling } from "./lib/recipes";
22
24
  import { startAutoArchive } from "./lib/done/auto-archive";
23
25
  import { startWorktreeSweep } from "./lib/worktree-sweep/start";
26
+ import { startClaimsLoop } from "./lib/claims";
24
27
  import { startUsageRollup, usageRollupEnabled } from "./lib/usage/nightly";
25
28
  import { resumePhoneOutbox } from "./lib/notify";
26
29
  import { resumePushOutbox } from "./lib/notify/push";
@@ -223,6 +226,8 @@ app.prepare().then(async () => {
223
226
  const folder = sessionFolder(id);
224
227
  if (folder) refreshGitSoon(folder);
225
228
  });
229
+ // An analyst hears each of its coding agent's turns as they end.
230
+ setTurnEndHook(analystTurnEndHook);
226
231
  // Terminal sessions are watched through tmux control mode, or tapped with
227
232
  // pipe-pane where tmux is too old for it.
228
233
  const control = await startTmuxControl();
@@ -452,7 +457,11 @@ app.prepare().then(async () => {
452
457
  void sweepSims();
453
458
  setInterval(sweepSims, 5 * 60_000);
454
459
  // Chat turns that kept running through a restart.
455
- void reattachChats().finally(() => startPauseWatcher());
460
+ void reattachChats().finally(() => {
461
+ startPauseWatcher();
462
+ // Digests a restart cut off, and turns that ended while it was down.
463
+ void resumeAnalystLinks();
464
+ });
456
465
  // Older chats get into the search index in the background, a batch at a time.
457
466
  if (process.env.AGENTOS_SEARCH_INDEX !== "off") startConversationBackfill();
458
467
  // Reviews this restart cut off run again and deliver their verdicts.
@@ -497,6 +506,8 @@ app.prepare().then(async () => {
497
506
  startAutoArchive();
498
507
  // Settled worktrees older than the setting are removed; branches stay.
499
508
  startWorktreeSweep();
509
+ // Who is working on what, across linked machines.
510
+ if (!process.env.AGENTOS_DEMO) startClaimsLoop();
500
511
  resumePhoneOutbox();
501
512
  resumePushOutbox();
502
513
  // The usage ledger's daily rollup and its ratchet alerts.
@@ -0,0 +1,22 @@
1
+ import { proxy } from "valtio";
2
+
3
+ // The claims sheet ("Work across machines"). `focus` narrows it to the claims
4
+ // of one overlapping pair, from a row's pill.
5
+ export const claimsUi = proxy<{ open: boolean; focus: string[] | null }>({
6
+ open: false,
7
+ focus: null,
8
+ });
9
+
10
+ export const claimsUiActions = {
11
+ open: (focus: string[] | null = null) => {
12
+ claimsUi.focus = focus;
13
+ claimsUi.open = true;
14
+ },
15
+ setOpen: (open: boolean) => {
16
+ claimsUi.open = open;
17
+ if (!open) claimsUi.focus = null;
18
+ },
19
+ showAll: () => {
20
+ claimsUi.focus = null;
21
+ },
22
+ };
@@ -0,0 +1,87 @@
1
+ import { proxy } from "valtio";
2
+ import {
3
+ DEFAULT_REQUEST,
4
+ type ExportRequest,
5
+ type ExportTurnInfo,
6
+ } from "@/lib/export/view";
7
+
8
+ // The export sheet, and picking turns in the chat for it. The request lives
9
+ // here so it survives closing the sheet to select turns.
10
+ export const exportUi = proxy<{
11
+ open: boolean;
12
+ sessionId: string | null;
13
+ title: string;
14
+ req: ExportRequest;
15
+ /** Open at the turn that began with this message, to the end. */
16
+ fromItemId: string | null;
17
+ /** The session's turns, as the last estimate listed them. */
18
+ turns: ExportTurnInfo[];
19
+ selecting: boolean;
20
+ pick: { from: number | null; to: number | null };
21
+ }>({
22
+ open: false,
23
+ sessionId: null,
24
+ title: "",
25
+ req: { ...DEFAULT_REQUEST },
26
+ fromItemId: null,
27
+ turns: [],
28
+ selecting: false,
29
+ pick: { from: null, to: null },
30
+ });
31
+
32
+ export const exportUiActions = {
33
+ open(sessionId: string, title: string, fromItemId: string | null = null) {
34
+ if (exportUi.sessionId !== sessionId) {
35
+ exportUi.turns = [];
36
+ exportUi.req = { ...exportUi.req, range: undefined };
37
+ }
38
+ exportUi.sessionId = sessionId;
39
+ exportUi.title = title;
40
+ exportUi.fromItemId = fromItemId;
41
+ exportUi.open = true;
42
+ },
43
+ close() {
44
+ exportUi.open = false;
45
+ exportUi.selecting = false;
46
+ exportUi.fromItemId = null;
47
+ },
48
+ setRequest(patch: Partial<ExportRequest>) {
49
+ exportUi.req = { ...exportUi.req, ...patch };
50
+ },
51
+ /** Close the sheet and let the chat's user messages be ticked. */
52
+ startSelect(from: number | null = null) {
53
+ exportUi.open = false;
54
+ exportUi.selecting = true;
55
+ exportUi.pick = { from, to: null };
56
+ },
57
+ /** A tap on a turn: the first sets the start, the second the end. */
58
+ tap(n: number) {
59
+ const { from, to } = exportUi.pick;
60
+ exportUi.pick =
61
+ from === null || to !== null ? { from: n, to: null } : { from, to: n };
62
+ },
63
+ /** Use the ticked turns as the range and go back to the sheet. */
64
+ finishSelect() {
65
+ const { from, to } = exportUi.pick;
66
+ if (from !== null) {
67
+ const end = to ?? from;
68
+ exportUi.req = {
69
+ ...exportUi.req,
70
+ range: { from: Math.min(from, end), to: Math.max(from, end) },
71
+ };
72
+ }
73
+ exportUi.selecting = false;
74
+ exportUi.open = true;
75
+ },
76
+ cancelSelect() {
77
+ exportUi.selecting = false;
78
+ exportUi.open = true;
79
+ },
80
+ };
81
+
82
+ /** The ticked turns, ordered: null until something is ticked. */
83
+ export function pickedRange(pick: { from: number | null; to: number | null }) {
84
+ if (pick.from === null) return null;
85
+ const end = pick.to ?? pick.from;
86
+ return { from: Math.min(pick.from, end), to: Math.max(pick.from, end) };
87
+ }
@@ -1,136 +0,0 @@
1
- import { NextResponse } from "next/server";
2
- import { db, queries, type Group } from "@/lib/db";
3
-
4
- // GET /api/groups/[...path] - Get a single group
5
- export async function GET(
6
- request: Request,
7
- { params }: { params: Promise<{ path: string[] }> }
8
- ) {
9
- const { path: pathParts } = await params;
10
- const path = pathParts.join("/");
11
-
12
- try {
13
- const group = queries.getGroup(db).get(path) as Group | undefined;
14
-
15
- if (!group) {
16
- return NextResponse.json({ error: "Group not found" }, { status: 404 });
17
- }
18
-
19
- return NextResponse.json({
20
- group: { ...group, expanded: Boolean(group.expanded) },
21
- });
22
- } catch (error) {
23
- console.error("Error fetching group:", error);
24
- return NextResponse.json(
25
- { error: "Failed to fetch group" },
26
- { status: 500 }
27
- );
28
- }
29
- }
30
-
31
- // PATCH /api/groups/[...path] - Update group (name, expanded, order)
32
- export async function PATCH(
33
- request: Request,
34
- { params }: { params: Promise<{ path: string[] }> }
35
- ) {
36
- const { path: pathParts } = await params;
37
- const path = pathParts.join("/");
38
-
39
- try {
40
- const body = await request.json();
41
- const { name, expanded, sort_order } = body;
42
-
43
- // Check if group exists
44
- const group = queries.getGroup(db).get(path) as Group | undefined;
45
- if (!group) {
46
- return NextResponse.json({ error: "Group not found" }, { status: 404 });
47
- }
48
-
49
- // Protect default group from being renamed
50
- if (path === "sessions" && name !== undefined && name !== "Sessions") {
51
- return NextResponse.json(
52
- { error: "Cannot rename the default group" },
53
- { status: 400 }
54
- );
55
- }
56
-
57
- // Update name
58
- if (name !== undefined) {
59
- queries.updateGroupName(db).run(name, path);
60
- }
61
-
62
- // Update expanded state
63
- if (expanded !== undefined) {
64
- queries.updateGroupExpanded(db).run(expanded ? 1 : 0, path);
65
- }
66
-
67
- // Update sort order
68
- if (sort_order !== undefined) {
69
- queries.updateGroupOrder(db).run(sort_order, path);
70
- }
71
-
72
- const updatedGroup = queries.getGroup(db).get(path) as Group;
73
- return NextResponse.json({
74
- group: { ...updatedGroup, expanded: Boolean(updatedGroup.expanded) },
75
- });
76
- } catch (error) {
77
- console.error("Error updating group:", error);
78
- return NextResponse.json(
79
- { error: "Failed to update group" },
80
- { status: 500 }
81
- );
82
- }
83
- }
84
-
85
- // DELETE /api/groups/[...path] - Delete group (moves sessions to parent or default)
86
- export async function DELETE(
87
- request: Request,
88
- { params }: { params: Promise<{ path: string[] }> }
89
- ) {
90
- const { path: pathParts } = await params;
91
- const path = pathParts.join("/");
92
-
93
- try {
94
- // Protect default group
95
- if (path === "sessions") {
96
- return NextResponse.json(
97
- { error: "Cannot delete the default group" },
98
- { status: 400 }
99
- );
100
- }
101
-
102
- // Check if group exists
103
- const group = queries.getGroup(db).get(path) as Group | undefined;
104
- if (!group) {
105
- return NextResponse.json({ error: "Group not found" }, { status: 404 });
106
- }
107
-
108
- // Find parent group or use default
109
- const pathParts2 = path.split("/");
110
- pathParts2.pop();
111
- const parentPath =
112
- pathParts2.length > 0 ? pathParts2.join("/") : "sessions";
113
-
114
- // Move all sessions in this group to parent
115
- queries.moveSessionsToGroup(db).run(parentPath, path);
116
-
117
- // Also move sessions from any subgroups
118
- const allGroups = queries.getAllGroups(db).all() as Group[];
119
- const subgroups = allGroups.filter((g) => g.path.startsWith(path + "/"));
120
- for (const subgroup of subgroups) {
121
- queries.moveSessionsToGroup(db).run(parentPath, subgroup.path);
122
- queries.deleteGroup(db).run(subgroup.path);
123
- }
124
-
125
- // Delete the group
126
- queries.deleteGroup(db).run(path);
127
-
128
- return NextResponse.json({ success: true, movedTo: parentPath });
129
- } catch (error) {
130
- console.error("Error deleting group:", error);
131
- return NextResponse.json(
132
- { error: "Failed to delete group" },
133
- { status: 500 }
134
- );
135
- }
136
- }
@@ -1,93 +0,0 @@
1
- import { NextResponse } from "next/server";
2
- import { db, queries, type Group } from "@/lib/db";
3
-
4
- // GET /api/groups - List all groups
5
- export async function GET() {
6
- try {
7
- const groups = queries.getAllGroups(db).all() as Group[];
8
-
9
- // Convert expanded from 0/1 to boolean
10
- const formattedGroups = groups.map((g) => ({
11
- ...g,
12
- expanded: Boolean(g.expanded),
13
- }));
14
-
15
- return NextResponse.json({ groups: formattedGroups });
16
- } catch (error) {
17
- console.error("Error fetching groups:", error);
18
- return NextResponse.json(
19
- { error: "Failed to fetch groups" },
20
- { status: 500 }
21
- );
22
- }
23
- }
24
-
25
- // POST /api/groups - Create a new group
26
- export async function POST(request: Request) {
27
- try {
28
- const body = await request.json();
29
- const { name, parentPath } = body;
30
-
31
- if (!name || typeof name !== "string") {
32
- return NextResponse.json({ error: "Name is required" }, { status: 400 });
33
- }
34
-
35
- // Sanitize name to create path
36
- const sanitizedName = name
37
- .toLowerCase()
38
- .replace(/[^a-z0-9-]/g, "-")
39
- .replace(/-+/g, "-")
40
- .replace(/^-|-$/g, "");
41
-
42
- if (!sanitizedName) {
43
- return NextResponse.json(
44
- { error: "Invalid group name" },
45
- { status: 400 }
46
- );
47
- }
48
-
49
- // Build full path
50
- const path = parentPath ? `${parentPath}/${sanitizedName}` : sanitizedName;
51
-
52
- // Check if group already exists
53
- const existing = queries.getGroup(db).get(path) as Group | undefined;
54
- if (existing) {
55
- return NextResponse.json(
56
- { error: "Group already exists", group: existing },
57
- { status: 409 }
58
- );
59
- }
60
-
61
- // If parent path specified, ensure parent exists
62
- if (parentPath) {
63
- const parent = queries.getGroup(db).get(parentPath) as Group | undefined;
64
- if (!parent) {
65
- return NextResponse.json(
66
- { error: "Parent group does not exist" },
67
- { status: 400 }
68
- );
69
- }
70
- }
71
-
72
- // Get max sort order for new group
73
- const groups = queries.getAllGroups(db).all() as Group[];
74
- const maxOrder = groups.reduce((max, g) => Math.max(max, g.sort_order), 0);
75
-
76
- // Create the group
77
- queries.createGroup(db).run(path, name, maxOrder + 1);
78
-
79
- const newGroup = queries.getGroup(db).get(path) as Group;
80
- return NextResponse.json(
81
- {
82
- group: { ...newGroup, expanded: Boolean(newGroup.expanded) },
83
- },
84
- { status: 201 }
85
- );
86
- } catch (error) {
87
- console.error("Error creating group:", error);
88
- return NextResponse.json(
89
- { error: "Failed to create group" },
90
- { status: 500 }
91
- );
92
- }
93
- }
@@ -1 +0,0 @@
1
- export { useToggleGroup, useCreateGroup, useDeleteGroup } from "./mutations";
@@ -1,95 +0,0 @@
1
- import { useMutation, useQueryClient } from "@tanstack/react-query";
2
- import { sessionKeys } from "../sessions/keys";
3
-
4
- export function useToggleGroup() {
5
- const queryClient = useQueryClient();
6
-
7
- return useMutation({
8
- mutationFn: async ({
9
- path,
10
- expanded,
11
- }: {
12
- path: string;
13
- expanded: boolean;
14
- }) => {
15
- const res = await fetch(`/api/groups/${encodeURIComponent(path)}`, {
16
- method: "PATCH",
17
- headers: { "Content-Type": "application/json" },
18
- body: JSON.stringify({ expanded }),
19
- });
20
- if (!res.ok) throw new Error("Failed to toggle group");
21
- return res.json();
22
- },
23
- onMutate: async ({ path, expanded }) => {
24
- await queryClient.cancelQueries({ queryKey: sessionKeys.list() });
25
- const previous = queryClient.getQueryData(sessionKeys.list());
26
- queryClient.setQueryData(
27
- sessionKeys.list(),
28
- (
29
- old:
30
- | {
31
- sessions: unknown[];
32
- groups: Array<{ path: string; expanded: boolean }>;
33
- }
34
- | undefined
35
- ) =>
36
- old
37
- ? {
38
- ...old,
39
- groups: old.groups.map((g) =>
40
- g.path === path ? { ...g, expanded } : g
41
- ),
42
- }
43
- : old
44
- );
45
- return { previous };
46
- },
47
- onError: (_, __, context) => {
48
- if (context?.previous) {
49
- queryClient.setQueryData(sessionKeys.list(), context.previous);
50
- }
51
- },
52
- });
53
- }
54
-
55
- export function useCreateGroup() {
56
- const queryClient = useQueryClient();
57
-
58
- return useMutation({
59
- mutationFn: async ({
60
- name,
61
- parentPath,
62
- }: {
63
- name: string;
64
- parentPath?: string;
65
- }) => {
66
- const res = await fetch("/api/groups", {
67
- method: "POST",
68
- headers: { "Content-Type": "application/json" },
69
- body: JSON.stringify({ name, parentPath }),
70
- });
71
- if (!res.ok) throw new Error("Failed to create group");
72
- return res.json();
73
- },
74
- onSuccess: () => {
75
- queryClient.invalidateQueries({ queryKey: sessionKeys.list() });
76
- },
77
- });
78
- }
79
-
80
- export function useDeleteGroup() {
81
- const queryClient = useQueryClient();
82
-
83
- return useMutation({
84
- mutationFn: async (path: string) => {
85
- const res = await fetch(`/api/groups/${encodeURIComponent(path)}`, {
86
- method: "DELETE",
87
- });
88
- if (!res.ok) throw new Error("Failed to delete group");
89
- return res.json();
90
- },
91
- onSuccess: () => {
92
- queryClient.invalidateQueries({ queryKey: sessionKeys.list() });
93
- },
94
- });
95
- }
@@ -1,37 +0,0 @@
1
- import { useCallback } from "react";
2
- import { useToggleGroup, useCreateGroup, useDeleteGroup } from "@/data/groups";
3
-
4
- export function useGroups() {
5
- const toggleMutation = useToggleGroup();
6
- const createMutation = useCreateGroup();
7
- const deleteMutation = useDeleteGroup();
8
-
9
- const toggleGroup = useCallback(
10
- async (path: string, expanded: boolean) => {
11
- await toggleMutation.mutateAsync({ path, expanded });
12
- },
13
- [toggleMutation]
14
- );
15
-
16
- const createGroup = useCallback(
17
- async (name: string, parentPath?: string) => {
18
- await createMutation.mutateAsync({ name, parentPath });
19
- },
20
- [createMutation]
21
- );
22
-
23
- const deleteGroup = useCallback(
24
- async (path: string) => {
25
- if (!confirm("Delete this group? Sessions will be moved to parent."))
26
- return;
27
- await deleteMutation.mutateAsync(path);
28
- },
29
- [deleteMutation]
30
- );
31
-
32
- return {
33
- toggleGroup,
34
- createGroup,
35
- deleteGroup,
36
- };
37
- }