termux-dev 1.6.0 → 1.8.0

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.
Files changed (96) hide show
  1. package/README.md +214 -166
  2. package/assets/banner.svg +23 -28
  3. package/assets/preview.png +0 -0
  4. package/assets/preview.svg +92 -10
  5. package/dist/cli/banner.js +1 -1
  6. package/dist/cli/clipboard-write.js +55 -0
  7. package/dist/cli/clipboard.js +182 -106
  8. package/dist/cli/colors.js +67 -0
  9. package/dist/cli/confirm.js +50 -0
  10. package/dist/cli/doctor.js +4 -4
  11. package/dist/cli/effort.js +80 -0
  12. package/dist/cli/export.js +3 -3
  13. package/dist/cli/files.js +19 -5
  14. package/dist/cli/headless.js +23 -11
  15. package/dist/cli/index.js +1377 -480
  16. package/dist/cli/keys.js +69 -0
  17. package/dist/cli/liveblock.js +45 -0
  18. package/dist/cli/log.js +19 -0
  19. package/dist/cli/markdown.js +158 -36
  20. package/dist/cli/mcp-panel.js +432 -0
  21. package/dist/cli/permission.js +84 -0
  22. package/dist/cli/picker.js +135 -0
  23. package/dist/cli/prompt.js +256 -56
  24. package/dist/cli/resume.js +15 -0
  25. package/dist/cli/rewind.js +51 -0
  26. package/dist/cli/runtime_errors.js +6 -0
  27. package/dist/cli/server.js +85 -24
  28. package/dist/cli/settings.js +307 -0
  29. package/dist/cli/statusline.js +1 -1
  30. package/dist/cli/theme.js +126 -108
  31. package/dist/cli/transcript.js +114 -0
  32. package/dist/cli/typeahead.js +371 -0
  33. package/dist/cli/ui.js +1021 -0
  34. package/dist/cli/updater.js +23 -21
  35. package/dist/cli/usage-panel.js +242 -0
  36. package/dist/cli/width.js +178 -0
  37. package/dist/core/capabilities.js +433 -0
  38. package/dist/core/catalog-util.js +27 -0
  39. package/dist/core/checkpoint.js +90 -0
  40. package/dist/core/context-stats.js +29 -0
  41. package/dist/core/cost.js +191 -0
  42. package/dist/core/formats.js +199 -0
  43. package/dist/core/history.js +51 -1
  44. package/dist/core/hooks.js +79 -0
  45. package/dist/core/i18n.js +42 -0
  46. package/dist/core/local-config.js +69 -0
  47. package/dist/core/locales/de.js +210 -0
  48. package/dist/core/locales/en.js +221 -0
  49. package/dist/core/locales/es.js +210 -0
  50. package/dist/core/locales/fr.js +210 -0
  51. package/dist/core/locales/ja.js +210 -0
  52. package/dist/core/locales/pt.js +210 -0
  53. package/dist/core/locales/ru.js +210 -0
  54. package/dist/core/locales/uk.js +210 -0
  55. package/dist/core/locales/zh.js +210 -0
  56. package/dist/core/loop.js +125 -49
  57. package/dist/core/memory.js +39 -12
  58. package/dist/core/models.js +33 -20
  59. package/dist/core/network.js +12 -11
  60. package/dist/core/notify.js +54 -0
  61. package/dist/core/planfile.js +43 -0
  62. package/dist/core/price-catalog.json +1 -0
  63. package/dist/core/prices.js +249 -0
  64. package/dist/core/reasoning-catalog.json +1 -0
  65. package/dist/core/reasoning.js +393 -0
  66. package/dist/core/redact.js +41 -0
  67. package/dist/core/secure-file.js +22 -0
  68. package/dist/core/session.js +16 -5
  69. package/dist/core/snapshot.js +14 -2
  70. package/dist/core/title.js +61 -0
  71. package/dist/core/types.js +1 -1
  72. package/dist/core/usage.js +43 -7
  73. package/dist/core/vision-catalog.json +1 -0
  74. package/dist/core/vision.js +123 -0
  75. package/dist/mcp/client.js +36 -5
  76. package/dist/mcp/http-client.js +174 -0
  77. package/dist/mcp/manager.js +330 -93
  78. package/dist/permissions/guard.js +385 -117
  79. package/dist/prompts/builder.js +46 -38
  80. package/dist/providers/anthropic.js +517 -0
  81. package/dist/providers/index.js +18 -1
  82. package/dist/providers/openai.js +138 -125
  83. package/dist/providers/shared.js +116 -0
  84. package/dist/providers/toolguard.js +101 -0
  85. package/dist/tools/bash.js +141 -51
  86. package/dist/tools/diagnostics.js +76 -48
  87. package/dist/tools/fs.js +77 -5
  88. package/dist/tools/index.js +2 -1
  89. package/dist/tools/mcp.js +72 -0
  90. package/dist/tools/packages.js +16 -1
  91. package/dist/tools/plan.js +8 -8
  92. package/dist/tools/questions.js +50 -54
  93. package/dist/tools/server.js +6 -0
  94. package/dist/tools/todo.js +20 -18
  95. package/dist/tools/web.js +143 -8
  96. package/package.json +1 -1
