@heyhuynhgiabuu/pi-diff 0.1.4 → 0.1.6
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 +1 -1
- package/biome.json +8 -1
- package/package.json +1 -1
- package/src/index.ts +168 -52
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ pi -e ./src/index.ts
|
|
|
43
43
|
|
|
44
44
|
## How It Works
|
|
45
45
|
|
|
46
|
-
pi-diff wraps the built-in `write` and `edit` tools from the pi SDK. When the agent writes or edits a file:
|
|
46
|
+
pi-diff wraps the built-in `write` and `edit` tools from the pi SDK, including single-edit and multi-edit `edit` calls. When the agent writes or edits a file:
|
|
47
47
|
|
|
48
48
|
1. **Before the write** — reads the existing file content
|
|
49
49
|
2. **Delegates** to the original SDK tool (file is actually written)
|
package/biome.json
CHANGED
|
@@ -11,7 +11,14 @@
|
|
|
11
11
|
"linter": {
|
|
12
12
|
"enabled": true,
|
|
13
13
|
"rules": {
|
|
14
|
-
"recommended": true
|
|
14
|
+
"recommended": true,
|
|
15
|
+
"suspicious": {
|
|
16
|
+
"noExplicitAny": "off",
|
|
17
|
+
"noFocusedTests": "off"
|
|
18
|
+
},
|
|
19
|
+
"correctness": {
|
|
20
|
+
"noUnusedVariables": "off"
|
|
21
|
+
}
|
|
15
22
|
}
|
|
16
23
|
},
|
|
17
24
|
"formatter": {
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -172,7 +172,11 @@ function deriveBgFromFg(fgAnsi: string, intensity: number): string {
|
|
|
172
172
|
/** Mix an accent color into a base color at the given intensity (0.0–1.0).
|
|
173
173
|
* Returns an ANSI 24-bit background escape. Used to derive diff backgrounds
|
|
174
174
|
* that blend with the tool box background (toolSuccessBg). */
|
|
175
|
-
function mixBg(
|
|
175
|
+
function mixBg(
|
|
176
|
+
base: { r: number; g: number; b: number },
|
|
177
|
+
accent: { r: number; g: number; b: number },
|
|
178
|
+
intensity: number,
|
|
179
|
+
): string {
|
|
176
180
|
const r = Math.round(base.r + (accent.r - base.r) * intensity);
|
|
177
181
|
const g = Math.round(base.g + (accent.g - base.g) * intensity);
|
|
178
182
|
const b = Math.round(base.b + (accent.b - base.b) * intensity);
|
|
@@ -207,15 +211,17 @@ function autoDeriveBgFromTheme(theme: any): void {
|
|
|
207
211
|
base = parsed;
|
|
208
212
|
BG_BASE = bgAnsi;
|
|
209
213
|
}
|
|
210
|
-
} catch {
|
|
214
|
+
} catch {
|
|
215
|
+
/* no toolSuccessBg — use black */
|
|
216
|
+
}
|
|
211
217
|
}
|
|
212
218
|
|
|
213
219
|
// Line backgrounds — subtle accent mixed into base (8–10%)
|
|
214
220
|
BG_ADD = mixBg(base, addRgb, 0.08);
|
|
215
|
-
BG_DEL = mixBg(base, delRgb, 0.
|
|
221
|
+
BG_DEL = mixBg(base, delRgb, 0.1);
|
|
216
222
|
|
|
217
223
|
// Word-level highlights — more visible (20–22%)
|
|
218
|
-
BG_ADD_W = mixBg(base, addRgb, 0.
|
|
224
|
+
BG_ADD_W = mixBg(base, addRgb, 0.2);
|
|
219
225
|
BG_DEL_W = mixBg(base, delRgb, 0.22);
|
|
220
226
|
|
|
221
227
|
// Gutters — subtler than lines (5–6%)
|
|
@@ -225,6 +231,10 @@ function autoDeriveBgFromTheme(theme: any): void {
|
|
|
225
231
|
// Empty filler and context — match the base
|
|
226
232
|
BG_EMPTY = BG_BASE;
|
|
227
233
|
|
|
234
|
+
// Update RST to re-apply base bg after every reset — prevents black
|
|
235
|
+
// flashes between styled segments when toolSuccessBg is non-black
|
|
236
|
+
RST = `\x1b[0m${BG_BASE}`;
|
|
237
|
+
|
|
228
238
|
// Rebuild derived constants
|
|
229
239
|
DIVIDER = `${FG_RULE}│${RST}`;
|
|
230
240
|
} catch {
|
|
@@ -234,10 +244,7 @@ function autoDeriveBgFromTheme(theme: any): void {
|
|
|
234
244
|
|
|
235
245
|
/** Load diff theme config from .pi/settings.json (project-level, then global). */
|
|
236
246
|
function loadDiffConfig(): DiffUserConfig {
|
|
237
|
-
const paths = [
|
|
238
|
-
`${process.cwd()}/.pi/settings.json`,
|
|
239
|
-
`${process.env.HOME ?? ""}/.pi/settings.json`,
|
|
240
|
-
];
|
|
247
|
+
const paths = [`${process.cwd()}/.pi/settings.json`, `${process.env.HOME ?? ""}/.pi/settings.json`];
|
|
241
248
|
for (const p of paths) {
|
|
242
249
|
try {
|
|
243
250
|
if (existsSync(p)) {
|
|
@@ -270,32 +277,66 @@ function applyDiffPalette(): void {
|
|
|
270
277
|
const applyBg = (envName: string | null, key: string, presetVal: string | undefined, set: (v: string) => void) => {
|
|
271
278
|
if (envName && process.env[envName]) return; // env override wins
|
|
272
279
|
const hex = ov[key] ?? presetVal;
|
|
273
|
-
if (hex) {
|
|
280
|
+
if (hex) {
|
|
281
|
+
const a = hexToBgAnsi(hex);
|
|
282
|
+
if (a) set(a);
|
|
283
|
+
}
|
|
274
284
|
};
|
|
275
285
|
// Helper: apply a hex fg color if not env-overridden
|
|
276
286
|
const applyFg = (envName: string | null, key: string, presetVal: string | undefined, set: (v: string) => void) => {
|
|
277
287
|
if (envName && process.env[envName]) return;
|
|
278
288
|
const hex = ov[key] ?? presetVal;
|
|
279
|
-
if (hex) {
|
|
289
|
+
if (hex) {
|
|
290
|
+
const a = hexToFgAnsi(hex);
|
|
291
|
+
if (a) set(a);
|
|
292
|
+
}
|
|
280
293
|
};
|
|
281
294
|
|
|
282
295
|
// --- Apply backgrounds ---
|
|
283
|
-
applyBg("DIFF_BG_ADD", "bgAdd", preset?.bgAdd, (v) => {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
applyBg("
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
applyBg(
|
|
296
|
+
applyBg("DIFF_BG_ADD", "bgAdd", preset?.bgAdd, (v) => {
|
|
297
|
+
BG_ADD = v;
|
|
298
|
+
});
|
|
299
|
+
applyBg("DIFF_BG_DEL", "bgDel", preset?.bgDel, (v) => {
|
|
300
|
+
BG_DEL = v;
|
|
301
|
+
});
|
|
302
|
+
applyBg("DIFF_BG_ADD_HL", "bgAddHighlight", preset?.bgAddHighlight, (v) => {
|
|
303
|
+
BG_ADD_W = v;
|
|
304
|
+
});
|
|
305
|
+
applyBg("DIFF_BG_DEL_HL", "bgDelHighlight", preset?.bgDelHighlight, (v) => {
|
|
306
|
+
BG_DEL_W = v;
|
|
307
|
+
});
|
|
308
|
+
applyBg("DIFF_BG_GUTTER_ADD", "bgGutterAdd", preset?.bgGutterAdd, (v) => {
|
|
309
|
+
BG_GUTTER_ADD = v;
|
|
310
|
+
});
|
|
311
|
+
applyBg("DIFF_BG_GUTTER_DEL", "bgGutterDel", preset?.bgGutterDel, (v) => {
|
|
312
|
+
BG_GUTTER_DEL = v;
|
|
313
|
+
});
|
|
314
|
+
applyBg(null, "bgEmpty", preset?.bgEmpty, (v) => {
|
|
315
|
+
BG_EMPTY = v;
|
|
316
|
+
});
|
|
290
317
|
|
|
291
318
|
// --- Apply foregrounds ---
|
|
292
|
-
applyFg("DIFF_FG_ADD", "fgAdd", preset?.fgAdd, (v) => {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
applyFg(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
applyFg(null, "
|
|
319
|
+
applyFg("DIFF_FG_ADD", "fgAdd", preset?.fgAdd, (v) => {
|
|
320
|
+
FG_ADD = v;
|
|
321
|
+
});
|
|
322
|
+
applyFg("DIFF_FG_DEL", "fgDel", preset?.fgDel, (v) => {
|
|
323
|
+
FG_DEL = v;
|
|
324
|
+
});
|
|
325
|
+
applyFg(null, "fgDim", preset?.fgDim, (v) => {
|
|
326
|
+
FG_DIM = v;
|
|
327
|
+
});
|
|
328
|
+
applyFg(null, "fgLnum", preset?.fgLnum, (v) => {
|
|
329
|
+
FG_LNUM = v;
|
|
330
|
+
});
|
|
331
|
+
applyFg(null, "fgRule", preset?.fgRule, (v) => {
|
|
332
|
+
FG_RULE = v;
|
|
333
|
+
});
|
|
334
|
+
applyFg(null, "fgStripe", preset?.fgStripe, (v) => {
|
|
335
|
+
FG_STRIPE = v;
|
|
336
|
+
});
|
|
337
|
+
applyFg(null, "fgSafeMuted", preset?.fgSafeMuted, (v) => {
|
|
338
|
+
FG_SAFE_MUTED = v;
|
|
339
|
+
});
|
|
299
340
|
|
|
300
341
|
// --- Shiki syntax theme ---
|
|
301
342
|
const shiki = ov.shikiTheme ?? preset?.shikiTheme;
|
|
@@ -370,7 +411,7 @@ const MAX_WRAP_ROWS_NARROW = 1; // <120 cols (truncate, no wrap)
|
|
|
370
411
|
// ANSI
|
|
371
412
|
// ---------------------------------------------------------------------------
|
|
372
413
|
|
|
373
|
-
|
|
414
|
+
let RST = "\x1b[0m";
|
|
374
415
|
const BOLD = "\x1b[1m";
|
|
375
416
|
const DIM = "\x1b[2m";
|
|
376
417
|
|
|
@@ -433,8 +474,13 @@ function resolveDiffColors(theme?: any): DiffColors {
|
|
|
433
474
|
try {
|
|
434
475
|
const bgAnsi = theme.getBgAnsi("toolSuccessBg");
|
|
435
476
|
const parsed = parseAnsiRgb(bgAnsi);
|
|
436
|
-
if (parsed)
|
|
437
|
-
|
|
477
|
+
if (parsed) {
|
|
478
|
+
BG_BASE = bgAnsi;
|
|
479
|
+
RST = `\x1b[0m${BG_BASE}`;
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
482
|
+
/* ignore */
|
|
483
|
+
}
|
|
438
484
|
}
|
|
439
485
|
|
|
440
486
|
// Auto-derive bg colors from theme on first render (if no explicit preset/overrides)
|
|
@@ -1417,66 +1463,128 @@ export default function diffRendererExtension(pi: any): void {
|
|
|
1417
1463
|
|
|
1418
1464
|
const origEdit = createEditTool(cwd);
|
|
1419
1465
|
|
|
1466
|
+
function getEditOperations(input: any): Array<{ oldText: string; newText: string }> {
|
|
1467
|
+
if (Array.isArray(input?.edits)) {
|
|
1468
|
+
return input.edits
|
|
1469
|
+
.map((edit: any) => ({
|
|
1470
|
+
oldText:
|
|
1471
|
+
typeof edit?.oldText === "string" ? edit.oldText : typeof edit?.old_text === "string" ? edit.old_text : "",
|
|
1472
|
+
newText:
|
|
1473
|
+
typeof edit?.newText === "string" ? edit.newText : typeof edit?.new_text === "string" ? edit.new_text : "",
|
|
1474
|
+
}))
|
|
1475
|
+
.filter((edit: { oldText: string; newText: string }) => edit.oldText && edit.oldText !== edit.newText);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
const oldText =
|
|
1479
|
+
typeof input?.oldText === "string" ? input.oldText : typeof input?.old_text === "string" ? input.old_text : "";
|
|
1480
|
+
const newText =
|
|
1481
|
+
typeof input?.newText === "string" ? input.newText : typeof input?.new_text === "string" ? input.new_text : "";
|
|
1482
|
+
return oldText && oldText !== newText ? [{ oldText, newText }] : [];
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function summarizeEditOperations(operations: Array<{ oldText: string; newText: string }>) {
|
|
1486
|
+
const diffs = operations.map((edit) => parseDiff(edit.oldText, edit.newText));
|
|
1487
|
+
const totalAdded = diffs.reduce((sum, diff) => sum + diff.added, 0);
|
|
1488
|
+
const totalRemoved = diffs.reduce((sum, diff) => sum + diff.removed, 0);
|
|
1489
|
+
return {
|
|
1490
|
+
diffs,
|
|
1491
|
+
totalAdded,
|
|
1492
|
+
totalRemoved,
|
|
1493
|
+
summary: summarize(totalAdded, totalRemoved),
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1420
1497
|
pi.registerTool({
|
|
1421
1498
|
...origEdit,
|
|
1422
1499
|
name: "edit",
|
|
1423
1500
|
|
|
1424
1501
|
async execute(tid: string, params: any, sig: any, upd: any, ctx: any) {
|
|
1425
1502
|
const fp = params.path ?? params.file_path ?? "";
|
|
1426
|
-
const
|
|
1427
|
-
const newText = params.newText ?? params.new_text ?? "";
|
|
1428
|
-
|
|
1503
|
+
const operations = getEditOperations(params);
|
|
1429
1504
|
const result = await origEdit.execute(tid, params, sig, upd, ctx);
|
|
1430
1505
|
|
|
1431
|
-
if (
|
|
1506
|
+
if (operations.length === 0) return result;
|
|
1507
|
+
|
|
1508
|
+
const { diffs, summary } = summarizeEditOperations(operations);
|
|
1509
|
+
if (operations.length === 1) {
|
|
1432
1510
|
let editLine = 0;
|
|
1433
1511
|
try {
|
|
1434
1512
|
if (fp && existsSync(fp)) {
|
|
1435
1513
|
const f = readFileSync(fp, "utf-8");
|
|
1436
|
-
const idx = f.indexOf(newText);
|
|
1514
|
+
const idx = f.indexOf(operations[0].newText);
|
|
1437
1515
|
if (idx >= 0) editLine = f.slice(0, idx).split("\n").length;
|
|
1438
1516
|
}
|
|
1439
1517
|
} catch {
|
|
1440
1518
|
editLine = 0;
|
|
1441
1519
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1520
|
+
(result as any).details = { _type: "editInfo", summary, editLine };
|
|
1521
|
+
return result;
|
|
1444
1522
|
}
|
|
1523
|
+
|
|
1524
|
+
(result as any).details = {
|
|
1525
|
+
_type: "multiEditInfo",
|
|
1526
|
+
summary,
|
|
1527
|
+
editCount: operations.length,
|
|
1528
|
+
diffLineCount: diffs.reduce((sum, diff) => sum + diff.lines.length, 0),
|
|
1529
|
+
};
|
|
1445
1530
|
return result;
|
|
1446
1531
|
},
|
|
1447
1532
|
|
|
1448
1533
|
renderCall(args: any, theme: any, ctx: any) {
|
|
1449
1534
|
const fp = args?.path ?? args?.file_path ?? "";
|
|
1450
|
-
const
|
|
1451
|
-
const newText = args?.newText ?? args?.new_text ?? "";
|
|
1535
|
+
const operations = getEditOperations(args);
|
|
1452
1536
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1453
1537
|
const hdr = `${theme.fg("toolTitle", theme.bold("edit"))} ${theme.fg("accent", sp(fp))}`;
|
|
1454
1538
|
|
|
1455
|
-
if (!(ctx.argsComplete &&
|
|
1539
|
+
if (!(ctx.argsComplete && operations.length > 0)) {
|
|
1456
1540
|
text.setText(hdr);
|
|
1457
1541
|
return text;
|
|
1458
1542
|
}
|
|
1459
1543
|
|
|
1460
|
-
const pk = JSON.stringify({ fp,
|
|
1544
|
+
const pk = JSON.stringify({ fp, operations, w: termW() });
|
|
1461
1545
|
if (ctx.state._pk !== pk) {
|
|
1462
1546
|
ctx.state._pk = pk;
|
|
1463
1547
|
ctx.state._pt = `${hdr} ${theme.fg("muted", "(rendering…)")}`;
|
|
1464
1548
|
const lg = lang(fp);
|
|
1465
|
-
const diff = parseDiff(oldText, newText);
|
|
1466
1549
|
const dc = resolveDiffColors(theme);
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1550
|
+
|
|
1551
|
+
if (operations.length === 1) {
|
|
1552
|
+
const diff = parseDiff(operations[0].oldText, operations[0].newText);
|
|
1553
|
+
renderSplit(diff, lg, MAX_PREVIEW_LINES, dc)
|
|
1554
|
+
.then((rendered) => {
|
|
1555
|
+
if (ctx.state._pk !== pk) return;
|
|
1556
|
+
ctx.state._pt = `${hdr}\n${summarize(diff.added, diff.removed)}\n${rendered}`;
|
|
1557
|
+
ctx.invalidate();
|
|
1558
|
+
})
|
|
1559
|
+
.catch(() => {
|
|
1560
|
+
if (ctx.state._pk !== pk) return;
|
|
1561
|
+
ctx.state._pt = `${hdr} ${summarize(diff.added, diff.removed)}`;
|
|
1562
|
+
ctx.invalidate();
|
|
1563
|
+
});
|
|
1564
|
+
} else {
|
|
1565
|
+
const { diffs, summary } = summarizeEditOperations(operations);
|
|
1566
|
+
const maxShown = Math.min(operations.length, 3);
|
|
1567
|
+
const previewLines = Math.max(8, Math.floor(MAX_PREVIEW_LINES / maxShown));
|
|
1568
|
+
Promise.all(
|
|
1569
|
+
diffs.slice(0, maxShown).map((diff, index) =>
|
|
1570
|
+
renderSplit(diff, lg, previewLines, dc)
|
|
1571
|
+
.then((rendered) => `Edit ${index + 1}/${operations.length}\n${rendered}`)
|
|
1572
|
+
.catch(() => `Edit ${index + 1}/${operations.length} ${summarize(diff.added, diff.removed)}`),
|
|
1573
|
+
),
|
|
1574
|
+
)
|
|
1575
|
+
.then((sections) => {
|
|
1576
|
+
if (ctx.state._pk !== pk) return;
|
|
1577
|
+
const remainder = operations.length - maxShown;
|
|
1578
|
+
const suffix = remainder > 0 ? `\n${theme.fg("muted", `… ${remainder} more edit blocks`)}` : "";
|
|
1579
|
+
ctx.state._pt = `${hdr}\n${operations.length} edits ${summary}\n\n${sections.join("\n\n")}${suffix}`;
|
|
1580
|
+
ctx.invalidate();
|
|
1581
|
+
})
|
|
1582
|
+
.catch(() => {
|
|
1583
|
+
if (ctx.state._pk !== pk) return;
|
|
1584
|
+
ctx.state._pt = `${hdr} ${operations.length} edits ${summary}`;
|
|
1585
|
+
ctx.invalidate();
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1480
1588
|
}
|
|
1481
1589
|
|
|
1482
1590
|
text.setText(ctx.state._pt ?? hdr);
|
|
@@ -1503,6 +1611,14 @@ export default function diffRendererExtension(pi: any): void {
|
|
|
1503
1611
|
text.setText(`${content}${" ".repeat(pad)}`);
|
|
1504
1612
|
return text;
|
|
1505
1613
|
}
|
|
1614
|
+
if (result.details?._type === "multiEditInfo") {
|
|
1615
|
+
const { summary: s, editCount, diffLineCount } = result.details;
|
|
1616
|
+
const content = ` ${editCount} edits ${s}${typeof diffLineCount === "number" ? ` ${theme.fg("muted", `(${diffLineCount} diff lines)`)}` : ""}`;
|
|
1617
|
+
const vis = content.replace(ANSI_RE, "").length;
|
|
1618
|
+
const pad = Math.max(0, termW() - vis);
|
|
1619
|
+
text.setText(`${content}${" ".repeat(pad)}`);
|
|
1620
|
+
return text;
|
|
1621
|
+
}
|
|
1506
1622
|
text.setText(` ${theme.fg("dim", String(result?.content?.[0]?.text ?? "edited").slice(0, 120))}`);
|
|
1507
1623
|
return text;
|
|
1508
1624
|
},
|