ikie-cli 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.js CHANGED
@@ -530,12 +530,13 @@ export class Agent {
530
530
  if (name === 'edit_file' && !result.startsWith('Error')) {
531
531
  const oldStr = String(input.old_string ?? '');
532
532
  const newStr = String(input.new_string ?? '');
533
- block = toolDiffBlock(oldStr, newStr, ms);
533
+ const path = typeof input.path === 'string' ? input.path : undefined;
534
+ block = toolDiffBlock(oldStr, newStr, ms, { path, indent: this.indent });
534
535
  }
535
536
  else {
536
- block = toolOutputBlock(result, ms);
537
+ block = toolOutputBlock(result, ms, this.indent);
537
538
  }
538
- process.stdout.write(`${this.indent}${block}\n`);
539
+ process.stdout.write(`${block}\n`);
539
540
  return result;
540
541
  }
541
542
  catch (err) {
package/dist/theme.d.ts CHANGED
@@ -62,9 +62,16 @@ export declare class InlineSpinner {
62
62
  }
63
63
  export declare function toolLine(name: string, args: string): string;
64
64
  /** Multi-line output block shown after a tool runs. */
65
- export declare function toolOutputBlock(result: string, ms: number): string;
66
- /** Colored diff block for edit_file / write_file. */
67
- export declare function toolDiffBlock(oldStr: string, newStr: string, ms: number): string;
65
+ export declare function toolOutputBlock(result: string, ms: number, indent?: string): string;
66
+ interface DiffOpts {
67
+ path?: string;
68
+ indent?: string;
69
+ }
70
+ /**
71
+ * Render an edit as a polished unified diff: line-number gutter, a few lines of
72
+ * surrounding context pulled from the file, and full-width muted red/green rows.
73
+ */
74
+ export declare function toolDiffBlock(oldStr: string, newStr: string, ms: number, opts?: DiffOpts): string;
68
75
  export declare function toolSuccessLine(ms: number, preview?: string): string;
69
76
  export declare function toolErrorLine(msg: string): string;
70
77
  export declare function successLine(msg: string): string;
@@ -72,3 +79,4 @@ export declare function errorLine(msg: string): string;
72
79
  export declare function warnLine(msg: string): string;
73
80
  export declare function infoLine(msg: string): string;
74
81
  export declare function permissionPrompt(toolName: string, preview: string): string;
82
+ export {};
package/dist/theme.js CHANGED
@@ -375,55 +375,159 @@ export function toolLine(name, args) {
375
375
  return `${tint('●')} ${c.white.bold(verb + badge)}${c.dim('(')}${c.muted(args)}${c.dim(')')}`;
376
376
  }
377
377
  /** Multi-line output block shown after a tool runs. */
378
- export function toolOutputBlock(result, ms) {
378
+ export function toolOutputBlock(result, ms, indent = ' ') {
379
379
  const time = c.muted(formatDuration(ms));
380
380
  const lines = result.split('\n').filter(l => l.trim() !== '');
381
381
  if (!lines.length)
382
- return ` ${c.muted('⎿')} ${time}`;
382
+ return `${indent}${c.muted('⎿')} ${time}`;
383
383
  const MAX = 5;
384
384
  const shown = lines.slice(0, MAX);
385
385
  const hidden = lines.length - MAX;
386
+ const cont = indent + ' ';
386
387
  const out = [];
387
- out.push(` ${c.muted('⎿')} ${time} ${c.dim(clampLine(shown[0]))}`);
388
+ out.push(`${indent}${c.muted('⎿')} ${time} ${c.dim(clampLine(shown[0]))}`);
388
389
  for (let i = 1; i < shown.length; i++) {
389
- out.push(` ${c.dim(clampLine(shown[i]))}`);
390
+ out.push(`${cont}${c.dim(clampLine(shown[i]))}`);
390
391
  }
391
392
  if (hidden > 0) {
392
- out.push(` ${c.muted(`… +${hidden} lines`)}`);
393
+ out.push(`${cont}${c.muted(`… +${hidden} lines`)}`);
393
394
  }
394
395
  return out.join('\n');
395
396
  }
396
- /** Colored diff block for edit_file / write_file. */
397
- export function toolDiffBlock(oldStr, newStr, ms) {
398
- const time = c.muted(formatDuration(ms));
399
- const removed = oldStr.split('\n');
400
- const added = newStr.split('\n');
401
- const nRemoved = removed.filter(l => l.trim()).length;
402
- const nAdded = added.filter(l => l.trim()).length;
403
- const parts = [];
404
- if (nAdded)
405
- parts.push(`Added ${nAdded} line${nAdded === 1 ? '' : 's'}`);
406
- if (nRemoved)
407
- parts.push(`removed ${nRemoved} line${nRemoved === 1 ? '' : 's'}`);
408
- const summary = parts.join(', ') || 'no changes';
397
+ // ── Diff rendering ────────────────────────────────────────────────────────────
398
+ // Diff palette (256-color, muted — chalk degrades gracefully on low-color TTYs).
399
+ const diffColors = {
400
+ delBg: chalk.bgAnsi256(52), // deep red
401
+ insBg: chalk.bgAnsi256(22), // deep green
402
+ delGutter: chalk.ansi256(210),
403
+ insGutter: chalk.ansi256(151),
404
+ onBg: chalk.ansi256(255),
405
+ ctxGutter: chalk.ansi256(240),
406
+ ctxCode: chalk.ansi256(248),
407
+ };
408
+ /** Classic LCS line diff interleaves equal/removed/added like a real unified diff. */
409
+ function lineDiff(a, b) {
410
+ const n = a.length, m = b.length;
411
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
412
+ for (let i = n - 1; i >= 0; i--) {
413
+ for (let j = m - 1; j >= 0; j--) {
414
+ dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
415
+ }
416
+ }
409
417
  const out = [];
410
- out.push(` ${c.muted('⎿')} ${time} ${c.dim(summary)}`);
411
- const MAX = 10;
412
- let shown = 0;
413
- for (const line of removed) {
414
- if (shown >= MAX)
415
- break;
416
- out.push(chalk.bgRed.white(` - ${clampLine(line, 116)}`));
417
- shown++;
418
+ let i = 0, j = 0;
419
+ while (i < n && j < m) {
420
+ if (a[i] === b[j]) {
421
+ out.push({ t: 'eq', s: a[i] });
422
+ i++;
423
+ j++;
424
+ }
425
+ else if (dp[i + 1][j] >= dp[i][j + 1]) {
426
+ out.push({ t: 'del', s: a[i] });
427
+ i++;
428
+ }
429
+ else {
430
+ out.push({ t: 'ins', s: b[j] });
431
+ j++;
432
+ }
433
+ }
434
+ while (i < n)
435
+ out.push({ t: 'del', s: a[i++] });
436
+ while (j < m)
437
+ out.push({ t: 'ins', s: b[j++] });
438
+ return out;
439
+ }
440
+ /**
441
+ * Render an edit as a polished unified diff: line-number gutter, a few lines of
442
+ * surrounding context pulled from the file, and full-width muted red/green rows.
443
+ */
444
+ export function toolDiffBlock(oldStr, newStr, ms, opts = {}) {
445
+ const indent = opts.indent ?? ' ';
446
+ const cols = Math.max(40, (process.stdout.columns ?? 100));
447
+ const time = c.muted(formatDuration(ms));
448
+ const oldLines = oldStr.split('\n');
449
+ const newLines = newStr.split('\n');
450
+ const ops = lineDiff(oldLines, newLines);
451
+ const nDel = ops.filter(o => o.t === 'del').length;
452
+ const nIns = ops.filter(o => o.t === 'ins').length;
453
+ // Anchor line numbers + context lines by locating the new text in the file.
454
+ let anchor = 1;
455
+ let fileLines = null;
456
+ const CTX = 3;
457
+ if (opts.path) {
458
+ try {
459
+ const content = readFileSync(opts.path, 'utf-8');
460
+ const idx = content.indexOf(newStr);
461
+ if (idx >= 0) {
462
+ anchor = (content.slice(0, idx).match(/\n/g)?.length ?? 0) + 1;
463
+ fileLines = content.split('\n');
464
+ }
465
+ }
466
+ catch { /* no context — fall back to anchorless diff */ }
418
467
  }
419
- for (const line of added) {
420
- if (shown >= MAX)
421
- break;
422
- out.push(chalk.bgGreen.black(` + ${clampLine(line, 116)}`));
423
- shown++;
468
+ const summaryParts = [];
469
+ if (nIns)
470
+ summaryParts.push(`Added ${nIns} line${nIns === 1 ? '' : 's'}`);
471
+ if (nDel)
472
+ summaryParts.push(`removed ${nDel} line${nDel === 1 ? '' : 's'}`);
473
+ const summary = summaryParts.join(', ') || 'no changes';
474
+ // Width of the line-number gutter.
475
+ const maxNum = anchor + Math.max(oldLines.length, newLines.length) + CTX;
476
+ const gw = String(maxNum).length;
477
+ const rowsRaw = [];
478
+ // Leading context (lines just before the edit).
479
+ if (fileLines) {
480
+ for (let k = Math.max(1, anchor - CTX); k < anchor; k++) {
481
+ rowsRaw.push({ kind: 'eq', num: k, text: fileLines[k - 1] ?? '' });
482
+ }
483
+ }
484
+ // The diff body — track separate old/new line counters off the same anchor.
485
+ let oldNum = anchor, newNum = anchor;
486
+ for (const op of ops) {
487
+ if (op.t === 'eq') {
488
+ rowsRaw.push({ kind: 'eq', num: newNum, text: op.s });
489
+ oldNum++;
490
+ newNum++;
491
+ }
492
+ else if (op.t === 'del') {
493
+ rowsRaw.push({ kind: 'del', num: oldNum, text: op.s });
494
+ oldNum++;
495
+ }
496
+ else {
497
+ rowsRaw.push({ kind: 'ins', num: newNum, text: op.s });
498
+ newNum++;
499
+ }
500
+ }
501
+ // Trailing context (lines just after the edit).
502
+ if (fileLines) {
503
+ const afterStart = newNum;
504
+ for (let k = afterStart; k < afterStart + CTX && k <= fileLines.length; k++) {
505
+ rowsRaw.push({ kind: 'eq', num: k, text: fileLines[k - 1] ?? '' });
506
+ }
507
+ }
508
+ const out = [];
509
+ out.push(`${indent}${c.muted('⎿')} ${time} ${c.dim(summary)}`);
510
+ const MAX = 16;
511
+ const codeWidth = cols - indent.length - gw - 4; // gutter + " x " marker + space
512
+ const render = rowsRaw.slice(0, MAX);
513
+ for (const r of render) {
514
+ const numStr = String(r.num).padStart(gw);
515
+ const code = r.text.length > codeWidth ? r.text.slice(0, codeWidth - 1) + '…' : r.text;
516
+ if (r.kind === 'eq') {
517
+ out.push(`${indent}${diffColors.ctxGutter(numStr)} ${diffColors.ctxCode(code)}`);
518
+ }
519
+ else {
520
+ const marker = r.kind === 'del' ? '-' : '+';
521
+ const bg = r.kind === 'del' ? diffColors.delBg : diffColors.insBg;
522
+ const gutter = r.kind === 'del' ? diffColors.delGutter : diffColors.insGutter;
523
+ const plainLen = numStr.length + 3 + code.length; // num + " x " + code
524
+ const pad = Math.max(0, cols - indent.length - plainLen);
525
+ const inner = `${gutter(numStr)} ${gutter(marker)} ${diffColors.onBg(code)}${' '.repeat(pad)}`;
526
+ out.push(`${indent}${bg(inner)}`);
527
+ }
424
528
  }
425
- if (shown >= MAX && (removed.length + added.length > MAX)) {
426
- out.push(` ${c.muted(`… +${removed.length + added.length - MAX} lines`)}`);
529
+ if (rowsRaw.length > MAX) {
530
+ out.push(`${indent}${c.muted(`… +${rowsRaw.length - MAX} lines`)}`);
427
531
  }
428
532
  return out.join('\n');
429
533
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikie-cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Agentic coding CLI — your terminal AI pair programmer",
5
5
  "type": "module",
6
6
  "bin": {