termux-dev 1.8.0 → 1.8.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.
package/README.md CHANGED
@@ -57,6 +57,8 @@ The terminal interface is built around a framed prompt that carries the status i
57
57
  Paste screenshots straight from the clipboard with `Ctrl+V` or `Alt+V` (also `/image`, or a pasted file path). They appear as badges like `[Image #1 203kb]` and are sent to the model as real images, unless the model is known to read text only. Needs `wl-clipboard` on Wayland, `xclip` on X11, `pngpaste` on macOS.
58
58
  - **Built-in Live Web Server (`/serve`):**
59
59
  Instantly preview web apps and HTML5 games on port 3000. Seamlessly opens on Android via `termux-open-url`.
60
+ - **Background tasks (`/tasks`):**
61
+ The agent can start a dev server, a watcher or a long test run in the background and keep working. It can wait for a line (`ready|error`), read the output later, and is told when a task ends or prints something it asked about; if you are at the prompt then, it picks the news up by itself. A `◈ 2 bg` badge in the prompt frame shows what runs; `/tasks` lists tasks, `/tasks <id>` shows output, `/tasks stop <id|all>` ends them. Starting a task asks for approval like `bash`; everything is stopped when devx exits. Set `"backgroundAutoWake": false` to stop the agent from reacting by itself.
60
62
  - **Instant Snapshot Rollback (`/undo`):**
61
63
  Every edit made with the file tools (`write_file`, `edit_file`) is recorded and can be rolled back with a single command. Changes made by shell commands (`bash`) are **not** tracked, and snapshots live in memory for the current session only.
62
64
  - **Self-Healing Diagnostics & Auto-Installer:**
@@ -170,6 +172,7 @@ Type `/` in the prompt to open the autocomplete command palette:
170
172
  | **`/rename`** | Rename the chat: `/rename <name>`, or without a name the AI names it again (`/title` is the same) |
171
173
  | **`/permissions`** | What is allowed without asking (`Session` / `Always` on an approval card); `/permissions clear` resets it |
172
174
  | **`/rewind`** | Go back to one of **your messages**: the chat after it is removed (the model forgets it too), the message returns to the input line to be edited; the files can go back as well (with git) |
175
+ | **`/tasks`** | Background tasks: list, `/tasks <id>` for output, `/tasks stop <id\|all>`, `/tasks clear` |
173
176
  | **`/undo`** | Revert all file changes from the last AI turn |
174
177
  | **`/diff`** | View colored git diff of modified project files |
175
178
  | **`/commit`** | AI-generated semantic commit message & git commit |
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.8.0</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.8.2</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>
@@ -9,9 +9,9 @@
9
9
  .i { font-style: italic; }
10
10
  .cur { animation: blink 1.1s steps(1) infinite; }
11
11
  @keyframes blink { 0%,55% { opacity: 1 } 56%,100% { opacity: 0 } }
12
- .spin { animation: pulse 1.6s ease-in-out infinite; }
13
- @keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: .35 } }
14
- @media (prefers-reduced-motion: reduce) { .cur, .spin { animation: none } }
12
+ .sp { opacity: 0; animation: spin .8s steps(1) infinite; }
13
+ @keyframes spin { 0% { opacity: 1 } 10% { opacity: 0 } 100% { opacity: 0 } }
14
+ @media (prefers-reduced-motion: reduce) { .cur { animation: none } .sp { animation: none } .sp.f0 { opacity: 1 } }
15
15
  </style>
16
16
  <rect x="8" y="8" width="924" height="642" rx="14" fill="#000000"/>
17
17
  <g clip-path="url(#clip)">
@@ -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.8.0</text>
35
+ <text x="112.0" y="62.0" class="t" fill="#5f6b7a">v1.8.2</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>
@@ -69,7 +69,16 @@
69
69
  <text x="70.0" y="458.0" class="t" fill="#5f6b7a">╰</text>
70
70
  <text x="86.8" y="458.0" class="t" fill="#9aa6b4">24 passed</text>
