@saadnvd1/agent-os 0.3.723 → 0.3.731

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 (150) hide show
  1. package/README.md +1 -0
  2. package/app/api/bus/ports/route.test.ts +43 -0
  3. package/app/api/bus/ports/route.ts +35 -0
  4. package/app/api/loops/route.ts +17 -0
  5. package/app/api/sessions/[id]/export/turn/route.test.ts +65 -0
  6. package/app/api/sessions/[id]/export/turn/route.ts +33 -0
  7. package/app/api/tasks/[id]/loop/cycles/[n]/log/route.ts +29 -0
  8. package/app/api/tasks/[id]/loop/draft-criteria/route.ts +17 -0
  9. package/app/api/tasks/[id]/loop/next/route.ts +25 -0
  10. package/app/api/tasks/[id]/loop/route.ts +72 -0
  11. package/app/api/tasks/[id]/loop/run/route.ts +12 -0
  12. package/app/api/tasks/[id]/loop/stop/route.ts +12 -0
  13. package/app/api/tasks/[id]/mode/route.ts +29 -0
  14. package/app/api/tasks/[id]/report/draft/route.ts +54 -0
  15. package/app/api/tasks/[id]/report/evidence/[evId]/route.ts +18 -0
  16. package/app/api/tasks/[id]/report/route.ts +39 -0
  17. package/app/api/workspaces/[id]/mode/route.ts +23 -0
  18. package/app/api/workspaces/[id]/reports/route.ts +17 -0
  19. package/app/page.tsx +8 -0
  20. package/components/Analyst/PairChip.tsx +131 -0
  21. package/components/Analyst/PairSwitch.tsx +76 -0
  22. package/components/Analyst/ReviewCard.tsx +272 -0
  23. package/components/Analyst/SignalChips.tsx +39 -0
  24. package/components/Analyst/StartAnalystSheet.tsx +109 -0
  25. package/components/Analyst/cardState.test.ts +109 -0
  26. package/components/Analyst/cardState.ts +95 -0
  27. package/components/Analyst/pair.ts +81 -0
  28. package/components/Chat/Code.tsx +3 -0
  29. package/components/Chat/Messages.tsx +8 -1
  30. package/components/Chat/TurnCopy.tsx +84 -0
  31. package/components/Chat/itemContext.ts +14 -0
  32. package/components/Draft/DraftChips.tsx +10 -0
  33. package/components/Draft/DraftPanel.tsx +7 -0
  34. package/components/Pane/DesktopTabBar.tsx +6 -0
  35. package/components/Pane/MobileTabBar.tsx +17 -0
  36. package/components/Pane/MobileViewPicker.tsx +4 -0
  37. package/components/Pane/PaneMenu.test.ts +21 -0
  38. package/components/Pane/PaneMenu.tsx +16 -0
  39. package/components/Pane/PaneViewToggle.tsx +12 -0
  40. package/components/Pane/index.tsx +19 -0
  41. package/components/Report/DraftCard.tsx +89 -0
  42. package/components/Report/EstimateBar.tsx +51 -0
  43. package/components/Report/EvidenceSheet.tsx +98 -0
  44. package/components/Report/ModeChooser.tsx +43 -0
  45. package/components/Report/ReportView.tsx +289 -0
  46. package/components/SessionList/GroupShelves.tsx +57 -22
  47. package/components/SessionList/MoveToGroupMenu.test.ts +77 -0
  48. package/components/SessionList/SessionRow.tsx +12 -1
  49. package/components/SessionList/SessionRowMenu.tsx +6 -0
  50. package/components/SessionList/SidebarContextMenu.test.ts +21 -1
  51. package/components/SessionList/SidebarContextMenu.tsx +33 -9
  52. package/components/SessionList/index.tsx +11 -2
  53. package/components/SessionList/useGroupDrag.ts +83 -0
  54. package/components/Sessions/RolePill.tsx +7 -0
  55. package/components/Tasks/Loop/LoopButton.tsx +74 -0
  56. package/components/Tasks/Loop/LoopSetup.tsx +146 -0
  57. package/components/Tasks/Loop/LoopView.tsx +350 -0
  58. package/components/Tasks/Loop/labels.ts +17 -0
  59. package/components/Tasks/TaskRow.tsx +2 -0
  60. package/components/Workspaces/WorkspaceModeForm.tsx +22 -0
  61. package/components/Workspaces/WorkspaceSettings.tsx +2 -0
  62. package/components/ui/segmented.tsx +55 -0
  63. package/components/ui/status-pill.tsx +3 -0
  64. package/contexts/PaneContext.tsx +21 -1
  65. package/data/exports/queries.ts +11 -0
  66. package/data/loops/index.ts +3 -0
  67. package/data/loops/keys.ts +4 -0
  68. package/data/loops/mutations.ts +68 -0
  69. package/data/loops/queries.ts +40 -0
  70. package/data/push/topics.ts +6 -0
  71. package/data/reports/index.ts +115 -0
  72. package/lib/agents/brief.ts +1 -1
  73. package/lib/analyst/forward.ts +1 -1
  74. package/lib/db/changes.ts +7 -0
  75. package/lib/db/migrations/20261021T1600_task_loops.ts +81 -0
  76. package/lib/db/migrations/20261021T1800_health_reports.ts +38 -0
  77. package/lib/done/index.ts +2 -0
  78. package/lib/drafts.ts +2 -0
  79. package/lib/export/markdown.ts +7 -1
  80. package/lib/export/turn.ts +48 -0
  81. package/lib/health/config.ts +2 -0
  82. package/lib/health/env/classes.ts +14 -1
  83. package/lib/health/port-wait-tick.ts +222 -0
  84. package/lib/health/port-wait.ts +160 -0
  85. package/lib/health/tick.ts +9 -2
  86. package/lib/health/types.ts +2 -0
  87. package/lib/loops/analyse.ts +46 -0
  88. package/lib/loops/analyst-prompt.ts +203 -0
  89. package/lib/loops/checkCriteria.ts +114 -0
  90. package/lib/loops/compactEvidence.ts +140 -0
  91. package/lib/loops/controls.ts +317 -0
  92. package/lib/loops/decideStop.ts +113 -0
  93. package/lib/loops/deps.ts +128 -0
  94. package/lib/loops/diffMap.ts +39 -0
  95. package/lib/loops/draftCriteria.ts +71 -0
  96. package/lib/loops/gatherEvidence.ts +265 -0
  97. package/lib/loops/hooks.ts +40 -0
  98. package/lib/loops/index.ts +7 -0
  99. package/lib/loops/route.ts +83 -0
  100. package/lib/loops/runCycle.ts +323 -0
  101. package/lib/loops/screenInstruction.ts +59 -0
  102. package/lib/loops/sendInstruction.ts +83 -0
  103. package/lib/loops/snapshot.ts +38 -0
  104. package/lib/loops/stopLoop.ts +86 -0
  105. package/lib/loops/store.ts +481 -0
  106. package/lib/loops/summary.ts +27 -0
  107. package/lib/loops/tickLoops.ts +141 -0
  108. package/lib/loops/types.ts +223 -0
  109. package/lib/loops/world.test-helpers.ts +223 -0
  110. package/lib/models/plan-step.ts +4 -1
  111. package/lib/orchestrator/digest.ts +2 -1
  112. package/lib/orchestrator/loop-guard.ts +5 -2
  113. package/lib/orchestrator/pr-wake.ts +4 -0
  114. package/lib/orchestrator/watcher.ts +8 -1
  115. package/lib/panes.ts +52 -0
  116. package/lib/plans/acceptance.ts +23 -0
  117. package/lib/{ports.ts → ports/allocatePorts.ts} +30 -7
  118. package/lib/ports/ensurePorts.ts +138 -0
  119. package/lib/ports/findHardcodedPorts.ts +116 -0
  120. package/lib/ports/index.ts +15 -0
  121. package/lib/ports/listListeners.ts +134 -0
  122. package/lib/ports/reassignPort.ts +45 -0
  123. package/lib/ports/types.ts +62 -0
  124. package/lib/ports/whoHolds.ts +155 -0
  125. package/lib/project-config/brief.ts +4 -2
  126. package/lib/project-config/session.ts +19 -3
  127. package/lib/reports/drafts.ts +87 -0
  128. package/lib/reports/end-hook.ts +17 -0
  129. package/lib/reports/estimate.ts +107 -0
  130. package/lib/reports/evidence/build.ts +193 -0
  131. package/lib/reports/evidence/gather.ts +229 -0
  132. package/lib/reports/fixtures.ts +17 -0
  133. package/lib/reports/generate.ts +159 -0
  134. package/lib/reports/hooks.ts +30 -0
  135. package/lib/reports/mode.ts +65 -0
  136. package/lib/reports/model.ts +115 -0
  137. package/lib/reports/read.ts +26 -0
  138. package/lib/reports/store.ts +180 -0
  139. package/lib/reports/template.ts +117 -0
  140. package/lib/reports/types.ts +135 -0
  141. package/lib/reports/validate.ts +142 -0
  142. package/lib/sidebar/groupDrop.ts +36 -0
  143. package/lib/sidebar/row-menu.ts +9 -0
  144. package/lib/tasks/contract/routes.ts +5 -0
  145. package/lib/tasks/contract/types.ts +3 -0
  146. package/lib/tasks/finish.ts +2 -0
  147. package/lib/tasks/move-carry.ts +1 -0
  148. package/lib/windows-ports.ts +36 -18
  149. package/package.json +2 -2
  150. package/stores/analystUi.ts +19 -0
