@oh-my-pi/pi-coding-agent 14.5.14 → 14.6.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 +49 -0
- package/package.json +7 -7
- package/src/autoresearch/command-resume.md +5 -8
- package/src/autoresearch/git.ts +41 -51
- package/src/autoresearch/helpers.ts +43 -359
- package/src/autoresearch/index.ts +281 -273
- package/src/autoresearch/prompt-setup.md +43 -0
- package/src/autoresearch/prompt.md +52 -193
- package/src/autoresearch/resume-message.md +2 -8
- package/src/autoresearch/state.ts +59 -166
- package/src/autoresearch/storage.ts +687 -0
- package/src/autoresearch/tools/init-experiment.ts +201 -290
- package/src/autoresearch/tools/log-experiment.ts +304 -517
- package/src/autoresearch/tools/run-experiment.ts +117 -296
- package/src/autoresearch/tools/update-notes.ts +116 -0
- package/src/autoresearch/types.ts +16 -66
- package/src/cli/list-models.ts +66 -0
- package/src/config/settings-schema.ts +1 -1
- package/src/config/settings.ts +20 -1
- package/src/cursor.ts +1 -1
- package/src/edit/index.ts +9 -31
- package/src/edit/line-hash.ts +70 -43
- package/src/edit/modes/hashline.lark +26 -0
- package/src/edit/modes/hashline.ts +898 -1099
- package/src/edit/modes/patch.ts +0 -7
- package/src/edit/modes/replace.ts +0 -4
- package/src/edit/renderer.ts +22 -20
- package/src/edit/streaming.ts +8 -28
- package/src/eval/eval.lark +24 -30
- package/src/eval/js/context-manager.ts +5 -162
- package/src/eval/js/prelude.txt +0 -12
- package/src/eval/parse.ts +129 -129
- package/src/eval/py/prelude.py +1 -219
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +2 -2
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/main.ts +18 -3
- package/src/modes/components/session-observer-overlay.ts +5 -2
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line.ts +3 -5
- package/src/modes/components/tree-selector.ts +4 -5
- package/src/modes/components/welcome.ts +11 -1
- package/src/modes/controllers/command-controller.ts +2 -6
- package/src/modes/controllers/event-controller.ts +7 -5
- package/src/modes/controllers/extension-ui-controller.ts +3 -15
- package/src/modes/controllers/input-controller.ts +0 -1
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +5 -7
- package/src/prompts/system/system-prompt.md +14 -38
- package/src/prompts/tools/ast-edit.md +8 -8
- package/src/prompts/tools/ast-grep.md +10 -10
- package/src/prompts/tools/eval.md +13 -31
- package/src/prompts/tools/find.md +2 -1
- package/src/prompts/tools/hashline.md +66 -57
- package/src/prompts/tools/search.md +2 -2
- package/src/session/agent-session.ts +1 -1
- package/src/session/session-manager.ts +17 -13
- package/src/tools/ast-edit.ts +141 -44
- package/src/tools/ast-grep.ts +112 -36
- package/src/tools/eval.ts +2 -53
- package/src/tools/find.ts +16 -15
- package/src/tools/gh-renderer.ts +184 -59
- package/src/tools/path-utils.ts +36 -196
- package/src/tools/search.ts +56 -35
- package/src/utils/edit-mode.ts +2 -11
- package/src/utils/file-display-mode.ts +1 -1
- package/src/utils/git.ts +59 -24
- package/src/utils/session-color.ts +0 -12
- package/src/utils/title-generator.ts +22 -38
- package/src/autoresearch/apply-contract-to-state.ts +0 -24
- package/src/autoresearch/contract.ts +0 -288
- package/src/edit/modes/atom.lark +0 -29
- package/src/edit/modes/atom.ts +0 -1773
- package/src/prompts/tools/atom.md +0 -150
|
@@ -1,69 +1,78 @@
|
|
|
1
|
-
|
|
1
|
+
Your patch language is a compact, line-anchored edit format.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A patch contains one or more file sections. The first non-blank line of every edit section **MUST** be `@PATH`.
|
|
4
|
+
Operations reference lines in the file by their line number and hash, called "Anchors", e.g. `5th`, `123ab`.
|
|
5
|
+
You **MUST** copy them verbatim from the latest output for the file you're editing.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
**Top level**
|
|
7
|
-
- `edits` — array of edit entries
|
|
8
|
-
- `path` (required) — file path for all edits in this request
|
|
7
|
+
This format is purely textual. The tool has NO awareness of language, indentation, brackets, fences, or table widths. You are responsible for emitting valid syntax in your replacements/insertions.
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
<ops>
|
|
10
|
+
@PATH header: subsequent ops apply to PATH
|
|
11
|
+
< ANCHOR insert lines BEFORE the anchored line (or BOF); payload follows as `|TEXT` lines
|
|
12
|
+
+ ANCHOR insert lines AFTER the anchored line (or EOF); payload follows as `|TEXT` lines
|
|
13
|
+
- A..B delete the line range (inclusive); `- A` for one line
|
|
14
|
+
= A..B replace the range with payload `|TEXT` lines, or with one blank line if no payload follows
|
|
15
|
+
</ops>
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
- `
|
|
18
|
-
|
|
17
|
+
<rules>
|
|
18
|
+
- Every line of inserted/replacement content **MUST** be emitted as a payload line starting with `|`.
|
|
19
|
+
- `|` is syntax, not content. The inserted text begins after the first `|`; use a bare `|` to insert a blank line.
|
|
20
|
+
- `< A` inserts before line A; `+ A` inserts after line A. `< BOF` / `+ BOF` both prepend; `< EOF` / `+ EOF` both append.
|
|
21
|
+
- `= A..B` replaces the inclusive range with the following payload lines. `= A` (or `= A..B`) with no payload blanks the range to a single empty line.
|
|
22
|
+
- `- A..B` deletes the inclusive range; omit `..B` for one line.
|
|
23
|
+
</rules>
|
|
24
|
+
|
|
25
|
+
<case file="a.ts">
|
|
26
|
+
{{hline 1 "const DEF = \"guest\";"}}
|
|
27
|
+
{{hline 2 ""}}
|
|
28
|
+
{{hline 3 "export function label(name) {"}}
|
|
29
|
+
{{hline 4 "\tconst clean = name || DEF;"}}
|
|
30
|
+
{{hline 5 "\treturn clean.trim();"}}
|
|
31
|
+
{{hline 6 "}"}}
|
|
32
|
+
</case>
|
|
19
33
|
|
|
20
34
|
<examples>
|
|
21
|
-
|
|
35
|
+
# Replace one line (preserve the leading tab from the original)
|
|
36
|
+
@a.ts
|
|
37
|
+
= {{hrefr 5}}
|
|
38
|
+
| return clean.trim().toUpperCase();
|
|
39
|
+
|
|
40
|
+
# Replace a contiguous range with multiple lines
|
|
41
|
+
@a.ts
|
|
42
|
+
= {{hrefr 3}}..{{hrefr 6}}
|
|
43
|
+
|export function label(name: string): string {
|
|
44
|
+
| const clean = (name || DEF).trim();
|
|
45
|
+
| return clean.length === 0 ? DEF : clean.toUpperCase();
|
|
46
|
+
|}
|
|
47
|
+
|
|
48
|
+
# Insert BEFORE a line
|
|
49
|
+
@a.ts
|
|
50
|
+
< {{hrefr 5}}
|
|
51
|
+
| const debug = false;
|
|
52
|
+
|
|
53
|
+
# Insert AFTER a line
|
|
54
|
+
@a.ts
|
|
55
|
+
+ {{hrefr 4}}
|
|
56
|
+
| if (clean.length === 0) return DEF;
|
|
57
|
+
|
|
58
|
+
# Append to end of file
|
|
59
|
+
@a.ts
|
|
60
|
+
+ EOF
|
|
61
|
+
|export const done = true;
|
|
22
62
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{{
|
|
26
|
-
{{hline 3 "const tag = \"DO NOT SHIP\";"}}
|
|
27
|
-
{{hline 4 ""}}
|
|
28
|
-
{{hline 5 "function alpha() {"}}
|
|
29
|
-
{{hline 6 "\tlog();"}}
|
|
30
|
-
{{hline 7 "}"}}
|
|
31
|
-
{{hline 8 ""}}
|
|
32
|
-
{{hline 9 "function beta() {"}}
|
|
33
|
-
{{hline 10 "\t// TODO: remove after migration"}}
|
|
34
|
-
{{hline 11 "\tlegacy();"}}
|
|
35
|
-
{{hline 12 "\ttry {"}}
|
|
36
|
-
{{hline 13 "\t\treturn parse(data);"}}
|
|
37
|
-
{{hline 14 "\t} catch (err) {"}}
|
|
38
|
-
{{hline 15 "\t\tconsole.error(err);"}}
|
|
39
|
-
{{hline 16 "\t\treturn null;"}}
|
|
40
|
-
{{hline 17 "\t}"}}
|
|
41
|
-
{{hline 18 "}"}}
|
|
42
|
-
```
|
|
63
|
+
# Delete a single line
|
|
64
|
+
@a.ts
|
|
65
|
+
- {{hrefr 2}}
|
|
43
66
|
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# Replace whole block including closing brace
|
|
48
|
-
Replace `alpha`'s entire body including the closing `}`. `end` **MUST** be {{href 7}} because `content` includes `}`.
|
|
49
|
-
`{path:"a.ts",edits:[{loc:{range:{pos:{{href 6}},end:{{href 7}}}},content:["\tvalidate();","\tlog();","}"]}]}`
|
|
50
|
-
**Wrong**: `end: {{href 6}}` — line 7 (`}`) survives AND content emits `}`, producing two closing braces.
|
|
51
|
-
# Replace one line
|
|
52
|
-
Single-line replace uses `pos == end`.
|
|
53
|
-
`{path:"a.ts",edits:[{loc:{range:{pos:{{href 2}},end:{{href 2}}}},content:["const timeout = 30_000;"]}]}`
|
|
54
|
-
# Delete a range
|
|
55
|
-
`{path:"a.ts",edits:[{loc:{range:{pos:{{href 10}},end:{{href 11}}}},content:null}]}`
|
|
56
|
-
# Insert before a sibling
|
|
57
|
-
When adding a sibling declaration, prefer `prepend` on the next declaration.
|
|
58
|
-
`{path:"a.ts",edits:[{loc:{prepend:{{href 9}}},content:["function gamma() {","\tvalidate();","}",""]}]}`
|
|
67
|
+
# Blank a line in place (no payload required)
|
|
68
|
+
@a.ts
|
|
69
|
+
= {{hrefr 2}}
|
|
59
70
|
</examples>
|
|
60
71
|
|
|
61
72
|
<critical>
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
- `content` must be literal file content with matching indentation. If the file uses tabs, use real tabs.
|
|
68
|
-
- You **MUST NOT** use this tool to reformat or clean up unrelated code — use project-specific linters or code formatters instead.
|
|
73
|
+
- Always copy anchors exactly from tool output, but **NEVER** include line content after the `|` separator in the op line.
|
|
74
|
+
- Only emit changed lines. Do not restate unchanged context as payload.
|
|
75
|
+
- Every inserted/replacement content line **MUST** start with `|`; raw content lines are invalid.
|
|
76
|
+
- Do not write unified diff syntax (`@@`, `-OLD`, `+NEW`).
|
|
77
|
+
- To replace a block, use one `= A..B` op followed by all replacement `|TEXT` payload lines.
|
|
69
78
|
</critical>
|
|
@@ -2,13 +2,13 @@ Searches files using powerful regex matching.
|
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- Supports full regex syntax (e.g., `log.*Error`, `function\\s+\\w+`); literal braces need escaping (`interface\\{\\}` for `interface{}` in Go)
|
|
5
|
-
- `
|
|
5
|
+
- `paths` is required and accepts an array of files, directories, globs, or internal URLs
|
|
6
6
|
- Cross-line patterns are detected from literal `\n` or escaped `\\n` in `pattern`
|
|
7
7
|
</instruction>
|
|
8
8
|
|
|
9
9
|
<output>
|
|
10
10
|
{{#if IS_HASHLINE_MODE}}
|
|
11
|
-
- Text output is anchor-prefixed: `*
|
|
11
|
+
- Text output is anchor-prefixed: `*5th|content` (match) or ` 9x}|content` (context, leading space). The 2-char suffix is a content fingerprint.
|
|
12
12
|
{{else}}
|
|
13
13
|
{{#if IS_LINE_NUMBER_MODE}}
|
|
14
14
|
- Text output is line-number-prefixed
|
|
@@ -5446,7 +5446,7 @@ export class AgentSession {
|
|
|
5446
5446
|
// service unavailable, network/connection/socket errors, fetch failed, terminated, retry delay exceeded
|
|
5447
5447
|
return (
|
|
5448
5448
|
isUnexpectedSocketCloseMessage(errorMessage) ||
|
|
5449
|
-
/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall/i.test(
|
|
5449
|
+
/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response/i.test(
|
|
5450
5450
|
errorMessage,
|
|
5451
5451
|
)
|
|
5452
5452
|
);
|
|
@@ -870,8 +870,8 @@ function sanitizeSessionName(value: string | undefined): string | undefined {
|
|
|
870
870
|
|
|
871
871
|
class RecentSessionInfo {
|
|
872
872
|
#fullName: string | undefined;
|
|
873
|
-
#name: string | undefined;
|
|
874
873
|
#timeAgo: string | undefined;
|
|
874
|
+
readonly #headerTimestamp: string | undefined;
|
|
875
875
|
|
|
876
876
|
constructor(
|
|
877
877
|
readonly path: string,
|
|
@@ -879,27 +879,31 @@ class RecentSessionInfo {
|
|
|
879
879
|
header: Record<string, unknown>,
|
|
880
880
|
firstPrompt?: string,
|
|
881
881
|
) {
|
|
882
|
-
//
|
|
882
|
+
// Prefer an explicit title, then the first user prompt. The raw UUID `id` is
|
|
883
|
+
// intentionally not used as a fallback: showing it as a "name" is unfriendly and
|
|
884
|
+
// indistinguishable from neighboring sessions in the UI. The friendly fallback is
|
|
885
|
+
// derived lazily in `fullName` from the session timestamp.
|
|
883
886
|
const trystr = (v: unknown) => (typeof v === "string" ? v : undefined);
|
|
884
|
-
this.#fullName =
|
|
885
|
-
|
|
886
|
-
sanitizeSessionName(firstPrompt) ??
|
|
887
|
-
sanitizeSessionName(trystr(header.id));
|
|
887
|
+
this.#fullName = sanitizeSessionName(trystr(header.title)) ?? sanitizeSessionName(firstPrompt);
|
|
888
|
+
this.#headerTimestamp = trystr(header.timestamp);
|
|
888
889
|
}
|
|
889
890
|
|
|
890
|
-
/**
|
|
891
|
+
/** Display name. Falls back to a timestamp-based label, never the raw UUID. */
|
|
891
892
|
get fullName(): string {
|
|
892
893
|
if (this.#fullName) return this.#fullName;
|
|
893
|
-
this.#
|
|
894
|
+
const ts = this.#headerTimestamp ? Date.parse(this.#headerTimestamp) : Number.NaN;
|
|
895
|
+
const date = new Date(Number.isFinite(ts) ? ts : this.mtime);
|
|
896
|
+
const time = date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
|
897
|
+
this.#fullName = `Untitled · ${time}`;
|
|
894
898
|
return this.#fullName;
|
|
895
899
|
}
|
|
896
900
|
|
|
897
|
-
/**
|
|
901
|
+
/**
|
|
902
|
+
* Display name without an arbitrary length cap. The renderer is responsible for
|
|
903
|
+
* width-aware truncation so adjacent fields (e.g. the relative time) stay visible.
|
|
904
|
+
*/
|
|
898
905
|
get name(): string {
|
|
899
|
-
|
|
900
|
-
const fullName = this.fullName;
|
|
901
|
-
this.#name = fullName.length <= 40 ? fullName : `${fullName.slice(0, 39)}…`;
|
|
902
|
-
return this.#name;
|
|
906
|
+
return this.fullName;
|
|
903
907
|
}
|
|
904
908
|
|
|
905
909
|
/** Human-readable relative time (e.g., "2 hours ago") */
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
1
2
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import { type AstReplaceChange, astEdit } from "@oh-my-pi/pi-natives";
|
|
3
|
+
import { type AstReplaceChange, type AstReplaceFileChange, astEdit } from "@oh-my-pi/pi-natives";
|
|
3
4
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
5
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
5
6
|
import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
hasGlobPathChars,
|
|
20
21
|
normalizePathLikeInput,
|
|
21
22
|
parseSearchPath,
|
|
22
|
-
|
|
23
|
+
resolveExplicitSearchPaths,
|
|
23
24
|
resolveToCwd,
|
|
24
25
|
} from "./path-utils";
|
|
25
26
|
import {
|
|
@@ -46,12 +47,106 @@ const astEditSchema = Type.Object({
|
|
|
46
47
|
minItems: 1,
|
|
47
48
|
description: "rewrite ops",
|
|
48
49
|
}),
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
paths: Type.Array(Type.String({ description: "file, directory, glob, or internal URL to rewrite" }), {
|
|
51
|
+
minItems: 1,
|
|
52
|
+
description: "files, directories, globs, or internal URLs to rewrite",
|
|
53
|
+
examples: [["src/"], ["src/foo.ts"], ["src/**/*.ts"], ["src/", "packages/"]],
|
|
52
54
|
}),
|
|
53
55
|
});
|
|
54
56
|
|
|
57
|
+
interface AstEditCallOptions {
|
|
58
|
+
rewrites: Record<string, string>;
|
|
59
|
+
dryRun: boolean;
|
|
60
|
+
maxFiles: number;
|
|
61
|
+
failOnParseError: boolean;
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface AstEditAggregatedResult {
|
|
66
|
+
changes: AstReplaceChange[];
|
|
67
|
+
fileChanges: AstReplaceFileChange[];
|
|
68
|
+
totalReplacements: number;
|
|
69
|
+
filesTouched: number;
|
|
70
|
+
filesSearched: number;
|
|
71
|
+
applied: boolean;
|
|
72
|
+
limitReached: boolean;
|
|
73
|
+
parseErrors?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function runAstEditTargets(
|
|
77
|
+
targets: Array<{ basePath: string; glob?: string }>,
|
|
78
|
+
commonBasePath: string,
|
|
79
|
+
options: AstEditCallOptions,
|
|
80
|
+
): Promise<AstEditAggregatedResult> {
|
|
81
|
+
const aggregatedChanges: AstReplaceChange[] = [];
|
|
82
|
+
const fileCounts = new Map<string, number>();
|
|
83
|
+
const parseErrors: string[] = [];
|
|
84
|
+
let totalReplacements = 0;
|
|
85
|
+
let filesSearched = 0;
|
|
86
|
+
let limitReached = false;
|
|
87
|
+
let applied = !options.dryRun;
|
|
88
|
+
for (const target of targets) {
|
|
89
|
+
const targetResult = await astEdit({
|
|
90
|
+
rewrites: options.rewrites,
|
|
91
|
+
path: target.basePath,
|
|
92
|
+
glob: target.glob,
|
|
93
|
+
dryRun: options.dryRun,
|
|
94
|
+
maxFiles: options.maxFiles,
|
|
95
|
+
failOnParseError: options.failOnParseError,
|
|
96
|
+
signal: options.signal,
|
|
97
|
+
});
|
|
98
|
+
totalReplacements += targetResult.totalReplacements;
|
|
99
|
+
filesSearched += targetResult.filesSearched;
|
|
100
|
+
limitReached = limitReached || targetResult.limitReached;
|
|
101
|
+
applied = applied && targetResult.applied;
|
|
102
|
+
if (targetResult.parseErrors) parseErrors.push(...targetResult.parseErrors);
|
|
103
|
+
for (const change of targetResult.changes) {
|
|
104
|
+
const absolute = path.resolve(target.basePath, change.path);
|
|
105
|
+
const rebased = path.relative(commonBasePath, absolute).replace(/\\/g, "/");
|
|
106
|
+
aggregatedChanges.push({ ...change, path: rebased });
|
|
107
|
+
}
|
|
108
|
+
for (const fileChange of targetResult.fileChanges) {
|
|
109
|
+
const absolute = path.resolve(target.basePath, fileChange.path);
|
|
110
|
+
const rebased = path.relative(commonBasePath, absolute).replace(/\\/g, "/");
|
|
111
|
+
fileCounts.set(rebased, (fileCounts.get(rebased) ?? 0) + fileChange.count);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const fileChanges: AstReplaceFileChange[] = Array.from(fileCounts, ([changePath, count]) => ({
|
|
115
|
+
path: changePath,
|
|
116
|
+
count,
|
|
117
|
+
}));
|
|
118
|
+
return {
|
|
119
|
+
changes: aggregatedChanges,
|
|
120
|
+
fileChanges,
|
|
121
|
+
totalReplacements,
|
|
122
|
+
filesTouched: fileChanges.length,
|
|
123
|
+
filesSearched,
|
|
124
|
+
applied,
|
|
125
|
+
limitReached,
|
|
126
|
+
parseErrors: parseErrors.length > 0 ? parseErrors : undefined,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function runAstEditOnce(
|
|
131
|
+
targets: Array<{ basePath: string; glob?: string }> | undefined,
|
|
132
|
+
resolvedSearchPath: string,
|
|
133
|
+
globFilter: string | undefined,
|
|
134
|
+
options: AstEditCallOptions,
|
|
135
|
+
): Promise<AstEditAggregatedResult> {
|
|
136
|
+
if (targets) {
|
|
137
|
+
return runAstEditTargets(targets, resolvedSearchPath, options);
|
|
138
|
+
}
|
|
139
|
+
return astEdit({
|
|
140
|
+
rewrites: options.rewrites,
|
|
141
|
+
path: resolvedSearchPath,
|
|
142
|
+
glob: globFilter,
|
|
143
|
+
dryRun: options.dryRun,
|
|
144
|
+
maxFiles: options.maxFiles,
|
|
145
|
+
failOnParseError: options.failOnParseError,
|
|
146
|
+
signal: options.signal,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
55
150
|
export interface AstEditToolDetails {
|
|
56
151
|
totalReplacements: number;
|
|
57
152
|
filesTouched: number;
|
|
@@ -107,40 +202,46 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
107
202
|
const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);
|
|
108
203
|
|
|
109
204
|
const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
|
|
110
|
-
let searchPath: string
|
|
111
|
-
let scopePath: string
|
|
205
|
+
let searchPath: string;
|
|
206
|
+
let scopePath: string;
|
|
112
207
|
let globFilter: string | undefined;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
208
|
+
let multiTargets: Array<{ basePath: string; glob?: string }> | undefined;
|
|
209
|
+
const rawPaths = params.paths.map(normalizePathLikeInput);
|
|
210
|
+
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
211
|
+
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
116
212
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
213
|
+
const internalRouter = this.session.internalRouter;
|
|
214
|
+
const resolvedPathInputs: string[] = [];
|
|
215
|
+
for (const rawPath of rawPaths) {
|
|
216
|
+
if (!internalRouter?.canHandle(rawPath)) {
|
|
217
|
+
resolvedPathInputs.push(rawPath);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (hasGlobPathChars(rawPath)) {
|
|
221
|
+
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
222
|
+
}
|
|
223
|
+
const resource = await internalRouter.resolve(rawPath);
|
|
224
|
+
if (!resource.sourcePath) {
|
|
225
|
+
throw new ToolError(`Cannot rewrite internal URL without backing file: ${rawPath}`);
|
|
226
|
+
}
|
|
227
|
+
resolvedPathInputs.push(resource.sourcePath);
|
|
228
|
+
}
|
|
229
|
+
if (resolvedPathInputs.length === 1) {
|
|
230
|
+
const parsedPath = parseSearchPath(resolvedPathInputs[0] ?? ".");
|
|
231
|
+
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
232
|
+
globFilter = parsedPath.glob;
|
|
233
|
+
scopePath = formatScopePath(searchPath);
|
|
234
|
+
} else {
|
|
235
|
+
const multiSearchPath = await resolveExplicitSearchPaths(resolvedPathInputs, this.session.cwd, globFilter);
|
|
236
|
+
if (!multiSearchPath) {
|
|
237
|
+
throw new ToolError("`paths` must contain at least one path or glob");
|
|
141
238
|
}
|
|
239
|
+
searchPath = multiSearchPath.basePath;
|
|
240
|
+
globFilter = multiSearchPath.targets ? undefined : multiSearchPath.glob;
|
|
241
|
+
multiTargets = multiSearchPath.targets;
|
|
242
|
+
scopePath = multiSearchPath.scopePath;
|
|
142
243
|
}
|
|
143
|
-
const resolvedSearchPath = searchPath
|
|
244
|
+
const resolvedSearchPath = searchPath;
|
|
144
245
|
scopePath = scopePath ?? formatScopePath(resolvedSearchPath);
|
|
145
246
|
let isDirectory: boolean;
|
|
146
247
|
try {
|
|
@@ -150,10 +251,8 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
150
251
|
throw new ToolError(`Path not found: ${scopePath}`);
|
|
151
252
|
}
|
|
152
253
|
|
|
153
|
-
const result = await
|
|
254
|
+
const result = await runAstEditOnce(multiTargets, resolvedSearchPath, globFilter, {
|
|
154
255
|
rewrites: normalizedRewrites,
|
|
155
|
-
path: resolvedSearchPath,
|
|
156
|
-
glob: globFilter,
|
|
157
256
|
dryRun: true,
|
|
158
257
|
maxFiles,
|
|
159
258
|
failOnParseError: false,
|
|
@@ -256,7 +355,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
256
355
|
count: fileReplacementCounts.get(filePath) ?? 0,
|
|
257
356
|
}));
|
|
258
357
|
if (result.limitReached) {
|
|
259
|
-
outputLines.push("", "Limit reached; narrow
|
|
358
|
+
outputLines.push("", "Limit reached; narrow paths.");
|
|
260
359
|
}
|
|
261
360
|
if (dedupedParseErrors.length) {
|
|
262
361
|
outputLines.push("", ...formatParseErrors(dedupedParseErrors));
|
|
@@ -270,10 +369,8 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
270
369
|
label: `AST Edit: ${result.totalReplacements} replacement${previewReplacementPlural} in ${result.filesTouched} file${previewFilePlural}`,
|
|
271
370
|
sourceToolName: this.name,
|
|
272
371
|
apply: async (_reason: string) => {
|
|
273
|
-
const applyResult = await
|
|
372
|
+
const applyResult = await runAstEditOnce(multiTargets, resolvedSearchPath, globFilter, {
|
|
274
373
|
rewrites: normalizedRewrites,
|
|
275
|
-
path: resolvedSearchPath,
|
|
276
|
-
glob: globFilter,
|
|
277
374
|
dryRun: false,
|
|
278
375
|
maxFiles,
|
|
279
376
|
failOnParseError: false,
|
|
@@ -349,7 +446,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
349
446
|
|
|
350
447
|
interface AstEditRenderArgs {
|
|
351
448
|
ops?: Array<{ pat?: string; out?: string }>;
|
|
352
|
-
|
|
449
|
+
paths?: string[];
|
|
353
450
|
}
|
|
354
451
|
|
|
355
452
|
const COLLAPSED_CHANGE_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
@@ -358,7 +455,7 @@ export const astEditToolRenderer = {
|
|
|
358
455
|
inline: true,
|
|
359
456
|
renderCall(args: AstEditRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
360
457
|
const meta: string[] = [];
|
|
361
|
-
if (args.
|
|
458
|
+
if (args.paths?.length) meta.push(`in ${args.paths.join(", ")}`);
|
|
362
459
|
const rewriteCount = args.ops?.length ?? 0;
|
|
363
460
|
if (rewriteCount > 1) meta.push(`${rewriteCount} rewrites`);
|
|
364
461
|
|