termux-dev 1.4.19 → 1.5.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 { 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;
@@ -645,7 +620,7 @@ 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}`,
@@ -686,7 +661,7 @@ async function handleSettings(config) {
686
661
  description: 'Limit how many tool steps (file edits, terminal commands) agent can do per request'
687
662
  },
688
663
  {
689
- name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.4.19 by ApvCode)')}`,
664
+ name: `${currentTh.colorFn('✨ About devx')} ${pc.dim(`(v${DEVX_VERSION} by ApvCode)`)}`,
690
665
  value: 'about',
691
666
  description: 'Terminal-Native AI Coding Agent created by ApvCode (https://github.com/apvcode/Termux-Dev)'
692
667
  },
@@ -702,7 +677,7 @@ async function handleSettings(config) {
702
677
  continue;
703
678
  }
704
679
  if (choice === 'about') {
705
- p.note(`⚡ devx v1.4.19 — Terminal-Native AI Coding Agent\n` +
680
+ p.note(`⚡ devx v${DEVX_VERSION} — Terminal-Native AI Coding Agent\n` +
706
681
  `🎨 Theme: ${currentTh.emoji} ${currentTh.name}\n` +
707
682
  `👤 Author: ApvCode (https://github.com/apvcode)\n` +
708
683
  `🌟 Repository: https://github.com/apvcode/Termux-Dev\n` +
@@ -798,7 +773,7 @@ export async function main() {
798
773
  program
799
774
  .name('devx')
800
775
  .description('Terminal-native AI coding assistant and vibe-coding agent')
801
- .version('1.4.19')
776
+ .version(DEVX_VERSION)
802
777
  .option('-p, --prompt <task>', 'Run one-shot task non-interactively (headless mode)')
803
778
  .option('-y, --yolo', 'Automatically approve all tool executions without confirmation')
804
779
  .option('-m, --model <model>', 'Specify AI model to use for this execution')
@@ -866,7 +841,7 @@ export async function main() {
866
841
  const sysPrompt = await buildSystemPrompt(planMode);
867
842
  history.addMessage({ role: 'system', content: sysPrompt });
868
843
  const sessionManager = new SessionManager(config.model, planMode);
869
- drawLogo();
844
+ drawLogo(config.banner || 'full');
870
845
  await runStartupUpdateCheck(config);
871
846
  await MCPManager.getInstance().init();
872
847
  let totalSessionCost = 0;
@@ -889,23 +864,38 @@ export async function main() {
889
864
  const cols = process.stdout.columns || 80;
890
865
  const modeName = planMode ? 'PLAN' : 'AGENT';
891
866
  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
- }
900
- }
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)}`);
867
+ if (config.ui?.powerlineStatus) {
868
+ const statusLine = renderPowerlineStatus({
869
+ mode: modeName,
870
+ model: config.model,
871
+ currentTokens,
872
+ maxTokens,
873
+ cost: totalSessionCost,
874
+ cols,
875
+ maxCostUSD: config.maxCostUSD,
876
+ isYolo: !!config.autoApprove
877
+ });
878
+ p.intro(statusLine);
905
879
  }
906
880
  else {
907
- // 1-line layout for wider desktop screens
908
- p.intro(`${badge} ${pc.dim(tokenStats)}`);
881
+ // Shorten model name if too long on narrow mobile screens
882
+ let displayModel = config.model;
883
+ if (cols < 75 && displayModel.length > 20) {
884
+ const parts = displayModel.split('/');
885
+ displayModel = parts.length > 1 ? parts.slice(1).join('/') : displayModel;
886
+ if (displayModel.length > 20) {
887
+ displayModel = displayModel.slice(0, 17) + '...';
888
+ }
889
+ }
890
+ const badge = theme.badgeFn(`devx | ${modeName} | ${displayModel}`);
891
+ if (cols < 75) {
892
+ // 2-line layout for mobile screens: perfectly aligned with clack box borders
893
+ p.intro(`${badge}\n${pc.dim('│')} ${pc.dim(tokenStats)}`);
894
+ }
895
+ else {
896
+ // 1-line layout for wider desktop screens
897
+ p.intro(`${badge} ${pc.dim(tokenStats)}`);
898
+ }
909
899
  }
910
900
  let answer = '';
911
901
  if (autoTriggerPrompt) {
@@ -918,7 +908,9 @@ export async function main() {
918
908
  message: 'Ask anything...',
919
909
  placeholder: 'Fix a TODO, type /help, or press Tab to switch mode',
920
910
  initialValue: currentDraft,
921
- planMode
911
+ planMode,
912
+ isYolo: !!config.autoApprove,
913
+ ui: config.ui
922
914
  });
923
915
  if (inputStr.startsWith('__TOGGLE_MODE__:')) {
924
916
  currentDraft = inputStr.slice('__TOGGLE_MODE__:'.length);
@@ -1615,30 +1607,35 @@ export async function main() {
1615
1607
  };
1616
1608
  const agent = new Agent(agentConfig, provider, tools, history, guard);
1617
1609
  const taskStartTime = Date.now();
1618
- const s = p.spinner();
1619
- s.start('Connecting...');
1620
- let spinnerActive = true;
1610
+ const abortController = new AbortController();
1611
+ let aborted = false;
1612
+ let isConnecting = true;
1613
+ let spinnerActive = false;
1621
1614
  let thinkingActive = false;
1622
1615
  let thinkingStartTime = 0;
1623
1616
  let textActive = false;
1617
+ const s = p.spinner();
1624
1618
  const streamer = new MarkdownStreamer();
1625
1619
  const thoughtStreamer = new SmoothStreamer((chunk) => {
1626
1620
  process.stdout.write(pc.dim(chunk));
1627
1621
  });
1622
+ const telemetry = new LiveTelemetryTracker();
1623
+ telemetry.start('connecting');
1628
1624
  const finishThinking = (extraNewline = true) => {
1629
1625
  if (!thinkingActive)
1630
1626
  return;
1631
1627
  thoughtStreamer.flush();
1632
1628
  const elapsedSec = thinkingStartTime > 0 ? ((Date.now() - thinkingStartTime) / 1000).toFixed(1) : '0.0';
1629
+ const teleStats = config.ui?.liveTelemetry && telemetry.getTotalTokens() > 0
1630
+ ? ` · ${LiveTelemetryTracker.formatTokenCount(telemetry.getTotalTokens())} tok · ${telemetry.getTokensPerSecond()} tok/s`
1631
+ : '';
1633
1632
  const width = Math.min(process.stdout.columns || 40, 52);
1634
- const label = `─── Thought for ${elapsedSec}s `;
1633
+ const label = `─── Thought for ${elapsedSec}s${teleStats} `;
1635
1634
  const fillLen = Math.max(2, width - label.length);
1636
1635
  process.stdout.write('\n\n' + pc.dim(label + '─'.repeat(fillLen)) + (extraNewline ? '\n\n' : '\n'));
1637
1636
  thinkingActive = false;
1638
1637
  thinkingStartTime = 0;
1639
1638
  };
1640
- const abortController = new AbortController();
1641
- let aborted = false;
1642
1639
  const stopGeneration = () => {
1643
1640
  if (aborted)
1644
1641
  return;
@@ -1648,13 +1645,19 @@ export async function main() {
1648
1645
  s.stop();
1649
1646
  spinnerActive = false;
1650
1647
  }
1648
+ telemetry.reset();
1651
1649
  thoughtStreamer.reset();
1652
1650
  finishThinking(true);
1653
1651
  streamer.finish();
1654
1652
  if (textActive) {
1655
1653
  process.stdout.write('\n');
1656
1654
  }
1657
- console.log(pc.yellow('\n⏹ Generation stopped / Ответ остановлен (Ctrl+C).'));
1655
+ if (isConnecting) {
1656
+ console.log(pc.yellow('\n⏹ Подключение отменено / Connection cancelled (Ctrl+C).'));
1657
+ }
1658
+ else {
1659
+ console.log(pc.yellow('\n⏹ Generation stopped / Ответ остановлен (Ctrl+C).'));
1660
+ }
1658
1661
  };
1659
1662
  const onSigInt = () => {
1660
1663
  stopGeneration();
@@ -1675,6 +1678,8 @@ export async function main() {
1675
1678
  }
1676
1679
  catch { }
1677
1680
  }
1681
+ s.start('Connecting...');
1682
+ spinnerActive = true;
1678
1683
  const turnStartTime = Date.now();
1679
1684
  let toolsExecutedCount = 0;
1680
1685
  try {
@@ -1682,6 +1687,9 @@ export async function main() {
1682
1687
  if (aborted)
1683
1688
  break;
1684
1689
  if (event.type === 'reasoning_delta') {
1690
+ isConnecting = false;
1691
+ telemetry.setPhase('thinking');
1692
+ telemetry.recordTokens(telemetry.estimateTokensFromText(event.delta));
1685
1693
  if (spinnerActive) {
1686
1694
  s.stop();
1687
1695
  spinnerActive = false;
@@ -1694,6 +1702,9 @@ export async function main() {
1694
1702
  thoughtStreamer.push(event.delta);
1695
1703
  }
1696
1704
  else if (event.type === 'text_delta') {
1705
+ isConnecting = false;
1706
+ telemetry.setPhase('generating');
1707
+ telemetry.recordTokens(telemetry.estimateTokensFromText(event.delta));
1697
1708
  if (spinnerActive) {
1698
1709
  s.stop();
1699
1710
  spinnerActive = false;
@@ -1705,6 +1716,7 @@ export async function main() {
1705
1716
  streamer.push(event.delta);
1706
1717
  }
1707
1718
  else if (event.type === 'text') {
1719
+ isConnecting = false;
1708
1720
  if (spinnerActive) {
1709
1721
  s.stop();
1710
1722
  spinnerActive = false;
@@ -1715,19 +1727,27 @@ export async function main() {
1715
1727
  }
1716
1728
  }
1717
1729
  else if (event.type === 'tool_generating') {
1730
+ isConnecting = false;
1731
+ telemetry.setPhase('tool');
1718
1732
  const theme = getCurrentTheme();
1719
1733
  const label = formatToolGeneratingLabel(event.name, event.targetHint);
1734
+ const teleStats = telemetry.getTotalTokens() > 0
1735
+ ? ` · ${LiveTelemetryTracker.formatTokenCount(telemetry.getTotalTokens())} tok · ${telemetry.getTokensPerSecond()} tok/s`
1736
+ : '';
1737
+ const telemetrySuffix = config.ui?.liveTelemetry ? ` ${pc.dim(`(${telemetry.getElapsedSeconds()}s${teleStats})`)}` : '';
1720
1738
  if (!spinnerActive) {
1721
1739
  streamer.finish();
1722
1740
  finishThinking(false);
1723
- s.start(theme.boldFn(label));
1741
+ s.start(theme.boldFn(label) + telemetrySuffix);
1724
1742
  spinnerActive = true;
1725
1743
  }
1726
1744
  else {
1727
- s.message(theme.boldFn(label));
1745
+ s.message(theme.boldFn(label) + telemetrySuffix);
1728
1746
  }
1729
1747
  }
1730
1748
  else if (event.type === 'tool_start') {
1749
+ isConnecting = false;
1750
+ telemetry.reset();
1731
1751
  if (spinnerActive) {
1732
1752
  s.stop();
1733
1753
  spinnerActive = false;
@@ -1746,6 +1766,7 @@ export async function main() {
1746
1766
  }
1747
1767
  else if (event.type === 'tool_end') {
1748
1768
  toolsExecutedCount++;
1769
+ GitStatusCache.invalidate();
1749
1770
  if (spinnerActive) {
1750
1771
  s.stop();
1751
1772
  spinnerActive = false;
@@ -1757,50 +1778,8 @@ export async function main() {
1757
1778
  process.stdout.write(parsed.displayCard);
1758
1779
  }
1759
1780
  else if (parsed.action === 'edit' && parsed.diffLines) {
1760
- const theme = getCurrentTheme();
1761
1781
  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}`));
