@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.
- package/CHANGELOG.md +73 -0
- package/README.md +1 -1
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/package.json +5 -3
- package/src/cli/args.ts +5 -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 +157 -15
- 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 +2 -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 +77 -35
- package/src/core/session-manager.ts +6 -6
- package/src/core/settings-manager.ts +16 -3
- 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 +2 -2
- package/src/core/tools/bash.ts +32 -155
- package/src/core/tools/context.ts +2 -2
- package/src/core/tools/edit-diff.ts +3 -3
- package/src/core/tools/edit.ts +18 -5
- 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 +3 -3
- package/src/core/tools/index.ts +48 -34
- package/src/core/tools/ls.ts +4 -4
- package/src/core/tools/lsp/client.ts +161 -90
- package/src/core/tools/lsp/config.ts +1 -1
- package/src/core/tools/lsp/edits.ts +2 -2
- package/src/core/tools/lsp/index.ts +15 -13
- package/src/core/tools/lsp/render.ts +2 -2
- package/src/core/tools/lsp/rust-analyzer.ts +3 -3
- 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/reviewer.md +52 -37
- 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 +30 -20
- 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 +4 -4
- package/src/index.ts +29 -18
- package/src/main.ts +37 -32
- 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 +281 -59
- 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 +4 -4
- package/src/modes/interactive/components/settings-defs.ts +1 -1
- package/src/modes/interactive/components/settings-selector.ts +5 -5
- 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 +26 -8
- package/src/modes/interactive/components/tree-selector.ts +3 -3
- 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 +2 -2
- package/src/modes/interactive/interactive-mode.ts +85 -41
- 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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output tool for reading agent/task outputs by ID.
|
|
3
|
+
*
|
|
4
|
+
* Resolves IDs like "reviewer_0" to artifact paths in the current session.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
10
|
+
import type { TextContent } from "@oh-my-pi/pi-ai";
|
|
11
|
+
import { Type } from "@sinclair/typebox";
|
|
12
|
+
import type { SessionContext } from "./index";
|
|
13
|
+
import { getArtifactsDir } from "./task/artifacts";
|
|
14
|
+
|
|
15
|
+
const outputSchema = Type.Object({
|
|
16
|
+
ids: Type.Array(Type.String(), {
|
|
17
|
+
description: "Agent output IDs to read (e.g., ['reviewer_0', 'explore_1'])",
|
|
18
|
+
minItems: 1,
|
|
19
|
+
}),
|
|
20
|
+
format: Type.Optional(
|
|
21
|
+
Type.Union([Type.Literal("raw"), Type.Literal("json"), Type.Literal("stripped")], {
|
|
22
|
+
description: "Output format: raw (default), json (structured), stripped (no ANSI)",
|
|
23
|
+
}),
|
|
24
|
+
),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** Metadata for a single output file */
|
|
28
|
+
interface OutputEntry {
|
|
29
|
+
id: string;
|
|
30
|
+
path: string;
|
|
31
|
+
lineCount: number;
|
|
32
|
+
charCount: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface OutputToolDetails {
|
|
36
|
+
outputs: OutputEntry[];
|
|
37
|
+
notFound?: string[];
|
|
38
|
+
availableIds?: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Strip ANSI escape codes from text */
|
|
42
|
+
function stripAnsi(text: string): string {
|
|
43
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** List available output IDs in artifacts directory */
|
|
47
|
+
function listAvailableOutputs(artifactsDir: string): string[] {
|
|
48
|
+
try {
|
|
49
|
+
const files = fs.readdirSync(artifactsDir);
|
|
50
|
+
return files.filter((f) => f.endsWith(".out.md")).map((f) => f.replace(".out.md", ""));
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Format byte count for display */
|
|
57
|
+
function formatBytes(bytes: number): string {
|
|
58
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
59
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
|
|
60
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createOutputTool(
|
|
64
|
+
_cwd: string,
|
|
65
|
+
sessionContext?: SessionContext,
|
|
66
|
+
): AgentTool<typeof outputSchema, OutputToolDetails> {
|
|
67
|
+
return {
|
|
68
|
+
name: "output",
|
|
69
|
+
label: "Output",
|
|
70
|
+
description: `Read full agent/task output by ID.
|
|
71
|
+
|
|
72
|
+
Use when the Task tool's truncated preview isn't sufficient for your needs.
|
|
73
|
+
The Task tool already returns summaries with line/char counts in its result.
|
|
74
|
+
|
|
75
|
+
Parameters:
|
|
76
|
+
- ids: Array of output IDs (e.g., ["reviewer_0", "explore_1"])
|
|
77
|
+
- format: "raw" (default), "json" (structured object), or "stripped" (no ANSI codes)
|
|
78
|
+
|
|
79
|
+
Returns the full output content. For unknown IDs, returns an error with available IDs.
|
|
80
|
+
|
|
81
|
+
Example: { "ids": ["reviewer_0"] }`,
|
|
82
|
+
parameters: outputSchema,
|
|
83
|
+
execute: async (
|
|
84
|
+
_toolCallId: string,
|
|
85
|
+
params: { ids: string[]; format?: "raw" | "json" | "stripped" },
|
|
86
|
+
): Promise<{ content: TextContent[]; details: OutputToolDetails }> => {
|
|
87
|
+
const sessionFile = sessionContext?.getSessionFile();
|
|
88
|
+
|
|
89
|
+
if (!sessionFile) {
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: "No session - output artifacts unavailable" }],
|
|
92
|
+
details: { outputs: [], notFound: params.ids },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const artifactsDir = getArtifactsDir(sessionFile);
|
|
97
|
+
if (!artifactsDir || !fs.existsSync(artifactsDir)) {
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: "text", text: "No artifacts directory found" }],
|
|
100
|
+
details: { outputs: [], notFound: params.ids },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const outputs: OutputEntry[] = [];
|
|
105
|
+
const notFound: string[] = [];
|
|
106
|
+
const format = params.format ?? "raw";
|
|
107
|
+
|
|
108
|
+
for (const id of params.ids) {
|
|
109
|
+
const outputPath = path.join(artifactsDir, `${id}.out.md`);
|
|
110
|
+
|
|
111
|
+
if (!fs.existsSync(outputPath)) {
|
|
112
|
+
notFound.push(id);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const content = fs.readFileSync(outputPath, "utf-8");
|
|
117
|
+
outputs.push({
|
|
118
|
+
id,
|
|
119
|
+
path: outputPath,
|
|
120
|
+
lineCount: content.split("\n").length,
|
|
121
|
+
charCount: content.length,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Error case: some IDs not found
|
|
126
|
+
if (notFound.length > 0) {
|
|
127
|
+
const available = listAvailableOutputs(artifactsDir);
|
|
128
|
+
const errorMsg =
|
|
129
|
+
available.length > 0
|
|
130
|
+
? `Not found: ${notFound.join(", ")}\nAvailable: ${available.join(", ")}`
|
|
131
|
+
: `Not found: ${notFound.join(", ")}\nNo outputs available in current session`;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text: errorMsg }],
|
|
135
|
+
details: { outputs, notFound, availableIds: available },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Success: build response based on format
|
|
140
|
+
let contentText: string;
|
|
141
|
+
|
|
142
|
+
if (format === "json") {
|
|
143
|
+
const jsonData = outputs.map((o) => ({
|
|
144
|
+
id: o.id,
|
|
145
|
+
lineCount: o.lineCount,
|
|
146
|
+
charCount: o.charCount,
|
|
147
|
+
content: fs.readFileSync(o.path, "utf-8"),
|
|
148
|
+
}));
|
|
149
|
+
contentText = JSON.stringify(jsonData, null, 2);
|
|
150
|
+
} else {
|
|
151
|
+
// raw or stripped
|
|
152
|
+
const parts = outputs.map((o) => {
|
|
153
|
+
let content = fs.readFileSync(o.path, "utf-8");
|
|
154
|
+
if (format === "stripped") {
|
|
155
|
+
content = stripAnsi(content);
|
|
156
|
+
}
|
|
157
|
+
// Add header for multiple outputs
|
|
158
|
+
if (outputs.length > 1) {
|
|
159
|
+
return `=== ${o.id} (${o.lineCount} lines, ${formatBytes(o.charCount)}) ===\n${content}`;
|
|
160
|
+
}
|
|
161
|
+
return content;
|
|
162
|
+
});
|
|
163
|
+
contentText = parts.join("\n\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: "text", text: contentText }],
|
|
168
|
+
details: { outputs },
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Default output tool using process.cwd() - for backwards compatibility */
|
|
175
|
+
export const outputTool = createOutputTool(process.cwd());
|
package/src/core/tools/read.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { access, readFile } from "node:fs/promises";
|
|
4
|
+
import { extname } from "node:path";
|
|
1
5
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
6
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
3
7
|
import { Type } from "@sinclair/typebox";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { extname } from "path";
|
|
8
|
-
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
|
|
9
|
-
import { resolveReadPath } from "./path-utils.js";
|
|
10
|
-
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.js";
|
|
8
|
+
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime";
|
|
9
|
+
import { resolveReadPath } from "./path-utils";
|
|
10
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate";
|
|
11
11
|
|
|
12
12
|
// Document types convertible via markitdown
|
|
13
13
|
const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
|
|
@@ -6,19 +6,20 @@
|
|
|
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
|
|
10
|
-
import type { RenderResultOptions } from "../custom-tools/types
|
|
11
|
-
import type { AskToolDetails } from "./ask
|
|
12
|
-
import type { FindToolDetails } from "./find
|
|
13
|
-
import type { GrepToolDetails } from "./grep
|
|
14
|
-
import type { LsToolDetails } from "./ls
|
|
15
|
-
import { renderCall as renderLspCall, renderResult as renderLspResult } from "./lsp/render
|
|
16
|
-
import type { LspToolDetails } from "./lsp/types
|
|
17
|
-
import type { NotebookToolDetails } from "./notebook
|
|
18
|
-
import {
|
|
19
|
-
import
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
9
|
+
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
10
|
+
import type { RenderResultOptions } from "../custom-tools/types";
|
|
11
|
+
import type { AskToolDetails } from "./ask";
|
|
12
|
+
import type { FindToolDetails } from "./find";
|
|
13
|
+
import type { GrepToolDetails } from "./grep";
|
|
14
|
+
import type { LsToolDetails } from "./ls";
|
|
15
|
+
import { renderCall as renderLspCall, renderResult as renderLspResult } from "./lsp/render";
|
|
16
|
+
import type { LspToolDetails } from "./lsp/types";
|
|
17
|
+
import type { NotebookToolDetails } from "./notebook";
|
|
18
|
+
import type { OutputToolDetails } from "./output";
|
|
19
|
+
import { renderCall as renderTaskCall, renderResult as renderTaskResult } from "./task/render";
|
|
20
|
+
import type { TaskToolDetails } from "./task/types";
|
|
21
|
+
import { renderWebFetchCall, renderWebFetchResult, type WebFetchToolDetails } from "./web-fetch";
|
|
22
|
+
import { renderWebSearchCall, renderWebSearchResult, type WebSearchRenderDetails } from "./web-search/render";
|
|
22
23
|
|
|
23
24
|
// Tree drawing characters
|
|
24
25
|
const TREE_MID = "├─";
|
|
@@ -416,6 +417,83 @@ const lspRenderer: ToolRenderer<LspArgs, LspToolDetails> = {
|
|
|
416
417
|
renderResult: renderLspResult,
|
|
417
418
|
};
|
|
418
419
|
|
|
420
|
+
// ============================================================================
|
|
421
|
+
// Output Renderer
|
|
422
|
+
// ============================================================================
|
|
423
|
+
|
|
424
|
+
interface OutputArgs {
|
|
425
|
+
ids: string[];
|
|
426
|
+
format?: "raw" | "json" | "stripped";
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Format byte count for display */
|
|
430
|
+
function formatBytes(bytes: number): string {
|
|
431
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
432
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
|
|
433
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const outputRenderer: ToolRenderer<OutputArgs, OutputToolDetails> = {
|
|
437
|
+
renderCall(args, theme) {
|
|
438
|
+
const ids = args.ids?.join(", ") ?? "?";
|
|
439
|
+
const label = theme.fg("toolTitle", theme.bold("output"));
|
|
440
|
+
const format = args.format && args.format !== "raw" ? theme.fg("muted", ` (${args.format})`) : "";
|
|
441
|
+
return new Text(`${label} ${theme.fg("dim", ids)}${format}`, 0, 0);
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
renderResult(result, { expanded }, theme) {
|
|
445
|
+
const details = result.details;
|
|
446
|
+
|
|
447
|
+
// Error case: some IDs not found
|
|
448
|
+
if (details?.notFound?.length) {
|
|
449
|
+
let text = `${theme.fg("error", ICON_ERROR)} Not found: ${details.notFound.join(", ")}`;
|
|
450
|
+
if (details.availableIds?.length) {
|
|
451
|
+
text += `\n${theme.fg("dim", "Available:")} ${details.availableIds.join(", ")}`;
|
|
452
|
+
} else {
|
|
453
|
+
text += `\n${theme.fg("dim", "No outputs available in current session")}`;
|
|
454
|
+
}
|
|
455
|
+
return new Text(text, 0, 0);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const outputs = details?.outputs ?? [];
|
|
459
|
+
|
|
460
|
+
// No session case
|
|
461
|
+
if (outputs.length === 0) {
|
|
462
|
+
const textContent = result.content?.find((c: any) => c.type === "text")?.text;
|
|
463
|
+
return new Text(
|
|
464
|
+
`${theme.fg("warning", ICON_WARNING)} ${theme.fg("muted", textContent || "No outputs")}`,
|
|
465
|
+
0,
|
|
466
|
+
0,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Success: single output
|
|
471
|
+
if (outputs.length === 1) {
|
|
472
|
+
const o = outputs[0];
|
|
473
|
+
const summary = `read ${o.id}.out.md (${o.lineCount} lines, ${formatBytes(o.charCount)})`;
|
|
474
|
+
return new Text(`${theme.fg("success", ICON_SUCCESS)} ${theme.fg("dim", summary)}`, 0, 0);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Success: multiple outputs (tree display)
|
|
478
|
+
const expandHint = expanded ? "" : theme.fg("dim", " (Ctrl+O to expand)");
|
|
479
|
+
let text = `${theme.fg("success", ICON_SUCCESS)} ${theme.fg("dim", `read ${outputs.length} outputs`)}${expandHint}`;
|
|
480
|
+
|
|
481
|
+
const maxOutputs = expanded ? outputs.length : Math.min(outputs.length, 5);
|
|
482
|
+
for (let i = 0; i < maxOutputs; i++) {
|
|
483
|
+
const o = outputs[i];
|
|
484
|
+
const isLast = i === maxOutputs - 1 && (expanded || outputs.length <= 5);
|
|
485
|
+
const branch = isLast ? TREE_END : TREE_MID;
|
|
486
|
+
text += `\n ${theme.fg("dim", branch)} ${theme.fg("accent", o.id)} ${theme.fg("dim", `(${o.lineCount} lines)`)}`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (!expanded && outputs.length > 5) {
|
|
490
|
+
text += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${outputs.length - 5} more outputs`)}`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return new Text(text, 0, 0);
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
|
|
419
497
|
// ============================================================================
|
|
420
498
|
// Task Renderer
|
|
421
499
|
// ============================================================================
|
|
@@ -534,6 +612,7 @@ export const toolRenderers: Record<
|
|
|
534
612
|
notebook: notebookRenderer,
|
|
535
613
|
ls: lsRenderer,
|
|
536
614
|
lsp: lspRenderer,
|
|
615
|
+
output: outputRenderer,
|
|
537
616
|
task: taskRenderer,
|
|
538
617
|
web_fetch: webFetchRenderer,
|
|
539
618
|
web_search: webSearchRenderer,
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review tools - report_finding and submit_review
|
|
3
|
+
*
|
|
4
|
+
* Used by the reviewer agent to report findings in a structured way.
|
|
5
|
+
* Both tools are hidden by default - only enabled when explicitly listed in agent's tools.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
9
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
10
|
+
import { Container, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
11
|
+
import { Type } from "@sinclair/typebox";
|
|
12
|
+
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
13
|
+
|
|
14
|
+
const PRIORITY_LABELS: Record<number, string> = {
|
|
15
|
+
0: "P0",
|
|
16
|
+
1: "P1",
|
|
17
|
+
2: "P2",
|
|
18
|
+
3: "P3",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const _PRIORITY_DESCRIPTIONS: Record<number, string> = {
|
|
22
|
+
0: "Drop everything to fix. Blocking release, operations, or major usage.",
|
|
23
|
+
1: "Urgent. Should be addressed in the next cycle.",
|
|
24
|
+
2: "Normal. To be fixed eventually.",
|
|
25
|
+
3: "Low. Nice to have.",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// report_finding schema
|
|
29
|
+
const ReportFindingParams = Type.Object({
|
|
30
|
+
title: Type.String({
|
|
31
|
+
description: "≤80 chars, imperative, prefixed with [P0-P3]. E.g., '[P1] Un-padding slices along wrong dimension'",
|
|
32
|
+
}),
|
|
33
|
+
body: Type.String({
|
|
34
|
+
description: "Markdown explaining why this is a problem. One paragraph max.",
|
|
35
|
+
}),
|
|
36
|
+
priority: Type.Union([Type.Literal(0), Type.Literal(1), Type.Literal(2), Type.Literal(3)], {
|
|
37
|
+
description: "0=P0 (critical), 1=P1 (urgent), 2=P2 (normal), 3=P3 (low)",
|
|
38
|
+
}),
|
|
39
|
+
confidence: Type.Number({
|
|
40
|
+
minimum: 0,
|
|
41
|
+
maximum: 1,
|
|
42
|
+
description: "Confidence score 0.0-1.0",
|
|
43
|
+
}),
|
|
44
|
+
file_path: Type.String({ description: "Absolute path to the file" }),
|
|
45
|
+
line_start: Type.Number({ description: "Start line of the issue" }),
|
|
46
|
+
line_end: Type.Number({ description: "End line of the issue" }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
interface ReportFindingDetails {
|
|
50
|
+
title: string;
|
|
51
|
+
body: string;
|
|
52
|
+
priority: number;
|
|
53
|
+
confidence: number;
|
|
54
|
+
file_path: string;
|
|
55
|
+
line_start: number;
|
|
56
|
+
line_end: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFindingDetails, Theme> = {
|
|
60
|
+
name: "report_finding",
|
|
61
|
+
label: "Report Finding",
|
|
62
|
+
description: "Report a code review finding. Use this for each issue found. Call submit_review when done.",
|
|
63
|
+
parameters: ReportFindingParams,
|
|
64
|
+
hidden: true,
|
|
65
|
+
|
|
66
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
67
|
+
const { title, body, priority, confidence, file_path, line_start, line_end } = params;
|
|
68
|
+
const location = `${file_path}:${line_start}${line_end !== line_start ? `-${line_end}` : ""}`;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
content: [
|
|
72
|
+
{
|
|
73
|
+
type: "text",
|
|
74
|
+
text: `Finding recorded: ${PRIORITY_LABELS[priority]} ${title}\nLocation: ${location}\nConfidence: ${(confidence * 100).toFixed(0)}%`,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
details: { title, body, priority, confidence, file_path, line_start, line_end },
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
renderCall(args, theme): Component {
|
|
82
|
+
const priority = PRIORITY_LABELS[args.priority as number] ?? "P?";
|
|
83
|
+
const color = args.priority === 0 ? "error" : args.priority === 1 ? "warning" : "muted";
|
|
84
|
+
const titleText = String(args.title).replace(/^\[P\d\]\s*/, "");
|
|
85
|
+
return new Text(
|
|
86
|
+
`${theme.fg("toolTitle", theme.bold("report_finding "))}${theme.fg(color, `[${priority}]`)} ${theme.fg("dim", titleText)}`,
|
|
87
|
+
0,
|
|
88
|
+
0,
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
renderResult(result, _options, theme): Component {
|
|
93
|
+
const { details } = result;
|
|
94
|
+
if (!details) {
|
|
95
|
+
const text = result.content[0];
|
|
96
|
+
return new Text(text?.type === "text" ? text.text : "", 0, 0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const priority = PRIORITY_LABELS[details.priority] ?? "P?";
|
|
100
|
+
const color = details.priority === 0 ? "error" : details.priority === 1 ? "warning" : "muted";
|
|
101
|
+
const location = `${details.file_path}:${details.line_start}${details.line_end !== details.line_start ? `-${details.line_end}` : ""}`;
|
|
102
|
+
|
|
103
|
+
return new Text(
|
|
104
|
+
`${theme.fg("success", "✓")} ${theme.fg(color, `[${priority}]`)} ${theme.fg("dim", location)}`,
|
|
105
|
+
0,
|
|
106
|
+
0,
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// submit_review schema
|
|
112
|
+
const SubmitReviewParams = Type.Object({
|
|
113
|
+
overall_correctness: Type.Union([Type.Literal("correct"), Type.Literal("incorrect")], {
|
|
114
|
+
description: "Whether the patch is correct (no bugs, tests won't break)",
|
|
115
|
+
}),
|
|
116
|
+
explanation: Type.String({
|
|
117
|
+
description: "1-3 sentence explanation justifying the verdict",
|
|
118
|
+
}),
|
|
119
|
+
confidence: Type.Number({
|
|
120
|
+
minimum: 0,
|
|
121
|
+
maximum: 1,
|
|
122
|
+
description: "Overall confidence score 0.0-1.0",
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
interface SubmitReviewDetails {
|
|
127
|
+
overall_correctness: "correct" | "incorrect";
|
|
128
|
+
explanation: string;
|
|
129
|
+
confidence: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const submitReviewTool: AgentTool<typeof SubmitReviewParams, SubmitReviewDetails, Theme> = {
|
|
133
|
+
name: "submit_review",
|
|
134
|
+
label: "Submit Review",
|
|
135
|
+
description: "Submit the final review verdict. Call this after all findings have been reported.",
|
|
136
|
+
parameters: SubmitReviewParams,
|
|
137
|
+
hidden: true,
|
|
138
|
+
|
|
139
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
140
|
+
const { overall_correctness, explanation, confidence } = params;
|
|
141
|
+
|
|
142
|
+
let summary = `## Review Summary\n\n`;
|
|
143
|
+
summary += `**Verdict:** ${overall_correctness === "correct" ? "✓ Patch is correct" : "✗ Patch is incorrect"}\n`;
|
|
144
|
+
summary += `**Confidence:** ${(confidence * 100).toFixed(0)}%\n\n`;
|
|
145
|
+
summary += explanation;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: "text", text: summary }],
|
|
149
|
+
details: { overall_correctness, explanation, confidence },
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
renderCall(args, theme): Component {
|
|
154
|
+
const verdict = args.overall_correctness === "correct" ? "correct" : "incorrect";
|
|
155
|
+
const color = args.overall_correctness === "correct" ? "success" : "error";
|
|
156
|
+
return new Text(
|
|
157
|
+
`${theme.fg("toolTitle", theme.bold("submit_review "))}${theme.fg(color, verdict)} ${theme.fg("dim", `(${((args.confidence as number) * 100).toFixed(0)}%)`)}`,
|
|
158
|
+
0,
|
|
159
|
+
0,
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
renderResult(result, { expanded }, theme): Component {
|
|
164
|
+
const { details } = result;
|
|
165
|
+
if (!details) {
|
|
166
|
+
const text = result.content[0];
|
|
167
|
+
return new Text(text?.type === "text" ? text.text : "", 0, 0);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const container = new Container();
|
|
171
|
+
const verdictColor = details.overall_correctness === "correct" ? "success" : "error";
|
|
172
|
+
const verdictIcon = details.overall_correctness === "correct" ? "✓" : "✗";
|
|
173
|
+
|
|
174
|
+
container.addChild(
|
|
175
|
+
new Text(
|
|
176
|
+
`${theme.fg(verdictColor, verdictIcon)} Patch is ${theme.fg(verdictColor, details.overall_correctness)} ${theme.fg("dim", `(${(details.confidence * 100).toFixed(0)}% confidence)`)}`,
|
|
177
|
+
0,
|
|
178
|
+
0,
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (expanded) {
|
|
183
|
+
container.addChild(new Spacer(1));
|
|
184
|
+
container.addChild(new Text(theme.fg("dim", details.explanation), 0, 0));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return container;
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export function createReportFindingTool(): AgentTool<typeof ReportFindingParams, ReportFindingDetails, Theme> {
|
|
192
|
+
return reportFindingTool;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function createSubmitReviewTool(): AgentTool<typeof SubmitReviewParams, SubmitReviewDetails, Theme> {
|
|
196
|
+
return submitReviewTool;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Re-export types for external use
|
|
200
|
+
export type { ReportFindingDetails, SubmitReviewDetails };
|
|
201
|
+
|
|
202
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
203
|
+
// Subprocess tool handlers - registered for extraction/rendering in task tool
|
|
204
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
import path from "node:path";
|
|
207
|
+
import { subprocessToolRegistry } from "./task/subprocess-tool-registry";
|
|
208
|
+
|
|
209
|
+
// Register report_finding handler
|
|
210
|
+
subprocessToolRegistry.register<ReportFindingDetails>("report_finding", {
|
|
211
|
+
extractData: (event) => event.result?.details as ReportFindingDetails | undefined,
|
|
212
|
+
|
|
213
|
+
renderInline: (data, theme) => {
|
|
214
|
+
const priority = PRIORITY_LABELS[data.priority] ?? "P?";
|
|
215
|
+
const color = data.priority === 0 ? "error" : data.priority === 1 ? "warning" : "muted";
|
|
216
|
+
const titleText = data.title.replace(/^\[P\d\]\s*/, "");
|
|
217
|
+
const loc = `${path.basename(data.file_path)}:${data.line_start}`;
|
|
218
|
+
return new Text(`${theme.fg(color, `[${priority}]`)} ${titleText} ${theme.fg("dim", loc)}`, 0, 0);
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
renderFinal: (allData, theme, expanded) => {
|
|
222
|
+
const container = new Container();
|
|
223
|
+
const displayCount = expanded ? allData.length : Math.min(3, allData.length);
|
|
224
|
+
|
|
225
|
+
for (let i = 0; i < displayCount; i++) {
|
|
226
|
+
const data = allData[i];
|
|
227
|
+
const priority = PRIORITY_LABELS[data.priority] ?? "P?";
|
|
228
|
+
const color = data.priority === 0 ? "error" : data.priority === 1 ? "warning" : "muted";
|
|
229
|
+
const titleText = data.title.replace(/^\[P\d\]\s*/, "");
|
|
230
|
+
const loc = `${path.basename(data.file_path)}:${data.line_start}`;
|
|
231
|
+
|
|
232
|
+
container.addChild(
|
|
233
|
+
new Text(` ${theme.fg(color, `[${priority}]`)} ${titleText} ${theme.fg("dim", loc)}`, 0, 0),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (expanded && data.body) {
|
|
237
|
+
container.addChild(new Text(` ${theme.fg("dim", data.body)}`, 0, 0));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (allData.length > displayCount) {
|
|
242
|
+
container.addChild(new Text(theme.fg("dim", ` ... ${allData.length - displayCount} more findings`), 0, 0));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return container;
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Register submit_review handler
|
|
250
|
+
subprocessToolRegistry.register<SubmitReviewDetails>("submit_review", {
|
|
251
|
+
extractData: (event) => event.result?.details as SubmitReviewDetails | undefined,
|
|
252
|
+
|
|
253
|
+
// Terminate subprocess after review is submitted
|
|
254
|
+
shouldTerminate: () => true,
|
|
255
|
+
|
|
256
|
+
renderInline: (data, theme) => {
|
|
257
|
+
const verdictColor = data.overall_correctness === "correct" ? "success" : "error";
|
|
258
|
+
const verdictIcon = data.overall_correctness === "correct" ? "✓" : "✗";
|
|
259
|
+
return new Text(
|
|
260
|
+
`${theme.fg(verdictColor, verdictIcon)} Review: ${theme.fg(verdictColor, data.overall_correctness)} (${(data.confidence * 100).toFixed(0)}%)`,
|
|
261
|
+
0,
|
|
262
|
+
0,
|
|
263
|
+
);
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
// Note: renderFinal is NOT used for submit_review - we use the combined
|
|
267
|
+
// renderReviewResult in render.ts to show verdict + findings together
|
|
268
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
-
import type { AgentDefinition, AgentSource } from "./types
|
|
11
|
+
import type { AgentDefinition, AgentSource } from "./types";
|
|
12
12
|
|
|
13
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const BUNDLED_AGENTS_DIR = path.join(__dirname, "bundled-agents");
|