@oh-my-pi/pi-coding-agent 14.3.0 → 14.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +98 -1
  2. package/package.json +7 -7
  3. package/src/autoresearch/prompt.md +1 -1
  4. package/src/commit/agentic/prompts/analyze-file.md +1 -1
  5. package/src/config/model-registry.ts +67 -15
  6. package/src/config/prompt-templates.ts +5 -5
  7. package/src/config/settings-schema.ts +4 -4
  8. package/src/cursor.ts +3 -8
  9. package/src/discovery/helpers.ts +3 -3
  10. package/src/edit/diff.ts +50 -47
  11. package/src/edit/index.ts +86 -57
  12. package/src/edit/line-hash.ts +743 -24
  13. package/src/edit/modes/apply-patch.ts +0 -9
  14. package/src/edit/modes/atom.ts +893 -0
  15. package/src/edit/modes/chunk.ts +14 -24
  16. package/src/edit/modes/hashline.ts +193 -146
  17. package/src/edit/modes/patch.ts +5 -9
  18. package/src/edit/modes/replace.ts +6 -11
  19. package/src/edit/renderer.ts +14 -10
  20. package/src/edit/streaming.ts +50 -16
  21. package/src/exec/bash-executor.ts +2 -4
  22. package/src/export/html/template.generated.ts +1 -1
  23. package/src/export/html/template.js +4 -12
  24. package/src/extensibility/custom-tools/types.ts +2 -0
  25. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  26. package/src/internal-urls/docs-index.generated.ts +2 -2
  27. package/src/lsp/defaults.json +142 -652
  28. package/src/lsp/index.ts +1 -1
  29. package/src/mcp/render.ts +1 -8
  30. package/src/modes/components/assistant-message.ts +4 -0
  31. package/src/modes/components/diff.ts +23 -14
  32. package/src/modes/components/footer.ts +21 -16
  33. package/src/modes/components/session-selector.ts +3 -3
  34. package/src/modes/components/settings-defs.ts +6 -1
  35. package/src/modes/components/todo-reminder.ts +1 -8
  36. package/src/modes/components/tool-execution.ts +1 -4
  37. package/src/modes/controllers/selector-controller.ts +1 -1
  38. package/src/modes/print-mode.ts +8 -0
  39. package/src/prompts/agents/librarian.md +1 -1
  40. package/src/prompts/agents/reviewer.md +4 -4
  41. package/src/prompts/ci-green-request.md +1 -1
  42. package/src/prompts/review-request.md +1 -1
  43. package/src/prompts/system/subagent-system-prompt.md +3 -3
  44. package/src/prompts/system/subagent-yield-reminder.md +11 -0
  45. package/src/prompts/system/system-prompt.md +3 -0
  46. package/src/prompts/tools/ask.md +3 -2
  47. package/src/prompts/tools/ast-edit.md +16 -20
  48. package/src/prompts/tools/ast-grep.md +19 -24
  49. package/src/prompts/tools/atom.md +87 -0
  50. package/src/prompts/tools/chunk-edit.md +37 -161
  51. package/src/prompts/tools/debug.md +4 -5
  52. package/src/prompts/tools/exit-plan-mode.md +4 -5
  53. package/src/prompts/tools/find.md +4 -8
  54. package/src/prompts/tools/github.md +18 -0
  55. package/src/prompts/tools/grep.md +4 -5
  56. package/src/prompts/tools/hashline.md +22 -89
  57. package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
  58. package/src/prompts/tools/inspect-image.md +6 -6
  59. package/src/prompts/tools/lsp.md +1 -1
  60. package/src/prompts/tools/patch.md +12 -19
  61. package/src/prompts/tools/python.md +3 -2
  62. package/src/prompts/tools/read-chunk.md +2 -3
  63. package/src/prompts/tools/read.md +2 -2
  64. package/src/prompts/tools/ssh.md +8 -17
  65. package/src/prompts/tools/todo-write.md +54 -41
  66. package/src/sdk.ts +14 -9
  67. package/src/session/agent-session.ts +25 -2
  68. package/src/session/session-manager.ts +4 -1
  69. package/src/task/executor.ts +43 -48
  70. package/src/task/render.ts +11 -13
  71. package/src/tools/ask.ts +7 -7
  72. package/src/tools/ast-edit.ts +45 -41
  73. package/src/tools/ast-grep.ts +77 -85
  74. package/src/tools/bash.ts +8 -9
  75. package/src/tools/browser.ts +32 -30
  76. package/src/tools/calculator.ts +4 -4
  77. package/src/tools/cancel-job.ts +1 -1
  78. package/src/tools/checkpoint.ts +2 -2
  79. package/src/tools/debug.ts +41 -37
  80. package/src/tools/exit-plan-mode.ts +1 -1
  81. package/src/tools/find.ts +4 -4
  82. package/src/tools/gh-renderer.ts +12 -4
  83. package/src/tools/gh.ts +509 -697
  84. package/src/tools/grep.ts +116 -131
  85. package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
  86. package/src/tools/index.ts +14 -32
  87. package/src/tools/inspect-image.ts +3 -3
  88. package/src/tools/json-tree.ts +114 -114
  89. package/src/tools/match-line-format.ts +8 -7
  90. package/src/tools/notebook.ts +8 -7
  91. package/src/tools/poll-tool.ts +2 -1
  92. package/src/tools/python.ts +9 -23
  93. package/src/tools/read.ts +32 -25
  94. package/src/tools/render-mermaid.ts +1 -1
  95. package/src/tools/render-utils.ts +18 -0
  96. package/src/tools/renderers.ts +2 -2
  97. package/src/tools/report-tool-issue.ts +3 -2
  98. package/src/tools/resolve.ts +1 -1
  99. package/src/tools/review.ts +12 -10
  100. package/src/tools/search-tool-bm25.ts +2 -4
  101. package/src/tools/ssh.ts +4 -4
  102. package/src/tools/todo-write.ts +172 -147
  103. package/src/tools/vim.ts +14 -15
  104. package/src/tools/write.ts +4 -4
  105. package/src/tools/{submit-result.ts → yield.ts} +11 -13
  106. package/src/utils/edit-mode.ts +2 -1
  107. package/src/utils/file-display-mode.ts +10 -5
  108. package/src/utils/git.ts +9 -5
  109. package/src/utils/shell-snapshot.ts +2 -3
  110. package/src/vim/render.ts +4 -4
  111. package/src/prompts/system/subagent-submit-reminder.md +0 -11
  112. package/src/prompts/tools/gh-issue-view.md +0 -11
  113. package/src/prompts/tools/gh-pr-checkout.md +0 -12
  114. package/src/prompts/tools/gh-pr-diff.md +0 -12
  115. package/src/prompts/tools/gh-pr-push.md +0 -12
  116. package/src/prompts/tools/gh-pr-view.md +0 -11
  117. package/src/prompts/tools/gh-repo-view.md +0 -11
  118. package/src/prompts/tools/gh-run-watch.md +0 -12
  119. package/src/prompts/tools/gh-search-issues.md +0 -11
  120. package/src/prompts/tools/gh-search-prs.md +0 -11
