@heyhuynhgiabuu/pi-diff 0.4.0 → 0.5.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 +101 -53
- package/dist/cli.js +4 -6
- package/dist/cli.js.map +1 -1
- package/dist/core/diff.js +2 -38
- package/dist/core/diff.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -196
- package/dist/index.js.map +1 -1
- package/dist/review/command.d.ts +2 -1
- package/dist/review/command.d.ts.map +1 -1
- package/dist/review/command.js +252 -174
- package/dist/review/command.js.map +1 -1
- package/dist/review/export.js +1 -4
- package/dist/review/export.js.map +1 -1
- package/dist/review/file-preview.js +7 -10
- package/dist/review/file-preview.js.map +1 -1
- package/dist/review/git.js +14 -22
- package/dist/review/git.js.map +1 -1
- package/dist/review/hunk-bridge.d.ts +54 -0
- package/dist/review/hunk-bridge.d.ts.map +1 -0
- package/dist/review/hunk-bridge.js +105 -0
- package/dist/review/hunk-bridge.js.map +1 -0
- package/dist/review/hunk-preview.d.ts.map +1 -1
- package/dist/review/hunk-preview.js +26 -63
- package/dist/review/hunk-preview.js.map +1 -1
- package/dist/review/interactive.js +3 -8
- package/dist/review/interactive.js.map +1 -1
- package/dist/review/model.js +9 -20
- package/dist/review/model.js.map +1 -1
- package/dist/review/prompt.js +3 -6
- package/dist/review/prompt.js.map +1 -1
- package/dist/review/session.js +22 -44
- package/dist/review/session.js.map +1 -1
- package/dist/review/tui.js +47 -51
- package/dist/review/tui.js.map +1 -1
- package/package.json +11 -1
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 (
|
|
249
|
-
const raw = JSON.parse(
|
|
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 =
|
|
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[
|
|
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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -875,9 +838,13 @@ function injectBg(ansiLine, ranges, baseBg, hlBg) {
|
|
|
875
838
|
if (m !== -1) {
|
|
876
839
|
const seq = ansiLine.slice(i, m + 1);
|
|
877
840
|
out += seq;
|
|
878
|
-
// Re-inject bg after
|
|
879
|
-
|
|
841
|
+
// Re-inject bg after any reset-like sequence.
|
|
842
|
+
// Shiki uses \x1b[39m (fg reset) between tokens — technically this
|
|
843
|
+
// doesn't clear background per ANSI spec, but some terminal
|
|
844
|
+
// emulators may treat it as a broader reset.
|
|
845
|
+
if (seq === "\x1b[0m" || seq === "\x1b[39m" || seq === "\x1b[49m") {
|
|
880
846
|
out += inHL ? hlBg : baseBg;
|
|
847
|
+
}
|
|
881
848
|
i = m + 1;
|
|
882
849
|
continue;
|
|
883
850
|
}
|
|
@@ -995,14 +962,19 @@ async function renderUnified(diff, language, max = MAX_RENDER_LINES, dc = DEFAUL
|
|
|
995
962
|
// 1:1 paired → word diff emphasis
|
|
996
963
|
const isPaired = dels.length === 1 && adds.length === 1;
|
|
997
964
|
const wd = isPaired ? wordDiffAnalysis(dels[0].l.content, adds[0].l.content) : null;
|
|
998
|
-
|
|
965
|
+
// Word-diff emphasis — only use when BOTH sides have ranges.
|
|
966
|
+
// When diffWords treats trailing punctuation as "common" while removing
|
|
967
|
+
// adjacent chars, only one side gets word highlights, creating a confusing
|
|
968
|
+
// visual ("off by 1" perception). Skip word-level in that case.
|
|
969
|
+
const wdBalanced = wd && wd.oldRanges.length > 0 && wd.newRanges.length > 0;
|
|
970
|
+
if (isPaired && wdBalanced && wd.similarity >= WORD_DIFF_MIN_SIM && canHL) {
|
|
999
971
|
const delBody = injectBg(dels[0].hl, wd.oldRanges, BG_DEL, BG_DEL_W);
|
|
1000
972
|
const addBody = injectBg(adds[0].hl, wd.newRanges, BG_ADD, BG_ADD_W);
|
|
1001
973
|
emitRow(dels[0].l.oldNum, "-", BG_GUTTER_DEL, `${dc.fgDel}${BOLD}`, delBody, BG_DEL);
|
|
1002
974
|
emitRow(adds[0].l.newNum, "+", BG_GUTTER_ADD, `${dc.fgAdd}${BOLD}`, addBody, BG_ADD);
|
|
1003
975
|
continue;
|
|
1004
976
|
}
|
|
1005
|
-
if (isPaired &&
|
|
977
|
+
if (isPaired && wdBalanced && wd.similarity >= WORD_DIFF_MIN_SIM && !canHL) {
|
|
1006
978
|
const pwd = plainWordDiff(dels[0].l.content, adds[0].l.content);
|
|
1007
979
|
emitRow(dels[0].l.oldNum, "-", BG_GUTTER_DEL, `${dc.fgDel}${BOLD}`, `${BG_DEL}${pwd.old}`, BG_DEL);
|
|
1008
980
|
emitRow(adds[0].l.newNum, "+", BG_GUTTER_ADD, `${dc.fgAdd}${BOLD}`, `${BG_ADD}${pwd.new}`, BG_ADD);
|
|
@@ -1166,9 +1138,9 @@ async function renderSplit(diff, language, max = MAX_PREVIEW_LINES, dc = DEFAULT
|
|
|
1166
1138
|
// ---------------------------------------------------------------------------
|
|
1167
1139
|
// Extension
|
|
1168
1140
|
// ---------------------------------------------------------------------------
|
|
1169
|
-
|
|
1141
|
+
export const __testing = {
|
|
1170
1142
|
normalizeShikiContrast,
|
|
1171
|
-
parseDiff
|
|
1143
|
+
parseDiff,
|
|
1172
1144
|
renderSplit,
|
|
1173
1145
|
renderUnified,
|
|
1174
1146
|
};
|
|
@@ -1193,9 +1165,9 @@ function normalizeOptionalPositiveInteger(value, name) {
|
|
|
1193
1165
|
throw new Error(`${name} must be a positive integer`);
|
|
1194
1166
|
return number;
|
|
1195
1167
|
}
|
|
1196
|
-
async function diffRendererExtension(pi) {
|
|
1168
|
+
export default async function diffRendererExtension(pi) {
|
|
1197
1169
|
// Apply diff theme palette from settings/presets before rendering
|
|
1198
|
-
(
|
|
1170
|
+
applySharedDiffPalette();
|
|
1199
1171
|
let createWriteTool, createEditTool, getMarkdownTheme, TextComponent, MarkdownComponent;
|
|
1200
1172
|
try {
|
|
1201
1173
|
const sdk = await import("@earendil-works/pi-coding-agent");
|
|
@@ -1215,8 +1187,7 @@ async function diffRendererExtension(pi) {
|
|
|
1215
1187
|
const cwd = process.cwd();
|
|
1216
1188
|
const home = process.env.HOME ?? "";
|
|
1217
1189
|
const sp = (p) => shortPath(cwd, home, p);
|
|
1218
|
-
|
|
1219
|
-
(0, command_js_1.registerReviewDiffCommand)(pi, cwd);
|
|
1190
|
+
registerReviewDiffCommand(pi, cwd);
|
|
1220
1191
|
pi.registerTool({
|
|
1221
1192
|
name: "review_git_diff",
|
|
1222
1193
|
label: "Review Git Diff",
|
|
@@ -1271,18 +1242,18 @@ Examples:
|
|
|
1271
1242
|
async execute(_tid, params = {}) {
|
|
1272
1243
|
try {
|
|
1273
1244
|
const safeParams = params ?? {};
|
|
1274
|
-
const diff = await
|
|
1245
|
+
const diff = await readGitDiff(cwd, reviewGitDiffMode(safeParams));
|
|
1275
1246
|
const maxLinesPerHunk = reviewGitDiffMaxLines(safeParams);
|
|
1276
1247
|
const markdown = safeParams.includeRawDiff || safeParams.raw
|
|
1277
|
-
?
|
|
1278
|
-
:
|
|
1248
|
+
? formatReviewMarkdown(diff, { includeRawDiff: true, maxLinesPerHunk })
|
|
1249
|
+
: formatInteractiveReviewPanel(diff, [], {
|
|
1279
1250
|
file: safeParams.file,
|
|
1280
1251
|
hunkId: safeParams.hunkId,
|
|
1281
1252
|
maxFiles: normalizeOptionalPositiveInteger(safeParams.maxFiles, "maxFiles"),
|
|
1282
1253
|
maxHunks: normalizeOptionalPositiveInteger(safeParams.maxHunks, "maxHunks"),
|
|
1283
1254
|
maxLinesPerHunk,
|
|
1284
1255
|
});
|
|
1285
|
-
const counts =
|
|
1256
|
+
const counts = countReviewDiffLines(diff);
|
|
1286
1257
|
return {
|
|
1287
1258
|
content: [{ type: "text", text: markdown }],
|
|
1288
1259
|
details: {
|
|
@@ -1292,7 +1263,7 @@ Examples:
|
|
|
1292
1263
|
fileCount: diff.files.length,
|
|
1293
1264
|
insertions: counts.insertions,
|
|
1294
1265
|
deletions: counts.deletions,
|
|
1295
|
-
commentCount:
|
|
1266
|
+
commentCount: 0,
|
|
1296
1267
|
focusedFile: safeParams.file,
|
|
1297
1268
|
focusedHunk: safeParams.hunkId,
|
|
1298
1269
|
},
|
|
@@ -1333,113 +1304,6 @@ Examples:
|
|
|
1333
1304
|
return text;
|
|
1334
1305
|
},
|
|
1335
1306
|
});
|
|
1336
|
-
pi.registerTool({
|
|
1337
|
-
name: "review_git_comment",
|
|
1338
|
-
label: "Draft Review Comment",
|
|
1339
|
-
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.",
|
|
1340
|
-
promptSnippet: "Draft an inline comment for the current Git review session.",
|
|
1341
|
-
parameters: {
|
|
1342
|
-
type: "object",
|
|
1343
|
-
properties: {
|
|
1344
|
-
file: { type: "string", description: "Changed file path to comment on." },
|
|
1345
|
-
line: { type: "number", description: "Optional old/new line number to comment on." },
|
|
1346
|
-
hunkId: { type: "string", description: "Optional hunk id from review_git_diff output." },
|
|
1347
|
-
body: { type: "string", description: "Comment body." },
|
|
1348
|
-
},
|
|
1349
|
-
required: ["file", "body"],
|
|
1350
|
-
additionalProperties: false,
|
|
1351
|
-
},
|
|
1352
|
-
async execute(_tid, params) {
|
|
1353
|
-
const file = typeof params?.file === "string" ? params.file.trim() : "";
|
|
1354
|
-
const body = typeof params?.body === "string" ? params.body.trim() : "";
|
|
1355
|
-
if (!file)
|
|
1356
|
-
return {
|
|
1357
|
-
content: [{ type: "text", text: "Error: file is required" }],
|
|
1358
|
-
details: { error: "file required" },
|
|
1359
|
-
};
|
|
1360
|
-
if (!body)
|
|
1361
|
-
return {
|
|
1362
|
-
content: [{ type: "text", text: "Error: body is required" }],
|
|
1363
|
-
details: { error: "body required" },
|
|
1364
|
-
};
|
|
1365
|
-
const comment = (0, interactive_js_1.createReviewComment)({
|
|
1366
|
-
comments: reviewComments,
|
|
1367
|
-
file,
|
|
1368
|
-
body,
|
|
1369
|
-
line: normalizeOptionalPositiveInteger(params.line, "line"),
|
|
1370
|
-
hunkId: typeof params.hunkId === "string" && params.hunkId.trim() ? params.hunkId.trim() : undefined,
|
|
1371
|
-
});
|
|
1372
|
-
reviewComments.push(comment);
|
|
1373
|
-
return {
|
|
1374
|
-
content: [
|
|
1375
|
-
{
|
|
1376
|
-
type: "text",
|
|
1377
|
-
text: `Drafted ${comment.id} on ${comment.file}${comment.line ? `:${comment.line}` : ""}\n\n${comment.body}`,
|
|
1378
|
-
},
|
|
1379
|
-
],
|
|
1380
|
-
details: { _type: "reviewGitComment", comment, commentCount: reviewComments.length },
|
|
1381
|
-
};
|
|
1382
|
-
},
|
|
1383
|
-
renderCall(args, theme, ctx) {
|
|
1384
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1385
|
-
text.setText(`${theme.fg("toolTitle", theme.bold("review_git_comment"))} ${theme.fg("accent", args?.file ?? "")}`);
|
|
1386
|
-
return text;
|
|
1387
|
-
},
|
|
1388
|
-
renderResult(result, _opt, theme, ctx) {
|
|
1389
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1390
|
-
if (ctx.isError || result.details?.error) {
|
|
1391
|
-
text.setText(`\n${theme.fg("error", result.details?.error ?? "review_git_comment failed")}`);
|
|
1392
|
-
return text;
|
|
1393
|
-
}
|
|
1394
|
-
const comment = result.details?.comment;
|
|
1395
|
-
text.setText(` ${theme.fg("success", `✓ drafted ${comment?.id ?? "comment"}`)}${theme.fg("muted", ` (${result.details?.commentCount ?? 0} total)`)}`);
|
|
1396
|
-
return text;
|
|
1397
|
-
},
|
|
1398
|
-
});
|
|
1399
|
-
pi.registerTool({
|
|
1400
|
-
name: "review_git_comments",
|
|
1401
|
-
label: "Review Comments",
|
|
1402
|
-
description: "List or clear drafted interactive Git review comments for the current Pi session.",
|
|
1403
|
-
promptSnippet: "List or clear drafted review comments.",
|
|
1404
|
-
parameters: {
|
|
1405
|
-
type: "object",
|
|
1406
|
-
properties: {
|
|
1407
|
-
clear: { type: "boolean", description: "Clear all drafted comments when true." },
|
|
1408
|
-
},
|
|
1409
|
-
additionalProperties: false,
|
|
1410
|
-
},
|
|
1411
|
-
async execute(_tid, params = {}) {
|
|
1412
|
-
if (params?.clear) {
|
|
1413
|
-
const cleared = reviewComments.length;
|
|
1414
|
-
reviewComments.length = 0;
|
|
1415
|
-
return {
|
|
1416
|
-
content: [{ type: "text", text: `Cleared ${cleared} drafted review comments.` }],
|
|
1417
|
-
details: { _type: "reviewGitComments", cleared, commentCount: 0 },
|
|
1418
|
-
};
|
|
1419
|
-
}
|
|
1420
|
-
const markdown = (0, interactive_js_1.formatReviewComments)(reviewComments);
|
|
1421
|
-
return {
|
|
1422
|
-
content: [{ type: "text", text: markdown }],
|
|
1423
|
-
details: { _type: "reviewGitComments", markdown, commentCount: reviewComments.length },
|
|
1424
|
-
};
|
|
1425
|
-
},
|
|
1426
|
-
renderCall(_args, theme, ctx) {
|
|
1427
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1428
|
-
text.setText(theme.fg("toolTitle", theme.bold("review_git_comments")));
|
|
1429
|
-
return text;
|
|
1430
|
-
},
|
|
1431
|
-
renderResult(result, _opt, theme, ctx) {
|
|
1432
|
-
if (MarkdownComponent && getMarkdownTheme && typeof result.details?.markdown === "string") {
|
|
1433
|
-
return new MarkdownComponent(result.details.markdown, 0, 0, getMarkdownTheme());
|
|
1434
|
-
}
|
|
1435
|
-
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1436
|
-
const cleared = typeof result.details?.cleared === "number"
|
|
1437
|
-
? `cleared ${result.details.cleared}`
|
|
1438
|
-
: `${result.details?.commentCount ?? 0} drafted`;
|
|
1439
|
-
text.setText(` ${theme.fg("muted", cleared)} review comments`);
|
|
1440
|
-
return text;
|
|
1441
|
-
},
|
|
1442
|
-
});
|
|
1443
1307
|
// =======================================================================
|
|
1444
1308
|
// write
|
|
1445
1309
|
// =======================================================================
|
|
@@ -1451,8 +1315,8 @@ Examples:
|
|
|
1451
1315
|
const fp = params.path ?? params.file_path ?? "";
|
|
1452
1316
|
let old = null;
|
|
1453
1317
|
try {
|
|
1454
|
-
if (fp &&
|
|
1455
|
-
old =
|
|
1318
|
+
if (fp && existsSync(fp))
|
|
1319
|
+
old = readFileSync(fp, "utf-8");
|
|
1456
1320
|
}
|
|
1457
1321
|
catch {
|
|
1458
1322
|
old = null;
|
|
@@ -1461,8 +1325,8 @@ Examples:
|
|
|
1461
1325
|
const content = params.content ?? "";
|
|
1462
1326
|
// Store in details — the only custom field TUI preserves in renderResult
|
|
1463
1327
|
if (old !== null && old !== content) {
|
|
1464
|
-
const diff =
|
|
1465
|
-
const lg = (
|
|
1328
|
+
const diff = parseDiff(old, content);
|
|
1329
|
+
const lg = detectDiffLanguage(fp);
|
|
1466
1330
|
result.details = { _type: "diff", summary: summarize(diff.added, diff.removed), diff, language: lg };
|
|
1467
1331
|
}
|
|
1468
1332
|
else if (old === null) {
|
|
@@ -1476,7 +1340,7 @@ Examples:
|
|
|
1476
1340
|
},
|
|
1477
1341
|
renderCall(args, theme, ctx) {
|
|
1478
1342
|
const fp = args?.path ?? args?.file_path ?? "";
|
|
1479
|
-
const isNew = !fp || !
|
|
1343
|
+
const isNew = !fp || !existsSync(fp);
|
|
1480
1344
|
const label = isNew ? "create" : "write";
|
|
1481
1345
|
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
1482
1346
|
const hdr = `${theme.fg("toolTitle", theme.bold(label))} ${theme.fg("accent", sp(fp))}`;
|
|
@@ -1488,11 +1352,11 @@ Examples:
|
|
|
1488
1352
|
}
|
|
1489
1353
|
// New file preview with Shiki
|
|
1490
1354
|
if (args?.content && ctx.argsComplete && isNew) {
|
|
1491
|
-
const previewKey = `create:${(
|
|
1355
|
+
const previewKey = `create:${sharedThemeCacheKey(theme)}:${fp}:${String(args.content).length}`;
|
|
1492
1356
|
if (ctx.state._previewKey !== previewKey) {
|
|
1493
1357
|
ctx.state._previewKey = previewKey;
|
|
1494
1358
|
ctx.state._previewText = hdr;
|
|
1495
|
-
const lg = (
|
|
1359
|
+
const lg = detectDiffLanguage(fp);
|
|
1496
1360
|
hlBlock(args.content, lg)
|
|
1497
1361
|
.then((lines) => {
|
|
1498
1362
|
if (ctx.state._previewKey !== previewKey)
|
|
@@ -1527,12 +1391,12 @@ Examples:
|
|
|
1527
1391
|
const d = result.details;
|
|
1528
1392
|
if (d?._type === "diff") {
|
|
1529
1393
|
const w = termW();
|
|
1530
|
-
const key = `wd:${(
|
|
1394
|
+
const key = `wd:${sharedThemeCacheKey(theme)}:${w}:${d.summary}:${d.diff?.lines?.length ?? 0}:${d.language ?? ""}`;
|
|
1531
1395
|
if (ctx.state._wdk !== key) {
|
|
1532
1396
|
ctx.state._wdk = key;
|
|
1533
1397
|
ctx.state._wdt = ` ${d.summary}\n${theme.fg("muted", " rendering diff…")}`;
|
|
1534
|
-
const dc = (
|
|
1535
|
-
(
|
|
1398
|
+
const dc = resolveSharedDiffColors(theme);
|
|
1399
|
+
renderSharedSplit(d.diff, d.language, MAX_RENDER_LINES, dc, w)
|
|
1536
1400
|
.then((rendered) => {
|
|
1537
1401
|
if (ctx.state._wdk !== key)
|
|
1538
1402
|
return;
|
|
@@ -1555,11 +1419,11 @@ Examples:
|
|
|
1555
1419
|
}
|
|
1556
1420
|
if (d?._type === "new") {
|
|
1557
1421
|
const { lines: lineCount, content: rawContent, filePath: fp } = d;
|
|
1558
|
-
const pk = `nf:${(
|
|
1422
|
+
const pk = `nf:${sharedThemeCacheKey(theme)}:${fp}:${lineCount}`;
|
|
1559
1423
|
if (ctx.state._nfk !== pk) {
|
|
1560
1424
|
ctx.state._nfk = pk;
|
|
1561
1425
|
ctx.state._nft = ` ${theme.fg("success", `✓ new file (${lineCount} lines)`)}`;
|
|
1562
|
-
const lg = (
|
|
1426
|
+
const lg = detectDiffLanguage(fp);
|
|
1563
1427
|
if (rawContent) {
|
|
1564
1428
|
hlBlock(rawContent, lg)
|
|
1565
1429
|
.then((hlLines) => {
|
|
@@ -1602,7 +1466,7 @@ Examples:
|
|
|
1602
1466
|
return oldText && oldText !== newText ? [{ oldText, newText }] : [];
|
|
1603
1467
|
}
|
|
1604
1468
|
function summarizeEditOperations(operations) {
|
|
1605
|
-
const diffs = operations.map((edit) =>
|
|
1469
|
+
const diffs = operations.map((edit) => parseDiff(edit.oldText, edit.newText));
|
|
1606
1470
|
const totalAdded = diffs.reduce((sum, diff) => sum + diff.added, 0);
|
|
1607
1471
|
const totalRemoved = diffs.reduce((sum, diff) => sum + diff.removed, 0);
|
|
1608
1472
|
return {
|
|
@@ -1625,8 +1489,8 @@ Examples:
|
|
|
1625
1489
|
if (operations.length === 1) {
|
|
1626
1490
|
let editLine = 0;
|
|
1627
1491
|
try {
|
|
1628
|
-
if (fp &&
|
|
1629
|
-
const f =
|
|
1492
|
+
if (fp && existsSync(fp)) {
|
|
1493
|
+
const f = readFileSync(fp, "utf-8");
|
|
1630
1494
|
const idx = f.indexOf(operations[0].newText);
|
|
1631
1495
|
if (idx >= 0)
|
|
1632
1496
|
editLine = f.slice(0, idx).split("\n").length;
|
|
@@ -1655,15 +1519,15 @@ Examples:
|
|
|
1655
1519
|
text.setText(hdr);
|
|
1656
1520
|
return text;
|
|
1657
1521
|
}
|
|
1658
|
-
const pk = JSON.stringify({ fp, operations, theme: (
|
|
1522
|
+
const pk = JSON.stringify({ fp, operations, theme: sharedThemeCacheKey(theme), w: termW() });
|
|
1659
1523
|
if (ctx.state._pk !== pk) {
|
|
1660
1524
|
ctx.state._pk = pk;
|
|
1661
1525
|
ctx.state._pt = `${hdr} ${theme.fg("muted", "(rendering…)")}`;
|
|
1662
|
-
const lg = (
|
|
1663
|
-
const dc = (
|
|
1526
|
+
const lg = detectDiffLanguage(fp);
|
|
1527
|
+
const dc = resolveSharedDiffColors(theme);
|
|
1664
1528
|
if (operations.length === 1) {
|
|
1665
|
-
const diff =
|
|
1666
|
-
(
|
|
1529
|
+
const diff = parseDiff(operations[0].oldText, operations[0].newText);
|
|
1530
|
+
renderSharedSplit(diff, lg, MAX_PREVIEW_LINES, dc, termW())
|
|
1667
1531
|
.then((rendered) => {
|
|
1668
1532
|
if (ctx.state._pk !== pk)
|
|
1669
1533
|
return;
|
|
@@ -1681,7 +1545,7 @@ Examples:
|
|
|
1681
1545
|
const { diffs, summary } = summarizeEditOperations(operations);
|
|
1682
1546
|
const maxShown = Math.min(operations.length, 3);
|
|
1683
1547
|
const previewLines = Math.max(8, Math.floor(MAX_PREVIEW_LINES / maxShown));
|
|
1684
|
-
Promise.all(diffs.slice(0, maxShown).map((diff, index) => (
|
|
1548
|
+
Promise.all(diffs.slice(0, maxShown).map((diff, index) => renderSharedSplit(diff, lg, previewLines, dc, termW())
|
|
1685
1549
|
.then((rendered) => `Edit ${index + 1}/${operations.length}\n${rendered}`)
|
|
1686
1550
|
.catch(() => `Edit ${index + 1}/${operations.length} ${summarize(diff.added, diff.removed)}`)))
|
|
1687
1551
|
.then((sections) => {
|