@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
@@ -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&apos;s server isn&apos;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
+ }