@heyhuynhgiabuu/pi-diff 0.1.5 → 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 +159 -50
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%)
|
|
@@ -238,10 +244,7 @@ function autoDeriveBgFromTheme(theme: any): void {
|
|
|
238
244
|
|
|
239
245
|
/** Load diff theme config from .pi/settings.json (project-level, then global). */
|
|
240
246
|
function loadDiffConfig(): DiffUserConfig {
|
|
241
|
-
const paths = [
|
|
242
|
-
`${process.cwd()}/.pi/settings.json`,
|
|
243
|
-
`${process.env.HOME ?? ""}/.pi/settings.json`,
|
|
244
|
-
];
|
|
247
|
+
const paths = [`${process.cwd()}/.pi/settings.json`, `${process.env.HOME ?? ""}/.pi/settings.json`];
|
|
245
248
|
for (const p of paths) {
|
|
246
249
|
try {
|
|
247
250
|
if (existsSync(p)) {
|
|
@@ -274,32 +277,66 @@ function applyDiffPalette(): void {
|
|
|
274
277
|
const applyBg = (envName: string | null, key: string, presetVal: string | undefined, set: (v: string) => void) => {
|
|
275
278
|
if (envName && process.env[envName]) return; // env override wins
|
|
276
279
|
const hex = ov[key] ?? presetVal;
|
|
277
|
-
if (hex) {
|
|
280
|
+
if (hex) {
|
|
281
|
+
const a = hexToBgAnsi(hex);
|
|
282
|
+
if (a) set(a);
|
|
283
|
+
}
|
|
278
284
|
};
|
|
279
285
|
// Helper: apply a hex fg color if not env-overridden
|
|
280
286
|
const applyFg = (envName: string | null, key: string, presetVal: string | undefined, set: (v: string) => void) => {
|
|
281
287
|
if (envName && process.env[envName]) return;
|
|
282
288
|
const hex = ov[key] ?? presetVal;
|
|
283
|
-
if (hex) {
|
|
289
|
+
if (hex) {
|
|
290
|
+
const a = hexToFgAnsi(hex);
|
|
291
|
+
if (a) set(a);
|
|
292
|
+
}
|
|
284
293
|
};
|
|
285
294
|
|
|
286
295
|
// --- Apply backgrounds ---
|
|
287
|
-
applyBg("DIFF_BG_ADD", "bgAdd", preset?.bgAdd, (v) => {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
applyBg("
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
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
|
+
});
|
|
294
317
|
|
|
295
318
|
// --- Apply foregrounds ---
|
|
296
|
-
applyFg("DIFF_FG_ADD", "fgAdd", preset?.fgAdd, (v) => {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
applyFg(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
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
|
+
});
|
|
303
340
|
|
|
304
341
|
// --- Shiki syntax theme ---
|
|
305
342
|
const shiki = ov.shikiTheme ?? preset?.shikiTheme;
|
|
@@ -441,7 +478,9 @@ function resolveDiffColors(theme?: any): DiffColors {
|
|
|
441
478
|
BG_BASE = bgAnsi;
|
|
442
479
|
RST = `\x1b[0m${BG_BASE}`;
|
|
443
480
|
}
|
|
444
|
-
} catch {
|
|
481
|
+
} catch {
|
|
482
|
+
/* ignore */
|
|
483
|
+
}
|
|
445
484
|
}
|
|
446
485
|
|
|
447
486
|
// Auto-derive bg colors from theme on first render (if no explicit preset/overrides)
|
|
@@ -1424,66 +1463,128 @@ export default function diffRendererExtension(pi: any): void {
|
|
|
1424
1463
|
|
|
1425
1464
|
const origEdit = createEditTool(cwd);
|
|
1426
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
|
+
|
|
1427
1497
|
pi.registerTool({
|
|
1428
1498
|
...origEdit,
|
|
1429
1499
|
name: "edit",
|
|
1430
1500
|
|
|
1431
1501
|
async execute(tid: string, params: any, sig: any, upd: any, ctx: any) {
|
|
1432
1502
|
const fp = params.path ?? params.file_path ?? "";
|
|
1433
|
-
const
|
|
1434
|
-
const newText = params.newText ?? params.new_text ?? "";
|
|
1435
|
-
|
|
1503
|
+
const operations = getEditOperations(params);
|
|
1436
1504
|
const result = await origEdit.execute(tid, params, sig, upd, ctx);
|
|
1437
1505
|
|
|
1438
|
-
if (
|
|
1506
|
+
if (operations.length === 0) return result;
|
|
1507
|
+
|
|
1508
|
+
const { diffs, summary } = summarizeEditOperations(operations);
|
|
1509
|
+
if (operations.length === 1) {
|
|
1439
1510
|
let editLine = 0;
|
|
1440
1511
|
try {
|
|
1441
1512
|
if (fp && existsSync(fp)) {
|
|
1442
1513
|
const f = readFileSync(fp, "utf-8");
|
|
1443
|
-
const idx = f.indexOf(newText);
|
|
1514
|
+
const idx = f.indexOf(operations[0].newText);
|
|
1444
1515
|
if (idx >= 0) editLine = f.slice(0, idx).split("\n").length;
|
|
1445
1516
|
}
|
|
1446
1517
|
} catch {
|
|
1447
1518
|
editLine = 0;
|
|
1448
1519
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1520
|
+
(result as any).details = { _type: "editInfo", summary, editLine };
|
|
1521
|
+
return result;
|
|
1451
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
|
+
};
|
|
1452
1530
|
return result;
|
|
1453
1531
|
},
|
|
1454
1532
|
|
|
1455
1533
|
renderCall(args: any, theme: any, ctx: any) {
|
|
1456
1534
|
const fp = args?.path ?? args?.file_path ?? "";
|
|
1457
|
-
const
|
|
1458
|
-
const newText = args?.newText ?? args?.new_text ?? "";
|
|
1535
|
+
const operations = getEditOperations(args);
|
|
1459
1536
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1460
1537
|
const hdr = `${theme.fg("toolTitle", theme.bold("edit"))} ${theme.fg("accent", sp(fp))}`;
|
|
1461
1538
|
|
|
1462
|
-
if (!(ctx.argsComplete &&
|
|
1539
|
+
if (!(ctx.argsComplete && operations.length > 0)) {
|
|
1463
1540
|
text.setText(hdr);
|
|
1464
1541
|
return text;
|
|
1465
1542
|
}
|
|
1466
1543
|
|
|
1467
|
-
const pk = JSON.stringify({ fp,
|
|
1544
|
+
const pk = JSON.stringify({ fp, operations, w: termW() });
|
|
1468
1545
|
if (ctx.state._pk !== pk) {
|
|
1469
1546
|
ctx.state._pk = pk;
|
|
1470
1547
|
ctx.state._pt = `${hdr} ${theme.fg("muted", "(rendering…)")}`;
|
|
1471
1548
|
const lg = lang(fp);
|
|
1472
|
-
const diff = parseDiff(oldText, newText);
|
|
1473
1549
|
const dc = resolveDiffColors(theme);
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
+
}
|
|
1487
1588
|
}
|
|
1488
1589
|
|
|
1489
1590
|
text.setText(ctx.state._pt ?? hdr);
|
|
@@ -1510,6 +1611,14 @@ export default function diffRendererExtension(pi: any): void {
|
|
|
1510
1611
|
text.setText(`${content}${" ".repeat(pad)}`);
|
|
1511
1612
|
return text;
|
|
1512
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
|
+
}
|
|
1513
1622
|
text.setText(` ${theme.fg("dim", String(result?.content?.[0]?.text ?? "edited").slice(0, 120))}`);
|
|
1514
1623
|
return text;
|
|
1515
1624
|
},
|