@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/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 filesByDirectory = new Map<string, string[]>();
336
- for (const relativePath of fileList) {
337
- const directory = path.dirname(relativePath).replace(/\\/g, "/");
338
- if (!filesByDirectory.has(directory)) {
339
- filesByDirectory.set(directory, []);
340
- }
341
- filesByDirectory.get(directory)!.push(relativePath);
342
- }
343
- for (const [directory, directoryFiles] of filesByDirectory) {
344
- if (directory === ".") {
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 };
@@ -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);
@@ -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}` : ""}`;
@@ -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
  }
@@ -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;
@@ -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 (model.api === "google-generative-ai" || model.api === "google-gemini-cli" || model.api === "google-vertex") {
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