package/src/tools/grep.ts CHANGED
@@ -19,29 +19,31 @@ import { createFileRecorder } from "./file-recorder";
19
19
  import { formatMatchLine } from "./match-line-format";
20
20
  import { formatFullOutputReference, type OutputMeta } from "./output-meta";
21
21
  import {
22
- combineSearchGlobs,
23
22
  hasGlobPathChars,
24
23
  normalizePathLikeInput,
25
24
  parseSearchPath,
26
25
  resolveMultiSearchPath,
27
26
  resolveToCwd,
28
27
  } from "./path-utils";
29
- import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
28
+ import {
29
+ formatCodeFrameLine,
30
+ formatCount,
31
+ formatEmptyMessage,
32
+ formatErrorMessage,
33
+ PREVIEW_LIMITS,
34
+ } from "./render-utils";
30
35
  import { ToolError } from "./tool-errors";
31
36
  import { toolResult } from "./tool-result";
32
37
 
33
38
  const grepSchema = Type.Object({
34
- pattern: Type.String({ description: "Regex pattern to search for" }),
35
- path: Type.Optional(Type.String({ description: "File or directory to search (default: cwd)" })),
36
- glob: Type.Optional(Type.String({ description: "Filter files by glob pattern (e.g., '*.js')" })),
37
- type: Type.Optional(Type.String({ description: "Filter by file type (e.g., js, py, rust)" })),
38
- i: Type.Optional(Type.Boolean({ description: "Case-insensitive search", default: false })),
39
- pre: Type.Optional(Type.Number({ description: "Lines of context before matches" })),
40
- post: Type.Optional(Type.Number({ description: "Lines of context after matches" })),
41
- multiline: Type.Optional(Type.Boolean({ description: "Enable multiline matching" })),
42
- gitignore: Type.Optional(Type.Boolean({ description: "Respect .gitignore files during search", default: true })),
43
- limit: Type.Optional(Type.Number({ description: "Limit output to first N matches", default: 20 })),
44
- offset: Type.Optional(Type.Number({ description: "Skip first N entries before applying limit", default: 0 })),
39
+ pattern: Type.String({ description: "regex pattern", examples: ["function\\s+\\w+", "TODO"] }),
40
+ path: Type.String({
41
+ description: "file, directory, glob, comma-separated paths, or internal URL to search",
42
+ examples: ["src/", "src/foo.ts", "src/**/*.ts"],
43
+ }),
44
+ i: Type.Optional(Type.Boolean({ description: "case-insensitive search", default: false })),
45
+ gitignore: Type.Optional(Type.Boolean({ description: "respect gitignore", default: true })),
46
+ skip: Type.Optional(Type.Number({ description: "matches to skip", default: 0 })),
45
47
  });
