@heyhuynhgiabuu/pi-diff 0.8.1 → 0.9.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/dist/index.js CHANGED
@@ -25,7 +25,7 @@ import { access as accessFile, readFile, writeFile } from "node:fs/promises";
25
25
  import { extname, relative } from "node:path";
26
26
  import { codeToANSI } from "@shikijs/cli";
27
27
  import * as Diff from "diff";
28
- import { executeApplyPatch, formatApplyPatchResult } from "./core/apply-patch.js";
28
+ import { executeApplyPatch, formatApplyPatchResult, parseApplyPatchInput, } from "./core/apply-patch.js";
29
29
  import { configIndicatorStyle, loadPiDiffConfig } from "./core/config.js";
30
30
  import { computeHunkBlocks, getSepStyle, parseDiff, parsePatchFiles, resolveSepStyle, sepLabelSplit, sepLabelUnified, } from "./core/diff.js";
31
31
  import { applyDiffPalette as applySharedDiffPalette, lang as detectDiffLanguage, renderSplit as renderSharedSplit, resolveDiffColors as resolveSharedDiffColors, themeCacheKey as sharedThemeCacheKey, } from "./review/hunk-preview.js";
@@ -1219,13 +1219,13 @@ export default async function diffRendererExtension(pi) {
1219
1219
  const TOOL_RESULT_INDENT = " ";
1220
1220
  const TOOL_HEADER_LEFT_PAD = 0;
1221
1221
  const DIFF_BODY_LEFT_PAD = 0;
1222
- /** Built-in `edit` tool diff result frame; intentionally offset from read/write/apply_patch previews. */
1222
+ /** Keep `edit` on the default host Box: no extra edge padding, one title/body separator. */
1223
1223
  const EDIT_DIFF_RESULT_FRAME = {
1224
- headerLeftPad: 1,
1225
- bodyLeftPad: 1,
1226
- topPad: 1,
1224
+ headerLeftPad: 0,
1225
+ bodyLeftPad: 0,
1226
+ topPad: 0,
1227
1227
  bottomPad: 1,
1228
- previewBottomPad: 1,
1228
+ previewBottomPad: 0,
1229
1229
  };
1230
1230
  function resolvePreviewDiffColors(theme) {
1231
1231
  resolveDiffColors(theme);
@@ -1266,8 +1266,8 @@ export default async function diffRendererExtension(pi) {
1266
1266
  }
1267
1267
  function formatToolErrorResult(name, message, theme) {
1268
1268
  const meta = theme.fg("error", theme.bold(formatToolHeaderName(name)));
1269
- const header = formatToolFrameHeaderText({ meta, theme, headerLeftPad: 1, topPad: 0, bottomPad: 1 });
1270
- return `${header}\n ${theme.fg("error", message)}\n`;
1269
+ const header = formatToolFrameHeaderText({ meta, theme, bottomPad: 1 });
1270
+ return `${header}\n${theme.fg("error", message)}`;
1271
1271
  }
1272
1272
  function summarizeApplyPatchChanges(changes, theme) {
1273
1273
  const labels = changes.map((change) => formatToolHeaderPath(theme, sp(change.path)));
@@ -1279,7 +1279,6 @@ export default async function diffRendererExtension(pi) {
1279
1279
  const applied = Array.isArray(result?.applied) ? result.applied : [];
1280
1280
  if (!applied.length)
1281
1281
  return false;
1282
- const w = termW();
1283
1282
  const previewable = applied.filter((change) => {
1284
1283
  if (typeof change?.path !== "string")
1285
1284
  return false;
@@ -1289,7 +1288,7 @@ export default async function diffRendererExtension(pi) {
1289
1288
  return typeof change.oldContent === "string";
1290
1289
  return false;
1291
1290
  });
1292
- if (previewable.length !== applied.length)
1291
+ if (!previewable.length)
1293
1292
  return false;
1294
1293
  if (previewable.length === 1) {
1295
1294
  const change = previewable[0];
@@ -1297,7 +1296,7 @@ export default async function diffRendererExtension(pi) {
1297
1296
  clearToolHeaderBg(text);
1298
1297
  resolvePreviewDiffColors(theme);
1299
1298
  const lineCount = change.newContent.split("\n").length;
1300
- const newHdr = bgLine(`${theme.fg("success", `✓ new file (${lineCount} lines)`)}`, w);
1299
+ const newHdr = theme.fg("success", `✓ new file (${lineCount} lines)`);
1301
1300
  const fp = change.path;
1302
1301
  const pk = `ap:nf:${sharedThemeCacheKey(theme)}:${fp}:${lineCount}`;
1303
1302
  if (ctx.state._nfk !== pk) {
@@ -1321,13 +1320,12 @@ export default async function diffRendererExtension(pi) {
1321
1320
  typeof change.oldContent === "string" &&
1322
1321
  typeof change.path === "string") {
1323
1322
  const parsed = parseDiff(change.oldContent, change.newContent ?? "");
1324
- setDiffPreviewTask(text, "ap", (width) => formatToolFrameHeader({
1323
+ setDiffPreviewTask(text, "ap", () => formatToolFrameHeaderText({
1325
1324
  meta: `${theme.fg("toolTitle", theme.bold(formatToolHeaderName("apply_patch")))}${TOOL_RESULT_INDENT}${theme.fg("muted", `(1 change)`)}${TOOL_RESULT_INDENT}${formatToolHeaderPath(theme, sp(change.path))}`,
1326
1325
  theme,
1327
- width,
1328
1326
  topPad: 0,
1329
1327
  bottomPad: 1,
1330
- }), parsed, detectDiffLanguage(change.path), MAX_PREVIEW_LINES, theme, ctx, { previewBottomPad: 1, compactGutter: true });
1328
+ }), parsed, detectDiffLanguage(change.path), MAX_PREVIEW_LINES, theme, ctx, { previewBottomPad: 0, compactGutter: true });
1331
1329
  return true;
1332
1330
  }
1333
1331
  return false;
@@ -1351,13 +1349,12 @@ export default async function diffRendererExtension(pi) {
1351
1349
  chars += parsed.chars;
1352
1350
  lines.push(...parsed.lines);
1353
1351
  }
1354
- setDiffPreviewTask(text, "ap", (width) => formatToolFrameHeader({
1352
+ setDiffPreviewTask(text, "ap", () => formatToolFrameHeaderText({
1355
1353
  meta: `${theme.fg("toolTitle", theme.bold(formatToolHeaderName("apply_patch")))}${TOOL_RESULT_INDENT}${theme.fg("muted", `(${previewable.length} changes)`)} ${summarizeThemed(added, removed, theme)}${TOOL_RESULT_INDENT}${summarizeApplyPatchChanges(previewable, theme)}`,
1356
1354
  theme,
1357
- width,
1358
1355
  topPad: 0,
1359
1356
  bottomPad: 1,
1360
- }), { lines, added, removed, chars }, mixedLanguage ? undefined : language, MAX_PREVIEW_LINES, theme, ctx, { previewBottomPad: 1, compactGutter: true });
1357
+ }), { lines, added, removed, chars }, mixedLanguage ? undefined : language, MAX_PREVIEW_LINES, theme, ctx, { previewBottomPad: 0, compactGutter: true });
1361
1358
  return true;
1362
1359
  }
1363
1360
  function editEditsCountLabel(edits, diffLines, theme) {
@@ -1442,7 +1439,7 @@ export default async function diffRendererExtension(pi) {
1442
1439
  return bottom ? `${main}\n${bottom}` : main;
1443
1440
  };
1444
1441
  text.__piDiffTask = {
1445
- placeholder: joinHeaderBody(termW(), padDiffBody(theme.fg("muted", " rendering diff…"), frame?.bodyLeftPad)),
1442
+ placeholder: joinHeaderBody(termW(), padDiffBody(theme.fg("muted", "rendering diff…"), frame?.bodyLeftPad)),
1446
1443
  fallback: header(termW()),
1447
1444
  invalidate: ctx.invalidate,
1448
1445
  key: (width) => {
@@ -1499,6 +1496,7 @@ export default async function diffRendererExtension(pi) {
1499
1496
  registerToolIfEnabled("write", {
1500
1497
  ...origWrite,
1501
1498
  name: "write",
1499
+ renderShell: "default",
1502
1500
  async execute(tid, params, sig, upd, ctx) {
1503
1501
  const fp = params.path ?? params.file_path ?? "";
1504
1502
  let old = null;
@@ -1547,7 +1545,6 @@ export default async function diffRendererExtension(pi) {
1547
1545
  const label = isNew ? "create" : "write";
1548
1546
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1549
1547
  resolveDiffColors(theme);
1550
- const w = termW();
1551
1548
  const stats = writeCallStatsSuffix(ctx.toolCallId, theme);
1552
1549
  if (args?.content && !ctx.argsComplete) {
1553
1550
  const n = String(args.content).split("\n").length;
@@ -1557,7 +1554,7 @@ export default async function diffRendererExtension(pi) {
1557
1554
  return text;
1558
1555
  }
1559
1556
  if (args?.content && ctx.argsComplete && isNew) {
1560
- const title = formatToolFrameHeader({ label, filePath: fp, theme, width: w, topPad: 0, bottomPad: 1 });
1557
+ const title = formatToolFrameHeaderText({ label, filePath: fp, theme, topPad: 0, bottomPad: 1 });
1561
1558
  const previewKey = `create:${sharedThemeCacheKey(theme)}:${fp}:${String(args.content).length}`;
1562
1559
  if (ctx.state._previewKey !== previewKey) {
1563
1560
  ctx.state._previewKey = previewKey;
@@ -1596,7 +1593,7 @@ export default async function diffRendererExtension(pi) {
1596
1593
  if (d?._type === "diff") {
1597
1594
  setDiffPreviewTask(text, "wd", "", d.diff, d.language, MAX_RENDER_LINES, theme, ctx, {
1598
1595
  omitHeader: true,
1599
- previewBottomPad: 1,
1596
+ previewBottomPad: 0,
1600
1597
  compactGutter: true,
1601
1598
  });
1602
1599
  return text;
@@ -1604,15 +1601,14 @@ export default async function diffRendererExtension(pi) {
1604
1601
  if (d?._type === "noChange") {
1605
1602
  text.__piDiffTask = undefined;
1606
1603
  clearToolHeaderBg(text);
1607
- text.setText(`${TOOL_RESULT_INDENT}${theme.fg("muted", "✓ no changes")}`);
1604
+ text.setText(theme.fg("muted", "✓ no changes"));
1608
1605
  return text;
1609
1606
  }
1610
1607
  if (d?._type === "new") {
1611
1608
  const { lines: lineCount, content: rawContent, filePath: fp } = d;
1612
1609
  clearToolHeaderBg(text);
1613
1610
  resolvePreviewDiffColors(theme);
1614
- const w = termW();
1615
- const newHdr = bgLine(`${theme.fg("success", `✓ new file (${lineCount} lines)`)}`, w);
1611
+ const newHdr = theme.fg("success", `✓ new file (${lineCount} lines)`);
1616
1612
  const pk = `nf:${sharedThemeCacheKey(theme)}:${fp}:${lineCount}`;
1617
1613
  if (ctx.state._nfk !== pk) {
1618
1614
  ctx.state._nfk = pk;
@@ -1629,7 +1625,7 @@ export default async function diffRendererExtension(pi) {
1629
1625
  const maxShow = hlLines.length;
1630
1626
  const preview = hlLines.slice(0, maxShow).join("\n").replace(/\n+$/, "");
1631
1627
  const rem = hlLines.length - maxShow;
1632
- const moreLine = rem > 0 ? `\n${bgLine(`${TOOL_RESULT_INDENT}${theme.fg("muted", `… ${rem} more lines`)}`, width)}` : "";
1628
+ const moreLine = rem > 0 ? `\n${bgLine(theme.fg("muted", `… ${rem} more lines`), width)}` : "";
1633
1629
  return `${newHdr}\n${padDiffBody(preview)}${moreLine}`;
1634
1630
  },
1635
1631
  };
@@ -1637,7 +1633,7 @@ export default async function diffRendererExtension(pi) {
1637
1633
  return text;
1638
1634
  }
1639
1635
  clearToolHeaderBg(text);
1640
- text.setText(`${TOOL_RESULT_INDENT}${theme.fg("dim", String(result?.content?.[0]?.text ?? "written").slice(0, 120))}`);
1636
+ text.setText(theme.fg("dim", String(result?.content?.[0]?.text ?? "written").slice(0, 120)));
1641
1637
  return text;
1642
1638
  },
1643
1639
  });
@@ -1698,6 +1694,7 @@ export default async function diffRendererExtension(pi) {
1698
1694
  registerToolIfEnabled("edit", {
1699
1695
  ...origEdit,
1700
1696
  name: "edit",
1697
+ renderShell: "default",
1701
1698
  parameters: {
1702
1699
  ...(origEdit.parameters || {}),
1703
1700
  properties: {
@@ -1752,34 +1749,19 @@ export default async function diffRendererExtension(pi) {
1752
1749
  },
1753
1750
  renderCall(args, theme, ctx) {
1754
1751
  const fp = args?.path ?? args?.file_path ?? "";
1755
- const operations = getEditOperations(args);
1756
1752
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1757
1753
  resolvePreviewDiffColors(theme);
1758
1754
  const stats = editCallStatsSuffix(ctx.toolCallId, theme);
1759
- if (ctx.argsComplete && operations.length > 0) {
1760
- setToolHeaderBg(text);
1761
- text.setText(formatToolFrameHeaderText({
1762
- label: "edit",
1763
- filePath: fp,
1764
- theme,
1765
- suffix: stats,
1766
- topPad: EDIT_DIFF_RESULT_FRAME.topPad,
1767
- bottomPad: EDIT_DIFF_RESULT_FRAME.bottomPad,
1768
- headerLeftPad: EDIT_DIFF_RESULT_FRAME.headerLeftPad,
1769
- }));
1770
- }
1771
- else {
1772
- text.setText(formatToolFrameHeader({
1773
- label: "edit",
1774
- filePath: fp,
1775
- theme,
1776
- width: termW(),
1777
- suffix: stats,
1778
- topPad: EDIT_DIFF_RESULT_FRAME.topPad,
1779
- bottomPad: EDIT_DIFF_RESULT_FRAME.bottomPad,
1780
- headerLeftPad: EDIT_DIFF_RESULT_FRAME.headerLeftPad,
1781
- }));
1782
- }
1755
+ setToolHeaderBg(text);
1756
+ text.setText(formatToolFrameHeaderText({
1757
+ label: "edit",
1758
+ filePath: fp,
1759
+ theme,
1760
+ suffix: stats,
1761
+ topPad: EDIT_DIFF_RESULT_FRAME.topPad,
1762
+ bottomPad: EDIT_DIFF_RESULT_FRAME.bottomPad,
1763
+ headerLeftPad: EDIT_DIFF_RESULT_FRAME.headerLeftPad,
1764
+ }));
1783
1765
  return text;
1784
1766
  },
1785
1767
  renderResult(result, _opt, theme, ctx) {
@@ -1821,50 +1803,117 @@ export default async function diffRendererExtension(pi) {
1821
1803
  }
1822
1804
  text.__piDiffTask = undefined;
1823
1805
  clearToolHeaderBg(text);
1824
- text.setText(`${TOOL_RESULT_INDENT}${theme.fg("dim", String(result?.content?.[0]?.text ?? "edited").slice(0, 120))}`);
1806
+ text.setText(theme.fg("dim", String(result?.content?.[0]?.text ?? "edited").slice(0, 120)));
1825
1807
  return text;
1826
1808
  },
1827
1809
  });
1828
1810
  registerToolIfEnabled("apply_patch", {
1829
1811
  name: "apply_patch",
1830
1812
  label: "apply_patch",
1813
+ renderShell: "default",
1831
1814
  description: "Multi-file patch engine. One call can add, update, delete, or move multiple files. Uses structured JSON changes array.",
1832
1815
  parameters: {
1833
1816
  type: "object",
1817
+ additionalProperties: false,
1834
1818
  properties: {
1835
1819
  changes: {
1836
1820
  type: "array",
1837
- description: "Array of file changes to apply atomically.",
1821
+ minItems: 1,
1822
+ description: "Changes are fully prepared before commit; filesystem rollback after a commit failure is best effort.",
1838
1823
  items: {
1839
- type: "object",
1840
- properties: {
1841
- path: { type: "string", description: "Absolute path to the file." },
1842
- action: {
1843
- type: "string",
1844
- enum: ["add", "update", "delete", "move"],
1845
- description: "The operation to perform.",
1824
+ oneOf: [
1825
+ {
1826
+ type: "object",
1827
+ additionalProperties: false,
1828
+ properties: {
1829
+ path: { type: "string", minLength: 1, description: "Path within the current workspace." },
1830
+ action: { type: "string", enum: ["add"] },
1831
+ content: { type: "string", description: "Content for the new file." },
1832
+ },
1833
+ required: ["path", "action", "content"],
1846
1834
  },
1847
- content: { type: "string", description: "Content for new files (action=add)." },
1848
- oldText: { type: "string", description: "Text to find for updates (action=update)." },
1849
- newText: { type: "string", description: "Replacement text for updates (action=update)." },
1850
- movePath: { type: "string", description: "Destination path for moves (action=move)." },
1851
- },
1852
- required: ["path", "action"],
1835
+ {
1836
+ type: "object",
1837
+ additionalProperties: false,
1838
+ properties: {
1839
+ path: { type: "string", minLength: 1, description: "Path within the current workspace." },
1840
+ action: { type: "string", enum: ["update"] },
1841
+ oldText: { type: "string", minLength: 1, description: "Unique text to find." },
1842
+ newText: { type: "string", description: "Replacement text; omitted means empty text." },
1843
+ },
1844
+ required: ["path", "action", "oldText"],
1845
+ },
1846
+ {
1847
+ type: "object",
1848
+ additionalProperties: false,
1849
+ properties: {
1850
+ path: { type: "string", minLength: 1, description: "Path within the current workspace." },
1851
+ action: { type: "string", enum: ["update"] },
1852
+ edits: {
1853
+ type: "array",
1854
+ minItems: 1,
1855
+ items: {
1856
+ type: "object",
1857
+ additionalProperties: false,
1858
+ properties: {
1859
+ oldText: { type: "string", minLength: 1 },
1860
+ newText: { type: "string" },
1861
+ },
1862
+ required: ["oldText", "newText"],
1863
+ },
1864
+ },
1865
+ },
1866
+ required: ["path", "action", "edits"],
1867
+ },
1868
+ {
1869
+ type: "object",
1870
+ additionalProperties: false,
1871
+ properties: {
1872
+ path: { type: "string", minLength: 1, description: "Path within the current workspace." },
1873
+ action: { type: "string", enum: ["delete"] },
1874
+ },
1875
+ required: ["path", "action"],
1876
+ },
1877
+ {
1878
+ type: "object",
1879
+ additionalProperties: false,
1880
+ properties: {
1881
+ path: { type: "string", minLength: 1, description: "Source path within the current workspace." },
1882
+ action: { type: "string", enum: ["move"] },
1883
+ movePath: {
1884
+ type: "string",
1885
+ minLength: 1,
1886
+ description: "Destination path within the current workspace.",
1887
+ },
1888
+ },
1889
+ required: ["path", "action", "movePath"],
1890
+ },
1891
+ ],
1853
1892
  },
1854
1893
  },
1855
1894
  },
1856
1895
  required: ["changes"],
1857
1896
  },
1858
- async execute(_tid, params) {
1859
- const changes = (params.changes ?? []).map((c) => ({
1860
- path: c.path,
1861
- action: c.action,
1862
- content: c.content,
1863
- oldText: c.oldText,
1864
- newText: c.newText,
1865
- movePath: c.movePath,
1866
- }));
1867
- const result = await executeApplyPatch(changes);
1897
+ async execute(_tid, params, _signal, _onUpdate, ctx) {
1898
+ let result;
1899
+ try {
1900
+ const changes = parseApplyPatchInput(params);
1901
+ const toolCwd = typeof ctx?.cwd === "string" ? ctx.cwd : cwd;
1902
+ result = await executeApplyPatch(changes, { cwd: toolCwd, root: toolCwd });
1903
+ }
1904
+ catch (error) {
1905
+ result = {
1906
+ ok: false,
1907
+ applied: [],
1908
+ errors: [
1909
+ {
1910
+ path: "",
1911
+ action: "input",
1912
+ error: error instanceof Error ? error.message : String(error),
1913
+ },
1914
+ ],
1915
+ };
1916
+ }
1868
1917
  const output = formatApplyPatchResult(result);
1869
1918
  return {
1870
1919
  content: [{ type: "text", text: output }],