@oh-my-pi/pi-coding-agent 14.1.2 → 14.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -2
- package/package.json +8 -8
- package/scripts/build-binary.ts +61 -0
- package/src/autoresearch/helpers.ts +10 -0
- package/src/autoresearch/index.ts +1 -11
- package/src/autoresearch/tools/init-experiment.ts +1 -10
- package/src/autoresearch/tools/log-experiment.ts +1 -11
- package/src/autoresearch/tools/run-experiment.ts +1 -10
- package/src/bun-imports.d.ts +6 -0
- package/src/cli/plugin-cli.ts +23 -45
- package/src/commit/agentic/tools/propose-commit.ts +1 -14
- package/src/commit/agentic/tools/split-commit.ts +1 -15
- package/src/commit/utils.ts +15 -1
- package/src/config/model-registry.ts +3 -3
- package/src/config/prompt-templates.ts +4 -12
- package/src/config/settings-schema.ts +27 -2
- package/src/config/settings.ts +1 -1
- package/src/dap/session.ts +8 -2
- package/src/discovery/claude-plugins.ts +61 -6
- package/src/discovery/codex.ts +2 -15
- package/src/discovery/gemini.ts +2 -15
- package/src/discovery/helpers.ts +40 -1
- package/src/discovery/opencode.ts +2 -15
- package/src/edit/apply-patch/index.ts +87 -0
- package/src/edit/apply-patch/parser.ts +174 -0
- package/src/edit/diff.ts +3 -14
- package/src/edit/index.ts +67 -3
- package/src/edit/modes/apply-patch.lark +19 -0
- package/src/edit/modes/apply-patch.ts +63 -0
- package/src/edit/modes/chunk.ts +6 -2
- package/src/edit/modes/hashline.ts +3 -3
- package/src/edit/modes/replace.ts +2 -13
- package/src/edit/read-file.ts +18 -0
- package/src/edit/renderer.ts +61 -33
- package/src/extensibility/extensions/compact-handler.ts +40 -0
- package/src/extensibility/extensions/runner.ts +11 -29
- package/src/extensibility/utils.ts +7 -1
- package/src/internal-urls/docs-index.generated.ts +9 -2
- package/src/lsp/client.ts +14 -5
- package/src/lsp/index.ts +53 -10
- package/src/lsp/render.ts +14 -2
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +1 -0
- package/src/mcp/manager.ts +29 -48
- package/src/memories/index.ts +7 -1
- package/src/modes/acp/acp-agent.ts +3 -16
- package/src/modes/components/model-selector.ts +15 -24
- package/src/modes/components/plugin-settings.ts +16 -5
- package/src/modes/components/read-tool-group.ts +92 -9
- package/src/modes/components/settings-defs.ts +18 -0
- package/src/modes/components/settings-selector.ts +2 -6
- package/src/modes/components/tool-execution.ts +61 -28
- package/src/modes/controllers/event-controller.ts +3 -1
- package/src/modes/controllers/extension-ui-controller.ts +99 -150
- package/src/modes/controllers/selector-controller.ts +3 -12
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/print-mode.ts +4 -22
- package/src/modes/rpc/rpc-mode.ts +18 -38
- package/src/modes/shared.ts +10 -1
- package/src/modes/utils/ui-helpers.ts +6 -2
- package/src/plan-mode/approved-plan.ts +5 -4
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/subagent-user-prompt.md +2 -2
- package/src/prompts/system/system-prompt.md +208 -243
- package/src/prompts/tools/apply-patch.md +67 -0
- package/src/prompts/tools/ast-edit.md +18 -23
- package/src/prompts/tools/ast-grep.md +25 -32
- package/src/prompts/tools/bash.md +11 -23
- package/src/prompts/tools/debug.md +8 -22
- package/src/prompts/tools/find.md +0 -4
- package/src/prompts/tools/grep.md +3 -5
- package/src/prompts/tools/hashline.md +16 -10
- package/src/prompts/tools/python.md +10 -14
- package/src/prompts/tools/read.md +17 -24
- package/src/prompts/tools/task.md +57 -21
- package/src/prompts/tools/todo-write.md +45 -67
- package/src/session/agent-session.ts +4 -4
- package/src/session/session-manager.ts +15 -7
- package/src/session/streaming-output.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +3 -14
- package/src/task/executor.ts +13 -34
- package/src/task/index.ts +82 -18
- package/src/task/simple-mode.ts +27 -0
- package/src/task/template.ts +17 -3
- package/src/task/types.ts +77 -30
- package/src/tools/ask.ts +2 -4
- package/src/tools/ast-edit.ts +41 -17
- package/src/tools/ast-grep.ts +8 -27
- package/src/tools/bash-skill-urls.ts +9 -7
- package/src/tools/bash.ts +66 -24
- package/src/tools/browser.ts +1 -1
- package/src/tools/fetch.ts +1 -14
- package/src/tools/file-recorder.ts +35 -0
- package/src/tools/find.ts +25 -29
- package/src/tools/gh-format.ts +12 -0
- package/src/tools/gh-renderer.ts +1 -8
- package/src/tools/gh.ts +6 -13
- package/src/tools/grep.ts +103 -59
- package/src/tools/jtd-to-json-schema.ts +16 -0
- package/src/tools/match-line-format.ts +20 -0
- package/src/tools/path-utils.ts +61 -5
- package/src/tools/plan-mode-guard.ts +6 -5
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/tools/render-utils.ts +38 -6
- package/src/tools/renderers.ts +1 -0
- package/src/tools/resolve.ts +12 -3
- package/src/tools/ssh.ts +3 -11
- package/src/tools/submit-result.ts +1 -13
- package/src/tools/todo-write.ts +137 -103
- package/src/tools/vim.ts +1 -1
- package/src/tools/write.ts +2 -23
- package/src/tui/code-cell.ts +12 -7
- package/src/utils/edit-mode.ts +3 -2
- package/src/utils/git.ts +1 -1
- package/src/vim/engine.ts +41 -58
- package/src/web/scrapers/crates-io.ts +1 -14
- package/src/web/scrapers/types.ts +13 -0
- package/src/web/search/providers/base.ts +13 -0
- package/src/web/search/providers/brave.ts +2 -5
- package/src/web/search/providers/codex.ts +20 -24
- package/src/web/search/providers/gemini.ts +39 -1
- package/src/web/search/providers/jina.ts +2 -5
- package/src/web/search/providers/kagi.ts +3 -8
- package/src/web/search/providers/kimi.ts +3 -7
- package/src/web/search/providers/parallel.ts +3 -8
- package/src/web/search/providers/synthetic.ts +3 -7
- package/src/web/search/providers/tavily.ts +15 -11
- package/src/web/search/providers/utils.ts +36 -0
- package/src/web/search/providers/zai.ts +3 -7
package/src/tools/grep.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
6
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { type Static, Type } from "@sinclair/typebox";
|
|
9
|
-
import { computeLineHash } from "../edit/line-hash";
|
|
10
9
|
import { type ChunkedGrepMatch, describeChunkedGrepMatch } from "../edit/modes/chunk";
|
|
11
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
11
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
@@ -16,6 +15,8 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
|
|
|
16
15
|
import { resolveEditMode } from "../utils/edit-mode";
|
|
17
16
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
18
17
|
import type { ToolSession } from ".";
|
|
18
|
+
import { createFileRecorder } from "./file-recorder";
|
|
19
|
+
import { formatMatchLine } from "./match-line-format";
|
|
19
20
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
20
21
|
import {
|
|
21
22
|
combineSearchGlobs,
|
|
@@ -34,13 +35,13 @@ const grepSchema = Type.Object({
|
|
|
34
35
|
path: Type.Optional(Type.String({ description: "File or directory to search (default: cwd)" })),
|
|
35
36
|
glob: Type.Optional(Type.String({ description: "Filter files by glob pattern (e.g., '*.js')" })),
|
|
36
37
|
type: Type.Optional(Type.String({ description: "Filter by file type (e.g., js, py, rust)" })),
|
|
37
|
-
i: Type.Optional(Type.Boolean({ description: "Case-insensitive search
|
|
38
|
+
i: Type.Optional(Type.Boolean({ description: "Case-insensitive search", default: false })),
|
|
38
39
|
pre: Type.Optional(Type.Number({ description: "Lines of context before matches" })),
|
|
39
40
|
post: Type.Optional(Type.Number({ description: "Lines of context after matches" })),
|
|
40
41
|
multiline: Type.Optional(Type.Boolean({ description: "Enable multiline matching" })),
|
|
41
|
-
gitignore: Type.Optional(Type.Boolean({ description: "Respect .gitignore files during search
|
|
42
|
-
limit: Type.Optional(Type.Number({ description: "Limit output to first N matches
|
|
43
|
-
offset: Type.Optional(Type.Number({ description: "Skip first N entries before applying limit
|
|
42
|
+
gitignore: Type.Optional(Type.Boolean({ description: "Respect .gitignore files during search", default: true })),
|
|
43
|
+
limit: Type.Optional(Type.Number({ description: "Limit output to first N matches", default: 20 })),
|
|
44
|
+
offset: Type.Optional(Type.Number({ description: "Skip first N entries before applying limit", default: 0 })),
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
export type GrepToolInput = Static<typeof grepSchema>;
|
|
@@ -123,6 +124,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
123
124
|
};
|
|
124
125
|
let searchPath: string;
|
|
125
126
|
let scopePath: string;
|
|
127
|
+
let exactFilePaths: string[] | undefined;
|
|
126
128
|
let globFilter = glob ? normalizePathLikeInput(glob) || undefined : undefined;
|
|
127
129
|
const internalRouter = this.session.internalRouter;
|
|
128
130
|
if (searchDir?.trim()) {
|
|
@@ -141,7 +143,8 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
141
143
|
const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
|
|
142
144
|
if (multiSearchPath) {
|
|
143
145
|
searchPath = multiSearchPath.basePath;
|
|
144
|
-
globFilter = multiSearchPath.glob;
|
|
146
|
+
globFilter = multiSearchPath.exactFilePaths ? undefined : multiSearchPath.glob;
|
|
147
|
+
exactFilePaths = multiSearchPath.exactFilePaths;
|
|
145
148
|
scopePath = multiSearchPath.scopePath;
|
|
146
149
|
} else {
|
|
147
150
|
const parsedPath = parseSearchPath(rawPath);
|
|
@@ -173,26 +176,61 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
173
176
|
// Run grep
|
|
174
177
|
let result: GrepResult;
|
|
175
178
|
try {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
179
|
+
if (exactFilePaths) {
|
|
180
|
+
const matches: GrepMatch[] = [];
|
|
181
|
+
let limitReached = false;
|
|
182
|
+
for (const exactFilePath of exactFilePaths) {
|
|
183
|
+
const fileResult = await grep(
|
|
184
|
+
{
|
|
185
|
+
pattern: normalizedPattern,
|
|
186
|
+
path: exactFilePath,
|
|
187
|
+
type: type?.trim() || undefined,
|
|
188
|
+
ignoreCase,
|
|
189
|
+
multiline: effectiveMultiline,
|
|
190
|
+
hidden: true,
|
|
191
|
+
gitignore: useGitignore,
|
|
192
|
+
cache: false,
|
|
193
|
+
contextBefore: normalizedContextBefore,
|
|
194
|
+
contextAfter: normalizedContextAfter,
|
|
195
|
+
maxColumns: DEFAULT_MAX_COLUMN,
|
|
196
|
+
mode: effectiveOutputMode,
|
|
197
|
+
},
|
|
198
|
+
undefined,
|
|
199
|
+
);
|
|
200
|
+
limitReached = limitReached || Boolean(fileResult.limitReached);
|
|
201
|
+
const relativeFilePath = path.relative(searchPath, exactFilePath).replace(/\\/g, "/");
|
|
202
|
+
matches.push(...fileResult.matches.map(match => ({ ...match, path: relativeFilePath })));
|
|
203
|
+
}
|
|
204
|
+
const offsetMatches = matches.slice(normalizedOffset);
|
|
205
|
+
result = {
|
|
206
|
+
matches: offsetMatches,
|
|
207
|
+
totalMatches: offsetMatches.length,
|
|
208
|
+
filesWithMatches: new Set(offsetMatches.map(match => match.path)).size,
|
|
209
|
+
filesSearched: exactFilePaths.length,
|
|
210
|
+
limitReached,
|
|
211
|
+
};
|
|
212
|
+
} else {
|
|
213
|
+
result = await grep(
|
|
214
|
+
{
|
|
215
|
+
pattern: normalizedPattern,
|
|
216
|
+
path: searchPath,
|
|
217
|
+
glob: globFilter,
|
|
218
|
+
type: type?.trim() || undefined,
|
|
219
|
+
ignoreCase,
|
|
220
|
+
multiline: effectiveMultiline,
|
|
221
|
+
hidden: true,
|
|
222
|
+
gitignore: useGitignore,
|
|
223
|
+
cache: false,
|
|
224
|
+
maxCount: internalLimit,
|
|
225
|
+
offset: normalizedOffset > 0 ? normalizedOffset : undefined,
|
|
226
|
+
contextBefore: normalizedContextBefore,
|
|
227
|
+
contextAfter: normalizedContextAfter,
|
|
228
|
+
maxColumns: DEFAULT_MAX_COLUMN,
|
|
229
|
+
mode: effectiveOutputMode,
|
|
230
|
+
},
|
|
231
|
+
undefined,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
196
234
|
} catch (err) {
|
|
197
235
|
if (err instanceof Error && err.message.startsWith("regex parse error")) {
|
|
198
236
|
throw new ToolError(err.message);
|
|
@@ -243,15 +281,8 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
243
281
|
? roundRobinSelect(result.matches, effectiveLimit)
|
|
244
282
|
: result.matches.slice(0, effectiveLimit);
|
|
245
283
|
const matchLimitReached = result.matches.length > effectiveLimit;
|
|
246
|
-
const
|
|
247
|
-
const fileList: string[] = [];
|
|
284
|
+
const { record: recordFile, list: fileList } = createFileRecorder();
|
|
248
285
|
const fileMatchCounts = new Map<string, number>();
|
|
249
|
-
const recordFile = (relativePath: string) => {
|
|
250
|
-
if (!files.has(relativePath)) {
|
|
251
|
-
files.add(relativePath);
|
|
252
|
-
fileList.push(relativePath);
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
286
|
if (selectedMatches.length === 0) {
|
|
256
287
|
const details: GrepToolDetails = {
|
|
257
288
|
scopePath,
|
|
@@ -264,6 +295,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
264
295
|
}
|
|
265
296
|
const outputLines: string[] = [];
|
|
266
297
|
let linesTruncated = false;
|
|
298
|
+
const hasContextLines = normalizedContextBefore > 0 || normalizedContextAfter > 0;
|
|
267
299
|
const matchesByFile = new Map<string, GrepMatch[]>();
|
|
268
300
|
for (const match of selectedMatches) {
|
|
269
301
|
const relativePath = formatPath(match.path);
|
|
@@ -295,10 +327,11 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
295
327
|
}
|
|
296
328
|
chunkMatchesByFile.get(match.displayPath)!.push(match);
|
|
297
329
|
}
|
|
298
|
-
const renderChunkedMatchesForFile = (relativePath: string) => {
|
|
330
|
+
const renderChunkedMatchesForFile = (relativePath: string): string[] => {
|
|
331
|
+
const renderedLines: string[] = [];
|
|
299
332
|
const fileMatches = chunkMatchesByFile.get(relativePath) ?? [];
|
|
300
333
|
if (fileMatches.length === 0) {
|
|
301
|
-
return;
|
|
334
|
+
return renderedLines;
|
|
302
335
|
}
|
|
303
336
|
const lineWidth = fileMatches[0]?.fileLineCount.toString().length ?? 1;
|
|
304
337
|
const matchesByChunk = new Map<string, ChunkedGrepMatch[]>();
|
|
@@ -316,13 +349,14 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
316
349
|
const anchor = chunkChecksum
|
|
317
350
|
? `${dashes}@${chunkPath}#${chunkChecksum}`
|
|
318
351
|
: `${dashes}@${chunkPath}`;
|
|
319
|
-
|
|
352
|
+
renderedLines.push(anchor);
|
|
320
353
|
}
|
|
321
354
|
for (const match of chunkMatches) {
|
|
322
|
-
|
|
355
|
+
renderedLines.push(` ${match.lineNumber.toString().padStart(lineWidth, " ")} |${match.line}`);
|
|
323
356
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
324
357
|
}
|
|
325
358
|
}
|
|
359
|
+
return renderedLines;
|
|
326
360
|
};
|
|
327
361
|
if (isDirectory) {
|
|
328
362
|
const filesByDirectory = new Map<string, string[]>();
|
|
@@ -336,26 +370,32 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
336
370
|
for (const [directory, directoryFiles] of filesByDirectory) {
|
|
337
371
|
if (directory === ".") {
|
|
338
372
|
for (const relativePath of directoryFiles) {
|
|
373
|
+
const renderedLines = renderChunkedMatchesForFile(relativePath);
|
|
374
|
+
if (renderedLines.length === 0) continue;
|
|
339
375
|
if (outputLines.length > 0) {
|
|
340
376
|
outputLines.push("");
|
|
341
377
|
}
|
|
342
378
|
outputLines.push(`# ${path.basename(relativePath)}`);
|
|
343
|
-
|
|
379
|
+
outputLines.push(...renderedLines);
|
|
344
380
|
}
|
|
345
381
|
continue;
|
|
346
382
|
}
|
|
383
|
+
const renderedFiles = directoryFiles
|
|
384
|
+
.map(relativePath => ({ relativePath, lines: renderChunkedMatchesForFile(relativePath) }))
|
|
385
|
+
.filter(file => file.lines.length > 0);
|
|
386
|
+
if (renderedFiles.length === 0) continue;
|
|
347
387
|
if (outputLines.length > 0) {
|
|
348
388
|
outputLines.push("");
|
|
349
389
|
}
|
|
350
390
|
outputLines.push(`# ${directory}`);
|
|
351
|
-
for (const relativePath of
|
|
391
|
+
for (const { relativePath, lines } of renderedFiles) {
|
|
352
392
|
outputLines.push(`## └─ ${path.basename(relativePath)}`);
|
|
353
|
-
|
|
393
|
+
outputLines.push(...lines);
|
|
354
394
|
}
|
|
355
395
|
}
|
|
356
396
|
} else {
|
|
357
397
|
for (const relativePath of fileList) {
|
|
358
|
-
renderChunkedMatchesForFile(relativePath);
|
|
398
|
+
outputLines.push(...renderChunkedMatchesForFile(relativePath));
|
|
359
399
|
}
|
|
360
400
|
}
|
|
361
401
|
const rawOutput = outputLines.join("\n");
|
|
@@ -386,7 +426,8 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
386
426
|
}
|
|
387
427
|
return resultBuilder.done();
|
|
388
428
|
}
|
|
389
|
-
const renderMatchesForFile = (relativePath: string) => {
|
|
429
|
+
const renderMatchesForFile = (relativePath: string): string[] => {
|
|
430
|
+
const renderedLines: string[] = [];
|
|
390
431
|
const fileMatches = matchesByFile.get(relativePath) ?? [];
|
|
391
432
|
for (const match of fileMatches) {
|
|
392
433
|
const lineNumbers: number[] = [match.lineNumber];
|
|
@@ -401,31 +442,25 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
401
442
|
}
|
|
402
443
|
}
|
|
403
444
|
const lineWidth = Math.max(...lineNumbers.map(value => value.toString().length));
|
|
404
|
-
const formatLine = (lineNumber: number, line: string, isMatch: boolean): string =>
|
|
405
|
-
|
|
406
|
-
if (useHashLines) {
|
|
407
|
-
const ref = `${lineNumber}#${computeLineHash(lineNumber, line)}`;
|
|
408
|
-
return `${ref}${separator}${line}`;
|
|
409
|
-
}
|
|
410
|
-
const padded = lineNumber.toString().padStart(lineWidth, " ");
|
|
411
|
-
return `${padded}${separator}${line}`;
|
|
412
|
-
};
|
|
445
|
+
const formatLine = (lineNumber: number, line: string, isMatch: boolean): string =>
|
|
446
|
+
formatMatchLine(lineNumber, line, isMatch, { useHashLines, lineWidth });
|
|
413
447
|
if (match.contextBefore) {
|
|
414
448
|
for (const ctx of match.contextBefore) {
|
|
415
|
-
|
|
449
|
+
renderedLines.push(formatLine(ctx.lineNumber, ctx.line, false));
|
|
416
450
|
}
|
|
417
451
|
}
|
|
418
|
-
|
|
452
|
+
renderedLines.push(formatLine(match.lineNumber, match.line, true));
|
|
419
453
|
if (match.truncated) {
|
|
420
454
|
linesTruncated = true;
|
|
421
455
|
}
|
|
422
456
|
if (match.contextAfter) {
|
|
423
457
|
for (const ctx of match.contextAfter) {
|
|
424
|
-
|
|
458
|
+
renderedLines.push(formatLine(ctx.lineNumber, ctx.line, false));
|
|
425
459
|
}
|
|
426
460
|
}
|
|
427
461
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
428
462
|
}
|
|
463
|
+
return renderedLines;
|
|
429
464
|
};
|
|
430
465
|
if (isDirectory) {
|
|
431
466
|
const filesByDirectory = new Map<string, string[]>();
|
|
@@ -439,28 +474,37 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
439
474
|
for (const [directory, directoryFiles] of filesByDirectory) {
|
|
440
475
|
if (directory === ".") {
|
|
441
476
|
for (const relativePath of directoryFiles) {
|
|
477
|
+
const renderedLines = renderMatchesForFile(relativePath);
|
|
478
|
+
if (renderedLines.length === 0) continue;
|
|
442
479
|
if (outputLines.length > 0) {
|
|
443
480
|
outputLines.push("");
|
|
444
481
|
}
|
|
445
482
|
outputLines.push(`# ${path.basename(relativePath)}`);
|
|
446
|
-
|
|
483
|
+
outputLines.push(...renderedLines);
|
|
447
484
|
}
|
|
448
485
|
continue;
|
|
449
486
|
}
|
|
487
|
+
const renderedFiles = directoryFiles
|
|
488
|
+
.map(relativePath => ({ relativePath, lines: renderMatchesForFile(relativePath) }))
|
|
489
|
+
.filter(file => file.lines.length > 0);
|
|
490
|
+
if (renderedFiles.length === 0) continue;
|
|
450
491
|
if (outputLines.length > 0) {
|
|
451
492
|
outputLines.push("");
|
|
452
493
|
}
|
|
453
494
|
outputLines.push(`# ${directory}`);
|
|
454
|
-
for (const relativePath of
|
|
495
|
+
for (const { relativePath, lines } of renderedFiles) {
|
|
455
496
|
outputLines.push(`## └─ ${path.basename(relativePath)}`);
|
|
456
|
-
|
|
497
|
+
outputLines.push(...lines);
|
|
457
498
|
}
|
|
458
499
|
}
|
|
459
500
|
} else {
|
|
460
501
|
for (const relativePath of fileList) {
|
|
461
|
-
renderMatchesForFile(relativePath);
|
|
502
|
+
outputLines.push(...renderMatchesForFile(relativePath));
|
|
462
503
|
}
|
|
463
504
|
}
|
|
505
|
+
if (hasContextLines && outputLines.length > 0) {
|
|
506
|
+
outputLines.unshift("[grep] match lines use ':'; context lines use '-'.");
|
|
507
|
+
}
|
|
464
508
|
const rawOutput = outputLines.join("\n");
|
|
465
509
|
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
466
510
|
const output = truncation.content;
|
|
@@ -197,3 +197,19 @@ function normalizeMixedSchemaNode(schema: unknown): unknown {
|
|
|
197
197
|
export function jtdToJsonSchema(schema: unknown): unknown {
|
|
198
198
|
return normalizeMixedSchemaNode(schema);
|
|
199
199
|
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Normalize a schema input that may be a JSON string, object, or null/undefined.
|
|
203
|
+
* Returns { normalized } on success, or { error } if JSON parsing fails.
|
|
204
|
+
*/
|
|
205
|
+
export function normalizeSchema(schema: unknown): { normalized?: unknown; error?: string } {
|
|
206
|
+
if (schema === undefined || schema === null) return {};
|
|
207
|
+
if (typeof schema === "string") {
|
|
208
|
+
try {
|
|
209
|
+
return { normalized: JSON.parse(schema) };
|
|
210
|
+
} catch (err) {
|
|
211
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { normalized: schema };
|
|
215
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { computeLineHash } from "../edit/line-hash";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Format a single line of match output for grep/ast-grep style results.
|
|
5
|
+
* Uses hashline refs when hashlines are enabled, otherwise pads the number.
|
|
6
|
+
*/
|
|
7
|
+
export function formatMatchLine(
|
|
8
|
+
lineNumber: number,
|
|
9
|
+
line: string,
|
|
10
|
+
isMatch: boolean,
|
|
11
|
+
options: { useHashLines: boolean; lineWidth: number },
|
|
12
|
+
): string {
|
|
13
|
+
const separator = isMatch ? ":" : "-";
|
|
14
|
+
if (options.useHashLines) {
|
|
15
|
+
const ref = `${lineNumber}#${computeLineHash(lineNumber, line)}`;
|
|
16
|
+
return `${ref}${separator}${line}`;
|
|
17
|
+
}
|
|
18
|
+
const padded = lineNumber.toString().padStart(options.lineWidth, " ");
|
|
19
|
+
return `${padded}${separator}${line}`;
|
|
20
|
+
}
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -74,7 +74,7 @@ function normalizeAtPrefix(filePath: string): string {
|
|
|
74
74
|
withoutAt.startsWith("artifact://") ||
|
|
75
75
|
withoutAt.startsWith("skill://") ||
|
|
76
76
|
withoutAt.startsWith("rule://") ||
|
|
77
|
-
withoutAt.startsWith("local
|
|
77
|
+
withoutAt.startsWith("local:") ||
|
|
78
78
|
withoutAt.startsWith("mcp://")
|
|
79
79
|
) {
|
|
80
80
|
return withoutAt;
|
|
@@ -110,6 +110,29 @@ export function expandPath(filePath: string): string {
|
|
|
110
110
|
return expandTilde(normalized);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
function assertNotInternalUrl(expanded: string, original: string): void {
|
|
114
|
+
for (const prefix of TOP_LEVEL_INTERNAL_URL_PREFIXES) {
|
|
115
|
+
if (expanded.startsWith(prefix)) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Path "${original}" uses internal scheme "${prefix}" and must be resolved through the proper protocol handler, not as a filesystem path.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function normalizeLocalScheme(filePath: string): string {
|
|
124
|
+
return filePath.replace(/^(local:)\/(?!\/)/, "$1//");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function isInternalUrlPath(filePath: string): boolean {
|
|
128
|
+
const normalized = normalizeLocalScheme(filePath);
|
|
129
|
+
const expandedAndNormalized = normalizeLocalScheme(expandPath(normalized));
|
|
130
|
+
for (const prefix of TOP_LEVEL_INTERNAL_URL_PREFIXES) {
|
|
131
|
+
if (expandedAndNormalized.startsWith(prefix)) return true;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
113
136
|
/**
|
|
114
137
|
* Resolve a path relative to the given cwd.
|
|
115
138
|
* Handles ~ expansion and absolute paths.
|
|
@@ -119,7 +142,12 @@ export function expandPath(filePath: string): string {
|
|
|
119
142
|
* filesystem root is almost never what they intended.
|
|
120
143
|
*/
|
|
121
144
|
export function resolveToCwd(filePath: string, cwd: string): string {
|
|
122
|
-
const
|
|
145
|
+
const normalized = normalizeLocalScheme(filePath);
|
|
146
|
+
const expanded = expandPath(normalized);
|
|
147
|
+
const expandedAndNormalized = normalizeLocalScheme(expanded);
|
|
148
|
+
|
|
149
|
+
assertNotInternalUrl(expandedAndNormalized, normalized);
|
|
150
|
+
|
|
123
151
|
if (/^\/+$/.test(expanded)) {
|
|
124
152
|
return cwd;
|
|
125
153
|
}
|
|
@@ -165,6 +193,7 @@ export interface ResolvedMultiSearchPath {
|
|
|
165
193
|
basePath: string;
|
|
166
194
|
glob?: string;
|
|
167
195
|
scopePath: string;
|
|
196
|
+
exactFilePaths?: string[];
|
|
168
197
|
}
|
|
169
198
|
|
|
170
199
|
export interface ResolvedMultiFindPattern {
|
|
@@ -410,6 +439,28 @@ async function areDelimitedTokensResolvable(
|
|
|
410
439
|
return true;
|
|
411
440
|
}
|
|
412
441
|
|
|
442
|
+
async function filterResolvableTokens(
|
|
443
|
+
tokens: string[],
|
|
444
|
+
cwd: string,
|
|
445
|
+
parseBasePath: (value: string) => string,
|
|
446
|
+
): Promise<string[]> {
|
|
447
|
+
const out: string[] = [];
|
|
448
|
+
for (const token of tokens) {
|
|
449
|
+
if (TOP_LEVEL_INTERNAL_URL_PREFIXES.some(prefix => token.startsWith(prefix))) continue;
|
|
450
|
+
const basePath = parseBasePath(token);
|
|
451
|
+
const resolvedBasePath = resolveToCwd(basePath, cwd);
|
|
452
|
+
if (await pathExists(resolvedBasePath)) {
|
|
453
|
+
out.push(token);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const resolvedExactPath = resolveToCwd(token, cwd);
|
|
457
|
+
if (await pathExists(resolvedExactPath)) {
|
|
458
|
+
out.push(token);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
|
|
413
464
|
async function splitDelimitedSearchInput(
|
|
414
465
|
rawInput: string,
|
|
415
466
|
cwd: string,
|
|
@@ -424,8 +475,11 @@ async function splitDelimitedSearchInput(
|
|
|
424
475
|
}
|
|
425
476
|
|
|
426
477
|
const commaSeparated = splitTopLevel(trimmed, "comma");
|
|
427
|
-
if (commaSeparated.length > 1
|
|
428
|
-
|
|
478
|
+
if (commaSeparated.length > 1) {
|
|
479
|
+
const resolvable = await filterResolvableTokens(commaSeparated, cwd, parseBasePath);
|
|
480
|
+
if (resolvable.length >= 1) {
|
|
481
|
+
return [...new Set(resolvable)];
|
|
482
|
+
}
|
|
429
483
|
}
|
|
430
484
|
|
|
431
485
|
const whitespaceSeparated = splitTopLevel(trimmed, "whitespace");
|
|
@@ -445,7 +499,7 @@ export async function resolveMultiSearchPath(
|
|
|
445
499
|
suffixGlob?: string,
|
|
446
500
|
): Promise<ResolvedMultiSearchPath | undefined> {
|
|
447
501
|
const pathItems = await splitDelimitedSearchInput(rawPath, cwd, value => parseSearchPath(value).basePath);
|
|
448
|
-
if (!pathItems || pathItems.length
|
|
502
|
+
if (!pathItems || pathItems.length < 1) {
|
|
449
503
|
return undefined;
|
|
450
504
|
}
|
|
451
505
|
|
|
@@ -458,6 +512,7 @@ export async function resolveMultiSearchPath(
|
|
|
458
512
|
}),
|
|
459
513
|
);
|
|
460
514
|
|
|
515
|
+
const allExactFiles = !suffixGlob && parsedItems.every(item => !item.parsedPath.glob && item.stat.isFile());
|
|
461
516
|
const commonBasePath = findCommonBasePath(parsedItems.map(item => item.absoluteBasePath));
|
|
462
517
|
const combinedPatterns = parsedItems.map(item => {
|
|
463
518
|
const relativeBasePath = normalizePosixPath(path.relative(commonBasePath, item.absoluteBasePath)) || ".";
|
|
@@ -479,6 +534,7 @@ export async function resolveMultiSearchPath(
|
|
|
479
534
|
basePath: commonBasePath,
|
|
480
535
|
glob: buildBraceUnion(combinedPatterns),
|
|
481
536
|
scopePath: toScopeDisplay(pathItems),
|
|
537
|
+
exactFilePaths: allExactFiles ? parsedItems.map(item => item.absoluteBasePath) : undefined,
|
|
482
538
|
};
|
|
483
539
|
}
|
|
484
540
|
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
2
2
|
import type { ToolSession } from ".";
|
|
3
|
-
import { resolveToCwd } from "./path-utils";
|
|
3
|
+
import { normalizeLocalScheme, resolveToCwd } from "./path-utils";
|
|
4
4
|
import { ToolError } from "./tool-errors";
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const LOCAL_SCHEME_PREFIX = "local:";
|
|
7
7
|
|
|
8
8
|
export function resolvePlanPath(session: ToolSession, targetPath: string): string {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const normalized = normalizeLocalScheme(targetPath);
|
|
10
|
+
if (normalized.startsWith(LOCAL_SCHEME_PREFIX)) {
|
|
11
|
+
return resolveLocalUrlToPath(normalized, {
|
|
11
12
|
getArtifactsDir: session.getArtifactsDir,
|
|
12
13
|
getSessionId: session.getSessionId,
|
|
13
14
|
});
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
return resolveToCwd(
|
|
17
|
+
return resolveToCwd(normalized, session.cwd);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function enforcePlanModeWrite(
|
package/src/tools/python.ts
CHANGED
|
@@ -52,7 +52,7 @@ export const pythonSchema = Type.Object({
|
|
|
52
52
|
}),
|
|
53
53
|
{ description: "Cells to execute sequentially in persistent kernel" },
|
|
54
54
|
),
|
|
55
|
-
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds
|
|
55
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds", default: 30 })),
|
|
56
56
|
cwd: Type.Optional(Type.String({ description: "Working directory (default: cwd)" })),
|
|
57
57
|
reset: Type.Optional(Type.Boolean({ description: "Restart kernel before execution" })),
|
|
58
58
|
});
|
package/src/tools/read.ts
CHANGED
|
@@ -371,7 +371,7 @@ function prependSuffixResolutionNotice(text: string, suffixResolution?: { from:
|
|
|
371
371
|
const readSchema = Type.Object({
|
|
372
372
|
path: Type.String({ description: "Path or URL to read" }),
|
|
373
373
|
sel: Type.Optional(Type.String({ description: "Selector: chunk path, L10-L50, or raw" })),
|
|
374
|
-
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds
|
|
374
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds", default: 20 })),
|
|
375
375
|
});
|
|
376
376
|
|
|
377
377
|
export type ReadToolInput = Static<typeof readSchema>;
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Provides consistent formatting, truncation, and display patterns across all
|
|
5
5
|
* tool renderers to ensure a unified TUI experience.
|
|
6
6
|
*/
|
|
7
|
+
|
|
7
8
|
import * as os from "node:os";
|
|
8
9
|
import * as path from "node:path";
|
|
10
|
+
import type { ToolCallContext } from "@oh-my-pi/pi-agent-core";
|
|
9
11
|
import type { Ellipsis } from "@oh-my-pi/pi-natives";
|
|
10
12
|
import { replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
11
13
|
import { pluralize } from "@oh-my-pi/pi-utils";
|
|
@@ -210,6 +212,10 @@ interface ParsedDiagnostic {
|
|
|
210
212
|
code?: string;
|
|
211
213
|
}
|
|
212
214
|
|
|
215
|
+
function sanitizeDiagnosticDisplayText(text: string): string {
|
|
216
|
+
return replaceTabs(text);
|
|
217
|
+
}
|
|
218
|
+
|
|
213
219
|
function getSeverityRank(severity: ParsedDiagnostic["severity"]): number {
|
|
214
220
|
switch (severity) {
|
|
215
221
|
case "error":
|
|
@@ -227,13 +233,13 @@ function parseDiagnosticMessage(msg: string): ParsedDiagnostic | null {
|
|
|
227
233
|
const match = msg.match(/^(.+?):(\d+):(\d+)\s+\[(\w+)\]\s+(?:\[([^\]]+)\]\s+)?(.+?)(?:\s+\(([^)]+)\))?$/);
|
|
228
234
|
if (!match) return null;
|
|
229
235
|
return {
|
|
230
|
-
filePath: match[1],
|
|
236
|
+
filePath: sanitizeDiagnosticDisplayText(match[1]),
|
|
231
237
|
line: parseInt(match[2], 10),
|
|
232
238
|
col: parseInt(match[3], 10),
|
|
233
239
|
severity: match[4] as ParsedDiagnostic["severity"],
|
|
234
|
-
source: match[5],
|
|
235
|
-
message: match[6],
|
|
236
|
-
code: match[7],
|
|
240
|
+
source: match[5] ? sanitizeDiagnosticDisplayText(match[5]) : undefined,
|
|
241
|
+
message: sanitizeDiagnosticDisplayText(match[6]),
|
|
242
|
+
code: match[7] ? sanitizeDiagnosticDisplayText(match[7]) : undefined,
|
|
237
243
|
};
|
|
238
244
|
}
|
|
239
245
|
|
|
@@ -255,7 +261,7 @@ export function formatDiagnostics(
|
|
|
255
261
|
existing.push(parsed);
|
|
256
262
|
byFile.set(parsed.filePath, existing);
|
|
257
263
|
} else {
|
|
258
|
-
unparsed.push(msg);
|
|
264
|
+
unparsed.push(sanitizeDiagnosticDisplayText(msg));
|
|
259
265
|
}
|
|
260
266
|
}
|
|
261
267
|
|
|
@@ -272,7 +278,8 @@ export function formatDiagnostics(
|
|
|
272
278
|
const headerIcon = diag.errored
|
|
273
279
|
? theme.styledSymbol("status.error", "error")
|
|
274
280
|
: theme.styledSymbol("status.warning", "warning");
|
|
275
|
-
|
|
281
|
+
const summary = sanitizeDiagnosticDisplayText(diag.summary);
|
|
282
|
+
let output = `\n\n${headerIcon} ${theme.fg("toolTitle", "Diagnostics")} ${theme.fg("dim", `(${summary})`)}`;
|
|
276
283
|
|
|
277
284
|
const maxDiags = expanded ? diag.messages.length : 5;
|
|
278
285
|
let diagsShown = 0;
|
|
@@ -616,3 +623,28 @@ export function formatParseErrors(errors: string[]): string[] {
|
|
|
616
623
|
: "Parse issues:";
|
|
617
624
|
return [header, ...capped.map(err => `- ${err}`)];
|
|
618
625
|
}
|
|
626
|
+
|
|
627
|
+
// =============================================================================
|
|
628
|
+
// LSP Batching
|
|
629
|
+
// =============================================================================
|
|
630
|
+
|
|
631
|
+
const LSP_BATCH_TOOLS = new Set(["edit", "write"]);
|
|
632
|
+
|
|
633
|
+
export interface LspBatchRequest {
|
|
634
|
+
id: string;
|
|
635
|
+
flush: boolean;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export function getLspBatchRequest(toolCall: ToolCallContext | undefined): LspBatchRequest | undefined {
|
|
639
|
+
if (!toolCall) {
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
642
|
+
const hasOtherWrites = toolCall.toolCalls.some(
|
|
643
|
+
(call, index) => index !== toolCall.index && LSP_BATCH_TOOLS.has(call.name),
|
|
644
|
+
);
|
|
645
|
+
if (!hasOtherWrites) {
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
const hasLaterWrites = toolCall.toolCalls.slice(toolCall.index + 1).some(call => LSP_BATCH_TOOLS.has(call.name));
|
|
649
|
+
return { id: toolCall.batchId, flush: !hasLaterWrites };
|
|
650
|
+
}
|
package/src/tools/renderers.ts
CHANGED
|
@@ -51,6 +51,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
51
51
|
python: pythonToolRenderer as ToolRenderer,
|
|
52
52
|
calc: calculatorToolRenderer as ToolRenderer,
|
|
53
53
|
edit: editToolRenderer as ToolRenderer,
|
|
54
|
+
apply_patch: editToolRenderer as ToolRenderer,
|
|
54
55
|
find: findToolRenderer as ToolRenderer,
|
|
55
56
|
grep: grepToolRenderer as ToolRenderer,
|
|
56
57
|
lsp: lspToolRenderer as ToolRenderer,
|
package/src/tools/resolve.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface ResolveToolDetails {
|
|
|
23
23
|
reason: string;
|
|
24
24
|
sourceToolName?: string;
|
|
25
25
|
label?: string;
|
|
26
|
+
sourceResultDetails?: unknown;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function resolveReasonPreview(reason?: string): string | undefined {
|
|
@@ -67,14 +68,21 @@ export function queueResolveHandler(
|
|
|
67
68
|
onRejected: () => "requeue",
|
|
68
69
|
onInvoked: async (input: unknown) => {
|
|
69
70
|
const params = input as ResolveParams;
|
|
71
|
+
const withResolveDetails = (result: AgentToolResult<unknown>): AgentToolResult<ResolveToolDetails> => ({
|
|
72
|
+
...result,
|
|
73
|
+
details: {
|
|
74
|
+
...detailsFor(params),
|
|
75
|
+
...(result.details != null ? { sourceResultDetails: result.details } : {}),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
70
78
|
if (params.action === "apply") {
|
|
71
79
|
const result = await options.apply(params.reason);
|
|
72
|
-
return
|
|
80
|
+
return withResolveDetails(result);
|
|
73
81
|
}
|
|
74
82
|
if (params.action === "discard" && options.reject != null) {
|
|
75
83
|
const result = await options.reject(params.reason);
|
|
76
84
|
if (result != null) {
|
|
77
|
-
return
|
|
85
|
+
return withResolveDetails(result);
|
|
78
86
|
}
|
|
79
87
|
}
|
|
80
88
|
return {
|
|
@@ -154,9 +162,10 @@ export const resolveToolRenderer = {
|
|
|
154
162
|
const reason = replaceTabs(details?.reason?.trim() || "No reason provided");
|
|
155
163
|
const action = details?.action ?? "apply";
|
|
156
164
|
const isApply = action === "apply" && !result.isError;
|
|
165
|
+
const isFailedApply = action === "apply" && result.isError;
|
|
157
166
|
const bgColor = result.isError ? "error" : isApply ? "success" : "warning";
|
|
158
167
|
const icon = isApply ? uiTheme.status.success : uiTheme.status.error;
|
|
159
|
-
const verb = isApply ? "Accept" : "Discard";
|
|
168
|
+
const verb = isApply ? "Accept" : isFailedApply ? "Failed" : "Discard";
|
|
160
169
|
const separator = ": ";
|
|
161
170
|
const separatorIndex = label.indexOf(separator);
|
|
162
171
|
const sourceLabel = separatorIndex > 0 ? label.slice(0, separatorIndex).trim() : undefined;
|