termux-dev 1.5.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.
- package/README.md +215 -163
- package/assets/banner.svg +23 -28
- package/assets/preview.png +0 -0
- package/assets/preview.svg +94 -0
- package/dist/cli/banner.js +7 -3
- 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/headless.js +29 -14
- package/dist/cli/index.js +1395 -474
- package/dist/cli/keys.js +69 -0
- package/dist/cli/liveblock.js +45 -0
- package/dist/cli/log.js +19 -0
- package/dist/cli/markdown.js +158 -36
- package/dist/cli/mcp-panel.js +432 -0
- package/dist/cli/permission.js +84 -0
- package/dist/cli/picker.js +135 -0
- package/dist/cli/prompt.js +338 -121
- package/dist/cli/prompt_palette.js +6 -0
- 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 +27 -1
- package/dist/cli/theme.js +126 -108
- package/dist/cli/transcript.js +114 -0
- package/dist/cli/typeahead.js +371 -0
- package/dist/cli/ui.js +1021 -0
- package/dist/cli/updater.js +23 -21
- package/dist/cli/usage-panel.js +242 -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 +210 -0
- package/dist/core/locales/en.js +221 -0
- package/dist/core/locales/es.js +210 -0
- package/dist/core/locales/fr.js +210 -0
- package/dist/core/locales/ja.js +210 -0
- package/dist/core/locales/pt.js +210 -0
- package/dist/core/locales/ru.js +210 -0
- package/dist/core/locales/uk.js +210 -0
- package/dist/core/locales/zh.js +210 -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 +4 -2
- package/assets/preview_work1.png +0 -0
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/markdown.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
+
this.sink(' ' + palette.ok('[✓] ') + palette.faint(taskText) + '\n');
|
|
105
227
|
}
|
|
106
228
|
else if (mark === '/' || mark === '>') {
|
|
107
|
-
|
|
229
|
+
this.sink(' ' + palette.bold(palette.ok('[ ] ' + taskText)) + '\n');
|
|
108
230
|
}
|
|
109
231
|
else {
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
+
this.sink('\n' + palette.bold(palette.accent(plainMarks(trimmed.slice(2)))) + '\n\n');
|
|
122
245
|
}
|
|
123
246
|
else if (trimmed.startsWith('## ')) {
|
|
124
|
-
|
|
247
|
+
this.sink('\n' + palette.bold(palette.text(plainMarks(trimmed.slice(3)))) + '\n');
|
|
125
248
|
}
|
|
126
|
-
else if (trimmed
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
+
this.sink(' ' + palette.accent(match[1]) + ' ' + this.formatInline(match[2]) + '\n');
|
|
136
261
|
}
|
|
137
262
|
else {
|
|
138
|
-
|
|
263
|
+
this.sink(this.formatInline(line) + '\n');
|
|
139
264
|
}
|
|
140
265
|
}
|
|
141
266
|
else if (trimmed.startsWith('> ')) {
|
|
142
|
-
|
|
267
|
+
this.sink(palette.faint(' │ ') + this.formatInline(trimmed.slice(2)) + '\n');
|
|
143
268
|
}
|
|
144
269
|
else if (trimmed === '---' || trimmed === '***' || trimmed === '___') {
|
|
145
|
-
|
|
270
|
+
this.sink(palette.rule('─'.repeat(ruleWidth(60))) + '\n');
|
|
146
271
|
}
|
|
147
272
|
else {
|
|
148
|
-
|
|
273
|
+
this.sink(this.formatInline(line) + '\n');
|
|
149
274
|
}
|
|
150
275
|
}
|
|
151
276
|
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;
|
|
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
|
-
|
|
284
|
+
this.sink(rendered + '\n');
|
|
168
285
|
}
|
|
169
286
|
catch {
|
|
170
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
}
|