@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
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Check,
|
|
6
|
+
CircleDashed,
|
|
7
|
+
CircleX,
|
|
8
|
+
Pause,
|
|
9
|
+
Play,
|
|
10
|
+
Send,
|
|
11
|
+
SkipForward,
|
|
12
|
+
Square,
|
|
13
|
+
} from "lucide-react";
|
|
14
|
+
import { Button } from "@/components/ui/button";
|
|
15
|
+
import { Notice } from "@/components/ui/notice";
|
|
16
|
+
import { StatusPill } from "@/components/ui/status-pill";
|
|
17
|
+
import {
|
|
18
|
+
useEditLoop,
|
|
19
|
+
useNextInstruction,
|
|
20
|
+
useRunLoop,
|
|
21
|
+
useStopLoop,
|
|
22
|
+
} from "@/data/loops";
|
|
23
|
+
import type { Criterion, CycleRow, LoopView as View } from "@/lib/loops/types";
|
|
24
|
+
import { STOP_LABEL, minutesText } from "./labels";
|
|
25
|
+
|
|
26
|
+
const CYCLE_LABEL: Record<CycleRow["state"], string> = {
|
|
27
|
+
claimed: "Gathering evidence",
|
|
28
|
+
decided: "Analyst decided",
|
|
29
|
+
counting: "Sending soon",
|
|
30
|
+
sending: "Sending",
|
|
31
|
+
sent: "Sent",
|
|
32
|
+
skipped: "Nothing to send",
|
|
33
|
+
stopped: "Stopped",
|
|
34
|
+
superseded: "You took over",
|
|
35
|
+
error: "Error",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function Stat({ label, value }: { label: string; value: string }) {
|
|
39
|
+
return (
|
|
40
|
+
<div className="bg-foreground/[0.04] min-w-0 rounded-lg px-3 py-2">
|
|
41
|
+
<p className="text-muted-foreground text-[11px]">{label}</p>
|
|
42
|
+
<p className="truncate text-sm font-semibold tabular-nums">{value}</p>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function CriterionRow({
|
|
48
|
+
c,
|
|
49
|
+
canEdit,
|
|
50
|
+
onWaive,
|
|
51
|
+
}: {
|
|
52
|
+
c: Criterion;
|
|
53
|
+
canEdit: boolean;
|
|
54
|
+
onWaive: () => void;
|
|
55
|
+
}) {
|
|
56
|
+
const Icon =
|
|
57
|
+
c.status === "met"
|
|
58
|
+
? Check
|
|
59
|
+
: c.status === "failing"
|
|
60
|
+
? CircleX
|
|
61
|
+
: CircleDashed;
|
|
62
|
+
return (
|
|
63
|
+
<li className="flex items-start gap-2 text-sm">
|
|
64
|
+
<Icon
|
|
65
|
+
className={`mt-0.5 h-4 w-4 shrink-0 ${
|
|
66
|
+
c.status === "met"
|
|
67
|
+
? "text-emerald-500"
|
|
68
|
+
: c.status === "failing"
|
|
69
|
+
? "text-red-500"
|
|
70
|
+
: "text-muted-foreground"
|
|
71
|
+
}`}
|
|
72
|
+
/>
|
|
73
|
+
<div className="min-w-0 flex-1">
|
|
74
|
+
<p
|
|
75
|
+
className={
|
|
76
|
+
c.status === "waived" ? "text-muted-foreground line-through" : ""
|
|
77
|
+
}
|
|
78
|
+
>
|
|
79
|
+
{c.text}
|
|
80
|
+
</p>
|
|
81
|
+
{c.evidence && (
|
|
82
|
+
<p className="text-muted-foreground font-mono text-[11px] break-words">
|
|
83
|
+
{c.evidence.kind}: {c.evidence.ref}
|
|
84
|
+
{c.evidence.note && ` · ${c.evidence.note}`}
|
|
85
|
+
{c.status === "met" && !c.verified && " · unverified"}
|
|
86
|
+
</p>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
{canEdit && c.status !== "met" && c.status !== "waived" && (
|
|
90
|
+
<Button
|
|
91
|
+
size="sm"
|
|
92
|
+
variant="ghost"
|
|
93
|
+
className="h-11 md:h-8"
|
|
94
|
+
onClick={onWaive}
|
|
95
|
+
>
|
|
96
|
+
Waive
|
|
97
|
+
</Button>
|
|
98
|
+
)}
|
|
99
|
+
</li>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function NextBar({ taskId, cycle }: { taskId: string; cycle: CycleRow }) {
|
|
104
|
+
const next = useNextInstruction(taskId);
|
|
105
|
+
const [now, setNow] = useState(() => Date.now());
|
|
106
|
+
const [text, setText] = useState(cycle.instruction ?? "");
|
|
107
|
+
const [editing, setEditing] = useState(false);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const t = setInterval(() => setNow(Date.now()), 1000);
|
|
110
|
+
return () => clearInterval(t);
|
|
111
|
+
}, []);
|
|
112
|
+
const left = cycle.sendAt
|
|
113
|
+
? Math.max(0, Math.ceil((cycle.sendAt - now) / 1000))
|
|
114
|
+
: null;
|
|
115
|
+
return (
|
|
116
|
+
<div className="bg-primary/10 space-y-2 rounded-xl p-3">
|
|
117
|
+
<div className="flex items-center justify-between gap-2">
|
|
118
|
+
<h3 className="text-sm font-medium">Next instruction</h3>
|
|
119
|
+
<span className="text-muted-foreground text-xs tabular-nums">
|
|
120
|
+
{left === null ? "held" : `sends in ${left}s`}
|
|
121
|
+
</span>
|
|
122
|
+
</div>
|
|
123
|
+
{editing ? (
|
|
124
|
+
<textarea
|
|
125
|
+
value={text}
|
|
126
|
+
onChange={(e) => setText(e.target.value)}
|
|
127
|
+
rows={5}
|
|
128
|
+
aria-label="Next instruction"
|
|
129
|
+
className="bg-background/60 w-full rounded-lg p-2 text-sm outline-none"
|
|
130
|
+
/>
|
|
131
|
+
) : (
|
|
132
|
+
<p className="text-sm whitespace-pre-wrap">{cycle.instruction}</p>
|
|
133
|
+
)}
|
|
134
|
+
{next.error && (
|
|
135
|
+
<p className="text-xs text-red-500">{next.error.message}</p>
|
|
136
|
+
)}
|
|
137
|
+
<div className="flex flex-wrap gap-2">
|
|
138
|
+
<Button
|
|
139
|
+
size="sm"
|
|
140
|
+
className="h-11 md:h-8"
|
|
141
|
+
disabled={next.isPending}
|
|
142
|
+
onClick={() =>
|
|
143
|
+
next.mutate({
|
|
144
|
+
action: "send",
|
|
145
|
+
instruction: editing ? text : undefined,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
>
|
|
149
|
+
<Send /> Send now
|
|
150
|
+
</Button>
|
|
151
|
+
<Button
|
|
152
|
+
size="sm"
|
|
153
|
+
variant="outline"
|
|
154
|
+
className="h-11 md:h-8"
|
|
155
|
+
onClick={() => setEditing((e) => !e)}
|
|
156
|
+
>
|
|
157
|
+
{editing ? "Done editing" : "Edit"}
|
|
158
|
+
</Button>
|
|
159
|
+
<Button
|
|
160
|
+
size="sm"
|
|
161
|
+
variant="outline"
|
|
162
|
+
className="h-11 md:h-8"
|
|
163
|
+
onClick={() => next.mutate({ action: "hold" })}
|
|
164
|
+
>
|
|
165
|
+
<Pause /> Hold
|
|
166
|
+
</Button>
|
|
167
|
+
<Button
|
|
168
|
+
size="sm"
|
|
169
|
+
variant="outline"
|
|
170
|
+
className="h-11 md:h-8"
|
|
171
|
+
onClick={() => next.mutate({ action: "skip" })}
|
|
172
|
+
>
|
|
173
|
+
<SkipForward /> Skip
|
|
174
|
+
</Button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function Cycle({ c }: { c: CycleRow }) {
|
|
181
|
+
const ev = c.evidence;
|
|
182
|
+
return (
|
|
183
|
+
<li className="bg-foreground/[0.03] space-y-1.5 rounded-xl p-3">
|
|
184
|
+
<div className="flex items-center justify-between gap-2">
|
|
185
|
+
<h4 className="text-sm font-medium">Cycle {c.n}</h4>
|
|
186
|
+
<StatusPill tone={c.state === "error" ? "amber" : "muted"}>
|
|
187
|
+
{CYCLE_LABEL[c.state]}
|
|
188
|
+
</StatusPill>
|
|
189
|
+
</div>
|
|
190
|
+
{ev && (
|
|
191
|
+
<div className="flex flex-wrap gap-1.5">
|
|
192
|
+
{ev.commands.map((cmd) => (
|
|
193
|
+
<StatusPill
|
|
194
|
+
key={cmd.name}
|
|
195
|
+
tone={cmd.exitCode === 0 ? "muted" : "amber"}
|
|
196
|
+
>
|
|
197
|
+
{cmd.name}{" "}
|
|
198
|
+
{cmd.skipped
|
|
199
|
+
? "skipped"
|
|
200
|
+
: cmd.exitCode === 0
|
|
201
|
+
? "passed"
|
|
202
|
+
: `${cmd.failed.length || cmd.errors.length} failing`}
|
|
203
|
+
</StatusPill>
|
|
204
|
+
))}
|
|
205
|
+
{!c.changed && <StatusPill tone="muted">no change</StatusPill>}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
{c.analysis?.summary && <p className="text-sm">{c.analysis.summary}</p>}
|
|
209
|
+
{c.analysis?.findings.map((f, i) => (
|
|
210
|
+
<p key={i} className="text-muted-foreground text-xs break-words">
|
|
211
|
+
{f.severity === "blocking" ? "Blocking" : "Minor"}: {f.text}
|
|
212
|
+
{f.file && (
|
|
213
|
+
<span className="font-mono">
|
|
214
|
+
{" "}
|
|
215
|
+
({f.file}
|
|
216
|
+
{f.line ? `:${f.line}` : ""})
|
|
217
|
+
</span>
|
|
218
|
+
)}
|
|
219
|
+
</p>
|
|
220
|
+
))}
|
|
221
|
+
{c.instructionSent && (
|
|
222
|
+
<p className="text-xs break-words">
|
|
223
|
+
<span className="text-muted-foreground">
|
|
224
|
+
{c.sender === "user" ? "You sent: " : "Sent: "}
|
|
225
|
+
</span>
|
|
226
|
+
{c.instructionSent}
|
|
227
|
+
</p>
|
|
228
|
+
)}
|
|
229
|
+
{!c.instructionSent && c.instruction && c.state !== "counting" && (
|
|
230
|
+
<p className="text-muted-foreground text-xs break-words">
|
|
231
|
+
Not sent: {c.instruction}
|
|
232
|
+
</p>
|
|
233
|
+
)}
|
|
234
|
+
{c.error && <p className="text-xs break-words text-red-500">{c.error}</p>}
|
|
235
|
+
</li>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function LoopView({ taskId, view }: { taskId: string; view: View }) {
|
|
240
|
+
const stop = useStopLoop(taskId);
|
|
241
|
+
const run = useRunLoop(taskId);
|
|
242
|
+
const edit = useEditLoop(taskId);
|
|
243
|
+
const { loop, criteria, cycles } = view;
|
|
244
|
+
const live = loop.state === "running";
|
|
245
|
+
const met = criteria.filter(
|
|
246
|
+
(c) => c.status === "met" || c.status === "waived"
|
|
247
|
+
).length;
|
|
248
|
+
const counting = cycles.find((c) => c.state === "counting" && c.instruction);
|
|
249
|
+
const limitStop = loop.stopReason?.startsWith("limit:");
|
|
250
|
+
const error =
|
|
251
|
+
stop.error?.message ?? run.error?.message ?? edit.error?.message;
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<div className="space-y-4">
|
|
255
|
+
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
|
256
|
+
<Stat label="Criteria" value={`${met}/${criteria.length}`} />
|
|
257
|
+
<Stat label="Cycles" value={`${cycles[0]?.n ?? 0}/${loop.maxCycles}`} />
|
|
258
|
+
<Stat
|
|
259
|
+
label="Spend"
|
|
260
|
+
value={`$${view.spentUsd.toFixed(2)} / $${loop.maxCostUsd}`}
|
|
261
|
+
/>
|
|
262
|
+
<Stat label="Elapsed" value={minutesText(view.elapsedMs)} />
|
|
263
|
+
</div>
|
|
264
|
+
{!live && loop.stopReason && (
|
|
265
|
+
<Notice
|
|
266
|
+
tone={
|
|
267
|
+
loop.stopReason === "done" || loop.stopReason === "stopped"
|
|
268
|
+
? "info"
|
|
269
|
+
: "warning"
|
|
270
|
+
}
|
|
271
|
+
action={
|
|
272
|
+
limitStop ? (
|
|
273
|
+
<Button
|
|
274
|
+
size="sm"
|
|
275
|
+
className="h-11 md:h-8"
|
|
276
|
+
disabled={edit.isPending}
|
|
277
|
+
onClick={() =>
|
|
278
|
+
edit.mutate({
|
|
279
|
+
extend: { cycles: 4, costUsd: 5, minutes: 60 },
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
>
|
|
283
|
+
Continue
|
|
284
|
+
</Button>
|
|
285
|
+
) : undefined
|
|
286
|
+
}
|
|
287
|
+
>
|
|
288
|
+
<p className="font-medium">{STOP_LABEL[loop.stopReason]}</p>
|
|
289
|
+
{loop.stopDetail && loop.stopReason !== "stopped" && (
|
|
290
|
+
<p className="text-muted-foreground">{loop.stopDetail}</p>
|
|
291
|
+
)}
|
|
292
|
+
</Notice>
|
|
293
|
+
)}
|
|
294
|
+
{live && view.holdReason && <Notice>{view.holdReason}</Notice>}
|
|
295
|
+
{live && counting && (
|
|
296
|
+
<NextBar key={counting.id} taskId={taskId} cycle={counting} />
|
|
297
|
+
)}
|
|
298
|
+
{error && <Notice tone="warning">{error}</Notice>}
|
|
299
|
+
<section className="space-y-2">
|
|
300
|
+
<h3 className="text-sm font-medium">Acceptance checklist</h3>
|
|
301
|
+
<ul className="space-y-2">
|
|
302
|
+
{criteria.map((c) => (
|
|
303
|
+
<CriterionRow
|
|
304
|
+
key={c.id}
|
|
305
|
+
c={c}
|
|
306
|
+
canEdit={live}
|
|
307
|
+
onWaive={() => edit.mutate({ waive: [c.id] })}
|
|
308
|
+
/>
|
|
309
|
+
))}
|
|
310
|
+
</ul>
|
|
311
|
+
</section>
|
|
312
|
+
{live && (
|
|
313
|
+
<div className="flex flex-wrap gap-2">
|
|
314
|
+
<Button
|
|
315
|
+
size="sm"
|
|
316
|
+
variant="outline"
|
|
317
|
+
className="h-11 md:h-8"
|
|
318
|
+
disabled={run.isPending}
|
|
319
|
+
onClick={() => run.mutate()}
|
|
320
|
+
>
|
|
321
|
+
<Play /> Run a cycle now
|
|
322
|
+
</Button>
|
|
323
|
+
<Button
|
|
324
|
+
size="sm"
|
|
325
|
+
variant="outline"
|
|
326
|
+
className="h-11 md:h-8"
|
|
327
|
+
disabled={stop.isPending}
|
|
328
|
+
onClick={() => stop.mutate()}
|
|
329
|
+
>
|
|
330
|
+
<Square /> Stop loop
|
|
331
|
+
</Button>
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
334
|
+
<section className="space-y-2">
|
|
335
|
+
<h3 className="text-sm font-medium">Cycles</h3>
|
|
336
|
+
{cycles.length === 0 ? (
|
|
337
|
+
<p className="text-muted-foreground text-sm">
|
|
338
|
+
The first cycle starts when the coding agent finishes a turn.
|
|
339
|
+
</p>
|
|
340
|
+
) : (
|
|
341
|
+
<ul className="space-y-2">
|
|
342
|
+
{cycles.map((c) => (
|
|
343
|
+
<Cycle key={c.id} c={c} />
|
|
344
|
+
))}
|
|
345
|
+
</ul>
|
|
346
|
+
)}
|
|
347
|
+
</section>
|
|
348
|
+
</div>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StopReason } from "@/lib/loops/types";
|
|
2
|
+
|
|
3
|
+
export const STOP_LABEL: Record<StopReason, string> = {
|
|
4
|
+
stopped: "Stopped by you",
|
|
5
|
+
blocked: "Needs you",
|
|
6
|
+
done: "All criteria met",
|
|
7
|
+
repeat: "The same failure kept coming back",
|
|
8
|
+
no_progress: "No progress",
|
|
9
|
+
"limit:cycles": "Reached its cycle limit",
|
|
10
|
+
"limit:cost": "Reached its cost cap",
|
|
11
|
+
"limit:time": "Reached its time limit",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const minutesText = (ms: number) => {
|
|
15
|
+
const m = Math.max(0, Math.round(ms / 60000));
|
|
16
|
+
return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
17
|
+
};
|
|
@@ -21,6 +21,7 @@ import { cn } from "@/lib/utils";
|
|
|
21
21
|
import { SimulatorIndicator } from "@/components/Simulator";
|
|
22
22
|
import { PlanReview } from "./PlanReview";
|
|
23
23
|
import { MoveTaskButton } from "./MoveTaskButton";
|
|
24
|
+
import { LoopButton } from "./Loop/LoopButton";
|
|
24
25
|
import { ResumeOnModel } from "./ResumeOnModel";
|
|
25
26
|
import { TASK_STATE as STATE } from "./task-state-labels";
|
|
26
27
|
import { formatTokens } from "@/components/Usage/format";
|
|
@@ -119,6 +120,7 @@ export function TaskRow({ task }: { task: TaskView }) {
|
|
|
119
120
|
)}
|
|
120
121
|
{!task.hostId && <SimulatorIndicator sessionId={task.id} />}
|
|
121
122
|
<MoveTaskButton task={task} onError={setMoveError} />
|
|
123
|
+
{!task.hostId && <LoopButton taskId={task.id} name={task.name} />}
|
|
122
124
|
{!task.hostId && <ResumeOnModel task={task} />}
|
|
123
125
|
<span className="flex-1" />
|
|
124
126
|
{confirmDrop ? (
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ModeChooser } from "@/components/Report/ModeChooser";
|
|
4
|
+
import { useSetWorkspaceMode, useWorkspaceMode } from "@/data/reports";
|
|
5
|
+
|
|
6
|
+
// A workspace's default operating mode; a task can override it.
|
|
7
|
+
export function WorkspaceModeForm({ workspaceId }: { workspaceId: string }) {
|
|
8
|
+
const { data: mode } = useWorkspaceMode(workspaceId);
|
|
9
|
+
const set = useSetWorkspaceMode(workspaceId);
|
|
10
|
+
if (!mode) return null;
|
|
11
|
+
return (
|
|
12
|
+
<div className="space-y-1.5">
|
|
13
|
+
<h4 className="text-sm font-medium">Operating mode</h4>
|
|
14
|
+
<ModeChooser
|
|
15
|
+
mode={mode}
|
|
16
|
+
onChange={(m) => set.mutate(m)}
|
|
17
|
+
disabled={set.isPending}
|
|
18
|
+
note="New tasks start in this mode; any task can change its own."
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -9,6 +9,7 @@ import { McpServersForm } from "./McpServersForm";
|
|
|
9
9
|
import { HqFolderForm } from "./HqFolderForm";
|
|
10
10
|
import { ProjectTiersForm } from "./ProjectTiersForm";
|
|
11
11
|
import { StallForm } from "./StallForm";
|
|
12
|
+
import { WorkspaceModeForm } from "./WorkspaceModeForm";
|
|
12
13
|
import { WorkspaceModelsForm } from "./WorkspaceModelsForm";
|
|
13
14
|
import { AwayWindowForm } from "@/components/Away/AwayWindowForm";
|
|
14
15
|
|
|
@@ -46,6 +47,7 @@ export function WorkspaceSettings() {
|
|
|
46
47
|
key={`${w.id}:${w.max_running_tasks ?? "off"}`}
|
|
47
48
|
workspace={w}
|
|
48
49
|
/>
|
|
50
|
+
<WorkspaceModeForm workspaceId={w.id} />
|
|
49
51
|
<RulesForm workspaceId={w.id} />
|
|
50
52
|
<WorkspaceModelsForm workspaceId={w.id} />
|
|
51
53
|
<McpServersForm workspace={w} />
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export interface SegmentedOption<T extends string> {
|
|
6
|
+
value: T;
|
|
7
|
+
label: string;
|
|
8
|
+
hint?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** One choice out of a few, side by side. Radio semantics, 44px targets. */
|
|
12
|
+
export function Segmented<T extends string>({
|
|
13
|
+
value,
|
|
14
|
+
options,
|
|
15
|
+
onChange,
|
|
16
|
+
label,
|
|
17
|
+
disabled,
|
|
18
|
+
}: {
|
|
19
|
+
value: T;
|
|
20
|
+
options: SegmentedOption<T>[];
|
|
21
|
+
onChange: (v: T) => void;
|
|
22
|
+
label: string;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
}) {
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
role="radiogroup"
|
|
28
|
+
aria-label={label}
|
|
29
|
+
className="bg-muted/60 grid auto-cols-fr grid-flow-col gap-1 rounded-lg p-1"
|
|
30
|
+
>
|
|
31
|
+
{options.map((o) => {
|
|
32
|
+
const on = o.value === value;
|
|
33
|
+
return (
|
|
34
|
+
<button
|
|
35
|
+
key={o.value}
|
|
36
|
+
type="button"
|
|
37
|
+
role="radio"
|
|
38
|
+
aria-checked={on}
|
|
39
|
+
title={o.hint}
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
onClick={() => !on && onChange(o.value)}
|
|
42
|
+
className={cn(
|
|
43
|
+
"min-h-11 rounded-md px-2 text-sm font-medium transition-colors disabled:opacity-50",
|
|
44
|
+
on
|
|
45
|
+
? "bg-card text-foreground shadow-sm"
|
|
46
|
+
: "text-muted-foreground hover:text-foreground"
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
{o.label}
|
|
50
|
+
</button>
|
|
51
|
+
);
|
|
52
|
+
})}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -4,6 +4,9 @@ import { cn } from "@/lib/utils";
|
|
|
4
4
|
const TONES = {
|
|
5
5
|
amber: "bg-amber-500/15 text-amber-700 dark:text-amber-400",
|
|
6
6
|
muted: "bg-muted text-muted-foreground",
|
|
7
|
+
green: "bg-green-500/15 text-green-700 dark:text-green-400",
|
|
8
|
+
primary: "bg-primary/15 text-primary",
|
|
9
|
+
red: "bg-red-500/15 text-red-700 dark:text-red-400",
|
|
7
10
|
} as const;
|
|
8
11
|
|
|
9
12
|
/** A small coloured pill for a status. A button when it has an onClick. */
|
package/contexts/PaneContext.tsx
CHANGED
|
@@ -23,11 +23,13 @@ import {
|
|
|
23
23
|
loadPaneState,
|
|
24
24
|
showDraft,
|
|
25
25
|
MAX_PANES,
|
|
26
|
+
openBeside as openBesideIn,
|
|
27
|
+
type OpenSession,
|
|
26
28
|
} from "@/lib/panes";
|
|
27
29
|
import { useViewport } from "@/hooks/useViewport";
|
|
28
30
|
|
|
29
31
|
export type ViewMode =
|
|
30
|
-
"terminal" | "files" | "git" | "workers" | "log" | "artifacts";
|
|
32
|
+
"terminal" | "files" | "git" | "workers" | "log" | "artifacts" | "report";
|
|
31
33
|
|
|
32
34
|
export interface PaneViewState {
|
|
33
35
|
viewMode: ViewMode;
|
|
@@ -59,6 +61,8 @@ interface PaneContextValue {
|
|
|
59
61
|
hostId?: string
|
|
60
62
|
) => void;
|
|
61
63
|
detachSession: (paneId: string) => void;
|
|
64
|
+
// Shows a session beside a pane: its pane if open, else a split or a tab.
|
|
65
|
+
openBeside: (paneId: string, open: OpenSession) => void;
|
|
62
66
|
openDraft: (paneId: string, draftId: string) => void;
|
|
63
67
|
getPaneData: (paneId: string) => PaneData;
|
|
64
68
|
getActiveTab: (paneId: string) => TabData | null;
|
|
@@ -270,6 +274,21 @@ export function PaneProvider({ children }: { children: ReactNode }) {
|
|
|
270
274
|
[]
|
|
271
275
|
);
|
|
272
276
|
|
|
277
|
+
// A phone shows one pane: the session takes this one's place.
|
|
278
|
+
const openBeside = useCallback(
|
|
279
|
+
(paneId: string, open: OpenSession) => {
|
|
280
|
+
if (isMobile)
|
|
281
|
+
attachSession(
|
|
282
|
+
paneId,
|
|
283
|
+
open.sessionId,
|
|
284
|
+
open.tmux,
|
|
285
|
+
open.hostId ?? undefined
|
|
286
|
+
);
|
|
287
|
+
else setState((prev) => openBesideIn(prev, paneId, open));
|
|
288
|
+
},
|
|
289
|
+
[isMobile, attachSession]
|
|
290
|
+
);
|
|
291
|
+
|
|
273
292
|
// Detach session from active tab
|
|
274
293
|
const detachSession = useCallback((paneId: string) => {
|
|
275
294
|
setState((prev) => {
|
|
@@ -393,6 +412,7 @@ export function PaneProvider({ children }: { children: ReactNode }) {
|
|
|
393
412
|
switchTab,
|
|
394
413
|
attachSession,
|
|
395
414
|
detachSession,
|
|
415
|
+
openBeside,
|
|
396
416
|
openDraft,
|
|
397
417
|
getPaneData,
|
|
398
418
|
getActiveTab,
|
|
@@ -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
|
+
}
|