@saadnvd1/agent-os 0.3.715 → 0.3.728
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/spawn/route.ts +22 -1
- package/app/api/loops/route.ts +17 -0
- package/app/api/models/availability/route.ts +9 -0
- package/app/api/models/choices/route.ts +9 -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]/model/route.ts +49 -0
- package/app/api/sessions/route.ts +16 -0
- package/app/api/tasks/[id]/loop/cycles/[n]/log/route.ts +29 -0
- package/app/api/tasks/[id]/loop/draft-criteria/route.ts +17 -0
- package/app/api/tasks/[id]/loop/next/route.ts +25 -0
- package/app/api/tasks/[id]/loop/route.ts +72 -0
- package/app/api/tasks/[id]/loop/run/route.ts +12 -0
- package/app/api/tasks/[id]/loop/stop/route.ts +12 -0
- package/app/api/tasks/[id]/mode/route.ts +29 -0
- package/app/api/tasks/[id]/report/draft/route.ts +54 -0
- package/app/api/tasks/[id]/report/evidence/[evId]/route.ts +18 -0
- package/app/api/tasks/[id]/report/route.ts +39 -0
- package/app/api/workspaces/[id]/mode/route.ts +23 -0
- package/app/api/workspaces/[id]/reports/route.ts +17 -0
- package/app/page.tsx +13 -0
- package/components/Analyst/PairChip.tsx +131 -0
- package/components/Analyst/PairSwitch.tsx +76 -0
- package/components/Analyst/ReviewCard.tsx +272 -0
- package/components/Analyst/SignalChips.tsx +39 -0
- package/components/Analyst/StartAnalystSheet.tsx +109 -0
- package/components/Analyst/cardState.test.ts +109 -0
- package/components/Analyst/cardState.ts +95 -0
- package/components/Analyst/pair.ts +81 -0
- package/components/Chat/Code.tsx +3 -0
- package/components/Chat/Messages.tsx +110 -11
- package/components/Chat/itemContext.ts +14 -0
- package/components/Draft/DraftChips.tsx +16 -1
- package/components/Draft/DraftPanel.tsx +58 -2
- 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/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 +8 -0
- package/components/Pane/MobileTabBar.tsx +29 -0
- package/components/Pane/MobileViewPicker.tsx +4 -0
- package/components/Pane/PaneMenu.test.ts +21 -0
- package/components/Pane/PaneMenu.tsx +26 -0
- package/components/Pane/PaneViewToggle.tsx +12 -0
- package/components/Pane/index.tsx +19 -0
- package/components/Report/DraftCard.tsx +89 -0
- package/components/Report/EstimateBar.tsx +51 -0
- package/components/Report/EvidenceSheet.tsx +98 -0
- package/components/Report/ModeChooser.tsx +43 -0
- package/components/Report/ReportView.tsx +289 -0
- package/components/SessionList/SessionRow.tsx +18 -7
- package/components/SessionList/SessionRowMenu.tsx +12 -0
- package/components/Sessions/RolePill.tsx +203 -0
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Share/ShareDialog.tsx +13 -0
- package/components/Tasks/Loop/LoopButton.tsx +74 -0
- package/components/Tasks/Loop/LoopSetup.tsx +146 -0
- package/components/Tasks/Loop/LoopView.tsx +350 -0
- package/components/Tasks/Loop/labels.ts +17 -0
- package/components/Tasks/TaskRow.tsx +2 -0
- package/components/Workspaces/WorkspaceModeForm.tsx +22 -0
- package/components/Workspaces/WorkspaceSettings.tsx +2 -0
- package/components/ui/segmented.tsx +55 -0
- package/components/ui/status-pill.tsx +3 -0
- package/contexts/PaneContext.tsx +21 -1
- 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/exports/index.ts +2 -0
- package/data/exports/keys.ts +3 -0
- package/data/exports/queries.ts +51 -0
- package/data/loops/index.ts +3 -0
- package/data/loops/keys.ts +4 -0
- package/data/loops/mutations.ts +68 -0
- package/data/loops/queries.ts +40 -0
- package/data/models/index.ts +98 -0
- package/data/push/topics.ts +11 -0
- package/data/reports/index.ts +115 -0
- package/data/sessions/queries.ts +26 -0
- package/lib/agents/brief.ts +1 -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 +5 -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 +3 -1
- package/lib/db/changes.ts +13 -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/migrations/20261021T1600_task_loops.ts +81 -0
- package/lib/db/migrations/20261021T1800_health_reports.ts +38 -0
- package/lib/db/session-parts/columns.ts +2 -0
- package/lib/db/types.ts +3 -0
- package/lib/done/archive.ts +2 -0
- package/lib/done/index.ts +2 -0
- package/lib/drafts.ts +6 -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/loops/analyse.ts +46 -0
- package/lib/loops/analyst-prompt.ts +203 -0
- package/lib/loops/checkCriteria.ts +114 -0
- package/lib/loops/compactEvidence.ts +140 -0
- package/lib/loops/controls.ts +317 -0
- package/lib/loops/decideStop.ts +113 -0
- package/lib/loops/deps.ts +128 -0
- package/lib/loops/diffMap.ts +39 -0
- package/lib/loops/draftCriteria.ts +71 -0
- package/lib/loops/gatherEvidence.ts +265 -0
- package/lib/loops/hooks.ts +40 -0
- package/lib/loops/index.ts +7 -0
- package/lib/loops/route.ts +83 -0
- package/lib/loops/runCycle.ts +323 -0
- package/lib/loops/screenInstruction.ts +59 -0
- package/lib/loops/sendInstruction.ts +83 -0
- package/lib/loops/snapshot.ts +38 -0
- package/lib/loops/stopLoop.ts +86 -0
- package/lib/loops/store.ts +481 -0
- package/lib/loops/summary.ts +27 -0
- package/lib/loops/tickLoops.ts +141 -0
- package/lib/loops/types.ts +223 -0
- package/lib/loops/world.test-helpers.ts +223 -0
- 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/plan-step.ts +4 -1
- 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/orchestrator/digest.ts +2 -1
- package/lib/orchestrator/facts.ts +7 -2
- package/lib/orchestrator/loop-guard.ts +5 -2
- package/lib/orchestrator/pr-wake.ts +4 -0
- package/lib/orchestrator/read.ts +5 -1
- package/lib/orchestrator/untrusted.ts +5 -0
- package/lib/orchestrator/watcher.ts +8 -1
- package/lib/panes.ts +52 -0
- package/lib/plans/acceptance.ts +23 -0
- package/lib/projects.ts +1 -0
- package/lib/reports/drafts.ts +87 -0
- package/lib/reports/end-hook.ts +17 -0
- package/lib/reports/estimate.ts +107 -0
- package/lib/reports/evidence/build.ts +193 -0
- package/lib/reports/evidence/gather.ts +229 -0
- package/lib/reports/fixtures.ts +17 -0
- package/lib/reports/generate.ts +159 -0
- package/lib/reports/hooks.ts +30 -0
- package/lib/reports/mode.ts +65 -0
- package/lib/reports/model.ts +115 -0
- package/lib/reports/read.ts +26 -0
- package/lib/reports/store.ts +180 -0
- package/lib/reports/template.ts +117 -0
- package/lib/reports/types.ts +135 -0
- package/lib/reports/validate.ts +142 -0
- package/lib/session-artifacts/agentos-files.ts +94 -0
- package/lib/session-artifacts/collect.ts +17 -0
- package/lib/sessions/crew.ts +72 -0
- package/lib/sessions/launch.ts +49 -9
- 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/row-menu.ts +15 -1
- package/lib/tasks/contract/routes.ts +5 -0
- package/lib/tasks/contract/types.ts +3 -0
- package/lib/tasks/finish.ts +2 -0
- package/lib/tasks/index.ts +6 -0
- package/lib/tasks/move-carry.ts +1 -0
- 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/package.json +2 -2
- package/server.ts +9 -1
- package/stores/analystUi.ts +19 -0
- package/stores/exportUi.ts +87 -0
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
KanbanSquare,
|
|
16
16
|
Link,
|
|
17
17
|
Share2,
|
|
18
|
+
Download,
|
|
18
19
|
Loader2,
|
|
19
20
|
Pencil,
|
|
20
21
|
Pin,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
RotateCcw,
|
|
27
28
|
Sparkles,
|
|
28
29
|
Trash2,
|
|
30
|
+
Eye,
|
|
29
31
|
} from "lucide-react";
|
|
30
32
|
import type { Session } from "@/lib/db";
|
|
31
33
|
import { selectionActions } from "@/stores/sessionSelection";
|
|
@@ -49,6 +51,8 @@ import type { SidebarRow } from "@/lib/sidebar/shelves";
|
|
|
49
51
|
import { useRowContext } from "./RowContext";
|
|
50
52
|
import { MoveMenuItems } from "@/components/Tasks/MoveMenuItems";
|
|
51
53
|
import { shareUiActions } from "@/stores/shareUi";
|
|
54
|
+
import { exportUiActions } from "@/stores/exportUi";
|
|
55
|
+
import { analystUiActions } from "@/stores/analystUi";
|
|
52
56
|
import { copySessionLink } from "@/hooks/useCopyToClipboard";
|
|
53
57
|
import { useLinkedHostNames } from "@/data/hosts";
|
|
54
58
|
import { remoteBlocks, remoteNote } from "@/lib/hosts/remote-menu";
|
|
@@ -196,6 +200,14 @@ export function SessionRowMenu({
|
|
|
196
200
|
title: session.name,
|
|
197
201
|
})
|
|
198
202
|
);
|
|
203
|
+
case "export":
|
|
204
|
+
return item("Export for AI…", Download, () =>
|
|
205
|
+
exportUiActions.open(session.id, session.name)
|
|
206
|
+
);
|
|
207
|
+
case "add-analyst":
|
|
208
|
+
return item("Add analyst…", Eye, () =>
|
|
209
|
+
analystUiActions.open(session.id, session.name)
|
|
210
|
+
);
|
|
199
211
|
case "fork":
|
|
200
212
|
return item("Fork session", Copy, () => ctx.onFork(session.id));
|
|
201
213
|
case "fresh-start":
|
|
@@ -0,0 +1,203 @@
|
|
|
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 { analystUiActions } from "@/stores/analystUi";
|
|
22
|
+
import type { Session } from "@/lib/db";
|
|
23
|
+
import {
|
|
24
|
+
AGENT_ROLES,
|
|
25
|
+
AGENT_ROLE_DEFS,
|
|
26
|
+
isAgentRole,
|
|
27
|
+
} from "@/lib/models/agent-roles";
|
|
28
|
+
import { cn } from "@/lib/utils";
|
|
29
|
+
import { uuid } from "@/lib/uuid";
|
|
30
|
+
|
|
31
|
+
const modelName = (ref: string | null) =>
|
|
32
|
+
ref ? ref.split("/").slice(1).join("/") : "model";
|
|
33
|
+
|
|
34
|
+
// The role a running session was started in and the model it runs on, with
|
|
35
|
+
// its crew: the sessions it works with, by @name. Nothing for a session
|
|
36
|
+
// started without a role. `compact` is a phone's small button.
|
|
37
|
+
export function RolePill({
|
|
38
|
+
session,
|
|
39
|
+
paneId,
|
|
40
|
+
compact,
|
|
41
|
+
}: {
|
|
42
|
+
session: Session | null | undefined;
|
|
43
|
+
paneId?: string;
|
|
44
|
+
compact?: boolean;
|
|
45
|
+
}) {
|
|
46
|
+
const [open, setOpen] = useState(false);
|
|
47
|
+
const role = isAgentRole(session?.agent_role) ? session.agent_role : null;
|
|
48
|
+
const crew = useCrewQuery(session?.id, !!role);
|
|
49
|
+
const switchModel = useSwitchSessionModel(session?.id ?? "");
|
|
50
|
+
const launch = useLaunchSession();
|
|
51
|
+
const { attachSession } = usePanes();
|
|
52
|
+
if (!session || !role) return null;
|
|
53
|
+
const me = crew.data?.members.find((m) => m.id === session.id);
|
|
54
|
+
const down = me?.availability === "down";
|
|
55
|
+
const label = AGENT_ROLE_DEFS[role].label;
|
|
56
|
+
const others = crew.data?.members.filter((m) => m.id !== session.id) ?? [];
|
|
57
|
+
|
|
58
|
+
const add = async (next: (typeof AGENT_ROLES)[number]) => {
|
|
59
|
+
// A chat's analyst is paired with it, to review each turn.
|
|
60
|
+
if (next === "analyst" && session.view === "chat") {
|
|
61
|
+
setOpen(false);
|
|
62
|
+
analystUiActions.open(session.id, session.name, paneId ?? null);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const { session: made } = await launch.mutateAsync({
|
|
67
|
+
id: uuid(),
|
|
68
|
+
projectId: session.project_id,
|
|
69
|
+
agentType: "claude",
|
|
70
|
+
model: "",
|
|
71
|
+
access: "full",
|
|
72
|
+
useWorktree: false,
|
|
73
|
+
baseBranch: null,
|
|
74
|
+
prompt: "",
|
|
75
|
+
agentRole: next,
|
|
76
|
+
crewId: session.crew_id ?? session.id,
|
|
77
|
+
});
|
|
78
|
+
if (paneId) attachSession(paneId, made.id, made.tmux_name ?? "");
|
|
79
|
+
setOpen(false);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
toast.error(error instanceof Error ? error.message : String(error));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
onClick={(e) => {
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
setOpen(true);
|
|
92
|
+
}}
|
|
93
|
+
aria-label={`${label}, ${modelName(me?.ref ?? null)}${down ? ", model not answering" : ""}`}
|
|
94
|
+
className={cn(
|
|
95
|
+
"ring-border/70 flex shrink-0 items-center gap-1.5 rounded-full text-xs ring-1 ring-inset",
|
|
96
|
+
down
|
|
97
|
+
? "bg-amber-500/10 text-amber-600 dark:text-amber-400"
|
|
98
|
+
: "bg-muted/60",
|
|
99
|
+
compact
|
|
100
|
+
? "-my-1.5 h-11 w-11 justify-center rounded-lg ring-0"
|
|
101
|
+
: "h-7 max-w-64 px-2.5"
|
|
102
|
+
)}
|
|
103
|
+
>
|
|
104
|
+
<span className="bg-accent text-primary grid h-4 w-4 shrink-0 place-items-center rounded text-[10px] font-semibold">
|
|
105
|
+
{label[0]}
|
|
106
|
+
</span>
|
|
107
|
+
{!compact && (
|
|
108
|
+
<>
|
|
109
|
+
<span className="truncate">
|
|
110
|
+
{label}
|
|
111
|
+
{me?.ref ? ` · ${modelName(me.ref)}` : ""}
|
|
112
|
+
</span>
|
|
113
|
+
{me && !down && <LocalityBadge locality={me.locality} />}
|
|
114
|
+
{down && <span className="shrink-0">unreachable</span>}
|
|
115
|
+
</>
|
|
116
|
+
)}
|
|
117
|
+
{compact && down && (
|
|
118
|
+
<span className="bg-destructive absolute h-2 w-2 translate-x-3 -translate-y-3 rounded-full" />
|
|
119
|
+
)}
|
|
120
|
+
</button>
|
|
121
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
122
|
+
<DialogContent>
|
|
123
|
+
<DialogHeader>
|
|
124
|
+
<DialogTitle>{label} · role and crew</DialogTitle>
|
|
125
|
+
<DialogDescription>
|
|
126
|
+
{AGENT_ROLE_DEFS[role].purpose}
|
|
127
|
+
</DialogDescription>
|
|
128
|
+
</DialogHeader>
|
|
129
|
+
<div className="bg-muted/40 space-y-2 rounded-xl p-3">
|
|
130
|
+
<div className="flex items-center justify-between gap-2">
|
|
131
|
+
<span className="flex min-w-0 items-center gap-2 text-sm font-medium">
|
|
132
|
+
<AvailabilityDot
|
|
133
|
+
availability={{ state: me?.availability ?? "unknown" }}
|
|
134
|
+
/>
|
|
135
|
+
<span className="truncate">{modelName(me?.ref ?? null)}</span>
|
|
136
|
+
</span>
|
|
137
|
+
{me && <LocalityBadge locality={me.locality} />}
|
|
138
|
+
</div>
|
|
139
|
+
{down && (
|
|
140
|
+
<p role="alert" className="text-destructive text-xs">
|
|
141
|
+
This model's server isn't answering. Your conversation
|
|
142
|
+
is kept: switch to another model to carry on.
|
|
143
|
+
</p>
|
|
144
|
+
)}
|
|
145
|
+
<RoleModelSelect
|
|
146
|
+
value={me?.ref ?? null}
|
|
147
|
+
inheritLabel="Switch model…"
|
|
148
|
+
disabled={switchModel.isPending}
|
|
149
|
+
onChange={(ref) =>
|
|
150
|
+
ref &&
|
|
151
|
+
switchModel.mutate(ref, {
|
|
152
|
+
onError: (e) => toast.error(e.message),
|
|
153
|
+
onSuccess: () => toast.success("Switched model"),
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
<div className="space-y-1.5">
|
|
159
|
+
<h3 className="text-muted-foreground text-xs font-medium uppercase">
|
|
160
|
+
Crew
|
|
161
|
+
</h3>
|
|
162
|
+
{others.length === 0 && (
|
|
163
|
+
<p className="text-muted-foreground text-sm">
|
|
164
|
+
Nobody else yet. Add a role to work with this session.
|
|
165
|
+
</p>
|
|
166
|
+
)}
|
|
167
|
+
{others.map((m) => (
|
|
168
|
+
<div key={m.id} className="flex items-center gap-2 text-sm">
|
|
169
|
+
<span className="min-w-0 flex-1">
|
|
170
|
+
<span className="block truncate">{m.name}</span>
|
|
171
|
+
<span className="text-muted-foreground block truncate font-mono text-xs">
|
|
172
|
+
{m.agentRole ? `@${m.agentRole}` : "no role"} ·{" "}
|
|
173
|
+
{modelName(m.ref)}
|
|
174
|
+
</span>
|
|
175
|
+
</span>
|
|
176
|
+
<AvailabilityDot availability={{ state: m.availability }} />
|
|
177
|
+
<LocalityBadge locality={m.locality} />
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
<div className="flex flex-wrap gap-2">
|
|
182
|
+
{AGENT_ROLES.filter(
|
|
183
|
+
(r) =>
|
|
184
|
+
r !== role &&
|
|
185
|
+
r !== "supervisor" &&
|
|
186
|
+
!others.some((m) => m.agentRole === r)
|
|
187
|
+
).map((r) => (
|
|
188
|
+
<Button
|
|
189
|
+
key={r}
|
|
190
|
+
variant="outline"
|
|
191
|
+
className="h-11 md:h-9"
|
|
192
|
+
disabled={launch.isPending}
|
|
193
|
+
onClick={() => void add(r)}
|
|
194
|
+
>
|
|
195
|
+
Add {AGENT_ROLE_DEFS[r].label}
|
|
196
|
+
</Button>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
</DialogContent>
|
|
200
|
+
</Dialog>
|
|
201
|
+
</>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -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,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Repeat } from "lucide-react";
|
|
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 { StatusPill } from "@/components/ui/status-pill";
|
|
14
|
+
import { useLoopSummaries, useTaskLoop } from "@/data/loops";
|
|
15
|
+
import { LoopSetup } from "./LoopSetup";
|
|
16
|
+
import { LoopView } from "./LoopView";
|
|
17
|
+
|
|
18
|
+
function LoopBody({ taskId }: { taskId: string }) {
|
|
19
|
+
const q = useTaskLoop(taskId);
|
|
20
|
+
const [setup, setSetup] = useState(false);
|
|
21
|
+
if (q.isPending)
|
|
22
|
+
return <p className="text-muted-foreground text-sm">Loading…</p>;
|
|
23
|
+
if (q.isError)
|
|
24
|
+
return <p className="text-sm text-red-500">{q.error.message}</p>;
|
|
25
|
+
const view = q.data;
|
|
26
|
+
const live = view?.loop.state === "running";
|
|
27
|
+
if (!view || setup) {
|
|
28
|
+
return <LoopSetup taskId={taskId} onStarted={() => setSetup(false)} />;
|
|
29
|
+
}
|
|
30
|
+
return (
|
|
31
|
+
<div className="space-y-4">
|
|
32
|
+
<LoopView taskId={taskId} view={view} />
|
|
33
|
+
{!live && (
|
|
34
|
+
<Button
|
|
35
|
+
className="h-11 w-full md:h-9"
|
|
36
|
+
variant="outline"
|
|
37
|
+
onClick={() => setSetup(true)}
|
|
38
|
+
>
|
|
39
|
+
Start a new loop
|
|
40
|
+
</Button>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The pill and button on a task row: the analyst loop for this task.
|
|
47
|
+
export function LoopButton({ taskId, name }: { taskId: string; name: string }) {
|
|
48
|
+
const [open, setOpen] = useState(false);
|
|
49
|
+
const summary = useLoopSummaries().data?.[taskId];
|
|
50
|
+
const live = summary?.state === "running";
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
{live && summary && (
|
|
54
|
+
<StatusPill tone="amber">
|
|
55
|
+
Loop {summary.cycle}/{summary.maxCycles} · {summary.met}/
|
|
56
|
+
{summary.total}
|
|
57
|
+
</StatusPill>
|
|
58
|
+
)}
|
|
59
|
+
<Button size="sm" variant="outline" onClick={() => setOpen(true)}>
|
|
60
|
+
<Repeat className="h-3.5 w-3.5" />
|
|
61
|
+
Loop
|
|
62
|
+
</Button>
|
|
63
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
64
|
+
<DialogContent className="max-h-[90dvh] overflow-y-auto sm:max-w-xl">
|
|
65
|
+
<DialogHeader>
|
|
66
|
+
<DialogTitle>Analyst loop</DialogTitle>
|
|
67
|
+
<DialogDescription className="truncate">{name}</DialogDescription>
|
|
68
|
+
</DialogHeader>
|
|
69
|
+
{open && <LoopBody taskId={taskId} />}
|
|
70
|
+
</DialogContent>
|
|
71
|
+
</Dialog>
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Plus, Sparkles, X } from "lucide-react";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Notice } from "@/components/ui/notice";
|
|
7
|
+
import { useDraftCriteria, useStartLoop } from "@/data/loops";
|
|
8
|
+
import { DEFAULT_LIMITS, LIMIT_BOUNDS, type Limits } from "@/lib/loops/types";
|
|
9
|
+
|
|
10
|
+
const FIELDS: {
|
|
11
|
+
key: "maxCycles" | "maxCostUsd" | "maxMinutes";
|
|
12
|
+
label: string;
|
|
13
|
+
unit: string;
|
|
14
|
+
}[] = [
|
|
15
|
+
{ key: "maxCycles", label: "Cycles", unit: "" },
|
|
16
|
+
{ key: "maxCostUsd", label: "Cost cap", unit: "$" },
|
|
17
|
+
{ key: "maxMinutes", label: "Time", unit: "min" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const input =
|
|
21
|
+
"bg-foreground/[0.05] focus-visible:ring-primary/40 min-h-11 w-full rounded-lg px-3 text-sm outline-none focus-visible:ring-[3px] md:min-h-9";
|
|
22
|
+
|
|
23
|
+
export function LoopSetup({
|
|
24
|
+
taskId,
|
|
25
|
+
onStarted,
|
|
26
|
+
}: {
|
|
27
|
+
taskId: string;
|
|
28
|
+
onStarted: () => void;
|
|
29
|
+
}) {
|
|
30
|
+
const draft = useDraftCriteria(taskId);
|
|
31
|
+
const start = useStartLoop(taskId);
|
|
32
|
+
const [criteria, setCriteria] = useState<string[]>([""]);
|
|
33
|
+
const [limits, setLimits] = useState<
|
|
34
|
+
Pick<Limits, "maxCycles" | "maxCostUsd" | "maxMinutes">
|
|
35
|
+
>({
|
|
36
|
+
maxCycles: DEFAULT_LIMITS.maxCycles,
|
|
37
|
+
maxCostUsd: DEFAULT_LIMITS.maxCostUsd,
|
|
38
|
+
maxMinutes: DEFAULT_LIMITS.maxMinutes,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
draft.mutate(undefined, {
|
|
43
|
+
onSuccess: (found) =>
|
|
44
|
+
found.length && setCriteria(found.map((c) => c.text)),
|
|
45
|
+
});
|
|
46
|
+
// Once, when the setup opens.
|
|
47
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const filled = criteria.map((c) => c.trim()).filter(Boolean);
|
|
51
|
+
const set = (i: number, text: string) =>
|
|
52
|
+
setCriteria((all) => all.map((c, j) => (j === i ? text : c)));
|
|
53
|
+
const error =
|
|
54
|
+
start.error?.message ??
|
|
55
|
+
(draft.isError ? "Couldn't draft the checklist" : null);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="space-y-4">
|
|
59
|
+
<p className="text-muted-foreground text-sm">
|
|
60
|
+
After each turn the analyst reads the real diff and test output, marks
|
|
61
|
+
this checklist with evidence, and sends the coding agent its next
|
|
62
|
+
instruction. It never merges or publishes.
|
|
63
|
+
</p>
|
|
64
|
+
<div className="space-y-2">
|
|
65
|
+
<div className="flex items-center justify-between">
|
|
66
|
+
<h3 className="text-sm font-medium">Acceptance criteria</h3>
|
|
67
|
+
{draft.isPending && (
|
|
68
|
+
<span className="text-muted-foreground text-xs">Drafting…</span>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
{criteria.map((text, i) => (
|
|
72
|
+
<div key={i} className="flex items-center gap-2">
|
|
73
|
+
<input
|
|
74
|
+
value={text}
|
|
75
|
+
onChange={(e) => set(i, e.target.value)}
|
|
76
|
+
placeholder="Something the analyst can check"
|
|
77
|
+
aria-label={`Criterion ${i + 1}`}
|
|
78
|
+
className={input}
|
|
79
|
+
/>
|
|
80
|
+
<Button
|
|
81
|
+
size="icon"
|
|
82
|
+
variant="ghost"
|
|
83
|
+
className="size-11 md:size-9"
|
|
84
|
+
aria-label="Remove criterion"
|
|
85
|
+
onClick={() =>
|
|
86
|
+
setCriteria((all) =>
|
|
87
|
+
all.length > 1 ? all.filter((_, j) => j !== i) : [""]
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
>
|
|
91
|
+
<X />
|
|
92
|
+
</Button>
|
|
93
|
+
</div>
|
|
94
|
+
))}
|
|
95
|
+
<Button
|
|
96
|
+
size="sm"
|
|
97
|
+
variant="ghost"
|
|
98
|
+
className="h-11 md:h-8"
|
|
99
|
+
onClick={() => setCriteria((all) => [...all, ""])}
|
|
100
|
+
>
|
|
101
|
+
<Plus /> Add criterion
|
|
102
|
+
</Button>
|
|
103
|
+
</div>
|
|
104
|
+
<div className="grid grid-cols-3 gap-2">
|
|
105
|
+
{FIELDS.map((f) => (
|
|
106
|
+
<label key={f.key} className="space-y-1 text-xs">
|
|
107
|
+
<span className="text-muted-foreground">
|
|
108
|
+
{f.label}
|
|
109
|
+
{f.unit && ` (${f.unit})`}
|
|
110
|
+
</span>
|
|
111
|
+
<input
|
|
112
|
+
type="number"
|
|
113
|
+
inputMode="numeric"
|
|
114
|
+
min={LIMIT_BOUNDS[f.key][0]}
|
|
115
|
+
max={LIMIT_BOUNDS[f.key][1]}
|
|
116
|
+
value={limits[f.key]}
|
|
117
|
+
onChange={(e) =>
|
|
118
|
+
setLimits((l) => ({ ...l, [f.key]: Number(e.target.value) }))
|
|
119
|
+
}
|
|
120
|
+
className={input}
|
|
121
|
+
/>
|
|
122
|
+
</label>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
{error && <Notice tone="warning">{error}</Notice>}
|
|
126
|
+
<Button
|
|
127
|
+
className="h-11 w-full md:h-9"
|
|
128
|
+
disabled={!filled.length || start.isPending}
|
|
129
|
+
onClick={() =>
|
|
130
|
+
start.mutate(
|
|
131
|
+
{
|
|
132
|
+
criteria: filled.map((text) => ({
|
|
133
|
+
text,
|
|
134
|
+
source: "user" as const,
|
|
135
|
+
})),
|
|
136
|
+
limits,
|
|
137
|
+
},
|
|
138
|
+
{ onSuccess: onStarted }
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
>
|
|
142
|
+
<Sparkles /> {start.isPending ? "Starting…" : "Start loop"}
|
|
143
|
+
</Button>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|