@oh-my-pi/pi-coding-agent 1.341.0 → 2.0.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 (151) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +1 -1
  3. package/examples/custom-tools/subagent/index.ts +1 -1
  4. package/package.json +5 -3
  5. package/src/cli/args.ts +5 -6
  6. package/src/cli/file-processor.ts +3 -3
  7. package/src/cli/list-models.ts +2 -2
  8. package/src/cli/plugin-cli.ts +1 -1
  9. package/src/cli/session-picker.ts +2 -2
  10. package/src/cli.ts +1 -1
  11. package/src/config.ts +3 -3
  12. package/src/core/agent-session.ts +157 -15
  13. package/src/core/bash-executor.ts +50 -10
  14. package/src/core/compaction/branch-summarization.ts +5 -5
  15. package/src/core/compaction/compaction.ts +3 -3
  16. package/src/core/compaction/index.ts +3 -3
  17. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  18. package/src/core/custom-commands/index.ts +15 -0
  19. package/src/core/custom-commands/loader.ts +232 -0
  20. package/src/core/custom-commands/types.ts +112 -0
  21. package/src/core/custom-tools/index.ts +3 -3
  22. package/src/core/custom-tools/loader.ts +10 -8
  23. package/src/core/custom-tools/types.ts +11 -6
  24. package/src/core/custom-tools/wrapper.ts +2 -1
  25. package/src/core/exec.ts +22 -12
  26. package/src/core/export-html/index.ts +5 -5
  27. package/src/core/file-mentions.ts +54 -0
  28. package/src/core/hooks/index.ts +5 -5
  29. package/src/core/hooks/loader.ts +21 -16
  30. package/src/core/hooks/runner.ts +6 -6
  31. package/src/core/hooks/tool-wrapper.ts +2 -2
  32. package/src/core/hooks/types.ts +12 -15
  33. package/src/core/index.ts +6 -6
  34. package/src/core/logger.ts +112 -0
  35. package/src/core/mcp/client.ts +3 -3
  36. package/src/core/mcp/config.ts +1 -1
  37. package/src/core/mcp/index.ts +12 -12
  38. package/src/core/mcp/loader.ts +2 -2
  39. package/src/core/mcp/manager.ts +6 -6
  40. package/src/core/mcp/tool-bridge.ts +3 -3
  41. package/src/core/mcp/transports/http.ts +1 -1
  42. package/src/core/mcp/transports/index.ts +2 -2
  43. package/src/core/mcp/transports/stdio.ts +1 -1
  44. package/src/core/messages.ts +22 -0
  45. package/src/core/model-registry.ts +2 -2
  46. package/src/core/model-resolver.ts +2 -2
  47. package/src/core/plugins/doctor.ts +1 -1
  48. package/src/core/plugins/index.ts +6 -6
  49. package/src/core/plugins/installer.ts +4 -4
  50. package/src/core/plugins/loader.ts +4 -9
  51. package/src/core/plugins/manager.ts +5 -5
  52. package/src/core/plugins/paths.ts +3 -3
  53. package/src/core/sdk.ts +77 -35
  54. package/src/core/session-manager.ts +6 -6
  55. package/src/core/settings-manager.ts +16 -3
  56. package/src/core/skills.ts +5 -5
  57. package/src/core/slash-commands.ts +60 -45
  58. package/src/core/system-prompt.ts +6 -6
  59. package/src/core/title-generator.ts +2 -2
  60. package/src/core/tools/bash.ts +32 -155
  61. package/src/core/tools/context.ts +2 -2
  62. package/src/core/tools/edit-diff.ts +3 -3
  63. package/src/core/tools/edit.ts +18 -5
  64. package/src/core/tools/exa/company.ts +3 -3
  65. package/src/core/tools/exa/index.ts +16 -17
  66. package/src/core/tools/exa/linkedin.ts +3 -3
  67. package/src/core/tools/exa/mcp-client.ts +9 -9
  68. package/src/core/tools/exa/render.ts +5 -5
  69. package/src/core/tools/exa/researcher.ts +3 -3
  70. package/src/core/tools/exa/search.ts +6 -5
  71. package/src/core/tools/exa/types.ts +5 -6
  72. package/src/core/tools/exa/websets.ts +3 -3
  73. package/src/core/tools/find.ts +3 -3
  74. package/src/core/tools/grep.ts +3 -3
  75. package/src/core/tools/index.ts +48 -34
  76. package/src/core/tools/ls.ts +4 -4
  77. package/src/core/tools/lsp/client.ts +161 -90
  78. package/src/core/tools/lsp/config.ts +1 -1
  79. package/src/core/tools/lsp/edits.ts +2 -2
  80. package/src/core/tools/lsp/index.ts +15 -13
  81. package/src/core/tools/lsp/render.ts +2 -2
  82. package/src/core/tools/lsp/rust-analyzer.ts +3 -3
  83. package/src/core/tools/lsp/utils.ts +1 -1
  84. package/src/core/tools/notebook.ts +1 -1
  85. package/src/core/tools/output.ts +175 -0
  86. package/src/core/tools/read.ts +7 -7
  87. package/src/core/tools/renderers.ts +92 -13
  88. package/src/core/tools/review.ts +268 -0
  89. package/src/core/tools/task/agents.ts +1 -1
  90. package/src/core/tools/task/bundled-agents/reviewer.md +52 -37
  91. package/src/core/tools/task/discovery.ts +2 -2
  92. package/src/core/tools/task/executor.ts +145 -28
  93. package/src/core/tools/task/index.ts +78 -30
  94. package/src/core/tools/task/model-resolver.ts +30 -20
  95. package/src/core/tools/task/parallel.ts +1 -1
  96. package/src/core/tools/task/render.ts +219 -30
  97. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  98. package/src/core/tools/task/types.ts +36 -2
  99. package/src/core/tools/web-fetch.ts +5 -3
  100. package/src/core/tools/web-search/auth.ts +1 -1
  101. package/src/core/tools/web-search/index.ts +17 -15
  102. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  103. package/src/core/tools/web-search/providers/exa.ts +3 -5
  104. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  105. package/src/core/tools/web-search/render.ts +3 -3
  106. package/src/core/tools/write.ts +4 -4
  107. package/src/index.ts +29 -18
  108. package/src/main.ts +37 -32
  109. package/src/migrations.ts +3 -3
  110. package/src/modes/index.ts +5 -5
  111. package/src/modes/interactive/components/armin.ts +1 -1
  112. package/src/modes/interactive/components/assistant-message.ts +1 -1
  113. package/src/modes/interactive/components/bash-execution.ts +4 -4
  114. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  115. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  116. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  117. package/src/modes/interactive/components/diff.ts +1 -1
  118. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  119. package/src/modes/interactive/components/footer.ts +5 -5
  120. package/src/modes/interactive/components/hook-editor.ts +2 -2
  121. package/src/modes/interactive/components/hook-input.ts +2 -2
  122. package/src/modes/interactive/components/hook-message.ts +3 -3
  123. package/src/modes/interactive/components/hook-selector.ts +2 -2
  124. package/src/modes/interactive/components/model-selector.ts +281 -59
  125. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  126. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  127. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  128. package/src/modes/interactive/components/session-selector.ts +4 -4
  129. package/src/modes/interactive/components/settings-defs.ts +1 -1
  130. package/src/modes/interactive/components/settings-selector.ts +5 -5
  131. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  132. package/src/modes/interactive/components/theme-selector.ts +2 -2
  133. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  134. package/src/modes/interactive/components/tool-execution.ts +26 -8
  135. package/src/modes/interactive/components/tree-selector.ts +3 -3
  136. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  137. package/src/modes/interactive/components/user-message.ts +1 -1
  138. package/src/modes/interactive/components/welcome.ts +2 -2
  139. package/src/modes/interactive/interactive-mode.ts +85 -41
  140. package/src/modes/interactive/theme/theme.ts +8 -7
  141. package/src/modes/print-mode.ts +4 -3
  142. package/src/modes/rpc/rpc-client.ts +4 -4
  143. package/src/modes/rpc/rpc-mode.ts +21 -11
  144. package/src/modes/rpc/rpc-types.ts +3 -3
  145. package/src/utils/changelog.ts +2 -2
  146. package/src/utils/clipboard.ts +1 -1
  147. package/src/utils/shell-snapshot.ts +218 -0
  148. package/src/utils/shell.ts +93 -13
  149. package/src/utils/tools-manager.ts +1 -1
  150. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  151. package/src/core/tools/exa/logger.ts +0 -56
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Model resolution with fuzzy pattern matching.
3
3
  *
