@heyhuynhgiabuu/pi-diff 0.6.10 → 0.7.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/README.md +4 -0
- package/dist/core/hashline-edit.d.ts +17 -0
- package/dist/core/hashline-edit.d.ts.map +1 -0
- package/dist/core/hashline-edit.js +42 -0
- package/dist/core/hashline-edit.js.map +1 -0
- package/dist/core/hashline-execute.d.ts +24 -0
- package/dist/core/hashline-execute.d.ts.map +1 -0
- package/dist/core/hashline-execute.js +94 -0
- package/dist/core/hashline-execute.js.map +1 -0
- package/dist/core/text-encoding.d.ts +19 -0
- package/dist/core/text-encoding.d.ts.map +1 -0
- package/dist/core/text-encoding.js +30 -0
- package/dist/core/text-encoding.js.map +1 -0
- package/dist/hashline.d.ts +61 -0
- package/dist/hashline.d.ts.map +1 -0
- package/dist/hashline.js +259 -0
- package/dist/hashline.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +347 -65
- package/dist/index.js.map +1 -1
- package/package.json +19 -2
package/dist/index.js
CHANGED
|
@@ -21,13 +21,16 @@
|
|
|
21
21
|
* • Async rendering with invalidate() for non-blocking preview
|
|
22
22
|
*/
|
|
23
23
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
24
|
-
import { extname, relative } from "node:path";
|
|
24
|
+
import { extname, relative, resolve } from "node:path";
|
|
25
25
|
import { codeToANSI } from "@shikijs/cli";
|
|
26
26
|
import * as Diff from "diff";
|
|
27
27
|
import { configIndicatorStyle } from "./core/config.js";
|
|
28
28
|
import { computeHunkBlocks, getSepStyle, parseDiff, parsePatchFiles, resolveSepStyle, sepLabelSplit, sepLabelUnified, } from "./core/diff.js";
|
|
29
29
|
import { replace } from "./core/replace.js";
|
|
30
30
|
import { registerEditGuard } from "./edit-guard.js";
|
|
31
|
+
import { initHashline } from "./hashline.js";
|
|
32
|
+
import { executeHashlineRead, HASHLINE_EDIT_DESC, HASHLINE_READ_DESC, runHashlineEdit, } from "./core/hashline-execute.js";
|
|
33
|
+
import * as fs from "node:fs";
|
|
31
34
|
import { applyDiffPalette as applySharedDiffPalette, lang as detectDiffLanguage, renderSplit as renderSharedSplit, resolveDiffColors as resolveSharedDiffColors, themeCacheKey as sharedThemeCacheKey, } from "./review/hunk-preview.js";
|
|
32
35
|
const DIFF_PRESETS = {
|
|
33
36
|
default: {
|
|
@@ -930,7 +933,7 @@ async function renderUnified(diff, language, max = MAX_RENDER_LINES, dc = DEFAUL
|
|
|
930
933
|
/** Emit a single stacked row with compact gutter + left border bar. */
|
|
931
934
|
function emitRow(num, sign, gutterBg, signFg, body, bodyBg = "") {
|
|
932
935
|
const borderFg = sign === "-" ? dc.fgDel : sign === "+" ? dc.fgAdd : "";
|
|
933
|
-
const border = borderFg ? `${borderFg}${getBorderBar()}${RST}` : `${BG_BASE}
|
|
936
|
+
const border = borderFg ? `${borderFg}${getBorderBar()}${RST}` : `${BG_BASE}`;
|
|
934
937
|
const numFg = borderFg || FG_LNUM;
|
|
935
938
|
const gutter = `${border}${gutterBg}${lnum(num, nw, numFg)}${gutterBg} ${signFg}${sign}${gutterBg} ${RST}`;
|
|
936
939
|
const contGutter = `${border}${gutterBg}${" ".repeat(nw + 3)}${RST}`;
|
|
@@ -1083,7 +1086,7 @@ async function renderSplit(diff, language, max = MAX_PREVIEW_LINES, dc = DEFAULT
|
|
|
1083
1086
|
const label = sepLabelSplit(getSepStyle(), line.hunkMeta, line.newNum, line.content);
|
|
1084
1087
|
if (!label)
|
|
1085
1088
|
return { gutter: "", contGutter: "", bodyRows: [""] };
|
|
1086
|
-
const g = `${BG_BASE}
|
|
1089
|
+
const g = `${BG_BASE}${FG_DIM}${fit("", nw + 3)}${RST}`;
|
|
1087
1090
|
return { gutter: g, contGutter: g, bodyRows: [`${BG_BASE}${FG_DIM}${fit(label, cw)}${RST}`] };
|
|
1088
1091
|
}
|
|
1089
1092
|
const isDel = line.type === "del", isAdd = line.type === "add";
|
|
@@ -1100,7 +1103,7 @@ async function renderSplit(diff, language, max = MAX_PREVIEW_LINES, dc = DEFAULT
|
|
|
1100
1103
|
: line.newNum;
|
|
1101
1104
|
// Border bar + colored line numbers for changed lines
|
|
1102
1105
|
const borderFg = isDel ? dc.fgDel : isAdd ? dc.fgAdd : "";
|
|
1103
|
-
const border = borderFg ? `${borderFg}${getBorderBar()}${RST}` :
|
|
1106
|
+
const border = borderFg ? `${borderFg}${getBorderBar()}${RST}` : `${BG_BASE}`;
|
|
1104
1107
|
const numFg = borderFg || FG_LNUM;
|
|
1105
1108
|
let body;
|
|
1106
1109
|
if (ranges && ranges.length > 0) {
|
|
@@ -1214,14 +1217,31 @@ export default async function diffRendererExtension(pi) {
|
|
|
1214
1217
|
return;
|
|
1215
1218
|
writeHeaderStatsByCallId.set(toolCallId, { added, removed });
|
|
1216
1219
|
}
|
|
1220
|
+
const hashlineEditHeaderStatsByCallId = new Map();
|
|
1221
|
+
function stashHashlineEditHeaderStats(toolCallId, diffLines, added, removed) {
|
|
1222
|
+
if (!toolCallId)
|
|
1223
|
+
return;
|
|
1224
|
+
hashlineEditHeaderStatsByCallId.set(toolCallId, { diffLines, added, removed });
|
|
1225
|
+
}
|
|
1226
|
+
function hashlineEditCallStatsSuffix(toolCallId, theme) {
|
|
1227
|
+
const raw = toolCallId ? hashlineEditHeaderStatsByCallId.get(toolCallId) : undefined;
|
|
1228
|
+
if (!raw)
|
|
1229
|
+
return "";
|
|
1230
|
+
const loc = raw.diffLines > 0
|
|
1231
|
+
? `${theme.fg("muted", `${raw.diffLines} diff line${raw.diffLines === 1 ? "" : "s"}`)} `
|
|
1232
|
+
: "";
|
|
1233
|
+
return `${TOOL_RESULT_INDENT}${loc}${summarizeThemed(raw.added, raw.removed, theme)}`;
|
|
1234
|
+
}
|
|
1217
1235
|
const cwd = process.cwd();
|
|
1218
1236
|
const home = process.env.HOME ?? "";
|
|
1219
1237
|
const sp = (p) => shortPath(cwd, home, p);
|
|
1220
1238
|
const TOOL_RESULT_INDENT = " ";
|
|
1221
|
-
const TOOL_HEADER_LEFT_PAD =
|
|
1222
|
-
const TOOL_HEADER_TOP_PAD = 1;
|
|
1223
|
-
const TOOL_PREVIEW_BOTTOM_PAD = 1;
|
|
1239
|
+
const TOOL_HEADER_LEFT_PAD = 0;
|
|
1224
1240
|
const DIFF_BODY_LEFT_PAD = 0;
|
|
1241
|
+
/** Extra frame padding for built-in `edit` tool diff results only. */
|
|
1242
|
+
const EDIT_DIFF_RESULT_FRAME = { headerLeftPad: 1, topPad: 1, bottomPad: 1, previewBottomPad: 1 };
|
|
1243
|
+
/** hashline_read / hashline_edit: one gap under call title (Pi shell only; no formatBottomPadding on call). */
|
|
1244
|
+
const HASHLINE_TOOL_RESULT_FRAME = { previewBottomPad: 0, callTitleBottomPad: 0 };
|
|
1225
1245
|
function resolvePreviewDiffColors(theme) {
|
|
1226
1246
|
resolveDiffColors(theme);
|
|
1227
1247
|
return resolveSharedDiffColors(theme);
|
|
@@ -1231,25 +1251,19 @@ export default async function diffRendererExtension(pi) {
|
|
|
1231
1251
|
const padding = " ".repeat(Math.max(0, renderWidth - strip(content).length));
|
|
1232
1252
|
return injectBg(`${content}${padding}`, [], BG_BASE, BG_BASE);
|
|
1233
1253
|
}
|
|
1234
|
-
function
|
|
1235
|
-
const
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
bgLine(
|
|
1246
|
-
|
|
1247
|
-
}
|
|
1248
|
-
function formatBottomPadding(width) {
|
|
1249
|
-
return Array.from({ length: TOOL_PREVIEW_BOTTOM_PAD }, () => bgLine("", width)).join("\n");
|
|
1250
|
-
}
|
|
1251
|
-
function formatToolCallHeader(label, filePath, theme, width, suffix = "") {
|
|
1252
|
-
return [formatToolTitle(label, filePath, theme, width, suffix), formatBottomPadding(width)].join("\n");
|
|
1254
|
+
function formatToolFrameHeader(opts) {
|
|
1255
|
+
const { width, topPad = 0, bottomPad = 0, headerLeftPad, suffix = "", label, filePath, theme, meta } = opts;
|
|
1256
|
+
const leftPad = " ".repeat(headerLeftPad ?? TOOL_HEADER_LEFT_PAD);
|
|
1257
|
+
const content = label !== undefined
|
|
1258
|
+
? `${leftPad}${theme.fg("toolTitle", theme.bold(label))}${filePath ? ` ${theme.fg("accent", sp(filePath))}` : ""}${suffix}`
|
|
1259
|
+
: `${leftPad}${meta ?? ""}`;
|
|
1260
|
+
const parts = [];
|
|
1261
|
+
for (let i = 0; i < topPad; i++)
|
|
1262
|
+
parts.push(bgLine("", width));
|
|
1263
|
+
parts.push(bgLine(content, width));
|
|
1264
|
+
for (let i = 0; i < bottomPad; i++)
|
|
1265
|
+
parts.push(bgLine("", width));
|
|
1266
|
+
return parts.join("\n");
|
|
1253
1267
|
}
|
|
1254
1268
|
function editEditsCountLabel(edits, diffLines, theme) {
|
|
1255
1269
|
const n = edits === 1 ? "1 edit" : `${edits} edits`;
|
|
@@ -1262,6 +1276,24 @@ export default async function diffRendererExtension(pi) {
|
|
|
1262
1276
|
const count = editEditsCountLabel(raw.edits, raw.diffLines, theme);
|
|
1263
1277
|
return `${TOOL_RESULT_INDENT}${theme.fg("muted", count)} ${summarizeThemed(raw.added, raw.removed, theme)}`;
|
|
1264
1278
|
}
|
|
1279
|
+
function formatEditDiffResultTitle(d, theme, width, frame) {
|
|
1280
|
+
const fp = d.filePath ?? d.summary ?? "";
|
|
1281
|
+
const edits = d.edits ?? d.editCount ?? 1;
|
|
1282
|
+
const diffLines = typeof d.diffLineCount === "number"
|
|
1283
|
+
? d.diffLineCount
|
|
1284
|
+
: (d.linesAdded ?? 0) + (d.linesRemoved ?? 0);
|
|
1285
|
+
const suffix = `${TOOL_RESULT_INDENT}${theme.fg("muted", editEditsCountLabel(edits, diffLines, theme))} ${summarizeThemed(d.linesAdded ?? 0, d.linesRemoved ?? 0, theme)}`;
|
|
1286
|
+
return formatToolFrameHeader({
|
|
1287
|
+
width,
|
|
1288
|
+
label: "edit",
|
|
1289
|
+
filePath: fp,
|
|
1290
|
+
suffix,
|
|
1291
|
+
theme,
|
|
1292
|
+
topPad: frame.topPad ?? 0,
|
|
1293
|
+
bottomPad: frame.bottomPad ?? 0,
|
|
1294
|
+
headerLeftPad: frame.headerLeftPad,
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1265
1297
|
function writeCallStatsSuffix(toolCallId, theme) {
|
|
1266
1298
|
const raw = toolCallId ? writeHeaderStatsByCallId.get(toolCallId) : undefined;
|
|
1267
1299
|
if (!raw)
|
|
@@ -1288,18 +1320,36 @@ export default async function diffRendererExtension(pi) {
|
|
|
1288
1320
|
resolvePreviewDiffColors(theme);
|
|
1289
1321
|
text.__piDiffTask = undefined;
|
|
1290
1322
|
const width = termW();
|
|
1291
|
-
text.setText(
|
|
1323
|
+
text.setText(formatToolFrameHeader({ meta, width, bottomPad: 0 }));
|
|
1292
1324
|
}
|
|
1293
|
-
function setDiffPreviewTask(text, keyPrefix,
|
|
1325
|
+
function setDiffPreviewTask(text, keyPrefix, metaOrHeader, diff, language, maxLines, theme, ctx, frame) {
|
|
1294
1326
|
const themeKey = sharedThemeCacheKey(theme);
|
|
1295
1327
|
const colors = resolvePreviewDiffColors(theme);
|
|
1296
|
-
const
|
|
1328
|
+
const headerKey = typeof metaOrHeader === "function" ? "fn:" + keyPrefix : metaOrHeader;
|
|
1329
|
+
const header = frame?.omitHeader
|
|
1330
|
+
? (_width) => ""
|
|
1331
|
+
: typeof metaOrHeader === "function"
|
|
1332
|
+
? metaOrHeader
|
|
1333
|
+
: (width) => formatToolFrameHeader({
|
|
1334
|
+
meta: metaOrHeader,
|
|
1335
|
+
width,
|
|
1336
|
+
topPad: frame?.topPad ?? 0,
|
|
1337
|
+
bottomPad: frame?.bottomPad ?? 0,
|
|
1338
|
+
headerLeftPad: frame?.headerLeftPad,
|
|
1339
|
+
});
|
|
1340
|
+
const joinHeaderBody = (width, body) => {
|
|
1341
|
+
const h = header(width);
|
|
1342
|
+
const bottomPad = Math.max(0, frame?.previewBottomPad ?? 0);
|
|
1343
|
+
const bottom = Array.from({ length: bottomPad }, () => bgLine("", width)).join("\n");
|
|
1344
|
+
const main = h ? `${h}\n${body}` : body;
|
|
1345
|
+
return bottom ? `${main}\n${bottom}` : main;
|
|
1346
|
+
};
|
|
1297
1347
|
text.__piDiffTask = {
|
|
1298
|
-
placeholder:
|
|
1299
|
-
fallback:
|
|
1348
|
+
placeholder: joinHeaderBody(termW(), padDiffBody(theme.fg("muted", " rendering diff…"))),
|
|
1349
|
+
fallback: header(termW()),
|
|
1300
1350
|
invalidate: ctx.invalidate,
|
|
1301
|
-
key: (width) => `${keyPrefix}:${themeKey}:${width}:${
|
|
1302
|
-
render: async (width) =>
|
|
1351
|
+
key: (width) => `${keyPrefix}:${themeKey}:${width}:${headerKey}:${diff.lines.length}:${language ?? ""}:${frame?.omitHeader ? "oh" : "h"}:${frame?.previewBottomPad ?? 0}`,
|
|
1352
|
+
render: async (width) => joinHeaderBody(width, await renderPaddedDiff(diff, language, maxLines, colors, width)),
|
|
1303
1353
|
};
|
|
1304
1354
|
}
|
|
1305
1355
|
/** Wrap a Text component so its render(width) kicks off async diff rendering
|
|
@@ -1400,11 +1450,11 @@ export default async function diffRendererExtension(pi) {
|
|
|
1400
1450
|
if (args?.content && !ctx.argsComplete) {
|
|
1401
1451
|
const n = String(args.content).split("\n").length;
|
|
1402
1452
|
const suffix = `${TOOL_RESULT_INDENT}${theme.fg("muted", `(${n} lines…)`)}${stats ? ` ${stats.trimStart()}` : ""}`;
|
|
1403
|
-
text.setText(
|
|
1453
|
+
text.setText(formatToolFrameHeader({ label, filePath: fp, theme, width: w, suffix, topPad: 0, bottomPad: 0 }));
|
|
1404
1454
|
return text;
|
|
1405
1455
|
}
|
|
1406
1456
|
if (args?.content && ctx.argsComplete && isNew) {
|
|
1407
|
-
const title =
|
|
1457
|
+
const title = formatToolFrameHeader({ label, filePath: fp, theme, width: w, topPad: 0, bottomPad: 0 });
|
|
1408
1458
|
const previewKey = `create:${sharedThemeCacheKey(theme)}:${fp}:${String(args.content).length}`;
|
|
1409
1459
|
if (ctx.state._previewKey !== previewKey) {
|
|
1410
1460
|
ctx.state._previewKey = previewKey;
|
|
@@ -1422,7 +1472,7 @@ export default async function diffRendererExtension(pi) {
|
|
|
1422
1472
|
text.setText(ctx.state._previewText ?? title);
|
|
1423
1473
|
return text;
|
|
1424
1474
|
}
|
|
1425
|
-
text.setText(
|
|
1475
|
+
text.setText(formatToolFrameHeader({ label, filePath: fp, theme, width: w, suffix: stats, topPad: 0, bottomPad: 0 }));
|
|
1426
1476
|
return text;
|
|
1427
1477
|
},
|
|
1428
1478
|
renderResult(result, _opt, theme, ctx) {
|
|
@@ -1438,7 +1488,7 @@ export default async function diffRendererExtension(pi) {
|
|
|
1438
1488
|
}
|
|
1439
1489
|
const d = result.details;
|
|
1440
1490
|
if (d?._type === "diff") {
|
|
1441
|
-
setDiffPreviewTask(text, "wd", "", d.diff, d.language, MAX_RENDER_LINES, theme, ctx);
|
|
1491
|
+
setDiffPreviewTask(text, "wd", "", d.diff, d.language, MAX_RENDER_LINES, theme, ctx, { omitHeader: true, previewBottomPad: 1 });
|
|
1442
1492
|
return text;
|
|
1443
1493
|
}
|
|
1444
1494
|
if (d?._type === "noChange") {
|
|
@@ -1448,31 +1498,32 @@ export default async function diffRendererExtension(pi) {
|
|
|
1448
1498
|
}
|
|
1449
1499
|
if (d?._type === "new") {
|
|
1450
1500
|
const { lines: lineCount, content: rawContent, filePath: fp } = d;
|
|
1501
|
+
resolvePreviewDiffColors(theme);
|
|
1451
1502
|
const w = termW();
|
|
1452
1503
|
const newHdr = bgLine(`${theme.fg("success", `✓ new file (${lineCount} lines)`)}`, w);
|
|
1453
1504
|
const pk = `nf:${sharedThemeCacheKey(theme)}:${fp}:${lineCount}`;
|
|
1454
1505
|
if (ctx.state._nfk !== pk) {
|
|
1455
1506
|
ctx.state._nfk = pk;
|
|
1456
|
-
ctx.state._nft = newHdr;
|
|
1457
1507
|
const lg = detectDiffLanguage(fp);
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1508
|
+
text.__piDiffTask = {
|
|
1509
|
+
placeholder: `${newHdr}\n${padDiffBody(theme.fg("muted", "rendering file…"))}`,
|
|
1510
|
+
fallback: `${newHdr}`,
|
|
1511
|
+
invalidate: ctx.invalidate,
|
|
1512
|
+
key: (width) => `nf:${sharedThemeCacheKey(theme)}:${fp}:${lineCount}:${width}`,
|
|
1513
|
+
render: async (width) => {
|
|
1514
|
+
if (!rawContent)
|
|
1515
|
+
return `${newHdr}`;
|
|
1516
|
+
const hlLines = await hlBlock(rawContent, lg);
|
|
1463
1517
|
const maxShow = hlLines.length;
|
|
1464
|
-
const preview = hlLines.slice(0, maxShow).join("\n");
|
|
1518
|
+
const preview = hlLines.slice(0, maxShow).join("\n").replace(/\n+$/, "");
|
|
1465
1519
|
const rem = hlLines.length - maxShow;
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
.catch(() => { });
|
|
1473
|
-
}
|
|
1520
|
+
const moreLine = rem > 0
|
|
1521
|
+
? `\n${bgLine(`${TOOL_RESULT_INDENT}${theme.fg("muted", `… ${rem} more lines`)}`, width)}`
|
|
1522
|
+
: "";
|
|
1523
|
+
return `${newHdr}\n${padDiffBody(preview)}${moreLine}`;
|
|
1524
|
+
},
|
|
1525
|
+
};
|
|
1474
1526
|
}
|
|
1475
|
-
text.setText(ctx.state._nft ?? newHdr);
|
|
1476
1527
|
return text;
|
|
1477
1528
|
}
|
|
1478
1529
|
text.setText(`${TOOL_RESULT_INDENT}${theme.fg("dim", String(result?.content?.[0]?.text ?? "written").slice(0, 120))}`);
|
|
@@ -1526,8 +1577,60 @@ export default async function diffRendererExtension(pi) {
|
|
|
1526
1577
|
pi.registerTool({
|
|
1527
1578
|
...origEdit,
|
|
1528
1579
|
name: "edit",
|
|
1580
|
+
parameters: {
|
|
1581
|
+
...(origEdit.parameters || {}),
|
|
1582
|
+
properties: {
|
|
1583
|
+
...(((origEdit.parameters || {}).properties) || {}),
|
|
1584
|
+
dryRun: {
|
|
1585
|
+
type: "boolean",
|
|
1586
|
+
description: "With hashlineChanges: validate anchors and return diff without writing (same as hashline_edit dryRun).",
|
|
1587
|
+
},
|
|
1588
|
+
hashlineChanges: {
|
|
1589
|
+
type: "array",
|
|
1590
|
+
description: "Hashline-anchored edits. Each entry: { hash_range_inclusive: [start, end], content_lines: string[] }. Preferred over oldString/newString — no fuzzy matching, no stale-view risk.",
|
|
1591
|
+
items: {
|
|
1592
|
+
type: "object",
|
|
1593
|
+
properties: {
|
|
1594
|
+
hash_range_inclusive: {
|
|
1595
|
+
type: "array",
|
|
1596
|
+
items: { type: "string" },
|
|
1597
|
+
minItems: 2,
|
|
1598
|
+
maxItems: 2,
|
|
1599
|
+
description: "[start_anchor, end_anchor] from a prior hashline_read. Use identical anchor for a single-line edit.",
|
|
1600
|
+
},
|
|
1601
|
+
content_lines: {
|
|
1602
|
+
type: "array",
|
|
1603
|
+
items: { type: "string" },
|
|
1604
|
+
description: "Replacement lines. Pass an empty array to delete the range.",
|
|
1605
|
+
},
|
|
1606
|
+
},
|
|
1607
|
+
required: ["hash_range_inclusive", "content_lines"],
|
|
1608
|
+
additionalProperties: false,
|
|
1609
|
+
},
|
|
1610
|
+
},
|
|
1611
|
+
},
|
|
1612
|
+
},
|
|
1529
1613
|
async execute(tid, params, sig, upd, ctx) {
|
|
1530
1614
|
const fp = params.path ?? params.file_path ?? "";
|
|
1615
|
+
// Hashline path: strict, no fuzzy fallback, atomic write.
|
|
1616
|
+
// If the model uses hashlineChanges, route exclusively here. Otherwise fall through
|
|
1617
|
+
// to the legacy 6-strategy oldString/newString path below.
|
|
1618
|
+
if (Array.isArray(params.hashlineChanges) && params.hashlineChanges.length > 0) {
|
|
1619
|
+
if (!fp) {
|
|
1620
|
+
return {
|
|
1621
|
+
content: [{ type: "text", text: "[E_BAD_INPUT] path required for hashlineChanges" }],
|
|
1622
|
+
isError: true,
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
const resolvedFp = resolve(cwd, fp);
|
|
1626
|
+
return runHashlineEdit({
|
|
1627
|
+
resolvedPath: resolvedFp,
|
|
1628
|
+
changes: params.hashlineChanges,
|
|
1629
|
+
dryRun: params.dryRun === true,
|
|
1630
|
+
toolCallId: tid,
|
|
1631
|
+
onDiffStats: (id, diff) => stashHashlineEditHeaderStats(id, diff.lines.length, diff.added, diff.removed),
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1531
1634
|
const operations = getEditOperations(params);
|
|
1532
1635
|
// Try cascading replace() first — smarter matching than SDK's exact-only edit
|
|
1533
1636
|
if (fp && operations.length > 0 && existsSync(fp)) {
|
|
@@ -1709,12 +1812,8 @@ export default async function diffRendererExtension(pi) {
|
|
|
1709
1812
|
const operations = getEditOperations(args);
|
|
1710
1813
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1711
1814
|
resolvePreviewDiffColors(theme);
|
|
1815
|
+
const stats = editCallStatsSuffix(ctx.toolCallId, theme);
|
|
1712
1816
|
if (ctx.argsComplete && operations.length > 0) {
|
|
1713
|
-
const { totalAdded, totalRemoved } = summarizeEditOperations(operations);
|
|
1714
|
-
// Compute the line number for the call preview by reading the
|
|
1715
|
-
// file and finding the first oldText. This lets the title show
|
|
1716
|
-
// "at line N" even before the edit executes, matching what the
|
|
1717
|
-
// result title will show.
|
|
1718
1817
|
let previewLine = 0;
|
|
1719
1818
|
try {
|
|
1720
1819
|
if (fp && existsSync(fp)) {
|
|
@@ -1728,10 +1827,28 @@ export default async function diffRendererExtension(pi) {
|
|
|
1728
1827
|
previewLine = 0;
|
|
1729
1828
|
}
|
|
1730
1829
|
const loc = previewLine > 0 ? ` ${theme.fg("muted", `at line ${previewLine}`)}` : "";
|
|
1731
|
-
text.setText(
|
|
1830
|
+
text.setText(formatToolFrameHeader({
|
|
1831
|
+
label: "edit",
|
|
1832
|
+
filePath: fp,
|
|
1833
|
+
theme,
|
|
1834
|
+
width: termW(),
|
|
1835
|
+
suffix: `${stats}${loc}`,
|
|
1836
|
+
topPad: EDIT_DIFF_RESULT_FRAME.topPad,
|
|
1837
|
+
bottomPad: EDIT_DIFF_RESULT_FRAME.bottomPad,
|
|
1838
|
+
headerLeftPad: EDIT_DIFF_RESULT_FRAME.headerLeftPad,
|
|
1839
|
+
}));
|
|
1732
1840
|
}
|
|
1733
1841
|
else {
|
|
1734
|
-
text.setText(
|
|
1842
|
+
text.setText(formatToolFrameHeader({
|
|
1843
|
+
label: "edit",
|
|
1844
|
+
filePath: fp,
|
|
1845
|
+
theme,
|
|
1846
|
+
width: termW(),
|
|
1847
|
+
suffix: stats,
|
|
1848
|
+
topPad: EDIT_DIFF_RESULT_FRAME.topPad,
|
|
1849
|
+
bottomPad: EDIT_DIFF_RESULT_FRAME.bottomPad,
|
|
1850
|
+
headerLeftPad: EDIT_DIFF_RESULT_FRAME.headerLeftPad,
|
|
1851
|
+
}));
|
|
1735
1852
|
}
|
|
1736
1853
|
return text;
|
|
1737
1854
|
},
|
|
@@ -1748,20 +1865,20 @@ export default async function diffRendererExtension(pi) {
|
|
|
1748
1865
|
}
|
|
1749
1866
|
const d = result.details;
|
|
1750
1867
|
if (d?._type === "editInfo" && d.diff) {
|
|
1751
|
-
setDiffPreviewTask(text, "ed", "", d.diff, d.language, MAX_PREVIEW_LINES, theme, ctx);
|
|
1868
|
+
setDiffPreviewTask(text, "ed", "", d.diff, d.language, MAX_PREVIEW_LINES, theme, ctx, { omitHeader: true, previewBottomPad: EDIT_DIFF_RESULT_FRAME.previewBottomPad });
|
|
1752
1869
|
return text;
|
|
1753
1870
|
}
|
|
1754
|
-
if (d?._type === "
|
|
1755
|
-
|
|
1871
|
+
if ((d?._type === "hashlineEditInfo" || d?._type === "hashlineEditDryRun") && d.diff) {
|
|
1872
|
+
setDiffPreviewTask(text, d.path, "", d.diff, undefined, MAX_PREVIEW_LINES, theme, ctx, { omitHeader: true, previewBottomPad: EDIT_DIFF_RESULT_FRAME.previewBottomPad });
|
|
1756
1873
|
return text;
|
|
1757
1874
|
}
|
|
1758
1875
|
if (d?._type === "multiEditInfo") {
|
|
1759
1876
|
const { editCount, diffLineCount, diff, language } = d;
|
|
1760
|
-
const meta = `${editCount} edits${diffLineCountLabel(diffLineCount, theme)}`;
|
|
1761
1877
|
if (diff) {
|
|
1762
|
-
setDiffPreviewTask(text, "me",
|
|
1878
|
+
setDiffPreviewTask(text, "me", "", diff, language, MAX_PREVIEW_LINES, theme, ctx, { omitHeader: true, previewBottomPad: EDIT_DIFF_RESULT_FRAME.previewBottomPad });
|
|
1763
1879
|
return text;
|
|
1764
1880
|
}
|
|
1881
|
+
const meta = `${editCount} edits${diffLineCountLabel(diffLineCount, theme)}`;
|
|
1765
1882
|
setToolHeaderText(text, meta, theme);
|
|
1766
1883
|
return text;
|
|
1767
1884
|
}
|
|
@@ -1770,6 +1887,171 @@ export default async function diffRendererExtension(pi) {
|
|
|
1770
1887
|
return text;
|
|
1771
1888
|
},
|
|
1772
1889
|
});
|
|
1890
|
+
// Eager-init the hashline engine on session start so first edit has no WASM latency.
|
|
1891
|
+
pi.on("session_start", async () => {
|
|
1892
|
+
try {
|
|
1893
|
+
await initHashline();
|
|
1894
|
+
}
|
|
1895
|
+
catch {
|
|
1896
|
+
// best-effort; lazy init still works on first edit
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
pi.registerTool({
|
|
1900
|
+
name: "hashline_read",
|
|
1901
|
+
label: "hashline_read",
|
|
1902
|
+
description: HASHLINE_READ_DESC,
|
|
1903
|
+
parameters: {
|
|
1904
|
+
type: "object",
|
|
1905
|
+
properties: {
|
|
1906
|
+
path: { type: "string", description: "Absolute or relative file path" },
|
|
1907
|
+
startLine: { type: "number", description: "Optional 1-indexed start line" },
|
|
1908
|
+
endLine: { type: "number", description: "Optional 1-indexed end line (inclusive)" },
|
|
1909
|
+
},
|
|
1910
|
+
required: ["path"],
|
|
1911
|
+
additionalProperties: false,
|
|
1912
|
+
},
|
|
1913
|
+
renderCall(args, theme, ctx) {
|
|
1914
|
+
const fp = args?.path ?? "";
|
|
1915
|
+
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1916
|
+
resolvePreviewDiffColors(theme);
|
|
1917
|
+
const range = typeof args?.startLine === "number" || typeof args?.endLine === "number"
|
|
1918
|
+
? ` ${theme.fg("muted", `lines ${args.startLine ?? 1}-${args.endLine ?? "…"}`)}`
|
|
1919
|
+
: "";
|
|
1920
|
+
text.setText(formatToolFrameHeader({
|
|
1921
|
+
label: "hashline_read",
|
|
1922
|
+
filePath: fp,
|
|
1923
|
+
theme,
|
|
1924
|
+
width: termW(),
|
|
1925
|
+
suffix: range,
|
|
1926
|
+
topPad: 0,
|
|
1927
|
+
bottomPad: HASHLINE_TOOL_RESULT_FRAME.callTitleBottomPad,
|
|
1928
|
+
}));
|
|
1929
|
+
return text;
|
|
1930
|
+
},
|
|
1931
|
+
async execute(_tid, params) {
|
|
1932
|
+
await initHashline();
|
|
1933
|
+
const fp = resolve(cwd, params.path);
|
|
1934
|
+
if (!fp) {
|
|
1935
|
+
return { content: [{ type: "text", text: "[E_BAD_INPUT] path required" }], isError: true, details: {} };
|
|
1936
|
+
}
|
|
1937
|
+
let content;
|
|
1938
|
+
try {
|
|
1939
|
+
content = await fs.promises.readFile(fp, "utf8");
|
|
1940
|
+
}
|
|
1941
|
+
catch (err) {
|
|
1942
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1943
|
+
return {
|
|
1944
|
+
content: [{ type: "text", text: `[E_READ_FAILED] cannot read ${fp}: ${msg}` }],
|
|
1945
|
+
isError: true,
|
|
1946
|
+
details: {},
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
const startLine = typeof params.startLine === "number" ? Math.max(1, params.startLine) : 1;
|
|
1950
|
+
const endLine = typeof params.endLine === "number" ? Math.max(startLine, params.endLine) : Infinity;
|
|
1951
|
+
return executeHashlineRead(fp, content, startLine, endLine);
|
|
1952
|
+
},
|
|
1953
|
+
renderResult(result, _opt, theme, ctx) {
|
|
1954
|
+
const text = getWidthAwareText(ctx.lastComponent);
|
|
1955
|
+
const d = result.details;
|
|
1956
|
+
const lineCount = d?.lineCount ?? 0;
|
|
1957
|
+
if (ctx.expanded === false) {
|
|
1958
|
+
text.__piDiffTask = undefined;
|
|
1959
|
+
const hint = theme.fg("dim", `(${lineCount} lines) ctrl+o to expand`);
|
|
1960
|
+
text.setText(hint);
|
|
1961
|
+
return text;
|
|
1962
|
+
}
|
|
1963
|
+
text.__piDiffTask = undefined;
|
|
1964
|
+
const out = (result.content || []).map((c) => c.text).join("\n");
|
|
1965
|
+
text.setText(out);
|
|
1966
|
+
return text;
|
|
1967
|
+
},
|
|
1968
|
+
});
|
|
1969
|
+
pi.registerTool({
|
|
1970
|
+
name: "hashline_edit",
|
|
1971
|
+
label: "hashline_edit",
|
|
1972
|
+
description: HASHLINE_EDIT_DESC,
|
|
1973
|
+
parameters: {
|
|
1974
|
+
type: "object",
|
|
1975
|
+
properties: {
|
|
1976
|
+
path: { type: "string", description: "Absolute or relative file path" },
|
|
1977
|
+
dryRun: {
|
|
1978
|
+
type: "boolean",
|
|
1979
|
+
description: "If true, validate anchors and return diff without writing the file.",
|
|
1980
|
+
},
|
|
1981
|
+
hashlineChanges: {
|
|
1982
|
+
type: "array",
|
|
1983
|
+
description: "Atomic list of range replacements (empty content_lines deletes the range).",
|
|
1984
|
+
items: {
|
|
1985
|
+
type: "object",
|
|
1986
|
+
properties: {
|
|
1987
|
+
hash_range_inclusive: {
|
|
1988
|
+
type: "array",
|
|
1989
|
+
description: "[start_anchor, end_anchor] from hashline_read (LINE│HASH│content).",
|
|
1990
|
+
items: { type: "string" },
|
|
1991
|
+
minItems: 2,
|
|
1992
|
+
maxItems: 2,
|
|
1993
|
+
},
|
|
1994
|
+
content_lines: {
|
|
1995
|
+
type: "array",
|
|
1996
|
+
description: "Replacement lines; use [] to delete the anchored range.",
|
|
1997
|
+
items: { type: "string" },
|
|
1998
|
+
},
|
|
1999
|
+
},
|
|
2000
|
+
required: ["hash_range_inclusive", "content_lines"],
|
|
2001
|
+
additionalProperties: false,
|
|
2002
|
+
},
|
|
2003
|
+
},
|
|
2004
|
+
},
|
|
2005
|
+
required: ["path", "hashlineChanges"],
|
|
2006
|
+
additionalProperties: false,
|
|
2007
|
+
},
|
|
2008
|
+
renderCall(args, theme, ctx) {
|
|
2009
|
+
const fp = args?.path ?? "";
|
|
2010
|
+
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
2011
|
+
resolvePreviewDiffColors(theme);
|
|
2012
|
+
const stats = hashlineEditCallStatsSuffix(ctx.toolCallId, theme);
|
|
2013
|
+
text.setText(formatToolFrameHeader({
|
|
2014
|
+
label: "hashline_edit",
|
|
2015
|
+
filePath: fp,
|
|
2016
|
+
theme,
|
|
2017
|
+
width: termW(),
|
|
2018
|
+
suffix: stats,
|
|
2019
|
+
topPad: 0,
|
|
2020
|
+
bottomPad: HASHLINE_TOOL_RESULT_FRAME.callTitleBottomPad,
|
|
2021
|
+
}));
|
|
2022
|
+
return text;
|
|
2023
|
+
},
|
|
2024
|
+
async execute(tid, params) {
|
|
2025
|
+
await initHashline();
|
|
2026
|
+
const fp = resolve(cwd, params.path);
|
|
2027
|
+
if (!fp) {
|
|
2028
|
+
return { content: [{ type: "text", text: "[E_BAD_INPUT] path required" }], isError: true, details: {} };
|
|
2029
|
+
}
|
|
2030
|
+
if (!Array.isArray(params.hashlineChanges) || params.hashlineChanges.length === 0) {
|
|
2031
|
+
return { content: [{ type: "text", text: "[E_EMPTY] hashlineChanges must be a non-empty array" }], isError: true, details: {} };
|
|
2032
|
+
}
|
|
2033
|
+
return runHashlineEdit({
|
|
2034
|
+
resolvedPath: fp,
|
|
2035
|
+
changes: params.hashlineChanges,
|
|
2036
|
+
dryRun: params.dryRun === true,
|
|
2037
|
+
toolCallId: tid,
|
|
2038
|
+
onDiffStats: (id, diff) => stashHashlineEditHeaderStats(id, diff.lines.length, diff.added, diff.removed),
|
|
2039
|
+
});
|
|
2040
|
+
},
|
|
2041
|
+
renderResult(result, _opt, theme, ctx) {
|
|
2042
|
+
const text = getWidthAwareText(ctx.lastComponent);
|
|
2043
|
+
const d = result.details;
|
|
2044
|
+
if ((d?._type === "hashlineEditInfo" || d?._type === "hashlineEditDryRun") && d.diff) {
|
|
2045
|
+
setDiffPreviewTask(text, "he", "", d.diff, undefined, MAX_RENDER_LINES, theme, ctx, { omitHeader: true, previewBottomPad: HASHLINE_TOOL_RESULT_FRAME.previewBottomPad });
|
|
2046
|
+
return text;
|
|
2047
|
+
}
|
|
2048
|
+
// Fallback for errors / no-diff
|
|
2049
|
+
text.__piDiffTask = undefined;
|
|
2050
|
+
const out = (result.content || []).map((c) => c.text).join("\n");
|
|
2051
|
+
text.setText(theme.fg(ctx.isError ? "error" : "muted", out));
|
|
2052
|
+
return text;
|
|
2053
|
+
},
|
|
2054
|
+
});
|
|
1773
2055
|
registerEditGuard(pi);
|
|
1774
2056
|
}
|
|
1775
2057
|
//# sourceMappingURL=index.js.map
|