@oh-my-pi/pi-coding-agent 14.5.14 → 14.6.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 (74) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/package.json +7 -7
  3. package/src/autoresearch/command-resume.md +5 -8
  4. package/src/autoresearch/git.ts +41 -51
  5. package/src/autoresearch/helpers.ts +43 -359
  6. package/src/autoresearch/index.ts +281 -273
  7. package/src/autoresearch/prompt-setup.md +43 -0
  8. package/src/autoresearch/prompt.md +52 -193
  9. package/src/autoresearch/resume-message.md +2 -8
  10. package/src/autoresearch/state.ts +59 -166
  11. package/src/autoresearch/storage.ts +687 -0
  12. package/src/autoresearch/tools/init-experiment.ts +201 -290
  13. package/src/autoresearch/tools/log-experiment.ts +304 -517
  14. package/src/autoresearch/tools/run-experiment.ts +117 -296
  15. package/src/autoresearch/tools/update-notes.ts +116 -0
  16. package/src/autoresearch/types.ts +16 -66
  17. package/src/cli/list-models.ts +66 -0
  18. package/src/config/settings-schema.ts +1 -1
  19. package/src/config/settings.ts +20 -1
  20. package/src/cursor.ts +1 -1
  21. package/src/edit/index.ts +9 -31
  22. package/src/edit/line-hash.ts +70 -43
  23. package/src/edit/modes/hashline.lark +26 -0
  24. package/src/edit/modes/hashline.ts +898 -1099
  25. package/src/edit/modes/patch.ts +0 -7
  26. package/src/edit/modes/replace.ts +0 -4
  27. package/src/edit/renderer.ts +22 -20
  28. package/src/edit/streaming.ts +8 -28
  29. package/src/eval/eval.lark +24 -30
  30. package/src/eval/js/context-manager.ts +5 -162
  31. package/src/eval/js/prelude.txt +0 -12
  32. package/src/eval/parse.ts +129 -129
  33. package/src/eval/py/prelude.py +1 -219
  34. package/src/export/html/template.generated.ts +1 -1
  35. package/src/export/html/template.js +2 -2
  36. package/src/internal-urls/docs-index.generated.ts +2 -2
  37. package/src/main.ts +18 -3
  38. package/src/modes/components/session-observer-overlay.ts +5 -2
  39. package/src/modes/components/status-line/segments.ts +1 -1
  40. package/src/modes/components/status-line.ts +3 -5
  41. package/src/modes/components/tree-selector.ts +4 -5
  42. package/src/modes/components/welcome.ts +11 -1
  43. package/src/modes/controllers/command-controller.ts +2 -6
  44. package/src/modes/controllers/event-controller.ts +7 -5
  45. package/src/modes/controllers/extension-ui-controller.ts +3 -15
  46. package/src/modes/controllers/input-controller.ts +0 -1
  47. package/src/modes/controllers/selector-controller.ts +1 -1
  48. package/src/modes/interactive-mode.ts +5 -7
  49. package/src/prompts/system/system-prompt.md +14 -38
  50. package/src/prompts/tools/ast-edit.md +8 -8
  51. package/src/prompts/tools/ast-grep.md +10 -10
  52. package/src/prompts/tools/eval.md +13 -31
  53. package/src/prompts/tools/find.md +2 -1
  54. package/src/prompts/tools/hashline.md +66 -57
  55. package/src/prompts/tools/search.md +2 -2
  56. package/src/session/agent-session.ts +1 -1
  57. package/src/session/session-manager.ts +17 -13
  58. package/src/tools/ast-edit.ts +141 -44
  59. package/src/tools/ast-grep.ts +112 -36
  60. package/src/tools/eval.ts +2 -53
  61. package/src/tools/find.ts +16 -15
  62. package/src/tools/gh-renderer.ts +184 -59
  63. package/src/tools/path-utils.ts +36 -196
  64. package/src/tools/search.ts +56 -35
  65. package/src/utils/edit-mode.ts +2 -11
  66. package/src/utils/file-display-mode.ts +1 -1
  67. package/src/utils/git.ts +59 -24
  68. package/src/utils/session-color.ts +0 -12
  69. package/src/utils/title-generator.ts +22 -38
  70. package/src/autoresearch/apply-contract-to-state.ts +0 -24
  71. package/src/autoresearch/contract.ts +0 -288
  72. package/src/edit/modes/atom.lark +0 -29
  73. package/src/edit/modes/atom.ts +0 -1773
  74. package/src/prompts/tools/atom.md +0 -150
