@oh-my-pi/pi-coding-agent 14.5.13 → 14.6.0

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 (105) hide show
  1. package/CHANGELOG.md +52 -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/commit/pipeline.ts +4 -3
  18. package/src/config/settings-schema.ts +1 -1
  19. package/src/config/settings.ts +20 -1
  20. package/src/config.ts +9 -6
  21. package/src/cursor.ts +1 -1
  22. package/src/edit/index.ts +9 -31
  23. package/src/edit/line-hash.ts +70 -43
  24. package/src/edit/modes/hashline.lark +26 -0
  25. package/src/edit/modes/hashline.ts +898 -1099
  26. package/src/edit/modes/patch.ts +0 -7
  27. package/src/edit/modes/replace.ts +0 -4
  28. package/src/edit/renderer.ts +22 -20
  29. package/src/edit/streaming.ts +8 -28
  30. package/src/eval/eval.lark +24 -30
  31. package/src/eval/js/context-manager.ts +5 -162
  32. package/src/eval/js/prelude.txt +0 -12
  33. package/src/eval/parse.ts +129 -129
  34. package/src/eval/py/kernel.ts +4 -4
  35. package/src/eval/py/prelude.py +1 -219
  36. package/src/export/html/template.generated.ts +1 -1
  37. package/src/export/html/template.js +2 -2
  38. package/src/internal-urls/docs-index.generated.ts +1 -1
  39. package/src/main.ts +10 -0
  40. package/src/mcp/manager.ts +22 -0
  41. package/src/modes/components/session-observer-overlay.ts +5 -2
  42. package/src/modes/components/status-line/segments.ts +1 -1
  43. package/src/modes/components/status-line.ts +3 -5
  44. package/src/modes/components/tree-selector.ts +4 -5
  45. package/src/modes/components/welcome.ts +11 -1
  46. package/src/modes/controllers/command-controller.ts +2 -6
  47. package/src/modes/controllers/event-controller.ts +1 -2
  48. package/src/modes/controllers/extension-ui-controller.ts +3 -15
  49. package/src/modes/controllers/input-controller.ts +0 -1
  50. package/src/modes/controllers/selector-controller.ts +1 -1
  51. package/src/modes/interactive-mode.ts +5 -7
  52. package/src/modes/rpc/rpc-client.ts +9 -0
  53. package/src/modes/rpc/rpc-mode.ts +6 -0
  54. package/src/modes/rpc/rpc-types.ts +9 -0
  55. package/src/prompts/system/system-prompt.md +14 -38
  56. package/src/prompts/tools/ast-edit.md +8 -8
  57. package/src/prompts/tools/ast-grep.md +10 -10
  58. package/src/prompts/tools/eval.md +13 -31
  59. package/src/prompts/tools/find.md +2 -1
  60. package/src/prompts/tools/hashline.md +66 -57
  61. package/src/prompts/tools/search.md +2 -2
  62. package/src/sdk.ts +19 -4
  63. package/src/session/agent-session.ts +110 -4
  64. package/src/session/session-manager.ts +17 -13
  65. package/src/task/agents.ts +4 -5
  66. package/src/tools/archive-reader.ts +9 -3
  67. package/src/tools/ast-edit.ts +141 -44
  68. package/src/tools/ast-grep.ts +112 -36
  69. package/src/tools/browser/readable.ts +11 -6
  70. package/src/tools/browser/tab-supervisor.ts +2 -2
  71. package/src/tools/browser.ts +5 -3
  72. package/src/tools/eval.ts +2 -53
  73. package/src/tools/find.ts +16 -15
  74. package/src/tools/image-gen.ts +2 -2
  75. package/src/tools/path-utils.ts +36 -196
  76. package/src/tools/search.ts +56 -35
  77. package/src/tools/write.ts +8 -1
  78. package/src/utils/edit-mode.ts +2 -11
  79. package/src/utils/file-display-mode.ts +1 -1
  80. package/src/utils/git.ts +17 -0
  81. package/src/utils/session-color.ts +0 -12
  82. package/src/utils/title-generator.ts +22 -38
  83. package/src/web/scrapers/crossref.ts +3 -3
  84. package/src/web/scrapers/devto.ts +1 -1
  85. package/src/web/scrapers/discourse.ts +5 -5
  86. package/src/web/scrapers/firefox-addons.ts +1 -1
  87. package/src/web/scrapers/flathub.ts +2 -2
  88. package/src/web/scrapers/gitlab.ts +1 -1
  89. package/src/web/scrapers/go-pkg.ts +2 -2
  90. package/src/web/scrapers/jetbrains-marketplace.ts +1 -1
  91. package/src/web/scrapers/mastodon.ts +9 -9
  92. package/src/web/scrapers/mdn.ts +11 -7
  93. package/src/web/scrapers/pub-dev.ts +1 -1
  94. package/src/web/scrapers/rawg.ts +3 -3
  95. package/src/web/scrapers/readthedocs.ts +1 -1
  96. package/src/web/scrapers/spdx.ts +1 -1
  97. package/src/web/scrapers/stackoverflow.ts +2 -2
  98. package/src/web/scrapers/types.ts +53 -39
  99. package/src/web/scrapers/w3c.ts +1 -1
  100. package/src/web/search/providers/gemini.ts +2 -2
  101. package/src/autoresearch/apply-contract-to-state.ts +0 -24
  102. package/src/autoresearch/contract.ts +0 -288
  103. package/src/edit/modes/atom.lark +0 -29
  104. package/src/edit/modes/atom.ts +0 -1773
  105. 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;