@@ -0,0 +1,131 @@
1
+ "use client";
2
+
3
+ import { toast } from "sonner";
4
+ import { Eye, Loader2 } from "lucide-react";
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuLabel,
10
+ DropdownMenuSeparator,
11
+ DropdownMenuTrigger,
12
+ } from "@/components/ui/dropdown-menu";
13
+ import { usePanes } from "@/contexts/PaneContext";
14
+ import {
15
+ useAskAgain,
16
+ useEndAnalyst,
17
+ useReviewNow,
18
+ useSessionAnalyst,
19
+ useSetAnalystPaused,
20
+ } from "@/data/analyst";
21
+ import type { Session } from "@/lib/db";
22
+ import { cn } from "@/lib/utils";
23
+ import {
24
+ PAIR_STATUS_LABEL,
25
+ pairStatus,
26
+ partnerOf,
27
+ paneTmux,
28
+ type PairStatus,
29
+ } from "./pair";
30
+
31
+ const DOT: Record<PairStatus, string> = {
32
+ paused: "bg-muted-foreground/50",
33
+ reviewing: "bg-primary animate-pulse",
34
+ recommendation: "bg-primary",
35
+ watching: "bg-green-500",
36
+ };
37
+
38
+ const fail = (e: Error) => toast.error(e.message);
39
+
40
+ // A paired chat's link to the other side, with the pairing's controls.
41
+ // Nothing for a session that isn't paired.
42
+ export function PairChip({
43
+ session,
44
+ paneId,
45
+ }: {
46
+ session: Session | null | undefined;
47
+ paneId: string;
48
+ }) {
49
+ const { data: link } = useSessionAnalyst(
50
+ session?.view === "chat" ? session.id : undefined
51
+ );
52
+ const { openBeside } = usePanes();
53
+ const id = link?.id ?? "";
54
+ const review = useReviewNow(id);
55
+ const pause = useSetAnalystPaused(id);
56
+ const end = useEndAnalyst(id);
57
+ const again = useAskAgain(id);
58
+ if (!session || !link || link.endedAt) return null;
59
+
60
+ const status = pairStatus(link);
61
+ const isCoder = link.coder.id === session.id;
62
+ const partner = partnerOf(link, session.id);
63
+ const latest = link.recommendations[0];
64
+
65
+ return (
66
+ <DropdownMenu>
67
+ <DropdownMenuTrigger asChild>
68
+ <button
69
+ type="button"
70
+ onClick={(e) => e.stopPropagation()}
71
+ title={PAIR_STATUS_LABEL[status]}
72
+ aria-label={`${isCoder ? "Analyst" : "Reviewing"} ${partner.name}: ${PAIR_STATUS_LABEL[status]}`}
73
+ className="ring-border/70 bg-muted/60 hover:bg-accent flex h-7 max-w-56 shrink-0 items-center gap-1.5 rounded-full px-2.5 text-xs ring-1 ring-inset"
74
+ >
75
+ <Eye className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
76
+ <span className="truncate">
77
+ {isCoder ? "Analyst" : "Coding agent"}
78
+ </span>
79
+ <span
80
+ className={cn("h-1.5 w-1.5 shrink-0 rounded-full", DOT[status])}
81
+ />
82
+ </button>
83
+ </DropdownMenuTrigger>
84
+ <DropdownMenuContent align="start" className="w-60">
85
+ <DropdownMenuLabel className="space-y-0.5">
86
+ <div className="text-muted-foreground text-[11px] font-normal">
87
+ {isCoder ? "Reviewed by" : "Reviewing"} ·{" "}
88
+ {PAIR_STATUS_LABEL[status]}
89
+ </div>
90
+ <div className="truncate text-sm">{partner.name}</div>
91
+ </DropdownMenuLabel>
92
+ <DropdownMenuItem
93
+ onSelect={() =>
94
+ openBeside(paneId, {
95
+ sessionId: partner.id,
96
+ tmux: paneTmux(partner),
97
+ })
98
+ }
99
+ >
100
+ Open {isCoder ? "analyst" : "coding agent"} beside
101
+ </DropdownMenuItem>
102
+ <DropdownMenuItem
103
+ disabled={review.isPending || !!link.pendingDigestId}
104
+ onSelect={() => review.mutate(undefined, { onError: fail })}
105
+ >
106
+ {review.isPending && <Loader2 className="animate-spin" />}
107
+ {link.pendingDigestId ? "Reviewing…" : "Review now"}
108
+ </DropdownMenuItem>
109
+ {latest?.state === "invalid" && (
110
+ <DropdownMenuItem
111
+ onSelect={() => again.mutate(latest.id, { onError: fail })}
112
+ >
113
+ Ask for the review again
114
+ </DropdownMenuItem>
115
+ )}
116
+ <DropdownMenuItem
117
+ onSelect={() => pause.mutate(!link.paused, { onError: fail })}
118
+ >
119
+ {link.paused ? "Resume reviews" : "Pause reviews"}
120
+ </DropdownMenuItem>
121
+ <DropdownMenuSeparator />
122
+ <DropdownMenuItem
123
+ variant="destructive"
124
+ onSelect={() => end.mutate(true, { onError: fail })}
125
+ >
126
+ Unlink and finish the analyst
127
+ </DropdownMenuItem>
128
+ </DropdownMenuContent>
129
+ </DropdownMenu>
130
+ );
131
+ }
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ import { usePanes } from "@/contexts/PaneContext";
4
+ import { useSessionAnalyst } from "@/data/analyst";
5
+ import type { Session } from "@/lib/db";
6
+ import { cn } from "@/lib/utils";
7
+ import { PAIR_STATUS_LABEL, pairStatus, paneTmux } from "./pair";
8
+
9
+ // A phone shows one pane: a paired chat switches between its two sides
10
+ // here, with a dot on the analyst while a recommendation waits.
11
+ export function PairSwitch({
12
+ session,
13
+ paneId,
14
+ }: {
15
+ session: Session | null | undefined;
16
+ paneId: string;
17
+ }) {
18
+ const { data: link } = useSessionAnalyst(
19
+ session?.view === "chat" ? session.id : undefined
20
+ );
21
+ const { attachSession } = usePanes();
22
+ if (!session || !link || link.endedAt) return null;
23
+
24
+ const status = pairStatus(link);
25
+ const sides = [
26
+ { side: link.coder, label: "Coding agent", note: null },
27
+ {
28
+ side: link.analyst,
29
+ label: "Analyst",
30
+ note: PAIR_STATUS_LABEL[status],
31
+ },
32
+ ];
33
+
34
+ return (
35
+ <div
36
+ role="tablist"
37
+ aria-label="Paired sessions"
38
+ className="bg-muted grid grid-cols-2 gap-1 px-2 pb-1.5"
39
+ onClick={(e) => e.stopPropagation()}
40
+ >
41
+ {sides.map(({ side, label, note }) => {
42
+ const active = side.id === session.id;
43
+ return (
44
+ <button
45
+ key={side.id}
46
+ type="button"
47
+ role="tab"
48
+ aria-selected={active}
49
+ onClick={() =>
50
+ !active && attachSession(paneId, side.id, paneTmux(side))
51
+ }
52
+ className={cn(
53
+ "flex h-11 min-w-0 items-center justify-center gap-1.5 rounded-lg text-sm",
54
+ active
55
+ ? "bg-background text-foreground font-medium shadow-sm"
56
+ : "text-muted-foreground"
57
+ )}
58
+ >
59
+ <span className="truncate">{label}</span>
60
+ {note && status === "recommendation" && !active && (
61
+ <span
62
+ aria-label="Recommendation waiting"
63
+ className="bg-primary h-2 w-2 shrink-0 rounded-full"
64
+ />
65
+ )}
66
+ {note && (
67
+ <span className="text-muted-foreground shrink-0 text-xs font-normal">
68
+ {note}
69
+ </span>
70
+ )}
71
+ </button>
72
+ );
73
+ })}
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,272 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { AlertTriangle, Check, Loader2, RotateCcw, Send } from "lucide-react";
5
+ import { toast } from "sonner";
6
+ import { Button } from "@/components/ui/button";
7
+ import { StatusPill } from "@/components/ui/status-pill";
8
+ import { Textarea } from "@/components/ui/textarea";
9
+ import { useBookmarkChat } from "@/components/Bookmarks/context";
10
+ import { useChatItem } from "@/components/Chat/itemContext";
11
+ import {
12
+ useAskAgain,
13
+ useDismissRecommendation,
14
+ useSendRecommendation,
15
+ useSessionAnalyst,
16
+ } from "@/data/analyst";
17
+ import { parseReviewJson } from "@/lib/analyst/parseReview";
18
+ import type { Issue, Recommendation } from "@/lib/analyst/types";
19
+ import { cn } from "@/lib/utils";
20
+ import { cardView } from "./cardState";
21
+ import { SignalChips } from "./SignalChips";
22
+
23
+ const SEVERITY_DOT: Record<Issue["severity"], string> = {
24
+ high: "bg-red-500",
25
+ medium: "bg-amber-500",
26
+ low: "bg-muted-foreground/50",
27
+ };
28
+
29
+ const fail = (e: Error) => toast.error(e.message);
30
+
31
+ /**
32
+ * An analyst's `agentos-review` block, drawn as a card: its summary, the
33
+ * issues it flagged and the prompt it recommends, with Send to Coding Agent,
34
+ * Edit and Dismiss while the stored card is open.
35
+ */
36
+ export function ReviewCard({ code }: { code: string }) {
37
+ const item = useChatItem();
38
+ const sessionId = useBookmarkChat()?.sessionId;
39
+ const { data: link } = useSessionAnalyst(sessionId);
40
+ const parsed = parseReviewJson(code);
41
+ const rec =
42
+ (item &&
43
+ link &&
44
+ link.analyst.id === sessionId &&
45
+ link.recommendations.find((r) => r.analystItemId === item.itemId)) ||
46
+ null;
47
+
48
+ if (!parsed.ok)
49
+ return (
50
+ <div className="bg-foreground/[0.04] text-muted-foreground not-prose my-3 space-y-2 rounded-xl px-3 py-2.5 text-xs">
51
+ <div className="flex items-center gap-2">
52
+ {item?.streaming ? (
53
+ <>
54
+ <Loader2 className="h-3.5 w-3.5 animate-spin" /> Writing the
55
+ review…
56
+ </>
57
+ ) : (
58
+ <>
59
+ <AlertTriangle className="h-3.5 w-3.5 shrink-0 text-amber-500" />
60
+ {parsed.error}
61
+ </>
62
+ )}
63
+ </div>
64
+ {rec && link && !item?.streaming && (
65
+ <Actions
66
+ linkId={link.id}
67
+ coderName={link.coder.name}
68
+ rec={rec}
69
+ prompt={null}
70
+ view={cardView(rec, null, link.coderLastTurn)}
71
+ />
72
+ )}
73
+ </div>
74
+ );
75
+
76
+ const review = parsed.review;
77
+ const view = cardView(
78
+ rec,
79
+ rec?.verdict ?? review.verdict,
80
+ link?.coderLastTurn ?? null
81
+ );
82
+
83
+ return (
84
+ <div className="bg-card hairline not-prose my-3 space-y-3 rounded-xl p-3 text-sm">
85
+ <div className="flex items-center gap-2">
86
+ <StatusPill tone={view.tone}>{view.label}</StatusPill>
87
+ <span className="text-muted-foreground text-xs">Analyst review</span>
88
+ </div>
89
+ <p className="leading-relaxed">{review.summary}</p>
90
+ {rec?.signals && <SignalChips signals={rec.signals} />}
91
+ {review.issues.length > 0 && (
92
+ <ul className="space-y-2">
93
+ {review.issues.map((issue, i) => (
94
+ <li key={i} className="flex gap-2">
95
+ <span
96
+ aria-label={`${issue.severity} severity`}
97
+ className={cn(
98
+ "mt-1.5 h-2 w-2 shrink-0 rounded-full",
99
+ SEVERITY_DOT[issue.severity]
100
+ )}
101
+ />
102
+ <div className="min-w-0">
103
+ <div className="font-medium">{issue.title}</div>
104
+ {issue.where && (
105
+ <div className="text-muted-foreground truncate font-mono text-[11px]">
106
+ {issue.where}
107
+ </div>
108
+ )}
109
+ {issue.detail && (
110
+ <div className="text-muted-foreground text-xs leading-relaxed">
111
+ {issue.detail}
112
+ </div>
113
+ )}
114
+ </div>
115
+ </li>
116
+ ))}
117
+ </ul>
118
+ )}
119
+ {view.notes.map((note) => (
120
+ <div
121
+ key={note}
122
+ className="flex items-start gap-2 rounded-lg bg-amber-500/10 px-2.5 py-2 text-xs text-amber-700 dark:text-amber-400"
123
+ >
124
+ <AlertTriangle className="mt-px h-3.5 w-3.5 shrink-0" />
125
+ {note}
126
+ </div>
127
+ ))}
128
+ {rec && link ? (
129
+ <Actions
130
+ linkId={link.id}
131
+ coderName={link.coder.name}
132
+ rec={rec}
133
+ prompt={review.prompt ?? null}
134
+ view={view}
135
+ />
136
+ ) : (
137
+ review.prompt && <PromptBox text={review.prompt} />
138
+ )}
139
+ </div>
140
+ );
141
+ }
142
+
143
+ function PromptBox({ text, muted }: { text: string; muted?: boolean }) {
144
+ return (
145
+ <div
146
+ className={cn(
147
+ "bg-foreground/[0.04] max-h-60 overflow-y-auto rounded-lg px-3 py-2 text-[13px] leading-relaxed whitespace-pre-wrap",
148
+ muted && "text-muted-foreground"
149
+ )}
150
+ >
151
+ {text}
152
+ </div>
153
+ );
154
+ }
155
+
156
+ function Actions({
157
+ linkId,
158
+ coderName,
159
+ rec,
160
+ prompt,
161
+ view,
162
+ }: {
163
+ linkId: string;
164
+ coderName: string;
165
+ rec: Recommendation;
166
+ prompt: string | null;
167
+ view: ReturnType<typeof cardView>;
168
+ }) {
169
+ const [draft, setDraft] = useState<string | null>(null);
170
+ const send = useSendRecommendation(linkId);
171
+ const dismiss = useDismissRecommendation(linkId);
172
+ const again = useAskAgain(linkId);
173
+ const busy = send.isPending || dismiss.isPending || again.isPending;
174
+
175
+ if (rec.state === "sent")
176
+ return (
177
+ <div className="space-y-1.5">
178
+ <div className="text-muted-foreground flex items-center gap-1.5 text-xs">
179
+ <Check className="h-3.5 w-3.5 text-green-500" /> Sent to {coderName}
180
+ </div>
181
+ {rec.sentText && <PromptBox text={rec.sentText} muted />}
182
+ </div>
183
+ );
184
+
185
+ if (draft !== null)
186
+ return (
187
+ <div className="space-y-2">
188
+ <Textarea
189
+ autoFocus
190
+ value={draft}
191
+ onChange={(e) => setDraft(e.target.value)}
192
+ aria-label="Prompt for the coding agent"
193
+ className="min-h-[160px]"
194
+ />
195
+ <div className="flex flex-wrap gap-2">
196
+ <Button
197
+ size="sm"
198
+ className="h-11 md:h-8"
199
+ disabled={busy || !draft.trim()}
200
+ onClick={() =>
201
+ send.mutate(
202
+ { recId: rec.id, text: draft.trim() },
203
+ { onError: fail, onSuccess: () => setDraft(null) }
204
+ )
205
+ }
206
+ >
207
+ <Send /> Send to Coding Agent
208
+ </Button>
209
+ <Button
210
+ size="sm"
211
+ variant="ghost"
212
+ className="h-11 md:h-8"
213
+ onClick={() => setDraft(null)}
214
+ >
215
+ Cancel
216
+ </Button>
217
+ </div>
218
+ </div>
219
+ );
220
+
221
+ return (
222
+ <div className="space-y-2">
223
+ {prompt && <PromptBox text={prompt} muted={rec.state !== "open"} />}
224
+ <div className="flex flex-wrap gap-2">
225
+ {view.canSend && (
226
+ <Button
227
+ size="sm"
228
+ className="h-11 md:h-8"
229
+ disabled={busy}
230
+ onClick={() => send.mutate({ recId: rec.id }, { onError: fail })}
231
+ >
232
+ {send.isPending ? <Loader2 className="animate-spin" /> : <Send />}
233
+ Send to Coding Agent
234
+ </Button>
235
+ )}
236
+ {view.canWrite && (
237
+ <Button
238
+ size="sm"
239
+ variant="outline"
240
+ className="h-11 md:h-8"
241
+ disabled={busy}
242
+ onClick={() => setDraft(prompt ?? "")}
243
+ >
244
+ {view.canSend ? "Edit" : "Ask coding agent anyway"}
245
+ </Button>
246
+ )}
247
+ {view.canAskAgain && (
248
+ <Button
249
+ size="sm"
250
+ variant="outline"
251
+ className="h-11 md:h-8"
252
+ disabled={busy}
253
+ onClick={() => again.mutate(rec.id, { onError: fail })}
254
+ >
255
+ <RotateCcw /> Ask again
256
+ </Button>
257
+ )}
258
+ {view.canDismiss && (
259
+ <Button
260
+ size="sm"
261
+ variant="ghost"
262
+ className="h-11 md:h-8"
263
+ disabled={busy}
264
+ onClick={() => dismiss.mutate(rec.id, { onError: fail })}
265
+ >
266
+ Dismiss
267
+ </Button>
268
+ )}
269
+ </div>
270
+ </div>
271
+ );
272
+ }
@@ -0,0 +1,39 @@
1
+ import type { DigestSignals } from "@/lib/analyst/types";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ const plural = (n: number, one: string) => `${n} ${one}${n === 1 ? "" : "s"}`;
5
+
6
+ const shortCommand = (c: string) => (c.length > 40 ? `${c.slice(0, 39)}…` : c);
7
+
8
+ /** What the reviewed turns did, read without a model: the facts under the card. */
9
+ export function SignalChips({ signals: s }: { signals: DigestSignals }) {
10
+ const chips: { text: string; tone?: "good" | "bad" }[] = [];
11
+ chips.push({ text: plural(s.toolCalls, "tool call") });
12
+ if (s.filesChanged.length)
13
+ chips.push({ text: plural(s.filesChanged.length, "file") + " changed" });
14
+ for (const c of s.checks)
15
+ chips.push({
16
+ text: `${shortCommand(c.command)} ${c.passed ? "passed" : "failed"}`,
17
+ tone: c.passed ? "good" : "bad",
18
+ });
19
+ if (s.failedTools)
20
+ chips.push({ text: plural(s.failedTools, "failed tool"), tone: "bad" });
21
+ if (s.errors) chips.push({ text: plural(s.errors, "error"), tone: "bad" });
22
+ return (
23
+ <div className="flex flex-wrap gap-1.5">
24
+ {chips.map((c) => (
25
+ <span
26
+ key={c.text}
27
+ className={cn(
28
+ "bg-foreground/[0.05] text-muted-foreground max-w-full truncate rounded-md px-1.5 py-0.5 font-mono text-[11px]",
29
+ c.tone === "good" &&
30
+ "bg-green-500/10 text-green-700 dark:text-green-400",
31
+ c.tone === "bad" && "bg-red-500/10 text-red-700 dark:text-red-400"
32
+ )}
33
+ >
34
+ {c.text}
35
+ </span>
36
+ ))}
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useSnapshot } from "valtio";
5
+ import { Loader2 } from "lucide-react";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from "@/components/ui/dialog";
14
+ import { Switch } from "@/components/ui/switch";
15
+ import { RoleModelSelect } from "@/components/Models/RoleModelSelect";
16
+ import { analystUi, analystUiActions } from "@/stores/analystUi";
17
+ import { useStartAnalystBeside } from "./pair";
18
+
19
+ // Pairs an analyst with a chat: it reviews each turn the coding agent
20
+ // finishes and recommends the next prompt, which the user sends or not.
21
+ export function StartAnalystSheet() {
22
+ const ui = useSnapshot(analystUi);
23
+ return (
24
+ <Dialog open={ui.open} onOpenChange={(o) => !o && analystUiActions.close()}>
25
+ {ui.open && ui.sessionId && (
26
+ <StartAnalystForm
27
+ key={ui.sessionId}
28
+ sessionId={ui.sessionId}
29
+ name={ui.name}
30
+ paneId={ui.paneId}
31
+ />
32
+ )}
33
+ </Dialog>
34
+ );
35
+ }
36
+
37
+ function StartAnalystForm({
38
+ sessionId,
39
+ name,
40
+ paneId,
41
+ }: {
42
+ sessionId: string;
43
+ name: string;
44
+ paneId: string | null;
45
+ }) {
46
+ const [model, setModel] = useState<string | null>(null);
47
+ const [reviewLast, setReviewLast] = useState(false);
48
+ const start = useStartAnalystBeside();
49
+
50
+ return (
51
+ <DialogContent className="sm:max-w-[460px]">
52
+ <DialogHeader>
53
+ <DialogTitle>Add an analyst</DialogTitle>
54
+ <DialogDescription>
55
+ A second chat that reviews each turn of{" "}
56
+ <span className="text-foreground font-medium">{name}</span>, explains
57
+ it in plain English, flags issues and drafts the next prompt. Nothing
58
+ reaches the coding agent until you press Send.
59
+ </DialogDescription>
60
+ </DialogHeader>
61
+ <div className="space-y-1.5">
62
+ <label
63
+ htmlFor="analyst-model"
64
+ className="text-muted-foreground text-xs font-medium uppercase"
65
+ >
66
+ Analyst model
67
+ </label>
68
+ <RoleModelSelect
69
+ id="analyst-model"
70
+ value={model}
71
+ inheritLabel="The analyst role's model"
72
+ claudeOnly
73
+ onChange={setModel}
74
+ />
75
+ </div>
76
+ <label className="flex min-h-11 items-center justify-between gap-3 text-sm">
77
+ <span>Review the last turn now</span>
78
+ <Switch checked={reviewLast} onCheckedChange={setReviewLast} />
79
+ </label>
80
+ <div className="flex justify-end gap-2">
81
+ <Button
82
+ variant="ghost"
83
+ className="h-11 md:h-9"
84
+ onClick={() => analystUiActions.close()}
85
+ >
86
+ Cancel
87
+ </Button>
88
+ <Button
89
+ className="h-11 md:h-9"
90
+ disabled={start.isPending}
91
+ onClick={async () => {
92
+ const link = await start.run(
93
+ {
94
+ coderSessionId: sessionId,
95
+ analystModel: model ?? undefined,
96
+ reviewLast,
97
+ },
98
+ paneId ?? undefined
99
+ );
100
+ if (link) analystUiActions.close();
101
+ }}
102
+ >
103
+ {start.isPending && <Loader2 className="animate-spin" />}
104
+ Add analyst
105
+ </Button>
106
+ </div>
107
+ </DialogContent>
108
+ );
109
+ }