@oh-my-pi/pi-coding-agent 1.340.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.
- package/CHANGELOG.md +115 -1
- package/README.md +1 -1
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/package.json +5 -3
- package/src/cli/args.ts +13 -6
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/list-models.ts +2 -2
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/session-picker.ts +2 -2
- package/src/cli.ts +1 -1
- package/src/config.ts +3 -3
- package/src/core/agent-session.ts +189 -29
- package/src/core/bash-executor.ts +50 -10
- package/src/core/compaction/branch-summarization.ts +5 -5
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/compaction/index.ts +3 -3
- package/src/core/custom-commands/bundled/review/index.ts +156 -0
- package/src/core/custom-commands/index.ts +15 -0
- package/src/core/custom-commands/loader.ts +232 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +3 -3
- package/src/core/custom-tools/loader.ts +10 -8
- package/src/core/custom-tools/types.ts +11 -6
- package/src/core/custom-tools/wrapper.ts +2 -1
- package/src/core/exec.ts +22 -12
- package/src/core/export-html/index.ts +5 -5
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +5 -5
- package/src/core/hooks/loader.ts +21 -16
- package/src/core/hooks/runner.ts +6 -6
- package/src/core/hooks/tool-wrapper.ts +2 -2
- package/src/core/hooks/types.ts +12 -15
- package/src/core/index.ts +6 -6
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +3 -3
- package/src/core/mcp/config.ts +1 -1
- package/src/core/mcp/index.ts +12 -12
- package/src/core/mcp/loader.ts +2 -2
- package/src/core/mcp/manager.ts +6 -6
- package/src/core/mcp/tool-bridge.ts +3 -3
- package/src/core/mcp/transports/http.ts +1 -1
- package/src/core/mcp/transports/index.ts +2 -2
- package/src/core/mcp/transports/stdio.ts +1 -1
- package/src/core/messages.ts +22 -0
- package/src/core/model-registry.ts +2 -2
- package/src/core/model-resolver.ts +103 -2
- package/src/core/plugins/doctor.ts +1 -1
- package/src/core/plugins/index.ts +6 -6
- package/src/core/plugins/installer.ts +4 -4
- package/src/core/plugins/loader.ts +4 -9
- package/src/core/plugins/manager.ts +5 -5
- package/src/core/plugins/paths.ts +3 -3
- package/src/core/sdk.ts +127 -52
- package/src/core/session-manager.ts +123 -20
- package/src/core/settings-manager.ts +106 -22
- package/src/core/skills.ts +5 -5
- package/src/core/slash-commands.ts +60 -45
- package/src/core/system-prompt.ts +6 -6
- package/src/core/title-generator.ts +94 -0
- package/src/core/tools/bash.ts +33 -157
- package/src/core/tools/context.ts +2 -2
- package/src/core/tools/edit-diff.ts +5 -5
- package/src/core/tools/edit.ts +60 -9
- package/src/core/tools/exa/company.ts +3 -3
- package/src/core/tools/exa/index.ts +16 -17
- package/src/core/tools/exa/linkedin.ts +3 -3
- package/src/core/tools/exa/mcp-client.ts +9 -9
- package/src/core/tools/exa/render.ts +5 -5
- package/src/core/tools/exa/researcher.ts +3 -3
- package/src/core/tools/exa/search.ts +6 -5
- package/src/core/tools/exa/types.ts +5 -6
- package/src/core/tools/exa/websets.ts +3 -3
- package/src/core/tools/find.ts +3 -3
- package/src/core/tools/grep.ts +6 -5
- package/src/core/tools/index.ts +114 -40
- package/src/core/tools/ls.ts +4 -4
- package/src/core/tools/lsp/client.ts +204 -108
- package/src/core/tools/lsp/config.ts +709 -35
- package/src/core/tools/lsp/edits.ts +2 -2
- package/src/core/tools/lsp/index.ts +432 -30
- package/src/core/tools/lsp/render.ts +2 -2
- package/src/core/tools/lsp/rust-analyzer.ts +3 -3
- package/src/core/tools/lsp/types.ts +5 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/output.ts +175 -0
- package/src/core/tools/read.ts +7 -7
- package/src/core/tools/renderers.ts +92 -13
- package/src/core/tools/review.ts +268 -0
- package/src/core/tools/task/agents.ts +1 -1
- package/src/core/tools/task/bundled-agents/explore.md +1 -1
- package/src/core/tools/task/bundled-agents/reviewer.md +53 -38
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +145 -28
- package/src/core/tools/task/index.ts +78 -30
- package/src/core/tools/task/model-resolver.ts +72 -13
- package/src/core/tools/task/parallel.ts +1 -1
- package/src/core/tools/task/render.ts +219 -30
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +36 -2
- package/src/core/tools/web-fetch.ts +5 -3
- package/src/core/tools/web-search/auth.ts +1 -1
- package/src/core/tools/web-search/index.ts +17 -15
- package/src/core/tools/web-search/providers/anthropic.ts +2 -2
- package/src/core/tools/web-search/providers/exa.ts +3 -5
- package/src/core/tools/web-search/providers/perplexity.ts +1 -1
- package/src/core/tools/web-search/render.ts +3 -3
- package/src/core/tools/write.ts +70 -7
- package/src/index.ts +33 -17
- package/src/main.ts +60 -34
- package/src/migrations.ts +3 -3
- package/src/modes/index.ts +5 -5
- package/src/modes/interactive/components/armin.ts +1 -1
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/bash-execution.ts +4 -4
- package/src/modes/interactive/components/bordered-loader.ts +2 -2
- package/src/modes/interactive/components/branch-summary-message.ts +2 -2
- package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
- package/src/modes/interactive/components/diff.ts +1 -1
- package/src/modes/interactive/components/dynamic-border.ts +1 -1
- package/src/modes/interactive/components/footer.ts +5 -5
- package/src/modes/interactive/components/hook-editor.ts +2 -2
- package/src/modes/interactive/components/hook-input.ts +2 -2
- package/src/modes/interactive/components/hook-message.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +2 -2
- package/src/modes/interactive/components/model-selector.ts +341 -41
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/plugin-settings.ts +4 -4
- package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
- package/src/modes/interactive/components/session-selector.ts +24 -11
- package/src/modes/interactive/components/settings-defs.ts +51 -3
- package/src/modes/interactive/components/settings-selector.ts +13 -16
- package/src/modes/interactive/components/show-images-selector.ts +2 -2
- package/src/modes/interactive/components/theme-selector.ts +2 -2
- package/src/modes/interactive/components/thinking-selector.ts +2 -2
- package/src/modes/interactive/components/tool-execution.ts +44 -8
- package/src/modes/interactive/components/tree-selector.ts +5 -5
- package/src/modes/interactive/components/user-message-selector.ts +2 -2
- package/src/modes/interactive/components/user-message.ts +1 -1
- package/src/modes/interactive/components/welcome.ts +42 -5
- package/src/modes/interactive/interactive-mode.ts +169 -48
- package/src/modes/interactive/theme/theme.ts +8 -7
- package/src/modes/print-mode.ts +4 -3
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +21 -11
- package/src/modes/rpc/rpc-types.ts +3 -3
- package/src/utils/changelog.ts +2 -2
- package/src/utils/clipboard.ts +1 -1
- package/src/utils/shell-snapshot.ts +218 -0
- package/src/utils/shell.ts +93 -13
- package/src/utils/tools-manager.ts +1 -1
- package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/exa/logger.ts +0 -56
|
@@ -1,14 +1,20 @@
|
|
|
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: "
|
|
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)
|
|
11
|
+
* - "pi/slow" → configured slow model from settings
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
12
18
|
|
|
13
19
|
/** pi command: 'pi.cmd' on Windows, 'pi' elsewhere */
|
|
14
20
|
const PI_CMD = process.platform === "win32" ? "pi.cmd" : "pi";
|
|
@@ -16,7 +22,7 @@ const PI_CMD = process.platform === "win32" ? "pi.cmd" : "pi";
|
|
|
16
22
|
/** Windows shell option for spawn/spawnSync */
|
|
17
23
|
const PI_SHELL_OPT = process.platform === "win32";
|
|
18
24
|
|
|
19
|
-
/** Cache for available models */
|
|
25
|
+
/** Cache for available models (provider/modelId format) */
|
|
20
26
|
let cachedModels: string[] | null = null;
|
|
21
27
|
|
|
22
28
|
/** Cache expiry time (5 minutes) */
|
|
@@ -26,6 +32,7 @@ const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
34
|
* Get available models from `pi --list-models`.
|
|
35
|
+
* Returns models in "provider/modelId" format.
|
|
29
36
|
* Caches the result for performance.
|
|
30
37
|
*/
|
|
31
38
|
export function getAvailableModels(): string[] {
|
|
@@ -47,13 +54,14 @@ export function getAvailableModels(): string[] {
|
|
|
47
54
|
return cachedModels;
|
|
48
55
|
}
|
|
49
56
|
|
|
50
|
-
// Parse output: skip header line, extract model
|
|
57
|
+
// Parse output: skip header line, extract provider/model
|
|
51
58
|
const lines = result.stdout.trim().split("\n");
|
|
52
59
|
cachedModels = lines
|
|
53
60
|
.slice(1) // Skip header
|
|
54
61
|
.map((line) => {
|
|
55
62
|
const parts = line.trim().split(/\s+/);
|
|
56
|
-
|
|
63
|
+
// Format: provider/modelId
|
|
64
|
+
return parts[0] && parts[1] ? `${parts[0]}/${parts[1]}` : "";
|
|
57
65
|
})
|
|
58
66
|
.filter(Boolean);
|
|
59
67
|
|
|
@@ -75,13 +83,52 @@ export function clearModelCache(): void {
|
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
/**
|
|
78
|
-
*
|
|
86
|
+
* Load model roles from settings file.
|
|
87
|
+
*/
|
|
88
|
+
function loadModelRoles(): Record<string, string> {
|
|
89
|
+
try {
|
|
90
|
+
const settingsPath = join(homedir(), ".pi", "agent", "settings.json");
|
|
91
|
+
if (!existsSync(settingsPath)) return {};
|
|
92
|
+
const content = readFileSync(settingsPath, "utf-8");
|
|
93
|
+
const settings = JSON.parse(content);
|
|
94
|
+
return settings.modelRoles ?? {};
|
|
95
|
+
} catch {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve a pi/<role> alias to a model string.
|
|
102
|
+
* Looks up the role in settings.modelRoles and returns the configured model.
|
|
103
|
+
* Returns undefined if the role isn't configured.
|
|
104
|
+
*/
|
|
105
|
+
function resolvePiAlias(role: string, availableModels: string[]): string | undefined {
|
|
106
|
+
const roles = loadModelRoles();
|
|
107
|
+
|
|
108
|
+
// Look up role in settings (case-insensitive)
|
|
109
|
+
const configured = roles[role] || roles[role.toLowerCase()];
|
|
110
|
+
if (!configured) return undefined;
|
|
111
|
+
|
|
112
|
+
// configured is in "provider/modelId" format, find in available models
|
|
113
|
+
return availableModels.find((m) => m.toLowerCase() === configured.toLowerCase());
|
|
114
|
+
}
|
|
115
|
+
|
|
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;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resolve a fuzzy model pattern to "provider/modelId" format.
|
|
79
126
|
*
|
|
80
127
|
* Supports comma-separated patterns (e.g., "gpt, opus") - tries each in order.
|
|
81
128
|
* Returns undefined if pattern is "default", undefined, or no match found.
|
|
82
129
|
*
|
|
83
130
|
* @param pattern - Model pattern to resolve
|
|
84
|
-
* @param availableModels - Optional pre-fetched list of available models
|
|
131
|
+
* @param availableModels - Optional pre-fetched list of available models (in provider/modelId format)
|
|
85
132
|
*/
|
|
86
133
|
export function resolveModelPattern(pattern: string | undefined, availableModels?: string[]): string | undefined {
|
|
87
134
|
if (!pattern || pattern === "default") {
|
|
@@ -97,16 +144,28 @@ export function resolveModelPattern(pattern: string | undefined, availableModels
|
|
|
97
144
|
// Split by comma, try each pattern in order
|
|
98
145
|
const patterns = pattern
|
|
99
146
|
.split(",")
|
|
100
|
-
.map((p) => p.trim()
|
|
147
|
+
.map((p) => p.trim())
|
|
101
148
|
.filter(Boolean);
|
|
102
149
|
|
|
103
150
|
for (const p of patterns) {
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
151
|
+
// Handle pi/<role> aliases - looks up role in settings.modelRoles
|
|
152
|
+
if (p.toLowerCase().startsWith("pi/")) {
|
|
153
|
+
const role = p.slice(3); // Remove "pi/" prefix
|
|
154
|
+
const resolved = resolvePiAlias(role, models);
|
|
155
|
+
if (resolved) return resolved;
|
|
156
|
+
continue; // Role not configured, try next pattern
|
|
157
|
+
}
|
|
158
|
+
|
|
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;
|
|
107
166
|
|
|
108
|
-
// Try fuzzy match (substring)
|
|
109
|
-
const fuzzyMatch = models.find((m) => m.toLowerCase().includes(p));
|
|
167
|
+
// Try fuzzy match on model ID (substring)
|
|
168
|
+
const fuzzyMatch = models.find((m) => getModelId(m).toLowerCase().includes(p.toLowerCase()));
|
|
110
169
|
if (fuzzyMatch) return fuzzyMatch;
|
|
111
170
|
}
|
|
112
171
|
|
|
@@ -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
|
|
12
|
-
import type { RenderResultOptions } from "../../custom-tools/types
|
|
13
|
-
import type {
|
|
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 =
|
|
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
|
-
|
|
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
|
|
142
|
-
const
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
statusLine
|
|
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
|
-
//
|
|
158
|
-
const
|
|
159
|
-
const
|
|
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
|
|
162
|
-
|
|
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
|
|
166
|
-
|
|
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
|
|
211
|
-
const
|
|
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
|
-
|
|
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 +=
|
|
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
|
|
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 (
|
|
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
|
|
2207
|
-
import type { CustomTool, CustomToolContext, RenderResultOptions } from "../custom-tools/types
|
|
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
|
|
13
|
+
import type { AnthropicAuthConfig, AuthJson, ModelsJson } from "./types";
|
|
14
14
|
|
|
15
15
|
const DEFAULT_BASE_URL = "https://api.anthropic.com";
|
|
16
16
|
|