@@ -26,13 +26,17 @@ function normalize(text: string | null | undefined): string | undefined {
26
26
  * CSS selector chain over the same pre-parsed DOM. Returns null if neither
27
27
  * path yields usable content.
28
28
  */
29
- export function extractReadableFromHtml(html: string, url: string, format: ReadableFormat): ReadableResult | null {
29
+ export async function extractReadableFromHtml(
30
+ html: string,
31
+ url: string,
32
+ format: ReadableFormat,
33
+ ): Promise<ReadableResult | null> {
30
34
  const { document } = parseHTML(html);
31
35
 
32
36
  // --- Primary: Readability article extraction ---
33
37
  const article = new Readability(document).parse();
34
38
  if (article) {
35
- const result = toReadableResult(url, format, article.textContent, article.content, {
39
+ const result = await toReadableResult(url, format, article.textContent, article.content, {
36
40
  title: article.title,
37
41
  byline: article.byline,
38
42
  excerpt: article.excerpt,
@@ -55,7 +59,7 @@ export function extractReadableFromHtml(html: string, url: string, format: Reada
55
59
  const innerHTML = el.innerHTML?.trim();
56
60
  const textContent = el.textContent?.trim();
57
61
  if (!innerHTML || !textContent) continue;
58
- const result = toReadableResult(url, format, textContent, innerHTML, {
62
+ const result = await toReadableResult(url, format, textContent, innerHTML, {
59
63
  title: document.title,
60
64
  excerpt: textContent.slice(0, 240),
61
65
  length: textContent.length,
@@ -67,15 +71,16 @@ export function extractReadableFromHtml(html: string, url: string, format: Reada
67
71
  }
68
72
 
69
73
  /** Shared builder for both extraction paths. */
70
- function toReadableResult(
74
+ async function toReadableResult(
71
75
  url: string,
72
76
  format: ReadableFormat,
73
77
  textContent: string | null | undefined,
74
78
  htmlContent: string | null | undefined,
75
79
  meta: { title?: string | null; byline?: string | null; excerpt?: string | null; length?: number | null },
76
- ): ReadableResult | null {
80
+ ): Promise<ReadableResult | null> {
77
81
  const text = normalize(textContent);
78
- const markdown = format === "markdown" ? (normalize(htmlToBasicMarkdown(htmlContent ?? "")) ?? text) : undefined;
82
+ const markdown =
83
+ format === "markdown" ? (normalize(await htmlToBasicMarkdown(htmlContent ?? "")) ?? text) : undefined;
79
84
  const normalizedText = format === "text" ? text : undefined;
80
85
  if (!normalizedText && !markdown) return null;
81
86
  return {
@@ -16,7 +16,6 @@ import type {
16
16
  WorkerInitPayload,
17
17
  WorkerOutbound,
18
18
  } from "./tab-protocol";
19
- import { WorkerCore } from "./tab-worker";
20
19
 
21
20
  interface WorkerHandle {
22
21
  send(msg: WorkerInbound, transferList?: Transferable[]): void;
@@ -398,7 +397,7 @@ function wrapBunWorker(worker: Worker): WorkerHandle {
398
397
  * entry. This preserves normal browser behavior but cannot interrupt synchronous
399
398
  * infinite loops because user code runs on the main thread.
400
399
  */
401
- function spawnInlineWorker(): WorkerHandle {
400
+ async function spawnInlineWorker(): Promise<WorkerHandle> {
402
401
  const hostListeners = new Set<(message: WorkerOutbound) => void>();
403
402
  const workerListeners = new Set<(message: WorkerInbound) => void>();
404
403
  const workerTransport: Transport = {
@@ -413,6 +412,7 @@ function spawnInlineWorker(): WorkerHandle {
413
412
  },
414
413
  close: () => {},
415
414
  };
415
+ const { WorkerCore } = await import("./tab-worker");
416
416
  new WorkerCore(workerTransport);
417
417
  return {
418
418
  mode: "inline",
@@ -110,12 +110,14 @@ function resolveBrowserKind(params: BrowserParams, session: ToolSession): Browse
110
110
  export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolDetails> {
111
111
  readonly name = "browser";
112
112
  readonly label = "Browser";
113
- readonly description: string;
114
113
  readonly parameters = browserSchema;
115
114
  readonly strict = true;
116
115
 
117
- constructor(private readonly session: ToolSession) {
118
- this.description = prompt.render(browserDescription, {});
116
+ constructor(private readonly session: ToolSession) {}
117
+ #description?: string;
118
+ get description(): string {
119
+ this.#description ??= prompt.render(browserDescription, {});
120
+ return this.#description;
119
121
  }
120
122
 
121
123
  /** Restart browser to apply mode changes (e.g. headless toggle). Drops only headless browsers. */
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
 
@@ -1,6 +1,6 @@
1
1
  import * as os from "node:os";
2
2
  import * as path from "node:path";
3
- import { getAntigravityHeaders, getEnvApiKey, type Model, StringEnum } from "@oh-my-pi/pi-ai";
3
+ import { getAntigravityUserAgent, getEnvApiKey, type Model, StringEnum } from "@oh-my-pi/pi-ai";
4
4
  import {
5
5
  CODEX_BASE_URL,
6
6
  getCodexAccountId,
@@ -1055,7 +1055,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1055
1055
  Authorization: `Bearer ${apiKey.apiKey}`,
1056
1056
  "Content-Type": "application/json",
1057
1057
  Accept: "text/event-stream",
1058
- ...getAntigravityHeaders(),
1058
+ "User-Agent": getAntigravityUserAgent(),
1059
1059
  },
1060
1060
  body: JSON.stringify(requestBody),
1061
1061
  signal: requestSignal,