termux-dev 1.4.8 → 1.4.10

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.8</tspan>
28
+ THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.4.10</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.8'
21
+ 'User-Agent': 'devx-doctor/1.4.10'
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.8 (headless) | ${planMode ? 'PLAN' : 'AGENT'} | ${config.model}`)));
38
+ console.log(pc.bold(pc.cyan(`⚡ devx v1.4.10 (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.8'),
487
+ ' ' + theme.boldFn('v1.4.10'),
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.8'),
502
+ indent + theme.boldFn('v1.4.10'),
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.8 • theme: ${currentTh.name})`)}`,
648
+ message: `${pc.bold('⚙️ Settings')} ${pc.dim(`(devx v1.4.10 • 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.8 by ApvCode)')}`,
689
+ name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.4.10 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.8 — Terminal-Native AI Coding Agent\n` +
705
+ p.note(`⚡ devx v1.4.10 — 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.8')
774
+ .version('1.4.10')
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')
@@ -1,7 +1,8 @@
1
+ import readline from 'readline';
1
2
  import pc from 'picocolors';
2
3
  import { scanProjectFiles } from './files.js';
3
4
  import { saveClipboardImage, processPastedFilePath } from './clipboard.js';
4
- import { getCurrentTheme, listThemes } from './theme.js';
5
+ import { getCurrentTheme } from './theme.js';
5
6
  import { CustomCommandManager } from '../core/commands.js';
