@oh-my-pi/pi-coding-agent 14.5.1 → 14.5.3
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 +43 -0
- package/package.json +8 -8
- package/src/config/prompt-templates.ts +6 -3
- package/src/edit/block.ts +308 -0
- package/src/edit/indent.ts +150 -0
- package/src/edit/modes/atom.ts +341 -114
- package/src/lsp/utils.ts +6 -36
- package/src/modes/components/status-line.ts +36 -0
- package/src/modes/controllers/event-controller.ts +27 -2
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/atom.md +64 -86
- package/src/prompts/tools/read.md +1 -1
- package/src/session/agent-session.ts +28 -1
- package/src/tools/ast-edit.ts +23 -44
- package/src/tools/ast-grep.ts +18 -42
- package/src/tools/checkpoint.ts +2 -0
- package/src/tools/exit-plan-mode.ts +1 -0
- package/src/tools/grep.ts +11 -46
- package/src/tools/grouped-file-output.ts +96 -0
- package/src/tools/read.ts +6 -0
- package/src/tools/report-tool-issue.ts +1 -0
- package/src/tools/resolve.ts +2 -0
- package/src/tools/review.ts +1 -0
- package/src/tools/todo-write.ts +1 -1
- package/src/tools/yield.ts +1 -0
- package/src/utils/tool-choice.ts +6 -1
package/src/tools/grep.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
|
|
|
14
14
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
15
15
|
import type { ToolSession } from ".";
|
|
16
16
|
import { createFileRecorder } from "./file-recorder";
|
|
17
|
+
import { formatGroupedFiles } from "./grouped-file-output";
|
|
17
18
|
import { formatMatchLine } from "./match-line-format";
|
|
18
19
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
19
20
|
import {
|
|
@@ -283,7 +284,6 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
283
284
|
}
|
|
284
285
|
const outputLines: string[] = [];
|
|
285
286
|
let linesTruncated = false;
|
|
286
|
-
const hasContextLines = normalizedContextBefore > 0 || normalizedContextAfter > 0;
|
|
287
287
|
const matchesByFile = new Map<string, GrepMatch[]>();
|
|
288
288
|
for (const match of selectedMatches) {
|
|
289
289
|
const relativePath = formatPath(match.path);
|
|
@@ -332,46 +332,16 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
332
332
|
return { model: modelOut, display: displayOut };
|
|
333
333
|
};
|
|
334
334
|
if (isDirectory) {
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
for (const relativePath of directoryFiles) {
|
|
346
|
-
const rendered = renderMatchesForFile(relativePath);
|
|
347
|
-
if (rendered.model.length === 0) continue;
|
|
348
|
-
if (outputLines.length > 0) {
|
|
349
|
-
outputLines.push("");
|
|
350
|
-
displayLines.push("");
|
|
351
|
-
}
|
|
352
|
-
const header = `# ${path.basename(relativePath)}`;
|
|
353
|
-
outputLines.push(header, ...rendered.model);
|
|
354
|
-
displayLines.push(header, ...rendered.display);
|
|
355
|
-
}
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
const renderedFiles = directoryFiles
|
|
359
|
-
.map(relativePath => ({ relativePath, rendered: renderMatchesForFile(relativePath) }))
|
|
360
|
-
.filter(file => file.rendered.model.length > 0);
|
|
361
|
-
if (renderedFiles.length === 0) continue;
|
|
362
|
-
if (outputLines.length > 0) {
|
|
363
|
-
outputLines.push("");
|
|
364
|
-
displayLines.push("");
|
|
365
|
-
}
|
|
366
|
-
const dirHeader = `# ${directory}`;
|
|
367
|
-
outputLines.push(dirHeader);
|
|
368
|
-
displayLines.push(dirHeader);
|
|
369
|
-
for (const { relativePath, rendered } of renderedFiles) {
|
|
370
|
-
const fileHeader = `## └─ ${path.basename(relativePath)}`;
|
|
371
|
-
outputLines.push(fileHeader, ...rendered.model);
|
|
372
|
-
displayLines.push(fileHeader, ...rendered.display);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
335
|
+
const grouped = formatGroupedFiles(fileList, relativePath => {
|
|
336
|
+
const rendered = renderMatchesForFile(relativePath);
|
|
337
|
+
return {
|
|
338
|
+
modelLines: rendered.model,
|
|
339
|
+
displayLines: rendered.display,
|
|
340
|
+
skip: rendered.model.length === 0,
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
outputLines.push(...grouped.model);
|
|
344
|
+
displayLines.push(...grouped.display);
|
|
375
345
|
} else {
|
|
376
346
|
for (const relativePath of fileList) {
|
|
377
347
|
const rendered = renderMatchesForFile(relativePath);
|
|
@@ -379,11 +349,6 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
379
349
|
displayLines.push(...rendered.display);
|
|
380
350
|
}
|
|
381
351
|
}
|
|
382
|
-
if (hasContextLines && outputLines.length > 0) {
|
|
383
|
-
outputLines.unshift(
|
|
384
|
-
"[grep] '*' marks match lines; leading space marks context. Anchor and content are separated by '|'.",
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
352
|
if (matchLimitReached || result.limitReached) {
|
|
388
353
|
outputLines.push("", limitMessage);
|
|
389
354
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One file's contribution to a grouped file output. The header itself is generated
|
|
5
|
+
* by `formatGroupedFiles` (single `#` for root files, `##` for files inside a dir);
|
|
6
|
+
* use `headerSuffix` to tack on extras like ` (1 replacement)`.
|
|
7
|
+
*/
|
|
8
|
+
export interface GroupedFileSection {
|
|
9
|
+
/** Optional suffix appended to the file header. */
|
|
10
|
+
headerSuffix?: string;
|
|
11
|
+
/** Body lines emitted into the textual model output. */
|
|
12
|
+
modelLines: string[];
|
|
13
|
+
/** Body lines emitted into the display output. Defaults to `modelLines`. */
|
|
14
|
+
displayLines?: string[];
|
|
15
|
+
/** When true, the file (and its header) is omitted entirely. */
|
|
16
|
+
skip?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GroupedFilesOutput {
|
|
20
|
+
model: string[];
|
|
21
|
+
display: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Render a list of files as directory-grouped sections shared by grep, ast-grep,
|
|
26
|
+
* ast-edit, and the LSP diagnostic formatter.
|
|
27
|
+
*
|
|
28
|
+
* Layout:
|
|
29
|
+
* # dir/
|
|
30
|
+
* ## file.ts
|
|
31
|
+
* …body…
|
|
32
|
+
*
|
|
33
|
+
* # otherdir/
|
|
34
|
+
* ## other.ts
|
|
35
|
+
* …body…
|
|
36
|
+
*
|
|
37
|
+
* Files in the project root (directory `.`) become single-`#` headers without a
|
|
38
|
+
* `## file` line, matching the existing convention.
|
|
39
|
+
*/
|
|
40
|
+
export function formatGroupedFiles(
|
|
41
|
+
files: string[],
|
|
42
|
+
renderFile: (filePath: string) => GroupedFileSection,
|
|
43
|
+
): GroupedFilesOutput {
|
|
44
|
+
const filesByDirectory = new Map<string, string[]>();
|
|
45
|
+
for (const filePath of files) {
|
|
46
|
+
const directory = path.dirname(filePath).replace(/\\/g, "/");
|
|
47
|
+
if (!filesByDirectory.has(directory)) {
|
|
48
|
+
filesByDirectory.set(directory, []);
|
|
49
|
+
}
|
|
50
|
+
filesByDirectory.get(directory)!.push(filePath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const model: string[] = [];
|
|
54
|
+
const display: string[] = [];
|
|
55
|
+
|
|
56
|
+
const pushSeparatorIfNeeded = () => {
|
|
57
|
+
if (model.length > 0) {
|
|
58
|
+
model.push("");
|
|
59
|
+
display.push("");
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
for (const [directory, dirFiles] of filesByDirectory) {
|
|
64
|
+
if (directory === ".") {
|
|
65
|
+
for (const filePath of dirFiles) {
|
|
66
|
+
const section = renderFile(filePath);
|
|
67
|
+
if (section.skip) continue;
|
|
68
|
+
pushSeparatorIfNeeded();
|
|
69
|
+
const header = `# ${path.basename(filePath)}${section.headerSuffix ?? ""}`;
|
|
70
|
+
model.push(header, ...section.modelLines);
|
|
71
|
+
display.push(header, ...(section.displayLines ?? section.modelLines));
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sections: Array<{ filePath: string; section: GroupedFileSection }> = [];
|
|
77
|
+
for (const filePath of dirFiles) {
|
|
78
|
+
const section = renderFile(filePath);
|
|
79
|
+
if (section.skip) continue;
|
|
80
|
+
sections.push({ filePath, section });
|
|
81
|
+
}
|
|
82
|
+
if (sections.length === 0) continue;
|
|
83
|
+
|
|
84
|
+
pushSeparatorIfNeeded();
|
|
85
|
+
const dirHeader = `# ${directory}/`;
|
|
86
|
+
model.push(dirHeader);
|
|
87
|
+
display.push(dirHeader);
|
|
88
|
+
for (const { filePath, section } of sections) {
|
|
89
|
+
const fileHeader = `## ${path.basename(filePath)}${section.headerSuffix ?? ""}`;
|
|
90
|
+
model.push(fileHeader, ...section.modelLines);
|
|
91
|
+
display.push(fileHeader, ...(section.displayLines ?? section.modelLines));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { model, display };
|
|
96
|
+
}
|
package/src/tools/read.ts
CHANGED
|
@@ -456,6 +456,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
456
456
|
readonly parameters = readSchema;
|
|
457
457
|
readonly nonAbortable = true;
|
|
458
458
|
readonly strict = true;
|
|
459
|
+
readonly intent = (args: Partial<ReadParams>): string => {
|
|
460
|
+
const p = typeof args.path === "string" ? args.path.trim() : "";
|
|
461
|
+
if (!p) return "Reading";
|
|
462
|
+
const isUrl = /^(https?|ftp):\/\//i.test(p);
|
|
463
|
+
return isUrl ? `Fetching ${p}` : `Reading ${p}`;
|
|
464
|
+
};
|
|
459
465
|
|
|
460
466
|
readonly #autoResizeImages: boolean;
|
|
461
467
|
readonly #defaultLimit: number;
|
|
@@ -60,6 +60,7 @@ export function createReportToolIssueTool(session: ToolSession): AgentTool {
|
|
|
60
60
|
strict: false,
|
|
61
61
|
description: "Report unexpected tool behavior for automated QA tracking.",
|
|
62
62
|
parameters: ReportToolIssueParams,
|
|
63
|
+
intent: "omit",
|
|
63
64
|
async execute(_toolCallId, rawParams) {
|
|
64
65
|
try {
|
|
65
66
|
const params = rawParams as { tool: string; report: string };
|
package/src/tools/resolve.ts
CHANGED
|
@@ -110,6 +110,8 @@ export class ResolveTool implements AgentTool<typeof resolveSchema, ResolveToolD
|
|
|
110
110
|
readonly description: string;
|
|
111
111
|
readonly parameters = resolveSchema;
|
|
112
112
|
readonly strict = true;
|
|
113
|
+
readonly intent = (args: Partial<ResolveParams>) =>
|
|
114
|
+
args.action === "discard" ? "Discarding pending action" : "Applying pending action";
|
|
113
115
|
|
|
114
116
|
constructor(private readonly session: ToolSession) {
|
|
115
117
|
this.description = prompt.render(resolveDescription);
|
package/src/tools/review.ts
CHANGED
|
@@ -135,6 +135,7 @@ export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFind
|
|
|
135
135
|
label: "Report Finding",
|
|
136
136
|
description: "Report a code review finding. Use this for each issue found. Call yield when done.",
|
|
137
137
|
parameters: ReportFindingParams,
|
|
138
|
+
intent: "omit",
|
|
138
139
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
139
140
|
const { title, body, priority, confidence, file_path, line_start, line_end } = params;
|
|
140
141
|
const location = `${file_path}:${line_start}${line_end !== line_start ? `-${line_end}` : ""}`;
|
package/src/tools/todo-write.ts
CHANGED
|
@@ -583,6 +583,7 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
|
|
|
583
583
|
readonly parameters = todoWriteSchema;
|
|
584
584
|
readonly concurrency = "exclusive";
|
|
585
585
|
readonly strict = true;
|
|
586
|
+
readonly intent = "omit" as const;
|
|
586
587
|
|
|
587
588
|
constructor(private readonly session: ToolSession) {
|
|
588
589
|
this.description = prompt.render(todoWriteDescription);
|
|
@@ -716,7 +717,6 @@ export const todoWriteToolRenderer = {
|
|
|
716
717
|
const lines: string[] = [header];
|
|
717
718
|
for (let p = 0; p < phases.length; p++) {
|
|
718
719
|
const phase = phases[p];
|
|
719
|
-
if (p > 0) lines.push("");
|
|
720
720
|
if (phases.length > 1) {
|
|
721
721
|
lines.push(uiTheme.fg("accent", chalk.bold(` ${phase.name}`)));
|
|
722
722
|
}
|
package/src/tools/yield.ts
CHANGED
|
@@ -49,6 +49,7 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
49
49
|
"The `data`/`error` wrapper is required — do not put your output directly in `result`.";
|
|
50
50
|
readonly parameters: TSchema;
|
|
51
51
|
strict = true;
|
|
52
|
+
readonly intent = "omit" as const;
|
|
52
53
|
lenientArgValidation = true;
|
|
53
54
|
|
|
54
55
|
readonly #validate?: ValidateFunction;
|
package/src/utils/tool-choice.ts
CHANGED
|
@@ -20,7 +20,12 @@ export function buildNamedToolChoice(toolName: string, model?: Model<Api>): Tool
|
|
|
20
20
|
return { type: "function", name: toolName };
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
if (
|
|
23
|
+
if (
|
|
24
|
+
model.api === "google-generative-ai" ||
|
|
25
|
+
model.api === "google-gemini-cli" ||
|
|
26
|
+
model.api === "google-vertex" ||
|
|
27
|
+
model.api === "ollama-chat"
|
|
28
|
+
) {
|
|
24
29
|
return "required";
|
|
25
30
|
}
|
|
26
31
|
|