@oh-my-pi/pi-coding-agent 12.19.0 → 12.19.2
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 +6 -0
- package/package.json +35 -27
- package/src/patch/hashline.ts +6 -286
- package/src/patch/index.ts +2 -53
- package/src/prompts/tools/hashline.md +0 -16
- package/src/session/agent-session.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [12.19.1] - 2026-02-22
|
|
6
|
+
### Removed
|
|
7
|
+
|
|
8
|
+
- Removed `replaceText` edit operation from hashline mode (substring-based text replacement)
|
|
9
|
+
- Removed autocorrect heuristics that attempted to detect and fix line merges and formatting rewrites in hashline edits
|
|
10
|
+
|
|
5
11
|
## [12.19.0] - 2026-02-22
|
|
6
12
|
### Added
|
|
7
13
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "12.19.
|
|
4
|
+
"version": "12.19.2",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
|
-
"author": "Can
|
|
7
|
+
"author": "Can Boluk",
|
|
8
8
|
"contributors": [
|
|
9
9
|
"Mario Zechner"
|
|
10
10
|
],
|
|
@@ -40,33 +40,33 @@
|
|
|
40
40
|
"test": "bun test"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@mozilla/readability": "0.6
|
|
44
|
-
"@oh-my-pi/omp-stats": "12.19.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "12.19.
|
|
46
|
-
"@oh-my-pi/pi-ai": "12.19.
|
|
47
|
-
"@oh-my-pi/pi-natives": "12.19.
|
|
48
|
-
"@oh-my-pi/pi-tui": "12.19.
|
|
49
|
-
"@oh-my-pi/pi-utils": "12.19.
|
|
50
|
-
"@sinclair/typebox": "^0.34
|
|
51
|
-
"@xterm/headless": "^6.0
|
|
52
|
-
"ajv": "^8.18
|
|
53
|
-
"chalk": "^5.6
|
|
54
|
-
"diff": "^8.0
|
|
55
|
-
"file-type": "^21.3
|
|
56
|
-
"glob": "^13.0
|
|
57
|
-
"handlebars": "^4.7
|
|
58
|
-
"ignore": "^7.0
|
|
59
|
-
"linkedom": "^0.18
|
|
60
|
-
"marked": "^17.0
|
|
61
|
-
"node-html-parser": "^7.0
|
|
62
|
-
"puppeteer": "^24.37
|
|
63
|
-
"smol-toml": "^1.6
|
|
64
|
-
"zod": "^4.3
|
|
43
|
+
"@mozilla/readability": "^0.6",
|
|
44
|
+
"@oh-my-pi/omp-stats": "12.19.2",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "12.19.2",
|
|
46
|
+
"@oh-my-pi/pi-ai": "12.19.2",
|
|
47
|
+
"@oh-my-pi/pi-natives": "12.19.2",
|
|
48
|
+
"@oh-my-pi/pi-tui": "12.19.2",
|
|
49
|
+
"@oh-my-pi/pi-utils": "12.19.2",
|
|
50
|
+
"@sinclair/typebox": "^0.34",
|
|
51
|
+
"@xterm/headless": "^6.0",
|
|
52
|
+
"ajv": "^8.18",
|
|
53
|
+
"chalk": "^5.6",
|
|
54
|
+
"diff": "^8.0",
|
|
55
|
+
"file-type": "^21.3",
|
|
56
|
+
"glob": "^13.0",
|
|
57
|
+
"handlebars": "^4.7",
|
|
58
|
+
"ignore": "^7.0",
|
|
59
|
+
"linkedom": "^0.18",
|
|
60
|
+
"marked": "^17.0",
|
|
61
|
+
"node-html-parser": "^7.0",
|
|
62
|
+
"puppeteer": "^24.37",
|
|
63
|
+
"smol-toml": "^1.6",
|
|
64
|
+
"zod": "^4.3"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@types/bun": "^1.3
|
|
68
|
-
"@types/ms": "^2.1
|
|
69
|
-
"ms": "^2.1
|
|
67
|
+
"@types/bun": "^1.3",
|
|
68
|
+
"@types/ms": "^2.1",
|
|
69
|
+
"ms": "^2.1"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|
|
72
72
|
"bun": ">=1.3.7"
|
|
@@ -87,6 +87,14 @@
|
|
|
87
87
|
"types": "./src/*.ts",
|
|
88
88
|
"import": "./src/*.ts"
|
|
89
89
|
},
|
|
90
|
+
"./async": {
|
|
91
|
+
"types": "./src/async/index.ts",
|
|
92
|
+
"import": "./src/async/index.ts"
|
|
93
|
+
},
|
|
94
|
+
"./async/*": {
|
|
95
|
+
"types": "./src/async/*.ts",
|
|
96
|
+
"import": "./src/async/*.ts"
|
|
97
|
+
},
|
|
90
98
|
"./capability": {
|
|
91
99
|
"types": "./src/capability/index.ts",
|
|
92
100
|
"import": "./src/capability/index.ts"
|
package/src/patch/hashline.ts
CHANGED
|
@@ -21,159 +21,6 @@ export type HashlineEdit =
|
|
|
21
21
|
| { op: "append"; after?: LineTag; content: string[] }
|
|
22
22
|
| { op: "prepend"; before?: LineTag; content: string[] }
|
|
23
23
|
| { op: "insert"; after: LineTag; before: LineTag; content: string[] };
|
|
24
|
-
export type ReplaceTextEdit = { op: "replaceText"; old_text: string; new_text: string; all?: boolean };
|
|
25
|
-
export type EditSpec = HashlineEdit | ReplaceTextEdit;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Compare two strings ignoring all whitespace differences.
|
|
29
|
-
*
|
|
30
|
-
* Returns true when the non-whitespace characters are identical — meaning
|
|
31
|
-
* the only differences are in spaces, tabs, or other whitespace.
|
|
32
|
-
*/
|
|
33
|
-
function equalsIgnoringWhitespace(a: string, b: string): boolean {
|
|
34
|
-
// Fast path: identical strings
|
|
35
|
-
if (a === b) return true;
|
|
36
|
-
// Compare with all whitespace removed
|
|
37
|
-
return a.replace(/\s+/g, "") === b.replace(/\s+/g, "");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function stripAllWhitespace(s: string): string {
|
|
41
|
-
return s.replace(/\s+/g, "");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function stripTrailingContinuationTokens(s: string): string {
|
|
45
|
-
// Heuristic: models often merge a continuation line into the prior line
|
|
46
|
-
// while also changing the trailing operator (e.g. `&&` → `||`).
|
|
47
|
-
// Strip common trailing continuation tokens so we can still detect merges.
|
|
48
|
-
return s.replace(/(?:&&|\|\||\?\?|\?|:|=|,|\+|-|\*|\/|\.|\()\s*$/u, "");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function stripMergeOperatorChars(s: string): string {
|
|
52
|
-
// Used for merge detection when the model changes a logical operator like
|
|
53
|
-
// `||` → `??` while also merging adjacent lines.
|
|
54
|
-
return s.replace(/[|&?]/g, "");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function leadingWhitespace(s: string): string {
|
|
58
|
-
const match = s.match(/^\s*/);
|
|
59
|
-
return match ? match[0] : "";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function restoreLeadingIndent(templateLine: string, line: string): string {
|
|
63
|
-
if (line.length === 0) return line;
|
|
64
|
-
const templateIndent = leadingWhitespace(templateLine);
|
|
65
|
-
if (templateIndent.length === 0) return line;
|
|
66
|
-
const indent = leadingWhitespace(line);
|
|
67
|
-
if (indent.length > 0) return line;
|
|
68
|
-
return templateIndent + line;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function restoreIndentForPairedReplacement(oldLines: string[], newLines: string[]): string[] {
|
|
72
|
-
if (oldLines.length !== newLines.length) return newLines;
|
|
73
|
-
let changed = false;
|
|
74
|
-
const out = new Array<string>(newLines.length);
|
|
75
|
-
for (let i = 0; i < newLines.length; i++) {
|
|
76
|
-
const restored = restoreLeadingIndent(oldLines[i], newLines[i]);
|
|
77
|
-
out[i] = restored;
|
|
78
|
-
if (restored !== newLines[i]) changed = true;
|
|
79
|
-
}
|
|
80
|
-
return changed ? out : newLines;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Undo pure formatting rewrites where the model reflows a single logical line
|
|
85
|
-
* into multiple lines (or similar), but the token stream is identical.
|
|
86
|
-
*/
|
|
87
|
-
function restoreOldWrappedLines(oldLines: string[], newLines: string[]): string[] {
|
|
88
|
-
if (oldLines.length === 0 || newLines.length < 2) return newLines;
|
|
89
|
-
|
|
90
|
-
const canonToOld = new Map<string, { line: string; count: number }>();
|
|
91
|
-
for (const line of oldLines) {
|
|
92
|
-
const canon = stripAllWhitespace(line);
|
|
93
|
-
const bucket = canonToOld.get(canon);
|
|
94
|
-
if (bucket) bucket.count++;
|
|
95
|
-
else canonToOld.set(canon, { line, count: 1 });
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const candidates: { start: number; len: number; replacement: string; canon: string }[] = [];
|
|
99
|
-
for (let start = 0; start < newLines.length; start++) {
|
|
100
|
-
for (let len = 2; len <= 10 && start + len <= newLines.length; len++) {
|
|
101
|
-
const canonSpan = stripAllWhitespace(newLines.slice(start, start + len).join(""));
|
|
102
|
-
const old = canonToOld.get(canonSpan);
|
|
103
|
-
if (old && old.count === 1 && canonSpan.length >= 6) {
|
|
104
|
-
candidates.push({ start, len, replacement: old.line, canon: canonSpan });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (candidates.length === 0) return newLines;
|
|
109
|
-
|
|
110
|
-
// Keep only spans whose canonical match is unique in the new output.
|
|
111
|
-
const canonCounts = new Map<string, number>();
|
|
112
|
-
for (const c of candidates) {
|
|
113
|
-
canonCounts.set(c.canon, (canonCounts.get(c.canon) ?? 0) + 1);
|
|
114
|
-
}
|
|
115
|
-
const uniqueCandidates = candidates.filter(c => (canonCounts.get(c.canon) ?? 0) === 1);
|
|
116
|
-
if (uniqueCandidates.length === 0) return newLines;
|
|
117
|
-
|
|
118
|
-
// Apply replacements back-to-front so indices remain stable.
|
|
119
|
-
uniqueCandidates.sort((a, b) => b.start - a.start);
|
|
120
|
-
const out = [...newLines];
|
|
121
|
-
for (const c of uniqueCandidates) {
|
|
122
|
-
out.splice(c.start, c.len, c.replacement);
|
|
123
|
-
}
|
|
124
|
-
return out;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function stripInsertAnchorEchoAfter(anchorLine: string, dstLines: string[]): string[] {
|
|
128
|
-
if (dstLines.length <= 1) return dstLines;
|
|
129
|
-
if (equalsIgnoringWhitespace(dstLines[0], anchorLine)) {
|
|
130
|
-
return dstLines.slice(1);
|
|
131
|
-
}
|
|
132
|
-
return dstLines;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function stripInsertAnchorEchoBefore(anchorLine: string, dstLines: string[]): string[] {
|
|
136
|
-
if (dstLines.length <= 1) return dstLines;
|
|
137
|
-
if (equalsIgnoringWhitespace(dstLines[dstLines.length - 1], anchorLine)) {
|
|
138
|
-
return dstLines.slice(0, -1);
|
|
139
|
-
}
|
|
140
|
-
return dstLines;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function stripInsertBoundaryEcho(afterLine: string, beforeLine: string, dstLines: string[]): string[] {
|
|
144
|
-
let out = dstLines;
|
|
145
|
-
if (out.length > 1 && equalsIgnoringWhitespace(out[0], afterLine)) {
|
|
146
|
-
out = out.slice(1);
|
|
147
|
-
}
|
|
148
|
-
if (out.length > 1 && equalsIgnoringWhitespace(out[out.length - 1], beforeLine)) {
|
|
149
|
-
out = out.slice(0, -1);
|
|
150
|
-
}
|
|
151
|
-
return out;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function stripRangeBoundaryEcho(fileLines: string[], startLine: number, endLine: number, dstLines: string[]): string[] {
|
|
155
|
-
// Only strip when the model replaced with multiple lines and grew the edit.
|
|
156
|
-
// This avoids turning a single-line replacement into a deletion.
|
|
157
|
-
const count = endLine - startLine + 1;
|
|
158
|
-
if (dstLines.length <= 1 || dstLines.length <= count) return dstLines;
|
|
159
|
-
|
|
160
|
-
let out = dstLines;
|
|
161
|
-
const beforeIdx = startLine - 2;
|
|
162
|
-
if (beforeIdx >= 0 && equalsIgnoringWhitespace(out[0], fileLines[beforeIdx])) {
|
|
163
|
-
out = out.slice(1);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const afterIdx = endLine;
|
|
167
|
-
if (
|
|
168
|
-
afterIdx < fileLines.length &&
|
|
169
|
-
out.length > 0 &&
|
|
170
|
-
equalsIgnoringWhitespace(out[out.length - 1], fileLines[afterIdx])
|
|
171
|
-
) {
|
|
172
|
-
out = out.slice(0, -1);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return out;
|
|
176
|
-
}
|
|
177
24
|
|
|
178
25
|
const NIBBLE_STR = "ZPMQVRWSNKTXJBYH";
|
|
179
26
|
|
|
@@ -594,38 +441,6 @@ export function applyHashlineEdits(
|
|
|
594
441
|
let firstChangedLine: number | undefined;
|
|
595
442
|
const noopEdits: Array<{ editIndex: number; loc: string; currentContent: string }> = [];
|
|
596
443
|
|
|
597
|
-
const autocorrect = Bun.env.PI_HL_AUTOCORRECT === "1";
|
|
598
|
-
|
|
599
|
-
function collectExplicitlyTouchedLines(): Set<number> {
|
|
600
|
-
const touched = new Set<number>();
|
|
601
|
-
for (const edit of edits) {
|
|
602
|
-
switch (edit.op) {
|
|
603
|
-
case "set":
|
|
604
|
-
touched.add(edit.tag.line);
|
|
605
|
-
break;
|
|
606
|
-
case "replace":
|
|
607
|
-
for (let ln = edit.first.line; ln <= edit.last.line; ln++) touched.add(ln);
|
|
608
|
-
break;
|
|
609
|
-
case "append":
|
|
610
|
-
if (edit.after) {
|
|
611
|
-
touched.add(edit.after.line);
|
|
612
|
-
}
|
|
613
|
-
break;
|
|
614
|
-
case "prepend":
|
|
615
|
-
if (edit.before) {
|
|
616
|
-
touched.add(edit.before.line);
|
|
617
|
-
}
|
|
618
|
-
break;
|
|
619
|
-
case "insert":
|
|
620
|
-
touched.add(edit.after.line);
|
|
621
|
-
touched.add(edit.before.line);
|
|
622
|
-
break;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
return touched;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const explicitlyTouchedLines = collectExplicitlyTouchedLines();
|
|
629
444
|
// Pre-validate: collect all hash mismatches before mutating
|
|
630
445
|
const mismatches: HashMismatch[] = [];
|
|
631
446
|
function validateRef(ref: { line: number; hash: string }): boolean {
|
|
@@ -765,35 +580,8 @@ export function applyHashlineEdits(
|
|
|
765
580
|
for (const { edit, idx } of annotated) {
|
|
766
581
|
switch (edit.op) {
|
|
767
582
|
case "set": {
|
|
768
|
-
const merged = autocorrect ? maybeExpandSingleLineMerge(edit.tag.line, edit.content) : null;
|
|
769
|
-
if (merged) {
|
|
770
|
-
const origLines = originalFileLines.slice(
|
|
771
|
-
merged.startLine - 1,
|
|
772
|
-
merged.startLine - 1 + merged.deleteCount,
|
|
773
|
-
);
|
|
774
|
-
let nextLines = merged.newLines;
|
|
775
|
-
nextLines = restoreIndentForPairedReplacement([origLines[0] ?? ""], nextLines);
|
|
776
|
-
|
|
777
|
-
if (origLines.every((line, i) => line === nextLines[i])) {
|
|
778
|
-
noopEdits.push({
|
|
779
|
-
editIndex: idx,
|
|
780
|
-
loc: `${edit.tag.line}#${edit.tag.hash}`,
|
|
781
|
-
currentContent: origLines.join("\n"),
|
|
782
|
-
});
|
|
783
|
-
break;
|
|
784
|
-
}
|
|
785
|
-
fileLines.splice(merged.startLine - 1, merged.deleteCount, ...nextLines);
|
|
786
|
-
trackFirstChanged(merged.startLine);
|
|
787
|
-
break;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
const count = 1;
|
|
791
583
|
const origLines = originalFileLines.slice(edit.tag.line - 1, edit.tag.line);
|
|
792
|
-
|
|
793
|
-
? stripRangeBoundaryEcho(originalFileLines, edit.tag.line, edit.tag.line, edit.content)
|
|
794
|
-
: edit.content;
|
|
795
|
-
stripped = autocorrect ? restoreOldWrappedLines(origLines, stripped) : stripped;
|
|
796
|
-
const newLines = autocorrect ? restoreIndentForPairedReplacement(origLines, stripped) : stripped;
|
|
584
|
+
const newLines = edit.content;
|
|
797
585
|
if (origLines.every((line, i) => line === newLines[i])) {
|
|
798
586
|
noopEdits.push({
|
|
799
587
|
editIndex: idx,
|
|
@@ -802,36 +590,19 @@ export function applyHashlineEdits(
|
|
|
802
590
|
});
|
|
803
591
|
break;
|
|
804
592
|
}
|
|
805
|
-
fileLines.splice(edit.tag.line - 1,
|
|
593
|
+
fileLines.splice(edit.tag.line - 1, 1, ...newLines);
|
|
806
594
|
trackFirstChanged(edit.tag.line);
|
|
807
595
|
break;
|
|
808
596
|
}
|
|
809
597
|
case "replace": {
|
|
810
598
|
const count = edit.last.line - edit.first.line + 1;
|
|
811
|
-
const
|
|
812
|
-
let stripped = autocorrect
|
|
813
|
-
? stripRangeBoundaryEcho(originalFileLines, edit.first.line, edit.last.line, edit.content)
|
|
814
|
-
: edit.content;
|
|
815
|
-
stripped = autocorrect ? restoreOldWrappedLines(origLines, stripped) : stripped;
|
|
816
|
-
const newLines = autocorrect ? restoreIndentForPairedReplacement(origLines, stripped) : stripped;
|
|
817
|
-
if (autocorrect && origLines.every((line, i) => line === newLines[i])) {
|
|
818
|
-
noopEdits.push({
|
|
819
|
-
editIndex: idx,
|
|
820
|
-
loc: `${edit.first.line}#${edit.first.hash}`,
|
|
821
|
-
currentContent: origLines.join("\n"),
|
|
822
|
-
});
|
|
823
|
-
break;
|
|
824
|
-
}
|
|
599
|
+
const newLines = edit.content;
|
|
825
600
|
fileLines.splice(edit.first.line - 1, count, ...newLines);
|
|
826
601
|
trackFirstChanged(edit.first.line);
|
|
827
602
|
break;
|
|
828
603
|
}
|
|
829
604
|
case "append": {
|
|
830
|
-
const inserted = edit.
|
|
831
|
-
? autocorrect
|
|
832
|
-
? stripInsertAnchorEchoAfter(originalFileLines[edit.after.line - 1], edit.content)
|
|
833
|
-
: edit.content
|
|
834
|
-
: edit.content;
|
|
605
|
+
const inserted = edit.content;
|
|
835
606
|
if (inserted.length === 0) {
|
|
836
607
|
noopEdits.push({
|
|
837
608
|
editIndex: idx,
|
|
@@ -855,11 +626,7 @@ export function applyHashlineEdits(
|
|
|
855
626
|
break;
|
|
856
627
|
}
|
|
857
628
|
case "prepend": {
|
|
858
|
-
const inserted = edit.
|
|
859
|
-
? autocorrect
|
|
860
|
-
? stripInsertAnchorEchoBefore(originalFileLines[edit.before.line - 1], edit.content)
|
|
861
|
-
: edit.content
|
|
862
|
-
: edit.content;
|
|
629
|
+
const inserted = edit.content;
|
|
863
630
|
if (inserted.length === 0) {
|
|
864
631
|
noopEdits.push({
|
|
865
632
|
editIndex: idx,
|
|
@@ -884,7 +651,7 @@ export function applyHashlineEdits(
|
|
|
884
651
|
case "insert": {
|
|
885
652
|
const afterLine = originalFileLines[edit.after.line - 1];
|
|
886
653
|
const beforeLine = originalFileLines[edit.before.line - 1];
|
|
887
|
-
const inserted =
|
|
654
|
+
const inserted = edit.content;
|
|
888
655
|
if (inserted.length === 0) {
|
|
889
656
|
noopEdits.push({
|
|
890
657
|
editIndex: idx,
|
|
@@ -911,51 +678,4 @@ export function applyHashlineEdits(
|
|
|
911
678
|
firstChangedLine = line;
|
|
912
679
|
}
|
|
913
680
|
}
|
|
914
|
-
|
|
915
|
-
function maybeExpandSingleLineMerge(
|
|
916
|
-
line: number,
|
|
917
|
-
content: string[],
|
|
918
|
-
): { startLine: number; deleteCount: number; newLines: string[] } | null {
|
|
919
|
-
if (content.length !== 1) return null;
|
|
920
|
-
if (line < 1 || line > fileLines.length) return null;
|
|
921
|
-
|
|
922
|
-
const newLine = content[0];
|
|
923
|
-
const newCanon = stripAllWhitespace(newLine);
|
|
924
|
-
const newCanonForMergeOps = stripMergeOperatorChars(newCanon);
|
|
925
|
-
if (newCanon.length === 0) return null;
|
|
926
|
-
|
|
927
|
-
const orig = fileLines[line - 1];
|
|
928
|
-
const origCanon = stripAllWhitespace(orig);
|
|
929
|
-
const origCanonForMatch = stripTrailingContinuationTokens(origCanon);
|
|
930
|
-
const origCanonForMergeOps = stripMergeOperatorChars(origCanon);
|
|
931
|
-
const origLooksLikeContinuation = origCanonForMatch.length < origCanon.length;
|
|
932
|
-
if (origCanon.length === 0) return null;
|
|
933
|
-
const nextIdx = line;
|
|
934
|
-
const prevIdx = line - 2;
|
|
935
|
-
// Case A: dst absorbed the next continuation line.
|
|
936
|
-
if (origLooksLikeContinuation && nextIdx < fileLines.length && !explicitlyTouchedLines.has(line + 1)) {
|
|
937
|
-
const next = fileLines[nextIdx];
|
|
938
|
-
const nextCanon = stripAllWhitespace(next);
|
|
939
|
-
const a = newCanon.indexOf(origCanonForMatch);
|
|
940
|
-
const b = newCanon.indexOf(nextCanon);
|
|
941
|
-
if (a !== -1 && b !== -1 && a < b && newCanon.length <= origCanon.length + nextCanon.length + 32) {
|
|
942
|
-
return { startLine: line, deleteCount: 2, newLines: [newLine] };
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
// Case B: dst absorbed the previous declaration/continuation line.
|
|
946
|
-
if (prevIdx >= 0 && !explicitlyTouchedLines.has(line - 1)) {
|
|
947
|
-
const prev = fileLines[prevIdx];
|
|
948
|
-
const prevCanon = stripAllWhitespace(prev);
|
|
949
|
-
const prevCanonForMatch = stripTrailingContinuationTokens(prevCanon);
|
|
950
|
-
const prevLooksLikeContinuation = prevCanonForMatch.length < prevCanon.length;
|
|
951
|
-
if (!prevLooksLikeContinuation) return null;
|
|
952
|
-
const a = newCanonForMergeOps.indexOf(stripMergeOperatorChars(prevCanonForMatch));
|
|
953
|
-
const b = newCanonForMergeOps.indexOf(origCanonForMergeOps);
|
|
954
|
-
if (a !== -1 && b !== -1 && a < b && newCanon.length <= prevCanon.length + origCanon.length + 32) {
|
|
955
|
-
return { startLine: line - 1, deleteCount: 2, newLines: [newLine] };
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
return null;
|
|
960
|
-
}
|
|
961
681
|
}
|
package/src/patch/index.ts
CHANGED
|
@@ -34,14 +34,7 @@ import { enforcePlanModeWrite, resolvePlanPath } from "../tools/plan-mode-guard"
|
|
|
34
34
|
import { applyPatch } from "./applicator";
|
|
35
35
|
import { generateDiffString, generateUnifiedDiffString, replaceText } from "./diff";
|
|
36
36
|
import { findMatch } from "./fuzzy";
|
|
37
|
-
import {
|
|
38
|
-
applyHashlineEdits,
|
|
39
|
-
computeLineHash,
|
|
40
|
-
type HashlineEdit,
|
|
41
|
-
type LineTag,
|
|
42
|
-
parseTag,
|
|
43
|
-
type ReplaceTextEdit,
|
|
44
|
-
} from "./hashline";
|
|
37
|
+
import { applyHashlineEdits, computeLineHash, type HashlineEdit, type LineTag, parseTag } from "./hashline";
|
|
45
38
|
import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize";
|
|
46
39
|
import { buildNormativeUpdateInput } from "./normative";
|
|
47
40
|
import { type EditToolDetails, getLspBatchRequest } from "./shared";
|
|
@@ -201,13 +194,6 @@ export function hashlineParseContent(edit: string | string[] | null): string[] {
|
|
|
201
194
|
if (lines[lines.length - 1].trim() === "") return lines.slice(0, -1);
|
|
202
195
|
return lines;
|
|
203
196
|
}
|
|
204
|
-
|
|
205
|
-
function hashlineParseContentString(edit: string | string[] | null): string {
|
|
206
|
-
if (edit === null) return "";
|
|
207
|
-
if (Array.isArray(edit)) return edit.join("\n");
|
|
208
|
-
return edit;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
197
|
const hashlineTargetEditSchema = Type.Object(
|
|
212
198
|
{
|
|
213
199
|
op: Type.Literal("set"),
|
|
@@ -255,25 +241,12 @@ const hashlineInsertEditSchema = Type.Object(
|
|
|
255
241
|
{ additionalProperties: false },
|
|
256
242
|
);
|
|
257
243
|
|
|
258
|
-
const hashlineReplaceTextEditSchema = Type.Object(
|
|
259
|
-
{
|
|
260
|
-
op: Type.Literal("replaceText"),
|
|
261
|
-
old_text: Type.String({ description: "Text to find", minLength: 1 }),
|
|
262
|
-
new_text: hashlineReplaceContentFormat("Replacement"),
|
|
263
|
-
all: Type.Optional(Type.Boolean({ description: "Replace all occurrences" })),
|
|
264
|
-
},
|
|
265
|
-
{ additionalProperties: false },
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
const HL_REPLACE_ENABLED = Bun.env.PI_HL_REPLACETXT === "1";
|
|
269
|
-
|
|
270
244
|
const hashlineEditSpecSchema = Type.Union([
|
|
271
245
|
hashlineTargetEditSchema,
|
|
272
246
|
hashlineRangeEditSchema,
|
|
273
247
|
hashlineAppendEditSchema,
|
|
274
248
|
hashlinePrependEditSchema,
|
|
275
249
|
hashlineInsertEditSchema,
|
|
276
|
-
...(HL_REPLACE_ENABLED ? [hashlineReplaceTextEditSchema] : []),
|
|
277
250
|
]);
|
|
278
251
|
|
|
279
252
|
const hashlineEditSchema = Type.Object(
|
|
@@ -486,7 +459,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
486
459
|
case "patch":
|
|
487
460
|
return renderPromptTemplate(patchDescription);
|
|
488
461
|
case "hashline":
|
|
489
|
-
return renderPromptTemplate(hashlineDescription
|
|
462
|
+
return renderPromptTemplate(hashlineDescription);
|
|
490
463
|
default:
|
|
491
464
|
return renderPromptTemplate(replaceDescription);
|
|
492
465
|
}
|
|
@@ -581,7 +554,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
581
554
|
}
|
|
582
555
|
|
|
583
556
|
const anchorEdits: HashlineEdit[] = [];
|
|
584
|
-
const replaceEdits: ReplaceTextEdit[] = [];
|
|
585
557
|
for (const edit of edits) {
|
|
586
558
|
switch (edit.op) {
|
|
587
559
|
case "set": {
|
|
@@ -643,16 +615,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
643
615
|
}
|
|
644
616
|
break;
|
|
645
617
|
}
|
|
646
|
-
case "replaceText": {
|
|
647
|
-
const { old_text, new_text, all } = edit;
|
|
648
|
-
replaceEdits.push({
|
|
649
|
-
op: "replaceText",
|
|
650
|
-
old_text: old_text,
|
|
651
|
-
new_text: hashlineParseContentString(new_text),
|
|
652
|
-
all: all ?? false,
|
|
653
|
-
});
|
|
654
|
-
break;
|
|
655
|
-
}
|
|
656
618
|
default:
|
|
657
619
|
throw new Error(`Invalid edit operation: ${JSON.stringify(edit)}`);
|
|
658
620
|
}
|
|
@@ -668,19 +630,6 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
668
630
|
const anchorResult = applyHashlineEdits(normalizedContent, anchorEdits);
|
|
669
631
|
normalizedContent = anchorResult.content;
|
|
670
632
|
|
|
671
|
-
// Apply content-replace edits (substr-style fuzzy replace)
|
|
672
|
-
for (const r of replaceEdits) {
|
|
673
|
-
if (r.old_text.length === 0) {
|
|
674
|
-
throw new Error("old_text must not be empty.");
|
|
675
|
-
}
|
|
676
|
-
const rep = replaceText(normalizedContent, r.old_text, r.new_text, {
|
|
677
|
-
fuzzy: this.#allowFuzzy,
|
|
678
|
-
all: r.all ?? false,
|
|
679
|
-
threshold: this.#fuzzyThreshold,
|
|
680
|
-
});
|
|
681
|
-
normalizedContent = rep.content;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
633
|
const result = {
|
|
685
634
|
content: normalizedContent,
|
|
686
635
|
firstChangedLine: anchorResult.firstChangedLine,
|
|
@@ -22,10 +22,6 @@ Apply precise file edits using `LINE#ID` tags, anchoring to the file content.
|
|
|
22
22
|
- `{ op: "prepend", before: "N#ID", content: […] }` or `{ op: "prepend", content: […] }` (no `before` = insert at beginning of file)
|
|
23
23
|
- `{ op: "append", after: "N#ID", content: […] }` or `{ op: "append", content: […] }` (no `after` = insert at end of file)
|
|
24
24
|
- `{ op: "insert", after: "N#ID", before: "N#ID", content: […] }` (between adjacent anchors; safest for blocks)
|
|
25
|
-
{{#if allowReplaceText}}
|
|
26
|
-
- **Content replace**
|
|
27
|
-
- `{ op: "replaceText", old_text: "…", new_text: "…", all?: boolean }`
|
|
28
|
-
{{/if}}
|
|
29
25
|
- **File-level controls**
|
|
30
26
|
- `{ delete: true, edits: [] }` deletes the file (cannot be combined with `rename`).
|
|
31
27
|
- `{ rename: "new/path.ts", edits: […] }` writes result to new path and removes old path.
|
|
@@ -180,18 +176,6 @@ content: ["function validate(data: unknown): boolean {", " return data != null
|
|
|
180
176
|
The trailing `""` in `content` preserves the blank-line separator. **Anchor to the structural line (`export function ...`), not the blank line above it** — blank lines are ambiguous and may be added or removed by other edits.
|
|
181
177
|
</example>
|
|
182
178
|
|
|
183
|
-
{{#if allowReplaceText}}
|
|
184
|
-
<example name="content replace (rare)">
|
|
185
|
-
```
|
|
186
|
-
op: "replaceText"
|
|
187
|
-
old_text: "x = 42"
|
|
188
|
-
new_text: "x = 99"
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
Use only when line anchors aren't available. `old_text` must match exactly one location in the file (or set `"all": true` for all occurrences).
|
|
192
|
-
</example>
|
|
193
|
-
{{/if}}
|
|
194
|
-
|
|
195
179
|
<example name="file delete">
|
|
196
180
|
```
|
|
197
181
|
path: "src/deprecated/legacy.ts"
|
|
@@ -2224,6 +2224,7 @@ export class AgentSession {
|
|
|
2224
2224
|
|
|
2225
2225
|
this.#disconnectFromAgent();
|
|
2226
2226
|
await this.abort();
|
|
2227
|
+
this.#asyncJobManager?.cancelAll();
|
|
2227
2228
|
this.agent.reset();
|
|
2228
2229
|
await this.sessionManager.flush();
|
|
2229
2230
|
await this.sessionManager.newSession(options);
|
|
@@ -2933,6 +2934,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2933
2934
|
|
|
2934
2935
|
// Start a new session
|
|
2935
2936
|
await this.sessionManager.flush();
|
|
2937
|
+
this.#asyncJobManager?.cancelAll();
|
|
2936
2938
|
await this.sessionManager.newSession();
|
|
2937
2939
|
this.agent.reset();
|
|
2938
2940
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
@@ -4126,6 +4128,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
4126
4128
|
|
|
4127
4129
|
// Flush pending writes before branching
|
|
4128
4130
|
await this.sessionManager.flush();
|
|
4131
|
+
this.#asyncJobManager?.cancelAll();
|
|
4129
4132
|
|
|
4130
4133
|
if (!selectedEntry.parentId) {
|
|
4131
4134
|
await this.sessionManager.newSession({ parentSession: previousSessionFile });
|