@heyhuynhgiabuu/pi-diff 0.7.5 → 0.8.0
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 +15 -9
- package/dist/core/apply-patch.d.ts +1 -1
- package/dist/core/apply-patch.d.ts.map +1 -1
- package/dist/core/apply-patch.js +148 -102
- package/dist/core/apply-patch.js.map +1 -1
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +15 -5
- package/dist/core/config.js.map +1 -1
- package/dist/core/diff.d.ts +0 -4
- package/dist/core/diff.d.ts.map +1 -1
- package/dist/core/diff.js +67 -47
- package/dist/core/diff.js.map +1 -1
- package/dist/core/replace.d.ts +11 -2
- package/dist/core/replace.d.ts.map +1 -1
- package/dist/core/replace.js +108 -2
- package/dist/core/replace.js.map +1 -1
- package/dist/hashline.d.ts +2 -3
- package/dist/hashline.d.ts.map +1 -1
- package/dist/hashline.js +2 -3
- package/dist/hashline.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +81 -203
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- package/dist/edit-guard.d.ts +0 -2
- package/dist/edit-guard.d.ts.map +0 -1
- package/dist/edit-guard.js +0 -34
- package/dist/edit-guard.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -20,20 +20,22 @@
|
|
|
20
20
|
* • Large-diff fallback (skip highlighting, still show diff)
|
|
21
21
|
* • Async rendering with invalidate() for non-blocking preview
|
|
22
22
|
*/
|
|
23
|
-
import { existsSync, readFileSync
|
|
23
|
+
import { constants, existsSync, readFileSync } from "node:fs";
|
|
24
|
+
import { access as accessFile, readFile, writeFile } from "node:fs/promises";
|
|
24
25
|
import { extname, relative } from "node:path";
|
|
25
26
|
import { codeToANSI } from "@shikijs/cli";
|
|
26
27
|
import * as Diff from "diff";
|
|
27
28
|
import { executeApplyPatch, formatApplyPatchResult } from "./core/apply-patch.js";
|
|
28
|
-
import { configIndicatorStyle } from "./core/config.js";
|
|
29
|
+
import { configIndicatorStyle, loadPiDiffConfig } from "./core/config.js";
|
|
29
30
|
import { computeHunkBlocks, getSepStyle, parseDiff, parsePatchFiles, resolveSepStyle, sepLabelSplit, sepLabelUnified, } from "./core/diff.js";
|
|
30
|
-
import { replace } from "./core/replace.js";
|
|
31
|
-
import { registerEditGuard } from "./edit-guard.js";
|
|
32
31
|
import { applyDiffPalette as applySharedDiffPalette, lang as detectDiffLanguage, renderSplit as renderSharedSplit, resolveDiffColors as resolveSharedDiffColors, themeCacheKey as sharedThemeCacheKey, } from "./review/hunk-preview.js";
|
|
33
32
|
const ARROW_PREFIXED_TOOL_HEADERS = new Set(["write", "create", "edit", "apply_patch"]);
|
|
34
33
|
function formatToolHeaderName(name) {
|
|
35
34
|
return ARROW_PREFIXED_TOOL_HEADERS.has(name) ? `← ${name}` : name;
|
|
36
35
|
}
|
|
36
|
+
function isToolResultError(result, context) {
|
|
37
|
+
return result.isError === true || context.isError === true;
|
|
38
|
+
}
|
|
37
39
|
function formatToolHeaderPath(theme, filePath) {
|
|
38
40
|
return theme.fg("toolTitle", filePath);
|
|
39
41
|
}
|
|
@@ -1164,6 +1166,7 @@ export const __testing = {
|
|
|
1164
1166
|
computeHunkBlocks,
|
|
1165
1167
|
formatToolHeaderName,
|
|
1166
1168
|
formatToolHeaderPath,
|
|
1169
|
+
isToolResultError,
|
|
1167
1170
|
normalizeShikiContrast,
|
|
1168
1171
|
getSepStyle,
|
|
1169
1172
|
parseDiff,
|
|
@@ -1173,10 +1176,6 @@ export const __testing = {
|
|
|
1173
1176
|
renderUnified,
|
|
1174
1177
|
};
|
|
1175
1178
|
export default async function diffRendererExtension(pi) {
|
|
1176
|
-
// pi-pretty sets toolOutputExpanded:false on session_start; write/edit default expanded.
|
|
1177
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
1178
|
-
ctx.ui.setToolsExpanded(true);
|
|
1179
|
-
});
|
|
1180
1179
|
// Apply diff theme palette from settings/presets before rendering
|
|
1181
1180
|
applySharedDiffPalette();
|
|
1182
1181
|
// Resolve hunk separator style from env var
|
|
@@ -1210,6 +1209,11 @@ export default async function diffRendererExtension(pi) {
|
|
|
1210
1209
|
writeHeaderStatsByCallId.set(toolCallId, { added, removed });
|
|
1211
1210
|
}
|
|
1212
1211
|
const cwd = process.cwd();
|
|
1212
|
+
const disabledTools = new Set(loadPiDiffConfig().disabledTools ?? []);
|
|
1213
|
+
const registerToolIfEnabled = (toolName, tool) => {
|
|
1214
|
+
if (!disabledTools.has(toolName))
|
|
1215
|
+
pi.registerTool(tool);
|
|
1216
|
+
};
|
|
1213
1217
|
const home = process.env.HOME ?? "";
|
|
1214
1218
|
const sp = (p) => shortPath(cwd, home, p);
|
|
1215
1219
|
const TOOL_RESULT_INDENT = " ";
|
|
@@ -1492,7 +1496,7 @@ export default async function diffRendererExtension(pi) {
|
|
|
1492
1496
|
// write
|
|
1493
1497
|
// =======================================================================
|
|
1494
1498
|
const origWrite = createWriteTool(cwd);
|
|
1495
|
-
|
|
1499
|
+
registerToolIfEnabled("write", {
|
|
1496
1500
|
...origWrite,
|
|
1497
1501
|
name: "write",
|
|
1498
1502
|
async execute(tid, params, sig, upd, ctx) {
|
|
@@ -1578,7 +1582,7 @@ export default async function diffRendererExtension(pi) {
|
|
|
1578
1582
|
},
|
|
1579
1583
|
renderResult(result, _opt, theme, ctx) {
|
|
1580
1584
|
const text = getWidthAwareText(ctx.lastComponent);
|
|
1581
|
-
if (ctx
|
|
1585
|
+
if (isToolResultError(result, ctx)) {
|
|
1582
1586
|
const e = result.content
|
|
1583
1587
|
?.filter((c) => c.type === "text")
|
|
1584
1588
|
.map((c) => c.text || "")
|
|
@@ -1654,18 +1658,44 @@ export default async function diffRendererExtension(pi) {
|
|
|
1654
1658
|
const newText = typeof input?.newText === "string" ? input.newText : typeof input?.new_text === "string" ? input.new_text : "";
|
|
1655
1659
|
return oldText && oldText !== newText ? [{ oldText, newText }] : [];
|
|
1656
1660
|
}
|
|
1657
|
-
function
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1661
|
+
function normalizeEditMatchText(text) {
|
|
1662
|
+
return text
|
|
1663
|
+
.replace(/\r\n/g, "\n")
|
|
1664
|
+
.replace(/\r/g, "\n")
|
|
1665
|
+
.normalize("NFKC")
|
|
1666
|
+
.split("\n")
|
|
1667
|
+
.map((line) => line.trimEnd())
|
|
1668
|
+
.join("\n")
|
|
1669
|
+
.replace(/[\u2018\u2019\u201A\u201B]/g, "'")
|
|
1670
|
+
.replace(/[\u201C\u201D\u201E\u201F]/g, '"')
|
|
1671
|
+
.replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-")
|
|
1672
|
+
.replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ");
|
|
1667
1673
|
}
|
|
1668
|
-
|
|
1674
|
+
function rejectOverlappingEditMatches(content, filePath, operations) {
|
|
1675
|
+
if (!filePath || operations.length === 0)
|
|
1676
|
+
return;
|
|
1677
|
+
if (content.startsWith("\uFEFF"))
|
|
1678
|
+
content = content.slice(1);
|
|
1679
|
+
const normalizedContent = normalizeEditMatchText(content);
|
|
1680
|
+
for (const [index, operation] of operations.entries()) {
|
|
1681
|
+
const oldText = normalizeEditMatchText(operation.oldText);
|
|
1682
|
+
if (!oldText)
|
|
1683
|
+
continue;
|
|
1684
|
+
let occurrences = 0;
|
|
1685
|
+
let offset = 0;
|
|
1686
|
+
while (true) {
|
|
1687
|
+
const match = normalizedContent.indexOf(oldText, offset);
|
|
1688
|
+
if (match === -1)
|
|
1689
|
+
break;
|
|
1690
|
+
occurrences++;
|
|
1691
|
+
if (occurrences > 1) {
|
|
1692
|
+
throw new Error(`Found ambiguous overlapping occurrences for edits[${index}] in ${filePath}. Provide more context to make oldText unique.`);
|
|
1693
|
+
}
|
|
1694
|
+
offset = match + 1;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
registerToolIfEnabled("edit", {
|
|
1669
1699
|
...origEdit,
|
|
1670
1700
|
name: "edit",
|
|
1671
1701
|
parameters: {
|
|
@@ -1687,175 +1717,37 @@ export default async function diffRendererExtension(pi) {
|
|
|
1687
1717
|
async execute(tid, params, sig, upd, ctx) {
|
|
1688
1718
|
const fp = params.path ?? params.file_path ?? "";
|
|
1689
1719
|
const operations = getEditOperations(params);
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
break;
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
if (replaceOk) {
|
|
1709
|
-
writeFileSync(fp, content, "utf-8");
|
|
1710
|
-
const { diffs, summary } = summarizeEditOperations(operations);
|
|
1711
|
-
const lg = detectDiffLanguage(fp);
|
|
1712
|
-
if (operations.length === 1) {
|
|
1713
|
-
let editLine = 0;
|
|
1714
|
-
try {
|
|
1715
|
-
const idx = content.indexOf(operations[0].newText);
|
|
1716
|
-
if (idx >= 0)
|
|
1717
|
-
editLine = content.slice(0, idx).split("\n").length;
|
|
1718
|
-
}
|
|
1719
|
-
catch {
|
|
1720
|
-
editLine = 0;
|
|
1721
|
-
}
|
|
1722
|
-
const useFull = !!params._expandGaps;
|
|
1723
|
-
const diffData = useFull ? parseDiff(operations[0].oldText, operations[0].newText, undefined) : diffs[0];
|
|
1724
|
-
return {
|
|
1725
|
-
content: [{ type: "text", text: `Edited ${sp(fp)}` }],
|
|
1726
|
-
details: {
|
|
1727
|
-
_type: "editInfo",
|
|
1728
|
-
summary: editLine > 0 ? `${summary} at line ${editLine}` : summary,
|
|
1729
|
-
filePath: fp,
|
|
1730
|
-
editLine,
|
|
1731
|
-
diff: diffData,
|
|
1732
|
-
language: lg,
|
|
1733
|
-
oldContent: operations[0].oldText,
|
|
1734
|
-
newContent: operations[0].newText,
|
|
1735
|
-
_replaceStrategy: firstStrategy,
|
|
1736
|
-
},
|
|
1737
|
-
};
|
|
1738
|
-
}
|
|
1739
|
-
// Compute the line of the first edit for the title summary
|
|
1740
|
-
const firstEditLine = (() => {
|
|
1741
|
-
if (!operations[0]?.newText)
|
|
1742
|
-
return 0;
|
|
1743
|
-
const idx = content.indexOf(operations[0].newText);
|
|
1744
|
-
if (idx < 0)
|
|
1745
|
-
return 0;
|
|
1746
|
-
return content.slice(0, idx).split("\n").length;
|
|
1747
|
-
})();
|
|
1748
|
-
// Merge all diffs into one combined view for rendering
|
|
1749
|
-
const merged = {
|
|
1750
|
-
lines: diffs.flatMap((diff, i) => [
|
|
1751
|
-
...(i > 0
|
|
1752
|
-
? [
|
|
1753
|
-
{
|
|
1754
|
-
type: "sep",
|
|
1755
|
-
oldNum: null,
|
|
1756
|
-
newNum: null,
|
|
1757
|
-
content: `───── Edit ${i + 1} ─────`,
|
|
1758
|
-
},
|
|
1759
|
-
]
|
|
1760
|
-
: []),
|
|
1761
|
-
...diff.lines,
|
|
1762
|
-
]),
|
|
1763
|
-
added: diffs.reduce((sum, diff) => sum + diff.added, 0),
|
|
1764
|
-
removed: diffs.reduce((sum, diff) => sum + diff.removed, 0),
|
|
1765
|
-
chars: diffs.reduce((sum, diff) => sum + diff.chars, 0),
|
|
1766
|
-
};
|
|
1767
|
-
return {
|
|
1768
|
-
content: [{ type: "text", text: `Edited ${sp(fp)}` }],
|
|
1769
|
-
details: {
|
|
1770
|
-
_type: "multiEditInfo",
|
|
1771
|
-
summary: firstEditLine > 0 ? `${summary} at line ${firstEditLine}` : summary,
|
|
1772
|
-
filePath: fp,
|
|
1773
|
-
editCount: operations.length,
|
|
1774
|
-
diffLineCount: merged.lines.length,
|
|
1775
|
-
diff: merged,
|
|
1776
|
-
language: lg,
|
|
1777
|
-
},
|
|
1778
|
-
};
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
catch (replaceError) {
|
|
1782
|
-
// replace() failed; fall through to SDK edit tool
|
|
1783
|
-
console.warn(`[pi-diff] replace() failed, falling back to SDK: ${replaceError instanceof Error ? replaceError.message : String(replaceError)}`);
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
const result = await origEdit.execute(tid, params, sig, upd, ctx);
|
|
1787
|
-
if (operations.length === 0)
|
|
1788
|
-
return result;
|
|
1789
|
-
const { diffs, summary } = summarizeEditOperations(operations);
|
|
1790
|
-
const lg = detectDiffLanguage(fp);
|
|
1791
|
-
if (operations.length === 1) {
|
|
1792
|
-
let editLine = 0;
|
|
1793
|
-
try {
|
|
1794
|
-
if (fp && existsSync(fp)) {
|
|
1795
|
-
const f = readFileSync(fp, "utf-8");
|
|
1796
|
-
const idx = f.indexOf(operations[0].newText);
|
|
1797
|
-
if (idx >= 0)
|
|
1798
|
-
editLine = f.slice(0, idx).split("\n").length;
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
catch {
|
|
1802
|
-
editLine = 0;
|
|
1803
|
-
}
|
|
1804
|
-
const useFull = !!params._expandGaps;
|
|
1805
|
-
const diffData = useFull ? parseDiff(operations[0].oldText, operations[0].newText, undefined) : diffs[0];
|
|
1806
|
-
result.details = {
|
|
1807
|
-
_type: "editInfo",
|
|
1808
|
-
summary: editLine > 0 ? `${summary} at line ${editLine}` : summary,
|
|
1809
|
-
filePath: fp,
|
|
1810
|
-
editLine,
|
|
1811
|
-
diff: diffData,
|
|
1812
|
-
language: lg,
|
|
1813
|
-
oldContent: operations[0].oldText,
|
|
1814
|
-
newContent: operations[0].newText,
|
|
1815
|
-
};
|
|
1720
|
+
const guardedEdit = createEditTool(cwd, {
|
|
1721
|
+
operations: {
|
|
1722
|
+
access: (filePath) => accessFile(filePath, constants.R_OK | constants.W_OK),
|
|
1723
|
+
readFile: async (filePath) => {
|
|
1724
|
+
const content = await readFile(filePath);
|
|
1725
|
+
rejectOverlappingEditMatches(content.toString("utf8"), fp, operations);
|
|
1726
|
+
return content;
|
|
1727
|
+
},
|
|
1728
|
+
writeFile: (filePath, content) => writeFile(filePath, content, "utf8"),
|
|
1729
|
+
},
|
|
1730
|
+
});
|
|
1731
|
+
const result = await guardedEdit.execute(tid, params, sig, upd, ctx);
|
|
1732
|
+
const patch = result?.details?.patch;
|
|
1733
|
+
const diff = typeof patch === "string" ? parsePatchFiles(patch)[0] : undefined;
|
|
1734
|
+
if (!diff || (diff.added === 0 && diff.removed === 0))
|
|
1816
1735
|
return result;
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
const
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
...(i > 0
|
|
1823
|
-
? [
|
|
1824
|
-
{
|
|
1825
|
-
type: "sep",
|
|
1826
|
-
oldNum: null,
|
|
1827
|
-
newNum: null,
|
|
1828
|
-
content: `───── Edit ${i + 1} ─────`,
|
|
1829
|
-
},
|
|
1830
|
-
]
|
|
1831
|
-
: []),
|
|
1832
|
-
...diff.lines,
|
|
1833
|
-
]),
|
|
1834
|
-
added: diffs.reduce((sum, diff) => sum + diff.added, 0),
|
|
1835
|
-
removed: diffs.reduce((sum, diff) => sum + diff.removed, 0),
|
|
1836
|
-
chars: diffs.reduce((sum, diff) => sum + diff.chars, 0),
|
|
1837
|
-
};
|
|
1838
|
-
let firstEditLine = 0;
|
|
1839
|
-
try {
|
|
1840
|
-
if (fp) {
|
|
1841
|
-
const f = readFileSync(fp, "utf8");
|
|
1842
|
-
const idx = f.indexOf(operations[0].newText);
|
|
1843
|
-
if (idx >= 0)
|
|
1844
|
-
firstEditLine = f.slice(0, idx).split("\n").length;
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
catch {
|
|
1848
|
-
firstEditLine = 0;
|
|
1849
|
-
}
|
|
1850
|
-
result.details = {
|
|
1851
|
-
_type: "multiEditInfo",
|
|
1852
|
-
summary: firstEditLine > 0 ? `${summary} at line ${firstEditLine}` : summary,
|
|
1736
|
+
const editCount = operations.length || (Array.isArray(params?.edits) ? params.edits.length : 1);
|
|
1737
|
+
stashEditHeaderStats(tid, editCount, diff.lines.length, diff.added, diff.removed);
|
|
1738
|
+
const details = {
|
|
1739
|
+
...result.details,
|
|
1740
|
+
_type: editCount > 1 ? "multiEditInfo" : "editInfo",
|
|
1853
1741
|
filePath: fp,
|
|
1854
|
-
|
|
1855
|
-
diffLineCount:
|
|
1856
|
-
|
|
1857
|
-
|
|
1742
|
+
diff,
|
|
1743
|
+
diffLineCount: diff.lines.length,
|
|
1744
|
+
editCount,
|
|
1745
|
+
linesAdded: diff.added,
|
|
1746
|
+
linesRemoved: diff.removed,
|
|
1747
|
+
language: detectDiffLanguage(fp),
|
|
1748
|
+
summary: summarize(diff.added, diff.removed),
|
|
1858
1749
|
};
|
|
1750
|
+
result.details = details;
|
|
1859
1751
|
return result;
|
|
1860
1752
|
},
|
|
1861
1753
|
renderCall(args, theme, ctx) {
|
|
@@ -1865,25 +1757,12 @@ export default async function diffRendererExtension(pi) {
|
|
|
1865
1757
|
resolvePreviewDiffColors(theme);
|
|
1866
1758
|
const stats = editCallStatsSuffix(ctx.toolCallId, theme);
|
|
1867
1759
|
if (ctx.argsComplete && operations.length > 0) {
|
|
1868
|
-
let previewLine = 0;
|
|
1869
|
-
try {
|
|
1870
|
-
if (fp && existsSync(fp)) {
|
|
1871
|
-
const cur = readFileSync(fp, "utf-8");
|
|
1872
|
-
const idx = cur.indexOf(operations[0].oldText);
|
|
1873
|
-
if (idx >= 0)
|
|
1874
|
-
previewLine = cur.slice(0, idx).split("\n").length;
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
catch {
|
|
1878
|
-
previewLine = 0;
|
|
1879
|
-
}
|
|
1880
|
-
const loc = previewLine > 0 ? `${TOOL_RESULT_INDENT}${theme.fg("muted", `at line ${previewLine}`)}` : "";
|
|
1881
1760
|
setToolHeaderBg(text);
|
|
1882
1761
|
text.setText(formatToolFrameHeaderText({
|
|
1883
1762
|
label: "edit",
|
|
1884
1763
|
filePath: fp,
|
|
1885
1764
|
theme,
|
|
1886
|
-
suffix:
|
|
1765
|
+
suffix: stats,
|
|
1887
1766
|
topPad: EDIT_DIFF_RESULT_FRAME.topPad,
|
|
1888
1767
|
bottomPad: EDIT_DIFF_RESULT_FRAME.bottomPad,
|
|
1889
1768
|
headerLeftPad: EDIT_DIFF_RESULT_FRAME.headerLeftPad,
|
|
@@ -1946,7 +1825,7 @@ export default async function diffRendererExtension(pi) {
|
|
|
1946
1825
|
return text;
|
|
1947
1826
|
},
|
|
1948
1827
|
});
|
|
1949
|
-
|
|
1828
|
+
registerToolIfEnabled("apply_patch", {
|
|
1950
1829
|
name: "apply_patch",
|
|
1951
1830
|
label: "apply_patch",
|
|
1952
1831
|
description: "Multi-file patch engine. One call can add, update, delete, or move multiple files. Uses structured JSON changes array.",
|
|
@@ -2032,6 +1911,5 @@ export default async function diffRendererExtension(pi) {
|
|
|
2032
1911
|
return text;
|
|
2033
1912
|
},
|
|
2034
1913
|
});
|
|
2035
|
-
registerEditGuard(pi);
|
|
2036
1914
|
}
|
|
2037
1915
|
//# sourceMappingURL=index.js.map
|