@oh-my-pi/pi-coding-agent 13.3.13 → 13.4.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 (63) hide show
  1. package/CHANGELOG.md +97 -7
  2. package/examples/sdk/README.md +22 -0
  3. package/package.json +7 -7
  4. package/src/capability/index.ts +1 -11
  5. package/src/commit/analysis/index.ts +4 -4
  6. package/src/config/settings-schema.ts +18 -15
  7. package/src/config/settings.ts +2 -20
  8. package/src/discovery/index.ts +1 -11
  9. package/src/exa/index.ts +1 -10
  10. package/src/extensibility/custom-commands/index.ts +2 -15
  11. package/src/extensibility/custom-tools/index.ts +3 -18
  12. package/src/extensibility/custom-tools/loader.ts +28 -5
  13. package/src/extensibility/custom-tools/types.ts +18 -1
  14. package/src/extensibility/extensions/index.ts +9 -130
  15. package/src/extensibility/extensions/types.ts +2 -1
  16. package/src/extensibility/hooks/index.ts +3 -14
  17. package/src/extensibility/plugins/index.ts +6 -31
  18. package/src/index.ts +28 -220
  19. package/src/internal-urls/docs-index.generated.ts +3 -2
  20. package/src/internal-urls/index.ts +11 -16
  21. package/src/mcp/index.ts +11 -37
  22. package/src/mcp/tool-bridge.ts +3 -42
  23. package/src/mcp/transports/index.ts +2 -2
  24. package/src/modes/components/extensions/index.ts +3 -3
  25. package/src/modes/components/index.ts +35 -40
  26. package/src/modes/interactive-mode.ts +4 -1
  27. package/src/modes/rpc/rpc-mode.ts +1 -7
  28. package/src/modes/theme/theme.ts +11 -10
  29. package/src/modes/types.ts +1 -1
  30. package/src/patch/index.ts +4 -20
  31. package/src/prompts/system/system-prompt.md +18 -4
  32. package/src/prompts/tools/ast-edit.md +33 -0
  33. package/src/prompts/tools/ast-grep.md +34 -0
  34. package/src/prompts/tools/bash.md +2 -2
  35. package/src/prompts/tools/hashline.md +1 -0
  36. package/src/prompts/tools/resolve.md +8 -0
  37. package/src/sdk.ts +27 -7
  38. package/src/session/agent-session.ts +25 -36
  39. package/src/session/session-manager.ts +0 -30
  40. package/src/slash-commands/builtin-registry.ts +4 -2
  41. package/src/stt/index.ts +3 -3
  42. package/src/task/types.ts +2 -2
  43. package/src/tools/ast-edit.ts +480 -0
  44. package/src/tools/ast-grep.ts +435 -0
  45. package/src/tools/bash.ts +3 -2
  46. package/src/tools/gemini-image.ts +3 -3
  47. package/src/tools/grep.ts +26 -8
  48. package/src/tools/index.ts +55 -57
  49. package/src/tools/pending-action.ts +33 -0
  50. package/src/tools/render-utils.ts +10 -0
  51. package/src/tools/renderers.ts +6 -4
  52. package/src/tools/resolve.ts +156 -0
  53. package/src/tools/submit-result.ts +1 -1
  54. package/src/web/search/index.ts +6 -4
  55. package/src/web/search/providers/anthropic.ts +2 -2
  56. package/src/web/search/providers/base.ts +3 -0
  57. package/src/web/search/providers/exa.ts +11 -5
  58. package/src/web/search/providers/gemini.ts +112 -24
  59. package/src/patch/normative.ts +0 -72
  60. package/src/prompts/tools/ast-find.md +0 -20
  61. package/src/prompts/tools/ast-replace.md +0 -21
  62. package/src/tools/ast-find.ts +0 -316
  63. package/src/tools/ast-replace.ts +0 -294
