termux-dev 1.0.2

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.
@@ -0,0 +1,553 @@
1
+ import pc from 'picocolors';
2
+ import { scanProjectFiles } from './files.js';
3
+ import { saveClipboardImage, processPastedFilePath } from './clipboard.js';
4
+ export const SLASH_COMMANDS = [
5
+ { cmd: '/new', desc: 'Start a new clean chat session' },
6
+ { cmd: '/resume', desc: 'Resume a previous chat session' },
7
+ { cmd: '/session', desc: 'Show active session ID, stats, and info' },
8
+ { cmd: '/session del', desc: 'Select and delete saved sessions' },
9
+ { cmd: '/settings', desc: 'Configure permissions & auto-approval' },
10
+ { cmd: '/update', desc: 'Check and install updates from GitHub' },
11
+ { cmd: '/model', desc: 'Switch model for current provider' },
12
+ { cmd: '/provider', desc: 'Switch AI provider (OpenRouter, Google, etc.)' },
13
+ { cmd: '/plan', desc: 'Switch to PLAN mode (architect & planner)' },
14
+ { cmd: '/agent', desc: 'Switch to AGENT mode (coder & executor)' },
15
+ { cmd: '/image', desc: 'Paste image from clipboard as [1.png 203kb]' },
16
+ { cmd: '/serve', desc: 'Start local web server for web/game preview' },
17
+ { cmd: '/memory', desc: 'View, add, or clear project memory bank' },
18
+ { cmd: '/undo', desc: 'Revert last file changes made by AI' },
19
+ { cmd: '/diff', desc: 'Show git diff of modified project files' },
20
+ { cmd: '/commit', desc: 'AI-generated git commit message & commit' },
21
+ { cmd: '/status', desc: 'Show git repository file status' },
22
+ { cmd: '/compact', desc: 'Compact & summarize chat context' },
23
+ { cmd: '/clear', desc: 'Clear screen & redraw banner' },
24
+ { cmd: '/init', desc: 'Generate AGENTS.md project instructions' },
25
+ { cmd: '/config', desc: 'View current configuration' },
26
+ { cmd: '/help', desc: 'Show all available commands' },
27
+ { cmd: '/exit', desc: 'Exit devx' }
28
+ ];
29
+ const GLOBAL_PROMPT_HISTORY = [];
30
+ export function askPrompt(opts = {}) {
31
+ return new Promise((resolve) => {
32
+ process.stdin.resume();
33
+ const msg = opts.message ? `${opts.message} ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}` : `Ask anything... ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`;
34
+ const placeholder = opts.placeholder || 'Describe a task, @file, /help, paste image, or press Tab to switch mode';
35
+ let input = opts.initialValue || '';
36
+ let cursorPos = input.length;
37
+ let selectedIndex = 0;
38
+ let lastDropdownLines = 0;
39
+ let disposed = false;
40
+ let historyIndex = GLOBAL_PROMPT_HISTORY.length;
41
+ let tempDraft = '';
42
+ let availableFiles = [];
43
+ // Preload project files for fast @ autocomplete
44
+ scanProjectFiles().then(files => {
45
+ availableFiles = files;
46
+ }).catch(() => { });
47
+ const pastes = [];
48
+ const imageAttachments = [];
49
+ const usedImageNames = new Set();
50
+ // Header printed once
51
+ console.log(pc.cyan('◆') + ' ' + pc.bold(msg));
52
+ if (process.stdin.isTTY) {
53
+ process.stdin.setRawMode(true);
54
+ }
55
+ if (process.stdout.isTTY) {
56
+ process.stdout.write('\x1b[?2004h');
57
+ }
58
+ function getDropdownItems() {
59
+ if (input.startsWith('/')) {
60
+ const q = input.trim().toLowerCase();
61
+ const filtered = SLASH_COMMANDS.filter(c => c.cmd.toLowerCase().startsWith(q) || q === '/');
62
+ return filtered.map(c => ({
63
+ label: c.cmd,
64
+ desc: c.desc,
65
+ replacement: c.cmd,
66
+ replaceStart: 0,
67
+ replaceLen: input.length
68
+ }));
69
+ }
70
+ // Check for @ mention at cursor position
71
+ const beforeCursor = input.slice(0, cursorPos);
72
+ const atMatch = beforeCursor.match(/@([a-zA-Z0-9_\-./]*)$/);
73
+ if (atMatch && availableFiles.length > 0) {
74
+ const query = atMatch[1].toLowerCase();
75
+ const matched = availableFiles
76
+ .filter(f => f.toLowerCase().includes(query) || query === '')
77
+ .slice(0, 8);
78
+ return matched.map(f => ({
79
+ label: `@${f}`,
80
+ desc: f.endsWith('/') ? 'directory' : 'file',
81
+ replacement: `@${f}${f.endsWith('/') ? '' : ' '}`,
82
+ replaceStart: cursorPos - atMatch[0].length,
83
+ replaceLen: atMatch[0].length
84
+ }));
85
+ }
86
+ return [];
87
+ }
88
+ function formatInputWithBadges(rawText) {
89
+ let formatted = rawText;
90
+ for (const p of pastes) {
91
+ const styled = pc.bold(pc.cyan(`[Pasted text #${p.id} +${p.linesCount} lines]`));
92
+ formatted = formatted.split(p.tag).join(styled);
93
+ }
94
+ for (const img of imageAttachments) {
95
+ const styled = pc.bold(pc.magenta(img.tag));
96
+ formatted = formatted.split(img.tag).join(styled);
97
+ }
98
+ return formatted;
99
+ }
100
+ function expandPastes(rawText) {
101
+ let result = rawText;
102
+ for (const p of pastes) {
103
+ result = result.split(p.tag).join(p.content);
104
+ }
105
+ for (const img of imageAttachments) {
106
+ result = result.split(img.tag).join(`@${img.filePath.replace(/\\/g, '/')} `);
107
+ }
108
+ return result;
109
+ }
110
+ function render() {
111
+ if (disposed)
112
+ return;
113
+ const items = getDropdownItems();
114
+ const dropdownLines = [];
115
+ if (items.length > 0) {
116
+ if (selectedIndex >= items.length)
117
+ selectedIndex = 0;
118
+ if (selectedIndex < 0)
119
+ selectedIndex = items.length - 1;
120
+ const boxWidth = Math.min((process.stdout.columns || 80) - 6, 60);
121
+ const pageSize = Math.min(5, Math.max(3, Math.floor(((process.stdout.rows || 24) - 4) / 2)));
122
+ const total = items.length;
123
+ let startIndex = 0;
124
+ if (total > pageSize) {
125
+ if (selectedIndex >= pageSize) {
126
+ startIndex = Math.min(selectedIndex - pageSize + 1, total - pageSize);
127
+ }
128
+ }
129
+ const endIndex = Math.min(startIndex + pageSize, total);
130
+ const hasMoreUp = startIndex > 0;
131
+ const hasMoreDown = endIndex < total;
132
+ let topBorderStr = '─'.repeat(boxWidth);
133
+ if (hasMoreUp) {
134
+ const mid = Math.max(0, Math.floor(boxWidth / 2) - 2);
135
+ topBorderStr = '─'.repeat(mid) + ' ▲ ' + '─'.repeat(Math.max(0, boxWidth - mid - 3));
136
+ }
137
+ let botBorderStr = '─'.repeat(boxWidth);
138
+ if (hasMoreDown) {
139
+ const mid = Math.max(0, Math.floor(boxWidth / 2) - 2);
140
+ botBorderStr = '─'.repeat(mid) + ' ▼ ' + '─'.repeat(Math.max(0, boxWidth - mid - 3));
141
+ }
142
+ dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╭' + topBorderStr + '╮'));
143
+ for (let i = startIndex; i < endIndex; i++) {
144
+ const item = items[i];
145
+ const isSelected = i === selectedIndex;
146
+ const labelStr = item.label.length > 20 ? item.label.slice(0, 19) + '…' : item.label.padEnd(20);
147
+ const maxDescLen = Math.max(6, boxWidth - 25);
148
+ const descStr = item.desc.length > maxDescLen ? item.desc.slice(0, maxDescLen - 3) + '...' : item.desc.padEnd(maxDescLen);
149
+ let row = ` ${isSelected ? pc.cyan('›') : ' '} ${isSelected ? pc.bold(pc.cyan(labelStr)) : pc.white(labelStr)} ${pc.gray(descStr)} `;
150
+ if (isSelected) {
151
+ row = pc.bgCyan(pc.black(` › ${labelStr} ${descStr} `));
152
+ }
153
+ dropdownLines.push(pc.dim('│') + ' ' + pc.dim('│') + row + pc.dim('│'));
154
+ }
155
+ dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╰' + botBorderStr + '╯'));
156
+ }
157
+ // 1. Draw/update input line (line 0)
158
+ let inputDisplay = pc.dim('│') + ' ';
159
+ if (input.length === 0) {
160
+ inputDisplay += pc.dim(placeholder);
161
+ }
162
+ else {
163
+ inputDisplay += formatInputWithBadges(input);
164
+ }
165
+ process.stdout.write(`\r\x1b[2K${inputDisplay}`);
166
+ // 2. Draw dropdown lines below, and clear any leftover old lines
167
+ const totalLinesToProcess = Math.max(dropdownLines.length, lastDropdownLines);
168
+ if (totalLinesToProcess > 0) {
169
+ for (let i = 0; i < totalLinesToProcess; i++) {
170
+ if (i < dropdownLines.length) {
171
+ process.stdout.write(`\n\x1b[2K${dropdownLines[i]}`);
172
+ }
173
+ else {
174
+ process.stdout.write(`\n\x1b[2K`);
175
+ }
176
+ }
177
+ // 3. Move cursor back UP to prompt line (line 0) at cursorPos column
178
+ process.stdout.write(`\x1b[${totalLinesToProcess}A\r\x1b[${3 + cursorPos}C`);
179
+ }
180
+ else {
181
+ process.stdout.write(`\r\x1b[${3 + cursorPos}C`);
182
+ }
183
+ lastDropdownLines = dropdownLines.length;
184
+ }
185
+ render();
186
+ function cleanup() {
187
+ disposed = true;
188
+ if (process.stdout.isTTY) {
189
+ process.stdout.write('\x1b[?2004l');
190
+ }
191
+ process.stdin.removeListener('data', onData);
192
+ if (process.stdin.isTTY) {
193
+ process.stdin.setRawMode(false);
194
+ }
195
+ process.stdin.pause();
196
+ }
197
+ function clearBoxAndExit(finalInput) {
198
+ if (lastDropdownLines > 0) {
199
+ for (let i = 0; i < lastDropdownLines; i++) {
200
+ process.stdout.write(`\n\x1b[2K`);
201
+ }
202
+ process.stdout.write(`\x1b[${lastDropdownLines}A\r`);
203
+ lastDropdownLines = 0;
204
+ }
205
+ const fullText = expandPastes(finalInput);
206
+ if (fullText.trim()) {
207
+ if (GLOBAL_PROMPT_HISTORY.length === 0 || GLOBAL_PROMPT_HISTORY[GLOBAL_PROMPT_HISTORY.length - 1] !== fullText.trim()) {
208
+ GLOBAL_PROMPT_HISTORY.push(fullText.trim());
209
+ }
210
+ }
211
+ const lines = fullText.split('\n');
212
+ process.stdout.write(`\r\x1b[2K${pc.dim('│')} ${formatInputWithBadges(lines[0])}\n`);
213
+ for (let i = 1; i < lines.length; i++) {
214
+ process.stdout.write(pc.dim('│') + ' ' + lines[i] + '\n');
215
+ }
216
+ process.stdout.write('\n');
217
+ cleanup();
218
+ resolve(fullText);
219
+ }
220
+ async function handlePaste(pastedContent) {
221
+ const cleaned = pastedContent.replace(/\x1b\[200~/g, '').replace(/\x1b\[201~/g, '');
222
+ // Check if pastedContent contains file path(s) to images
223
+ const pathCandidates = cleaned.split(/[&;\n\r]+/).map(s => s.trim()).filter(Boolean);
224
+ let foundAnyImage = false;
225
+ for (const cand of pathCandidates) {
226
+ const imgRes = await processPastedFilePath(cand, usedImageNames);
227
+ if (imgRes) {
228
+ foundAnyImage = true;
229
+ const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
230
+ imageAttachments.push({
231
+ tag,
232
+ fileName: imgRes.fileName,
233
+ filePath: imgRes.filePath,
234
+ sizeStr: imgRes.sizeStr
235
+ });
236
+ usedImageNames.add(imgRes.fileName);
237
+ input = input.slice(0, cursorPos) + tag + ' ' + input.slice(cursorPos);
238
+ cursorPos += tag.length + 1;
239
+ }
240
+ }
241
+ if (foundAnyImage) {
242
+ selectedIndex = 0;
243
+ render();
244
+ return;
245
+ }
246
+ const normalized = cleaned.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
247
+ const lines = normalized.split('\n');
248
+ if (lines.length >= 2 || normalized.length > 80) {
249
+ const linesCount = lines.length;
250
+ const id = pastes.length + 1;
251
+ const tag = `[Pasted text #${id} +${linesCount} lines]`;
252
+ pastes.push({ id, tag, content: normalized, linesCount });
253
+ input = input.slice(0, cursorPos) + tag + input.slice(cursorPos);
254
+ cursorPos += tag.length;
255
+ selectedIndex = 0;
256
+ render();
257
+ }
258
+ else {
259
+ input = input.slice(0, cursorPos) + normalized + input.slice(cursorPos);
260
+ cursorPos += normalized.length;
261
+ selectedIndex = 0;
262
+ render();
263
+ }
264
+ }
265
+ function getTagSpans() {
266
+ const spans = [];
267
+ for (const p of pastes) {
268
+ let pos = 0;
269
+ while ((pos = input.indexOf(p.tag, pos)) !== -1) {
270
+ spans.push({
271
+ start: pos,
272
+ end: pos + p.tag.length,
273
+ tag: p.tag
274
+ });
275
+ pos += p.tag.length;
276
+ }
277
+ }
278
+ for (const img of imageAttachments) {
279
+ let pos = 0;
280
+ while ((pos = input.indexOf(img.tag, pos)) !== -1) {
281
+ spans.push({
282
+ start: pos,
283
+ end: pos + img.tag.length,
284
+ tag: img.tag
285
+ });
286
+ pos += img.tag.length;
287
+ }
288
+ }
289
+ return spans.sort((a, b) => a.start - b.start);
290
+ }
291
+ let pasteBuffer = '';
292
+ let inBracketedPaste = false;
293
+ function onData(chunk) {
294
+ if (disposed)
295
+ return;
296
+ const str = chunk.toString('utf8');
297
+ // Check for bracketed paste start/end
298
+ if (str.includes('\x1b[200~')) {
299
+ inBracketedPaste = true;
300
+ pasteBuffer = '';
301
+ }
302
+ if (inBracketedPaste) {
303
+ pasteBuffer += str;
304
+ if (pasteBuffer.includes('\x1b[201~')) {
305
+ inBracketedPaste = false;
306
+ handlePaste(pasteBuffer);
307
+ pasteBuffer = '';
308
+ }
309
+ return;
310
+ }
311
+ // Check for raw multi-character paste with newlines or large size
312
+ if (str.length > 1 && (str.includes('\n') || str.includes('\r') || str.length > 80)) {
313
+ handlePaste(str);
314
+ return;
315
+ }
316
+ // Handle individual keys
317
+ // Ctrl+V (ASCII 22 / 0x16) or Ctrl+P (ASCII 16 / 0x10)
318
+ if (str === '\x16' || str === '\x10' || (str.length === 1 && (str.charCodeAt(0) === 22 || str.charCodeAt(0) === 16))) {
319
+ saveClipboardImage('image.png', usedImageNames).then((imgRes) => {
320
+ if (imgRes) {
321
+ const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
322
+ imageAttachments.push({
323
+ tag,
324
+ fileName: imgRes.fileName,
325
+ filePath: imgRes.filePath,
326
+ sizeStr: imgRes.sizeStr
327
+ });
328
+ usedImageNames.add(imgRes.fileName);
329
+ input = input.slice(0, cursorPos) + tag + ' ' + input.slice(cursorPos);
330
+ cursorPos += tag.length + 1;
331
+ render();
332
+ }
333
+ }).catch(() => { });
334
+ return;
335
+ }
336
+ // Ctrl+C
337
+ if (str === '\x03' || (str.length === 1 && str.charCodeAt(0) === 3)) {
338
+ if (lastDropdownLines > 0) {
339
+ for (let i = 0; i < lastDropdownLines; i++) {
340
+ process.stdout.write(`\n\x1b[2K`);
341
+ }
342
+ process.stdout.write(`\x1b[${lastDropdownLines}A\r`);
343
+ lastDropdownLines = 0;
344
+ }
345
+ process.stdout.write(`\r\x1b[2K\n`);
346
+ cleanup();
347
+ resolve('__CANCEL__');
348
+ return;
349
+ }
350
+ const items = getDropdownItems();
351
+ // Enter / Return
352
+ if (str === '\r' || str === '\n' || (str.length === 1 && (str.charCodeAt(0) === 13 || str.charCodeAt(0) === 10))) {
353
+ // Direct /image command
354
+ if (input.trim() === '/image' || (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length && items[selectedIndex].label === '/image')) {
355
+ saveClipboardImage('image.png', usedImageNames).then((imgRes) => {
356
+ if (imgRes) {
357
+ const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
358
+ imageAttachments.push({
359
+ tag,
360
+ fileName: imgRes.fileName,
361
+ filePath: imgRes.filePath,
362
+ sizeStr: imgRes.sizeStr
363
+ });
364
+ usedImageNames.add(imgRes.fileName);
365
+ input = `${tag} `;
366
+ cursorPos = input.length;
367
+ render();
368
+ }
369
+ else {
370
+ process.stdout.write(`\r\x1b[2K${pc.yellow('⚠️ No image in clipboard. Take a screenshot first (Win+Shift+S)\n')}`);
371
+ input = '';
372
+ cursorPos = 0;
373
+ render();
374
+ }
375
+ }).catch(() => { });
376
+ return;
377
+ }
378
+ if (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length) {
379
+ const selected = items[selectedIndex];
380
+ if (input.startsWith('/')) {
381
+ input = selected.replacement;
382
+ clearBoxAndExit(input);
383
+ return;
384
+ }
385
+ else {
386
+ // @ mention autocomplete on Enter
387
+ input = input.slice(0, selected.replaceStart) + selected.replacement + input.slice(selected.replaceStart + selected.replaceLen);
388
+ cursorPos = selected.replaceStart + selected.replacement.length;
389
+ render();
390
+ return;
391
+ }
392
+ }
393
+ clearBoxAndExit(input);
394
+ return;
395
+ }
396
+ // Tab
397
+ if (str === '\t' || (str.length === 1 && str.charCodeAt(0) === 9)) {
398
+ if (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length) {
399
+ const selected = items[selectedIndex];
400
+ input = input.slice(0, selected.replaceStart) + selected.replacement + input.slice(selected.replaceStart + selected.replaceLen);
401
+ cursorPos = selected.replaceStart + selected.replacement.length;
402
+ render();
403
+ }
404
+ else {
405
+ if (lastDropdownLines > 0) {
406
+ for (let i = 0; i < lastDropdownLines; i++) {
407
+ process.stdout.write(`\n\x1b[2K`);
408
+ }
409
+ process.stdout.write(`\x1b[${lastDropdownLines}A\r`);
410
+ lastDropdownLines = 0;
411
+ }
412
+ process.stdout.write(`\r\x1b[2K`);
413
+ cleanup();
414
+ resolve(`__TOGGLE_MODE__:${input}`);
415
+ }
416
+ return;
417
+ }
418
+ // Up Arrow
419
+ if (str === '\x1b[A' || str === '\x1bOA') {
420
+ if (items.length > 0) {
421
+ selectedIndex = (selectedIndex - 1 + items.length) % items.length;
422
+ render();
423
+ }
424
+ else if (GLOBAL_PROMPT_HISTORY.length > 0 && historyIndex > 0) {
425
+ if (historyIndex === GLOBAL_PROMPT_HISTORY.length) {
426
+ tempDraft = input;
427
+ }
428
+ historyIndex--;
429
+ input = GLOBAL_PROMPT_HISTORY[historyIndex];
430
+ cursorPos = input.length;
431
+ render();
432
+ }
433
+ return;
434
+ }
435
+ // Down Arrow
436
+ if (str === '\x1b[B' || str === '\x1bOB') {
437
+ if (items.length > 0) {
438
+ selectedIndex = (selectedIndex + 1) % items.length;
439
+ render();
440
+ }
441
+ else if (historyIndex < GLOBAL_PROMPT_HISTORY.length) {
442
+ historyIndex++;
443
+ if (historyIndex === GLOBAL_PROMPT_HISTORY.length) {
444
+ input = tempDraft;
445
+ }
446
+ else {
447
+ input = GLOBAL_PROMPT_HISTORY[historyIndex];
448
+ }
449
+ cursorPos = input.length;
450
+ render();
451
+ }
452
+ return;
453
+ }
454
+ // Left Arrow (Jump across whole paste badge atomically)
455
+ if (str === '\x1b[D' || str === '\x1bOD') {
456
+ if (cursorPos > 0) {
457
+ const spans = getTagSpans();
458
+ const endingSpan = spans.find(s => s.end === cursorPos);
459
+ if (endingSpan) {
460
+ cursorPos = endingSpan.start;
461
+ }
462
+ else {
463
+ cursorPos--;
464
+ const inside = spans.find(s => cursorPos > s.start && cursorPos < s.end);
465
+ if (inside)
466
+ cursorPos = inside.start;
467
+ }
468
+ render();
469
+ }
470
+ return;
471
+ }
472
+ // Right Arrow (Jump across whole paste badge atomically)
473
+ if (str === '\x1b[C' || str === '\x1bOC') {
474
+ if (cursorPos < input.length) {
475
+ const spans = getTagSpans();
476
+ const startingSpan = spans.find(s => s.start === cursorPos);
477
+ if (startingSpan) {
478
+ cursorPos = startingSpan.end;
479
+ }
480
+ else {
481
+ cursorPos++;
482
+ const inside = spans.find(s => cursorPos > s.start && cursorPos < s.end);
483
+ if (inside)
484
+ cursorPos = inside.end;
485
+ }
486
+ render();
487
+ }
488
+ return;
489
+ }
490
+ // Home
491
+ if (str === '\x1b[H' || str === '\x1b[1~') {
492
+ cursorPos = 0;
493
+ render();
494
+ return;
495
+ }
496
+ // End
497
+ if (str === '\x1b[F' || str === '\x1b[4~') {
498
+ cursorPos = input.length;
499
+ render();
500
+ return;
501
+ }
502
+ // Delete key (\x1b[3~)
503
+ if (str === '\x1b[3~') {
504
+ if (cursorPos < input.length) {
505
+ const spans = getTagSpans();
506
+ const startingSpan = spans.find(s => s.start === cursorPos);
507
+ if (startingSpan) {
508
+ input = input.slice(0, startingSpan.start) + input.slice(startingSpan.end);
509
+ }
510
+ else {
511
+ input = input.slice(0, cursorPos) + input.slice(cursorPos + 1);
512
+ }
513
+ selectedIndex = 0;
514
+ render();
515
+ }
516
+ return;
517
+ }
518
+ // Backspace
519
+ if (str === '\x08' || str === '\x7f' || (str.length === 1 && (str.charCodeAt(0) === 8 || str.charCodeAt(0) === 127))) {
520
+ if (cursorPos > 0) {
521
+ const spans = getTagSpans();
522
+ const endingSpan = spans.find(s => s.end === cursorPos);
523
+ if (endingSpan) {
524
+ input = input.slice(0, endingSpan.start) + input.slice(endingSpan.end);
525
+ cursorPos = endingSpan.start;
526
+ }
527
+ else {
528
+ input = input.slice(0, cursorPos - 1) + input.slice(cursorPos);
529
+ cursorPos--;
530
+ }
531
+ selectedIndex = 0;
532
+ render();
533
+ }
534
+ return;
535
+ }
536
+ // Ignore unknown escape sequences
537
+ if (str.startsWith('\x1b')) {
538
+ return;
539
+ }
540
+ // Normal character input - snap out of span if needed
541
+ const spans = getTagSpans();
542
+ const inside = spans.find(s => cursorPos > s.start && cursorPos < s.end);
543
+ if (inside) {
544
+ cursorPos = inside.end;
545
+ }
546
+ input = input.slice(0, cursorPos) + str + input.slice(cursorPos);
547
+ cursorPos += str.length;
548
+ selectedIndex = 0;
549
+ render();
550
+ }
551
+ process.stdin.on('data', onData);
552
+ });
553
+ }