6
7
  export const SLASH_COMMANDS = [
7
8
  { cmd: '/new', desc: 'Start a new clean chat session' },
@@ -34,377 +35,92 @@ export const SLASH_COMMANDS = [
34
35
  { cmd: '/exit', desc: 'Exit devx' }
35
36
  ];
36
37
  const GLOBAL_PROMPT_HISTORY = [];
37
- function stripAnsi(str) {
38
- return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
39
- }
40
38
  export function askPrompt(opts = {}) {
41
39
  return new Promise((resolve) => {
42
- process.stdin.resume();
43
40
  const msg = opts.message
44
41
  ? `${opts.message} ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`
45
42
  : `Ask anything... ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`;
46
- const placeholder = opts.placeholder || 'Describe a task, @file, /help, paste image, or press Tab to switch mode';
47
- let input = opts.initialValue || '';
48
- let cursorPos = input.length;
49
- let selectedIndex = 0;
50
- let lastRenderedDropdownLines = 0;
51
- let disposed = false;
52
- let historyIndex = GLOBAL_PROMPT_HISTORY.length;
53
- let tempDraft = '';
43
+ const theme = getCurrentTheme();
44
+ console.log(theme.colorFn('◆') + ' ' + pc.bold(msg));
54
45
  let availableFiles = [];
55
46
  let customCommandsList = [];
56
- // Preload project files and custom slash commands
57
- scanProjectFiles()
58
- .then((files) => (availableFiles = files))
59
- .catch(() => { });
47
+ scanProjectFiles().then((f) => (availableFiles = f)).catch(() => { });
60
48
  CustomCommandManager.listCommands()
61
- .then((cmds) => (customCommandsList = cmds.map((c) => ({ cmd: c.cmd, desc: c.desc }))))
49
+ .then((c) => (customCommandsList = c.map((x) => ({ cmd: x.cmd, desc: x.desc }))))
62
50
  .catch(() => { });
63
- const pastes = [];
64
51
  const imageAttachments = [];
65
52
  const usedImageNames = new Set();
66
- const theme = getCurrentTheme();
67
- console.log(theme.colorFn('◆') + ' ' + pc.bold(msg));
68
- if (process.stdin.isTTY) {
69
- process.stdin.setRawMode(true);
70
- }
71
- if (process.stdout.isTTY) {
72
- process.stdout.write('\x1b[?2004h');
73
- }
74
- function getDropdownItems() {
75
- // 1. Theme command autocomplete
76
- if (input.startsWith('/theme ') || input.startsWith('/themes ') || input === '/theme') {
77
- const afterCmd = input.replace(/^\/(?:theme|themes)\s*/i, '').trim().toLowerCase();
78
- const themes = listThemes();
79
- const matched = themes.filter((t) => !afterCmd ||
80
- t.id.toLowerCase().startsWith(afterCmd) ||
81
- t.name.toLowerCase().includes(afterCmd));
82
- const list = [];
83
- if (!afterCmd || '/theme'.startsWith(input.trim().toLowerCase())) {
84
- list.push({
85
- label: '/theme',
86
- desc: 'Interactive UI theme picker menu',
87
- replacement: '/theme',
88
- replaceStart: 0,
89
- replaceLen: input.length
90
- });
91
- }
92
- for (const t of matched) {
93
- list.push({
94
- label: `/theme ${t.id}`,
95
- desc: `${t.emoji} ${t.name} (${t.desc})`,
96
- replacement: `/theme ${t.id}`,
97
- replaceStart: 0,
98
- replaceLen: input.length
99
- });
100
- }
101
- return list;
102
- }
103
- // 2. All Slash commands autocomplete
104
- if (input.startsWith('/')) {
105
- const q = input.trim().toLowerCase();
106
- const baseList = [...SLASH_COMMANDS];
107
- for (const cc of customCommandsList) {
108
- if (!baseList.some((b) => b.cmd === cc.cmd)) {
109
- baseList.push({ cmd: cc.cmd, desc: cc.desc });
110
- }
111
- }
112
- const filtered = baseList.filter((c) => c.cmd.toLowerCase().startsWith(q) || q === '/');
113
- return filtered.map((c) => ({
114
- label: c.cmd,
115
- desc: c.desc,
116
- replacement: c.cmd,
117
- replaceStart: 0,
118
- replaceLen: input.length
119
- }));
120
- }
121
- // 3. @file autocomplete at cursor position
122
- const beforeCursor = input.slice(0, cursorPos);
123
- const atMatch = beforeCursor.match(/@([a-zA-Z0-9_\-./]*)$/);
53
+ const completer = (line) => {
54
+ const allCmds = [
55
+ ...SLASH_COMMANDS.map((c) => c.cmd),
56
+ ...customCommandsList.map((c) => c.cmd)
57
+ ];
58
+ // 1. Slash commands autocomplete
59
+ if (line.startsWith('/')) {
60
+ const q = line.trim().toLowerCase();
61
+ const hits = allCmds.filter((cmd) => cmd.toLowerCase().startsWith(q));
62
+ return [hits.length ? hits : allCmds, line];
63
+ }
64
+ // 2. @file autocomplete
65
+ const atMatch = line.match(/@([a-zA-Z0-9_\-./]*)$/);
124
66
  if (atMatch && availableFiles.length > 0) {
125
67
  const query = atMatch[1].toLowerCase();
126
- const matched = availableFiles
68
+ const hits = availableFiles
127
69
  .filter((f) => f.toLowerCase().includes(query) || query === '')
128
- .slice(0, 8);
129
- return matched.map((f) => ({
130
- label: `@${f}`,
131
- desc: f.endsWith('/') ? 'directory' : 'file',
132
- replacement: `@${f}${f.endsWith('/') ? '' : ' '}`,
133
- replaceStart: cursorPos - atMatch[0].length,
134
- replaceLen: atMatch[0].length
135
- }));
136
- }
137
- return [];
138
- }
139
- function formatInputWithBadges(rawText) {
140
- let formatted = rawText;
141
- for (const p of pastes) {
142
- const styled = pc.bold(pc.cyan(`[Pasted text #${p.id} +${p.linesCount} lines]`));
143
- formatted = formatted.split(p.tag).join(styled);
144
- }
145
- for (const img of imageAttachments) {
146
- const styled = pc.bold(pc.magenta(img.tag));
147
- formatted = formatted.split(img.tag).join(styled);
148
- }
149
- return formatted;
150
- }
151
- function expandPastes(rawText) {
152
- let result = rawText;
153
- for (const p of pastes) {
154
- result = result.split(p.tag).join(p.content);
155
- }
156
- for (const img of imageAttachments) {
157
- result = result.split(img.tag).join(`@${img.filePath.replace(/\\/g, '/')} `);
158
- }
159
- return result;
160
- }
161
- function render() {
162
- if (disposed)
163
- return;
164
- const cols = Math.max(28, process.stdout.columns || 80);
165
- const rows = Math.max(8, process.stdout.rows || 24);
166
- const items = getDropdownItems();
167
- const dropdownLines = [];
168
- if (items.length > 0) {
169
- if (selectedIndex >= items.length)
170
- selectedIndex = 0;
171
- if (selectedIndex < 0)
172
- selectedIndex = items.length - 1;
173
- const innerBoxWidth = Math.max(16, Math.min(cols - 8, 54));
174
- const isNarrowOrShort = rows <= 16 || cols < 50;
175
- const pageSize = isNarrowOrShort
176
- ? Math.max(2, Math.min(3, rows - 5))
177
- : Math.min(5, Math.max(3, Math.floor((rows - 4) / 2)));
178
- const total = items.length;
179
- let startIndex = 0;
180
- if (total > pageSize) {
181
- if (selectedIndex >= pageSize) {
182
- startIndex = Math.min(selectedIndex - pageSize + 1, total - pageSize);
183
- }
184
- }
185
- const endIndex = Math.min(startIndex + pageSize, total);
186
- const hasMoreUp = startIndex > 0;
187
- const hasMoreDown = endIndex < total;
188
- let topBorderStr = '─'.repeat(innerBoxWidth);
189
- if (hasMoreUp) {
190
- const mid = Math.max(0, Math.floor(innerBoxWidth / 2) - 2);
191
- topBorderStr = '─'.repeat(mid) + ' ▲ ' + '─'.repeat(Math.max(0, innerBoxWidth - mid - 3));
192
- }
193
- let botBorderStr = '─'.repeat(innerBoxWidth);
194
- if (hasMoreDown) {
195
- const mid = Math.max(0, Math.floor(innerBoxWidth / 2) - 2);
196
- botBorderStr = '─'.repeat(mid) + ' ▼ ' + '─'.repeat(Math.max(0, innerBoxWidth - mid - 3));
197
- }
198
- dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╭' + topBorderStr + '╮'));
199
- for (let i = startIndex; i < endIndex; i++) {
200
- const item = items[i];
201
- const isSelected = i === selectedIndex;
202
- const pointer = isSelected ? '› ' : ' ';
203
- const availWidth = innerBoxWidth - 2;
204
- let row = '';
205
- if (availWidth < 20) {
206
- const labelStr = item.label.length > availWidth
207
- ? item.label.slice(0, availWidth - 1) + '…'
208
- : item.label.padEnd(availWidth);
209
- const plain = pointer + labelStr;
210
- row = isSelected ? theme.badgeFn(plain) : pointer + theme.boldFn(labelStr);
211
- }
212
- else {
213
- const labelMax = Math.min(15, Math.floor(availWidth * 0.45));
214
- const labelStr = item.label.length > labelMax
215
- ? item.label.slice(0, labelMax - 1) + '…'
216
- : item.label.padEnd(labelMax);
217
- const descMax = availWidth - labelMax - 1;
218
- const descStr = item.desc.length > descMax
219
- ? item.desc.slice(0, descMax - 1) + '…'
220
- : item.desc.padEnd(descMax);
221
- const plain = `${pointer}${labelStr} ${descStr}`;
222
- if (isSelected) {
223
- row = theme.badgeFn(plain);
224
- }
225
- else {
226
- row = `${pointer}${theme.boldFn(labelStr)} ${pc.gray(descStr)}`;
227
- }
228
- }
229
- const currentLen = stripAnsi(row).length;
230
- if (currentLen < innerBoxWidth) {
231
- row += ' '.repeat(innerBoxWidth - currentLen);
232
- }
233
- dropdownLines.push(pc.dim('│') + ' ' + pc.dim('│') + row + pc.dim('│'));
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`);
240
- }
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);
247
- }
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`);
265
- }
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;
270
- }
271
- render();
272
- function cleanup() {
273
- disposed = true;
274
- if (process.stdout.isTTY) {
275
- process.stdout.write('\x1b[?2004l');
276
- }
277
- process.stdin.removeListener('data', onData);
278
- if (process.stdin.isTTY) {
279
- process.stdin.setRawMode(false);
280
- }
281
- process.stdin.pause();
282
- }
283
- function clearBoxAndExit(finalInput) {
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
- }
291
- const fullText = expandPastes(finalInput);
292
- if (fullText.trim()) {
293
- if (GLOBAL_PROMPT_HISTORY.length === 0 ||
294
- GLOBAL_PROMPT_HISTORY[GLOBAL_PROMPT_HISTORY.length - 1] !== fullText.trim()) {
295
- GLOBAL_PROMPT_HISTORY.push(fullText.trim());
296
- }
297
- }
298
- process.stdout.write(`\r\x1b[2K${pc.dim('│')} ${formatInputWithBadges(finalInput)}\n\n`);
299
- cleanup();
300
- resolve(fullText);
301
- }
302
- async function handlePaste(pastedContent) {
303
- const cleaned = pastedContent.replace(/\x1b\[200~/g, '').replace(/\x1b\[201~/g, '');
304
- // Check if pastedContent contains file path(s) to images
305
- const pathCandidates = cleaned
306
- .split(/[&;\n\r]+/)
307
- .map((s) => s.trim())
308
- .filter(Boolean);
309
- let foundAnyImage = false;
310
- for (const cand of pathCandidates) {
311
- const imgRes = await processPastedFilePath(cand, usedImageNames);
312
- if (imgRes) {
313
- foundAnyImage = true;
314
- const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
315
- imageAttachments.push({
316
- tag,
317
- fileName: imgRes.fileName,
318
- filePath: imgRes.filePath,
319
- sizeStr: imgRes.sizeStr
320
- });
321
- usedImageNames.add(imgRes.fileName);
322
- input = input.slice(0, cursorPos) + tag + ' ' + input.slice(cursorPos);
323
- cursorPos += tag.length + 1;
324
- }
325
- }
326
- if (foundAnyImage) {
327
- selectedIndex = 0;
328
- render();
329
- return;
330
- }
331
- const normalized = cleaned.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
332
- const lines = normalized.split('\n');
333
- if (lines.length >= 2 || normalized.length > 80) {
334
- const linesCount = lines.length;
335
- const id = pastes.length + 1;
336
- const tag = `[Pasted text #${id} +${linesCount} lines]`;
337
- pastes.push({ id, tag, content: normalized, linesCount });
338
- input = input.slice(0, cursorPos) + tag + input.slice(cursorPos);
339
- cursorPos += tag.length;
340
- selectedIndex = 0;
341
- render();
342
- }
343
- else {
344
- input = input.slice(0, cursorPos) + normalized + input.slice(cursorPos);
345
- cursorPos += normalized.length;
346
- selectedIndex = 0;
347
- render();
348
- }
70
+ .slice(0, 15)
71
+ .map((f) => `@${f}${f.endsWith('/') ? '' : ' '}`);
72
+ return [hits, atMatch[0]];
73
+ }
74
+ return [[], line];
75
+ };
76
+ const rl = readline.createInterface({
77
+ input: process.stdin,
78
+ output: process.stdout,
79
+ prompt: pc.dim('│') + ' ',
80
+ completer,
81
+ historySize: 100
82
+ });
83
+ // Populate history in readline
84
+ if (GLOBAL_PROMPT_HISTORY.length > 0) {
85
+ rl.history = [...GLOBAL_PROMPT_HISTORY].reverse();
349
86
  }
350
- function getTagSpans() {
351
- const spans = [];
352
- for (const p of pastes) {
353
- let pos = 0;
354
- while ((pos = input.indexOf(p.tag, pos)) !== -1) {
355
- spans.push({
356
- start: pos,
357
- end: pos + p.tag.length,
358
- tag: p.tag
359
- });
360
- pos += p.tag.length;
361
- }
362
- }
363
- for (const img of imageAttachments) {
364
- let pos = 0;
365
- while ((pos = input.indexOf(img.tag, pos)) !== -1) {
366
- spans.push({
367
- start: pos,
368
- end: pos + img.tag.length,
369
- tag: img.tag
370
- });
371
- pos += img.tag.length;
372
- }
373
- }
374
- return spans.sort((a, b) => a.start - b.start);
375
- }
376
- let pasteBuffer = '';
377
- let inBracketedPaste = false;
378
- function onData(chunk) {
379
- if (disposed)
87
+ let isClosed = false;
88
+ const cleanup = () => {
89
+ if (isClosed)
90
+ return;
91
+ isClosed = true;
92
+ process.stdin.removeListener('keypress', onKeypress);
93
+ rl.close();
94
+ };
95
+ const onKeypress = (str, key) => {
96
+ if (isClosed)
97
+ return;
98
+ const curLine = rl.line || '';
99
+ // INSTANT COMMAND MENU TRIGGER
100
+ // If user types '/' as the first character, instantly launch the robust search menu!
101
+ if (str === '/' && curLine.length === 0) {
102
+ readline.cursorTo(process.stdout, 0);
103
+ readline.clearLine(process.stdout, 0);
104
+ cleanup();
105
+ resolve('/');
380
106
  return;
381
- const str = chunk.toString('utf8');
382
- // Check for bracketed paste start/end
383
- if (str.includes('\x1b[200~')) {
384
- inBracketedPaste = true;
385
- pasteBuffer = '';
386
107
  }
387
- if (inBracketedPaste) {
388
- pasteBuffer += str;
389
- if (pasteBuffer.includes('\x1b[201~')) {
390
- inBracketedPaste = false;
391
- handlePaste(pasteBuffer);
392
- pasteBuffer = '';
108
+ // Tab key pressed
109
+ if (key && key.name === 'tab') {
110
+ // If line is empty or does not start with / or contain @, switch mode
111
+ if (curLine.trim() === '' || (!curLine.startsWith('/') && !curLine.includes('@'))) {
112
+ readline.cursorTo(process.stdout, 0);
113
+ readline.clearLine(process.stdout, 0);
114
+ cleanup();
115
+ resolve(`__TOGGLE_MODE__:${curLine}`);
116
+ return;
393
117
  }
394
- return;
395
118
  }
396
- // Check for raw multi-character paste with newlines or large size
397
- if (str.length > 1 && (str.includes('\n') || str.includes('\r') || str.length > 80)) {
398
- handlePaste(str);
399
- return;
400
- }
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))) {
119
+ // Ctrl+V or Ctrl+P: Clipboard image paste
120
+ if ((key && key.ctrl && (key.name === 'v' || key.name === 'p')) || str === '\x16' || str === '\x10') {
405
121
  saveClipboardImage('image.png', usedImageNames)
406
122
  .then((imgRes) => {
407
- if (imgRes) {
123
+ if (imgRes && !isClosed) {
408
124
  const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
409
125
  imageAttachments.push({
410
126
  tag,
@@ -413,259 +129,82 @@ export function askPrompt(opts = {}) {
413
129
  sizeStr: imgRes.sizeStr
414
130
  });
415
131
  usedImageNames.add(imgRes.fileName);
416
- input = input.slice(0, cursorPos) + tag + ' ' + input.slice(cursorPos);
417
- cursorPos += tag.length + 1;
418
- render();
132
+ rl.write(`${tag} `);
419
133
  }
420
134
  })
421
135
  .catch(() => { });
422
- return;
423
136
  }
424
- // Ctrl+C
425
- if (str === '\x03' || (str.length === 1 && str.charCodeAt(0) === 3)) {
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`);
434
- cleanup();
435
- resolve('__CANCEL__');
436
- return;
437
- }
438
- const items = getDropdownItems();
439
- // Enter / Return
440
- if (str === '\r' ||
441
- str === '\n' ||
442
- (str.length === 1 && (str.charCodeAt(0) === 13 || str.charCodeAt(0) === 10))) {
443
- // Direct /image command
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) => {
451
- if (imgRes) {
452
- const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
453
- imageAttachments.push({
454
- tag,
455
- fileName: imgRes.fileName,
456
- filePath: imgRes.filePath,
457
- sizeStr: imgRes.sizeStr
458
- });
459
- usedImageNames.add(imgRes.fileName);
460
- input = `${tag} `;
461
- cursorPos = input.length;
462
- render();
463
- }
464
- else {
465
- process.stdout.write(`\r\x1b[2K${pc.yellow('⚠️ No image in clipboard. Take a screenshot first (Win+Shift+S)\n')}`);
466
- input = '';
467
- cursorPos = 0;
468
- render();
469
- }
470
- })
471
- .catch(() => { });
472
- return;
473
- }
474
- if (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length) {
475
- const selected = items[selectedIndex];
476
- if (input.startsWith('/')) {
477
- input = selected.replacement;
478
- clearBoxAndExit(input);
479
- return;
137
+ };
138
+ if (process.stdin.isTTY) {
139
+ readline.emitKeypressEvents(process.stdin, rl);
140
+ process.stdin.on('keypress', onKeypress);
141
+ }
142
+ if (opts.initialValue) {
143
+ rl.write(opts.initialValue);
144
+ }
145
+ rl.prompt();
146
+ rl.on('line', async (rawLine) => {
147
+ cleanup();
148
+ let line = rawLine.trim();
149
+ // Handle direct /image command
150
+ if (line === '/image') {
151
+ try {
152
+ const imgRes = await saveClipboardImage('image.png', usedImageNames);
153
+ if (imgRes) {
154
+ const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
155
+ imageAttachments.push({
156
+ tag,
157
+ fileName: imgRes.fileName,
158
+ filePath: imgRes.filePath,
159
+ sizeStr: imgRes.sizeStr
160
+ });
161
+ usedImageNames.add(imgRes.fileName);
162
+ line = tag;
480
163
  }
481
164
  else {
482
- // @ mention autocomplete on Enter
483
- input =
484
- input.slice(0, selected.replaceStart) +
485
- selected.replacement +
486
- input.slice(selected.replaceStart + selected.replaceLen);
487
- cursorPos = selected.replaceStart + selected.replacement.length;
488
- render();
165
+ console.log(pc.yellow('⚠️ No image in clipboard. Take a screenshot first (Win+Shift+S)\n'));
166
+ resolve('');
489
167
  return;
490
168
  }
491
169
  }
492
- clearBoxAndExit(input);
493
- return;
494
- }
495
- // Tab
496
- if (str === '\t' || (str.length === 1 && str.charCodeAt(0) === 9)) {
497
- if (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length) {
498
- const selected = items[selectedIndex];
499
- input =
500
- input.slice(0, selected.replaceStart) +
501
- selected.replacement +
502
- input.slice(selected.replaceStart + selected.replaceLen);
503
- cursorPos = selected.replaceStart + selected.replacement.length;
504
- render();
505
- }
506
- else {
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`);
515
- cleanup();
516
- resolve(`__TOGGLE_MODE__:${input}`);
517
- }
518
- return;
519
- }
520
- // Up Arrow
521
- if (str === '\x1b[A' || str === '\x1bOA') {
522
- if (items.length > 0) {
523
- selectedIndex = (selectedIndex - 1 + items.length) % items.length;
524
- render();
525
- }
526
- else if (GLOBAL_PROMPT_HISTORY.length > 0 && historyIndex > 0) {
527
- if (historyIndex === GLOBAL_PROMPT_HISTORY.length) {
528
- tempDraft = input;
529
- }
530
- historyIndex--;
531
- input = GLOBAL_PROMPT_HISTORY[historyIndex];
532
- cursorPos = input.length;
533
- render();
534
- }
535
- return;
536
- }
537
- // Down Arrow
538
- if (str === '\x1b[B' || str === '\x1bOB') {
539
- if (items.length > 0) {
540
- selectedIndex = (selectedIndex + 1) % items.length;
541
- render();
542
- }
543
- else if (historyIndex < GLOBAL_PROMPT_HISTORY.length) {
544
- historyIndex++;
545
- if (historyIndex === GLOBAL_PROMPT_HISTORY.length) {
546
- input = tempDraft;
547
- }
548
- else {
549
- input = GLOBAL_PROMPT_HISTORY[historyIndex];
550
- }
551
- cursorPos = input.length;
552
- render();
553
- }
554
- return;
555
- }
556
- // Left Arrow
557
- if (str === '\x1b[D' || str === '\x1bOD') {
558
- if (cursorPos > 0) {
559
- const spans = getTagSpans();
560
- const endingSpan = spans.find((s) => s.end === cursorPos);
561
- if (endingSpan) {
562
- cursorPos = endingSpan.start;
563
- }
564
- else {
565
- cursorPos--;
566
- const inside = spans.find((s) => cursorPos > s.start && cursorPos < s.end);
567
- if (inside)
568
- cursorPos = inside.start;
569
- }
570
- render();
170
+ catch {
171
+ resolve('');
172
+ return;
571
173
  }
572
- return;
573
174
  }
574
- // Right Arrow
575
- if (str === '\x1b[C' || str === '\x1bOC') {
576
- if (cursorPos < input.length) {
577
- const spans = getTagSpans();
578
- const startingSpan = spans.find((s) => s.start === cursorPos);
579
- if (startingSpan) {
580
- cursorPos = startingSpan.end;
581
- }
582
- else {
583
- cursorPos++;
584
- const inside = spans.find((s) => cursorPos > s.start && cursorPos < s.end);
585
- if (inside)
586
- cursorPos = inside.end;
587
- }
588
- render();
175
+ // Check for pasted file path to image
176
+ if (line.match(/\.(png|jpe?g|webp|gif|bmp)$/i)) {
177
+ const imgRes = await processPastedFilePath(line, usedImageNames);
178
+ if (imgRes) {
179
+ const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
180
+ imageAttachments.push({
181
+ tag,
182
+ fileName: imgRes.fileName,
183
+ filePath: imgRes.filePath,
184
+ sizeStr: imgRes.sizeStr
185
+ });
186
+ usedImageNames.add(imgRes.fileName);
187
+ line = tag;
589
188
  }
590
- return;
591
189
  }
592
- // Home
593
- if (str === '\x1b[H' || str === '\x1b[1~') {
594
- cursorPos = 0;
595
- render();
596
- return;
597
- }
598
- // End
599
- if (str === '\x1b[F' || str === '\x1b[4~') {
600
- cursorPos = input.length;
601
- render();
602
- return;
603
- }
604
- // Delete key
605
- if (str === '\x1b[3~') {
606
- if (cursorPos < input.length) {
607
- const spans = getTagSpans();
608
- const startingSpan = spans.find((s) => s.start === cursorPos);
609
- if (startingSpan) {
610
- input = input.slice(0, startingSpan.start) + input.slice(startingSpan.end);
611
- }
612
- else {
613
- input = input.slice(0, cursorPos) + input.slice(cursorPos + 1);
614
- }
615
- selectedIndex = 0;
616
- render();
617
- }
618
- return;
190
+ // Expand image attachment tags to @path
191
+ let expanded = line;
192
+ for (const img of imageAttachments) {
193
+ expanded = expanded.split(img.tag).join(`@${img.filePath.replace(/\\/g, '/')} `);
619
194
  }
620
- // Backspace / DEL handling
621
- if (str.includes('\x08') || str.includes('\x7f')) {
622
- for (let i = 0; i < str.length; i++) {
623
- const ch = str[i];
624
- if (ch === '\x08' || ch === '\x7f') {
625
- if (cursorPos > 0) {
626
- const spans = getTagSpans();
627
- const endingSpan = spans.find((s) => s.end === cursorPos);
628
- if (endingSpan) {
629
- input = input.slice(0, endingSpan.start) + input.slice(endingSpan.end);
630
- cursorPos = endingSpan.start;
631
- }
632
- else {
633
- input = input.slice(0, cursorPos - 1) + input.slice(cursorPos);
634
- cursorPos--;
635
- }
636
- }
637
- }
638
- else if (ch.charCodeAt(0) >= 32) {
639
- const spans = getTagSpans();
640
- const inside = spans.find((s) => cursorPos > s.start && cursorPos < s.end);
641
- if (inside)
642
- cursorPos = inside.end;
643
- input = input.slice(0, cursorPos) + ch + input.slice(cursorPos);
644
- cursorPos++;
645
- }
195
+ if (expanded.trim()) {
196
+ if (GLOBAL_PROMPT_HISTORY.length === 0 ||
197
+ GLOBAL_PROMPT_HISTORY[GLOBAL_PROMPT_HISTORY.length - 1] !== expanded.trim()) {
198
+ GLOBAL_PROMPT_HISTORY.push(expanded.trim());
646
199
  }
647
- selectedIndex = 0;
648
- render();
649
- return;
650
- }
651
- // Ignore unknown escape sequences
652
- if (str.startsWith('\x1b')) {
653
- return;
654
- }
655
- // Normal character input - clean control characters
656
- const cleanStr = str.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
657
- if (!cleanStr)
658
- return;
659
- const spans = getTagSpans();
660
- const inside = spans.find((s) => cursorPos > s.start && cursorPos < s.end);
661
- if (inside) {
662
- cursorPos = inside.end;
663
200
  }
664
- input = input.slice(0, cursorPos) + cleanStr + input.slice(cursorPos);
665
- cursorPos += cleanStr.length;
666
- selectedIndex = 0;
667
- render();
668
- }
669
- process.stdin.on('data', onData);
201
+ console.log();
202
+ resolve(expanded);
203
+ });
204
+ rl.on('SIGINT', () => {
205
+ cleanup();
206
+ console.log();
207
+ resolve('__CANCEL__');
208
+ });
670
209
  });
671
210
  }
@@ -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.8 &bull; Terminal-Native AI Assistant</div>
142
+ <div class="footer">devx v1.4.10 &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.8'
66
+ version: '1.4.10'
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.8",
3
+ "version": "1.4.10",
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",