@@ -0,0 +1,435 @@
1
+ import * as path from "node:path";
2
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
3
+ import { type AstFindMatch, astGrep } from "@oh-my-pi/pi-natives";
4
+ import type { Component } from "@oh-my-pi/pi-tui";
5
+ import { Text } from "@oh-my-pi/pi-tui";
6
+ import { untilAborted } from "@oh-my-pi/pi-utils";
7
+ import { type Static, Type } from "@sinclair/typebox";
8
+ import { renderPromptTemplate } from "../config/prompt-templates";
9
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
+ import type { Theme } from "../modes/theme/theme";
11
+ import { computeLineHash } from "../patch/hashline";
12
+ import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text" };
13
+ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
14
+ import { resolveFileDisplayMode } from "../utils/file-display-mode";
15
+ import type { ToolSession } from ".";
16
+ import type { OutputMeta } from "./output-meta";
17
+ import { hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
18
+ import {
19
+ formatCount,
20
+ formatEmptyMessage,
21
+ formatErrorMessage,
22
+ formatParseErrors,
23
+ PARSE_ERRORS_LIMIT,
24
+ PREVIEW_LIMITS,
25
+ } from "./render-utils";
26
+ import { ToolError } from "./tool-errors";
27
+ import { toolResult } from "./tool-result";
28
+
29
+ const astGrepSchema = Type.Object({
30
+ patterns: Type.Array(Type.String(), { minItems: 1, description: "AST patterns to match" }),
31
+ lang: Type.Optional(Type.String({ description: "Language override" })),
32
+ path: Type.Optional(Type.String({ description: "File, directory, or glob pattern to search (default: cwd)" })),
33
+ selector: Type.Optional(Type.String({ description: "Optional selector for contextual pattern mode" })),
34
+ limit: Type.Optional(Type.Number({ description: "Max matches (default: 50)" })),
35
+ offset: Type.Optional(Type.Number({ description: "Skip first N matches (default: 0)" })),
36
+ context: Type.Optional(Type.Number({ description: "Context lines around each match" })),
37
+ });
38
+
39
+ export interface AstGrepToolDetails {
40
+ matchCount: number;
41
+ fileCount: number;
42
+ filesSearched: number;
43
+ limitReached: boolean;
44
+ parseErrors?: string[];
45
+ scopePath?: string;
46
+ files?: string[];
47
+ fileMatches?: Array<{ path: string; count: number }>;
48
+ meta?: OutputMeta;
49
+ }
50
+
51
+ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolDetails> {
52
+ readonly name = "ast_grep";
53
+ readonly label = "AST Grep";
54
+ readonly description: string;
55
+ readonly parameters = astGrepSchema;
56
+ readonly strict = true;
57
+
58
+ constructor(private readonly session: ToolSession) {
59
+ this.description = renderPromptTemplate(astGrepDescription);
60
+ }
61
+
62
+ async execute(
63
+ _toolCallId: string,
64
+ params: Static<typeof astGrepSchema>,
65
+ signal?: AbortSignal,
66
+ _onUpdate?: AgentToolUpdateCallback<AstGrepToolDetails>,
67
+ _context?: AgentToolContext,
68
+ ): Promise<AgentToolResult<AstGrepToolDetails>> {
69
+ return untilAborted(signal, async () => {
70
+ const patterns = [
71
+ ...new Set(params.patterns.map(pattern => pattern.trim()).filter(pattern => pattern.length > 0)),
72
+ ];
73
+ if (patterns.length === 0) {
74
+ throw new ToolError("`patterns` must include at least one non-empty pattern");
75
+ }
76
+ const limit = params.limit === undefined ? 50 : Math.floor(params.limit);
77
+ if (!Number.isFinite(limit) || limit < 1) {
78
+ throw new ToolError("Limit must be a positive number");
79
+ }
80
+ const offset = params.offset === undefined ? 0 : Math.floor(params.offset);
81
+ if (!Number.isFinite(offset) || offset < 0) {
82
+ throw new ToolError("Offset must be a non-negative number");
83
+ }
84
+ const context = params.context === undefined ? undefined : Math.floor(params.context);
85
+ if (context !== undefined && (!Number.isFinite(context) || context < 0)) {
86
+ throw new ToolError("Context must be a non-negative number");
87
+ }
88
+
89
+ let searchPath: string | undefined;
90
+ let globFilter: string | undefined;
91
+ const rawPath = params.path?.trim();
92
+ if (rawPath) {
93
+ const internalRouter = this.session.internalRouter;
94
+ if (internalRouter?.canHandle(rawPath)) {
95
+ if (hasGlobPathChars(rawPath)) {
96
+ throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
97
+ }
98
+ const resource = await internalRouter.resolve(rawPath);
99
+ if (!resource.sourcePath) {
100
+ throw new ToolError(`Cannot search internal URL without backing file: ${rawPath}`);
101
+ }
102
+ searchPath = resource.sourcePath;
103
+ } else {
104
+ const parsedPath = parseSearchPath(rawPath);
105
+ searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
106
+ globFilter = parsedPath.glob;
107
+ }
108
+ }
109
+
110
+ const resolvedSearchPath = searchPath ?? resolveToCwd(".", this.session.cwd);
111
+ const scopePath = (() => {
112
+ const relative = path.relative(this.session.cwd, resolvedSearchPath).replace(/\\/g, "/");
113
+ return relative.length === 0 ? "." : relative;
114
+ })();
115
+ let isDirectory: boolean;
116
+ try {
117
+ const stat = await Bun.file(resolvedSearchPath).stat();
118
+ isDirectory = stat.isDirectory();
119
+ } catch {
120
+ throw new ToolError(`Path not found: ${resolvedSearchPath}`);
121
+ }
122
+
123
+ const result = await astGrep({
124
+ patterns,
125
+ lang: params.lang?.trim(),
126
+ path: resolvedSearchPath,
127
+ glob: globFilter,
128
+ selector: params.selector?.trim(),
129
+ limit,
130
+ offset,
131
+ context,
132
+ includeMeta: true,
133
+ signal,
134
+ });
135
+
136
+ const formatPath = (filePath: string): string => {
137
+ const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
138
+ if (isDirectory) {
139
+ return cleanPath.replace(/\\/g, "/");
140
+ }
141
+ return path.basename(cleanPath);
142
+ };
143
+
144
+ const files = new Set<string>();
145
+ const fileList: string[] = [];
146
+ const fileMatchCounts = new Map<string, number>();
147
+ const matchesByFile = new Map<string, AstFindMatch[]>();
148
+ const recordFile = (relativePath: string) => {
149
+ if (!files.has(relativePath)) {
150
+ files.add(relativePath);
151
+ fileList.push(relativePath);
152
+ }
153
+ };
154
+ for (const match of result.matches) {
155
+ const relativePath = formatPath(match.path);
156
+ recordFile(relativePath);
157
+ if (!matchesByFile.has(relativePath)) {
158
+ matchesByFile.set(relativePath, []);
159
+ }
160
+ matchesByFile.get(relativePath)!.push(match);
161
+ }
162
+
163
+ const baseDetails: AstGrepToolDetails = {
164
+ matchCount: result.totalMatches,
165
+ fileCount: result.filesWithMatches,
166
+ filesSearched: result.filesSearched,
167
+ limitReached: result.limitReached,
168
+ parseErrors: result.parseErrors,
169
+ scopePath,
170
+ files: fileList,
171
+ fileMatches: [],
172
+ };
173
+
174
+ if (result.matches.length === 0) {
175
+ const parseMessage = result.parseErrors?.length
176
+ ? `\n${formatParseErrors(result.parseErrors).join("\n")}`
177
+ : "";
178
+ return toolResult(baseDetails).text(`No matches found${parseMessage}`).done();
179
+ }
180
+
181
+ const useHashLines = resolveFileDisplayMode(this.session).hashLines;
182
+ const outputLines: string[] = [];
183
+ const renderMatchesForFile = (relativePath: string) => {
184
+ const fileMatches = matchesByFile.get(relativePath) ?? [];
185
+ for (const match of fileMatches) {
186
+ const matchLines = match.text.split("\n");
187
+ const lineNumbers = matchLines.map((_, index) => match.startLine + index);
188
+ const lineWidth = Math.max(...lineNumbers.map(value => value.toString().length));
189
+ const formatLine = (lineNumber: number, line: string, isMatch: boolean): string => {
190
+ if (useHashLines) {
191
+ const ref = `${lineNumber}#${computeLineHash(lineNumber, line)}`;
192
+ return isMatch ? `>>${ref}:${line}` : ` ${ref}:${line}`;
193
+ }
194
+ const padded = lineNumber.toString().padStart(lineWidth, " ");
195
+ return isMatch ? `>>${padded}:${line}` : ` ${padded}:${line}`;
196
+ };
197
+ for (let index = 0; index < matchLines.length; index++) {
198
+ outputLines.push(formatLine(match.startLine + index, matchLines[index], index === 0));
199
+ }
200
+ if (match.metaVariables && Object.keys(match.metaVariables).length > 0) {
201
+ const serializedMeta = Object.entries(match.metaVariables)
202
+ .sort(([left], [right]) => left.localeCompare(right))
203
+ .map(([key, value]) => `${key}=${value}`)
204
+ .join(", ");
205
+ outputLines.push(` meta: ${serializedMeta}`);
206
+ }
207
+ fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
208
+ }
209
+ };
210
+
211
+ if (isDirectory) {
212
+ const filesByDirectory = new Map<string, string[]>();
213
+ for (const relativePath of fileList) {
214
+ const directory = path.dirname(relativePath).replace(/\\/g, "/");
215
+ if (!filesByDirectory.has(directory)) {
216
+ filesByDirectory.set(directory, []);
217
+ }
218
+ filesByDirectory.get(directory)!.push(relativePath);
219
+ }
220
+ for (const [directory, directoryFiles] of filesByDirectory) {
221
+ if (directory === ".") {
222
+ for (const relativePath of directoryFiles) {
223
+ if (outputLines.length > 0) {
224
+ outputLines.push("");
225
+ }
226
+ outputLines.push(`# ${path.basename(relativePath)}`);
227
+ renderMatchesForFile(relativePath);
228
+ }
229
+ continue;
230
+ }
231
+ if (outputLines.length > 0) {
232
+ outputLines.push("");
233
+ }
234
+ outputLines.push(`# ${directory}`);
235
+ for (const relativePath of directoryFiles) {
236
+ outputLines.push(`## └─ ${path.basename(relativePath)}`);
237
+ renderMatchesForFile(relativePath);
238
+ }
239
+ }
240
+ } else {
241
+ for (const relativePath of fileList) {
242
+ renderMatchesForFile(relativePath);
243
+ }
244
+ }
245
+
246
+ const details: AstGrepToolDetails = {
247
+ ...baseDetails,
248
+ fileMatches: fileList.map(filePath => ({
249
+ path: filePath,
250
+ count: fileMatchCounts.get(filePath) ?? 0,
251
+ })),
252
+ };
253
+ if (result.limitReached) {
254
+ outputLines.push("", "Result limit reached; narrow path pattern or increase limit.");
255
+ }
256
+ if (result.parseErrors?.length) {
257
+ outputLines.push("", ...formatParseErrors(result.parseErrors));
258
+ }
259
+
260
+ return toolResult(details).text(outputLines.join("\n")).done();
261
+ });
262
+ }
263
+ }
264
+
265
+ // =============================================================================
266
+ // TUI Renderer
267
+ // =============================================================================
268
+
269
+ interface AstGrepRenderArgs {
270
+ patterns?: string[];
271
+ lang?: string;
272
+ path?: string;
273
+ selector?: string;
274
+ limit?: number;
275
+ offset?: number;
276
+ context?: number;
277
+ }
278
+
279
+ const COLLAPSED_MATCH_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
280
+
281
+ export const astGrepToolRenderer = {
282
+ inline: true,
283
+ renderCall(args: AstGrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
284
+ const meta: string[] = [];
285
+ if (args.lang) meta.push(`lang:${args.lang}`);
286
+ if (args.path) meta.push(`in ${args.path}`);
287
+ if (args.selector) meta.push("selector");
288
+ if (args.limit !== undefined && args.limit > 0) meta.push(`limit:${args.limit}`);
289
+ if (args.offset !== undefined && args.offset > 0) meta.push(`offset:${args.offset}`);
290
+ if (args.context !== undefined) meta.push(`context:${args.context}`);
291
+ if (args.patterns && args.patterns.length > 1) meta.push(`${args.patterns.length} patterns`);
292
+
293
+ const description =
294
+ args.patterns?.length === 1 ? args.patterns[0] : args.patterns ? `${args.patterns.length} patterns` : "?";
295
+ const text = renderStatusLine({ icon: "pending", title: "AST Grep", description, meta }, uiTheme);
296
+ return new Text(text, 0, 0);
297
+ },
298
+
299
+ renderResult(
300
+ result: { content: Array<{ type: string; text?: string }>; details?: AstGrepToolDetails; isError?: boolean },
301
+ options: RenderResultOptions,
302
+ uiTheme: Theme,
303
+ args?: AstGrepRenderArgs,
304
+ ): Component {
305
+ const details = result.details;
306
+
307
+ if (result.isError) {
308
+ const errorText = result.content?.find(c => c.type === "text")?.text || "Unknown error";
309
+ return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
310
+ }
311
+
312
+ const matchCount = details?.matchCount ?? 0;
313
+ const fileCount = details?.fileCount ?? 0;
314
+ const filesSearched = details?.filesSearched ?? 0;
315
+ const limitReached = details?.limitReached ?? false;
316
+
317
+ if (matchCount === 0) {
318
+ const description = args?.patterns?.length === 1 ? args.patterns[0] : undefined;
319
+ const meta = ["0 matches"];
320
+ if (details?.scopePath) meta.push(`in ${details.scopePath}`);
321
+ if (filesSearched > 0) meta.push(`searched ${filesSearched}`);
322
+ const header = renderStatusLine({ icon: "warning", title: "AST Grep", description, meta }, uiTheme);
323
+ const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
324
+ if (details?.parseErrors?.length) {
325
+ const capped = details.parseErrors.slice(0, PARSE_ERRORS_LIMIT);
326
+ for (const err of capped) {
327
+ lines.push(uiTheme.fg("warning", ` - ${err}`));
328
+ }
329
+ if (details.parseErrors.length > PARSE_ERRORS_LIMIT) {
330
+ lines.push(uiTheme.fg("dim", ` … ${details.parseErrors.length - PARSE_ERRORS_LIMIT} more`));
331
+ }
332
+ }
333
+ return new Text(lines.join("\n"), 0, 0);
334
+ }
335
+
336
+ const summaryParts = [formatCount("match", matchCount), formatCount("file", fileCount)];
337
+ const meta = [...summaryParts];
338
+ if (details?.scopePath) meta.push(`in ${details.scopePath}`);
339
+ meta.push(`searched ${filesSearched}`);
340
+ if (limitReached) meta.push(uiTheme.fg("warning", "limit reached"));
341
+ const description = args?.patterns?.length === 1 ? args.patterns[0] : undefined;
342
+ const header = renderStatusLine(
343
+ { icon: limitReached ? "warning" : "success", title: "AST Grep", description, meta },
344
+ uiTheme,
345
+ );
346
+
347
+ const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
348
+ const rawLines = textContent.split("\n");
349
+ const hasSeparators = rawLines.some(line => line.trim().length === 0);
350
+ const allGroups: string[][] = [];
351
+ if (hasSeparators) {
352
+ let current: string[] = [];
353
+ for (const line of rawLines) {
354
+ if (line.trim().length === 0) {
355
+ if (current.length > 0) {
356
+ allGroups.push(current);
357
+ current = [];
358
+ }
359
+ continue;
360
+ }
361
+ current.push(line);
362
+ }
363
+ if (current.length > 0) allGroups.push(current);
364
+ } else {
365
+ const nonEmpty = rawLines.filter(line => line.trim().length > 0);
366
+ if (nonEmpty.length > 0) {
367
+ allGroups.push(nonEmpty);
368
+ }
369
+ }
370
+ const matchGroups = allGroups.filter(
371
+ group => !group[0]?.startsWith("Result limit reached") && !group[0]?.startsWith("Parse issues:"),
372
+ );
373
+
374
+ const getCollapsedMatchLimit = (groups: string[][], maxLines: number): number => {
375
+ if (groups.length === 0) return 0;
376
+ let usedLines = 0;
377
+ let count = 0;
378
+ for (const group of groups) {
379
+ if (count > 0 && usedLines + group.length > maxLines) break;
380
+ usedLines += group.length;
381
+ count += 1;
382
+ if (usedLines >= maxLines) break;
383
+ }
384
+ return count;
385
+ };
386
+
387
+ const extraLines: string[] = [];
388
+ if (limitReached) {
389
+ extraLines.push(uiTheme.fg("warning", "limit reached; narrow path pattern or increase limit"));
390
+ }
391
+ if (details?.parseErrors?.length) {
392
+ const total = details.parseErrors.length;
393
+ const label =
394
+ total > PARSE_ERRORS_LIMIT
395
+ ? `${PARSE_ERRORS_LIMIT} / ${total} parse issues`
396
+ : `${total} parse issue${total !== 1 ? "s" : ""}`;
397
+ extraLines.push(uiTheme.fg("warning", label));
398
+ }
399
+
400
+ let cached: RenderCache | undefined;
401
+ return {
402
+ render(width: number): string[] {
403
+ const { expanded } = options;
404
+ const key = new Hasher().bool(expanded).u32(width).digest();
405
+ if (cached?.key === key) return cached.lines;
406
+ const maxCollapsed = expanded
407
+ ? matchGroups.length
408
+ : getCollapsedMatchLimit(matchGroups, COLLAPSED_MATCH_LIMIT);
409
+ const matchLines = renderTreeList(
410
+ {
411
+ items: matchGroups,
412
+ expanded,
413
+ maxCollapsed,
414
+ itemType: "match",
415
+ renderItem: group =>
416
+ group.map(line => {
417
+ if (line.startsWith("## ")) return uiTheme.fg("dim", line);
418
+ if (line.startsWith("# ")) return uiTheme.fg("accent", line);
419
+ if (line.startsWith(" meta:")) return uiTheme.fg("dim", line);
420
+ return uiTheme.fg("toolOutput", line);
421
+ }),
422
+ },
423
+ uiTheme,
424
+ );
425
+ const rendered = [header, ...matchLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
426
+ cached = { key, lines: rendered };
427
+ return rendered;
428
+ },
429
+ invalidate() {
430
+ cached = undefined;
431
+ },
432
+ };
433
+ },
434
+ mergeCallAndResult: true,
435
+ };
package/src/tools/bash.ts CHANGED
@@ -377,7 +377,8 @@ export const bashToolRenderer = {
377
377
  ): Component {
378
378
  const cmdText = args ? formatBashCommand(args, uiTheme) : undefined;
379
379
  const isError = result.isError === true;
380
- const header = renderStatusLine({ icon: isError ? "error" : "success", title: "Bash" }, uiTheme);
380
+ const icon = options.isPartial ? "pending" : isError ? "error" : "success";
381
+ const header = renderStatusLine({ icon, title: "Bash" }, uiTheme);
381
382
  const details = result.details;
382
383
  const outputBlock = new CachedOutputBlock();
383
384
 
@@ -438,7 +439,7 @@ export const bashToolRenderer = {
438
439
  return outputBlock.render(
439
440
  {
440
441
  header,
441
- state: isError ? "error" : "success",
442
+ state: options.isPartial ? "pending" : isError ? "error" : "success",
442
443
  sections: [
443
444
  { lines: cmdText ? [uiTheme.fg("dim", cmdText)] : [] },
444
445
  { label: uiTheme.fg("toolTitle", "Output"), lines: outputLines },
@@ -27,7 +27,7 @@ interface ImageApiKey {
27
27
  projectId?: string;
28
28
  }
29
29
 
30
- const responseModalitySchema = StringEnum(["Image", "Text"]);
30
+ const responseModalitySchema = StringEnum(["IMAGE", "TEXT"]);
31
31
  const aspectRatioSchema = StringEnum(["1:1", "3:4", "4:3", "9:16", "16:9"], {
32
32
  description: "Aspect ratio (1:1, 3:4, 4:3, 9:16, 16:9).",
33
33
  });
@@ -536,7 +536,7 @@ function buildAntigravityRequest(
536
536
  contents: [{ role: "user", parts }],
537
537
  systemInstruction: { parts: [{ text: IMAGE_SYSTEM_INSTRUCTION }] },
538
538
  generationConfig: {
539
- responseModalities: ["Image"],
539
+ responseModalities: ["IMAGE"],
540
540
  imageConfig,
541
541
  candidateCount: 1,
542
542
  },
@@ -788,7 +788,7 @@ export const geminiImageTool: CustomTool<typeof geminiImageSchema, GeminiImageTo
788
788
  responseModalities: GeminiResponseModality[];
789
789
  imageConfig?: { aspectRatio?: string; imageSize?: string };
790
790
  } = {
791
- responseModalities: ["Image"],
791
+ responseModalities: ["IMAGE"],
792
792
  };
793
793
 
794
794
  if (params.aspect_ratio || params.image_size) {
package/src/tools/grep.ts CHANGED
@@ -16,7 +16,7 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
16
16
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
17
17
  import type { ToolSession } from ".";
18
18
  import { formatFullOutputReference, type OutputMeta } from "./output-meta";
19
- import { resolveToCwd } from "./path-utils";
19
+ import { hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
20
20
  import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
21
21
  import { ToolError } from "./tool-errors";
22
22
  import { toolResult } from "./tool-result";
@@ -106,15 +106,33 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
106
106
 
107
107
  const useHashLines = resolveFileDisplayMode(this.session).hashLines;
108
108
  let searchPath: string;
109
+ let globFilter = glob?.trim() || undefined;
109
110
  const internalRouter = this.session.internalRouter;
110
- if (searchDir && internalRouter?.canHandle(searchDir)) {
111
- const resource = await internalRouter.resolve(searchDir);
112
- if (!resource.sourcePath) {
113
- throw new ToolError(`Cannot grep internal URL without a backing file: ${searchDir}`);
111
+ if (searchDir?.trim()) {
112
+ const rawPath = searchDir.trim();
113
+ if (internalRouter?.canHandle(rawPath)) {
114
+ if (hasGlobPathChars(rawPath)) {
115
+ throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
116
+ }
117
+ const resource = await internalRouter.resolve(rawPath);
118
+ if (!resource.sourcePath) {
119
+ throw new ToolError(`Cannot grep internal URL without a backing file: ${rawPath}`);
120
+ }
121
+ searchPath = resource.sourcePath;
122
+ } else {
123
+ const parsedPath = parseSearchPath(rawPath);
124
+ searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
125
+ if (parsedPath.glob) {
126
+ if (globFilter) {
127
+ throw new ToolError(
128
+ "When path already includes glob characters, omit the separate glob parameter",
129
+ );
130
+ }
131
+ globFilter = parsedPath.glob;
132
+ }
114
133
  }
115
- searchPath = resource.sourcePath;
116
134
  } else {
117
- searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
135
+ searchPath = resolveToCwd(".", this.session.cwd);
118
136
  }
119
137
  const scopePath = (() => {
120
138
  const relative = path.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
@@ -139,7 +157,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
139
157
  result = await grep({
140
158
  pattern: normalizedPattern,
141
159
  path: searchPath,
142
- glob: glob?.trim() || undefined,
160
+ glob: globFilter,
143
161
  type: type?.trim() || undefined,
144
162
  ignoreCase,
145
163
  multiline: effectiveMultiline,