@semalt-ai/code 1.7.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/.claude/settings.local.json +8 -0
- package/ARCHITECTURE.md +99 -0
- package/CLAUDE.md +349 -0
- package/index.js +69 -7
- package/lib/agent.js +508 -39
- package/lib/api.js +225 -79
- package/lib/args.js +31 -0
- package/lib/audit.js +31 -0
- package/lib/commands.js +959 -307
- package/lib/config.js +51 -5
- package/lib/constants.js +56 -0
- package/lib/context.js +2 -6
- package/lib/metrics.js +94 -0
- package/lib/permissions.js +180 -49
- package/lib/prompts.js +89 -13
- package/lib/storage.js +96 -0
- package/lib/tools.js +896 -35
- package/lib/ui/ansi.js +64 -0
- package/lib/ui/chat-history.js +217 -0
- package/lib/ui/create-ui.js +474 -0
- package/lib/ui/diff.js +243 -0
- package/lib/ui/input-field.js +1176 -0
- package/lib/ui/layout.js +53 -0
- package/lib/ui/legacy.js +130 -0
- package/lib/ui/status-bar.js +130 -0
- package/lib/ui/stream.js +158 -0
- package/lib/ui/utils.js +45 -0
- package/lib/ui.js +42 -598
- package/package.json +1 -1
- package/path +1 -0
package/lib/ui/layout.js
ADDED
|
@@ -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 };
|
package/lib/ui/legacy.js
ADDED
|
@@ -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 };
|
package/lib/ui/stream.js
ADDED
|
@@ -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 };
|
package/lib/ui/utils.js
ADDED
|
@@ -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 };
|