@pi-archimedes/diff 0.5.0 → 0.7.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/package.json +1 -1
- package/src/ansi/codes.ts +157 -0
- package/src/ansi/colors.ts +80 -0
- package/src/ansi/index.ts +21 -0
- package/src/ansi/manip.ts +113 -0
- package/src/diff-component.ts +139 -0
- package/src/index.ts +15 -301
- package/src/render/index.ts +21 -0
- package/src/render/shared.ts +130 -0
- package/src/render/split.ts +192 -0
- package/src/render/unified.ts +159 -0
- package/src/shiki.ts +1 -1
- package/src/tools/edit.ts +170 -0
- package/src/tools/index.ts +4 -0
- package/src/tools/write.ts +148 -0
- package/src/word-diff.ts +1 -1
- package/src/ansi.ts +0 -282
- package/src/render.ts +0 -393
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** Unified diff view rendering. */
|
|
2
|
+
|
|
3
|
+
import type { BundledLanguage } from "shiki";
|
|
4
|
+
import * as Ansi from "../ansi/index.js";
|
|
5
|
+
import type { DiffBg, DiffColors } from "../ansi/index.js";
|
|
6
|
+
import { DEFAULT_DIFF_COLORS, DEFAULT_DIFF_BG } from "../ansi/index.js";
|
|
7
|
+
import { hlBlock } from "../shiki.js";
|
|
8
|
+
import { wordDiffAnalysis, injectBg, plainWordDiff } from "../word-diff.js";
|
|
9
|
+
import type { DiffLine, ParsedDiff } from "../core/diff.js";
|
|
10
|
+
import {
|
|
11
|
+
MAX_HL_CHARS,
|
|
12
|
+
MAX_RENDER_LINES,
|
|
13
|
+
WORD_DIFF_MIN_SIM,
|
|
14
|
+
DEFAULT_TERM_WIDTH,
|
|
15
|
+
adaptiveWrapRows,
|
|
16
|
+
wrapAnsi,
|
|
17
|
+
} from "./shared.js";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Unified view
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Backward-compatible wrapper — reads width from stdout, returns joined string.
|
|
25
|
+
* Prefer `renderUnifiedLines` for Component-based rendering.
|
|
26
|
+
*/
|
|
27
|
+
export async function renderUnified(
|
|
28
|
+
diff: ParsedDiff,
|
|
29
|
+
language: BundledLanguage | undefined,
|
|
30
|
+
max = MAX_RENDER_LINES,
|
|
31
|
+
dc: DiffColors = DEFAULT_DIFF_COLORS,
|
|
32
|
+
): Promise<string> {
|
|
33
|
+
const width = process.stdout.columns ?? DEFAULT_TERM_WIDTH;
|
|
34
|
+
// Build DiffBg from current (possibly theme-derived) global aliases.
|
|
35
|
+
const dbg: DiffBg = {
|
|
36
|
+
bgAdd: Ansi.BG_ADD,
|
|
37
|
+
bgDel: Ansi.BG_DEL,
|
|
38
|
+
bgAddW: Ansi.BG_ADD_W,
|
|
39
|
+
bgDelW: Ansi.BG_DEL_W,
|
|
40
|
+
bgGutterAdd: Ansi.BG_GUTTER_ADD,
|
|
41
|
+
bgGutterDel: Ansi.BG_GUTTER_DEL,
|
|
42
|
+
bgEmpty: Ansi.BG_EMPTY,
|
|
43
|
+
bgBase: Ansi.BG_BASE,
|
|
44
|
+
rst: Ansi.BG_BASE === "\x1b[49m" ? "\x1b[0m" : `\x1b[0m${Ansi.BG_BASE}`,
|
|
45
|
+
divider: Ansi.DIVIDER,
|
|
46
|
+
};
|
|
47
|
+
const lines = await renderUnifiedLines(diff, language, width, max, dc, dbg);
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function renderUnifiedLines(
|
|
52
|
+
diff: ParsedDiff,
|
|
53
|
+
language: BundledLanguage | undefined,
|
|
54
|
+
width: number,
|
|
55
|
+
max = MAX_RENDER_LINES,
|
|
56
|
+
dc: DiffColors = DEFAULT_DIFF_COLORS,
|
|
57
|
+
dbg: DiffBg = DEFAULT_DIFF_BG,
|
|
58
|
+
): Promise<string[]> {
|
|
59
|
+
if (!diff.lines.length) return [];
|
|
60
|
+
|
|
61
|
+
const vis = diff.lines.slice(0, max);
|
|
62
|
+
const nw = Math.max(2, String(Math.max(...vis.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length);
|
|
63
|
+
const gw = nw + 5;
|
|
64
|
+
const cw = Math.max(20, width - gw);
|
|
65
|
+
const canHL = diff.chars <= MAX_HL_CHARS && vis.length <= MAX_RENDER_LINES;
|
|
66
|
+
const rst = dbg.rst;
|
|
67
|
+
const bgBase = dbg.bgBase;
|
|
68
|
+
|
|
69
|
+
const oldSrc: string[] = [], newSrc: string[] = [];
|
|
70
|
+
for (const l of vis) {
|
|
71
|
+
if (l.type === "ctx" || l.type === "del") oldSrc.push(l.content);
|
|
72
|
+
if (l.type === "ctx" || l.type === "add") newSrc.push(l.content);
|
|
73
|
+
}
|
|
74
|
+
const [oldHL, newHL] = canHL
|
|
75
|
+
? await Promise.all([hlBlock(oldSrc.join("\n"), language), hlBlock(newSrc.join("\n"), language)])
|
|
76
|
+
: [oldSrc, newSrc];
|
|
77
|
+
|
|
78
|
+
let oI = 0, nI = 0, idx = 0;
|
|
79
|
+
const out: string[] = [];
|
|
80
|
+
out.push(Ansi.rule(dbg, width));
|
|
81
|
+
|
|
82
|
+
function emitRow(
|
|
83
|
+
num: number | null, sign: string, gutterBg: string, signFg: string, body: string, bodyBg = "",
|
|
84
|
+
): void {
|
|
85
|
+
const borderFg = sign === "-" ? dc.fgDel : sign === "+" ? dc.fgAdd : "";
|
|
86
|
+
const border = borderFg ? `${borderFg}${Ansi.BORDER_BAR}${rst}` : `${bgBase} `;
|
|
87
|
+
const numFg = borderFg || Ansi.FG_LNUM;
|
|
88
|
+
const gutter = `${border}${gutterBg}${Ansi.lnum(num, nw, numFg)}${signFg}${sign}${rst} ${dbg.divider} `;
|
|
89
|
+
const contGutter = `${border}${gutterBg}${" ".repeat(nw + 1)}${rst} ${dbg.divider} `;
|
|
90
|
+
const rows = wrapAnsi(Ansi.tabs(body), cw, adaptiveWrapRows(cw), bodyBg, rst);
|
|
91
|
+
out.push(`${gutter}${rows[0]}${rst}`);
|
|
92
|
+
for (let r = 1; r < rows.length; r++) out.push(`${contGutter}${rows[r]}${rst}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
while (idx < vis.length) {
|
|
96
|
+
const l = vis[idx]!;
|
|
97
|
+
|
|
98
|
+
if (l.type === "sep") {
|
|
99
|
+
const gap = l.newNum;
|
|
100
|
+
const label = gap && gap > 0 ? ` ${gap} unmodified lines ` : "···";
|
|
101
|
+
const totalW = Math.min(width, 72);
|
|
102
|
+
const pad = Math.max(0, totalW - label.length - 2);
|
|
103
|
+
const half1 = Math.floor(pad / 2), half2 = pad - half1;
|
|
104
|
+
out.push(`${bgBase}${Ansi.FG_DIM}${"─".repeat(half1)}${label}${"─".repeat(half2)}${rst}`);
|
|
105
|
+
idx++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (l.type === "ctx") {
|
|
110
|
+
const hl = oldHL[oI] ?? l.content;
|
|
111
|
+
emitRow(l.newNum, " ", bgBase, dc.fgCtx, `${bgBase}${Ansi.DIM}${hl}`, bgBase);
|
|
112
|
+
oI++; nI++; idx++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const dels: Array<{ l: DiffLine; hl: string }> = [];
|
|
117
|
+
while (idx < vis.length && vis[idx]!.type === "del") {
|
|
118
|
+
dels.push({ l: vis[idx]!, hl: oldHL[oI] ?? vis[idx]!.content });
|
|
119
|
+
oI++; idx++;
|
|
120
|
+
}
|
|
121
|
+
const adds: Array<{ l: DiffLine; hl: string }> = [];
|
|
122
|
+
while (idx < vis.length && vis[idx]!.type === "add") {
|
|
123
|
+
adds.push({ l: vis[idx]!, hl: newHL[nI] ?? vis[idx]!.content });
|
|
124
|
+
nI++; idx++;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const isPaired = dels.length === 1 && adds.length === 1;
|
|
128
|
+
const wd = isPaired ? wordDiffAnalysis(dels[0]!.l.content, adds[0]!.l.content) : null;
|
|
129
|
+
|
|
130
|
+
if (isPaired && wd && wd.similarity >= WORD_DIFF_MIN_SIM && canHL) {
|
|
131
|
+
const delBody = injectBg(dels[0]!.hl, wd.oldRanges, dbg.bgDel, dbg.bgDelW);
|
|
132
|
+
const addBody = injectBg(adds[0]!.hl, wd.newRanges, dbg.bgAdd, dbg.bgAddW);
|
|
133
|
+
emitRow(dels[0]!.l.oldNum, "-", dbg.bgGutterDel, `${dc.fgDel}${Ansi.BOLD}`, delBody, dbg.bgDel);
|
|
134
|
+
emitRow(adds[0]!.l.newNum, "+", dbg.bgGutterAdd, `${dc.fgAdd}${Ansi.BOLD}`, addBody, dbg.bgAdd);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (isPaired && wd && wd.similarity >= WORD_DIFF_MIN_SIM && !canHL) {
|
|
138
|
+
const pwd = plainWordDiff(dels[0]!.l.content, adds[0]!.l.content);
|
|
139
|
+
emitRow(dels[0]!.l.oldNum, "-", dbg.bgGutterDel, `${dc.fgDel}${Ansi.BOLD}`, `${dbg.bgDel}${pwd.old}`, dbg.bgDel);
|
|
140
|
+
emitRow(adds[0]!.l.newNum, "+", dbg.bgGutterAdd, `${dc.fgAdd}${Ansi.BOLD}`, `${dbg.bgAdd}${pwd.new}`, dbg.bgAdd);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const d of dels) {
|
|
145
|
+
const body = canHL ? `${dbg.bgDel}${d.hl}` : `${dbg.bgDel}${d.l.content}`;
|
|
146
|
+
emitRow(d.l.oldNum, "-", dbg.bgGutterDel, `${dc.fgDel}${Ansi.BOLD}`, body, dbg.bgDel);
|
|
147
|
+
}
|
|
148
|
+
for (const a of adds) {
|
|
149
|
+
const body = canHL ? `${dbg.bgAdd}${a.hl}` : `${dbg.bgAdd}${a.l.content}`;
|
|
150
|
+
emitRow(a.l.newNum, "+", dbg.bgGutterAdd, `${dc.fgAdd}${Ansi.BOLD}`, body, dbg.bgAdd);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
out.push(Ansi.rule(dbg, width));
|
|
155
|
+
if (diff.lines.length > vis.length) {
|
|
156
|
+
out.push(`${bgBase}${Ansi.FG_DIM} … ${diff.lines.length - vis.length} more lines${rst}`);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
package/src/shiki.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { extname } from "node:path";
|
|
|
4
4
|
import { codeToANSI } from "@shikijs/cli";
|
|
5
5
|
import type { BundledLanguage, BundledTheme } from "shiki";
|
|
6
6
|
|
|
7
|
-
import { normalizeShikiContrast } from "./ansi.js";
|
|
7
|
+
import { normalizeShikiContrast } from "./ansi/index.js";
|
|
8
8
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// Config access
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/** Edit tool override with diff rendering. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { Box, Spacer, Text } from "@earendil-works/pi-tui";
|
|
6
|
+
import { parseDiff } from "../core/diff.js";
|
|
7
|
+
import * as Ansi from "../ansi/index.js";
|
|
8
|
+
import { themeCacheKey } from "../ansi/index.js";
|
|
9
|
+
import { lang } from "../shiki.js";
|
|
10
|
+
import { MAX_PREVIEW_LINES } from "../render/index.js";
|
|
11
|
+
import { DiffComponent } from "../diff-component.js";
|
|
12
|
+
|
|
13
|
+
/** Register the edit tool override. */
|
|
14
|
+
export function registerEditTool(
|
|
15
|
+
pi: ExtensionAPI,
|
|
16
|
+
cwd: string,
|
|
17
|
+
home: string,
|
|
18
|
+
createEditTool: (cwd: string) => any,
|
|
19
|
+
): void {
|
|
20
|
+
const sp = (p: string) => Ansi.shortPath(cwd, home, p);
|
|
21
|
+
const origEdit = createEditTool(cwd);
|
|
22
|
+
|
|
23
|
+
function getEditOperations(input: any): Array<{ oldText: string; newText: string }> {
|
|
24
|
+
if (Array.isArray(input?.edits)) {
|
|
25
|
+
return input.edits
|
|
26
|
+
.map((edit: any) => ({
|
|
27
|
+
oldText: typeof edit?.oldText === "string" ? edit.oldText : typeof edit?.old_text === "string" ? edit.old_text : "",
|
|
28
|
+
newText: typeof edit?.newText === "string" ? edit.newText : typeof edit?.new_text === "string" ? edit.new_text : "",
|
|
29
|
+
}))
|
|
30
|
+
.filter((edit: { oldText: string; newText: string }) => edit.oldText && edit.oldText !== edit.newText);
|
|
31
|
+
}
|
|
32
|
+
const oldText = typeof input?.oldText === "string" ? input.oldText : typeof input?.old_text === "string" ? input.old_text : "";
|
|
33
|
+
const newText = typeof input?.newText === "string" ? input.newText : typeof input?.new_text === "string" ? input.new_text : "";
|
|
34
|
+
return oldText && oldText !== newText ? [{ oldText, newText }] : [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function summarizeEditOperations(operations: Array<{ oldText: string; newText: string }>) {
|
|
38
|
+
const diffs = operations.map((edit) => parseDiff(edit.oldText, edit.newText));
|
|
39
|
+
const totalAdded = diffs.reduce((sum, diff) => sum + diff.added, 0);
|
|
40
|
+
const totalRemoved = diffs.reduce((sum, diff) => sum + diff.removed, 0);
|
|
41
|
+
return { diffs, totalAdded, totalRemoved, summary: Ansi.summarize(totalAdded, totalRemoved) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pi.registerTool({
|
|
45
|
+
...origEdit,
|
|
46
|
+
name: "edit",
|
|
47
|
+
renderShell: "self",
|
|
48
|
+
|
|
49
|
+
async execute(tid: string, params: any, sig: any, upd: any, ctx: any) {
|
|
50
|
+
const fp = params.path ?? params.file_path ?? "";
|
|
51
|
+
const operations = getEditOperations(params);
|
|
52
|
+
const result = await origEdit.execute(tid, params, sig, upd, ctx);
|
|
53
|
+
|
|
54
|
+
if (operations.length === 0) return result;
|
|
55
|
+
|
|
56
|
+
const { diffs, summary } = summarizeEditOperations(operations);
|
|
57
|
+
if (operations.length === 1) {
|
|
58
|
+
let editLine = 0;
|
|
59
|
+
try {
|
|
60
|
+
if (fp && existsSync(fp)) {
|
|
61
|
+
const f = readFileSync(fp, "utf-8");
|
|
62
|
+
const idx = f.indexOf(operations[0]!.newText);
|
|
63
|
+
if (idx >= 0) editLine = f.slice(0, idx).split("\n").length;
|
|
64
|
+
}
|
|
65
|
+
} catch { editLine = 0; }
|
|
66
|
+
(result as any).details = { _type: "editInfo", summary, editLine };
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
(result as any).details = {
|
|
71
|
+
_type: "multiEditInfo",
|
|
72
|
+
summary,
|
|
73
|
+
editCount: operations.length,
|
|
74
|
+
diffLineCount: diffs.reduce((sum, diff) => sum + diff.lines.length, 0),
|
|
75
|
+
};
|
|
76
|
+
return result;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
renderCall(args: any, theme: any, ctx: any) {
|
|
80
|
+
const fp = args?.path ?? args?.file_path ?? "";
|
|
81
|
+
const operations = getEditOperations(args);
|
|
82
|
+
|
|
83
|
+
// Box with padding + background — matches built-in edit tool pattern
|
|
84
|
+
const box = ctx.state.callBox as Box | undefined;
|
|
85
|
+
if (box) {
|
|
86
|
+
box.setBgFn((s: string) => theme.bg("toolPendingBg", s));
|
|
87
|
+
}
|
|
88
|
+
const shell = box ?? new Box(1, 1, (s: string) => theme.bg("toolPendingBg", s));
|
|
89
|
+
if (!box) ctx.state.callBox = shell;
|
|
90
|
+
|
|
91
|
+
const hdr = `${theme.fg("toolTitle", theme.bold("edit"))} ${theme.fg("accent", sp(fp))}`;
|
|
92
|
+
|
|
93
|
+
if (!(ctx.argsComplete && operations.length > 0)) {
|
|
94
|
+
shell.clear();
|
|
95
|
+
shell.addChild(new Text(hdr, 0, 0));
|
|
96
|
+
return shell;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const pk = JSON.stringify({ fp, operations, theme: themeCacheKey(theme) });
|
|
100
|
+
if (ctx.state._pk === pk && shell.children.length > 1) {
|
|
101
|
+
return shell;
|
|
102
|
+
}
|
|
103
|
+
ctx.state._pk = pk;
|
|
104
|
+
|
|
105
|
+
shell.clear();
|
|
106
|
+
shell.addChild(new Text(hdr, 0, 0));
|
|
107
|
+
|
|
108
|
+
const lg = lang(fp);
|
|
109
|
+
const { diffs } = summarizeEditOperations(operations);
|
|
110
|
+
|
|
111
|
+
if (operations.length === 1) {
|
|
112
|
+
const diff = diffs[0];
|
|
113
|
+
if (diff) {
|
|
114
|
+
shell.addChild(new Spacer(1));
|
|
115
|
+
shell.addChild(new DiffComponent(diff, lg, theme, MAX_PREVIEW_LINES));
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
const maxShown = Math.min(operations.length, 3);
|
|
119
|
+
const previewLines = Math.max(8, Math.floor(MAX_PREVIEW_LINES / maxShown));
|
|
120
|
+
for (let i = 0; i < maxShown && i < diffs.length; i++) {
|
|
121
|
+
const diff = diffs[i];
|
|
122
|
+
if (!diff) continue;
|
|
123
|
+
if (i > 0) shell.addChild(new Spacer(1));
|
|
124
|
+
shell.addChild(new Text(
|
|
125
|
+
theme.fg("muted", `Edit ${i + 1}/${operations.length}`),
|
|
126
|
+
0, 0,
|
|
127
|
+
));
|
|
128
|
+
shell.addChild(new DiffComponent(diff, lg, theme, previewLines));
|
|
129
|
+
}
|
|
130
|
+
const remainder = operations.length - maxShown;
|
|
131
|
+
if (remainder > 0) {
|
|
132
|
+
shell.addChild(new Spacer(1));
|
|
133
|
+
shell.addChild(new Text(
|
|
134
|
+
` ${theme.fg("dim", `… ${remainder} more edit blocks`)}`,
|
|
135
|
+
0, 0,
|
|
136
|
+
));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return shell;
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
renderResult(result: any, _opt: any, theme: any, ctx: any) {
|
|
144
|
+
// Update the call Box background to reflect final state.
|
|
145
|
+
const box = ctx.state.callBox as Box | undefined;
|
|
146
|
+
if (box) {
|
|
147
|
+
const bgKey = ctx.isError ? "toolErrorBg" : "toolSuccessBg";
|
|
148
|
+
box.setBgFn((s: string) => theme.bg(bgKey, s));
|
|
149
|
+
}
|
|
150
|
+
// Show summary below the diff
|
|
151
|
+
if (ctx.isError) {
|
|
152
|
+
const e = result.content
|
|
153
|
+
?.filter((c: any) => c.type === "text")
|
|
154
|
+
.map((c: any) => c.text || "")
|
|
155
|
+
.join("\n") ?? "Error";
|
|
156
|
+
return new Text(`\n${theme.fg("error", e)}`, 1, 0);
|
|
157
|
+
}
|
|
158
|
+
if (result.details?._type === "editInfo") {
|
|
159
|
+
const { summary: s, editLine } = result.details;
|
|
160
|
+
const loc = editLine > 0 ? ` ${theme.fg("muted", `at line ${editLine}`)}` : "";
|
|
161
|
+
return new Text(` ${s}${loc}`, 1, 0);
|
|
162
|
+
}
|
|
163
|
+
if (result.details?._type === "multiEditInfo") {
|
|
164
|
+
const { summary: s, editCount, diffLineCount } = result.details;
|
|
165
|
+
return new Text(` ${editCount} edits ${s}${typeof diffLineCount === "number" ? ` ${theme.fg("muted", `(${diffLineCount} diff lines)`)}` : ""}`, 1, 0);
|
|
166
|
+
}
|
|
167
|
+
return new Text("", 0, 0);
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** Write tool override with diff rendering. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
6
|
+
import { parseDiff } from "../core/diff.js";
|
|
7
|
+
import * as Ansi from "../ansi/index.js";
|
|
8
|
+
import { themeCacheKey } from "../ansi/index.js";
|
|
9
|
+
import { lang, hlBlock } from "../shiki.js";
|
|
10
|
+
import { DiffComponent } from "../diff-component.js";
|
|
11
|
+
import { MAX_RENDER_LINES } from "../render/index.js";
|
|
12
|
+
|
|
13
|
+
/** Register the write tool override. */
|
|
14
|
+
export function registerWriteTool(
|
|
15
|
+
pi: ExtensionAPI,
|
|
16
|
+
cwd: string,
|
|
17
|
+
home: string,
|
|
18
|
+
createWriteTool: (cwd: string) => any,
|
|
19
|
+
TextComponent: new (text?: string, paddingX?: number, paddingY?: number) => Component,
|
|
20
|
+
): void {
|
|
21
|
+
const sp = (p: string) => Ansi.shortPath(cwd, home, p);
|
|
22
|
+
const origWrite = createWriteTool(cwd);
|
|
23
|
+
|
|
24
|
+
pi.registerTool({
|
|
25
|
+
...origWrite,
|
|
26
|
+
name: "write",
|
|
27
|
+
|
|
28
|
+
async execute(tid: string, params: any, sig: any, upd: any, ctx: any) {
|
|
29
|
+
const fp = params.path ?? params.file_path ?? "";
|
|
30
|
+
let old: string | null = null;
|
|
31
|
+
try {
|
|
32
|
+
if (fp && existsSync(fp)) old = readFileSync(fp, "utf-8");
|
|
33
|
+
} catch {
|
|
34
|
+
old = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await origWrite.execute(tid, params, sig, upd, ctx);
|
|
38
|
+
const content = params.content ?? "";
|
|
39
|
+
|
|
40
|
+
if (old !== null && old !== content) {
|
|
41
|
+
const diff = parseDiff(old, content);
|
|
42
|
+
const lg = lang(fp);
|
|
43
|
+
(result as any).details = {
|
|
44
|
+
_type: "diff",
|
|
45
|
+
summary: Ansi.summarize(diff.added, diff.removed),
|
|
46
|
+
diff,
|
|
47
|
+
language: lg,
|
|
48
|
+
};
|
|
49
|
+
} else if (old === null) {
|
|
50
|
+
const lineCount = content ? content.split("\n").length : 0;
|
|
51
|
+
(result as any).details = { _type: "new", lines: lineCount, content, filePath: fp };
|
|
52
|
+
} else if (old === content) {
|
|
53
|
+
(result as any).details = { _type: "noChange" };
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
renderCall(args: any, theme: any, ctx: any) {
|
|
59
|
+
const fp = args?.path ?? args?.file_path ?? "";
|
|
60
|
+
const isNew = !fp || !existsSync(fp);
|
|
61
|
+
const label = isNew ? "create" : "write";
|
|
62
|
+
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
63
|
+
const hdr = `${theme.fg("toolTitle", theme.bold(label))} ${theme.fg("accent", sp(fp))}`;
|
|
64
|
+
|
|
65
|
+
if (args?.content && !ctx.argsComplete) {
|
|
66
|
+
const n = String(args.content).split("\n").length;
|
|
67
|
+
text.setText(`${hdr} ${theme.fg("muted", `(${n} lines…)`)}`);
|
|
68
|
+
return text;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (args?.content && ctx.argsComplete && isNew) {
|
|
72
|
+
const previewKey = `create:${themeCacheKey(theme)}:${fp}:${String(args.content).length}`;
|
|
73
|
+
if (ctx.state._previewKey !== previewKey) {
|
|
74
|
+
ctx.state._previewKey = previewKey;
|
|
75
|
+
ctx.state._previewText = hdr;
|
|
76
|
+
const lg = lang(fp);
|
|
77
|
+
hlBlock(args.content, lg)
|
|
78
|
+
.then((lines: string[]) => {
|
|
79
|
+
if (ctx.state._previewKey !== previewKey) return;
|
|
80
|
+
const maxShow = ctx.expanded ? lines.length : 16;
|
|
81
|
+
const preview = lines.slice(0, maxShow).join("\n");
|
|
82
|
+
const rem = lines.length - maxShow;
|
|
83
|
+
let out = `${hdr}\n\n${preview}`;
|
|
84
|
+
if (rem > 0) out += `\n${theme.fg("muted", `… (${rem} more lines, ${lines.length} total)`)}`;
|
|
85
|
+
ctx.state._previewText = out;
|
|
86
|
+
ctx.invalidate();
|
|
87
|
+
})
|
|
88
|
+
.catch(() => {});
|
|
89
|
+
}
|
|
90
|
+
text.setText(ctx.state._previewText ?? hdr);
|
|
91
|
+
return text;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
text.setText(hdr);
|
|
95
|
+
return text;
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
renderResult(result: any, _opt: any, theme: any, ctx: any) {
|
|
99
|
+
const text = ctx.lastComponent ?? new TextComponent("", 0, 0);
|
|
100
|
+
if (ctx.isError) {
|
|
101
|
+
const e = result.content
|
|
102
|
+
?.filter((c: any) => c.type === "text")
|
|
103
|
+
.map((c: any) => c.text || "")
|
|
104
|
+
.join("\n") ?? "Error";
|
|
105
|
+
text.setText(`\n${theme.fg("error", e)}`);
|
|
106
|
+
return text;
|
|
107
|
+
}
|
|
108
|
+
const d = result.details;
|
|
109
|
+
if (d?._type === "diff") {
|
|
110
|
+
const comp = ctx.lastComponent instanceof DiffComponent
|
|
111
|
+
? ctx.lastComponent
|
|
112
|
+
: new DiffComponent(d.diff, d.language, theme, MAX_RENDER_LINES);
|
|
113
|
+
return comp;
|
|
114
|
+
}
|
|
115
|
+
if (d?._type === "noChange") {
|
|
116
|
+
text.setText(` ${theme.fg("muted", "✓ no changes")}`);
|
|
117
|
+
return text;
|
|
118
|
+
}
|
|
119
|
+
if (d?._type === "new") {
|
|
120
|
+
const { lines: lineCount, content: rawContent, filePath: fp } = d;
|
|
121
|
+
const pk = `nf:${themeCacheKey(theme)}:${fp}:${lineCount}`;
|
|
122
|
+
if (ctx.state._nfk !== pk) {
|
|
123
|
+
ctx.state._nfk = pk;
|
|
124
|
+
ctx.state._nft = ` ${theme.fg("success", `✓ new file (${lineCount} lines)`)}`;
|
|
125
|
+
const lg = lang(fp);
|
|
126
|
+
if (rawContent) {
|
|
127
|
+
hlBlock(rawContent, lg)
|
|
128
|
+
.then((hlLines: string[]) => {
|
|
129
|
+
if (ctx.state._nfk !== pk) return;
|
|
130
|
+
const maxShow = ctx.expanded ? hlLines.length : 12;
|
|
131
|
+
const preview = hlLines.slice(0, maxShow).join("\n");
|
|
132
|
+
const rem = hlLines.length - maxShow;
|
|
133
|
+
let out = ` ${theme.fg("success", `✓ new file (${lineCount} lines)`)}` + `\n${preview}`;
|
|
134
|
+
if (rem > 0) out += `\n${theme.fg("muted", ` … ${rem} more lines`)}`;
|
|
135
|
+
ctx.state._nft = out;
|
|
136
|
+
ctx.invalidate();
|
|
137
|
+
})
|
|
138
|
+
.catch(() => {});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
text.setText(ctx.state._nft ?? ` ${theme.fg("success", `✓ new file (${lineCount} lines)`)}`);
|
|
142
|
+
return text;
|
|
143
|
+
}
|
|
144
|
+
text.setText(` ${theme.fg("dim", String(result?.content?.[0]?.text ?? "written").slice(0, 120))}`);
|
|
145
|
+
return text;
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
package/src/word-diff.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Word diff analysis and background injection. */
|
|
2
2
|
|
|
3
3
|
import * as Diff from "diff";
|
|
4
|
-
import * as Ansi from "./ansi.js";
|
|
4
|
+
import * as Ansi from "./ansi/index.js";
|
|
5
5
|
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
// Word diff analysis
|