termux-dev 1.4.6 → 1.4.8

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.6</tspan>
28
+ THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.4.8</tspan>
29
29
  </text>
30
30
 
31
31
  <!-- Top Accent Line -->
@@ -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.6'
21
+ 'User-Agent': 'devx-doctor/1.4.8'
22
22
  }
23
23
  }, (res) => {
24
24
  res.resume(); // consume response data to free up memory and release socket
@@ -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.6 (headless) | ${planMode ? 'PLAN' : 'AGENT'} | ${config.model}`)));
38
+ console.log(pc.bold(pc.cyan(`⚡ devx v1.4.8 (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
@@ -484,7 +484,7 @@ function drawLogo() {
484
484
  theme.colorFn(' █▀▀▄ █▀▀▀ █ █ █ █'),
485
485
  theme.colorFn(' █ █ █▀▀▀ ▀▄▀ ▀▄▀ '),
486
486
  theme.colorFn(' █▄▄▀ █▄▄▄ ▀ ▀ ▀ '),
487
- ' ' + theme.boldFn('v1.4.6'),
487
+ ' ' + theme.boldFn('v1.4.8'),
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.6'),
502
+ indent + theme.boldFn('v1.4.8'),
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.6 • theme: ${currentTh.name})`)}`,
648
+ message: `${pc.bold('⚙️ Settings')} ${pc.dim(`(devx v1.4.8 • 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.6 by ApvCode)')}`,
689
+ name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.4.8 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.6 — Terminal-Native AI Coding Agent\n` +
705
+ p.note(`⚡ devx v1.4.8 — 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` +
@@ -771,7 +771,7 @@ export async function main() {
771
771
  program
772
772
  .name('devx')
773
773
  .description('Terminal-native AI coding assistant and vibe-coding agent')
774
- .version('1.4.6')
774
+ .version('1.4.8')
775
775
  .option('-p, --prompt <task>', 'Run one-shot task non-interactively (headless mode)')
776
776
  .option('-y, --yolo', 'Automatically approve all tool executions without confirmation')
777
777
  .option('-m, --model <model>', 'Specify AI model to use for this execution')
@@ -34,32 +34,36 @@ export const SLASH_COMMANDS = [
34
34
  { cmd: '/exit', desc: 'Exit devx' }
35
35
  ];
36
36
  const GLOBAL_PROMPT_HISTORY = [];
37
+ function stripAnsi(str) {
38
+ return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
39
+ }
37
40
  export function askPrompt(opts = {}) {
38
41
  return new Promise((resolve) => {
39
42
  process.stdin.resume();
40
- const msg = opts.message ? `${opts.message} ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}` : `Ask anything... ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`;
43
+ const msg = opts.message
44
+ ? `${opts.message} ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`
45
+ : `Ask anything... ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`;
41
46
  const placeholder = opts.placeholder || 'Describe a task, @file, /help, paste image, or press Tab to switch mode';
42
47
  let input = opts.initialValue || '';
43
48
  let cursorPos = input.length;
44
49
  let selectedIndex = 0;
45
- let lastTotalLines = 0;
50
+ let lastRenderedDropdownLines = 0;
46
51
  let disposed = false;
47
52
  let historyIndex = GLOBAL_PROMPT_HISTORY.length;
48
53
  let tempDraft = '';
49
54
  let availableFiles = [];
50
55
  let customCommandsList = [];
51
56
  // Preload project files and custom slash commands
52
- scanProjectFiles().then(files => {
53
- availableFiles = files;
54
- }).catch(() => { });
55
- CustomCommandManager.listCommands().then(cmds => {
56
- customCommandsList = cmds.map(c => ({ cmd: c.cmd, desc: c.desc }));
57
- }).catch(() => { });
57
+ scanProjectFiles()
58
+ .then((files) => (availableFiles = files))
59
+ .catch(() => { });
60
+ CustomCommandManager.listCommands()
61
+ .then((cmds) => (customCommandsList = cmds.map((c) => ({ cmd: c.cmd, desc: c.desc }))))
62
+ .catch(() => { });
58
63
  const pastes = [];
59
64
  const imageAttachments = [];
60
65
  const usedImageNames = new Set();
61
66
  const theme = getCurrentTheme();
62
- // Header printed once
63
67
  console.log(theme.colorFn('◆') + ' ' + pc.bold(msg));
64
68
  if (process.stdin.isTTY) {
65
69
  process.stdin.setRawMode(true);
@@ -68,10 +72,11 @@ export function askPrompt(opts = {}) {
68
72
  process.stdout.write('\x1b[?2004h');
69
73
  }
70
74
  function getDropdownItems() {
75
+ // 1. Theme command autocomplete
71
76
  if (input.startsWith('/theme ') || input.startsWith('/themes ') || input === '/theme') {
72
77
  const afterCmd = input.replace(/^\/(?:theme|themes)\s*/i, '').trim().toLowerCase();
73
78
  const themes = listThemes();
74
- const matched = themes.filter(t => !afterCmd ||
79
+ const matched = themes.filter((t) => !afterCmd ||
75
80
  t.id.toLowerCase().startsWith(afterCmd) ||
76
81
  t.name.toLowerCase().includes(afterCmd));
77
82
  const list = [];
@@ -95,16 +100,17 @@ export function askPrompt(opts = {}) {
95
100
  }
96
101
  return list;
97
102
  }
103
+ // 2. All Slash commands autocomplete
98
104
  if (input.startsWith('/')) {
99
105
  const q = input.trim().toLowerCase();
100
106
  const baseList = [...SLASH_COMMANDS];
101
107
  for (const cc of customCommandsList) {
102
- if (!baseList.some(b => b.cmd === cc.cmd)) {
108
+ if (!baseList.some((b) => b.cmd === cc.cmd)) {
103
109
  baseList.push({ cmd: cc.cmd, desc: cc.desc });
104
110
  }
105
111
  }
106
- const filtered = baseList.filter(c => c.cmd.toLowerCase().startsWith(q) || q === '/');
107
- return filtered.map(c => ({
112
+ const filtered = baseList.filter((c) => c.cmd.toLowerCase().startsWith(q) || q === '/');
113
+ return filtered.map((c) => ({
108
114
  label: c.cmd,
109
115
  desc: c.desc,
110
116
  replacement: c.cmd,
@@ -112,15 +118,15 @@ export function askPrompt(opts = {}) {
112
118
  replaceLen: input.length
113
119
  }));
114
120
  }
115
- // Check for @ mention at cursor position
121
+ // 3. @file autocomplete at cursor position
116
122
  const beforeCursor = input.slice(0, cursorPos);
117
123
  const atMatch = beforeCursor.match(/@([a-zA-Z0-9_\-./]*)$/);
118
124
  if (atMatch && availableFiles.length > 0) {
119
125
  const query = atMatch[1].toLowerCase();
120
126
  const matched = availableFiles
121
- .filter(f => f.toLowerCase().includes(query) || query === '')
127
+ .filter((f) => f.toLowerCase().includes(query) || query === '')
122
128
  .slice(0, 8);
123
- return matched.map(f => ({
129
+ return matched.map((f) => ({
124
130
  label: `@${f}`,
125
131
  desc: f.endsWith('/') ? 'directory' : 'file',
126
132
  replacement: `@${f}${f.endsWith('/') ? '' : ' '}`,
@@ -152,47 +158,13 @@ export function askPrompt(opts = {}) {
152
158
  }
153
159
  return result;
154
160
  }
155
- function stripAnsi(str) {
156
- return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
157
- }
158
- // Helper: clear all previously rendered lines and move cursor to line 0
159
- function clearPreviousRender() {
160
- if (lastTotalLines > 1) {
161
- process.stdout.write(`\x1b[${lastTotalLines - 1}A`);
162
- }
163
- for (let i = 0; i < lastTotalLines; i++) {
164
- process.stdout.write(`\r\x1b[2K`);
165
- if (i < lastTotalLines - 1)
166
- process.stdout.write('\n');
167
- }
168
- if (lastTotalLines > 1) {
169
- process.stdout.write(`\x1b[${lastTotalLines - 1}A`);
170
- }
171
- }
172
161
  function render() {
173
162
  if (disposed)
174
163
  return;
175
- const items = getDropdownItems();
176
164
  const cols = Math.max(28, process.stdout.columns || 80);
177
165
  const rows = Math.max(8, process.stdout.rows || 24);
178
- const prefixLen = 3;
179
- const maxPerLine = Math.max(10, cols - prefixLen - 1);
180
- const allLines = [];
181
- let inputLineCount = 0;
182
- if (input.length === 0) {
183
- const displayPlace = placeholder.length > maxPerLine ? placeholder.slice(0, maxPerLine - 1) + '…' : placeholder;
184
- allLines.push(pc.dim('│') + ' ' + pc.dim(displayPlace));
185
- inputLineCount = 1;
186
- }
187
- else {
188
- let pos = 0;
189
- while (pos < input.length) {
190
- const chunk = input.slice(pos, pos + maxPerLine);
191
- allLines.push(pc.dim('│') + ' ' + formatInputWithBadges(chunk));
192
- pos += maxPerLine;
193
- }
194
- inputLineCount = allLines.length;
195
- }
166
+ const items = getDropdownItems();
167
+ const dropdownLines = [];
196
168
  if (items.length > 0) {
197
169
  if (selectedIndex >= items.length)
198
170
  selectedIndex = 0;
@@ -223,7 +195,7 @@ export function askPrompt(opts = {}) {
223
195
  const mid = Math.max(0, Math.floor(innerBoxWidth / 2) - 2);
224
196
  botBorderStr = '─'.repeat(mid) + ' ▼ ' + '─'.repeat(Math.max(0, innerBoxWidth - mid - 3));
225
197
  }
226
- allLines.push(pc.dim('│') + ' ' + pc.dim('╭' + topBorderStr + '╮'));
198
+ dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╭' + topBorderStr + '╮'));
227
199
  for (let i = startIndex; i < endIndex; i++) {
228
200
  const item = items[i];
229
201
  const isSelected = i === selectedIndex;
@@ -231,15 +203,21 @@ export function askPrompt(opts = {}) {
231
203
  const availWidth = innerBoxWidth - 2;
232
204
  let row = '';
233
205
  if (availWidth < 20) {
234
- const labelStr = item.label.length > availWidth ? item.label.slice(0, availWidth - 1) + '…' : item.label.padEnd(availWidth);
206
+ const labelStr = item.label.length > availWidth
207
+ ? item.label.slice(0, availWidth - 1) + '…'
208
+ : item.label.padEnd(availWidth);
235
209
  const plain = pointer + labelStr;
236
- row = isSelected ? theme.badgeFn(plain) : (pointer + theme.boldFn(labelStr));
210
+ row = isSelected ? theme.badgeFn(plain) : pointer + theme.boldFn(labelStr);
237
211
  }
238
212
  else {
239
213
  const labelMax = Math.min(15, Math.floor(availWidth * 0.45));
240
- const labelStr = item.label.length > labelMax ? item.label.slice(0, labelMax - 1) + '…' : item.label.padEnd(labelMax);
214
+ const labelStr = item.label.length > labelMax
215
+ ? item.label.slice(0, labelMax - 1) + '…'
216
+ : item.label.padEnd(labelMax);
241
217
  const descMax = availWidth - labelMax - 1;
242
- const descStr = item.desc.length > descMax ? item.desc.slice(0, descMax - 1) + '…' : item.desc.padEnd(descMax);
218
+ const descStr = item.desc.length > descMax
219
+ ? item.desc.slice(0, descMax - 1) + '…'
220
+ : item.desc.padEnd(descMax);
243
221
  const plain = `${pointer}${labelStr} ${descStr}`;
244
222
  if (isSelected) {
245
223
  row = theme.badgeFn(plain);
@@ -252,33 +230,43 @@ export function askPrompt(opts = {}) {
252
230
  if (currentLen < innerBoxWidth) {
253
231
  row += ' '.repeat(innerBoxWidth - currentLen);
254
232
  }
255
- allLines.push(pc.dim('│') + ' ' + pc.dim('│') + row + pc.dim('│'));
256
- }
257
- allLines.push(pc.dim('│') + ' ' + pc.dim('╰' + botBorderStr + '╯'));
258
- }
259
- clearPreviousRender();
260
- const maxLinesToWrite = Math.max(allLines.length, lastTotalLines);
261
- for (let i = 0; i < maxLinesToWrite; i++) {
262
- process.stdout.write('\r\x1b[2K');
263
- if (i < allLines.length) {
264
- process.stdout.write(allLines[i]);
265
- }
266
- if (i < maxLinesToWrite - 1) {
267
- process.stdout.write('\n');
233
+ dropdownLines.push(pc.dim('│') + ' ' + pc.dim('│') + row + pc.dim('│'));
268
234
  }
235
+ dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╰' + botBorderStr + '╯'));
236
+ }
237
+ // 1. Move cursor up if previous dropdown had lines
238
+ if (lastRenderedDropdownLines > 0) {
239
+ process.stdout.write(`\x1b[${lastRenderedDropdownLines}A`);
269
240
  }
270
- const cursorInputLine = input.length === 0 ? 0 : Math.min(Math.floor(cursorPos / maxPerLine), inputLineCount - 1);
271
- const cursorCol = input.length === 0 ? prefixLen : prefixLen + (cursorPos % maxPerLine);
272
- const currentLine = maxLinesToWrite - 1;
273
- const moveUp = currentLine - cursorInputLine;
274
- if (moveUp > 0) {
275
- process.stdout.write(`\x1b[${moveUp}A`);
241
+ // 2. Clear line 0 and write prompt line
242
+ let inputDisplay = pc.dim('│') + ' ';
243
+ if (input.length === 0) {
244
+ const maxPlace = Math.max(12, cols - 8);
245
+ const displayPlace = placeholder.length > maxPlace ? placeholder.slice(0, maxPlace - 1) + '…' : placeholder;
246
+ inputDisplay += pc.dim(displayPlace);
276
247
  }
277
- process.stdout.write('\r');
278
- if (cursorCol > 0) {
279
- process.stdout.write(`\x1b[${cursorCol}C`);
248
+ else {
249
+ inputDisplay += formatInputWithBadges(input);
250
+ }
251
+ process.stdout.write(`\r\x1b[2K${inputDisplay}`);
252
+ // 3. Draw new dropdown lines below, and clear extra lines from previous render
253
+ const maxDropdowns = Math.max(dropdownLines.length, lastRenderedDropdownLines);
254
+ if (maxDropdowns > 0) {
255
+ for (let i = 0; i < maxDropdowns; i++) {
256
+ if (i < dropdownLines.length) {
257
+ process.stdout.write(`\n\x1b[2K${dropdownLines[i]}`);
258
+ }
259
+ else {
260
+ process.stdout.write(`\n\x1b[2K`);
261
+ }
262
+ }
263
+ // Move cursor back up to input line (line 0)
264
+ process.stdout.write(`\x1b[${maxDropdowns}A`);
280
265
  }
281
- lastTotalLines = allLines.length;
266
+ // 4. Place cursor at column 3 + cursorPos
267
+ const cursorCol = 3 + cursorPos;
268
+ process.stdout.write(`\r\x1b[${cursorCol}C`);
269
+ lastRenderedDropdownLines = dropdownLines.length;
282
270
  }
283
271
  render();
284
272
  function cleanup() {
@@ -293,27 +281,31 @@ export function askPrompt(opts = {}) {
293
281
  process.stdin.pause();
294
282
  }
295
283
  function clearBoxAndExit(finalInput) {
296
- clearPreviousRender();
297
- process.stdout.write('\r\x1b[2K');
284
+ if (lastRenderedDropdownLines > 0) {
285
+ for (let i = 0; i < lastRenderedDropdownLines; i++) {
286
+ process.stdout.write(`\n\x1b[2K`);
287
+ }
288
+ process.stdout.write(`\x1b[${lastRenderedDropdownLines}A`);
289
+ lastRenderedDropdownLines = 0;
290
+ }
298
291
  const fullText = expandPastes(finalInput);
299
292
  if (fullText.trim()) {
300
- if (GLOBAL_PROMPT_HISTORY.length === 0 || GLOBAL_PROMPT_HISTORY[GLOBAL_PROMPT_HISTORY.length - 1] !== fullText.trim()) {
293
+ if (GLOBAL_PROMPT_HISTORY.length === 0 ||
294
+ GLOBAL_PROMPT_HISTORY[GLOBAL_PROMPT_HISTORY.length - 1] !== fullText.trim()) {
301
295
  GLOBAL_PROMPT_HISTORY.push(fullText.trim());
302
296
  }
303
297
  }
304
- const lines = fullText.split('\n');
305
- process.stdout.write(`${pc.dim('│')} ${formatInputWithBadges(lines[0])}\n`);
306
- for (let i = 1; i < lines.length; i++) {
307
- process.stdout.write(pc.dim('│') + ' ' + lines[i] + '\n');
308
- }
309
- process.stdout.write('\n');
298
+ process.stdout.write(`\r\x1b[2K${pc.dim('│')} ${formatInputWithBadges(finalInput)}\n\n`);
310
299
  cleanup();
311
300
  resolve(fullText);
312
301
  }
313
302
  async function handlePaste(pastedContent) {
314
303
  const cleaned = pastedContent.replace(/\x1b\[200~/g, '').replace(/\x1b\[201~/g, '');
315
304
  // Check if pastedContent contains file path(s) to images
316
- const pathCandidates = cleaned.split(/[&;\n\r]+/).map(s => s.trim()).filter(Boolean);
305
+ const pathCandidates = cleaned
306
+ .split(/[&;\n\r]+/)
307
+ .map((s) => s.trim())
308
+ .filter(Boolean);
317
309
  let foundAnyImage = false;
318
310
  for (const cand of pathCandidates) {
319
311
  const imgRes = await processPastedFilePath(cand, usedImageNames);
@@ -406,10 +398,12 @@ export function askPrompt(opts = {}) {
406
398
  handlePaste(str);
407
399
  return;
408
400
  }
409
- // Handle individual keys
410
- // Ctrl+V (ASCII 22 / 0x16) or Ctrl+P (ASCII 16 / 0x10)
411
- if (str === '\x16' || str === '\x10' || (str.length === 1 && (str.charCodeAt(0) === 22 || str.charCodeAt(0) === 16))) {
412
- saveClipboardImage('image.png', usedImageNames).then((imgRes) => {
401
+ // Ctrl+V / Ctrl+P: Clipboard image paste
402
+ if (str === '\x16' ||
403
+ str === '\x10' ||
404
+ (str.length === 1 && (str.charCodeAt(0) === 22 || str.charCodeAt(0) === 16))) {
405
+ saveClipboardImage('image.png', usedImageNames)
406
+ .then((imgRes) => {
413
407
  if (imgRes) {
414
408
  const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
415
409
  imageAttachments.push({
@@ -423,23 +417,37 @@ export function askPrompt(opts = {}) {
423
417
  cursorPos += tag.length + 1;
424
418
  render();
425
419
  }
426
- }).catch(() => { });
420
+ })
421
+ .catch(() => { });
427
422
  return;
428
423
  }
429
424
  // Ctrl+C
430
425
  if (str === '\x03' || (str.length === 1 && str.charCodeAt(0) === 3)) {
431
- clearPreviousRender();
432
- process.stdout.write('\r\x1b[2K\n');
426
+ if (lastRenderedDropdownLines > 0) {
427
+ for (let i = 0; i < lastRenderedDropdownLines; i++) {
428
+ process.stdout.write(`\n\x1b[2K`);
429
+ }
430
+ process.stdout.write(`\x1b[${lastRenderedDropdownLines}A`);
431
+ lastRenderedDropdownLines = 0;
432
+ }
433
+ process.stdout.write(`\r\x1b[2K\n`);
433
434
  cleanup();
434
435
  resolve('__CANCEL__');
435
436
  return;
436
437
  }
437
438
  const items = getDropdownItems();
438
439
  // Enter / Return
439
- if (str === '\r' || str === '\n' || (str.length === 1 && (str.charCodeAt(0) === 13 || str.charCodeAt(0) === 10))) {
440
+ if (str === '\r' ||
441
+ str === '\n' ||
442
+ (str.length === 1 && (str.charCodeAt(0) === 13 || str.charCodeAt(0) === 10))) {
440
443
  // Direct /image command
441
- if (input.trim() === '/image' || (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length && items[selectedIndex].label === '/image')) {
442
- saveClipboardImage('image.png', usedImageNames).then((imgRes) => {
444
+ if (input.trim() === '/image' ||
445
+ (items.length > 0 &&
446
+ selectedIndex >= 0 &&
447
+ selectedIndex < items.length &&
448
+ items[selectedIndex].label === '/image')) {
449
+ saveClipboardImage('image.png', usedImageNames)
450
+ .then((imgRes) => {
443
451
  if (imgRes) {
444
452
  const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
445
453
  imageAttachments.push({
@@ -459,7 +467,8 @@ export function askPrompt(opts = {}) {
459
467
  cursorPos = 0;
460
468
  render();
461
469
  }
462
- }).catch(() => { });
470
+ })
471
+ .catch(() => { });
463
472
  return;
464
473
  }
465
474
  if (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length) {
@@ -471,7 +480,10 @@ export function askPrompt(opts = {}) {
471
480
  }
472
481
  else {
473
482
  // @ mention autocomplete on Enter
474
- input = input.slice(0, selected.replaceStart) + selected.replacement + input.slice(selected.replaceStart + selected.replaceLen);
483
+ input =
484
+ input.slice(0, selected.replaceStart) +
485
+ selected.replacement +
486
+ input.slice(selected.replaceStart + selected.replaceLen);
475
487
  cursorPos = selected.replaceStart + selected.replacement.length;
476
488
  render();
477
489
  return;
@@ -484,13 +496,22 @@ export function askPrompt(opts = {}) {
484
496
  if (str === '\t' || (str.length === 1 && str.charCodeAt(0) === 9)) {
485
497
  if (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length) {
486
498
  const selected = items[selectedIndex];
487
- input = input.slice(0, selected.replaceStart) + selected.replacement + input.slice(selected.replaceStart + selected.replaceLen);
499
+ input =
500
+ input.slice(0, selected.replaceStart) +
501
+ selected.replacement +
502
+ input.slice(selected.replaceStart + selected.replaceLen);
488
503
  cursorPos = selected.replaceStart + selected.replacement.length;
489
504
  render();
490
505
  }
491
506
  else {
492
- clearPreviousRender();
493
- process.stdout.write('\r\x1b[2K');
507
+ if (lastRenderedDropdownLines > 0) {
508
+ for (let i = 0; i < lastRenderedDropdownLines; i++) {
509
+ process.stdout.write(`\n\x1b[2K`);
510
+ }
511
+ process.stdout.write(`\x1b[${lastRenderedDropdownLines}A`);
512
+ lastRenderedDropdownLines = 0;
513
+ }
514
+ process.stdout.write(`\r\x1b[2K`);
494
515
  cleanup();
495
516
  resolve(`__TOGGLE_MODE__:${input}`);
496
517
  }
@@ -532,17 +553,17 @@ export function askPrompt(opts = {}) {
532
553
  }
533
554
  return;
534
555
  }
535
- // Left Arrow (Jump across whole paste badge atomically)
556
+ // Left Arrow
536
557
  if (str === '\x1b[D' || str === '\x1bOD') {
537
558
  if (cursorPos > 0) {
538
559
  const spans = getTagSpans();
539
- const endingSpan = spans.find(s => s.end === cursorPos);
560
+ const endingSpan = spans.find((s) => s.end === cursorPos);
540
561
  if (endingSpan) {
541
562
  cursorPos = endingSpan.start;
542
563
  }
543
564
  else {
544
565
  cursorPos--;
545
- const inside = spans.find(s => cursorPos > s.start && cursorPos < s.end);
566
+ const inside = spans.find((s) => cursorPos > s.start && cursorPos < s.end);
546
567
  if (inside)
547
568
  cursorPos = inside.start;
548
569
  }
@@ -550,17 +571,17 @@ export function askPrompt(opts = {}) {
550
571
  }
551
572
  return;
552
573
  }
553
- // Right Arrow (Jump across whole paste badge atomically)
574
+ // Right Arrow
554
575
  if (str === '\x1b[C' || str === '\x1bOC') {
555
576
  if (cursorPos < input.length) {
556
577
  const spans = getTagSpans();
557
- const startingSpan = spans.find(s => s.start === cursorPos);
578
+ const startingSpan = spans.find((s) => s.start === cursorPos);
558
579
  if (startingSpan) {
559
580
  cursorPos = startingSpan.end;
560
581
  }
561
582
  else {
562
583
  cursorPos++;
563
- const inside = spans.find(s => cursorPos > s.start && cursorPos < s.end);
584
+ const inside = spans.find((s) => cursorPos > s.start && cursorPos < s.end);
564
585
  if (inside)
565
586
  cursorPos = inside.end;
566
587
  }
@@ -580,11 +601,11 @@ export function askPrompt(opts = {}) {
580
601
  render();
581
602
  return;
582
603
  }
583
- // Delete key (\x1b[3~)
604
+ // Delete key
584
605
  if (str === '\x1b[3~') {
585
606
  if (cursorPos < input.length) {
586
607
  const spans = getTagSpans();
587
- const startingSpan = spans.find(s => s.start === cursorPos);
608
+ const startingSpan = spans.find((s) => s.start === cursorPos);
588
609
  if (startingSpan) {
589
610
  input = input.slice(0, startingSpan.start) + input.slice(startingSpan.end);
590
611
  }
@@ -596,14 +617,14 @@ export function askPrompt(opts = {}) {
596
617
  }
597
618
  return;
598
619
  }
599
- // Backspace / DEL handling (handles single or multiple backspaces from mobile IME)
620
+ // Backspace / DEL handling
600
621
  if (str.includes('\x08') || str.includes('\x7f')) {
601
622
  for (let i = 0; i < str.length; i++) {
602
623
  const ch = str[i];
603
624
  if (ch === '\x08' || ch === '\x7f') {
604
625
  if (cursorPos > 0) {
605
626
  const spans = getTagSpans();
606
- const endingSpan = spans.find(s => s.end === cursorPos);
627
+ const endingSpan = spans.find((s) => s.end === cursorPos);
607
628
  if (endingSpan) {
608
629
  input = input.slice(0, endingSpan.start) + input.slice(endingSpan.end);
609
630
  cursorPos = endingSpan.start;
@@ -616,7 +637,7 @@ export function askPrompt(opts = {}) {
616
637
  }
617
638
  else if (ch.charCodeAt(0) >= 32) {
618
639
  const spans = getTagSpans();
619
- const inside = spans.find(s => cursorPos > s.start && cursorPos < s.end);
640
+ const inside = spans.find((s) => cursorPos > s.start && cursorPos < s.end);
620
641
  if (inside)
621
642
  cursorPos = inside.end;
622
643
  input = input.slice(0, cursorPos) + ch + input.slice(cursorPos);
@@ -636,7 +657,7 @@ export function askPrompt(opts = {}) {
636
657
  if (!cleanStr)
637
658
  return;
638
659
  const spans = getTagSpans();
639
- const inside = spans.find(s => cursorPos > s.start && cursorPos < s.end);
660
+ const inside = spans.find((s) => cursorPos > s.start && cursorPos < s.end);
640
661
  if (inside) {
641
662
  cursorPos = inside.end;
642
663
  }
@@ -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.6 &bull; Terminal-Native AI Assistant</div>
142
+ <div class="footer">devx v1.4.8 &bull; Terminal-Native AI Assistant</div>
143
143
  </div>
144
144
  </body>
145
145
  </html>`;
@@ -63,7 +63,7 @@ export class MCPClient {
63
63
  capabilities: {},
64
64
  clientInfo: {
65
65
  name: 'devx',
66
- version: '1.4.6'
66
+ version: '1.4.8'
67
67
  }
68
68
  });
69
69
  if (!initResult) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termux-dev",
3
- "version": "1.4.6",
3
+ "version": "1.4.8",
4
4
  "description": "Ultra-fast, terminal-native AI coding assistant and agent built for Android Termux, Windows, macOS, and Linux.",
5
5
  "main": "dist/cli/index.js",
6
6
  "type": "module",