@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
|
@@ -1,17 +1,77 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useState } from "react";
|
|
3
4
|
import { Button } from "@/components/ui/button";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Activity,
|
|
7
|
+
ExternalLink,
|
|
8
|
+
Monitor,
|
|
9
|
+
Server,
|
|
10
|
+
Trash2,
|
|
11
|
+
PlugZap,
|
|
12
|
+
Loader2,
|
|
13
|
+
Link2,
|
|
14
|
+
Unlink,
|
|
15
|
+
} from "lucide-react";
|
|
16
|
+
import { Notice } from "@/components/ui/notice";
|
|
5
17
|
import type { Host } from "@/lib/db";
|
|
6
18
|
import { useDeleteHost, useLinkHost, useTestHost } from "@/data/hosts";
|
|
19
|
+
import { useUnlinkPeer } from "@/data/peers";
|
|
20
|
+
import { NEEDS_PRO, UPGRADE_URL } from "@/lib/peer/reasons";
|
|
7
21
|
import { cn } from "@/lib/utils";
|
|
22
|
+
import { PeerActivitySheet } from "./PeerActivitySheet";
|
|
23
|
+
|
|
24
|
+
function connectLine(host: Host): string {
|
|
25
|
+
if (host.peer_state === "active")
|
|
26
|
+
return "Linked through Connect · tasks run on its AgentOS";
|
|
27
|
+
if (host.peer_state === "suspended") return "Through Connect · suspended";
|
|
28
|
+
if (host.peer_state === "unlinked")
|
|
29
|
+
return `Through Connect · ${host.peer_note ?? "unlinked"}`;
|
|
30
|
+
return "Through Connect · not linked";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Why a Connect link is suspended, with the way out when there is one. */
|
|
34
|
+
function SuspendedNote({ reason }: { reason: string | null | undefined }) {
|
|
35
|
+
const plan = reason === NEEDS_PRO;
|
|
36
|
+
return (
|
|
37
|
+
<Notice
|
|
38
|
+
tone="warning"
|
|
39
|
+
className="mt-2"
|
|
40
|
+
action={
|
|
41
|
+
plan && (
|
|
42
|
+
<Button
|
|
43
|
+
asChild
|
|
44
|
+
variant="outline"
|
|
45
|
+
size="sm"
|
|
46
|
+
className="min-h-11 sm:min-h-8"
|
|
47
|
+
>
|
|
48
|
+
<a href={UPGRADE_URL} target="_blank" rel="noopener noreferrer">
|
|
49
|
+
Upgrade
|
|
50
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
51
|
+
</a>
|
|
52
|
+
</Button>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
{plan
|
|
57
|
+
? "Linking machines needs Pro. Calls both ways are refused until you upgrade; it comes back without linking again."
|
|
58
|
+
: `Suspended: ${reason ?? "no reason given"}. Calls both ways are refused; it comes back on its own if the machine returns to your account.`}
|
|
59
|
+
</Notice>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
8
62
|
|
|
9
63
|
export function HostRow({ host }: { host: Host }) {
|
|
10
64
|
const deleteHost = useDeleteHost();
|
|
11
65
|
const testHost = useTestHost();
|
|
12
66
|
const linkHost = useLinkHost();
|
|
67
|
+
const unlink = useUnlinkPeer();
|
|
68
|
+
const [activity, setActivity] = useState(false);
|
|
69
|
+
const [confirmUnlink, setConfirmUnlink] = useState(false);
|
|
13
70
|
const isLocal = host.id === "local";
|
|
14
71
|
const viaConnect = host.kind === "connect";
|
|
72
|
+
const pinned =
|
|
73
|
+
viaConnect &&
|
|
74
|
+
(host.peer_state === "active" || host.peer_state === "suspended");
|
|
15
75
|
const result = testHost.data;
|
|
16
76
|
const Icon = isLocal ? Monitor : Server;
|
|
17
77
|
|
|
@@ -25,9 +85,7 @@ export function HostRow({ host }: { host: Host }) {
|
|
|
25
85
|
{isLocal
|
|
26
86
|
? "Where agent-os is running"
|
|
27
87
|
: viaConnect
|
|
28
|
-
? host
|
|
29
|
-
? "Linked through Connect · tasks run on its AgentOS"
|
|
30
|
-
: "Through Connect · not linked"
|
|
88
|
+
? connectLine(host)
|
|
31
89
|
: host.linked
|
|
32
90
|
? `${host.ssh_target} · tasks run on its AgentOS`
|
|
33
91
|
: host.ssh_target}
|
|
@@ -70,16 +128,46 @@ export function HostRow({ host }: { host: Host }) {
|
|
|
70
128
|
</Button>
|
|
71
129
|
</>
|
|
72
130
|
)}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
131
|
+
{viaConnect && host.peer_state && (
|
|
132
|
+
<Button
|
|
133
|
+
variant="ghost"
|
|
134
|
+
size="icon-sm"
|
|
135
|
+
className="h-11 w-11 sm:h-8 sm:w-8"
|
|
136
|
+
onClick={() => setActivity(true)}
|
|
137
|
+
aria-label="Activity"
|
|
138
|
+
title="Every call to and from this machine"
|
|
139
|
+
>
|
|
140
|
+
<Activity className="h-4 w-4" />
|
|
141
|
+
</Button>
|
|
142
|
+
)}
|
|
143
|
+
{pinned ? (
|
|
144
|
+
<Button
|
|
145
|
+
variant="ghost"
|
|
146
|
+
size="icon-sm"
|
|
147
|
+
className="h-11 w-11 sm:h-8 sm:w-8"
|
|
148
|
+
onClick={() => setConfirmUnlink(true)}
|
|
149
|
+
disabled={unlink.isPending || confirmUnlink}
|
|
150
|
+
aria-label="Unlink"
|
|
151
|
+
title="Unlink: it can no longer call this machine, nor this one it"
|
|
152
|
+
>
|
|
153
|
+
{unlink.isPending ? (
|
|
154
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
155
|
+
) : (
|
|
156
|
+
<Unlink className="h-4 w-4" />
|
|
157
|
+
)}
|
|
158
|
+
</Button>
|
|
159
|
+
) : (
|
|
160
|
+
<Button
|
|
161
|
+
variant="ghost"
|
|
162
|
+
size="icon-sm"
|
|
163
|
+
className="h-11 w-11 sm:h-8 sm:w-8"
|
|
164
|
+
onClick={() => deleteHost.mutate(host.id)}
|
|
165
|
+
disabled={deleteHost.isPending}
|
|
166
|
+
aria-label="Remove machine"
|
|
167
|
+
>
|
|
168
|
+
<Trash2 className="h-4 w-4" />
|
|
169
|
+
</Button>
|
|
170
|
+
)}
|
|
83
171
|
</>
|
|
84
172
|
)}
|
|
85
173
|
</div>
|
|
@@ -107,6 +195,59 @@ export function HostRow({ host }: { host: Host }) {
|
|
|
107
195
|
{linkHost.error.message}
|
|
108
196
|
</p>
|
|
109
197
|
)}
|
|
198
|
+
{host.peer_state === "suspended" && (
|
|
199
|
+
<SuspendedNote reason={host.peer_note} />
|
|
200
|
+
)}
|
|
201
|
+
{confirmUnlink && (
|
|
202
|
+
<Notice
|
|
203
|
+
tone="warning"
|
|
204
|
+
className="mt-2"
|
|
205
|
+
action={
|
|
206
|
+
<div className="flex gap-2">
|
|
207
|
+
<Button
|
|
208
|
+
variant="ghost"
|
|
209
|
+
size="sm"
|
|
210
|
+
className="min-h-11 sm:min-h-8"
|
|
211
|
+
onClick={() => setConfirmUnlink(false)}
|
|
212
|
+
>
|
|
213
|
+
Cancel
|
|
214
|
+
</Button>
|
|
215
|
+
<Button
|
|
216
|
+
variant="destructive"
|
|
217
|
+
size="sm"
|
|
218
|
+
className="min-h-11 sm:min-h-8"
|
|
219
|
+
onClick={() => {
|
|
220
|
+
setConfirmUnlink(false);
|
|
221
|
+
unlink.mutate(host.id);
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
Unlink
|
|
225
|
+
</Button>
|
|
226
|
+
</div>
|
|
227
|
+
}
|
|
228
|
+
>
|
|
229
|
+
Unlink {host.name}? It's unlinked on both machines, and linking
|
|
230
|
+
them again needs a fresh pairing with new codes.
|
|
231
|
+
</Notice>
|
|
232
|
+
)}
|
|
233
|
+
{unlink.data && !unlink.data.told && (
|
|
234
|
+
<p className="text-muted-foreground mt-1.5 text-xs">
|
|
235
|
+
Unlinked here. {host.name} couldn't be told now; its next call is
|
|
236
|
+
refused and it will show as unlinked.
|
|
237
|
+
</p>
|
|
238
|
+
)}
|
|
239
|
+
{unlink.error && (
|
|
240
|
+
<p className="text-destructive mt-1.5 text-xs">
|
|
241
|
+
{unlink.error.message}
|
|
242
|
+
</p>
|
|
243
|
+
)}
|
|
244
|
+
{viaConnect && (
|
|
245
|
+
<PeerActivitySheet
|
|
246
|
+
hostId={activity ? host.id : null}
|
|
247
|
+
name={host.name}
|
|
248
|
+
onClose={() => setActivity(false)}
|
|
249
|
+
/>
|
|
250
|
+
)}
|
|
110
251
|
{deleteHost.error && (
|
|
111
252
|
<p className="text-destructive mt-1.5 text-xs">
|
|
112
253
|
{deleteHost.error.message}
|
|
@@ -28,6 +28,9 @@ function Origin({ request }: { request: PendingView }) {
|
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const asking = (r: PendingView) =>
|
|
32
|
+
r.state === "accepted" ? "is linked: finish there" : "wants to link";
|
|
33
|
+
|
|
31
34
|
/** On the machine being asked: type the code the other one shows. */
|
|
32
35
|
export function IncomingLinkSheet({
|
|
33
36
|
request,
|
|
@@ -46,7 +49,12 @@ export function IncomingLinkSheet({
|
|
|
46
49
|
onClose();
|
|
47
50
|
};
|
|
48
51
|
const left = (accept.error as { attemptsLeft?: number } | null)?.attemptsLeft;
|
|
49
|
-
|
|
52
|
+
// Held by the server too, so a reload on this machine still shows it.
|
|
53
|
+
const done =
|
|
54
|
+
accept.data ??
|
|
55
|
+
(request?.confirmCode
|
|
56
|
+
? { name: request.name, confirmCode: request.confirmCode }
|
|
57
|
+
: null);
|
|
50
58
|
|
|
51
59
|
if (done)
|
|
52
60
|
return (
|
|
@@ -154,8 +162,9 @@ export function IncomingLinks() {
|
|
|
154
162
|
const [open, setOpen] = useState<PendingView | null>(null);
|
|
155
163
|
// Hidden until a request not seen yet arrives; it covers the header on phones.
|
|
156
164
|
const [later, setLater] = useState<string | null>(null);
|
|
157
|
-
const
|
|
158
|
-
const
|
|
165
|
+
const shown = requests.filter((r) => !later?.includes(r.id));
|
|
166
|
+
const first = shown[0];
|
|
167
|
+
const hidden = shown.length === 0;
|
|
159
168
|
return (
|
|
160
169
|
<>
|
|
161
170
|
{first && !open && !hidden && (
|
|
@@ -166,8 +175,8 @@ export function IncomingLinks() {
|
|
|
166
175
|
<ShieldAlert className="h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
167
176
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
168
177
|
<p className="truncate text-sm">
|
|
169
|
-
<span className="font-medium">{first.name}</span>
|
|
170
|
-
{
|
|
178
|
+
<span className="font-medium">{first.name}</span> {asking(first)}
|
|
179
|
+
{shown.length > 1 ? ` (+${shown.length - 1} more)` : ""}
|
|
171
180
|
</p>
|
|
172
181
|
<Origin request={first} />
|
|
173
182
|
</div>
|
|
@@ -176,20 +185,29 @@ export function IncomingLinks() {
|
|
|
176
185
|
className="min-h-11 sm:min-h-8"
|
|
177
186
|
onClick={() => setOpen(first)}
|
|
178
187
|
>
|
|
179
|
-
Review
|
|
188
|
+
{first.state === "accepted" ? "Show code" : "Review"}
|
|
180
189
|
</Button>
|
|
181
190
|
<Button
|
|
182
191
|
size="icon"
|
|
183
192
|
variant="ghost"
|
|
184
193
|
aria-label="Later"
|
|
185
194
|
className="h-11 w-11 shrink-0 sm:h-8 sm:w-8"
|
|
186
|
-
onClick={() =>
|
|
195
|
+
onClick={() =>
|
|
196
|
+
setLater(`${later ?? ""} ${shown.map((r) => r.id).join(" ")}`)
|
|
197
|
+
}
|
|
187
198
|
>
|
|
188
199
|
<X className="h-4 w-4" />
|
|
189
200
|
</Button>
|
|
190
201
|
</div>
|
|
191
202
|
)}
|
|
192
|
-
<IncomingLinkSheet
|
|
203
|
+
<IncomingLinkSheet
|
|
204
|
+
request={open}
|
|
205
|
+
onClose={() => {
|
|
206
|
+
// Answered, or put off: either way it stops covering the page.
|
|
207
|
+
if (open) setLater(`${later ?? ""} ${open.id}`);
|
|
208
|
+
setOpen(null);
|
|
209
|
+
}}
|
|
210
|
+
/>
|
|
193
211
|
</>
|
|
194
212
|
);
|
|
195
213
|
}
|
|
@@ -209,7 +227,7 @@ export function WaitingLinks() {
|
|
|
209
227
|
<ShieldAlert className="h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
210
228
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
211
229
|
<p className="truncate text-sm">
|
|
212
|
-
<span className="font-medium">{r.name}</span>
|
|
230
|
+
<span className="font-medium">{r.name}</span> {asking(r)}
|
|
213
231
|
</p>
|
|
214
232
|
<Origin request={r} />
|
|
215
233
|
</div>
|
|
@@ -218,7 +236,7 @@ export function WaitingLinks() {
|
|
|
218
236
|
className="min-h-11 sm:min-h-8"
|
|
219
237
|
onClick={() => setOpen(r)}
|
|
220
238
|
>
|
|
221
|
-
Type code
|
|
239
|
+
{r.state === "accepted" ? "Show code" : "Type code"}
|
|
222
240
|
</Button>
|
|
223
241
|
</div>
|
|
224
242
|
))}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowDownLeft, ArrowUpRight } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
} from "@/components/ui/dialog";
|
|
11
|
+
import { usePeerAudit } from "@/data/peers";
|
|
12
|
+
import type { AuditRow, Outcome } from "@/lib/peer/audit";
|
|
13
|
+
import { relativeTime } from "@/lib/schedules/when";
|
|
14
|
+
import { cn } from "@/lib/utils";
|
|
15
|
+
import { SHEET } from "./sheet";
|
|
16
|
+
|
|
17
|
+
const REFUSED: Record<Exclude<Outcome, "ok">, string> = {
|
|
18
|
+
denied_route: "route not open to it",
|
|
19
|
+
bad_envelope: "malformed",
|
|
20
|
+
too_large: "too large",
|
|
21
|
+
too_slow: "too slow",
|
|
22
|
+
busy: "busy",
|
|
23
|
+
bad_sig: "bad signature",
|
|
24
|
+
stale: "clocks differ",
|
|
25
|
+
replay: "replayed",
|
|
26
|
+
unknown_peer: "not linked",
|
|
27
|
+
suspended: "suspended",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function Row({ row }: { row: AuditRow }) {
|
|
31
|
+
const Arrow = row.direction === "in" ? ArrowDownLeft : ArrowUpRight;
|
|
32
|
+
const refused = row.outcome !== "ok";
|
|
33
|
+
return (
|
|
34
|
+
<li className="flex items-start gap-2 py-2">
|
|
35
|
+
<Arrow
|
|
36
|
+
className="text-muted-foreground mt-0.5 h-4 w-4 shrink-0"
|
|
37
|
+
aria-label={row.direction === "in" ? "Called here" : "Called there"}
|
|
38
|
+
/>
|
|
39
|
+
<div className="min-w-0 flex-1">
|
|
40
|
+
<p className="truncate font-mono text-xs">
|
|
41
|
+
{row.method} {row.route}
|
|
42
|
+
</p>
|
|
43
|
+
<p className="text-muted-foreground text-xs">
|
|
44
|
+
{relativeTime(row.at)}
|
|
45
|
+
{row.count > 1 && ` · ${row.count} calls this hour`}
|
|
46
|
+
{` · ${Math.round(row.ms)} ms`}
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
<span
|
|
50
|
+
className={cn(
|
|
51
|
+
"shrink-0 rounded-md px-1.5 py-0.5 text-xs tabular-nums",
|
|
52
|
+
refused
|
|
53
|
+
? "bg-destructive/10 text-destructive"
|
|
54
|
+
: row.status >= 400
|
|
55
|
+
? "bg-amber-500/10 text-amber-700 dark:text-amber-400"
|
|
56
|
+
: "bg-muted text-muted-foreground"
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
{refused ? REFUSED[row.outcome as Exclude<Outcome, "ok">] : row.status}
|
|
60
|
+
</span>
|
|
61
|
+
</li>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Machines → a linked machine → Activity: every call, both ways. */
|
|
66
|
+
export function PeerActivitySheet({
|
|
67
|
+
hostId,
|
|
68
|
+
name,
|
|
69
|
+
onClose,
|
|
70
|
+
}: {
|
|
71
|
+
hostId: string | null;
|
|
72
|
+
name: string;
|
|
73
|
+
onClose: () => void;
|
|
74
|
+
}) {
|
|
75
|
+
const { data: rows, isPending, error } = usePeerAudit(hostId);
|
|
76
|
+
return (
|
|
77
|
+
<Dialog open={!!hostId} onOpenChange={(o) => !o && onClose()}>
|
|
78
|
+
<DialogContent className={cn(SHEET, "max-h-[85vh] [&>*]:min-w-0")}>
|
|
79
|
+
<DialogHeader>
|
|
80
|
+
<DialogTitle>Activity with {name}</DialogTitle>
|
|
81
|
+
<DialogDescription>
|
|
82
|
+
Calls between this machine and {name} over the last 30 days. Reads
|
|
83
|
+
repeated within an hour are counted on one line.
|
|
84
|
+
</DialogDescription>
|
|
85
|
+
</DialogHeader>
|
|
86
|
+
<div className="-mx-1 max-h-[60vh] overflow-y-auto px-1">
|
|
87
|
+
{isPending && hostId ? (
|
|
88
|
+
<div className="space-y-2" aria-label="Loading">
|
|
89
|
+
{[0, 1, 2].map((i) => (
|
|
90
|
+
<div
|
|
91
|
+
key={i}
|
|
92
|
+
className="bg-muted/40 h-10 animate-pulse rounded-md"
|
|
93
|
+
/>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
) : error ? (
|
|
97
|
+
<p className="text-destructive text-sm">{error.message}</p>
|
|
98
|
+
) : !rows?.length ? (
|
|
99
|
+
<p className="text-muted-foreground text-sm">No calls yet.</p>
|
|
100
|
+
) : (
|
|
101
|
+
<ul className="space-y-0.5">
|
|
102
|
+
{rows.map((r, i) => (
|
|
103
|
+
<Row key={`${r.at}-${i}`} row={r} />
|
|
104
|
+
))}
|
|
105
|
+
</ul>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</DialogContent>
|
|
109
|
+
</Dialog>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AlertTriangle } from "lucide-react";
|
|
4
|
+
import type { AgentRoleView } from "@/lib/models/agent-roles-view";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { RoleModelSelect } from "./RoleModelSelect";
|
|
7
|
+
|
|
8
|
+
const SOURCE: Record<string, string> = {
|
|
9
|
+
task: "Set for the task",
|
|
10
|
+
project: "From this project",
|
|
11
|
+
workspace: "From the workspace",
|
|
12
|
+
settings: "Set in Settings",
|
|
13
|
+
env: "From the server's environment",
|
|
14
|
+
inherited: "Same as Gate review until set here",
|
|
15
|
+
default: "Default",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const initial = (label: string) => label.slice(0, 1);
|
|
19
|
+
|
|
20
|
+
// One role: what it does, the model it runs on, where that came from, and
|
|
21
|
+
// whether the model's server answers. A role whose local model is down says so.
|
|
22
|
+
export function AgentRoleCard({
|
|
23
|
+
role,
|
|
24
|
+
scope,
|
|
25
|
+
pending,
|
|
26
|
+
onChange,
|
|
27
|
+
onFallback,
|
|
28
|
+
}: {
|
|
29
|
+
role: AgentRoleView;
|
|
30
|
+
scope: "settings" | "workspace" | "project";
|
|
31
|
+
pending: boolean;
|
|
32
|
+
onChange: (ref: string | null) => void;
|
|
33
|
+
onFallback: (ref: string | null) => void;
|
|
34
|
+
}) {
|
|
35
|
+
const down = role.choice?.availability.state === "down";
|
|
36
|
+
const label = `Default (${role.default.split("/").slice(1).join("/")})`;
|
|
37
|
+
const inheritLabel =
|
|
38
|
+
scope === "settings"
|
|
39
|
+
? role.modelRole === "advise"
|
|
40
|
+
? "Same as Gate review"
|
|
41
|
+
: label
|
|
42
|
+
: "Inherit";
|
|
43
|
+
return (
|
|
44
|
+
<li
|
|
45
|
+
className={cn(
|
|
46
|
+
"bg-muted/40 space-y-2 rounded-xl px-3 py-3",
|
|
47
|
+
down && "ring-destructive/50 ring-1"
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<label htmlFor={`agent-role-${role.id}`} className="flex gap-2.5">
|
|
51
|
+
<span className="bg-accent text-primary grid h-8 w-8 shrink-0 place-items-center rounded-lg text-sm font-semibold">
|
|
52
|
+
{initial(role.label)}
|
|
53
|
+
</span>
|
|
54
|
+
<span className="min-w-0">
|
|
55
|
+
<span className="block text-sm font-medium">{role.label}</span>
|
|
56
|
+
<span className="text-muted-foreground block text-xs">
|
|
57
|
+
{role.purpose}
|
|
58
|
+
</span>
|
|
59
|
+
</span>
|
|
60
|
+
</label>
|
|
61
|
+
<RoleModelSelect
|
|
62
|
+
id={`agent-role-${role.id}`}
|
|
63
|
+
value={role.value}
|
|
64
|
+
inheritLabel={
|
|
65
|
+
role.value === null && role.binding
|
|
66
|
+
? `${inheritLabel} · ${role.binding.ref.split("/").slice(1).join("/")}`
|
|
67
|
+
: inheritLabel
|
|
68
|
+
}
|
|
69
|
+
disabled={pending}
|
|
70
|
+
onChange={onChange}
|
|
71
|
+
/>
|
|
72
|
+
<p className="text-muted-foreground text-xs">
|
|
73
|
+
{role.error
|
|
74
|
+
? role.error
|
|
75
|
+
: role.binding
|
|
76
|
+
? `${SOURCE[role.binding.source] ?? role.binding.source}${role.choice?.endpoint ? ` · ${role.choice.endpoint}` : ""}`
|
|
77
|
+
: ""}
|
|
78
|
+
</p>
|
|
79
|
+
{down && (
|
|
80
|
+
<div
|
|
81
|
+
role="alert"
|
|
82
|
+
className="bg-destructive/10 text-foreground ring-destructive/35 flex gap-2 rounded-lg p-2.5 text-xs ring-1"
|
|
83
|
+
>
|
|
84
|
+
<AlertTriangle className="text-destructive mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
85
|
+
<span>
|
|
86
|
+
<b className="font-medium">
|
|
87
|
+
This model's server isn't answering.
|
|
88
|
+
</b>{" "}
|
|
89
|
+
{role.choice?.availability.error
|
|
90
|
+
? `${role.choice.availability.error}. `
|
|
91
|
+
: ""}
|
|
92
|
+
New {role.label} sessions can't start until it is back
|
|
93
|
+
{role.fallback ? ", or you start on the fallback" : ""}.
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
{scope === "settings" && (
|
|
98
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
99
|
+
<span className="text-muted-foreground text-xs">Fallback</span>
|
|
100
|
+
<div className="min-w-44 flex-1">
|
|
101
|
+
<RoleModelSelect
|
|
102
|
+
id={`agent-role-fallback-${role.id}`}
|
|
103
|
+
value={role.fallback}
|
|
104
|
+
inheritLabel="None (don't fall back)"
|
|
105
|
+
claudeOnly
|
|
106
|
+
disabled={pending}
|
|
107
|
+
onChange={onFallback}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</li>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useAgentRolesQuery, useSaveAgentRoles } from "@/data/models";
|
|
5
|
+
import { useProjectsQuery } from "@/data/projects/queries";
|
|
6
|
+
import { useWorkspacesQuery } from "@/data/workspaces";
|
|
7
|
+
import type { RoleMap } from "@/lib/models/config";
|
|
8
|
+
import type { RoleScope } from "@/lib/models/agent-roles-view";
|
|
9
|
+
import { cn } from "@/lib/utils";
|
|
10
|
+
import { AgentRoleCard } from "./AgentRoleCard";
|
|
11
|
+
|
|
12
|
+
const SCOPES: { id: RoleScope; label: string }[] = [
|
|
13
|
+
{ id: "settings", label: "Default" },
|
|
14
|
+
{ id: "workspace", label: "Workspace" },
|
|
15
|
+
{ id: "project", label: "Project" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// Settings > Agent roles: the model each of Developer, Analyst, Reviewer and
|
|
19
|
+
// Supervisor runs on, for everything or for one workspace or project.
|
|
20
|
+
export function AgentRolesSection() {
|
|
21
|
+
const [scope, setScope] = useState<RoleScope>("settings");
|
|
22
|
+
const [target, setTarget] = useState<string | null>(null);
|
|
23
|
+
const workspaces = useWorkspacesQuery();
|
|
24
|
+
const projects = useProjectsQuery();
|
|
25
|
+
const options =
|
|
26
|
+
scope === "workspace"
|
|
27
|
+
? (workspaces.data ?? []).map((w) => ({ id: w.id, name: w.name }))
|
|
28
|
+
: scope === "project"
|
|
29
|
+
? (projects.data ?? [])
|
|
30
|
+
.filter((p) => !p.is_uncategorized)
|
|
31
|
+
.map((p) => ({ id: p.id, name: p.name }))
|
|
32
|
+
: [];
|
|
33
|
+
const id = scope === "settings" ? null : (target ?? options[0]?.id ?? null);
|
|
34
|
+
const { data, isPending, isError, error } = useAgentRolesQuery(scope, id);
|
|
35
|
+
const save = useSaveAgentRoles(scope, id);
|
|
36
|
+
|
|
37
|
+
const saved = (key: "value" | "fallback"): RoleMap =>
|
|
38
|
+
Object.fromEntries(
|
|
39
|
+
(data?.roles ?? [])
|
|
40
|
+
.filter((r) => r[key])
|
|
41
|
+
.map((r) => [r.modelRole, r[key] as string])
|
|
42
|
+
);
|
|
43
|
+
const setRole = (modelRole: string, ref: string | null) => {
|
|
44
|
+
const roles: RoleMap = { ...saved("value") };
|
|
45
|
+
if (ref) roles[modelRole as keyof RoleMap] = ref;
|
|
46
|
+
else delete roles[modelRole as keyof RoleMap];
|
|
47
|
+
save.mutate({ roles });
|
|
48
|
+
};
|
|
49
|
+
const setFallback = (modelRole: string, ref: string | null) => {
|
|
50
|
+
const fallbacks: RoleMap = { ...saved("fallback") };
|
|
51
|
+
if (ref) fallbacks[modelRole as keyof RoleMap] = ref;
|
|
52
|
+
else delete fallbacks[modelRole as keyof RoleMap];
|
|
53
|
+
save.mutate({ roles: saved("value"), fallbacks });
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="space-y-4">
|
|
58
|
+
<div
|
|
59
|
+
role="tablist"
|
|
60
|
+
className="bg-muted inline-flex max-w-full gap-0.5 overflow-x-auto rounded-xl p-0.5"
|
|
61
|
+
>
|
|
62
|
+
{SCOPES.map((s) => (
|
|
63
|
+
<button
|
|
64
|
+
key={s.id}
|
|
65
|
+
role="tab"
|
|
66
|
+
aria-selected={scope === s.id}
|
|
67
|
+
type="button"
|
|
68
|
+
onClick={() => {
|
|
69
|
+
setScope(s.id);
|
|
70
|
+
setTarget(null);
|
|
71
|
+
}}
|
|
72
|
+
className={cn(
|
|
73
|
+
"h-11 rounded-lg px-3 text-sm whitespace-nowrap md:h-8",
|
|
74
|
+
scope === s.id
|
|
75
|
+
? "bg-background text-foreground ring-border ring-1"
|
|
76
|
+
: "text-muted-foreground hover:text-foreground"
|
|
77
|
+
)}
|
|
78
|
+
>
|
|
79
|
+
{s.label}
|
|
80
|
+
</button>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
{scope !== "settings" && (
|
|
84
|
+
<select
|
|
85
|
+
aria-label={scope === "workspace" ? "Workspace" : "Project"}
|
|
86
|
+
value={id ?? ""}
|
|
87
|
+
onChange={(e) => setTarget(e.target.value)}
|
|
88
|
+
className="border-input bg-background h-11 w-full rounded-md border px-3 text-sm md:h-9"
|
|
89
|
+
>
|
|
90
|
+
{options.length === 0 && <option value="">None yet</option>}
|
|
91
|
+
{options.map((o) => (
|
|
92
|
+
<option key={o.id} value={o.id}>
|
|
93
|
+
{o.name}
|
|
94
|
+
</option>
|
|
95
|
+
))}
|
|
96
|
+
</select>
|
|
97
|
+
)}
|
|
98
|
+
<p className="text-muted-foreground text-xs">
|
|
99
|
+
The model each role runs on. A workspace or project can override these,
|
|
100
|
+
and a session can when it starts. A Pi model (a server you run) runs
|
|
101
|
+
sessions started in the role; tasks and the merge gate stay on Claude
|
|
102
|
+
Code models. Planning, gate review, scope check, helpers and watchers
|
|
103
|
+
are in Models.
|
|
104
|
+
</p>
|
|
105
|
+
{scope !== "settings" && !id ? (
|
|
106
|
+
<p className="text-muted-foreground text-sm">
|
|
107
|
+
There is no {scope} to set roles for yet.
|
|
108
|
+
</p>
|
|
109
|
+
) : isPending ? (
|
|
110
|
+
<div className="bg-muted/40 h-64 animate-pulse rounded-lg" />
|
|
111
|
+
) : isError ? (
|
|
112
|
+
<p className="text-destructive text-sm">{error.message}</p>
|
|
113
|
+
) : data ? (
|
|
114
|
+
<ul className="space-y-2.5">
|
|
115
|
+
{data.roles.map((r) => (
|
|
116
|
+
<AgentRoleCard
|
|
117
|
+
key={r.id}
|
|
118
|
+
role={r}
|
|
119
|
+
scope={scope}
|
|
120
|
+
pending={save.isPending}
|
|
121
|
+
onChange={(ref) => setRole(r.modelRole, ref)}
|
|
122
|
+
onFallback={(ref) => setFallback(r.modelRole, ref)}
|
|
123
|
+
/>
|
|
124
|
+
))}
|
|
125
|
+
</ul>
|
|
126
|
+
) : null}
|
|
127
|
+
{save.isError && (
|
|
128
|
+
<p className="text-destructive text-xs">{save.error.message}</p>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|