@oh-my-pi/pi-coding-agent 1.341.0 → 2.1.1337

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 (158) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +1 -1
  3. package/examples/custom-tools/subagent/index.ts +1 -1
  4. package/package.json +10 -9
  5. package/src/bun-imports.d.ts +16 -0
  6. package/src/cli/args.ts +5 -6
  7. package/src/cli/file-processor.ts +3 -3
  8. package/src/cli/list-models.ts +2 -2
  9. package/src/cli/plugin-cli.ts +1 -1
  10. package/src/cli/session-picker.ts +2 -2
  11. package/src/cli/update-cli.ts +273 -0
  12. package/src/cli.ts +1 -1
  13. package/src/config.ts +23 -75
  14. package/src/core/agent-session.ts +158 -16
  15. package/src/core/auth-storage.ts +2 -3
  16. package/src/core/bash-executor.ts +50 -10
  17. package/src/core/compaction/branch-summarization.ts +5 -5
  18. package/src/core/compaction/compaction.ts +3 -3
  19. package/src/core/compaction/index.ts +3 -3
  20. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  21. package/src/core/custom-commands/index.ts +15 -0
  22. package/src/core/custom-commands/loader.ts +232 -0
  23. package/src/core/custom-commands/types.ts +112 -0
  24. package/src/core/custom-tools/index.ts +3 -3
  25. package/src/core/custom-tools/loader.ts +10 -8
  26. package/src/core/custom-tools/types.ts +11 -6
  27. package/src/core/custom-tools/wrapper.ts +2 -1
  28. package/src/core/exec.ts +22 -12
  29. package/src/core/export-html/index.ts +38 -123
  30. package/src/core/export-html/template.css +0 -7
  31. package/src/core/export-html/template.html +3 -4
  32. package/src/core/export-html/template.macro.ts +24 -0
  33. package/src/core/file-mentions.ts +54 -0
  34. package/src/core/hooks/index.ts +5 -5
  35. package/src/core/hooks/loader.ts +21 -16
  36. package/src/core/hooks/runner.ts +6 -6
  37. package/src/core/hooks/tool-wrapper.ts +2 -2
  38. package/src/core/hooks/types.ts +12 -15
  39. package/src/core/index.ts +6 -6
  40. package/src/core/logger.ts +112 -0
  41. package/src/core/mcp/client.ts +3 -3
  42. package/src/core/mcp/config.ts +1 -1
  43. package/src/core/mcp/index.ts +12 -12
  44. package/src/core/mcp/loader.ts +2 -2
  45. package/src/core/mcp/manager.ts +6 -6
  46. package/src/core/mcp/tool-bridge.ts +3 -3
  47. package/src/core/mcp/transports/http.ts +1 -1
  48. package/src/core/mcp/transports/index.ts +2 -2
  49. package/src/core/mcp/transports/stdio.ts +1 -1
  50. package/src/core/messages.ts +22 -0
  51. package/src/core/model-registry.ts +2 -2
  52. package/src/core/model-resolver.ts +2 -2
  53. package/src/core/plugins/doctor.ts +1 -1
  54. package/src/core/plugins/index.ts +6 -6
  55. package/src/core/plugins/installer.ts +4 -4
  56. package/src/core/plugins/loader.ts +4 -9
  57. package/src/core/plugins/manager.ts +5 -5
  58. package/src/core/plugins/paths.ts +3 -3
  59. package/src/core/sdk.ts +77 -35
  60. package/src/core/session-manager.ts +6 -6
  61. package/src/core/settings-manager.ts +16 -3
  62. package/src/core/skills.ts +5 -5
  63. package/src/core/slash-commands.ts +60 -45
  64. package/src/core/system-prompt.ts +6 -6
  65. package/src/core/title-generator.ts +2 -2
  66. package/src/core/tools/bash.ts +32 -155
  67. package/src/core/tools/context.ts +2 -2
  68. package/src/core/tools/edit-diff.ts +3 -3
  69. package/src/core/tools/edit.ts +18 -5
  70. package/src/core/tools/exa/company.ts +3 -3
  71. package/src/core/tools/exa/index.ts +16 -17
  72. package/src/core/tools/exa/linkedin.ts +3 -3
  73. package/src/core/tools/exa/mcp-client.ts +9 -9
  74. package/src/core/tools/exa/render.ts +5 -5
  75. package/src/core/tools/exa/researcher.ts +3 -3
  76. package/src/core/tools/exa/search.ts +6 -5
  77. package/src/core/tools/exa/types.ts +5 -6
  78. package/src/core/tools/exa/websets.ts +3 -3
  79. package/src/core/tools/find.ts +3 -3
  80. package/src/core/tools/grep.ts +3 -3
  81. package/src/core/tools/index.ts +48 -34
  82. package/src/core/tools/ls.ts +4 -4
  83. package/src/core/tools/lsp/client.ts +161 -90
  84. package/src/core/tools/lsp/config.ts +1 -1
  85. package/src/core/tools/lsp/edits.ts +2 -2
  86. package/src/core/tools/lsp/index.ts +15 -13
  87. package/src/core/tools/lsp/render.ts +2 -2
  88. package/src/core/tools/lsp/rust-analyzer.ts +3 -3
  89. package/src/core/tools/lsp/utils.ts +1 -1
  90. package/src/core/tools/notebook.ts +1 -1
  91. package/src/core/tools/output.ts +175 -0
  92. package/src/core/tools/read.ts +7 -7
  93. package/src/core/tools/renderers.ts +92 -13
  94. package/src/core/tools/review.ts +268 -0
  95. package/src/core/tools/task/agents.ts +22 -38
  96. package/src/core/tools/task/bundled-agents/reviewer.md +52 -37
  97. package/src/core/tools/task/commands.ts +31 -10
  98. package/src/core/tools/task/discovery.ts +2 -2
  99. package/src/core/tools/task/executor.ts +145 -28
  100. package/src/core/tools/task/index.ts +78 -30
  101. package/src/core/tools/task/model-resolver.ts +30 -20
  102. package/src/core/tools/task/parallel.ts +1 -1
  103. package/src/core/tools/task/render.ts +219 -30
  104. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  105. package/src/core/tools/task/types.ts +36 -2
  106. package/src/core/tools/web-fetch.ts +5 -3
  107. package/src/core/tools/web-search/auth.ts +1 -1
  108. package/src/core/tools/web-search/index.ts +17 -15
  109. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  110. package/src/core/tools/web-search/providers/exa.ts +3 -5
  111. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  112. package/src/core/tools/web-search/render.ts +3 -3
  113. package/src/core/tools/write.ts +4 -4
  114. package/src/index.ts +29 -18
  115. package/src/main.ts +50 -33
  116. package/src/migrations.ts +3 -3
  117. package/src/modes/index.ts +5 -5
  118. package/src/modes/interactive/components/armin.ts +1 -1
  119. package/src/modes/interactive/components/assistant-message.ts +1 -1
  120. package/src/modes/interactive/components/bash-execution.ts +4 -4
  121. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  122. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  123. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  124. package/src/modes/interactive/components/diff.ts +1 -1
  125. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  126. package/src/modes/interactive/components/footer.ts +5 -5
  127. package/src/modes/interactive/components/hook-editor.ts +2 -2
  128. package/src/modes/interactive/components/hook-input.ts +2 -2
  129. package/src/modes/interactive/components/hook-message.ts +3 -3
  130. package/src/modes/interactive/components/hook-selector.ts +2 -2
  131. package/src/modes/interactive/components/model-selector.ts +281 -59
  132. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  133. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  134. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  135. package/src/modes/interactive/components/session-selector.ts +4 -4
  136. package/src/modes/interactive/components/settings-defs.ts +1 -1
  137. package/src/modes/interactive/components/settings-selector.ts +5 -5
  138. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  139. package/src/modes/interactive/components/theme-selector.ts +2 -2
  140. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  141. package/src/modes/interactive/components/tool-execution.ts +26 -8
  142. package/src/modes/interactive/components/tree-selector.ts +3 -3
  143. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  144. package/src/modes/interactive/components/user-message.ts +1 -1
  145. package/src/modes/interactive/components/welcome.ts +2 -2
  146. package/src/modes/interactive/interactive-mode.ts +86 -42
  147. package/src/modes/interactive/theme/theme.ts +15 -17
  148. package/src/modes/print-mode.ts +4 -3
  149. package/src/modes/rpc/rpc-client.ts +4 -4
  150. package/src/modes/rpc/rpc-mode.ts +22 -12
  151. package/src/modes/rpc/rpc-types.ts +3 -3
  152. package/src/utils/changelog.ts +2 -2
  153. package/src/utils/clipboard.ts +1 -1
  154. package/src/utils/shell-snapshot.ts +218 -0
  155. package/src/utils/shell.ts +93 -13
  156. package/src/utils/tools-manager.ts +1 -1
  157. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  158. package/src/core/tools/exa/logger.ts +0 -56