1782
+ console.log(formatDiffBox(parsed.path, parsed.diffLines, cols, parsed.summary, !!config.ui?.diffColors));
1804
1783
  }
1805
1784
  else if (parsed.summary) {
1806
1785
  console.log(pc.green(` └─ ${parsed.summary}`));
@@ -1824,6 +1803,7 @@ export async function main() {
1824
1803
  totalSessionCost += event.usage.cost || 0;
1825
1804
  }
1826
1805
  else if (event.type === 'reconnecting') {
1806
+ telemetry.reset();
1827
1807
  if (spinnerActive) {
1828
1808
  s.stop();
1829
1809
  spinnerActive = false;
@@ -1878,7 +1858,6 @@ export async function main() {
1878
1858
  if (textActive) {
1879
1859
  process.stdout.write('\n');
1880
1860
  }
1881
- globalSnapshotManager.finishTurn();
1882
1861
  const turnElapsedMs = Date.now() - turnStartTime;
1883
1862
  const shouldNotify = (turnElapsedMs >= 15000) || (toolsExecutedCount > 0) || (planMode && lastPlanReady !== null);
1884
1863
  if (shouldNotify && !aborted) {
@@ -1947,6 +1926,7 @@ export async function main() {
1947
1926
  }
1948
1927
  }
1949
1928
  finally {
1929
+ globalSnapshotManager.finishTurn();
1950
1930
  activeAbortHandler = null;
1951
1931
  process.removeListener('SIGINT', onSigInt);
1952
1932
  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
+ }
@@ -3,6 +3,9 @@ import { scanProjectFiles } from './files.js';
3
3
  import { saveClipboardImage, processPastedFilePath } from './clipboard.js';
4
4
  import { getCurrentTheme } from './theme.js';
5
5
  import { CustomCommandManager } from '../core/commands.js';
6
+ import { formatPromptHeader } from './modeborder.js';
7
+ import { filterCommandPalette, estimatePromptTokens, formatPromptTokenBadge } from './prompt_palette.js';
8
+ import { PromptHistoryNavigator } from './history_search.js';
6
9
  export const SLASH_COMMANDS = [
7
10
  { cmd: '/new', desc: 'Start a new clean chat session' },
8
11
  { cmd: '/resume', desc: 'Resume a previous chat session' },
@@ -39,11 +42,17 @@ function stripAnsi(str) {
39
42
  }
40
43
  export function askPrompt(opts = {}) {
41
44
  return new Promise((resolve) => {
42
- const msg = opts.message
43
- ? `${opts.message} ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`
44
- : `Ask anything... ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`;
45
45
  const theme = getCurrentTheme();
46
- console.log(theme.colorFn('◆') + ' ' + pc.bold(msg));
46
+ if (opts.ui?.modeBorder) {
47
+ const header = formatPromptHeader(!!opts.planMode, !!opts.isYolo, opts.message);
48
+ console.log(header);
49
+ }
50
+ else {
51
+ const msg = opts.message
52
+ ? `${opts.message} ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`
53
+ : `Ask anything... ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`;
54
+ console.log(theme.colorFn('◆') + ' ' + pc.bold(msg));
55
+ }
47
56
  let availableFiles = [];
48
57
  let customCommandsList = [];
49
58
  scanProjectFiles().then((f) => (availableFiles = f)).catch(() => { });
@@ -54,8 +63,14 @@ export function askPrompt(opts = {}) {
54
63
  let cursorPos = input.length;
55
64
  let selectedIndex = 0;
56
65
  let lastRenderedDropdownLines = 0;
57
- let historyIndex = GLOBAL_PROMPT_HISTORY.length;
58
- let tempDraft = '';
66
+ const historyNav = new PromptHistoryNavigator(GLOBAL_PROMPT_HISTORY, !!opts.ui?.promptHistoryFuzzy);
67
+ PromptHistoryNavigator.loadPersistent().then((persisted) => {
68
+ for (const p of persisted) {
69
+ if (!GLOBAL_PROMPT_HISTORY.includes(p)) {
70
+ GLOBAL_PROMPT_HISTORY.push(p);
71
+ }
72
+ }
73
+ }).catch(() => { });
59
74
  const imageAttachments = [];
60
75
  const usedImageNames = new Set();
61
76
  const pastedBlocks = [];
@@ -133,18 +148,34 @@ export function askPrompt(opts = {}) {
133
148
  function getDropdownItems() {
134
149
  if (input.startsWith('/')) {
135
150
  const q = input.trim().toLowerCase();
136
- const baseList = [
137
- ...SLASH_COMMANDS.map((c) => ({ label: c.cmd, desc: c.desc, replacement: c.cmd })),
138
- ...customCommandsList.map((c) => ({ label: c.cmd, desc: `(custom) ${c.desc}`, replacement: c.cmd }))
139
- ];
140
- const filtered = baseList.filter((c) => c.label.toLowerCase().startsWith(q) || q === '/');
141
- return filtered.map((item) => ({
142
- label: item.label,
143
- desc: item.desc,
144
- replacement: item.replacement,
145
- replaceStart: 0,
146
- replaceLen: input.length
147
- }));
151
+ if (opts.ui?.commandPalette) {
152
+ const baseList = [
153
+ ...SLASH_COMMANDS.map((c) => ({ cmd: c.cmd, desc: c.desc })),
154
+ ...customCommandsList.map((c) => ({ cmd: c.cmd, desc: `(custom) ${c.desc}` }))
155
+ ];
156
+ const filtered = filterCommandPalette(input, baseList).slice(0, 15);
157
+ return filtered.map((item) => ({
158
+ label: item.cmd,
159
+ desc: item.desc,
160
+ replacement: `${item.cmd} `,
161
+ replaceStart: 0,
162
+ replaceLen: input.length
163
+ }));
164
+ }
165
+ else {
166
+ const baseList = [
167
+ ...SLASH_COMMANDS.map((c) => ({ label: c.cmd, desc: c.desc, replacement: c.cmd })),
168
+ ...customCommandsList.map((c) => ({ label: c.cmd, desc: `(custom) ${c.desc}`, replacement: c.cmd }))
169
+ ];
170
+ const filtered = baseList.filter((c) => c.label.toLowerCase().startsWith(q) || q === '/');
171
+ return filtered.map((item) => ({
172
+ label: item.label,
173
+ desc: item.desc,
174
+ replacement: item.replacement,
175
+ replaceStart: 0,
176
+ replaceLen: input.length
177
+ }));
178
+ }
148
179
  }
149
180
  const beforeCursor = input.slice(0, cursorPos);
150
181
  const atMatch = beforeCursor.match(/@([a-zA-Z0-9_\-./\u0400-\u04FF]*)$/);
@@ -252,9 +283,11 @@ export function askPrompt(opts = {}) {
252
283
  renderCursorCol = 3;
253
284
  }
254
285
  else {
255
- const maxInputLen = Math.max(10, cols - 6);
286
+ const tokenBadge = opts.ui?.inputTokenCounter ? formatPromptTokenBadge(estimatePromptTokens(input), 40) : '';
287
+ const badgeSuffix = tokenBadge ? ` ${tokenBadge}` : '';
288
+ const maxInputLen = Math.max(10, cols - 6 - (tokenBadge ? 12 : 0));
256
289
  if (input.length <= maxInputLen) {
257
- inputDisplay += formatInputWithBadges(input) + '\x1b[0m';
290
+ inputDisplay += formatInputWithBadges(input) + badgeSuffix + '\x1b[0m';
258
291
  renderCursorCol = 3 + cursorPos;
259
292
  }
260
293
  else {
@@ -263,7 +296,7 @@ export function askPrompt(opts = {}) {
263
296
  const visibleChunk = input.slice(start, start + maxInputLen);
264
297
  const prefix = start > 0 ? '…' : '';
265
298
  const suffix = start + maxInputLen < input.length ? '…' : '';
266
- inputDisplay += pc.dim(prefix) + formatInputWithBadges(visibleChunk) + pc.dim(suffix) + '\x1b[0m';
299
+ inputDisplay += pc.dim(prefix) + formatInputWithBadges(visibleChunk) + pc.dim(suffix) + badgeSuffix + '\x1b[0m';
267
300
  renderCursorCol = 3 + (prefix ? 1 : 0) + (cursorPos - start);
268
301
  }
269
302
  }
@@ -473,14 +506,13 @@ export function askPrompt(opts = {}) {
473
506
  selectedIndex = (selectedIndex - 1 + items.length) % items.length;
474
507
  render();
475
508
  }
476
- else if (GLOBAL_PROMPT_HISTORY.length > 0 && historyIndex > 0) {
477
- if (historyIndex === GLOBAL_PROMPT_HISTORY.length) {
478
- tempDraft = input;
509
+ else {
510
+ const prev = historyNav.navigateUp(input);
511
+ if (prev !== null) {
512
+ input = prev;
513
+ cursorPos = input.length;
514
+ render();
479
515
  }
480
- historyIndex--;
481
- input = GLOBAL_PROMPT_HISTORY[historyIndex];
482
- cursorPos = input.length;
483
- render();
484
516
  }
485
517
  return;
486
518
  }
@@ -490,16 +522,13 @@ export function askPrompt(opts = {}) {
490
522
  selectedIndex = (selectedIndex + 1) % items.length;
491
523
  render();
492
524
  }
493
- else if (historyIndex < GLOBAL_PROMPT_HISTORY.length) {
494
- historyIndex++;
495
- if (historyIndex === GLOBAL_PROMPT_HISTORY.length) {
496
- input = tempDraft;
497
- }
498
- else {
499
- input = GLOBAL_PROMPT_HISTORY[historyIndex];
525
+ else {
526
+ const next = historyNav.navigateDown();
527
+ if (next !== null) {
528
+ input = next;
529
+ cursorPos = input.length;
530
+ render();
500
531
  }
501
- cursorPos = input.length;
502
- render();
503
532
  }
504
533
  return;
505
534
  }
@@ -0,0 +1,37 @@
1
+ import pc from 'picocolors';
2
+ export function filterCommandPalette(query, commands) {
3
+ if (!query || query === '/') {
4
+ return [...commands];
5
+ }
6
+ const q = query.toLowerCase().replace(/^\//, '');
7
+ return commands.filter((c) => {
8
+ const name = c.cmd.toLowerCase().replace(/^\//, '');
9
+ return name.includes(q) || c.desc.toLowerCase().includes(q);
10
+ });
11
+ }
12
+ export function estimatePromptTokens(text) {
13
+ if (!text)
14
+ return 0;
15
+ let tokens = 0;
16
+ for (let i = 0; i < text.length; i++) {
17
+ const code = text.charCodeAt(i);
18
+ if (code >= 0x0400 && code <= 0x04ff) {
19
+ tokens += 0.8;
20
+ }
21
+ else if (code > 0x07ff) {
22
+ tokens += 1.2;
23
+ }
24
+ else {
25
+ tokens += 0.25;
26
+ }
27
+ }
28
+ return Math.max(1, Math.round(tokens));
29
+ }
30
+ export function formatPromptTokenBadge(tokens, threshold = 30) {
31
+ if (tokens < threshold)
32
+ return '';
33
+ if (tokens >= 1000) {
34
+ return pc.dim(`(~${(tokens / 1000).toFixed(1)}k tok)`);
35
+ }
36
+ return pc.dim(`(~${tokens} tok)`);
37
+ }
@@ -7,6 +7,7 @@ import { spawn } from 'child_process';
7
7
  import pc from 'picocolors';
8
8
  import qrcode from 'qrcode-terminal';
9
9
  import { getTheme } from './theme.js';
10
+ import { DEVX_VERSION } from '../core/types.js';
10
11
  const MIME_TYPES = {
11
12
  '.html': 'text/html; charset=utf-8',
12
13
  '.htm': 'text/html; charset=utf-8',
@@ -139,7 +140,7 @@ function renderDirectoryHtml(dirPath, relPath, files, port) {
139
140
  ${parentLink}
140
141
  ${items || '<li style="padding: 20px; text-align: center; color: #6e7681;">No visible files in this directory</li>'}
141
142
  </ul>
142
- <div class="footer">devx v1.4.19 &bull; Terminal-Native AI Assistant</div>
143
+ <div class="footer">devx v${DEVX_VERSION} &bull; Terminal-Native AI Assistant</div>
143
144
  </div>
144
145
  </body>
145
146
  </html>`;