@@ -0,0 +1,69 @@
1
+ // One terminal "key": a full escape sequence (arrows, Home, Delete...), Alt+Enter, Alt+Backspace, a lone Esc,
2
+ // or a single character. The `u` flag keeps characters outside the BMP in one piece.
3
+ const KEY = /\x1b\[[0-9;:?]*[ -/]*[@-~]|\x1bO[\s\S]|\x1b[\r\n\x7f]|\x1b|[\s\S]/gu;
4
+ /**
5
+ * Splits raw input into separate key presses.
6
+ *
7
+ * A terminal may deliver several keys in one chunk (holding an arrow key, a fast typist, a
8
+ * slow connection, Termux batching input), for example "\x1b[B\x1b[B\x1b[B" for three
9
+ * presses of Down. Code that compares a whole chunk with one key would miss them all.
10
+ */
11
+ export function splitKeys(chunk) {
12
+ return chunk.match(KEY) ?? [];
13
+ }
14
+ /** Erases the word before the cursor (Ctrl+W in a shell); Ctrl+Backspace and Alt+Backspace are mapped to it. */
15
+ export const WORD_BACKSPACE = '\x17';
16
+ /** What a terminal sends for Ctrl+Enter once the extended keyboard modes below are on. */
17
+ export const CTRL_ENTER = '\x1b[13;5u';
18
+ /**
19
+ * Asks the terminal to tell Ctrl+Enter apart from Enter (a plain terminal sends the same
20
+ * byte for both). Two protocols are requested, a terminal answers the one it knows:
21
+ * the kitty keyboard protocol (kitty, foot, ghostty, wezterm, alacritty, ...) and xterm's
22
+ * modifyOtherKeys. Terminals that know neither ignore both, and Alt+Enter still works.
23
+ * While on, the keys arrive as `CSI … u`: run them through `normalizeKey`.
24
+ */
25
+ export const EXTENDED_KEYS_ON = '\x1b[>1u\x1b[>4;1m';
26
+ export const EXTENDED_KEYS_OFF = '\x1b[<u\x1b[>4;0m';
27
+ /**
28
+ * Turns the extended encodings back into the keys the rest of the code knows:
29
+ * `CSI 99;5u` and `CSI 27;5;99~` become Ctrl+C (0x03), `CSI 27u` becomes Esc, and every way
30
+ * of sending Ctrl+Enter or Alt+Enter becomes `CTRL_ENTER`. Anything else is returned as is.
31
+ */
32
+ export function normalizeKey(key) {
33
+ if (key === '\x1b\r' || key === '\x1b\n')
34
+ return CTRL_ENTER;
35
+ if (key === '\x1b\x7f')
36
+ return WORD_BACKSPACE; // Alt+Backspace
37
+ const m = /^\x1b\[(?:27;(\d+);(\d+)~|(\d+)(?:;(\d+))?(?::\d+)?u)$/.exec(key);
38
+ if (!m)
39
+ return key;
40
+ // modifyOtherKeys is `27;mods;code`, the kitty protocol is `code;mods`.
41
+ const code = Number(m[2] ?? m[3]);
42
+ const mods = Math.max(0, Number(m[1] ?? m[4] ?? 1) - 1);
43
+ const shift = (mods & 1) !== 0;
44
+ const alt = (mods & 2) !== 0;
45
+ const ctrl = (mods & 4) !== 0;
46
+ // Ctrl+Backspace and Alt+Backspace: erase a word.
47
+ if ((code === 127 || code === 8) && (ctrl || alt))
48
+ return WORD_BACKSPACE;
49
+ if (code === 13 || code === 10)
50
+ return ctrl || alt ? CTRL_ENTER : shift ? key : '\r';
51
+ if (alt)
52
+ return key;
53
+ if (ctrl) {
54
+ if (code >= 97 && code <= 122)
55
+ return String.fromCharCode(code - 96);
56
+ if (code >= 65 && code <= 90)
57
+ return String.fromCharCode(code - 64);
58
+ return key;
59
+ }
60
+ if (code === 27)
61
+ return '\x1b';
62
+ if (code === 9)
63
+ return '\t';
64
+ if (code === 127 || code === 8)
65
+ return String.fromCharCode(code);
66
+ if (code >= 32 && code !== 127 && !shift)
67
+ return String.fromCodePoint(code);
68
+ return key;
69
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * A block of terminal rows that is redrawn in place.
3
+ *
4
+ * The cursor rests on the last row of the block. A redraw goes back up to the first row,
5
+ * rewrites every row and wipes the rows left over from a taller previous version, so the
6
+ * screen never grows however often the content changes. The cursor is hidden while the
7
+ * block is on screen.
8
+ */
9
+ export class LiveBlock {
10
+ out;
11
+ rows = 0;
12
+ constructor(out) {
13
+ this.out = out;
14
+ }
15
+ get height() {
16
+ return this.rows;
17
+ }
18
+ draw(lines) {
19
+ let buf = '\x1b[?25l';
20
+ if (this.rows > 1)
21
+ buf += `\x1b[${this.rows - 1}A`;
22
+ buf += '\r';
23
+ lines.forEach((line, i) => {
24
+ buf += '\x1b[2K' + line + '\x1b[0m' + (i < lines.length - 1 ? '\n' : '');
25
+ });
26
+ let extra = 0;
27
+ for (let k = lines.length; k < this.rows; k++) {
28
+ buf += '\n\x1b[2K';
29
+ extra++;
30
+ }
31
+ if (extra > 0)
32
+ buf += `\x1b[${extra}A`;
33
+ this.out.write(buf);
34
+ this.rows = lines.length;
35
+ }
36
+ /** Removes the block from the screen and shows the cursor again. */
37
+ erase() {
38
+ if (this.rows === 0) {
39
+ this.out.write('\x1b[?25h');
40
+ return;
41
+ }
42
+ this.out.write(`\r${this.rows > 1 ? `\x1b[${this.rows - 1}A` : ''}\x1b[J\x1b[?25h`);
43
+ this.rows = 0;
44
+ }
45
+ }
@@ -0,0 +1,19 @@
1
+ import { palette, renderNotice } from './ui.js';
2
+ import { stripAnsi } from './width.js';
3
+ const print = (kind, message) => console.log(renderNotice(kind, stripAnsi(message)));
4
+ export const log = {
5
+ info: (message) => print('info', message),
6
+ success: (message) => print('ok', message),
7
+ warn: (message) => print('warn', message),
8
+ error: (message) => print('error', message),
9
+ message: (message) => print('info', message),
10
+ };
11
+ /** A titled block of lines (the command list, the config), rows kept as they are. */
12
+ export function note(text, title = '') {
13
+ console.log();
14
+ if (title)
15
+ console.log(' ' + palette.bold(palette.accent(title)));
16
+ for (const row of stripAnsi(text).split('\n'))
17
+ console.log(' ' + palette.muted(row));
18
+ console.log();
19
+ }
@@ -1,16 +1,110 @@
1
1
  import { marked } from 'marked';
2
2
  import { markedTerminal } from 'marked-terminal';
3
3
  import pc from 'picocolors';
4
+ import { palette } from './ui.js';
4
5
  marked.use(markedTerminal({
5
6
  width: Math.min(process.stdout.columns || 80, 100),
6
7
  reflowText: false,
8
+ showSectionPrefix: false,
7
9
  tab: 2
8
10
  }));
11
+ /**
12
+ * Inline markdown (`code`, **bold**, *italic*) for text that arrives in pieces. The state of an open
13
+ * marker is kept between pieces, so a span that runs over several words (`**seven words in bold**`)
14
+ * is styled as a whole and the marks themselves never reach the screen. A mark that cannot be a
15
+ * mark (`2 * 3`, a lone `*`) stays as it is. Every piece is closed at its end and opened again at
16
+ * the start of the next one, so a line that is wrapped or cut in between stays well-formed.
17
+ */
18
+ export class InlineStyler {
19
+ bold = false;
20
+ italic = false;
21
+ code = false;
22
+ /** The character before the next one: whether a mark opens or closes depends on it. */
23
+ prev = ' ';
24
+ codeOn;
25
+ codeOff;
26
+ constructor() {
27
+ const [on, off] = palette.warn('\u0001').split('\u0001');
28
+ this.codeOn = on ?? '';
29
+ this.codeOff = off ?? '';
30
+ }
31
+ /** A new line: nothing stays open across a line break. */
32
+ endLine() {
33
+ this.bold = this.italic = this.code = false;
34
+ this.prev = ' ';
35
+ }
36
+ open() {
37
+ return (this.bold ? '\x1b[1m' : '') + (this.italic ? '\x1b[3m' : '') + (this.code ? this.codeOn : '');
38
+ }
39
+ close() {
40
+ return (this.code ? this.codeOff : '') + (this.italic ? '\x1b[23m' : '') + (this.bold ? '\x1b[22m' : '');
41
+ }
42
+ format(text) {
43
+ if (!text)
44
+ return '';
45
+ const colour = !!(pc.isColorSupported);
46
+ let out = colour ? this.open() : '';
47
+ const chars = Array.from(text);
48
+ const isWord = (c) => !!c && /[\p{L}\p{N}]/u.test(c);
49
+ const isSpace = (c) => c === undefined || /\s/.test(c);
50
+ for (let i = 0; i < chars.length; i++) {
51
+ const c = chars[i];
52
+ const next = chars[i + 1];
53
+ if (c === '`') {
54
+ // A code span: inside it nothing else is a mark.
55
+ this.code = !this.code;
56
+ out += colour ? (this.code ? this.codeOn : this.codeOff) : '';
57
+ this.prev = c;
58
+ continue;
59
+ }
60
+ if (this.code) {
61
+ out += c;
62
+ this.prev = c;
63
+ continue;
64
+ }
65
+ if (c === '*' && next === '*') {
66
+ const after = chars[i + 2];
67
+ const opens = !this.bold && !isSpace(after) && after !== '*' && !isWord(this.prev);
68
+ const closes = this.bold && !isSpace(this.prev);
69
+ if (opens || closes) {
70
+ this.bold = opens;
71
+ out += colour ? (opens ? '\x1b[1m' : '\x1b[22m') : '';
72
+ i++;
73
+ this.prev = '*';
74
+ continue;
75
+ }
76
+ }
77
+ else if (c === '*') {
78
+ const opens = !this.italic && !isSpace(next) && next !== '*' && !isWord(this.prev);
79
+ const closes = this.italic && !isSpace(this.prev) && !isWord(next);
80
+ if (opens || closes) {
81
+ this.italic = opens;
82
+ out += colour ? (opens ? '\x1b[3m' : '\x1b[23m') : '';
83
+ this.prev = '*';
84
+ continue;
85
+ }
86
+ }
87
+ out += c;
88
+ this.prev = c;
89
+ }
90
+ return out + (colour ? this.close() : '');
91
+ }
92
+ }
93
+ /** Width of horizontal rules / code frames: leaves room for the 2-column gutter. */
94
+ function ruleWidth(max) {
95
+ return Math.max(10, Math.min((process.stdout.columns || 40) - 4, max));
96
+ }
97
+ /** A code fence of the language `markdown` / `md` holds markdown to be shown formatted, not source to be shown as it is (`/copy` gives the source). */
98
+ const MD_FENCE_LANG = /^(?:markdown|md)$/i;
99
+ /** Takes the fences off ```markdown blocks so that their content is rendered as markdown. */
100
+ export function unfenceMarkdown(text) {
101
+ return text.replace(/^[ \t]*```[ \t]*(?:markdown|md)[^\n]*\n([\s\S]*?)^[ \t]*```[ \t]*$/gim, (_m, body) => body.replace(/\n$/, ''));
102
+ }
9
103
  export function renderMarkdown(content) {
10
104
  if (!content)
11
105
  return '';
12
106
  try {
13
- return marked.parse(content).trim();
107
+ return marked.parse(unfenceMarkdown(content)).trim();
14
108
  }
15
109
  catch {
16
110
  return content;
@@ -21,11 +115,23 @@ export function renderMarkdown(content) {
21
115
  * Avoids heavy synchronous AST re-parsing on every chunk, keeping token streaming buttery smooth.
22
116
  */
23
117
  export class MarkdownStreamer {
118
+ sink = (text) => {
119
+ process.stdout.write(text);
120
+ };
24
121
  buffer = '';
25
122
  inCodeBlock = false;
123
+ /** Inside a ```markdown fence: the lines are markdown, drawn formatted and without a frame. */
124
+ inMdFence = false;
26
125
  codeBlockLang = '';
27
126
  inTable = false;
28
127
  tableBuffer = [];
128
+ styler = new InlineStyler();
129
+ /** The buffer starts a line. After words of a line were written it does not: its rest is prose, not a list item or a heading. */
130
+ atLineStart = true;
131
+ /** Redirects the output (e.g. through a GutterWriter). Defaults to stdout. */
132
+ setSink(sink) {
133
+ this.sink = sink;
134
+ }
29
135
  push(chunk) {
30
136
  this.buffer += chunk;
31
137
  // 1. Process completed lines
@@ -33,13 +139,20 @@ export class MarkdownStreamer {
33
139
  const lines = this.buffer.split('\n');
34
140
  this.buffer = lines.pop() || '';
35
141
  for (const line of lines) {
36
- this.processLine(line);
142
+ if (this.atLineStart)
143
+ this.processLine(line);
144
+ else
145
+ this.sink(this.formatInline(line) + '\n');
146
+ this.atLineStart = true;
147
+ this.styler.endLine();
37
148
  }
38
149
  }
39
150
  // 2. Stream regular prose words immediately in real-time
40
151
  if (!this.inCodeBlock && !this.inTable && this.buffer.length > 0) {
41
152
  const trimmed = this.buffer.trimStart();
42
- const isSpecialLine = trimmed.startsWith('#') ||
153
+ // Only spaces so far (the indent of a nested item): wait for what follows.
154
+ const onlyBlank = trimmed.length === 0;
155
+ const isSpecialLine = onlyBlank || this.atLineStart && (trimmed.startsWith('#') ||
43
156
  trimmed.startsWith('|') ||
44
157
  trimmed.startsWith('```') ||
45
158
  trimmed.startsWith('- [') ||
@@ -51,13 +164,14 @@ export class MarkdownStreamer {
51
164
  /^\d+\.\s/.test(trimmed) ||
52
165
  trimmed.startsWith('> ') ||
53
166
  trimmed === '---' ||
54
- trimmed === '***';
167
+ trimmed === '***');
55
168
  if (!isSpecialLine) {
56
169
  const lastSpaceIdx = Math.max(this.buffer.lastIndexOf(' '), this.buffer.lastIndexOf('\t'));
57
170
  if (lastSpaceIdx > 0) {
58
171
  const wordsToFlush = this.buffer.slice(0, lastSpaceIdx + 1);
59
172
  this.buffer = this.buffer.slice(lastSpaceIdx + 1);
60
- process.stdout.write(this.formatInline(wordsToFlush));
173
+ this.sink(this.formatInline(wordsToFlush));
174
+ this.atLineStart = false;
61
175
  }
62
176
  }
63
177
  }
@@ -66,23 +180,31 @@ export class MarkdownStreamer {
66
180
  const trimmed = line.trim();
67
181
  // 1. Code block boundary
68
182
  if (trimmed.startsWith('```')) {
183
+ if (this.inMdFence) {
184
+ this.inMdFence = false;
185
+ return;
186
+ }
187
+ if (!this.inCodeBlock && MD_FENCE_LANG.test(trimmed.slice(3).trim().split(/\s+/)[0] ?? '')) {
188
+ this.inMdFence = true;
189
+ return;
190
+ }
69
191
  if (this.inCodeBlock) {
70
192
  this.inCodeBlock = false;
71
193
  this.codeBlockLang = '';
72
- process.stdout.write(pc.dim('└' + '─'.repeat(Math.min(process.stdout.columns || 40, 50))) + '\n');
194
+ this.sink(palette.rule('└' + '─'.repeat(ruleWidth(50))) + '\n');
73
195
  return;
74
196
  }
75
197
  else {
76
198
  this.inCodeBlock = true;
77
199
  this.codeBlockLang = trimmed.slice(3).trim();
78
200
  const langTag = this.codeBlockLang ? ` [${this.codeBlockLang}] ` : ' ';
79
- process.stdout.write('\n' + pc.cyan('┌─' + langTag + '─'.repeat(Math.max(2, Math.min(process.stdout.columns || 40, 50) - langTag.length - 2))) + '\n');
201
+ this.sink('\n' + palette.accent('┌─' + langTag + '─'.repeat(Math.max(2, ruleWidth(50) - langTag.length - 2))) + '\n');
80
202
  return;
81
203
  }
82
204
  }
83
205
  // 2. Inside code block: fast syntax-tinted output
84
206
  if (this.inCodeBlock) {
85
- process.stdout.write(pc.green('│ ') + pc.white(line) + '\n');
207
+ this.sink(palette.rule('│ ') + palette.text(line) + '\n');
86
208
  return;
87
209
  }
88
210
  // 3. Tables
@@ -101,73 +223,68 @@ export class MarkdownStreamer {
101
223
  const mark = todoMatch[2].toLowerCase();
102
224
  const taskText = this.formatInline(todoMatch[3]);
103
225
  if (mark === 'x' || mark === '✓') {
104
- process.stdout.write(' ' + pc.green('[✓] ') + pc.dim(taskText) + '\n');
226
+ this.sink(' ' + palette.ok('[✓] ') + palette.faint(taskText) + '\n');
105
227
  }
106
228
  else if (mark === '/' || mark === '>') {
107
- process.stdout.write(' ' + pc.bold(pc.green('[ ] ' + taskText)) + '\n');
229
+ this.sink(' ' + palette.bold(palette.ok('[ ] ' + taskText)) + '\n');
108
230
  }
109
231
  else {
110
- process.stdout.write(' ' + pc.dim('[ ] ') + pc.white(taskText) + '\n');
232
+ this.sink(' ' + palette.faint('[ ] ') + palette.text(taskText) + '\n');
111
233
  }
112
234
  return;
113
235
  }
114
236
  // 5. OpenCode Action arrows (→ Read ..., → Edit ...)
115
237
  if (trimmed.startsWith('→ ')) {
116
- process.stdout.write(pc.bold(pc.cyan('→ ')) + pc.white(this.formatInline(trimmed.slice(2))) + '\n');
238
+ this.sink(palette.bold(palette.accent('→ ')) + palette.text(this.formatInline(trimmed.slice(2))) + '\n');
117
239
  return;
118
240
  }
119
241
  // 6. Regular line fast formatting (Headers, Bullets, Bold, Dims)
242
+ const plainMarks = (text) => text.replace(/\*{1,2}([^*\n]+)\*{1,2}/g, '$1').replace(/`/g, '');
120
243
  if (trimmed.startsWith('# ')) {
121
- process.stdout.write('\n' + pc.bold(pc.cyan(trimmed.slice(2))) + '\n\n');
244
+ this.sink('\n' + palette.bold(palette.accent(plainMarks(trimmed.slice(2)))) + '\n\n');
122
245
  }
123
246
  else if (trimmed.startsWith('## ')) {
124
- process.stdout.write('\n' + pc.bold(pc.white(trimmed.slice(3))) + '\n');
247
+ this.sink('\n' + palette.bold(palette.text(plainMarks(trimmed.slice(3)))) + '\n');
125
248
  }
126
- else if (trimmed.startsWith('### ')) {
127
- process.stdout.write('\n' + pc.bold(pc.yellow(trimmed.slice(4))) + '\n');
249
+ else if (/^#{3,6} /.test(trimmed)) {
250
+ this.sink('\n' + palette.bold(palette.warn(plainMarks(trimmed.replace(/^#+ /, '')))) + '\n');
128
251
  }
129
252
  else if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
130
- process.stdout.write(' ' + pc.cyan('• ') + this.formatInline(line.slice(2)) + '\n');
253
+ // A nested item keeps its indentation.
254
+ const indent = ' '.repeat(Math.min(8, line.length - line.trimStart().length));
255
+ this.sink(' ' + indent + palette.accent('• ') + this.formatInline(trimmed.slice(2)) + '\n');
131
256
  }
132
257
  else if (/^\d+\.\s/.test(trimmed)) {
133
258
  const match = trimmed.match(/^(\d+\.)\s*(.*)$/);
134
259
  if (match) {
135
- process.stdout.write(' ' + pc.cyan(match[1]) + ' ' + this.formatInline(match[2]) + '\n');
260
+ this.sink(' ' + palette.accent(match[1]) + ' ' + this.formatInline(match[2]) + '\n');
136
261
  }
137
262
  else {
138
- process.stdout.write(this.formatInline(line) + '\n');
263
+ this.sink(this.formatInline(line) + '\n');
139
264
  }
140
265
  }
141
266
  else if (trimmed.startsWith('> ')) {
142
- process.stdout.write(pc.dim(' │ ' + trimmed.slice(2)) + '\n');
267
+ this.sink(palette.faint(' │ ') + this.formatInline(trimmed.slice(2)) + '\n');
143
268
  }
144
269
  else if (trimmed === '---' || trimmed === '***' || trimmed === '___') {
145
- process.stdout.write(pc.dim('─'.repeat(Math.min(process.stdout.columns || 40, 60))) + '\n');
270
+ this.sink(palette.rule('─'.repeat(ruleWidth(60))) + '\n');
146
271
  }
147
272
  else {
148
- process.stdout.write(this.formatInline(line) + '\n');
273
+ this.sink(this.formatInline(line) + '\n');
149
274
  }
150
275
  }
151
276
  formatInline(text) {
152
- if (!text)
153
- return '';
154
- // Format `code`
155
- let formatted = text.replace(/`([^`]+)`/g, (_, c) => pc.yellow(c));
156
- // Format **bold**
157
- formatted = formatted.replace(/\*\*([^*]+)\*\*/g, (_, b) => pc.bold(b));
158
- // Format *italic*
159
- formatted = formatted.replace(/\*([^*]+)\*/g, (_, i) => pc.italic(i));
160
- return formatted;
277
+ return this.styler.format(text);
161
278
  }
162
279
  flushTable() {
163
280
  if (this.tableBuffer.length > 0) {
164
281
  const tableMd = this.tableBuffer.join('\n');
165
282
  try {
166
283
  const rendered = renderMarkdown(tableMd);
167
- process.stdout.write(rendered + '\n');
284
+ this.sink(rendered + '\n');
168
285
  }
169
286
  catch {
170
- process.stdout.write(tableMd + '\n');
287
+ this.sink(tableMd + '\n');
171
288
  }
172
289
  this.tableBuffer = [];
173
290
  }
@@ -175,9 +292,12 @@ export class MarkdownStreamer {
175
292
  reset() {
176
293
  this.buffer = '';
177
294
  this.inCodeBlock = false;
295
+ this.inMdFence = false;
178
296
  this.codeBlockLang = '';
179
297
  this.inTable = false;
180
298
  this.tableBuffer = [];
299
+ this.atLineStart = true;
300
+ this.styler.endLine();
181
301
  }
182
302
  finish() {
183
303
  if (this.buffer) {
@@ -190,14 +310,16 @@ export class MarkdownStreamer {
190
310
  this.flushTable();
191
311
  if (this.buffer) {
192
312
  if (this.inCodeBlock) {
193
- process.stdout.write(pc.green('│ ') + pc.white(this.buffer) + '\n');
194
- process.stdout.write(pc.dim('└' + '─'.repeat(Math.min(process.stdout.columns || 40, 50))) + '\n');
313
+ this.sink(palette.rule('│ ') + palette.text(this.buffer) + '\n');
314
+ this.sink(palette.rule('└' + '─'.repeat(ruleWidth(50))) + '\n');
195
315
  this.inCodeBlock = false;
196
316
  }
197
317
  else {
198
- process.stdout.write(this.formatInline(this.buffer) + '\n');
318
+ this.sink(this.formatInline(this.buffer) + '\n');
199
319
  }
200
320
  this.buffer = '';
321
+ this.atLineStart = true;
322
+ this.styler.endLine();
201
323
  }
202
324
  }
203
325
  }