termux-dev 1.4.11 → 1.4.13

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.11</tspan>
28
+ THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.4.13</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.11'
21
+ 'User-Agent': 'devx-doctor/1.4.13'
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.11 (headless) | ${planMode ? 'PLAN' : 'AGENT'} | ${config.model}`)));
38
+ console.log(pc.bold(pc.cyan(`⚡ devx v1.4.13 (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.11'),
487
+ ' ' + theme.boldFn('v1.4.13'),
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.11'),
502
+ indent + theme.boldFn('v1.4.13'),
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.11 • theme: ${currentTh.name})`)}`,
648
+ message: `${pc.bold('⚙️ Settings')} ${pc.dim(`(devx v1.4.13 • 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.11 by ApvCode)')}`,
689
+ name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.4.13 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.11 — Terminal-Native AI Coding Agent\n` +
705
+ p.note(`⚡ devx v1.4.13 — 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.11')
774
+ .version('1.4.13')
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,4 +1,3 @@
1
- import readline from 'readline';
2
1
  import pc from 'picocolors';
3
2
  import { scanProjectFiles } from './files.js';
4
3
  import { saveClipboardImage, processPastedFilePath } from './clipboard.js';
@@ -35,6 +34,9 @@ export const SLASH_COMMANDS = [
35
34
  { cmd: '/exit', desc: 'Exit devx' }
36
35
  ];
37
36
  const GLOBAL_PROMPT_HISTORY = [];