71
71
  <text x="86.8" y="480.0" class="t" fill="#5f6b7a">… +3 lines (ctrl+o to expand)</text>
72
- <text x="44.8" y="512.0" class="t spin" fill="#5ed3e0">⠹</text>
72
+ <text x="44.8" y="512.0" class="t sp f0" style="animation-delay:0.00s" fill="#5ed3e0">⠋</text>
73
+ <text x="44.8" y="512.0" class="t sp f1" style="animation-delay:0.08s" fill="#5ed3e0">⠙</text>
74
+ <text x="44.8" y="512.0" class="t sp f2" style="animation-delay:0.16s" fill="#5ed3e0">⠹</text>
75
+ <text x="44.8" y="512.0" class="t sp f3" style="animation-delay:0.24s" fill="#5ed3e0">⠸</text>
76
+ <text x="44.8" y="512.0" class="t sp f4" style="animation-delay:0.32s" fill="#5ed3e0">⠼</text>
77
+ <text x="44.8" y="512.0" class="t sp f5" style="animation-delay:0.40s" fill="#5ed3e0">⠴</text>
78
+ <text x="44.8" y="512.0" class="t sp f6" style="animation-delay:0.48s" fill="#5ed3e0">⠦</text>
79
+ <text x="44.8" y="512.0" class="t sp f7" style="animation-delay:0.56s" fill="#5ed3e0">⠧</text>
80
+ <text x="44.8" y="512.0" class="t sp f8" style="animation-delay:0.64s" fill="#5ed3e0">⠇</text>
81
+ <text x="44.8" y="512.0" class="t sp f9" style="animation-delay:0.72s" fill="#5ed3e0">⠏</text>
73
82
  <text x="61.6" y="512.0" class="t" fill="#e5eaf1">Writing</text>
74
83
  <text x="145.6" y="512.0" class="t" fill="#5f6b7a">(12s · ↓ 1.4k tokens · ctrl+c to stop)</text>
75
84
  <text x="44.8" y="544.0" class="t" fill="#5ed3e0">●</text>
