nex-code 0.3.5 → 0.3.7
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 +23 -1
- package/dist/bundle.js +505 -0
- package/dist/nex-code.js +485 -0
- package/package.json +8 -6
- package/bin/nex-code.js +0 -99
- package/cli/agent.js +0 -835
- package/cli/compactor.js +0 -85
- package/cli/context-engine.js +0 -507
- package/cli/context.js +0 -98
- package/cli/costs.js +0 -290
- package/cli/diff.js +0 -366
- package/cli/file-history.js +0 -94
- package/cli/format.js +0 -211
- package/cli/fuzzy-match.js +0 -270
- package/cli/git.js +0 -211
- package/cli/hooks.js +0 -173
- package/cli/index.js +0 -1289
- package/cli/mcp.js +0 -284
- package/cli/memory.js +0 -170
- package/cli/ollama.js +0 -130
- package/cli/permissions.js +0 -124
- package/cli/picker.js +0 -201
- package/cli/planner.js +0 -282
- package/cli/providers/anthropic.js +0 -333
- package/cli/providers/base.js +0 -116
- package/cli/providers/gemini.js +0 -239
- package/cli/providers/local.js +0 -249
- package/cli/providers/ollama.js +0 -228
- package/cli/providers/openai.js +0 -237
- package/cli/providers/registry.js +0 -454
- package/cli/render.js +0 -495
- package/cli/safety.js +0 -241
- package/cli/session.js +0 -133
- package/cli/skills.js +0 -412
- package/cli/spinner.js +0 -371
- package/cli/sub-agent.js +0 -441
- package/cli/tasks.js +0 -179
- package/cli/tool-tiers.js +0 -164
- package/cli/tool-validator.js +0 -138
- package/cli/tools.js +0 -1050
- package/cli/ui.js +0 -93
package/cli/render.js
DELETED
|
@@ -1,495 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cli/render.js — Rich Terminal Rendering
|
|
3
|
-
* Markdown rendering, syntax highlighting, table formatting
|
|
4
|
-
* Zero dependencies — uses ANSI escape codes directly
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const { C } = require('./ui');
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Render markdown-like text for terminal output
|
|
11
|
-
* Supports: headers, bold, italic, code, code blocks, lists, links
|
|
12
|
-
* @param {string} text
|
|
13
|
-
* @returns {string}
|
|
14
|
-
*/
|
|
15
|
-
function renderMarkdown(text) {
|
|
16
|
-
if (!text) return '';
|
|
17
|
-
|
|
18
|
-
const lines = text.split('\n');
|
|
19
|
-
const rendered = [];
|
|
20
|
-
let inCodeBlock = false;
|
|
21
|
-
let codeBlockLang = '';
|
|
22
|
-
|
|
23
|
-
for (const line of lines) {
|
|
24
|
-
// Code block toggle
|
|
25
|
-
if (line.trim().startsWith('```')) {
|
|
26
|
-
if (inCodeBlock) {
|
|
27
|
-
rendered.push(`${C.dim}${'─'.repeat(40)}${C.reset}`);
|
|
28
|
-
inCodeBlock = false;
|
|
29
|
-
codeBlockLang = '';
|
|
30
|
-
} else {
|
|
31
|
-
inCodeBlock = true;
|
|
32
|
-
codeBlockLang = line.trim().substring(3).trim();
|
|
33
|
-
const label = codeBlockLang ? ` ${codeBlockLang} ` : '';
|
|
34
|
-
rendered.push(`${C.dim}${'─'.repeat(3)}${label}${'─'.repeat(Math.max(0, 37 - label.length))}${C.reset}`);
|
|
35
|
-
}
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (inCodeBlock) {
|
|
40
|
-
rendered.push(` ${highlightCode(line, codeBlockLang)}`);
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Headers
|
|
45
|
-
if (line.startsWith('### ')) {
|
|
46
|
-
rendered.push(`${C.bold}${C.cyan} ${line.substring(4)}${C.reset}`);
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (line.startsWith('## ')) {
|
|
50
|
-
rendered.push(`${C.bold}${C.cyan} ${line.substring(3)}${C.reset}`);
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (line.startsWith('# ')) {
|
|
54
|
-
rendered.push(`${C.bold}${C.cyan}${line.substring(2)}${C.reset}`);
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Lists
|
|
59
|
-
if (/^\s*[-*]\s/.test(line)) {
|
|
60
|
-
const indent = line.match(/^(\s*)/)[1];
|
|
61
|
-
const content = line.replace(/^\s*[-*]\s/, '');
|
|
62
|
-
rendered.push(`${indent}${C.cyan}•${C.reset} ${renderInline(content)}`);
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Numbered lists
|
|
67
|
-
if (/^\s*\d+\.\s/.test(line)) {
|
|
68
|
-
const match = line.match(/^(\s*)(\d+)\.\s(.*)/);
|
|
69
|
-
if (match) {
|
|
70
|
-
rendered.push(`${match[1]}${C.cyan}${match[2]}.${C.reset} ${renderInline(match[3])}`);
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Regular line
|
|
76
|
-
rendered.push(renderInline(line));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return rendered.join('\n');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Render inline markdown (bold, italic, code, links)
|
|
84
|
-
* @param {string} text
|
|
85
|
-
* @returns {string}
|
|
86
|
-
*/
|
|
87
|
-
function renderInline(text) {
|
|
88
|
-
if (!text) return '';
|
|
89
|
-
|
|
90
|
-
return text
|
|
91
|
-
// Inline code `code`
|
|
92
|
-
.replace(/`([^`]+)`/g, `${C.cyan}$1${C.reset}`)
|
|
93
|
-
// Bold **text**
|
|
94
|
-
.replace(/\*\*([^*]+)\*\*/g, `${C.bold}$1${C.reset}`)
|
|
95
|
-
// Italic *text*
|
|
96
|
-
.replace(/\*([^*]+)\*/g, `${C.dim}$1${C.reset}`)
|
|
97
|
-
// Links [text](url)
|
|
98
|
-
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, `${C.cyan}$1${C.reset} ${C.dim}($2)${C.reset}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Basic syntax highlighting for code
|
|
103
|
-
* @param {string} line
|
|
104
|
-
* @param {string} lang
|
|
105
|
-
* @returns {string}
|
|
106
|
-
*/
|
|
107
|
-
function highlightCode(line, lang) {
|
|
108
|
-
if (!line) return '';
|
|
109
|
-
|
|
110
|
-
const jsLangs = ['js', 'javascript', 'ts', 'typescript', 'jsx', 'tsx'];
|
|
111
|
-
if (jsLangs.includes(lang) || !lang) {
|
|
112
|
-
return highlightJS(line);
|
|
113
|
-
}
|
|
114
|
-
if (lang === 'bash' || lang === 'sh' || lang === 'shell' || lang === 'zsh') {
|
|
115
|
-
return highlightBash(line);
|
|
116
|
-
}
|
|
117
|
-
if (lang === 'json' || lang === 'jsonc') {
|
|
118
|
-
return highlightJSON(line);
|
|
119
|
-
}
|
|
120
|
-
if (lang === 'python' || lang === 'py') {
|
|
121
|
-
return highlightPython(line);
|
|
122
|
-
}
|
|
123
|
-
if (lang === 'go' || lang === 'golang') {
|
|
124
|
-
return highlightGo(line);
|
|
125
|
-
}
|
|
126
|
-
if (lang === 'rust' || lang === 'rs') {
|
|
127
|
-
return highlightRust(line);
|
|
128
|
-
}
|
|
129
|
-
if (lang === 'css' || lang === 'scss' || lang === 'less') {
|
|
130
|
-
return highlightCSS(line);
|
|
131
|
-
}
|
|
132
|
-
if (lang === 'html' || lang === 'xml' || lang === 'svg' || lang === 'htm') {
|
|
133
|
-
return highlightHTML(line);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Default: no highlighting
|
|
137
|
-
return line;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function highlightJS(line) {
|
|
141
|
-
const keywords = /\b(const|let|var|function|return|if|else|for|while|class|import|export|from|require|async|await|new|this|throw|try|catch|switch|case|break|default|typeof|instanceof)\b/g;
|
|
142
|
-
const strings = /(["'`])(?:(?=(\\?))\2.)*?\1/g;
|
|
143
|
-
const comments = /(\/\/.*$)/;
|
|
144
|
-
const numbers = /\b(\d+\.?\d*)\b/g;
|
|
145
|
-
|
|
146
|
-
let result = line;
|
|
147
|
-
// Order matters: comments last (they override everything)
|
|
148
|
-
result = result.replace(numbers, `${C.yellow}$1${C.reset}`);
|
|
149
|
-
result = result.replace(keywords, `${C.magenta}$1${C.reset}`);
|
|
150
|
-
result = result.replace(strings, `${C.green}$&${C.reset}`);
|
|
151
|
-
result = result.replace(comments, `${C.dim}$1${C.reset}`);
|
|
152
|
-
|
|
153
|
-
return result;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function highlightBash(line) {
|
|
157
|
-
const commands = /^(\s*)([\w-]+)/;
|
|
158
|
-
const flags = /(--?\w[\w-]*)/g;
|
|
159
|
-
const strings = /(["'])(?:(?=(\\?))\2.)*?\1/g;
|
|
160
|
-
const comments = /(#.*$)/;
|
|
161
|
-
|
|
162
|
-
let result = line;
|
|
163
|
-
result = result.replace(flags, `${C.cyan}$1${C.reset}`);
|
|
164
|
-
result = result.replace(commands, `$1${C.green}$2${C.reset}`);
|
|
165
|
-
result = result.replace(strings, `${C.yellow}$&${C.reset}`);
|
|
166
|
-
result = result.replace(comments, `${C.dim}$1${C.reset}`);
|
|
167
|
-
|
|
168
|
-
return result;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function highlightJSON(line) {
|
|
172
|
-
const keys = /("[\w-]+")\s*:/g;
|
|
173
|
-
const strings = /:\s*("(?:[^"\\]|\\.)*")/g;
|
|
174
|
-
const numbers = /:\s*(\d+\.?\d*)/g;
|
|
175
|
-
const booleans = /:\s*(true|false|null)/g;
|
|
176
|
-
|
|
177
|
-
let result = line;
|
|
178
|
-
result = result.replace(keys, `${C.cyan}$1${C.reset}:`);
|
|
179
|
-
result = result.replace(strings, `: ${C.green}$1${C.reset}`);
|
|
180
|
-
result = result.replace(numbers, `: ${C.yellow}$1${C.reset}`);
|
|
181
|
-
result = result.replace(booleans, `: ${C.magenta}$1${C.reset}`);
|
|
182
|
-
|
|
183
|
-
return result;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function highlightPython(line) {
|
|
187
|
-
const keywords = /\b(def|class|if|elif|else|for|while|return|import|from|as|try|except|finally|raise|with|yield|lambda|pass|break|continue|and|or|not|in|is|None|True|False|self|async|await|nonlocal|global)\b/g;
|
|
188
|
-
const strings = /("""[\s\S]*?"""|'''[\s\S]*?'''|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g;
|
|
189
|
-
const comments = /(#.*$)/;
|
|
190
|
-
const numbers = /\b(\d+\.?\d*)\b/g;
|
|
191
|
-
const decorators = /^(\s*@\w+)/;
|
|
192
|
-
|
|
193
|
-
let result = line;
|
|
194
|
-
result = result.replace(numbers, `${C.yellow}$1${C.reset}`);
|
|
195
|
-
result = result.replace(keywords, `${C.magenta}$1${C.reset}`);
|
|
196
|
-
result = result.replace(decorators, `${C.cyan}$1${C.reset}`);
|
|
197
|
-
result = result.replace(strings, `${C.green}$&${C.reset}`);
|
|
198
|
-
result = result.replace(comments, `${C.dim}$1${C.reset}`);
|
|
199
|
-
|
|
200
|
-
return result;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function highlightGo(line) {
|
|
204
|
-
const keywords = /\b(func|package|import|var|const|type|struct|interface|map|chan|go|defer|return|if|else|for|range|switch|case|default|break|continue|select|fallthrough|nil|true|false|make|new|len|cap|append|copy|delete|panic|recover)\b/g;
|
|
205
|
-
const types = /\b(string|int|int8|int16|int32|int64|uint|uint8|uint16|uint32|uint64|float32|float64|bool|byte|rune|error|any)\b/g;
|
|
206
|
-
const strings = /(["'`])(?:(?=(\\?))\2.)*?\1/g;
|
|
207
|
-
const comments = /(\/\/.*$)/;
|
|
208
|
-
const numbers = /\b(\d+\.?\d*)\b/g;
|
|
209
|
-
|
|
210
|
-
let result = line;
|
|
211
|
-
result = result.replace(numbers, `${C.yellow}$1${C.reset}`);
|
|
212
|
-
result = result.replace(types, `${C.cyan}$1${C.reset}`);
|
|
213
|
-
result = result.replace(keywords, `${C.magenta}$1${C.reset}`);
|
|
214
|
-
result = result.replace(strings, `${C.green}$&${C.reset}`);
|
|
215
|
-
result = result.replace(comments, `${C.dim}$1${C.reset}`);
|
|
216
|
-
|
|
217
|
-
return result;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function highlightRust(line) {
|
|
221
|
-
const keywords = /\b(fn|let|mut|const|struct|enum|impl|trait|pub|use|mod|crate|self|super|match|if|else|for|while|loop|return|break|continue|where|as|in|ref|move|async|await|unsafe|extern|type|static|dyn|macro_rules)\b/g;
|
|
222
|
-
const types = /\b(i8|i16|i32|i64|i128|u8|u16|u32|u64|u128|f32|f64|bool|char|str|String|Vec|Option|Result|Box|Rc|Arc|Self|Some|None|Ok|Err|true|false)\b/g;
|
|
223
|
-
const strings = /(["'])(?:(?=(\\?))\2.)*?\1/g;
|
|
224
|
-
const comments = /(\/\/.*$)/;
|
|
225
|
-
const numbers = /\b(\d+\.?\d*)\b/g;
|
|
226
|
-
const macros = /\b(\w+!)/g;
|
|
227
|
-
|
|
228
|
-
let result = line;
|
|
229
|
-
result = result.replace(numbers, `${C.yellow}$1${C.reset}`);
|
|
230
|
-
result = result.replace(types, `${C.cyan}$1${C.reset}`);
|
|
231
|
-
result = result.replace(keywords, `${C.magenta}$1${C.reset}`);
|
|
232
|
-
result = result.replace(macros, `${C.yellow}$1${C.reset}`);
|
|
233
|
-
result = result.replace(strings, `${C.green}$&${C.reset}`);
|
|
234
|
-
result = result.replace(comments, `${C.dim}$1${C.reset}`);
|
|
235
|
-
|
|
236
|
-
return result;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function highlightCSS(line) {
|
|
240
|
-
const properties = /^(\s*)([\w-]+)\s*:/;
|
|
241
|
-
const values = /:\s*([^;]+)/;
|
|
242
|
-
const selectors = /^(\s*[.#@][\w-]+)/;
|
|
243
|
-
const numbers = /\b(\d+\.?\d*(px|em|rem|%|vh|vw|s|ms|deg|fr)?)\b/g;
|
|
244
|
-
const comments = /(\/\*.*?\*\/|\/\/.*$)/;
|
|
245
|
-
const colors = /(#[0-9a-fA-F]{3,8})\b/g;
|
|
246
|
-
|
|
247
|
-
let result = line;
|
|
248
|
-
result = result.replace(colors, `${C.yellow}$1${C.reset}`);
|
|
249
|
-
result = result.replace(numbers, `${C.yellow}$1${C.reset}`);
|
|
250
|
-
result = result.replace(properties, `$1${C.cyan}$2${C.reset}:`);
|
|
251
|
-
result = result.replace(selectors, `$1${C.magenta}$&${C.reset}`);
|
|
252
|
-
result = result.replace(comments, `${C.dim}$1${C.reset}`);
|
|
253
|
-
|
|
254
|
-
return result;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function highlightHTML(line) {
|
|
258
|
-
const tags = /<\/?(\w[\w-]*)/g;
|
|
259
|
-
const attrs = /\s([\w-]+)=/g;
|
|
260
|
-
const strings = /(["'])(?:(?=(\\?))\2.)*?\1/g;
|
|
261
|
-
const comments = /(<!--.*?-->)/g;
|
|
262
|
-
const entities = /(&\w+;)/g;
|
|
263
|
-
|
|
264
|
-
let result = line;
|
|
265
|
-
result = result.replace(comments, `${C.dim}$1${C.reset}`);
|
|
266
|
-
result = result.replace(strings, `${C.green}$&${C.reset}`);
|
|
267
|
-
result = result.replace(tags, `<${C.magenta}$1${C.reset}`);
|
|
268
|
-
result = result.replace(attrs, ` ${C.cyan}$1${C.reset}=`);
|
|
269
|
-
result = result.replace(entities, `${C.yellow}$1${C.reset}`);
|
|
270
|
-
|
|
271
|
-
return result;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Render a table in the terminal
|
|
276
|
-
* @param {string[]} headers
|
|
277
|
-
* @param {string[][]} rows
|
|
278
|
-
* @returns {string}
|
|
279
|
-
*/
|
|
280
|
-
function renderTable(headers, rows) {
|
|
281
|
-
if (!headers || headers.length === 0) return '';
|
|
282
|
-
|
|
283
|
-
// Calculate column widths
|
|
284
|
-
const widths = headers.map((h, i) => {
|
|
285
|
-
const maxRow = rows.reduce((max, row) => Math.max(max, (row[i] || '').length), 0);
|
|
286
|
-
return Math.max(h.length, maxRow);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
const sep = widths.map((w) => '─'.repeat(w + 2)).join('┼');
|
|
290
|
-
const headerLine = headers.map((h, i) => ` ${C.bold}${h.padEnd(widths[i])}${C.reset} `).join('│');
|
|
291
|
-
|
|
292
|
-
const lines = [];
|
|
293
|
-
lines.push(`${C.dim}┌${sep.replace(/┼/g, '┬')}┐${C.reset}`);
|
|
294
|
-
lines.push(`${C.dim}│${C.reset}${headerLine}${C.dim}│${C.reset}`);
|
|
295
|
-
lines.push(`${C.dim}├${sep}┤${C.reset}`);
|
|
296
|
-
|
|
297
|
-
for (const row of rows) {
|
|
298
|
-
const rowLine = headers.map((_, i) => ` ${(row[i] || '').padEnd(widths[i])} `).join(`${C.dim}│${C.reset}`);
|
|
299
|
-
lines.push(`${C.dim}│${C.reset}${rowLine}${C.dim}│${C.reset}`);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
lines.push(`${C.dim}└${sep.replace(/┼/g, '┴')}┘${C.reset}`);
|
|
303
|
-
return lines.join('\n');
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Render a progress bar
|
|
308
|
-
* @param {string} label
|
|
309
|
-
* @param {number} current
|
|
310
|
-
* @param {number} total
|
|
311
|
-
* @param {number} width
|
|
312
|
-
* @returns {string}
|
|
313
|
-
*/
|
|
314
|
-
function renderProgress(label, current, total, width = 30) {
|
|
315
|
-
const pct = total > 0 ? Math.round((current / total) * 100) : 0;
|
|
316
|
-
const filled = Math.round((pct / 100) * width);
|
|
317
|
-
const empty = width - filled;
|
|
318
|
-
const color = pct >= 100 ? C.green : pct > 50 ? C.yellow : C.cyan;
|
|
319
|
-
|
|
320
|
-
return ` ${label} ${color}${'█'.repeat(filled)}${C.dim}${'░'.repeat(empty)}${C.reset} ${pct}% (${current}/${total})`;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* StreamRenderer — renders markdown line-by-line as tokens arrive.
|
|
325
|
-
* Buffers partial lines, flushes complete lines with rendering.
|
|
326
|
-
*/
|
|
327
|
-
class StreamRenderer {
|
|
328
|
-
constructor() {
|
|
329
|
-
this.buffer = '';
|
|
330
|
-
this.inCodeBlock = false;
|
|
331
|
-
this.codeBlockLang = '';
|
|
332
|
-
this.lineCount = 0;
|
|
333
|
-
// Streaming cursor state
|
|
334
|
-
this._cursorTimer = null;
|
|
335
|
-
this._cursorFrame = 0;
|
|
336
|
-
this._cursorActive = false;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/** Write to stdout, silently ignoring EPIPE errors after abort */
|
|
340
|
-
_safeWrite(data) {
|
|
341
|
-
try { process.stdout.write(data); } catch (e) { if (e.code !== 'EPIPE') throw e; }
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/** Write to stderr (same stream as Spinner) for cursor animation */
|
|
345
|
-
_cursorWrite(data) {
|
|
346
|
-
try { process.stderr.write(data); } catch (e) { if (e.code !== 'EPIPE') throw e; }
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
startCursor() {
|
|
350
|
-
this._cursorActive = true;
|
|
351
|
-
this._cursorFrame = 0;
|
|
352
|
-
this._cursorWrite('\x1b[?25l'); // hide terminal cursor
|
|
353
|
-
this._renderCursor();
|
|
354
|
-
this._cursorTimer = setInterval(() => this._renderCursor(), 80);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
_renderCursor() {
|
|
358
|
-
// Same braille spinner as Thinking... — seamless continuation on stderr
|
|
359
|
-
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
360
|
-
const f = frames[this._cursorFrame % frames.length];
|
|
361
|
-
this._cursorWrite(`\x1b[2K\r\x1b[36m${f}\x1b[0m`);
|
|
362
|
-
this._cursorFrame++;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
_clearCursorLine() {
|
|
366
|
-
if (this._cursorActive) {
|
|
367
|
-
this._cursorWrite('\x1b[2K\r');
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
stopCursor() {
|
|
372
|
-
if (this._cursorTimer) {
|
|
373
|
-
clearInterval(this._cursorTimer);
|
|
374
|
-
this._cursorTimer = null;
|
|
375
|
-
}
|
|
376
|
-
if (this._cursorActive) {
|
|
377
|
-
this._cursorWrite('\x1b[2K\r\x1b[?25h'); // clear line + show terminal cursor
|
|
378
|
-
this._cursorActive = false;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Push a token chunk into the stream renderer.
|
|
384
|
-
* Renders complete lines immediately; buffers partial lines.
|
|
385
|
-
*/
|
|
386
|
-
push(text) {
|
|
387
|
-
if (!text) return;
|
|
388
|
-
this._clearCursorLine();
|
|
389
|
-
this.buffer += text;
|
|
390
|
-
|
|
391
|
-
// Process all complete lines
|
|
392
|
-
let nlIdx;
|
|
393
|
-
while ((nlIdx = this.buffer.indexOf('\n')) !== -1) {
|
|
394
|
-
const line = this.buffer.substring(0, nlIdx);
|
|
395
|
-
this.buffer = this.buffer.substring(nlIdx + 1);
|
|
396
|
-
this._renderLine(line);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (this._cursorActive) {
|
|
400
|
-
this._renderCursor();
|
|
401
|
-
if (this._cursorTimer) clearInterval(this._cursorTimer);
|
|
402
|
-
this._cursorTimer = setInterval(() => this._renderCursor(), 120);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Flush remaining buffer content (call at end of stream).
|
|
408
|
-
*/
|
|
409
|
-
flush() {
|
|
410
|
-
this.stopCursor();
|
|
411
|
-
if (this.buffer) {
|
|
412
|
-
this._renderLine(this.buffer);
|
|
413
|
-
this.buffer = '';
|
|
414
|
-
}
|
|
415
|
-
// Reset state
|
|
416
|
-
if (this.inCodeBlock) {
|
|
417
|
-
this._safeWrite(`${C.dim}${'─'.repeat(40)}${C.reset}\n`);
|
|
418
|
-
this.inCodeBlock = false;
|
|
419
|
-
this.codeBlockLang = '';
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
_renderLine(line) {
|
|
424
|
-
// Code block toggle
|
|
425
|
-
if (line.trim().startsWith('```')) {
|
|
426
|
-
if (this.inCodeBlock) {
|
|
427
|
-
this._safeWrite(`${C.dim}${'─'.repeat(40)}${C.reset}\n`);
|
|
428
|
-
this.inCodeBlock = false;
|
|
429
|
-
this.codeBlockLang = '';
|
|
430
|
-
} else {
|
|
431
|
-
this.inCodeBlock = true;
|
|
432
|
-
this.codeBlockLang = line.trim().substring(3).trim();
|
|
433
|
-
const label = this.codeBlockLang ? ` ${this.codeBlockLang} ` : '';
|
|
434
|
-
this._safeWrite(`${C.dim}${'─'.repeat(3)}${label}${'─'.repeat(Math.max(0, 37 - label.length))}${C.reset}\n`);
|
|
435
|
-
}
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (this.inCodeBlock) {
|
|
440
|
-
this._safeWrite(` ${highlightCode(line, this.codeBlockLang)}\n`);
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Headers
|
|
445
|
-
if (line.startsWith('### ')) {
|
|
446
|
-
this._safeWrite(`${C.bold}${C.cyan} ${line.substring(4)}${C.reset}\n`);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
if (line.startsWith('## ')) {
|
|
450
|
-
this._safeWrite(`${C.bold}${C.cyan} ${line.substring(3)}${C.reset}\n`);
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
if (line.startsWith('# ')) {
|
|
454
|
-
this._safeWrite(`${C.bold}${C.cyan}${line.substring(2)}${C.reset}\n`);
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Lists
|
|
459
|
-
if (/^\s*[-*]\s/.test(line)) {
|
|
460
|
-
const indent = line.match(/^(\s*)/)[1];
|
|
461
|
-
const content = line.replace(/^\s*[-*]\s/, '');
|
|
462
|
-
this._safeWrite(`${indent}${C.cyan}•${C.reset} ${renderInline(content)}\n`);
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Numbered lists
|
|
467
|
-
if (/^\s*\d+\.\s/.test(line)) {
|
|
468
|
-
const match = line.match(/^(\s*)(\d+)\.\s(.*)/);
|
|
469
|
-
if (match) {
|
|
470
|
-
this._safeWrite(`${match[1]}${C.cyan}${match[2]}.${C.reset} ${renderInline(match[3])}\n`);
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Regular line
|
|
476
|
-
this._safeWrite(`${renderInline(line)}\n`);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
module.exports = {
|
|
481
|
-
renderMarkdown,
|
|
482
|
-
renderInline,
|
|
483
|
-
highlightCode,
|
|
484
|
-
highlightJS,
|
|
485
|
-
highlightBash,
|
|
486
|
-
highlightJSON,
|
|
487
|
-
highlightPython,
|
|
488
|
-
highlightGo,
|
|
489
|
-
highlightRust,
|
|
490
|
-
highlightCSS,
|
|
491
|
-
highlightHTML,
|
|
492
|
-
renderTable,
|
|
493
|
-
renderProgress,
|
|
494
|
-
StreamRenderer,
|
|
495
|
-
};
|