@ekkos/cli 1.2.15 → 1.2.16

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.
@@ -565,21 +565,24 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
565
565
  // 3. Claude Code pane remains fully independent
566
566
  // 4. Text selection works across panes without interference
567
567
  // ══════════════════════════════════════════════════════════════════════════
568
- // Open /dev/tty for blessed to use (gives it direct terminal access)
568
+ // Open /dev/tty for blessed to use (gives it direct terminal access).
569
+ // On macOS/Linux in tmux, this isolates the dashboard from stdout piping.
570
+ // On Windows (no /dev/tty), stdin/stdout is correct — blessed handles the
571
+ // alternate screen buffer natively for clean rendering.
569
572
  const fs = require('fs');
570
573
  let ttyInput = process.stdin;
571
574
  let ttyOutput = process.stdout;
572
- try {
573
- // Create readable/writable streams from /dev/tty file descriptors
574
- const ttyFd = fs.openSync('/dev/tty', 'r+');
575
- ttyInput = new (require('tty').ReadStream)(ttyFd);
576
- ttyOutput = new (require('tty').WriteStream)(ttyFd);
577
- }
578
- catch (e) {
579
- // Fallback: if /dev/tty unavailable (some CI/remote environments),
580
- // use stdin/stdout but disable all event handling
581
- ttyInput = process.stdin;
582
- ttyOutput = process.stdout;
575
+ if (inTmux) {
576
+ try {
577
+ const ttyFd = fs.openSync('/dev/tty', 'r+');
578
+ ttyInput = new (require('tty').ReadStream)(ttyFd);
579
+ ttyOutput = new (require('tty').WriteStream)(ttyFd);
580
+ }
581
+ catch (e) {
582
+ // Fallback: if /dev/tty unavailable, use stdin/stdout
583
+ ttyInput = process.stdin;
584
+ ttyOutput = process.stdout;
585
+ }
583
586
  }
584
587
  const screen = blessed.screen({
585
588
  smartCSR: true,
@@ -596,16 +599,17 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
596
599
  resizeTimeout: 300, // Debounce resize events
597
600
  });
598
601
  // ══════════════════════════════════════════════════════════════════════════
599
- // DISABLE TERMINAL CONTROL SEQUENCE HIJACKING
600
- // Prevent blessed from sending control sequences that might interfere with
601
- // other panes in tmux. The /dev/tty approach already isolates this, but we
602
- // also disable the raw mode entirely to be extra safe.
602
+ // DISABLE TERMINAL CONTROL SEQUENCE HIJACKING (TMUX ONLY)
603
+ // When running in a tmux split pane alongside Claude Code, prevent blessed
604
+ // from sending control sequences that would interfere with the other pane.
605
+ // In standalone mode (including Windows Terminal split), blessed needs the
606
+ // alternate buffer and raw mode for clean rendering without artifacts.
603
607
  // ══════════════════════════════════════════════════════════════════════════
604
- if (screen.program) {
605
- // Override alternateBuffer to do nothing
608
+ if (inTmux && screen.program) {
609
+ // Override alternateBuffer to do nothing (tmux pane isolation)
606
610
  screen.program.alternateBuffer = () => { };
607
611
  screen.program.normalBuffer = () => { };
608
- // Don't enter raw mode — let the OS handle it
612
+ // Don't enter raw mode — let the OS handle it (tmux pane isolation)
609
613
  if (screen.program.setRawMode) {
610
614
  screen.program.setRawMode = (enabled) => {
611
615
  // Silently ignore raw mode requests
@@ -1077,72 +1081,50 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
1077
1081
  let header = '';
1078
1082
  let separator = '';
1079
1083
  let rows = [];
1080
- // Responsive table layouts keep headers visible even in very narrow panes.
1081
- if (w >= 60) {
1082
- // Full mode: Turn, Model, Context, Cache Rd, Cache Wr, Output, Cost
1083
- const colNum = 4;
1084
- const colM = 7;
1085
- const colCtx = 7;
1086
- const colCost = 8;
1087
- const nDividers = 6;
1088
- const fixedW = colNum + colM + colCtx + colCost;
1089
- const flexTotal = w - fixedW - nDividers;
1090
- const rdW = Math.max(10, Math.floor(flexTotal * 0.35));
1091
- const wrW = Math.max(11, Math.floor(flexTotal * 0.30));
1092
- const outW = Math.max(6, flexTotal - rdW - wrW);
1093
- header = `${pad('Turn', colNum)}${div}${rpad('Model', colM)}${div}${rpad('Contex', colCtx)}${div}${rpad('Cache Read', rdW)}${div}${rpad('Cache Write', wrW)}${div}${rpad('Output', outW)}${div}${rpad('Cost', colCost)}`;
1094
- separator = `${'─'.repeat(colNum)}┼${'─'.repeat(colM)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(rdW)}┼${'─'.repeat(wrW)}┼${'─'.repeat(outW)}┼${'─'.repeat(colCost)}`;
1095
- rows = turns.map(t => {
1096
- const mTag = modelTag(t.routedModel);
1097
- const mColor = t.routedModel.includes('haiku') ? 'green' : t.routedModel.includes('sonnet') ? 'blue' : 'magenta';
1098
- const costFlag = t.cost > 1.0 ? '{red-fg}' : t.cost > 0.5 ? '{yellow-fg}' : '{white-fg}';
1099
- const costEnd = t.cost > 1.0 ? '{/red-fg}' : t.cost > 0.5 ? '{/yellow-fg}' : '{/white-fg}';
1100
- return (pad(String(t.turn), colNum) + div +
1101
- `{${mColor}-fg}${cpad(mTag, colM)}{/${mColor}-fg}` + div +
1102
- rpad(`${t.contextPct.toFixed(0)}%`, colCtx) + div +
1103
- `{green-fg}${rpad(fmtK(t.cacheRead), rdW)}{/green-fg}` + div +
1104
- `{yellow-fg}${rpad(fmtK(t.cacheCreate), wrW)}{/yellow-fg}` + div +
1105
- `{cyan-fg}${rpad(fmtK(t.output), outW)}{/cyan-fg}` + div +
1106
- costFlag + rpad(`$${t.cost.toFixed(2)}`, colCost) + costEnd);
1107
- });
1108
- }
1109
- else if (w >= 30) {
1110
- // Compact mode: drop cache split columns to preserve readable headers.
1111
- const colNum = 4;
1112
- const colM = 6;
1113
- const colCtx = 6;
1114
- const colCost = 6;
1115
- const nDividers = 4;
1116
- const outW = Math.max(4, w - (colNum + colM + colCtx + colCost + nDividers));
1117
- header = `${pad('Turn', colNum)}${div}${rpad('Model', colM)}${div}${rpad('Context', colCtx)}${div}${rpad('Output', outW)}${div}${rpad('Cost', colCost)}`;
1118
- separator = `${'─'.repeat(colNum)}┼${'─'.repeat(colM)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(outW)}┼${'─'.repeat(colCost)}`;
1119
- rows = turns.map(t => {
1120
- const mTag = modelTag(t.routedModel);
1121
- const mColor = t.routedModel.includes('haiku') ? 'green' : t.routedModel.includes('sonnet') ? 'blue' : 'magenta';
1122
- const costFlag = t.cost > 1.0 ? '{red-fg}' : t.cost > 0.5 ? '{yellow-fg}' : '{white-fg}';
1123
- const costEnd = t.cost > 1.0 ? '{/red-fg}' : t.cost > 0.5 ? '{/yellow-fg}' : '{/white-fg}';
1124
- return (pad(String(t.turn), colNum) + div +
1125
- `{${mColor}-fg}${cpad(mTag, colM)}{/${mColor}-fg}` + div +
1126
- rpad(`${t.contextPct.toFixed(0)}%`, colCtx) + div +
1127
- `{cyan-fg}${rpad(fmtK(t.output), outW)}{/cyan-fg}` + div +
1128
- costFlag + rpad(`$${t.cost.toFixed(2)}`, colCost) + costEnd);
1129
- });
1130
- }
1131
- else {
1132
- // Minimal mode: guaranteed no-wrap fallback.
1133
- const colNum = 4;
1134
- const colCtx = 6;
1135
- const colCost = 6;
1136
- header = `${pad('Turn', colNum)}${div}${rpad('Context', colCtx)}${div}${rpad('Cost', colCost)}`;
1137
- separator = `${'─'.repeat(colNum)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(colCost)}`;
1138
- rows = turns.map(t => {
1139
- const costFlag = t.cost > 1.0 ? '{red-fg}' : t.cost > 0.5 ? '{yellow-fg}' : '{white-fg}';
1140
- const costEnd = t.cost > 1.0 ? '{/red-fg}' : t.cost > 0.5 ? '{/yellow-fg}' : '{/white-fg}';
1141
- return (pad(String(t.turn), colNum) + div +
1142
- rpad(`${t.contextPct.toFixed(0)}%`, colCtx) + div +
1143
- costFlag + rpad(`$${t.cost.toFixed(2)}`, colCost) + costEnd);
1144
- });
1084
+ // Always show all 7 columns: Turn, Model, Context, Cache Rd, Cache Wr, Output, Cost
1085
+ // Shrink flex columns to fit narrow panes instead of dropping them.
1086
+ const colNum = 4;
1087
+ const colM = 7;
1088
+ const colCtx = 7;
1089
+ const colCost = 8;
1090
+ const nDividers = 6;
1091
+ const fixedW = colNum + colM + colCtx + colCost;
1092
+ const flexTotal = Math.max(0, w - fixedW - nDividers);
1093
+ let rdW = Math.max(5, Math.floor(flexTotal * 0.35));
1094
+ let wrW = Math.max(5, Math.floor(flexTotal * 0.30));
1095
+ let outW = Math.max(6, flexTotal - rdW - wrW);
1096
+ // Trim flex columns if they overflow available width
1097
+ let totalW = fixedW + nDividers + rdW + wrW + outW;
1098
+ if (totalW > w) {
1099
+ let overflow = totalW - w;
1100
+ const trimOut = Math.min(overflow, Math.max(0, outW - 4));
1101
+ outW -= trimOut;
1102
+ overflow -= trimOut;
1103
+ if (overflow > 0) {
1104
+ const trimWr = Math.min(overflow, Math.max(0, wrW - 4));
1105
+ wrW -= trimWr;
1106
+ overflow -= trimWr;
1107
+ }
1108
+ if (overflow > 0) {
1109
+ const trimRd = Math.min(overflow, Math.max(0, rdW - 4));
1110
+ rdW -= trimRd;
1111
+ }
1145
1112
  }
1113
+ header = `{bold}${pad('Turn', colNum)}${div}${pad('Model', colM)}${div}${pad('Context', colCtx)}${div}${rpad('Cache Rd', rdW)}${div}${rpad('Cache Wr', wrW)}${div}${rpad('Output', outW)}${div}${rpad('Cost', colCost)}{/bold}`;
1114
+ separator = `{gray-fg}${'─'.repeat(colNum)}┼${'─'.repeat(colM)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(rdW)}┼${'─'.repeat(wrW)}┼${'─'.repeat(outW)}┼${'─'.repeat(colCost)}{/gray-fg}`;
1115
+ rows = turns.map(t => {
1116
+ const mTag = modelTag(t.routedModel);
1117
+ const mColor = t.routedModel.includes('haiku') ? 'green' : t.routedModel.includes('sonnet') ? 'blue' : 'magenta';
1118
+ const costFlag = t.cost > 1.0 ? '{red-fg}' : t.cost > 0.5 ? '{yellow-fg}' : '{white-fg}';
1119
+ const costEnd = t.cost > 1.0 ? '{/red-fg}' : t.cost > 0.5 ? '{/yellow-fg}' : '{/white-fg}';
1120
+ return (pad(String(t.turn), colNum) + div +
1121
+ `{${mColor}-fg}${pad(mTag, colM)}{/${mColor}-fg}` + div +
1122
+ pad(`${t.contextPct.toFixed(0)}%`, colCtx) + div +
1123
+ `{green-fg}${rpad(fmtK(t.cacheRead), rdW)}{/green-fg}` + div +
1124
+ `{yellow-fg}${rpad(fmtK(t.cacheCreate), wrW)}{/yellow-fg}` + div +
1125
+ `{cyan-fg}${rpad(fmtK(t.output), outW)}{/cyan-fg}` + div +
1126
+ costFlag + rpad(`$${t.cost.toFixed(2)}`, colCost) + costEnd);
1127
+ });
1146
1128
  const lines = [header, separator, ...rows];
1147
1129
  turnBox.setContent(lines.join('\n'));
1148
1130
  // Restore scroll position AFTER content update
@@ -1404,11 +1386,13 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
1404
1386
  // Delay first ccusage call — let blessed render first, then load heavy data
1405
1387
  setTimeout(() => updateWindowBox(), 2000);
1406
1388
  const pollInterval = setInterval(updateDashboard, refreshMs);
1389
+ // Animation interval — slower on Windows to reduce blessed redraw flicker
1390
+ const animMs = process.platform === 'win32' ? 1000 : 500;
1407
1391
  const headerAnimInterval = setInterval(() => {
1408
1392
  // Keep advancing across the full session label; wrap at a large value.
1409
1393
  waveOffset = (waveOffset + 1) % 1000000;
1410
1394
  renderHeader();
1411
- }, 500);
1395
+ }, animMs);
1412
1396
  const fortuneInterval = setInterval(() => {
1413
1397
  if (activeFortunes.length === 0)
1414
1398
  return;
@@ -912,10 +912,11 @@ function launchWithWindowsTerminal(options) {
912
912
  if (options.noProxy)
913
913
  runArgs.push('--skip-proxy');
914
914
  // Write a temp batch file to avoid all quoting issues
915
+ // wt.exe doesn't resolve PATH for npm global bins — must use `cmd /c`
915
916
  const batPath = path.join(os.tmpdir(), `ekkos-wt-${launchTime}.cmd`);
916
917
  const batContent = [
917
918
  '@echo off',
918
- `wt --title "Claude Code" -d "${cwd}" ekkos ${runArgs.join(' ')} ; split-pane -V -s 0.4 --title "ekkOS Dashboard" -d "${cwd}" ekkos dashboard --wait-for-new --refresh 2000`,
919
+ `wt --title "Claude Code" -d "${cwd}" cmd /c ekkos ${runArgs.join(' ')} ; split-pane -V -s 0.4 --title "ekkOS Dashboard" -d "${cwd}" cmd /c ekkos dashboard --wait-for-new --refresh 2000`,
919
920
  ].join('\r\n');
920
921
  try {
921
922
  fs.writeFileSync(batPath, batContent);
@@ -1083,7 +1084,10 @@ async function run(options) {
1083
1084
  }
1084
1085
  }
1085
1086
  // Check PTY availability early (deterministic, no async race)
1086
- const loadedPty = await loadPty();
1087
+ // Windows: SKIP node-pty entirely. ConPTY corrupts Ink's cursor management sequences
1088
+ // (hide/show/move), causing ghost block cursor artifacts. Use stdio:'inherit' spawn
1089
+ // instead so Ink gets direct terminal access. Hooks still handle session tracking.
1090
+ const loadedPty = isWindows ? null : await loadPty();
1087
1091
  const usePty = loadedPty !== null;
1088
1092
  // ══════════════════════════════════════════════════════════════════════════
1089
1093
  // CONCURRENT STARTUP: Spawn Claude while animation runs
@@ -1319,8 +1323,10 @@ async function run(options) {
1319
1323
  console.log('');
1320
1324
  }
1321
1325
  else {
1326
+ // Static banner for Windows / remote / no-splash — no cursor manipulation
1327
+ console.log('');
1328
+ console.log(chalk_1.default.hex('#FF6B35').bold(' ekkOS_Pulse') + chalk_1.default.gray(' — Context is finite. Intelligence isn\'t.'));
1322
1329
  console.log('');
1323
- console.log(chalk_1.default.cyan(' ekkOS remote session ready'));
1324
1330
  if (bypass) {
1325
1331
  console.log(chalk_1.default.yellow(' ⚡ Bypass permissions mode enabled'));
1326
1332
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "1.2.15",
3
+ "version": "1.2.16",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {