agent-sh 0.9.0 → 0.10.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 +14 -21
- package/dist/agent/agent-loop.d.ts +43 -3
- package/dist/agent/agent-loop.js +811 -128
- package/dist/agent/conversation-state.d.ts +72 -21
- package/dist/agent/conversation-state.js +357 -150
- package/dist/agent/history-file.d.ts +13 -4
- package/dist/agent/history-file.js +110 -36
- package/dist/agent/nuclear-form.d.ts +28 -3
- package/dist/agent/nuclear-form.js +84 -3
- package/dist/agent/skills.d.ts +2 -4
- package/dist/agent/skills.js +10 -4
- package/dist/agent/subagent.d.ts +23 -0
- package/dist/agent/subagent.js +53 -11
- package/dist/agent/system-prompt.d.ts +34 -1
- package/dist/agent/system-prompt.js +96 -47
- package/dist/agent/token-budget.d.ts +5 -4
- package/dist/agent/token-budget.js +14 -19
- package/dist/agent/tool-protocol.d.ts +23 -1
- package/dist/agent/tool-protocol.js +169 -4
- package/dist/agent/tools/bash.js +3 -3
- package/dist/agent/tools/edit-file.js +9 -6
- package/dist/agent/tools/glob.js +4 -2
- package/dist/agent/tools/grep.js +27 -3
- package/dist/agent/tools/ls.js +5 -6
- package/dist/agent/types.d.ts +1 -1
- package/dist/context-manager.d.ts +17 -0
- package/dist/context-manager.js +37 -4
- package/dist/core.js +27 -6
- package/dist/event-bus.d.ts +59 -2
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.js +50 -13
- package/dist/extensions/agent-backend.d.ts +8 -7
- package/dist/extensions/agent-backend.js +69 -48
- package/dist/extensions/index.js +0 -1
- package/dist/extensions/slash-commands.js +14 -9
- package/dist/extensions/tui-renderer.js +62 -78
- package/dist/index.js +25 -6
- package/dist/settings.d.ts +36 -5
- package/dist/settings.js +53 -9
- package/dist/shell/input-handler.d.ts +2 -1
- package/dist/shell/input-handler.js +82 -73
- package/dist/shell/shell.js +19 -2
- package/dist/types.d.ts +12 -0
- package/dist/utils/ansi.d.ts +5 -0
- package/dist/utils/ansi.js +1 -1
- package/dist/utils/compositor.d.ts +5 -0
- package/dist/utils/compositor.js +31 -3
- package/dist/utils/diff-renderer.d.ts +9 -0
- package/dist/utils/diff-renderer.js +221 -143
- package/dist/utils/diff.d.ts +21 -2
- package/dist/utils/diff.js +165 -89
- package/dist/utils/handler-registry.d.ts +5 -0
- package/dist/utils/handler-registry.js +6 -0
- package/dist/utils/line-editor.d.ts +11 -1
- package/dist/utils/line-editor.js +44 -5
- package/dist/utils/tool-display.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -4
- package/examples/extensions/ash-acp-bridge/src/index.ts +4 -1
- package/examples/extensions/ash-mcp-bridge/index.ts +13 -3
- package/examples/extensions/claude-code-bridge/index.ts +198 -51
- package/examples/extensions/claude-code-bridge/package.json +1 -0
- package/examples/extensions/interactive-prompts.ts +39 -25
- package/examples/extensions/overlay-agent.ts +3 -3
- package/examples/extensions/peer-mesh.ts +115 -0
- package/examples/extensions/pi-bridge/index.ts +2 -2
- package/examples/extensions/questionnaire.ts +16 -5
- package/examples/extensions/subagents.ts +19 -4
- package/examples/extensions/terminal-buffer.ts +163 -0
- package/examples/extensions/user-shell.ts +136 -0
- package/examples/extensions/web-access.ts +8 -0
- package/package.json +36 -2
- package/dist/agent/tools/display.d.ts +0 -13
- package/dist/agent/tools/display.js +0 -70
- package/dist/agent/tools/user-shell.d.ts +0 -13
- package/dist/agent/tools/user-shell.js +0 -87
- package/dist/extensions/terminal-buffer.d.ts +0 -14
- package/dist/extensions/terminal-buffer.js +0 -134
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Uses token-level LCS for word-level inline diff highlighting.
|
|
7
7
|
*/
|
|
8
8
|
import { highlight } from "cli-highlight";
|
|
9
|
-
import { visibleLen } from "./ansi.js";
|
|
9
|
+
import { visibleLen, charWidth } from "./ansi.js";
|
|
10
10
|
import { palette as p } from "./palette.js";
|
|
11
11
|
// ── Constants ────────────────────────────────────────────────────
|
|
12
12
|
const SPLIT_MIN_WIDTH = 120;
|
|
@@ -223,14 +223,8 @@ function renderSummary(diff) {
|
|
|
223
223
|
return [`${p.success}+${diff.added} lines${p.reset} ${p.dim}(new file)${p.reset}`];
|
|
224
224
|
return [`${p.success}+${diff.added}${p.reset} ${p.error}-${diff.removed}${p.reset}`];
|
|
225
225
|
}
|
|
226
|
-
|
|
227
|
-
function renderUnified(diff, opts) {
|
|
228
|
-
const useTrueColor = opts.trueColor !== false;
|
|
229
|
-
const useSyntax = opts.syntaxHighlight !== false;
|
|
230
|
-
const lang = useSyntax ? detectLanguage(opts.filePath) : undefined;
|
|
226
|
+
function unifiedLayout(diff, opts) {
|
|
231
227
|
const textWidth = opts.width;
|
|
232
|
-
const output = [];
|
|
233
|
-
// Compute max line number width across all hunks
|
|
234
228
|
let maxNo = 0;
|
|
235
229
|
for (const hunk of diff.hunks) {
|
|
236
230
|
for (const line of hunk.lines) {
|
|
@@ -240,88 +234,90 @@ function renderUnified(diff, opts) {
|
|
|
240
234
|
}
|
|
241
235
|
}
|
|
242
236
|
const noW = Math.max(String(maxNo).length, 1);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
237
|
+
return {
|
|
238
|
+
noW,
|
|
239
|
+
lineTextW: Math.max(1, textWidth - noW - 5),
|
|
240
|
+
textWidth,
|
|
241
|
+
useTrueColor: opts.trueColor !== false,
|
|
242
|
+
lang: opts.syntaxHighlight !== false ? detectLanguage(opts.filePath) : undefined,
|
|
243
|
+
removedPalette: { rowBg: p.errorBg, emphBg: p.errorBgEmph },
|
|
244
|
+
addedPalette: { rowBg: p.successBg, emphBg: p.successBgEmph },
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function renderUnifiedHunk(hunk, layout) {
|
|
248
|
+
const { noW, lineTextW, textWidth, useTrueColor, lang, removedPalette, addedPalette } = layout;
|
|
249
|
+
const out = [];
|
|
250
|
+
const pairs = findChangePairs(hunk);
|
|
251
|
+
const renderedAsPartOfPair = new Set();
|
|
252
|
+
for (let i = 0; i < hunk.lines.length; i++) {
|
|
253
|
+
const line = hunk.lines[i];
|
|
254
|
+
const no = String(line.type === "removed" ? (line.oldNo ?? "") : (line.newNo ?? line.oldNo ?? "")).padStart(noW);
|
|
255
|
+
if (line.type === "context") {
|
|
256
|
+
const raw = truncateText(line.text, lineTextW);
|
|
257
|
+
const text = lang ? highlightLine(raw, lang) : raw;
|
|
258
|
+
out.push(` ${p.dim}${no} │${p.reset} ${p.dim}${text}${p.reset}`);
|
|
259
|
+
continue;
|
|
252
260
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (
|
|
261
|
+
if (line.type === "removed") {
|
|
262
|
+
const pair = pairs.get(i);
|
|
263
|
+
let removedText;
|
|
264
|
+
let addedText = null;
|
|
265
|
+
let addedNo = null;
|
|
266
|
+
if (pair && pair.removedIdx === i) {
|
|
267
|
+
const highlighted = highlightInlineChanges(line.text, pair.added.text, removedPalette, addedPalette, useTrueColor, lang);
|
|
268
|
+
removedText = truncateText(highlighted.old, lineTextW);
|
|
269
|
+
addedText = truncateText(highlighted.new, lineTextW);
|
|
270
|
+
addedNo = String(pair.added.newNo ?? "").padStart(noW);
|
|
271
|
+
renderedAsPartOfPair.add(pair.addedIdx);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
259
274
|
const raw = truncateText(line.text, lineTextW);
|
|
260
|
-
|
|
261
|
-
output.push(` ${p.dim}${no} │${p.reset} ${p.dim}${text}${p.reset}`);
|
|
262
|
-
continue;
|
|
275
|
+
removedText = lang ? highlightLine(raw, lang) : raw;
|
|
263
276
|
}
|
|
264
|
-
if (
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
removedText = truncateText(highlighted.old, lineTextW);
|
|
272
|
-
addedText = truncateText(highlighted.new, lineTextW);
|
|
273
|
-
addedNo = String(pair.added.newNo ?? "").padStart(noW);
|
|
274
|
-
renderedAsPartOfPair.add(pair.addedIdx);
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
// Unpaired removed line — syntax highlight the whole line
|
|
278
|
-
const raw = truncateText(line.text, lineTextW);
|
|
279
|
-
removedText = lang ? highlightLine(raw, lang) : raw;
|
|
280
|
-
}
|
|
277
|
+
if (useTrueColor) {
|
|
278
|
+
out.push(padToWidth(`${p.errorBg}${p.error}- ${no} │ ${preserveBg(removedText, p.errorBg)}${p.reset}`, textWidth));
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
out.push(`${p.error}- ${no} │ ${removedText}${p.reset}`);
|
|
282
|
+
}
|
|
283
|
+
if (addedText !== null && addedNo !== null) {
|
|
281
284
|
if (useTrueColor) {
|
|
282
|
-
|
|
283
|
-
output.push(padToWidth(rowContent, textWidth));
|
|
285
|
+
out.push(padToWidth(`${p.successBg}${p.success}+ ${addedNo} │ ${preserveBg(addedText, p.successBg)}${p.reset}`, textWidth));
|
|
284
286
|
}
|
|
285
287
|
else {
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
if (addedText !== null && addedNo !== null) {
|
|
289
|
-
if (useTrueColor) {
|
|
290
|
-
const rowContent = `${p.successBg}${p.success}+ ${addedNo} │ ${preserveBg(addedText, p.successBg)}${p.reset}`;
|
|
291
|
-
output.push(padToWidth(rowContent, textWidth));
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
output.push(`${p.success}+ ${addedNo} │ ${addedText}${p.reset}`);
|
|
295
|
-
}
|
|
288
|
+
out.push(`${p.success}+ ${addedNo} │ ${addedText}${p.reset}`);
|
|
296
289
|
}
|
|
290
|
+
}
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (line.type === "added") {
|
|
294
|
+
if (renderedAsPartOfPair.has(i))
|
|
297
295
|
continue;
|
|
296
|
+
const raw = truncateText(line.text, lineTextW);
|
|
297
|
+
const text = lang ? highlightLine(raw, lang) : raw;
|
|
298
|
+
if (useTrueColor) {
|
|
299
|
+
out.push(padToWidth(`${p.successBg}${p.success}+ ${no} │ ${preserveBg(text, p.successBg)}${p.reset}`, textWidth));
|
|
298
300
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
continue;
|
|
302
|
-
const raw = truncateText(line.text, lineTextW);
|
|
303
|
-
const text = lang ? highlightLine(raw, lang) : raw;
|
|
304
|
-
if (useTrueColor) {
|
|
305
|
-
const rowContent = `${p.successBg}${p.success}+ ${no} │ ${preserveBg(text, p.successBg)}${p.reset}`;
|
|
306
|
-
output.push(padToWidth(rowContent, textWidth));
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
output.push(`${p.success}+ ${no} │ ${text}${p.reset}`);
|
|
310
|
-
}
|
|
301
|
+
else {
|
|
302
|
+
out.push(`${p.success}+ ${no} │ ${text}${p.reset}`);
|
|
311
303
|
}
|
|
312
304
|
}
|
|
313
305
|
}
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
function renderUnified(diff, opts) {
|
|
309
|
+
const layout = unifiedLayout(diff, opts);
|
|
310
|
+
const output = [];
|
|
311
|
+
for (let hunkIdx = 0; hunkIdx < diff.hunks.length; hunkIdx++) {
|
|
312
|
+
if (hunkIdx > 0)
|
|
313
|
+
output.push(` ${p.dim}⋯${p.reset}`);
|
|
314
|
+
output.push(...renderUnifiedHunk(diff.hunks[hunkIdx], layout));
|
|
315
|
+
}
|
|
314
316
|
return output;
|
|
315
317
|
}
|
|
316
|
-
|
|
317
|
-
function renderSplit(diff, opts) {
|
|
318
|
-
const useTrueColor = opts.trueColor !== false;
|
|
319
|
-
const useSyntax = opts.syntaxHighlight !== false;
|
|
320
|
-
const lang = useSyntax ? detectLanguage(opts.filePath) : undefined;
|
|
318
|
+
function splitLayout(diff, opts) {
|
|
321
319
|
const totalWidth = opts.width;
|
|
322
|
-
// 3 chars for " │ " separator
|
|
323
320
|
const colWidth = Math.max(1, Math.floor((totalWidth - 3) / 2));
|
|
324
|
-
// Compute max line number width
|
|
325
321
|
let maxNo = 0;
|
|
326
322
|
for (const hunk of diff.hunks) {
|
|
327
323
|
for (const line of hunk.lines) {
|
|
@@ -331,78 +327,87 @@ function renderSplit(diff, opts) {
|
|
|
331
327
|
}
|
|
332
328
|
}
|
|
333
329
|
const noW = Math.max(String(maxNo).length, 1);
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (!row.left || row.left.type === "context") {
|
|
377
|
-
leftCol = padToWidth(`${p.dim}${leftNo} │${p.reset} ${p.dim}${leftText}${p.reset}`, colWidth);
|
|
378
|
-
}
|
|
379
|
-
else if (row.left.type === "removed") {
|
|
380
|
-
if (useTrueColor) {
|
|
381
|
-
leftCol = padToWidth(`${p.errorBg}${p.error}${leftNo} │ ${preserveBg(leftText, p.errorBg)}${p.reset}`, colWidth);
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
leftCol = padToWidth(`${p.error}${leftNo} │ ${leftText}${p.reset}`, colWidth);
|
|
385
|
-
}
|
|
330
|
+
return {
|
|
331
|
+
colWidth,
|
|
332
|
+
noW,
|
|
333
|
+
textW: Math.max(1, colWidth - noW - 3),
|
|
334
|
+
useTrueColor: opts.trueColor !== false,
|
|
335
|
+
lang: opts.syntaxHighlight !== false ? detectLanguage(opts.filePath) : undefined,
|
|
336
|
+
removedPalette: { rowBg: p.errorBg, emphBg: p.errorBgEmph },
|
|
337
|
+
addedPalette: { rowBg: p.successBg, emphBg: p.successBgEmph },
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function renderSplitHunk(hunk, layout) {
|
|
341
|
+
const { colWidth, noW, textW, useTrueColor, lang, removedPalette, addedPalette } = layout;
|
|
342
|
+
const out = [];
|
|
343
|
+
const rows = buildSplitRows(hunk);
|
|
344
|
+
for (const row of rows) {
|
|
345
|
+
const leftNo = row.left
|
|
346
|
+
? String(row.left.oldNo ?? row.left.newNo ?? "").padStart(noW)
|
|
347
|
+
: " ".repeat(noW);
|
|
348
|
+
const rightNo = row.right
|
|
349
|
+
? String(row.right.newNo ?? row.right.oldNo ?? "").padStart(noW)
|
|
350
|
+
: " ".repeat(noW);
|
|
351
|
+
let leftText = row.left ? truncateText(row.left.text, textW) : "";
|
|
352
|
+
let rightText = row.right ? truncateText(row.right.text, textW) : "";
|
|
353
|
+
if (row.left && row.right && row.left.type === "removed" && row.right.type === "added") {
|
|
354
|
+
const highlighted = highlightInlineChanges(row.left.text, row.right.text, removedPalette, addedPalette, useTrueColor, lang);
|
|
355
|
+
leftText = truncateText(highlighted.old, textW);
|
|
356
|
+
rightText = truncateText(highlighted.new, textW);
|
|
357
|
+
}
|
|
358
|
+
else if (lang) {
|
|
359
|
+
if (leftText)
|
|
360
|
+
leftText = highlightLine(leftText, lang);
|
|
361
|
+
if (rightText)
|
|
362
|
+
rightText = highlightLine(rightText, lang);
|
|
363
|
+
}
|
|
364
|
+
let leftCol;
|
|
365
|
+
let rightCol;
|
|
366
|
+
if (!row.left || row.left.type === "context") {
|
|
367
|
+
leftCol = padToWidth(`${p.dim}${leftNo} │${p.reset} ${p.dim}${leftText}${p.reset}`, colWidth);
|
|
368
|
+
}
|
|
369
|
+
else if (row.left.type === "removed") {
|
|
370
|
+
if (useTrueColor) {
|
|
371
|
+
leftCol = padToWidth(`${p.errorBg}${p.error}${leftNo} │ ${preserveBg(leftText, p.errorBg)}${p.reset}`, colWidth);
|
|
386
372
|
}
|
|
387
373
|
else {
|
|
388
|
-
leftCol = padToWidth(`${p.
|
|
389
|
-
}
|
|
390
|
-
if (!row.right || row.right.type === "context") {
|
|
391
|
-
rightCol = padToWidth(`${p.dim}${rightNo} │${p.reset} ${p.dim}${rightText}${p.reset}`, colWidth);
|
|
374
|
+
leftCol = padToWidth(`${p.error}${leftNo} │ ${leftText}${p.reset}`, colWidth);
|
|
392
375
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
leftCol = padToWidth(`${p.dim}${leftNo} │${p.reset} ${leftText}`, colWidth);
|
|
379
|
+
}
|
|
380
|
+
if (!row.right || row.right.type === "context") {
|
|
381
|
+
rightCol = padToWidth(`${p.dim}${rightNo} │${p.reset} ${p.dim}${rightText}${p.reset}`, colWidth);
|
|
382
|
+
}
|
|
383
|
+
else if (row.right.type === "added") {
|
|
384
|
+
if (useTrueColor) {
|
|
385
|
+
rightCol = padToWidth(`${p.successBg}${p.success}${rightNo} │ ${preserveBg(rightText, p.successBg)}${p.reset}`, colWidth);
|
|
400
386
|
}
|
|
401
387
|
else {
|
|
402
|
-
rightCol = padToWidth(`${p.
|
|
388
|
+
rightCol = padToWidth(`${p.success}${rightNo} │ ${rightText}${p.reset}`, colWidth);
|
|
403
389
|
}
|
|
404
|
-
output.push(`${leftCol} ${p.dim}│${p.reset} ${rightCol}`);
|
|
405
390
|
}
|
|
391
|
+
else {
|
|
392
|
+
rightCol = padToWidth(`${p.dim}${rightNo} │${p.reset} ${rightText}`, colWidth);
|
|
393
|
+
}
|
|
394
|
+
out.push(`${leftCol} ${p.dim}│${p.reset} ${rightCol}`);
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
function renderSplit(diff, opts) {
|
|
399
|
+
const layout = splitLayout(diff, opts);
|
|
400
|
+
const output = [];
|
|
401
|
+
// Column header
|
|
402
|
+
const leftHeader = padToWidth(`${p.dim}${"─".repeat(layout.colWidth)}${p.reset}`, layout.colWidth);
|
|
403
|
+
const rightHeader = padToWidth(`${p.dim}${"─".repeat(layout.colWidth)}${p.reset}`, layout.colWidth);
|
|
404
|
+
output.push(`${leftHeader} ${p.dim}│${p.reset} ${rightHeader}`);
|
|
405
|
+
for (let hunkIdx = 0; hunkIdx < diff.hunks.length; hunkIdx++) {
|
|
406
|
+
if (hunkIdx > 0) {
|
|
407
|
+
output.push(`${p.dim}${" ".repeat(layout.colWidth)} │ ${" ".repeat(layout.colWidth)}${p.reset}`);
|
|
408
|
+
output.push(`${p.dim}${"·".repeat(layout.colWidth)} │ ${"·".repeat(layout.colWidth)}${p.reset}`);
|
|
409
|
+
}
|
|
410
|
+
output.push(...renderSplitHunk(diff.hunks[hunkIdx], layout));
|
|
406
411
|
}
|
|
407
412
|
return output;
|
|
408
413
|
}
|
|
@@ -439,6 +444,35 @@ function buildSplitRows(hunk) {
|
|
|
439
444
|
}
|
|
440
445
|
return rows;
|
|
441
446
|
}
|
|
447
|
+
async function renderUnifiedAsync(diff, opts, yieldFn) {
|
|
448
|
+
const layout = unifiedLayout(diff, opts);
|
|
449
|
+
const output = [];
|
|
450
|
+
for (let hunkIdx = 0; hunkIdx < diff.hunks.length; hunkIdx++) {
|
|
451
|
+
if (hunkIdx > 0)
|
|
452
|
+
await yieldFn();
|
|
453
|
+
if (hunkIdx > 0)
|
|
454
|
+
output.push(` ${p.dim}⋯${p.reset}`);
|
|
455
|
+
output.push(...renderUnifiedHunk(diff.hunks[hunkIdx], layout));
|
|
456
|
+
}
|
|
457
|
+
return output;
|
|
458
|
+
}
|
|
459
|
+
async function renderSplitAsync(diff, opts, yieldFn) {
|
|
460
|
+
const layout = splitLayout(diff, opts);
|
|
461
|
+
const output = [];
|
|
462
|
+
const leftHeader = padToWidth(`${p.dim}${"─".repeat(layout.colWidth)}${p.reset}`, layout.colWidth);
|
|
463
|
+
const rightHeader = padToWidth(`${p.dim}${"─".repeat(layout.colWidth)}${p.reset}`, layout.colWidth);
|
|
464
|
+
output.push(`${leftHeader} ${p.dim}│${p.reset} ${rightHeader}`);
|
|
465
|
+
for (let hunkIdx = 0; hunkIdx < diff.hunks.length; hunkIdx++) {
|
|
466
|
+
if (hunkIdx > 0)
|
|
467
|
+
await yieldFn();
|
|
468
|
+
if (hunkIdx > 0) {
|
|
469
|
+
output.push(`${p.dim}${" ".repeat(layout.colWidth)} │ ${" ".repeat(layout.colWidth)}${p.reset}`);
|
|
470
|
+
output.push(`${p.dim}${"·".repeat(layout.colWidth)} │ ${"·".repeat(layout.colWidth)}${p.reset}`);
|
|
471
|
+
}
|
|
472
|
+
output.push(...renderSplitHunk(diff.hunks[hunkIdx], layout));
|
|
473
|
+
}
|
|
474
|
+
return output;
|
|
475
|
+
}
|
|
442
476
|
// ── Utilities ────────────────────────────────────────────────────
|
|
443
477
|
/**
|
|
444
478
|
* Truncate text to fit within maxWidth visible characters.
|
|
@@ -451,11 +485,11 @@ function truncateText(text, maxWidth) {
|
|
|
451
485
|
return text;
|
|
452
486
|
if (maxWidth <= 1)
|
|
453
487
|
return "…";
|
|
454
|
-
//
|
|
488
|
+
// Advance by visible width (CJK = 2), skipping ANSI sequences.
|
|
489
|
+
// Reserve one column for the trailing ellipsis.
|
|
455
490
|
let visible = 0;
|
|
456
491
|
let i = 0;
|
|
457
492
|
while (i < text.length && visible < maxWidth - 1) {
|
|
458
|
-
// Check for ANSI escape sequence
|
|
459
493
|
if (text[i] === "\x1b" && text[i + 1] === "[") {
|
|
460
494
|
const end = text.indexOf("m", i);
|
|
461
495
|
if (end !== -1) {
|
|
@@ -463,8 +497,12 @@ function truncateText(text, maxWidth) {
|
|
|
463
497
|
continue;
|
|
464
498
|
}
|
|
465
499
|
}
|
|
466
|
-
|
|
467
|
-
|
|
500
|
+
const cp = text.codePointAt(i) ?? 0;
|
|
501
|
+
const cw = charWidth(cp);
|
|
502
|
+
if (visible + cw > maxWidth - 1)
|
|
503
|
+
break;
|
|
504
|
+
visible += cw;
|
|
505
|
+
i += cp > 0xffff ? 2 : 1;
|
|
468
506
|
}
|
|
469
507
|
return text.slice(0, i) + p.reset + "…";
|
|
470
508
|
}
|
|
@@ -592,3 +630,43 @@ export function renderDiff(diff, opts) {
|
|
|
592
630
|
}
|
|
593
631
|
return [header, ...bodyLines];
|
|
594
632
|
}
|
|
633
|
+
/**
|
|
634
|
+
* Async variant of renderDiff that yields to the event loop between hunks.
|
|
635
|
+
* Use when rendering in a context where a spinner or other UI needs to stay
|
|
636
|
+
* responsive (e.g. showing a large diff during a permission prompt).
|
|
637
|
+
*
|
|
638
|
+
* @param onLines - Callback invoked with each batch of rendered lines as they
|
|
639
|
+
* are produced. Allows progressive/streaming display.
|
|
640
|
+
*/
|
|
641
|
+
export async function renderDiffAsync(diff, opts, onLines) {
|
|
642
|
+
if (diff.isIdentical) {
|
|
643
|
+
onLines([`${p.dim}(no changes)${p.reset}`]);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const mode = opts.mode ?? selectMode(opts.width);
|
|
647
|
+
const maxLines = opts.maxLines ?? 50;
|
|
648
|
+
const header = buildHeader(diff, opts.filePath);
|
|
649
|
+
if (mode === "summary") {
|
|
650
|
+
onLines([header, ...renderSummary(diff)]);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
// Trim context lines from hunks if the diff would exceed the budget
|
|
654
|
+
const trimmed = { ...diff, hunks: trimHunksToFit(diff.hunks, maxLines) };
|
|
655
|
+
const yieldFn = () => new Promise(r => setImmediate(r));
|
|
656
|
+
let bodyLines;
|
|
657
|
+
switch (mode) {
|
|
658
|
+
case "split":
|
|
659
|
+
bodyLines = await renderSplitAsync(trimmed, opts, yieldFn);
|
|
660
|
+
break;
|
|
661
|
+
case "unified":
|
|
662
|
+
bodyLines = await renderUnifiedAsync(trimmed, opts, yieldFn);
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
// Final safety net — if still over budget, simple tail truncation.
|
|
666
|
+
if (bodyLines.length > maxLines) {
|
|
667
|
+
const overflow = bodyLines.length - maxLines;
|
|
668
|
+
bodyLines = bodyLines.slice(0, maxLines);
|
|
669
|
+
bodyLines.push(`${p.dim}… ${overflow} more lines${p.reset}`);
|
|
670
|
+
}
|
|
671
|
+
onLines([header, ...bodyLines]);
|
|
672
|
+
}
|
package/dist/utils/diff.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Line-level diff computation, powered by the `diff` npm package.
|
|
3
|
+
*
|
|
4
|
+
* Exposes a unified `DiffResult` interface consumed by the diff renderer.
|
|
5
|
+
* Three entry points cover the main use cases:
|
|
6
|
+
*
|
|
7
|
+
* computeDiff — full-file diff (write_file, or when edit region can't be located)
|
|
8
|
+
* computeEditDiff — edit_file: locates the edit region, builds the new file, full diff
|
|
9
|
+
* computeInputDiff — fast preview: diffs only old_text vs new_text, no file I/O
|
|
3
10
|
*/
|
|
4
11
|
export interface DiffLine {
|
|
5
12
|
type: "context" | "added" | "removed";
|
|
@@ -19,6 +26,18 @@ export interface DiffResult {
|
|
|
19
26
|
}
|
|
20
27
|
/**
|
|
21
28
|
* Compute a line-level diff between old and new file content.
|
|
22
|
-
* Returns grouped hunks with 3 lines of context around each change.
|
|
23
29
|
*/
|
|
24
30
|
export declare function computeDiff(oldText: string | null, newText: string): DiffResult;
|
|
31
|
+
/**
|
|
32
|
+
* Compute a diff for an edit operation where we know the old/new text.
|
|
33
|
+
* Locates the edit region(s) in the file, constructs the full new file,
|
|
34
|
+
* then diffs the whole thing so line numbers are file-relative.
|
|
35
|
+
*/
|
|
36
|
+
export declare function computeEditDiff(oldFileText: string, editOld: string, editNew: string, replaceAll?: boolean): DiffResult;
|
|
37
|
+
/**
|
|
38
|
+
* Diff two edit strings directly — no file read needed.
|
|
39
|
+
* Line numbers are relative to the edit region, not the file.
|
|
40
|
+
* Use for permission prompt previews where speed matters more than
|
|
41
|
+
* exact file-relative line numbers.
|
|
42
|
+
*/
|
|
43
|
+
export declare function computeInputDiff(oldText: string, newText: string): DiffResult;
|