@semalt-ai/code 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.
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ const { getCols, getRows } = require('./utils');
4
+
5
+ class LayoutManager {
6
+ constructor() {
7
+ this.cols = getCols();
8
+ this._termRows = getRows();
9
+ this._rows = this._termRows;
10
+ this.historyStart = 1; // first row of the scroll / history region
11
+ this._contentLines = 0; // lines written into the history region so far
12
+ this.inputHeight = 1;
13
+ this._resizeCb = null;
14
+ this._heightChangeCb = null;
15
+ this._handler = () => {
16
+ this.cols = getCols();
17
+ this._termRows = getRows();
18
+ this._rows = this._termRows;
19
+ if (this._resizeCb) this._resizeCb();
20
+ };
21
+ process.stdout.on('resize', this._handler);
22
+ }
23
+
24
+ get rows() { return this._rows; }
25
+ set rows(n) {
26
+ const min = this.historyStart + 1 + this.inputHeight + 3;
27
+ this._rows = Math.max(min, Math.min(n, this._termRows));
28
+ }
29
+
30
+ get historyRows() { return Math.max(this.historyStart, this._rows - 3 - this.inputHeight); }
31
+ get statusRow() { return this._rows - this.inputHeight - 2; }
32
+ get separatorRow() { return this._rows - this.inputHeight - 1; }
33
+ get inputRow() { return this._rows - this.inputHeight; }
34
+ get hintsRow() { return this._rows; }
35
+
36
+ onResize(cb) { this._resizeCb = cb; }
37
+ onInputHeightChange(cb) { this._heightChangeCb = cb; }
38
+
39
+ setInputHeight(n) {
40
+ const h = Math.max(1, Math.min(5, n));
41
+ if (h === this.inputHeight) return;
42
+ this.inputHeight = h;
43
+ if (this._heightChangeCb) this._heightChangeCb();
44
+ }
45
+
46
+ moveTo(row, col) { process.stdout.write(`\x1b[${row};${col || 1}H`); }
47
+
48
+ destroy() {
49
+ process.stdout.removeListener('resize', this._handler);
50
+ }
51
+ }
52
+
53
+ module.exports = { LayoutManager };
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+
5
+ const { RST, THEME } = require('./ansi');
6
+ const { getCols } = require('./utils');
7
+
8
+ // ─── StatusBar ─────────────────────────────────────────────────────────────────
9
+ // Used by cmdCode and cmdEdit for non-TUI status display.
10
+ // Also holds the static StatusBar.current shim wired by createUI.
11
+
12
+ class StatusBar {
13
+ constructor() {
14
+ this._fields = { status: 'idle', model: '', tokens: '', ctx: '', latency: '', elapsed: '' };
15
+ this._animFrames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
16
+ this._animIdx = 0; this._animTimer = null;
17
+ this._onResize = () => this._handleResize();
18
+ if (process.stdout.isTTY) { process.stdout.on('resize', this._onResize); this._reserveBottom(); }
19
+ StatusBar.current = this;
20
+ }
21
+ _rows() { return process.stdout.rows || 24; }
22
+ _reserveBottom() { const rows = this._rows(); process.stdout.write(`\x1b7\x1b[1;${rows - 1}r\x1b8`); this.render(); }
23
+ _handleResize() { const rows = this._rows(); process.stdout.write(`\x1b7\x1b[1;${rows - 1}r\x1b8`); this.render(); }
24
+ _startAnim() { if (this._animTimer) return; this._animTimer = setInterval(() => { this._animIdx = (this._animIdx + 1) % this._animFrames.length; this.render(); }, 100); }
25
+ _stopAnim() { if (this._animTimer) { clearInterval(this._animTimer); this._animTimer = null; } this._animIdx = 0; }
26
+ render() {
27
+ if (!process.stdout.isTTY) return;
28
+ const { loadConfig } = require('../config');
29
+ const showTokens = loadConfig().show_token_count !== false;
30
+ const parts = [];
31
+ if (this._fields.status) {
32
+ if (this._fields.status === 'streaming' || this._fields.status === 'thinking') {
33
+ parts.push(`${THEME.tool}${this._animFrames[this._animIdx]}${THEME.reset} ${THEME.dim}${this._fields.status === 'thinking' ? 'Thinking…' : 'Streaming…'}${THEME.reset}`);
34
+ } else { parts.push(`${THEME.dim}${this._fields.status}${THEME.reset}`); }
35
+ }
36
+ if (this._fields.model) parts.push(`${THEME.sys}${this._fields.model}${THEME.reset}`);
37
+ if (showTokens && this._fields.tokens) parts.push(`${THEME.dim}${this._fields.tokens}${THEME.reset}`);
38
+ if (showTokens && this._fields.ctx) parts.push(this._fields.ctx);
39
+ if (this._fields.latency) parts.push(`${THEME.dim}${this._fields.latency}${THEME.reset}`);
40
+ if (this._fields.elapsed) parts.push(`${THEME.dim}${this._fields.elapsed}${THEME.reset}`);
41
+ const line = parts.join(` ${THEME.dim}│${THEME.reset} `);
42
+ const rows = this._rows();
43
+ process.stdout.write(`\x1b7\x1b[${rows};0H\x1b[K${line}\x1b8`);
44
+ }
45
+ update(fields) {
46
+ if (!process.stdout.isTTY) return;
47
+ Object.assign(this._fields, fields);
48
+ if (this._fields.status === 'streaming' || this._fields.status === 'thinking') this._startAnim();
49
+ else this._stopAnim();
50
+ this.render();
51
+ }
52
+ liveUpdate(fields) { if (!process.stdout.isTTY) return; Object.assign(this._fields, fields); }
53
+ destroy() {
54
+ if (!process.stdout.isTTY) return;
55
+ this._stopAnim();
56
+ process.stdout.removeListener('resize', this._onResize);
57
+ const rows = this._rows();
58
+ process.stdout.write(`\x1b7\x1b[${rows};0H\x1b[K\x1b[r\x1b8`);
59
+ if (StatusBar.current === this) StatusBar.current = null;
60
+ }
61
+ }
62
+ StatusBar.current = null;
63
+
64
+ // ─── interactiveSelect ────────────────────────────────────────────────────────
65
+
66
+ async function interactiveSelect(items, renderItem, options) {
67
+ const opts = options || {};
68
+ const { initialIndex = 0 } = opts;
69
+ return new Promise((resolve) => {
70
+ if (!process.stdout.isTTY || !process.stdin.isTTY) { resolve(null); return; }
71
+ let selectedIndex = Math.max(0, Math.min(initialIndex, items.length - 1));
72
+ let done = false;
73
+ const wasRaw = typeof process.stdin.isRaw === 'boolean' ? process.stdin.isRaw : false;
74
+ const render = () => {
75
+ readline.moveCursor(process.stdout, 0, -items.length);
76
+ for (let i = 0; i < items.length; i++) {
77
+ readline.cursorTo(process.stdout, 0); readline.clearLine(process.stdout, 0);
78
+ const line = renderItem(items[i], i === selectedIndex);
79
+ process.stdout.write(line + '\n');
80
+ }
81
+ };
82
+ for (let i = 0; i < items.length; i++) {
83
+ const line = renderItem(items[i], i === selectedIndex);
84
+ process.stdout.write(line + '\n');
85
+ }
86
+ process.stdin.setRawMode(true); process.stdin.resume();
87
+ const finish = (result) => {
88
+ if (done) return; done = true;
89
+ process.stdin.setRawMode(wasRaw); process.stdin.removeListener('data', onData); process.stdin.pause();
90
+ // Collapse: show only the selected item, clear the rest
91
+ const displayIdx = result !== null ? result : items.length - 1;
92
+ readline.moveCursor(process.stdout, 0, -items.length);
93
+ readline.cursorTo(process.stdout, 0);
94
+ process.stdout.write('\x1b[2K' + renderItem(items[displayIdx], true, true) + '\x1b[J\n');
95
+ resolve(result);
96
+ };
97
+ const onData = (chunk) => {
98
+ if (done) return;
99
+ const data = chunk.toString('utf8');
100
+ if (data[0] === '\x0f') { if (opts.onExpand) opts.onExpand(); return; }
101
+ if (data === '\x03' || data === '\x1b' || data === 'q') { finish(null); return; }
102
+ if (data === '\r' || data === '\n') { finish(selectedIndex); return; }
103
+ if (data === '\x1b[A' || data === 'k') { selectedIndex = Math.max(0, selectedIndex - 1); render(); return; }
104
+ if (data === '\x1b[B' || data === 'j') { selectedIndex = Math.min(items.length - 1, selectedIndex + 1); render(); return; }
105
+ };
106
+ process.stdin.on('data', onData);
107
+ });
108
+ }
109
+
110
+ // ─── SelectMenu ───────────────────────────────────────────────────────────────
111
+
112
+ class SelectMenu {
113
+ constructor(options) {
114
+ this.options = options;
115
+ this.selectedIdx = 0;
116
+ }
117
+
118
+ moveUp() { this.selectedIdx = Math.max(0, this.selectedIdx - 1); }
119
+ moveDown() { this.selectedIdx = Math.min(this.options.length - 1, this.selectedIdx + 1); }
120
+ current() { return this.options[this.selectedIdx]; }
121
+
122
+ render() {
123
+ return this.options.map((opt, i) => {
124
+ if (i === this.selectedIdx) return ` \x1b[7m › ${opt} \x1b[27m`;
125
+ return ` ${opt}`;
126
+ }).join('\n');
127
+ }
128
+ }
129
+
130
+ module.exports = { StatusBar, interactiveSelect, SelectMenu };
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ const { RST, DIM, FG_RED, SPINNER_DEFS } = require('./ansi');
4
+ const { stripAnsi } = require('./utils');
5
+
6
+ class FullStatusBar {
7
+ constructor(layout) {
8
+ this._layout = layout;
9
+ this._state = 'idle';
10
+ this._label = 'Ready';
11
+ this._model = '';
12
+ this._totalTokens = 0;
13
+ this._speed = 0;
14
+ this._streamStart = null;
15
+ this._streamTokens = 0;
16
+ this._tokensSinceUpdate = 0;
17
+ this._animIdx = 0;
18
+ this._animTimer = null;
19
+ this._paused = false;
20
+ this._clockTimer = setInterval(() => this._renderBar(), 1000);
21
+ }
22
+
23
+ pause() { this._paused = true; }
24
+ resume() { this._paused = false; this._renderBar(); }
25
+
26
+ setModel(name) {
27
+ this._model = name || '';
28
+ this._renderBar();
29
+ }
30
+
31
+ update(state, label) {
32
+ this._state = state || 'idle';
33
+ if (label !== undefined) this._label = label;
34
+
35
+ // An explicit update must always be visible — unpause so _renderBar() runs.
36
+ // The pause flag only suppresses timer-driven clock/spinner redraws.
37
+ this._paused = false;
38
+
39
+ if (state === 'streaming') {
40
+ if (!this._streamStart) { this._streamStart = Date.now(); this._streamTokens = 0; }
41
+ } else {
42
+ this._streamStart = null; this._streamTokens = 0; this._speed = 0;
43
+ }
44
+
45
+ const animStates = ['thinking', 'streaming', 'tool'];
46
+ if (animStates.includes(state) && !this._animTimer) {
47
+ this._animTimer = setInterval(() => { this._animIdx++; this._renderBar(); }, 100);
48
+ } else if (!animStates.includes(state) && this._animTimer) {
49
+ clearInterval(this._animTimer); this._animTimer = null; this._animIdx = 0;
50
+ }
51
+ if (state === 'idle') this.drawSeparator();
52
+ this._renderBar();
53
+ }
54
+
55
+ onToken() {
56
+ this._streamTokens++;
57
+ this._tokensSinceUpdate++;
58
+ if (this._tokensSinceUpdate >= 10) {
59
+ this._tokensSinceUpdate = 0;
60
+ if (this._streamStart) {
61
+ const elapsed = (Date.now() - this._streamStart) / 1000;
62
+ this._speed = elapsed > 0 ? Math.round(this._streamTokens / elapsed) : 0;
63
+ }
64
+ this._renderBar();
65
+ }
66
+ }
67
+
68
+ updateMetrics(data) {
69
+ if (data && data.totalTokens !== undefined) this._totalTokens = data.totalTokens;
70
+ this._renderBar();
71
+ }
72
+
73
+ liveUpdate(fields) {
74
+ if (fields && fields.tokens) {
75
+ const n = parseInt(String(fields.tokens), 10);
76
+ if (!isNaN(n)) this._totalTokens = n;
77
+ }
78
+ this._renderBar();
79
+ }
80
+
81
+ _renderBar() {
82
+ if (this._paused) return;
83
+ const layout = this._layout;
84
+ const cols = layout.cols;
85
+ const row = layout.statusRow;
86
+ let left = '';
87
+ const state = this._state;
88
+
89
+ if (state === 'idle') {
90
+ left = '';
91
+ } else if (state === 'waiting') {
92
+ left = `${DIM}● Waiting for input${RST}`;
93
+ } else if (state === 'error') {
94
+ left = `${FG_RED}✕ ${this._label}${RST}`;
95
+ } else {
96
+ const sp = SPINNER_DEFS[state] || SPINNER_DEFS.thinking;
97
+ const frame = sp.frames[this._animIdx % sp.frames.length];
98
+ left = `${sp.color}${frame}${RST} ${this._label}`;
99
+ }
100
+
101
+ const now = new Date();
102
+ const timePart = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}`;
103
+ const rightParts = [timePart];
104
+ if (this._model) rightParts.push(this._model);
105
+ if (this._totalTokens > 0) rightParts.push(`${this._totalTokens.toLocaleString()} tok`);
106
+ if (state === 'streaming' && this._speed > 0) rightParts.push(`${this._speed} t/s`);
107
+
108
+ const rightVisible = rightParts.join(' · ');
109
+ const rightAnsi = `${DIM}${rightParts.join(` ${DIM}·${RST}${DIM} `)}${RST}`;
110
+ const leftLen = stripAnsi(left).length;
111
+ const rightLen = rightVisible.length;
112
+ const padding = Math.max(1, cols - leftLen - rightLen);
113
+ const line = left + ' '.repeat(padding) + rightAnsi;
114
+
115
+ process.stdout.write(`\x1b7\x1b[${row};1H\x1b[2K${line}\x1b8`);
116
+ }
117
+
118
+ drawSeparator() {
119
+ const layout = this._layout;
120
+ const { FG_DARK } = require('./ansi');
121
+ process.stdout.write(`\x1b7\x1b[${layout.separatorRow};1H\x1b[2K${FG_DARK}${'─'.repeat(layout.cols)}${RST}\x1b8`);
122
+ }
123
+
124
+ destroy() {
125
+ if (this._animTimer) { clearInterval(this._animTimer); this._animTimer = null; }
126
+ if (this._clockTimer) { clearInterval(this._clockTimer); this._clockTimer = null; }
127
+ }
128
+ }
129
+
130
+ module.exports = { FullStatusBar };
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+
5
+ const { RST, DIM, FG_DARK, FG_CODE_BG, FG_CODE_BORDER, FG_CODE_LANG, FG_TAG, FG_FILEPATH, KEYWORDS } = require('./ansi');
6
+ const { renderMarkdown, _mdInline } = require('./diff');
7
+
8
+ const THINK_OPEN = '<think>', THINK_CLOSE = '</think>';
9
+
10
+ class StreamRenderer {
11
+ constructor(options) {
12
+ const opts = options || {};
13
+ this.buffer = ''; this.inCodeBlock = false; this.codeLang = '';
14
+ this.inThinkBlock = false; this._thinkDotCount = 0;
15
+ this._showThink = opts.showThink || false;
16
+ this.inToolCall = null;
17
+ this._firstLinePrefix = opts.firstLinePrefix || null;
18
+ this._firstLineDone = false; this._firstToken = false;
19
+ this._streamedText = ''; this._hasToolCall = false; this._linesWritten = 0;
20
+ }
21
+
22
+ _write(str) {
23
+ for (let i = 0; i < str.length; i++) if (str[i] === '\n') this._linesWritten++;
24
+ process.stdout.write(str);
25
+ }
26
+
27
+ feed(chunk) {
28
+ if (!this._firstToken && chunk) {
29
+ this._firstToken = true;
30
+ const { StatusBar } = require('./legacy');
31
+ if (StatusBar.current) StatusBar.current.update({ status: 'streaming' });
32
+ }
33
+ this._streamedText += chunk;
34
+ this.buffer += chunk;
35
+ while (this.buffer.includes('\n')) {
36
+ const idx = this.buffer.indexOf('\n');
37
+ const line = this.buffer.slice(0, idx);
38
+ this.buffer = this.buffer.slice(idx + 1);
39
+ this._renderLine(line);
40
+ }
41
+ }
42
+
43
+ flush() {
44
+ if (this.buffer) { this._renderLine(this.buffer); this.buffer = ''; }
45
+ if (this.inThinkBlock) { if (this._showThink) this._write(`${FG_DARK}⟨/thinking⟩${RST}\n`); this.inThinkBlock = false; }
46
+ if (this.inToolCall) { this._renderToolCall(this.inToolCall); this.inToolCall = null; }
47
+ if (this.inCodeBlock) { this._write(` ${FG_CODE_BORDER}╰${'─'.repeat(50)}${RST}\n`); this.inCodeBlock = false; }
48
+ const { StatusBar } = require('./legacy');
49
+ if (StatusBar.current) StatusBar.current.update({ status: 'idle' });
50
+ if (process.stdout.isTTY && !this._hasToolCall && !this._showThink && this._streamedText.trim()) {
51
+ let prose = this._streamedText;
52
+ let ts = prose.indexOf(THINK_OPEN);
53
+ while (ts !== -1) { const te = prose.indexOf(THINK_CLOSE, ts); if (te !== -1) { prose = prose.slice(0, ts) + prose.slice(te + THINK_CLOSE.length); ts = prose.indexOf(THINK_OPEN); } else { prose = prose.slice(0, ts); break; } }
54
+ prose = prose.split(THINK_CLOSE).join('');
55
+ if (prose.trim()) {
56
+ if (this._linesWritten > 0) { readline.moveCursor(process.stdout, 0, -this._linesWritten); readline.cursorTo(process.stdout, 0); process.stdout.write('\x1b[0J'); }
57
+ renderMarkdown(prose);
58
+ }
59
+ }
60
+ }
61
+
62
+ _emitFirstPrefix() { if (this._firstLinePrefix && !this._firstLineDone) { this._firstLineDone = true; return this._firstLinePrefix; } return null; }
63
+
64
+ _renderLine(line) {
65
+ if (line.trim() === '') return;
66
+ if (this.inThinkBlock) {
67
+ const closeIdx = line.indexOf(THINK_CLOSE);
68
+ if (closeIdx !== -1) {
69
+ const before = line.slice(0, closeIdx), afterClose = line.slice(closeIdx + THINK_CLOSE.length);
70
+ if (this._showThink && before.trim()) this._write(` ${DIM}${FG_DARK}◆ ${before}${RST}\n`);
71
+ this.inThinkBlock = false;
72
+ if (this._showThink) this._write(`${FG_DARK}⟨/thinking⟩${RST}\n`);
73
+ if (afterClose.trim()) this._renderLine(afterClose);
74
+ } else { if (this._showThink && line.trim()) this._write(` ${DIM}${FG_DARK}◆ ${line}${RST}\n`); }
75
+ return;
76
+ }
77
+ const thinkOpenIdx = line.indexOf(THINK_OPEN);
78
+ if (thinkOpenIdx !== -1) {
79
+ const before = line.slice(0, thinkOpenIdx);
80
+ if (before.trim()) this._renderLine(before);
81
+ const after = line.slice(thinkOpenIdx + THINK_OPEN.length);
82
+ const closeIdx = after.indexOf(THINK_CLOSE);
83
+ if (this._showThink) this._write(`\n ${FG_DARK}${DIM}⟨thinking⟩${RST}`);
84
+ if (closeIdx !== -1) {
85
+ const content = after.slice(0, closeIdx);
86
+ if (this._showThink) { if (content.trim()) this._write(` ${DIM}${FG_DARK}${content}${RST}`); this._write(`${FG_DARK}⟨/thinking⟩${RST}\n`); }
87
+ const afterClose = after.slice(closeIdx + THINK_CLOSE.length);
88
+ if (afterClose.trim()) this._renderLine(afterClose);
89
+ } else { this.inThinkBlock = true; this._thinkDotCount = 0; if (this._showThink && after.trim()) this._write(`\n ${DIM}${FG_DARK}◆ ${after}${RST}`); }
90
+ return;
91
+ }
92
+ if (line.trim() === THINK_CLOSE) return;
93
+ if (/^<\/(exec|shell|run_command|run|read_file|write_file|append_file|echo|list_dir|search_files|delete_file|make_dir|remove_dir|get_env|download|move_file|copy_file|set_env|edit_file|search_in_file|replace_in_file|upload)>/.test(line.trim())) return;
94
+ if (this.inToolCall) {
95
+ const closeTag = `</${this.inToolCall.name}>`;
96
+ const closeIdx = line.indexOf(closeTag);
97
+ if (closeIdx !== -1) { const before = line.slice(0, closeIdx); if (before.trim()) this.inToolCall.contentLines.push(before); this._renderToolCall(this.inToolCall); this.inToolCall = null; }
98
+ else { this.inToolCall.contentLines.push(line); }
99
+ return;
100
+ }
101
+ const wm = line.match(/^<write_file\s+path="([^"]+)">([\s\S]*)/);
102
+ if (wm) {
103
+ const pfx = this._emitFirstPrefix(); if (pfx) this._write(` ${pfx}\n`);
104
+ const filePath = wm[1], rest = wm[2], closeIdx = rest.indexOf('</write_file>');
105
+ if (closeIdx !== -1) this._renderToolCall({ name: 'write_file', filePath, contentLines: rest.slice(0, closeIdx).split('\n') });
106
+ else { this.inToolCall = { name: 'write_file', filePath, contentLines: rest ? [rest] : [] }; this._hasToolCall = true; }
107
+ return;
108
+ }
109
+ const tm = line.match(/^<(exec|shell|run_command|run|read_file|append_file|echo|list_dir|search_files|delete_file|make_dir|remove_dir|get_env|download|move_file|copy_file|set_env|edit_file|search_in_file|replace_in_file|upload)>([\s\S]*)/);
110
+ if (tm) {
111
+ const pfx = this._emitFirstPrefix(); if (pfx) this._write(` ${pfx}\n`);
112
+ const tagName = tm[1], rest = tm[2], closeTag = `</${tagName}>`, closeIdx = rest.indexOf(closeTag);
113
+ if (closeIdx !== -1) this._renderToolCall({ name: tagName, contentLines: rest.slice(0, closeIdx) ? [rest.slice(0, closeIdx)] : [] });
114
+ else { this.inToolCall = { name: tagName, contentLines: rest ? [rest] : [] }; this._hasToolCall = true; }
115
+ return;
116
+ }
117
+ if (!this.inCodeBlock && line.length >= 3 && line[0] === '`' && line[1] === '`' && line[2] === '`') {
118
+ const pfx = this._emitFirstPrefix(); if (pfx) this._write(` ${pfx}\n`);
119
+ this.codeLang = line.slice(3).trim();
120
+ const label = this.codeLang ? ` ${this.codeLang} ` : '';
121
+ const barLen = Math.max(1, 30 - label.length);
122
+ this._write(` ${FG_CODE_BORDER}╭${'─'.repeat(20)}${RST}${FG_CODE_LANG}${label}${RST}${FG_CODE_BORDER}${'─'.repeat(barLen)}${RST}\n`);
123
+ this.inCodeBlock = true; return;
124
+ }
125
+ if (this.inCodeBlock) {
126
+ if (line.trim() === '```') { this._write(` ${FG_CODE_BORDER}╰${'─'.repeat(50)}${RST}\n`); this.inCodeBlock = false; }
127
+ else { this._write(` ${FG_CODE_BORDER}│${RST} ${FG_CODE_BG}${this._colorizeCode(line)}${RST}\n`); }
128
+ return;
129
+ }
130
+ const pfx = this._emitFirstPrefix(); if (pfx) this._write(` ${pfx}`);
131
+ this._write(` ${_mdInline(line)}\n`);
132
+ }
133
+
134
+ _renderToolCall(call) {
135
+ this._hasToolCall = true;
136
+ const name = call.name, lines = call.contentLines || [], content = lines.join('\n').trim();
137
+ if (name === 'write_file') { process.stdout.write(` ${FG_TAG}◆ write_file${RST} ${FG_FILEPATH}${call.filePath || ''}${RST} ${FG_DARK}(${lines.length} lines)${RST}\n`); return; }
138
+ if (name === 'read_file') { process.stdout.write(` ${FG_TAG}◆ read_file${RST} ${FG_FILEPATH}${content}${RST}\n`); return; }
139
+ if (name === 'exec' || name === 'shell' || name === 'run_command' || name === 'run') { process.stdout.write(` ${FG_TAG}◆ exec${RST} ${FG_DARK}${content}${RST}\n`); return; }
140
+ const preview = content.length > 80 ? content.slice(0, 77) + '...' : content;
141
+ process.stdout.write(` ${FG_TAG}◆ ${name}${RST} ${FG_DARK}${preview}${RST}\n`);
142
+ }
143
+
144
+ _colorizeCode(line) {
145
+ const C_KW = '\x1b[38;5;176m', C_STR = '\x1b[38;5;114m', C_CMT = '\x1b[38;5;242m', C_NUM = '\x1b[38;5;215m', C_RST = `${RST}${FG_CODE_BG}`;
146
+ let result = '', i = 0;
147
+ while (i < line.length) {
148
+ if (line[i] === '#' || (line[i] === '/' && line[i+1] === '/')) { result += `${C_CMT}${line.slice(i)}${C_RST}`; break; }
149
+ if (line[i] === '"' || line[i] === "'") { const quote = line[i]; let j = i+1; while (j < line.length && line[j] !== quote) { if (line[j] === '\\') j++; j++; } j = Math.min(j+1, line.length); result += `${C_STR}${line.slice(i,j)}${C_RST}`; i = j; continue; }
150
+ if (/[a-zA-Z_]/.test(line[i])) { let j = i; while (j < line.length && /\w/.test(line[j])) j++; const word = line.slice(i,j); result += KEYWORDS.has(word) ? `${C_KW}${word}${C_RST}` : word; i = j; continue; }
151
+ if (/\d/.test(line[i])) { let j = i; while (j < line.length && /[\d.]/.test(line[j])) j++; result += `${C_NUM}${line.slice(i,j)}${C_RST}`; i = j; continue; }
152
+ result += line[i++];
153
+ }
154
+ return result;
155
+ }
156
+ }
157
+
158
+ module.exports = { StreamRenderer };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ function getCols() { return process.stdout.columns || 80; }
4
+ function getRows() { return process.stdout.rows || 24; }
5
+
6
+ function stripAnsi(str) {
7
+ return str.replace(/\x1b\[[^m]*m/g, '');
8
+ }
9
+
10
+ function hr(char, color) {
11
+ const { FG_DARK, RST } = require('./ansi');
12
+ const c = char || '─';
13
+ const col = color || FG_DARK;
14
+ process.stdout.write(`${col}${c.repeat(getCols())}${RST}\n`);
15
+ }
16
+
17
+ function boxLine(text, width) {
18
+ const { FG_DARK, RST, BOX_V } = require('./ansi');
19
+ const w = width || (getCols() - 4);
20
+ const visible = stripAnsi(text).length;
21
+ const pad = Math.max(0, w - visible);
22
+ return ` ${FG_DARK}${BOX_V}${RST} ${text}${' '.repeat(pad)}${FG_DARK}${BOX_V}${RST}`;
23
+ }
24
+
25
+ function insertCharAt(text, index, value) {
26
+ const chars = Array.from(text || '');
27
+ chars.splice(index, 0, value);
28
+ return chars.join('');
29
+ }
30
+
31
+ function removeCharAt(text, index) {
32
+ const chars = Array.from(text || '');
33
+ chars.splice(index, 1);
34
+ return chars.join('');
35
+ }
36
+
37
+ function isPrintableKey(str, key) {
38
+ const k = key || {};
39
+ if (!str || k.ctrl || k.meta) return false;
40
+ if (k.name === 'return' || k.name === 'enter' || k.name === 'tab') return false;
41
+ if (k.name && ['up','down','left','right','home','end','pageup','pagedown','escape','delete','backspace'].includes(k.name)) return false;
42
+ return !/[\x00-\x1f\x7f]/.test(str);
43
+ }
44
+
45
+ module.exports = { getCols, getRows, stripAnsi, hr, boxLine, insertCharAt, removeCharAt, isPrintableKey };