@heyhuynhgiabuu/pi-diff 0.7.6 → 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 +3 -9
- package/dist/core/apply-patch.d.ts +1 -1
- package/dist/core/apply-patch.js +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 +4 -2
- package/dist/core/replace.d.ts.map +1 -1
- package/dist/core/replace.js +4 -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.map +1 -1
- package/dist/index.js +67 -195
- 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,15 +20,14 @@
|
|
|
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
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) {
|
|
@@ -1659,16 +1658,42 @@ export default async function diffRendererExtension(pi) {
|
|
|
1659
1658
|
const newText = typeof input?.newText === "string" ? input.newText : typeof input?.new_text === "string" ? input.new_text : "";
|
|
1660
1659
|
return oldText && oldText !== newText ? [{ oldText, newText }] : [];
|
|
1661
1660
|
}
|
|
1662
|
-
function
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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, " ");
|
|
1673
|
+
}
|
|
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
|
+
}
|
|
1672
1697
|
}
|
|
1673
1698
|
registerToolIfEnabled("edit", {
|
|
1674
1699
|
...origEdit,
|
|
@@ -1692,175 +1717,37 @@ export default async function diffRendererExtension(pi) {
|
|
|
1692
1717
|
async execute(tid, params, sig, upd, ctx) {
|
|
1693
1718
|
const fp = params.path ?? params.file_path ?? "";
|
|
1694
1719
|
const operations = getEditOperations(params);
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
break;
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
if (replaceOk) {
|
|
1714
|
-
writeFileSync(fp, content, "utf-8");
|
|
1715
|
-
const { diffs, summary } = summarizeEditOperations(operations);
|
|
1716
|
-
const lg = detectDiffLanguage(fp);
|
|
1717
|
-
if (operations.length === 1) {
|
|
1718
|
-
let editLine = 0;
|
|
1719
|
-
try {
|
|
1720
|
-
const idx = content.indexOf(operations[0].newText);
|
|
1721
|
-
if (idx >= 0)
|
|
1722
|
-
editLine = content.slice(0, idx).split("\n").length;
|
|
1723
|
-
}
|
|
1724
|
-
catch {
|
|
1725
|
-
editLine = 0;
|
|
1726
|
-
}
|
|
1727
|
-
const useFull = !!params._expandGaps;
|
|
1728
|
-
const diffData = useFull ? parseDiff(operations[0].oldText, operations[0].newText, undefined) : diffs[0];
|
|
1729
|
-
return {
|
|
1730
|
-
content: [{ type: "text", text: `Edited ${sp(fp)}` }],
|
|
1731
|
-
details: {
|
|
1732
|
-
_type: "editInfo",
|
|
1733
|
-
summary: editLine > 0 ? `${summary} at line ${editLine}` : summary,
|
|
1734
|
-
filePath: fp,
|
|
1735
|
-
editLine,
|
|
1736
|
-
diff: diffData,
|
|
1737
|
-
language: lg,
|
|
1738
|
-
oldContent: operations[0].oldText,
|
|
1739
|
-
newContent: operations[0].newText,
|
|
1740
|
-
_replaceStrategy: firstStrategy,
|
|
1741
|
-
},
|
|
1742
|
-
};
|
|
1743
|
-
}
|
|
1744
|
-
// Compute the line of the first edit for the title summary
|
|
1745
|
-
const firstEditLine = (() => {
|
|
1746
|
-
if (!operations[0]?.newText)
|
|
1747
|
-
return 0;
|
|
1748
|
-
const idx = content.indexOf(operations[0].newText);
|
|
1749
|
-
if (idx < 0)
|
|
1750
|
-
return 0;
|
|
1751
|
-
return content.slice(0, idx).split("\n").length;
|
|
1752
|
-
})();
|
|
1753
|
-
// Merge all diffs into one combined view for rendering
|
|
1754
|
-
const merged = {
|
|
1755
|
-
lines: diffs.flatMap((diff, i) => [
|
|
1756
|
-
...(i > 0
|
|
1757
|
-
? [
|
|
1758
|
-
{
|
|
1759
|
-
type: "sep",
|
|
1760
|
-
oldNum: null,
|
|
1761
|
-
newNum: null,
|
|
1762
|
-
content: `───── Edit ${i + 1} ─────`,
|
|
1763
|
-
},
|
|
1764
|
-
]
|
|
1765
|
-
: []),
|
|
1766
|
-
...diff.lines,
|
|
1767
|
-
]),
|
|
1768
|
-
added: diffs.reduce((sum, diff) => sum + diff.added, 0),
|
|
1769
|
-
removed: diffs.reduce((sum, diff) => sum + diff.removed, 0),
|
|
1770
|
-
chars: diffs.reduce((sum, diff) => sum + diff.chars, 0),
|
|
1771
|
-
};
|
|
1772
|
-
return {
|
|
1773
|
-
content: [{ type: "text", text: `Edited ${sp(fp)}` }],
|
|
1774
|
-
details: {
|
|
1775
|
-
_type: "multiEditInfo",
|
|
1776
|
-
summary: firstEditLine > 0 ? `${summary} at line ${firstEditLine}` : summary,
|
|
1777
|
-
filePath: fp,
|
|
1778
|
-
editCount: operations.length,
|
|
1779
|
-
diffLineCount: merged.lines.length,
|
|
1780
|
-
diff: merged,
|
|
1781
|
-
language: lg,
|
|
1782
|
-
},
|
|
1783
|
-
};
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
catch (replaceError) {
|
|
1787
|
-
// replace() failed; fall through to SDK edit tool
|
|
1788
|
-
console.warn(`[pi-diff] replace() failed, falling back to SDK: ${replaceError instanceof Error ? replaceError.message : String(replaceError)}`);
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
const result = await origEdit.execute(tid, params, sig, upd, ctx);
|
|
1792
|
-
if (operations.length === 0)
|
|
1793
|
-
return result;
|
|
1794
|
-
const { diffs, summary } = summarizeEditOperations(operations);
|
|
1795
|
-
const lg = detectDiffLanguage(fp);
|
|
1796
|
-
if (operations.length === 1) {
|
|
1797
|
-
let editLine = 0;
|
|
1798
|
-
try {
|
|
1799
|
-
if (fp && existsSync(fp)) {
|
|
1800
|
-
const f = readFileSync(fp, "utf-8");
|
|
1801
|
-
const idx = f.indexOf(operations[0].newText);
|
|
1802
|
-
if (idx >= 0)
|
|
1803
|
-
editLine = f.slice(0, idx).split("\n").length;
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
catch {
|
|
1807
|
-
editLine = 0;
|
|
1808
|
-
}
|
|
1809
|
-
const useFull = !!params._expandGaps;
|
|
1810
|
-
const diffData = useFull ? parseDiff(operations[0].oldText, operations[0].newText, undefined) : diffs[0];
|
|
1811
|
-
result.details = {
|
|
1812
|
-
_type: "editInfo",
|
|
1813
|
-
summary: editLine > 0 ? `${summary} at line ${editLine}` : summary,
|
|
1814
|
-
filePath: fp,
|
|
1815
|
-
editLine,
|
|
1816
|
-
diff: diffData,
|
|
1817
|
-
language: lg,
|
|
1818
|
-
oldContent: operations[0].oldText,
|
|
1819
|
-
newContent: operations[0].newText,
|
|
1820
|
-
};
|
|
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))
|
|
1821
1735
|
return result;
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
const
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
...(i > 0
|
|
1828
|
-
? [
|
|
1829
|
-
{
|
|
1830
|
-
type: "sep",
|
|
1831
|
-
oldNum: null,
|
|
1832
|
-
newNum: null,
|
|
1833
|
-
content: `───── Edit ${i + 1} ─────`,
|
|
1834
|
-
},
|
|
1835
|
-
]
|
|
1836
|
-
: []),
|
|
1837
|
-
...diff.lines,
|
|
1838
|
-
]),
|
|
1839
|
-
added: diffs.reduce((sum, diff) => sum + diff.added, 0),
|
|
1840
|
-
removed: diffs.reduce((sum, diff) => sum + diff.removed, 0),
|
|
1841
|
-
chars: diffs.reduce((sum, diff) => sum + diff.chars, 0),
|
|
1842
|
-
};
|
|
1843
|
-
let firstEditLine = 0;
|
|
1844
|
-
try {
|
|
1845
|
-
if (fp) {
|
|
1846
|
-
const f = readFileSync(fp, "utf8");
|
|
1847
|
-
const idx = f.indexOf(operations[0].newText);
|
|
1848
|
-
if (idx >= 0)
|
|
1849
|
-
firstEditLine = f.slice(0, idx).split("\n").length;
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
catch {
|
|
1853
|
-
firstEditLine = 0;
|
|
1854
|
-
}
|
|
1855
|
-
result.details = {
|
|
1856
|
-
_type: "multiEditInfo",
|
|
1857
|
-
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",
|
|
1858
1741
|
filePath: fp,
|
|
1859
|
-
|
|
1860
|
-
diffLineCount:
|
|
1861
|
-
|
|
1862
|
-
|
|
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),
|
|
1863
1749
|
};
|
|
1750
|
+
result.details = details;
|
|
1864
1751
|
return result;
|
|
1865
1752
|
},
|
|
1866
1753
|
renderCall(args, theme, ctx) {
|
|
@@ -1870,25 +1757,12 @@ export default async function diffRendererExtension(pi) {
|
|
|
1870
1757
|
resolvePreviewDiffColors(theme);
|
|
1871
1758
|
const stats = editCallStatsSuffix(ctx.toolCallId, theme);
|
|
1872
1759
|
if (ctx.argsComplete && operations.length > 0) {
|
|
1873
|
-
let previewLine = 0;
|
|
1874
|
-
try {
|
|
1875
|
-
if (fp && existsSync(fp)) {
|
|
1876
|
-
const cur = readFileSync(fp, "utf-8");
|
|
1877
|
-
const idx = cur.indexOf(operations[0].oldText);
|
|
1878
|
-
if (idx >= 0)
|
|
1879
|
-
previewLine = cur.slice(0, idx).split("\n").length;
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
catch {
|
|
1883
|
-
previewLine = 0;
|
|
1884
|
-
}
|
|
1885
|
-
const loc = previewLine > 0 ? `${TOOL_RESULT_INDENT}${theme.fg("muted", `at line ${previewLine}`)}` : "";
|
|
1886
1760
|
setToolHeaderBg(text);
|
|
1887
1761
|
text.setText(formatToolFrameHeaderText({
|
|
1888
1762
|
label: "edit",
|
|
1889
1763
|
filePath: fp,
|
|
1890
1764
|
theme,
|
|
1891
|
-
suffix:
|
|
1765
|
+
suffix: stats,
|
|
1892
1766
|
topPad: EDIT_DIFF_RESULT_FRAME.topPad,
|
|
1893
1767
|
bottomPad: EDIT_DIFF_RESULT_FRAME.bottomPad,
|
|
1894
1768
|
headerLeftPad: EDIT_DIFF_RESULT_FRAME.headerLeftPad,
|
|
@@ -2037,7 +1911,5 @@ export default async function diffRendererExtension(pi) {
|
|
|
2037
1911
|
return text;
|
|
2038
1912
|
},
|
|
2039
1913
|
});
|
|
2040
|
-
if (!disabledTools.has("edit"))
|
|
2041
|
-
registerEditGuard(pi);
|
|
2042
1914
|
}
|
|
2043
1915
|
//# sourceMappingURL=index.js.map
|