@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
@@ -16,6 +16,7 @@ import { cn } from "@/lib/utils";
16
16
  import { ConductorPanel } from "@/components/ConductorPanel";
17
17
  import { useFileEditor } from "@/hooks/useFileEditor";
18
18
  import { MobileTabBar } from "./MobileTabBar";
19
+ import { PairSwitch } from "@/components/Analyst/PairSwitch";
19
20
  import { openFind } from "@/components/Chat/useFind";
20
21
  import { DesktopTabBar } from "./DesktopTabBar";
21
22
  import {
@@ -46,6 +47,10 @@ const ArtifactsPanel = dynamic(
46
47
  ),
47
48
  { ssr: false }
48
49
  );
50
+ const ReportView = dynamic(
51
+ () => import("@/components/Report/ReportView").then((mod) => mod.ReportView),
52
+ { ssr: false }
53
+ );
49
54
  const LogDocument = dynamic(
50
55
  () =>
51
56
  import("@/components/SessionLogs/LogDocument").then(
@@ -215,6 +220,7 @@ export const Pane = memo(function Pane({
215
220
  !!session?.task_prompt &&
216
221
  (session.task_status === "merged" || session.pr_status === "merged");
217
222
  const hasLog = !!useTaskLogQuery(session?.id, merged).data;
223
+ const hasReport = !!session?.task_prompt;
218
224
 
219
225
  // Get current project and its repositories
220
226
  const currentProject = useMemo(() => {
@@ -471,6 +477,7 @@ export const Pane = memo(function Pane({
471
477
  viewMode={viewMode}
472
478
  isConductor={isConductor}
473
479
  hasLog={hasLog}
480
+ hasReport={hasReport}
474
481
  workerCount={workerCount}
475
482
  showGit={showGit}
476
483
  onMenuClick={onMenuClick}
@@ -494,6 +501,7 @@ export const Pane = memo(function Pane({
494
501
  isFocused={isFocused}
495
502
  isConductor={isConductor}
496
503
  hasLog={hasLog}
504
+ hasReport={hasReport}
497
505
  workerCount={workerCount}
498
506
  canSplit={canSplit}
499
507
  canClose={canClose}
@@ -514,6 +522,7 @@ export const Pane = memo(function Pane({
514
522
  bookmarksSessionId={bookmarksSessionId}
515
523
  />
516
524
  )}
525
+ {isMobile && <PairSwitch session={session} paneId={paneId} />}
517
526
 
518
527
  {/* Content Area - Mobile: simple flex, Desktop: resizable panels */}
519
528
  {isMobile ? (
@@ -558,6 +567,11 @@ export const Pane = memo(function Pane({
558
567
  </div>
559
568
  )}
560
569
 
570
+ {/* Report */}
571
+ {viewMode === "report" && session && (
572
+ <ReportView sessionId={session.id} name={session.name} />
573
+ )}
574
+
561
575
  {/* Log */}
562
576
  {viewMode === "log" && session && (
563
577
  <LogDocument sessionId={session.id} />
@@ -629,6 +643,11 @@ export const Pane = memo(function Pane({
629
643
  </div>
630
644
  )}
631
645
 
646
+ {/* Report */}
647
+ {viewMode === "report" && session && (
648
+ <ReportView sessionId={session.id} name={session.name} />
649
+ )}
650
+
632
651
  {/* Log */}
633
652
  {viewMode === "log" && session && (
634
653
  <LogDocument sessionId={session.id} />
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Textarea } from "@/components/ui/textarea";
6
+ import { useReportDraft } from "@/data/reports";
7
+ import type { NextPromptDraft } from "@/lib/reports/types";
8
+
9
+ // The analyst's drafted next prompt, shown in full. Approve & send is the one
10
+ // click; nothing is sent without it.
11
+ export function DraftCard({
12
+ sessionId,
13
+ draft,
14
+ }: {
15
+ sessionId: string;
16
+ draft: NextPromptDraft;
17
+ }) {
18
+ const act = useReportDraft(sessionId);
19
+ const [editing, setEditing] = useState(false);
20
+ const [text, setText] = useState(draft.text);
21
+ const ref = { source: draft.source, id: draft.id, linkId: draft.linkId };
22
+ const decided = draft.status !== "pending";
23
+
24
+ if (decided)
25
+ return (
26
+ <p className="text-muted-foreground bg-muted/40 rounded-lg px-3 py-2 text-sm">
27
+ {draft.status === "sent"
28
+ ? "Sent to the coding agent."
29
+ : draft.status === "dismissed"
30
+ ? "Draft dismissed."
31
+ : "A newer report replaced this draft."}
32
+ </p>
33
+ );
34
+
35
+ return (
36
+ <section
37
+ aria-label="Drafted next prompt"
38
+ className="bg-card space-y-3 rounded-xl p-4 shadow-sm"
39
+ >
40
+ <h3 className="text-sm font-semibold">Drafted next prompt</h3>
41
+ {editing ? (
42
+ <Textarea
43
+ value={text}
44
+ onChange={(e) => setText(e.target.value)}
45
+ rows={6}
46
+ aria-label="Edit the prompt"
47
+ />
48
+ ) : (
49
+ <p className="bg-muted/50 rounded-lg p-3 text-sm whitespace-pre-wrap">
50
+ {draft.text}
51
+ </p>
52
+ )}
53
+ {act.isError && (
54
+ <p className="text-destructive text-sm">{act.error.message}</p>
55
+ )}
56
+ <div className="flex flex-wrap gap-2">
57
+ <Button
58
+ className="min-h-11 flex-1"
59
+ disabled={act.isPending || !text.trim()}
60
+ onClick={() =>
61
+ act.mutate({
62
+ ...ref,
63
+ action: "send",
64
+ text: editing ? text : undefined,
65
+ })
66
+ }
67
+ >
68
+ Approve &amp; send
69
+ </Button>
70
+ <Button
71
+ variant="secondary"
72
+ className="min-h-11"
73
+ disabled={act.isPending}
74
+ onClick={() => setEditing((e) => !e)}
75
+ >
76
+ {editing ? "Cancel edit" : "Edit"}
77
+ </Button>
78
+ <Button
79
+ variant="ghost"
80
+ className="min-h-11"
81
+ disabled={act.isPending}
82
+ onClick={() => act.mutate({ ...ref, action: "dismiss" })}
83
+ >
84
+ Dismiss
85
+ </Button>
86
+ </div>
87
+ </section>
88
+ );
89
+ }
@@ -0,0 +1,51 @@
1
+ import type { Estimate } from "@/lib/reports/types";
2
+ import { estimateLabel, estimatePercent } from "@/lib/reports/types";
3
+
4
+ // The percentage never shows without its label: it is an estimate, and the
5
+ // label says how it was counted.
6
+ export function EstimateBar({ estimate }: { estimate: Estimate }) {
7
+ const pct = estimatePercent(estimate);
8
+ const how =
9
+ estimate.basis === "acceptance"
10
+ ? "Acceptance items the plan listed, counted when a passing test, check or review backs them."
11
+ : "The plan has no acceptance items, so this counts delivery stages: plan, push, pull request, CI, review, merge.";
12
+ return (
13
+ <div className="space-y-2" title={how}>
14
+ <div className="flex items-baseline justify-between gap-3">
15
+ <span className="text-muted-foreground text-xs">
16
+ {estimateLabel(estimate)}
17
+ </span>
18
+ <span className="text-sm font-semibold tabular-nums">{pct}%</span>
19
+ </div>
20
+ <div
21
+ role="progressbar"
22
+ aria-valuemin={0}
23
+ aria-valuemax={estimate.total}
24
+ aria-valuenow={estimate.done}
25
+ aria-label={estimateLabel(estimate)}
26
+ className="bg-muted h-2 overflow-hidden rounded-full"
27
+ >
28
+ <div
29
+ className="bg-primary h-full rounded-full"
30
+ style={{ width: `${pct}%` }}
31
+ />
32
+ </div>
33
+ <details className="text-xs">
34
+ <summary className="text-muted-foreground flex min-h-11 cursor-pointer items-center">
35
+ How this is counted
36
+ </summary>
37
+ <p className="text-muted-foreground pb-2">{how}</p>
38
+ <ul className="space-y-1 pb-1">
39
+ {estimate.items.map((i) => (
40
+ <li key={i.label} className="flex gap-2">
41
+ <span aria-hidden>{i.verified ? "✓" : "○"}</span>
42
+ <span className={i.verified ? "" : "text-muted-foreground"}>
43
+ {i.label}
44
+ </span>
45
+ </li>
46
+ ))}
47
+ </ul>
48
+ </details>
49
+ </div>
50
+ );
51
+ }
@@ -0,0 +1,98 @@
1
+ "use client";
2
+
3
+ import { ExternalLink } from "lucide-react";
4
+ import {
5
+ ASheet,
6
+ ASheetContent,
7
+ ASheetDescription,
8
+ ASheetHeader,
9
+ ASheetTitle,
10
+ } from "@/components/a/ASheet";
11
+ import { StatusPill } from "@/components/ui/status-pill";
12
+ import { useMediaQuery } from "@/hooks/useMediaQuery";
13
+ import type { EvidenceRef } from "@/lib/reports/types";
14
+
15
+ const TONE = {
16
+ pass: "green",
17
+ fail: "red",
18
+ pending: "amber",
19
+ info: "muted",
20
+ } as const;
21
+ const WORD = {
22
+ pass: "Passed",
23
+ fail: "Failed",
24
+ pending: "Waiting",
25
+ info: "Info",
26
+ } as const;
27
+
28
+ // What a report line rests on. The text is untrusted output: shown, never run.
29
+ export function EvidenceSheet({
30
+ evidence,
31
+ stale,
32
+ onClose,
33
+ }: {
34
+ evidence: EvidenceRef | null;
35
+ stale: boolean;
36
+ onClose: () => void;
37
+ }) {
38
+ const wide = useMediaQuery("(min-width: 768px)");
39
+ return (
40
+ <ASheet open={!!evidence} onOpenChange={(o) => !o && onClose()}>
41
+ <ASheetContent
42
+ side={wide ? "right" : "bottom"}
43
+ className={wide ? "w-full max-w-md" : "max-h-[80dvh] rounded-t-2xl"}
44
+ >
45
+ {evidence && (
46
+ <>
47
+ <ASheetHeader className="pr-12">
48
+ <ASheetTitle className="break-words">
49
+ {evidence.label}
50
+ </ASheetTitle>
51
+ <ASheetDescription className="flex flex-wrap items-center gap-2">
52
+ <StatusPill tone={TONE[evidence.status]}>
53
+ {WORD[evidence.status]}
54
+ </StatusPill>
55
+ <span>{evidence.kind}</span>
56
+ {evidence.sha && (
57
+ <span className="font-mono">{evidence.sha.slice(0, 7)}</span>
58
+ )}
59
+ {stale && evidence.sha && <span>older push</span>}
60
+ </ASheetDescription>
61
+ </ASheetHeader>
62
+ <div className="min-h-0 flex-1 space-y-3 overflow-y-auto px-4 pb-6">
63
+ {evidence.excerpt ? (
64
+ <pre className="bg-muted/60 max-h-[50dvh] overflow-auto rounded-lg p-3 text-xs break-words whitespace-pre-wrap">
65
+ {evidence.excerpt}
66
+ </pre>
67
+ ) : (
68
+ <p className="text-muted-foreground text-sm">
69
+ No output was kept for this one.
70
+ </p>
71
+ )}
72
+ <div className="flex flex-wrap gap-2">
73
+ {evidence.url && (
74
+ <a
75
+ href={evidence.url}
76
+ target="_blank"
77
+ rel="noopener noreferrer"
78
+ className="bg-secondary inline-flex min-h-11 items-center gap-1.5 rounded-lg px-3 text-sm"
79
+ >
80
+ Open in CI <ExternalLink className="h-3.5 w-3.5" />
81
+ </a>
82
+ )}
83
+ {evidence.href && (
84
+ <a
85
+ href={evidence.href}
86
+ className="bg-secondary inline-flex min-h-11 items-center rounded-lg px-3 text-sm"
87
+ >
88
+ Show in the task
89
+ </a>
90
+ )}
91
+ </div>
92
+ </div>
93
+ </>
94
+ )}
95
+ </ASheetContent>
96
+ </ASheet>
97
+ );
98
+ }
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import { Segmented } from "@/components/ui/segmented";
4
+ import {
5
+ MODE_COPY,
6
+ OPERATING_MODES,
7
+ type OperatingMode,
8
+ } from "@/lib/reports/types";
9
+
10
+ const OPTIONS = OPERATING_MODES.map((m) => ({
11
+ value: m,
12
+ label: MODE_COPY[m].label,
13
+ hint: MODE_COPY[m].blurb,
14
+ }));
15
+
16
+ // Manual / Assisted / Autonomous, with what the chosen one does underneath.
17
+ export function ModeChooser({
18
+ mode,
19
+ onChange,
20
+ disabled,
21
+ note,
22
+ }: {
23
+ mode: OperatingMode;
24
+ onChange: (m: OperatingMode) => void;
25
+ disabled?: boolean;
26
+ note?: string;
27
+ }) {
28
+ return (
29
+ <div className="space-y-1.5">
30
+ <Segmented
31
+ label="Operating mode"
32
+ value={mode}
33
+ options={OPTIONS}
34
+ onChange={onChange}
35
+ disabled={disabled}
36
+ />
37
+ <p className="text-muted-foreground text-xs">
38
+ {MODE_COPY[mode].blurb}
39
+ {note ? ` ${note}` : ""}
40
+ </p>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,289 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import {
5
+ AlertTriangle,
6
+ ArrowRight,
7
+ CheckCircle2,
8
+ Hand,
9
+ Loader2,
10
+ RefreshCw,
11
+ type LucideIcon,
12
+ } from "lucide-react";
13
+ import { Button } from "@/components/ui/button";
14
+ import { StatusPill } from "@/components/ui/status-pill";
15
+ import { useGenerateReport, useTaskMode, useTaskReport } from "@/data/reports";
16
+ import {
17
+ MODE_COPY,
18
+ type EvidenceRef,
19
+ type HealthReport,
20
+ type LineTone,
21
+ } from "@/lib/reports/types";
22
+ import { DraftCard } from "./DraftCard";
23
+ import { EstimateBar } from "./EstimateBar";
24
+ import { EvidenceSheet } from "./EvidenceSheet";
25
+ import { ModeChooser } from "./ModeChooser";
26
+
27
+ const TONES: Record<LineTone, { icon: LucideIcon; cls: string; word: string }> =
28
+ {
29
+ done: {
30
+ icon: CheckCircle2,
31
+ cls: "text-green-600 dark:text-green-400",
32
+ word: "Done",
33
+ },
34
+ warn: {
35
+ icon: AlertTriangle,
36
+ cls: "text-amber-600 dark:text-amber-400",
37
+ word: "Needs attention",
38
+ },
39
+ blocked: {
40
+ icon: Hand,
41
+ cls: "text-red-600 dark:text-red-400",
42
+ word: "Blocked",
43
+ },
44
+ next: { icon: ArrowRight, cls: "text-primary", word: "Next" },
45
+ };
46
+
47
+ const STATE_TONE = {
48
+ merged: "green",
49
+ done: "green",
50
+ working: "primary",
51
+ review: "primary",
52
+ "checks-failing": "red",
53
+ blocked: "red",
54
+ } as const;
55
+
56
+ const when = (iso: string) =>
57
+ new Date(
58
+ iso.includes("T") ? iso : `${iso.replace(" ", "T")}Z`
59
+ ).toLocaleTimeString([], {
60
+ hour: "numeric",
61
+ minute: "2-digit",
62
+ });
63
+
64
+ // A task's plain-English health report: what's done, what needs attention,
65
+ // what's next, each line backed by evidence that opens underneath.
66
+ export function ReportView({
67
+ sessionId,
68
+ name,
69
+ }: {
70
+ sessionId: string;
71
+ name?: string;
72
+ }) {
73
+ const { data, isPending, isError, error } = useTaskReport(sessionId);
74
+ const generate = useGenerateReport(sessionId);
75
+ const setMode = useTaskMode(sessionId);
76
+ const [open, setOpen] = useState<EvidenceRef | null>(null);
77
+ const [changingMode, setChangingMode] = useState(false);
78
+ const asked = useRef(false);
79
+
80
+ const report = data?.report ?? null;
81
+ // Opening the view builds the first report, or a fresh one when the head moved.
82
+ useEffect(() => {
83
+ if (!data || asked.current || data.generating) return;
84
+ if (!report || report.stale) {
85
+ asked.current = true;
86
+ generate.mutate();
87
+ }
88
+ }, [data, report, generate]);
89
+
90
+ if (isPending)
91
+ return <div className="bg-muted/40 m-4 h-40 animate-pulse rounded-lg" />;
92
+ if (isError)
93
+ return <p className="text-destructive p-4 text-sm">{error.message}</p>;
94
+
95
+ const mode = data.mode;
96
+ const busy = data.generating || generate.isPending;
97
+
98
+ return (
99
+ <div className="h-full overflow-y-auto">
100
+ <div className="mx-auto max-w-2xl space-y-5 px-4 py-4">
101
+ <header className="space-y-2">
102
+ <div className="flex flex-wrap items-center gap-2">
103
+ <h2 className="text-lg font-semibold">Your Project Progress</h2>
104
+ {report && (
105
+ <StatusPill
106
+ tone={
107
+ STATE_TONE[report.state as keyof typeof STATE_TONE] ?? "muted"
108
+ }
109
+ >
110
+ {report.state.replace("-", " ")}
111
+ </StatusPill>
112
+ )}
113
+ <button
114
+ type="button"
115
+ onClick={() => setChangingMode((c) => !c)}
116
+ aria-expanded={changingMode}
117
+ className="bg-secondary ml-auto inline-flex min-h-11 items-center rounded-lg px-3 text-xs"
118
+ >
119
+ {MODE_COPY[mode.mode].label}
120
+ {mode.source === "task" ? " · this task" : ""}
121
+ </button>
122
+ </div>
123
+ {name && (
124
+ <p className="text-muted-foreground truncate text-sm">{name}</p>
125
+ )}
126
+ {changingMode && (
127
+ <div className="bg-muted/40 space-y-2 rounded-lg p-3">
128
+ <ModeChooser
129
+ mode={mode.mode}
130
+ onChange={(m) => setMode.mutate(m)}
131
+ disabled={setMode.isPending}
132
+ />
133
+ {mode.source === "task" && (
134
+ <Button
135
+ variant="ghost"
136
+ className="min-h-11"
137
+ onClick={() => setMode.mutate(null)}
138
+ >
139
+ Use the workspace default
140
+ </Button>
141
+ )}
142
+ </div>
143
+ )}
144
+ </header>
145
+
146
+ {!report ? (
147
+ <div className="bg-card space-y-3 rounded-xl p-5 text-center shadow-sm">
148
+ <p className="text-sm">
149
+ {busy
150
+ ? "Reading the evidence…"
151
+ : "No report has been built for this task yet."}
152
+ </p>
153
+ {busy ? (
154
+ <Loader2 className="mx-auto h-5 w-5 animate-spin" />
155
+ ) : (
156
+ <Button className="min-h-11" onClick={() => generate.mutate()}>
157
+ Build the report
158
+ </Button>
159
+ )}
160
+ </div>
161
+ ) : (
162
+ <ReportBody
163
+ report={report}
164
+ sessionId={sessionId}
165
+ mode={mode.mode}
166
+ busy={busy}
167
+ onRefresh={() => generate.mutate()}
168
+ onOpen={setOpen}
169
+ />
170
+ )}
171
+ </div>
172
+ <EvidenceSheet
173
+ evidence={open}
174
+ stale={!!open?.sha && !!report?.head && open.sha !== report.head}
175
+ onClose={() => setOpen(null)}
176
+ />
177
+ </div>
178
+ );
179
+ }
180
+
181
+ function ReportBody({
182
+ report,
183
+ sessionId,
184
+ mode,
185
+ busy,
186
+ onRefresh,
187
+ onOpen,
188
+ }: {
189
+ report: HealthReport;
190
+ sessionId: string;
191
+ mode: string;
192
+ busy: boolean;
193
+ onRefresh: () => void;
194
+ onOpen: (e: EvidenceRef) => void;
195
+ }) {
196
+ const byId = new Map(report.evidence.map((e) => [e.id, e]));
197
+ const chips = (ids: string[]) =>
198
+ ids
199
+ .map((id) => byId.get(id))
200
+ .filter((e): e is EvidenceRef => !!e)
201
+ .map((e) => (
202
+ <button
203
+ key={e.id}
204
+ type="button"
205
+ onClick={() => onOpen(e)}
206
+ className="bg-secondary text-muted-foreground hover:text-foreground relative max-w-full truncate rounded-md px-2 py-0.5 text-[11px] before:absolute before:inset-x-0 before:-inset-y-3"
207
+ >
208
+ {e.label}
209
+ </button>
210
+ ));
211
+
212
+ return (
213
+ <>
214
+ {report.stale && (
215
+ <p className="rounded-lg bg-amber-500/10 px-3 py-2 text-sm text-amber-700 dark:text-amber-400">
216
+ New work was pushed since this report.
217
+ </p>
218
+ )}
219
+ <section className="bg-card space-y-4 rounded-xl p-4 shadow-sm">
220
+ <EstimateBar estimate={report.estimate} />
221
+ <p className="text-base font-medium">{report.headline}</p>
222
+ <ul className="space-y-3">
223
+ {report.lines.map((l) => {
224
+ const t = TONES[l.tone];
225
+ return (
226
+ <li key={l.id} className="flex gap-3">
227
+ <t.icon
228
+ className={`mt-0.5 h-5 w-5 shrink-0 ${t.cls}`}
229
+ aria-label={t.word}
230
+ />
231
+ <div className="min-w-0 flex-1 space-y-1.5">
232
+ <p className="text-sm">{l.text}</p>
233
+ <div className="flex flex-wrap gap-1.5">
234
+ {chips(l.evidence)}
235
+ </div>
236
+ </div>
237
+ </li>
238
+ );
239
+ })}
240
+ </ul>
241
+ </section>
242
+
243
+ {report.recommendation && (
244
+ <section className="bg-card space-y-2 rounded-xl p-4 shadow-sm">
245
+ <h3 className="text-sm font-semibold">Analyst recommendation</h3>
246
+ <p className="text-sm">{report.recommendation.text}</p>
247
+ <div className="flex flex-wrap gap-1.5">
248
+ {chips(report.recommendation.evidence)}
249
+ </div>
250
+ </section>
251
+ )}
252
+
253
+ {report.draft && mode !== "manual" && (
254
+ <DraftCard
255
+ key={`${report.draft.source}:${report.draft.id}`}
256
+ sessionId={sessionId}
257
+ draft={report.draft}
258
+ />
259
+ )}
260
+
261
+ <footer className="text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
262
+ <span>
263
+ Built from {report.evidence.length} item
264
+ {report.evidence.length === 1 ? "" : "s"} · {when(report.createdAt)}
265
+ {report.source === "model" && report.model
266
+ ? ` · ${report.model}`
267
+ : " · no model"}
268
+ </span>
269
+ {report.dropped > 0 && (
270
+ <span>
271
+ {report.dropped} unsupported claim{report.dropped === 1 ? "" : "s"}{" "}
272
+ left out
273
+ </span>
274
+ )}
275
+ <Button
276
+ variant="ghost"
277
+ className="ml-auto min-h-11"
278
+ disabled={busy}
279
+ onClick={onRefresh}
280
+ >
281
+ <RefreshCw
282
+ className={`mr-1.5 h-3.5 w-3.5 ${busy ? "animate-spin" : ""}`}
283
+ />
284
+ Refresh
285
+ </Button>
286
+ </footer>
287
+ </>
288
+ );
289
+ }
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { AGENT_ROLE_DEFS, isAgentRole } from "@/lib/models/agent-roles";
3
4
  import { isFork } from "@/lib/sessions/fork";
4
5
  import { memo, useState } from "react";
5
6
  import { useSnapshot } from "valtio";
@@ -126,14 +127,24 @@ export const SessionRow = memo(function SessionRow({
126
127
  }}
127
128
  />
128
129
  ) : (
129
- <span
130
- className={cn(
131
- "truncate text-sm",
132
- muted && !active ? "text-muted-foreground" : "text-foreground",
133
- active && "font-medium"
130
+ <span className="flex min-w-0 items-center gap-1.5">
131
+ {isAgentRole(session.agent_role) && (
132
+ <span
133
+ title={AGENT_ROLE_DEFS[session.agent_role].label}
134
+ className="bg-accent text-primary grid h-4 w-4 shrink-0 place-items-center rounded text-[10px] font-semibold"
135
+ >
136
+ {AGENT_ROLE_DEFS[session.agent_role].label[0]}
137
+ </span>
134
138
  )}
135
- >
136
- {session.name}
139
+ <span
140
+ className={cn(
141
+ "truncate text-sm",
142
+ muted && !active ? "text-muted-foreground" : "text-foreground",
143
+ active && "font-medium"
144
+ )}
145
+ >
146
+ {session.name}
147
+ </span>
137
148
  </span>
138
149
  )}
139
150
  <RowSubtitle