@@ -7,10 +7,20 @@
7
7
 
8
8
  import path from "node:path";
9
9
  import type { Component } from "@oh-my-pi/pi-tui";
10
- import { Text } from "@oh-my-pi/pi-tui";
11
- import type { Theme } from "../../../modes/interactive/theme/theme.js";
12
- import type { RenderResultOptions } from "../../custom-tools/types.js";
13
- import type { AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types.js";
10
+ import { Container, Text } from "@oh-my-pi/pi-tui";
11
+ import type { Theme } from "../../../modes/interactive/theme/theme";
12
+ import type { RenderResultOptions } from "../../custom-tools/types";
13
+ import type { ReportFindingDetails, SubmitReviewDetails } from "../review";
14
+ import { subprocessToolRegistry } from "./subprocess-tool-registry";
15
+ import type { AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types";
16
+
17
+ /** Priority labels for review findings */
18
+ const PRIORITY_LABELS: Record<number, string> = {
19
+ 0: "P0",
20
+ 1: "P1",
21
+ 2: "P2",
22
+ 3: "P3",
23
+ };
14
24
 
15
25
  /**
16
26
  * Format token count for display (e.g., 1.5k, 25k).
@@ -52,6 +62,8 @@ function getStatusIcon(status: AgentProgress["status"]): string {
52
62
  return "✓";
53
63
  case "failed":
54
64
  return "✗";
65
+ case "aborted":
66
+ return "⊘";
55
67
  }
56
68
  }
57
69
 
@@ -82,10 +94,16 @@ function renderAgentProgress(progress: AgentProgress, isLast: boolean, expanded:
82
94
  const continuePrefix = isLast ? " " : "│ ";
83
95
 
84
96
  const icon = getStatusIcon(progress.status);
85
- const iconColor = progress.status === "completed" ? "success" : progress.status === "failed" ? "error" : "accent";
97
+ const iconColor =
98
+ progress.status === "completed"
99
+ ? "success"
100
+ : progress.status === "failed" || progress.status === "aborted"
101
+ ? "error"
102
+ : "accent";
86
103
 
87
- // Main status line
88
- let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", progress.agent)}`;
104
+ // Main status line - include index for Output tool ID derivation
105
+ const agentId = `${progress.agent}(${progress.index})`;
106
+ let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", agentId)}`;
89
107
 
90
108
  if (progress.status === "running") {
91
109
  const taskPreview = truncate(progress.task, 40);
@@ -98,6 +116,8 @@ function renderAgentProgress(progress: AgentProgress, isLast: boolean, expanded:
98
116
  statusLine += `: ${theme.fg("success", "done")}`;
99
117
  statusLine += ` · ${theme.fg("dim", `${progress.toolCount} tools`)}`;
100
118
  statusLine += ` · ${theme.fg("dim", `${formatTokens(progress.tokens)} tokens`)}`;
119
+ } else if (progress.status === "aborted") {
120
+ statusLine += `: ${theme.fg("error", "aborted")}`;
101
121
  } else if (progress.status === "failed") {
102
122
  statusLine += `: ${theme.fg("error", "failed")}`;
103
123
  }
@@ -119,6 +139,26 @@ function renderAgentProgress(progress: AgentProgress, isLast: boolean, expanded:
119
139
  lines.push(toolLine);
120
140
  }
121
141
 
142
+ // Render extracted tool data inline (e.g., review findings)
143
+ if (progress.extractedToolData) {
144
+ for (const [toolName, dataArray] of Object.entries(progress.extractedToolData)) {
145
+ const handler = subprocessToolRegistry.getHandler(toolName);
146
+ if (handler?.renderInline) {
147
+ // Show last few items inline
148
+ const recentData = (dataArray as unknown[]).slice(-3);
149
+ for (const data of recentData) {
150
+ const component = handler.renderInline(data, theme);
151
+ if (component instanceof Text) {
152
+ lines.push(`${continuePrefix}${component.getText()}`);
153
+ }
154
+ }
155
+ if (dataArray.length > 3) {
156
+ lines.push(`${continuePrefix}${theme.fg("dim", `... ${dataArray.length - 3} more`)}`);
157
+ }
158
+ }
159
+ }
160
+ }
161
+
122
162
  // Expanded view: recent output and tools
123
163
  if (expanded && progress.status === "running") {
124
164
  // Recent output
@@ -130,6 +170,93 @@ function renderAgentProgress(progress: AgentProgress, isLast: boolean, expanded:
130
170
  return lines;
131
171
  }
132
172
 
173
+ /**
174
+ * Render review result with combined verdict + findings in tree structure.
175
+ */
176
+ function renderReviewResult(
177
+ summary: SubmitReviewDetails,
178
+ findings: ReportFindingDetails[],
179
+ continuePrefix: string,
180
+ expanded: boolean,
181
+ theme: Theme,
182
+ ): string[] {
183
+ const lines: string[] = [];
184
+
185
+ // Verdict line
186
+ const verdictColor = summary.overall_correctness === "correct" ? "success" : "error";
187
+ const verdictIcon = summary.overall_correctness === "correct" ? "✓" : "✗";
188
+ lines.push(
189
+ `${continuePrefix}${theme.fg(verdictColor, verdictIcon)} Patch is ${theme.fg(verdictColor, summary.overall_correctness)} ${theme.fg("dim", `(${(summary.confidence * 100).toFixed(0)}% confidence)`)}`,
190
+ );
191
+
192
+ // Explanation preview (first ~80 chars when collapsed, full when expanded)
193
+ if (summary.explanation) {
194
+ if (expanded) {
195
+ // Full explanation, wrapped
196
+ const explanationLines = summary.explanation.split("\n");
197
+ for (const line of explanationLines) {
198
+ lines.push(`${continuePrefix}${theme.fg("dim", line)}`);
199
+ }
200
+ } else {
201
+ // Preview: first sentence or ~100 chars
202
+ const preview = truncate(`${summary.explanation.split(/[.!?]/)[0]}.`, 100);
203
+ lines.push(`${continuePrefix}${theme.fg("dim", preview)}`);
204
+ }
205
+ }
206
+
207
+ // Findings in tree structure
208
+ if (findings.length > 0) {
209
+ lines.push(`${continuePrefix}`); // Spacing
210
+ lines.push(...renderFindings(findings, continuePrefix, expanded, theme));
211
+ }
212
+
213
+ return lines;
214
+ }
215
+
216
+ /**
217
+ * Render review findings list (used with and without submit_review).
218
+ */
219
+ function renderFindings(
220
+ findings: ReportFindingDetails[],
221
+ continuePrefix: string,
222
+ expanded: boolean,
223
+ theme: Theme,
224
+ ): string[] {
225
+ const lines: string[] = [];
226
+ const displayCount = expanded ? findings.length : Math.min(3, findings.length);
227
+
228
+ for (let i = 0; i < displayCount; i++) {
229
+ const finding = findings[i];
230
+ const isLastFinding = i === displayCount - 1 && (expanded || findings.length <= 3);
231
+ const findingPrefix = isLastFinding ? "└─" : "├─";
232
+ const findingContinue = isLastFinding ? " " : "│ ";
233
+
234
+ const priority = PRIORITY_LABELS[finding.priority] ?? "P?";
235
+ const color = finding.priority === 0 ? "error" : finding.priority === 1 ? "warning" : "muted";
236
+ const titleText = finding.title.replace(/^\[P\d\]\s*/, "");
237
+ const loc = `${path.basename(finding.file_path)}:${finding.line_start}`;
238
+
239
+ lines.push(
240
+ `${continuePrefix}${findingPrefix} ${theme.fg(color, `[${priority}]`)} ${titleText} ${theme.fg("dim", loc)}`,
241
+ );
242
+
243
+ // Show body when expanded
244
+ if (expanded && finding.body) {
245
+ // Wrap body text
246
+ const bodyLines = finding.body.split("\n");
247
+ for (const bodyLine of bodyLines) {
248
+ lines.push(`${continuePrefix}${findingContinue}${theme.fg("dim", bodyLine)}`);
249
+ }
250
+ }
251
+ }
252
+
253
+ if (!expanded && findings.length > 3) {
254
+ lines.push(`${continuePrefix}${theme.fg("dim", `... ${findings.length - 3} more findings`)}`);
255
+ }
256
+
257
+ return lines;
258
+ }
259
+
133
260
  /**
134
261
  * Render final result for a single agent.
135
262
  */
@@ -138,14 +265,19 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
138
265
  const prefix = isLast ? "└─" : "├─";
139
266
  const continuePrefix = isLast ? " " : "│ ";
140
267
 
141
- const success = result.exitCode === 0;
142
- const icon = success ? "✓" : "✗";
268
+ const aborted = result.aborted ?? false;
269
+ const success = !aborted && result.exitCode === 0;
270
+ const icon = aborted ? "⊘" : success ? "✓" : "✗";
143
271
  const iconColor = success ? "success" : "error";
144
-
145
- // Main status line
146
- let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", result.agent)}`;
147
- statusLine += `: ${theme.fg(iconColor, success ? "done" : "failed")}`;
148
- statusLine += ` · ${theme.fg("dim", `${formatTokens(result.tokens)} tokens`)}`;
272
+ const statusText = aborted ? "aborted" : success ? "done" : "failed";
273
+
274
+ // Main status line - include index for Output tool ID derivation
275
+ const agentId = `${result.agent}(${result.index})`;
276
+ let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", agentId)}`;
277
+ statusLine += `: ${theme.fg(iconColor, statusText)}`;
278
+ if (result.tokens > 0) {
279
+ statusLine += ` · ${theme.fg("dim", `${formatTokens(result.tokens)} tokens`)}`;
280
+ }
149
281
  statusLine += ` · ${theme.fg("dim", formatDuration(result.durationMs))}`;
150
282
 
151
283
  if (result.truncated) {
@@ -154,16 +286,69 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
154
286
 
155
287
  lines.push(statusLine);
156
288
 
157
- // Output preview
158
- const outputLines = result.output.split("\n").filter((l) => l.trim());
159
- const previewCount = expanded ? 8 : 3;
289
+ // Check for review result (submit_review + report_finding)
290
+ const submitReviewData = result.extractedToolData?.submit_review as SubmitReviewDetails[] | undefined;
291
+ const reportFindingData = result.extractedToolData?.report_finding as ReportFindingDetails[] | undefined;
292
+
293
+ if (submitReviewData && submitReviewData.length > 0) {
294
+ // Use combined review renderer
295
+ const summary = submitReviewData[submitReviewData.length - 1];
296
+ const findings = reportFindingData ?? [];
297
+ lines.push(...renderReviewResult(summary, findings, continuePrefix, expanded, theme));
298
+ return lines;
299
+ }
300
+ if (reportFindingData && reportFindingData.length > 0) {
301
+ lines.push(
302
+ `${continuePrefix}${theme.fg("warning", "!")} ${theme.fg("dim", "Review summary missing (submit_review not called)")}`,
303
+ );
304
+ lines.push(`${continuePrefix}`); // Spacing
305
+ lines.push(...renderFindings(reportFindingData, continuePrefix, expanded, theme));
306
+ return lines;
307
+ }
160
308
 
161
- for (const line of outputLines.slice(0, previewCount)) {
162
- lines.push(`${continuePrefix}${theme.fg("dim", truncate(line, 70))}`);
309
+ // Check for extracted tool data with custom renderers (skip review tools)
310
+ let hasCustomRendering = false;
311
+ if (result.extractedToolData) {
312
+ for (const [toolName, dataArray] of Object.entries(result.extractedToolData)) {
313
+ // Skip review tools - handled above
314
+ if (toolName === "submit_review" || toolName === "report_finding") continue;
315
+
316
+ const handler = subprocessToolRegistry.getHandler(toolName);
317
+ if (handler?.renderFinal && (dataArray as unknown[]).length > 0) {
318
+ hasCustomRendering = true;
319
+ const component = handler.renderFinal(dataArray as unknown[], theme, expanded);
320
+ if (component instanceof Text) {
321
+ // Prefix each line with continuePrefix
322
+ const text = component.getText();
323
+ for (const line of text.split("\n")) {
324
+ if (line.trim()) {
325
+ lines.push(`${continuePrefix}${line}`);
326
+ }
327
+ }
328
+ } else if (component instanceof Container) {
329
+ // For containers, render each child
330
+ for (const child of (component as Container).children) {
331
+ if (child instanceof Text) {
332
+ lines.push(`${continuePrefix}${child.getText()}`);
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
163
338
  }
164
339
 
165
- if (outputLines.length > previewCount) {
166
- lines.push(`${continuePrefix}${theme.fg("dim", `... ${outputLines.length - previewCount} more lines`)}`);
340
+ // Fallback to output preview if no custom rendering
341
+ if (!hasCustomRendering) {
342
+ const outputLines = result.output.split("\n").filter((l) => l.trim());
343
+ const previewCount = expanded ? 8 : 3;
344
+
345
+ for (const line of outputLines.slice(0, previewCount)) {
346
+ lines.push(`${continuePrefix}${theme.fg("dim", truncate(line, 70))}`);
347
+ }
348
+
349
+ if (outputLines.length > previewCount) {
350
+ lines.push(`${continuePrefix}${theme.fg("dim", `... ${outputLines.length - previewCount} more lines`)}`);
351
+ }
167
352
  }
168
353
 
169
354
  // Error message
@@ -207,21 +392,25 @@ export function renderResult(
207
392
  });
208
393
 
209
394
  // Summary line
210
- const successCount = details.results.filter((r) => r.exitCode === 0).length;
211
- const failCount = details.results.length - successCount;
395
+ const abortedCount = details.results.filter((r) => r.aborted).length;
396
+ const successCount = details.results.filter((r) => !r.aborted && r.exitCode === 0).length;
397
+ const failCount = details.results.length - successCount - abortedCount;
212
398
  let summary = `\n${theme.fg("dim", "Total:")} `;
213
- summary += theme.fg("success", `${successCount} succeeded`);
399
+ if (abortedCount > 0) {
400
+ summary += theme.fg("error", `${abortedCount} aborted`);
401
+ if (successCount > 0 || failCount > 0) summary += ", ";
402
+ }
403
+ if (successCount > 0) {
404
+ summary += theme.fg("success", `${successCount} succeeded`);
405
+ if (failCount > 0) summary += ", ";
406
+ }
214
407
  if (failCount > 0) {
215
- summary += `, ${theme.fg("error", `${failCount} failed`)}`;
408
+ summary += theme.fg("error", `${failCount} failed`);
216
409
  }
217
410
  summary += ` · ${theme.fg("dim", formatDuration(details.totalDurationMs))}`;
218
411
  lines.push(summary);
219
412
 
220
- // Artifacts location
221
- if (details.outputPaths && details.outputPaths.length > 0) {
222
- const artifactsDir = path.dirname(details.outputPaths[0]);
223
- lines.push(`${theme.fg("dim", "Artifacts:")} ${theme.fg("muted", artifactsDir)}`);
224
- }
413
+ // Artifacts suppressed from user view - available via session file
225
414
  }
226
415
 
227
416
  if (lines.length === 0) {
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Registry for handling tool events from subprocess agents.
3
+ *
4
+ * Tools can register handlers to:
5
+ * - Extract structured data from their execution results
6
+ * - Trigger subprocess termination on completion
7
+ * - Provide custom rendering for realtime/final display
8
+ */
9
+
10
+ import type { Component } from "@oh-my-pi/pi-tui";
11
+ import type { Theme } from "../../../modes/interactive/theme/theme";
12
+
13
+ /** Event from subprocess tool execution (parsed from JSONL) */
14
+ export interface SubprocessToolEvent {
15
+ toolName: string;
16
+ toolCallId: string;
17
+ args?: Record<string, unknown>;
18
+ result?: {
19
+ content: Array<{ type: string; text?: string }>;
20
+ details?: unknown;
21
+ };
22
+ isError?: boolean;
23
+ }
24
+
25
+ /** Handler for subprocess tool events */
26
+ export interface SubprocessToolHandler<TData = unknown> {
27
+ /**
28
+ * Extract structured data from tool result.
29
+ * Extracted data is accumulated in progress.extractedToolData[toolName][].
30
+ */
31
+ extractData?: (event: SubprocessToolEvent) => TData | undefined;
32
+
33
+ /**
34
+ * Whether this tool's completion should terminate the subprocess.
35
+ * Return true to send SIGTERM after the tool completes.
36
+ */
37
+ shouldTerminate?: (event: SubprocessToolEvent) => boolean;
38
+
39
+ /**
40
+ * Render a single data item inline during streaming progress.
41
+ * Called for each tool execution end event.
42
+ */
43
+ renderInline?: (data: TData, theme: Theme) => Component;
44
+
45
+ /**
46
+ * Render accumulated data in the final result view.
47
+ * Called once with all accumulated data for this tool.
48
+ */
49
+ renderFinal?: (allData: TData[], theme: Theme, expanded: boolean) => Component;
50
+ }
51
+
52
+ /** Registry for subprocess tool handlers */
53
+ class SubprocessToolRegistryImpl {
54
+ private handlers = new Map<string, SubprocessToolHandler>();
55
+
56
+ /**
57
+ * Register a handler for a tool's subprocess events.
58
+ */
59
+ register<T>(toolName: string, handler: SubprocessToolHandler<T>): void {
60
+ this.handlers.set(toolName, handler as SubprocessToolHandler);
61
+ }
62
+
63
+ /**
64
+ * Get the handler for a tool, if registered.
65
+ */
66
+ getHandler(toolName: string): SubprocessToolHandler | undefined {
67
+ return this.handlers.get(toolName);
68
+ }
69
+
70
+ /**
71
+ * Check if a tool has a registered handler.
72
+ */
73
+ hasHandler(toolName: string): boolean {
74
+ return this.handlers.has(toolName);
75
+ }
76
+
77
+ /**
78
+ * Get all registered tool names.
79
+ */
80
+ getRegisteredTools(): string[] {
81
+ return Array.from(this.handlers.keys());
82
+ }
83
+ }
84
+
85
+ /** Singleton registry instance */
86
+ export const subprocessToolRegistry = new SubprocessToolRegistryImpl();
87
+
88
+ /** Type helper for extracted tool data in progress/result */
89
+ export type ExtractedToolData = Record<string, unknown[]>;
@@ -27,9 +27,12 @@ export const MAX_OUTPUT_LINES = 5000;
27
27
  /** Maximum agents to show in description */
28
28
  export const MAX_AGENTS_IN_DESCRIPTION = 10;
29
29
 
30
- /** Environment variable to inhibit subagent spawning */
30
+ /** Environment variable to inhibit subagent spawning (legacy, still checked for backwards compat) */
31
31
  export const PI_NO_SUBAGENTS_ENV = "PI_NO_SUBAGENTS";
32
32
 
33
+ /** Environment variable containing blocked agent name (self-recursion prevention) */
34
+ export const PI_BLOCKED_AGENT_ENV = "PI_BLOCKED_AGENT";
35
+
33
36
  /** Task tool parameters */
34
37
  export const taskSchema = Type.Object({
35
38
  context: Type.Optional(Type.String({ description: "Shared context prepended to all task prompts" })),
@@ -41,6 +44,30 @@ export const taskSchema = Type.Object({
41
44
 
42
45
  export type TaskParams = Static<typeof taskSchema>;
43
46
 
47
+ /** A code review finding reported by the reviewer agent */
48
+ export interface ReviewFinding {
49
+ title: string;
50
+ body: string;
51
+ priority: number;
52
+ confidence: number;
53
+ file_path: string;
54
+ line_start: number;
55
+ line_end: number;
56
+ }
57
+
58
+ /** Review summary submitted by the reviewer agent */
59
+ export interface ReviewSummary {
60
+ overall_correctness: "correct" | "incorrect";
61
+ explanation: string;
62
+ confidence: number;
63
+ }
64
+
65
+ /** Structured review data extracted from reviewer agent */
66
+ export interface ReviewData {
67
+ findings: ReviewFinding[];
68
+ summary?: ReviewSummary;
69
+ }
70
+
44
71
  /** Agent definition (bundled or discovered) */
45
72
  export interface AgentDefinition {
46
73
  name: string;
@@ -58,7 +85,7 @@ export interface AgentProgress {
58
85
  index: number;
59
86
  agent: string;
60
87
  agentSource: AgentSource;
61
- status: "pending" | "running" | "completed" | "failed";
88
+ status: "pending" | "running" | "completed" | "failed" | "aborted";
62
89
  task: string;
63
90
  currentTool?: string;
64
91
  currentToolArgs?: string;
@@ -69,6 +96,8 @@ export interface AgentProgress {
69
96
  tokens: number;
70
97
  durationMs: number;
71
98
  modelOverride?: string;
99
+ /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
100
+ extractedToolData?: Record<string, unknown[]>;
72
101
  }
73
102
 
74
103
  /** Result from a single agent execution */
@@ -85,8 +114,13 @@ export interface SingleResult {
85
114
  tokens: number;
86
115
  modelOverride?: string;
87
116
  error?: string;
117
+ aborted?: boolean;
88
118
  jsonlEvents?: string[];
89
119
  artifactPaths?: { inputPath: string; outputPath: string; jsonlPath?: string };
120
+ /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
121
+ extractedToolData?: Record<string, unknown[]>;
122
+ /** Output metadata for Output tool integration */
123
+ outputMeta?: { lineCount: number; charCount: number };
90
124
  }
91
125
 
92
126
  /** Tool details for TUI rendering */
@@ -5,6 +5,7 @@ import * as path from "node:path";
5
5
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
6
6
  import { Type } from "@sinclair/typebox";
7
7
  import { parse as parseHtml } from "node-html-parser";
8
+ import { logger } from "../logger";
8
9
 
9
10
  // =============================================================================
10
11
  // Types and Constants
@@ -174,9 +175,10 @@ async function loadPage(url: string, options: LoadPageOptions = {}): Promise<Loa
174
175
  }
175
176
 
176
177
  return { content, contentType, finalUrl, ok: true, status: response.status };
177
- } catch (_err) {
178
+ } catch (err) {
178
179
  // On last attempt, return failure
179
180
  if (attempt === USER_AGENTS.length - 1) {
181
+ logger.debug("Web fetch failed after retries", { url, error: String(err) });
180
182
  return { content: "", contentType: "", finalUrl: url, ok: false };
181
183
  }
182
184
  // Otherwise retry with next UA
@@ -2203,8 +2205,8 @@ export const webFetchTool = createWebFetchTool(process.cwd());
2203
2205
 
2204
2206
  import type { Component } from "@oh-my-pi/pi-tui";
2205
2207
  import { Text } from "@oh-my-pi/pi-tui";
2206
- import type { Theme } from "../../modes/interactive/theme/theme.js";
2207
- import type { CustomTool, CustomToolContext, RenderResultOptions } from "../custom-tools/types.js";
2208
+ import type { Theme } from "../../modes/interactive/theme/theme";
2209
+ import type { CustomTool, CustomToolContext, RenderResultOptions } from "../custom-tools/types";
2208
2210
 
2209
2211
  // Tree formatting constants
2210
2212
  const TREE_MID = "├─";
@@ -10,7 +10,7 @@
10
10
 
11
11
  import * as os from "node:os";
12
12
  import * as path from "node:path";
13
- import type { AnthropicAuthConfig, AuthJson, ModelsJson } from "./types.js";
13
+ import type { AnthropicAuthConfig, AuthJson, ModelsJson } from "./types";
14
14
 
15
15
  const DEFAULT_BASE_URL = "https://api.anthropic.com";
16
16
 
@@ -14,16 +14,16 @@
14
14
 
15
15
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
16
16
  import { Type } from "@sinclair/typebox";
17
- import type { Theme } from "../../../modes/interactive/theme/theme.js";
18
- import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../custom-tools/types.js";
19
- import { callExaTool, findApiKey as findExaKey, formatSearchResults, isSearchResponse } from "../exa/mcp-client.js";
20
- import { renderExaCall, renderExaResult } from "../exa/render.js";
21
- import type { ExaRenderDetails } from "../exa/types.js";
22
- import { searchAnthropic } from "./providers/anthropic.js";
23
- import { searchExa } from "./providers/exa.js";
24
- import { findApiKey as findPerplexityKey, searchPerplexity } from "./providers/perplexity.js";
25
- import { formatAge, renderWebSearchCall, renderWebSearchResult, type WebSearchRenderDetails } from "./render.js";
26
- import type { WebSearchProvider, WebSearchResponse } from "./types.js";
17
+ import type { Theme } from "../../../modes/interactive/theme/theme";
18
+ import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../custom-tools/types";
19
+ import { callExaTool, findApiKey as findExaKey, formatSearchResults, isSearchResponse } from "../exa/mcp-client";
20
+ import { renderExaCall, renderExaResult } from "../exa/render";
21
+ import type { ExaRenderDetails } from "../exa/types";
22
+ import { searchAnthropic } from "./providers/anthropic";
23
+ import { searchExa } from "./providers/exa";
24
+ import { findApiKey as findPerplexityKey, searchPerplexity } from "./providers/perplexity";
25
+ import { formatAge, renderWebSearchCall, renderWebSearchResult, type WebSearchRenderDetails } from "./render";
26
+ import type { WebSearchProvider, WebSearchResponse } from "./types";
27
27
 
28
28
  /** Web search parameters schema */
29
29
  export const webSearchSchema = Type.Object({
@@ -345,7 +345,9 @@ Parameters:
345
345
  parameters: webSearchDeepSchema,
346
346
 
347
347
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
348
- return executeExaTool("deep_search_exa", params as Record<string, unknown>, "web_search_deep");
348
+ const { num_results, ...rest } = params as Record<string, unknown>;
349
+ const args = { ...rest, type: "deep", numResults: num_results ?? 10 };
350
+ return executeExaTool("web_search_exa", args, "web_search_deep");
349
351
  },
350
352
 
351
353
  renderCall(args, theme) {
@@ -404,7 +406,7 @@ Parameters:
404
406
  parameters: webSearchCrawlSchema,
405
407
 
406
408
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
407
- return executeExaTool("crawling_exa", params as Record<string, unknown>, "web_search_crawl");
409
+ return executeExaTool("crawling", params as Record<string, unknown>, "web_search_crawl");
408
410
  },
409
411
 
410
412
  renderCall(args, theme) {
@@ -435,7 +437,7 @@ Parameters:
435
437
  parameters: webSearchLinkedinSchema,
436
438
 
437
439
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
438
- return executeExaTool("linkedin_search_exa", params as Record<string, unknown>, "web_search_linkedin");
440
+ return executeExaTool("linkedin_search", params as Record<string, unknown>, "web_search_linkedin");
439
441
  },
440
442
 
441
443
  renderCall(args, theme) {
@@ -465,7 +467,7 @@ Parameters:
465
467
  parameters: webSearchCompanySchema,
466
468
 
467
469
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
468
- return executeExaTool("company_research_exa", params as Record<string, unknown>, "web_search_company");
470
+ return executeExaTool("company_research", params as Record<string, unknown>, "web_search_company");
469
471
  },
470
472
 
471
473
  renderCall(args, theme) {
@@ -534,4 +536,4 @@ export async function hasExaWebSearch(): Promise<boolean> {
534
536
  return exaKey !== null;
535
537
  }
536
538
 
537
- export type { WebSearchProvider, WebSearchResponse } from "./types.js";
539
+ export type { WebSearchProvider, WebSearchResponse } from "./types";
@@ -5,7 +5,7 @@
5
5
  * Returns synthesized answers with citations and source metadata.
6
6
  */
7
7
 
8
- import { buildAnthropicHeaders, buildAnthropicUrl, findAnthropicAuth, getEnv } from "../auth.js";
8
+ import { buildAnthropicHeaders, buildAnthropicUrl, findAnthropicAuth, getEnv } from "../auth";
9
9
  import type {
10
10
  AnthropicApiResponse,
11
11
  AnthropicAuthConfig,
@@ -13,7 +13,7 @@ import type {
13
13
  WebSearchCitation,
14
14
  WebSearchResponse,
15
15
  WebSearchSource,
16
- } from "../types.js";
16
+ } from "../types";
17
17
 
18
18
  const DEFAULT_MODEL = "claude-sonnet-4-5-20250514";
19
19
  const DEFAULT_MAX_TOKENS = 4096;
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import * as os from "node:os";
9
- import type { WebSearchResponse, WebSearchSource } from "../types.js";
9
+ import type { WebSearchResponse, WebSearchSource } from "../types";
10
10
 
11
11
  const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
12
12
 
@@ -248,10 +248,8 @@ export async function searchExa(params: ExaSearchParams): Promise<WebSearchRespo
248
248
 
249
249
  const args: Record<string, unknown> = {
250
250
  query: params.query,
251
- num_results: params.num_results ?? 10,
251
+ numResults: params.num_results ?? 10,
252
252
  type: params.type ?? "auto",
253
- text: true, // Include text for richer results
254
- highlights: true,
255
253
  };
256
254
 
257
255
  if (params.include_domains?.length) {
@@ -267,7 +265,7 @@ export async function searchExa(params: ExaSearchParams): Promise<WebSearchRespo
267
265
  args.end_published_date = params.end_published_date;
268
266
  }
269
267
 
270
- const response = await callExaMCP(apiKey, "web_search", args);
268
+ const response = await callExaMCP(apiKey, "web_search_exa", args);
271
269
 
272
270
  if (response.error) {
273
271
  throw new Error(`Exa MCP error: ${response.error.message}`);
@@ -12,7 +12,7 @@ import type {
12
12
  WebSearchCitation,
13
13
  WebSearchResponse,
14
14
  WebSearchSource,
15
- } from "../types.js";
15
+ } from "../types";
16
16
 
17
17
  const PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions";
18
18
 
@@ -6,9 +6,9 @@
6
6
 
7
7
  import type { Component } from "@oh-my-pi/pi-tui";
8
8
  import { Text } from "@oh-my-pi/pi-tui";
9
- import type { Theme } from "../../../modes/interactive/theme/theme.js";
10
- import type { RenderResultOptions } from "../../custom-tools/types.js";
11
- import type { WebSearchResponse } from "./types.js";
9
+ import type { Theme } from "../../../modes/interactive/theme/theme";
10
+ import type { RenderResultOptions } from "../../custom-tools/types";
11
+ import type { WebSearchResponse } from "./types";
12
12
 
13
13
  // Tree formatting constants
14
14
  const TREE_MID = "├─";