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.
@@ -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' },
@@ -27,7 +30,7 @@ export const SLASH_COMMANDS = [
27
30
  { cmd: '/commit', desc: 'AI-generated git commit message & commit' },
28
31
  { cmd: '/status', desc: 'Show git repository file status' },
29
32
  { cmd: '/compact', desc: 'Compact & summarize chat context' },
30
- { cmd: '/clear', desc: 'Clear screen & redraw banner' },
33
+ { cmd: '/clear', desc: 'Clear screen' },
31
34
  { cmd: '/init', desc: 'Generate AGENTS.md project instructions' },
32
35
  { cmd: '/config', desc: 'View current configuration' },
33
36
  { cmd: '/help', desc: 'Show all available commands' },
@@ -39,11 +42,22 @@ 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
+ const cleanLayout = opts.ui?.layout !== 'classic';
47
+ const promptPrefix = cleanLayout ? pc.cyan('›') + ' ' : pc.dim('│') + ' ';
48
+ const promptPrefixWidth = cleanLayout ? 2 : 3;
49
+ if (!cleanLayout) {
50
+ if (opts.ui?.modeBorder) {
51
+ const header = formatPromptHeader(!!opts.planMode, !!opts.isYolo, opts.message);
52
+ console.log(header);
53
+ }
54
+ else {
55
+ const msg = opts.message
56
+ ? `${opts.message} ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`
57
+ : `Ask anything... ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`;
58
+ console.log(theme.colorFn('◆') + ' ' + pc.bold(msg));
59
+ }
60
+ }
47
61
  let availableFiles = [];
48
62
  let customCommandsList = [];
49
63
  scanProjectFiles().then((f) => (availableFiles = f)).catch(() => { });
@@ -54,8 +68,14 @@ export function askPrompt(opts = {}) {
54
68
  let cursorPos = input.length;
55
69
  let selectedIndex = 0;
56
70
  let lastRenderedDropdownLines = 0;
57
- let historyIndex = GLOBAL_PROMPT_HISTORY.length;
58
- let tempDraft = '';
71
+ const historyNav = new PromptHistoryNavigator(GLOBAL_PROMPT_HISTORY, !!opts.ui?.promptHistoryFuzzy);
72
+ PromptHistoryNavigator.loadPersistent().then((persisted) => {
73
+ for (const p of persisted) {
74
+ if (!GLOBAL_PROMPT_HISTORY.includes(p)) {
75
+ GLOBAL_PROMPT_HISTORY.push(p);
76
+ }
77
+ }
78
+ }).catch(() => { });
59
79
  const imageAttachments = [];
60
80
  const usedImageNames = new Set();
61
81
  const pastedBlocks = [];
@@ -82,7 +102,7 @@ export function askPrompt(opts = {}) {
82
102
  return spans;
83
103
  }
84
104
  function formatInputWithBadges(str) {
85
- return str.replace(/(\[(?:Pasted text #\d+ \+\d+ lines|\d+\.png \d+kb)\])/g, (match) => theme.badgeFn(` ${match.slice(1, -1)} `) + '\x1b[0m');
105
+ return str.replace(/(\[(?:Pasted text #\d+ \+\d+ lines|\d+\.png \d+kb)\])/g, (match) => (cleanLayout ? pc.dim(match) : theme.badgeFn(` ${match.slice(1, -1)} `)) + '\x1b[0m');
86
106
  }
87
107
  function expandPastes(text) {
88
108
  let expanded = text;
@@ -133,18 +153,34 @@ export function askPrompt(opts = {}) {
133
153
  function getDropdownItems() {
134
154
  if (input.startsWith('/')) {
135
155
  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
- }));
156
+ if (cleanLayout || opts.ui?.commandPalette) {
157
+ const baseList = [
158
+ ...SLASH_COMMANDS.map((c) => ({ cmd: c.cmd, desc: c.desc })),
159
+ ...customCommandsList.map((c) => ({ cmd: c.cmd, desc: `(custom) ${c.desc}` }))
160
+ ];
161
+ const filtered = filterCommandPalette(input, baseList).slice(0, 15);
162
+ return filtered.map((item) => ({
163
+ label: item.cmd,
164
+ desc: item.desc,
165
+ replacement: `${item.cmd} `,
166
+ replaceStart: 0,
167
+ replaceLen: input.length
168
+ }));
169
+ }
170
+ else {
171
+ const baseList = [
172
+ ...SLASH_COMMANDS.map((c) => ({ label: c.cmd, desc: c.desc, replacement: c.cmd })),
173
+ ...customCommandsList.map((c) => ({ label: c.cmd, desc: `(custom) ${c.desc}`, replacement: c.cmd }))
174
+ ];
175
+ const filtered = baseList.filter((c) => c.label.toLowerCase().startsWith(q) || q === '/');
176
+ return filtered.map((item) => ({
177
+ label: item.label,
178
+ desc: item.desc,
179
+ replacement: item.replacement,
180
+ replaceStart: 0,
181
+ replaceLen: input.length
182
+ }));
183
+ }
148
184
  }
149
185
  const beforeCursor = input.slice(0, cursorPos);
150
186
  const atMatch = beforeCursor.match(/@([a-zA-Z0-9_\-./\u0400-\u04FF]*)$/);
@@ -180,8 +216,6 @@ export function askPrompt(opts = {}) {
180
216
  // Limit dropdown height gracefully so it always fits comfortably
181
217
  const maxDropdownRows = Math.max(2, Math.min(rows - 3, 5));
182
218
  const pageSize = Math.min(4, Math.max(2, Math.min(maxDropdownRows - 2, Math.floor(rows / 4))));
183
- // Strict boundary: left margin (2) + box borders (2) + inner width <= cols - 4
184
- // This mathematically prevents terminal auto-wrapping on narrow mobile screens
185
219
  const boxWidth = Math.max(12, Math.min(cols - 6, 46));
186
220
  const innerWidth = Math.max(8, boxWidth - 2);
187
221
  const total = items.length;
@@ -192,70 +226,86 @@ export function askPrompt(opts = {}) {
192
226
  }
193
227
  }
194
228
  const endIndex = Math.min(startIndex + pageSize, total);
195
- const hasMoreUp = startIndex > 0;
196
- const hasMoreDown = endIndex < total;
197
- let topBorderStr = '─'.repeat(innerWidth);
198
- if (hasMoreUp) {
199
- const mid = Math.max(0, Math.floor(innerWidth / 2) - 2);
200
- topBorderStr = '─'.repeat(mid) + ' ▲ ' + '─'.repeat(Math.max(0, innerWidth - mid - 3));
201
- }
202
- let botBorderStr = '─'.repeat(innerWidth);
203
- if (hasMoreDown) {
204
- const mid = Math.max(0, Math.floor(innerWidth / 2) - 2);
205
- botBorderStr = '─'.repeat(mid) + ' ▼ ' + '─'.repeat(Math.max(0, innerWidth - mid - 3));
206
- }
207
- dropdownLines.push(pc.dim('│ ') + pc.dim('╭' + topBorderStr + '╮') + '\x1b[0m');
208
- for (let i = startIndex; i < endIndex; i++) {
209
- const item = items[i];
210
- const isSelected = i === selectedIndex;
211
- const pointer = isSelected ? '› ' : ' ';
212
- const avail = Math.max(4, innerWidth - pointer.length);
213
- let rowText = '';
214
- if (avail < 16) {
215
- const labelStr = item.label.length > avail
216
- ? item.label.slice(0, avail - 1) + '…'
217
- : item.label.padEnd(avail, ' ');
218
- rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr);
229
+ if (cleanLayout) {
230
+ for (let i = startIndex; i < endIndex; i++) {
231
+ const item = items[i];
232
+ const selected = i === selectedIndex;
233
+ const labelWidth = Math.min(20, cols - 5);
234
+ const label = item.label.length > labelWidth ? `${item.label.slice(0, labelWidth - 1)}…` : item.label;
235
+ const descWidth = cols - label.length - 7;
236
+ const desc = descWidth >= 5
237
+ ? item.desc.slice(0, descWidth)
238
+ : '';
239
+ dropdownLines.push(` ${selected ? pc.cyan('›') : ' '} ${selected ? pc.bold(label) : label}${desc ? ` ${pc.dim(desc)}` : ''}`);
219
240
  }
220
- else {
221
- const labelMax = Math.min(12, Math.max(6, Math.floor(avail * 0.38)));
222
- const labelStr = item.label.length > labelMax
223
- ? item.label.slice(0, labelMax - 1) + '…'
224
- : item.label.padEnd(labelMax, ' ');
225
- const descMax = Math.max(3, avail - labelMax - 1);
226
- const descStr = item.desc.length > descMax
227
- ? item.desc.slice(0, descMax - 1) + '…'
228
- : item.desc.padEnd(descMax, ' ');
229
- rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr) + ' ' + pc.gray(descStr);
241
+ }
242
+ else {
243
+ const hasMoreUp = startIndex > 0;
244
+ const hasMoreDown = endIndex < total;
245
+ let topBorderStr = '─'.repeat(innerWidth);
246
+ if (hasMoreUp) {
247
+ const mid = Math.max(0, Math.floor(innerWidth / 2) - 2);
248
+ topBorderStr = '─'.repeat(mid) + ' ▲ ' + '─'.repeat(Math.max(0, innerWidth - mid - 3));
230
249
  }
231
- // Calculate visible plain length without ANSI
232
- const plainLen = rowText.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').length;
233
- const padCount = Math.max(0, innerWidth - plainLen);
234
- const fullRow = rowText + ' '.repeat(padCount);
235
- if (isSelected) {
236
- const cleanFull = fullRow.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
237
- dropdownLines.push(pc.dim('│ ') + pc.dim('│') + theme.badgeFn(cleanFull) + pc.dim('│') + '\x1b[0m');
250
+ let botBorderStr = '─'.repeat(innerWidth);
251
+ if (hasMoreDown) {
252
+ const mid = Math.max(0, Math.floor(innerWidth / 2) - 2);
253
+ botBorderStr = '─'.repeat(mid) + ' ▼ ' + '─'.repeat(Math.max(0, innerWidth - mid - 3));
238
254
  }
239
- else {
240
- dropdownLines.push(pc.dim('│ ') + pc.dim('│') + fullRow + pc.dim('│') + '\x1b[0m');
255
+ dropdownLines.push(pc.dim('│ ') + pc.dim('╭' + topBorderStr + '╮') + '\x1b[0m');
256
+ for (let i = startIndex; i < endIndex; i++) {
257
+ const item = items[i];
258
+ const isSelected = i === selectedIndex;
259
+ const pointer = isSelected ? '› ' : ' ';
260
+ const avail = Math.max(4, innerWidth - pointer.length);
261
+ let rowText = '';
262
+ if (avail < 16) {
263
+ const labelStr = item.label.length > avail
264
+ ? item.label.slice(0, avail - 1) + '…'
265
+ : item.label.padEnd(avail, ' ');
266
+ rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr);
267
+ }
268
+ else {
269
+ const labelMax = Math.min(12, Math.max(6, Math.floor(avail * 0.38)));
270
+ const labelStr = item.label.length > labelMax
271
+ ? item.label.slice(0, labelMax - 1) + '…'
272
+ : item.label.padEnd(labelMax, ' ');
273
+ const descMax = Math.max(3, avail - labelMax - 1);
274
+ const descStr = item.desc.length > descMax
275
+ ? item.desc.slice(0, descMax - 1) + '…'
276
+ : item.desc.padEnd(descMax, ' ');
277
+ rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr) + ' ' + pc.gray(descStr);
278
+ }
279
+ const plainLen = rowText.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').length;
280
+ const padCount = Math.max(0, innerWidth - plainLen);
281
+ const fullRow = rowText + ' '.repeat(padCount);
282
+ if (isSelected) {
283
+ const cleanFull = fullRow.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
284
+ dropdownLines.push(pc.dim('│ ') + pc.dim('│') + theme.badgeFn(cleanFull) + pc.dim('│') + '\x1b[0m');
285
+ }
286
+ else {
287
+ dropdownLines.push(pc.dim('│ ') + pc.dim('│') + fullRow + pc.dim('│') + '\x1b[0m');
288
+ }
241
289
  }
290
+ dropdownLines.push(pc.dim('│ ') + pc.dim('╰' + botBorderStr + '╯') + '\x1b[0m');
242
291
  }
243
- dropdownLines.push(pc.dim('│ ') + pc.dim('╰' + botBorderStr + '╯') + '\x1b[0m');
244
292
  }
245
293
  // 1. Format and write the Prompt line safely
246
- let inputDisplay = pc.dim('│') + ' ';
247
- let renderCursorCol = 3;
294
+ let inputDisplay = promptPrefix;
295
+ let renderCursorCol = promptPrefixWidth;
248
296
  if (input.length === 0) {
249
- const maxPlace = Math.max(10, cols - 6);
297
+ const maxPlace = Math.max(10, cols - promptPrefixWidth - 2);
250
298
  const displayPlace = placeholder.length > maxPlace ? placeholder.slice(0, maxPlace - 1) + '…' : placeholder;
251
299
  inputDisplay += pc.dim(displayPlace) + '\x1b[0m';
252
- renderCursorCol = 3;
300
+ renderCursorCol = promptPrefixWidth;
253
301
  }
254
302
  else {
255
- const maxInputLen = Math.max(10, cols - 6);
303
+ const tokenBadge = !cleanLayout && opts.ui?.inputTokenCounter ? formatPromptTokenBadge(estimatePromptTokens(input), 40) : '';
304
+ const badgeSuffix = tokenBadge ? ` ${tokenBadge}` : '';
305
+ const maxInputLen = Math.max(10, cols - promptPrefixWidth - 3 - (tokenBadge ? 12 : 0));
256
306
  if (input.length <= maxInputLen) {
257
- inputDisplay += formatInputWithBadges(input) + '\x1b[0m';
258
- renderCursorCol = 3 + cursorPos;
307
+ inputDisplay += formatInputWithBadges(input) + badgeSuffix + '\x1b[0m';
308
+ renderCursorCol = promptPrefixWidth + cursorPos;
259
309
  }
260
310
  else {
261
311
  // Horizontal scrolling to prevent auto-wrapping
@@ -263,8 +313,8 @@ export function askPrompt(opts = {}) {
263
313
  const visibleChunk = input.slice(start, start + maxInputLen);
264
314
  const prefix = start > 0 ? '…' : '';
265
315
  const suffix = start + maxInputLen < input.length ? '…' : '';
266
- inputDisplay += pc.dim(prefix) + formatInputWithBadges(visibleChunk) + pc.dim(suffix) + '\x1b[0m';
267
- renderCursorCol = 3 + (prefix ? 1 : 0) + (cursorPos - start);
316
+ inputDisplay += pc.dim(prefix) + formatInputWithBadges(visibleChunk) + pc.dim(suffix) + badgeSuffix + '\x1b[0m';
317
+ renderCursorCol = promptPrefixWidth + (prefix ? 1 : 0) + (cursorPos - start);
268
318
  }
269
319
  }
270
320
  // Write prompt line
@@ -323,7 +373,7 @@ export function askPrompt(opts = {}) {
323
373
  GLOBAL_PROMPT_HISTORY.push(fullText.trim());
324
374
  }
325
375
  }
326
- process.stdout.write(`\x1b[0m\r\x1b[2K${pc.dim('│')} ${formatInputWithBadges(finalInput)}\x1b[0m\n\n`);
376
+ process.stdout.write(`\x1b[0m\r\x1b[2K${promptPrefix}${formatInputWithBadges(finalInput)}\x1b[0m\n\n`);
327
377
  cleanup();
328
378
  resolve(fullText);
329
379
  }
@@ -473,14 +523,13 @@ export function askPrompt(opts = {}) {
473
523
  selectedIndex = (selectedIndex - 1 + items.length) % items.length;
474
524
  render();
475
525
  }
476
- else if (GLOBAL_PROMPT_HISTORY.length > 0 && historyIndex > 0) {
477
- if (historyIndex === GLOBAL_PROMPT_HISTORY.length) {
478
- tempDraft = input;
526
+ else {
527
+ const prev = historyNav.navigateUp(input);
528
+ if (prev !== null) {
529
+ input = prev;
530
+ cursorPos = input.length;
531
+ render();
479
532
  }
480
- historyIndex--;
481
- input = GLOBAL_PROMPT_HISTORY[historyIndex];
482
- cursorPos = input.length;
483
- render();
484
533
  }
485
534
  return;
486
535
  }
@@ -490,16 +539,13 @@ export function askPrompt(opts = {}) {
490
539
  selectedIndex = (selectedIndex + 1) % items.length;
491
540
  render();
492
541
  }
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];
542
+ else {
543
+ const next = historyNav.navigateDown();
544
+ if (next !== null) {
545
+ input = next;
546
+ cursorPos = input.length;
547
+ render();
500
548
  }
501
- cursorPos = input.length;
502
- render();
503
549
  }
504
550
  return;
505
551
  }
@@ -0,0 +1,43 @@
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
+ }).sort((a, b) => {
11
+ const rank = (cmd) => {
12
+ const name = cmd.toLowerCase().replace(/^\//, '');
13
+ return name.startsWith(q) ? 0 : name.includes(q) ? 1 : 2;
14
+ };
15
+ return rank(a.cmd) - rank(b.cmd);
16
+ });
17
+ }
18
+ export function estimatePromptTokens(text) {
19
+ if (!text)
20
+ return 0;
21
+ let tokens = 0;
22
+ for (let i = 0; i < text.length; i++) {
23
+ const code = text.charCodeAt(i);
24
+ if (code >= 0x0400 && code <= 0x04ff) {
25
+ tokens += 0.8;
26
+ }
27
+ else if (code > 0x07ff) {
28
+ tokens += 1.2;
29
+ }
30
+ else {
31
+ tokens += 0.25;
32
+ }
33
+ }
34
+ return Math.max(1, Math.round(tokens));
35
+ }
36
+ export function formatPromptTokenBadge(tokens, threshold = 30) {
37
+ if (tokens < threshold)
38
+ return '';
39
+ if (tokens >= 1000) {
40
+ return pc.dim(`(~${(tokens / 1000).toFixed(1)}k tok)`);
41
+ }
42
+ return pc.dim(`(~${tokens} tok)`);
43
+ }
@@ -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>`;
@@ -0,0 +1,133 @@
1
+ import { execSync } from 'child_process';
2
+ import pc from 'picocolors';
3
+ import { getCurrentTheme } from './theme.js';
4
+ export class GitStatusCache {
5
+ static cachedInfo = undefined;
6
+ static lastCheckTime = 0;
7
+ static TTL_MS = 10000; // 10s fallback TTL
8
+ static getStatus(forceRefresh = false) {
9
+ const now = Date.now();
10
+ if (!forceRefresh && GitStatusCache.cachedInfo !== undefined && (now - GitStatusCache.lastCheckTime < GitStatusCache.TTL_MS)) {
11
+ return GitStatusCache.cachedInfo;
12
+ }
13
+ GitStatusCache.lastCheckTime = now;
14
+ try {
15
+ const branch = execSync('git branch --show-current', {
16
+ stdio: ['ignore', 'pipe', 'ignore'],
17
+ encoding: 'utf8',
18
+ timeout: 1000
19
+ }).trim();
20
+ if (!branch) {
21
+ GitStatusCache.cachedInfo = null;
22
+ return null;
23
+ }
24
+ const status = execSync('git status --porcelain', {
25
+ stdio: ['ignore', 'pipe', 'ignore'],
26
+ encoding: 'utf8',
27
+ timeout: 1000
28
+ }).trim();
29
+ const isDirty = status.length > 0;
30
+ GitStatusCache.cachedInfo = { branch, isDirty };
31
+ return GitStatusCache.cachedInfo;
32
+ }
33
+ catch {
34
+ GitStatusCache.cachedInfo = null;
35
+ return null;
36
+ }
37
+ }
38
+ static invalidate() {
39
+ GitStatusCache.cachedInfo = undefined;
40
+ GitStatusCache.lastCheckTime = 0;
41
+ }
42
+ }
43
+ export function renderProgressBar(percent, barWidth = 10) {
44
+ const clamped = Math.max(0, Math.min(100, percent));
45
+ const filledCount = Math.round((clamped / 100) * barWidth);
46
+ const emptyCount = Math.max(0, barWidth - filledCount);
47
+ const bar = '█'.repeat(filledCount) + '░'.repeat(emptyCount);
48
+ if (clamped >= 90)
49
+ return pc.red(`[${bar}] ${clamped}%`);
50
+ if (clamped >= 75)
51
+ return pc.yellow(`[${bar}] ${clamped}%`);
52
+ return pc.cyan(`[${bar}] ${clamped}%`);
53
+ }
54
+ export function formatCostBadge(cost, maxCostUSD) {
55
+ const costFormatted = `$${cost.toFixed(4)}`;
56
+ if (!maxCostUSD || maxCostUSD <= 0) {
57
+ return pc.green(costFormatted);
58
+ }
59
+ const ratio = cost / maxCostUSD;
60
+ if (ratio >= 1.0) {
61
+ return pc.red(costFormatted);
62
+ }
63
+ if (ratio >= 0.8) {
64
+ return pc.yellow(costFormatted);
65
+ }
66
+ return pc.green(costFormatted);
67
+ }
68
+ function clipLabel(label, width) {
69
+ if (label.length <= width)
70
+ return label;
71
+ return width > 1 ? `${label.slice(0, width - 1)}…` : label.slice(0, width);
72
+ }
73
+ export function renderCleanStatus(opts) {
74
+ const cols = Math.max(20, opts.cols || process.stdout.columns || 80);
75
+ const available = cols - 2;
76
+ const mode = opts.isYolo ? 'YOLO' : opts.mode;
77
+ const modelWidth = Math.max(4, available - mode.length - 2);
78
+ const shortModel = opts.model.length > modelWidth && opts.model.includes('/')
79
+ ? opts.model.split('/').pop() || opts.model
80
+ : opts.model;
81
+ const model = clipLabel(shortModel, modelWidth);
82
+ const percent = Math.min(100, Math.round((opts.currentTokens / Math.max(1, opts.maxTokens)) * 100));
83
+ const contextLabel = cols < 38 ? 'ctx' : 'context';
84
+ let details = `${contextLabel} ${percent}% · $${opts.cost.toFixed(4)}`;
85
+ if (details.length > available)
86
+ details = `${percent}% · $${opts.cost.toFixed(2)}`;
87
+ const git = GitStatusCache.getStatus();
88
+ const branch = git ? ` · ${git.branch}${git.isDirty ? '*' : ''}` : '';
89
+ if (details.length + branch.length <= available)
90
+ details += branch;
91
+ details = clipLabel(details, available);
92
+ return `${pc.cyan(mode.toLowerCase())} ${pc.bold(model)}\n${pc.dim(details)}`;
93
+ }
94
+ export function renderPowerlineStatus(opts) {
95
+ const cols = opts.cols || process.stdout.columns || 80;
96
+ const theme = getCurrentTheme();
97
+ // Mode badge
98
+ let modeBadge = '';
99
+ if (opts.isYolo) {
100
+ modeBadge = pc.bgRed(pc.bold(pc.white(' YOLO ')));
101
+ }
102
+ else if (opts.mode === 'PLAN') {
103
+ modeBadge = pc.bgCyan(pc.bold(pc.black(' PLAN ')));
104
+ }
105
+ else {
106
+ modeBadge = pc.bgGreen(pc.bold(pc.black(' AGENT ')));
107
+ }
108
+ // Model shortening for narrow screens
109
+ let displayModel = opts.model;
110
+ if (cols < 75 && displayModel.length > 18) {
111
+ const parts = displayModel.split('/');
112
+ displayModel = parts.length > 1 ? parts.slice(1).join('/') : displayModel;
113
+ if (displayModel.length > 18) {
114
+ displayModel = displayModel.slice(0, 15) + '…';
115
+ }
116
+ }
117
+ // Git info
118
+ const git = GitStatusCache.getStatus();
119
+ const gitStr = git ? ` · ${pc.magenta(git.branch)}${git.isDirty ? pc.yellow('✱') : ''}` : '';
120
+ // Context progress
121
+ const usagePercent = Math.min(100, Math.round((opts.currentTokens / Math.max(1, opts.maxTokens)) * 100));
122
+ const barWidth = cols < 60 ? 6 : 10;
123
+ const progressStr = renderProgressBar(usagePercent, barWidth);
124
+ const costStr = formatCostBadge(opts.cost, opts.maxCostUSD);
125
+ if (cols < 65) {
126
+ // 2-line layout for mobile Termux
127
+ const line1 = `${modeBadge} ${pc.bold(displayModel)}${gitStr}`;
128
+ const line2 = `${pc.dim('│')} Context ${progressStr} · ${costStr}`;
129
+ return `${line1}\n${line2}`;
130
+ }
131
+ // 1-line powerline layout for desktop
132
+ return `${modeBadge} ${pc.bold(displayModel)}${gitStr} · Context ${progressStr} · ${costStr}`;
133
+ }
@@ -0,0 +1,105 @@
1
+ import pc from 'picocolors';
2
+ export class LiveTelemetryTracker {
3
+ startTime = 0;
4
+ phase = 'connecting';
5
+ totalTokens = 0;
6
+ samples = [];
7
+ windowMs = 2000;
8
+ constructor(windowMs = 2000) {
9
+ this.windowMs = windowMs;
10
+ }
11
+ start(phase = 'connecting') {
12
+ this.startTime = Date.now();
13
+ this.phase = phase;
14
+ this.totalTokens = 0;
15
+ this.samples = [];
16
+ }
17
+ setPhase(phase) {
18
+ this.phase = phase;
19
+ }
20
+ recordTokens(count) {
21
+ if (count <= 0)
22
+ return;
23
+ const now = Date.now();
24
+ this.totalTokens += count;
25
+ this.samples.push({ timestamp: now, tokens: count });
26
+ this.pruneOldSamples(now);
27
+ }
28
+ estimateTokensFromText(text) {
29
+ if (!text)
30
+ return 0;
31
+ // Fast token estimation: ~0.8 tokens per Cyrillic char, ~0.25 per ASCII
32
+ let tokens = 0;
33
+ for (let i = 0; i < text.length; i++) {
34
+ const code = text.charCodeAt(i);
35
+ if (code >= 0x0400 && code <= 0x04ff) {
36
+ tokens += 0.8;
37
+ }
38
+ else if (code > 0x07ff) {
39
+ tokens += 1.2;
40
+ }
41
+ else {
42
+ tokens += 0.25;
43
+ }
44
+ }
45
+ return Math.max(1, Math.round(tokens));
46
+ }
47
+ pruneOldSamples(now) {
48
+ const threshold = now - this.windowMs;
49
+ while (this.samples.length > 0 && this.samples[0].timestamp < threshold) {
50
+ this.samples.shift();
51
+ }
52
+ }
53
+ getTokensPerSecond() {
54
+ const now = Date.now();
55
+ this.pruneOldSamples(now);
56
+ if (this.samples.length === 0)
57
+ return 0;
58
+ const windowTokens = this.samples.reduce((acc, s) => acc + s.tokens, 0);
59
+ const earliest = this.samples[0].timestamp;
60
+ const durationSec = Math.max(0.2, (now - earliest) / 1000);
61
+ return Math.round(windowTokens / durationSec);
62
+ }
63
+ getElapsedSeconds() {
64
+ if (this.startTime === 0)
65
+ return 0;
66
+ return Math.max(0, Math.round((Date.now() - this.startTime) / 1000));
67
+ }
68
+ getTotalTokens() {
69
+ return this.totalTokens;
70
+ }
71
+ static formatTokenCount(n) {
72
+ if (n >= 1_000_000)
73
+ return `${(n / 1_000_000).toFixed(1)}M`;
74
+ if (n >= 1_000)
75
+ return `${(n / 1_000).toFixed(1)}k`;
76
+ return `${Math.round(n)}`;
77
+ }
78
+ formatLabel() {
79
+ const secs = `${this.getElapsedSeconds()}s`;
80
+ const tokens = LiveTelemetryTracker.formatTokenCount(this.totalTokens);
81
+ const tps = this.getTokensPerSecond();
82
+ if (this.phase === 'connecting') {
83
+ return `connecting… ${pc.dim(secs)}`;
84
+ }
85
+ if (this.phase === 'thinking') {
86
+ if (this.totalTokens > 0) {
87
+ return `thinking… ${pc.dim(`${secs} · ${tokens} tok · ${tps} tok/s`)}`;
88
+ }
89
+ return `thinking… ${pc.dim(secs)}`;
90
+ }
91
+ if (this.phase === 'generating') {
92
+ if (this.totalTokens > 0) {
93
+ return `generating… ${pc.dim(`${secs} · ${tokens} tok · ${tps} tok/s`)}`;
94
+ }
95
+ return `generating… ${pc.dim(secs)}`;
96
+ }
97
+ return `executing… ${pc.dim(secs)}`;
98
+ }
99
+ reset() {
100
+ this.startTime = 0;
101
+ this.phase = 'connecting';
102
+ this.totalTokens = 0;
103
+ this.samples = [];
104
+ }
105
+ }
@@ -1 +1 @@
1
- export {};
1
+ export const DEVX_VERSION = '1.6.0';
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'child_process';
2
+ import { DEVX_VERSION } from '../core/types.js';
2
3
  export class MCPClient {
3
4
  name;
4
5
  config;
@@ -63,7 +64,7 @@ export class MCPClient {
63
64
  capabilities: {},
64
65
  clientInfo: {
65
66
  name: 'devx',
66
- version: '1.4.19'
67
+ version: DEVX_VERSION
67
68
  }
68
69
  });
69
70
  if (!initResult) {