@@ -1,3 +1,4 @@
1
+ import * as path from "node:path";
1
2
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
3
  import { type AstFindMatch, astGrep } from "@oh-my-pi/pi-natives";
3
4
  import type { Component } from "@oh-my-pi/pi-tui";
@@ -19,7 +20,7 @@ import {
19
20
  hasGlobPathChars,
20
21
  normalizePathLikeInput,
21
22
  parseSearchPath,
22
- resolveMultiSearchPath,
23
+ resolveExplicitSearchPaths,
23
24
  resolveToCwd,
24
25
  } from "./path-utils";
25
26
  import {
@@ -37,13 +38,71 @@ import { toolResult } from "./tool-result";
37
38
 
38
39
  const astGrepSchema = Type.Object({
39
40
  pat: Type.String({ description: "ast pattern", examples: ["console.log($$$)"] }),
40
- path: Type.String({
41
- description: "file, directory, glob, or comma-separated paths to search",
42
- examples: ["src/", "src/foo.ts", "src/**/*.ts"],
41
+ paths: Type.Array(Type.String({ description: "file, directory, glob, or internal URL to search" }), {
42
+ minItems: 1,
43
+ description: "files, directories, globs, or internal URLs to search",
44
+ examples: [["src/"], ["src/foo.ts"], ["src/**/*.ts"], ["src/", "packages/"]],
43
45
  }),
44
46
  skip: Type.Optional(Type.Number({ description: "matches to skip", default: 0 })),
45
47
  });
46
48
 
49
+ async function runMultiTargetAstGrep(
50
+ targets: Array<{ basePath: string; glob?: string }>,
51
+ options: { patterns: string[]; commonBasePath: string; skip: number; limit: number; signal?: AbortSignal },
52
+ ): Promise<{
53
+ matches: AstFindMatch[];
54
+ totalMatches: number;
55
+ filesWithMatches: number;
56
+ filesSearched: number;
57
+ limitReached: boolean;
58
+ parseErrors?: string[];
59
+ }> {
60
+ const aggregatedMatches: AstFindMatch[] = [];
61
+ const parseErrors: string[] = [];
62
+ let totalMatches = 0;
63
+ let filesSearched = 0;
64
+ let limitReached = false;
65
+ for (const target of targets) {
66
+ const targetResult = await astGrep({
67
+ patterns: options.patterns,
68
+ path: target.basePath,
69
+ glob: target.glob,
70
+ offset: 0,
71
+ limit: options.skip + options.limit + 1,
72
+ includeMeta: true,
73
+ signal: options.signal,
74
+ });
75
+ totalMatches += targetResult.totalMatches;
76
+ filesSearched += targetResult.filesSearched;
77
+ limitReached = limitReached || targetResult.limitReached;
78
+ if (targetResult.parseErrors) parseErrors.push(...targetResult.parseErrors);
79
+ for (const match of targetResult.matches) {
80
+ const absolute = path.resolve(target.basePath, match.path);
81
+ const rebased = path.relative(options.commonBasePath, absolute).replace(/\\/g, "/");
82
+ aggregatedMatches.push({ ...match, path: rebased });
83
+ }
84
+ }
85
+ aggregatedMatches.sort((left, right) => {
86
+ const pathCmp = left.path.localeCompare(right.path);
87
+ if (pathCmp !== 0) return pathCmp;
88
+ if (left.startLine !== right.startLine) return left.startLine - right.startLine;
89
+ if (left.startColumn !== right.startColumn) return left.startColumn - right.startColumn;
90
+ if (left.byteStart !== right.byteStart) return left.byteStart - right.byteStart;
91
+ return left.byteEnd - right.byteEnd;
92
+ });
93
+ const visible = aggregatedMatches.slice(options.skip);
94
+ const paged = visible.slice(0, options.limit);
95
+ const filesWithMatches = new Set(aggregatedMatches.map(match => match.path)).size;
96
+ return {
97
+ matches: paged,
98
+ totalMatches,
99
+ filesWithMatches,
100
+ filesSearched,
101
+ limitReached: limitReached || visible.length > options.limit,
102
+ parseErrors: parseErrors.length > 0 ? parseErrors : undefined,
103
+ };
104
+ }
105
+
47
106
  export interface AstGrepToolDetails {
48
107
  matchCount: number;
49
108
  fileCount: number;
@@ -88,15 +147,21 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
88
147
  throw new ToolError("skip must be a non-negative number");
89
148
  }
90
149
  const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
91
- let searchPath: string | undefined;
92
- let scopePath: string | undefined;
150
+ let searchPath: string;
151
+ let scopePath: string;
93
152
  let globFilter: string | undefined;
94
- const rawPath = normalizePathLikeInput(params.path);
95
- if (rawPath.length === 0) {
96
- throw new ToolError("`path` must be a non-empty path or glob");
153
+ let multiTargets: Array<{ basePath: string; glob?: string }> | undefined;
154
+ const rawPaths = params.paths.map(normalizePathLikeInput);
155
+ if (rawPaths.some(rawPath => rawPath.length === 0)) {
156
+ throw new ToolError("`paths` must contain non-empty paths or globs");
97
157
  }
98
158
  const internalRouter = this.session.internalRouter;
99
- if (internalRouter?.canHandle(rawPath)) {
159
+ const resolvedPathInputs: string[] = [];
160
+ for (const rawPath of rawPaths) {
161
+ if (!internalRouter?.canHandle(rawPath)) {
162
+ resolvedPathInputs.push(rawPath);
163
+ continue;
164
+ }
100
165
  if (hasGlobPathChars(rawPath)) {
101
166
  throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
102
167
  }
@@ -104,23 +169,25 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
104
169
  if (!resource.sourcePath) {
105
170
  throw new ToolError(`Cannot search internal URL without backing file: ${rawPath}`);
106
171
  }
107
- searchPath = resource.sourcePath;
172
+ resolvedPathInputs.push(resource.sourcePath);
173
+ }
174
+ if (resolvedPathInputs.length === 1) {
175
+ const parsedPath = parseSearchPath(resolvedPathInputs[0] ?? ".");
176
+ searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
177
+ globFilter = parsedPath.glob;
108
178
  scopePath = formatScopePath(searchPath);
109
179
  } else {
110
- const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
111
- if (multiSearchPath) {
112
- searchPath = multiSearchPath.basePath;
113
- globFilter = multiSearchPath.glob;
114
- scopePath = multiSearchPath.scopePath;
115
- } else {
116
- const parsedPath = parseSearchPath(rawPath);
117
- searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
118
- globFilter = parsedPath.glob;
119
- scopePath = formatScopePath(searchPath);
180
+ const multiSearchPath = await resolveExplicitSearchPaths(resolvedPathInputs, this.session.cwd, globFilter);
181
+ if (!multiSearchPath) {
182
+ throw new ToolError("`paths` must contain at least one path or glob");
120
183
  }
184
+ searchPath = multiSearchPath.basePath;
185
+ globFilter = multiSearchPath.targets ? undefined : multiSearchPath.glob;
186
+ multiTargets = multiSearchPath.targets;
187
+ scopePath = multiSearchPath.scopePath;
121
188
  }
122
189
 
123
- const resolvedSearchPath = searchPath ?? resolveToCwd(".", this.session.cwd);
190
+ const resolvedSearchPath = searchPath;
124
191
  scopePath = scopePath ?? formatScopePath(resolvedSearchPath);
125
192
  let isDirectory: boolean;
126
193
  try {
@@ -130,14 +197,23 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
130
197
  throw new ToolError(`Path not found: ${scopePath}`);
131
198
  }
132
199
 
133
- const result = await astGrep({
134
- patterns,
135
- path: resolvedSearchPath,
136
- glob: globFilter,
137
- offset: skip,
138
- includeMeta: true,
139
- signal,
140
- });
200
+ const DEFAULT_AST_LIMIT = 50;
201
+ const result = multiTargets
202
+ ? await runMultiTargetAstGrep(multiTargets, {
203
+ patterns,
204
+ commonBasePath: resolvedSearchPath,
205
+ skip,
206
+ limit: DEFAULT_AST_LIMIT,
207
+ signal,
208
+ })
209
+ : await astGrep({
210
+ patterns,
211
+ path: resolvedSearchPath,
212
+ glob: globFilter,
213
+ offset: skip,
214
+ includeMeta: true,
215
+ signal,
216
+ });
141
217
 
142
218
  const normalizedParseErrors = (result.parseErrors ?? []).map(error => {
143
219
  const parseError = error.match(/^.+: (.+: parse error \(syntax tree contains error nodes\))$/);
@@ -172,7 +248,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
172
248
 
173
249
  if (result.matches.length === 0) {
174
250
  const noMatchMessage = dedupedParseErrors.length
175
- ? "No matches found. Parse issues mean the query may be mis-scoped; narrow `path` before concluding absence."
251
+ ? "No matches found. Parse issues mean the query may be mis-scoped; narrow `paths` before concluding absence."
176
252
  : "No matches found";
177
253
  const parseMessage = dedupedParseErrors.length
178
254
  ? `\n${formatParseErrors(dedupedParseErrors).join("\n")}`
@@ -238,7 +314,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
238
314
  displayContent: displayLines.join("\n"),
239
315
  };
240
316
  if (result.limitReached) {
241
- outputLines.push("", "Result limit reached; narrow path pattern or increase limit.");
317
+ outputLines.push("", "Result limit reached; narrow paths or increase limit.");
242
318
  }
243
319
  if (dedupedParseErrors.length) {
244
320
  outputLines.push("", ...formatParseErrors(dedupedParseErrors));
@@ -255,7 +331,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
255
331
 
256
332
  interface AstGrepRenderArgs {
257
333
  pat?: string;
258
- path?: string;
334
+ paths?: string[];
259
335
  skip?: number;
260
336
  }
261
337
 
@@ -265,7 +341,7 @@ export const astGrepToolRenderer = {
265
341
  inline: true,
266
342
  renderCall(args: AstGrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
267
343
  const meta: string[] = [];
268
- if (args.path) meta.push(`in ${args.path}`);
344
+ if (args.paths?.length) meta.push(`in ${args.paths.join(", ")}`);
269
345
  if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
270
346
 
271
347
  const description = args.pat ?? "?";
@@ -299,7 +375,7 @@ export const astGrepToolRenderer = {
299
375
  const header = renderStatusLine({ icon: "warning", title: "AST Grep", description, meta }, uiTheme);
300
376
  const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
301
377
  if (details?.parseErrors?.length) {
302
- lines.push(uiTheme.fg("warning", "Query may be mis-scoped; narrow `path` before concluding absence"));
378
+ lines.push(uiTheme.fg("warning", "Query may be mis-scoped; narrow `paths` before concluding absence"));
303
379
  const capped = details.parseErrors.slice(0, PARSE_ERRORS_LIMIT);
304
380
  for (const err of capped) {
305
381
  lines.push(uiTheme.fg("warning", ` - ${err}`));
@@ -351,7 +427,7 @@ export const astGrepToolRenderer = {
351
427
 
352
428
  const extraLines: string[] = [];
353
429
  if (limitReached) {
354
- extraLines.push(uiTheme.fg("warning", "limit reached; narrow path pattern or increase limit"));
430
+ extraLines.push(uiTheme.fg("warning", "limit reached; narrow paths or increase limit"));
355
431
  }
356
432
  if (details?.parseErrors?.length) {
357
433
  const total = details.parseErrors.length;
package/src/tools/eval.ts CHANGED
@@ -26,7 +26,7 @@ export const EVAL_DEFAULT_PREVIEW_LINES = 10;
26
26
 
27
27
  export const evalSchema = Type.Object({
28
28
  input: Type.String({
29
- description: "atom-style eval input containing CELL sections, fenced code, and optional RESET directive",
29
+ description: "eval input as a sequence of `===== <info> =====` cell headers followed by code",
30
30
  }),
31
31
  });
32
32
  export type EvalToolParams = Static<typeof evalSchema>;
@@ -250,7 +250,7 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
250
250
  let previousRuntimeLanguage: EvalLanguage | undefined;
251
251
  const cells: ResolvedEvalCell[] = [];
252
252
  for (const cell of parsedInput.cells) {
253
- const requested = cell.languageOrigin === "fence" ? cell.language : (previousRuntimeLanguage ?? undefined);
253
+ const requested = cell.languageOrigin === "header" ? cell.language : (previousRuntimeLanguage ?? undefined);
254
254
  const resolved = await resolveBackend(session, requested, cell.code);
255
255
  previousRuntimeLanguage = resolved.backend.id;
256
256
  cells.push({
@@ -600,12 +600,6 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
600
600
  pwd: "icon.folder",
601
601
  mkdir: "icon.folder",
602
602
  tree: "icon.folder",
603
- stat: "icon.folder",
604
- find: "icon.file",
605
- grep: "icon.file",
606
- rgrep: "icon.file",
607
- glob: "icon.file",
608
- sed: "icon.file",
609
603
  git_status: "icon.git",
610
604
  git_diff: "icon.git",
611
605
  git_log: "icon.git",
@@ -642,19 +636,6 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
642
636
  parts.push(`${data.files} file${(data.files as number) !== 1 ? "s" : ""}`);
643
637
  parts.push(`${data.chars} chars`);
644
638
  break;
645
- case "find":
646
- case "glob":
647
- parts.push(`${data.count} match${(data.count as number) !== 1 ? "es" : ""}`);
648
- if (data.pattern) parts.push(`for "${truncateToWidth(String(data.pattern), 20)}"`);
649
- break;
650
- case "grep":
651
- parts.push(`${data.count} match${(data.count as number) !== 1 ? "es" : ""}`);
652
- if (data.path) parts.push(`in ${shortenPath(String(data.path))}`);
653
- break;
654
- case "rgrep":
655
- parts.push(`${data.count} match${(data.count as number) !== 1 ? "es" : ""}`);
656
- if (data.pattern) parts.push(`for "${truncateToWidth(String(data.pattern), 20)}"`);
657
- break;
658
639
  case "ls":
659
640
  parts.push(`${data.count} entr${(data.count as number) !== 1 ? "ies" : "y"}`);
660
641
  break;
@@ -667,18 +648,6 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
667
648
  parts.push(`${data.count} variable${(data.count as number) !== 1 ? "s" : ""}`);
668
649
  }
669
650
  break;
670
- case "stat":
671
- if (data.is_dir) {
672
- parts.push("directory");
673
- } else {
674
- parts.push(`${data.size} bytes`);
675
- }
676
- if (data.path) parts.push(shortenPath(String(data.path)));
677
- break;
678
- case "sed":
679
- parts.push(`${data.count} replacement${(data.count as number) !== 1 ? "s" : ""}`);
680
- if (data.path) parts.push(`in ${shortenPath(String(data.path))}`);
681
- break;
682
651
  case "git_status":
683
652
  if (data.clean) {
684
653
  parts.push("clean");
@@ -759,29 +728,9 @@ function formatStatusEventExpanded(event: EvalStatusEvent, theme: Theme): string
759
728
  };
760
729
 
761
730
  switch (op) {
762
- case "find":
763
- case "glob":
764
- if (data.matches) addItems(data.matches as unknown[], m => String(m));
765
- break;
766
731
  case "ls":
767
732
  if (data.items) addItems(data.items as unknown[], m => String(m));
768
733
  break;
769
- case "grep":
770
- if (data.hits) {
771
- addItems(data.hits as unknown[], h => {
772
- const hit = h as { line: number; text: string };
773
- return `${hit.line}: ${truncateToWidth(hit.text, 60)}`;
774
- });
775
- }
776
- break;
777
- case "rgrep":
778
- if (data.hits) {
779
- addItems(data.hits as unknown[], h => {
780
- const hit = h as { file: string; line: number; text: string };
781
- return `${shortenPath(hit.file)}:${hit.line}: ${truncateToWidth(hit.text, 50)}`;
782
- });
783
- }
784
- break;
785
734
  case "env":
786
735
  if (data.keys) addItems(data.keys as unknown[], k => String(k), 10);
787
736
  break;
package/src/tools/find.ts CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  formatPathRelativeToCwd,
28
28
  normalizePathLikeInput,
29
29
  parseFindPattern,
30
- resolveMultiFindPattern,
30
+ resolveExplicitFindPatterns,
31
31
  resolveToCwd,
32
32
  } from "./path-utils";
33
33
  import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
@@ -35,9 +35,10 @@ import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
35
35
  import { toolResult } from "./tool-result";
36
36
 
37
37
  const findSchema = Type.Object({
38
- pattern: Type.String({
39
- description: "glob including search path",
40
- examples: ["src/**/*.ts", "lib/*.json", "apps/,packages/", "*.ts"],
38
+ paths: Type.Array(Type.String({ description: "glob including search path" }), {
39
+ minItems: 1,
40
+ description: "globs including search paths",
41
+ examples: [["src/**/*.ts"], ["lib/*.json"], ["apps/", "packages/"], ["*.ts"]],
41
42
  }),
42
43
  hidden: Type.Optional(Type.Boolean({ description: "include hidden files", default: true })),
43
44
  limit: Type.Optional(Type.Number({ description: "max results", default: 1000 })),
@@ -104,17 +105,17 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
104
105
  onUpdate?: AgentToolUpdateCallback<FindToolDetails>,
105
106
  _context?: AgentToolContext,
106
107
  ): Promise<AgentToolResult<FindToolDetails>> {
107
- const { pattern, limit, hidden } = params;
108
+ const { paths, limit, hidden } = params;
108
109
 
109
110
  return untilAborted(signal, async () => {
110
111
  const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
111
- const normalizedPattern = normalizePathLikeInput(pattern).replace(/\\/g, "/");
112
- if (!normalizedPattern) {
113
- throw new ToolError("Pattern must not be empty");
112
+ const normalizedPatterns = paths.map(input => normalizePathLikeInput(input).replace(/\\/g, "/"));
113
+ if (normalizedPatterns.some(pattern => pattern.length === 0)) {
114
+ throw new ToolError("`paths` must contain non-empty globs or paths");
114
115
  }
115
116
 
116
- const multiPattern = await resolveMultiFindPattern(normalizedPattern, this.session.cwd);
117
- const parsedPattern = multiPattern ? null : parseFindPattern(normalizedPattern);
117
+ const multiPattern = await resolveExplicitFindPatterns(normalizedPatterns, this.session.cwd);
118
+ const parsedPattern = multiPattern ? null : parseFindPattern(normalizedPatterns[0] ?? ".");
118
119
  const hasGlob = multiPattern ? true : (parsedPattern?.hasGlob ?? false);
119
120
  const globPattern = multiPattern?.globPattern ?? parsedPattern?.globPattern ?? "**/*";
120
121
  const searchPath = resolveToCwd(multiPattern?.basePath ?? parsedPattern?.basePath ?? ".", this.session.cwd);
@@ -292,7 +293,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
292
293
  // =============================================================================
293
294
 
294
295
  interface FindRenderArgs {
295
- pattern: string;
296
+ paths?: string[];
296
297
  limit?: number;
297
298
  }
298
299
 
@@ -305,7 +306,7 @@ export const findToolRenderer = {
305
306
  if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
306
307
 
307
308
  const text = renderStatusLine(
308
- { icon: "pending", title: "Find", description: args.pattern || "*", meta },
309
+ { icon: "pending", title: "Find", description: args.paths?.join(", ") || "*", meta },
309
310
  uiTheme,
310
311
  );
311
312
  return new Text(text, 0, 0);
@@ -342,7 +343,7 @@ export const findToolRenderer = {
342
343
  {
343
344
  icon: "success",
344
345
  title: "Find",
345
- description: args?.pattern,
346
+ description: args?.paths?.join(", "),
346
347
  meta: [formatCount("file", lines.length)],
347
348
  },
348
349
  uiTheme,
@@ -381,7 +382,7 @@ export const findToolRenderer = {
381
382
 
382
383
  if (fileCount === 0) {
383
384
  const header = renderStatusLine(
384
- { icon: "warning", title: "Find", description: args?.pattern, meta: ["0 files"] },
385
+ { icon: "warning", title: "Find", description: args?.paths?.join(", "), meta: ["0 files"] },
385
386
  uiTheme,
386
387
  );
387
388
  return new Text([header, formatEmptyMessage("No files found", uiTheme)].join("\n"), 0, 0);
@@ -390,7 +391,7 @@ export const findToolRenderer = {
390
391
  if (details?.scopePath) meta.push(`in ${details.scopePath}`);
391
392
  if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
392
393
  const header = renderStatusLine(
393
- { icon: truncated ? "warning" : "success", title: "Find", description: args?.pattern, meta },
394
+ { icon: truncated ? "warning" : "success", title: "Find", description: args?.paths?.join(", "), meta },
394
395
  uiTheme,
395
396
  );
396
397