@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
@@ -19,6 +19,7 @@ import { statusDetector } from "@/lib/status-detector";
19
19
  import { deleteItems } from "@/lib/chat/store";
20
20
  import { clearQueue } from "@/lib/chat/queued";
21
21
  import { recordPreviousName } from "@/lib/session-names";
22
+ import { getProject, moveSessionToProject } from "@/lib/projects";
22
23
  import { generateBranchName, getCurrentBranch, renameBranch } from "@/lib/git";
23
24
  import { runInBackground } from "@/lib/async-operations";
24
25
  import {
@@ -206,9 +207,15 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
206
207
  updates.push("system_prompt = ?");
207
208
  values.push(body.systemPrompt);
208
209
  }
209
- if (body.groupPath !== undefined) {
210
- updates.push("group_path = ?");
211
- values.push(body.groupPath);
210
+
211
+ let clearedGroup: string | null = null;
212
+ if (typeof body.projectId === "string" && !existing.role) {
213
+ if (!getProject(body.projectId))
214
+ return NextResponse.json(
215
+ { error: "Project not found" },
216
+ { status: 404 }
217
+ );
218
+ clearedGroup = moveSessionToProject(id, body.projectId).clearedGroup;
212
219
  }
213
220
 
214
221
  if (updates.length > 0) {
@@ -221,7 +228,10 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
221
228
  }
222
229
 
223
230
  const session = queries.getSession(db).get(id) as Session;
224
- return NextResponse.json({ session });
231
+ return NextResponse.json({
232
+ session,
233
+ ...(clearedGroup && { clearedGroup }),
234
+ });
225
235
  } catch (error) {
226
236
  console.error("Error updating session:", error);
227
237
  return NextResponse.json(
@@ -1,3 +1,4 @@
1
+ import { inheritGroup } from "@/lib/session-groups/assignSessions";
1
2
  import { effectiveAutoApprove } from "@/lib/approvals/no-approvals";
2
3
  import { routedRun } from "@/lib/models/launch";
3
4
  import { NextRequest, NextResponse } from "next/server";
@@ -305,6 +306,7 @@ export async function POST(
305
306
  session.host_id
306
307
  );
307
308
 
309
+ inheritGroup(session.id, newId);
308
310
  newSession = queries.getSession(db).get(newId) as Session;
309
311
  const newTmuxSession = tmuxName;
310
312
 
@@ -0,0 +1,122 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import { seedSession, seedWorkspace } from "@/lib/orchestrator/testing";
3
+
4
+ const { db } = await import("@/lib/db");
5
+ const { machineId } = await import("@/lib/claims/machine");
6
+ const { upsertLocal, upsertRemote } = await import("@/lib/claims/store");
7
+ const { refreshOverlapCache, resetOverlapCache } =
8
+ await import("@/lib/claims/overlapCache");
9
+ const { GET } = await import("./route");
10
+
11
+ const REPO = "git.example.test/team/widgets";
12
+
13
+ beforeEach(() => {
14
+ db.prepare(`DELETE FROM work_claims`).run();
15
+ resetOverlapCache();
16
+ });
17
+
18
+ function localClaim(sessionId: string, files: string[]) {
19
+ upsertLocal(
20
+ { id: machineId(), name: "here" },
21
+ {
22
+ sessionId,
23
+ sessionName: "Tidy auth",
24
+ repo: REPO,
25
+ branch: "feature/tidy",
26
+ base: "main",
27
+ goal: "Tidy auth",
28
+ files,
29
+ filesMore: 0,
30
+ prUrl: null,
31
+ }
32
+ );
33
+ }
34
+
35
+ const remote = (id: string, files: string[], branch = "feature/login") =>
36
+ upsertRemote(machineId(), "peer-1", {
37
+ id,
38
+ origin: "box-machine",
39
+ origin_name: "box",
40
+ hops: 0,
41
+ source: "session",
42
+ session_name: "Rework login sessions",
43
+ repo: REPO,
44
+ branch,
45
+ base: "main",
46
+ goal: "Rework login sessions",
47
+ files,
48
+ files_more: 0,
49
+ pr_url: null,
50
+ ttl_s: 600,
51
+ age_s: 60,
52
+ released: null,
53
+ });
54
+
55
+ const FILES = ["a/one.ts", "a/two.ts", "a/three.ts"];
56
+
57
+ const rows = async () => {
58
+ const body = await (await GET()).json();
59
+ return body as {
60
+ sessions: { id: string; overlap?: Record<string, unknown> | null }[];
61
+ claims: { id: string; fileCount: number; files?: string[] }[];
62
+ claimOverlaps: { a: string; b: string; strength: string }[];
63
+ };
64
+ };
65
+
66
+ describe("GET /api/sessions with claims", () => {
67
+ it("puts a strong overlap on the session that has it, and no other", async () => {
68
+ const t = seedWorkspace();
69
+ const mine = seedSession({ projectId: t.app.id, name: "Tidy auth" });
70
+ const other = seedSession({ projectId: t.app.id, name: "Unrelated" });
71
+ localClaim(mine, FILES);
72
+ remote("claim-box-1", FILES);
73
+ refreshOverlapCache();
74
+ const body = await rows();
75
+ expect(body.sessions.find((s) => s.id === mine)?.overlap).toEqual({
76
+ strength: "strong",
77
+ machine: "box",
78
+ goal: "Rework login sessions",
79
+ shared: 3,
80
+ claimId: "claim-box-1",
81
+ mineId: `s-${mine}`,
82
+ });
83
+ expect(body.sessions.find((s) => s.id === other)?.overlap).toBeUndefined();
84
+ });
85
+
86
+ it("marks a small file overlap weak", async () => {
87
+ const t = seedWorkspace();
88
+ const mine = seedSession({ projectId: t.app.id, name: "Tidy auth" });
89
+ localClaim(mine, ["a/1.ts", "a/2.ts", "a/3.ts", "a/4.ts", "a/5.ts"]);
90
+ remote(
91
+ "claim-box-2",
92
+ ["a/1.ts", "a/2.ts", "b/1.ts", "b/2.ts", "b/3.ts"],
93
+ "feature/other"
94
+ );
95
+ refreshOverlapCache();
96
+ const row = (await rows()).sessions.find((s) => s.id === mine);
97
+ expect(row?.overlap?.strength).toBe("weak");
98
+ });
99
+
100
+ it("lists claims without their paths, and the overlapping pairs", async () => {
101
+ const t = seedWorkspace();
102
+ const mine = seedSession({ projectId: t.app.id, name: "Tidy auth" });
103
+ localClaim(mine, FILES);
104
+ remote("claim-box-1", FILES);
105
+ refreshOverlapCache();
106
+ const body = await rows();
107
+ expect(body.claims.map((c) => c.id).sort()).toEqual(
108
+ [`s-${mine}`, "claim-box-1"].sort()
109
+ );
110
+ expect(body.claims.every((c) => c.fileCount === 3 && !c.files)).toBe(true);
111
+ expect(body.claimOverlaps).toHaveLength(1);
112
+ expect(body.claimOverlaps[0].strength).toBe("strong");
113
+ });
114
+
115
+ it("serves no claims before the loop has run", async () => {
116
+ const t = seedWorkspace();
117
+ seedSession({ projectId: t.app.id, name: "Tidy auth" });
118
+ const body = await rows();
119
+ expect(body.claims).toEqual([]);
120
+ expect(body.sessions.every((s) => !s.overlap)).toBe(true);
121
+ });
122
+ });
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ import { assignSessions } from "@/lib/session-groups";
3
+ import { groupRoute } from "@/lib/session-groups/route";
4
+
5
+ const body = z.object({
6
+ ids: z.array(z.string()).min(1).max(200),
7
+ groupId: z.string().nullable(),
8
+ });
9
+
10
+ // POST /api/sessions/group { ids, groupId | null } - the multi-select move.
11
+ // All or none: one session of another workspace refuses the lot.
12
+ export async function POST(request: Request) {
13
+ return groupRoute(request, body, (b) => ({
14
+ ok: true,
15
+ ...assignSessions(b.ids, b.groupId),
16
+ }));
17
+ }
@@ -1,28 +1,47 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { getDb, queries, type Session, type Group } from "@/lib/db";
2
+ import { getDb, queries, type Session } from "@/lib/db";
3
+ import { listGroups } from "@/lib/session-groups";
3
4
  import { isValidAgentType } from "@/lib/providers";
4
5
  import { clientSend } from "@/lib/chat/client-send";
5
6
  import { CHAT_ACCESS } from "@/lib/chat/events";
6
7
  import { launchSession } from "@/lib/sessions/launch";
8
+ import { namesLine, waitBadges } from "@/lib/bus/waits/store";
9
+ import { claimsSnapshot } from "@/lib/claims";
7
10
  import { isProjectRef } from "@/lib/tasks/project-ref";
8
11
 
9
- // GET /api/sessions - List all sessions and groups
12
+ // GET /api/sessions - List the sidebar's sessions and the user's groups
10
13
  export async function GET() {
11
14
  try {
12
15
  const db = getDb();
13
16
  // Merged tasks are listed (the sidebar files them under Done). Dropped
14
17
  // tasks live in the Tasks panel, a moved one on the machine it moved to,
15
18
  // and archived sessions in the Archived view: none is in the sidebar.
16
- const sessions = queries.getSidebarSessions(db).all() as Session[];
17
- const groups = queries.getAllGroups(db).all() as Group[];
18
-
19
- // Convert expanded from 0/1 to boolean
20
- const formattedGroups = groups.map((g) => ({
21
- ...g,
22
- expanded: Boolean(g.expanded),
23
- }));
24
-
25
- return NextResponse.json({ sessions, groups: formattedGroups });
19
+ const badges = waitBadges();
20
+ const claims = claimsSnapshot();
21
+ const sessions = (queries.getSidebarSessions(db).all() as Session[]).map(
22
+ (s) => {
23
+ const b = badges.get(s.id);
24
+ const overlap = claims.bySession.get(s.id);
25
+ const row = overlap ? { ...s, overlap } : s;
26
+ return b
27
+ ? {
28
+ ...row,
29
+ waiting_on: b.on.length
30
+ ? namesLine(b.on.map((w) => w.name))
31
+ : null,
32
+ waited_by: b.by.length
33
+ ? namesLine(b.by.map((w) => w.name))
34
+ : null,
35
+ }
36
+ : row;
37
+ }
38
+ );
39
+ return NextResponse.json({
40
+ sessions,
41
+ sessionGroups: listGroups(),
42
+ claims: claims.claims,
43
+ claimOverlaps: claims.overlaps,
44
+ });
26
45
  } catch (error) {
27
46
  console.error("Error fetching sessions:", error);
28
47
  return NextResponse.json(
@@ -78,7 +97,6 @@ export async function POST(request: NextRequest) {
78
97
  name: str(body.name),
79
98
  view: body.view === "terminal" ? "terminal" : undefined,
80
99
  parentSessionId: str(body.parentSessionId),
81
- groupPath: str(body.groupPath) ?? undefined,
82
100
  systemPrompt: str(body.systemPrompt),
83
101
  });
84
102
  const db = getDb();
package/app/page.tsx CHANGED
@@ -34,6 +34,7 @@ if (typeof window !== "undefined") {
34
34
  import { PaneProvider, usePanes } from "@/contexts/PaneContext";
35
35
  import { Pane } from "@/components/Pane";
36
36
  import { AwayReport } from "@/components/Away/AwayReport";
37
+ import { IncomingLinks } from "@/components/Hosts/IncomingLinkSheet";
37
38
  import type { BrowserNotifications } from "@/components/Settings/NotificationsSection";
38
39
  import { useNotifications } from "@/hooks/useNotifications";
39
40
  import { useViewport } from "@/hooks/useViewport";
@@ -121,6 +122,10 @@ const LumifyHubDialogs = dynamic(
121
122
  ),
122
123
  { ssr: false }
123
124
  );
125
+ const ClaimsSheet = dynamic(
126
+ () => import("@/components/Claims/ClaimsSheet").then((m) => m.ClaimsSheet),
127
+ { ssr: false }
128
+ );
124
129
  const UsageDialog = dynamic(
125
130
  () => import("@/components/Usage/UsageDialog").then((m) => m.UsageDialog),
126
131
  { ssr: false }
@@ -158,9 +163,11 @@ const AppDialogs = memo(function AppDialogs({
158
163
  <CleanupDialog />
159
164
  <LumifyHubDialogs />
160
165
  <UsageDialog />
166
+ <ClaimsSheet />
161
167
  <MoveDialog />
162
168
  <ShareDialog />
163
169
  <AwayReport />
170
+ <IncomingLinks />
164
171
  <CommandPalette />
165
172
  </>
166
173
  );
@@ -0,0 +1,38 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ // The paths a claim touches, the ones another claim also touches marked.
6
+ export function ClaimFiles({
7
+ files,
8
+ more,
9
+ shared,
10
+ }: {
11
+ files: string[];
12
+ more: number;
13
+ shared: ReadonlySet<string>;
14
+ }) {
15
+ return (
16
+ <ul className="bg-muted/40 mt-1 max-h-48 overflow-y-auto rounded-lg p-2 font-mono text-[11px] leading-5">
17
+ {files.map((f) => (
18
+ <li
19
+ key={f}
20
+ className={cn(
21
+ "break-all",
22
+ shared.has(f)
23
+ ? "text-amber-700 dark:text-amber-400"
24
+ : "text-muted-foreground"
25
+ )}
26
+ >
27
+ {f}
28
+ </li>
29
+ ))}
30
+ {more > 0 && (
31
+ <li className="text-muted-foreground pt-1">and {more} more</li>
32
+ )}
33
+ {files.length === 0 && more === 0 && (
34
+ <li className="text-muted-foreground">No changed files yet</li>
35
+ )}
36
+ </ul>
37
+ );
38
+ }
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { ChevronRight, ExternalLink, GitBranch } from "lucide-react";
5
+ import { cn } from "@/lib/utils";
6
+ import type { Claim } from "@/lib/claims/types";
7
+ import { ClaimFiles } from "./ClaimFiles";
8
+ import { startedText } from "./format";
9
+
10
+ // One piece of claimed work: what, where, since when; its files fold open.
11
+ export function ClaimRow({
12
+ claim,
13
+ here,
14
+ now,
15
+ shared,
16
+ }: {
17
+ claim: Claim;
18
+ here: boolean;
19
+ now: number;
20
+ shared: ReadonlySet<string>;
21
+ }) {
22
+ const [open, setOpen] = useState(false);
23
+ const count = claim.files.length + claim.filesMore;
24
+ return (
25
+ <li className="bg-muted/40 rounded-xl" data-claim={claim.id}>
26
+ <button
27
+ type="button"
28
+ onClick={() => setOpen((v) => !v)}
29
+ aria-expanded={open}
30
+ className="flex min-h-11 w-full items-start gap-2 rounded-xl p-3 text-left"
31
+ >
32
+ <ChevronRight
33
+ aria-hidden
34
+ className={cn(
35
+ "text-muted-foreground mt-0.5 h-4 w-4 shrink-0 transition-transform",
36
+ open && "rotate-90"
37
+ )}
38
+ />
39
+ <span className="min-w-0 flex-1">
40
+ <span className="block text-sm font-medium break-words">
41
+ {claim.goal}
42
+ </span>
43
+ <span className="text-muted-foreground mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs">
44
+ <span>
45
+ {here ? `${claim.originName} (here)` : claim.originName}
46
+ </span>
47
+ {claim.branch && (
48
+ <span className="flex min-w-0 items-center gap-1">
49
+ <GitBranch aria-hidden className="h-3 w-3 shrink-0" />
50
+ <span className="truncate">{claim.branch}</span>
51
+ </span>
52
+ )}
53
+ <span>{startedText(claim, now)}</span>
54
+ <span>
55
+ {count} file{count === 1 ? "" : "s"}
56
+ </span>
57
+ </span>
58
+ </span>
59
+ </button>
60
+ {claim.prUrl && (
61
+ <a
62
+ href={claim.prUrl}
63
+ target="_blank"
64
+ rel="noreferrer"
65
+ className="text-primary flex min-h-11 items-center gap-1 px-3 pb-2 pl-9 text-xs hover:underline"
66
+ >
67
+ <ExternalLink aria-hidden className="h-3 w-3" />
68
+ Pull request
69
+ </a>
70
+ )}
71
+ {open && (
72
+ <div className="px-3 pb-3 pl-9">
73
+ <ClaimFiles
74
+ files={claim.files}
75
+ more={claim.filesMore}
76
+ shared={shared}
77
+ />
78
+ </div>
79
+ )}
80
+ </li>
81
+ );
82
+ }
@@ -0,0 +1,176 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { useSnapshot } from "valtio";
5
+ import { GitCompareArrows } from "lucide-react";
6
+ import { StatusPill } from "@/components/ui/status-pill";
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from "@/components/ui/dialog";
14
+ import { useNow } from "@/components/Chat/useNow";
15
+ import { useClaims } from "@/data/claims";
16
+ import { claimsUi, claimsUiActions } from "@/stores/claimsUi";
17
+ import { cn } from "@/lib/utils";
18
+ import { ClaimRow } from "./ClaimRow";
19
+ import { QUIET_MS, focusView, groupByRepo, lastHeardText } from "./format";
20
+
21
+ const Skeleton = () => (
22
+ <div className="space-y-2" aria-label="Loading claims">
23
+ {[0, 1, 2].map((i) => (
24
+ <div key={i} className="bg-muted/40 h-16 animate-pulse rounded-xl" />
25
+ ))}
26
+ </div>
27
+ );
28
+
29
+ // Work across machines: what each linked machine's sessions are on, the
30
+ // overlaps first, and when each machine was last heard from.
31
+ export function ClaimsSheet() {
32
+ const snap = useSnapshot(claimsUi);
33
+ const { data, isPending, isError, dataUpdatedAt } = useClaims({
34
+ enabled: snap.open,
35
+ });
36
+ const tick = useNow(snap.open);
37
+ const now = tick + (data ? data.now - dataUpdatedAt : 0);
38
+ const view = useMemo(
39
+ () => (data ? focusView(data, snap.focus ? [...snap.focus] : null) : null),
40
+ [data, snap.focus]
41
+ );
42
+ const byId = useMemo(
43
+ () => new Map((view?.claims ?? []).map((c) => [c.id, c])),
44
+ [view]
45
+ );
46
+ const sharedOf = (id: string) =>
47
+ new Set(
48
+ (view?.overlaps ?? [])
49
+ .filter((o) => o.a === id || o.b === id)
50
+ .flatMap((o) => o.shared)
51
+ );
52
+ const here = data?.machine.id;
53
+
54
+ return (
55
+ <Dialog open={snap.open} onOpenChange={claimsUiActions.setOpen}>
56
+ <DialogContent className="max-h-[90vh] max-w-xl overflow-y-auto [&>*]:min-w-0">
57
+ <DialogHeader>
58
+ <DialogTitle>Work across machines</DialogTitle>
59
+ <DialogDescription className="sr-only">
60
+ What sessions on this and linked machines are working on, and where
61
+ the work overlaps.
62
+ </DialogDescription>
63
+ </DialogHeader>
64
+ {isPending && <Skeleton />}
65
+ {isError && (
66
+ <p className="text-destructive text-sm">Couldn’t load claims.</p>
67
+ )}
68
+ {data && view && (
69
+ <div className="space-y-4">
70
+ <ul className="flex flex-wrap gap-2" aria-label="Machines">
71
+ {data.machines.map((m) => {
72
+ const quiet =
73
+ !m.self &&
74
+ (m.lastHeardAt === null || now - m.lastHeardAt > QUIET_MS);
75
+ return (
76
+ <li
77
+ key={m.id}
78
+ className="bg-muted/40 flex items-center gap-2 rounded-lg px-3 py-2 text-xs"
79
+ >
80
+ <span className="font-medium">{m.name}</span>
81
+ <span
82
+ className={cn(
83
+ "text-muted-foreground",
84
+ quiet && "text-amber-700 dark:text-amber-400"
85
+ )}
86
+ >
87
+ {lastHeardText(m.lastHeardAt, now)}
88
+ </span>
89
+ </li>
90
+ );
91
+ })}
92
+ </ul>
93
+ {snap.focus && (
94
+ <button
95
+ type="button"
96
+ onClick={claimsUiActions.showAll}
97
+ className="text-primary min-h-11 text-sm"
98
+ >
99
+ Showing one pair. Show all work
100
+ </button>
101
+ )}
102
+ {view.overlaps.length > 0 && (
103
+ <section className="space-y-2" aria-label="Overlaps">
104
+ <h3 className="text-muted-foreground text-xs font-semibold tracking-wide uppercase">
105
+ Overlaps
106
+ </h3>
107
+ <ul className="space-y-2">
108
+ {view.overlaps.map((o) => {
109
+ const a = byId.get(o.a);
110
+ const b = byId.get(o.b);
111
+ if (!a || !b) return null;
112
+ return (
113
+ <li
114
+ key={`${o.a}|${o.b}`}
115
+ className="bg-muted/40 flex items-start gap-2 rounded-xl p-3 text-sm"
116
+ >
117
+ <StatusPill
118
+ tone={o.strength === "strong" ? "amber" : "muted"}
119
+ >
120
+ <GitCompareArrows
121
+ aria-hidden
122
+ className="h-2.5 w-2.5"
123
+ />
124
+ {o.strength}
125
+ </StatusPill>
126
+ <span className="min-w-0 flex-1 break-words">
127
+ {a.goal} on {a.originName} and {b.goal} on{" "}
128
+ {b.originName}
129
+ <span className="text-muted-foreground block text-xs">
130
+ {[
131
+ o.kinds.join(" + "),
132
+ o.shared.length
133
+ ? `${o.shared.length} shared file${o.shared.length === 1 ? "" : "s"}`
134
+ : null,
135
+ ]
136
+ .filter(Boolean)
137
+ .join(" · ")}
138
+ </span>
139
+ </span>
140
+ </li>
141
+ );
142
+ })}
143
+ </ul>
144
+ </section>
145
+ )}
146
+ {view.claims.length === 0 ? (
147
+ <p className="text-muted-foreground py-6 text-center text-sm">
148
+ Nothing is claimed right now. Work shows up here while a session
149
+ has changes in a repository.
150
+ </p>
151
+ ) : (
152
+ groupByRepo(view.claims).map((g) => (
153
+ <section key={g.repo} className="space-y-2" aria-label={g.repo}>
154
+ <h3 className="text-muted-foreground truncate text-xs font-semibold">
155
+ {g.repo}
156
+ </h3>
157
+ <ul className="space-y-2">
158
+ {g.claims.map((c) => (
159
+ <ClaimRow
160
+ key={c.id}
161
+ claim={c}
162
+ here={c.origin === here}
163
+ now={now}
164
+ shared={sharedOf(c.id)}
165
+ />
166
+ ))}
167
+ </ul>
168
+ </section>
169
+ ))
170
+ )}
171
+ </div>
172
+ )}
173
+ </DialogContent>
174
+ </Dialog>
175
+ );
176
+ }
@@ -0,0 +1,31 @@
1
+ "use client";
2
+
3
+ import { GitCompareArrows } from "lucide-react";
4
+ import { StatusPill } from "@/components/ui/status-pill";
5
+ import type { Session } from "@/lib/db";
6
+ import { claimsUiActions } from "@/stores/claimsUi";
7
+
8
+ // "Overlaps: box" on a session row: amber when the overlap is strong, muted
9
+ // when weak. Tapping opens the claims sheet on that pair.
10
+ export function OverlapPill({
11
+ overlap,
12
+ }: {
13
+ overlap: NonNullable<Session["overlap"]>;
14
+ }) {
15
+ const strong = overlap.strength === "strong";
16
+ const files = overlap.shared
17
+ ? ` · ${overlap.shared} shared file${overlap.shared === 1 ? "" : "s"}`
18
+ : "";
19
+ return (
20
+ <StatusPill
21
+ tone={strong ? "amber" : "muted"}
22
+ data-tip={`${strong ? "Strong" : "Weak"} overlap with "${overlap.goal}" on ${overlap.machine}${files}. Tap to see both.`}
23
+ aria-label={`Overlaps with ${overlap.machine}`}
24
+ data-overlap={overlap.strength}
25
+ onClick={() => claimsUiActions.open([overlap.mineId, overlap.claimId])}
26
+ >
27
+ <GitCompareArrows aria-hidden className="h-2.5 w-2.5 shrink-0" />
28
+ <span className="truncate">Overlaps: {overlap.machine}</span>
29
+ </StatusPill>
30
+ );
31
+ }