termux-dev 1.4.18 → 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.
@@ -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]*)$/);
@@ -178,9 +209,12 @@ export function askPrompt(opts = {}) {
178
209
  if (selectedIndex < 0)
179
210
  selectedIndex = 0;
180
211
  // Limit dropdown height gracefully so it always fits comfortably
181
- const maxDropdownRows = Math.max(2, Math.min(rows - 3, 6));
212
+ const maxDropdownRows = Math.max(2, Math.min(rows - 3, 5));
182
213
  const pageSize = Math.min(4, Math.max(2, Math.min(maxDropdownRows - 2, Math.floor(rows / 4))));
183
- const innerBoxWidth = Math.max(10, Math.min(cols - 8, 48));
214
+ // Strict boundary: left margin (2) + box borders (2) + inner width <= cols - 4
215
+ // This mathematically prevents terminal auto-wrapping on narrow mobile screens
216
+ const boxWidth = Math.max(12, Math.min(cols - 6, 46));
217
+ const innerWidth = Math.max(8, boxWidth - 2);
184
218
  const total = items.length;
185
219
  let startIndex = 0;
186
220
  if (total > pageSize) {
@@ -191,52 +225,53 @@ export function askPrompt(opts = {}) {
191
225
  const endIndex = Math.min(startIndex + pageSize, total);
192
226
  const hasMoreUp = startIndex > 0;
193
227
  const hasMoreDown = endIndex < total;
194
- let topBorderStr = '─'.repeat(innerBoxWidth);
228
+ let topBorderStr = '─'.repeat(innerWidth);
195
229
  if (hasMoreUp) {
196
- const mid = Math.max(0, Math.floor(innerBoxWidth / 2) - 2);
197
- topBorderStr = '─'.repeat(mid) + ' ▲ ' + '─'.repeat(Math.max(0, innerBoxWidth - mid - 3));
230
+ const mid = Math.max(0, Math.floor(innerWidth / 2) - 2);
231
+ topBorderStr = '─'.repeat(mid) + ' ▲ ' + '─'.repeat(Math.max(0, innerWidth - mid - 3));
198
232
  }
199
- let botBorderStr = '─'.repeat(innerBoxWidth);
233
+ let botBorderStr = '─'.repeat(innerWidth);
200
234
  if (hasMoreDown) {
201
- const mid = Math.max(0, Math.floor(innerBoxWidth / 2) - 2);
202
- botBorderStr = '─'.repeat(mid) + ' ▼ ' + '─'.repeat(Math.max(0, innerBoxWidth - mid - 3));
235
+ const mid = Math.max(0, Math.floor(innerWidth / 2) - 2);
236
+ botBorderStr = '─'.repeat(mid) + ' ▼ ' + '─'.repeat(Math.max(0, innerWidth - mid - 3));
203
237
  }
204
- dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╭' + topBorderStr + '╮') + '\x1b[0m');
238
+ dropdownLines.push(pc.dim('│ ') + pc.dim('╭' + topBorderStr + '╮') + '\x1b[0m');
205
239
  for (let i = startIndex; i < endIndex; i++) {
206
240
  const item = items[i];
207
241
  const isSelected = i === selectedIndex;
208
242
  const pointer = isSelected ? '› ' : ' ';
209
- const availWidth = innerBoxWidth;
210
- let row = '';
211
- if (availWidth < 20) {
212
- const labelStr = item.label.length > availWidth - 2
213
- ? item.label.slice(0, availWidth - 3) + '…'
214
- : item.label.padEnd(availWidth - 2, ' ');
215
- const plain = (pointer + labelStr).padEnd(availWidth, ' ').slice(0, availWidth);
216
- row = isSelected
217
- ? theme.badgeFn(plain) + '\x1b[0m'
218
- : pointer + theme.boldFn(labelStr) + '\x1b[0m';
243
+ const avail = Math.max(4, innerWidth - pointer.length);
244
+ let rowText = '';
245
+ if (avail < 16) {
246
+ const labelStr = item.label.length > avail
247
+ ? item.label.slice(0, avail - 1) + '…'
248
+ : item.label.padEnd(avail, ' ');
249
+ rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr);
219
250
  }
220
251
  else {
221
- const labelMax = Math.min(13, Math.max(6, Math.floor(availWidth * 0.35)));
252
+ const labelMax = Math.min(12, Math.max(6, Math.floor(avail * 0.38)));
222
253
  const labelStr = item.label.length > labelMax
223
254
  ? item.label.slice(0, labelMax - 1) + '…'
224
255
  : item.label.padEnd(labelMax, ' ');
225
- const descMax = Math.max(4, availWidth - labelMax - pointer.length - 1);
256
+ const descMax = Math.max(3, avail - labelMax - 1);
226
257
  const descStr = item.desc.length > descMax
227
258
  ? item.desc.slice(0, descMax - 1) + '…'
228
259
  : item.desc.padEnd(descMax, ' ');
229
- const plain = (pointer + labelStr + ' ' + descStr).padEnd(availWidth, ' ').slice(0, availWidth);
230
- if (isSelected) {
231
- row = theme.badgeFn(plain) + '\x1b[0m';
232
- }
233
- else {
234
- row = `${pointer}${theme.boldFn(labelStr)} ${pc.gray(descStr)}\x1b[0m`;
235
- }
260
+ rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr) + ' ' + pc.gray(descStr);
261
+ }
262
+ // Calculate visible plain length without ANSI
263
+ const plainLen = rowText.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').length;
264
+ const padCount = Math.max(0, innerWidth - plainLen);
265
+ const fullRow = rowText + ' '.repeat(padCount);
266
+ if (isSelected) {
267
+ const cleanFull = fullRow.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
268
+ dropdownLines.push(pc.dim('│ ') + pc.dim('│') + theme.badgeFn(cleanFull) + pc.dim('│') + '\x1b[0m');
269
+ }
270
+ else {
271
+ dropdownLines.push(pc.dim('│ ') + pc.dim('│') + fullRow + pc.dim('│') + '\x1b[0m');
236
272
  }
237
- dropdownLines.push(pc.dim('│') + ' ' + pc.dim('│') + row + pc.dim('│') + '\x1b[0m');
238
273
  }
239
- dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╰' + botBorderStr + '╯') + '\x1b[0m');
274
+ dropdownLines.push(pc.dim('│ ') + pc.dim('╰' + botBorderStr + '╯') + '\x1b[0m');
240
275
  }
241
276
  // 1. Format and write the Prompt line safely
242
277
  let inputDisplay = pc.dim('│') + ' ';
@@ -248,9 +283,11 @@ export function askPrompt(opts = {}) {
248
283
  renderCursorCol = 3;
249
284
  }
250
285
  else {
251
- 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));
252
289
  if (input.length <= maxInputLen) {
253
- inputDisplay += formatInputWithBadges(input) + '\x1b[0m';
290
+ inputDisplay += formatInputWithBadges(input) + badgeSuffix + '\x1b[0m';
254
291
  renderCursorCol = 3 + cursorPos;
255
292
  }
256
293
  else {
@@ -259,7 +296,7 @@ export function askPrompt(opts = {}) {
259
296
  const visibleChunk = input.slice(start, start + maxInputLen);
260
297
  const prefix = start > 0 ? '…' : '';
261
298
  const suffix = start + maxInputLen < input.length ? '…' : '';
262
- inputDisplay += pc.dim(prefix) + formatInputWithBadges(visibleChunk) + pc.dim(suffix) + '\x1b[0m';
299
+ inputDisplay += pc.dim(prefix) + formatInputWithBadges(visibleChunk) + pc.dim(suffix) + badgeSuffix + '\x1b[0m';
263
300
  renderCursorCol = 3 + (prefix ? 1 : 0) + (cursorPos - start);
264
301
  }
265
302
  }
@@ -361,6 +398,10 @@ export function askPrompt(opts = {}) {
361
398
  handlePaste(str);
362
399
  return;
363
400
  }
401
+ // Ignore EOF / Ctrl+D (\x04)
402
+ if (str === '\x04') {
403
+ return;
404
+ }
364
405
  // Ctrl+C / SIGINT
365
406
  if (str === '\x03') {
366
407
  if (lastRenderedDropdownLines > 0) {
@@ -465,14 +506,13 @@ export function askPrompt(opts = {}) {
465
506
  selectedIndex = (selectedIndex - 1 + items.length) % items.length;
466
507
  render();
467
508
  }
468
- else if (GLOBAL_PROMPT_HISTORY.length > 0 && historyIndex > 0) {
469
- if (historyIndex === GLOBAL_PROMPT_HISTORY.length) {
470
- tempDraft = input;
509
+ else {
510
+ const prev = historyNav.navigateUp(input);
511
+ if (prev !== null) {
512
+ input = prev;
513
+ cursorPos = input.length;
514
+ render();
471
515
  }
472
- historyIndex--;
473
- input = GLOBAL_PROMPT_HISTORY[historyIndex];
474
- cursorPos = input.length;
475
- render();
476
516
  }
477
517
  return;
478
518
  }
@@ -482,16 +522,13 @@ export function askPrompt(opts = {}) {
482
522
  selectedIndex = (selectedIndex + 1) % items.length;
483
523
  render();
484
524
  }
485
- else if (historyIndex < GLOBAL_PROMPT_HISTORY.length) {
486
- historyIndex++;
487
- if (historyIndex === GLOBAL_PROMPT_HISTORY.length) {
488
- input = tempDraft;
489
- }
490
- else {
491
- 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();
492
531
  }
493
- cursorPos = input.length;
494
- render();
495
532
  }
496
533
  return;
497
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.18 &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,107 @@
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
+ export function renderPowerlineStatus(opts) {
69
+ const cols = opts.cols || process.stdout.columns || 80;
70
+ const theme = getCurrentTheme();
71
+ // Mode badge
72
+ let modeBadge = '';
73
+ if (opts.isYolo) {
74
+ modeBadge = pc.bgRed(pc.bold(pc.white(' YOLO ')));
75
+ }
76
+ else if (opts.mode === 'PLAN') {
77
+ modeBadge = pc.bgCyan(pc.bold(pc.black(' PLAN ')));
78
+ }
79
+ else {
80
+ modeBadge = pc.bgGreen(pc.bold(pc.black(' AGENT ')));
81
+ }
82
+ // Model shortening for narrow screens
83
+ let displayModel = opts.model;
84
+ if (cols < 75 && displayModel.length > 18) {
85
+ const parts = displayModel.split('/');
86
+ displayModel = parts.length > 1 ? parts.slice(1).join('/') : displayModel;
87
+ if (displayModel.length > 18) {
88
+ displayModel = displayModel.slice(0, 15) + '…';
89
+ }
90
+ }
91
+ // Git info
92
+ const git = GitStatusCache.getStatus();
93
+ const gitStr = git ? ` · ${pc.magenta(git.branch)}${git.isDirty ? pc.yellow('✱') : ''}` : '';
94
+ // Context progress
95
+ const usagePercent = Math.min(100, Math.round((opts.currentTokens / Math.max(1, opts.maxTokens)) * 100));
96
+ const barWidth = cols < 60 ? 6 : 10;
97
+ const progressStr = renderProgressBar(usagePercent, barWidth);
98
+ const costStr = formatCostBadge(opts.cost, opts.maxCostUSD);
99
+ if (cols < 65) {
100
+ // 2-line layout for mobile Termux
101
+ const line1 = `${modeBadge} ${pc.bold(displayModel)}${gitStr}`;
102
+ const line2 = `${pc.dim('│')} Context ${progressStr} · ${costStr}`;
103
+ return `${line1}\n${line2}`;
104
+ }
105
+ // 1-line powerline layout for desktop
106
+ return `${modeBadge} ${pc.bold(displayModel)}${gitStr} · Context ${progressStr} · ${costStr}`;
107
+ }
@@ -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.5.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.18'
67
+ version: DEVX_VERSION
67
68
  }
68
69
  });
69
70
  if (!initResult) {
@@ -25,14 +25,19 @@ function isDangerousBashCommand(commandStr) {
25
25
  return false;
26
26
  // Normalize whitespace: collapse multiple spaces/tabs into single space
27
27
  const norm = commandStr.toLowerCase().replace(/\s+/g, ' ').trim();
28
- // Pattern matching rm with combined flags (-rf, -fr, -r -f, etc.) targeting root, home, termux prefix, or glob
29
- const isDangerousRm = /rm\s+-(?:[a-z]*r[a-z]*f|[a-z]*f[a-z]*r)\s+(?:\/|\/\*|~|~\/|\*|\$home|\$prefix|\/data\/data\/com\.termux)/i.test(norm) ||
30
- /rm\s+-[a-z]*r[a-z]*\s+-[a-z]*f[a-z]*\s+(?:\/|\/\*|~|~\/|\*|\$home|\$prefix|\/data\/data\/com\.termux)/i.test(norm) ||
31
- /rm\s+-[a-z]*f[a-z]*\s+-[a-z]*r[a-z]*\s+(?:\/|\/\*|~|~\/|\*|\$home|\$prefix|\/data\/data\/com\.termux)/i.test(norm);
28
+ // Pattern matching rm with combined flags (-rf, -fr, -r -f, etc.) targeting root, home, termux prefix, glob, or current/parent dir
29
+ const isDangerousRm = /rm\s+-(?:[a-z]*r[a-z]*f|[a-z]*f[a-z]*r)\s+(?:\/|\/\*|~|~\/|\*|\.|\.\/|\.\.\/?.*|\$home|\$prefix|\/data\/data\/com\.termux)/i.test(norm) ||
30
+ /rm\s+-[a-z]*r[a-z]*\s+-[a-z]*f[a-z]*\s+(?:\/|\/\*|~|~\/|\*|\.|\.\/|\.\.\/?.*|\$home|\$prefix|\/data\/data\/com\.termux)/i.test(norm) ||
31
+ /rm\s+-[a-z]*f[a-z]*\s+-[a-z]*r[a-z]*\s+(?:\/|\/\*|~|~\/|\*|\.|\.\/|\.\.\/?.*|\$home|\$prefix|\/data\/data\/com\.termux)/i.test(norm);
32
32
  return (isDangerousRm ||
33
33
  norm.includes('rm -rf /') ||
34
34
  norm.includes('rm -rf ~') ||
35
35
  norm.includes('rm -rf *') ||
36
+ norm.includes('rm -rf .') ||
37
+ norm.includes('rm -rf ./') ||
38
+ norm.includes('rm -rf ..') ||
39
+ norm.includes('rm -fr .') ||
40
+ norm.includes('rm -fr ./') ||
36
41
  norm.includes('rm -rf $prefix') ||
37
42
  norm.includes('rm -rf $home') ||
38
43
  norm.includes('rm -rf /data/data/com.termux') ||
@@ -220,11 +220,12 @@ export class OpenAIProvider {
220
220
  const trimmed = line.trim();
221
221
  if (!trimmed || trimmed.startsWith(':'))
222
222
  continue;
223
- if (trimmed === 'data: [DONE]')
224
- continue;
225
- if (trimmed.startsWith('data: ')) {
223
+ if (trimmed.startsWith('data:')) {
224
+ const dataPayload = trimmed.replace(/^data:\s*/, '');
225
+ if (dataPayload === '[DONE]')
226
+ continue;
226
227
  try {
227
- const json = JSON.parse(trimmed.slice(6));
228
+ const json = JSON.parse(dataPayload);
228
229
  if (json.error) {
229
230
  const errMsg = json.error.message || json.error.code || JSON.stringify(json.error);
230
231
  throw new Error(`Provider Stream Error: ${errMsg}`);