@heyhuynhgiabuu/pi-diff 0.4.1 → 0.5.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
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  /**
3
2
  * pi-diff — Shiki-powered terminal diff renderer for pi.
4
3
  *
@@ -21,52 +20,16 @@
21
20
  * • Large-diff fallback (skip highlighting, still show diff)
22
21
  * • Async rendering with invalidate() for non-blocking preview
23
22
  */
24
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
- if (k2 === undefined) k2 = k;
26
- var desc = Object.getOwnPropertyDescriptor(m, k);
27
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
- desc = { enumerable: true, get: function() { return m[k]; } };
29
- }
30
- Object.defineProperty(o, k2, desc);
31
- }) : (function(o, m, k, k2) {
32
- if (k2 === undefined) k2 = k;
33
- o[k2] = m[k];
34
- }));
35
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
- Object.defineProperty(o, "default", { enumerable: true, value: v });
37
- }) : function(o, v) {
38
- o["default"] = v;
39
- });
40
- var __importStar = (this && this.__importStar) || (function () {
41
- var ownKeys = function(o) {
42
- ownKeys = Object.getOwnPropertyNames || function (o) {
43
- var ar = [];
44
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
- return ar;
46
- };
47
- return ownKeys(o);
48
- };
49
- return function (mod) {
50
- if (mod && mod.__esModule) return mod;
51
- var result = {};
52
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
- __setModuleDefault(result, mod);
54
- return result;
55
- };
56
- })();
57
- Object.defineProperty(exports, "__esModule", { value: true });
58
- exports.__testing = void 0;
59
- exports.default = diffRendererExtension;
60
- const node_fs_1 = require("node:fs");
61
- const node_path_1 = require("node:path");
62
- const cli_1 = require("@shikijs/cli");
63
- const Diff = __importStar(require("diff"));
64
- const diff_js_1 = require("./core/diff.js");
65
- const command_js_1 = require("./review/command.js");
66
- const export_js_1 = require("./review/export.js");
67
- const git_js_1 = require("./review/git.js");
68
- const hunk_preview_js_1 = require("./review/hunk-preview.js");
69
- const interactive_js_1 = require("./review/interactive.js");
23
+ import { existsSync, readFileSync } from "node:fs";
24
+ import { extname, relative } from "node:path";
25
+ import { codeToANSI } from "@shikijs/cli";
26
+ import * as Diff from "diff";
27
+ import { parseDiff } from "./core/diff.js";
28
+ import { registerReviewDiffCommand } from "./review/command.js";
29
+ import { formatReviewMarkdown } from "./review/export.js";
30
+ import { countReviewDiffLines, readGitDiff } from "./review/git.js";
31
+ import { applyDiffPalette as applySharedDiffPalette, lang as detectDiffLanguage, renderSplit as renderSharedSplit, resolveDiffColors as resolveSharedDiffColors, themeCacheKey as sharedThemeCacheKey, } from "./review/hunk-preview.js";
32
+ import { formatInteractiveReviewPanel } from "./review/interactive.js";
70
33
  const DIFF_PRESETS = {
71
34
  default: {
72
35
  name: "default",
@@ -245,8 +208,8 @@ function loadDiffConfig() {
245
208
  const paths = [`${process.cwd()}/.pi/settings.json`, `${process.env.HOME ?? ""}/.pi/settings.json`];
246
209
  for (const p of paths) {
247
210
  try {
248
- if ((0, node_fs_1.existsSync)(p)) {
249
- const raw = JSON.parse((0, node_fs_1.readFileSync)(p, "utf-8"));
211
+ if (existsSync(p)) {
212
+ const raw = JSON.parse(readFileSync(p, "utf-8"));
250
213
  if (raw.diffTheme || raw.diffColors) {
251
214
  return { diffTheme: raw.diffTheme, diffColors: raw.diffColors };
252
215
  }
@@ -681,7 +644,7 @@ function lnum(n, w, fg = FG_LNUM) {
681
644
  function shortPath(cwd, home, p) {
682
645
  if (!p)
683
646
  return "";
684
- const r = (0, node_path_1.relative)(cwd, p);
647
+ const r = relative(cwd, p);
685
648
  if (!r.startsWith("..") && !r.startsWith("/"))
686
649
  return r;
687
650
  return p.replace(home, "~");
@@ -777,14 +740,14 @@ const EXT_LANG = {
777
740
  vue: "vue",
778
741
  };
779
742
  function lang(fp) {
780
- return EXT_LANG[(0, node_path_1.extname)(fp).slice(1).toLowerCase()];
743
+ return EXT_LANG[extname(fp).slice(1).toLowerCase()];
781
744
  }
782
745
  // ---------------------------------------------------------------------------
783
746
  // Shiki ANSI cache + pre-warm
784
747
  // ---------------------------------------------------------------------------
785
748
  // Pre-warm the Shiki singleton (loads WASM grammars + theme) so the first
786
749
  // diff render doesn't pay the ~200-500ms startup cost.
787
- (0, cli_1.codeToANSI)("", "typescript", THEME).catch(() => { });
750
+ codeToANSI("", "typescript", THEME).catch(() => { });
788
751
  const _cache = new Map();
789
752
  function _touch(k, v) {
790
753
  _cache.delete(k);
@@ -807,7 +770,7 @@ async function hlBlock(code, language) {
807
770
  if (hit)
808
771
  return _touch(k, hit);
809
772
  try {
810
- const ansi = normalizeShikiContrast(await (0, cli_1.codeToANSI)(code, language, THEME));
773
+ const ansi = normalizeShikiContrast(await codeToANSI(code, language, THEME));
811
774
  const out = (ansi.endsWith("\n") ? ansi.slice(0, -1) : ansi).split("\n");
812
775
  return _touch(k, out);
813
776
  }
@@ -1175,9 +1138,9 @@ async function renderSplit(diff, language, max = MAX_PREVIEW_LINES, dc = DEFAULT
1175
1138
  // ---------------------------------------------------------------------------
1176
1139
  // Extension
1177
1140
  // ---------------------------------------------------------------------------
1178
- exports.__testing = {
1141
+ export const __testing = {
1179
1142
  normalizeShikiContrast,
1180
- parseDiff: diff_js_1.parseDiff,
1143
+ parseDiff,
1181
1144
  renderSplit,
1182
1145
  renderUnified,
1183
1146
  };
@@ -1202,9 +1165,9 @@ function normalizeOptionalPositiveInteger(value, name) {
1202
1165
  throw new Error(`${name} must be a positive integer`);
1203
1166
  return number;
1204
1167
  }
1205
- async function diffRendererExtension(pi) {
1168
+ export default async function diffRendererExtension(pi) {
1206
1169
  // Apply diff theme palette from settings/presets before rendering
1207
- (0, hunk_preview_js_1.applyDiffPalette)();
1170
+ applySharedDiffPalette();
1208
1171
  let createWriteTool, createEditTool, getMarkdownTheme, TextComponent, MarkdownComponent;
1209
1172
  try {
1210
1173
  const sdk = await import("@earendil-works/pi-coding-agent");
@@ -1224,8 +1187,45 @@ async function diffRendererExtension(pi) {
1224
1187
  const cwd = process.cwd();
1225
1188
  const home = process.env.HOME ?? "";
1226
1189
  const sp = (p) => shortPath(cwd, home, p);
1227
- const reviewComments = [];
1228
- (0, command_js_1.registerReviewDiffCommand)(pi, cwd);
1190
+ /** Wrap a Text component so its render(width) kicks off async diff rendering
1191
+ * using the real TUI width (which accounts for the sidebar). */
1192
+ function getWidthAwareText(lastComponent) {
1193
+ const text = (lastComponent ?? new TextComponent("", 0, 0));
1194
+ if (text.__piDiffWidthAware)
1195
+ return text;
1196
+ const baseRender = typeof text.render === "function" ? text.render.bind(text) : null;
1197
+ if (!baseRender)
1198
+ return text;
1199
+ text.__piDiffWidthAware = true;
1200
+ text.__piDiffRender = baseRender;
1201
+ text.render = (width) => {
1202
+ const task = text.__piDiffTask;
1203
+ if (task) {
1204
+ const renderWidth = Math.max(1, Math.floor(width || termW()));
1205
+ const key = task.key(renderWidth);
1206
+ if (text.__piDiffRenderedKey !== key) {
1207
+ text.__piDiffRenderedKey = key;
1208
+ text.setText(task.placeholder);
1209
+ Promise.resolve(task.render(renderWidth))
1210
+ .then((rendered) => {
1211
+ if (text.__piDiffRenderedKey !== key)
1212
+ return;
1213
+ text.setText(rendered);
1214
+ task.invalidate?.();
1215
+ })
1216
+ .catch(() => {
1217
+ if (text.__piDiffRenderedKey !== key)
1218
+ return;
1219
+ text.setText(task.fallback);
1220
+ task.invalidate?.();
1221
+ });
1222
+ }
1223
+ }
1224
+ return text.__piDiffRender(width);
1225
+ };
1226
+ return text;
1227
+ }
1228
+ registerReviewDiffCommand(pi, cwd);
1229
1229
  pi.registerTool({
1230
1230
  name: "review_git_diff",
1231
1231
  label: "Review Git Diff",
@@ -1279,31 +1279,32 @@ Examples:
1279
1279
  },
1280
1280
  async execute(_tid, params = {}) {
1281
1281
  try {
1282
- const safeParams = params ?? {};
1283
- const diff = await (0, git_js_1.readGitDiff)(cwd, reviewGitDiffMode(safeParams));
1282
+ const safeParams = (params ?? {});
1283
+ const diff = await readGitDiff(cwd, reviewGitDiffMode(safeParams));
1284
1284
  const maxLinesPerHunk = reviewGitDiffMaxLines(safeParams);
1285
1285
  const markdown = safeParams.includeRawDiff || safeParams.raw
1286
- ? (0, export_js_1.formatReviewMarkdown)(diff, { includeRawDiff: true, maxLinesPerHunk })
1287
- : (0, interactive_js_1.formatInteractiveReviewPanel)(diff, reviewComments, {
1286
+ ? formatReviewMarkdown(diff, { includeRawDiff: true, maxLinesPerHunk })
1287
+ : formatInteractiveReviewPanel(diff, [], {
1288
1288
  file: safeParams.file,
1289
1289
  hunkId: safeParams.hunkId,
1290
1290
  maxFiles: normalizeOptionalPositiveInteger(safeParams.maxFiles, "maxFiles"),
1291
1291
  maxHunks: normalizeOptionalPositiveInteger(safeParams.maxHunks, "maxHunks"),
1292
1292
  maxLinesPerHunk,
1293
1293
  });
1294
- const counts = (0, git_js_1.countReviewDiffLines)(diff);
1294
+ const counts = countReviewDiffLines(diff);
1295
1295
  return {
1296
1296
  content: [{ type: "text", text: markdown }],
1297
1297
  details: {
1298
1298
  _type: "reviewGitDiff",
1299
1299
  markdown,
1300
- mode: diff.mode,
1300
+ mode: String(diff.mode),
1301
1301
  fileCount: diff.files.length,
1302
1302
  insertions: counts.insertions,
1303
1303
  deletions: counts.deletions,
1304
- commentCount: reviewComments.length,
1304
+ commentCount: 0,
1305
1305
  focusedFile: safeParams.file,
1306
1306
  focusedHunk: safeParams.hunkId,
1307
+ error: undefined,
1307
1308
  },
1308
1309
  };
1309
1310
  }
@@ -1311,14 +1312,15 @@ Examples:
1311
1312
  const message = error instanceof Error ? error.message : String(error);
1312
1313
  return {
1313
1314
  content: [{ type: "text", text: `Error: ${message}` }],
1314
- details: { _type: "reviewGitDiff", error: message },
1315
+ details: { _type: "reviewGitDiff", markdown: "", mode: "error", fileCount: 0, insertions: 0, deletions: 0, commentCount: 0, focusedFile: undefined, focusedHunk: undefined, error: message },
1315
1316
  };
1316
1317
  }
1317
1318
  },
1318
1319
  renderCall(args, theme, ctx) {
1319
1320
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1320
- const base = typeof args?.base === "string" && args.base.trim() ? ` vs ${args.base.trim()}` : " working tree";
1321
- const focus = typeof args?.file === "string" && args.file.trim() ? ` ${args.file.trim()}` : "";
1321
+ const safeArgs = args;
1322
+ const base = typeof safeArgs?.base === "string" && safeArgs.base.trim() ? ` vs ${safeArgs.base.trim()}` : " working tree";
1323
+ const focus = typeof safeArgs?.file === "string" && safeArgs.file.trim() ? ` • ${safeArgs.file.trim()}` : "";
1322
1324
  text.setText(`${theme.fg("toolTitle", theme.bold("review_git_diff"))}${theme.fg("muted", `${base}${focus}`)}`);
1323
1325
  return text;
1324
1326
  },
@@ -1342,113 +1344,6 @@ Examples:
1342
1344
  return text;
1343
1345
  },
1344
1346
  });
1345
- pi.registerTool({
1346
- name: "review_git_comment",
1347
- label: "Draft Review Comment",
1348
- description: "Draft an inline code-review comment for the current interactive Git review. This stores comments in memory for the current Pi session and does not modify files or submit anything externally.",
1349
- promptSnippet: "Draft an inline comment for the current Git review session.",
1350
- parameters: {
1351
- type: "object",
1352
- properties: {
1353
- file: { type: "string", description: "Changed file path to comment on." },
1354
- line: { type: "number", description: "Optional old/new line number to comment on." },
1355
- hunkId: { type: "string", description: "Optional hunk id from review_git_diff output." },
1356
- body: { type: "string", description: "Comment body." },
1357
- },
1358
- required: ["file", "body"],
1359
- additionalProperties: false,
1360
- },
1361
- async execute(_tid, params) {
1362
- const file = typeof params?.file === "string" ? params.file.trim() : "";
1363
- const body = typeof params?.body === "string" ? params.body.trim() : "";
1364
- if (!file)
1365
- return {
1366
- content: [{ type: "text", text: "Error: file is required" }],
1367
- details: { error: "file required" },
1368
- };
1369
- if (!body)
1370
- return {
1371
- content: [{ type: "text", text: "Error: body is required" }],
1372
- details: { error: "body required" },
1373
- };
1374
- const comment = (0, interactive_js_1.createReviewComment)({
1375
- comments: reviewComments,
1376
- file,
1377
- body,
1378
- line: normalizeOptionalPositiveInteger(params.line, "line"),
1379
- hunkId: typeof params.hunkId === "string" && params.hunkId.trim() ? params.hunkId.trim() : undefined,
1380
- });
1381
- reviewComments.push(comment);
1382
- return {
1383
- content: [
1384
- {
1385
- type: "text",
1386
- text: `Drafted ${comment.id} on ${comment.file}${comment.line ? `:${comment.line}` : ""}\n\n${comment.body}`,
1387
- },
1388
- ],
1389
- details: { _type: "reviewGitComment", comment, commentCount: reviewComments.length },
1390
- };
1391
- },
1392
- renderCall(args, theme, ctx) {
1393
- const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1394
- text.setText(`${theme.fg("toolTitle", theme.bold("review_git_comment"))} ${theme.fg("accent", args?.file ?? "")}`);
1395
- return text;
1396
- },
1397
- renderResult(result, _opt, theme, ctx) {
1398
- const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1399
- if (ctx.isError || result.details?.error) {
1400
- text.setText(`\n${theme.fg("error", result.details?.error ?? "review_git_comment failed")}`);
1401
- return text;
1402
- }
1403
- const comment = result.details?.comment;
1404
- text.setText(` ${theme.fg("success", `✓ drafted ${comment?.id ?? "comment"}`)}${theme.fg("muted", ` (${result.details?.commentCount ?? 0} total)`)}`);
1405
- return text;
1406
- },
1407
- });
1408
- pi.registerTool({
1409
- name: "review_git_comments",
1410
- label: "Review Comments",
1411
- description: "List or clear drafted interactive Git review comments for the current Pi session.",
1412
- promptSnippet: "List or clear drafted review comments.",
1413
- parameters: {
1414
- type: "object",
1415
- properties: {
1416
- clear: { type: "boolean", description: "Clear all drafted comments when true." },
1417
- },
1418
- additionalProperties: false,
1419
- },
1420
- async execute(_tid, params = {}) {
1421
- if (params?.clear) {
1422
- const cleared = reviewComments.length;
1423
- reviewComments.length = 0;
1424
- return {
1425
- content: [{ type: "text", text: `Cleared ${cleared} drafted review comments.` }],
1426
- details: { _type: "reviewGitComments", cleared, commentCount: 0 },
1427
- };
1428
- }
1429
- const markdown = (0, interactive_js_1.formatReviewComments)(reviewComments);
1430
- return {
1431
- content: [{ type: "text", text: markdown }],
1432
- details: { _type: "reviewGitComments", markdown, commentCount: reviewComments.length },
1433
- };
1434
- },
1435
- renderCall(_args, theme, ctx) {
1436
- const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1437
- text.setText(theme.fg("toolTitle", theme.bold("review_git_comments")));
1438
- return text;
1439
- },
1440
- renderResult(result, _opt, theme, ctx) {
1441
- if (MarkdownComponent && getMarkdownTheme && typeof result.details?.markdown === "string") {
1442
- return new MarkdownComponent(result.details.markdown, 0, 0, getMarkdownTheme());
1443
- }
1444
- const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1445
- const cleared = typeof result.details?.cleared === "number"
1446
- ? `cleared ${result.details.cleared}`
1447
- : `${result.details?.commentCount ?? 0} drafted`;
1448
- text.setText(` ${theme.fg("muted", cleared)} review comments`);
1449
- return text;
1450
- },
1451
- });
1452
1347
  // =======================================================================
1453
1348
  // write
1454
1349
  // =======================================================================
@@ -1460,8 +1355,8 @@ Examples:
1460
1355
  const fp = params.path ?? params.file_path ?? "";
1461
1356
  let old = null;
1462
1357
  try {
1463
- if (fp && (0, node_fs_1.existsSync)(fp))
1464
- old = (0, node_fs_1.readFileSync)(fp, "utf-8");
1358
+ if (fp && existsSync(fp))
1359
+ old = readFileSync(fp, "utf-8");
1465
1360
  }
1466
1361
  catch {
1467
1362
  old = null;
@@ -1470,8 +1365,8 @@ Examples:
1470
1365
  const content = params.content ?? "";
1471
1366
  // Store in details — the only custom field TUI preserves in renderResult
1472
1367
  if (old !== null && old !== content) {
1473
- const diff = (0, diff_js_1.parseDiff)(old, content);
1474
- const lg = (0, hunk_preview_js_1.lang)(fp);
1368
+ const diff = parseDiff(old, content);
1369
+ const lg = detectDiffLanguage(fp);
1475
1370
  result.details = { _type: "diff", summary: summarize(diff.added, diff.removed), diff, language: lg };
1476
1371
  }
1477
1372
  else if (old === null) {
@@ -1485,7 +1380,7 @@ Examples:
1485
1380
  },
1486
1381
  renderCall(args, theme, ctx) {
1487
1382
  const fp = args?.path ?? args?.file_path ?? "";
1488
- const isNew = !fp || !(0, node_fs_1.existsSync)(fp);
1383
+ const isNew = !fp || !existsSync(fp);
1489
1384
  const label = isNew ? "create" : "write";
1490
1385
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1491
1386
  const hdr = `${theme.fg("toolTitle", theme.bold(label))} ${theme.fg("accent", sp(fp))}`;
@@ -1497,11 +1392,11 @@ Examples:
1497
1392
  }
1498
1393
  // New file preview with Shiki
1499
1394
  if (args?.content && ctx.argsComplete && isNew) {
1500
- const previewKey = `create:${(0, hunk_preview_js_1.themeCacheKey)(theme)}:${fp}:${String(args.content).length}`;
1395
+ const previewKey = `create:${sharedThemeCacheKey(theme)}:${fp}:${String(args.content).length}`;
1501
1396
  if (ctx.state._previewKey !== previewKey) {
1502
1397
  ctx.state._previewKey = previewKey;
1503
1398
  ctx.state._previewText = hdr;
1504
- const lg = (0, hunk_preview_js_1.lang)(fp);
1399
+ const lg = detectDiffLanguage(fp);
1505
1400
  hlBlock(args.content, lg)
1506
1401
  .then((lines) => {
1507
1402
  if (ctx.state._previewKey !== previewKey)
@@ -1524,51 +1419,41 @@ Examples:
1524
1419
  return text;
1525
1420
  },
1526
1421
  renderResult(result, _opt, theme, ctx) {
1527
- const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1422
+ const text = getWidthAwareText(ctx.lastComponent);
1528
1423
  if (ctx.isError) {
1529
1424
  const e = result.content
1530
1425
  ?.filter((c) => c.type === "text")
1531
1426
  .map((c) => c.text || "")
1532
1427
  .join("\n") ?? "Error";
1428
+ text.__piDiffTask = undefined;
1533
1429
  text.setText(`\n${theme.fg("error", e)}`);
1534
1430
  return text;
1535
1431
  }
1536
1432
  const d = result.details;
1537
1433
  if (d?._type === "diff") {
1538
- const w = termW();
1539
- const key = `wd:${(0, hunk_preview_js_1.themeCacheKey)(theme)}:${w}:${d.summary}:${d.diff?.lines?.length ?? 0}:${d.language ?? ""}`;
1540
- if (ctx.state._wdk !== key) {
1541
- ctx.state._wdk = key;
1542
- ctx.state._wdt = ` ${d.summary}\n${theme.fg("muted", " rendering diff…")}`;
1543
- const dc = (0, hunk_preview_js_1.resolveDiffColors)(theme);
1544
- (0, hunk_preview_js_1.renderSplit)(d.diff, d.language, MAX_RENDER_LINES, dc, w)
1545
- .then((rendered) => {
1546
- if (ctx.state._wdk !== key)
1547
- return;
1548
- ctx.state._wdt = ` ${d.summary}\n${rendered}`;
1549
- ctx.invalidate();
1550
- })
1551
- .catch(() => {
1552
- if (ctx.state._wdk !== key)
1553
- return;
1554
- ctx.state._wdt = ` ${d.summary}`;
1555
- ctx.invalidate();
1556
- });
1557
- }
1558
- text.setText(ctx.state._wdt ?? ` ${d.summary}`);
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
+ };
1559
1443
  return text;
1560
1444
  }
1561
1445
  if (d?._type === "noChange") {
1446
+ text.__piDiffTask = undefined;
1562
1447
  text.setText(` ${theme.fg("muted", "✓ no changes")}`);
1563
1448
  return text;
1564
1449
  }
1565
1450
  if (d?._type === "new") {
1566
1451
  const { lines: lineCount, content: rawContent, filePath: fp } = d;
1567
- const pk = `nf:${(0, hunk_preview_js_1.themeCacheKey)(theme)}:${fp}:${lineCount}`;
1452
+ const pk = `nf:${sharedThemeCacheKey(theme)}:${fp}:${lineCount}`;
1568
1453
  if (ctx.state._nfk !== pk) {
1569
1454
  ctx.state._nfk = pk;
1570
1455
  ctx.state._nft = ` ${theme.fg("success", `✓ new file (${lineCount} lines)`)}`;
1571
- const lg = (0, hunk_preview_js_1.lang)(fp);
1456
+ const lg = detectDiffLanguage(fp);
1572
1457
  if (rawContent) {
1573
1458
  hlBlock(rawContent, lg)
1574
1459
  .then((hlLines) => {
@@ -1611,7 +1496,7 @@ Examples:
1611
1496
  return oldText && oldText !== newText ? [{ oldText, newText }] : [];
1612
1497
  }
1613
1498
  function summarizeEditOperations(operations) {
1614
- const diffs = operations.map((edit) => (0, diff_js_1.parseDiff)(edit.oldText, edit.newText));
1499
+ const diffs = operations.map((edit) => parseDiff(edit.oldText, edit.newText));
1615
1500
  const totalAdded = diffs.reduce((sum, diff) => sum + diff.added, 0);
1616
1501
  const totalRemoved = diffs.reduce((sum, diff) => sum + diff.removed, 0);
1617
1502
  return {
@@ -1631,11 +1516,12 @@ Examples:
1631
1516
  if (operations.length === 0)
1632
1517
  return result;
1633
1518
  const { diffs, summary } = summarizeEditOperations(operations);
1519
+ const lg = detectDiffLanguage(fp);
1634
1520
  if (operations.length === 1) {
1635
1521
  let editLine = 0;
1636
1522
  try {
1637
- if (fp && (0, node_fs_1.existsSync)(fp)) {
1638
- const f = (0, node_fs_1.readFileSync)(fp, "utf-8");
1523
+ if (fp && existsSync(fp)) {
1524
+ const f = readFileSync(fp, "utf-8");
1639
1525
  const idx = f.indexOf(operations[0].newText);
1640
1526
  if (idx >= 0)
1641
1527
  editLine = f.slice(0, idx).split("\n").length;
@@ -1644,7 +1530,7 @@ Examples:
1644
1530
  catch {
1645
1531
  editLine = 0;
1646
1532
  }
1647
- result.details = { _type: "editInfo", summary, editLine };
1533
+ result.details = { _type: "editInfo", summary, editLine, diff: diffs[0], language: lg };
1648
1534
  return result;
1649
1535
  }
1650
1536
  result.details = {
@@ -1660,85 +1546,54 @@ Examples:
1660
1546
  const operations = getEditOperations(args);
1661
1547
  const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1662
1548
  const hdr = `${theme.fg("toolTitle", theme.bold("edit"))} ${theme.fg("accent", sp(fp))}`;
1663
- if (!(ctx.argsComplete && operations.length > 0)) {
1664
- text.setText(hdr);
1665
- return text;
1549
+ if (ctx.argsComplete && operations.length > 0) {
1550
+ const { totalAdded, totalRemoved } = summarizeEditOperations(operations);
1551
+ text.setText(`${hdr} ${theme.fg("muted", summarize(totalAdded, totalRemoved))}`);
1666
1552
  }
1667
- const pk = JSON.stringify({ fp, operations, theme: (0, hunk_preview_js_1.themeCacheKey)(theme), w: termW() });
1668
- if (ctx.state._pk !== pk) {
1669
- ctx.state._pk = pk;
1670
- ctx.state._pt = `${hdr} ${theme.fg("muted", "(rendering…)")}`;
1671
- const lg = (0, hunk_preview_js_1.lang)(fp);
1672
- const dc = (0, hunk_preview_js_1.resolveDiffColors)(theme);
1673
- if (operations.length === 1) {
1674
- const diff = (0, diff_js_1.parseDiff)(operations[0].oldText, operations[0].newText);
1675
- (0, hunk_preview_js_1.renderSplit)(diff, lg, MAX_PREVIEW_LINES, dc, termW())
1676
- .then((rendered) => {
1677
- if (ctx.state._pk !== pk)
1678
- return;
1679
- ctx.state._pt = `${hdr}\n${summarize(diff.added, diff.removed)}\n${rendered}`;
1680
- ctx.invalidate();
1681
- })
1682
- .catch(() => {
1683
- if (ctx.state._pk !== pk)
1684
- return;
1685
- ctx.state._pt = `${hdr} ${summarize(diff.added, diff.removed)}`;
1686
- ctx.invalidate();
1687
- });
1688
- }
1689
- else {
1690
- const { diffs, summary } = summarizeEditOperations(operations);
1691
- const maxShown = Math.min(operations.length, 3);
1692
- const previewLines = Math.max(8, Math.floor(MAX_PREVIEW_LINES / maxShown));
1693
- Promise.all(diffs.slice(0, maxShown).map((diff, index) => (0, hunk_preview_js_1.renderSplit)(diff, lg, previewLines, dc, termW())
1694
- .then((rendered) => `Edit ${index + 1}/${operations.length}\n${rendered}`)
1695
- .catch(() => `Edit ${index + 1}/${operations.length} ${summarize(diff.added, diff.removed)}`)))
1696
- .then((sections) => {
1697
- if (ctx.state._pk !== pk)
1698
- return;
1699
- const remainder = operations.length - maxShown;
1700
- const suffix = remainder > 0 ? `\n${theme.fg("muted", `… ${remainder} more edit blocks`)}` : "";
1701
- ctx.state._pt = `${hdr}\n${operations.length} edits ${summary}\n\n${sections.join("\n\n")}${suffix}`;
1702
- ctx.invalidate();
1703
- })
1704
- .catch(() => {
1705
- if (ctx.state._pk !== pk)
1706
- return;
1707
- ctx.state._pt = `${hdr} ${operations.length} edits ${summary}`;
1708
- ctx.invalidate();
1709
- });
1710
- }
1553
+ else {
1554
+ text.setText(hdr);
1711
1555
  }
1712
- text.setText(ctx.state._pt ?? hdr);
1713
1556
  return text;
1714
1557
  },
1715
1558
  renderResult(result, _opt, theme, ctx) {
1716
- const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
1559
+ const text = getWidthAwareText(ctx.lastComponent);
1717
1560
  if (ctx.isError) {
1718
1561
  const e = result.content
1719
1562
  ?.filter((c) => c.type === "text")
1720
1563
  .map((c) => c.text || "")
1721
1564
  .join("\n") ?? "Error";
1565
+ text.__piDiffTask = undefined;
1722
1566
  text.setText(`\n${theme.fg("error", e)}`);
1723
1567
  return text;
1724
1568
  }
1725
- if (result.details?._type === "editInfo") {
1726
- const { summary: s, editLine } = result.details;
1569
+ const d = result.details;
1570
+ if (d?._type === "editInfo" && d.diff) {
1571
+ const themeKey = sharedThemeCacheKey(theme);
1572
+ const dc = resolveSharedDiffColors(theme);
1573
+ const loc = d.editLine > 0 ? ` ${theme.fg("muted", `at line ${d.editLine}`)}` : "";
1574
+ text.__piDiffTask = {
1575
+ placeholder: ` ${d.summary}${loc}\n${theme.fg("muted", " rendering diff…")}`,
1576
+ fallback: ` ${d.summary}${loc}`,
1577
+ invalidate: ctx.invalidate,
1578
+ key: (width) => `ed:${themeKey}:${width}:${d.summary}:${d.diff?.lines?.length ?? 0}:${d.language ?? ""}`,
1579
+ render: async (width) => ` ${d.summary}${loc}\n${await renderSharedSplit(d.diff, d.language, MAX_PREVIEW_LINES, dc, width)}`,
1580
+ };
1581
+ return text;
1582
+ }
1583
+ if (d?._type === "editInfo") {
1584
+ const { summary: s, editLine } = d;
1727
1585
  const loc = editLine > 0 ? ` ${theme.fg("muted", `at line ${editLine}`)}` : "";
1728
- const content = ` ${s}${loc}`;
1729
- const vis = content.replace(ANSI_RE, "").length;
1730
- const pad = Math.max(0, termW() - vis);
1731
- text.setText(`${content}${" ".repeat(pad)}`);
1586
+ text.__piDiffTask = undefined;
1587
+ text.setText(` ${s}${loc}`);
1732
1588
  return text;
1733
1589
  }
1734
- if (result.details?._type === "multiEditInfo") {
1735
- const { summary: s, editCount, diffLineCount } = result.details;
1736
- const content = ` ${editCount} edits ${s}${typeof diffLineCount === "number" ? ` ${theme.fg("muted", `(${diffLineCount} diff lines)`)}` : ""}`;
1737
- const vis = content.replace(ANSI_RE, "").length;
1738
- const pad = Math.max(0, termW() - vis);
1739
- text.setText(`${content}${" ".repeat(pad)}`);
1590
+ if (d?._type === "multiEditInfo") {
1591
+ const { summary: s, editCount, diffLineCount } = d;
1592
+ text.__piDiffTask = undefined;
1593
+ text.setText(` ${editCount} edits ${s}${typeof diffLineCount === "number" ? ` ${theme.fg("muted", `(${diffLineCount} diff lines)`)}` : ""}`);
1740
1594
  return text;
1741
1595
  }
1596
+ text.__piDiffTask = undefined;
1742
1597
  text.setText(` ${theme.fg("dim", String(result?.content?.[0]?.text ?? "edited").slice(0, 120))}`);
1743
1598
  return text;
1744
1599
  },