@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,132 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useAgentRolesQuery, useSaveAgentRoles } from "@/data/models";
5
+ import { useProjectsQuery } from "@/data/projects/queries";
6
+ import { useWorkspacesQuery } from "@/data/workspaces";
7
+ import type { RoleMap } from "@/lib/models/config";
8
+ import type { RoleScope } from "@/lib/models/agent-roles-view";
9
+ import { cn } from "@/lib/utils";
10
+ import { AgentRoleCard } from "./AgentRoleCard";
11
+
12
+ const SCOPES: { id: RoleScope; label: string }[] = [
13
+ { id: "settings", label: "Default" },
14
+ { id: "workspace", label: "Workspace" },
15
+ { id: "project", label: "Project" },
16
+ ];
17
+
18
+ // Settings > Agent roles: the model each of Developer, Analyst, Reviewer and
19
+ // Supervisor runs on, for everything or for one workspace or project.
20
+ export function AgentRolesSection() {
21
+ const [scope, setScope] = useState<RoleScope>("settings");
22
+ const [target, setTarget] = useState<string | null>(null);
23
+ const workspaces = useWorkspacesQuery();
24
+ const projects = useProjectsQuery();
25
+ const options =
26
+ scope === "workspace"
27
+ ? (workspaces.data ?? []).map((w) => ({ id: w.id, name: w.name }))
28
+ : scope === "project"
29
+ ? (projects.data ?? [])
30
+ .filter((p) => !p.is_uncategorized)
31
+ .map((p) => ({ id: p.id, name: p.name }))
32
+ : [];
33
+ const id = scope === "settings" ? null : (target ?? options[0]?.id ?? null);
34
+ const { data, isPending, isError, error } = useAgentRolesQuery(scope, id);
35
+ const save = useSaveAgentRoles(scope, id);
36
+
37
+ const saved = (key: "value" | "fallback"): RoleMap =>
38
+ Object.fromEntries(
39
+ (data?.roles ?? [])
40
+ .filter((r) => r[key])
41
+ .map((r) => [r.modelRole, r[key] as string])
42
+ );
43
+ const setRole = (modelRole: string, ref: string | null) => {
44
+ const roles: RoleMap = { ...saved("value") };
45
+ if (ref) roles[modelRole as keyof RoleMap] = ref;
46
+ else delete roles[modelRole as keyof RoleMap];
47
+ save.mutate({ roles });
48
+ };
49
+ const setFallback = (modelRole: string, ref: string | null) => {
50
+ const fallbacks: RoleMap = { ...saved("fallback") };
51
+ if (ref) fallbacks[modelRole as keyof RoleMap] = ref;
52
+ else delete fallbacks[modelRole as keyof RoleMap];
53
+ save.mutate({ roles: saved("value"), fallbacks });
54
+ };
55
+
56
+ return (
57
+ <div className="space-y-4">
58
+ <div
59
+ role="tablist"
60
+ className="bg-muted inline-flex max-w-full gap-0.5 overflow-x-auto rounded-xl p-0.5"
61
+ >
62
+ {SCOPES.map((s) => (
63
+ <button
64
+ key={s.id}
65
+ role="tab"
66
+ aria-selected={scope === s.id}
67
+ type="button"
68
+ onClick={() => {
69
+ setScope(s.id);
70
+ setTarget(null);
71
+ }}
72
+ className={cn(
73
+ "h-11 rounded-lg px-3 text-sm whitespace-nowrap md:h-8",
74
+ scope === s.id
75
+ ? "bg-background text-foreground ring-border ring-1"
76
+ : "text-muted-foreground hover:text-foreground"
77
+ )}
78
+ >
79
+ {s.label}
80
+ </button>
81
+ ))}
82
+ </div>
83
+ {scope !== "settings" && (
84
+ <select
85
+ aria-label={scope === "workspace" ? "Workspace" : "Project"}
86
+ value={id ?? ""}
87
+ onChange={(e) => setTarget(e.target.value)}
88
+ className="border-input bg-background h-11 w-full rounded-md border px-3 text-sm md:h-9"
89
+ >
90
+ {options.length === 0 && <option value="">None yet</option>}
91
+ {options.map((o) => (
92
+ <option key={o.id} value={o.id}>
93
+ {o.name}
94
+ </option>
95
+ ))}
96
+ </select>
97
+ )}
98
+ <p className="text-muted-foreground text-xs">
99
+ The model each role runs on. A workspace or project can override these,
100
+ and a session can when it starts. A Pi model (a server you run) runs
101
+ sessions started in the role; tasks and the merge gate stay on Claude
102
+ Code models. Planning, gate review, scope check, helpers and watchers
103
+ are in Models.
104
+ </p>
105
+ {scope !== "settings" && !id ? (
106
+ <p className="text-muted-foreground text-sm">
107
+ There is no {scope} to set roles for yet.
108
+ </p>
109
+ ) : isPending ? (
110
+ <div className="bg-muted/40 h-64 animate-pulse rounded-lg" />
111
+ ) : isError ? (
112
+ <p className="text-destructive text-sm">{error.message}</p>
113
+ ) : data ? (
114
+ <ul className="space-y-2.5">
115
+ {data.roles.map((r) => (
116
+ <AgentRoleCard
117
+ key={r.id}
118
+ role={r}
119
+ scope={scope}
120
+ pending={save.isPending}
121
+ onChange={(ref) => setRole(r.modelRole, ref)}
122
+ onFallback={(ref) => setFallback(r.modelRole, ref)}
123
+ />
124
+ ))}
125
+ </ul>
126
+ ) : null}
127
+ {save.isError && (
128
+ <p className="text-destructive text-xs">{save.error.message}</p>
129
+ )}
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,48 @@
1
+ import type { Availability } from "@/lib/models/availability";
2
+ import type { Locality } from "@/lib/models/locality";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ // Whether a model runs on this machine's own networks or in the cloud.
6
+ export function LocalityBadge({ locality }: { locality: Locality }) {
7
+ return (
8
+ <span
9
+ className={cn(
10
+ "inline-flex h-5 shrink-0 items-center rounded-full px-2 text-[11px] font-medium",
11
+ locality === "local"
12
+ ? "bg-emerald-500/15 text-emerald-700 dark:text-emerald-400"
13
+ : "bg-muted text-muted-foreground ring-border ring-1 ring-inset"
14
+ )}
15
+ >
16
+ {locality === "local" ? "Local" : "Cloud"}
17
+ </span>
18
+ );
19
+ }
20
+
21
+ const DOT = {
22
+ up: "bg-emerald-500",
23
+ down: "bg-destructive",
24
+ unknown: "bg-muted-foreground/50",
25
+ } as const;
26
+
27
+ const TITLE = {
28
+ up: "Answering",
29
+ down: "Not answering",
30
+ unknown: "Not checked",
31
+ } as const;
32
+
33
+ // The last availability check: green answering, red not, grey not checked.
34
+ export function AvailabilityDot({
35
+ availability,
36
+ }: {
37
+ availability: Pick<Availability, "state"> | undefined;
38
+ }) {
39
+ const state = availability?.state ?? "unknown";
40
+ return (
41
+ <span
42
+ role="img"
43
+ aria-label={TITLE[state]}
44
+ title={TITLE[state]}
45
+ className={cn("inline-block h-2 w-2 shrink-0 rounded-full", DOT[state])}
46
+ />
47
+ );
48
+ }
@@ -11,6 +11,7 @@ import {
11
11
  type ModelProvider,
12
12
  } from "@/lib/models/providers";
13
13
  import { SubagentModels } from "./SubagentModels";
14
+ import { INTERNAL_MODEL_ROLES } from "@/lib/models/agent-roles";
14
15
  import { MODEL_ROLES, ROLE_LABELS } from "@/lib/models/roles";
15
16
  import { ModelSelect } from "./ModelSelect";
16
17
  import { ProviderCard } from "./ProviderCard";
@@ -58,11 +59,12 @@ export function ModelsSection() {
58
59
  <div className="space-y-6">
59
60
  <section className="space-y-2">
60
61
  <p className="text-muted-foreground text-sm">
61
- Which model each kind of work runs on. A workspace can set its own
62
+ Which model each kind of work runs on. Developer, Analyst, Reviewer
63
+ and Supervisor are in Agent roles. A workspace can set its own
62
64
  (Settings › Workspaces), and a task its own when it starts or resumes.
63
65
  </p>
64
66
  <ul className="space-y-2">
65
- {MODEL_ROLES.map((role) => (
67
+ {INTERNAL_MODEL_ROLES(MODEL_ROLES).map((role) => (
66
68
  <li
67
69
  key={role}
68
70
  className="bg-muted/40 space-y-1.5 rounded-lg px-3 py-2.5"
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import {
4
+ Select,
5
+ SelectContent,
6
+ SelectGroup,
7
+ SelectItem,
8
+ SelectLabel,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from "@/components/ui/select";
12
+ import { useModelChoices } from "@/data/models";
13
+ import type { ModelChoice } from "@/lib/models/catalog";
14
+ import { AvailabilityDot, LocalityBadge } from "./ModelBadges";
15
+
16
+ const INHERIT = "__inherit";
17
+
18
+ function Row({ choice }: { choice: ModelChoice }) {
19
+ return (
20
+ <span className="flex min-w-0 flex-1 items-center gap-2">
21
+ <AvailabilityDot availability={choice.availability} />
22
+ <span className="truncate">{choice.label}</span>
23
+ <LocalityBadge locality={choice.locality} />
24
+ </span>
25
+ );
26
+ }
27
+
28
+ // A model picker over the server's list: grouped by provider, each model
29
+ // labelled Local or Cloud with whether its server answers. `claudeOnly`
30
+ // leaves out the models that run through Pi.
31
+ export function RoleModelSelect({
32
+ id,
33
+ value,
34
+ inheritLabel,
35
+ claudeOnly,
36
+ gateOnly,
37
+ disabled,
38
+ onChange,
39
+ }: {
40
+ id?: string;
41
+ value: string | null;
42
+ inheritLabel: string;
43
+ claudeOnly?: boolean;
44
+ // Only models that can serve a merge gate are selectable.
45
+ gateOnly?: boolean;
46
+ disabled?: boolean;
47
+ onChange: (ref: string | null) => void;
48
+ }) {
49
+ const { data } = useModelChoices();
50
+ const choices = (data ?? []).filter((c) => !claudeOnly || c.agent !== "pi");
51
+ const groups = [...new Set(choices.map((c) => c.group))];
52
+ const current = value ? choices.find((c) => c.ref === value) : undefined;
53
+ return (
54
+ <Select
55
+ value={value ?? INHERIT}
56
+ onValueChange={(v) => onChange(v === INHERIT ? null : v)}
57
+ disabled={disabled}
58
+ >
59
+ <SelectTrigger id={id} className="h-11 md:h-9">
60
+ <SelectValue>
61
+ {current ? <Row choice={current} /> : (value ?? inheritLabel)}
62
+ </SelectValue>
63
+ </SelectTrigger>
64
+ <SelectContent>
65
+ <SelectItem value={INHERIT}>{inheritLabel}</SelectItem>
66
+ {value && !current && <SelectItem value={value}>{value}</SelectItem>}
67
+ {groups.map((g) => (
68
+ <SelectGroup key={g}>
69
+ <SelectLabel>{g}</SelectLabel>
70
+ {choices
71
+ .filter((c) => c.group === g)
72
+ .map((c) => (
73
+ <SelectItem
74
+ key={c.ref}
75
+ value={c.ref}
76
+ disabled={gateOnly && !c.gateCapable}
77
+ className="min-h-11 md:min-h-0"
78
+ >
79
+ <Row choice={c} />
80
+ </SelectItem>
81
+ ))}
82
+ </SelectGroup>
83
+ ))}
84
+ </SelectContent>
85
+ </Select>
86
+ );
87
+ }
@@ -15,6 +15,8 @@ import type { Session } from "@/lib/db";
15
15
  import { PaneViewToggle } from "./PaneViewToggle";
16
16
  import { PaneMenu } from "./PaneMenu";
17
17
  import { HostBadge } from "@/components/Hosts/HostBadge";
18
+ import { RolePill } from "@/components/Sessions/RolePill";
19
+ import { PairChip } from "@/components/Analyst/PairChip";
18
20
  import { usePaneBarSlots } from "./PaneBarSlots";
19
21
  import { ChatBookmarksButton } from "@/components/Bookmarks/ChatBookmarksButton";
20
22
  import type { ViewMode } from "@/contexts/PaneContext";
@@ -36,6 +38,7 @@ interface DesktopTabBarProps {
36
38
  isFocused: boolean;
37
39
  isConductor: boolean;
38
40
  hasLog?: boolean;
41
+ hasReport?: boolean;
39
42
  workerCount: number;
40
43
  canSplit: boolean;
41
44
  canClose: boolean;
@@ -67,6 +70,7 @@ export function DesktopTabBar({
67
70
  isFocused,
68
71
  isConductor,
69
72
  hasLog,
73
+ hasReport,
70
74
  workerCount,
71
75
  canSplit,
72
76
  canClose,
@@ -161,6 +165,8 @@ export function DesktopTabBar({
161
165
  </div>
162
166
 
163
167
  <div className="flex shrink-0 items-center gap-1.5">
168
+ <RolePill session={session} paneId={paneId} />
169
+ <PairChip session={session} paneId={paneId} />
164
170
  <HostBadge hostId={session?.host_id} />
165
171
  <ContextMeter sessionId={session?.id} />
166
172
  <SessionPrStage sessionId={session?.id} />
@@ -180,6 +186,7 @@ export function DesktopTabBar({
180
186
  isLocalSession={isLocalSession}
181
187
  isConductor={isConductor}
182
188
  hasLog={hasLog}
189
+ hasReport={hasReport}
183
190
  workerCount={workerCount}
184
191
  showGit={showGit}
185
192
  gitDrawerOpen={gitDrawerOpen}
@@ -192,6 +199,7 @@ export function DesktopTabBar({
192
199
  <div className="flex items-center">
193
200
  <PaneMenu
194
201
  session={session}
202
+ paneId={paneId}
195
203
  bookmarksSessionId={bookmarksSessionId}
196
204
  canSplit={canSplit}
197
205
  canClose={canClose}
@@ -24,15 +24,21 @@ import {
24
24
  Search,
25
25
  Star,
26
26
  Share2,
27
+ Download,
27
28
  TextSearch,
29
+ Eye,
28
30
  } from "lucide-react";
29
31
  import { cn } from "@/lib/utils";
30
32
  import { useAwayQuery } from "@/data/away";
31
33
  import { awayUntil } from "@/components/Away/time";
32
34
  import type { Session, Project } from "@/lib/db";
33
35
  import { HostBadge } from "@/components/Hosts/HostBadge";
36
+ import { RolePill } from "@/components/Sessions/RolePill";
34
37
  import { bookmarksUiActions } from "@/stores/bookmarksUi";
35
38
  import { shareUiActions } from "@/stores/shareUi";
39
+ import { exportUiActions } from "@/stores/exportUi";
40
+ import { analystUiActions } from "@/stores/analystUi";
41
+ import { useCanAddAnalyst } from "@/components/Analyst/pair";
36
42
  import { MoveMenuItems } from "@/components/Tasks/MoveMenuItems";
37
43
  import { ChatBookmarksButton } from "@/components/Bookmarks/ChatBookmarksButton";
38
44
  import type { ViewMode } from "@/contexts/PaneContext";
@@ -47,6 +53,7 @@ interface MobileTabBarProps {
47
53
  viewMode: ViewMode;
48
54
  isConductor: boolean;
49
55
  hasLog?: boolean;
56
+ hasReport?: boolean;
50
57
  workerCount: number;
51
58
  // Local, and the folder is not known to be outside a git repository.
52
59
  showGit: boolean;
@@ -67,6 +74,7 @@ export function MobileTabBar({
67
74
  viewMode,
68
75
  isConductor,
69
76
  hasLog,
77
+ hasReport,
70
78
  workerCount,
71
79
  showGit,
72
80
  onMenuClick,
@@ -125,6 +133,7 @@ export function MobileTabBar({
125
133
  };
126
134
 
127
135
  const isLocalSession = !session?.host_id || session.host_id === "local";
136
+ const canAddAnalyst = useCanAddAnalyst(session);
128
137
 
129
138
  return (
130
139
  <div
@@ -219,6 +228,24 @@ export function MobileTabBar({
219
228
  <Share2 className="h-4 w-4" />
220
229
  Share…
221
230
  </DropdownMenuItem>
231
+ <DropdownMenuItem
232
+ onSelect={() =>
233
+ exportUiActions.open(session.id, session.name)
234
+ }
235
+ >
236
+ <Download className="h-4 w-4" />
237
+ Export for AI…
238
+ </DropdownMenuItem>
239
+ {canAddAnalyst && (
240
+ <DropdownMenuItem
241
+ onSelect={() =>
242
+ analystUiActions.open(session.id, session.name)
243
+ }
244
+ >
245
+ <Eye className="h-4 w-4" />
246
+ Add analyst…
247
+ </DropdownMenuItem>
248
+ )}
222
249
  <DropdownMenuSeparator />
223
250
  </>
224
251
  )}
@@ -270,6 +297,7 @@ export function MobileTabBar({
270
297
  </button>
271
298
  </div>
272
299
 
300
+ <RolePill session={session} compact />
273
301
  {bookmarksSessionId && (
274
302
  <ChatBookmarksButton
275
303
  sessionId={bookmarksSessionId}
@@ -363,6 +391,7 @@ export function MobileTabBar({
363
391
  isLocalSession={isLocalSession}
364
392
  showGit={showGit}
365
393
  hasLog={hasLog}
394
+ hasReport={hasReport}
366
395
  isConductor={isConductor}
367
396
  workerCount={workerCount}
368
397
  />
@@ -7,6 +7,7 @@ import {
7
7
  GitBranch,
8
8
  Package,
9
9
  ScrollText,
10
+ ClipboardList,
10
11
  Terminal as TerminalIcon,
11
12
  Users,
12
13
  type LucideIcon,
@@ -31,6 +32,7 @@ export interface MobileViewAvailability {
31
32
  // Local, and the folder is not known to be outside a git repository.
32
33
  showGit: boolean;
33
34
  hasLog?: boolean;
35
+ hasReport?: boolean;
34
36
  isConductor: boolean;
35
37
  workerCount: number;
36
38
  }
@@ -40,6 +42,7 @@ export function mobileViews({
40
42
  isLocalSession,
41
43
  showGit,
42
44
  hasLog,
45
+ hasReport,
43
46
  isConductor,
44
47
  workerCount,
45
48
  }: MobileViewAvailability): MobileView[] {
@@ -48,6 +51,7 @@ export function mobileViews({
48
51
  isLocalSession && { mode: "files", label: "Files", icon: FolderOpen },
49
52
  showGit && { mode: "git", label: "Git", icon: GitBranch },
50
53
  { mode: "artifacts", label: "Artifacts", icon: Package },
54
+ !!hasReport && { mode: "report", label: "Report", icon: ClipboardList },
51
55
  !!hasLog && { mode: "log", label: "Log", icon: ScrollText },
52
56
  isConductor && {
53
57
  mode: "workers",
@@ -6,10 +6,12 @@ import type { Session } from "@/lib/db/types";
6
6
  import { bookmarksUi } from "@/stores/bookmarksUi";
7
7
  import { docsUi } from "@/stores/docsUi";
8
8
  import { paletteUi } from "@/stores/palette";
9
+ import { analystUi } from "@/stores/analystUi";
9
10
 
10
11
  const data = vi.hoisted(() => ({
11
12
  bookmarks: [] as unknown[],
12
13
  docs: null as { id: string } | null,
14
+ canAddAnalyst: false,
13
15
  }));
14
16
  vi.mock("@/data/bookmarks", () => ({
15
17
  useSessionBookmarks: () => ({ data: data.bookmarks }),
@@ -17,6 +19,9 @@ vi.mock("@/data/bookmarks", () => ({
17
19
  vi.mock("@/data/lumifyhub/docs", () => ({
18
20
  useDocsWorkspace: () => data.docs,
19
21
  }));
22
+ vi.mock("@/components/Analyst/pair", () => ({
23
+ useCanAddAnalyst: () => data.canAddAnalyst,
24
+ }));
20
25
  vi.mock("@/components/Tasks/MoveMenuItems", () => ({
21
26
  MoveMenuItems: () => null,
22
27
  }));
@@ -60,6 +65,8 @@ describe("PaneMenu", () => {
60
65
  root = createRoot(host);
61
66
  data.bookmarks = [];
62
67
  data.docs = null;
68
+ data.canAddAnalyst = false;
69
+ analystUi.open = false;
63
70
  paletteUi.open = false;
64
71
  bookmarksUi.chatSessionId = null;
65
72
  docsUi.workspaceId = null;
@@ -73,6 +80,7 @@ describe("PaneMenu", () => {
73
80
  root.render(
74
81
  createElement(PaneMenu, {
75
82
  session,
83
+ paneId: "pane-1",
76
84
  bookmarksSessionId,
77
85
  canSplit: true,
78
86
  canClose: true,
@@ -115,4 +123,17 @@ describe("PaneMenu", () => {
115
123
  act(() => item("Docs")?.click());
116
124
  expect(docsUi.workspaceId).toBe("w1");
117
125
  });
126
+
127
+ it("offers Add analyst only when the chat can have one, beside this pane", () => {
128
+ render();
129
+ expect(item("Add analyst")).toBeUndefined();
130
+ data.canAddAnalyst = true;
131
+ render();
132
+ act(() => item("Add analyst")?.click());
133
+ expect(analystUi).toMatchObject({
134
+ open: true,
135
+ sessionId: "s1",
136
+ paneId: "pane-1",
137
+ });
138
+ });
118
139
  });
@@ -8,9 +8,11 @@ import {
8
8
  Link,
9
9
  MoreHorizontal,
10
10
  Share2,
11
+ Download,
11
12
  SplitSquareHorizontal,
12
13
  SplitSquareVertical,
13
14
  Star,
15
+ Eye,
14
16
  Unplug,
15
17
  X,
16
18
  } from "lucide-react";
@@ -27,6 +29,9 @@ import {
27
29
  import { copySessionLink, writeClipboard } from "@/hooks/useCopyToClipboard";
28
30
  import type { Session } from "@/lib/db";
29
31
  import { shareUiActions } from "@/stores/shareUi";
32
+ import { exportUiActions } from "@/stores/exportUi";
33
+ import { analystUiActions } from "@/stores/analystUi";
34
+ import { useCanAddAnalyst } from "@/components/Analyst/pair";
30
35
  import { useSessionBookmarks } from "@/data/bookmarks";
31
36
  import { useDocsWorkspace } from "@/data/lumifyhub/docs";
32
37
  import { bookmarksUiActions } from "@/stores/bookmarksUi";
@@ -36,6 +41,7 @@ import { MoveMenuItems } from "@/components/Tasks/MoveMenuItems";
36
41
 
37
42
  interface PaneMenuProps {
38
43
  session: Session | null | undefined;
44
+ paneId?: string;
39
45
  // The chat on show, for its bookmarks; null for a terminal or another view.
40
46
  bookmarksSessionId?: string | null;
41
47
  canSplit: boolean;
@@ -51,6 +57,7 @@ interface PaneMenuProps {
51
57
  // the bar.
52
58
  export function PaneMenu({
53
59
  session,
60
+ paneId,
54
61
  bookmarksSessionId,
55
62
  canSplit,
56
63
  canClose,
@@ -62,6 +69,7 @@ export function PaneMenu({
62
69
  }: PaneMenuProps) {
63
70
  const { data: bookmarks = [] } = useSessionBookmarks(bookmarksSessionId);
64
71
  const docs = useDocsWorkspace(session?.project_id);
72
+ const canAddAnalyst = useCanAddAnalyst(session);
65
73
  const copyId = async (id: string) => {
66
74
  try {
67
75
  await writeClipboard(id);
@@ -117,6 +125,24 @@ export function PaneMenu({
117
125
  Share…
118
126
  </DropdownMenuItem>
119
127
  )}
128
+ {session.view === "chat" && (
129
+ <DropdownMenuItem
130
+ onSelect={() => exportUiActions.open(session.id, session.name)}
131
+ >
132
+ <Download className="h-4 w-4" />
133
+ Export for AI…
134
+ </DropdownMenuItem>
135
+ )}
136
+ {canAddAnalyst && session && (
137
+ <DropdownMenuItem
138
+ onSelect={() =>
139
+ analystUiActions.open(session.id, session.name, paneId)
140
+ }
141
+ >
142
+ <Eye className="h-4 w-4" />
143
+ Add analyst…
144
+ </DropdownMenuItem>
145
+ )}
120
146
  <DropdownMenuSeparator />
121
147
  <MoveMenuItems
122
148
  session={session}
@@ -6,6 +6,7 @@ import {
6
6
  Home,
7
7
  Package,
8
8
  ScrollText,
9
+ ClipboardList,
9
10
  Users,
10
11
  } from "lucide-react";
11
12
  import {
@@ -27,6 +28,7 @@ interface PaneViewToggleProps {
27
28
  isConductor: boolean;
28
29
  // The task merged and has a log.
29
30
  hasLog?: boolean;
31
+ hasReport?: boolean;
30
32
  workerCount: number;
31
33
  gitDrawerOpen: boolean;
32
34
  shellDrawerOpen: boolean;
@@ -85,6 +87,7 @@ export function PaneViewToggle({
85
87
  showGit,
86
88
  isConductor,
87
89
  hasLog,
90
+ hasReport,
88
91
  workerCount,
89
92
  gitDrawerOpen,
90
93
  shellDrawerOpen,
@@ -138,6 +141,15 @@ export function PaneViewToggle({
138
141
  <Package className="h-3.5 w-3.5" />
139
142
  </ToggleButton>
140
143
  )}
144
+ {hasReport && (
145
+ <ToggleButton
146
+ label="Report"
147
+ active={viewMode === "report"}
148
+ onClick={() => onViewModeChange("report")}
149
+ >
150
+ <ClipboardList className="h-3.5 w-3.5" />
151
+ </ToggleButton>
152
+ )}
141
153
  {hasLog && (
142
154
  <ToggleButton
143
155
  label="Log"