termux-dev 1.4.19 → 1.6.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/dist/cli/index.js CHANGED
@@ -7,6 +7,7 @@ import path from 'path';
7
7
  import fs from 'fs/promises';
8
8
  import fsSync from 'fs';
9
9
  import { execSync, execFileSync } from 'child_process';
10
+ import { DEVX_VERSION } from '../core/types.js';
10
11
  import { History } from '../core/history.js';
11
12
  import { Agent } from '../core/loop.js';
12
13
  import { buildSystemPrompt } from '../prompts/builder.js';
@@ -32,6 +33,10 @@ import { CustomCommandManager } from '../core/commands.js';
32
33
  import { UsageTracker } from '../core/usage.js';
33
34
  import { SessionExporter } from './export.js';
34
35
  import { MCPManager } from '../mcp/manager.js';
36
+ import { LiveTelemetryTracker } from './telemetry.js';
37
+ import { renderCleanStatus, renderPowerlineStatus, GitStatusCache } from './statusline.js';
38
+ import { formatDiffBox } from './diff.js';
39
+ import { drawBanner } from './banner.js';
35
40
  const CONFIG_PATH = path.join(os.homedir(), '.devxrc.json');
36
41
  function maskApiKey(key) {
37
42
  if (!key)
@@ -473,40 +478,10 @@ function clearTerminalScreen() {
473
478
  console.clear();
474
479
  }
475
480
  }
