termux-dev 1.6.0 → 1.8.1
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 +214 -166
- package/assets/banner.svg +23 -28
- package/assets/preview.png +0 -0
- package/assets/preview.svg +101 -10
- package/dist/cli/banner.js +3 -4
- package/dist/cli/clipboard-write.js +55 -0
- package/dist/cli/clipboard.js +182 -106
- package/dist/cli/colors.js +67 -0
- package/dist/cli/confirm.js +50 -0
- package/dist/cli/doctor.js +4 -4
- package/dist/cli/effort.js +80 -0
- package/dist/cli/export.js +3 -3
- package/dist/cli/files.js +19 -5
- package/dist/cli/finder.js +241 -0
- package/dist/cli/headless.js +23 -11
- package/dist/cli/index.js +1557 -744
- package/dist/cli/keys.js +69 -0
- package/dist/cli/liveblock.js +45 -0
- package/dist/cli/log.js +19 -0
- package/dist/cli/logo.js +30 -0
- package/dist/cli/markdown.js +174 -37
- package/dist/cli/mcp-panel.js +432 -0
- package/dist/cli/permission.js +84 -0
- package/dist/cli/picker.js +143 -0
- package/dist/cli/prompt.js +256 -56
- package/dist/cli/resume.js +15 -0
- package/dist/cli/rewind.js +51 -0
- package/dist/cli/runtime_errors.js +6 -0
- package/dist/cli/server.js +85 -24
- package/dist/cli/settings.js +307 -0
- package/dist/cli/statusline.js +1 -1
- package/dist/cli/theme.js +126 -108
- package/dist/cli/transcript.js +134 -0
- package/dist/cli/typeahead.js +371 -0
- package/dist/cli/ui.js +1022 -0
- package/dist/cli/updater.js +23 -21
- package/dist/cli/usage-panel.js +242 -0
- package/dist/cli/welcome.js +56 -0
- package/dist/cli/width.js +178 -0
- package/dist/core/capabilities.js +433 -0
- package/dist/core/catalog-util.js +27 -0
- package/dist/core/checkpoint.js +90 -0
- package/dist/core/context-stats.js +29 -0
- package/dist/core/cost.js +191 -0
- package/dist/core/formats.js +199 -0
- package/dist/core/history.js +51 -1
- package/dist/core/hooks.js +79 -0
- package/dist/core/i18n.js +42 -0
- package/dist/core/local-config.js +69 -0
- package/dist/core/locales/de.js +265 -0
- package/dist/core/locales/en.js +276 -0
- package/dist/core/locales/es.js +265 -0
- package/dist/core/locales/fr.js +265 -0
- package/dist/core/locales/ja.js +265 -0
- package/dist/core/locales/pt.js +265 -0
- package/dist/core/locales/ru.js +265 -0
- package/dist/core/locales/uk.js +265 -0
- package/dist/core/locales/zh.js +265 -0
- package/dist/core/loop.js +125 -49
- package/dist/core/memory.js +39 -12
- package/dist/core/models.js +33 -20
- package/dist/core/network.js +12 -11
- package/dist/core/notify.js +54 -0
- package/dist/core/planfile.js +43 -0
- package/dist/core/price-catalog.json +1 -0
- package/dist/core/prices.js +249 -0
- package/dist/core/reasoning-catalog.json +1 -0
- package/dist/core/reasoning.js +393 -0
- package/dist/core/redact.js +41 -0
- package/dist/core/secure-file.js +22 -0
- package/dist/core/session.js +16 -5
- package/dist/core/snapshot.js +14 -2
- package/dist/core/title.js +61 -0
- package/dist/core/types.js +1 -1
- package/dist/core/usage.js +43 -7
- package/dist/core/vision-catalog.json +1 -0
- package/dist/core/vision.js +123 -0
- package/dist/mcp/client.js +36 -5
- package/dist/mcp/http-client.js +174 -0
- package/dist/mcp/manager.js +330 -93
- package/dist/permissions/guard.js +385 -117
- package/dist/prompts/builder.js +46 -38
- package/dist/providers/anthropic.js +517 -0
- package/dist/providers/index.js +18 -1
- package/dist/providers/openai.js +138 -125
- package/dist/providers/shared.js +116 -0
- package/dist/providers/toolguard.js +101 -0
- package/dist/tools/bash.js +141 -51
- package/dist/tools/diagnostics.js +76 -48
- package/dist/tools/fs.js +77 -5
- package/dist/tools/index.js +2 -1
- package/dist/tools/mcp.js +72 -0
- package/dist/tools/packages.js +16 -1
- package/dist/tools/plan.js +8 -8
- package/dist/tools/questions.js +50 -54
- package/dist/tools/server.js +6 -0
- package/dist/tools/todo.js +20 -18
- package/dist/tools/web.js +143 -8
- package/package.json +1 -1
package/dist/cli/keys.js
ADDED
|
@@ -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
|
+
}
|
package/dist/cli/log.js
ADDED
|
@@ -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
|
+
}
|
package/dist/cli/logo.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The devx wordmark, drawn from a small pixel font: every letter is 5 pixels wide and 6 tall, and two
|
|
3
|
+
* pixel rows share one terminal row with half blocks, so the pixels are square and the shapes stay clean.
|
|
4
|
+
*/
|
|
5
|
+
const FONT = {
|
|
6
|
+
D: ['████ ', '█ █', '█ █', '█ █', '█ █', '████ '],
|
|
7
|
+
E: ['█████', '█ ', '████ ', '█ ', '█ ', '█████'],
|
|
8
|
+
V: ['█ █', '█ █', '█ █', '█ █', ' █ █ ', ' █ '],
|
|
9
|
+
X: ['█ █', ' █ █ ', ' █ ', ' █ ', ' █ █ ', '█ █'],
|
|
10
|
+
};
|
|
11
|
+
export function wordmark(text = 'DEVX', gap = 1) {
|
|
12
|
+
const letters = Array.from(text).map((c) => FONT[c]);
|
|
13
|
+
const rows = letters[0].length;
|
|
14
|
+
const out = [];
|
|
15
|
+
for (let r = 0; r < rows; r += 2) {
|
|
16
|
+
let line = '';
|
|
17
|
+
letters.forEach((glyph, i) => {
|
|
18
|
+
if (i > 0)
|
|
19
|
+
line += ' '.repeat(gap);
|
|
20
|
+
for (let x = 0; x < glyph[r].length; x++) {
|
|
21
|
+
const top = glyph[r][x] !== ' ';
|
|
22
|
+
const bottom = glyph[r + 1][x] !== ' ';
|
|
23
|
+
line += top && bottom ? '█' : top ? '▀' : bottom ? '▄' : ' ';
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
out.push(line);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
export const DEVX_LOGO = wordmark();
|
package/dist/cli/markdown.js
CHANGED
|
@@ -1,16 +1,124 @@
|
|
|
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
|
+
// marked-terminal prints the text of a list item as it was written (`code` and **bold** with their
|
|
12
|
+
// marks); the inline tokens of the item have to be rendered.
|
|
13
|
+
marked.use({
|
|
14
|
+
renderer: {
|
|
15
|
+
text(token) {
|
|
16
|
+
if (token && typeof token === 'object' && Array.isArray(token.tokens) && token.tokens.length > 0) {
|
|
17
|
+
return this.parser.parseInline(token.tokens);
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Inline markdown (`code`, **bold**, *italic*) for text that arrives in pieces. The state of an open
|
|
25
|
+
* marker is kept between pieces, so a span that runs over several words (`**seven words in bold**`)
|
|
26
|
+
* is styled as a whole and the marks themselves never reach the screen. A mark that cannot be a
|
|
27
|
+
* mark (`2 * 3`, a lone `*`) stays as it is. Every piece is closed at its end and opened again at
|
|
28
|
+
* the start of the next one, so a line that is wrapped or cut in between stays well-formed.
|
|
29
|
+
*/
|
|
30
|
+
export class InlineStyler {
|
|
31
|
+
bold = false;
|
|
32
|
+
italic = false;
|
|
33
|
+
code = false;
|
|
34
|
+
/** The character before the next one: whether a mark opens or closes depends on it. */
|
|
35
|
+
prev = ' ';
|
|
36
|
+
codeOn;
|
|
37
|
+
codeOff;
|
|
38
|
+
constructor() {
|
|
39
|
+
const [on, off] = palette.warn('\u0001').split('\u0001');
|
|
40
|
+
this.codeOn = on ?? '';
|
|
41
|
+
this.codeOff = off ?? '';
|
|
42
|
+
}
|
|
43
|
+
/** A new line: nothing stays open across a line break. */
|
|
44
|
+
endLine() {
|
|
45
|
+
this.bold = this.italic = this.code = false;
|
|
46
|
+
this.prev = ' ';
|
|
47
|
+
}
|
|
48
|
+
open() {
|
|
49
|
+
return (this.bold ? '\x1b[1m' : '') + (this.italic ? '\x1b[3m' : '') + (this.code ? this.codeOn : '');
|
|
50
|
+
}
|
|
51
|
+
close() {
|
|
52
|
+
return (this.code ? this.codeOff : '') + (this.italic ? '\x1b[23m' : '') + (this.bold ? '\x1b[22m' : '');
|
|
53
|
+
}
|
|
54
|
+
format(text) {
|
|
55
|
+
if (!text)
|
|
56
|
+
return '';
|
|
57
|
+
const colour = !!(pc.isColorSupported);
|
|
58
|
+
let out = colour ? this.open() : '';
|
|
59
|
+
const chars = Array.from(text);
|
|
60
|
+
const isWord = (c) => !!c && /[\p{L}\p{N}]/u.test(c);
|
|
61
|
+
const isSpace = (c) => c === undefined || /\s/.test(c);
|
|
62
|
+
for (let i = 0; i < chars.length; i++) {
|
|
63
|
+
const c = chars[i];
|
|
64
|
+
const next = chars[i + 1];
|
|
65
|
+
if (c === '`') {
|
|
66
|
+
// A code span: inside it nothing else is a mark.
|
|
67
|
+
this.code = !this.code;
|
|
68
|
+
out += colour ? (this.code ? this.codeOn : this.codeOff) : '';
|
|
69
|
+
this.prev = c;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (this.code) {
|
|
73
|
+
out += c;
|
|
74
|
+
this.prev = c;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (c === '*' && next === '*') {
|
|
78
|
+
const after = chars[i + 2];
|
|
79
|
+
const opens = !this.bold && !isSpace(after) && after !== '*' && !isWord(this.prev);
|
|
80
|
+
const closes = this.bold && !isSpace(this.prev);
|
|
81
|
+
if (opens || closes) {
|
|
82
|
+
this.bold = opens;
|
|
83
|
+
out += colour ? (opens ? '\x1b[1m' : '\x1b[22m') : '';
|
|
84
|
+
i++;
|
|
85
|
+
this.prev = '*';
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else if (c === '*') {
|
|
90
|
+
const opens = !this.italic && !isSpace(next) && next !== '*' && !isWord(this.prev);
|
|
91
|
+
const closes = this.italic && !isSpace(this.prev) && !isWord(next);
|
|
92
|
+
if (opens || closes) {
|
|
93
|
+
this.italic = opens;
|
|
94
|
+
out += colour ? (opens ? '\x1b[3m' : '\x1b[23m') : '';
|
|
95
|
+
this.prev = '*';
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
out += c;
|
|
100
|
+
this.prev = c;
|
|
101
|
+
}
|
|
102
|
+
return out + (colour ? this.close() : '');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Width of horizontal rules / code frames: leaves room for the 2-column gutter. */
|
|
106
|
+
function ruleWidth(max) {
|
|
107
|
+
return Math.max(10, Math.min((process.stdout.columns || 40) - 4, max));
|
|
108
|
+
}
|
|
109
|
+
/** 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). */
|
|
110
|
+
const MD_FENCE_LANG = /^(?:markdown|md)$/i;
|
|
111
|
+
/** Takes the fences off ```markdown blocks so that their content is rendered as markdown. */
|
|
112
|
+
export function unfenceMarkdown(text) {
|
|
113
|
+
return text.replace(/^[ \t]*```[ \t]*(?:markdown|md)[^\n]*\n([\s\S]*?)^[ \t]*```[ \t]*$/gim, (_m, body) => body.replace(/\n$/, ''));
|
|
114
|
+
}
|
|
9
115
|
export function renderMarkdown(content) {
|
|
10
116
|
if (!content)
|
|
11
117
|
return '';
|
|
12
118
|
try {
|
|
13
|
-
|
|
119
|
+
const text = marked.parse(unfenceMarkdown(content)).trim();
|
|
120
|
+
// The list marker of marked-terminal is `*`: draw a bullet instead.
|
|
121
|
+
return text.replace(/^((?:\s|\x1b\[[0-9;]*m)*)\* /gm, '$1• ');
|
|
14
122
|
}
|
|
15
123
|
catch {
|
|
16
124
|
return content;
|
|
@@ -21,11 +129,23 @@ export function renderMarkdown(content) {
|
|
|
21
129
|
* Avoids heavy synchronous AST re-parsing on every chunk, keeping token streaming buttery smooth.
|
|
22
130
|
*/
|
|
23
131
|
export class MarkdownStreamer {
|
|
132
|
+
sink = (text) => {
|
|
133
|
+
process.stdout.write(text);
|
|
134
|
+
};
|
|
24
135
|
buffer = '';
|
|
25
136
|
inCodeBlock = false;
|
|
137
|
+
/** Inside a ```markdown fence: the lines are markdown, drawn formatted and without a frame. */
|
|
138
|
+
inMdFence = false;
|
|
26
139
|
codeBlockLang = '';
|
|
27
140
|
inTable = false;
|
|
28
141
|
tableBuffer = [];
|
|
142
|
+
styler = new InlineStyler();
|
|
143
|
+
/** 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. */
|
|
144
|
+
atLineStart = true;
|
|
145
|
+
/** Redirects the output (e.g. through a GutterWriter). Defaults to stdout. */
|
|
146
|
+
setSink(sink) {
|
|
147
|
+
this.sink = sink;
|
|
148
|
+
}
|
|
29
149
|
push(chunk) {
|
|
30
150
|
this.buffer += chunk;
|
|
31
151
|
// 1. Process completed lines
|
|
@@ -33,13 +153,20 @@ export class MarkdownStreamer {
|
|
|
33
153
|
const lines = this.buffer.split('\n');
|
|
34
154
|
this.buffer = lines.pop() || '';
|
|
35
155
|
for (const line of lines) {
|
|
36
|
-
this.
|
|
156
|
+
if (this.atLineStart)
|
|
157
|
+
this.processLine(line);
|
|
158
|
+
else
|
|
159
|
+
this.sink(this.formatInline(line) + '\n');
|
|
160
|
+
this.atLineStart = true;
|
|
161
|
+
this.styler.endLine();
|
|
37
162
|
}
|
|
38
163
|
}
|
|
39
164
|
// 2. Stream regular prose words immediately in real-time
|
|
40
165
|
if (!this.inCodeBlock && !this.inTable && this.buffer.length > 0) {
|
|
41
166
|
const trimmed = this.buffer.trimStart();
|
|
42
|
-
|
|
167
|
+
// Only spaces so far (the indent of a nested item): wait for what follows.
|
|
168
|
+
const onlyBlank = trimmed.length === 0;
|
|
169
|
+
const isSpecialLine = onlyBlank || this.atLineStart && (trimmed.startsWith('#') ||
|
|
43
170
|
trimmed.startsWith('|') ||
|
|
44
171
|
trimmed.startsWith('```') ||
|
|
45
172
|
trimmed.startsWith('- [') ||
|
|
@@ -51,13 +178,15 @@ export class MarkdownStreamer {
|
|
|
51
178
|
/^\d+\.\s/.test(trimmed) ||
|
|
52
179
|
trimmed.startsWith('> ') ||
|
|
53
180
|
trimmed === '---' ||
|
|
54
|
-
trimmed === '***';
|
|
181
|
+
trimmed === '***');
|
|
55
182
|
if (!isSpecialLine) {
|
|
56
183
|
const lastSpaceIdx = Math.max(this.buffer.lastIndexOf(' '), this.buffer.lastIndexOf('\t'));
|
|
57
|
-
|
|
184
|
+
// At the start of a line, a first word that is not finished yet (` *`, the marker of an indented item) waits.
|
|
185
|
+
if (lastSpaceIdx > 0 && (!this.atLineStart || /\S\s/.test(this.buffer))) {
|
|
58
186
|
const wordsToFlush = this.buffer.slice(0, lastSpaceIdx + 1);
|
|
59
187
|
this.buffer = this.buffer.slice(lastSpaceIdx + 1);
|
|
60
|
-
|
|
188
|
+
this.sink(this.formatInline(wordsToFlush));
|
|
189
|
+
this.atLineStart = false;
|
|
61
190
|
}
|
|
62
191
|
}
|
|
63
192
|
}
|
|
@@ -66,23 +195,31 @@ export class MarkdownStreamer {
|
|
|
66
195
|
const trimmed = line.trim();
|
|
67
196
|
// 1. Code block boundary
|
|
68
197
|
if (trimmed.startsWith('```')) {
|
|
198
|
+
if (this.inMdFence) {
|
|
199
|
+
this.inMdFence = false;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!this.inCodeBlock && MD_FENCE_LANG.test(trimmed.slice(3).trim().split(/\s+/)[0] ?? '')) {
|
|
203
|
+
this.inMdFence = true;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
69
206
|
if (this.inCodeBlock) {
|
|
70
207
|
this.inCodeBlock = false;
|
|
71
208
|
this.codeBlockLang = '';
|
|
72
|
-
|
|
209
|
+
this.sink(palette.rule('└' + '─'.repeat(ruleWidth(50))) + '\n');
|
|
73
210
|
return;
|
|
74
211
|
}
|
|
75
212
|
else {
|
|
76
213
|
this.inCodeBlock = true;
|
|
77
214
|
this.codeBlockLang = trimmed.slice(3).trim();
|
|
78
215
|
const langTag = this.codeBlockLang ? ` [${this.codeBlockLang}] ` : ' ';
|
|
79
|
-
|
|
216
|
+
this.sink('\n' + palette.accent('┌─' + langTag + '─'.repeat(Math.max(2, ruleWidth(50) - langTag.length - 2))) + '\n');
|
|
80
217
|
return;
|
|
81
218
|
}
|
|
82
219
|
}
|
|
83
220
|
// 2. Inside code block: fast syntax-tinted output
|
|
84
221
|
if (this.inCodeBlock) {
|
|
85
|
-
|
|
222
|
+
this.sink(palette.rule('│ ') + palette.text(line) + '\n');
|
|
86
223
|
return;
|
|
87
224
|
}
|
|
88
225
|
// 3. Tables
|
|
@@ -101,73 +238,68 @@ export class MarkdownStreamer {
|
|
|
101
238
|
const mark = todoMatch[2].toLowerCase();
|
|
102
239
|
const taskText = this.formatInline(todoMatch[3]);
|
|
103
240
|
if (mark === 'x' || mark === '✓') {
|
|
104
|
-
|
|
241
|
+
this.sink(' ' + palette.ok('[✓] ') + palette.faint(taskText) + '\n');
|
|
105
242
|
}
|
|
106
243
|
else if (mark === '/' || mark === '>') {
|
|
107
|
-
|
|
244
|
+
this.sink(' ' + palette.bold(palette.ok('[ ] ' + taskText)) + '\n');
|
|
108
245
|
}
|
|
109
246
|
else {
|
|
110
|
-
|
|
247
|
+
this.sink(' ' + palette.faint('[ ] ') + palette.text(taskText) + '\n');
|
|
111
248
|
}
|
|
112
249
|
return;
|
|
113
250
|
}
|
|
114
251
|
// 5. OpenCode Action arrows (→ Read ..., → Edit ...)
|
|
115
252
|
if (trimmed.startsWith('→ ')) {
|
|
116
|
-
|
|
253
|
+
this.sink(palette.bold(palette.accent('→ ')) + palette.text(this.formatInline(trimmed.slice(2))) + '\n');
|
|
117
254
|
return;
|
|
118
255
|
}
|
|
119
256
|
// 6. Regular line fast formatting (Headers, Bullets, Bold, Dims)
|
|
257
|
+
const plainMarks = (text) => text.replace(/\*{1,2}([^*\n]+)\*{1,2}/g, '$1').replace(/`/g, '');
|
|
120
258
|
if (trimmed.startsWith('# ')) {
|
|
121
|
-
|
|
259
|
+
this.sink('\n' + palette.bold(palette.accent(plainMarks(trimmed.slice(2)))) + '\n\n');
|
|
122
260
|
}
|
|
123
261
|
else if (trimmed.startsWith('## ')) {
|
|
124
|
-
|
|
262
|
+
this.sink('\n' + palette.bold(palette.text(plainMarks(trimmed.slice(3)))) + '\n');
|
|
125
263
|
}
|
|
126
|
-
else if (trimmed
|
|
127
|
-
|
|
264
|
+
else if (/^#{3,6} /.test(trimmed)) {
|
|
265
|
+
this.sink('\n' + palette.bold(palette.warn(plainMarks(trimmed.replace(/^#+ /, '')))) + '\n');
|
|
128
266
|
}
|
|
129
267
|
else if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
|
130
|
-
|
|
268
|
+
// A nested item keeps its indentation.
|
|
269
|
+
const indent = ' '.repeat(Math.min(8, line.length - line.trimStart().length));
|
|
270
|
+
this.sink(' ' + indent + palette.accent('• ') + this.formatInline(trimmed.slice(2)) + '\n');
|
|
131
271
|
}
|
|
132
272
|
else if (/^\d+\.\s/.test(trimmed)) {
|
|
133
273
|
const match = trimmed.match(/^(\d+\.)\s*(.*)$/);
|
|
134
274
|
if (match) {
|
|
135
|
-
|
|
275
|
+
this.sink(' ' + palette.accent(match[1]) + ' ' + this.formatInline(match[2]) + '\n');
|
|
136
276
|
}
|
|
137
277
|
else {
|
|
138
|
-
|
|
278
|
+
this.sink(this.formatInline(line) + '\n');
|
|
139
279
|
}
|
|
140
280
|
}
|
|
141
281
|
else if (trimmed.startsWith('> ')) {
|
|
142
|
-
|
|
282
|
+
this.sink(palette.faint(' │ ') + this.formatInline(trimmed.slice(2)) + '\n');
|
|
143
283
|
}
|
|
144
284
|
else if (trimmed === '---' || trimmed === '***' || trimmed === '___') {
|
|
145
|
-
|
|
285
|
+
this.sink(palette.rule('─'.repeat(ruleWidth(60))) + '\n');
|
|
146
286
|
}
|
|
147
287
|
else {
|
|
148
|
-
|
|
288
|
+
this.sink(this.formatInline(line) + '\n');
|
|
149
289
|
}
|
|
150
290
|
}
|
|
151
291
|
formatInline(text) {
|
|
152
|
-
|
|
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;
|
|
292
|
+
return this.styler.format(text);
|
|
161
293
|
}
|
|
162
294
|
flushTable() {
|
|
163
295
|
if (this.tableBuffer.length > 0) {
|
|
164
296
|
const tableMd = this.tableBuffer.join('\n');
|
|
165
297
|
try {
|
|
166
298
|
const rendered = renderMarkdown(tableMd);
|
|
167
|
-
|
|
299
|
+
this.sink(rendered + '\n');
|
|
168
300
|
}
|
|
169
301
|
catch {
|
|
170
|
-
|
|
302
|
+
this.sink(tableMd + '\n');
|
|
171
303
|
}
|
|
172
304
|
this.tableBuffer = [];
|
|
173
305
|
}
|
|
@@ -175,9 +307,12 @@ export class MarkdownStreamer {
|
|
|
175
307
|
reset() {
|
|
176
308
|
this.buffer = '';
|
|
177
309
|
this.inCodeBlock = false;
|
|
310
|
+
this.inMdFence = false;
|
|
178
311
|
this.codeBlockLang = '';
|
|
179
312
|
this.inTable = false;
|
|
180
313
|
this.tableBuffer = [];
|
|
314
|
+
this.atLineStart = true;
|
|
315
|
+
this.styler.endLine();
|
|
181
316
|
}
|
|
182
317
|
finish() {
|
|
183
318
|
if (this.buffer) {
|
|
@@ -190,14 +325,16 @@ export class MarkdownStreamer {
|
|
|
190
325
|
this.flushTable();
|
|
191
326
|
if (this.buffer) {
|
|
192
327
|
if (this.inCodeBlock) {
|
|
193
|
-
|
|
194
|
-
|
|
328
|
+
this.sink(palette.rule('│ ') + palette.text(this.buffer) + '\n');
|
|
329
|
+
this.sink(palette.rule('└' + '─'.repeat(ruleWidth(50))) + '\n');
|
|
195
330
|
this.inCodeBlock = false;
|
|
196
331
|
}
|
|
197
332
|
else {
|
|
198
|
-
|
|
333
|
+
this.sink(this.formatInline(this.buffer) + '\n');
|
|
199
334
|
}
|
|
200
335
|
this.buffer = '';
|
|
336
|
+
this.atLineStart = true;
|
|
337
|
+
this.styler.endLine();
|
|
201
338
|
}
|
|
202
339
|
}
|
|
203
340
|
}
|