agent-sh 0.8.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 +27 -43
- package/dist/agent/agent-loop.d.ts +69 -6
- package/dist/agent/agent-loop.js +954 -153
- package/dist/agent/conversation-state.d.ts +74 -21
- package/dist/agent/conversation-state.js +361 -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 +88 -6
- 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 +37 -5
- package/dist/agent/system-prompt.js +100 -67
- package/dist/{token-budget.d.ts → agent/token-budget.d.ts} +5 -4
- package/dist/{token-budget.js → agent/token-budget.js} +15 -20
- package/dist/agent/tool-protocol.d.ts +105 -0
- package/dist/agent/tool-protocol.js +551 -0
- 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 +22 -2
- package/dist/context-manager.d.ts +17 -0
- package/dist/context-manager.js +37 -4
- package/dist/core.d.ts +7 -7
- package/dist/core.js +99 -196
- package/dist/event-bus.d.ts +85 -2
- package/dist/event-bus.js +20 -1
- package/dist/executor.d.ts +4 -3
- package/dist/executor.js +18 -15
- package/dist/extension-loader.d.ts +5 -0
- package/dist/extension-loader.js +143 -19
- package/dist/extensions/agent-backend.d.ts +14 -0
- package/dist/extensions/agent-backend.js +188 -0
- package/dist/extensions/command-suggest.d.ts +3 -3
- package/dist/extensions/command-suggest.js +4 -3
- package/dist/extensions/index.d.ts +19 -0
- package/dist/extensions/index.js +24 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +30 -10
- package/dist/extensions/tui-renderer.js +117 -113
- package/dist/index.js +39 -26
- package/dist/settings.d.ts +40 -3
- package/dist/settings.js +57 -10
- package/dist/{input-handler.d.ts → shell/input-handler.d.ts} +3 -2
- package/dist/{input-handler.js → shell/input-handler.js} +111 -85
- package/dist/{output-parser.d.ts → shell/output-parser.d.ts} +1 -1
- package/dist/{output-parser.js → shell/output-parser.js} +1 -1
- package/dist/{shell.d.ts → shell/shell.d.ts} +8 -2
- package/dist/{shell.js → shell/shell.js} +39 -8
- package/dist/types.d.ts +61 -10
- package/dist/utils/ansi.d.ts +5 -0
- package/dist/utils/ansi.js +1 -1
- package/dist/utils/compositor.d.ts +67 -0
- package/dist/utils/compositor.js +116 -0
- package/dist/utils/diff-renderer.d.ts +9 -0
- package/dist/utils/diff-renderer.js +312 -146
- package/dist/utils/diff.d.ts +21 -2
- package/dist/utils/diff.js +165 -89
- package/dist/utils/floating-panel.d.ts +2 -0
- package/dist/utils/floating-panel.js +30 -14
- package/dist/utils/handler-registry.d.ts +31 -10
- package/dist/utils/handler-registry.js +58 -16
- package/dist/utils/line-editor.d.ts +33 -3
- package/dist/utils/line-editor.js +221 -44
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +1 -1
- package/dist/utils/message-utils.d.ts +35 -0
- package/dist/utils/message-utils.js +75 -0
- package/dist/utils/terminal-buffer.d.ts +5 -1
- package/dist/utils/terminal-buffer.js +18 -2
- package/dist/utils/tool-display.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -4
- package/dist/utils/tool-interactive.d.ts +12 -0
- package/dist/utils/tool-interactive.js +53 -0
- package/examples/extensions/ash-acp-bridge/README.md +39 -0
- package/examples/extensions/ash-acp-bridge/package.json +23 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +574 -0
- package/examples/extensions/ash-acp-bridge/tsconfig.json +14 -0
- package/examples/extensions/ash-mcp-bridge/README.md +72 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +164 -0
- package/examples/extensions/ash-mcp-bridge/package.json +9 -0
- 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 +98 -112
- package/examples/extensions/overlay-agent.ts +84 -38
- package/examples/extensions/peer-mesh.ts +565 -0
- package/examples/extensions/pi-bridge/index.ts +2 -2
- package/examples/extensions/questionnaire.ts +260 -0
- package/examples/extensions/subagents.ts +19 -4
- package/examples/extensions/terminal-buffer.ts +32 -53
- package/examples/extensions/tmux-pane.ts +307 -0
- package/examples/extensions/user-shell.ts +136 -0
- package/examples/extensions/web-access.ts +335 -0
- package/package.json +44 -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/overlay-agent.d.ts +0 -14
- package/dist/extensions/overlay-agent.js +0 -147
- package/dist/extensions/terminal-buffer.d.ts +0 -14
- package/dist/extensions/terminal-buffer.js +0 -125
|
@@ -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.
|
|
374
|
+
leftCol = padToWidth(`${p.error}${leftNo} │ ${leftText}${p.reset}`, colWidth);
|
|
389
375
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
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,11 +497,100 @@ 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
|
}
|
|
509
|
+
// ── Truncation ──────────────────────────────────────────────────
|
|
510
|
+
/**
|
|
511
|
+
* Trim context lines from hunks so the rendered output fits within a budget.
|
|
512
|
+
* Change lines are never removed — only the surrounding context shrinks.
|
|
513
|
+
*/
|
|
514
|
+
function trimHunksToFit(hunks, maxLines) {
|
|
515
|
+
// Count change lines across all hunks
|
|
516
|
+
let changeCount = 0;
|
|
517
|
+
for (const hunk of hunks) {
|
|
518
|
+
for (const line of hunk.lines) {
|
|
519
|
+
if (line.type !== "context")
|
|
520
|
+
changeCount++;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Separators between hunks
|
|
524
|
+
const separators = Math.max(0, hunks.length - 1);
|
|
525
|
+
// How many context lines can we afford?
|
|
526
|
+
const contextBudget = Math.max(0, maxLines - changeCount - separators);
|
|
527
|
+
// Count total context to see if trimming is needed
|
|
528
|
+
let totalContext = 0;
|
|
529
|
+
for (const hunk of hunks) {
|
|
530
|
+
for (const line of hunk.lines) {
|
|
531
|
+
if (line.type === "context")
|
|
532
|
+
totalContext++;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (totalContext <= contextBudget)
|
|
536
|
+
return hunks;
|
|
537
|
+
// Determine how many context lines to keep per side of each change.
|
|
538
|
+
// Binary-search for the largest per-side context that fits.
|
|
539
|
+
let lo = 0;
|
|
540
|
+
let hi = 3; // original context size from groupHunks
|
|
541
|
+
while (lo < hi) {
|
|
542
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
543
|
+
if (countContextWithLimit(hunks, mid) <= contextBudget)
|
|
544
|
+
lo = mid;
|
|
545
|
+
else
|
|
546
|
+
hi = mid - 1;
|
|
547
|
+
}
|
|
548
|
+
return rebuildHunks(hunks, lo);
|
|
549
|
+
}
|
|
550
|
+
/** Count how many context lines remain if we keep at most `ctx` per side of each change. */
|
|
551
|
+
function countContextWithLimit(hunks, ctx) {
|
|
552
|
+
let count = 0;
|
|
553
|
+
for (const hunk of hunks) {
|
|
554
|
+
const lines = hunk.lines;
|
|
555
|
+
for (let i = 0; i < lines.length; i++) {
|
|
556
|
+
if (lines[i].type !== "context")
|
|
557
|
+
continue;
|
|
558
|
+
// Keep this context line if it's within `ctx` of any change
|
|
559
|
+
let nearChange = false;
|
|
560
|
+
for (let d = 1; d <= ctx; d++) {
|
|
561
|
+
if ((i - d >= 0 && lines[i - d].type !== "context") ||
|
|
562
|
+
(i + d < lines.length && lines[i + d].type !== "context")) {
|
|
563
|
+
nearChange = true;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (nearChange)
|
|
568
|
+
count++;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return count;
|
|
572
|
+
}
|
|
573
|
+
/** Rebuild hunks keeping only context lines within `ctx` distance of a change. */
|
|
574
|
+
function rebuildHunks(hunks, ctx) {
|
|
575
|
+
return hunks.map((hunk) => {
|
|
576
|
+
const lines = hunk.lines;
|
|
577
|
+
const kept = [];
|
|
578
|
+
for (let i = 0; i < lines.length; i++) {
|
|
579
|
+
if (lines[i].type !== "context") {
|
|
580
|
+
kept.push(lines[i]);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
for (let d = 1; d <= ctx; d++) {
|
|
584
|
+
if ((i - d >= 0 && lines[i - d].type !== "context") ||
|
|
585
|
+
(i + d < lines.length && lines[i + d].type !== "context")) {
|
|
586
|
+
kept.push(lines[i]);
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return { lines: kept };
|
|
592
|
+
});
|
|
593
|
+
}
|
|
471
594
|
// ── Public API ───────────────────────────────────────────────────
|
|
472
595
|
/** Select display mode based on available terminal width. */
|
|
473
596
|
export function selectMode(width) {
|
|
@@ -487,16 +610,19 @@ export function renderDiff(diff, opts) {
|
|
|
487
610
|
if (mode === "summary") {
|
|
488
611
|
return [header, ...renderSummary(diff)];
|
|
489
612
|
}
|
|
613
|
+
// Trim context lines from hunks if the diff would exceed the budget,
|
|
614
|
+
// so that actual changes are always visible.
|
|
615
|
+
const trimmed = { ...diff, hunks: trimHunksToFit(diff.hunks, maxLines) };
|
|
490
616
|
let bodyLines;
|
|
491
617
|
switch (mode) {
|
|
492
618
|
case "split":
|
|
493
|
-
bodyLines = renderSplit(
|
|
619
|
+
bodyLines = renderSplit(trimmed, opts);
|
|
494
620
|
break;
|
|
495
621
|
case "unified":
|
|
496
|
-
bodyLines = renderUnified(
|
|
622
|
+
bodyLines = renderUnified(trimmed, opts);
|
|
497
623
|
break;
|
|
498
624
|
}
|
|
499
|
-
//
|
|
625
|
+
// Final safety net — if still over budget, simple tail truncation.
|
|
500
626
|
if (bodyLines.length > maxLines) {
|
|
501
627
|
const overflow = bodyLines.length - maxLines;
|
|
502
628
|
bodyLines = bodyLines.slice(0, maxLines);
|
|
@@ -504,3 +630,43 @@ export function renderDiff(diff, opts) {
|
|
|
504
630
|
}
|
|
505
631
|
return [header, ...bodyLines];
|
|
506
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;
|