@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.
- package/README.md +1 -0
- package/app/api/bus/groups/route.ts +44 -0
- package/app/api/bus/send/route.ts +11 -1
- package/app/api/bus/spawn/route.ts +12 -1
- package/app/api/bus/wait/route.ts +22 -0
- 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/hello/reveal/route.ts +6 -0
- package/app/api/peer/hello/route.ts +9 -0
- package/app/api/peer/hello/status/route.ts +6 -0
- 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/account/route.ts +9 -0
- package/app/api/peers/gate.test.ts +174 -0
- package/app/api/peers/link/[id]/confirm/route.ts +27 -0
- package/app/api/peers/link/[id]/route.ts +20 -0
- package/app/api/peers/link/route.ts +23 -0
- package/app/api/peers/pending/[id]/accept/route.ts +31 -0
- package/app/api/peers/pending/[id]/decline/route.ts +13 -0
- package/app/api/peers/pending/route.test.ts +37 -0
- package/app/api/peers/pending/route.ts +14 -0
- 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 +31 -13
- package/app/page.tsx +7 -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 +194 -0
- package/components/Hosts/HostRow.tsx +195 -45
- package/components/Hosts/HostsDialog.tsx +49 -37
- package/components/Hosts/IncomingLinkSheet.tsx +246 -0
- package/components/Hosts/LinkCodeSheet.tsx +186 -0
- package/components/Hosts/PeerActivitySheet.tsx +111 -0
- package/components/Hosts/sheet.ts +3 -0
- package/components/SessionList/GroupShelves.tsx +195 -0
- package/components/SessionList/MoveToGroupMenu.tsx +105 -0
- package/components/SessionList/RowSubtitle.tsx +18 -2
- 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/Welcome/ConnectBlock.tsx +1 -1
- 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/index.ts +2 -0
- package/data/peers/keys.ts +7 -0
- package/data/peers/queries.ts +172 -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/brief.ts +3 -1
- package/lib/agents/spawn.ts +4 -0
- package/lib/bus/format.ts +25 -4
- package/lib/bus/index.ts +121 -106
- package/lib/bus/resolve.ts +16 -2
- package/lib/bus/targets.ts +122 -0
- package/lib/bus/waits/kick.ts +12 -0
- package/lib/bus/waits/lines.ts +69 -0
- package/lib/bus/waits/register.ts +83 -0
- package/lib/bus/waits/store.ts +196 -0
- package/lib/bus/waits/tick.ts +192 -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/connect/control-api.ts +108 -2
- package/lib/connect/heartbeat.ts +11 -3
- package/lib/connect/serve.ts +2 -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/20261020T1600_bus_waits.ts +35 -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 +20 -8
- package/lib/doctor/checks.ts +64 -1
- package/lib/done/archive.ts +2 -0
- 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 +19 -1
- package/lib/orchestrator/facts.ts +22 -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 +270 -0
- package/lib/peer/allowlist.ts +1 -1
- package/lib/peer/cli.ts +24 -0
- package/lib/peer/client.ts +26 -1
- package/lib/peer/code.ts +3 -0
- package/lib/peer/hello-route.ts +32 -0
- package/lib/peer/hello.ts +346 -0
- package/lib/peer/link-cli.ts +248 -0
- package/lib/peer/links.ts +117 -5
- package/lib/peer/machines.ts +84 -0
- package/lib/peer/name.ts +32 -0
- package/lib/peer/notify.ts +32 -0
- package/lib/peer/pairing-out.ts +382 -0
- package/lib/peer/pairing.ts +432 -0
- package/lib/peer/pin.ts +62 -0
- package/lib/peer/reasons.ts +7 -0
- package/lib/peer/revoke.ts +75 -0
- package/lib/peer/rpc-intake.ts +37 -8
- package/lib/peer/rpc-server.ts +72 -6
- package/lib/peer/unlink.ts +62 -0
- package/lib/peer/unlinked-proof.ts +44 -0
- package/lib/projects.ts +5 -2
- package/lib/schedules/when.ts +1 -1
- package/lib/security/auth.ts +10 -2
- package/lib/security/demo.ts +7 -0
- package/lib/security/gate.ts +3 -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/end.ts +3 -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 +3 -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 +5 -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 +6 -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,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Notice } from "@/components/ui/notice";
|
|
5
|
+
import type { StartWarning } from "@/lib/claims/startWarning";
|
|
6
|
+
|
|
7
|
+
// Above the composer when a person's task start overlaps claimed work. They
|
|
8
|
+
// choose: start anyway, queue it behind that work, or go back and edit.
|
|
9
|
+
export function StartClaimsNotice({
|
|
10
|
+
warnings,
|
|
11
|
+
busy,
|
|
12
|
+
onStart,
|
|
13
|
+
onDismiss,
|
|
14
|
+
}: {
|
|
15
|
+
warnings: StartWarning[];
|
|
16
|
+
busy: boolean;
|
|
17
|
+
onStart: (after?: string) => void;
|
|
18
|
+
onDismiss: () => void;
|
|
19
|
+
}) {
|
|
20
|
+
const top = warnings[0];
|
|
21
|
+
if (!top) return null;
|
|
22
|
+
const files = top.shared.slice(0, 3).join(", ");
|
|
23
|
+
return (
|
|
24
|
+
<Notice
|
|
25
|
+
tone="warning"
|
|
26
|
+
action={
|
|
27
|
+
<div className="flex flex-wrap gap-2">
|
|
28
|
+
<Button
|
|
29
|
+
size="sm"
|
|
30
|
+
variant="outline"
|
|
31
|
+
className="min-h-11 md:min-h-8"
|
|
32
|
+
disabled={busy}
|
|
33
|
+
onClick={() => onStart()}
|
|
34
|
+
>
|
|
35
|
+
Start anyway
|
|
36
|
+
</Button>
|
|
37
|
+
<Button
|
|
38
|
+
size="sm"
|
|
39
|
+
className="min-h-11 md:min-h-8"
|
|
40
|
+
disabled={busy}
|
|
41
|
+
onClick={() => onStart(`claim:${top.claim.id}`)}
|
|
42
|
+
>
|
|
43
|
+
Start after it
|
|
44
|
+
</Button>
|
|
45
|
+
<Button
|
|
46
|
+
size="sm"
|
|
47
|
+
variant="ghost"
|
|
48
|
+
className="min-h-11 md:min-h-8"
|
|
49
|
+
disabled={busy}
|
|
50
|
+
onClick={onDismiss}
|
|
51
|
+
>
|
|
52
|
+
Edit
|
|
53
|
+
</Button>
|
|
54
|
+
</div>
|
|
55
|
+
}
|
|
56
|
+
>
|
|
57
|
+
<p className="font-medium">
|
|
58
|
+
{top.strength === "strong"
|
|
59
|
+
? "Someone is already on this"
|
|
60
|
+
: "This may overlap"}
|
|
61
|
+
{warnings.length > 1 ? ` (${warnings.length} claims)` : ""}
|
|
62
|
+
</p>
|
|
63
|
+
<p className="text-muted-foreground text-xs break-words">
|
|
64
|
+
“{top.claim.goal}” on {top.claim.machine}
|
|
65
|
+
{top.claim.branch ? ` (${top.claim.branch})` : ""}
|
|
66
|
+
{files ? ` touches ${files}` : ""}
|
|
67
|
+
</p>
|
|
68
|
+
</Notice>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { claimKeys } from "@/data/claims/keys";
|
|
5
|
+
import { claimsUi, claimsUiActions } from "@/stores/claimsUi";
|
|
6
|
+
import { ClaimFiles } from "./ClaimFiles";
|
|
7
|
+
import { OverlapPill } from "./OverlapPill";
|
|
8
|
+
import { StartClaimsNotice } from "./StartClaimsNotice";
|
|
9
|
+
import { focusView, groupByRepo, lastHeardText, startedText } from "./format";
|
|
10
|
+
import type { Claim } from "@/lib/claims/types";
|
|
11
|
+
|
|
12
|
+
const overlap = {
|
|
13
|
+
strength: "strong" as const,
|
|
14
|
+
machine: "box",
|
|
15
|
+
goal: "Rework login",
|
|
16
|
+
shared: 3,
|
|
17
|
+
claimId: "r-1",
|
|
18
|
+
mineId: "s-1",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
beforeEach(() => claimsUiActions.setOpen(false));
|
|
22
|
+
|
|
23
|
+
describe("OverlapPill", () => {
|
|
24
|
+
it("reads Overlaps: <machine>, amber when strong", () => {
|
|
25
|
+
const html = renderToStaticMarkup(createElement(OverlapPill, { overlap }));
|
|
26
|
+
expect(html).toContain("Overlaps: box");
|
|
27
|
+
expect(html).toContain("text-amber-700");
|
|
28
|
+
expect(html).toContain('data-overlap="strong"');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("is muted when weak", () => {
|
|
32
|
+
const html = renderToStaticMarkup(
|
|
33
|
+
createElement(OverlapPill, { overlap: { ...overlap, strength: "weak" } })
|
|
34
|
+
);
|
|
35
|
+
expect(html).toContain("text-muted-foreground");
|
|
36
|
+
expect(html).not.toContain("text-amber-700");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("opens the claims sheet on that pair when tapped", () => {
|
|
40
|
+
const el = OverlapPill({ overlap }) as { props: { onClick: () => void } };
|
|
41
|
+
el.props.onClick();
|
|
42
|
+
expect(claimsUi.open).toBe(true);
|
|
43
|
+
expect([...(claimsUi.focus ?? [])]).toEqual(["s-1", "r-1"]);
|
|
44
|
+
claimsUiActions.showAll();
|
|
45
|
+
expect(claimsUi.focus).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("StartClaimsNotice", () => {
|
|
50
|
+
const warnings = [
|
|
51
|
+
{
|
|
52
|
+
strength: "strong" as const,
|
|
53
|
+
kinds: ["files" as const],
|
|
54
|
+
shared: ["a.ts", "b.ts"],
|
|
55
|
+
claim: {
|
|
56
|
+
id: "r-1",
|
|
57
|
+
goal: "Rework login",
|
|
58
|
+
machine: "box",
|
|
59
|
+
branch: "feature/login",
|
|
60
|
+
sessionName: null,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
const html = renderToStaticMarkup(
|
|
65
|
+
createElement(StartClaimsNotice, {
|
|
66
|
+
warnings,
|
|
67
|
+
busy: false,
|
|
68
|
+
onStart: () => {},
|
|
69
|
+
onDismiss: () => {},
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
it("names the work and offers both choices", () => {
|
|
74
|
+
expect(html).toContain("Rework login");
|
|
75
|
+
expect(html).toContain("on box");
|
|
76
|
+
expect(html).toContain("a.ts, b.ts");
|
|
77
|
+
expect(html).toContain("Start anyway");
|
|
78
|
+
expect(html).toContain("Start after it");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("renders nothing without a warning", () => {
|
|
82
|
+
expect(
|
|
83
|
+
renderToStaticMarkup(
|
|
84
|
+
createElement(StartClaimsNotice, {
|
|
85
|
+
warnings: [],
|
|
86
|
+
busy: false,
|
|
87
|
+
onStart: () => {},
|
|
88
|
+
onDismiss: () => {},
|
|
89
|
+
})
|
|
90
|
+
)
|
|
91
|
+
).toBe("");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("claims keys and helpers", () => {
|
|
96
|
+
it("keys the list by project", () => {
|
|
97
|
+
expect(claimKeys.list()).toEqual(["claims", "list", ""]);
|
|
98
|
+
expect(claimKeys.list("p1")).toEqual(["claims", "list", "p1"]);
|
|
99
|
+
expect(claimKeys.list("p1").slice(0, 1)).toEqual(claimKeys.all);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("words ages and last heard", () => {
|
|
103
|
+
expect(startedText({ startedAt: 0 }, 12 * 60_000)).toBe("started 12m ago");
|
|
104
|
+
expect(startedText({ startedAt: 0 }, 5_000)).toBe("just started");
|
|
105
|
+
expect(lastHeardText(0, 6 * 60_000)).toBe("last heard 6m ago");
|
|
106
|
+
expect(lastHeardText(null, 0)).toBe("this machine");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const c = (id: string, repo: string, startedAt: number) =>
|
|
110
|
+
({ id, repo, startedAt, files: [], filesMore: 0 }) as unknown as Claim;
|
|
111
|
+
|
|
112
|
+
it("groups by repo, oldest work first", () => {
|
|
113
|
+
const groups = groupByRepo([
|
|
114
|
+
c("a", "r2", 5),
|
|
115
|
+
c("b", "r1", 9),
|
|
116
|
+
c("c", "r2", 1),
|
|
117
|
+
]);
|
|
118
|
+
expect(groups.map((g) => [g.repo, g.claims.map((x) => x.id)])).toEqual([
|
|
119
|
+
["r2", ["c", "a"]],
|
|
120
|
+
["r1", ["b"]],
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("narrows to one pair", () => {
|
|
125
|
+
const view = {
|
|
126
|
+
claims: [c("a", "r", 1), c("b", "r", 2), c("z", "r", 3)],
|
|
127
|
+
overlaps: [
|
|
128
|
+
{
|
|
129
|
+
a: "a",
|
|
130
|
+
b: "b",
|
|
131
|
+
kinds: ["files" as const],
|
|
132
|
+
shared: [],
|
|
133
|
+
strength: "weak" as const,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
a: "b",
|
|
137
|
+
b: "z",
|
|
138
|
+
kinds: ["files" as const],
|
|
139
|
+
shared: [],
|
|
140
|
+
strength: "weak" as const,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
const f = focusView(view, ["a", "b"]);
|
|
145
|
+
expect(f.claims.map((x) => x.id)).toEqual(["a", "b"]);
|
|
146
|
+
expect(f.overlaps).toHaveLength(1);
|
|
147
|
+
expect(focusView(view, null)).toBe(view);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("marks shared files in a claim's list", () => {
|
|
151
|
+
const html = renderToStaticMarkup(
|
|
152
|
+
createElement(ClaimFiles, {
|
|
153
|
+
files: ["a.ts", "b.ts"],
|
|
154
|
+
more: 2,
|
|
155
|
+
shared: new Set(["a.ts"]),
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
expect(html).toContain("and 2 more");
|
|
159
|
+
expect(html.match(/text-amber-700/g)).toHaveLength(1);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Claim } from "@/lib/claims/types";
|
|
2
|
+
import type { ClaimsView } from "@/lib/claims/view";
|
|
3
|
+
|
|
4
|
+
const span = (ms: number) => {
|
|
5
|
+
const mins = Math.floor(Math.max(0, ms) / 60_000);
|
|
6
|
+
if (mins < 1) return "under a minute";
|
|
7
|
+
if (mins < 60) return `${mins}m`;
|
|
8
|
+
if (mins < 24 * 60) return `${Math.floor(mins / 60)}h`;
|
|
9
|
+
return `${Math.floor(mins / (24 * 60))}d`;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** "started 12m ago". */
|
|
13
|
+
export const startedText = (c: Pick<Claim, "startedAt">, now: number) => {
|
|
14
|
+
const s = span(now - c.startedAt);
|
|
15
|
+
return s === "under a minute" ? "just started" : `started ${s} ago`;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** "last heard 6m ago". */
|
|
19
|
+
export const lastHeardText = (at: number | null, now: number) => {
|
|
20
|
+
if (at === null) return "this machine";
|
|
21
|
+
const s = span(now - at);
|
|
22
|
+
return s === "under a minute" ? "heard just now" : `last heard ${s} ago`;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** A machine not heard from for this long is shown as quiet. */
|
|
26
|
+
export const QUIET_MS = 3 * 60_000;
|
|
27
|
+
|
|
28
|
+
export interface RepoGroup {
|
|
29
|
+
repo: string;
|
|
30
|
+
claims: Claim[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Claims by repository, the repository with the oldest work first. */
|
|
34
|
+
export function groupByRepo(claims: Claim[]): RepoGroup[] {
|
|
35
|
+
const byRepo = new Map<string, Claim[]>();
|
|
36
|
+
for (const c of claims)
|
|
37
|
+
byRepo.set(c.repo, [...(byRepo.get(c.repo) ?? []), c]);
|
|
38
|
+
return [...byRepo]
|
|
39
|
+
.map(([repo, list]) => ({
|
|
40
|
+
repo,
|
|
41
|
+
claims: [...list].sort((a, b) => a.startedAt - b.startedAt),
|
|
42
|
+
}))
|
|
43
|
+
.sort((a, b) => a.claims[0].startedAt - b.claims[0].startedAt);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The claims and overlaps of one pair, or everything when no pair is set. */
|
|
47
|
+
export function focusView(
|
|
48
|
+
view: Pick<ClaimsView, "claims" | "overlaps">,
|
|
49
|
+
focus: string[] | null
|
|
50
|
+
) {
|
|
51
|
+
if (!focus) return view;
|
|
52
|
+
const ids = new Set(focus);
|
|
53
|
+
return {
|
|
54
|
+
claims: view.claims.filter((c) => ids.has(c.id)),
|
|
55
|
+
overlaps: view.overlaps.filter((o) => ids.has(o.a) && ids.has(o.b)),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -15,6 +15,9 @@ import { useGitCheck } from "@/data/git/queries";
|
|
|
15
15
|
import { useSelectedWorkspace } from "@/hooks/useSelectedWorkspace";
|
|
16
16
|
import { useLaunchSession } from "@/data/sessions";
|
|
17
17
|
import { useCreateTask } from "@/data/tasks";
|
|
18
|
+
import { useCheckStart } from "@/data/claims";
|
|
19
|
+
import { StartClaimsNotice } from "@/components/Claims";
|
|
20
|
+
import type { StartWarning } from "@/lib/claims/startWarning";
|
|
18
21
|
import { usePanes } from "@/contexts/PaneContext";
|
|
19
22
|
import { setPendingPrompt } from "@/stores/initialPrompt";
|
|
20
23
|
import { draftsActions, draftsStore, newDraft } from "@/stores/drafts";
|
|
@@ -53,8 +56,15 @@ export function DraftPanel({
|
|
|
53
56
|
const launch = useLaunchSession();
|
|
54
57
|
const { workspace } = useSelectedWorkspace();
|
|
55
58
|
const createTask = useCreateTask();
|
|
59
|
+
const checkStart = useCheckStart();
|
|
56
60
|
const [prefill, setPrefill] = useState<{ text: string; at: number }>();
|
|
57
|
-
|
|
61
|
+
// A task start that overlaps claimed work waits here for the person's choice.
|
|
62
|
+
const [held, setHeld] = useState<{
|
|
63
|
+
text: string;
|
|
64
|
+
warnings: StartWarning[];
|
|
65
|
+
} | null>(null);
|
|
66
|
+
const pending =
|
|
67
|
+
launch.isPending || createTask.isPending || checkStart.isPending;
|
|
58
68
|
|
|
59
69
|
if (!hydrated) return null;
|
|
60
70
|
if (!draft)
|
|
@@ -67,15 +77,32 @@ export function DraftPanel({
|
|
|
67
77
|
</div>
|
|
68
78
|
);
|
|
69
79
|
|
|
70
|
-
const send = async (
|
|
80
|
+
const send = async (
|
|
81
|
+
text: string,
|
|
82
|
+
images: ChatImage[],
|
|
83
|
+
files: ChatFile[],
|
|
84
|
+
claim?: { after?: string }
|
|
85
|
+
) => {
|
|
71
86
|
try {
|
|
72
87
|
if (draft.openPr) {
|
|
73
88
|
if (!project) throw new Error("Pick a project for a task");
|
|
74
89
|
if (images.length || files.length)
|
|
75
90
|
throw new Error("A task takes text only");
|
|
91
|
+
if (!claim && draft.hostId === project.host_id) {
|
|
92
|
+
const warnings = await checkStart.mutateAsync({
|
|
93
|
+
projectId: project.id,
|
|
94
|
+
prompt: text,
|
|
95
|
+
});
|
|
96
|
+
if (warnings.length) {
|
|
97
|
+
setHeld({ text, warnings });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
setHeld(null);
|
|
76
102
|
const { session, queued } = await createTask.mutateAsync({
|
|
77
103
|
projectId: project.id,
|
|
78
104
|
prompt: text,
|
|
105
|
+
after: claim?.after,
|
|
79
106
|
model: draft.taskModel ?? undefined,
|
|
80
107
|
priority: draft.taskPriority,
|
|
81
108
|
baseBranch: draft.baseBranch ?? undefined,
|
|
@@ -145,6 +172,17 @@ export function DraftPanel({
|
|
|
145
172
|
git={git}
|
|
146
173
|
onChange={(patch) => draftsActions.update(draft.id, patch)}
|
|
147
174
|
/>
|
|
175
|
+
{held && (
|
|
176
|
+
<StartClaimsNotice
|
|
177
|
+
warnings={held.warnings}
|
|
178
|
+
busy={pending}
|
|
179
|
+
onStart={(after) => void send(held.text, [], [], { after })}
|
|
180
|
+
onDismiss={() => {
|
|
181
|
+
setPrefill({ text: held.text, at: Date.now() });
|
|
182
|
+
setHeld(null);
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
148
186
|
<Composer
|
|
149
187
|
draftKey={draftComposerKey(draft.id)}
|
|
150
188
|
attachScope="draft"
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { ExternalLink, Link2, Loader2, RefreshCw } from "lucide-react";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Notice } from "@/components/ui/notice";
|
|
7
|
+
import { LinkForm } from "@/components/Welcome/ConnectBlock";
|
|
8
|
+
import {
|
|
9
|
+
useAccountMachines,
|
|
10
|
+
useRefreshAccount,
|
|
11
|
+
useStartLink,
|
|
12
|
+
} from "@/data/peers";
|
|
13
|
+
import { UPGRADE_URL } from "@/lib/peer/reasons";
|
|
14
|
+
import type { AccountMachine } from "@/lib/peer/machines";
|
|
15
|
+
import { cn } from "@/lib/utils";
|
|
16
|
+
import { WaitingLinks } from "./IncomingLinkSheet";
|
|
17
|
+
import { LinkCodeSheet } from "./LinkCodeSheet";
|
|
18
|
+
|
|
19
|
+
function MachineRow({
|
|
20
|
+
machine,
|
|
21
|
+
canLink,
|
|
22
|
+
onLink,
|
|
23
|
+
starting,
|
|
24
|
+
}: {
|
|
25
|
+
machine: AccountMachine;
|
|
26
|
+
canLink: boolean;
|
|
27
|
+
onLink: () => void;
|
|
28
|
+
starting: boolean;
|
|
29
|
+
}) {
|
|
30
|
+
const linked = machine.linkState === "active";
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className={cn(
|
|
34
|
+
"bg-muted/40 flex items-center gap-3 rounded-lg px-3 py-2.5",
|
|
35
|
+
!canLink && !linked && "opacity-60"
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
<span
|
|
39
|
+
className={cn(
|
|
40
|
+
"h-2 w-2 shrink-0 rounded-full",
|
|
41
|
+
machine.online ? "bg-emerald-500" : "bg-muted-foreground/40"
|
|
42
|
+
)}
|
|
43
|
+
aria-label={machine.online ? "Online" : "Offline"}
|
|
44
|
+
/>
|
|
45
|
+
<div className="min-w-0 flex-1">
|
|
46
|
+
<p className="truncate text-sm font-medium">{machine.name}</p>
|
|
47
|
+
<p className="text-muted-foreground truncate text-xs">
|
|
48
|
+
{[
|
|
49
|
+
machine.online ? "Online" : "Offline",
|
|
50
|
+
machine.version && `AgentOS ${machine.version}`,
|
|
51
|
+
]
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join(" · ")}
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
{linked ? (
|
|
57
|
+
<span className="text-xs font-medium text-emerald-600 dark:text-emerald-400">
|
|
58
|
+
Linked
|
|
59
|
+
</span>
|
|
60
|
+
) : machine.linkState === "suspended" ? (
|
|
61
|
+
<span
|
|
62
|
+
className="text-xs text-amber-700 dark:text-amber-400"
|
|
63
|
+
title={machine.suspendedReason ?? undefined}
|
|
64
|
+
>
|
|
65
|
+
Suspended
|
|
66
|
+
</span>
|
|
67
|
+
) : (
|
|
68
|
+
<Button
|
|
69
|
+
variant="outline"
|
|
70
|
+
size="sm"
|
|
71
|
+
className="min-h-11 sm:min-h-8"
|
|
72
|
+
disabled={!canLink || starting}
|
|
73
|
+
onClick={onLink}
|
|
74
|
+
>
|
|
75
|
+
{starting ? (
|
|
76
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
77
|
+
) : (
|
|
78
|
+
<Link2 className="h-4 w-4" />
|
|
79
|
+
)}
|
|
80
|
+
Link
|
|
81
|
+
</Button>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Machines → "On your AgentOS account": link them through Connect. */
|
|
88
|
+
export function AccountMachines() {
|
|
89
|
+
const { data, isPending, error } = useAccountMachines(true);
|
|
90
|
+
const refresh = useRefreshAccount();
|
|
91
|
+
const start = useStartLink();
|
|
92
|
+
const [linkId, setLinkId] = useState<string | null>(null);
|
|
93
|
+
const [target, setTarget] = useState<string | null>(null);
|
|
94
|
+
|
|
95
|
+
const link = (machineId: string) => {
|
|
96
|
+
setTarget(machineId);
|
|
97
|
+
start.mutate(machineId, { onSuccess: (l) => setLinkId(l.id) });
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (isPending)
|
|
101
|
+
return <div className="bg-muted/40 h-14 animate-pulse rounded-lg" />;
|
|
102
|
+
if (error) return <p className="text-destructive text-xs">{error.message}</p>;
|
|
103
|
+
if (!data.enrolled)
|
|
104
|
+
return (
|
|
105
|
+
<div className="space-y-2 rounded-lg border p-3">
|
|
106
|
+
<p className="text-sm font-medium">
|
|
107
|
+
Connect this machine to link it with your others
|
|
108
|
+
</p>
|
|
109
|
+
<LinkForm />
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const entitled = data.entitlements?.linkMachines === true;
|
|
114
|
+
return (
|
|
115
|
+
<section className="space-y-2" aria-label="On your AgentOS account">
|
|
116
|
+
<div className="flex items-center justify-between gap-2">
|
|
117
|
+
<p className="text-sm font-medium">On your AgentOS account</p>
|
|
118
|
+
<Button
|
|
119
|
+
variant="ghost"
|
|
120
|
+
size="icon-sm"
|
|
121
|
+
className="h-11 w-11 sm:h-8 sm:w-8"
|
|
122
|
+
aria-label="Check your account again"
|
|
123
|
+
disabled={refresh.isPending}
|
|
124
|
+
onClick={() => refresh.mutate()}
|
|
125
|
+
>
|
|
126
|
+
<RefreshCw
|
|
127
|
+
className={cn("h-4 w-4", refresh.isPending && "animate-spin")}
|
|
128
|
+
/>
|
|
129
|
+
</Button>
|
|
130
|
+
</div>
|
|
131
|
+
{data.uncheckedSince && (
|
|
132
|
+
<Notice tone="warning">
|
|
133
|
+
Couldn't check your account since{" "}
|
|
134
|
+
{new Date(data.uncheckedSince).toLocaleString([], {
|
|
135
|
+
dateStyle: "medium",
|
|
136
|
+
timeStyle: "short",
|
|
137
|
+
})}
|
|
138
|
+
. Machines already linked keep working; new links wait until it's
|
|
139
|
+
back.
|
|
140
|
+
</Notice>
|
|
141
|
+
)}
|
|
142
|
+
{data.entitlements && !entitled && (
|
|
143
|
+
<Notice
|
|
144
|
+
action={
|
|
145
|
+
<Button asChild variant="outline" className="min-h-11 sm:min-h-9">
|
|
146
|
+
<a href={UPGRADE_URL} target="_blank" rel="noopener noreferrer">
|
|
147
|
+
Upgrade
|
|
148
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
149
|
+
</a>
|
|
150
|
+
</Button>
|
|
151
|
+
}
|
|
152
|
+
>
|
|
153
|
+
Link your machines with Pro: tasks move between them over any network,
|
|
154
|
+
end-to-end encrypted.
|
|
155
|
+
</Notice>
|
|
156
|
+
)}
|
|
157
|
+
{entitled && <WaitingLinks />}
|
|
158
|
+
{data.error && (
|
|
159
|
+
<p className="text-muted-foreground text-xs">{data.error}</p>
|
|
160
|
+
)}
|
|
161
|
+
{data.machines.length === 0 ? (
|
|
162
|
+
<p className="text-muted-foreground text-xs">
|
|
163
|
+
No other machines on your account yet. Run{" "}
|
|
164
|
+
<code className="font-mono">agent-os connect</code> on another
|
|
165
|
+
machine, then link it here.
|
|
166
|
+
</p>
|
|
167
|
+
) : (
|
|
168
|
+
data.machines.map((m) => (
|
|
169
|
+
<MachineRow
|
|
170
|
+
key={m.id}
|
|
171
|
+
machine={m}
|
|
172
|
+
canLink={entitled}
|
|
173
|
+
starting={start.isPending && target === m.id}
|
|
174
|
+
onLink={() => link(m.id)}
|
|
175
|
+
/>
|
|
176
|
+
))
|
|
177
|
+
)}
|
|
178
|
+
{start.error && (
|
|
179
|
+
<p className="text-destructive text-xs">{start.error.message}</p>
|
|
180
|
+
)}
|
|
181
|
+
<LinkCodeSheet
|
|
182
|
+
linkId={linkId}
|
|
183
|
+
onClose={() => {
|
|
184
|
+
setLinkId(null);
|
|
185
|
+
start.reset();
|
|
186
|
+
}}
|
|
187
|
+
onRetry={() => {
|
|
188
|
+
setLinkId(null);
|
|
189
|
+
if (target) link(target);
|
|
190
|
+
}}
|
|
191
|
+
/>
|
|
192
|
+
</section>
|
|
193
|
+
);
|
|
194
|
+
}
|