@heyhuynhgiabuu/pi-diff 0.6.0 → 0.6.1

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
@@ -24,22 +24,22 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
24
24
  import { extname, relative } from "node:path";
25
25
  import { codeToANSI } from "@shikijs/cli";
26
26
  import * as Diff from "diff";
27
- import { computeHunkBlocks, getSepStyle, parseDiff, parsePatchFiles, resolveSepStyle, sepLabelSplit, sepLabelUnified } from "./core/diff.js";
28
27
  import { configIndicatorStyle } from "./core/config.js";
28
+ import { computeHunkBlocks, getSepStyle, parseDiff, parsePatchFiles, resolveSepStyle, sepLabelSplit, sepLabelUnified, } from "./core/diff.js";
29
+ import { replace } from "./core/replace.js";
29
30
  import { resolveLinesFromPatch } from "./core/resolve-lines.js";
30
31
  import { registerReviewDiffCommand } from "./review/command.js";
31
- import { replace } from "./core/replace.js";
32
32
  import { applyDiffPalette as applySharedDiffPalette, lang as detectDiffLanguage, renderSplit as renderSharedSplit, resolveDiffColors as resolveSharedDiffColors, themeCacheKey as sharedThemeCacheKey, } from "./review/hunk-preview.js";
33
33
  const DIFF_PRESETS = {
34
34
  default: {
35
35
  name: "default",
36
36
  description: "Original pi-diff colors — tuned for dark theme bases (~#1e1e2e)",
37
- bgAdd: "#162620",
38
- bgDel: "#2d1919",
39
- bgAddHighlight: "#234b32",
40
- bgDelHighlight: "#502323",
41
- bgGutterAdd: "#12201a",
42
- bgGutterDel: "#261616",
37
+ bgAdd: "#1a3324",
38
+ bgDel: "#3d2020",
39
+ bgAddHighlight: "#2d5c3a",
40
+ bgDelHighlight: "#5c2d2d",
41
+ bgGutterAdd: "#16281e",
42
+ bgGutterDel: "#301c1c",
43
43
  bgEmpty: "#121212",
44
44
  fgDim: "#505050",
45
45
  fgLnum: "#646464",
@@ -50,12 +50,12 @@ const DIFF_PRESETS = {
50
50
  midnight: {
51
51
  name: "midnight",
52
52
  description: "Subtle tints for pure black (#000000) terminal backgrounds",
53
- bgAdd: "#0d1a12",
54
- bgDel: "#1a0d0d",
53
+ bgAdd: "#101e15",
54
+ bgDel: "#1e1010",
55
55
  bgAddHighlight: "#1a3825",
56
56
  bgDelHighlight: "#381a1a",
57
- bgGutterAdd: "#091208",
58
- bgGutterDel: "#120908",
57
+ bgGutterAdd: "#0c180c",
58
+ bgGutterDel: "#180c0c",
59
59
  bgEmpty: "#080808",
60
60
  fgDim: "#404040",
61
61
  fgLnum: "#505050",
@@ -66,12 +66,12 @@ const DIFF_PRESETS = {
66
66
  subtle: {
67
67
  name: "subtle",
68
68
  description: "Minimal backgrounds — barely-there tints for a clean look",
69
- bgAdd: "#081008",
70
- bgDel: "#100808",
69
+ bgAdd: "#0c160c",
70
+ bgDel: "#160c0c",
71
71
  bgAddHighlight: "#122818",
72
72
  bgDelHighlight: "#281212",
73
- bgGutterAdd: "#060c06",
74
- bgGutterDel: "#0c0606",
73
+ bgGutterAdd: "#080e08",
74
+ bgGutterDel: "#0e0808",
75
75
  bgEmpty: "#060606",
76
76
  fgDim: "#383838",
77
77
  fgLnum: "#484848",
@@ -82,12 +82,12 @@ const DIFF_PRESETS = {
82
82
  neon: {
83
83
  name: "neon",
84
84
  description: "Higher contrast backgrounds for better visibility",
85
- bgAdd: "#1a3320",
86
- bgDel: "#331a16",
85
+ bgAdd: "#1e3a28",
86
+ bgDel: "#3a1c18",
87
87
  bgAddHighlight: "#2d5c3a",
88
88
  bgDelHighlight: "#5c2d2d",
89
- bgGutterAdd: "#142818",
90
- bgGutterDel: "#28120e",
89
+ bgGutterAdd: "#182c1c",
90
+ bgGutterDel: "#2c1410",
91
91
  bgEmpty: "#141414",
92
92
  fgDim: "#606060",
93
93
  fgLnum: "#787878",
@@ -182,15 +182,15 @@ function autoDeriveBgFromTheme(theme) {
182
182
  /* no toolErrorBg — use toolSuccessBg/black */
183
183
  }
184
184
  }
185
- // Line backgrounds — subtle accent mixed into the matching tool-state base (810%)
186
- BG_ADD = mixBg(addBase, addRgb, 0.08);
187
- BG_DEL = mixBg(delBase, delRgb, 0.1);
188
- // Word-level highlights — more visible (2022%)
189
- BG_ADD_W = mixBg(addBase, addRgb, 0.2);
190
- BG_DEL_W = mixBg(delBase, delRgb, 0.22);
191
- // Gutters — subtler than lines (56%)
192
- BG_GUTTER_ADD = mixBg(addBase, addRgb, 0.05);
193
- BG_GUTTER_DEL = mixBg(delBase, delRgb, 0.06);
185
+ // Line backgrounds — visible accent mixed into the matching tool-state base (1518%)
186
+ BG_ADD = mixBg(addBase, addRgb, 0.15);
187
+ BG_DEL = mixBg(delBase, delRgb, 0.18);
188
+ // Word-level highlights — more prominent (3035%)
189
+ BG_ADD_W = mixBg(addBase, addRgb, 0.3);
190
+ BG_DEL_W = mixBg(delBase, delRgb, 0.35);
191
+ // Gutters — slightly subtler than lines (1012%)
192
+ BG_GUTTER_ADD = mixBg(addBase, addRgb, 0.1);
193
+ BG_GUTTER_DEL = mixBg(delBase, delRgb, 0.12);
194
194
  // Empty filler and context — match the success/context base
195
195
  BG_EMPTY = BG_BASE;
196
196
  // Update RST to re-apply base bg after every reset — prevents black
@@ -205,7 +205,10 @@ function autoDeriveBgFromTheme(theme) {
205
205
  }
206
206
  /** Load diff theme config from .pi/settings.json (project-level, then global). */
207
207
  function loadDiffConfig() {
208
- const paths = [`${process.cwd()}/.pi/settings.json`, `${process.env.HOME ?? ""}/.pi/settings.json`];
208
+ const paths = [
209
+ `${process.cwd()}/.pi/settings.json`,
210
+ `${process.env.HOME ?? ""}/.pi/settings.json`,
211
+ ];
209
212
  for (const p of paths) {
210
213
  try {
211
214
  if (existsSync(p)) {
@@ -363,16 +366,15 @@ const MAX_WRAP_ROWS_NARROW = 1; // <120 cols (truncate, no wrap)
363
366
  // ANSI
364
367
  // ---------------------------------------------------------------------------
365
368
  let RST = "\x1b[0m";
366
- const BOLD = "\x1b[1m";
367
369
  const DIM = "\x1b[2m";
368
370
  // Subtle diff backgrounds — muted tones to let syntax fg shine through
369
371
  // Override via env: DIFF_BG_ADD="#1a3320" etc. (hex "#RRGGBB" format)
370
- let BG_ADD = envBg("DIFF_BG_ADD", "\x1b[48;2;22;38;32m"); // muted teal-green
371
- let BG_DEL = envBg("DIFF_BG_DEL", "\x1b[48;2;45;25;25m"); // muted brown-red
372
- let BG_ADD_W = envBg("DIFF_BG_ADD_HL", "\x1b[48;2;35;75;50m"); // word-level emphasis
373
- let BG_DEL_W = envBg("DIFF_BG_DEL_HL", "\x1b[48;2;80;35;35m");
374
- let BG_GUTTER_ADD = envBg("DIFF_BG_GUTTER_ADD", "\x1b[48;2;18;32;26m");
375
- let BG_GUTTER_DEL = envBg("DIFF_BG_GUTTER_DEL", "\x1b[48;2;38;22;22m");
372
+ let BG_ADD = envBg("DIFF_BG_ADD", "\x1b[48;2;30;52;40m"); // visible teal-green
373
+ let BG_DEL = envBg("DIFF_BG_DEL", "\x1b[48;2;60;30;30m"); // visible brown-red
374
+ let BG_ADD_W = envBg("DIFF_BG_ADD_HL", "\x1b[48;2;45;90;60m"); // word-level emphasis
375
+ let BG_DEL_W = envBg("DIFF_BG_DEL_HL", "\x1b[48;2;100;45;45m");
376
+ let BG_GUTTER_ADD = envBg("DIFF_BG_GUTTER_ADD", "\x1b[48;2;24;42;32m");
377
+ let BG_GUTTER_DEL = envBg("DIFF_BG_GUTTER_DEL", "\x1b[48;2;48;28;28m");
376
378
  const BG_GUTTER_CTX = ""; // use terminal default bg for context gutters
377
379
  let BG_EMPTY = "\x1b[48;2;18;18;18m"; // filler rows when one side is shorter
378
380
  // Diff foregrounds — override via env: DIFF_FG_ADD="#50d264" etc.
@@ -676,7 +678,7 @@ function shouldUseSplit(diff, tw, maxRows = MAX_PREVIEW_LINES) {
676
678
  return false;
677
679
  const nw = Math.max(2, String(Math.max(...diff.lines.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length);
678
680
  const half = Math.floor((tw - 1) / 2); // -1 for center divider
679
- const gw = nw + 5; // border + num + sign + sp + │ + sp
681
+ const gw = nw + 4; // border + num + spaces around sign
680
682
  const cw = Math.max(12, half - gw);
681
683
  if (cw < SPLIT_MIN_CODE_WIDTH)
682
684
  return false;
@@ -828,8 +830,6 @@ function wordDiffAnalysis(a, b) {
828
830
  * Uses sorted-range pointer scan instead of Set (avoids O(totalChars) Set creation).
829
831
  */
830
832
  function injectBg(ansiLine, ranges, baseBg, hlBg) {
831
- if (!ranges.length)
832
- return baseBg + ansiLine + RST;
833
833
  let out = baseBg;
834
834
  let vis = 0;
835
835
  let inHL = false;
@@ -898,7 +898,7 @@ async function renderUnified(diff, language, max = MAX_RENDER_LINES, dc = DEFAUL
898
898
  const vis = diff.lines.slice(0, max);
899
899
  const tw = termW();
900
900
  const nw = Math.max(2, String(Math.max(...vis.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length);
901
- const gw = nw + 5; // border + num + sign + sp + │ + sp
901
+ const gw = nw + 4; // border + num + spaces around sign
902
902
  const cw = Math.max(20, tw - gw);
903
903
  const canHL = diff.chars <= MAX_HL_CHARS && vis.length <= MAX_RENDER_LINES;
904
904
  // Build separate old/new code blocks for highlighting
@@ -910,18 +910,20 @@ async function renderUnified(diff, language, max = MAX_RENDER_LINES, dc = DEFAUL
910
910
  newSrc.push(l.content);
911
911
  }
912
912
  const [oldHL, newHL] = canHL
913
- ? await Promise.all([hlBlock(oldSrc.join("\n"), language), hlBlock(newSrc.join("\n"), language)])
913
+ ? await Promise.all([
914
+ hlBlock(oldSrc.join("\n"), language),
915
+ hlBlock(newSrc.join("\n"), language),
916
+ ])
914
917
  : [oldSrc, newSrc];
915
918
  let oI = 0, nI = 0, idx = 0;
916
919
  const out = [];
917
- out.push(rule(tw));
918
920
  /** Emit a single stacked row with compact gutter + left border bar. */
919
921
  function emitRow(num, sign, gutterBg, signFg, body, bodyBg = "") {
920
922
  const borderFg = sign === "-" ? dc.fgDel : sign === "+" ? dc.fgAdd : "";
921
923
  const border = borderFg ? `${borderFg}${getBorderBar()}${RST}` : `${BG_BASE} `;
922
924
  const numFg = borderFg || FG_LNUM;
923
- const gutter = `${border}${gutterBg}${lnum(num, nw, numFg)}${signFg}${sign}${RST} ${DIVIDER} `;
924
- const contGutter = `${border}${gutterBg}${" ".repeat(nw + 1)}${RST} ${DIVIDER} `;
925
+ const gutter = `${border}${gutterBg}${lnum(num, nw, numFg)}${gutterBg} ${signFg}${sign}${gutterBg} ${RST}`;
926
+ const contGutter = `${border}${gutterBg}${" ".repeat(nw + 3)}${RST}`;
925
927
  const rows = wrapAnsi(tabs(body), cw, adaptiveWrapRows(), bodyBg);
926
928
  out.push(`${gutter}${rows[0]}${RST}`);
927
929
  for (let r = 1; r < rows.length; r++)
@@ -972,27 +974,26 @@ async function renderUnified(diff, language, max = MAX_RENDER_LINES, dc = DEFAUL
972
974
  if (isPaired && wdBalanced && wd.similarity >= WORD_DIFF_MIN_SIM && canHL) {
973
975
  const delBody = injectBg(dels[0].hl, wd.oldRanges, BG_DEL, BG_DEL_W);
974
976
  const addBody = injectBg(adds[0].hl, wd.newRanges, BG_ADD, BG_ADD_W);
975
- emitRow(dels[0].l.oldNum, "-", BG_GUTTER_DEL, `${dc.fgDel}${BOLD}`, delBody, BG_DEL);
976
- emitRow(adds[0].l.newNum, "+", BG_GUTTER_ADD, `${dc.fgAdd}${BOLD}`, addBody, BG_ADD);
977
+ emitRow(dels[0].l.oldNum, "-", BG_GUTTER_DEL, dc.fgDel, delBody, BG_DEL);
978
+ emitRow(adds[0].l.newNum, "+", BG_GUTTER_ADD, dc.fgAdd, addBody, BG_ADD);
977
979
  continue;
978
980
  }
979
981
  if (isPaired && wdBalanced && wd.similarity >= WORD_DIFF_MIN_SIM && !canHL) {
980
982
  const pwd = plainWordDiff(dels[0].l.content, adds[0].l.content);
981
- emitRow(dels[0].l.oldNum, "-", BG_GUTTER_DEL, `${dc.fgDel}${BOLD}`, `${BG_DEL}${pwd.old}`, BG_DEL);
982
- emitRow(adds[0].l.newNum, "+", BG_GUTTER_ADD, `${dc.fgAdd}${BOLD}`, `${BG_ADD}${pwd.new}`, BG_ADD);
983
+ emitRow(dels[0].l.oldNum, "-", BG_GUTTER_DEL, dc.fgDel, `${BG_DEL}${pwd.old}`, BG_DEL);
984
+ emitRow(adds[0].l.newNum, "+", BG_GUTTER_ADD, dc.fgAdd, `${BG_ADD}${pwd.new}`, BG_ADD);
983
985
  continue;
984
986
  }
985
987
  // Multi-line blocks — syntax highlighted with diff bg
986
988
  for (const d of dels) {
987
- const body = canHL ? `${BG_DEL}${d.hl}` : `${BG_DEL}${d.l.content}`;
988
- emitRow(d.l.oldNum, "-", BG_GUTTER_DEL, `${dc.fgDel}${BOLD}`, body, BG_DEL);
989
+ const body = canHL ? injectBg(d.hl, [], BG_DEL, BG_DEL) : `${BG_DEL}${d.l.content}`;
990
+ emitRow(d.l.oldNum, "-", BG_GUTTER_DEL, dc.fgDel, body, BG_DEL);
989
991
  }
990
992
  for (const a of adds) {
991
- const body = canHL ? `${BG_ADD}${a.hl}` : `${BG_ADD}${a.l.content}`;
992
- emitRow(a.l.newNum, "+", BG_GUTTER_ADD, `${dc.fgAdd}${BOLD}`, body, BG_ADD);
993
+ const body = canHL ? injectBg(a.hl, [], BG_ADD, BG_ADD) : `${BG_ADD}${a.l.content}`;
994
+ emitRow(a.l.newNum, "+", BG_GUTTER_ADD, dc.fgAdd, body, BG_ADD);
993
995
  }
994
996
  }
995
- out.push(rule(tw));
996
997
  if (diff.lines.length > vis.length) {
997
998
  out.push(`${BG_BASE}${FG_DIM} … ${diff.lines.length - vis.length} more lines${RST}`);
998
999
  }
@@ -1039,7 +1040,7 @@ async function renderSplit(diff, language, max = MAX_PREVIEW_LINES, dc = DEFAULT
1039
1040
  const vis = rows.slice(0, max);
1040
1041
  const half = Math.floor((tw - 1) / 2); // -1 for center divider
1041
1042
  const nw = Math.max(2, String(Math.max(...diff.lines.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length);
1042
- const gw = nw + 5; // border + num + sign + sp + │ + sp
1043
+ const gw = nw + 4; // border + num + spaces around sign
1043
1044
  const cw = Math.max(12, half - gw);
1044
1045
  const canHL = diff.chars <= MAX_HL_CHARS && vis.length * 2 <= MAX_RENDER_LINES * 2;
1045
1046
  // Build separate code blocks per side
@@ -1051,22 +1052,25 @@ async function renderSplit(diff, language, max = MAX_PREVIEW_LINES, dc = DEFAULT
1051
1052
  rightSrc.push(r.right.content);
1052
1053
  }
1053
1054
  const [leftHL, rightHL] = canHL
1054
- ? await Promise.all([hlBlock(leftSrc.join("\n"), language), hlBlock(rightSrc.join("\n"), language)])
1055
+ ? await Promise.all([
1056
+ hlBlock(leftSrc.join("\n"), language),
1057
+ hlBlock(rightSrc.join("\n"), language),
1058
+ ])
1055
1059
  : [leftSrc, rightSrc];
1056
1060
  let lI = 0, rI = 0;
1057
1061
  let stripeRow = 0; // tracks row index for diagonal stripe offset
1058
1062
  function half_build(line, hl, ranges, side) {
1059
1063
  // Empty filler — diagonal stripes
1060
1064
  if (!line) {
1061
- const gw2 = nw + 2; // number + sign + space before
1065
+ const gw2 = nw + 4; // border + number + spaces around sign
1062
1066
  const gPat = FG_STRIPE + "╱".repeat(gw2) + RST;
1063
- const g = ` ${gPat}${FG_RULE}│${RST} `;
1067
+ const g = gPat;
1064
1068
  return { gutter: g, contGutter: g, bodyRows: [stripes(cw, stripeRow)] };
1065
1069
  }
1066
1070
  // Hunk separator with optional function context
1067
1071
  if (line.type === "sep") {
1068
1072
  const label = sepLabelSplit(getSepStyle(), line.hunkMeta, line.newNum, line.content);
1069
- const g = `${BG_BASE} ${FG_DIM}${fit("", nw + 2)}${RST}${FG_RULE}│${RST} `;
1073
+ const g = `${BG_BASE} ${FG_DIM}${fit("", nw + 3)}${RST}`;
1070
1074
  return { gutter: g, contGutter: g, bodyRows: [`${BG_BASE}${FG_DIM}${fit(label, cw)}${RST}`] };
1071
1075
  }
1072
1076
  const isDel = line.type === "del", isAdd = line.type === "add";
@@ -1074,7 +1078,13 @@ async function renderSplit(diff, language, max = MAX_PREVIEW_LINES, dc = DEFAULT
1074
1078
  const cBg = isDel ? BG_DEL : isAdd ? BG_ADD : BG_BASE;
1075
1079
  const sFg = isDel ? dc.fgDel : isAdd ? dc.fgAdd : dc.fgCtx;
1076
1080
  const sign = isDel ? "-" : isAdd ? "+" : " ";
1077
- const num = isDel ? line.oldNum : isAdd ? line.newNum : side === "left" ? line.oldNum : line.newNum;
1081
+ const num = isDel
1082
+ ? line.oldNum
1083
+ : isAdd
1084
+ ? line.newNum
1085
+ : side === "left"
1086
+ ? line.oldNum
1087
+ : line.newNum;
1078
1088
  // Border bar + colored line numbers for changed lines
1079
1089
  const borderFg = isDel ? dc.fgDel : isAdd ? dc.fgAdd : "";
1080
1090
  const border = borderFg ? `${borderFg}${getBorderBar()}${RST}` : ` ${BG_BASE}`;
@@ -1084,13 +1094,13 @@ async function renderSplit(diff, language, max = MAX_PREVIEW_LINES, dc = DEFAULT
1084
1094
  body = injectBg(hl, ranges, cBg, isDel ? BG_DEL_W : BG_ADD_W);
1085
1095
  }
1086
1096
  else if (isDel || isAdd) {
1087
- body = `${cBg}${hl}`;
1097
+ body = injectBg(hl, [], cBg, cBg);
1088
1098
  }
1089
1099
  else {
1090
1100
  body = `${BG_BASE}${DIM}${hl}`;
1091
1101
  }
1092
- const gutter = `${border}${gBg}${lnum(num, nw, numFg)}${sFg}${BOLD}${sign}${RST} ${FG_RULE}│${RST} `;
1093
- const contGutter = `${border}${gBg}${" ".repeat(nw + 1)}${RST} ${FG_RULE}│${RST} `;
1102
+ const gutter = `${border}${gBg}${lnum(num, nw, numFg)}${gBg} ${sFg}${sign}${gBg} ${RST}`;
1103
+ const contGutter = `${border}${gBg}${" ".repeat(nw + 3)}${RST}`;
1094
1104
  const bodyRows = wrapAnsi(tabs(body), cw, adaptiveWrapRows(), cBg);
1095
1105
  return { gutter, contGutter, bodyRows };
1096
1106
  }
@@ -1099,7 +1109,6 @@ async function renderSplit(diff, language, max = MAX_PREVIEW_LINES, dc = DEFAULT
1099
1109
  const hdrOld = `${BG_BASE}${" ".repeat(Math.max(0, nw - 2))}${dc.fgDel}${DIM}old${RST}`;
1100
1110
  const hdrNew = `${BG_BASE}${" ".repeat(Math.max(0, nw - 2))}${dc.fgAdd}${DIM}new${RST}`;
1101
1111
  out.push(`${BG_BASE}${hdrOld}${" ".repeat(Math.max(0, half - nw - 1))}${FG_RULE}┊${RST}${hdrNew}`);
1102
- out.push(`${rule(half)}${FG_RULE}┊${RST}${rule(half)}`);
1103
1112
  for (const r of vis) {
1104
1113
  const leftLine = r.left, rightLine = r.right;
1105
1114
  const paired = leftLine && rightLine && leftLine.type === "del" && rightLine.type === "add";
@@ -1131,13 +1140,14 @@ async function renderSplit(diff, language, max = MAX_PREVIEW_LINES, dc = DEFAULT
1131
1140
  for (let row = 0; row < maxRows; row++) {
1132
1141
  const lg = row === 0 ? lResult.gutter : lResult.contGutter;
1133
1142
  const rg = row === 0 ? rResult.gutter : rResult.contGutter;
1134
- const lb = lResult.bodyRows[row] ?? (leftIsEmpty ? stripes(cw, stripeRow) : `${BG_EMPTY}${" ".repeat(cw)}${RST}`);
1135
- const rb = rResult.bodyRows[row] ?? (rightIsEmpty ? stripes(cw, stripeRow) : `${BG_EMPTY}${" ".repeat(cw)}${RST}`);
1143
+ const lb = lResult.bodyRows[row] ??
1144
+ (leftIsEmpty ? stripes(cw, stripeRow) : `${BG_EMPTY}${" ".repeat(cw)}${RST}`);
1145
+ const rb = rResult.bodyRows[row] ??
1146
+ (rightIsEmpty ? stripes(cw, stripeRow) : `${BG_EMPTY}${" ".repeat(cw)}${RST}`);
1136
1147
  out.push(`${lg}${lb}${DIVIDER}${rg}${rb}`);
1137
1148
  stripeRow++;
1138
1149
  }
1139
1150
  }
1140
- out.push(`${rule(half)}${FG_RULE}┊${RST}${rule(half)}`);
1141
1151
  if (rows.length > vis.length) {
1142
1152
  out.push(`${BG_BASE}${FG_DIM} … ${rows.length - vis.length} more lines${RST}`);
1143
1153
  }
@@ -1180,6 +1190,70 @@ export default async function diffRendererExtension(pi) {
1180
1190
  const cwd = process.cwd();
1181
1191
  const home = process.env.HOME ?? "";
1182
1192
  const sp = (p) => shortPath(cwd, home, p);
1193
+ const TOOL_HEADER_LEFT_PAD = 2;
1194
+ const TOOL_HEADER_TOP_PAD = 1;
1195
+ const TOOL_PREVIEW_BOTTOM_PAD = 1;
1196
+ const DIFF_BODY_LEFT_PAD = 1;
1197
+ function resolvePreviewDiffColors(theme) {
1198
+ resolveDiffColors(theme);
1199
+ return resolveSharedDiffColors(theme);
1200
+ }
1201
+ function bgLine(content, width) {
1202
+ const renderWidth = Math.max(1, width);
1203
+ const padding = " ".repeat(Math.max(0, renderWidth - strip(content).length));
1204
+ return injectBg(`${content}${padding}`, [], BG_BASE, BG_BASE);
1205
+ }
1206
+ function formatToolHeader(summary, width) {
1207
+ const leftPad = " ".repeat(TOOL_HEADER_LEFT_PAD);
1208
+ const meta = `${leftPad}${summary}`;
1209
+ return bgLine(meta, width);
1210
+ }
1211
+ function formatToolTitle(label, filePath, theme, width, suffix = "") {
1212
+ const leftPad = " ".repeat(TOOL_HEADER_LEFT_PAD);
1213
+ const displayPath = filePath ? ` ${theme.fg("accent", sp(filePath))}` : "";
1214
+ const top = Array.from({ length: TOOL_HEADER_TOP_PAD }, () => bgLine("", width));
1215
+ return [
1216
+ ...top,
1217
+ bgLine(`${leftPad}${theme.fg("toolTitle", theme.bold(label))}${displayPath}${suffix}`, width),
1218
+ ].join("\n");
1219
+ }
1220
+ function formatBottomPadding(width) {
1221
+ return Array.from({ length: TOOL_PREVIEW_BOTTOM_PAD }, () => bgLine("", width)).join("\n");
1222
+ }
1223
+ function padDiffBody(rendered) {
1224
+ const leftPad = `${BG_BASE}${" ".repeat(DIFF_BODY_LEFT_PAD)}${RST}`;
1225
+ return rendered
1226
+ .split("\n")
1227
+ .map((line) => `${leftPad}${line}`)
1228
+ .join("\n");
1229
+ }
1230
+ async function renderPaddedDiff(diff, language, maxLines, colors, width) {
1231
+ const bodyWidth = Math.max(1, width - DIFF_BODY_LEFT_PAD);
1232
+ return padDiffBody(await renderSharedSplit(diff, language, maxLines, colors, bodyWidth));
1233
+ }
1234
+ function diffLineCountLabel(diffLineCount, theme) {
1235
+ if (typeof diffLineCount !== "number")
1236
+ return "";
1237
+ return ` ${theme.fg("muted", `(${diffLineCount} diff lines)`)}`;
1238
+ }
1239
+ function setToolHeaderText(text, meta, theme) {
1240
+ resolvePreviewDiffColors(theme);
1241
+ text.__piDiffTask = undefined;
1242
+ const width = termW();
1243
+ text.setText(`${formatToolHeader(meta, width)}\n${formatBottomPadding(width)}`);
1244
+ }
1245
+ function setDiffPreviewTask(text, keyPrefix, meta, diff, language, maxLines, theme, ctx) {
1246
+ const themeKey = sharedThemeCacheKey(theme);
1247
+ const colors = resolvePreviewDiffColors(theme);
1248
+ const header = (width) => formatToolHeader(meta, width);
1249
+ text.__piDiffTask = {
1250
+ placeholder: `${header(termW())}\n${padDiffBody(theme.fg("muted", "rendering diff…"))}\n${formatBottomPadding(termW())}`,
1251
+ fallback: `${header(termW())}\n${formatBottomPadding(termW())}`,
1252
+ invalidate: ctx.invalidate,
1253
+ key: (width) => `${keyPrefix}:${themeKey}:${width}:${meta}:${diff.lines.length}:${language ?? ""}`,
1254
+ render: async (width) => `${header(width)}\n${await renderPaddedDiff(diff, language, maxLines, colors, width)}\n${formatBottomPadding(width)}`,
1255
+ };
1256
+ }
1183
1257
  /** Wrap a Text component so its render(width) kicks off async diff rendering
1184
1258
  * using the real TUI width (which accounts for the sidebar). */
1185
1259
  function getWidthAwareText(lastComponent) {
@@ -1214,7 +1288,7 @@ export default async function diffRendererExtension(pi) {
1214
1288
  });
1215
1289
  }
1216
1290
  }
1217
- return text.__piDiffRender(width);
1291
+ return text.__piDiffRender?.(width) ?? [];
1218
1292
  };
1219
1293
  return text;
1220
1294
  }
@@ -1263,7 +1337,11 @@ Examples:
1263
1337
  try {
1264
1338
  const { existingCode, filePath, patchText, fileContent } = params ?? {};
1265
1339
  if (!existingCode || !filePath) {
1266
- return { content: [{ type: "text", text: "Error: existingCode and filePath are required" }] };
1340
+ return {
1341
+ content: [
1342
+ { type: "text", text: "Error: existingCode and filePath are required" },
1343
+ ],
1344
+ };
1267
1345
  }
1268
1346
  // Fetch git diff if patchText not provided
1269
1347
  let patch = patchText;
@@ -1289,25 +1367,42 @@ Examples:
1289
1367
  }
1290
1368
  catch {
1291
1369
  return {
1292
- content: [{ type: "text", text: `Error: could not read git diff for ${filePath}` }],
1370
+ content: [
1371
+ { type: "text", text: `Error: could not read git diff for ${filePath}` },
1372
+ ],
1293
1373
  };
1294
1374
  }
1295
1375
  }
1296
1376
  if (!patch) {
1297
1377
  return {
1298
- content: [{ type: "text", text: `No diff found for ${filePath} — file may not have changes` }],
1378
+ content: [
1379
+ {
1380
+ type: "text",
1381
+ text: `No diff found for ${filePath} — file may not have changes`,
1382
+ },
1383
+ ],
1299
1384
  details: { unresolved: true },
1300
1385
  };
1301
1386
  }
1302
1387
  const result = resolveLinesFromPatch(existingCode, patch, fileContent);
1303
1388
  if ("unresolved" in result) {
1304
1389
  return {
1305
- content: [{ type: "text", text: `Could not resolve line numbers for existing_code in ${filePath}` }],
1390
+ content: [
1391
+ {
1392
+ type: "text",
1393
+ text: `Could not resolve line numbers for existing_code in ${filePath}`,
1394
+ },
1395
+ ],
1306
1396
  details: { unresolved: true },
1307
1397
  };
1308
1398
  }
1309
1399
  return {
1310
- content: [{ type: "text", text: `Resolved ${filePath}:${result.startLine}-${result.endLine}` }],
1400
+ content: [
1401
+ {
1402
+ type: "text",
1403
+ text: `Resolved ${filePath}:${result.startLine}-${result.endLine}`,
1404
+ },
1405
+ ],
1311
1406
  details: { startLine: result.startLine, endLine: result.endLine },
1312
1407
  };
1313
1408
  }
@@ -1367,11 +1462,24 @@ Examples:
1367
1462
  const useFull = !!params._expandGaps;
1368
1463
  const diff = parseDiff(old, content, useFull ? undefined : 3);
1369
1464
  const lg = detectDiffLanguage(fp);
1370
- result.details = { _type: "diff", summary: summarize(diff.added, diff.removed), diff, language: lg, oldContent: old, newContent: content };
1465
+ result.details = {
1466
+ _type: "diff",
1467
+ summary: summarize(diff.added, diff.removed),
1468
+ filePath: fp,
1469
+ diff,
1470
+ language: lg,
1471
+ oldContent: old,
1472
+ newContent: content,
1473
+ };
1371
1474
  }
1372
1475
  else if (old === null) {
1373
1476
  const lineCount = content ? content.split("\n").length : 0;
1374
- result.details = { _type: "new", lines: lineCount, content: content ?? "", filePath: fp };
1477
+ result.details = {
1478
+ _type: "new",
1479
+ lines: lineCount,
1480
+ content: content ?? "",
1481
+ filePath: fp,
1482
+ };
1375
1483
  }
1376
1484
  else if (old === content) {
1377
1485
  result.details = { _type: "noChange" };
@@ -1431,15 +1539,7 @@ Examples:
1431
1539
  }
1432
1540
  const d = result.details;
1433
1541
  if (d?._type === "diff") {
1434
- const themeKey = sharedThemeCacheKey(theme);
1435
- const dc = resolveSharedDiffColors(theme);
1436
- text.__piDiffTask = {
1437
- placeholder: ` ${d.summary}\n${theme.fg("muted", " rendering diff…")}`,
1438
- fallback: ` ${d.summary}`,
1439
- invalidate: ctx.invalidate,
1440
- key: (width) => `wd:${themeKey}:${width}:${d.summary}:${d.diff?.lines?.length ?? 0}:${d.language ?? ""}`,
1441
- render: async (width) => ` ${d.summary}\n${await renderSharedSplit(d.diff, d.language, MAX_RENDER_LINES, dc, width)}`,
1442
- };
1542
+ setDiffPreviewTask(text, "wd", d.summary, d.diff, d.language, MAX_RENDER_LINES, theme, ctx);
1443
1543
  return text;
1444
1544
  }
1445
1545
  if (d?._type === "noChange") {
@@ -1486,13 +1586,29 @@ Examples:
1486
1586
  if (Array.isArray(input?.edits)) {
1487
1587
  return input.edits
1488
1588
  .map((edit) => ({
1489
- oldText: typeof edit?.oldText === "string" ? edit.oldText : typeof edit?.old_text === "string" ? edit.old_text : "",
1490
- newText: typeof edit?.newText === "string" ? edit.newText : typeof edit?.new_text === "string" ? edit.new_text : "",
1589
+ oldText: typeof edit?.oldText === "string"
1590
+ ? edit.oldText
1591
+ : typeof edit?.old_text === "string"
1592
+ ? edit.old_text
1593
+ : "",
1594
+ newText: typeof edit?.newText === "string"
1595
+ ? edit.newText
1596
+ : typeof edit?.new_text === "string"
1597
+ ? edit.new_text
1598
+ : "",
1491
1599
  }))
1492
1600
  .filter((edit) => edit.oldText && edit.oldText !== edit.newText);
1493
1601
  }
1494
- const oldText = typeof input?.oldText === "string" ? input.oldText : typeof input?.old_text === "string" ? input.old_text : "";
1495
- const newText = typeof input?.newText === "string" ? input.newText : typeof input?.new_text === "string" ? input.new_text : "";
1602
+ const oldText = typeof input?.oldText === "string"
1603
+ ? input.oldText
1604
+ : typeof input?.old_text === "string"
1605
+ ? input.old_text
1606
+ : "";
1607
+ const newText = typeof input?.newText === "string"
1608
+ ? input.newText
1609
+ : typeof input?.new_text === "string"
1610
+ ? input.new_text
1611
+ : "";
1496
1612
  return oldText && oldText !== newText ? [{ oldText, newText }] : [];
1497
1613
  }
1498
1614
  function summarizeEditOperations(operations) {
@@ -1553,6 +1669,7 @@ Examples:
1553
1669
  details: {
1554
1670
  _type: "editInfo",
1555
1671
  summary,
1672
+ filePath: fp,
1556
1673
  editLine,
1557
1674
  diff: diffData,
1558
1675
  language: lg,
@@ -1565,7 +1682,16 @@ Examples:
1565
1682
  // Merge all diffs into one combined view for rendering
1566
1683
  const merged = {
1567
1684
  lines: diffs.flatMap((diff, i) => [
1568
- ...(i > 0 ? [{ type: "sep", oldNum: null, newNum: null, content: `───── Edit ${i + 1} ─────` }] : []),
1685
+ ...(i > 0
1686
+ ? [
1687
+ {
1688
+ type: "sep",
1689
+ oldNum: null,
1690
+ newNum: null,
1691
+ content: `───── Edit ${i + 1} ─────`,
1692
+ },
1693
+ ]
1694
+ : []),
1569
1695
  ...diff.lines,
1570
1696
  ]),
1571
1697
  added: diffs.reduce((sum, diff) => sum + diff.added, 0),
@@ -1577,6 +1703,7 @@ Examples:
1577
1703
  details: {
1578
1704
  _type: "multiEditInfo",
1579
1705
  summary,
1706
+ filePath: fp,
1580
1707
  editCount: operations.length,
1581
1708
  diffLineCount: merged.lines.length,
1582
1709
  diff: merged,
@@ -1615,6 +1742,7 @@ Examples:
1615
1742
  result.details = {
1616
1743
  _type: "editInfo",
1617
1744
  summary,
1745
+ filePath: fp,
1618
1746
  editLine,
1619
1747
  diff: diffData,
1620
1748
  language: lg,
@@ -1627,7 +1755,16 @@ Examples:
1627
1755
  const merged = {
1628
1756
  lines: diffs.flatMap((diff, i) => [
1629
1757
  // Add separator between multiple edits
1630
- ...(i > 0 ? [{ type: "sep", oldNum: null, newNum: null, content: `───── Edit ${i + 1} ─────` }] : []),
1758
+ ...(i > 0
1759
+ ? [
1760
+ {
1761
+ type: "sep",
1762
+ oldNum: null,
1763
+ newNum: null,
1764
+ content: `───── Edit ${i + 1} ─────`,
1765
+ },
1766
+ ]
1767
+ : []),
1631
1768
  ...diff.lines,
1632
1769
  ]),
1633
1770
  added: diffs.reduce((sum, diff) => sum + diff.added, 0),
@@ -1637,6 +1774,7 @@ Examples:
1637
1774
  result.details = {
1638
1775
  _type: "multiEditInfo",
1639
1776
  summary,
1777
+ filePath: fp,
1640
1778
  editCount: operations.length,
1641
1779
  diffLineCount: merged.lines.length,
1642
1780
  diff: merged,
@@ -1648,13 +1786,13 @@ Examples:
1648
1786
  const fp = args?.path ?? args?.file_path ?? "";
1649
1787
  const operations = getEditOperations(args);
1650
1788
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1651
- const hdr = `${theme.fg("toolTitle", theme.bold("edit"))} ${theme.fg("accent", sp(fp))}`;
1789
+ resolvePreviewDiffColors(theme);
1652
1790
  if (ctx.argsComplete && operations.length > 0) {
1653
1791
  const { totalAdded, totalRemoved } = summarizeEditOperations(operations);
1654
- text.setText(`${hdr} ${theme.fg("muted", summarize(totalAdded, totalRemoved))}`);
1792
+ text.setText(formatToolTitle("edit", fp, theme, termW(), ` ${theme.fg("muted", summarize(totalAdded, totalRemoved))}`));
1655
1793
  }
1656
1794
  else {
1657
- text.setText(hdr);
1795
+ text.setText(formatToolTitle("edit", fp, theme, termW()));
1658
1796
  }
1659
1797
  return text;
1660
1798
  },
@@ -1671,42 +1809,24 @@ Examples:
1671
1809
  }
1672
1810
  const d = result.details;
1673
1811
  if (d?._type === "editInfo" && d.diff) {
1674
- const themeKey = sharedThemeCacheKey(theme);
1675
- const dc = resolveSharedDiffColors(theme);
1676
1812
  const loc = d.editLine > 0 ? ` ${theme.fg("muted", `at line ${d.editLine}`)}` : "";
1677
- text.__piDiffTask = {
1678
- placeholder: ` ${d.summary}${loc}\n${theme.fg("muted", " rendering diff…")}`,
1679
- fallback: ` ${d.summary}${loc}`,
1680
- invalidate: ctx.invalidate,
1681
- key: (width) => `ed:${themeKey}:${width}:${d.summary}:${d.diff?.lines?.length ?? 0}:${d.language ?? ""}`,
1682
- render: async (width) => ` ${d.summary}${loc}\n${await renderSharedSplit(d.diff, d.language, MAX_PREVIEW_LINES, dc, width)}`,
1683
- };
1813
+ setDiffPreviewTask(text, "ed", `${d.summary}${loc}`, d.diff, d.language, MAX_PREVIEW_LINES, theme, ctx);
1684
1814
  return text;
1685
1815
  }
1686
1816
  if (d?._type === "editInfo") {
1687
1817
  const { summary: s, editLine } = d;
1688
1818
  const loc = editLine > 0 ? ` ${theme.fg("muted", `at line ${editLine}`)}` : "";
1689
- text.__piDiffTask = undefined;
1690
- text.setText(` ${s}${loc}`);
1819
+ setToolHeaderText(text, `${s}${loc}`, theme);
1691
1820
  return text;
1692
1821
  }
1693
1822
  if (d?._type === "multiEditInfo") {
1694
1823
  const { summary: s, editCount, diffLineCount, diff, language } = d;
1824
+ const meta = `${editCount} edits ${s}${diffLineCountLabel(diffLineCount, theme)}`;
1695
1825
  if (diff) {
1696
- const themeKey = sharedThemeCacheKey(theme);
1697
- const dc = resolveSharedDiffColors(theme);
1698
- text.__piDiffTask = {
1699
- placeholder: ` ${editCount} edits ${s}
1700
- ${theme.fg("muted", " rendering diff…")}`,
1701
- fallback: ` ${editCount} edits ${s}${typeof diffLineCount === "number" ? ` ${theme.fg("muted", `(${diffLineCount} diff lines)`)}` : ""}`,
1702
- invalidate: ctx.invalidate,
1703
- key: (width) => `me:${themeKey}:${width}:${s}:${diff?.lines?.length ?? 0}:${language ?? ""}`,
1704
- render: async (width) => ` ${editCount} edits ${s}${typeof diffLineCount === "number" ? ` ${theme.fg("muted", `(${diffLineCount} diff lines)`)}` : ""}\n${await renderSharedSplit(diff, language, MAX_PREVIEW_LINES, dc, width)}`,
1705
- };
1826
+ setDiffPreviewTask(text, "me", meta, diff, language, MAX_PREVIEW_LINES, theme, ctx);
1706
1827
  return text;
1707
1828
  }
1708
- text.__piDiffTask = undefined;
1709
- text.setText(` ${editCount} edits ${s}${typeof diffLineCount === "number" ? ` ${theme.fg("muted", `(${diffLineCount} diff lines)`)}` : ""}`);
1829
+ setToolHeaderText(text, meta, theme);
1710
1830
  return text;
1711
1831
  }
1712
1832
  text.__piDiffTask = undefined;