termux-dev 1.9.7 → 1.9.9

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/README.md CHANGED
@@ -22,6 +22,10 @@ Built specifically for **Android Termux**, **Windows**, **macOS**, and **Linux**
22
22
 
23
23
  <br/><br/>
24
24
 
25
+ <a href="assets/demo.mp4"><img src="assets/demo.gif" alt="devx demo: plan, build, test, preview with a QR code" width="880" /></a>
26
+
27
+ <br/>
28
+
25
29
  <img src="assets/preview.svg" alt="The devx terminal interface" width="880" />
26
30
 
27
31
  </div>
@@ -235,7 +239,7 @@ Type `/` in the prompt to open the autocomplete command palette:
235
239
  - **`@filename`** — Fast fuzzy file autocomplete and full file embedding.
236
240
  - **`Backspace`** — Atomically deletes entire badges `[Pasted text #1]` and `[1.png 203kb]` in one press.
237
241
  - **`Ctrl+Backspace`** / **`Alt+Backspace`** — Erase the text word by word (also while the agent works).
238
- - **`Ctrl+Enter`** — While typing: start a new line (a new paragraph) instead of sending. `Alt+Enter` and `Shift+Enter` do the same where the terminal reports them; on a terminal that cannot tell them from `Enter` (a phone keyboard, for one) type `\` before `Enter`.
242
+ - **`Ctrl+Enter`** — While typing: start a new line (a new paragraph) instead of sending. `Alt+Enter` and `Shift+Enter` do the same where the terminal reports them; on a terminal that cannot tell them from `Enter` (a phone keyboard, for one) type `\` before `Enter`. devx asks the terminal to tell them apart only where that is known to work (kitty, Ghostty, WezTerm, iTerm2, foot, Alacritty); Termux and other terminals that read that request as a cursor or style code never get it. `DEVX_EXTENDED_KEYS=1` (or `0`) forces it on (or off).
239
243
  - **`Ctrl+P`** / **`Ctrl+O`** — Unfold or fold the reasoning of the model (`Ctrl+P`) and the commands with their output (`Ctrl+O`) in the whole chat, also what is already above. Both are folded by default; works while the agent works and at the prompt.
240
244
  - **`Esc`** — Stop the current answer and whatever it started (a running command); what you typed waits in the next prompt. Esc also closes every list and card.
241
245
  - **`Ctrl+C`** — Stop current generation immediately or exit.
package/assets/banner.svg CHANGED
@@ -16,7 +16,7 @@
16
16
  <rect width="1000" height="280" fill="url(#bg)"/>
17
17
  </g>
18
18
  <rect x="1.5" y="1.5" width="997" height="277" rx="18" fill="none" stroke="#23252c" stroke-width="1.5"/>
19
- <text x="500.0" y="70" text-anchor="middle" font-family="'Inter',system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif" font-size="13" font-weight="600" letter-spacing="5" fill="#7d8896">THE TERMINAL-NATIVE AI CODING AGENT<tspan dx="16" fill="#2fe3ee" font-weight="700">v1.9.7</tspan></text>
19
+ <text x="500.0" y="70" text-anchor="middle" font-family="'Inter',system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif" font-size="13" font-weight="600" letter-spacing="5" fill="#7d8896">THE TERMINAL-NATIVE AI CODING AGENT<tspan dx="16" fill="#2fe3ee" font-weight="700">v1.9.9</tspan></text>
20
20
  <g>
21
21
  <text x="500.0" y="178" text-anchor="middle" font-family="'JetBrains Mono','Fira Code','SF Mono',ui-monospace,Menlo,Consolas,'DejaVu Sans Mono',monospace" font-size="112" font-weight="800" letter-spacing="14" fill="#2fe3ee">DEVX</text>
22
22
  </g>
@@ -32,7 +32,7 @@
32
32
  <rect x="595.2" y="574.0" width="243.2" height="19" fill="#000000"/>
33
33
  <text x="44.8" y="62.0" class="t" fill="#b794f6">▌</text>
34
34
  <text x="61.6" y="62.0" class="t b" fill="#f4f7fb">devx</text>
35
- <text x="112.0" y="62.0" class="t" fill="#5f6b7a">v1.9.7</text>
35
+ <text x="112.0" y="62.0" class="t" fill="#5f6b7a">v1.9.9</text>
36
36
  <text x="61.6" y="106.0" class="t" fill="#5f6b7a">model</text>
37
37
  <text x="128.8" y="106.0" class="t" fill="#e5eaf1">deepseek-v4.1-flash · think high</text>
38
38
  <text x="406.0" y="106.0" class="t" fill="#5f6b7a">/model</text>
package/dist/cli/index.js CHANGED
@@ -69,7 +69,7 @@ import { modelAcceptsImages } from '../core/vision.js';
69
69
  import { contextBreakdown } from '../core/context-stats.js';
70
70
  import { runUsagePanel } from './usage-panel.js';
71
71
  import { TurnComposer } from './typeahead.js';
72
- import { EXTENDED_KEYS_OFF, normalizeKey, splitKeys } from './keys.js';
72
+ import { extendedKeysOff, normalizeKey, splitKeys } from './keys.js';
73
73
  import { LANGUAGES, getLanguage, setLanguage, t } from '../core/i18n.js';
74
74
  import { anthropicOnlyModels, formatFromBaseUrl, refreshFormats } from '../core/formats.js';
75
75
  import { providerFormat } from '../providers/index.js';
@@ -594,7 +594,7 @@ process.on('exit', () => {
594
594
  process.stdin.setRawMode(false);
595
595
  }
596
596
  if (process.stdout.isTTY)
597
- process.stdout.write(EXTENDED_KEYS_OFF + '\x1B[?1004l' + RESTORE_TERMINAL_TITLE + '\x1B[?25h');
597
+ process.stdout.write(extendedKeysOff() + '\x1B[?1004l' + RESTORE_TERMINAL_TITLE + '\x1B[?25h');
598
598
  resetTerminalTheme();
599
599
  });
600
600
  process.on('SIGINT', () => {
@@ -606,11 +606,18 @@ process.on('SIGINT', () => {
606
606
  process.stdin.setRawMode(false);
607
607
  process.stdin.pause();
608
608
  }
609
- process.stdout.write(EXTENDED_KEYS_OFF + '\x1B[?1004l' + RESTORE_TERMINAL_TITLE + '\x1B[?25h'); // restore cursor
609
+ process.stdout.write(extendedKeysOff() + '\x1B[?1004l' + RESTORE_TERMINAL_TITLE + '\x1B[?25h'); // restore cursor
610
610
  resetTerminalTheme();
611
611
  process.exit(0);
612
612
  }
613
613
  });
614
+ /**
615
+ * The new version has been started (it owns the terminal now) and this process ends when it ends.
616
+ * Nothing else may run here: a second prompt on the same terminal takes keys from the first.
617
+ */
618
+ function waitForRestartedDevx() {
619
+ return new Promise(() => { });
620
+ }
614
621
  function clearTerminalScreen() {
615
622
  if (process.stdout.isTTY) {
616
623
  // \x1b[2J: clear screen, \x1b[3J: clear scrollback buffer (crucial for Termux/xterm), \x1b[H: cursor to home
@@ -933,7 +940,8 @@ export async function main() {
933
940
  MemoryManager.enabled = config.enableMemory !== false;
934
941
  const sessionManager = new SessionManager(config.model, planMode);
935
942
  drawLogo(config.banner || 'full');
936
- await runStartupUpdateCheck(config, { save: saveConfig });
943
+ if ((await runStartupUpdateCheck(config, { save: saveConfig })) === 'updated')
944
+ await waitForRestartedDevx();
937
945
  history.addMessage({ role: 'system', content: await buildSystemPrompt(planMode) });
938
946
  // The servers start while the prompt is already there (starting them takes seconds: `npx` and the
939
947
  // network). What needs them waits for `mcpReady`: the first request, and /mcp. When they are up the
@@ -1281,7 +1289,8 @@ export async function main() {
1281
1289
  continue;
1282
1290
  }
1283
1291
  if (cmd === '/update') {
1284
- await runManualUpdate();
1292
+ if ((await runManualUpdate()) === 'updated')
1293
+ await waitForRestartedDevx();
1285
1294
  continue;
1286
1295
  }
1287
1296
  if (cmd === '/help') {
package/dist/cli/keys.js CHANGED
@@ -28,11 +28,50 @@ export function isNewlineKey(key) {
28
28
  * Asks the terminal to tell Ctrl+Enter and Shift+Enter apart from Enter (a plain terminal sends
29
29
  * the same byte for all of them). Two protocols are requested, a terminal answers the one it knows:
30
30
  * the kitty keyboard protocol (kitty, foot, ghostty, wezterm, alacritty, ...) and xterm's
31
- * modifyOtherKeys. Terminals that know neither ignore both, and Alt+Enter still works.
32
- * While on, the keys arrive as `CSI … u`: run them through `normalizeKey`.
31
+ * modifyOtherKeys. While on, the keys arrive as `CSI … u`: run them through `normalizeKey`.
32
+ *
33
+ * Only sent to terminals that are known to understand them (`extendedKeysSupported`): a terminal
34
+ * that does not parses `CSI > 1 u` as "restore the cursor" and `CSI > 4 ; 1 m` as "underline, bold",
35
+ * which throws the prompt to the top of the screen and draws lines under every row (Termux, UserLAnd).
36
+ */
37
+ const EXTENDED_KEYS_ON_SEQ = '\x1b[>1u\x1b[>4;1m';
38
+ const EXTENDED_KEYS_OFF_SEQ = '\x1b[<u\x1b[>4;0m';
39
+ const KEYBOARD_PROTOCOL_PROGRAMS = new Set(['ghostty', 'wezterm', 'iterm.app', 'kitty', 'foot', 'alacritty', 'rio', 'contour']);
40
+ /**
41
+ * Does this terminal understand the extended keyboard modes? Decided from the environment, and
42
+ * only for terminals known to: a wrong guess in the other direction costs Ctrl+Enter (Alt+Enter
43
+ * and a backslash before Enter still break the line), a wrong guess in this one wrecks the screen.
44
+ * `DEVX_EXTENDED_KEYS=1` forces it on, `DEVX_EXTENDED_KEYS=0` off.
33
45
  */
34
- export const EXTENDED_KEYS_ON = '\x1b[>1u\x1b[>4;1m';
35
- export const EXTENDED_KEYS_OFF = '\x1b[<u\x1b[>4;0m';
46
+ export function extendedKeysSupported(env = process.env, platform = process.platform) {
47
+ const forced = (env.DEVX_EXTENDED_KEYS ?? '').trim().toLowerCase();
48
+ if (['1', 'on', 'true', 'yes'].includes(forced))
49
+ return true;
50
+ if (['0', 'off', 'false', 'no'].includes(forced))
51
+ return false;
52
+ // Android: Termux, UserLAnd and other terminals built on the same emulator.
53
+ if (platform === 'android' || env.TERMUX_VERSION || env.PREFIX?.includes('com.termux') || env.ANDROID_ROOT || env.ANDROID_DATA)
54
+ return false;
55
+ // Multiplexers sit between devx and the real terminal: they decide for themselves.
56
+ if (env.TMUX || env.STY)
57
+ return false;
58
+ const term = (env.TERM ?? '').toLowerCase();
59
+ if (term.startsWith('screen') || term.startsWith('tmux') || term === 'linux' || term === 'dumb')
60
+ return false;
61
+ if (env.KITTY_WINDOW_ID || env.GHOSTTY_RESOURCES_DIR || env.WEZTERM_EXECUTABLE)
62
+ return true;
63
+ if (/kitty|ghostty|foot|alacritty|wezterm/.test(term))
64
+ return true;
65
+ return KEYBOARD_PROTOCOL_PROGRAMS.has((env.TERM_PROGRAM ?? '').toLowerCase());
66
+ }
67
+ /** What to write to turn the extended keyboard modes on (nothing on a terminal that does not know them). */
68
+ export function extendedKeysOn() {
69
+ return extendedKeysSupported() ? EXTENDED_KEYS_ON_SEQ : '';
70
+ }
71
+ /** What to write to turn them off again. */
72
+ export function extendedKeysOff() {
73
+ return extendedKeysSupported() ? EXTENDED_KEYS_OFF_SEQ : '';
74
+ }
36
75
  /**
37
76
  * Turns the extended encodings back into the keys the rest of the code knows:
38
77
  * `CSI 99;5u` and `CSI 27;5;99~` become Ctrl+C (0x03), `CSI 27u` becomes Esc, and every way
@@ -8,7 +8,7 @@ import { filterCommandPalette, estimatePromptTokens, formatPromptTokenBadge } fr
8
8
  import { PromptHistoryNavigator } from './history_search.js';
9
9
  import { buildPromptFrame, moveCursorRow, palette, renderUserEcho, resolveMode } from './ui.js';
10
10
  import { displayWidth, padEndWidth, rowsToTop, truncateWidth } from './width.js';
11
- import { EXTENDED_KEYS_OFF, EXTENDED_KEYS_ON, isNewlineKey, normalizeKey } from './keys.js';
11
+ import { extendedKeysOff, extendedKeysOn, isNewlineKey, normalizeKey } from './keys.js';
12
12
  export const SLASH_COMMANDS = [
13
13
  { cmd: '/new', desc: 'Start a new clean chat session' },
14
14
  { cmd: '/resume', desc: 'Resume a previous chat session' },
@@ -99,7 +99,7 @@ export function askPrompt(opts = {}) {
99
99
  process.stdin.setRawMode(true);
100
100
  process.stdout.write('\x1b[?2004h'); // Enable bracketed paste mode
101
101
  // Lets the terminal tell Ctrl+Enter and Shift+Enter (a new paragraph) from Enter (send).
102
- process.stdout.write(EXTENDED_KEYS_ON);
102
+ process.stdout.write(extendedKeysOn());
103
103
  }
104
104
  // ── framed prompt (clean layout) ──
105
105
  // The frame is a block of rows; the cursor rests on the input row. Redrawing moves up
@@ -552,7 +552,7 @@ export function askPrompt(opts = {}) {
552
552
  disposed = true;
553
553
  unsubscribeOwner?.();
554
554
  if (process.stdout.isTTY) {
555
- process.stdout.write('\x1b[?2004l' + EXTENDED_KEYS_OFF);
555
+ process.stdout.write('\x1b[?2004l' + extendedKeysOff());
556
556
  }
557
557
  process.stdout.removeListener('resize', onResize);
558
558
  process.stdin.removeListener('data', onData);
@@ -1,9 +1,9 @@
1
1
  import fs from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- import { EXTENDED_KEYS_OFF, EXTENDED_KEYS_ON, isNewlineKey, normalizeKey, splitKeys } from './keys.js';
4
+ import { extendedKeysOff, extendedKeysOn, isNewlineKey, normalizeKey, splitKeys } from './keys.js';
5
5
  import { buildPromptFrame, palette } from './ui.js';
6
- import { displayWidth, truncateWidth } from './width.js';
6
+ import { columnAfter, displayWidth, truncateWidth } from './width.js';
7
7
  import { t } from '../core/i18n.js';
8
8
  /** The down arrow in every form a terminal may send it: `ESC [ B`, `ESC O B` (application mode), `ESC [ 1 ; mods B` (with modifier fields). */
9
9
  const DOWN_ARROW = /^\x1b(?:\[|O)(?:1;[\d:]+)?B$/;
@@ -153,6 +153,8 @@ export class TurnComposer {
153
153
  state = { text: '', cursor: 0 };
154
154
  queue = [];
155
155
  lines = [];
156
+ /** The column the output cursor is in: the frame is drawn under it and the cursor comes back to this column. */
157
+ col = 0;
156
158
  original = null;
157
159
  timer = null;
158
160
  /** Called on ctrl+p (thinking) and ctrl+o (tool output). */
@@ -207,9 +209,10 @@ export class TurnComposer {
207
209
  this.active = true;
208
210
  this.suspended = false;
209
211
  this.shown = false;
212
+ this.col = 0;
210
213
  this.original = this.out.write.bind(this.out);
211
214
  this.out.write = ((chunk, encoding, cb) => this.through(chunk, encoding, cb));
212
- this.write(EXTENDED_KEYS_ON + this.drawSeq());
215
+ this.write(extendedKeysOn() + this.drawSeq());
213
216
  // The working line animates: its time and counter are redrawn while the output is quiet.
214
217
  if (this.status) {
215
218
  this.timer = setInterval(() => this.tick(), 60);
@@ -225,7 +228,7 @@ export class TurnComposer {
225
228
  this.timer = null;
226
229
  }
227
230
  if (this.active) {
228
- this.write(this.eraseSeq() + EXTENDED_KEYS_OFF + '\x1b[?25h');
231
+ this.write(this.eraseSeq() + extendedKeysOff() + '\x1b[?25h');
229
232
  if (this.original)
230
233
  this.out.write = this.original;
231
234
  this.original = null;
@@ -241,14 +244,14 @@ export class TurnComposer {
241
244
  if (!this.active || this.suspended)
242
245
  return;
243
246
  // The card or question that takes over reads plain keys: the extended encodings go off for it.
244
- this.write(this.eraseSeq() + EXTENDED_KEYS_OFF);
247
+ this.write(this.eraseSeq() + extendedKeysOff());
245
248
  this.suspended = true;
246
249
  }
247
250
  resume() {
248
251
  if (!this.active || !this.suspended)
249
252
  return;
250
253
  this.suspended = false;
251
- this.write(EXTENDED_KEYS_ON + this.drawSeq());
254
+ this.write(extendedKeysOn() + this.drawSeq());
252
255
  }
253
256
  /** Builds the frame again (the context percentage or the cost changed) and draws it. */
254
257
  refresh() {
@@ -298,17 +301,27 @@ export class TurnComposer {
298
301
  /** The write that replaces the terminal's own for the length of the turn. */
299
302
  through(chunk, encoding, cb) {
300
303
  const raw = this.original;
301
- if (!this.active || this.suspended)
304
+ const text = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
305
+ if (!this.active || this.suspended) {
306
+ this.advanceColumn(text);
302
307
  return raw(chunk, encoding, cb);
308
+ }
309
+ // The old frame is wiped from the column the cursor is in now; the new one comes back to the column the text ends in.
310
+ const erase = this.eraseSeq();
311
+ this.advanceColumn(text);
303
312
  if (typeof chunk === 'string') {
304
313
  // The callback form `write(chunk, cb)` has the callback in the second place.
305
- return raw(this.eraseSeq() + chunk + this.drawSeq(), encoding, cb);
314
+ return raw(erase + chunk + this.drawSeq(), encoding, cb);
306
315
  }
307
- raw(this.eraseSeq());
316
+ raw(erase);
308
317
  const result = raw(chunk, encoding, cb);
309
318
  raw(this.drawSeq());
310
319
  return result;
311
320
  }
321
+ /** Where the cursor is after this text (also while a card has the screen, so that the frame comes back to the right column). */
322
+ advanceColumn(text) {
323
+ this.col = columnAfter(this.col, text, this.out.columns ?? 80);
324
+ }
312
325
  write(text) {
313
326
  if (text)
314
327
  (this.original ?? this.out.write.bind(this.out))(text);
@@ -339,12 +352,25 @@ export class TurnComposer {
339
352
  oneRow(line) {
340
353
  return line.replace(/[\r\n\x00-\x08\x0b-\x1a\x1c-\x1f]+/g, ' ');
341
354
  }
342
- /** Wipes the frame: everything under the cursor row. The cursor stays on its text. */
355
+ /** Puts the cursor back in the column of the output, on the row it is on. */
356
+ backToColumn() {
357
+ const cols = Math.max(1, this.out.columns ?? 80);
358
+ return `\x1b[${Math.min(this.col, cols - 1) + 1}G`;
359
+ }
360
+ /**
361
+ * Wipes the frame: everything under the cursor row. The cursor stays on its text.
362
+ *
363
+ * Only relative moves are used, no saved cursor (ESC 7 / ESC 8): the saved position is a screen row,
364
+ * and when the window changes size in the middle of a write (the keyboard of a phone comes and goes)
365
+ * some terminals do not move it with the text, and the cursor comes back to the wrong row.
366
+ */
343
367
  eraseSeq() {
344
368
  if (!this.shown)
345
369
  return '';
346
370
  this.shown = false;
347
- return '\x1b7\x1b[1B\r\x1b[J\x1b8';
371
+ // The erase starts from plain attributes: a style left on by the text above (an underline, a
372
+ // background) would be painted into every row it wipes (stripes across the screen).
373
+ return '\x1b[0m\x1b[1B\r\x1b[J\x1b[1A' + this.backToColumn();
348
374
  }
349
375
  /** Draws the frame under the cursor row and puts the cursor back where the text goes on. */
350
376
  drawSeq() {
@@ -356,9 +382,9 @@ export class TurnComposer {
356
382
  // The real cursor only marks where the output goes on. Whatever showed it in the meantime
357
383
  // (an approval card, a spinner that stopped) must not leave it blinking under the output.
358
384
  // ESC D moves down (scrolling at the bottom edge) and keeps the column; then back up.
359
- let seq = '\x1b[?25l' + '\x1bD'.repeat(n) + `\x1b[${n}A` + '\x1b7';
385
+ let seq = '\x1b[?25l' + '\x1bD'.repeat(n) + `\x1b[${n}A`;
360
386
  for (const line of lines)
361
- seq += `\x1b[1B\r\x1b[2K${line}\x1b[0m`;
362
- return seq + '\x1b8';
387
+ seq += `\x1b[0m\x1b[1B\r\x1b[2K${line}\x1b[0m`;
388
+ return seq + `\x1b[${n}A` + this.backToColumn();
363
389
  }
364
390
  }
@@ -350,14 +350,13 @@ export async function runStartupUpdateCheck(config, deps = {}) {
350
350
  if (result.updateAvailable) {
351
351
  if (config.skippedUpdate === result.latestVersion)
352
352
  return;
353
- await presentUpdate(result, {
353
+ return presentUpdate(result, {
354
354
  offerSkip: true,
355
355
  save: async (version) => {
356
356
  config.skippedUpdate = version;
357
357
  await deps.save?.(config);
358
358
  },
359
359
  }, deps);
360
- return;
361
360
  }
362
361
  if (result.error)
363
362
  return; // offline at start-up is not worth a word
@@ -375,7 +374,7 @@ export async function runManualUpdate(deps = {}) {
375
374
  const result = await withCheckingLine(out, current, () => (deps.check ?? checkForUpdates)(10000));
376
375
  const cols = columnsOf(out);
377
376
  if (result.updateAvailable) {
378
- await presentUpdate(result, { offerSkip: false }, deps);
377
+ return presentUpdate(result, { offerSkip: false }, deps);
379
378
  }
380
379
  else if (result.error) {
381
380
  out.write(renderNotice('warn', t('update.offline', { reason: result.error }), cols) + '\n');
@@ -280,9 +280,26 @@ export async function runUpdateSteps(steps, events = {}, now = Date.now) {
280
280
  }
281
281
  return { ok: true };
282
282
  }
283
- /** Starts devx again, with the same arguments, and ends this process when the new one ends. */
283
+ /**
284
+ * Starts devx again, with the same arguments, and ends this process when the new one ends.
285
+ *
286
+ * From here on this process only waits: the terminal belongs to the new one. It stops reading the
287
+ * keyboard (two programs reading one terminal take the keys from each other, and a typed line
288
+ * can end up in the shell) and no longer reacts to Ctrl+C itself (the new devx gets it).
289
+ */
284
290
  export function restartDevx() {
291
+ try {
292
+ process.stdin.pause();
293
+ }
294
+ catch { }
285
295
  const child = spawn(process.argv[0], process.argv.slice(1), { stdio: 'inherit' });
296
+ process.removeAllListeners('SIGINT');
297
+ process.on('SIGINT', () => { });
298
+ process.removeAllListeners('SIGTERM');
299
+ process.on('SIGTERM', () => { try {
300
+ child.kill('SIGTERM');
301
+ }
302
+ catch { } });
286
303
  child.on('exit', (code) => process.exit(code ?? 0));
287
304
  child.on('error', () => process.exit(0));
288
305
  }
package/dist/cli/width.js CHANGED
@@ -289,3 +289,35 @@ export function rowsToTop(lineWidths, cursorRow, cursorCol, cols) {
289
289
  rows += Math.max(1, Math.ceil(lineWidths[i] / width));
290
290
  return rows + Math.floor(Math.max(0, cursorCol) / width);
291
291
  }
292
+ /**
293
+ * The column (0-based, in cells) the cursor is in after `text` is written starting at column `col`
294
+ * of a terminal `cols` wide. Escape sequences take no room; a line break and a carriage return go
295
+ * to column 0; a character that does not fit wraps to the next row. A cursor that has just filled
296
+ * the last column is reported as `cols` (the terminal holds it there until the next character).
297
+ */
298
+ export function columnAfter(col, text, cols) {
299
+ const width = Math.max(1, cols);
300
+ let at = Math.max(0, Math.min(col, width));
301
+ const plain = text.replace(ANSI, '').replace(/\x1b[0-9A-Za-z=>]/g, '');
302
+ for (const ch of plain) {
303
+ if (ch === '\n' || ch === '\r') {
304
+ at = 0;
305
+ continue;
306
+ }
307
+ if (ch === '\b') {
308
+ at = Math.max(0, at - 1);
309
+ continue;
310
+ }
311
+ if (ch === '\t') {
312
+ at = Math.min(width - 1, (Math.floor(at / 8) + 1) * 8);
313
+ continue;
314
+ }
315
+ const w = codePointWidth(ch.codePointAt(0));
316
+ if (w === 0)
317
+ continue;
318
+ if (at + w > width)
319
+ at = 0;
320
+ at += w;
321
+ }
322
+ return at;
323
+ }
@@ -1 +1 @@
1
- export const DEVX_VERSION = '1.9.7';
1
+ export const DEVX_VERSION = '1.9.9';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termux-dev",
3
- "version": "1.9.7",
3
+ "version": "1.9.9",
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",