termux-dev 1.4.12 → 1.4.14

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/assets/banner.svg CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  <!-- Tagline & Badge -->
27
27
  <text x="425" y="142" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="600" fill="#8b949e" text-anchor="middle" letter-spacing="3">
28
- THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.4.12</tspan>
28
+ THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.4.14</tspan>
29
29
  </text>
30
30
 
31
31
  <!-- Top Accent Line -->
@@ -4,6 +4,15 @@ import path from 'path';
4
4
  import os from 'os';
5
5
  import { exec } from 'child_process';
6
6
  const MEDIA_DIR = path.join(process.cwd(), '.devx', 'media');
7
+ export function expandHomePath(p) {
8
+ if (!p)
9
+ return p;
10
+ const trimmed = p.trim().replace(/^['"]|['"]$/g, '');
11
+ if (trimmed === '~' || trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
12
+ return path.join(os.homedir(), trimmed.slice(2));
13
+ }
14
+ return trimmed;
15
+ }
7
16
  export function formatFileSize(bytes) {
8
17
  if (bytes < 1024)
9
18
  return `${bytes}b`;
@@ -107,7 +116,7 @@ if ($img -ne $null) {
107
116
  });
108
117
  }
109
118
  export async function processPastedFilePath(rawPath, existingNames) {
110
- const cleanPath = rawPath.trim().replace(/^['"&]+|['"]+$/g, '').trim();
119
+ const cleanPath = expandHomePath(rawPath).trim().replace(/^['"&]+|['"]+$/g, '').trim();
111
120
  const resolved = path.resolve(process.cwd(), cleanPath);
112
121
  const imageExts = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg', '.bmp']);
113
122
  const ext = path.extname(resolved).toLowerCase();
@@ -18,7 +18,7 @@ async function pingUrl(url) {
18
18
  const req = https.get(url, {
19
19
  timeout: 3000,
20
20
  headers: {
21
- 'User-Agent': 'devx-doctor/1.4.12'
21
+ 'User-Agent': 'devx-doctor/1.4.14'
22
22
  }
23
23
  }, (res) => {
24
24
  res.resume(); // consume response data to free up memory and release socket
package/dist/cli/files.js CHANGED
@@ -58,7 +58,7 @@ const IMAGE_EXTS = {
58
58
  '.svg': 'image/svg+xml'
59
59
  };
60
60
  export async function resolveAtMentions(text) {
61
- const mentionRegex = /@([a-zA-Z0-9_\-./]+)/g;
61
+ const mentionRegex = /@([a-zA-Z0-9_\-./\u0400-\u04FF]+)/g;
62
62
  const attachments = [];
63
63
  const images = [];
64
64
  const seenPaths = new Set();
@@ -35,7 +35,7 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
35
35
  });
36
36
  try {
37
37
  if (!isQuiet && !isJson) {
38
- console.log(pc.bold(pc.cyan(`⚡ devx v1.4.12 (headless) | ${planMode ? 'PLAN' : 'AGENT'} | ${config.model}`)));
38
+ console.log(pc.bold(pc.cyan(`⚡ devx v1.4.14 (headless) | ${planMode ? 'PLAN' : 'AGENT'} | ${config.model}`)));
39
39
  console.log(pc.dim(`Task: ${userPrompt}\n`));
40
40
  }
41
41
  for await (const event of agent.run(abortController.signal)) {
package/dist/cli/index.js CHANGED
@@ -16,7 +16,7 @@ import { CLIConsoleGuard } from '../permissions/guard.js';
16
16
  import { globalSnapshotManager } from '../core/snapshot.js';
17
17
  import { MemoryManager } from '../core/memory.js';
18
18
  import { startServer, stopServer, displayServerBanner } from './server.js';
19
- import { notifyDevice } from '../core/notify.js';
19
+ import { notifyDevice, acquireWakeLock } from '../core/notify.js';
20
20
  import { runDoctor } from './doctor.js';
21
21
  import { ALL_PROVIDERS } from './providers.js';
22
22
  import { getModelContextLimit } from '../core/models.js';
@@ -484,7 +484,7 @@ function drawLogo() {
484
484
  theme.colorFn(' █▀▀▄ █▀▀▀ █ █ █ █'),
485
485
  theme.colorFn(' █ █ █▀▀▀ ▀▄▀ ▀▄▀ '),
486
486
  theme.colorFn(' █▄▄▀ █▄▄▄ ▀ ▀ ▀ '),
487
- ' ' + theme.boldFn('v1.4.12'),
487
+ ' ' + theme.boldFn('v1.4.14'),
488
488
  ''
489
489
  ];
490
490
  for (const line of logo) {
@@ -499,7 +499,7 @@ function drawLogo() {
499
499
  indent + theme.colorFn('▀▀▀█▀▀▀ █▀▀▀ █▀▀█ █▄ ▄█ █ █ ▀▄ ▄▀ █▀▀▄ █▀▀▀ █ █'),
500
500
  indent + theme.colorFn(' █ █▀▀▀ █▄▄▀ █ █ █ █ █ █ ▀▀ █ █ █▀▀▀ █ █'),
501
501
  indent + theme.colorFn(' █ █▄▄▄ █ ▀▄ █ █ ▀▄▄▀ ▄▀ ▀▄ █▄▄▀ █▄▄▄ ▀▄▀ '),
502
- indent + theme.boldFn('v1.4.12'),
502
+ indent + theme.boldFn('v1.4.14'),
503
503
  ''
504
504
  ];
505
505
  for (const line of logo) {
@@ -645,7 +645,7 @@ async function handleSettings(config) {
645
645
  const maxIterLabel = maxIter >= 9999 ? 'Unlimited' : `${maxIter} steps`;
646
646
  const currentTh = getCurrentTheme();
647
647
  const choice = await select({
648
- message: `${pc.bold('⚙️ Settings')} ${pc.dim(`(devx v1.4.12 • theme: ${currentTh.name})`)}`,
648
+ message: `${pc.bold('⚙️ Settings')} ${pc.dim(`(devx v1.4.14 • theme: ${currentTh.name})`)}`,
649
649
  choices: [
650
650
  {
651
651
  name: `🎨 Color Theme: ${currentTh.emoji} ${currentTh.name}`,
@@ -686,7 +686,7 @@ async function handleSettings(config) {
686
686
  description: 'Limit how many tool steps (file edits, terminal commands) agent can do per request'
687
687
  },
688
688
  {
689
- name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.4.12 by ApvCode)')}`,
689
+ name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.4.14 by ApvCode)')}`,
690
690
  value: 'about',
691
691
  description: 'Terminal-Native AI Coding Agent created by ApvCode (https://github.com/apvcode/Termux-Dev)'
692
692
  },
@@ -702,7 +702,7 @@ async function handleSettings(config) {
702
702
  continue;
703
703
  }
704
704
  if (choice === 'about') {
705
- p.note(`⚡ devx v1.4.12 — Terminal-Native AI Coding Agent\n` +
705
+ p.note(`⚡ devx v1.4.14 — Terminal-Native AI Coding Agent\n` +
706
706
  `🎨 Theme: ${currentTh.emoji} ${currentTh.name}\n` +
707
707
  `👤 Author: ApvCode (https://github.com/apvcode)\n` +
708
708
  `🌟 Repository: https://github.com/apvcode/Termux-Dev\n` +
@@ -767,11 +767,38 @@ async function handleSettings(config) {
767
767
  return config;
768
768
  }
769
769
  export async function main() {
770
+ // === Termux / Android Stability: Global Signal Handlers ===
771
+ // Ignore SIGHUP so the process survives when Termux is minimized
772
+ if (process.platform !== 'win32') {
773
+ process.on('SIGHUP', () => {
774
+ // Termux minimized or window lost focus — keep process alive
775
+ });
776
+ }
777
+ // Graceful shutdown on SIGTERM (closing Termux tab / kill command)
778
+ process.on('SIGTERM', () => {
779
+ try {
780
+ resetTerminalTheme();
781
+ MCPManager.getInstance().stopAll();
782
+ }
783
+ catch { }
784
+ process.exit(0);
785
+ });
786
+ // Protect against crash on background I/O errors (EIO/EPIPE/ERR_STREAM_DESTROYED)
787
+ process.on('uncaughtException', (err) => {
788
+ const isIgnorableIo = err.code === 'EIO' || err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED';
789
+ if (isIgnorableIo)
790
+ return;
791
+ console.error('\n[devx runtime error]:', err.message);
792
+ });
793
+ process.on('unhandledRejection', (reason) => {
794
+ // Prevent Node.js crash from unhandled network promise rejections
795
+ });
796
+ acquireWakeLock();
770
797
  const program = new Command();
771
798
  program
772
799
  .name('devx')
773
800
  .description('Terminal-native AI coding assistant and vibe-coding agent')
774
- .version('1.4.12')
801
+ .version('1.4.14')
775
802
  .option('-p, --prompt <task>', 'Run one-shot task non-interactively (headless mode)')
776
803
  .option('-y, --yolo', 'Automatically approve all tool executions without confirmation')
777
804
  .option('-m, --model <model>', 'Specify AI model to use for this execution')
@@ -69,6 +69,9 @@ export function askPrompt(opts = {}) {
69
69
  process.stdin.setRawMode(true);
70
70
  process.stdout.write('\x1b[?2004h'); // Enable bracketed paste mode
71
71
  }
72
+ const onResize = () => { if (!disposed)
73
+ render(); };
74
+ process.stdout.on('resize', onResize);
72
75
  function getTagSpans() {
73
76
  const spans = [];
74
77
  const regex = /\[(?:Pasted text #\d+ \+\d+ lines|\d+\.png \d+kb)\]/g;
@@ -79,7 +82,7 @@ export function askPrompt(opts = {}) {
79
82
  return spans;
80
83
  }
81
84
  function formatInputWithBadges(str) {
82
- return str.replace(/(\[(?:Pasted text #\d+ \+\d+ lines|\d+\.png \d+kb)\])/g, (match) => theme.badgeFn(` ${match.slice(1, -1)} `));
85
+ return str.replace(/(\[(?:Pasted text #\d+ \+\d+ lines|\d+\.png \d+kb)\])/g, (match) => theme.badgeFn(` ${match.slice(1, -1)} `) + '\x1b[0m');
83
86
  }
84
87
  function expandPastes(text) {
85
88
  let expanded = text;
@@ -95,13 +98,11 @@ export function askPrompt(opts = {}) {
95
98
  const trimmed = text.trim();
96
99
  const lines = trimmed.split(/\r\n|\r|\n/);
97
100
  if (lines.length <= 1 && text.length < 80) {
98
- // Short paste: insert normally
99
101
  input = input.slice(0, cursorPos) + text + input.slice(cursorPos);
100
102
  cursorPos += text.length;
101
103
  render();
102
104
  return;
103
105
  }
104
- // Check if pasted text is an image path
105
106
  if (trimmed.match(/\.(png|jpe?g|webp|gif|bmp)$/i)) {
106
107
  processPastedFilePath(trimmed, usedImageNames)
107
108
  .then((imgRes) => {
@@ -130,7 +131,6 @@ export function askPrompt(opts = {}) {
130
131
  render();
131
132
  }
132
133
  function getDropdownItems() {
133
- // 1. Slash commands dropdown (when input starts with /)
134
134
  if (input.startsWith('/')) {
135
135
  const q = input.trim().toLowerCase();
136
136
  const baseList = [
@@ -146,9 +146,8 @@ export function askPrompt(opts = {}) {
146
146
  replaceLen: input.length
147
147
  }));
148
148
  }
149
- // 2. @ file mention autocomplete
150
149
  const beforeCursor = input.slice(0, cursorPos);
151
- const atMatch = beforeCursor.match(/@([a-zA-Z0-9_\-./]*)$/);
150
+ const atMatch = beforeCursor.match(/@([a-zA-Z0-9_\-./\u0400-\u04FF]*)$/);
152
151
  if (atMatch && availableFiles.length > 0) {
153
152
  const query = atMatch[1].toLowerCase();
154
153
  const replaceStart = cursorPos - atMatch[0].length;
@@ -179,7 +178,7 @@ export function askPrompt(opts = {}) {
179
178
  if (selectedIndex < 0)
180
179
  selectedIndex = 0;
181
180
  const pageSize = Math.min(4, Math.max(2, Math.floor(rows / 4)));
182
- const innerBoxWidth = Math.max(14, Math.min(cols - 6, 50));
181
+ const innerBoxWidth = Math.max(10, Math.min(cols - 8, 48));
183
182
  const total = items.length;
184
183
  let startIndex = 0;
185
184
  if (total > pageSize) {
@@ -200,47 +199,42 @@ export function askPrompt(opts = {}) {
200
199
  const mid = Math.max(0, Math.floor(innerBoxWidth / 2) - 2);
201
200
  botBorderStr = '─'.repeat(mid) + ' ▼ ' + '─'.repeat(Math.max(0, innerBoxWidth - mid - 3));
202
201
  }
203
- dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╭' + topBorderStr + '╮'));
202
+ dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╭' + topBorderStr + '╮') + '\x1b[0m');
204
203
  for (let i = startIndex; i < endIndex; i++) {
205
204
  const item = items[i];
206
205
  const isSelected = i === selectedIndex;
207
206
  const pointer = isSelected ? '› ' : ' ';
208
- const availWidth = innerBoxWidth - 2;
207
+ const availWidth = innerBoxWidth;
209
208
  let row = '';
210
- if (availWidth < 22) {
211
- const labelStr = item.label.length > availWidth
212
- ? item.label.slice(0, availWidth - 1) + '…'
213
- : item.label.padEnd(availWidth);
214
- const plain = pointer + labelStr;
215
- row = isSelected ? theme.badgeFn(plain) : pointer + theme.boldFn(labelStr);
209
+ if (availWidth < 20) {
210
+ const labelStr = item.label.length > availWidth - 2
211
+ ? item.label.slice(0, availWidth - 3) + '…'
212
+ : item.label.padEnd(availWidth - 2, ' ');
213
+ const plain = (pointer + labelStr).padEnd(availWidth, ' ').slice(0, availWidth);
214
+ row = isSelected
215
+ ? theme.badgeFn(plain) + '\x1b[0m'
216
+ : pointer + theme.boldFn(labelStr) + '\x1b[0m';
216
217
  }
217
218
  else {
218
- const labelMax = Math.min(14, Math.floor(availWidth * 0.42));
219
+ const labelMax = Math.min(13, Math.max(6, Math.floor(availWidth * 0.35)));
219
220
  const labelStr = item.label.length > labelMax
220
221
  ? item.label.slice(0, labelMax - 1) + '…'
221
- : item.label.padEnd(labelMax);
222
- const descMax = availWidth - labelMax - 1;
222
+ : item.label.padEnd(labelMax, ' ');
223
+ const descMax = Math.max(4, availWidth - labelMax - pointer.length - 1);
223
224
  const descStr = item.desc.length > descMax
224
225
  ? item.desc.slice(0, descMax - 1) + '…'
225
- : item.desc.padEnd(descMax);
226
- const plain = `${pointer}${labelStr} ${descStr}`;
226
+ : item.desc.padEnd(descMax, ' ');
227
+ const plain = (pointer + labelStr + ' ' + descStr).padEnd(availWidth, ' ').slice(0, availWidth);
227
228
  if (isSelected) {
228
- row = theme.badgeFn(plain);
229
+ row = theme.badgeFn(plain) + '\x1b[0m';
229
230
  }
230
231
  else {
231
- row = `${pointer}${theme.boldFn(labelStr)} ${pc.gray(descStr)}`;
232
+ row = `${pointer}${theme.boldFn(labelStr)} ${pc.gray(descStr)}\x1b[0m`;
232
233
  }
233
234
  }
234
- const currentLen = stripAnsi(row).length;
235
- if (currentLen < innerBoxWidth) {
236
- row += ' '.repeat(innerBoxWidth - currentLen);
237
- }
238
- else if (currentLen > innerBoxWidth) {
239
- row = row.slice(0, innerBoxWidth);
240
- }
241
- dropdownLines.push(pc.dim('│') + ' ' + pc.dim('│') + row + pc.dim('│'));
235
+ dropdownLines.push(pc.dim('│') + ' ' + pc.dim('│') + row + pc.dim('│') + '\x1b[0m');
242
236
  }
243
- dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╰' + botBorderStr + '╯'));
237
+ dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╰' + botBorderStr + '╯') + '\x1b[0m');
244
238
  }
245
239
  // 1. Format and write the Prompt line safely (guaranteed <= cols - 1 chars)
246
240
  let inputDisplay = pc.dim('│') + ' ';
@@ -248,13 +242,13 @@ export function askPrompt(opts = {}) {
248
242
  if (input.length === 0) {
249
243
  const maxPlace = Math.max(10, cols - 6);
250
244
  const displayPlace = placeholder.length > maxPlace ? placeholder.slice(0, maxPlace - 1) + '…' : placeholder;
251
- inputDisplay += pc.dim(displayPlace);
245
+ inputDisplay += pc.dim(displayPlace) + '\x1b[0m';
252
246
  renderCursorCol = 3;
253
247
  }
254
248
  else {
255
249
  const maxInputLen = Math.max(10, cols - 6);
256
250
  if (input.length <= maxInputLen) {
257
- inputDisplay += formatInputWithBadges(input);
251
+ inputDisplay += formatInputWithBadges(input) + '\x1b[0m';
258
252
  renderCursorCol = 3 + cursorPos;
259
253
  }
260
254
  else {
@@ -263,24 +257,24 @@ export function askPrompt(opts = {}) {
263
257
  const visibleChunk = input.slice(start, start + maxInputLen);
264
258
  const prefix = start > 0 ? '…' : '';
265
259
  const suffix = start + maxInputLen < input.length ? '…' : '';
266
- inputDisplay += pc.dim(prefix) + formatInputWithBadges(visibleChunk) + pc.dim(suffix);
260
+ inputDisplay += pc.dim(prefix) + formatInputWithBadges(visibleChunk) + pc.dim(suffix) + '\x1b[0m';
267
261
  renderCursorCol = 3 + (prefix ? 1 : 0) + (cursorPos - start);
268
262
  }
269
263
  }
270
- process.stdout.write(`\r\x1b[2K${inputDisplay}`);
264
+ process.stdout.write(`\x1b[0m\r\x1b[2K${inputDisplay}\x1b[0m`);
271
265
  // 2. Draw dropdown lines below, and clear extra lines from previous render
272
266
  const maxDropdowns = Math.max(dropdownLines.length, lastRenderedDropdownLines);
273
267
  if (maxDropdowns > 0) {
274
268
  for (let i = 0; i < maxDropdowns; i++) {
275
269
  if (i < dropdownLines.length) {
276
- process.stdout.write(`\n\x1b[2K${dropdownLines[i]}`);
270
+ process.stdout.write(`\n\x1b[0m\x1b[2K${dropdownLines[i]}\x1b[0m`);
277
271
  }
278
272
  else {
279
- process.stdout.write(`\n\x1b[2K`);
273
+ process.stdout.write(`\n\x1b[0m\x1b[2K`);
280
274
  }
281
275
  }
282
276
  // Move cursor back up to input line (line 0)
283
- process.stdout.write(`\x1b[${maxDropdowns}A`);
277
+ process.stdout.write(`\x1b[0m\x1b[${maxDropdowns}A`);
284
278
  }
285
279
  // 3. Place cursor precisely
286
280
  process.stdout.write(`\r\x1b[${renderCursorCol}C`);
@@ -292,6 +286,7 @@ export function askPrompt(opts = {}) {
292
286
  if (process.stdout.isTTY) {
293
287
  process.stdout.write('\x1b[?2004l');
294
288
  }
289
+ process.stdout.removeListener('resize', onResize);
295
290
  process.stdin.removeListener('data', onData);
296
291
  if (process.stdin.isTTY) {
297
292
  process.stdin.setRawMode(false);
@@ -301,9 +296,9 @@ export function askPrompt(opts = {}) {
301
296
  function clearBoxAndExit(finalInput) {
302
297
  if (lastRenderedDropdownLines > 0) {
303
298
  for (let i = 0; i < lastRenderedDropdownLines; i++) {
304
- process.stdout.write(`\n\x1b[2K`);
299
+ process.stdout.write(`\n\x1b[0m\x1b[2K`);
305
300
  }
306
- process.stdout.write(`\x1b[${lastRenderedDropdownLines}A`);
301
+ process.stdout.write(`\x1b[0m\x1b[${lastRenderedDropdownLines}A`);
307
302
  lastRenderedDropdownLines = 0;
308
303
  }
309
304
  const fullText = expandPastes(finalInput);
@@ -313,7 +308,7 @@ export function askPrompt(opts = {}) {
313
308
  GLOBAL_PROMPT_HISTORY.push(fullText.trim());
314
309
  }
315
310
  }
316
- process.stdout.write(`\r\x1b[2K${pc.dim('│')} ${formatInputWithBadges(finalInput)}\n\n`);
311
+ process.stdout.write(`\x1b[0m\r\x1b[2K${pc.dim('│')} ${formatInputWithBadges(finalInput)}\x1b[0m\n\n`);
317
312
  cleanup();
318
313
  resolve(fullText);
319
314
  }
@@ -359,12 +354,12 @@ export function askPrompt(opts = {}) {
359
354
  if (str === '\x03') {
360
355
  if (lastRenderedDropdownLines > 0) {
361
356
  for (let i = 0; i < lastRenderedDropdownLines; i++) {
362
- process.stdout.write(`\n\x1b[2K`);
357
+ process.stdout.write(`\n\x1b[0m\x1b[2K`);
363
358
  }
364
- process.stdout.write(`\x1b[${lastRenderedDropdownLines}A`);
359
+ process.stdout.write(`\x1b[0m\x1b[${lastRenderedDropdownLines}A`);
365
360
  lastRenderedDropdownLines = 0;
366
361
  }
367
- process.stdout.write(`\r\x1b[2K\n`);
362
+ process.stdout.write(`\x1b[0m\r\x1b[2K\n`);
368
363
  cleanup();
369
364
  resolve('__CANCEL__');
370
365
  return;
@@ -396,7 +391,7 @@ export function askPrompt(opts = {}) {
396
391
  render();
397
392
  }
398
393
  else {
399
- process.stdout.write(`\r\x1b[2K${pc.yellow('⚠️ No image in clipboard. Take a screenshot first (Win+Shift+S)\n')}`);
394
+ process.stdout.write(`\x1b[0m\r\x1b[2K${pc.yellow('⚠️ No image in clipboard. Take a screenshot first (Win+Shift+S)\n')}`);
400
395
  input = '';
401
396
  cursorPos = 0;
402
397
  render();
@@ -440,12 +435,12 @@ export function askPrompt(opts = {}) {
440
435
  else {
441
436
  if (lastRenderedDropdownLines > 0) {
442
437
  for (let i = 0; i < lastRenderedDropdownLines; i++) {
443
- process.stdout.write(`\n\x1b[2K`);
438
+ process.stdout.write(`\n\x1b[0m\x1b[2K`);
444
439
  }
445
- process.stdout.write(`\x1b[${lastRenderedDropdownLines}A`);
440
+ process.stdout.write(`\x1b[0m\x1b[${lastRenderedDropdownLines}A`);
446
441
  lastRenderedDropdownLines = 0;
447
442
  }
448
- process.stdout.write(`\r\x1b[2K`);
443
+ process.stdout.write(`\x1b[0m\r\x1b[2K`);
449
444
  cleanup();
450
445
  resolve(`__TOGGLE_MODE__:${input}`);
451
446
  }
@@ -551,7 +546,7 @@ export function askPrompt(opts = {}) {
551
546
  }
552
547
  return;
553
548
  }
554
- // Backspace / DEL handling (including Android IME chunks)
549
+ // Backspace / DEL handling
555
550
  if (str.includes('\x08') || str.includes('\x7f')) {
556
551
  for (let i = 0; i < str.length; i++) {
557
552
  const ch = str[i];
@@ -564,8 +559,11 @@ export function askPrompt(opts = {}) {
564
559
  cursorPos = endingSpan.start;
565
560
  }
566
561
  else {
567
- input = input.slice(0, cursorPos - 1) + input.slice(cursorPos);
568
- cursorPos--;
562
+ // Handle surrogate pairs (emoji)
563
+ const prevChar = input.charCodeAt(cursorPos - 1);
564
+ const deleteCount = (prevChar >= 0xDC00 && prevChar <= 0xDFFF && cursorPos >= 2) ? 2 : 1;
565
+ input = input.slice(0, cursorPos - deleteCount) + input.slice(cursorPos);
566
+ cursorPos -= deleteCount;
569
567
  }
570
568
  }
571
569
  }
@@ -139,7 +139,7 @@ function renderDirectoryHtml(dirPath, relPath, files, port) {
139
139
  ${parentLink}
140
140
  ${items || '<li style="padding: 20px; text-align: center; color: #6e7681;">No visible files in this directory</li>'}
141
141
  </ul>
142
- <div class="footer">devx v1.4.12 &bull; Terminal-Native AI Assistant</div>
142
+ <div class="footer">devx v1.4.14 &bull; Terminal-Native AI Assistant</div>
143
143
  </div>
144
144
  </body>
145
145
  </html>`;
@@ -224,15 +224,18 @@ export async function startServer(preferredPort = 3000) {
224
224
  return;
225
225
  }
226
226
  // 4. SPA Fallback: check if root index.html exists
227
- const rootIndex = path.join(cwd, 'index.html');
228
- if (fsSync.existsSync(rootIndex)) {
229
- const content = await fs.readFile(rootIndex);
230
- res.writeHead(200, {
231
- 'Content-Type': 'text/html; charset=utf-8',
232
- 'Access-Control-Allow-Origin': '*'
233
- });
234
- res.end(content);
235
- return;
227
+ const hasAssetExt = path.extname(reqPath).length > 0;
228
+ if (!hasAssetExt) {
229
+ const rootIndex = path.join(cwd, 'index.html');
230
+ if (fsSync.existsSync(rootIndex)) {
231
+ const content = await fs.readFile(rootIndex);
232
+ res.writeHead(200, {
233
+ 'Content-Type': 'text/html; charset=utf-8',
234
+ 'Access-Control-Allow-Origin': '*'
235
+ });
236
+ res.end(content);
237
+ return;
238
+ }
236
239
  }
237
240
  res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
238
241
  res.end(`404 Not Found: ${reqPath}`);
package/dist/cli/theme.js CHANGED
@@ -8,7 +8,7 @@ export const THEMES = {
8
8
  colorFn: (s) => pc.cyan(s),
9
9
  boldFn: (s) => pc.bold(pc.cyan(s)),
10
10
  accentFn: (s) => pc.blue(s),
11
- badgeFn: (s) => pc.bgCyan(pc.black(` ${s} `)),
11
+ badgeFn: (s) => pc.bgCyan(pc.black(s)),
12
12
  diffAddBg: (s) => pc.bgCyan(pc.black(s)),
13
13
  diffRemoveBg: (s) => pc.bgBlue(pc.white(s)),
14
14
  hex: '#00f2fe'
@@ -21,7 +21,7 @@ export const THEMES = {
21
21
  colorFn: (s) => pc.magenta(s),
22
22
  boldFn: (s) => pc.bold(pc.magenta(s)),
23
23
  accentFn: (s) => pc.blue(s),
24
- badgeFn: (s) => pc.bgMagenta(pc.black(` ${s} `)),
24
+ badgeFn: (s) => pc.bgMagenta(pc.black(s)),
25
25
  diffAddBg: (s) => pc.bgMagenta(pc.black(s)),
26
26
  diffRemoveBg: (s) => pc.bgBlue(pc.white(s)),
27
27
  hex: '#d946ef'
@@ -34,7 +34,7 @@ export const THEMES = {
34
34
  colorFn: (s) => pc.green(s),
35
35
  boldFn: (s) => pc.bold(pc.green(s)),
36
36
  accentFn: (s) => pc.cyan(s),
37
- badgeFn: (s) => pc.bgGreen(pc.black(` ${s} `)),
37
+ badgeFn: (s) => pc.bgGreen(pc.black(s)),
38
38
  diffAddBg: (s) => pc.bgGreen(pc.black(s)),
39
39
  diffRemoveBg: (s) => pc.bgRed(pc.white(s)),
40
40
  hex: '#22c55e'
@@ -47,7 +47,7 @@ export const THEMES = {
47
47
  colorFn: (s) => pc.yellow(s),
48
48
  boldFn: (s) => pc.bold(pc.yellow(s)),
49
49
  accentFn: (s) => pc.red(s),
50
- badgeFn: (s) => pc.bgYellow(pc.black(` ${s} `)),
50
+ badgeFn: (s) => pc.bgYellow(pc.black(s)),
51
51
  diffAddBg: (s) => pc.bgYellow(pc.black(s)),
52
52
  diffRemoveBg: (s) => pc.bgRed(pc.white(s)),
53
53
  hex: '#f59e0b'
@@ -60,7 +60,7 @@ export const THEMES = {
60
60
  colorFn: (s) => pc.red(s),
61
61
  boldFn: (s) => pc.bold(pc.red(s)),
62
62
  accentFn: (s) => pc.magenta(s),
63
- badgeFn: (s) => pc.bgRed(pc.white(` ${s} `)),
63
+ badgeFn: (s) => pc.bgRed(pc.white(s)),
64
64
  diffAddBg: (s) => pc.bgRed(pc.white(s)),
65
65
  diffRemoveBg: (s) => pc.bgMagenta(pc.white(s)),
66
66
  hex: '#ef4444'
@@ -73,7 +73,7 @@ export const THEMES = {
73
73
  colorFn: (s) => pc.white(s),
74
74
  boldFn: (s) => pc.bold(pc.white(s)),
75
75
  accentFn: (s) => pc.dim(s),
76
- badgeFn: (s) => pc.bgWhite(pc.black(` ${s} `)),
76
+ badgeFn: (s) => pc.bgWhite(pc.black(s)),
77
77
  diffAddBg: (s) => pc.bgWhite(pc.black(s)),
78
78
  diffRemoveBg: (s) => pc.bgBlack(pc.white(s)),
79
79
  hex: '#ffffff'
@@ -69,7 +69,7 @@ export class CustomCommandManager {
69
69
  // Support positional parameters: $1, $2, etc.
70
70
  const parts = trimmedArgs.split(/\s+/);
71
71
  for (let i = 0; i < parts.length; i++) {
72
- expanded = expanded.replace(new RegExp(`\\$${i + 1}`, 'g'), () => parts[i]);
72
+ expanded = expanded.replace(new RegExp(`\\$${i + 1}(?!\\d)`, 'g'), () => parts[i]);
73
73
  }
74
74
  return expanded;
75
75
  }
@@ -36,18 +36,36 @@ export class History {
36
36
  const chatOnly = this.messages.filter(m => m.role !== 'system');
37
37
  return this.estimateTokens(chatOnly);
38
38
  }
39
- // Эвристика: кол-во токенов = длина всех строк / 3.5
40
39
  estimateTokens(messages) {
41
- let chars = 0;
40
+ let tokens = 0;
41
+ const countStringTokens = (str) => {
42
+ if (!str)
43
+ return 0;
44
+ let t = 0;
45
+ for (let i = 0; i < str.length; i++) {
46
+ const code = str.charCodeAt(i);
47
+ if (code >= 0x0400 && code <= 0x04FF) {
48
+ t += 0.8;
49
+ }
50
+ else if (code > 0x07FF) {
51
+ t += 1.2;
52
+ }
53
+ else {
54
+ t += 0.25;
55
+ }
56
+ }
57
+ return t;
58
+ };
42
59
  for (const m of messages) {
43
- chars += m.content?.length || 0;
60
+ tokens += countStringTokens(m.content || '');
44
61
  if (m.tool_calls) {
45
62
  for (const tc of m.tool_calls) {
46
- chars += (tc.name.length + JSON.stringify(tc.arguments).length);
63
+ tokens += countStringTokens(tc.name);
64
+ tokens += countStringTokens(JSON.stringify(tc.arguments));
47
65
  }
48
66
  }
49
67
  }
50
- return Math.ceil(chars / 3.5);
68
+ return Math.ceil(tokens);
51
69
  }
52
70
  groupIntoBlocks(messages) {
53
71
  const blocks = [];
package/dist/core/loop.js CHANGED
@@ -159,8 +159,20 @@ export class Agent {
159
159
  if (!response.toolCalls || response.toolCalls.length === 0) {
160
160
  break;
161
161
  }
162
+ const executedToolCallIds = new Set();
162
163
  for (const call of response.toolCalls) {
163
164
  if (signal?.aborted) {
165
+ // Add synthetic responses for ALL remaining unexecuted tool calls
166
+ for (const remaining of response.toolCalls) {
167
+ if (!executedToolCallIds.has(remaining.id)) {
168
+ this.history.addMessage({
169
+ role: 'tool',
170
+ content: '[Operation cancelled by user]',
171
+ tool_call_id: remaining.id,
172
+ name: remaining.name,
173
+ });
174
+ }
175
+ }
164
176
  return;
165
177
  }
166
178
  const actionDesc = getActionDescription(call.name, call.arguments);
@@ -197,6 +209,7 @@ export class Agent {
197
209
  tool_call_id: call.id,
198
210
  name: call.name,
199
211
  });
212
+ executedToolCallIds.add(call.id);
200
213
  }
201
214
  iterations++;
202
215
  }