pi-hashline-edit-pro 0.13.0 → 0.13.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/index.ts +10 -10
- package/package.json +1 -1
- package/src/hashline/hash.ts +0 -3
- package/src/hashline/index.ts +0 -1
- package/src/hashline/resolve.ts +29 -30
- package/src/replace-normalize.ts +32 -47
- package/src/replace.ts +40 -52
package/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { join, isAbsolute } from "path";
|
|
|
4
4
|
import { lineHashes, initHasher, fmtRegion } from "./src/hashline";
|
|
5
5
|
import { regReplace } from "./src/replace";
|
|
6
6
|
import { regRead, formatPaginationHint } from "./src/read";
|
|
7
|
-
import { toLF } from "./src/replace-diff";
|
|
7
|
+
import { toLF, stripBOM } from "./src/replace-diff";
|
|
8
8
|
import { visLines } from "./src/utils";
|
|
9
9
|
import { AUTO_READ_MAX } from "./src/constants";
|
|
10
10
|
|
|
@@ -12,10 +12,15 @@ export default function (pi: ExtensionAPI): void {
|
|
|
12
12
|
regRead(pi);
|
|
13
13
|
regReplace(pi);
|
|
14
14
|
|
|
15
|
+
const debugValue = process.env.PI_HASHLINE_DEBUG;
|
|
16
|
+
|
|
15
17
|
pi.on("session_start", async (_event, ctx) => {
|
|
16
18
|
const active = pi.getActiveTools();
|
|
17
19
|
pi.setActiveTools(active.filter((t) => t !== "edit"));
|
|
18
20
|
await initHasher();
|
|
21
|
+
if (debugValue === "1" || debugValue === "true") {
|
|
22
|
+
ctx.ui.notify("Hashline Edit mode active", "info");
|
|
23
|
+
}
|
|
19
24
|
});
|
|
20
25
|
|
|
21
26
|
const autoReadValue = process.env.PI_HASHLINE_AUTO_READ;
|
|
@@ -40,8 +45,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
40
45
|
try {
|
|
41
46
|
const absolutePath = isAbsolute(filePath) ? filePath : join(ctx.cwd, filePath);
|
|
42
47
|
const content = await readFile(absolutePath, "utf-8");
|
|
43
|
-
|
|
44
|
-
const normalized = toLF(
|
|
48
|
+
const { text: rawContent } = stripBOM(content);
|
|
49
|
+
const normalized = toLF(rawContent);
|
|
45
50
|
const visibleLines = visLines(normalized);
|
|
46
51
|
|
|
47
52
|
if (visibleLines.length === 0) return;
|
|
@@ -65,14 +70,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
65
70
|
],
|
|
66
71
|
};
|
|
67
72
|
}
|
|
68
|
-
} catch {
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error("Auto-read after write failed:", error);
|
|
69
75
|
}
|
|
70
76
|
});
|
|
71
77
|
|
|
72
|
-
const debugValue = process.env.PI_HASHLINE_DEBUG;
|
|
73
|
-
if (debugValue === "1" || debugValue === "true") {
|
|
74
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
75
|
-
ctx.ui.notify("Hashline Edit mode active", "info");
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
78
|
}
|
package/package.json
CHANGED
package/src/hashline/hash.ts
CHANGED
package/src/hashline/index.ts
CHANGED
package/src/hashline/resolve.ts
CHANGED
|
@@ -222,6 +222,31 @@ export function descEdit(edit: RHEdit): string {
|
|
|
222
222
|
return `replace ${edit.hash_range_inclusive[0].hash}-${edit.hash_range_inclusive[1].hash}`;
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
function checkBoundaryDup(
|
|
226
|
+
adjacentLine: string | undefined,
|
|
227
|
+
replacementEdge: string | undefined,
|
|
228
|
+
kind: "trailing" | "leading",
|
|
229
|
+
survivingLineIndex: number,
|
|
230
|
+
fileLines: string[],
|
|
231
|
+
editIndex: number,
|
|
232
|
+
): BDupWarn | null {
|
|
233
|
+
if (
|
|
234
|
+
adjacentLine === undefined ||
|
|
235
|
+
replacementEdge === undefined ||
|
|
236
|
+
replacementEdge.length === 0 ||
|
|
237
|
+
replacementEdge !== adjacentLine
|
|
238
|
+
) return null;
|
|
239
|
+
return {
|
|
240
|
+
kind,
|
|
241
|
+
survivingLineContent: adjacentLine,
|
|
242
|
+
survivingLineIndex,
|
|
243
|
+
occurrence: fileLines.slice(0, survivingLineIndex).filter((l) => l === adjacentLine).length,
|
|
244
|
+
replacementLineContent: replacementEdge,
|
|
245
|
+
editIndex,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
225
250
|
export function valEdits(
|
|
226
251
|
edits: HEdit[],
|
|
227
252
|
fileLines: string[],
|
|
@@ -258,38 +283,12 @@ export function valEdits(
|
|
|
258
283
|
const endLine = endResolved.line;
|
|
259
284
|
const nextLine = fileLines[endLine];
|
|
260
285
|
const replacementLastLine = edit.content_lines.at(-1);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
replacementLastLine !== undefined &&
|
|
264
|
-
replacementLastLine.length > 0 &&
|
|
265
|
-
replacementLastLine === nextLine
|
|
266
|
-
) {
|
|
267
|
-
boundaryWarnings.push({
|
|
268
|
-
kind: "trailing",
|
|
269
|
-
survivingLineContent: nextLine,
|
|
270
|
-
survivingLineIndex: endLine,
|
|
271
|
-
occurrence: fileLines.slice(0, endLine).filter(l => l === nextLine).length,
|
|
272
|
-
replacementLineContent: replacementLastLine,
|
|
273
|
-
editIndex: resolved.length,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
286
|
+
const trailing = checkBoundaryDup(nextLine, replacementLastLine, "trailing", endLine, fileLines, resolved.length);
|
|
287
|
+
if (trailing) boundaryWarnings.push(trailing);
|
|
276
288
|
const prevLine = fileLines[startResolved.line - 2];
|
|
277
289
|
const replacementFirstLine = edit.content_lines[0];
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
replacementFirstLine !== undefined &&
|
|
281
|
-
replacementFirstLine.length > 0 &&
|
|
282
|
-
replacementFirstLine === prevLine
|
|
283
|
-
) {
|
|
284
|
-
boundaryWarnings.push({
|
|
285
|
-
kind: "leading",
|
|
286
|
-
survivingLineContent: prevLine,
|
|
287
|
-
survivingLineIndex: startResolved.line - 2,
|
|
288
|
-
occurrence: fileLines.slice(0, startResolved.line - 2).filter(l => l === prevLine).length,
|
|
289
|
-
replacementLineContent: replacementFirstLine,
|
|
290
|
-
editIndex: resolved.length,
|
|
291
|
-
});
|
|
292
|
-
}
|
|
290
|
+
const leading = checkBoundaryDup(prevLine, replacementFirstLine, "leading", startResolved.line - 2, fileLines, resolved.length);
|
|
291
|
+
if (leading) boundaryWarnings.push(leading);
|
|
293
292
|
resolved.push({
|
|
294
293
|
hash_range_inclusive: [startResolved, endResolved],
|
|
295
294
|
content_lines: edit.content_lines,
|
package/src/replace-normalize.ts
CHANGED
|
@@ -1,46 +1,12 @@
|
|
|
1
1
|
import { isRec, has } from "./utils";
|
|
2
2
|
|
|
3
|
-
function
|
|
4
|
-
if (typeof
|
|
5
|
-
return changes;
|
|
6
|
-
}
|
|
3
|
+
function tryParseJSON<T>(value: unknown, guard: (v: unknown) => v is T): T | undefined {
|
|
4
|
+
if (typeof value !== "string") return undefined;
|
|
7
5
|
try {
|
|
8
|
-
const parsed: unknown = JSON.parse(
|
|
9
|
-
|
|
10
|
-
} catch {
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function coerceContentLines(changes: unknown): unknown {
|
|
16
|
-
if (!Array.isArray(changes)) return changes;
|
|
17
|
-
return changes.map((change: unknown) => {
|
|
18
|
-
if (!isRec(change)) return change;
|
|
19
|
-
if (typeof change.content_lines !== "string") return change;
|
|
20
|
-
try {
|
|
21
|
-
const parsed: unknown = JSON.parse(change.content_lines);
|
|
22
|
-
if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
|
|
23
|
-
return { ...change, content_lines: parsed };
|
|
24
|
-
}
|
|
25
|
-
} catch {
|
|
26
|
-
// not valid JSON, leave as-is for downstream validation
|
|
27
|
-
}
|
|
28
|
-
return change;
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function coerceChangeItems(changes: unknown): unknown {
|
|
33
|
-
if (!Array.isArray(changes)) return changes;
|
|
34
|
-
return changes.map((item: unknown) => {
|
|
35
|
-
if (typeof item !== "string") return item;
|
|
36
|
-
try {
|
|
37
|
-
const parsed: unknown = JSON.parse(item);
|
|
38
|
-
if (isRec(parsed)) return parsed;
|
|
39
|
-
} catch {
|
|
40
|
-
// not valid JSON, leave as-is for downstream validation
|
|
41
|
-
}
|
|
42
|
-
return item;
|
|
43
|
-
});
|
|
6
|
+
const parsed: unknown = JSON.parse(value);
|
|
7
|
+
if (guard(parsed)) return parsed;
|
|
8
|
+
} catch {}
|
|
9
|
+
return undefined;
|
|
44
10
|
}
|
|
45
11
|
|
|
46
12
|
export function normReq(input: unknown): unknown {
|
|
@@ -59,14 +25,33 @@ export function normReq(input: unknown): unknown {
|
|
|
59
25
|
const hasEditsField = has(record, "edits");
|
|
60
26
|
|
|
61
27
|
if (hasChangesField) {
|
|
62
|
-
record.changes
|
|
63
|
-
|
|
64
|
-
|
|
28
|
+
const raw = tryParseJSON(record.changes, Array.isArray) ?? record.changes;
|
|
29
|
+
if (Array.isArray(raw)) {
|
|
30
|
+
record.changes = raw
|
|
31
|
+
.map((item: unknown) => tryParseJSON(item, isRec) ?? item)
|
|
32
|
+
.map((change: unknown) => {
|
|
33
|
+
if (!isRec(change)) return change;
|
|
34
|
+
if (typeof change.content_lines !== "string") return change;
|
|
35
|
+
const parsed = tryParseJSON(change.content_lines, (v): v is string[] =>
|
|
36
|
+
Array.isArray(v) && v.every((i) => typeof i === "string"),
|
|
37
|
+
);
|
|
38
|
+
return parsed ? { ...change, content_lines: parsed } : change;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
65
41
|
} else if (hasEditsField) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
42
|
+
const raw = tryParseJSON(record.edits, Array.isArray) ?? record.edits;
|
|
43
|
+
if (Array.isArray(raw)) {
|
|
44
|
+
record.changes = raw
|
|
45
|
+
.map((item: unknown) => tryParseJSON(item, isRec) ?? item)
|
|
46
|
+
.map((change: unknown) => {
|
|
47
|
+
if (!isRec(change)) return change;
|
|
48
|
+
if (typeof change.content_lines !== "string") return change;
|
|
49
|
+
const parsed = tryParseJSON(change.content_lines, (v): v is string[] =>
|
|
50
|
+
Array.isArray(v) && v.every((i) => typeof i === "string"),
|
|
51
|
+
);
|
|
52
|
+
return parsed ? { ...change, content_lines: parsed } : change;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
70
55
|
delete record.edits;
|
|
71
56
|
}
|
|
72
57
|
|
package/src/replace.ts
CHANGED
|
@@ -86,6 +86,21 @@ export type ReplaceDetails = {
|
|
|
86
86
|
metrics?: RMetrics;
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
+
interface PipelineResult {
|
|
90
|
+
path: string;
|
|
91
|
+
toolEdits: HTEdit[];
|
|
92
|
+
originalNormalized: string;
|
|
93
|
+
result: string;
|
|
94
|
+
bom: string;
|
|
95
|
+
originalEnding: "\r\n" | "\n";
|
|
96
|
+
hadUtf8DecodeErrors: boolean;
|
|
97
|
+
warnings: string[];
|
|
98
|
+
noopEdits?: { editIndex: number; loc: string; currentContent: string }[];
|
|
99
|
+
firstChangedLine?: number;
|
|
100
|
+
lastChangedLine?: number;
|
|
101
|
+
originalHashes?: string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
89
104
|
const E_DESC = loadP("../prompts/replace.md");
|
|
90
105
|
const E_SNIPPET = loadP("../prompts/replace-snippet.md");
|
|
91
106
|
const E_GUIDE = loadGuide("../prompts/replace-guidelines.md");
|
|
@@ -122,20 +137,7 @@ async function execPipeline(
|
|
|
122
137
|
cwd: string,
|
|
123
138
|
accessMode: number,
|
|
124
139
|
signal?: AbortSignal,
|
|
125
|
-
): Promise<{
|
|
126
|
-
path: string;
|
|
127
|
-
toolEdits: HTEdit[];
|
|
128
|
-
originalNormalized: string;
|
|
129
|
-
result: string;
|
|
130
|
-
bom: string;
|
|
131
|
-
originalEnding: "\r\n" | "\n";
|
|
132
|
-
hadUtf8DecodeErrors: boolean;
|
|
133
|
-
warnings: string[];
|
|
134
|
-
noopEdits?: { editIndex: number; loc: string; currentContent: string }[];
|
|
135
|
-
firstChangedLine?: number;
|
|
136
|
-
lastChangedLine?: number;
|
|
137
|
-
originalHashes?: string[];
|
|
138
|
-
}> {
|
|
140
|
+
): Promise<PipelineResult> {
|
|
139
141
|
|
|
140
142
|
const path = params.path;
|
|
141
143
|
const toolEdits = Array.isArray(params.changes)
|
|
@@ -205,6 +207,21 @@ type ToolDef = ToolDefinition<
|
|
|
205
207
|
ReplaceDetails,
|
|
206
208
|
RRState
|
|
207
209
|
> & { renderShell?: "default" | "self" };
|
|
210
|
+
function reuseText(context: any, content: string): Text {
|
|
211
|
+
const t = context.lastComponent instanceof Text
|
|
212
|
+
? context.lastComponent
|
|
213
|
+
: new Text("", 0, 0);
|
|
214
|
+
t.setText(content);
|
|
215
|
+
return t;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function reuseMarkdown(context: any, content: string, theme: any): Markdown {
|
|
219
|
+
const m = context.lastComponent instanceof Markdown
|
|
220
|
+
? context.lastComponent
|
|
221
|
+
: new Markdown("", 0, 0, mkMdTheme(theme));
|
|
222
|
+
m.setText(content);
|
|
223
|
+
return m;
|
|
224
|
+
}
|
|
208
225
|
|
|
209
226
|
const toolDef: ToolDef = {
|
|
210
227
|
name: "replace",
|
|
@@ -273,10 +290,7 @@ const toolDef: ToolDef = {
|
|
|
273
290
|
|
|
274
291
|
renderResult(result, { isPartial }, theme, context) {
|
|
275
292
|
if (isPartial) {
|
|
276
|
-
|
|
277
|
-
(context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
278
|
-
text.setText(theme.fg("warning", "Editing..."));
|
|
279
|
-
return text;
|
|
293
|
+
return reuseText(context, theme.fg("warning", "Editing..."));
|
|
280
294
|
}
|
|
281
295
|
|
|
282
296
|
const typedResult = result as {
|
|
@@ -292,44 +306,18 @@ const toolDef: ToolDef = {
|
|
|
292
306
|
}
|
|
293
307
|
|
|
294
308
|
if (context.isError) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const text =
|
|
299
|
-
context.lastComponent instanceof Text
|
|
300
|
-
? context.lastComponent
|
|
301
|
-
: new Text("", 0, 0);
|
|
302
|
-
text.setText(`\n${theme.fg("error", renderedText)}`);
|
|
303
|
-
return text;
|
|
309
|
+
return renderedText
|
|
310
|
+
? reuseText(context, `\n${theme.fg("error", renderedText)}`)
|
|
311
|
+
: new Text("", 0, 0);
|
|
304
312
|
}
|
|
305
313
|
|
|
306
314
|
if (isApplied(typedResult.details)) {
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
typedResult.details,
|
|
310
|
-
theme,
|
|
311
|
-
);
|
|
312
|
-
if (!appliedChangedText) {
|
|
313
|
-
return new Text("", 0, 0);
|
|
314
|
-
}
|
|
315
|
-
const text =
|
|
316
|
-
context.lastComponent instanceof Text
|
|
317
|
-
? context.lastComponent
|
|
318
|
-
: new Text("", 0, 0);
|
|
319
|
-
text.setText(appliedChangedText);
|
|
320
|
-
return text;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (!renderedText) {
|
|
324
|
-
return new Text("", 0, 0);
|
|
315
|
+
const appliedText = buildAppliedText(renderedText, typedResult.details, theme);
|
|
316
|
+
return appliedText ? reuseText(context, appliedText) : new Text("", 0, 0);
|
|
325
317
|
}
|
|
326
318
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
? context.lastComponent
|
|
330
|
-
: new Markdown("", 0, 0, mkMdTheme(theme));
|
|
331
|
-
markdown.setText(fmtResultMd(renderedText));
|
|
332
|
-
return markdown;
|
|
319
|
+
if (!renderedText) return new Text("", 0, 0);
|
|
320
|
+
return reuseMarkdown(context, fmtResultMd(renderedText), theme);
|
|
333
321
|
},
|
|
334
322
|
|
|
335
323
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -353,7 +341,7 @@ const toolDef: ToolDef = {
|
|
|
353
341
|
firstChangedLine,
|
|
354
342
|
lastChangedLine,
|
|
355
343
|
} = await execPipeline(
|
|
356
|
-
|
|
344
|
+
normalizedParams,
|
|
357
345
|
ctx.cwd,
|
|
358
346
|
constants.R_OK | constants.W_OK,
|
|
359
347
|
signal,
|