@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.
- package/CHANGELOG.md +97 -7
- package/examples/sdk/README.md +22 -0
- package/package.json +7 -7
- package/src/capability/index.ts +1 -11
- package/src/commit/analysis/index.ts +4 -4
- package/src/config/settings-schema.ts +18 -15
- package/src/config/settings.ts +2 -20
- package/src/discovery/index.ts +1 -11
- package/src/exa/index.ts +1 -10
- package/src/extensibility/custom-commands/index.ts +2 -15
- package/src/extensibility/custom-tools/index.ts +3 -18
- package/src/extensibility/custom-tools/loader.ts +28 -5
- package/src/extensibility/custom-tools/types.ts +18 -1
- package/src/extensibility/extensions/index.ts +9 -130
- package/src/extensibility/extensions/types.ts +2 -1
- package/src/extensibility/hooks/index.ts +3 -14
- package/src/extensibility/plugins/index.ts +6 -31
- package/src/index.ts +28 -220
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/internal-urls/index.ts +11 -16
- package/src/mcp/index.ts +11 -37
- package/src/mcp/tool-bridge.ts +3 -42
- package/src/mcp/transports/index.ts +2 -2
- package/src/modes/components/extensions/index.ts +3 -3
- package/src/modes/components/index.ts +35 -40
- package/src/modes/interactive-mode.ts +4 -1
- package/src/modes/rpc/rpc-mode.ts +1 -7
- package/src/modes/theme/theme.ts +11 -10
- package/src/modes/types.ts +1 -1
- package/src/patch/index.ts +4 -20
- package/src/prompts/system/system-prompt.md +18 -4
- package/src/prompts/tools/ast-edit.md +33 -0
- package/src/prompts/tools/ast-grep.md +34 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/hashline.md +1 -0
- package/src/prompts/tools/resolve.md +8 -0
- package/src/sdk.ts +27 -7
- package/src/session/agent-session.ts +25 -36
- package/src/session/session-manager.ts +0 -30
- package/src/slash-commands/builtin-registry.ts +4 -2
- package/src/stt/index.ts +3 -3
- package/src/task/types.ts +2 -2
- package/src/tools/ast-edit.ts +480 -0
- package/src/tools/ast-grep.ts +435 -0
- package/src/tools/bash.ts +3 -2
- package/src/tools/gemini-image.ts +3 -3
- package/src/tools/grep.ts +26 -8
- package/src/tools/index.ts +55 -57
- package/src/tools/pending-action.ts +33 -0
- package/src/tools/render-utils.ts +10 -0
- package/src/tools/renderers.ts +6 -4
- package/src/tools/resolve.ts +156 -0
- package/src/tools/submit-result.ts +1 -1
- package/src/web/search/index.ts +6 -4
- package/src/web/search/providers/anthropic.ts +2 -2
- package/src/web/search/providers/base.ts +3 -0
- package/src/web/search/providers/exa.ts +11 -5
- package/src/web/search/providers/gemini.ts +112 -24
- package/src/patch/normative.ts +0 -72
- package/src/prompts/tools/ast-find.md +0 -20
- package/src/prompts/tools/ast-replace.md +0 -21
- package/src/tools/ast-find.ts +0 -316
- 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
|
|
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(["
|
|
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: ["
|
|
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: ["
|
|
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
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
|
|
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(
|
|
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:
|
|
160
|
+
glob: globFilter,
|
|
143
161
|
type: type?.trim() || undefined,
|
|
144
162
|
ignoreCase,
|
|
145
163
|
multiline: effectiveMultiline,
|