@saadnvd1/agent-os 0.3.700 → 0.3.723
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/app/api/agent-roles/route.test.ts +67 -0
- package/app/api/agent-roles/route.ts +61 -0
- package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
- package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
- package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
- package/app/api/analyst/[id]/review/route.ts +22 -0
- package/app/api/analyst/[id]/route.ts +44 -0
- package/app/api/analyst/route.ts +10 -0
- package/app/api/bus/groups/route.ts +44 -0
- package/app/api/bus/spawn/route.ts +34 -2
- 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/models/availability/route.ts +9 -0
- package/app/api/models/choices/route.ts +9 -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/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]/analyst/route.ts +11 -0
- package/app/api/sessions/[id]/crew/route.ts +38 -0
- package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
- package/app/api/sessions/[id]/export/route.test.ts +125 -0
- package/app/api/sessions/[id]/export/route.ts +34 -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]/model/route.ts +49 -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 -14
- package/app/page.tsx +10 -0
- package/components/Chat/Messages.tsx +104 -10
- 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/DraftChips.tsx +6 -1
- package/components/Draft/DraftPanel.tsx +90 -3
- package/components/Draft/ModelUnavailableDialog.tsx +78 -0
- package/components/Draft/RoleChips.tsx +110 -0
- package/components/Export/ExportActions.tsx +136 -0
- package/components/Export/ExportSheet.tsx +217 -0
- package/components/Export/RangePicker.tsx +144 -0
- package/components/Export/Seg.tsx +38 -0
- package/components/Export/TurnSelectBar.tsx +45 -0
- 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/Models/AgentRoleCard.tsx +114 -0
- package/components/Models/AgentRolesSection.tsx +132 -0
- package/components/Models/ModelBadges.tsx +48 -0
- package/components/Models/ModelsSection.tsx +4 -2
- package/components/Models/RoleModelSelect.tsx +87 -0
- package/components/Pane/DesktopTabBar.tsx +2 -0
- package/components/Pane/MobileTabBar.tsx +12 -0
- package/components/Pane/PaneMenu.tsx +10 -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 +19 -7
- package/components/SessionList/SessionRowMenu.tsx +23 -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/Sessions/RolePill.tsx +196 -0
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Share/ShareDialog.tsx +13 -0
- package/components/ui/notice.tsx +46 -0
- package/components/ui/status-pill.tsx +39 -0
- package/data/analyst/index.ts +3 -0
- package/data/analyst/keys.ts +6 -0
- package/data/analyst/mutations.ts +116 -0
- package/data/analyst/queries.ts +35 -0
- package/data/claims/index.ts +2 -0
- package/data/claims/keys.ts +5 -0
- package/data/claims/queries.ts +47 -0
- package/data/exports/index.ts +2 -0
- package/data/exports/keys.ts +3 -0
- package/data/exports/queries.ts +51 -0
- package/data/models/index.ts +98 -0
- package/data/peers/keys.ts +1 -0
- package/data/peers/queries.ts +31 -0
- package/data/push/topics.ts +9 -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 +36 -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 +1 -0
- package/lib/agents/spawn.ts +4 -0
- package/lib/analyst/__fixtures__/items.ts +57 -0
- package/lib/analyst/__fixtures__/mocks.ts +49 -0
- package/lib/analyst/brief.ts +17 -0
- package/lib/analyst/buildDigest.ts +204 -0
- package/lib/analyst/createLink.ts +98 -0
- package/lib/analyst/endLink.ts +31 -0
- package/lib/analyst/forward.ts +109 -0
- package/lib/analyst/index.ts +16 -0
- package/lib/analyst/linkView.ts +38 -0
- package/lib/analyst/onTurnEnd.ts +119 -0
- package/lib/analyst/parseReview.ts +41 -0
- package/lib/analyst/pushDigest.ts +143 -0
- package/lib/analyst/resume.ts +35 -0
- package/lib/analyst/route.ts +50 -0
- package/lib/analyst/signals.ts +121 -0
- package/lib/analyst/store.ts +303 -0
- package/lib/analyst/types.ts +140 -0
- package/lib/bus/condense.ts +5 -1
- package/lib/bus/index.ts +13 -0
- package/lib/bus/read-scope.ts +12 -0
- package/lib/bus/role-alias.ts +70 -0
- package/lib/bus/targets.ts +3 -0
- package/lib/chat/drivers/opencode-mapper.ts +55 -1
- package/lib/chat/drivers/pi-approvals.ts +7 -4
- package/lib/chat/drivers/pi-mapper.ts +38 -0
- package/lib/chat/drivers/pi-text-tools.ts +104 -0
- package/lib/chat/drivers/pi.ts +8 -3
- package/lib/chat/drivers/turn-items.ts +36 -11
- package/lib/chat/drivers/turn-outcome.ts +108 -0
- package/lib/chat/events.ts +8 -0
- package/lib/chat/registry.ts +13 -0
- package/lib/chat/store.ts +19 -0
- package/lib/chat/turns.ts +1 -0
- package/lib/chat/worker/extras.ts +4 -2
- package/lib/claims/collectLocal.ts +123 -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 +12 -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/migrations/20261021T1400_session_agent_roles.ts +20 -0
- package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
- package/lib/db/queries.ts +0 -40
- package/lib/db/session-parts/columns.ts +3 -0
- package/lib/db/types.ts +19 -8
- package/lib/doctor/checks.ts +64 -1
- package/lib/done/archive.ts +2 -0
- package/lib/drafts.ts +4 -0
- package/lib/export/build.ts +470 -0
- package/lib/export/compact.ts +156 -0
- package/lib/export/estimate.ts +83 -0
- package/lib/export/fixtures.ts +45 -0
- package/lib/export/html.tsx +195 -0
- package/lib/export/json.ts +39 -0
- package/lib/export/load.ts +64 -0
- package/lib/export/markdown.ts +127 -0
- package/lib/export/request.ts +36 -0
- package/lib/export/service.ts +142 -0
- package/lib/export/summary.ts +86 -0
- package/lib/export/turns.ts +63 -0
- package/lib/export/view.ts +173 -0
- package/lib/git-remote.ts +43 -0
- package/lib/hosts/index.ts +10 -1
- package/lib/models/agent-binding.ts +102 -0
- package/lib/models/agent-roles-view.ts +157 -0
- package/lib/models/agent-roles.ts +87 -0
- package/lib/models/availability-refs.ts +17 -0
- package/lib/models/availability.ts +91 -0
- package/lib/models/catalog.ts +156 -0
- package/lib/models/config.ts +27 -0
- package/lib/models/locality.ts +33 -0
- package/lib/models/providers.ts +11 -0
- package/lib/models/resolve.ts +50 -12
- package/lib/models/roles.ts +12 -0
- package/lib/needs-you.ts +1 -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 +12 -2
- package/lib/orchestrator/read.ts +5 -1
- 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/untrusted.ts +5 -0
- 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 +6 -2
- package/lib/security/demo.ts +7 -0
- package/lib/session-artifacts/agentos-files.ts +94 -0
- package/lib/session-artifacts/collect.ts +17 -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/crew.ts +72 -0
- package/lib/sessions/launch.ts +54 -11
- package/lib/settings-url.ts +1 -0
- package/lib/share/markdown.ts +2 -2
- package/lib/share/redact.ts +16 -10
- package/lib/share/render.tsx +1 -1
- package/lib/share/snapshot.ts +2 -2
- package/lib/sidebar/rollup.ts +1 -0
- package/lib/sidebar/row-menu.ts +8 -1
- 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 +11 -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-deps/caches.ts +3 -2
- package/lib/worktree-deps/clone.ts +3 -1
- package/lib/worktree-deps/paths.ts +26 -0
- 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 +12 -1
- package/stores/claimsUi.ts +22 -0
- package/stores/exportUi.ts +87 -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,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
|
+
}
|
|
@@ -21,6 +21,7 @@ import { useAgentStatusQuery } from "@/data/agents";
|
|
|
21
21
|
import { useProjectMachinesQuery } from "@/data/projects";
|
|
22
22
|
import { modelChoices, useModelsQuery } from "@/data/models";
|
|
23
23
|
import type { AgentProbe } from "@/lib/agents/probe";
|
|
24
|
+
import { RoleChips } from "./RoleChips";
|
|
24
25
|
import { ChipHeading, ChipItem, ChipMenu, ChipToggle } from "./Chip";
|
|
25
26
|
import {
|
|
26
27
|
DEFAULT_PRIORITY,
|
|
@@ -168,7 +169,7 @@ export function DraftChips({
|
|
|
168
169
|
</ChipMenu>
|
|
169
170
|
)}
|
|
170
171
|
|
|
171
|
-
{!draft.openPr && (
|
|
172
|
+
{!draft.openPr && !draft.agentRole && (
|
|
172
173
|
<ChipMenu icon={Bot} name="Agent" label={agentLabel(draft.agentType)}>
|
|
173
174
|
{AGENT_OPTIONS.map((o) => (
|
|
174
175
|
<ChipItem
|
|
@@ -189,6 +190,10 @@ export function DraftChips({
|
|
|
189
190
|
</ChipMenu>
|
|
190
191
|
)}
|
|
191
192
|
|
|
193
|
+
{!draft.openPr && local && (
|
|
194
|
+
<RoleChips draft={draft} onChange={onChange} />
|
|
195
|
+
)}
|
|
196
|
+
|
|
192
197
|
{project && local && git?.isGitRepo && (
|
|
193
198
|
<ChipMenu
|
|
194
199
|
icon={GitBranch}
|
|
@@ -14,12 +14,18 @@ import { useHostsQuery } from "@/data/hosts";
|
|
|
14
14
|
import { useGitCheck } from "@/data/git/queries";
|
|
15
15
|
import { useSelectedWorkspace } from "@/hooks/useSelectedWorkspace";
|
|
16
16
|
import { useLaunchSession } from "@/data/sessions";
|
|
17
|
+
import { ModelUnavailableError } from "@/data/sessions/queries";
|
|
18
|
+
import { isAgentRole } from "@/lib/models/agent-roles";
|
|
17
19
|
import { useCreateTask } from "@/data/tasks";
|
|
20
|
+
import { useCheckStart } from "@/data/claims";
|
|
21
|
+
import { StartClaimsNotice } from "@/components/Claims";
|
|
22
|
+
import type { StartWarning } from "@/lib/claims/startWarning";
|
|
18
23
|
import { usePanes } from "@/contexts/PaneContext";
|
|
19
24
|
import { setPendingPrompt } from "@/stores/initialPrompt";
|
|
20
25
|
import { draftsActions, draftsStore, newDraft } from "@/stores/drafts";
|
|
21
26
|
import { DraftChips } from "./DraftChips";
|
|
22
27
|
import { DraftHeading } from "./DraftHeading";
|
|
28
|
+
import { ModelUnavailableDialog } from "./ModelUnavailableDialog";
|
|
23
29
|
import { draftModels } from "./draftModels";
|
|
24
30
|
|
|
25
31
|
// A new session before its first send: choices on top, a composer below.
|
|
@@ -53,8 +59,22 @@ export function DraftPanel({
|
|
|
53
59
|
const launch = useLaunchSession();
|
|
54
60
|
const { workspace } = useSelectedWorkspace();
|
|
55
61
|
const createTask = useCreateTask();
|
|
62
|
+
const checkStart = useCheckStart();
|
|
56
63
|
const [prefill, setPrefill] = useState<{ text: string; at: number }>();
|
|
57
|
-
|
|
64
|
+
// A task start that overlaps claimed work waits here for the person's choice.
|
|
65
|
+
const [held, setHeld] = useState<{
|
|
66
|
+
text: string;
|
|
67
|
+
warnings: StartWarning[];
|
|
68
|
+
} | null>(null);
|
|
69
|
+
// A role's model server that didn't answer, with what was being sent.
|
|
70
|
+
const [down, setDown] = useState<{
|
|
71
|
+
error: ModelUnavailableError;
|
|
72
|
+
text: string;
|
|
73
|
+
images: ChatImage[];
|
|
74
|
+
files: ChatFile[];
|
|
75
|
+
} | null>(null);
|
|
76
|
+
const pending =
|
|
77
|
+
launch.isPending || createTask.isPending || checkStart.isPending;
|
|
58
78
|
|
|
59
79
|
if (!hydrated) return null;
|
|
60
80
|
if (!draft)
|
|
@@ -67,15 +87,32 @@ export function DraftPanel({
|
|
|
67
87
|
</div>
|
|
68
88
|
);
|
|
69
89
|
|
|
70
|
-
const send = async (
|
|
90
|
+
const send = async (
|
|
91
|
+
text: string,
|
|
92
|
+
images: ChatImage[],
|
|
93
|
+
files: ChatFile[],
|
|
94
|
+
claim?: { after?: string; onUnavailable?: "fallback" }
|
|
95
|
+
) => {
|
|
71
96
|
try {
|
|
72
97
|
if (draft.openPr) {
|
|
73
98
|
if (!project) throw new Error("Pick a project for a task");
|
|
74
99
|
if (images.length || files.length)
|
|
75
100
|
throw new Error("A task takes text only");
|
|
101
|
+
if (!claim && draft.hostId === project.host_id) {
|
|
102
|
+
const warnings = await checkStart.mutateAsync({
|
|
103
|
+
projectId: project.id,
|
|
104
|
+
prompt: text,
|
|
105
|
+
});
|
|
106
|
+
if (warnings.length) {
|
|
107
|
+
setHeld({ text, warnings });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
setHeld(null);
|
|
76
112
|
const { session, queued } = await createTask.mutateAsync({
|
|
77
113
|
projectId: project.id,
|
|
78
114
|
prompt: text,
|
|
115
|
+
after: claim?.after,
|
|
79
116
|
model: draft.taskModel ?? undefined,
|
|
80
117
|
priority: draft.taskPriority,
|
|
81
118
|
baseBranch: draft.baseBranch ?? undefined,
|
|
@@ -102,7 +139,17 @@ export function DraftPanel({
|
|
|
102
139
|
? draft.hostId
|
|
103
140
|
: undefined,
|
|
104
141
|
agentType: draft.agentType,
|
|
105
|
-
model: draft.
|
|
142
|
+
model: isAgentRole(draft.agentRole)
|
|
143
|
+
? (draft.roleModel ?? "")
|
|
144
|
+
: draft.model,
|
|
145
|
+
...(isAgentRole(draft.agentRole)
|
|
146
|
+
? {
|
|
147
|
+
agentRole: draft.agentRole,
|
|
148
|
+
...(claim?.onUnavailable && {
|
|
149
|
+
onUnavailable: claim.onUnavailable,
|
|
150
|
+
}),
|
|
151
|
+
}
|
|
152
|
+
: {}),
|
|
106
153
|
access: draft.access,
|
|
107
154
|
useWorktree: draft.useWorktree && !!git?.isGitRepo,
|
|
108
155
|
baseBranch: draft.baseBranch,
|
|
@@ -126,6 +173,10 @@ export function DraftPanel({
|
|
|
126
173
|
session.host_id
|
|
127
174
|
);
|
|
128
175
|
} catch (error) {
|
|
176
|
+
if (error instanceof ModelUnavailableError) {
|
|
177
|
+
setDown({ error, text, images, files });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
129
180
|
setPrefill({ text, at: Date.now() });
|
|
130
181
|
toast.error(error instanceof Error ? error.message : String(error));
|
|
131
182
|
}
|
|
@@ -145,6 +196,42 @@ export function DraftPanel({
|
|
|
145
196
|
git={git}
|
|
146
197
|
onChange={(patch) => draftsActions.update(draft.id, patch)}
|
|
147
198
|
/>
|
|
199
|
+
<ModelUnavailableDialog
|
|
200
|
+
error={down?.error ?? null}
|
|
201
|
+
busy={pending}
|
|
202
|
+
onClose={() => {
|
|
203
|
+
if (down) setPrefill({ text: down.text, at: Date.now() });
|
|
204
|
+
setDown(null);
|
|
205
|
+
}}
|
|
206
|
+
onRetry={() => {
|
|
207
|
+
const d = down;
|
|
208
|
+
setDown(null);
|
|
209
|
+
if (d) void send(d.text, d.images, d.files);
|
|
210
|
+
}}
|
|
211
|
+
onFallback={() => {
|
|
212
|
+
const d = down;
|
|
213
|
+
setDown(null);
|
|
214
|
+
if (d)
|
|
215
|
+
void send(d.text, d.images, d.files, {
|
|
216
|
+
onUnavailable: "fallback",
|
|
217
|
+
});
|
|
218
|
+
}}
|
|
219
|
+
onPick={() => {
|
|
220
|
+
if (down) setPrefill({ text: down.text, at: Date.now() });
|
|
221
|
+
setDown(null);
|
|
222
|
+
}}
|
|
223
|
+
/>
|
|
224
|
+
{held && (
|
|
225
|
+
<StartClaimsNotice
|
|
226
|
+
warnings={held.warnings}
|
|
227
|
+
busy={pending}
|
|
228
|
+
onStart={(after) => void send(held.text, [], [], { after })}
|
|
229
|
+
onDismiss={() => {
|
|
230
|
+
setPrefill({ text: held.text, at: Date.now() });
|
|
231
|
+
setHeld(null);
|
|
232
|
+
}}
|
|
233
|
+
/>
|
|
234
|
+
)}
|
|
148
235
|
<Composer
|
|
149
236
|
draftKey={draftComposerKey(draft.id)}
|
|
150
237
|
attachScope="draft"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
} from "@/components/ui/dialog";
|
|
11
|
+
import { AGENT_ROLE_DEFS, isAgentRole } from "@/lib/models/agent-roles";
|
|
12
|
+
import type { ModelUnavailableError } from "@/data/sessions/queries";
|
|
13
|
+
|
|
14
|
+
// A session in a role couldn't start: its model's server isn't answering.
|
|
15
|
+
// A local model never turns into a cloud one on its own: starting on the
|
|
16
|
+
// role's fallback is a choice made here.
|
|
17
|
+
export function ModelUnavailableDialog({
|
|
18
|
+
error,
|
|
19
|
+
busy,
|
|
20
|
+
onRetry,
|
|
21
|
+
onFallback,
|
|
22
|
+
onPick,
|
|
23
|
+
onClose,
|
|
24
|
+
}: {
|
|
25
|
+
error: ModelUnavailableError | null;
|
|
26
|
+
busy: boolean;
|
|
27
|
+
onRetry: () => void;
|
|
28
|
+
onFallback: () => void;
|
|
29
|
+
onPick: () => void;
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
}) {
|
|
32
|
+
const role =
|
|
33
|
+
error && isAgentRole(error.role)
|
|
34
|
+
? AGENT_ROLE_DEFS[error.role].label
|
|
35
|
+
: "role";
|
|
36
|
+
const name = (ref: string) => ref.split("/").slice(1).join("/");
|
|
37
|
+
return (
|
|
38
|
+
<Dialog open={!!error} onOpenChange={(o) => !o && onClose()}>
|
|
39
|
+
<DialogContent>
|
|
40
|
+
<DialogHeader>
|
|
41
|
+
<DialogTitle>Can't start the {role}</DialogTitle>
|
|
42
|
+
<DialogDescription>
|
|
43
|
+
{error ? `${name(error.ref)}'s server isn't answering.` : ""}
|
|
44
|
+
</DialogDescription>
|
|
45
|
+
</DialogHeader>
|
|
46
|
+
{error && (
|
|
47
|
+
<p className="bg-muted/60 text-muted-foreground rounded-lg px-3 py-2 font-mono text-xs break-words">
|
|
48
|
+
{error.message}
|
|
49
|
+
</p>
|
|
50
|
+
)}
|
|
51
|
+
<div className="flex flex-col gap-2">
|
|
52
|
+
<Button className="h-11" disabled={busy} onClick={onRetry}>
|
|
53
|
+
Retry
|
|
54
|
+
</Button>
|
|
55
|
+
{error?.fallback && (
|
|
56
|
+
<Button
|
|
57
|
+
variant="outline"
|
|
58
|
+
className="h-11"
|
|
59
|
+
disabled={busy}
|
|
60
|
+
onClick={onFallback}
|
|
61
|
+
>
|
|
62
|
+
Start on {name(error.fallback)}
|
|
63
|
+
</Button>
|
|
64
|
+
)}
|
|
65
|
+
<Button variant="outline" className="h-11" onClick={onPick}>
|
|
66
|
+
Pick another model
|
|
67
|
+
</Button>
|
|
68
|
+
</div>
|
|
69
|
+
{error?.fallback && (
|
|
70
|
+
<p className="text-muted-foreground text-xs">
|
|
71
|
+
Starting on the fallback sends your code to wherever that model
|
|
72
|
+
runs.
|
|
73
|
+
</p>
|
|
74
|
+
)}
|
|
75
|
+
</DialogContent>
|
|
76
|
+
</Dialog>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Cpu, Users } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
AvailabilityDot,
|
|
6
|
+
LocalityBadge,
|
|
7
|
+
} from "@/components/Models/ModelBadges";
|
|
8
|
+
import { useAgentRolesQuery, useModelChoices } from "@/data/models";
|
|
9
|
+
import type { Draft } from "@/lib/drafts";
|
|
10
|
+
import {
|
|
11
|
+
AGENT_ROLES,
|
|
12
|
+
AGENT_ROLE_DEFS,
|
|
13
|
+
isAgentRole,
|
|
14
|
+
} from "@/lib/models/agent-roles";
|
|
15
|
+
import { ChipItem, ChipMenu } from "./Chip";
|
|
16
|
+
|
|
17
|
+
// A draft's role and the model it runs on. The role's binding gives the
|
|
18
|
+
// model (Settings > Agent roles); the model chip overrides it for this
|
|
19
|
+
// session. Both lists come from the server.
|
|
20
|
+
export function RoleChips({
|
|
21
|
+
draft,
|
|
22
|
+
onChange,
|
|
23
|
+
}: {
|
|
24
|
+
draft: Draft;
|
|
25
|
+
onChange: (patch: Partial<Draft>) => void;
|
|
26
|
+
}) {
|
|
27
|
+
const roles = useAgentRolesQuery("settings");
|
|
28
|
+
const { data: choices = [] } = useModelChoices(!!draft.agentRole);
|
|
29
|
+
const role = isAgentRole(draft.agentRole) ? draft.agentRole : null;
|
|
30
|
+
const bound = roles.data?.roles.find((r) => r.id === role);
|
|
31
|
+
const override = draft.roleModel
|
|
32
|
+
? choices.find((c) => c.ref === draft.roleModel)
|
|
33
|
+
: undefined;
|
|
34
|
+
const current = override ?? bound?.choice ?? null;
|
|
35
|
+
const modelName = (ref: string | undefined | null) =>
|
|
36
|
+
ref ? ref.split("/").slice(1).join("/") : "…";
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<ChipMenu
|
|
40
|
+
icon={Users}
|
|
41
|
+
name="Role"
|
|
42
|
+
label={role ? AGENT_ROLE_DEFS[role].label : "No role"}
|
|
43
|
+
>
|
|
44
|
+
<ChipItem
|
|
45
|
+
checked={!role}
|
|
46
|
+
onSelect={() => onChange({ agentRole: null, roleModel: null })}
|
|
47
|
+
hint="A plain session on the interactive model"
|
|
48
|
+
>
|
|
49
|
+
No role
|
|
50
|
+
</ChipItem>
|
|
51
|
+
{AGENT_ROLES.map((r) => {
|
|
52
|
+
const view = roles.data?.roles.find((x) => x.id === r);
|
|
53
|
+
return (
|
|
54
|
+
<ChipItem
|
|
55
|
+
key={r}
|
|
56
|
+
checked={r === role}
|
|
57
|
+
onSelect={() =>
|
|
58
|
+
onChange({ agentRole: r, roleModel: null, openPr: false })
|
|
59
|
+
}
|
|
60
|
+
hint={`${AGENT_ROLE_DEFS[r].purpose}${view?.binding ? ` · ${modelName(view.binding.ref)}` : ""}`}
|
|
61
|
+
>
|
|
62
|
+
{AGENT_ROLE_DEFS[r].label}
|
|
63
|
+
</ChipItem>
|
|
64
|
+
);
|
|
65
|
+
})}
|
|
66
|
+
</ChipMenu>
|
|
67
|
+
{role && (
|
|
68
|
+
<ChipMenu
|
|
69
|
+
icon={Cpu}
|
|
70
|
+
name="Model"
|
|
71
|
+
label={
|
|
72
|
+
draft.roleModel
|
|
73
|
+
? modelName(draft.roleModel)
|
|
74
|
+
: `Role model: ${modelName(bound?.binding?.ref)}`
|
|
75
|
+
}
|
|
76
|
+
>
|
|
77
|
+
<ChipItem
|
|
78
|
+
checked={!draft.roleModel}
|
|
79
|
+
onSelect={() => onChange({ roleModel: null })}
|
|
80
|
+
hint={bound?.binding ? modelName(bound.binding.ref) : undefined}
|
|
81
|
+
>
|
|
82
|
+
Role model
|
|
83
|
+
</ChipItem>
|
|
84
|
+
{choices.map((c) => (
|
|
85
|
+
<ChipItem
|
|
86
|
+
key={c.ref}
|
|
87
|
+
checked={c.ref === draft.roleModel}
|
|
88
|
+
onSelect={() => onChange({ roleModel: c.ref })}
|
|
89
|
+
hint={c.group}
|
|
90
|
+
>
|
|
91
|
+
<span className="flex items-center gap-1.5">
|
|
92
|
+
<AvailabilityDot availability={c.availability} />
|
|
93
|
+
{c.label}
|
|
94
|
+
<LocalityBadge locality={c.locality} />
|
|
95
|
+
</span>
|
|
96
|
+
</ChipItem>
|
|
97
|
+
))}
|
|
98
|
+
</ChipMenu>
|
|
99
|
+
)}
|
|
100
|
+
{role && current?.availability.state === "down" && (
|
|
101
|
+
<span
|
|
102
|
+
role="status"
|
|
103
|
+
className="text-destructive flex h-11 items-center text-xs md:h-8"
|
|
104
|
+
>
|
|
105
|
+
{modelName(current.ref)} isn't answering
|
|
106
|
+
</span>
|
|
107
|
+
)}
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
}
|