@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,196 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
} from "@/components/ui/dialog";
|
|
13
|
+
import {
|
|
14
|
+
AvailabilityDot,
|
|
15
|
+
LocalityBadge,
|
|
16
|
+
} from "@/components/Models/ModelBadges";
|
|
17
|
+
import { RoleModelSelect } from "@/components/Models/RoleModelSelect";
|
|
18
|
+
import { useCrewQuery, useSwitchSessionModel } from "@/data/models";
|
|
19
|
+
import { useLaunchSession } from "@/data/sessions";
|
|
20
|
+
import { usePanes } from "@/contexts/PaneContext";
|
|
21
|
+
import type { Session } from "@/lib/db";
|
|
22
|
+
import {
|
|
23
|
+
AGENT_ROLES,
|
|
24
|
+
AGENT_ROLE_DEFS,
|
|
25
|
+
isAgentRole,
|
|
26
|
+
} from "@/lib/models/agent-roles";
|
|
27
|
+
import { cn } from "@/lib/utils";
|
|
28
|
+
import { uuid } from "@/lib/uuid";
|
|
29
|
+
|
|
30
|
+
const modelName = (ref: string | null) =>
|
|
31
|
+
ref ? ref.split("/").slice(1).join("/") : "model";
|
|
32
|
+
|
|
33
|
+
// The role a running session was started in and the model it runs on, with
|
|
34
|
+
// its crew: the sessions it works with, by @name. Nothing for a session
|
|
35
|
+
// started without a role. `compact` is a phone's small button.
|
|
36
|
+
export function RolePill({
|
|
37
|
+
session,
|
|
38
|
+
paneId,
|
|
39
|
+
compact,
|
|
40
|
+
}: {
|
|
41
|
+
session: Session | null | undefined;
|
|
42
|
+
paneId?: string;
|
|
43
|
+
compact?: boolean;
|
|
44
|
+
}) {
|
|
45
|
+
const [open, setOpen] = useState(false);
|
|
46
|
+
const role = isAgentRole(session?.agent_role) ? session.agent_role : null;
|
|
47
|
+
const crew = useCrewQuery(session?.id, !!role);
|
|
48
|
+
const switchModel = useSwitchSessionModel(session?.id ?? "");
|
|
49
|
+
const launch = useLaunchSession();
|
|
50
|
+
const { attachSession } = usePanes();
|
|
51
|
+
if (!session || !role) return null;
|
|
52
|
+
const me = crew.data?.members.find((m) => m.id === session.id);
|
|
53
|
+
const down = me?.availability === "down";
|
|
54
|
+
const label = AGENT_ROLE_DEFS[role].label;
|
|
55
|
+
const others = crew.data?.members.filter((m) => m.id !== session.id) ?? [];
|
|
56
|
+
|
|
57
|
+
const add = async (next: (typeof AGENT_ROLES)[number]) => {
|
|
58
|
+
try {
|
|
59
|
+
const { session: made } = await launch.mutateAsync({
|
|
60
|
+
id: uuid(),
|
|
61
|
+
projectId: session.project_id,
|
|
62
|
+
agentType: "claude",
|
|
63
|
+
model: "",
|
|
64
|
+
access: "full",
|
|
65
|
+
useWorktree: false,
|
|
66
|
+
baseBranch: null,
|
|
67
|
+
prompt: "",
|
|
68
|
+
agentRole: next,
|
|
69
|
+
crewId: session.crew_id ?? session.id,
|
|
70
|
+
});
|
|
71
|
+
if (paneId) attachSession(paneId, made.id, made.tmux_name ?? "");
|
|
72
|
+
setOpen(false);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
toast.error(error instanceof Error ? error.message : String(error));
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
onClick={(e) => {
|
|
83
|
+
e.stopPropagation();
|
|
84
|
+
setOpen(true);
|
|
85
|
+
}}
|
|
86
|
+
aria-label={`${label}, ${modelName(me?.ref ?? null)}${down ? ", model not answering" : ""}`}
|
|
87
|
+
className={cn(
|
|
88
|
+
"ring-border/70 flex shrink-0 items-center gap-1.5 rounded-full text-xs ring-1 ring-inset",
|
|
89
|
+
down
|
|
90
|
+
? "bg-amber-500/10 text-amber-600 dark:text-amber-400"
|
|
91
|
+
: "bg-muted/60",
|
|
92
|
+
compact
|
|
93
|
+
? "-my-1.5 h-11 w-11 justify-center rounded-lg ring-0"
|
|
94
|
+
: "h-7 max-w-64 px-2.5"
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
<span className="bg-accent text-primary grid h-4 w-4 shrink-0 place-items-center rounded text-[10px] font-semibold">
|
|
98
|
+
{label[0]}
|
|
99
|
+
</span>
|
|
100
|
+
{!compact && (
|
|
101
|
+
<>
|
|
102
|
+
<span className="truncate">
|
|
103
|
+
{label}
|
|
104
|
+
{me?.ref ? ` · ${modelName(me.ref)}` : ""}
|
|
105
|
+
</span>
|
|
106
|
+
{me && !down && <LocalityBadge locality={me.locality} />}
|
|
107
|
+
{down && <span className="shrink-0">unreachable</span>}
|
|
108
|
+
</>
|
|
109
|
+
)}
|
|
110
|
+
{compact && down && (
|
|
111
|
+
<span className="bg-destructive absolute h-2 w-2 translate-x-3 -translate-y-3 rounded-full" />
|
|
112
|
+
)}
|
|
113
|
+
</button>
|
|
114
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
115
|
+
<DialogContent>
|
|
116
|
+
<DialogHeader>
|
|
117
|
+
<DialogTitle>{label} · role and crew</DialogTitle>
|
|
118
|
+
<DialogDescription>
|
|
119
|
+
{AGENT_ROLE_DEFS[role].purpose}
|
|
120
|
+
</DialogDescription>
|
|
121
|
+
</DialogHeader>
|
|
122
|
+
<div className="bg-muted/40 space-y-2 rounded-xl p-3">
|
|
123
|
+
<div className="flex items-center justify-between gap-2">
|
|
124
|
+
<span className="flex min-w-0 items-center gap-2 text-sm font-medium">
|
|
125
|
+
<AvailabilityDot
|
|
126
|
+
availability={{ state: me?.availability ?? "unknown" }}
|
|
127
|
+
/>
|
|
128
|
+
<span className="truncate">{modelName(me?.ref ?? null)}</span>
|
|
129
|
+
</span>
|
|
130
|
+
{me && <LocalityBadge locality={me.locality} />}
|
|
131
|
+
</div>
|
|
132
|
+
{down && (
|
|
133
|
+
<p role="alert" className="text-destructive text-xs">
|
|
134
|
+
This model's server isn't answering. Your conversation
|
|
135
|
+
is kept: switch to another model to carry on.
|
|
136
|
+
</p>
|
|
137
|
+
)}
|
|
138
|
+
<RoleModelSelect
|
|
139
|
+
value={me?.ref ?? null}
|
|
140
|
+
inheritLabel="Switch model…"
|
|
141
|
+
disabled={switchModel.isPending}
|
|
142
|
+
onChange={(ref) =>
|
|
143
|
+
ref &&
|
|
144
|
+
switchModel.mutate(ref, {
|
|
145
|
+
onError: (e) => toast.error(e.message),
|
|
146
|
+
onSuccess: () => toast.success("Switched model"),
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
<div className="space-y-1.5">
|
|
152
|
+
<h3 className="text-muted-foreground text-xs font-medium uppercase">
|
|
153
|
+
Crew
|
|
154
|
+
</h3>
|
|
155
|
+
{others.length === 0 && (
|
|
156
|
+
<p className="text-muted-foreground text-sm">
|
|
157
|
+
Nobody else yet. Add a role to work with this session.
|
|
158
|
+
</p>
|
|
159
|
+
)}
|
|
160
|
+
{others.map((m) => (
|
|
161
|
+
<div key={m.id} className="flex items-center gap-2 text-sm">
|
|
162
|
+
<span className="min-w-0 flex-1">
|
|
163
|
+
<span className="block truncate">{m.name}</span>
|
|
164
|
+
<span className="text-muted-foreground block truncate font-mono text-xs">
|
|
165
|
+
{m.agentRole ? `@${m.agentRole}` : "no role"} ·{" "}
|
|
166
|
+
{modelName(m.ref)}
|
|
167
|
+
</span>
|
|
168
|
+
</span>
|
|
169
|
+
<AvailabilityDot availability={{ state: m.availability }} />
|
|
170
|
+
<LocalityBadge locality={m.locality} />
|
|
171
|
+
</div>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
<div className="flex flex-wrap gap-2">
|
|
175
|
+
{AGENT_ROLES.filter(
|
|
176
|
+
(r) =>
|
|
177
|
+
r !== role &&
|
|
178
|
+
r !== "supervisor" &&
|
|
179
|
+
!others.some((m) => m.agentRole === r)
|
|
180
|
+
).map((r) => (
|
|
181
|
+
<Button
|
|
182
|
+
key={r}
|
|
183
|
+
variant="outline"
|
|
184
|
+
className="h-11 md:h-9"
|
|
185
|
+
disabled={launch.isPending}
|
|
186
|
+
onClick={() => void add(r)}
|
|
187
|
+
>
|
|
188
|
+
Add {AGENT_ROLE_DEFS[r].label}
|
|
189
|
+
</Button>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
</DialogContent>
|
|
193
|
+
</Dialog>
|
|
194
|
+
</>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
FlaskConical,
|
|
20
20
|
RefreshCw,
|
|
21
21
|
Smartphone,
|
|
22
|
+
Users,
|
|
22
23
|
X,
|
|
23
24
|
type LucideIcon,
|
|
24
25
|
} from "lucide-react";
|
|
@@ -34,6 +35,7 @@ import { AuditsPanel } from "@/components/Audits/AuditsPanel";
|
|
|
34
35
|
import { HealthPanel } from "@/components/Health/HealthPanel";
|
|
35
36
|
import { DevicesPanel } from "@/components/Devices";
|
|
36
37
|
import { GlobalMergeSection } from "@/components/Merge/GlobalMergeSection";
|
|
38
|
+
import { AgentRolesSection } from "@/components/Models/AgentRolesSection";
|
|
37
39
|
import { ModelsSection } from "@/components/Models/ModelsSection";
|
|
38
40
|
import { SharedLinksPanel } from "@/components/Shares/SharedLinksPanel";
|
|
39
41
|
import { SchedulesPanel } from "@/components/Schedules";
|
|
@@ -87,6 +89,11 @@ export const SECTION_INFO: Record<
|
|
|
87
89
|
hint: "Which model each kind of work runs on, and providers",
|
|
88
90
|
icon: Cpu,
|
|
89
91
|
},
|
|
92
|
+
"agent-roles": {
|
|
93
|
+
label: "Agent roles",
|
|
94
|
+
hint: "Developer, Analyst, Reviewer and Supervisor: a model each, local or cloud",
|
|
95
|
+
icon: Users,
|
|
96
|
+
},
|
|
90
97
|
notifications: {
|
|
91
98
|
label: "Notifications",
|
|
92
99
|
hint: "Sound, browser alerts, your phone",
|
|
@@ -145,6 +152,8 @@ function SectionBody({
|
|
|
145
152
|
return <WorkspaceSettings />;
|
|
146
153
|
case "models":
|
|
147
154
|
return <ModelsSection />;
|
|
155
|
+
case "agent-roles":
|
|
156
|
+
return <AgentRolesSection />;
|
|
148
157
|
case "devices":
|
|
149
158
|
return <DevicesPanel />;
|
|
150
159
|
case "shares":
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type SharePreview,
|
|
29
29
|
} from "@/data/shares";
|
|
30
30
|
import { shareUi, shareUiActions } from "@/stores/shareUi";
|
|
31
|
+
import { exportUiActions } from "@/stores/exportUi";
|
|
31
32
|
import {
|
|
32
33
|
DEFAULT_EXPIRY,
|
|
33
34
|
EXPIRIES,
|
|
@@ -271,6 +272,18 @@ function ShareBody({
|
|
|
271
272
|
{error && preview && (
|
|
272
273
|
<p className="text-destructive text-sm">{error}</p>
|
|
273
274
|
)}
|
|
275
|
+
{kind === "chat" && sessionId && (
|
|
276
|
+
<button
|
|
277
|
+
type="button"
|
|
278
|
+
onClick={() => {
|
|
279
|
+
shareUiActions.close();
|
|
280
|
+
exportUiActions.open(sessionId, target.title);
|
|
281
|
+
}}
|
|
282
|
+
className="text-muted-foreground hover:text-foreground min-h-11 self-start text-xs underline underline-offset-2"
|
|
283
|
+
>
|
|
284
|
+
Export for AI instead…
|
|
285
|
+
</button>
|
|
286
|
+
)}
|
|
274
287
|
</div>
|
|
275
288
|
</div>
|
|
276
289
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { AlertTriangle, Info } from "lucide-react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const TONES = {
|
|
6
|
+
info: {
|
|
7
|
+
box: "bg-muted/60",
|
|
8
|
+
icon: <Info className="text-muted-foreground mt-0.5 h-4 w-4 shrink-0" />,
|
|
9
|
+
},
|
|
10
|
+
warning: {
|
|
11
|
+
box: "bg-amber-500/10",
|
|
12
|
+
icon: (
|
|
13
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
14
|
+
),
|
|
15
|
+
},
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
/** A short message in a tinted box, with an optional action on the right. */
|
|
19
|
+
export function Notice({
|
|
20
|
+
tone = "info",
|
|
21
|
+
action,
|
|
22
|
+
className,
|
|
23
|
+
children,
|
|
24
|
+
}: {
|
|
25
|
+
tone?: keyof typeof TONES;
|
|
26
|
+
action?: ReactNode;
|
|
27
|
+
className?: string;
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
}) {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
role="status"
|
|
33
|
+
className={cn(
|
|
34
|
+
"flex flex-col gap-2 rounded-lg p-3 text-sm sm:flex-row sm:items-center",
|
|
35
|
+
TONES[tone].box,
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
<div className="flex min-w-0 flex-1 gap-2">
|
|
40
|
+
{TONES[tone].icon}
|
|
41
|
+
<div className="min-w-0 flex-1">{children}</div>
|
|
42
|
+
</div>
|
|
43
|
+
{action}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
const TONES = {
|
|
5
|
+
amber: "bg-amber-500/15 text-amber-700 dark:text-amber-400",
|
|
6
|
+
muted: "bg-muted text-muted-foreground",
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
/** A small coloured pill for a status. A button when it has an onClick. */
|
|
10
|
+
export function StatusPill({
|
|
11
|
+
tone,
|
|
12
|
+
className,
|
|
13
|
+
onClick,
|
|
14
|
+
...props
|
|
15
|
+
}: Omit<ComponentProps<"span">, "onClick"> & {
|
|
16
|
+
tone: keyof typeof TONES;
|
|
17
|
+
onClick?: () => void;
|
|
18
|
+
}) {
|
|
19
|
+
const classes = cn(
|
|
20
|
+
"inline-flex max-w-full min-w-0 items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-semibold",
|
|
21
|
+
TONES[tone],
|
|
22
|
+
className
|
|
23
|
+
);
|
|
24
|
+
if (!onClick) return <span className={classes} {...props} />;
|
|
25
|
+
return (
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
onClick={(e) => {
|
|
29
|
+
e.stopPropagation();
|
|
30
|
+
onClick();
|
|
31
|
+
}}
|
|
32
|
+
className={cn(
|
|
33
|
+
classes,
|
|
34
|
+
"relative cursor-pointer before:absolute before:-inset-x-1 before:-inset-y-3 hover:brightness-110"
|
|
35
|
+
)}
|
|
36
|
+
{...(props as ComponentProps<"button">)}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import type { AnalystLinkView, Recommendation } from "@/lib/analyst/types";
|
|
3
|
+
import { sessionKeys } from "../sessions/keys";
|
|
4
|
+
import { analystKeys } from "./keys";
|
|
5
|
+
|
|
6
|
+
async function call<T>(
|
|
7
|
+
url: string,
|
|
8
|
+
method: string,
|
|
9
|
+
body?: unknown
|
|
10
|
+
): Promise<T> {
|
|
11
|
+
const res = await fetch(url, {
|
|
12
|
+
method,
|
|
13
|
+
headers: { "Content-Type": "application/json" },
|
|
14
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
15
|
+
});
|
|
16
|
+
const data = await res.json().catch(() => ({}));
|
|
17
|
+
if (!res.ok) throw new Error(data.error || "Something went wrong");
|
|
18
|
+
return data as T;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const link = (id: string) => `/api/analyst/${encodeURIComponent(id)}`;
|
|
22
|
+
const rec = (id: string, recId: string, action: string) =>
|
|
23
|
+
`${link(id)}/recommendations/${encodeURIComponent(recId)}/${action}`;
|
|
24
|
+
|
|
25
|
+
function useRefresh() {
|
|
26
|
+
const client = useQueryClient();
|
|
27
|
+
return () => {
|
|
28
|
+
void client.invalidateQueries({ queryKey: analystKeys.all });
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface StartAnalystInput {
|
|
33
|
+
coderSessionId?: string;
|
|
34
|
+
newCoder?: {
|
|
35
|
+
projectId: string;
|
|
36
|
+
agentType: string;
|
|
37
|
+
model?: string;
|
|
38
|
+
prompt?: string;
|
|
39
|
+
useWorktree?: boolean;
|
|
40
|
+
};
|
|
41
|
+
analystModel?: string;
|
|
42
|
+
reviewLast?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function useStartAnalyst() {
|
|
46
|
+
const client = useQueryClient();
|
|
47
|
+
return useMutation({
|
|
48
|
+
mutationFn: (input: StartAnalystInput) =>
|
|
49
|
+
call<{ link: AnalystLinkView }>("/api/analyst", "POST", input).then(
|
|
50
|
+
(r) => r.link
|
|
51
|
+
),
|
|
52
|
+
onSuccess: () => {
|
|
53
|
+
void client.invalidateQueries({ queryKey: analystKeys.all });
|
|
54
|
+
void client.invalidateQueries({ queryKey: sessionKeys.all });
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function useSendRecommendation(linkId: string) {
|
|
60
|
+
const refresh = useRefresh();
|
|
61
|
+
return useMutation({
|
|
62
|
+
mutationFn: ({ recId, text }: { recId: string; text?: string }) =>
|
|
63
|
+
call<{ recommendation: Recommendation }>(
|
|
64
|
+
rec(linkId, recId, "send"),
|
|
65
|
+
"POST",
|
|
66
|
+
text === undefined ? {} : { text }
|
|
67
|
+
),
|
|
68
|
+
onSettled: refresh,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useDismissRecommendation(linkId: string) {
|
|
73
|
+
const refresh = useRefresh();
|
|
74
|
+
return useMutation({
|
|
75
|
+
mutationFn: (recId: string) =>
|
|
76
|
+
call(rec(linkId, recId, "dismiss"), "POST", {}),
|
|
77
|
+
onSettled: refresh,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function useAskAgain(linkId: string) {
|
|
82
|
+
const refresh = useRefresh();
|
|
83
|
+
return useMutation({
|
|
84
|
+
mutationFn: (recId: string) =>
|
|
85
|
+
call(rec(linkId, recId, "again"), "POST", {}),
|
|
86
|
+
onSettled: refresh,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function useReviewNow(linkId: string) {
|
|
91
|
+
const refresh = useRefresh();
|
|
92
|
+
return useMutation({
|
|
93
|
+
mutationFn: () => call(`${link(linkId)}/review`, "POST", {}),
|
|
94
|
+
onSettled: refresh,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function useSetAnalystPaused(linkId: string) {
|
|
99
|
+
const refresh = useRefresh();
|
|
100
|
+
return useMutation({
|
|
101
|
+
mutationFn: (paused: boolean) => call(link(linkId), "PATCH", { paused }),
|
|
102
|
+
onSettled: refresh,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function useEndAnalyst(linkId: string) {
|
|
107
|
+
const client = useQueryClient();
|
|
108
|
+
return useMutation({
|
|
109
|
+
mutationFn: (endAnalyst: boolean) =>
|
|
110
|
+
call(link(linkId), "DELETE", { endAnalyst }),
|
|
111
|
+
onSettled: () => {
|
|
112
|
+
void client.invalidateQueries({ queryKey: analystKeys.all });
|
|
113
|
+
void client.invalidateQueries({ queryKey: sessionKeys.all });
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import type { AnalystLinkView } from "@/lib/analyst/types";
|
|
3
|
+
import { usePollWhenOffline } from "../push/connection";
|
|
4
|
+
import { analystKeys } from "./keys";
|
|
5
|
+
|
|
6
|
+
async function read(url: string): Promise<AnalystLinkView | null> {
|
|
7
|
+
const res = await fetch(url);
|
|
8
|
+
if (res.status === 404) return null;
|
|
9
|
+
const body = await res.json().catch(() => ({}));
|
|
10
|
+
if (!res.ok) throw new Error(body.error || "Couldn't load the analyst");
|
|
11
|
+
return body.link ?? null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// The pairing a session is in, as coder or analyst, or null. Pushed when a
|
|
15
|
+
// pairing or a card changes; it polls only while the stream is down.
|
|
16
|
+
export function useSessionAnalyst(sessionId: string | undefined) {
|
|
17
|
+
return useQuery({
|
|
18
|
+
queryKey: analystKeys.ofSession(sessionId ?? ""),
|
|
19
|
+
queryFn: () =>
|
|
20
|
+
read(`/api/sessions/${encodeURIComponent(sessionId!)}/analyst`),
|
|
21
|
+
enabled: !!sessionId,
|
|
22
|
+
staleTime: 5000,
|
|
23
|
+
refetchInterval: usePollWhenOffline(15000),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useAnalystLink(linkId: string | undefined) {
|
|
28
|
+
return useQuery({
|
|
29
|
+
queryKey: analystKeys.link(linkId ?? ""),
|
|
30
|
+
queryFn: () => read(`/api/analyst/${encodeURIComponent(linkId!)}`),
|
|
31
|
+
enabled: !!linkId,
|
|
32
|
+
staleTime: 5000,
|
|
33
|
+
refetchInterval: usePollWhenOffline(15000),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
2
|
+
import type { ClaimsView } from "@/lib/claims/view";
|
|
3
|
+
import type { StartWarning } from "@/lib/claims/startWarning";
|
|
4
|
+
import { usePollWhenOffline } from "../push/connection";
|
|
5
|
+
import { claimKeys } from "./keys";
|
|
6
|
+
|
|
7
|
+
// Claims here and on linked machines. Pushed when a claim changes; it polls
|
|
8
|
+
// only while the stream is down.
|
|
9
|
+
export function useClaims(opts: { project?: string; enabled?: boolean } = {}) {
|
|
10
|
+
const { project, enabled = true } = opts;
|
|
11
|
+
return useQuery({
|
|
12
|
+
queryKey: claimKeys.list(project),
|
|
13
|
+
queryFn: async (): Promise<ClaimsView> => {
|
|
14
|
+
const res = await fetch(
|
|
15
|
+
`/api/claims${project ? `?project=${encodeURIComponent(project)}` : ""}`
|
|
16
|
+
);
|
|
17
|
+
if (!res.ok) throw new Error("Failed to load claims");
|
|
18
|
+
return res.json();
|
|
19
|
+
},
|
|
20
|
+
enabled,
|
|
21
|
+
staleTime: 5000,
|
|
22
|
+
refetchInterval: usePollWhenOffline(15000),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StartCheckInput {
|
|
27
|
+
projectId: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
prompt: string;
|
|
30
|
+
branch?: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Who else is on the work a person is about to start. A failure means no
|
|
34
|
+
// warning: it never stops a start.
|
|
35
|
+
export function useCheckStart() {
|
|
36
|
+
return useMutation({
|
|
37
|
+
mutationFn: async (input: StartCheckInput): Promise<StartWarning[]> => {
|
|
38
|
+
const res = await fetch("/api/claims/check", {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "Content-Type": "application/json" },
|
|
41
|
+
body: JSON.stringify(input),
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) return [];
|
|
44
|
+
return ((await res.json()) as { overlaps: StartWarning[] }).overlaps;
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useMutation } from "@tanstack/react-query";
|
|
2
|
+
import {
|
|
3
|
+
exportFileName,
|
|
4
|
+
rangeSlug,
|
|
5
|
+
type ExportEstimate,
|
|
6
|
+
type ExportRequest,
|
|
7
|
+
} from "@/lib/export/view";
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
ExportEstimate,
|
|
11
|
+
ExportRequest,
|
|
12
|
+
ExportTurnInfo,
|
|
13
|
+
ExportWarning,
|
|
14
|
+
} from "@/lib/export/view";
|
|
15
|
+
|
|
16
|
+
const post = (url: string, body: unknown) =>
|
|
17
|
+
fetch(url, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { "Content-Type": "application/json" },
|
|
20
|
+
body: JSON.stringify(body),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
async function failure(res: Response): Promise<never> {
|
|
24
|
+
const data = await res.json().catch(() => ({}));
|
|
25
|
+
throw new Error(data.error || "Export failed");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What an export would hold: size, turns, warnings. No file text. */
|
|
29
|
+
export function useExportEstimate(sessionId: string) {
|
|
30
|
+
return useMutation({
|
|
31
|
+
mutationFn: async (req: ExportRequest): Promise<ExportEstimate> => {
|
|
32
|
+
const res = await post(`/api/sessions/${sessionId}/export/estimate`, req);
|
|
33
|
+
return res.ok ? res.json() : failure(res);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The file itself, as the server built it. */
|
|
39
|
+
export async function fetchExport(
|
|
40
|
+
sessionId: string,
|
|
41
|
+
req: ExportRequest,
|
|
42
|
+
title: string,
|
|
43
|
+
sel: { from: number; to: number; of: number } | null
|
|
44
|
+
): Promise<{ blob: Blob; name: string }> {
|
|
45
|
+
const res = await post(`/api/sessions/${sessionId}/export`, req);
|
|
46
|
+
if (!res.ok) return failure(res);
|
|
47
|
+
return {
|
|
48
|
+
blob: await res.blob(),
|
|
49
|
+
name: exportFileName(title, sel ? rangeSlug(sel) : "all", req.format),
|
|
50
|
+
};
|
|
51
|
+
}
|