476
- function drawLogo() {
481
+ function drawLogo(bannerMode = 'full') {
477
482
  const cols = process.stdout.columns || 80;
478
483
  clearTerminalScreen();
479
- const theme = getCurrentTheme();
480
- if (cols < 56) {
481
- // Ultra-clean compact ASCII for small mobile screens (width: ~26 chars)
482
- const logo = [
483
- '',
484
- theme.colorFn(' █▀▀▄ █▀▀▀ █ █ █ █'),
485
- theme.colorFn(' █ █ █▀▀▀ ▀▄▀ ▀▄▀ '),
486
- theme.colorFn(' █▄▄▀ █▄▄▄ ▀ ▀ ▀ '),
487
- ' ' + theme.boldFn('v1.4.19'),
488
- ''
489
- ];
490
- for (const line of logo) {
491
- console.log(line);
492
- }
493
- }
494
- else {
495
- // Full TERMUX-DEV banner (width: 53 chars)
496
- const indent = cols < 68 ? ' ' : ' ';
497
- const logo = [
498
- '',
499
- indent + theme.colorFn('▀▀▀█▀▀▀ █▀▀▀ █▀▀█ █▄ ▄█ █ █ ▀▄ ▄▀ █▀▀▄ █▀▀▀ █ █'),
500
- indent + theme.colorFn(' █ █▀▀▀ █▄▄▀ █ █ █ █ █ █ ▀▀ █ █ █▀▀▀ █ █'),
501
- indent + theme.colorFn(' █ █▄▄▄ █ ▀▄ █ █ ▀▄▄▀ ▄▀ ▀▄ █▄▄▀ █▄▄▄ ▀▄▀ '),
502
- indent + theme.boldFn('v1.4.19'),
503
- ''
504
- ];
505
- for (const line of logo) {
506
- console.log(line);
507
- }
508
- }
509
- console.log();
484
+ drawBanner(bannerMode, DEVX_VERSION, cols);
510
485
  }
511
486
  function formatSessionTime(timestamp) {
512
487
  const diff = Date.now() - timestamp;
@@ -548,11 +523,11 @@ function formatToolGeneratingLabel(name, targetHint) {
548
523
  return `→ Clarifying questions...`;
549
524
  return `→ ${name}${target}...`;
550
525
  }
551
- async function handleSessionDelete() {
526
+ async function handleSessionDelete(bannerMode) {
552
527
  while (true) {
553
528
  const sessions = await SessionManager.listSessions();
554
529
  if (sessions.length === 0) {
555
- drawLogo();
530
+ drawLogo(bannerMode);
556
531
  p.log.warn('No saved sessions found.');
557
532
  break;
558
533
  }
@@ -587,7 +562,7 @@ async function handleSessionDelete() {
587
562
  break;
588
563
  }
589
564
  if (!chosenSessionId || chosenSessionId === '__cancel__') {
590
- drawLogo();
565
+ drawLogo(bannerMode);
591
566
  p.log.info('Session deletion finished.');
592
567
  break;
593
568
  }
@@ -598,7 +573,7 @@ async function handleSessionDelete() {
598
573
  });
599
574
  if (!p.isCancel(confirmAll) && confirmAll) {
600
575
  const count = await SessionManager.deleteAllSessions();
601
- drawLogo();
576
+ drawLogo(bannerMode);
602
577
  p.log.success(`Deleted all ${count} sessions.`);
603
578
  break;
604
579
  }
@@ -607,7 +582,7 @@ async function handleSessionDelete() {
607
582
  const target = sessions.find(s => s.id === chosenSessionId);
608
583
  const title = target ? target.title : chosenSessionId;
609
584
  const ok = await SessionManager.deleteSession(chosenSessionId);
610
- drawLogo();
585
+ drawLogo(bannerMode);
611
586
  if (ok) {
612
587
  p.log.success(`Deleted session: "${title}"`);
613
588
  }
@@ -632,7 +607,7 @@ async function handleThemeSelect(config) {
632
607
  config.theme = selected;
633
608
  await saveConfig(config);
634
609
  const th = setActiveTheme(selected);
635
- drawLogo();
610
+ drawLogo(config.banner || 'full');
636
611
  p.log.success(th.boldFn(`🎨 Theme switched to ${th.emoji} ${th.name}!`));
637
612
  }
638
613
  }
@@ -645,13 +620,18 @@ async function handleSettings(config) {
645
620
  const maxIterLabel = maxIter >= 9999 ? 'Unlimited' : `${maxIter} steps`;
646
621
  const currentTh = getCurrentTheme();
647
622
  const choice = await select({
648
- message: `${pc.bold('⚙️ Settings')} ${pc.dim(`(devx v1.4.19 • theme: ${currentTh.name})`)}`,
623
+ message: `${pc.bold('⚙️ Settings')} ${pc.dim(`(devx v${DEVX_VERSION} • theme: ${currentTh.name})`)}`,
649
624
  choices: [
650
625
  {
651
626
  name: `🎨 Color Theme: ${currentTh.emoji} ${currentTh.name}`,
652
627
  value: 'change_theme',
653
628
  description: `Switch UI accent colors (${currentTh.desc})`
654
629
  },
630
+ {
631
+ name: `Interface: ${config.ui?.layout === 'classic' ? 'Classic' : 'Clean'}`,
632
+ value: 'toggle_layout',
633
+ description: 'Switch between the compact layout and the original terminal UI'
634
+ },
655
635
  {
656
636
  name: `${config.pureBlackTheme !== false ? pc.green('🖤 Pure Black Background: ON') : pc.yellow('🖤 Pure Black Background: OFF')}`,
657
637
  value: 'toggle_black_theme',
@@ -686,7 +666,7 @@ async function handleSettings(config) {
686
666
  description: 'Limit how many tool steps (file edits, terminal commands) agent can do per request'
687
667
  },
688
668
  {
689
- name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.4.19 by ApvCode)')}`,
669
+ name: `${currentTh.colorFn('✨ About devx')} ${pc.dim(`(v${DEVX_VERSION} by ApvCode)`)}`,
690
670
  value: 'about',
691
671
  description: 'Terminal-Native AI Coding Agent created by ApvCode (https://github.com/apvcode/Termux-Dev)'
692
672
  },
@@ -701,8 +681,16 @@ async function handleSettings(config) {
701
681
  await handleThemeSelect(config);
702
682
  continue;
703
683
  }
684
+ if (choice === 'toggle_layout') {
685
+ config.ui = config.ui || {};
686
+ config.ui.layout = config.ui.layout === 'classic' ? 'clean' : 'classic';
687
+ config.banner = config.ui.layout === 'clean' ? 'clean' : 'full';
688
+ await saveConfig(config);
689
+ drawLogo(config.banner);
690
+ continue;
691
+ }
704
692
  if (choice === 'about') {
705
- p.note(`⚡ devx v1.4.19 — Terminal-Native AI Coding Agent\n` +
693
+ p.note(`⚡ devx v${DEVX_VERSION} — Terminal-Native AI Coding Agent\n` +
706
694
  `🎨 Theme: ${currentTh.emoji} ${currentTh.name}\n` +
707
695
  `👤 Author: ApvCode (https://github.com/apvcode)\n` +
708
696
  `🌟 Repository: https://github.com/apvcode/Termux-Dev\n` +
@@ -719,7 +707,7 @@ async function handleSettings(config) {
719
707
  else {
720
708
  resetTerminalTheme();
721
709
  }
722
- drawLogo();
710
+ drawLogo(config.banner || 'full');
723
711
  p.log.success(`Pure Black background: ${config.pureBlackTheme ? pc.bold(pc.green('ON (Deep Black)')) : pc.bold(pc.yellow('OFF (System Default)'))}`);
724
712
  continue;
725
713
  }
@@ -798,7 +786,7 @@ export async function main() {
798
786
  program
799
787
  .name('devx')
800
788
  .description('Terminal-native AI coding assistant and vibe-coding agent')
801
- .version('1.4.19')
789
+ .version(DEVX_VERSION)
802
790
  .option('-p, --prompt <task>', 'Run one-shot task non-interactively (headless mode)')
803
791
  .option('-y, --yolo', 'Automatically approve all tool executions without confirmation')
804
792
  .option('-m, --model <model>', 'Specify AI model to use for this execution')
@@ -810,6 +798,9 @@ export async function main() {
810
798
  const planModeInitial = !!options.plan;
811
799
  const isHeadless = !!options.prompt;
812
800
  let config = await loadConfig(!isHeadless);
801
+ config.banner = config.ui?.layout === 'classic'
802
+ ? config.banner || 'full'
803
+ : config.banner === 'off' ? 'off' : 'clean';
813
804
  if (options.model) {
814
805
  config.model = options.model;
815
806
  config.maxContextTokens = getModelContextLimit(options.model);
@@ -866,7 +857,7 @@ export async function main() {
866
857
  const sysPrompt = await buildSystemPrompt(planMode);
867
858
  history.addMessage({ role: 'system', content: sysPrompt });
868
859
  const sessionManager = new SessionManager(config.model, planMode);
869
- drawLogo();
860
+ drawLogo(config.banner || 'full');
870
861
  await runStartupUpdateCheck(config);
871
862
  await MCPManager.getInstance().init();
872
863
  let totalSessionCost = 0;
@@ -889,36 +880,68 @@ export async function main() {
889
880
  const cols = process.stdout.columns || 80;
890
881
  const modeName = planMode ? 'PLAN' : 'AGENT';
891
882
  const theme = getCurrentTheme();
892
- // Shorten model name if too long on narrow mobile screens
893
- let displayModel = config.model;
894
- if (cols < 75 && displayModel.length > 20) {
895
- const parts = displayModel.split('/');
896
- displayModel = parts.length > 1 ? parts.slice(1).join('/') : displayModel;
897
- if (displayModel.length > 20) {
898
- displayModel = displayModel.slice(0, 17) + '...';
899
- }
883
+ if (config.ui?.layout !== 'classic') {
884
+ console.log(renderCleanStatus({
885
+ mode: modeName,
886
+ model: config.model,
887
+ currentTokens,
888
+ maxTokens,
889
+ cost: totalSessionCost,
890
+ cols,
891
+ isYolo: !!config.autoApprove
892
+ }) + '\n');
900
893
  }
901
- const badge = theme.badgeFn(`devx | ${modeName} | ${displayModel}`);
902
- if (cols < 75) {
903
- // 2-line layout for mobile screens: perfectly aligned with clack box borders
904
- p.intro(`${badge}\n${pc.dim('│')} ${pc.dim(tokenStats)}`);
894
+ else if (config.ui?.powerlineStatus) {
895
+ const statusLine = renderPowerlineStatus({
896
+ mode: modeName,
897
+ model: config.model,
898
+ currentTokens,
899
+ maxTokens,
900
+ cost: totalSessionCost,
901
+ cols,
902
+ maxCostUSD: config.maxCostUSD,
903
+ isYolo: !!config.autoApprove
904
+ });
905
+ p.intro(statusLine);
905
906
  }
906
907
  else {
907
- // 1-line layout for wider desktop screens
908
- p.intro(`${badge} ${pc.dim(tokenStats)}`);
908
+ // Shorten model name if too long on narrow mobile screens
909
+ let displayModel = config.model;
910
+ if (cols < 75 && displayModel.length > 20) {
911
+ const parts = displayModel.split('/');
912
+ displayModel = parts.length > 1 ? parts.slice(1).join('/') : displayModel;
913
+ if (displayModel.length > 20) {
914
+ displayModel = displayModel.slice(0, 17) + '...';
915
+ }
916
+ }
917
+ const badge = theme.badgeFn(`devx | ${modeName} | ${displayModel}`);
918
+ if (cols < 75) {
919
+ // 2-line layout for mobile screens: perfectly aligned with clack box borders
920
+ p.intro(`${badge}\n${pc.dim('│')} ${pc.dim(tokenStats)}`);
921
+ }
922
+ else {
923
+ // 1-line layout for wider desktop screens
924
+ p.intro(`${badge} ${pc.dim(tokenStats)}`);
925
+ }
909
926
  }
910
927
  let answer = '';
911
928
  if (autoTriggerPrompt) {
912
929
  answer = autoTriggerPrompt;
913
930
  autoTriggerPrompt = '';
914
- console.log(theme.colorFn('◆') + ' ' + pc.bold(pc.white(answer)));
931
+ console.log(config.ui?.layout === 'classic'
932
+ ? theme.colorFn('◆') + ' ' + pc.bold(pc.white(answer))
933
+ : pc.cyan('›') + ' ' + answer);
915
934
  }
916
935
  else {
917
936
  const inputStr = await askPrompt({
918
937
  message: 'Ask anything...',
919
- placeholder: 'Fix a TODO, type /help, or press Tab to switch mode',
938
+ placeholder: config.ui?.layout === 'classic'
939
+ ? 'Fix a TODO, type /help, or press Tab to switch mode'
940
+ : 'Ask or describe a task…',
920
941
  initialValue: currentDraft,
921
- planMode
942
+ planMode,
943
+ isYolo: !!config.autoApprove,
944
+ ui: config.ui
922
945
  });
923
946
  if (inputStr.startsWith('__TOGGLE_MODE__:')) {
924
947
  currentDraft = inputStr.slice('__TOGGLE_MODE__:'.length);
@@ -1022,7 +1045,7 @@ export async function main() {
1022
1045
  continue;
1023
1046
  }
1024
1047
  if (picked === '/session del') {
1025
- await handleSessionDelete();
1048
+ await handleSessionDelete(config.banner || 'full');
1026
1049
  continue;
1027
1050
  }
1028
1051
  cmd = picked;
@@ -1034,7 +1057,7 @@ export async function main() {
1034
1057
  }
1035
1058
  if (cmd === '/settings') {
1036
1059
  config = await handleSettings(config);
1037
- drawLogo();
1060
+ drawLogo(config.banner || 'full');
1038
1061
  continue;
1039
1062
  }
1040
1063
  if (cmd === '/update') {
@@ -1066,9 +1089,7 @@ export async function main() {
1066
1089
  break;
1067
1090
  }
1068
1091
  if (cmd === '/clear') {
1069
- history.clear();
1070
- history.addMessage({ role: 'system', content: await buildSystemPrompt(planMode) });
1071
- p.log.success('History cleared.');
1092
+ drawLogo(config.banner || 'full');
1072
1093
  continue;
1073
1094
  }
1074
1095
  if (cmd === '/new' || cmd === '/reset') {
@@ -1076,7 +1097,7 @@ export async function main() {
1076
1097
  history = new History();
1077
1098
  history.addMessage({ role: 'system', content: await buildSystemPrompt(planMode) });
1078
1099
  totalSessionCost = 0;
1079
- drawLogo();
1100
+ drawLogo(config.banner || 'full');
1080
1101
  const initialPrompt = parts.slice(1).join(' ').trim();
1081
1102
  if (initialPrompt) {
1082
1103
  p.log.success('Started new session with prompt.');
@@ -1090,7 +1111,7 @@ export async function main() {
1090
1111
  else if (cmd === '/session' || cmd === '/sessions' || cmd === '/resume' || cmd === '/history') {
1091
1112
  const sub = (parts[1] || '').toLowerCase();
1092
1113
  if (sub === 'del' || sub === 'delete' || sub === 'rm') {
1093
- await handleSessionDelete();
1114
+ await handleSessionDelete(config.banner || 'full');
1094
1115
  continue;
1095
1116
  }
1096
1117
  const sessions = await SessionManager.listSessions();
@@ -1132,7 +1153,7 @@ export async function main() {
1132
1153
  continue;
1133
1154
  }
1134
1155
  if (chosenSessionId === '__delete_mode__') {
1135
- await handleSessionDelete();
1156
+ await handleSessionDelete(config.banner || 'full');
1136
1157
  continue;
1137
1158
  }
1138
1159
  const loaded = await SessionManager.loadSession(chosenSessionId);
@@ -1150,7 +1171,7 @@ export async function main() {
1150
1171
  planMode = loaded.planMode;
1151
1172
  }
1152
1173
  totalSessionCost = loaded.totalCost || 0;
1153
- drawLogo();
1174
+ drawLogo(config.banner || 'full');
1154
1175
  const nonSysMessages = loaded.messages.filter(m => m.role !== 'system');
1155
1176
  const totalMsgs = nonSysMessages.length;
1156
1177
  const displayLimit = 20;
@@ -1202,7 +1223,7 @@ export async function main() {
1202
1223
  planMode = true;
1203
1224
  const newSys = await buildSystemPrompt(true);
1204
1225
  history.updateSystemPrompt(newSys);
1205
- drawLogo();
1226
+ drawLogo(config.banner || 'full');
1206
1227
  p.log.success(pc.cyan('Switched to PLAN mode (Architect & Planner). Modifying tools are disabled.'));
1207
1228
  continue;
1208
1229
  }
@@ -1210,7 +1231,7 @@ export async function main() {
1210
1231
  planMode = false;
1211
1232
  const newSys = await buildSystemPrompt(false);
1212
1233
  history.updateSystemPrompt(newSys);
1213
- drawLogo();
1234
+ drawLogo(config.banner || 'full');
1214
1235
  p.log.success(pc.green('Switched to AGENT mode (Coder & Executor). Full tools enabled.'));
1215
1236
  continue;
1216
1237
  }
@@ -1374,11 +1395,6 @@ export async function main() {
1374
1395
  }
1375
1396
  continue;
1376
1397
  }
1377
- if (cmd === '/clear') {
1378
- console.clear();
1379
- drawLogo();
1380
- continue;
1381
- }
1382
1398
  if (cmd === '/session' && parts[1]?.toLowerCase() !== 'del') {
1383
1399
  const cur = sessionManager.getSession();
1384
1400
  const nonSys = history.getMessages().filter(m => m.role !== 'system');
@@ -1440,7 +1456,7 @@ export async function main() {
1440
1456
  config.theme = matched.id;
1441
1457
  await saveConfig(config);
1442
1458
  const th = setActiveTheme(matched.id);
1443
- drawLogo();
1459
+ drawLogo(config.banner || 'full');
1444
1460
  p.log.success(th.boldFn(`🎨 Theme switched to ${th.emoji} ${th.name}!`));
1445
1461
  }
1446
1462
  else {
@@ -1526,7 +1542,7 @@ export async function main() {
1526
1542
  }
1527
1543
  if (cmd === '/model') {
1528
1544
  const res = await selectModel(config.baseUrl, config.apiKey, config.model, config.provider || '', true);
1529
- drawLogo();
1545
+ drawLogo(config.banner || 'full');
1530
1546
  if (res) {
1531
1547
  config.model = res.model;
1532
1548
  config.maxContextTokens = res.contextLimit;
@@ -1540,7 +1556,7 @@ export async function main() {
1540
1556
  }
1541
1557
  if (cmd === '/provider' || cmd === '/providers') {
1542
1558
  const newConfig = await selectProvider(config, true);
1543
- drawLogo();
1559
+ drawLogo(config.banner || 'full');
1544
1560
  if (newConfig) {
1545
1561
  config = newConfig;
1546
1562
  p.log.success(`Switched provider to: ${pc.bold(config.provider || 'custom')} (${config.model})`);
@@ -1615,30 +1631,35 @@ export async function main() {
1615
1631
  };
1616
1632
  const agent = new Agent(agentConfig, provider, tools, history, guard);
1617
1633
  const taskStartTime = Date.now();
1618
- const s = p.spinner();
1619
- s.start('Connecting...');
1620
- let spinnerActive = true;
1634
+ const abortController = new AbortController();
1635
+ let aborted = false;
1636
+ let isConnecting = true;
1637
+ let spinnerActive = false;
1621
1638
  let thinkingActive = false;
1622
1639
  let thinkingStartTime = 0;
1623
1640
  let textActive = false;
1641
+ const s = p.spinner();
1624
1642
  const streamer = new MarkdownStreamer();
1625
1643
  const thoughtStreamer = new SmoothStreamer((chunk) => {
1626
1644
  process.stdout.write(pc.dim(chunk));
1627
1645
  });
1646
+ const telemetry = new LiveTelemetryTracker();
1647
+ telemetry.start('connecting');
1628
1648
  const finishThinking = (extraNewline = true) => {
1629
1649
  if (!thinkingActive)
1630
1650
  return;
1631
1651
  thoughtStreamer.flush();
1632
1652
  const elapsedSec = thinkingStartTime > 0 ? ((Date.now() - thinkingStartTime) / 1000).toFixed(1) : '0.0';
1653
+ const teleStats = config.ui?.liveTelemetry && telemetry.getTotalTokens() > 0
1654
+ ? ` · ${LiveTelemetryTracker.formatTokenCount(telemetry.getTotalTokens())} tok · ${telemetry.getTokensPerSecond()} tok/s`
1655
+ : '';
1633
1656
  const width = Math.min(process.stdout.columns || 40, 52);
1634
- const label = `─── Thought for ${elapsedSec}s `;
1657
+ const label = `─── Thought for ${elapsedSec}s${teleStats} `;
1635
1658
  const fillLen = Math.max(2, width - label.length);
1636
1659
  process.stdout.write('\n\n' + pc.dim(label + '─'.repeat(fillLen)) + (extraNewline ? '\n\n' : '\n'));
1637
1660
  thinkingActive = false;
1638
1661
  thinkingStartTime = 0;
1639
1662
  };
1640
- const abortController = new AbortController();
1641
- let aborted = false;
1642
1663
  const stopGeneration = () => {
1643
1664
  if (aborted)
1644
1665
  return;
@@ -1648,13 +1669,19 @@ export async function main() {
1648
1669
  s.stop();
1649
1670
  spinnerActive = false;
1650
1671
  }
1672
+ telemetry.reset();
1651
1673
  thoughtStreamer.reset();
1652
1674
  finishThinking(true);
1653
1675
  streamer.finish();
1654
1676
  if (textActive) {
1655
1677
  process.stdout.write('\n');
1656
1678
  }
1657
- console.log(pc.yellow('\n⏹ Generation stopped / Ответ остановлен (Ctrl+C).'));
1679
+ if (isConnecting) {
1680
+ console.log(pc.yellow('\n⏹ Подключение отменено / Connection cancelled (Ctrl+C).'));
1681
+ }
1682
+ else {
1683
+ console.log(pc.yellow('\n⏹ Generation stopped / Ответ остановлен (Ctrl+C).'));
1684
+ }
1658
1685
  };
1659
1686
  const onSigInt = () => {
1660
1687
  stopGeneration();
@@ -1675,6 +1702,8 @@ export async function main() {
1675
1702
  }
1676
1703
  catch { }
1677
1704
  }
1705
+ s.start('Connecting...');
1706
+ spinnerActive = true;
1678
1707
  const turnStartTime = Date.now();
1679
1708
  let toolsExecutedCount = 0;
1680
1709
  try {
@@ -1682,6 +1711,9 @@ export async function main() {
1682
1711
  if (aborted)
1683
1712
  break;
1684
1713
  if (event.type === 'reasoning_delta') {
1714
+ isConnecting = false;
1715
+ telemetry.setPhase('thinking');
1716
+ telemetry.recordTokens(telemetry.estimateTokensFromText(event.delta));
1685
1717
  if (spinnerActive) {
1686
1718
  s.stop();
1687
1719
  spinnerActive = false;
@@ -1694,6 +1726,9 @@ export async function main() {
1694
1726
  thoughtStreamer.push(event.delta);
1695
1727
  }
1696
1728
  else if (event.type === 'text_delta') {
1729
+ isConnecting = false;
1730
+ telemetry.setPhase('generating');
1731
+ telemetry.recordTokens(telemetry.estimateTokensFromText(event.delta));
1697
1732
  if (spinnerActive) {
1698
1733
  s.stop();
1699
1734
  spinnerActive = false;
@@ -1705,6 +1740,7 @@ export async function main() {
1705
1740
  streamer.push(event.delta);
1706
1741
  }
1707
1742
  else if (event.type === 'text') {
1743
+ isConnecting = false;
1708
1744
  if (spinnerActive) {
1709
1745
  s.stop();
1710
1746
  spinnerActive = false;
@@ -1715,19 +1751,27 @@ export async function main() {
1715
1751
  }
1716
1752
  }
1717
1753
  else if (event.type === 'tool_generating') {
1754
+ isConnecting = false;
1755
+ telemetry.setPhase('tool');
1718
1756
  const theme = getCurrentTheme();
1719
1757
  const label = formatToolGeneratingLabel(event.name, event.targetHint);
1758
+ const teleStats = telemetry.getTotalTokens() > 0
1759
+ ? ` · ${LiveTelemetryTracker.formatTokenCount(telemetry.getTotalTokens())} tok · ${telemetry.getTokensPerSecond()} tok/s`
1760
+ : '';
1761
+ const telemetrySuffix = config.ui?.liveTelemetry ? ` ${pc.dim(`(${telemetry.getElapsedSeconds()}s${teleStats})`)}` : '';
1720
1762
  if (!spinnerActive) {
1721
1763
  streamer.finish();
1722
1764
  finishThinking(false);
1723
- s.start(theme.boldFn(label));
1765
+ s.start(theme.boldFn(label) + telemetrySuffix);
1724
1766
  spinnerActive = true;
1725
1767
  }
1726
1768
  else {
1727
- s.message(theme.boldFn(label));
1769
+ s.message(theme.boldFn(label) + telemetrySuffix);
1728
1770
  }
1729
1771
  }
1730
1772
  else if (event.type === 'tool_start') {
1773
+ isConnecting = false;
1774
+ telemetry.reset();
1731
1775
  if (spinnerActive) {
1732
1776
  s.stop();
1733
1777
  spinnerActive = false;
@@ -1746,6 +1790,7 @@ export async function main() {
1746
1790
  }
1747
1791
  else if (event.type === 'tool_end') {
1748
1792
  toolsExecutedCount++;
1793
+ GitStatusCache.invalidate();
1749
1794
  if (spinnerActive) {
1750
1795
  s.stop();
1751
1796
  spinnerActive = false;
@@ -1757,50 +1802,8 @@ export async function main() {
1757
1802
  process.stdout.write(parsed.displayCard);
1758
1803
  }
1759
1804
  else if (parsed.action === 'edit' && parsed.diffLines) {
1760
- const theme = getCurrentTheme();
1761
1805
  const cols = Math.min(process.stdout.columns || 80, 80);
1762
- const boxWidth = Math.max(30, Math.min(cols - 4, 76));
1763
- const fileName = typeof parsed.path === 'string' ? parsed.path.split(/[\/\\]/).pop() || 'file' : 'file';
1764
- const fillCount = Math.max(2, boxWidth - 5 - fileName.length);
1765
- console.log(theme.colorFn('┌─ ') + pc.bold(fileName) + ' ' + theme.colorFn('─'.repeat(fillCount) + '┐'));
1766
- const maxShown = Math.min(parsed.diffLines.length, 30);
1767
- for (let i = 0; i < maxShown; i++) {
1768
- const line = parsed.diffLines[i];
1769
- const match = line.match(/^(\d+)(\s+[+\- ]\s+)(.*)$/);
1770
- if (match) {
1771
- const lineNum = match[1].padStart(4, ' ');
1772
- const symbol = match[2];
1773
- const contentStr = match[3] || '';
1774
- const prefix = symbol.includes('-') ? '-' : symbol.includes('+') ? '+' : ' ';
1775
- const maxCodeLen = Math.max(10, boxWidth - 11);
1776
- const paddedCode = contentStr.length > maxCodeLen
1777
- ? contentStr.substring(0, maxCodeLen - 1) + '…'
1778
- : contentStr.padEnd(maxCodeLen, ' ');
1779
- const innerRow = ` ${lineNum} ${prefix} ${paddedCode} `;
1780
- if (prefix === '-') {
1781
- console.log(theme.colorFn('│') + theme.diffRemoveBg(innerRow) + theme.colorFn('│'));
1782
- }
1783
- else if (prefix === '+') {
1784
- console.log(theme.colorFn('│') + theme.diffAddBg(innerRow) + theme.colorFn('│'));
1785
- }
1786
- else {
1787
- console.log(theme.colorFn('│') + pc.dim(innerRow) + theme.colorFn('│'));
1788
- }
1789
- }
1790
- else {
1791
- const maxLen = Math.max(10, boxWidth - 4);
1792
- const padded = line.length > maxLen ? line.substring(0, maxLen - 1) + '…' : line.padEnd(maxLen, ' ');
1793
- console.log(theme.colorFn('│ ') + pc.white(padded) + theme.colorFn(' │'));
1794
- }
1795
- }
1796
- if (parsed.diffLines.length > maxShown) {
1797
- const dots = `... +${parsed.diffLines.length - maxShown} more lines`;
1798
- const maxLen = Math.max(10, boxWidth - 4);
1799
- const paddedDots = dots.length > maxLen ? dots.substring(0, maxLen - 1) + '…' : dots.padEnd(maxLen, ' ');
1800
- console.log(theme.colorFn('│ ') + pc.dim(paddedDots) + theme.colorFn(' │'));
1801
- }
1802
- console.log(theme.colorFn('└' + '─'.repeat(boxWidth - 2) + '┘'));
1803
- console.log(theme.boldFn(` └─ ${parsed.summary}`));
1806
+ console.log(formatDiffBox(parsed.path, parsed.diffLines, cols, parsed.summary, !!config.ui?.diffColors));
1804
1807
  }
1805
1808
  else if (parsed.summary) {
1806
1809
  console.log(pc.green(` └─ ${parsed.summary}`));
@@ -1824,6 +1827,7 @@ export async function main() {
1824
1827
  totalSessionCost += event.usage.cost || 0;
1825
1828
  }
1826
1829
  else if (event.type === 'reconnecting') {
1830
+ telemetry.reset();
1827
1831
  if (spinnerActive) {
1828
1832
  s.stop();
1829
1833
  spinnerActive = false;
@@ -1878,7 +1882,6 @@ export async function main() {
1878
1882
  if (textActive) {
1879
1883
  process.stdout.write('\n');
1880
1884
  }
1881
- globalSnapshotManager.finishTurn();
1882
1885
  const turnElapsedMs = Date.now() - turnStartTime;
1883
1886
  const shouldNotify = (turnElapsedMs >= 15000) || (toolsExecutedCount > 0) || (planMode && lastPlanReady !== null);
1884
1887
  if (shouldNotify && !aborted) {
@@ -1947,6 +1950,7 @@ export async function main() {
1947
1950
  }
1948
1951
  }
1949
1952
  finally {
1953
+ globalSnapshotManager.finishTurn();
1950
1954
  activeAbortHandler = null;
1951
1955
  process.removeListener('SIGINT', onSigInt);
1952
1956
  if (process.stdin.isTTY) {
@@ -0,0 +1,31 @@
1
+ import pc from 'picocolors';
2
+ export function getModeIndicator(planMode, isYolo = false) {
3
+ if (isYolo) {
4
+ return {
5
+ mode: 'YOLO',
6
+ icon: pc.red('◆'),
7
+ badge: pc.bgRed(pc.bold(pc.white(' YOLO '))),
8
+ accentFn: pc.red
9
+ };
10
+ }
11
+ if (planMode) {
12
+ return {
13
+ mode: 'PLAN',
14
+ icon: pc.cyan('◆'),
15
+ badge: pc.bgCyan(pc.bold(pc.black(' PLAN '))),
16
+ accentFn: pc.cyan
17
+ };
18
+ }
19
+ return {
20
+ mode: 'AGENT',
21
+ icon: pc.green('◆'),
22
+ badge: pc.bgGreen(pc.bold(pc.black(' AGENT '))),
23
+ accentFn: pc.green
24
+ };
25
+ }
26
+ export function formatPromptHeader(planMode, isYolo = false, customMessage) {
27
+ const indicator = getModeIndicator(planMode, isYolo);
28
+ const targetMode = planMode ? 'AGENT' : 'PLAN';
29
+ const msg = customMessage || 'Ask anything...';
30
+ return `${indicator.icon} ${pc.bold(msg)} ${pc.dim(`(Tab = ${targetMode})`)}`;
31
+ }