@mrclrchtr/supi-review 2.6.1 → 2.8.0

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.
@@ -1,12 +1,38 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
3
- import type { ReviewFailureDebugInfo, ReviewItem, ReviewResult } from "../types.ts";
4
- import { formatLevel, formatLocation } from "./format-content.ts";
3
+ import {
4
+ formatChildFailureCopy,
5
+ formatChildFailureDiagnostics,
6
+ } from "../tool/child-failure-diagnostics.ts";
7
+ import type {
8
+ BriefSynthesisRunResult,
9
+ ChildFailureDiagnostics,
10
+ ReviewItem,
11
+ ReviewResult,
12
+ ReviewSnapshot,
13
+ } from "../types.ts";
14
+ import { formatBriefSynthesisFailureCopy, formatLevel, formatLocation } from "./format-content.ts";
5
15
 
6
16
  /** Register the custom TUI renderer for `supi-review` messages. */
17
+ interface BriefSynthesisFailureMessage {
18
+ result: Exclude<BriefSynthesisRunResult, { kind: "success" }>;
19
+ snapshot: ReviewSnapshot;
20
+ modelId: string;
21
+ }
22
+
23
+ interface ReviewMessageDetails {
24
+ result?: ReviewResult;
25
+ briefSynthesisFailure?: BriefSynthesisFailureMessage;
26
+ }
27
+
7
28
  export function registerReviewRenderer(pi: ExtensionAPI): void {
8
29
  pi.registerMessageRenderer("supi-review", (message, { expanded }, theme) => {
9
- const result = (message.details as { result?: ReviewResult } | undefined)?.result;
30
+ const details = message.details as ReviewMessageDetails | undefined;
31
+ if (details?.briefSynthesisFailure) {
32
+ return renderBriefSynthesisFailure(details.briefSynthesisFailure, theme);
33
+ }
34
+
35
+ const result = details?.result;
10
36
  if (!result) {
11
37
  return new Text(theme.fg("dim", "No review data"), 1, 0);
12
38
  }
@@ -26,6 +52,31 @@ export function registerReviewRenderer(pi: ExtensionAPI): void {
26
52
  });
27
53
  }
28
54
 
55
+ function renderBriefSynthesisFailure(
56
+ failure: BriefSynthesisFailureMessage,
57
+ theme: Parameters<Parameters<ExtensionAPI["registerMessageRenderer"]>[1]>[2],
58
+ ): Container {
59
+ const container = new Container();
60
+ const title =
61
+ failure.result.kind === "failed"
62
+ ? "◆ Brief Synthesis Failed"
63
+ : failure.result.kind === "timeout"
64
+ ? "◆ Brief Synthesis Timed Out"
65
+ : "◆ Brief Synthesis Canceled";
66
+ const color = failure.result.kind === "failed" ? "error" : "warning";
67
+
68
+ container.addChild(new Text(theme.fg(color, title), 1, 0));
69
+ container.addChild(new Spacer(1));
70
+ container.addChild(new Text(theme.fg("muted", `Model: ${failure.modelId}`), 1, 0));
71
+ container.addChild(new Text(theme.fg("muted", `Snapshot: ${failure.snapshot.title}`), 1, 0));
72
+ container.addChild(new Spacer(1));
73
+ container.addChild(
74
+ new Text(theme.fg(color, formatBriefSynthesisFailureCopy(failure.result)), 1, 0),
75
+ );
76
+ renderChildFailureDiagnostics(container, failure.result.diagnostics, theme);
77
+ return container;
78
+ }
79
+
29
80
  function renderSuccess(
30
81
  result: Extract<ReviewResult, { kind: "success" }>,
31
82
  theme: Parameters<Parameters<ExtensionAPI["registerMessageRenderer"]>[1]>[2],
@@ -160,8 +211,10 @@ function renderFailed(
160
211
  container.addChild(new Text(theme.fg("muted", `Model: ${result.modelId}`), 1, 0));
161
212
  container.addChild(new Text(theme.fg("muted", `Snapshot: ${result.snapshot.title}`), 1, 0));
162
213
  container.addChild(new Spacer(1));
163
- container.addChild(new Text(theme.fg("error", result.reason), 1, 0));
164
- renderFailureDebug(container, result.debug, theme);
214
+ container.addChild(
215
+ new Text(theme.fg("error", formatChildFailureCopy("reviewer", result.failureCode)), 1, 0),
216
+ );
217
+ renderChildFailureDiagnostics(container, result.diagnostics, theme);
165
218
  return container;
166
219
  }
167
220
 
@@ -175,19 +228,8 @@ function renderTimeout(
175
228
  container.addChild(new Text(theme.fg("muted", `Model: ${result.modelId}`), 1, 0));
176
229
  container.addChild(new Text(theme.fg("muted", `Snapshot: ${result.snapshot.title}`), 1, 0));
177
230
  container.addChild(new Spacer(1));
178
- container.addChild(
179
- new Text(
180
- theme.fg("warning", `Reviewer exceeded the ${(result.timeoutMs / 1000).toFixed(0)}s timeout`),
181
- 1,
182
- 0,
183
- ),
184
- );
185
- if (result.partialOutput) {
186
- container.addChild(new Spacer(1));
187
- container.addChild(new Text(theme.fg("dim", "Partial output:"), 1, 0));
188
- container.addChild(new Text(theme.fg("dim", result.partialOutput.slice(0, 500)), 1, 0));
189
- }
190
- renderFailureDebug(container, result.debug, theme);
231
+ container.addChild(new Text(theme.fg("warning", "Reviewer timed out."), 1, 0));
232
+ renderChildFailureDiagnostics(container, result.diagnostics, theme);
191
233
  return container;
192
234
  }
193
235
 
@@ -197,41 +239,21 @@ function renderCanceled(
197
239
  ): Container {
198
240
  const container = new Container();
199
241
  container.addChild(new Text(theme.fg("warning", "◆ Review Canceled"), 1, 0));
200
- renderFailureDebug(container, result.debug, theme);
242
+ container.addChild(new Text(theme.fg("warning", "Reviewer was canceled."), 1, 0));
243
+ renderChildFailureDiagnostics(container, result.diagnostics, theme);
201
244
  return container;
202
245
  }
203
246
 
204
- function renderFailureDebug(
247
+ function renderChildFailureDiagnostics(
205
248
  container: Container,
206
- debug: ReviewFailureDebugInfo | undefined,
249
+ diagnostics: ChildFailureDiagnostics | undefined,
207
250
  theme: Parameters<Parameters<ExtensionAPI["registerMessageRenderer"]>[1]>[2],
208
251
  ): void {
209
- if (!debug) return;
210
-
211
- const lines = [
212
- `Turns: ${debug.turns} · Tool uses: ${debug.toolUses}`,
213
- debug.tokens
214
- ? `Tokens: ${debug.tokens.input} in / ${debug.tokens.output} out / ${debug.tokens.total} total`
215
- : undefined,
216
- debug.recentEvents && debug.recentEvents.length > 0
217
- ? `Recent events: ${debug.recentEvents.join(" → ")}`
218
- : undefined,
219
- debug.lastAssistantStopReason
220
- ? `Last assistant stop: ${debug.lastAssistantStopReason}`
221
- : undefined,
222
- debug.lastAssistantToolCalls && debug.lastAssistantToolCalls.length > 0
223
- ? `Last assistant tools: ${debug.lastAssistantToolCalls.join(", ")}`
224
- : undefined,
225
- debug.lastAssistantErrorMessage
226
- ? `Last assistant error: ${debug.lastAssistantErrorMessage}`
227
- : undefined,
228
- ].filter((line): line is string => !!line);
229
-
230
- if (lines.length === 0) return;
252
+ if (!diagnostics) return;
231
253
 
232
254
  container.addChild(new Spacer(1));
233
- container.addChild(new Text(theme.fg("dim", "Debug:"), 1, 0));
234
- for (const line of lines) {
255
+ container.addChild(new Text(theme.fg("dim", "Diagnostics:"), 1, 0));
256
+ for (const line of formatChildFailureDiagnostics(diagnostics)) {
235
257
  container.addChild(new Text(theme.fg("dim", line), 1, 0));
236
258
  }
237
259
  }
@@ -1,6 +1,10 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Container, Spacer, Text } from "@earendil-works/pi-tui";
3
3
  import type { PrepareAgentReviewInput, RunAgentReviewInput } from "../tool/agent-review-schemas.ts";
4
+ import {
5
+ formatChildFailureCopy,
6
+ formatChildFailureDiagnostics,
7
+ } from "../tool/child-failure-diagnostics.ts";
4
8
  import type {
5
9
  AgentReviewBatchDetails,
6
10
  PreparedAgentReviewDetails,
@@ -150,6 +154,7 @@ export function renderRunReviewResult(
150
154
  }
151
155
 
152
156
  if (expanded) {
157
+ renderReviewerDiagnostics(container, details, theme);
153
158
  container.addChild(new Spacer(1));
154
159
  for (const line of formatCritique(details.evaluation.critique)) {
155
160
  container.addChild(new Text(theme.fg("dim", line), 0, 0));
@@ -165,6 +170,28 @@ export function renderRunReviewResult(
165
170
  return container;
166
171
  }
167
172
 
173
+ function renderReviewerDiagnostics(
174
+ container: Container,
175
+ details: AgentReviewBatchDetails,
176
+ theme: ReviewTheme,
177
+ ): void {
178
+ if (
179
+ !details.results.some((entry) => entry.result.kind !== "success" && entry.result.diagnostics)
180
+ ) {
181
+ return;
182
+ }
183
+
184
+ container.addChild(new Spacer(1));
185
+ container.addChild(new Text(theme.fg("accent", "Reviewer diagnostics"), 0, 0));
186
+ for (const { assignment, result } of details.results) {
187
+ if (result.kind === "success" || !result.diagnostics) continue;
188
+ container.addChild(new Text(theme.fg("muted", `${assignment.id}:`), 0, 0));
189
+ for (const line of formatChildFailureDiagnostics(result.diagnostics)) {
190
+ container.addChild(new Text(theme.fg("dim", line), 0, 0));
191
+ }
192
+ }
193
+ }
194
+
168
195
  function renderProgress(details: AgentReviewProgressDetails, theme: ReviewTheme): Text {
169
196
  if (details.phase === "prepare") {
170
197
  return new Text(theme.fg("warning", "Synthesizing review brief…"), 0, 0);
@@ -189,7 +216,11 @@ function summarizeResult(result: AgentReviewBatchDetails["results"][number]["res
189
216
  text: `${result.output.items.length} item(s), ${result.output.overall_correctness}`,
190
217
  };
191
218
  case "failed":
192
- return { icon: "✗", color: "error", text: result.reason };
219
+ return {
220
+ icon: "✗",
221
+ color: "error",
222
+ text: formatChildFailureCopy("reviewer", result.failureCode),
223
+ };
193
224
  case "canceled":
194
225
  return { icon: "○", color: "warning", text: "canceled" };
195
226
  case "timeout":
@@ -1,124 +0,0 @@
1
- import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
2
- import type { ReviewFailureDebugInfo, ReviewProgress } from "../types.ts";
3
- import { buildProgressTokens, extractAssistantText } from "./runner-helpers.ts";
4
-
5
- export const RECENT_EVENTS_MAX = 10;
6
- export const LAST_ASSISTANT_TEXT_DEBUG_MAX = 2_000;
7
-
8
- export interface LastAssistantDebugInfo {
9
- text?: string;
10
- stopReason?: string;
11
- errorMessage?: string;
12
- toolCalls?: string[];
13
- }
14
-
15
- export function extractLastAssistantDebugFromMessages(
16
- messages: ArrayLike<Record<string, unknown>> | undefined,
17
- ): LastAssistantDebugInfo | undefined {
18
- if (!messages) return undefined;
19
- for (let i = messages.length - 1; i >= 0; i--) {
20
- const message = messages[i];
21
- if (message?.role !== "assistant") continue;
22
-
23
- const text = extractAssistantText(message.content);
24
- const stopReason =
25
- typeof message.stopReason === "string" ? (message.stopReason as string) : undefined;
26
- const errorMessage =
27
- typeof message.errorMessage === "string" ? (message.errorMessage as string) : undefined;
28
- const toolCalls = extractAssistantToolCalls(message.content);
29
-
30
- return {
31
- text: text ? truncateText(text, LAST_ASSISTANT_TEXT_DEBUG_MAX) : undefined,
32
- stopReason,
33
- errorMessage,
34
- toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
35
- };
36
- }
37
- return undefined;
38
- }
39
-
40
- export function extractLastAssistantDebug(
41
- session: AgentSession,
42
- ): LastAssistantDebugInfo | undefined {
43
- return extractLastAssistantDebugFromMessages(
44
- session.messages as unknown as Array<Record<string, unknown>>,
45
- );
46
- }
47
-
48
- function extractAssistantToolCalls(content: unknown): string[] {
49
- if (!Array.isArray(content)) return [];
50
-
51
- return content
52
- .map((part) => {
53
- if (typeof part !== "object" || !part) return undefined;
54
- const toolPart = part as { type?: unknown; name?: unknown };
55
- return toolPart.type === "toolCall" && typeof toolPart.name === "string"
56
- ? toolPart.name
57
- : undefined;
58
- })
59
- .filter((name): name is string => !!name);
60
- }
61
-
62
- export function summarizeSessionEvent(event: AgentSessionEvent): string | undefined {
63
- switch (event.type) {
64
- case "message_end": {
65
- const message = event.message as unknown as Record<string, unknown>;
66
- if (message.role !== "assistant") return undefined;
67
- const stopReason =
68
- typeof message.stopReason === "string" ? String(message.stopReason) : undefined;
69
- const suffix = stopReason ? `:${stopReason}` : "";
70
- return `assistant:end${suffix}`;
71
- }
72
- case "tool_execution_start":
73
- return `tool:start:${event.toolName}`;
74
- case "tool_execution_end":
75
- return `tool:end:${event.toolName}${event.isError ? ":error" : ""}`;
76
- case "turn_end":
77
- return "turn:end";
78
- case "agent_end":
79
- return `agent:end${event.willRetry ? ":retry" : ""}`;
80
- case "agent_settled":
81
- return "agent:settled";
82
- case "auto_retry_start":
83
- return `retry:start:${event.attempt}/${event.maxAttempts}`;
84
- case "auto_retry_end":
85
- return `retry:end:${event.success ? "success" : "failed"}`;
86
- default:
87
- return undefined;
88
- }
89
- }
90
-
91
- export function pushRecentEvent(recentEvents: string[], summary: string | undefined): void {
92
- if (!summary) return;
93
- recentEvents.push(summary);
94
- if (recentEvents.length > RECENT_EVENTS_MAX) {
95
- recentEvents.splice(0, recentEvents.length - RECENT_EVENTS_MAX);
96
- }
97
- }
98
-
99
- export interface BuildFailureDebugInput {
100
- progress: ReviewProgress;
101
- session: AgentSession;
102
- recentEvents: string[];
103
- }
104
-
105
- export function buildFailureDebug(input: BuildFailureDebugInput): ReviewFailureDebugInfo {
106
- input.progress.tokens = buildProgressTokens(() => input.session.getSessionStats());
107
- const lastAssistant = extractLastAssistantDebug(input.session);
108
-
109
- return {
110
- turns: input.progress.turns,
111
- toolUses: input.progress.toolUses,
112
- tokens: input.progress.tokens,
113
- recentEvents: input.recentEvents.length > 0 ? [...input.recentEvents] : undefined,
114
- lastAssistantText: lastAssistant?.text,
115
- lastAssistantStopReason: lastAssistant?.stopReason,
116
- lastAssistantErrorMessage: lastAssistant?.errorMessage,
117
- lastAssistantToolCalls: lastAssistant?.toolCalls,
118
- };
119
- }
120
-
121
- function truncateText(text: string, maxLen: number): string {
122
- if (text.length <= maxLen) return text;
123
- return `${text.slice(0, maxLen)}... (${text.length - maxLen} more chars)`;
124
- }