37
+ function stripAnsi(str) {
38
+ return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
39
+ }
38
40
  export function askPrompt(opts = {}) {
39
41
  return new Promise((resolve) => {
40
42
  const msg = opts.message
@@ -48,79 +50,60 @@ export function askPrompt(opts = {}) {
48
50
  CustomCommandManager.listCommands()
49
51
  .then((c) => (customCommandsList = c.map((x) => ({ cmd: x.cmd, desc: x.desc }))))
50
52
  .catch(() => { });
53
+ let input = opts.initialValue || '';
54
+ let cursorPos = input.length;
55
+ let selectedIndex = 0;
56
+ let lastRenderedDropdownLines = 0;
57
+ let historyIndex = GLOBAL_PROMPT_HISTORY.length;
58
+ let tempDraft = '';
51
59
  const imageAttachments = [];
52
60
  const usedImageNames = new Set();
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_\-./]*)$/);
66
- if (atMatch && availableFiles.length > 0) {
67
- const query = atMatch[1].toLowerCase();
68
- const hits = availableFiles
69
- .filter((f) => f.toLowerCase().includes(query) || query === '')
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();
61
+ const pastedBlocks = [];
62
+ let nextPasteId = 1;
63
+ let inBracketedPaste = false;
64
+ let bracketedBuffer = '';
65
+ const placeholder = opts.placeholder || 'Describe a task, @file, /help, paste image, or press Tab to switch mode';
66
+ let disposed = false;
67
+ process.stdin.resume();
68
+ if (process.stdin.isTTY) {
69
+ process.stdin.setRawMode(true);
70
+ process.stdout.write('\x1b[?2004h'); // Enable bracketed paste mode
86
71
  }
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 === '/') {
102
- readline.cursorTo(process.stdout, 0);
103
- readline.clearLine(process.stdout, 0);
104
- cleanup();
105
- resolve('/');
106
- return;
72
+ function getTagSpans() {
73
+ const spans = [];
74
+ const regex = /\[(?:Pasted text #\d+ \+\d+ lines|\d+\.png \d+kb)\]/g;
75
+ let match;
76
+ while ((match = regex.exec(input)) !== null) {
77
+ spans.push({ start: match.index, end: match.index + match[0].length });
107
78
  }
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;
117
- }
79
+ return spans;
80
+ }
81
+ function formatInputWithBadges(str) {
82
+ return str.replace(/(\[(?:Pasted text #\d+ \+\d+ lines|\d+\.png \d+kb)\])/g, (match) => theme.badgeFn(` ${match.slice(1, -1)} `) + '\x1b[0m');
83
+ }
84
+ function expandPastes(text) {
85
+ let expanded = text;
86
+ for (const block of pastedBlocks) {
87
+ expanded = expanded.split(block.tag).join(block.text);
88
+ }
89
+ for (const img of imageAttachments) {
90
+ expanded = expanded.split(img.tag).join(`@${img.filePath.replace(/\\/g, '/')} `);
91
+ }
92
+ return expanded;
93
+ }
94
+ function handlePaste(text) {
95
+ const trimmed = text.trim();
96
+ const lines = trimmed.split(/\r\n|\r|\n/);
97
+ if (lines.length <= 1 && text.length < 80) {
98
+ input = input.slice(0, cursorPos) + text + input.slice(cursorPos);
99
+ cursorPos += text.length;
100
+ render();
101
+ return;
118
102
  }
119
- // Ctrl+V or Ctrl+P: Clipboard image paste
120
- if ((key && key.ctrl && (key.name === 'v' || key.name === 'p')) || str === '\x16' || str === '\x10') {
121
- saveClipboardImage('image.png', usedImageNames)
103
+ if (trimmed.match(/\.(png|jpe?g|webp|gif|bmp)$/i)) {
104
+ processPastedFilePath(trimmed, usedImageNames)
122
105
  .then((imgRes) => {
123
- if (imgRes && !isClosed) {
106
+ if (imgRes && !disposed) {
124
107
  const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
125
108
  imageAttachments.push({
126
109
  tag,
@@ -129,82 +112,485 @@ export function askPrompt(opts = {}) {
129
112
  sizeStr: imgRes.sizeStr
130
113
  });
131
114
  usedImageNames.add(imgRes.fileName);
132
- rl.write(`${tag} `);
115
+ input = input.slice(0, cursorPos) + tag + ' ' + input.slice(cursorPos);
116
+ cursorPos += tag.length + 1;
117
+ render();
133
118
  }
134
119
  })
135
120
  .catch(() => { });
121
+ return;
136
122
  }
137
- };
138
- if (process.stdin.isTTY) {
139
- readline.emitKeypressEvents(process.stdin, rl);
140
- process.stdin.on('keypress', onKeypress);
123
+ const id = nextPasteId++;
124
+ const tag = `[Pasted text #${id} +${lines.length} lines]`;
125
+ pastedBlocks.push({ id, lines: lines.length, text, tag });
126
+ input = input.slice(0, cursorPos) + tag + input.slice(cursorPos);
127
+ cursorPos += tag.length;
128
+ render();
141
129
  }
142
- if (opts.initialValue) {
143
- rl.write(opts.initialValue);
130
+ function getDropdownItems() {
131
+ if (input.startsWith('/')) {
132
+ const q = input.trim().toLowerCase();
133
+ const baseList = [
134
+ ...SLASH_COMMANDS.map((c) => ({ label: c.cmd, desc: c.desc, replacement: c.cmd })),
135
+ ...customCommandsList.map((c) => ({ label: c.cmd, desc: `(custom) ${c.desc}`, replacement: c.cmd }))
136
+ ];
137
+ const filtered = baseList.filter((c) => c.label.toLowerCase().startsWith(q) || q === '/');
138
+ return filtered.map((item) => ({
139
+ label: item.label,
140
+ desc: item.desc,
141
+ replacement: item.replacement,
142
+ replaceStart: 0,
143
+ replaceLen: input.length
144
+ }));
145
+ }
146
+ const beforeCursor = input.slice(0, cursorPos);
147
+ const atMatch = beforeCursor.match(/@([a-zA-Z0-9_\-./]*)$/);
148
+ if (atMatch && availableFiles.length > 0) {
149
+ const query = atMatch[1].toLowerCase();
150
+ const replaceStart = cursorPos - atMatch[0].length;
151
+ const replaceLen = atMatch[0].length;
152
+ const filtered = availableFiles
153
+ .filter((f) => f.toLowerCase().includes(query) || query === '')
154
+ .slice(0, 15);
155
+ return filtered.map((file) => ({
156
+ label: `@${file}`,
157
+ desc: file.endsWith('/') ? 'Directory' : 'File',
158
+ replacement: `@${file}${file.endsWith('/') ? '' : ' '}`,
159
+ replaceStart,
160
+ replaceLen
161
+ }));
162
+ }
163
+ return [];
144
164
  }
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;
165
+ function render() {
166
+ if (disposed)
167
+ return;
168
+ const cols = Math.max(20, process.stdout.columns || 40);
169
+ const rows = Math.max(8, process.stdout.rows || 20);
170
+ const items = getDropdownItems();
171
+ const dropdownLines = [];
172
+ if (items.length > 0) {
173
+ if (selectedIndex >= items.length)
174
+ selectedIndex = items.length - 1;
175
+ if (selectedIndex < 0)
176
+ selectedIndex = 0;
177
+ const pageSize = Math.min(4, Math.max(2, Math.floor(rows / 4)));
178
+ const innerBoxWidth = Math.max(10, Math.min(cols - 8, 48));
179
+ const total = items.length;
180
+ let startIndex = 0;
181
+ if (total > pageSize) {
182
+ if (selectedIndex >= pageSize) {
183
+ startIndex = Math.min(selectedIndex - pageSize + 1, total - pageSize);
184
+ }
185
+ }
186
+ const endIndex = Math.min(startIndex + pageSize, total);
187
+ const hasMoreUp = startIndex > 0;
188
+ const hasMoreDown = endIndex < total;
189
+ let topBorderStr = '─'.repeat(innerBoxWidth);
190
+ if (hasMoreUp) {
191
+ const mid = Math.max(0, Math.floor(innerBoxWidth / 2) - 2);
192
+ topBorderStr = '─'.repeat(mid) + ' ▲ ' + '─'.repeat(Math.max(0, innerBoxWidth - mid - 3));
193
+ }
194
+ let botBorderStr = '─'.repeat(innerBoxWidth);
195
+ if (hasMoreDown) {
196
+ const mid = Math.max(0, Math.floor(innerBoxWidth / 2) - 2);
197
+ botBorderStr = '─'.repeat(mid) + ' ▼ ' + '─'.repeat(Math.max(0, innerBoxWidth - mid - 3));
198
+ }
199
+ dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╭' + topBorderStr + '╮') + '\x1b[0m');
200
+ for (let i = startIndex; i < endIndex; i++) {
201
+ const item = items[i];
202
+ const isSelected = i === selectedIndex;
203
+ const pointer = isSelected ? '› ' : ' ';
204
+ const availWidth = innerBoxWidth;
205
+ let row = '';
206
+ if (availWidth < 20) {
207
+ const labelStr = item.label.length > availWidth - 2
208
+ ? item.label.slice(0, availWidth - 3) + '…'
209
+ : item.label.padEnd(availWidth - 2, ' ');
210
+ const plain = (pointer + labelStr).padEnd(availWidth, ' ').slice(0, availWidth);
211
+ row = isSelected
212
+ ? theme.badgeFn(plain) + '\x1b[0m'
213
+ : pointer + theme.boldFn(labelStr) + '\x1b[0m';
163
214
  }
164
215
  else {
165
- console.log(pc.yellow('⚠️ No image in clipboard. Take a screenshot first (Win+Shift+S)\n'));
166
- resolve('');
167
- return;
216
+ const labelMax = Math.min(13, Math.max(6, Math.floor(availWidth * 0.35)));
217
+ const labelStr = item.label.length > labelMax
218
+ ? item.label.slice(0, labelMax - 1) + '…'
219
+ : item.label.padEnd(labelMax, ' ');
220
+ const descMax = Math.max(4, availWidth - labelMax - pointer.length - 1);
221
+ const descStr = item.desc.length > descMax
222
+ ? item.desc.slice(0, descMax - 1) + '…'
223
+ : item.desc.padEnd(descMax, ' ');
224
+ const plain = (pointer + labelStr + ' ' + descStr).padEnd(availWidth, ' ').slice(0, availWidth);
225
+ if (isSelected) {
226
+ row = theme.badgeFn(plain) + '\x1b[0m';
227
+ }
228
+ else {
229
+ row = `${pointer}${theme.boldFn(labelStr)} ${pc.gray(descStr)}\x1b[0m`;
230
+ }
168
231
  }
232
+ dropdownLines.push(pc.dim('│') + ' ' + pc.dim('│') + row + pc.dim('│') + '\x1b[0m');
169
233
  }
170
- catch {
171
- resolve('');
172
- return;
234
+ dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╰' + botBorderStr + '╯') + '\x1b[0m');
235
+ }
236
+ // 1. Format and write the Prompt line safely (guaranteed <= cols - 1 chars)
237
+ let inputDisplay = pc.dim('│') + ' ';
238
+ let renderCursorCol = 3;
239
+ if (input.length === 0) {
240
+ const maxPlace = Math.max(10, cols - 6);
241
+ const displayPlace = placeholder.length > maxPlace ? placeholder.slice(0, maxPlace - 1) + '…' : placeholder;
242
+ inputDisplay += pc.dim(displayPlace) + '\x1b[0m';
243
+ renderCursorCol = 3;
244
+ }
245
+ else {
246
+ const maxInputLen = Math.max(10, cols - 6);
247
+ if (input.length <= maxInputLen) {
248
+ inputDisplay += formatInputWithBadges(input) + '\x1b[0m';
249
+ renderCursorCol = 3 + cursorPos;
250
+ }
251
+ else {
252
+ // Horizontal scrolling to prevent auto-wrapping
253
+ const start = Math.max(0, Math.min(cursorPos - Math.floor(maxInputLen / 2), input.length - maxInputLen));
254
+ const visibleChunk = input.slice(start, start + maxInputLen);
255
+ const prefix = start > 0 ? '…' : '';
256
+ const suffix = start + maxInputLen < input.length ? '…' : '';
257
+ inputDisplay += pc.dim(prefix) + formatInputWithBadges(visibleChunk) + pc.dim(suffix) + '\x1b[0m';
258
+ renderCursorCol = 3 + (prefix ? 1 : 0) + (cursorPos - start);
173
259
  }
174
260
  }
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;
261
+ process.stdout.write(`\x1b[0m\r\x1b[2K${inputDisplay}\x1b[0m`);
262
+ // 2. Draw dropdown lines below, and clear extra lines from previous render
263
+ const maxDropdowns = Math.max(dropdownLines.length, lastRenderedDropdownLines);
264
+ if (maxDropdowns > 0) {
265
+ for (let i = 0; i < maxDropdowns; i++) {
266
+ if (i < dropdownLines.length) {
267
+ process.stdout.write(`\n\x1b[0m\x1b[2K${dropdownLines[i]}\x1b[0m`);
268
+ }
269
+ else {
270
+ process.stdout.write(`\n\x1b[0m\x1b[2K`);
271
+ }
188
272
  }
273
+ // Move cursor back up to input line (line 0)
274
+ process.stdout.write(`\x1b[0m\x1b[${maxDropdowns}A`);
189
275
  }
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, '/')} `);
276
+ // 3. Place cursor precisely
277
+ process.stdout.write(`\r\x1b[${renderCursorCol}C`);
278
+ lastRenderedDropdownLines = dropdownLines.length;
279
+ }
280
+ render();
281
+ function cleanup() {
282
+ disposed = true;
283
+ if (process.stdout.isTTY) {
284
+ process.stdout.write('\x1b[?2004l');
285
+ }
286
+ process.stdin.removeListener('data', onData);
287
+ if (process.stdin.isTTY) {
288
+ process.stdin.setRawMode(false);
194
289
  }
195
- if (expanded.trim()) {
290
+ process.stdin.pause();
291
+ }
292
+ function clearBoxAndExit(finalInput) {
293
+ if (lastRenderedDropdownLines > 0) {
294
+ for (let i = 0; i < lastRenderedDropdownLines; i++) {
295
+ process.stdout.write(`\n\x1b[0m\x1b[2K`);
296
+ }
297
+ process.stdout.write(`\x1b[0m\x1b[${lastRenderedDropdownLines}A`);
298
+ lastRenderedDropdownLines = 0;
299
+ }
300
+ const fullText = expandPastes(finalInput);
301
+ if (fullText.trim()) {
196
302
  if (GLOBAL_PROMPT_HISTORY.length === 0 ||
197
- GLOBAL_PROMPT_HISTORY[GLOBAL_PROMPT_HISTORY.length - 1] !== expanded.trim()) {
198
- GLOBAL_PROMPT_HISTORY.push(expanded.trim());
303
+ GLOBAL_PROMPT_HISTORY[GLOBAL_PROMPT_HISTORY.length - 1] !== fullText.trim()) {
304
+ GLOBAL_PROMPT_HISTORY.push(fullText.trim());
199
305
  }
200
306
  }
201
- console.log();
202
- resolve(expanded);
203
- });
204
- rl.on('SIGINT', () => {
307
+ process.stdout.write(`\x1b[0m\r\x1b[2K${pc.dim('│')} ${formatInputWithBadges(finalInput)}\x1b[0m\n\n`);
205
308
  cleanup();
206
- console.log();
207
- resolve('__CANCEL__');
208
- });
309
+ resolve(fullText);
310
+ }
311
+ function onData(data) {
312
+ if (disposed)
313
+ return;
314
+ const str = data.toString('utf-8');
315
+ // Bracketed paste mode handling
316
+ if (str.includes('\x1b[200~')) {
317
+ inBracketedPaste = true;
318
+ bracketedBuffer = '';
319
+ const afterTag = str.split('\x1b[200~')[1] || '';
320
+ if (afterTag.includes('\x1b[201~')) {
321
+ const content = afterTag.split('\x1b[201~')[0];
322
+ inBracketedPaste = false;
323
+ handlePaste(content);
324
+ return;
325
+ }
326
+ else {
327
+ bracketedBuffer += afterTag;
328
+ return;
329
+ }
330
+ }
331
+ if (inBracketedPaste) {
332
+ if (str.includes('\x1b[201~')) {
333
+ const beforeTag = str.split('\x1b[201~')[0];
334
+ bracketedBuffer += beforeTag;
335
+ inBracketedPaste = false;
336
+ handlePaste(bracketedBuffer);
337
+ return;
338
+ }
339
+ else {
340
+ bracketedBuffer += str;
341
+ return;
342
+ }
343
+ }
344
+ // Check for raw multi-character paste with newlines or large size
345
+ if (str.length > 1 && (str.includes('\n') || str.includes('\r') || str.length > 80)) {
346
+ handlePaste(str);
347
+ return;
348
+ }
349
+ // Ctrl+C / SIGINT
350
+ if (str === '\x03') {
351
+ if (lastRenderedDropdownLines > 0) {
352
+ for (let i = 0; i < lastRenderedDropdownLines; i++) {
353
+ process.stdout.write(`\n\x1b[0m\x1b[2K`);
354
+ }
355
+ process.stdout.write(`\x1b[0m\x1b[${lastRenderedDropdownLines}A`);
356
+ lastRenderedDropdownLines = 0;
357
+ }
358
+ process.stdout.write(`\x1b[0m\r\x1b[2K\n`);
359
+ cleanup();
360
+ resolve('__CANCEL__');
361
+ return;
362
+ }
363
+ const items = getDropdownItems();
364
+ // Enter / Return
365
+ if (str === '\r' ||
366
+ str === '\n' ||
367
+ (str.length === 1 && (str.charCodeAt(0) === 13 || str.charCodeAt(0) === 10))) {
368
+ // Direct /image command
369
+ if (input.trim() === '/image' ||
370
+ (items.length > 0 &&
371
+ selectedIndex >= 0 &&
372
+ selectedIndex < items.length &&
373
+ items[selectedIndex].label === '/image')) {
374
+ saveClipboardImage('image.png', usedImageNames)
375
+ .then((imgRes) => {
376
+ if (imgRes) {
377
+ const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
378
+ imageAttachments.push({
379
+ tag,
380
+ fileName: imgRes.fileName,
381
+ filePath: imgRes.filePath,
382
+ sizeStr: imgRes.sizeStr
383
+ });
384
+ usedImageNames.add(imgRes.fileName);
385
+ input = `${tag} `;
386
+ cursorPos = input.length;
387
+ render();
388
+ }
389
+ else {
390
+ process.stdout.write(`\x1b[0m\r\x1b[2K${pc.yellow('⚠️ No image in clipboard. Take a screenshot first (Win+Shift+S)\n')}`);
391
+ input = '';
392
+ cursorPos = 0;
393
+ render();
394
+ }
395
+ })
396
+ .catch(() => { });
397
+ return;
398
+ }
399
+ if (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length) {
400
+ const selected = items[selectedIndex];
401
+ if (input.startsWith('/')) {
402
+ input = selected.replacement;
403
+ clearBoxAndExit(input);
404
+ return;
405
+ }
406
+ else {
407
+ // @ mention autocomplete on Enter
408
+ input =
409
+ input.slice(0, selected.replaceStart) +
410
+ selected.replacement +
411
+ input.slice(selected.replaceStart + selected.replaceLen);
412
+ cursorPos = selected.replaceStart + selected.replacement.length;
413
+ render();
414
+ return;
415
+ }
416
+ }
417
+ clearBoxAndExit(input);
418
+ return;
419
+ }
420
+ // Tab
421
+ if (str === '\t' || (str.length === 1 && str.charCodeAt(0) === 9)) {
422
+ if (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length) {
423
+ const selected = items[selectedIndex];
424
+ input =
425
+ input.slice(0, selected.replaceStart) +
426
+ selected.replacement +
427
+ input.slice(selected.replaceStart + selected.replaceLen);
428
+ cursorPos = selected.replaceStart + selected.replacement.length;
429
+ render();
430
+ }
431
+ else {
432
+ if (lastRenderedDropdownLines > 0) {
433
+ for (let i = 0; i < lastRenderedDropdownLines; i++) {
434
+ process.stdout.write(`\n\x1b[0m\x1b[2K`);
435
+ }
436
+ process.stdout.write(`\x1b[0m\x1b[${lastRenderedDropdownLines}A`);
437
+ lastRenderedDropdownLines = 0;
438
+ }
439
+ process.stdout.write(`\x1b[0m\r\x1b[2K`);
440
+ cleanup();
441
+ resolve(`__TOGGLE_MODE__:${input}`);
442
+ }
443
+ return;
444
+ }
445
+ // Up Arrow
446
+ if (str === '\x1b[A' || str === '\x1bOA') {
447
+ if (items.length > 0) {
448
+ selectedIndex = (selectedIndex - 1 + items.length) % items.length;
449
+ render();
450
+ }
451
+ else if (GLOBAL_PROMPT_HISTORY.length > 0 && historyIndex > 0) {
452
+ if (historyIndex === GLOBAL_PROMPT_HISTORY.length) {
453
+ tempDraft = input;
454
+ }
455
+ historyIndex--;
456
+ input = GLOBAL_PROMPT_HISTORY[historyIndex];
457
+ cursorPos = input.length;
458
+ render();
459
+ }
460
+ return;
461
+ }
462
+ // Down Arrow
463
+ if (str === '\x1b[B' || str === '\x1bOB') {
464
+ if (items.length > 0) {
465
+ selectedIndex = (selectedIndex + 1) % items.length;
466
+ render();
467
+ }
468
+ else if (historyIndex < GLOBAL_PROMPT_HISTORY.length) {
469
+ historyIndex++;
470
+ if (historyIndex === GLOBAL_PROMPT_HISTORY.length) {
471
+ input = tempDraft;
472
+ }
473
+ else {
474
+ input = GLOBAL_PROMPT_HISTORY[historyIndex];
475
+ }
476
+ cursorPos = input.length;
477
+ render();
478
+ }
479
+ return;
480
+ }
481
+ // Left Arrow
482
+ if (str === '\x1b[D' || str === '\x1bOD') {
483
+ if (cursorPos > 0) {
484
+ const spans = getTagSpans();
485
+ const endingSpan = spans.find((s) => s.end === cursorPos);
486
+ if (endingSpan) {
487
+ cursorPos = endingSpan.start;
488
+ }
489
+ else {
490
+ cursorPos--;
491
+ const inside = spans.find((s) => cursorPos > s.start && cursorPos < s.end);
492
+ if (inside)
493
+ cursorPos = inside.start;
494
+ }
495
+ render();
496
+ }
497
+ return;
498
+ }
499
+ // Right Arrow
500
+ if (str === '\x1b[C' || str === '\x1bOC') {
501
+ if (cursorPos < input.length) {
502
+ const spans = getTagSpans();
503
+ const startingSpan = spans.find((s) => s.start === cursorPos);
504
+ if (startingSpan) {
505
+ cursorPos = startingSpan.end;
506
+ }
507
+ else {
508
+ cursorPos++;
509
+ const inside = spans.find((s) => cursorPos > s.start && cursorPos < s.end);
510
+ if (inside)
511
+ cursorPos = inside.end;
512
+ }
513
+ render();
514
+ }
515
+ return;
516
+ }
517
+ // Home
518
+ if (str === '\x1b[H' || str === '\x1b[1~') {
519
+ cursorPos = 0;
520
+ render();
521
+ return;
522
+ }
523
+ // End
524
+ if (str === '\x1b[F' || str === '\x1b[4~') {
525
+ cursorPos = input.length;
526
+ render();
527
+ return;
528
+ }
529
+ // Delete key
530
+ if (str === '\x1b[3~') {
531
+ if (cursorPos < input.length) {
532
+ const spans = getTagSpans();
533
+ const startingSpan = spans.find((s) => s.start === cursorPos);
534
+ if (startingSpan) {
535
+ input = input.slice(0, startingSpan.start) + input.slice(startingSpan.end);
536
+ }
537
+ else {
538
+ input = input.slice(0, cursorPos) + input.slice(cursorPos + 1);
539
+ }
540
+ selectedIndex = 0;
541
+ render();
542
+ }
543
+ return;
544
+ }
545
+ // Backspace / DEL handling
546
+ if (str.includes('\x08') || str.includes('\x7f')) {
547
+ for (let i = 0; i < str.length; i++) {
548
+ const ch = str[i];
549
+ if (ch === '\x08' || ch === '\x7f') {
550
+ if (cursorPos > 0) {
551
+ const spans = getTagSpans();
552
+ const endingSpan = spans.find((s) => s.end === cursorPos);
553
+ if (endingSpan) {
554
+ input = input.slice(0, endingSpan.start) + input.slice(endingSpan.end);
555
+ cursorPos = endingSpan.start;
556
+ }
557
+ else {
558
+ input = input.slice(0, cursorPos - 1) + input.slice(cursorPos);
559
+ cursorPos--;
560
+ }
561
+ }
562
+ }
563
+ else if (ch.charCodeAt(0) >= 32) {
564
+ const spans = getTagSpans();
565
+ const inside = spans.find((s) => cursorPos > s.start && cursorPos < s.end);
566
+ if (inside)
567
+ cursorPos = inside.end;
568
+ input = input.slice(0, cursorPos) + ch + input.slice(cursorPos);
569
+ cursorPos++;
570
+ }
571
+ }
572
+ selectedIndex = 0;
573
+ render();
574
+ return;
575
+ }
576
+ // Ignore unknown escape sequences
577
+ if (str.startsWith('\x1b')) {
578
+ return;
579
+ }
580
+ // Normal character input - clean control characters
581
+ const cleanStr = str.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
582
+ if (!cleanStr)
583
+ return;
584
+ const spans = getTagSpans();
585
+ const inside = spans.find((s) => cursorPos > s.start && cursorPos < s.end);
586
+ if (inside) {
587
+ cursorPos = inside.end;
588
+ }
589
+ input = input.slice(0, cursorPos) + cleanStr + input.slice(cursorPos);
590
+ cursorPos += cleanStr.length;
591
+ selectedIndex = 0;
592
+ render();
593
+ }
594
+ process.stdin.on('data', onData);
209
595
  });
210
596
  }
@@ -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.11 &bull; Terminal-Native AI Assistant</div>
142
+ <div class="footer">devx v1.4.13 &bull; Terminal-Native AI Assistant</div>
143
143
  </div>
144
144
  </body>
145
145
  </html>`;
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'
@@ -63,7 +63,7 @@ export class MCPClient {
63
63
  capabilities: {},
64
64
  clientInfo: {
65
65
  name: 'devx',
66
- version: '1.4.11'
66
+ version: '1.4.13'
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.11",
3
+ "version": "1.4.13",
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",