ikie-cli 0.1.12 → 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/repl.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as readline from 'node:readline';
2
2
  import { execSync, exec } from 'child_process';
3
3
  import { restoreStdinListeners } from './agent.js';
4
- import { c, PROMPT, CONTINUE_PROMPT, printPromptHeader, modeTag, drawBanner, infoLine, successLine, errorLine, THEMES, setTheme, stripAnsi, contextRing, } from './theme.js';
4
+ import { c, PROMPT, CONTINUE_PROMPT, PROMPT_ARROW, printPromptHeader, modeTag, drawBanner, infoLine, successLine, errorLine, THEMES, setTheme, stripAnsi, contextRing, } from './theme.js';
5
5
  import { renderMarkdown } from './renderer.js';
6
6
  import { loadAllMemory } from './memory.js';
7
7
  import { HOME_DIR, saveConfig, DEFAULT_MODEL, IKIE_HOST, IKIE_API_BASE, isLoggedIn, getApiKey } from './config.js';
@@ -985,7 +985,7 @@ export async function startREPL(agent, config, projectContext, oneShot) {
985
985
  const pct = tokens / contextWindow;
986
986
  const ring = tokens > 0 ? contextRing(Math.min(pct, 1)) : '';
987
987
  const prompt = ring
988
- ? `${c.primary('╰─')} ${ring} ${c.primary(' ')}`
988
+ ? `${c.primary('╰─')} ${ring} ${c.primary(PROMPT_ARROW + ' ')}`
989
989
  : PROMPT;
990
990
  if (imageState.pending.length) {
991
991
  const labels = imageState.pending.map(image => `[Image #${image.id}]`).join(' ');
package/dist/theme.d.ts CHANGED
@@ -44,6 +44,7 @@ export declare function contextRing(pct: number): string;
44
44
  export declare function printPromptHeader(mode?: 'agent' | 'plan'): void;
45
45
  export declare const PROMPT: string;
46
46
  export declare const CONTINUE_PROMPT: string;
47
+ export declare const PROMPT_ARROW: string;
47
48
  export declare class InlineSpinner {
48
49
  private timer;
49
50
  private delayTimer;
@@ -61,9 +62,16 @@ export declare class InlineSpinner {
61
62
  }
62
63
  export declare function toolLine(name: string, args: string): string;
63
64
  /** Multi-line output block shown after a tool runs. */
64
- export declare function toolOutputBlock(result: string, ms: number): string;
65
- /** Colored diff block for edit_file / write_file. */
66
- 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;
67
75
  export declare function toolSuccessLine(ms: number, preview?: string): string;
68
76
  export declare function toolErrorLine(msg: string): string;
69
77
  export declare function successLine(msg: string): string;
@@ -71,3 +79,4 @@ export declare function errorLine(msg: string): string;
71
79
  export declare function warnLine(msg: string): string;
72
80
  export declare function infoLine(msg: string): string;
73
81
  export declare function permissionPrompt(toolName: string, preview: string): string;
82
+ export {};
package/dist/theme.js CHANGED
@@ -268,15 +268,20 @@ export function contextRing(pct) {
268
268
  const color = pct < 0.7 ? c.success : pct < 0.85 ? c.warning : c.error;
269
269
  return `${color(char)} ${color(`${Math.round(pct * 100)}%`)}`;
270
270
  }
271
+ const IS_WIN = process.platform === 'win32';
272
+ const CH = IS_WIN
273
+ ? { tl: '+-', prompt: '\\->', cont: '| ', arrow: '>', dot: '●' }
274
+ : { tl: '╭─', prompt: '╰─❯', cont: '│ ', arrow: '❯', dot: '●' };
271
275
  export function printPromptHeader(mode = 'agent') {
272
276
  const cwdName = basename(process.cwd()) || '/';
273
277
  const branch = getGitBranchFast();
274
278
  const gitSegment = branch ? ` ${c.muted('on')} ${c.secondary(branch)}` : '';
275
279
  const themeSegment = ` ${c.muted('theme')} ${c.secondary(activeTheme.name)}`;
276
- process.stdout.write(`\n${c.primary('╭─')} ${c.primary.bold('ikie')} ${c.muted('·')} ${modeTag(mode)}${gitSegment}${themeSegment} ${c.muted('in')} ${c.accent(cwdName)}\n`);
280
+ process.stdout.write(`\n${c.primary(CH.tl)} ${c.primary.bold('ikie')} ${c.muted('·')} ${modeTag(mode)}${gitSegment}${themeSegment} ${c.muted('in')} ${c.accent(cwdName)}\n`);
277
281
  }
278
- export const PROMPT = c.primary('╰─❯ ');
279
- export const CONTINUE_PROMPT = c.primary('│ ');
282
+ export const PROMPT = c.primary(`${CH.prompt} `);
283
+ export const CONTINUE_PROMPT = c.primary(CH.cont);
284
+ export const PROMPT_ARROW = CH.arrow;
280
285
  export class InlineSpinner {
281
286
  timer = null;
282
287
  delayTimer = null;
@@ -370,55 +375,159 @@ export function toolLine(name, args) {
370
375
  return `${tint('●')} ${c.white.bold(verb + badge)}${c.dim('(')}${c.muted(args)}${c.dim(')')}`;
371
376
  }
372
377
  /** Multi-line output block shown after a tool runs. */
373
- export function toolOutputBlock(result, ms) {
378
+ export function toolOutputBlock(result, ms, indent = ' ') {
374
379
  const time = c.muted(formatDuration(ms));
375
380
  const lines = result.split('\n').filter(l => l.trim() !== '');
376
381
  if (!lines.length)
377
- return ` ${c.muted('⎿')} ${time}`;
382
+ return `${indent}${c.muted('⎿')} ${time}`;
378
383
  const MAX = 5;
379
384
  const shown = lines.slice(0, MAX);
380
385
  const hidden = lines.length - MAX;
386
+ const cont = indent + ' ';
381
387
  const out = [];
382
- out.push(` ${c.muted('⎿')} ${time} ${c.dim(clampLine(shown[0]))}`);
388
+ out.push(`${indent}${c.muted('⎿')} ${time} ${c.dim(clampLine(shown[0]))}`);
383
389
  for (let i = 1; i < shown.length; i++) {
384
- out.push(` ${c.dim(clampLine(shown[i]))}`);
390
+ out.push(`${cont}${c.dim(clampLine(shown[i]))}`);
385
391
  }
386
392
  if (hidden > 0) {
387
- out.push(` ${c.muted(`… +${hidden} lines`)}`);
393
+ out.push(`${cont}${c.muted(`… +${hidden} lines`)}`);
388
394
  }
389
395
  return out.join('\n');
390
396
  }
391
- /** Colored diff block for edit_file / write_file. */
392
- export function toolDiffBlock(oldStr, newStr, ms) {
393
- const time = c.muted(formatDuration(ms));
394
- const removed = oldStr.split('\n');
395
- const added = newStr.split('\n');
396
- const nRemoved = removed.filter(l => l.trim()).length;
397
- const nAdded = added.filter(l => l.trim()).length;
398
- const parts = [];
399
- if (nAdded)
400
- parts.push(`Added ${nAdded} line${nAdded === 1 ? '' : 's'}`);
401
- if (nRemoved)
402
- parts.push(`removed ${nRemoved} line${nRemoved === 1 ? '' : 's'}`);
403
- 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
+ }
404
417
  const out = [];
405
- out.push(` ${c.muted('⎿')} ${time} ${c.dim(summary)}`);
406
- const MAX = 10;
407
- let shown = 0;
408
- for (const line of removed) {
409
- if (shown >= MAX)
410
- break;
411
- out.push(chalk.bgRed.white(` - ${clampLine(line, 116)}`));
412
- 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 */ }
413
467
  }
414
- for (const line of added) {
415
- if (shown >= MAX)
416
- break;
417
- out.push(chalk.bgGreen.black(` + ${clampLine(line, 116)}`));
418
- 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
+ }
419
528
  }
420
- if (shown >= MAX && (removed.length + added.length > MAX)) {
421
- 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`)}`);
422
531
  }
423
532
  return out.join('\n');
424
533
  }
@@ -456,5 +565,5 @@ export function permissionPrompt(toolName, preview) {
456
565
  `${c.error.bold('n')} ${c.muted('deny')} ` +
457
566
  `${c.info.bold('a')} ${c.muted('always')} ` +
458
567
  `${c.muted.bold('!')} ${c.muted('never')}\n` +
459
- ` ${c.muted('❯')} `);
568
+ ` ${c.muted(CH.arrow)} `);
460
569
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikie-cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Agentic coding CLI — your terminal AI pair programmer",
5
5
  "type": "module",
6
6
  "bin": {