@oh-my-pi/pi-coding-agent 10.3.2 → 10.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.
- package/CHANGELOG.md +61 -0
- package/package.json +16 -11
- package/src/capability/index.ts +9 -0
- package/src/cli/update-cli.ts +2 -5
- package/src/config/settings-schema.ts +39 -1
- package/src/cursor.ts +1 -1
- package/src/extensibility/custom-tools/wrapper.ts +9 -33
- package/src/extensibility/extensions/wrapper.ts +18 -31
- package/src/extensibility/hooks/tool-wrapper.ts +6 -16
- package/src/extensibility/tool-proxy.ts +25 -0
- package/src/index.ts +1 -0
- package/src/ipy/executor.ts +107 -3
- package/src/ipy/gateway-coordinator.ts +0 -4
- package/src/ipy/kernel.ts +65 -175
- package/src/main.ts +17 -0
- package/src/mcp/render.ts +10 -226
- package/src/modes/components/tool-execution.ts +83 -96
- package/src/modes/controllers/input-controller.ts +38 -0
- package/src/modes/interactive-mode.ts +13 -0
- package/src/patch/index.ts +1 -0
- package/src/prompts/system/system-prompt.md +5 -2
- package/src/prompts/tools/ask.md +6 -9
- package/src/prompts/tools/browser.md +26 -0
- package/src/prompts/tools/grep.md +4 -8
- package/src/prompts/tools/task.md +29 -4
- package/src/sdk.ts +21 -0
- package/src/session/session-manager.ts +1 -0
- package/src/task/executor.ts +5 -47
- package/src/tools/ask.ts +60 -71
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser.ts +1138 -0
- package/src/tools/find.ts +11 -2
- package/src/tools/grep.ts +111 -107
- package/src/tools/index.ts +4 -0
- package/src/tools/json-tree.ts +231 -0
- package/src/tools/notebook.ts +1 -0
- package/src/tools/puppeteer/00_stealth_tampering.txt +63 -0
- package/src/tools/puppeteer/01_stealth_activity.txt +20 -0
- package/src/tools/puppeteer/02_stealth_hairline.txt +11 -0
- package/src/tools/puppeteer/03_stealth_botd.txt +384 -0
- package/src/tools/puppeteer/04_stealth_iframe.txt +81 -0
- package/src/tools/puppeteer/05_stealth_webgl.txt +75 -0
- package/src/tools/puppeteer/06_stealth_screen.txt +72 -0
- package/src/tools/puppeteer/07_stealth_fonts.txt +97 -0
- package/src/tools/puppeteer/08_stealth_audio.txt +51 -0
- package/src/tools/puppeteer/09_stealth_locale.txt +46 -0
- package/src/tools/puppeteer/10_stealth_plugins.txt +206 -0
- package/src/tools/puppeteer/11_stealth_hardware.txt +8 -0
- package/src/tools/puppeteer/12_stealth_codecs.txt +40 -0
- package/src/tools/puppeteer/13_stealth_worker.txt +74 -0
- package/src/tools/python.ts +1 -0
- package/src/tools/ssh.ts +1 -0
- package/src/tools/todo-write.ts +1 -0
- package/src/tools/write.ts +1 -0
- package/src/web/search/index.ts +15 -4
- package/src/web/search/providers/jina.ts +76 -0
- package/src/web/search/render.ts +3 -1
- package/src/web/search/types.ts +2 -2
package/src/tools/find.ts
CHANGED
|
@@ -286,8 +286,9 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
286
286
|
: undefined;
|
|
287
287
|
const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
|
|
288
288
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
289
|
-
|
|
290
|
-
|
|
289
|
+
|
|
290
|
+
const doGlob = async (useGitignore: boolean) =>
|
|
291
|
+
untilAborted(combinedSignal, () =>
|
|
291
292
|
glob(
|
|
292
293
|
{
|
|
293
294
|
pattern: globPattern,
|
|
@@ -296,10 +297,18 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
296
297
|
hidden: includeHidden,
|
|
297
298
|
maxResults: effectiveLimit,
|
|
298
299
|
sortByMtime: true,
|
|
300
|
+
gitignore: useGitignore,
|
|
299
301
|
},
|
|
300
302
|
onMatch,
|
|
301
303
|
),
|
|
302
304
|
);
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
let result = await doGlob(true);
|
|
308
|
+
// If gitignore filtering yielded nothing, retry without it
|
|
309
|
+
if (result.matches.length === 0) {
|
|
310
|
+
result = await doGlob(false);
|
|
311
|
+
}
|
|
303
312
|
matches = result.matches;
|
|
304
313
|
} catch (error) {
|
|
305
314
|
if (error instanceof Error && error.name === "AbortError") {
|
package/src/tools/grep.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as nodePath from "node:path";
|
|
2
2
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
|
-
|
|
4
|
-
import {
|
|
3
|
+
|
|
4
|
+
import { grep as wasmGrep } from "@oh-my-pi/pi-natives";
|
|
5
5
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
@@ -10,7 +10,7 @@ import { renderPromptTemplate } from "../config/prompt-templates";
|
|
|
10
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
11
|
import type { Theme } from "../modes/theme/theme";
|
|
12
12
|
import grepDescription from "../prompts/tools/grep.md" with { type: "text" };
|
|
13
|
-
import {
|
|
13
|
+
import { renderStatusLine, renderTreeList } from "../tui";
|
|
14
14
|
import type { ToolSession } from ".";
|
|
15
15
|
import type { OutputMeta } from "./output-meta";
|
|
16
16
|
import { resolveToCwd } from "./path-utils";
|
|
@@ -24,16 +24,11 @@ const grepSchema = Type.Object({
|
|
|
24
24
|
path: Type.Optional(Type.String({ description: "File or directory to search (default: cwd)" })),
|
|
25
25
|
glob: Type.Optional(Type.String({ description: "Filter files by glob pattern (e.g., '*.js')" })),
|
|
26
26
|
type: Type.Optional(Type.String({ description: "Filter by file type (e.g., js, py, rust)" })),
|
|
27
|
-
output_mode: Type.Optional(
|
|
28
|
-
StringEnum(["filesWithMatches", "content", "count"], {
|
|
29
|
-
description: "Output format (default: files_with_matches)",
|
|
30
|
-
}),
|
|
31
|
-
),
|
|
32
27
|
i: Type.Optional(Type.Boolean({ description: "Case-insensitive search (default: false)" })),
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
multiline: Type.Optional(Type.Boolean({ description: "Enable multiline matching
|
|
36
|
-
limit: Type.Optional(Type.Number({ description: "Limit output to first N matches (default: 100
|
|
28
|
+
pre: Type.Optional(Type.Number({ description: "Lines of context before matches" })),
|
|
29
|
+
post: Type.Optional(Type.Number({ description: "Lines of context after matches" })),
|
|
30
|
+
multiline: Type.Optional(Type.Boolean({ description: "Enable multiline matching" })),
|
|
31
|
+
limit: Type.Optional(Type.Number({ description: "Limit output to first N matches (default: 100)" })),
|
|
37
32
|
offset: Type.Optional(Type.Number({ description: "Skip first N entries before applying limit (default: 0)" })),
|
|
38
33
|
});
|
|
39
34
|
|
|
@@ -50,7 +45,6 @@ export interface GrepToolDetails {
|
|
|
50
45
|
fileCount?: number;
|
|
51
46
|
files?: string[];
|
|
52
47
|
fileMatches?: Array<{ path: string; count: number }>;
|
|
53
|
-
mode?: "content" | "filesWithMatches" | "count";
|
|
54
48
|
truncated?: boolean;
|
|
55
49
|
error?: string;
|
|
56
50
|
}
|
|
@@ -86,7 +80,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
86
80
|
_onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
|
|
87
81
|
_toolContext?: AgentToolContext,
|
|
88
82
|
): Promise<AgentToolResult<GrepToolDetails>> {
|
|
89
|
-
const { pattern, path: searchDir, glob, type,
|
|
83
|
+
const { pattern, path: searchDir, glob, type, i, pre, post, multiline, limit, offset } = params;
|
|
90
84
|
|
|
91
85
|
return untilAborted(signal, async () => {
|
|
92
86
|
const normalizedPattern = pattern.trim();
|
|
@@ -105,10 +99,13 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
105
99
|
}
|
|
106
100
|
const normalizedLimit = rawLimit !== undefined && rawLimit > 0 ? rawLimit : undefined;
|
|
107
101
|
|
|
108
|
-
const
|
|
109
|
-
const
|
|
102
|
+
const defaultContextBefore = this.session.settings.get("grep.contextBefore");
|
|
103
|
+
const defaultContextAfter = this.session.settings.get("grep.contextAfter");
|
|
104
|
+
const normalizedContextBefore = pre ?? defaultContextBefore;
|
|
105
|
+
const normalizedContextAfter = post ?? defaultContextAfter;
|
|
110
106
|
const ignoreCase = i ?? false;
|
|
111
|
-
const
|
|
107
|
+
const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
|
|
108
|
+
const effectiveMultiline = multiline ?? patternHasNewline;
|
|
112
109
|
|
|
113
110
|
const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
|
|
114
111
|
const scopePath = (() => {
|
|
@@ -124,9 +121,8 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
124
121
|
throw new ToolError(`Path not found: ${searchPath}`);
|
|
125
122
|
}
|
|
126
123
|
|
|
127
|
-
const effectiveOutputMode =
|
|
128
|
-
const effectiveLimit =
|
|
129
|
-
effectiveOutputMode === "content" ? (normalizedLimit ?? DEFAULT_MATCH_LIMIT) : normalizedLimit;
|
|
124
|
+
const effectiveOutputMode = "content";
|
|
125
|
+
const effectiveLimit = normalizedLimit ?? DEFAULT_MATCH_LIMIT;
|
|
130
126
|
|
|
131
127
|
// Run WASM grep
|
|
132
128
|
let result: Awaited<ReturnType<typeof wasmGrep>>;
|
|
@@ -137,11 +133,12 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
137
133
|
glob: glob?.trim() || undefined,
|
|
138
134
|
type: type?.trim() || undefined,
|
|
139
135
|
ignoreCase,
|
|
140
|
-
multiline:
|
|
136
|
+
multiline: effectiveMultiline,
|
|
141
137
|
hidden: true,
|
|
142
138
|
maxCount: effectiveLimit,
|
|
143
139
|
offset: normalizedOffset > 0 ? normalizedOffset : undefined,
|
|
144
|
-
|
|
140
|
+
contextBefore: normalizedContextBefore,
|
|
141
|
+
contextAfter: normalizedContextAfter,
|
|
145
142
|
maxColumns: DEFAULT_MAX_COLUMN,
|
|
146
143
|
mode: effectiveOutputMode,
|
|
147
144
|
});
|
|
@@ -180,71 +177,66 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
180
177
|
matchCount: 0,
|
|
181
178
|
fileCount: 0,
|
|
182
179
|
files: [],
|
|
183
|
-
mode: effectiveOutputMode,
|
|
184
180
|
truncated: false,
|
|
185
181
|
};
|
|
186
182
|
return toolResult(details).text("No matches found").done();
|
|
187
183
|
}
|
|
188
184
|
|
|
189
|
-
|
|
185
|
+
const outputLines: string[] = [];
|
|
190
186
|
let linesTruncated = false;
|
|
187
|
+
let matchIndex = 0;
|
|
191
188
|
|
|
192
189
|
for (const match of result.matches) {
|
|
193
190
|
recordFile(match.path);
|
|
194
191
|
const relativePath = formatPath(match.path);
|
|
195
192
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
193
|
+
matchIndex += 1;
|
|
194
|
+
if (matchIndex > 1) {
|
|
195
|
+
outputLines.push("");
|
|
196
|
+
}
|
|
197
|
+
outputLines.push(`${matchIndex}. ${relativePath}:${match.lineNumber}`);
|
|
198
|
+
|
|
199
|
+
const lineNumbers: number[] = [match.lineNumber];
|
|
200
|
+
if (match.contextBefore) {
|
|
201
|
+
for (const ctx of match.contextBefore) {
|
|
202
|
+
lineNumbers.push(ctx.lineNumber);
|
|
206
203
|
}
|
|
204
|
+
}
|
|
205
|
+
if (match.contextAfter) {
|
|
206
|
+
for (const ctx of match.contextAfter) {
|
|
207
|
+
lineNumbers.push(ctx.lineNumber);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const lineWidth = Math.max(...lineNumbers.map(value => value.toString().length));
|
|
207
211
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
: `${relativePath}: ${match.line}`,
|
|
213
|
-
);
|
|
212
|
+
const formatLine = (lineNumber: number, line: string, isMatch: boolean): string => {
|
|
213
|
+
const padded = lineNumber.toString().padStart(lineWidth, " ");
|
|
214
|
+
return isMatch ? `>>${padded} ${line}` : ` ${padded} ${line}`;
|
|
215
|
+
};
|
|
214
216
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
+
// Add context before
|
|
218
|
+
if (match.contextBefore) {
|
|
219
|
+
for (const ctx of match.contextBefore) {
|
|
220
|
+
outputLines.push(formatLine(ctx.lineNumber, ctx.line, false));
|
|
217
221
|
}
|
|
222
|
+
}
|
|
218
223
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
for (const ctx of match.contextAfter) {
|
|
222
|
-
outputLines.push(
|
|
223
|
-
showLineNumbers
|
|
224
|
-
? `${relativePath}-${ctx.lineNumber}- ${ctx.line}`
|
|
225
|
-
: `${relativePath}- ${ctx.line}`,
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
224
|
+
// Add match line
|
|
225
|
+
outputLines.push(formatLine(match.lineNumber, match.line, true));
|
|
229
226
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
} else if (effectiveOutputMode === "filesWithMatches") {
|
|
233
|
-
// One line per file
|
|
234
|
-
const matchWithCount = match as WasmGrepMatch & { matchCount?: number };
|
|
235
|
-
fileMatchCounts.set(relativePath, matchWithCount.matchCount ?? 1);
|
|
236
|
-
} else {
|
|
237
|
-
// count mode
|
|
238
|
-
const matchWithCount = match as WasmGrepMatch & { matchCount?: number };
|
|
239
|
-
fileMatchCounts.set(relativePath, matchWithCount.matchCount ?? 0);
|
|
227
|
+
if (match.truncated) {
|
|
228
|
+
linesTruncated = true;
|
|
240
229
|
}
|
|
241
|
-
}
|
|
242
230
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
231
|
+
// Add context after
|
|
232
|
+
if (match.contextAfter) {
|
|
233
|
+
for (const ctx of match.contextAfter) {
|
|
234
|
+
outputLines.push(formatLine(ctx.lineNumber, ctx.line, false));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Track per-file counts
|
|
239
|
+
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
248
240
|
}
|
|
249
241
|
|
|
250
242
|
const rawOutput = outputLines.join("\n");
|
|
@@ -261,7 +253,6 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
261
253
|
path,
|
|
262
254
|
count: fileMatchCounts.get(path) ?? 0,
|
|
263
255
|
})),
|
|
264
|
-
mode: effectiveOutputMode,
|
|
265
256
|
truncated,
|
|
266
257
|
matchLimitReached: result.limitReached ? effectiveLimit : undefined,
|
|
267
258
|
};
|
|
@@ -295,15 +286,13 @@ interface GrepRenderArgs {
|
|
|
295
286
|
glob?: string;
|
|
296
287
|
type?: string;
|
|
297
288
|
i?: boolean;
|
|
298
|
-
|
|
299
|
-
|
|
289
|
+
pre?: number;
|
|
290
|
+
post?: number;
|
|
300
291
|
multiline?: boolean;
|
|
301
|
-
output_mode?: string;
|
|
302
292
|
limit?: number;
|
|
303
293
|
offset?: number;
|
|
304
294
|
}
|
|
305
295
|
|
|
306
|
-
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
307
296
|
const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
308
297
|
|
|
309
298
|
export const grepToolRenderer = {
|
|
@@ -313,10 +302,13 @@ export const grepToolRenderer = {
|
|
|
313
302
|
if (args.path) meta.push(`in ${args.path}`);
|
|
314
303
|
if (args.glob) meta.push(`glob:${args.glob}`);
|
|
315
304
|
if (args.type) meta.push(`type:${args.type}`);
|
|
316
|
-
if (args.output_mode && args.output_mode !== "filesWithMatches") meta.push(`mode:${args.output_mode}`);
|
|
317
305
|
if (args.i) meta.push("case:insensitive");
|
|
318
|
-
if (args.
|
|
319
|
-
|
|
306
|
+
if (args.pre !== undefined && args.pre > 0) {
|
|
307
|
+
meta.push(`pre:${args.pre}`);
|
|
308
|
+
}
|
|
309
|
+
if (args.post !== undefined && args.post > 0) {
|
|
310
|
+
meta.push(`post:${args.post}`);
|
|
311
|
+
}
|
|
320
312
|
if (args.multiline) meta.push("multiline");
|
|
321
313
|
if (args.limit !== undefined && args.limit > 0) meta.push(`limit:${args.limit}`);
|
|
322
314
|
if (args.offset !== undefined && args.offset > 0) meta.push(`offset:${args.offset}`);
|
|
@@ -369,13 +361,11 @@ export const grepToolRenderer = {
|
|
|
369
361
|
|
|
370
362
|
const matchCount = details?.matchCount ?? 0;
|
|
371
363
|
const fileCount = details?.fileCount ?? 0;
|
|
372
|
-
const mode = details?.mode ?? "filesWithMatches";
|
|
373
364
|
const truncation = details?.meta?.truncation;
|
|
374
365
|
const limits = details?.meta?.limits;
|
|
375
366
|
const truncated = Boolean(
|
|
376
367
|
details?.truncated || truncation || limits?.matchLimit || limits?.resultLimit || limits?.columnTruncated,
|
|
377
368
|
);
|
|
378
|
-
const files = details?.files ?? [];
|
|
379
369
|
|
|
380
370
|
if (matchCount === 0) {
|
|
381
371
|
const header = renderStatusLine(
|
|
@@ -385,10 +375,7 @@ export const grepToolRenderer = {
|
|
|
385
375
|
return new Text([header, formatEmptyMessage("No matches found", uiTheme)].join("\n"), 0, 0);
|
|
386
376
|
}
|
|
387
377
|
|
|
388
|
-
const summaryParts =
|
|
389
|
-
mode === "filesWithMatches"
|
|
390
|
-
? [formatCount("file", fileCount)]
|
|
391
|
-
: [formatCount("match", matchCount), formatCount("file", fileCount)];
|
|
378
|
+
const summaryParts = [formatCount("match", matchCount), formatCount("file", fileCount)];
|
|
392
379
|
const meta = [...summaryParts];
|
|
393
380
|
if (details?.scopePath) meta.push(`in ${details.scopePath}`);
|
|
394
381
|
if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
|
|
@@ -398,34 +385,51 @@ export const grepToolRenderer = {
|
|
|
398
385
|
uiTheme,
|
|
399
386
|
);
|
|
400
387
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
388
|
+
const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
389
|
+
const rawLines = textContent.split("\n");
|
|
390
|
+
const hasSeparators = rawLines.some(line => line.trim().length === 0);
|
|
391
|
+
const matchGroups: string[][] = [];
|
|
392
|
+
if (hasSeparators) {
|
|
393
|
+
let current: string[] = [];
|
|
394
|
+
for (const line of rawLines) {
|
|
395
|
+
if (line.trim().length === 0) {
|
|
396
|
+
if (current.length > 0) {
|
|
397
|
+
matchGroups.push(current);
|
|
398
|
+
current = [];
|
|
399
|
+
}
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
current.push(line);
|
|
403
|
+
}
|
|
404
|
+
if (current.length > 0) matchGroups.push(current);
|
|
405
|
+
} else {
|
|
406
|
+
for (const line of rawLines) {
|
|
407
|
+
if (line.trim().length === 0) continue;
|
|
408
|
+
matchGroups.push([line]);
|
|
409
|
+
}
|
|
415
410
|
}
|
|
416
411
|
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
412
|
+
const getCollapsedMatchLimit = (groups: string[][], maxLines: number): number => {
|
|
413
|
+
if (groups.length === 0) return 0;
|
|
414
|
+
let usedLines = 0;
|
|
415
|
+
let count = 0;
|
|
416
|
+
for (const group of groups) {
|
|
417
|
+
if (count > 0 && usedLines + group.length > maxLines) break;
|
|
418
|
+
usedLines += group.length;
|
|
419
|
+
count += 1;
|
|
420
|
+
if (usedLines >= maxLines) break;
|
|
421
|
+
}
|
|
422
|
+
return count;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const maxCollapsed = expanded ? matchGroups.length : getCollapsedMatchLimit(matchGroups, COLLAPSED_TEXT_LIMIT);
|
|
426
|
+
const matchLines = renderTreeList(
|
|
421
427
|
{
|
|
422
|
-
|
|
423
|
-
path: entry.path,
|
|
424
|
-
isDirectory: entry.path.endsWith("/"),
|
|
425
|
-
meta: entry.count !== undefined ? `(${entry.count} match${entry.count !== 1 ? "es" : ""})` : undefined,
|
|
426
|
-
})),
|
|
428
|
+
items: matchGroups,
|
|
427
429
|
expanded,
|
|
428
|
-
maxCollapsed
|
|
430
|
+
maxCollapsed,
|
|
431
|
+
itemType: "match",
|
|
432
|
+
renderItem: group => group.map(line => uiTheme.fg("toolOutput", line)),
|
|
429
433
|
},
|
|
430
434
|
uiTheme,
|
|
431
435
|
);
|
|
@@ -440,7 +444,7 @@ export const grepToolRenderer = {
|
|
|
440
444
|
const extraLines =
|
|
441
445
|
truncationReasons.length > 0 ? [uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`)] : [];
|
|
442
446
|
|
|
443
|
-
return new Text([header, ...
|
|
447
|
+
return new Text([header, ...matchLines, ...extraLines].join("\n"), 0, 0);
|
|
444
448
|
},
|
|
445
449
|
mergeCallAndResult: true,
|
|
446
450
|
};
|
package/src/tools/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { time } from "../utils/timings";
|
|
|
17
17
|
import { WebSearchTool } from "../web/search";
|
|
18
18
|
import { AskTool } from "./ask";
|
|
19
19
|
import { BashTool } from "./bash";
|
|
20
|
+
import { BrowserTool } from "./browser";
|
|
20
21
|
import { CalculatorTool } from "./calculator";
|
|
21
22
|
import { ExitPlanModeTool } from "./exit-plan-mode";
|
|
22
23
|
import { FetchTool } from "./fetch";
|
|
@@ -69,6 +70,7 @@ export {
|
|
|
69
70
|
} from "../web/search";
|
|
70
71
|
export { AskTool, type AskToolDetails } from "./ask";
|
|
71
72
|
export { BashTool, type BashToolDetails, type BashToolOptions } from "./bash";
|
|
73
|
+
export { BrowserTool, type BrowserToolDetails } from "./browser";
|
|
72
74
|
export { CalculatorTool, type CalculatorToolDetails } from "./calculator";
|
|
73
75
|
export { type ExitPlanModeDetails, ExitPlanModeTool } from "./exit-plan-mode";
|
|
74
76
|
export { FetchTool, type FetchToolDetails } from "./fetch";
|
|
@@ -171,6 +173,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
171
173
|
lsp: LspTool.createIf,
|
|
172
174
|
notebook: s => new NotebookTool(s),
|
|
173
175
|
read: s => new ReadTool(s),
|
|
176
|
+
browser: s => new BrowserTool(s),
|
|
174
177
|
task: TaskTool.create,
|
|
175
178
|
todo_write: s => new TodoWriteTool(s),
|
|
176
179
|
fetch: s => new FetchTool(s),
|
|
@@ -283,6 +286,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
283
286
|
if (name === "web_search") return session.settings.get("web_search.enabled");
|
|
284
287
|
if (name === "lsp") return session.settings.get("lsp.enabled");
|
|
285
288
|
if (name === "calc") return session.settings.get("calc.enabled");
|
|
289
|
+
if (name === "browser") return session.settings.get("browser.enabled");
|
|
286
290
|
return true;
|
|
287
291
|
};
|
|
288
292
|
if (includeSubmitResult && requestedTools && !requestedTools.includes("submit_result")) {
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON tree rendering utilities shared across tool renderers.
|
|
3
|
+
*/
|
|
4
|
+
import type { Theme } from "../modes/theme/theme";
|
|
5
|
+
import { truncateToWidth } from "./render-utils";
|
|
6
|
+
|
|
7
|
+
/** Max depth for JSON tree rendering */
|
|
8
|
+
export const JSON_TREE_MAX_DEPTH_COLLAPSED = 2;
|
|
9
|
+
export const JSON_TREE_MAX_DEPTH_EXPANDED = 6;
|
|
10
|
+
export const JSON_TREE_MAX_LINES_COLLAPSED = 6;
|
|
11
|
+
export const JSON_TREE_MAX_LINES_EXPANDED = 200;
|
|
12
|
+
export const JSON_TREE_SCALAR_LEN_COLLAPSED = 60;
|
|
13
|
+
export const JSON_TREE_SCALAR_LEN_EXPANDED = 2000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Format a scalar value for inline display.
|
|
17
|
+
*/
|
|
18
|
+
export function formatScalar(value: unknown, maxLen: number): string {
|
|
19
|
+
if (value === null) return "null";
|
|
20
|
+
if (value === undefined) return "undefined";
|
|
21
|
+
if (typeof value === "boolean") return String(value);
|
|
22
|
+
if (typeof value === "number") return String(value);
|
|
23
|
+
if (typeof value === "string") {
|
|
24
|
+
const escaped = value.replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
25
|
+
const truncated = truncateToWidth(escaped, maxLen);
|
|
26
|
+
return `"${truncated}"`;
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(value)) return `[${value.length} items]`;
|
|
29
|
+
if (typeof value === "object") {
|
|
30
|
+
const keys = Object.keys(value);
|
|
31
|
+
return `{${keys.length} keys}`;
|
|
32
|
+
}
|
|
33
|
+
return String(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format args inline for collapsed view.
|
|
38
|
+
*/
|
|
39
|
+
export function formatArgsInline(args: Record<string, unknown>, maxWidth: number): string {
|
|
40
|
+
const entries = Object.entries(args);
|
|
41
|
+
if (entries.length === 0) return "";
|
|
42
|
+
|
|
43
|
+
// Single arg: show key=value
|
|
44
|
+
if (entries.length === 1) {
|
|
45
|
+
const [key, value] = entries[0];
|
|
46
|
+
return `${key}=${formatScalar(value, maxWidth - key.length - 1)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Multiple args: show key=value, key=value...
|
|
50
|
+
const pairs: string[] = [];
|
|
51
|
+
let totalLen = 0;
|
|
52
|
+
|
|
53
|
+
for (const [key, value] of entries) {
|
|
54
|
+
const valueStr = formatScalar(value, 24);
|
|
55
|
+
const pairStr = `${key}=${valueStr}`;
|
|
56
|
+
const addLen = pairs.length > 0 ? pairStr.length + 2 : pairStr.length;
|
|
57
|
+
|
|
58
|
+
if (totalLen + addLen > maxWidth && pairs.length > 0) {
|
|
59
|
+
pairs.push("…");
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pairs.push(pairStr);
|
|
64
|
+
totalLen += addLen;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return pairs.join(", ");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build tree prefix for nested rendering.
|
|
72
|
+
*/
|
|
73
|
+
function buildTreePrefix(ancestors: boolean[], theme: Theme): string {
|
|
74
|
+
return ancestors.map(hasNext => (hasNext ? `${theme.tree.vertical} ` : " ")).join("");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Render a JSON value as tree lines.
|
|
79
|
+
*/
|
|
80
|
+
export function renderJsonTreeLines(
|
|
81
|
+
value: unknown,
|
|
82
|
+
theme: Theme,
|
|
83
|
+
maxDepth: number,
|
|
84
|
+
maxLines: number,
|
|
85
|
+
maxScalarLen: number,
|
|
86
|
+
): { lines: string[]; truncated: boolean } {
|
|
87
|
+
const lines: string[] = [];
|
|
88
|
+
let truncated = false;
|
|
89
|
+
|
|
90
|
+
const iconObject = theme.styledSymbol("icon.folder", "muted");
|
|
91
|
+
const iconArray = theme.styledSymbol("icon.package", "muted");
|
|
92
|
+
const iconScalar = theme.styledSymbol("icon.file", "muted");
|
|
93
|
+
|
|
94
|
+
const pushLine = (line: string): boolean => {
|
|
95
|
+
if (lines.length >= maxLines) {
|
|
96
|
+
truncated = true;
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
lines.push(line);
|
|
100
|
+
return true;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const renderNode = (val: unknown, key: string | undefined, ancestors: boolean[], isLast: boolean, depth: number) => {
|
|
104
|
+
if (lines.length >= maxLines) {
|
|
105
|
+
truncated = true;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const connector = isLast ? theme.tree.last : theme.tree.branch;
|
|
110
|
+
const prefix = `${buildTreePrefix(ancestors, theme)}${theme.fg("dim", connector)} `;
|
|
111
|
+
|
|
112
|
+
// Handle scalars
|
|
113
|
+
if (val === null || val === undefined || typeof val !== "object") {
|
|
114
|
+
const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
|
|
115
|
+
|
|
116
|
+
// Special handling for multiline strings
|
|
117
|
+
if (typeof val === "string" && val.includes("\n")) {
|
|
118
|
+
const strLines = val.split("\n");
|
|
119
|
+
const maxStrLines = Math.min(strLines.length, Math.max(1, maxLines - lines.length - 1));
|
|
120
|
+
const continuePrefix = buildTreePrefix([...ancestors, !isLast], theme);
|
|
121
|
+
|
|
122
|
+
// First line with label
|
|
123
|
+
const firstLine = truncateToWidth(strLines[0], maxScalarLen);
|
|
124
|
+
pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", `"${firstLine}`)}`);
|
|
125
|
+
|
|
126
|
+
// Subsequent lines indented
|
|
127
|
+
for (let i = 1; i < maxStrLines; i++) {
|
|
128
|
+
if (lines.length >= maxLines) {
|
|
129
|
+
truncated = true;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
const line = truncateToWidth(strLines[i], maxScalarLen);
|
|
133
|
+
pushLine(`${continuePrefix} ${theme.fg("dim", ` ${line}`)}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Show truncation and closing quote
|
|
137
|
+
if (strLines.length > maxStrLines) {
|
|
138
|
+
truncated = true;
|
|
139
|
+
pushLine(`${continuePrefix} ${theme.fg("dim", ` …(${strLines.length - maxStrLines} more lines)"`)}`);
|
|
140
|
+
} else {
|
|
141
|
+
// Add closing quote to last line - need to modify the last pushed line
|
|
142
|
+
const lastIdx = lines.length - 1;
|
|
143
|
+
lines[lastIdx] = `${lines[lastIdx]}${theme.fg("dim", '"')}`;
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const scalar = formatScalar(val, maxScalarLen);
|
|
149
|
+
pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", scalar)}`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Handle arrays
|
|
154
|
+
if (Array.isArray(val)) {
|
|
155
|
+
const header = key ? theme.fg("muted", key) : theme.fg("muted", "array");
|
|
156
|
+
pushLine(`${prefix}${iconArray} ${header}`);
|
|
157
|
+
if (val.length === 0) {
|
|
158
|
+
pushLine(
|
|
159
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "[]")}`,
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (depth >= maxDepth) {
|
|
164
|
+
pushLine(
|
|
165
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "…")}`,
|
|
166
|
+
);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const nextAncestors = [...ancestors, !isLast];
|
|
170
|
+
for (let i = 0; i < val.length; i++) {
|
|
171
|
+
renderNode(val[i], `[${i}]`, nextAncestors, i === val.length - 1, depth + 1);
|
|
172
|
+
if (lines.length >= maxLines) {
|
|
173
|
+
truncated = true;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Handle objects
|
|
181
|
+
const header = key ? theme.fg("muted", key) : theme.fg("muted", "object");
|
|
182
|
+
pushLine(`${prefix}${iconObject} ${header}`);
|
|
183
|
+
const entries = Object.entries(val as Record<string, unknown>);
|
|
184
|
+
if (entries.length === 0) {
|
|
185
|
+
pushLine(
|
|
186
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "{}")}`,
|
|
187
|
+
);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (depth >= maxDepth) {
|
|
191
|
+
pushLine(
|
|
192
|
+
`${buildTreePrefix([...ancestors, !isLast], theme)}${theme.fg("dim", theme.tree.last)} ${theme.fg("dim", "…")}`,
|
|
193
|
+
);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const nextAncestors = [...ancestors, !isLast];
|
|
197
|
+
for (let i = 0; i < entries.length; i++) {
|
|
198
|
+
const [childKey, child] = entries[i];
|
|
199
|
+
renderNode(child, childKey, nextAncestors, i === entries.length - 1, depth + 1);
|
|
200
|
+
if (lines.length >= maxLines) {
|
|
201
|
+
truncated = true;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Render root level
|
|
208
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
209
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
210
|
+
for (let i = 0; i < entries.length; i++) {
|
|
211
|
+
const [childKey, child] = entries[i];
|
|
212
|
+
renderNode(child, childKey, [], i === entries.length - 1, 1);
|
|
213
|
+
if (lines.length >= maxLines) {
|
|
214
|
+
truncated = true;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} else if (Array.isArray(value)) {
|
|
219
|
+
for (let i = 0; i < value.length; i++) {
|
|
220
|
+
renderNode(value[i], `[${i}]`, [], i === value.length - 1, 1);
|
|
221
|
+
if (lines.length >= maxLines) {
|
|
222
|
+
truncated = true;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
renderNode(value, undefined, [], true, 0);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { lines, truncated };
|
|
231
|
+
}
|
package/src/tools/notebook.ts
CHANGED
|
@@ -65,6 +65,7 @@ export class NotebookTool implements AgentTool<typeof notebookSchema, NotebookTo
|
|
|
65
65
|
public readonly description =
|
|
66
66
|
"Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path. The cell_number is 0-indexed. Use edit_mode=insert to add a new cell at the index specified by cell_number. Use edit_mode=delete to delete the cell at the index specified by cell_number.";
|
|
67
67
|
public readonly parameters = notebookSchema;
|
|
68
|
+
public readonly concurrency = "exclusive";
|
|
68
69
|
|
|
69
70
|
private readonly session: ToolSession;
|
|
70
71
|
|