@@ -0,0 +1,116 @@
1
+ import { formatDuration } from '../core/background.js';
2
+ import { palette } from './ui.js';
3
+ import { displayWidth, padEndWidth, truncateWidth } from './width.js';
4
+ /** What the prompt frame shows: how many tasks run, how many ended badly and were not looked at yet. */
5
+ export function backgroundSummary(manager, seenFailures) {
6
+ const all = manager.list();
7
+ return {
8
+ running: all.filter((t) => t.status === 'running').length,
9
+ failed: all.filter((t) => t.status === 'failed' && !seenFailures.has(t.id)).length,
10
+ };
11
+ }
12
+ const STATE_LABEL = {
13
+ running: 'running',
14
+ exited: 'done',
15
+ failed: 'failed',
16
+ stopped: 'stopped',
17
+ };
18
+ function stateCell(info) {
19
+ const label = info.status === 'failed' && info.code !== null ? `failed ${info.code}` : STATE_LABEL[info.status];
20
+ const padded = label.padEnd(9);
21
+ if (info.status === 'running')
22
+ return palette.ok('● ' + padded);
23
+ if (info.status === 'failed')
24
+ return palette.err('✗ ' + padded);
25
+ return palette.faint('○ ' + padded);
26
+ }
27
+ /** The list of tasks for `/tasks`: one row each, the newest output line under a running one. */
28
+ export function renderTaskList(manager, cols, now = Date.now()) {
29
+ const tasks = manager.list();
30
+ if (tasks.length === 0) {
31
+ return [` ${palette.faint('·')} ${palette.muted('No background tasks. The agent starts them for servers, watchers and long runs.')}`];
32
+ }
33
+ const rows = [];
34
+ const running = tasks.filter((t) => t.status === 'running').length;
35
+ rows.push(` ${palette.bold('Background tasks')} ${palette.faint(`· ${running} running, ${tasks.length - running} ended`)}`);
36
+ for (const t of tasks) {
37
+ const age = formatDuration((t.endedAt ?? now) - t.startedAt);
38
+ const head = ` ${palette.accent(padEndWidth(t.id, 4))} ${stateCell(t)} ${palette.faint(padEndWidth(age, 7))} `;
39
+ rows.push(head + palette.text(truncateWidth(t.label, Math.max(8, cols - displayWidth(head) - 1))));
40
+ const last = lastLine(manager, t.id);
41
+ if (last)
42
+ rows.push(` ${' '.repeat(4)} ${palette.faint('╰')} ${palette.muted(truncateWidth(last, Math.max(8, cols - 14)))}`);
43
+ }
44
+ rows.push('');
45
+ rows.push(` ${palette.faint(truncateWidth('/tasks <id> output · /tasks stop <id|all> · /tasks clear', Math.max(8, cols - 4)))}`);
46
+ return rows;
47
+ }
48
+ function lastLine(manager, id) {
49
+ try {
50
+ const { text } = manager.read(id, { mode: 'tail', lines: 3 });
51
+ const lines = text.split('\n').map((l) => l.trim()).filter(Boolean);
52
+ return lines[lines.length - 1] ?? '';
53
+ }
54
+ catch {
55
+ return '';
56
+ }
57
+ }
58
+ /** The output of one task for `/tasks <id>`. */
59
+ export function renderTaskOutput(manager, id, cols, lines = 40) {
60
+ const info = manager.get(id);
61
+ if (!info)
62
+ return [` ${palette.err('✗')} ${palette.err(`No task "${id}". Type /tasks to see them.`)}`];
63
+ const { text, omitted } = manager.read(id, { mode: 'tail', lines });
64
+ const rows = [` ${palette.accent(info.id)} ${stateCell(info)} ${palette.text(truncateWidth(info.label, Math.max(8, cols - 22)))}`];
65
+ rows.push(` ${palette.faint(truncateWidth(info.command, Math.max(8, cols - 4)))}`);
66
+ if (omitted > 0)
67
+ rows.push(` ${palette.faint(`… ${omitted} earlier line${omitted === 1 ? '' : 's'}`)}`);
68
+ if (!text)
69
+ rows.push(` ${palette.faint('(no output yet)')}`);
70
+ for (const line of text.split('\n'))
71
+ if (text)
72
+ rows.push(' ' + palette.muted(truncateWidth(line, Math.max(8, cols - 4))));
73
+ return rows;
74
+ }
75
+ /** The notes the model received, short: one row per note (the first line of each). */
76
+ export function noteRows(content) {
77
+ const body = content.replace(/^\[devx · background tasks\]\n?/, '');
78
+ return body
79
+ .split(/\n\n(?=bg\d+ )/)
80
+ .map((note) => note.split('\n')[0].trim())
81
+ .filter(Boolean);
82
+ }
83
+ /** `/tasks ...`: returns what to print. Stopping is asynchronous, so this is too. */
84
+ export async function runTasksCommand(args, manager, cols, markSeen) {
85
+ const [first, second] = args;
86
+ const word = (first ?? '').toLowerCase();
87
+ if (!first || word === 'list' || word === 'ls') {
88
+ markSeen(manager.list().map((t) => t.id));
89
+ return renderTaskList(manager, cols);
90
+ }
91
+ if (word === 'stop' || word === 'kill') {
92
+ if (!second)
93
+ return [` ${palette.warn('!')} ${palette.warn('Usage: /tasks stop <id|all>')}`];
94
+ if (second === 'all') {
95
+ const n = await manager.stopAll();
96
+ return [` ${palette.ok('✓')} ${palette.text(n === 0 ? 'Nothing was running' : `Stopped ${n} task${n === 1 ? '' : 's'}`)}`];
97
+ }
98
+ try {
99
+ const info = await manager.stop(second);
100
+ return [` ${palette.ok('✓')} ${palette.text(`${info.id} ${STATE_LABEL[info.status]}`)}`];
101
+ }
102
+ catch (err) {
103
+ return [` ${palette.err('✗')} ${palette.err(err.message)}`];
104
+ }
105
+ }
106
+ if (word === 'clear') {
107
+ const n = manager.clearFinished();
108
+ return [` ${palette.ok('✓')} ${palette.text(n === 0 ? 'No ended tasks to remove' : `Removed ${n} ended task${n === 1 ? '' : 's'}`)}`];
109
+ }
110
+ // `/tasks bg1`, `/tasks output bg1`, `/tasks logs bg1`
111
+ const id = ['output', 'logs', 'log', 'show'].includes(word) ? second : first;
112
+ if (!id)
113
+ return [` ${palette.warn('!')} ${palette.warn('Usage: /tasks output <id>')}`];
114
+ markSeen([id]);
115
+ return renderTaskOutput(manager, id, cols);
116
+ }
@@ -1,5 +1,6 @@
1
1
  import pc from 'picocolors';
