@oh-my-pi/pi-coding-agent 14.3.0 → 14.4.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 +98 -1
- package/package.json +7 -7
- package/src/autoresearch/prompt.md +1 -1
- package/src/commit/agentic/prompts/analyze-file.md +1 -1
- package/src/config/model-registry.ts +67 -15
- package/src/config/prompt-templates.ts +5 -5
- package/src/config/settings-schema.ts +4 -4
- package/src/cursor.ts +3 -8
- package/src/discovery/helpers.ts +3 -3
- package/src/edit/diff.ts +50 -47
- package/src/edit/index.ts +86 -57
- package/src/edit/line-hash.ts +743 -24
- package/src/edit/modes/apply-patch.ts +0 -9
- package/src/edit/modes/atom.ts +893 -0
- package/src/edit/modes/chunk.ts +14 -24
- package/src/edit/modes/hashline.ts +193 -146
- package/src/edit/modes/patch.ts +5 -9
- package/src/edit/modes/replace.ts +6 -11
- package/src/edit/renderer.ts +14 -10
- package/src/edit/streaming.ts +50 -16
- package/src/exec/bash-executor.ts +2 -4
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +4 -12
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/defaults.json +142 -652
- package/src/lsp/index.ts +1 -1
- package/src/mcp/render.ts +1 -8
- package/src/modes/components/assistant-message.ts +4 -0
- package/src/modes/components/diff.ts +23 -14
- package/src/modes/components/footer.ts +21 -16
- package/src/modes/components/session-selector.ts +3 -3
- package/src/modes/components/settings-defs.ts +6 -1
- package/src/modes/components/todo-reminder.ts +1 -8
- package/src/modes/components/tool-execution.ts +1 -4
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/print-mode.ts +8 -0
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/reviewer.md +4 -4
- package/src/prompts/ci-green-request.md +1 -1
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +3 -3
- package/src/prompts/system/subagent-yield-reminder.md +11 -0
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/tools/ask.md +3 -2
- package/src/prompts/tools/ast-edit.md +16 -20
- package/src/prompts/tools/ast-grep.md +19 -24
- package/src/prompts/tools/atom.md +87 -0
- package/src/prompts/tools/chunk-edit.md +37 -161
- package/src/prompts/tools/debug.md +4 -5
- package/src/prompts/tools/exit-plan-mode.md +4 -5
- package/src/prompts/tools/find.md +4 -8
- package/src/prompts/tools/github.md +18 -0
- package/src/prompts/tools/grep.md +4 -5
- package/src/prompts/tools/hashline.md +22 -89
- package/src/prompts/tools/{gemini-image.md → image-gen.md} +1 -1
- package/src/prompts/tools/inspect-image.md +6 -6
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/patch.md +12 -19
- package/src/prompts/tools/python.md +3 -2
- package/src/prompts/tools/read-chunk.md +2 -3
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/ssh.md +8 -17
- package/src/prompts/tools/todo-write.md +54 -41
- package/src/sdk.ts +14 -9
- package/src/session/agent-session.ts +25 -2
- package/src/session/session-manager.ts +4 -1
- package/src/task/executor.ts +43 -48
- package/src/task/render.ts +11 -13
- package/src/tools/ask.ts +7 -7
- package/src/tools/ast-edit.ts +45 -41
- package/src/tools/ast-grep.ts +77 -85
- package/src/tools/bash.ts +8 -9
- package/src/tools/browser.ts +32 -30
- package/src/tools/calculator.ts +4 -4
- package/src/tools/cancel-job.ts +1 -1
- package/src/tools/checkpoint.ts +2 -2
- package/src/tools/debug.ts +41 -37
- package/src/tools/exit-plan-mode.ts +1 -1
- package/src/tools/find.ts +4 -4
- package/src/tools/gh-renderer.ts +12 -4
- package/src/tools/gh.ts +509 -697
- package/src/tools/grep.ts +116 -131
- package/src/tools/{gemini-image.ts → image-gen.ts} +459 -60
- package/src/tools/index.ts +14 -32
- package/src/tools/inspect-image.ts +3 -3
- package/src/tools/json-tree.ts +114 -114
- package/src/tools/match-line-format.ts +8 -7
- package/src/tools/notebook.ts +8 -7
- package/src/tools/poll-tool.ts +2 -1
- package/src/tools/python.ts +9 -23
- package/src/tools/read.ts +32 -25
- package/src/tools/render-mermaid.ts +1 -1
- package/src/tools/render-utils.ts +18 -0
- package/src/tools/renderers.ts +2 -2
- package/src/tools/report-tool-issue.ts +3 -2
- package/src/tools/resolve.ts +1 -1
- package/src/tools/review.ts +12 -10
- package/src/tools/search-tool-bm25.ts +2 -4
- package/src/tools/ssh.ts +4 -4
- package/src/tools/todo-write.ts +172 -147
- package/src/tools/vim.ts +14 -15
- package/src/tools/write.ts +4 -4
- package/src/tools/{submit-result.ts → yield.ts} +11 -13
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/file-display-mode.ts +10 -5
- package/src/utils/git.ts +9 -5
- package/src/utils/shell-snapshot.ts +2 -3
- package/src/vim/render.ts +4 -4
- package/src/prompts/system/subagent-submit-reminder.md +0 -11
- package/src/prompts/tools/gh-issue-view.md +0 -11
- package/src/prompts/tools/gh-pr-checkout.md +0 -12
- package/src/prompts/tools/gh-pr-diff.md +0 -12
- package/src/prompts/tools/gh-pr-push.md +0 -12
- package/src/prompts/tools/gh-pr-view.md +0 -11
- package/src/prompts/tools/gh-repo-view.md +0 -11
- package/src/prompts/tools/gh-run-watch.md +0 -12
- package/src/prompts/tools/gh-search-issues.md +0 -11
- package/src/prompts/tools/gh-search-prs.md +0 -11
package/src/task/render.ts
CHANGED
|
@@ -101,12 +101,12 @@ function formatTaskId(id: string): string {
|
|
|
101
101
|
return `${indices} ${labels}`;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const
|
|
104
|
+
const MISSING_YIELD_WARNING_PREFIX = "SYSTEM WARNING: Subagent exited without calling yield tool";
|
|
105
105
|
|
|
106
|
-
function
|
|
106
|
+
function extractMissingYieldWarning(output: string): { warning?: string; rest: string } {
|
|
107
107
|
const lines = output.split("\n");
|
|
108
108
|
const firstLine = lines[0]?.trim() ?? "";
|
|
109
|
-
if (!firstLine.startsWith(
|
|
109
|
+
if (!firstLine.startsWith(MISSING_YIELD_WARNING_PREFIX)) {
|
|
110
110
|
return { rest: output };
|
|
111
111
|
}
|
|
112
112
|
const rest = lines
|
|
@@ -572,9 +572,9 @@ function renderAgentProgress(
|
|
|
572
572
|
|
|
573
573
|
// Render extracted tool data inline (e.g., review findings)
|
|
574
574
|
if (progress.extractedToolData) {
|
|
575
|
-
// For completed tasks, check for review verdict from
|
|
575
|
+
// For completed tasks, check for review verdict from yield tool
|
|
576
576
|
if (progress.status === "completed") {
|
|
577
|
-
const completeData = progress.extractedToolData.
|
|
577
|
+
const completeData = progress.extractedToolData.yield as Array<{ data: unknown }> | undefined;
|
|
578
578
|
const reportFindingData = normalizeReportFindings(progress.extractedToolData.report_finding);
|
|
579
579
|
const reviewData = completeData
|
|
580
580
|
?.map(c => c.data as SubmitReviewDetails)
|
|
@@ -731,9 +731,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
731
731
|
const prefix = isLast ? theme.fg("dim", theme.tree.last) : theme.fg("dim", theme.tree.branch);
|
|
732
732
|
const continuePrefix = isLast ? " " : `${theme.fg("dim", theme.tree.vertical)} `;
|
|
733
733
|
|
|
734
|
-
const { warning: missingCompleteWarning, rest: outputWithoutWarning } =
|
|
735
|
-
result.output,
|
|
736
|
-
);
|
|
734
|
+
const { warning: missingCompleteWarning, rest: outputWithoutWarning } = extractMissingYieldWarning(result.output);
|
|
737
735
|
const aborted = result.aborted ?? false;
|
|
738
736
|
const mergeFailed = !aborted && result.exitCode === 0 && !!result.error;
|
|
739
737
|
const success = !aborted && result.exitCode === 0 && !result.error;
|
|
@@ -783,11 +781,11 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
783
781
|
`${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg("dim", truncateToWidth(replaceTabs(result.abortReason), 80))}`,
|
|
784
782
|
);
|
|
785
783
|
}
|
|
786
|
-
// Check for review result (
|
|
787
|
-
const completeData = result.extractedToolData?.
|
|
784
|
+
// Check for review result (yield with review schema + report_finding)
|
|
785
|
+
const completeData = result.extractedToolData?.yield as Array<{ data: unknown }> | undefined;
|
|
788
786
|
const reportFindingData = normalizeReportFindings(result.extractedToolData?.report_finding);
|
|
789
787
|
|
|
790
|
-
// Extract review verdict from
|
|
788
|
+
// Extract review verdict from yield tool's data field if it matches SubmitReviewDetails
|
|
791
789
|
const reviewData = completeData
|
|
792
790
|
?.map(c => c.data as SubmitReviewDetails)
|
|
793
791
|
.filter(d => d && typeof d === "object" && "overall_correctness" in d);
|
|
@@ -804,7 +802,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
804
802
|
const hasCompleteData = completeData && completeData.length > 0;
|
|
805
803
|
const message = hasCompleteData
|
|
806
804
|
? "Review verdict missing expected fields"
|
|
807
|
-
: "Review incomplete (
|
|
805
|
+
: "Review incomplete (yield not called)";
|
|
808
806
|
lines.push(`${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg("dim", message)}`);
|
|
809
807
|
lines.push(`${continuePrefix}${formatFindingSummary(reportFindingData, theme)}`);
|
|
810
808
|
lines.push(...renderFindings(reportFindingData, continuePrefix, expanded, theme));
|
|
@@ -817,7 +815,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
817
815
|
if (result.extractedToolData) {
|
|
818
816
|
for (const [toolName, dataArray] of Object.entries(result.extractedToolData)) {
|
|
819
817
|
// Skip review tools - handled above
|
|
820
|
-
if (toolName === "
|
|
818
|
+
if (toolName === "yield" || toolName === "report_finding") continue;
|
|
821
819
|
|
|
822
820
|
const handler = subprocessToolRegistry.getHandler(toolName);
|
|
823
821
|
if (handler?.renderFinal && (dataArray as unknown[]).length > 0) {
|
package/src/tools/ask.ts
CHANGED
|
@@ -32,19 +32,19 @@ import { ToolAbortError } from "./tool-errors";
|
|
|
32
32
|
// =============================================================================
|
|
33
33
|
|
|
34
34
|
const OptionItem = Type.Object({
|
|
35
|
-
label: Type.String({ description: "
|
|
35
|
+
label: Type.String({ description: "display label" }),
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
const QuestionItem = Type.Object({
|
|
39
|
-
id: Type.String({ description: "
|
|
40
|
-
question: Type.String({ description: "
|
|
41
|
-
options: Type.Array(OptionItem, { description: "
|
|
42
|
-
multi: Type.Optional(Type.Boolean({ description: "
|
|
43
|
-
recommended: Type.Optional(Type.Number({ description: "
|
|
39
|
+
id: Type.String({ description: "question id", examples: ["auth", "cache"] }),
|
|
40
|
+
question: Type.String({ description: "question text" }),
|
|
41
|
+
options: Type.Array(OptionItem, { description: "available options" }),
|
|
42
|
+
multi: Type.Optional(Type.Boolean({ description: "allow multiple selections" })),
|
|
43
|
+
recommended: Type.Optional(Type.Number({ description: "recommended option index" })),
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
const askSchema = Type.Object({
|
|
47
|
-
questions: Type.Array(QuestionItem, { description: "
|
|
47
|
+
questions: Type.Array(QuestionItem, { description: "questions to ask", minItems: 1 }),
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
export type AskToolInput = Static<typeof askSchema>;
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
5
5
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
|
-
import { computeLineHash } from "../edit/line-hash";
|
|
8
|
+
import { computeLineHash, HASHLINE_CONTENT_SEPARATOR } from "../edit/line-hash";
|
|
9
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
10
|
import type { Theme } from "../modes/theme/theme";
|
|
11
11
|
import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
|
|
@@ -15,7 +15,6 @@ import type { ToolSession } from ".";
|
|
|
15
15
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
16
16
|
import type { OutputMeta } from "./output-meta";
|
|
17
17
|
import {
|
|
18
|
-
combineSearchGlobs,
|
|
19
18
|
hasGlobPathChars,
|
|
20
19
|
normalizePathLikeInput,
|
|
21
20
|
parseSearchPath,
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
} from "./path-utils";
|
|
25
24
|
import {
|
|
26
25
|
dedupeParseErrors,
|
|
26
|
+
formatCodeFrameLine,
|
|
27
27
|
formatCount,
|
|
28
28
|
formatEmptyMessage,
|
|
29
29
|
formatErrorMessage,
|
|
@@ -36,20 +36,19 @@ import { ToolError } from "./tool-errors";
|
|
|
36
36
|
import { toolResult } from "./tool-result";
|
|
37
37
|
|
|
38
38
|
const astEditOpSchema = Type.Object({
|
|
39
|
-
pat: Type.String({ description: "
|
|
40
|
-
out: Type.String({ description: "
|
|
39
|
+
pat: Type.String({ description: "ast pattern", examples: ["oldFn($$$ARGS)"] }),
|
|
40
|
+
out: Type.String({ description: "replacement template", examples: ["newFn($$$ARGS)"] }),
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
const astEditSchema = Type.Object({
|
|
44
44
|
ops: Type.Array(astEditOpSchema, {
|
|
45
45
|
minItems: 1,
|
|
46
|
-
description: "
|
|
46
|
+
description: "rewrite ops",
|
|
47
|
+
}),
|
|
48
|
+
path: Type.String({
|
|
49
|
+
description: "file, directory, glob, or comma-separated paths to rewrite",
|
|
50
|
+
examples: ["src/", "src/foo.ts", "src/**/*.ts"],
|
|
47
51
|
}),
|
|
48
|
-
lang: Type.Optional(Type.String({ description: "Language override" })),
|
|
49
|
-
path: Type.Optional(Type.String({ description: "File, directory, or glob pattern to rewrite (default: cwd)" })),
|
|
50
|
-
glob: Type.Optional(Type.String({ description: "Optional glob filter relative to path" })),
|
|
51
|
-
sel: Type.Optional(Type.String({ description: "Optional selector for contextual pattern mode" })),
|
|
52
|
-
limit: Type.Optional(Type.Number({ description: "Max total replacements" })),
|
|
53
52
|
});
|
|
54
53
|
|
|
55
54
|
export interface AstEditToolDetails {
|
|
@@ -63,6 +62,9 @@ export interface AstEditToolDetails {
|
|
|
63
62
|
files?: string[];
|
|
64
63
|
fileReplacements?: Array<{ path: string; count: number }>;
|
|
65
64
|
meta?: OutputMeta;
|
|
65
|
+
/** Pre-formatted text for the user-visible TUI render. Mirrors `result.text` lines but uses
|
|
66
|
+
* a `│` gutter (no model-only hashline anchors). The TUI uses this directly so it never parses model-facing text. */
|
|
67
|
+
displayContent?: string;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolDetails> {
|
|
@@ -101,10 +103,6 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
101
103
|
seenPatterns.add(pat);
|
|
102
104
|
}
|
|
103
105
|
const normalizedRewrites = Object.fromEntries(ops);
|
|
104
|
-
const maxReplacements = params.limit !== undefined ? Math.floor(params.limit) : undefined;
|
|
105
|
-
if (maxReplacements !== undefined && (!Number.isFinite(maxReplacements) || maxReplacements < 1)) {
|
|
106
|
-
throw new ToolError("limit must be a positive number");
|
|
107
|
-
}
|
|
108
106
|
const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);
|
|
109
107
|
|
|
110
108
|
const formatScopePath = (targetPath: string): string => {
|
|
@@ -113,8 +111,11 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
113
111
|
};
|
|
114
112
|
let searchPath: string | undefined;
|
|
115
113
|
let scopePath: string | undefined;
|
|
116
|
-
let globFilter
|
|
117
|
-
const rawPath =
|
|
114
|
+
let globFilter: string | undefined;
|
|
115
|
+
const rawPath = normalizePathLikeInput(params.path);
|
|
116
|
+
if (rawPath.length === 0) {
|
|
117
|
+
throw new ToolError("`path` must be a non-empty path or glob");
|
|
118
|
+
}
|
|
118
119
|
if (rawPath) {
|
|
119
120
|
const internalRouter = this.session.internalRouter;
|
|
120
121
|
if (internalRouter?.canHandle(rawPath)) {
|
|
@@ -136,7 +137,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
136
137
|
} else {
|
|
137
138
|
const parsedPath = parseSearchPath(rawPath);
|
|
138
139
|
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
139
|
-
globFilter =
|
|
140
|
+
globFilter = parsedPath.glob;
|
|
140
141
|
scopePath = formatScopePath(searchPath);
|
|
141
142
|
}
|
|
142
143
|
}
|
|
@@ -153,12 +154,9 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
153
154
|
|
|
154
155
|
const result = await astEdit({
|
|
155
156
|
rewrites: normalizedRewrites,
|
|
156
|
-
lang: params.lang?.trim(),
|
|
157
157
|
path: resolvedSearchPath,
|
|
158
158
|
glob: globFilter,
|
|
159
|
-
selector: params.sel?.trim(),
|
|
160
159
|
dryRun: true,
|
|
161
|
-
maxReplacements,
|
|
162
160
|
maxFiles,
|
|
163
161
|
failOnParseError: false,
|
|
164
162
|
signal,
|
|
@@ -205,24 +203,29 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
205
203
|
|
|
206
204
|
const useHashLines = resolveFileDisplayMode(this.session).hashLines;
|
|
207
205
|
const outputLines: string[] = [];
|
|
206
|
+
const displayLines: string[] = [];
|
|
208
207
|
const renderChangesForFile = (relativePath: string) => {
|
|
209
208
|
const fileChanges = changesByFile.get(relativePath) ?? [];
|
|
210
|
-
const
|
|
211
|
-
|
|
209
|
+
const lineNumberWidth = fileChanges.reduce(
|
|
210
|
+
(width, change) => Math.max(width, String(change.startLine).length),
|
|
211
|
+
0,
|
|
212
|
+
);
|
|
212
213
|
for (const change of fileChanges) {
|
|
213
214
|
const beforeFirstLine = change.before.split("\n", 1)[0] ?? "";
|
|
214
215
|
const afterFirstLine = change.after.split("\n", 1)[0] ?? "";
|
|
215
216
|
const beforeLine = beforeFirstLine.slice(0, 120);
|
|
216
217
|
const afterLine = afterFirstLine.slice(0, 120);
|
|
217
218
|
const beforeRef = useHashLines
|
|
218
|
-
? `${change.startLine}
|
|
219
|
-
: `${change.startLine
|
|
219
|
+
? `${change.startLine}${computeLineHash(change.startLine, beforeFirstLine)}`
|
|
220
|
+
: `${change.startLine}:${change.startColumn}`;
|
|
220
221
|
const afterRef = useHashLines
|
|
221
|
-
? `${change.startLine}
|
|
222
|
-
: `${change.startLine
|
|
223
|
-
const lineSeparator = useHashLines ?
|
|
222
|
+
? `${change.startLine}${computeLineHash(change.startLine, afterFirstLine)}`
|
|
223
|
+
: `${change.startLine}:${change.startColumn}`;
|
|
224
|
+
const lineSeparator = useHashLines ? HASHLINE_CONTENT_SEPARATOR : " ";
|
|
224
225
|
outputLines.push(`-${beforeRef}${lineSeparator}${beforeLine}`);
|
|
225
226
|
outputLines.push(`+${afterRef}${lineSeparator}${afterLine}`);
|
|
227
|
+
displayLines.push(formatCodeFrameLine("-", change.startLine, beforeLine, lineNumberWidth));
|
|
228
|
+
displayLines.push(formatCodeFrameLine("+", change.startLine, afterLine, lineNumberWidth));
|
|
226
229
|
}
|
|
227
230
|
};
|
|
228
231
|
|
|
@@ -240,20 +243,28 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
240
243
|
for (const relativePath of directoryFiles) {
|
|
241
244
|
if (outputLines.length > 0) {
|
|
242
245
|
outputLines.push("");
|
|
246
|
+
displayLines.push("");
|
|
243
247
|
}
|
|
244
248
|
const count = fileReplacementCounts.get(relativePath) ?? 0;
|
|
245
|
-
|
|
249
|
+
const header = `# ${path.basename(relativePath)} (${formatCount("replacement", count)})`;
|
|
250
|
+
outputLines.push(header);
|
|
251
|
+
displayLines.push(header);
|
|
246
252
|
renderChangesForFile(relativePath);
|
|
247
253
|
}
|
|
248
254
|
continue;
|
|
249
255
|
}
|
|
250
256
|
if (outputLines.length > 0) {
|
|
251
257
|
outputLines.push("");
|
|
258
|
+
displayLines.push("");
|
|
252
259
|
}
|
|
253
|
-
|
|
260
|
+
const dirHeader = `# ${directory}`;
|
|
261
|
+
outputLines.push(dirHeader);
|
|
262
|
+
displayLines.push(dirHeader);
|
|
254
263
|
for (const relativePath of directoryFiles) {
|
|
255
264
|
const count = fileReplacementCounts.get(relativePath) ?? 0;
|
|
256
|
-
|
|
265
|
+
const fileHeader = `## └─ ${path.basename(relativePath)} (${formatCount("replacement", count)})`;
|
|
266
|
+
outputLines.push(fileHeader);
|
|
267
|
+
displayLines.push(fileHeader);
|
|
257
268
|
renderChangesForFile(relativePath);
|
|
258
269
|
}
|
|
259
270
|
}
|
|
@@ -268,7 +279,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
268
279
|
count: fileReplacementCounts.get(filePath) ?? 0,
|
|
269
280
|
}));
|
|
270
281
|
if (result.limitReached) {
|
|
271
|
-
outputLines.push("", "Limit reached; narrow path
|
|
282
|
+
outputLines.push("", "Limit reached; narrow path.");
|
|
272
283
|
}
|
|
273
284
|
if (dedupedParseErrors.length) {
|
|
274
285
|
outputLines.push("", ...formatParseErrors(dedupedParseErrors));
|
|
@@ -284,12 +295,9 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
284
295
|
apply: async (_reason: string) => {
|
|
285
296
|
const applyResult = await astEdit({
|
|
286
297
|
rewrites: normalizedRewrites,
|
|
287
|
-
lang: params.lang?.trim(),
|
|
288
298
|
path: resolvedSearchPath,
|
|
289
299
|
glob: globFilter,
|
|
290
|
-
selector: params.sel?.trim(),
|
|
291
300
|
dryRun: false,
|
|
292
|
-
maxReplacements,
|
|
293
301
|
maxFiles,
|
|
294
302
|
failOnParseError: false,
|
|
295
303
|
});
|
|
@@ -351,6 +359,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
351
359
|
const details: AstEditToolDetails = {
|
|
352
360
|
...baseDetails,
|
|
353
361
|
fileReplacements,
|
|
362
|
+
displayContent: displayLines.join("\n"),
|
|
354
363
|
};
|
|
355
364
|
return toolResult(details).text(outputLines.join("\n")).done();
|
|
356
365
|
});
|
|
@@ -363,10 +372,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
363
372
|
|
|
364
373
|
interface AstEditRenderArgs {
|
|
365
374
|
ops?: Array<{ pat?: string; out?: string }>;
|
|
366
|
-
lang?: string;
|
|
367
375
|
path?: string;
|
|
368
|
-
sel?: string;
|
|
369
|
-
limit?: number;
|
|
370
376
|
}
|
|
371
377
|
|
|
372
378
|
const COLLAPSED_CHANGE_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
@@ -375,9 +381,7 @@ export const astEditToolRenderer = {
|
|
|
375
381
|
inline: true,
|
|
376
382
|
renderCall(args: AstEditRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
377
383
|
const meta: string[] = [];
|
|
378
|
-
if (args.lang) meta.push(`lang:${args.lang}`);
|
|
379
384
|
if (args.path) meta.push(`in ${args.path}`);
|
|
380
|
-
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
|
381
385
|
const rewriteCount = args.ops?.length ?? 0;
|
|
382
386
|
if (rewriteCount > 1) meta.push(`${rewriteCount} rewrites`);
|
|
383
387
|
|
|
@@ -432,7 +436,7 @@ export const astEditToolRenderer = {
|
|
|
432
436
|
const rewriteCount = args?.ops?.length ?? 0;
|
|
433
437
|
const description = rewriteCount === 1 ? args?.ops?.[0]?.pat : undefined;
|
|
434
438
|
|
|
435
|
-
const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
439
|
+
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
436
440
|
const rawLines = textContent.split("\n");
|
|
437
441
|
const hasSeparators = rawLines.some(line => line.trim().length === 0);
|
|
438
442
|
const allGroups: string[][] = [];
|
|
@@ -467,7 +471,7 @@ export const astEditToolRenderer = {
|
|
|
467
471
|
|
|
468
472
|
const extraLines: string[] = [];
|
|
469
473
|
if (limitReached) {
|
|
470
|
-
extraLines.push(uiTheme.fg("warning", "limit reached; narrow path
|
|
474
|
+
extraLines.push(uiTheme.fg("warning", "limit reached; narrow path"));
|
|
471
475
|
}
|
|
472
476
|
if (details?.parseErrors?.length) {
|
|
473
477
|
const total = details.parseErrors.length;
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -15,7 +15,6 @@ import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
|
15
15
|
import { formatMatchLine } from "./match-line-format";
|
|
16
16
|
import type { OutputMeta } from "./output-meta";
|
|
17
17
|
import {
|
|
18
|
-
combineSearchGlobs,
|
|
19
18
|
hasGlobPathChars,
|
|
20
19
|
normalizePathLikeInput,
|
|
21
20
|
parseSearchPath,
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
} from "./path-utils";
|
|
25
24
|
import {
|
|
26
25
|
dedupeParseErrors,
|
|
26
|
+
formatCodeFrameLine,
|
|
27
27
|
formatCount,
|
|
28
28
|
formatEmptyMessage,
|
|
29
29
|
formatErrorMessage,
|
|
@@ -35,14 +35,12 @@ import { ToolError } from "./tool-errors";
|
|
|
35
35
|
import { toolResult } from "./tool-result";
|
|
36
36
|
|
|
37
37
|
const astGrepSchema = Type.Object({
|
|
38
|
-
pat: Type.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
offset: Type.Optional(Type.Number({ description: "Skip first N matches", default: 0 })),
|
|
45
|
-
context: Type.Optional(Type.Number({ description: "Context lines around each match" })),
|
|
38
|
+
pat: Type.String({ description: "ast pattern", examples: ["console.log($$$)"] }),
|
|
39
|
+
path: Type.String({
|
|
40
|
+
description: "file, directory, glob, or comma-separated paths to search",
|
|
41
|
+
examples: ["src/", "src/foo.ts", "src/**/*.ts"],
|
|
42
|
+
}),
|
|
43
|
+
skip: Type.Optional(Type.Number({ description: "matches to skip", default: 0 })),
|
|
46
44
|
});
|
|
47
45
|
|
|
48
46
|
export interface AstGrepToolDetails {
|
|
@@ -55,6 +53,9 @@ export interface AstGrepToolDetails {
|
|
|
55
53
|
files?: string[];
|
|
56
54
|
fileMatches?: Array<{ path: string; count: number }>;
|
|
57
55
|
meta?: OutputMeta;
|
|
56
|
+
/** Pre-formatted text for the user-visible TUI render. Mirrors `result.text` lines but uses
|
|
57
|
+
* a `│` gutter and `*` to mark match lines. The TUI uses this directly so it never parses model-facing text. */
|
|
58
|
+
displayContent?: string;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolDetails> {
|
|
@@ -76,55 +77,48 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
76
77
|
_context?: AgentToolContext,
|
|
77
78
|
): Promise<AgentToolResult<AstGrepToolDetails>> {
|
|
78
79
|
return untilAborted(signal, async () => {
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
81
|
-
throw new ToolError("`pat` must
|
|
80
|
+
const pattern = params.pat.trim();
|
|
81
|
+
if (pattern.length === 0) {
|
|
82
|
+
throw new ToolError("`pat` must be a non-empty pattern");
|
|
82
83
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
const patterns = [pattern];
|
|
85
|
+
const skip = params.skip === undefined ? 0 : Math.floor(params.skip);
|
|
86
|
+
if (!Number.isFinite(skip) || skip < 0) {
|
|
87
|
+
throw new ToolError("skip must be a non-negative number");
|
|
86
88
|
}
|
|
87
|
-
const offset = params.offset === undefined ? 0 : Math.floor(params.offset);
|
|
88
|
-
if (!Number.isFinite(offset) || offset < 0) {
|
|
89
|
-
throw new ToolError("Offset must be a non-negative number");
|
|
90
|
-
}
|
|
91
|
-
const context = params.context === undefined ? undefined : Math.floor(params.context);
|
|
92
|
-
if (context !== undefined && (!Number.isFinite(context) || context < 0)) {
|
|
93
|
-
throw new ToolError("Context must be a non-negative number");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
89
|
const formatScopePath = (targetPath: string): string => {
|
|
97
90
|
const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
|
|
98
91
|
return relative.length === 0 ? "." : relative;
|
|
99
92
|
};
|
|
100
93
|
let searchPath: string | undefined;
|
|
101
94
|
let scopePath: string | undefined;
|
|
102
|
-
let globFilter
|
|
103
|
-
const rawPath =
|
|
104
|
-
if (rawPath) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
95
|
+
let globFilter: string | undefined;
|
|
96
|
+
const rawPath = normalizePathLikeInput(params.path);
|
|
97
|
+
if (rawPath.length === 0) {
|
|
98
|
+
throw new ToolError("`path` must be a non-empty path or glob");
|
|
99
|
+
}
|
|
100
|
+
const internalRouter = this.session.internalRouter;
|
|
101
|
+
if (internalRouter?.canHandle(rawPath)) {
|
|
102
|
+
if (hasGlobPathChars(rawPath)) {
|
|
103
|
+
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
104
|
+
}
|
|
105
|
+
const resource = await internalRouter.resolve(rawPath);
|
|
106
|
+
if (!resource.sourcePath) {
|
|
107
|
+
throw new ToolError(`Cannot search internal URL without backing file: ${rawPath}`);
|
|
108
|
+
}
|
|
109
|
+
searchPath = resource.sourcePath;
|
|
110
|
+
scopePath = formatScopePath(searchPath);
|
|
111
|
+
} else {
|
|
112
|
+
const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
|
|
113
|
+
if (multiSearchPath) {
|
|
114
|
+
searchPath = multiSearchPath.basePath;
|
|
115
|
+
globFilter = multiSearchPath.glob;
|
|
116
|
+
scopePath = multiSearchPath.scopePath;
|
|
116
117
|
} else {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
scopePath = multiSearchPath.scopePath;
|
|
122
|
-
} else {
|
|
123
|
-
const parsedPath = parseSearchPath(rawPath);
|
|
124
|
-
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
125
|
-
globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
|
|
126
|
-
scopePath = formatScopePath(searchPath);
|
|
127
|
-
}
|
|
118
|
+
const parsedPath = parseSearchPath(rawPath);
|
|
119
|
+
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
120
|
+
globFilter = parsedPath.glob;
|
|
121
|
+
scopePath = formatScopePath(searchPath);
|
|
128
122
|
}
|
|
129
123
|
}
|
|
130
124
|
|
|
@@ -140,13 +134,9 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
140
134
|
|
|
141
135
|
const result = await astGrep({
|
|
142
136
|
patterns,
|
|
143
|
-
lang: params.lang?.trim(),
|
|
144
137
|
path: resolvedSearchPath,
|
|
145
138
|
glob: globFilter,
|
|
146
|
-
|
|
147
|
-
limit,
|
|
148
|
-
offset,
|
|
149
|
-
context,
|
|
139
|
+
offset: skip,
|
|
150
140
|
includeMeta: true,
|
|
151
141
|
signal,
|
|
152
142
|
});
|
|
@@ -183,7 +173,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
183
173
|
|
|
184
174
|
if (result.matches.length === 0) {
|
|
185
175
|
const noMatchMessage = dedupedParseErrors.length
|
|
186
|
-
? "No matches found. Parse issues mean the query may be mis-scoped; narrow `path
|
|
176
|
+
? "No matches found. Parse issues mean the query may be mis-scoped; narrow `path` before concluding absence."
|
|
187
177
|
: "No matches found";
|
|
188
178
|
const parseMessage = dedupedParseErrors.length
|
|
189
179
|
? `\n${formatParseErrors(dedupedParseErrors).join("\n")}`
|
|
@@ -193,16 +183,22 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
193
183
|
|
|
194
184
|
const useHashLines = resolveFileDisplayMode(this.session).hashLines;
|
|
195
185
|
const outputLines: string[] = [];
|
|
186
|
+
const displayLines: string[] = [];
|
|
196
187
|
const renderMatchesForFile = (relativePath: string) => {
|
|
197
188
|
const fileMatches = matchesByFile.get(relativePath) ?? [];
|
|
189
|
+
const lineNumberWidth = fileMatches.reduce((width, match) => {
|
|
190
|
+
const lineCount = match.text.split("\n").length;
|
|
191
|
+
const endLine = match.startLine + lineCount - 1;
|
|
192
|
+
return Math.max(width, String(match.startLine).length, String(endLine).length);
|
|
193
|
+
}, 0);
|
|
198
194
|
for (const match of fileMatches) {
|
|
199
195
|
const matchLines = match.text.split("\n");
|
|
200
|
-
const lineNumbers = matchLines.map((_, index) => match.startLine + index);
|
|
201
|
-
const lineWidth = Math.max(...lineNumbers.map(value => value.toString().length));
|
|
202
|
-
const formatLine = (lineNumber: number, line: string, isMatch: boolean): string =>
|
|
203
|
-
formatMatchLine(lineNumber, line, isMatch, { useHashLines, lineWidth });
|
|
204
196
|
for (let index = 0; index < matchLines.length; index++) {
|
|
205
|
-
|
|
197
|
+
const lineNumber = match.startLine + index;
|
|
198
|
+
const isMatch = index === 0;
|
|
199
|
+
const line = matchLines[index] ?? "";
|
|
200
|
+
outputLines.push(formatMatchLine(lineNumber, line, isMatch, { useHashLines }));
|
|
201
|
+
displayLines.push(formatCodeFrameLine(isMatch ? "*" : " ", lineNumber, line, lineNumberWidth));
|
|
206
202
|
}
|
|
207
203
|
if (match.metaVariables && Object.keys(match.metaVariables).length > 0) {
|
|
208
204
|
const serializedMeta = Object.entries(match.metaVariables)
|
|
@@ -210,6 +206,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
210
206
|
.map(([key, value]) => `${key}=${value}`)
|
|
211
207
|
.join(", ");
|
|
212
208
|
outputLines.push(` meta: ${serializedMeta}`);
|
|
209
|
+
displayLines.push(` meta: ${serializedMeta}`);
|
|
213
210
|
}
|
|
214
211
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
215
212
|
}
|
|
@@ -229,18 +226,26 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
229
226
|
for (const relativePath of directoryFiles) {
|
|
230
227
|
if (outputLines.length > 0) {
|
|
231
228
|
outputLines.push("");
|
|
229
|
+
displayLines.push("");
|
|
232
230
|
}
|
|
233
|
-
|
|
231
|
+
const header = `# ${path.basename(relativePath)}`;
|
|
232
|
+
outputLines.push(header);
|
|
233
|
+
displayLines.push(header);
|
|
234
234
|
renderMatchesForFile(relativePath);
|
|
235
235
|
}
|
|
236
236
|
continue;
|
|
237
237
|
}
|
|
238
238
|
if (outputLines.length > 0) {
|
|
239
239
|
outputLines.push("");
|
|
240
|
+
displayLines.push("");
|
|
240
241
|
}
|
|
241
|
-
|
|
242
|
+
const dirHeader = `# ${directory}`;
|
|
243
|
+
outputLines.push(dirHeader);
|
|
244
|
+
displayLines.push(dirHeader);
|
|
242
245
|
for (const relativePath of directoryFiles) {
|
|
243
|
-
|
|
246
|
+
const fileHeader = `## └─ ${path.basename(relativePath)}`;
|
|
247
|
+
outputLines.push(fileHeader);
|
|
248
|
+
displayLines.push(fileHeader);
|
|
244
249
|
renderMatchesForFile(relativePath);
|
|
245
250
|
}
|
|
246
251
|
}
|
|
@@ -256,6 +261,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
256
261
|
path: filePath,
|
|
257
262
|
count: fileMatchCounts.get(filePath) ?? 0,
|
|
258
263
|
})),
|
|
264
|
+
displayContent: displayLines.join("\n"),
|
|
259
265
|
};
|
|
260
266
|
if (result.limitReached) {
|
|
261
267
|
outputLines.push("", "Result limit reached; narrow path pattern or increase limit.");
|
|
@@ -274,13 +280,9 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
274
280
|
// =============================================================================
|
|
275
281
|
|
|
276
282
|
interface AstGrepRenderArgs {
|
|
277
|
-
pat?: string
|
|
278
|
-
lang?: string;
|
|
283
|
+
pat?: string;
|
|
279
284
|
path?: string;
|
|
280
|
-
|
|
281
|
-
limit?: number;
|
|
282
|
-
offset?: number;
|
|
283
|
-
context?: number;
|
|
285
|
+
skip?: number;
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
const COLLAPSED_MATCH_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
@@ -289,15 +291,10 @@ export const astGrepToolRenderer = {
|
|
|
289
291
|
inline: true,
|
|
290
292
|
renderCall(args: AstGrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
291
293
|
const meta: string[] = [];
|
|
292
|
-
if (args.lang) meta.push(`lang:${args.lang}`);
|
|
293
294
|
if (args.path) meta.push(`in ${args.path}`);
|
|
294
|
-
if (args.
|
|
295
|
-
if (args.limit !== undefined && args.limit > 0) meta.push(`limit:${args.limit}`);
|
|
296
|
-
if (args.offset !== undefined && args.offset > 0) meta.push(`offset:${args.offset}`);
|
|
297
|
-
if (args.context !== undefined) meta.push(`context:${args.context}`);
|
|
298
|
-
if (args.pat && args.pat.length > 1) meta.push(`${args.pat.length} patterns`);
|
|
295
|
+
if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
|
|
299
296
|
|
|
300
|
-
const description = args.pat
|
|
297
|
+
const description = args.pat ?? "?";
|
|
301
298
|
const text = renderStatusLine({ icon: "pending", title: "AST Grep", description, meta }, uiTheme);
|
|
302
299
|
return new Text(text, 0, 0);
|
|
303
300
|
},
|
|
@@ -321,19 +318,14 @@ export const astGrepToolRenderer = {
|
|
|
321
318
|
const limitReached = details?.limitReached ?? false;
|
|
322
319
|
|
|
323
320
|
if (matchCount === 0) {
|
|
324
|
-
const description = args?.pat
|
|
321
|
+
const description = args?.pat;
|
|
325
322
|
const meta = ["0 matches"];
|
|
326
323
|
if (details?.scopePath) meta.push(`in ${details.scopePath}`);
|
|
327
324
|
if (filesSearched > 0) meta.push(`searched ${filesSearched}`);
|
|
328
325
|
const header = renderStatusLine({ icon: "warning", title: "AST Grep", description, meta }, uiTheme);
|
|
329
326
|
const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
|
|
330
327
|
if (details?.parseErrors?.length) {
|
|
331
|
-
lines.push(
|
|
332
|
-
uiTheme.fg(
|
|
333
|
-
"warning",
|
|
334
|
-
"Query may be mis-scoped; narrow `path`/`glob` or set `lang` before concluding absence",
|
|
335
|
-
),
|
|
336
|
-
);
|
|
328
|
+
lines.push(uiTheme.fg("warning", "Query may be mis-scoped; narrow `path` before concluding absence"));
|
|
337
329
|
const capped = details.parseErrors.slice(0, PARSE_ERRORS_LIMIT);
|
|
338
330
|
for (const err of capped) {
|
|
339
331
|
lines.push(uiTheme.fg("warning", ` - ${err}`));
|
|
@@ -350,13 +342,13 @@ export const astGrepToolRenderer = {
|
|
|
350
342
|
if (details?.scopePath) meta.push(`in ${details.scopePath}`);
|
|
351
343
|
meta.push(`searched ${filesSearched}`);
|
|
352
344
|
if (limitReached) meta.push(uiTheme.fg("warning", "limit reached"));
|
|
353
|
-
const description = args?.pat
|
|
345
|
+
const description = args?.pat;
|
|
354
346
|
const header = renderStatusLine(
|
|
355
347
|
{ icon: limitReached ? "warning" : "success", title: "AST Grep", description, meta },
|
|
356
348
|
uiTheme,
|
|
357
349
|
);
|
|
358
350
|
|
|
359
|
-
const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
351
|
+
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
360
352
|
const rawLines = textContent.split("\n");
|
|
361
353
|
const hasSeparators = rawLines.some(line => line.trim().length === 0);
|
|
362
354
|
const allGroups: string[][] = [];
|