4
+ * Returns models in "provider/modelId" format for use with --model flag.
5
+ *
4
6
  * Supports:
5
- * - Exact match: "claude-opus-4-5"
6
- * - Fuzzy match: "opus" → "claude-opus-4-5"
7
+ * - Exact match: "gpt-5.2" → "p-openai/gpt-5.2"
8
+ * - Fuzzy match: "opus" → "p-anthropic/claude-opus-4-5"
7
9
  * - Comma fallback: "gpt, opus" → tries gpt first, then opus
8
10
  * - "default" → undefined (use system default)
9
- * - "pi/default" → configured default model from settings
10
- * - "pi/smol" → configured smol model from settings
11
+ * - "pi/slow" → configured slow model from settings
11
12
  */
12
13
 
13
14
  import { spawnSync } from "node:child_process";
@@ -21,7 +22,7 @@ const PI_CMD = process.platform === "win32" ? "pi.cmd" : "pi";
21
22
  /** Windows shell option for spawn/spawnSync */
22
23
  const PI_SHELL_OPT = process.platform === "win32";
23
24
 
24
- /** Cache for available models */
25
+ /** Cache for available models (provider/modelId format) */
25
26
  let cachedModels: string[] | null = null;
26
27
 
27
28
  /** Cache expiry time (5 minutes) */
