@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.
- package/README.md +1 -0
- package/app/api/bus/groups/route.ts +44 -0
- package/app/api/bus/spawn/route.ts +12 -1
- package/app/api/claims/[id]/route.ts +20 -0
- package/app/api/claims/check/route.ts +23 -0
- package/app/api/claims/check.test.ts +113 -0
- package/app/api/claims/exchange/route.ts +42 -0
- package/app/api/claims/outside.test.ts +90 -0
- package/app/api/claims/route.test.ts +193 -0
- package/app/api/claims/route.ts +73 -0
- package/app/api/hosts/[id]/route.ts +9 -1
- package/app/api/peer/unlink/route.ts +17 -0
- package/app/api/peers/[hostId]/audit/route.ts +14 -0
- package/app/api/peers/[hostId]/route.ts +20 -0
- package/app/api/peers/gate.test.ts +64 -1
- package/app/api/peers/pending/route.test.ts +37 -0
- package/app/api/peers/pending/route.ts +11 -4
- package/app/api/session-groups/[id]/restore/route.ts +11 -0
- package/app/api/session-groups/[id]/route.ts +25 -0
- package/app/api/session-groups/order/route.ts +15 -0
- package/app/api/session-groups/route.ts +21 -0
- package/app/api/sessions/[id]/fork/route.ts +3 -0
- package/app/api/sessions/[id]/group/route.ts +18 -0
- package/app/api/sessions/[id]/route.ts +14 -4
- package/app/api/sessions/[id]/summarize/route.ts +2 -0
- package/app/api/sessions/claims.test.ts +122 -0
- package/app/api/sessions/group/route.ts +17 -0
- package/app/api/sessions/route.ts +15 -14
- package/app/page.tsx +5 -0
- package/components/Claims/ClaimFiles.tsx +38 -0
- package/components/Claims/ClaimRow.tsx +82 -0
- package/components/Claims/ClaimsSheet.tsx +176 -0
- package/components/Claims/OverlapPill.tsx +31 -0
- package/components/Claims/StartClaimsNotice.tsx +70 -0
- package/components/Claims/claims-ui.test.ts +161 -0
- package/components/Claims/format.ts +57 -0
- package/components/Claims/index.ts +3 -0
- package/components/Draft/DraftPanel.tsx +40 -2
- package/components/Hosts/AccountMachines.tsx +32 -18
- package/components/Hosts/HostRow.tsx +155 -14
- package/components/Hosts/IncomingLinkSheet.tsx +28 -10
- package/components/Hosts/PeerActivitySheet.tsx +111 -0
- package/components/SessionList/GroupShelves.tsx +195 -0
- package/components/SessionList/MoveToGroupMenu.tsx +105 -0
- package/components/SessionList/RowSubtitle.tsx +7 -1
- package/components/SessionList/SelectionToolbar.tsx +52 -1
- package/components/SessionList/SessionRow.tsx +1 -0
- package/components/SessionList/SessionRowMenu.tsx +17 -0
- package/components/SessionList/SessionShelves.tsx +5 -0
- package/components/SessionList/SidebarBody.tsx +6 -0
- package/components/SessionList/SidebarContextMenu.test.ts +104 -0
- package/components/SessionList/SidebarContextMenu.tsx +59 -0
- package/components/SessionList/SidebarHeader.tsx +5 -0
- package/components/SessionList/WorkspaceSwitcher.tsx +23 -6
- package/components/SessionList/index.tsx +18 -14
- package/components/SessionList/useSidebarData.ts +10 -0
- package/components/ui/notice.tsx +46 -0
- package/components/ui/status-pill.tsx +39 -0
- package/data/claims/index.ts +2 -0
- package/data/claims/keys.ts +5 -0
- package/data/claims/queries.ts +47 -0
- package/data/peers/keys.ts +1 -0
- package/data/peers/queries.ts +31 -0
- package/data/push/topics.ts +4 -0
- package/data/session-groups/index.ts +156 -0
- package/data/sessions/index.ts +0 -1
- package/data/sessions/pin.ts +2 -6
- package/data/sessions/queries.ts +10 -29
- package/data/sessions/shelf.ts +2 -6
- package/data/tasks/index.ts +2 -0
- package/hooks/useSessions.ts +0 -12
- package/lib/__fixtures__/peer-rpc.ts +65 -33
- package/lib/agents/spawn.ts +4 -0
- package/lib/bus/index.ts +8 -0
- package/lib/claims/collectLocal.ts +121 -0
- package/lib/claims/describe.ts +41 -0
- package/lib/claims/exchange.ts +91 -0
- package/lib/claims/goal.ts +45 -0
- package/lib/claims/index.ts +10 -0
- package/lib/claims/loop.ts +51 -0
- package/lib/claims/machine.ts +21 -0
- package/lib/claims/mine.ts +43 -0
- package/lib/claims/notifyOverlaps.ts +101 -0
- package/lib/claims/onMerged.ts +85 -0
- package/lib/claims/outside.ts +36 -0
- package/lib/claims/overlap.ts +87 -0
- package/lib/claims/overlapCache.ts +86 -0
- package/lib/claims/overlapText.ts +36 -0
- package/lib/claims/peers.ts +56 -0
- package/lib/claims/startCheck.ts +100 -0
- package/lib/claims/startWarning.ts +29 -0
- package/lib/claims/store.ts +366 -0
- package/lib/claims/types.ts +102 -0
- package/lib/claims/view.ts +80 -0
- package/lib/claims/yield.ts +24 -0
- package/lib/db/changes.ts +6 -0
- package/lib/db/migrations/20261020T1400_work_claims.ts +35 -0
- package/lib/db/migrations/20261020T1515_peer_unlinked.ts +22 -0
- package/lib/db/migrations/20261020T1700_clean_connect_names.ts +18 -0
- package/lib/db/migrations/20261020T1800_session_groups.ts +49 -0
- package/lib/db/migrations/20261020T1900_task_queue_after_claim.ts +13 -0
- package/lib/db/migrations/20261020T2000_claim_notices.ts +20 -0
- package/lib/db/migrations/20261020T2100_claims_via_host.ts +21 -0
- package/lib/db/migrations/20261020T2200_work_claims_watch.ts +10 -0
- package/lib/db/queries.ts +0 -40
- package/lib/db/session-parts/columns.ts +1 -0
- package/lib/db/types.ts +16 -8
- package/lib/doctor/checks.ts +64 -1
- package/lib/git-remote.ts +43 -0
- package/lib/hosts/index.ts +10 -1
- package/lib/orchestration.ts +2 -0
- package/lib/orchestrator/act.ts +54 -4
- package/lib/orchestrator/brief.ts +2 -1
- package/lib/orchestrator/claim-wait.ts +71 -0
- package/lib/orchestrator/conditions.ts +28 -1
- package/lib/orchestrator/describe.ts +6 -1
- package/lib/orchestrator/facts.ts +5 -0
- package/lib/orchestrator/serve.ts +37 -2
- package/lib/orchestrator/tool-names.ts +1 -0
- package/lib/orchestrator/tool-schemas.ts +31 -1
- package/lib/orchestrator/tools.ts +3 -1
- package/lib/orchestrator/watcher.ts +19 -1
- package/lib/peer/account.ts +74 -2
- package/lib/peer/allowlist.ts +1 -1
- package/lib/peer/client.ts +26 -1
- package/lib/peer/link-cli.ts +19 -4
- package/lib/peer/links.ts +117 -5
- package/lib/peer/machines.ts +14 -2
- package/lib/peer/name.ts +32 -0
- package/lib/peer/pairing.ts +34 -5
- package/lib/peer/pin.ts +10 -3
- package/lib/peer/reasons.ts +7 -0
- package/lib/peer/revoke.ts +75 -0
- package/lib/peer/rpc-server.ts +53 -5
- package/lib/peer/unlink.ts +62 -0
- package/lib/peer/unlinked-proof.ts +44 -0
- package/lib/projects.ts +5 -2
- package/lib/security/demo.ts +7 -0
- package/lib/session-groups/assignSessions.ts +81 -0
- package/lib/session-groups/cli.ts +61 -0
- package/lib/session-groups/createGroup.ts +28 -0
- package/lib/session-groups/index.ts +21 -0
- package/lib/session-groups/removeGroup.ts +36 -0
- package/lib/session-groups/reorderGroups.ts +31 -0
- package/lib/session-groups/resolveGroup.ts +19 -0
- package/lib/session-groups/route.ts +32 -0
- package/lib/session-groups/store.ts +85 -0
- package/lib/session-groups/types.ts +25 -0
- package/lib/session-groups/updateGroup.ts +35 -0
- package/lib/session-groups/workspace.ts +21 -0
- package/lib/sessions/launch.ts +5 -2
- package/lib/sidebar/rollup.ts +1 -0
- package/lib/sidebar/row-menu.ts +2 -0
- package/lib/sidebar/shelves.ts +90 -15
- package/lib/sidebar/visible-order.ts +5 -0
- package/lib/tasks/hold-checks.ts +3 -1
- package/lib/tasks/holds.ts +3 -0
- package/lib/tasks/index.ts +5 -0
- package/lib/tasks/queue-rows.ts +8 -1
- package/lib/tasks/queue-start.ts +1 -0
- package/lib/tasks/queue-types.ts +9 -1
- package/lib/tasks/queue.ts +15 -5
- package/lib/tasks/schedule-types.ts +1 -0
- package/lib/test-temp.ts +19 -3
- package/lib/workspaces.ts +11 -3
- package/lib/worktree-sweep/testTemp.ts +23 -106
- package/package.json +1 -1
- package/scripts/agent-os +1 -1
- package/scripts/autodeploy +2 -0
- package/scripts/autodeploy.test.ts +31 -1
- package/scripts/claim-hook.sh +31 -0
- package/scripts/claim-hook.test.ts +230 -0
- package/scripts/lib/cli-links.sh +36 -0
- package/scripts/lib/commands.sh +3 -0
- package/scripts/lib/machine.sh +27 -2
- package/scripts/lib/machine.test.ts +25 -3
- package/scripts/sync-cli-links +7 -0
- package/scripts/sync-cli-links.test.ts +69 -0
- package/server.ts +3 -0
- package/stores/claimsUi.ts +22 -0
- package/app/api/groups/[...path]/route.ts +0 -136
- package/app/api/groups/route.ts +0 -93
- package/data/groups/index.ts +0 -1
- package/data/groups/mutations.ts +0 -95
- package/hooks/useGroups.ts +0 -37
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { PEER_HEADER, TRUST_HEADER } from "@/lib/security/auth";
|
|
3
|
+
import { getHost } from "@/lib/hosts";
|
|
4
|
+
import { unlinkedByPeer } from "@/lib/peer/links";
|
|
5
|
+
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
// POST /api/peer/unlink - a linked machine unlinked this one. Reached only
|
|
9
|
+
// through its signed envelope (lib/peer/rpc-server.ts), which the gate turns
|
|
10
|
+
// into `peer` trust naming the host it's pinned to; nothing else may call it.
|
|
11
|
+
export function POST(request: NextRequest) {
|
|
12
|
+
const hostId = request.headers.get(PEER_HEADER);
|
|
13
|
+
if (request.headers.get(TRUST_HEADER) !== "peer" || !hostId)
|
|
14
|
+
return NextResponse.json({ error: "forbidden" }, { status: 403 });
|
|
15
|
+
const name = getHost(hostId)?.name ?? "the other machine";
|
|
16
|
+
return NextResponse.json({ unlinked: unlinkedByPeer(hostId, name) });
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { requireApprover } from "@/lib/security/approver";
|
|
3
|
+
import { peerAudit } from "@/lib/peer/audit";
|
|
4
|
+
|
|
5
|
+
// GET /api/peers/:hostId/audit - every call to and from that linked machine,
|
|
6
|
+
// newest first: Machines → Activity.
|
|
7
|
+
export async function GET(
|
|
8
|
+
request: NextRequest,
|
|
9
|
+
{ params }: { params: Promise<{ hostId: string }> }
|
|
10
|
+
) {
|
|
11
|
+
const gate = requireApprover(request);
|
|
12
|
+
if (!gate.ok) return gate.response;
|
|
13
|
+
return NextResponse.json({ rows: peerAudit((await params).hostId) });
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { requireApprover } from "@/lib/security/approver";
|
|
3
|
+
import { unlinkPeer } from "@/lib/peer/unlink";
|
|
4
|
+
|
|
5
|
+
// DELETE /api/peers/:hostId - unlink a machine linked through Connect: its
|
|
6
|
+
// pin goes here at once, and it's told to drop its side. Takes the same
|
|
7
|
+
// trust as linking.
|
|
8
|
+
export async function DELETE(
|
|
9
|
+
request: NextRequest,
|
|
10
|
+
{ params }: { params: Promise<{ hostId: string }> }
|
|
11
|
+
) {
|
|
12
|
+
const gate = requireApprover(request);
|
|
13
|
+
if (!gate.ok) return gate.response;
|
|
14
|
+
try {
|
|
15
|
+
return NextResponse.json(await unlinkPeer((await params).hostId));
|
|
16
|
+
} catch (err) {
|
|
17
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
18
|
+
return NextResponse.json({ error }, { status: 404 });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -9,6 +9,9 @@ const link = await import("./link/route");
|
|
|
9
9
|
const cancel = await import("./link/[id]/route");
|
|
10
10
|
const confirm = await import("./link/[id]/confirm/route");
|
|
11
11
|
const hello = await import("../peer/hello/route");
|
|
12
|
+
const unlink = await import("./[hostId]/route");
|
|
13
|
+
const audit = await import("./[hostId]/audit/route");
|
|
14
|
+
const hostDelete = await import("../hosts/[id]/route");
|
|
12
15
|
|
|
13
16
|
const plain = mintDevice("phone").device.id;
|
|
14
17
|
const approver = mintDevice("tablet", null, true).device.id;
|
|
@@ -24,7 +27,10 @@ const req = (
|
|
|
24
27
|
...(o.device && { "x-agentos-device": o.device }),
|
|
25
28
|
"content-type": "application/json",
|
|
26
29
|
},
|
|
27
|
-
body:
|
|
30
|
+
body:
|
|
31
|
+
o.method === "GET"
|
|
32
|
+
? undefined
|
|
33
|
+
: (o.body ?? JSON.stringify({ code: "123456" })),
|
|
28
34
|
});
|
|
29
35
|
const params = { params: Promise.resolve({ id: "nope" }) };
|
|
30
36
|
|
|
@@ -105,7 +111,64 @@ describe("linking routes", () => {
|
|
|
105
111
|
"/api/peers/account",
|
|
106
112
|
"/api/peers/link",
|
|
107
113
|
"/api/peers/link/x/confirm",
|
|
114
|
+
"/api/peers/h1",
|
|
115
|
+
"/api/peers/h1/audit",
|
|
116
|
+
"/api/peer/unlink",
|
|
108
117
|
])
|
|
109
118
|
expect(isPublicPath(p)).toBe(false);
|
|
110
119
|
});
|
|
120
|
+
|
|
121
|
+
it("unlink and Activity take the same trust as linking", async () => {
|
|
122
|
+
const hostParams = { params: Promise.resolve({ hostId: "nope" }) };
|
|
123
|
+
for (const who of [
|
|
124
|
+
{},
|
|
125
|
+
{ trust: "device", device: plain },
|
|
126
|
+
{ trust: "peer" },
|
|
127
|
+
{ trust: "open" },
|
|
128
|
+
]) {
|
|
129
|
+
const del = req("/api/peers/nope", {
|
|
130
|
+
...who,
|
|
131
|
+
method: "DELETE",
|
|
132
|
+
body: "",
|
|
133
|
+
});
|
|
134
|
+
expect((await unlink.DELETE(del, hostParams)).status).toBe(403);
|
|
135
|
+
const get = req("/api/peers/nope/audit", { ...who, method: "GET" });
|
|
136
|
+
expect((await audit.GET(get, hostParams)).status).toBe(403);
|
|
137
|
+
}
|
|
138
|
+
const asApprover = { trust: "device", device: approver };
|
|
139
|
+
expect(
|
|
140
|
+
(
|
|
141
|
+
await unlink.DELETE(
|
|
142
|
+
req("/x", { ...asApprover, method: "DELETE", body: "" }),
|
|
143
|
+
hostParams
|
|
144
|
+
)
|
|
145
|
+
).status
|
|
146
|
+
).toBe(404);
|
|
147
|
+
const rows = await audit.GET(
|
|
148
|
+
req("/x", { ...asApprover, method: "GET" }),
|
|
149
|
+
hostParams
|
|
150
|
+
);
|
|
151
|
+
expect(await rows.json()).toEqual({ rows: [] });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("won't remove a Connect-linked machine without unlinking it", async () => {
|
|
155
|
+
const { createConnectHost } = await import("@/lib/hosts");
|
|
156
|
+
const { savePin, pinForHost } = await import("@/lib/peer/links");
|
|
157
|
+
const id = createConnectHost("Linked").id;
|
|
158
|
+
savePin({
|
|
159
|
+
hostId: id,
|
|
160
|
+
machineId: "f0f0f0f0",
|
|
161
|
+
hostname: "m-0000000000000000.runagentos.com",
|
|
162
|
+
signPub: "x",
|
|
163
|
+
boxPub: "y",
|
|
164
|
+
});
|
|
165
|
+
const res = await hostDelete.DELETE(
|
|
166
|
+
req("/x", { method: "DELETE", body: "" }),
|
|
167
|
+
{
|
|
168
|
+
params: Promise.resolve({ id }),
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
expect(res.status).toBe(409);
|
|
172
|
+
expect(pinForHost(id)).not.toBeNull();
|
|
173
|
+
});
|
|
111
174
|
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { NextRequest } from "next/server";
|
|
3
|
+
import { mintDevice } from "@/lib/security/devices";
|
|
4
|
+
|
|
5
|
+
const seen: unknown[] = [];
|
|
6
|
+
vi.mock("@/lib/peer/pairing", () => ({
|
|
7
|
+
pendingRequests: (_now: number, o: unknown) => {
|
|
8
|
+
seen.push(o);
|
|
9
|
+
return [];
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
12
|
+
const { GET } = await import("./route");
|
|
13
|
+
|
|
14
|
+
const get = (headers: Record<string, string>) =>
|
|
15
|
+
GET(new NextRequest("http://localhost/api/peers/pending", { headers }));
|
|
16
|
+
|
|
17
|
+
describe("GET /api/peers/pending", () => {
|
|
18
|
+
it("hands out the second code only to a caller that could accept", async () => {
|
|
19
|
+
const plain = mintDevice("phone").device.id;
|
|
20
|
+
const approver = mintDevice("tablet", null, true).device.id;
|
|
21
|
+
for (const h of [
|
|
22
|
+
{} as Record<string, string>,
|
|
23
|
+
{ "x-agentos-trust": "device", "x-agentos-device": plain },
|
|
24
|
+
{ "x-agentos-trust": "peer" },
|
|
25
|
+
])
|
|
26
|
+
expect((await get(h)).status).toBe(200);
|
|
27
|
+
await get({ "x-agentos-trust": "loopback" });
|
|
28
|
+
await get({ "x-agentos-trust": "device", "x-agentos-device": approver });
|
|
29
|
+
expect(seen).toEqual([
|
|
30
|
+
{ withCodes: false },
|
|
31
|
+
{ withCodes: false },
|
|
32
|
+
{ withCodes: false },
|
|
33
|
+
{ withCodes: true },
|
|
34
|
+
{ withCodes: true },
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { approverOf } from "@/lib/security/approver";
|
|
2
3
|
import { pendingRequests } from "@/lib/peer/pairing";
|
|
3
4
|
|
|
4
|
-
// GET /api/peers/pending - machines asking to link here, waiting for the
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// GET /api/peers/pending - machines asking to link here, waiting for the
|
|
6
|
+
// code, and accepted ones whose second code is still to be typed there. That
|
|
7
|
+
// code goes only to a caller that could have accepted.
|
|
8
|
+
export function GET(request: NextRequest) {
|
|
9
|
+
return NextResponse.json({
|
|
10
|
+
requests: pendingRequests(Date.now(), {
|
|
11
|
+
withCodes: approverOf(request.headers) !== null,
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
7
14
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { restoreGroup } from "@/lib/session-groups";
|
|
2
|
+
import { run } from "@/lib/session-groups/route";
|
|
3
|
+
|
|
4
|
+
// POST /api/session-groups/:id/restore - undo a delete.
|
|
5
|
+
export async function POST(
|
|
6
|
+
_request: Request,
|
|
7
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
8
|
+
) {
|
|
9
|
+
const { id } = await params;
|
|
10
|
+
return run(() => ({ sessionGroup: restoreGroup(id) }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { deleteGroup, updateGroup } from "@/lib/session-groups";
|
|
3
|
+
import { groupRoute, run } from "@/lib/session-groups/route";
|
|
4
|
+
|
|
5
|
+
type Ctx = { params: Promise<{ id: string }> };
|
|
6
|
+
|
|
7
|
+
const patch = z.object({
|
|
8
|
+
name: z.string().optional(),
|
|
9
|
+
collapsed: z.boolean().optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// PATCH /api/session-groups/:id { name?, collapsed? } - rename or fold.
|
|
13
|
+
export async function PATCH(request: Request, { params }: Ctx) {
|
|
14
|
+
const { id } = await params;
|
|
15
|
+
return groupRoute(request, patch, (b) => ({
|
|
16
|
+
sessionGroup: updateGroup(id, b),
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// DELETE /api/session-groups/:id - soft delete; its sessions fall back to the
|
|
21
|
+
// status shelves and restore brings them back.
|
|
22
|
+
export async function DELETE(_request: Request, { params }: Ctx) {
|
|
23
|
+
const { id } = await params;
|
|
24
|
+
return run(() => ({ sessionGroup: deleteGroup(id) }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { reorderGroups } from "@/lib/session-groups";
|
|
3
|
+
import { groupRoute } from "@/lib/session-groups/route";
|
|
4
|
+
|
|
5
|
+
const order = z.object({
|
|
6
|
+
workspaceId: z.string().nullable(),
|
|
7
|
+
ids: z.array(z.string()),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// POST /api/session-groups/order { workspaceId, ids } - a workspace's order.
|
|
11
|
+
export async function POST(request: Request) {
|
|
12
|
+
return groupRoute(request, order, (b) => ({
|
|
13
|
+
sessionGroups: reorderGroups(b.workspaceId, b.ids),
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createGroup, listGroups } from "@/lib/session-groups";
|
|
3
|
+
import { groupRoute, run } from "@/lib/session-groups/route";
|
|
4
|
+
|
|
5
|
+
const create = z.object({
|
|
6
|
+
name: z.string({ error: "A group needs a name" }),
|
|
7
|
+
workspaceId: z.string().nullable(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// GET /api/session-groups - every live group.
|
|
11
|
+
export async function GET() {
|
|
12
|
+
return run(() => ({ sessionGroups: listGroups() }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// POST /api/session-groups { name, workspaceId } - create one at the bottom
|
|
16
|
+
// of its workspace's order.
|
|
17
|
+
export async function POST(request: Request) {
|
|
18
|
+
return groupRoute(request, create, (b) => ({
|
|
19
|
+
sessionGroup: createGroup(b.workspaceId, b.name),
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inheritGroup } from "@/lib/session-groups/assignSessions";
|
|
1
2
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
3
|
import { randomUUID } from "crypto";
|
|
3
4
|
import { getDb, queries, type Session, type Message } from "@/lib/db";
|
|
@@ -63,6 +64,8 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
|
|
63
64
|
parent.host_id
|
|
64
65
|
);
|
|
65
66
|
|
|
67
|
+
inheritGroup(parentId, newId);
|
|
68
|
+
|
|
66
69
|
// NOTE: We do NOT copy claude_session_id here.
|
|
67
70
|
// When the forked session is first attached, it will use --fork-session flag
|
|
68
71
|
// with the parent's claude_session_id to create a new branched conversation.
|
|
@@ -0,0 +1,18 @@
|
|
|
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({ groupId: z.string().nullable() });
|
|
6
|
+
|
|
7
|
+
// POST /api/sessions/:id/group { groupId | null } - join a group of the
|
|
8
|
+
// session's workspace, or leave.
|
|
9
|
+
export async function POST(
|
|
10
|
+
request: Request,
|
|
11
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
12
|
+
) {
|
|
13
|
+
const { id } = await params;
|
|
14
|
+
return groupRoute(request, body, (b) => ({
|
|
15
|
+
ok: true,
|
|
16
|
+
...assignSessions([id], b.groupId),
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
@@ -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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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({
|
|
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,13 +1,15 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { getDb, queries, type Session
|
|
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";
|
|
7
8
|
import { namesLine, waitBadges } from "@/lib/bus/waits/store";
|
|
9
|
+
import { claimsSnapshot } from "@/lib/claims";
|
|
8
10
|
import { isProjectRef } from "@/lib/tasks/project-ref";
|
|
9
11
|
|
|
10
|
-
// GET /api/sessions - List
|
|
12
|
+
// GET /api/sessions - List the sidebar's sessions and the user's groups
|
|
11
13
|
export async function GET() {
|
|
12
14
|
try {
|
|
13
15
|
const db = getDb();
|
|
@@ -15,12 +17,15 @@ export async function GET() {
|
|
|
15
17
|
// tasks live in the Tasks panel, a moved one on the machine it moved to,
|
|
16
18
|
// and archived sessions in the Archived view: none is in the sidebar.
|
|
17
19
|
const badges = waitBadges();
|
|
20
|
+
const claims = claimsSnapshot();
|
|
18
21
|
const sessions = (queries.getSidebarSessions(db).all() as Session[]).map(
|
|
19
22
|
(s) => {
|
|
20
23
|
const b = badges.get(s.id);
|
|
24
|
+
const overlap = claims.bySession.get(s.id);
|
|
25
|
+
const row = overlap ? { ...s, overlap } : s;
|
|
21
26
|
return b
|
|
22
27
|
? {
|
|
23
|
-
...
|
|
28
|
+
...row,
|
|
24
29
|
waiting_on: b.on.length
|
|
25
30
|
? namesLine(b.on.map((w) => w.name))
|
|
26
31
|
: null,
|
|
@@ -28,18 +33,15 @@ export async function GET() {
|
|
|
28
33
|
? namesLine(b.by.map((w) => w.name))
|
|
29
34
|
: null,
|
|
30
35
|
}
|
|
31
|
-
:
|
|
36
|
+
: row;
|
|
32
37
|
}
|
|
33
38
|
);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}));
|
|
41
|
-
|
|
42
|
-
return NextResponse.json({ sessions, groups: formattedGroups });
|
|
39
|
+
return NextResponse.json({
|
|
40
|
+
sessions,
|
|
41
|
+
sessionGroups: listGroups(),
|
|
42
|
+
claims: claims.claims,
|
|
43
|
+
claimOverlaps: claims.overlaps,
|
|
44
|
+
});
|
|
43
45
|
} catch (error) {
|
|
44
46
|
console.error("Error fetching sessions:", error);
|
|
45
47
|
return NextResponse.json(
|
|
@@ -95,7 +97,6 @@ export async function POST(request: NextRequest) {
|
|
|
95
97
|
name: str(body.name),
|
|
96
98
|
view: body.view === "terminal" ? "terminal" : undefined,
|
|
97
99
|
parentSessionId: str(body.parentSessionId),
|
|
98
|
-
groupPath: str(body.groupPath) ?? undefined,
|
|
99
100
|
systemPrompt: str(body.systemPrompt),
|
|
100
101
|
});
|
|
101
102
|
const db = getDb();
|
package/app/page.tsx
CHANGED
|
@@ -122,6 +122,10 @@ const LumifyHubDialogs = dynamic(
|
|
|
122
122
|
),
|
|
123
123
|
{ ssr: false }
|
|
124
124
|
);
|
|
125
|
+
const ClaimsSheet = dynamic(
|
|
126
|
+
() => import("@/components/Claims/ClaimsSheet").then((m) => m.ClaimsSheet),
|
|
127
|
+
{ ssr: false }
|
|
128
|
+
);
|
|
125
129
|
const UsageDialog = dynamic(
|
|
126
130
|
() => import("@/components/Usage/UsageDialog").then((m) => m.UsageDialog),
|
|
127
131
|
{ ssr: false }
|
|
@@ -159,6 +163,7 @@ const AppDialogs = memo(function AppDialogs({
|
|
|
159
163
|
<CleanupDialog />
|
|
160
164
|
<LumifyHubDialogs />
|
|
161
165
|
<UsageDialog />
|
|
166
|
+
<ClaimsSheet />
|
|
162
167
|
<MoveDialog />
|
|
163
168
|
<ShareDialog />
|
|
164
169
|
<AwayReport />
|
|
@@ -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
|
+
}
|