46
48
 
47
49
  export type GrepToolInput = Static<typeof grepSchema>;
@@ -61,6 +63,10 @@ export interface GrepToolDetails {
61
63
  fileMatches?: Array<{ path: string; count: number }>;
62
64
  truncated?: boolean;
63
65
  error?: string;
66
+ /** Pre-formatted text for the user-visible TUI render. Mirrors the model-facing
67
+ * `result.text` lines but uses a `│` gutter and `*` to mark match lines (vs space for
68
+ * context). The TUI uses this directly so it never parses model-facing hashline anchors. */
69
+ displayContent?: string;
64
70
  }
65
71
 
66
72
  type GrepParams = Static<typeof grepSchema>;
@@ -88,7 +94,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
88
94
  _onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
89
95
  _toolContext?: AgentToolContext,
90
96
  ): Promise<AgentToolResult<GrepToolDetails>> {
91
- const { pattern, path: searchDir, glob, type, i, gitignore, pre, post, multiline, limit, offset } = params;
97
+ const { pattern, path: searchDir, i, gitignore, skip } = params;
92
98
 
93
99
  return untilAborted(signal, async () => {
94
100
  const normalizedPattern = pattern.trim();
@@ -97,25 +103,16 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
97
103
  throw new ToolError("Pattern must not be empty");
98
104
  }
99
105
 
100
- const normalizedOffset = offset === undefined ? 0 : Number.isFinite(offset) ? Math.floor(offset) : Number.NaN;
101
- if (normalizedOffset < 0 || !Number.isFinite(normalizedOffset)) {
102
- throw new ToolError("Offset must be a non-negative number");
103
- }
104
-
105
- const rawLimit = limit === undefined ? undefined : Number.isFinite(limit) ? Math.floor(limit) : Number.NaN;
106
- if (rawLimit !== undefined && (!Number.isFinite(rawLimit) || rawLimit < 0)) {
107
- throw new ToolError("Limit must be a non-negative number");
106
+ const normalizedSkip = skip === undefined ? 0 : Number.isFinite(skip) ? Math.floor(skip) : Number.NaN;
107
+ if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
108
+ throw new ToolError("Skip must be a non-negative number");
108
109
  }
109
- const normalizedLimit = rawLimit !== undefined && rawLimit > 0 ? rawLimit : undefined;
110
-
111
- const defaultContextBefore = this.session.settings.get("grep.contextBefore");
112
- const defaultContextAfter = this.session.settings.get("grep.contextAfter");
113
- const normalizedContextBefore = pre ?? defaultContextBefore;
114
- const normalizedContextAfter = post ?? defaultContextAfter;
110
+ const normalizedContextBefore = this.session.settings.get("grep.contextBefore");
111
+ const normalizedContextAfter = this.session.settings.get("grep.contextAfter");
115
112
  const ignoreCase = i ?? false;
116
113
  const useGitignore = gitignore ?? true;
117
114
  const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
118
- const effectiveMultiline = multiline ?? patternHasNewline;
115
+ const effectiveMultiline = patternHasNewline;
119
116
 
120
117
  const useHashLines = resolveFileDisplayMode(this.session).hashLines;
121
118
  const formatScopePath = (targetPath: string): string => {
@@ -125,41 +122,36 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
125
122
  let searchPath: string;
126
123
  let scopePath: string;
127
124
  let exactFilePaths: string[] | undefined;
128
- let globFilter = glob ? normalizePathLikeInput(glob) || undefined : undefined;
125
+ let globFilter: string | undefined;
126
+ const rawPath = normalizePathLikeInput(searchDir);
127
+ if (rawPath.length === 0) {
128
+ throw new ToolError("`path` must be a non-empty path or glob");
129
+ }
129
130
  const internalRouter = this.session.internalRouter;
130
- if (searchDir?.trim()) {
131
- const rawPath = normalizePathLikeInput(searchDir);
132
- if (internalRouter?.canHandle(rawPath)) {
133
- if (hasGlobPathChars(rawPath)) {
134
- throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
135
- }
136
- const resource = await internalRouter.resolve(rawPath);
137
- if (!resource.sourcePath) {
138
- throw new ToolError(`Cannot grep internal URL without a backing file: ${rawPath}`);
139
- }
140
- searchPath = resource.sourcePath;
141
- scopePath = formatScopePath(searchPath);
142
- } else {
143
- const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
144
- if (multiSearchPath) {
145
- searchPath = multiSearchPath.basePath;
146
- globFilter = multiSearchPath.exactFilePaths ? undefined : multiSearchPath.glob;
147
- exactFilePaths = multiSearchPath.exactFilePaths;
148
- scopePath = multiSearchPath.scopePath;
149
- } else {
150
- const parsedPath = parseSearchPath(rawPath);
151
- searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
152
- if (parsedPath.glob) {
153
- globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
154
- }
155
- scopePath = formatScopePath(searchPath);
156
- }
131
+ if (internalRouter?.canHandle(rawPath)) {
132
+ if (hasGlobPathChars(rawPath)) {
133
+ throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
134
+ }
135
+ const resource = await internalRouter.resolve(rawPath);
136
+ if (!resource.sourcePath) {
137
+ throw new ToolError(`Cannot grep internal URL without a backing file: ${rawPath}`);
157
138
  }
139
+ searchPath = resource.sourcePath;
140
+ scopePath = formatScopePath(searchPath);
158
141
  } else {
159
- searchPath = resolveToCwd(".", this.session.cwd);
160
- scopePath = ".";
142
+ const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
143
+ if (multiSearchPath) {
144
+ searchPath = multiSearchPath.basePath;
145
+ globFilter = multiSearchPath.exactFilePaths ? undefined : multiSearchPath.glob;
146
+ exactFilePaths = multiSearchPath.exactFilePaths;
147
+ scopePath = multiSearchPath.scopePath;
148
+ } else {
149
+ const parsedPath = parseSearchPath(rawPath);
150
+ searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
151
+ globFilter = parsedPath.glob;
152
+ scopePath = formatScopePath(searchPath);
153
+ }
161
154
  }
162
-
163
155
  let isDirectory: boolean;
164
156
  try {
165
157
  const stat = await Bun.file(searchPath).stat();
@@ -170,7 +162,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
170
162
  }
171
163
 
172
164
  const effectiveOutputMode = GrepOutputMode.Content;
173
- const effectiveLimit = normalizedLimit ?? DEFAULT_MATCH_LIMIT;
165
+ const effectiveLimit = DEFAULT_MATCH_LIMIT;
174
166
  const internalLimit = Math.min(effectiveLimit * 5, 2000);
175
167
 
176
168
  // Run grep
@@ -184,7 +176,6 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
184
176
  {
185
177
  pattern: normalizedPattern,
186
178
  path: exactFilePath,
187
- type: type?.trim() || undefined,
188
179
  ignoreCase,
189
180
  multiline: effectiveMultiline,
190
181
  hidden: true,
@@ -201,7 +192,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
201
192
  const relativeFilePath = path.relative(searchPath, exactFilePath).replace(/\\/g, "/");
202
193
  matches.push(...fileResult.matches.map(match => ({ ...match, path: relativeFilePath })));
203
194
  }
204
- const offsetMatches = matches.slice(normalizedOffset);
195
+ const offsetMatches = matches.slice(normalizedSkip);
205
196
  result = {
206
197
  matches: offsetMatches,
207
198
  totalMatches: offsetMatches.length,
@@ -215,14 +206,13 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
215
206
  pattern: normalizedPattern,
216
207
  path: searchPath,
217
208
  glob: globFilter,
218
- type: type?.trim() || undefined,
219
209
  ignoreCase,
220
210
  multiline: effectiveMultiline,
221
211
  hidden: true,
222
212
  gitignore: useGitignore,
223
213
  cache: false,
224
214
  maxCount: internalLimit,
225
- offset: normalizedOffset > 0 ? normalizedOffset : undefined,
215
+ offset: normalizedSkip > 0 ? normalizedSkip : undefined,
226
216
  contextBefore: normalizedContextBefore,
227
217
  contextAfter: normalizedContextAfter,
228
218
  maxColumns: DEFAULT_MAX_COLUMN,
@@ -281,6 +271,8 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
281
271
  ? roundRobinSelect(result.matches, effectiveLimit)
282
272
  : result.matches.slice(0, effectiveLimit);
283
273
  const matchLimitReached = result.matches.length > effectiveLimit;
274
+ const nextSkip = normalizedSkip + selectedMatches.length;
275
+ const limitMessage = `Result limit reached; narrow path or use skip=${nextSkip}.`;
284
276
  const { record: recordFile, list: fileList } = createFileRecorder();
285
277
  const fileMatchCounts = new Map<string, number>();
286
278
  if (selectedMatches.length === 0) {
@@ -333,7 +325,6 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
333
325
  if (fileMatches.length === 0) {
334
326
  return renderedLines;
335
327
  }
336
- const lineWidth = fileMatches[0]?.fileLineCount.toString().length ?? 1;
337
328
  const matchesByChunk = new Map<string, ChunkedGrepMatch[]>();
338
329
  for (const match of fileMatches) {
339
330
  const chunkKey = match.chunkPath ?? "";
@@ -352,7 +343,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
352
343
  renderedLines.push(anchor);
353
344
  }
354
345
  for (const match of chunkMatches) {
355
- renderedLines.push(` ${match.lineNumber.toString().padStart(lineWidth, " ")} |${match.line}`);
346
+ renderedLines.push(` ${match.lineNumber}|${match.line}`);
356
347
  fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
357
348
  }
358
349
  }
@@ -398,6 +389,9 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
398
389
  outputLines.push(...renderChunkedMatchesForFile(relativePath));
399
390
  }
400
391
  }
392
+ if (matchLimitReached || result.limitReached) {
393
+ outputLines.push("", limitMessage);
394
+ }
401
395
  const rawOutput = outputLines.join("\n");
402
396
  const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
403
397
  const truncated = Boolean(matchLimitReached || result.limitReached || truncation.truncated);
@@ -415,52 +409,49 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
415
409
  resultLimitReached: result.limitReached ? internalLimit : undefined,
416
410
  };
417
411
  if (truncation.truncated) details.truncation = truncation;
418
- const resultBuilder = toolResult(details)
419
- .text(truncation.content)
420
- .limits({
421
- matchLimit: matchLimitReached ? effectiveLimit : undefined,
422
- resultLimit: result.limitReached ? internalLimit : undefined,
423
- });
412
+ const resultBuilder = toolResult(details).text(truncation.content);
424
413
  if (truncation.truncated) {
425
414
  resultBuilder.truncation(truncation, { direction: "head" });
426
415
  }
427
416
  return resultBuilder.done();
428
417
  }
429
- const renderMatchesForFile = (relativePath: string): string[] => {
430
- const renderedLines: string[] = [];
418
+ const displayLines: string[] = [];
419
+ const renderMatchesForFile = (relativePath: string): { model: string[]; display: string[] } => {
420
+ const modelOut: string[] = [];
421
+ const displayOut: string[] = [];
431
422
  const fileMatches = matchesByFile.get(relativePath) ?? [];
432
- for (const match of fileMatches) {
433
- const lineNumbers: number[] = [match.lineNumber];
434
- if (match.contextBefore) {
435
- for (const ctx of match.contextBefore) {
436
- lineNumbers.push(ctx.lineNumber);
437
- }
423
+ const lineNumberWidth = fileMatches.reduce((width, match) => {
424
+ let nextWidth = Math.max(width, String(match.lineNumber).length);
425
+ for (const ctx of match.contextBefore ?? []) {
426
+ nextWidth = Math.max(nextWidth, String(ctx.lineNumber).length);
438
427
  }
439
- if (match.contextAfter) {
440
- for (const ctx of match.contextAfter) {
441
- lineNumbers.push(ctx.lineNumber);
442
- }
428
+ for (const ctx of match.contextAfter ?? []) {
429
+ nextWidth = Math.max(nextWidth, String(ctx.lineNumber).length);
443
430
  }
444
- const lineWidth = Math.max(...lineNumbers.map(value => value.toString().length));
445
- const formatLine = (lineNumber: number, line: string, isMatch: boolean): string =>
446
- formatMatchLine(lineNumber, line, isMatch, { useHashLines, lineWidth });
431
+ return nextWidth;
432
+ }, 0);
433
+ for (const match of fileMatches) {
434
+ const pushLine = (lineNumber: number, line: string, isMatch: boolean) => {
435
+ modelOut.push(formatMatchLine(lineNumber, line, isMatch, { useHashLines }));
436
+ displayOut.push(formatCodeFrameLine(isMatch ? "*" : " ", lineNumber, line, lineNumberWidth));
437
+ };
447
438
  if (match.contextBefore) {
448
439
  for (const ctx of match.contextBefore) {
449
- renderedLines.push(formatLine(ctx.lineNumber, ctx.line, false));
440
+ pushLine(ctx.lineNumber, ctx.line, false);
450
441
  }
451
442
  }
452
- renderedLines.push(formatLine(match.lineNumber, match.line, true));
443
+ pushLine(match.lineNumber, match.line, true);
453
444
  if (match.truncated) {
454
445
  linesTruncated = true;
455
446
  }
456
447
  if (match.contextAfter) {
457
448
  for (const ctx of match.contextAfter) {
458
- renderedLines.push(formatLine(ctx.lineNumber, ctx.line, false));
449
+ pushLine(ctx.lineNumber, ctx.line, false);
459
450
  }
460
451
  }
461
452
  fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
462
453
  }
463
- return renderedLines;
454
+ return { model: modelOut, display: displayOut };
464
455
  };
465
456
  if (isDirectory) {
466
457
  const filesByDirectory = new Map<string, string[]>();
@@ -474,36 +465,47 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
474
465
  for (const [directory, directoryFiles] of filesByDirectory) {
475
466
  if (directory === ".") {
476
467
  for (const relativePath of directoryFiles) {
477
- const renderedLines = renderMatchesForFile(relativePath);
478
- if (renderedLines.length === 0) continue;
468
+ const rendered = renderMatchesForFile(relativePath);
469
+ if (rendered.model.length === 0) continue;
479
470
  if (outputLines.length > 0) {
480
471
  outputLines.push("");
472
+ displayLines.push("");
481
473
  }
482
- outputLines.push(`# ${path.basename(relativePath)}`);
483
- outputLines.push(...renderedLines);
474
+ const header = `# ${path.basename(relativePath)}`;
475
+ outputLines.push(header, ...rendered.model);
476
+ displayLines.push(header, ...rendered.display);
484
477
  }
485
478
  continue;
486
479
  }
487
480
  const renderedFiles = directoryFiles
488
- .map(relativePath => ({ relativePath, lines: renderMatchesForFile(relativePath) }))
489
- .filter(file => file.lines.length > 0);
481
+ .map(relativePath => ({ relativePath, rendered: renderMatchesForFile(relativePath) }))
482
+ .filter(file => file.rendered.model.length > 0);
490
483
  if (renderedFiles.length === 0) continue;
491
484
  if (outputLines.length > 0) {
492
485
  outputLines.push("");
486
+ displayLines.push("");
493
487
  }
494
- outputLines.push(`# ${directory}`);
495
- for (const { relativePath, lines } of renderedFiles) {
496
- outputLines.push(`## └─ ${path.basename(relativePath)}`);
497
- outputLines.push(...lines);
488
+ const dirHeader = `# ${directory}`;
489
+ outputLines.push(dirHeader);
490
+ displayLines.push(dirHeader);
491
+ for (const { relativePath, rendered } of renderedFiles) {
492
+ const fileHeader = `## └─ ${path.basename(relativePath)}`;
493
+ outputLines.push(fileHeader, ...rendered.model);
494
+ displayLines.push(fileHeader, ...rendered.display);
498
495
  }
499
496
  }
500
497
  } else {
501
498
  for (const relativePath of fileList) {
502
- outputLines.push(...renderMatchesForFile(relativePath));
499
+ const rendered = renderMatchesForFile(relativePath);
500
+ outputLines.push(...rendered.model);
501
+ displayLines.push(...rendered.display);
503
502
  }
504
503
  }
505
504
  if (hasContextLines && outputLines.length > 0) {
506
- outputLines.unshift("[grep] match lines use ':'; context lines use '-'.");
505
+ outputLines.unshift("[grep] match lines use '>'; context lines use ':'.");
506
+ }
507
+ if (matchLimitReached || result.limitReached) {
508
+ outputLines.push("", limitMessage);
507
509
  }
508
510
  const rawOutput = outputLines.join("\n");
509
511
  const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
@@ -521,16 +523,13 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
521
523
  truncated,
522
524
  matchLimitReached: matchLimitReached ? effectiveLimit : undefined,
523
525
  resultLimitReached: result.limitReached ? internalLimit : undefined,
526
+ displayContent: displayLines.join("\n"),
524
527
  };
525
528
  if (truncation.truncated) details.truncation = truncation;
526
529
  if (linesTruncated) details.linesTruncated = true;
527
530
  const resultBuilder = toolResult(details)
528
531
  .text(output)
529
- .limits({
530
- matchLimit: matchLimitReached ? effectiveLimit : undefined,
531
- resultLimit: result.limitReached ? internalLimit : undefined,
532
- columnMax: linesTruncated ? DEFAULT_MAX_COLUMN : undefined,
533
- });
532
+ .limits({ columnMax: linesTruncated ? DEFAULT_MAX_COLUMN : undefined });
534
533
  if (truncation.truncated) {
535
534
  resultBuilder.truncation(truncation, { direction: "head" });
536
535
  }
@@ -546,15 +545,9 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
546
545
  interface GrepRenderArgs {
547
546
  pattern: string;
548
547
  path?: string;
549
- glob?: string;
550
- type?: string;
551
548
  i?: boolean;
552
549
  gitignore?: boolean;
553
- pre?: number;
554
- post?: number;
555
- multiline?: boolean;
556
- limit?: number;
557
- offset?: number;
550
+ skip?: number;
558
551
  }
559
552
 
560
553
  const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
@@ -564,19 +557,9 @@ export const grepToolRenderer = {
564
557
  renderCall(args: GrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
565
558
  const meta: string[] = [];
566
559
  if (args.path) meta.push(`in ${args.path}`);
567
- if (args.glob) meta.push(`glob:${args.glob}`);
568
- if (args.type) meta.push(`type:${args.type}`);
569
560
  if (args.i) meta.push("case:insensitive");
570
561
  if (args.gitignore === false) meta.push("gitignore:false");
571
- if (args.pre !== undefined && args.pre > 0) {
572
- meta.push(`pre:${args.pre}`);
573
- }
574
- if (args.post !== undefined && args.post > 0) {
575
- meta.push(`post:${args.post}`);
576
- }
577
- if (args.multiline) meta.push("multiline");
578
- if (args.limit !== undefined && args.limit > 0) meta.push(`limit:${args.limit}`);
579
- if (args.offset !== undefined && args.offset > 0) meta.push(`offset:${args.offset}`);
562
+ if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
580
563
 
581
564
  const text = renderStatusLine(
582
565
  { icon: "pending", title: "Grep", description: args.pattern || "?", meta },
@@ -601,7 +584,7 @@ export const grepToolRenderer = {
601
584
  const hasDetailedData = details?.matchCount !== undefined || details?.fileCount !== undefined;
602
585
 
603
586
  if (!hasDetailedData) {
604
- const textContent = result.content?.find(c => c.type === "text")?.text;
587
+ const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text;
605
588
  if (!textContent || textContent === "No matches found") {
606
589
  return new Text(formatEmptyMessage("No matches found", uiTheme), 0, 0);
607
590
  }
@@ -664,7 +647,7 @@ export const grepToolRenderer = {
664
647
  uiTheme,
665
648
  );
666
649
 
667
- const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
650
+ const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
668
651
  const rawLines = textContent.split("\n");
669
652
  const hasSeparators = rawLines.some(line => line.trim().length === 0);
670
653
  const matchGroups: string[][] = [];
@@ -688,9 +671,11 @@ export const grepToolRenderer = {
688
671
  }
689
672
  }
690
673
 
674
+ const renderedMatchLimit = details?.matchLimitReached ?? limits?.matchLimit?.reached;
675
+ const renderedResultLimit = details?.resultLimitReached ?? limits?.resultLimit?.reached;
691
676
  const truncationReasons: string[] = [];
692
- if (limits?.matchLimit) truncationReasons.push(`limit ${limits.matchLimit.reached} matches`);
693
- if (limits?.resultLimit) truncationReasons.push(`limit ${limits.resultLimit.reached} results`);
677
+ if (renderedMatchLimit) truncationReasons.push(`first ${renderedMatchLimit} matches`);
678
+ if (renderedResultLimit) truncationReasons.push(`first ${renderedResultLimit} results`);
694
679
  if (truncation) truncationReasons.push(truncation.truncatedBy === "lines" ? "line limit" : "size limit");
695
680
  if (limits?.columnTruncated) truncationReasons.push(`line length ${limits.columnTruncated.maxColumn}`);
696
681
  if (truncation?.artifactId) truncationReasons.push(formatFullOutputReference(truncation.artifactId));