@@ -31,6 +32,7 @@ const CACHE_TTL_MS = 5 * 60 * 1000;
31
32
 
32
33
  /**
33
34
  * Get available models from `pi --list-models`.
35
+ * Returns models in "provider/modelId" format.
34
36
  * Caches the result for performance.
35
37
  */
36
38
  export function getAvailableModels(): string[] {
@@ -52,13 +54,14 @@ export function getAvailableModels(): string[] {
52
54
  return cachedModels;
53
55
  }
54
56
 
55
- // Parse output: skip header line, extract model column
57
+ // Parse output: skip header line, extract provider/model
56
58
  const lines = result.stdout.trim().split("\n");
57
59
  cachedModels = lines
58
60
  .slice(1) // Skip header
59
61
  .map((line) => {
60
62
  const parts = line.trim().split(/\s+/);
61
- return parts[1]; // Model name is second column
63
+ // Format: provider/modelId
64
+ return parts[0] && parts[1] ? `${parts[0]}/${parts[1]}` : "";
62
65
  })
63
66
  .filter(Boolean);
64
67
 
@@ -106,23 +109,26 @@ function resolvePiAlias(role: string, availableModels: string[]): string | undef
106
109
  const configured = roles[role] || roles[role.toLowerCase()];
107
110
  if (!configured) return undefined;
108
111
 
109
- // configured is in "provider/modelId" format, extract just the modelId for matching
110
- const slashIdx = configured.indexOf("/");
111
- if (slashIdx <= 0) return undefined;
112
+ // configured is in "provider/modelId" format, find in available models
113
+ return availableModels.find((m) => m.toLowerCase() === configured.toLowerCase());
114
+ }
112
115
 
113
- const modelId = configured.slice(slashIdx + 1);
114
- // Find in available models
115
- return availableModels.find((m) => m.toLowerCase() === modelId.toLowerCase());
116
+ /**
117
+ * Extract model ID from "provider/modelId" format.
118
+ */
119
+ function getModelId(fullModel: string): string {
120
+ const slashIdx = fullModel.indexOf("/");
121
+ return slashIdx > 0 ? fullModel.slice(slashIdx + 1) : fullModel;
116
122
  }
117
123
 
118
124
  /**
119
- * Resolve a fuzzy model pattern to an actual model name.
125
+ * Resolve a fuzzy model pattern to "provider/modelId" format.
120
126
  *
121
127
  * Supports comma-separated patterns (e.g., "gpt, opus") - tries each in order.
122
128
  * Returns undefined if pattern is "default", undefined, or no match found.
123
129
  *
124
130
  * @param pattern - Model pattern to resolve
125
- * @param availableModels - Optional pre-fetched list of available models
131
+ * @param availableModels - Optional pre-fetched list of available models (in provider/modelId format)
126
132
  */
127
133
  export function resolveModelPattern(pattern: string | undefined, availableModels?: string[]): string | undefined {
128
134
  if (!pattern || pattern === "default") {
@@ -150,12 +156,16 @@ export function resolveModelPattern(pattern: string | undefined, availableModels
150
156
  continue; // Role not configured, try next pattern
151
157
  }
152
158
 
153
- // Try exact match first
154
- const exactMatch = models.find((m) => m.toLowerCase() === p.toLowerCase());
155
- if (exactMatch) return exactMatch;
159
+ // Try exact match on full provider/modelId
160
+ const exactFull = models.find((m) => m.toLowerCase() === p.toLowerCase());
161
+ if (exactFull) return exactFull;
162
+
163
+ // Try exact match on model ID only
164
+ const exactId = models.find((m) => getModelId(m).toLowerCase() === p.toLowerCase());
165
+ if (exactId) return exactId;
156
166
 
157
- // Try fuzzy match (substring)
158
- const fuzzyMatch = models.find((m) => m.toLowerCase().includes(p.toLowerCase()));
167
+ // Try fuzzy match on model ID (substring)
168
+ const fuzzyMatch = models.find((m) => getModelId(m).toLowerCase().includes(p.toLowerCase()));
159
169
  if (fuzzyMatch) return fuzzyMatch;
160
170
  }
161
171
 
@@ -2,7 +2,7 @@
2
2
  * Parallel execution with concurrency control.
3
3
  */
4
4
 
5
- import { MAX_CONCURRENCY } from "./types.js";
5
+ import { MAX_CONCURRENCY } from "./types";
6
6
 
7
7
  /**
8
8
  * Execute items with a concurrency limit using a worker pool pattern.
@@ -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