2
2
  import { getCurrentTheme } from './theme.js';
3
+ import { DEVX_LOGO } from './logo.js';
3
4
  import { DEVX_VERSION } from '../core/types.js';
4
5
  export function renderBannerLines(mode = 'full', version = DEVX_VERSION, cols = 80) {
5
6
  if (mode === 'off') {
@@ -18,9 +19,7 @@ export function renderBannerLines(mode = 'full', version = DEVX_VERSION, cols =
18
19
  if (cols < 68) {
19
20
  // Compact ASCII for narrow mobile screens (~26 chars)
20
21
  return [
21
- theme.colorFn(' █▀▀▄ █▀▀▀ █ █ █ █'),
22
- theme.colorFn(' █ █ █▀▀▀ ▀▄▀ ▀▄▀ '),
23
- theme.colorFn(' █▄▄▀ █▄▄▄ ▀ ▀ ▀ '),
22
+ ...DEVX_LOGO.map((l) => ' ' + theme.colorFn(l)),
24
23
  ' ' + theme.boldFn(`v${version}`)
25
24
  ];
26
25
  }
@@ -0,0 +1,241 @@
1
+ import { LiveBlock } from './liveblock.js';
2
+ import { WORD_BACKSPACE, normalizeKey, splitKeys } from './keys.js';
3
+ import { windowItems } from './picker.js';
4
+ import { palette } from './ui.js';
5
+ import { displayWidth, padEndWidth, tailWidth, truncateWidth, wrapWords } from './width.js';
6
+ import { t } from '../core/i18n.js';
7
+ /** Every word typed must be found in the label, the id or the keywords; matches at the start of the label come first. */
8
+ export function filterItems(items, query) {
9
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
10
+ const pinned = items.filter((i) => i.pinned);
11
+ const rest = items.filter((i) => !i.pinned);
12
+ if (words.length === 0)
13
+ return [...pinned, ...rest];
14
+ const scored = [];
15
+ for (const item of rest) {
16
+ const hay = `${item.label} ${item.keywords ?? ''}`.toLowerCase();
17
+ if (!words.every((w) => hay.includes(w)))
18
+ continue;
19
+ const label = item.label.toLowerCase();
20
+ scored.push({ item, score: label.startsWith(words[0]) ? 0 : label.includes(words[0]) ? 1 : 2 });
21
+ }
22
+ scored.sort((a, b) => a.score - b.score); // stable: the original order is kept inside a score
23
+ return [...pinned, ...scored.map((s) => s.item)];
24
+ }
25
+ function hintLine(width, typing) {
26
+ const variants = [
27
+ typing
28
+ ? [['type', t('finder.hint.filter')], ['↑↓', t('picker.hint.move')], ['enter', t('picker.hint.select')], ['esc', t('picker.hint.cancel')]]
29
+ : [['enter', t('picker.hint.confirm')], ['esc', t('picker.hint.cancel')]],
30
+ [['↑↓', ''], ['enter', ''], ['esc', '']],
31
+ [['enter', ''], ['esc', '']],
32
+ ];
33
+ for (const items of variants) {
34
+ const text = items.map(([k, label]) => palette.text(k) + (label ? ' ' + palette.faint(label) : '')).join(palette.faint(items[0][1] ? ' · ' : ' '));
35
+ if (displayWidth(text) <= width)
36
+ return text;
37
+ }
38
+ return '';
39
+ }
40
+ /** The card around some content rows; `rows` are already padded to the inner width by the caller's `box`. */
41
+ function frame(title, subtitle, cols, body, footer) {
42
+ if (cols < 60) {
43
+ const width = Math.max(20, cols - 1);
44
+ const head = ' ' + palette.bold(palette.accent(title)) + (subtitle ? palette.faint(' · ' + truncateWidth(subtitle, Math.max(6, width - displayWidth(title) - 6))) : '');
45
+ const plain = (c, shaded = false) => (shaded ? palette.shade(padEndWidth(' ' + c, width)) : ' ' + c);
46
+ return [head, ...body(width - 1, plain), ' ' + footer];
47
+ }
48
+ const W = Math.min(cols - 1, 76);
49
+ const iw = W - 4;
50
+ const rule = palette.rule;
51
+ const box = (content, shaded = false) => rule('│') + (shaded ? palette.shade(' ' + padEndWidth(content, iw) + ' ') : ' ' + padEndWidth(content, iw) + ' ') + rule('│');
52
+ const head = ' ' + palette.bold(palette.accent(title)) + ' ';
53
+ const sub = subtitle ? ' ' + palette.muted(truncateWidth(subtitle, Math.max(6, W - 12 - displayWidth(title)))) + ' ' : '';
54
+ return [
55
+ rule('╭─') + head + rule('─'.repeat(Math.max(0, W - 4 - displayWidth(head) - displayWidth(sub)))) + sub + rule('─╮'),
56
+ box(''),
57
+ ...body(iw, box),
58
+ rule('╰' + '─'.repeat(W - 2) + '╯'),
59
+ ' ' + footer,
60
+ ];
61
+ }
62
+ /** Where the focus starts: on the first match, not on a pinned row such as "type a name". */
63
+ export function firstMatch(shown) {
64
+ const i = shown.findIndex((item) => !item.pinned);
65
+ return i === -1 ? 0 : i;
66
+ }
67
+ export function renderFinder(options, query, shown, focus, cols, termRows = 40) {
68
+ const maxItems = Math.max(3, termRows - 9 - (options.notes?.length ?? 0));
69
+ return frame(options.title, options.subtitle ?? `${shown.filter((i) => !i.pinned).length}/${options.items.filter((i) => !i.pinned).length}`, cols, (iw, box) => {
70
+ const lines = [];
71
+ const typed = query ? palette.text(tailWidth(query, Math.max(4, iw - 6))) : palette.faint(truncateWidth(options.placeholder ?? t('finder.placeholder'), Math.max(4, iw - 6)));
72
+ lines.push(box(' ' + palette.accent('/') + ' ' + (query ? typed + palette.accent('█') : palette.accent('█') + typed)));
73
+ lines.push(box(palette.rule('─'.repeat(Math.max(0, iw - 1)))));
74
+ if (shown.length === 0) {
75
+ lines.push(box(' ' + palette.faint(t('finder.empty'))));
76
+ }
77
+ else {
78
+ const labelWidth = Math.min(Math.min(options.labelWidth ?? 24, Math.max(...shown.map((i) => displayWidth(i.label)), 4)) + 2, Math.max(10, iw - 12));
79
+ const rows = shown.map((item, index) => {
80
+ const focused = index === focus;
81
+ const isCurrent = options.current !== undefined && item.value === options.current;
82
+ const mark = focused ? palette.accent('❯') : ' ';
83
+ const dot = item.pinned ? palette.accent('+') : isCurrent ? palette.accent('●') : palette.faint('○');
84
+ const label = focused ? palette.bold(palette.text(truncateWidth(item.label, labelWidth - 1))) : palette.muted(truncateWidth(item.label, labelWidth - 1));
85
+ const prefix = ` ${mark} ${dot} ${padEndWidth(label, labelWidth)}`;
86
+ const room = Math.max(0, iw - displayWidth(prefix) - 1);
87
+ const detail = item.detail && room > 6 ? palette.faint(truncateWidth(item.detail, room)) : '';
88
+ return { text: prefix + detail, index };
89
+ });
90
+ for (const r of windowItems(rows, focus, maxItems, iw))
91
+ lines.push(box(r.text, r.index === focus));
92
+ }
93
+ const notes = (options.notes ?? []).flatMap((n) => wrapWords(n, iw - 3));
94
+ if (notes.length > 0) {
95
+ lines.push(box(''));
96
+ for (const line of notes)
97
+ lines.push(box(' ' + palette.faint(line)));
98
+ }
99
+ return lines;
100
+ }, hintLine(cols - 2, true));
101
+ }
102
+ /** What a key does to a text that is being typed: `undefined` when the key is not an edit. */
103
+ export function editText(value, key) {
104
+ if (key === '\x7f' || key === '\b')
105
+ return Array.from(value).slice(0, -1).join('');
106
+ if (key === WORD_BACKSPACE)
107
+ return value.replace(/\s*\S*$/, '');
108
+ if (key === '\x15')
109
+ return ''; // Ctrl+U
110
+ if (/^[^\x00-\x1f\x7f\x1b]+$/.test(key))
111
+ return value + key; // printable text, also a pasted chunk
112
+ return undefined;
113
+ }
114
+ function openIO(io) {
115
+ const { input } = io;
116
+ const wasRaw = !!input.isRaw;
117
+ input.setRawMode?.(true);
118
+ input.resume();
119
+ return () => { if (!wasRaw)
120
+ input.setRawMode?.(false); };
121
+ }
122
+ /** The list narrowed by typing. Resolves with the chosen value, or undefined when cancelled. */
123
+ export function runFinder(options, io = { input: process.stdin, output: process.stdout }) {
124
+ return new Promise((resolve) => {
125
+ const { input, output } = io;
126
+ const block = new LiveBlock(output);
127
+ let query = '';
128
+ let shown = filterItems(options.items, query);
129
+ const at = shown.findIndex((i) => options.current !== undefined && i.value === options.current);
130
+ let focus = at >= 0 ? at : firstMatch(shown);
131
+ let done = false;
132
+ const rows = () => output.rows || 40;
133
+ const draw = () => block.draw(renderFinder(options, query, shown, focus, output.columns || 80, rows()));
134
+ const restore = openIO(io);
135
+ const finish = (value) => {
136
+ done = true;
137
+ input.off('data', onData);
138
+ block.erase();
139
+ restore();
140
+ resolve(value);
141
+ };
142
+ const move = (delta) => {
143
+ const next = Math.max(0, Math.min(shown.length - 1, focus + delta));
144
+ if (next !== focus) {
145
+ focus = next;
146
+ draw();
147
+ }
148
+ };
149
+ const handleKey = (raw) => {
150
+ const key = normalizeKey(raw);
151
+ if (key === '\x1b[A' || key === '\x1bOA')
152
+ return move(-1);
153
+ if (key === '\x1b[B' || key === '\x1bOB')
154
+ return move(1);
155
+ if (key === '\x1b[5~')
156
+ return move(-8);
157
+ if (key === '\x1b[6~')
158
+ return move(8);
159
+ if (key === '\r' || key === '\n')
160
+ return shown.length > 0 ? finish(shown[focus].value) : undefined;
161
+ if (key === '\x1b' || key === '\x03')
162
+ return finish(undefined);
163
+ const edited = editText(query, key);
164
+ if (edited !== undefined && edited !== query) {
165
+ query = edited;
166
+ shown = filterItems(options.items, query);
167
+ focus = firstMatch(shown);
168
+ draw();
169
+ }
170
+ };
171
+ const onData = (chunk) => {
172
+ for (const key of splitKeys(chunk.toString())) {
173
+ if (done)
174
+ return;
175
+ handleKey(key);
176
+ }
177
+ };
178
+ input.on('data', onData);
179
+ draw();
180
+ });
181
+ }
182
+ export function renderTextInput(options, value, cols) {
183
+ return frame(options.title, options.subtitle, cols, (iw, box) => {
184
+ const lines = [];
185
+ for (const line of wrapWords(options.label, iw - 2))
186
+ lines.push(box(' ' + palette.bold(palette.text(line))));
187
+ const shownValue = options.secret ? '•'.repeat(Array.from(value).length) : value;
188
+ const room = Math.max(4, iw - 6);
189
+ const text = value
190
+ ? palette.text(tailWidth(shownValue, room)) + palette.accent('█')
191
+ : palette.accent('█') + palette.faint(truncateWidth(options.placeholder ?? '', room));
192
+ lines.push(box(''));
193
+ lines.push(box(' ' + palette.accent('❯') + ' ' + text, true));
194
+ const notes = (options.notes ?? []).flatMap((n) => wrapWords(n, iw - 3));
195
+ if (notes.length > 0) {
196
+ lines.push(box(''));
197
+ for (const line of notes)
198
+ lines.push(box(' ' + palette.faint(line)));
199
+ }
200
+ return lines;
201
+ }, hintLine(cols - 2, false));
202
+ }
203
+ /** A one-line input. Resolves with the text (never empty), or undefined when cancelled. */
204
+ export function runTextInput(options, io = { input: process.stdin, output: process.stdout }) {
205
+ return new Promise((resolve) => {
206
+ const { input, output } = io;
207
+ const block = new LiveBlock(output);
208
+ let value = options.initial ?? '';
209
+ let done = false;
210
+ const draw = () => block.draw(renderTextInput(options, value, output.columns || 80));
211
+ const restore = openIO(io);
212
+ const finish = (result) => {
213
+ done = true;
214
+ input.off('data', onData);
215
+ block.erase();
216
+ restore();
217
+ resolve(result);
218
+ };
219
+ const handleKey = (raw) => {
220
+ const key = normalizeKey(raw);
221
+ if (key === '\r' || key === '\n')
222
+ return value.trim() ? finish(value.trim()) : undefined;
223
+ if (key === '\x1b' || key === '\x03')
224
+ return finish(undefined);
225
+ const edited = editText(value, key);
226
+ if (edited !== undefined && edited !== value) {
227
+ value = edited;
228
+ draw();
229
+ }
230
+ };
231
+ const onData = (chunk) => {
232
+ for (const key of splitKeys(chunk.toString())) {
233
+ if (done)
234
+ return;
235
+ handleKey(key);
236
+ }
237
+ };
238
+ input.on('data', onData);
239
+ draw();
240
+ });
241
+ }
@@ -10,6 +10,7 @@ import { CLIConsoleGuard } from '../permissions/guard.js';
10
10
  import { globalSnapshotManager } from '../core/snapshot.js';
11
11
  import { UsageTracker } from '../core/usage.js';
12
12
  import { MCPManager } from '../mcp/manager.js';
13
+ import { getBackgroundManager } from '../core/background.js';
13
14
  export async function runHeadlessMode(userPrompt, config, options = {}) {
14
15
  const planMode = !!options.planMode;
15
16
  const isYolo = !!options.yolo || !!config.autoApprove;
@@ -48,7 +49,8 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
48
49
  console.log(pc.bold(pc.cyan(`devx v${DEVX_VERSION} (headless) | ${planMode ? 'PLAN' : 'AGENT'} | ${config.model}`)));
49
50
  console.log(pc.dim(`Task: ${userPrompt}\n`));
50
51
  }
51
- for await (const event of agent.run(abortController.signal)) {
52
+ const bgManager = getBackgroundManager();
53
+ for await (const event of agent.run(abortController.signal, { takeNotes: () => bgManager.takeNotes(), hasNotes: () => bgManager.hasNotes() })) {
52
54
  if (event.type === 'reasoning_delta') {
53
55
  accumulatedReasoning += event.delta;
54
56
  if (!isQuiet && !isJson) {