@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.
Files changed (242) hide show
  1. package/README.md +2 -0
  2. package/app/api/agent-roles/route.test.ts +67 -0
  3. package/app/api/agent-roles/route.ts +61 -0
  4. package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
  5. package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
  6. package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
  7. package/app/api/analyst/[id]/review/route.ts +22 -0
  8. package/app/api/analyst/[id]/route.ts +44 -0
  9. package/app/api/analyst/route.ts +10 -0
  10. package/app/api/bus/spawn/route.ts +22 -1
  11. package/app/api/loops/route.ts +17 -0
  12. package/app/api/models/availability/route.ts +9 -0
  13. package/app/api/models/choices/route.ts +9 -0
  14. package/app/api/sessions/[id]/analyst/route.ts +11 -0
  15. package/app/api/sessions/[id]/crew/route.ts +38 -0
  16. package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
  17. package/app/api/sessions/[id]/export/route.test.ts +125 -0
  18. package/app/api/sessions/[id]/export/route.ts +34 -0
  19. package/app/api/sessions/[id]/model/route.ts +49 -0
  20. package/app/api/sessions/route.ts +16 -0
  21. package/app/api/tasks/[id]/loop/cycles/[n]/log/route.ts +29 -0
  22. package/app/api/tasks/[id]/loop/draft-criteria/route.ts +17 -0
  23. package/app/api/tasks/[id]/loop/next/route.ts +25 -0
  24. package/app/api/tasks/[id]/loop/route.ts +72 -0
  25. package/app/api/tasks/[id]/loop/run/route.ts +12 -0
  26. package/app/api/tasks/[id]/loop/stop/route.ts +12 -0
  27. package/app/api/tasks/[id]/mode/route.ts +29 -0
  28. package/app/api/tasks/[id]/report/draft/route.ts +54 -0
  29. package/app/api/tasks/[id]/report/evidence/[evId]/route.ts +18 -0
  30. package/app/api/tasks/[id]/report/route.ts +39 -0
  31. package/app/api/workspaces/[id]/mode/route.ts +23 -0
  32. package/app/api/workspaces/[id]/reports/route.ts +17 -0
  33. package/app/page.tsx +13 -0
  34. package/components/Analyst/PairChip.tsx +131 -0
  35. package/components/Analyst/PairSwitch.tsx +76 -0
  36. package/components/Analyst/ReviewCard.tsx +272 -0
  37. package/components/Analyst/SignalChips.tsx +39 -0
  38. package/components/Analyst/StartAnalystSheet.tsx +109 -0
  39. package/components/Analyst/cardState.test.ts +109 -0
  40. package/components/Analyst/cardState.ts +95 -0
  41. package/components/Analyst/pair.ts +81 -0
  42. package/components/Chat/Code.tsx +3 -0
  43. package/components/Chat/Messages.tsx +110 -11
  44. package/components/Chat/itemContext.ts +14 -0
  45. package/components/Draft/DraftChips.tsx +16 -1
  46. package/components/Draft/DraftPanel.tsx +58 -2
  47. package/components/Draft/ModelUnavailableDialog.tsx +78 -0
  48. package/components/Draft/RoleChips.tsx +110 -0
  49. package/components/Export/ExportActions.tsx +136 -0
  50. package/components/Export/ExportSheet.tsx +217 -0
  51. package/components/Export/RangePicker.tsx +144 -0
  52. package/components/Export/Seg.tsx +38 -0
  53. package/components/Export/TurnSelectBar.tsx +45 -0
  54. package/components/Models/AgentRoleCard.tsx +114 -0
  55. package/components/Models/AgentRolesSection.tsx +132 -0
  56. package/components/Models/ModelBadges.tsx +48 -0
  57. package/components/Models/ModelsSection.tsx +4 -2
  58. package/components/Models/RoleModelSelect.tsx +87 -0
  59. package/components/Pane/DesktopTabBar.tsx +8 -0
  60. package/components/Pane/MobileTabBar.tsx +29 -0
  61. package/components/Pane/MobileViewPicker.tsx +4 -0
  62. package/components/Pane/PaneMenu.test.ts +21 -0
  63. package/components/Pane/PaneMenu.tsx +26 -0
  64. package/components/Pane/PaneViewToggle.tsx +12 -0
  65. package/components/Pane/index.tsx +19 -0
  66. package/components/Report/DraftCard.tsx +89 -0
  67. package/components/Report/EstimateBar.tsx +51 -0
  68. package/components/Report/EvidenceSheet.tsx +98 -0
  69. package/components/Report/ModeChooser.tsx +43 -0
  70. package/components/Report/ReportView.tsx +289 -0
  71. package/components/SessionList/SessionRow.tsx +18 -7
  72. package/components/SessionList/SessionRowMenu.tsx +12 -0
  73. package/components/Sessions/RolePill.tsx +203 -0
  74. package/components/Settings/SettingsDialog.tsx +9 -0
  75. package/components/Share/ShareDialog.tsx +13 -0
  76. package/components/Tasks/Loop/LoopButton.tsx +74 -0
  77. package/components/Tasks/Loop/LoopSetup.tsx +146 -0
  78. package/components/Tasks/Loop/LoopView.tsx +350 -0
  79. package/components/Tasks/Loop/labels.ts +17 -0
  80. package/components/Tasks/TaskRow.tsx +2 -0
  81. package/components/Workspaces/WorkspaceModeForm.tsx +22 -0
  82. package/components/Workspaces/WorkspaceSettings.tsx +2 -0
  83. package/components/ui/segmented.tsx +55 -0
  84. package/components/ui/status-pill.tsx +3 -0
  85. package/contexts/PaneContext.tsx +21 -1
  86. package/data/analyst/index.ts +3 -0
  87. package/data/analyst/keys.ts +6 -0
  88. package/data/analyst/mutations.ts +116 -0
  89. package/data/analyst/queries.ts +35 -0
  90. package/data/exports/index.ts +2 -0
  91. package/data/exports/keys.ts +3 -0
  92. package/data/exports/queries.ts +51 -0
  93. package/data/loops/index.ts +3 -0
  94. package/data/loops/keys.ts +4 -0
  95. package/data/loops/mutations.ts +68 -0
  96. package/data/loops/queries.ts +40 -0
  97. package/data/models/index.ts +98 -0
  98. package/data/push/topics.ts +11 -0
  99. package/data/reports/index.ts +115 -0
  100. package/data/sessions/queries.ts +26 -0
  101. package/lib/agents/brief.ts +1 -0
  102. package/lib/analyst/__fixtures__/items.ts +57 -0
  103. package/lib/analyst/__fixtures__/mocks.ts +49 -0
  104. package/lib/analyst/brief.ts +17 -0
  105. package/lib/analyst/buildDigest.ts +204 -0
  106. package/lib/analyst/createLink.ts +98 -0
  107. package/lib/analyst/endLink.ts +31 -0
  108. package/lib/analyst/forward.ts +109 -0
  109. package/lib/analyst/index.ts +16 -0
  110. package/lib/analyst/linkView.ts +38 -0
  111. package/lib/analyst/onTurnEnd.ts +119 -0
  112. package/lib/analyst/parseReview.ts +41 -0
  113. package/lib/analyst/pushDigest.ts +143 -0
  114. package/lib/analyst/resume.ts +35 -0
  115. package/lib/analyst/route.ts +50 -0
  116. package/lib/analyst/signals.ts +121 -0
  117. package/lib/analyst/store.ts +303 -0
  118. package/lib/analyst/types.ts +140 -0
  119. package/lib/bus/condense.ts +5 -1
  120. package/lib/bus/index.ts +5 -0
  121. package/lib/bus/read-scope.ts +12 -0
  122. package/lib/bus/role-alias.ts +70 -0
  123. package/lib/bus/targets.ts +3 -0
  124. package/lib/chat/drivers/opencode-mapper.ts +55 -1
  125. package/lib/chat/drivers/pi-approvals.ts +7 -4
  126. package/lib/chat/drivers/pi-mapper.ts +38 -0
  127. package/lib/chat/drivers/pi-text-tools.ts +104 -0
  128. package/lib/chat/drivers/pi.ts +8 -3
  129. package/lib/chat/drivers/turn-items.ts +36 -11
  130. package/lib/chat/drivers/turn-outcome.ts +108 -0
  131. package/lib/chat/events.ts +8 -0
  132. package/lib/chat/registry.ts +13 -0
  133. package/lib/chat/store.ts +19 -0
  134. package/lib/chat/turns.ts +1 -0
  135. package/lib/chat/worker/extras.ts +4 -2
  136. package/lib/claims/collectLocal.ts +3 -1
  137. package/lib/db/changes.ts +13 -0
  138. package/lib/db/migrations/20261021T1400_session_agent_roles.ts +20 -0
  139. package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
  140. package/lib/db/migrations/20261021T1600_task_loops.ts +81 -0
  141. package/lib/db/migrations/20261021T1800_health_reports.ts +38 -0
  142. package/lib/db/session-parts/columns.ts +2 -0
  143. package/lib/db/types.ts +3 -0
  144. package/lib/done/archive.ts +2 -0
  145. package/lib/done/index.ts +2 -0
  146. package/lib/drafts.ts +6 -0
  147. package/lib/export/build.ts +470 -0
  148. package/lib/export/compact.ts +156 -0
  149. package/lib/export/estimate.ts +83 -0
  150. package/lib/export/fixtures.ts +45 -0
  151. package/lib/export/html.tsx +195 -0
  152. package/lib/export/json.ts +39 -0
  153. package/lib/export/load.ts +64 -0
  154. package/lib/export/markdown.ts +127 -0
  155. package/lib/export/request.ts +36 -0
  156. package/lib/export/service.ts +142 -0
  157. package/lib/export/summary.ts +86 -0
  158. package/lib/export/turns.ts +63 -0
  159. package/lib/export/view.ts +173 -0
  160. package/lib/loops/analyse.ts +46 -0
  161. package/lib/loops/analyst-prompt.ts +203 -0
  162. package/lib/loops/checkCriteria.ts +114 -0
  163. package/lib/loops/compactEvidence.ts +140 -0
  164. package/lib/loops/controls.ts +317 -0
  165. package/lib/loops/decideStop.ts +113 -0
  166. package/lib/loops/deps.ts +128 -0
  167. package/lib/loops/diffMap.ts +39 -0
  168. package/lib/loops/draftCriteria.ts +71 -0
  169. package/lib/loops/gatherEvidence.ts +265 -0
  170. package/lib/loops/hooks.ts +40 -0
  171. package/lib/loops/index.ts +7 -0
  172. package/lib/loops/route.ts +83 -0
  173. package/lib/loops/runCycle.ts +323 -0
  174. package/lib/loops/screenInstruction.ts +59 -0
  175. package/lib/loops/sendInstruction.ts +83 -0
  176. package/lib/loops/snapshot.ts +38 -0
  177. package/lib/loops/stopLoop.ts +86 -0
  178. package/lib/loops/store.ts +481 -0
  179. package/lib/loops/summary.ts +27 -0
  180. package/lib/loops/tickLoops.ts +141 -0
  181. package/lib/loops/types.ts +223 -0
  182. package/lib/loops/world.test-helpers.ts +223 -0
  183. package/lib/models/agent-binding.ts +102 -0
  184. package/lib/models/agent-roles-view.ts +157 -0
  185. package/lib/models/agent-roles.ts +87 -0
  186. package/lib/models/availability-refs.ts +17 -0
  187. package/lib/models/availability.ts +91 -0
  188. package/lib/models/catalog.ts +156 -0
  189. package/lib/models/config.ts +27 -0
  190. package/lib/models/locality.ts +33 -0
  191. package/lib/models/plan-step.ts +4 -1
  192. package/lib/models/providers.ts +11 -0
  193. package/lib/models/resolve.ts +50 -12
  194. package/lib/models/roles.ts +12 -0
  195. package/lib/needs-you.ts +1 -1
  196. package/lib/orchestrator/digest.ts +2 -1
  197. package/lib/orchestrator/facts.ts +7 -2
  198. package/lib/orchestrator/loop-guard.ts +5 -2
  199. package/lib/orchestrator/pr-wake.ts +4 -0
  200. package/lib/orchestrator/read.ts +5 -1
  201. package/lib/orchestrator/untrusted.ts +5 -0
  202. package/lib/orchestrator/watcher.ts +8 -1
  203. package/lib/panes.ts +52 -0
  204. package/lib/plans/acceptance.ts +23 -0
  205. package/lib/projects.ts +1 -0
  206. package/lib/reports/drafts.ts +87 -0
  207. package/lib/reports/end-hook.ts +17 -0
  208. package/lib/reports/estimate.ts +107 -0
  209. package/lib/reports/evidence/build.ts +193 -0
  210. package/lib/reports/evidence/gather.ts +229 -0
  211. package/lib/reports/fixtures.ts +17 -0
  212. package/lib/reports/generate.ts +159 -0
  213. package/lib/reports/hooks.ts +30 -0
  214. package/lib/reports/mode.ts +65 -0
  215. package/lib/reports/model.ts +115 -0
  216. package/lib/reports/read.ts +26 -0
  217. package/lib/reports/store.ts +180 -0
  218. package/lib/reports/template.ts +117 -0
  219. package/lib/reports/types.ts +135 -0
  220. package/lib/reports/validate.ts +142 -0
  221. package/lib/session-artifacts/agentos-files.ts +94 -0
  222. package/lib/session-artifacts/collect.ts +17 -0
  223. package/lib/sessions/crew.ts +72 -0
  224. package/lib/sessions/launch.ts +49 -9
  225. package/lib/settings-url.ts +1 -0
  226. package/lib/share/markdown.ts +2 -2
  227. package/lib/share/redact.ts +16 -10
  228. package/lib/share/render.tsx +1 -1
  229. package/lib/share/snapshot.ts +2 -2
  230. package/lib/sidebar/row-menu.ts +15 -1
  231. package/lib/tasks/contract/routes.ts +5 -0
  232. package/lib/tasks/contract/types.ts +3 -0
  233. package/lib/tasks/finish.ts +2 -0
  234. package/lib/tasks/index.ts +6 -0
  235. package/lib/tasks/move-carry.ts +1 -0
  236. package/lib/worktree-deps/caches.ts +3 -2
  237. package/lib/worktree-deps/clone.ts +3 -1
  238. package/lib/worktree-deps/paths.ts +26 -0
  239. package/package.json +2 -2
  240. package/server.ts +9 -1
  241. package/stores/analystUi.ts +19 -0
  242. 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. */
@@ -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,3 @@
1
+ export { analystKeys } from "./keys";
2
+ export { useAnalystLink, useSessionAnalyst } from "./queries";
3
+ export * from "./mutations";
@@ -0,0 +1,6 @@
1
+ export const analystKeys = {
2
+ all: ["analyst"] as const,
3
+ link: (id: string) => [...analystKeys.all, "link", id] as const,
4
+ ofSession: (sessionId: string) =>
5
+ [...analystKeys.all, "session", sessionId] as const,
6
+ };
@@ -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
+ }