winter-super-cli 2026.6.7 → 2026.6.8
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/package.json +1 -1
- package/src/cli/diff-view.js +88 -32
- package/src/cli/input-controller.js +39 -17
- package/src/cli/markdown-format.js +12 -7
- package/src/cli/repl.js +112 -36
- package/src/cli/terminal-manager.js +74 -0
- package/src/cli/terminal-ui.js +2 -3
- package/src/cli/tui.js +16 -2
package/package.json
CHANGED
package/src/cli/diff-view.js
CHANGED
|
@@ -8,9 +8,16 @@ import { promises as fs } from 'fs';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import readline from 'readline';
|
|
10
10
|
import { spawn } from 'child_process';
|
|
11
|
+
import { highlight } from 'cli-highlight';
|
|
11
12
|
import { renderBox, terminalWidth, stripAnsi, wrapText, visibleWidth } from './terminal-ui.js';
|
|
12
13
|
import { colors } from './snowflake-logo.js';
|
|
13
14
|
|
|
15
|
+
// Setup background colors if not defined in snowflake-logo
|
|
16
|
+
const bgRed = '\x1b[41m';
|
|
17
|
+
const bgGreen = '\x1b[42m';
|
|
18
|
+
const bgDarkRed = '\x1b[48;5;52m';
|
|
19
|
+
const bgDarkGreen = '\x1b[48;5;22m';
|
|
20
|
+
|
|
14
21
|
export class DiffView {
|
|
15
22
|
constructor(options = {}) {
|
|
16
23
|
this.projectPath = options.projectPath || process.cwd();
|
|
@@ -124,42 +131,91 @@ export class DiffView {
|
|
|
124
131
|
// ── Private Methods ─────────────────────────────────
|
|
125
132
|
|
|
126
133
|
_renderDiff(title, diff, width) {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
const innerWidth = Math.max(40, width - 6);
|
|
135
|
+
const header = `${colors.bright} ${title} ${colors.reset} ${colors.dim}— ${diff.additions} additions, ${diff.removals} deletions${colors.reset}`;
|
|
136
|
+
|
|
137
|
+
console.log(`\n${colors.magenta}┌${'─'.repeat(width - 2)}┐${colors.reset}`);
|
|
138
|
+
console.log(`${colors.magenta}│${colors.reset} ${header}${''.padEnd(Math.max(0, width - 4 - stripAnsi(header).length))}${colors.magenta}│${colors.reset}`);
|
|
139
|
+
console.log(`${colors.magenta}├${'─'.repeat(width - 2)}┤${colors.reset}`);
|
|
131
140
|
|
|
132
|
-
|
|
141
|
+
const maxLines = 40;
|
|
142
|
+
let printed = 0;
|
|
143
|
+
let lineNum = 0;
|
|
144
|
+
const contextLines = 2; // lines of context around changes
|
|
133
145
|
|
|
146
|
+
// Build display entries with context
|
|
147
|
+
const entries = [];
|
|
134
148
|
for (const part of diff.raw) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
const lines = part.value.replace(/\n$/, '').split('\n');
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
lineNum++;
|
|
152
|
+
if (part.added) {
|
|
153
|
+
entries.push({ type: 'add', num: lineNum, text: line });
|
|
154
|
+
} else if (part.removed) {
|
|
155
|
+
entries.push({ type: 'del', num: lineNum, text: line });
|
|
156
|
+
} else {
|
|
157
|
+
entries.push({ type: 'ctx', num: lineNum, text: line });
|
|
158
|
+
}
|
|
138
159
|
}
|
|
160
|
+
}
|
|
139
161
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
162
|
+
// Find which context lines to show (near changes)
|
|
163
|
+
const changeIndices = new Set();
|
|
164
|
+
entries.forEach((e, i) => {
|
|
165
|
+
if (e.type !== 'ctx') {
|
|
166
|
+
for (let j = Math.max(0, i - contextLines); j <= Math.min(entries.length - 1, i + contextLines); j++) {
|
|
167
|
+
changeIndices.add(j);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
143
171
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
172
|
+
let lastPrinted = -1;
|
|
173
|
+
for (let i = 0; i < entries.length && printed < maxLines; i++) {
|
|
174
|
+
const e = entries[i];
|
|
175
|
+
if (e.type === 'ctx' && !changeIndices.has(i)) continue;
|
|
176
|
+
|
|
177
|
+
// Show separator if there's a gap
|
|
178
|
+
if (lastPrinted >= 0 && i - lastPrinted > 1) {
|
|
179
|
+
console.log(`${colors.magenta}│${colors.reset} ${colors.dim}${'·'.repeat(Math.min(20, innerWidth))}${colors.reset}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const numStr = String(e.num).padStart(4);
|
|
183
|
+
const maxText = Math.max(10, innerWidth - 8);
|
|
184
|
+
const truncated = e.text.length > maxText ? e.text.slice(0, maxText - 3) + '...' : e.text;
|
|
185
|
+
|
|
186
|
+
// Detect language from file extension for highlight
|
|
187
|
+
const ext = path.extname(title).slice(1) || 'javascript';
|
|
188
|
+
const syntaxHighlight = (text) => {
|
|
189
|
+
try {
|
|
190
|
+
return highlight(text, { language: ext, ignoreIllegals: true });
|
|
191
|
+
} catch (e) {
|
|
192
|
+
return text;
|
|
152
193
|
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
if (e.type === 'add') {
|
|
197
|
+
const lineContent = syntaxHighlight(truncated);
|
|
198
|
+
console.log(`${colors.magenta}│${colors.reset} ${bgDarkGreen}${colors.white}${numStr} + ${lineContent}${' '.repeat(Math.max(0, innerWidth - stripAnsi(truncated).length - 8))}${colors.reset}`);
|
|
199
|
+
} else if (e.type === 'del') {
|
|
200
|
+
const lineContent = syntaxHighlight(truncated);
|
|
201
|
+
console.log(`${colors.magenta}│${colors.reset} ${bgDarkRed}${colors.white}${numStr} - ${lineContent}${' '.repeat(Math.max(0, innerWidth - stripAnsi(truncated).length - 8))}${colors.reset}`);
|
|
202
|
+
} else {
|
|
203
|
+
const lineContent = syntaxHighlight(truncated);
|
|
204
|
+
console.log(`${colors.magenta}│${colors.reset} ${colors.dim}${numStr} ${colors.reset}${lineContent}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
printed++;
|
|
208
|
+
lastPrinted = i;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (printed >= maxLines && entries.length > maxLines) {
|
|
212
|
+
const remaining = entries.filter(e => e.type !== 'ctx').length - printed;
|
|
213
|
+
if (remaining > 0) {
|
|
214
|
+
console.log(`${colors.magenta}│${colors.reset} ${colors.dim} ... and ${remaining} more changes${colors.reset}`);
|
|
153
215
|
}
|
|
154
216
|
}
|
|
155
217
|
|
|
156
|
-
console.log(
|
|
157
|
-
title: ` ${title} `,
|
|
158
|
-
width,
|
|
159
|
-
borderColor: colors.magenta,
|
|
160
|
-
titleColor: colors.bright,
|
|
161
|
-
body,
|
|
162
|
-
})}\n`);
|
|
218
|
+
console.log(`${colors.magenta}└${'─'.repeat(width - 2)}┘${colors.reset}\n`);
|
|
163
219
|
}
|
|
164
220
|
|
|
165
221
|
async _promptChoice() {
|
|
@@ -236,12 +292,12 @@ export class DiffView {
|
|
|
236
292
|
};
|
|
237
293
|
|
|
238
294
|
rl.question(
|
|
239
|
-
`\n${colors.cyan}Options:${colors.reset}\n` +
|
|
240
|
-
` ${colors.green}[a]${colors.reset} —
|
|
241
|
-
` ${colors.red}[r]${colors.reset} —
|
|
242
|
-
` ${colors.yellow}[m]${colors.reset} —
|
|
243
|
-
` ${colors.dim}[s]${colors.reset} — Skip\n` +
|
|
244
|
-
`${colors.yellow}Choose [a/r/m/s]: ${colors.reset}`,
|
|
295
|
+
`\n${colors.cyan}Edit Options:${colors.reset}\n` +
|
|
296
|
+
` ${colors.green}[a]${colors.reset} Accept — Apply the complete diff\n` +
|
|
297
|
+
` ${colors.red}[r]${colors.reset} Reject — Discard these changes\n` +
|
|
298
|
+
` ${colors.yellow}[m]${colors.reset} Manual — Open file in $EDITOR to manually resolve\n` +
|
|
299
|
+
` ${colors.dim}[s]${colors.reset} Skip — Skip for now\n` +
|
|
300
|
+
`${colors.yellow}👉 Choose [a/r/m/s]: ${colors.reset}`,
|
|
245
301
|
onAnswer
|
|
246
302
|
);
|
|
247
303
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import readline from 'readline';
|
|
2
2
|
import { colors } from './snowflake-logo.js';
|
|
3
|
-
import {
|
|
3
|
+
import { padVisible, renderBox, terminalWidth } from './terminal-ui.js';
|
|
4
4
|
import { buildTuiSnapshot, renderInputPanel } from './tui.js';
|
|
5
|
+
import { terminalManager } from './terminal-manager.js';
|
|
5
6
|
|
|
6
7
|
export class WinterInputController {
|
|
7
8
|
constructor(repl) {
|
|
@@ -20,17 +21,48 @@ export class WinterInputController {
|
|
|
20
21
|
: '';
|
|
21
22
|
|
|
22
23
|
const lines = [panel.top + queueTag, panel.status, panel.hint].filter(l => l && l.trim() !== '');
|
|
23
|
-
process.stdout.write('\n' + lines.join('\n') + '\n');
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const redrawFn = () => {
|
|
26
|
+
// Don't redraw if it shouldn't be visible anymore
|
|
27
|
+
if (!terminalManager.isPromptVisible) return;
|
|
28
|
+
process.stdout.write('\n' + lines.join('\n') + '\n');
|
|
29
|
+
if (typeof repl.rl?.setPrompt === 'function') {
|
|
30
|
+
repl.rl.setPrompt(panel.prompt);
|
|
31
|
+
}
|
|
32
|
+
if (repl.slashMenu?.open) {
|
|
33
|
+
this.renderSlashMenu();
|
|
34
|
+
} else {
|
|
35
|
+
if (repl.running && !repl.readlineClosed) {
|
|
36
|
+
repl.rl?.prompt?.(true);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const getLinesCountFn = () => {
|
|
42
|
+
let count = lines.length + 2; // empty line + lines + prompt line
|
|
43
|
+
if (repl.slashMenu?.open && repl.slashMenu?.printedLines) {
|
|
44
|
+
count += repl.slashMenu.printedLines;
|
|
45
|
+
}
|
|
46
|
+
return count;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const onHideFn = () => {
|
|
50
|
+
if (repl.slashMenu) {
|
|
51
|
+
repl.slashMenu.printedLines = 0;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
terminalManager.setPromptState(true, getLinesCountFn, redrawFn, onHideFn);
|
|
56
|
+
redrawFn();
|
|
29
57
|
}
|
|
30
58
|
|
|
31
59
|
closeInputBox() {
|
|
32
60
|
const repl = this.repl;
|
|
33
61
|
if (!repl.running || repl.readlineClosed) return;
|
|
62
|
+
|
|
63
|
+
terminalManager.hidePrompt();
|
|
64
|
+
terminalManager.setPromptState(false);
|
|
65
|
+
|
|
34
66
|
const panel = this.buildInputPanel();
|
|
35
67
|
process.stdout.write(`${panel.bottom}\n`);
|
|
36
68
|
}
|
|
@@ -174,6 +206,7 @@ export class WinterInputController {
|
|
|
174
206
|
|
|
175
207
|
readline.moveCursor(process.stdout, 0, -printedLines);
|
|
176
208
|
readline.clearScreenDown(process.stdout);
|
|
209
|
+
repl.slashMenu.printedLines = 0;
|
|
177
210
|
}
|
|
178
211
|
|
|
179
212
|
handleSlashMenuKey(key = {}) {
|
|
@@ -247,23 +280,12 @@ export class WinterInputController {
|
|
|
247
280
|
|
|
248
281
|
this.clearSlashMenuRender();
|
|
249
282
|
|
|
250
|
-
const ASCII_BOX = {
|
|
251
|
-
topLeft: '+',
|
|
252
|
-
topRight: '+',
|
|
253
|
-
bottomLeft: '+',
|
|
254
|
-
bottomRight: '+',
|
|
255
|
-
horizontal: '-',
|
|
256
|
-
vertical: '|',
|
|
257
|
-
teeLeft: '+',
|
|
258
|
-
teeRight: '+',
|
|
259
|
-
};
|
|
260
283
|
const rendered = renderBox({
|
|
261
284
|
title: 'Command Palette',
|
|
262
285
|
width: terminalWidth(66, 110, 88),
|
|
263
286
|
borderColor: colors.magenta,
|
|
264
287
|
titleColor: colors.cyan,
|
|
265
288
|
body,
|
|
266
|
-
boxChars: ASCII_BOX,
|
|
267
289
|
});
|
|
268
290
|
|
|
269
291
|
process.stdout.write(`\n${rendered}\n`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { highlight } from 'cli-highlight';
|
|
2
2
|
|
|
3
3
|
import { colors } from './snowflake-logo.js';
|
|
4
|
-
import { renderBox, terminalWidth, visibleWidth, wrapText, padVisible } from './terminal-ui.js';
|
|
4
|
+
import { renderBox, terminalWidth, visibleWidth, wrapText, padVisible, getBoxChars } from './terminal-ui.js';
|
|
5
5
|
|
|
6
6
|
export function formatMarkdown(text) {
|
|
7
7
|
if (!text) return '';
|
|
@@ -98,19 +98,24 @@ function renderMarkdownTableBlock(tableLines) {
|
|
|
98
98
|
const columnCount = Math.max(...rows.map(row => row.length), 0);
|
|
99
99
|
if (columnCount === 0) return tableLines.join('\n');
|
|
100
100
|
|
|
101
|
-
const boxWidth = Math.max(60, Math.min(terminalWidth(60, 100, 84), 100));
|
|
102
|
-
const innerWidth = boxWidth - 4;
|
|
103
|
-
const separatorWidth = (columnCount - 1) * 3;
|
|
104
|
-
const availableWidth = Math.max(columnCount * 8, innerWidth - separatorWidth);
|
|
105
|
-
|
|
106
101
|
const widestCells = Array.from({ length: columnCount }, (_, columnIndex) => {
|
|
107
102
|
return Math.max(8, ...rows.map(row => visibleWidth(row[columnIndex] || '')));
|
|
108
103
|
});
|
|
109
104
|
|
|
105
|
+
const separatorWidth = (columnCount - 1) * 3;
|
|
110
106
|
const widestTotal = widestCells.reduce((sum, width) => sum + width, 0);
|
|
107
|
+
const requiredInnerWidth = widestTotal + separatorWidth;
|
|
108
|
+
|
|
109
|
+
const maxAllowedWidth = terminalWidth(60, 160, 120);
|
|
110
|
+
const innerWidth = Math.min(requiredInnerWidth, maxAllowedWidth - 4);
|
|
111
|
+
const availableWidth = Math.max(columnCount * 8, innerWidth - separatorWidth);
|
|
112
|
+
const boxWidth = innerWidth + 4;
|
|
113
|
+
|
|
111
114
|
const scale = widestTotal > availableWidth ? availableWidth / widestTotal : 1;
|
|
112
115
|
const columnWidths = widestCells.map(width => Math.max(8, Math.floor(width * scale)));
|
|
113
116
|
|
|
117
|
+
const verticalChar = getBoxChars().vertical;
|
|
118
|
+
|
|
114
119
|
const renderRow = (cells) => {
|
|
115
120
|
const wrappedCells = cells.map((cell, index) => wrapText(cell || '', columnWidths[index]));
|
|
116
121
|
const lineCount = Math.max(...wrappedCells.map(lines => lines.length), 1);
|
|
@@ -122,7 +127,7 @@ function renderMarkdownTableBlock(tableLines) {
|
|
|
122
127
|
const cellLine = wrappedCells[columnIndex][lineIndex] || '';
|
|
123
128
|
parts.push(padVisible(cellLine, columnWidths[columnIndex]));
|
|
124
129
|
}
|
|
125
|
-
rendered.push(parts.join(
|
|
130
|
+
rendered.push(parts.join(` ${colors.dim}${verticalChar}${colors.reset} `));
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
return rendered;
|
package/src/cli/repl.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
renderStartupTui,
|
|
17
17
|
renderStatusPanel,
|
|
18
18
|
} from './tui.js';
|
|
19
|
+
import { terminalManager } from './terminal-manager.js';
|
|
19
20
|
import { WinterInputController } from './input-controller.js';
|
|
20
21
|
import { ToolExecutor } from '../tools/executor.js';
|
|
21
22
|
import { SessionManager } from '../session/manager.js';
|
|
@@ -125,6 +126,8 @@ export class WinterREPL {
|
|
|
125
126
|
this.watchers = [];
|
|
126
127
|
this.startupNotices = [];
|
|
127
128
|
this._fixedPanel = Boolean(process.stdout.isTTY) && process.env.WINTER_FIXED_PANEL_TUI !== '0';
|
|
129
|
+
|
|
130
|
+
terminalManager.install();
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
async initCodebaseSearch() {
|
|
@@ -654,22 +657,106 @@ export class WinterREPL {
|
|
|
654
657
|
// Hiển thị prompt lần đầu tiên ngay khi khởi động xong.
|
|
655
658
|
this.showInputPrompt();
|
|
656
659
|
|
|
657
|
-
|
|
660
|
+
// Paste buffer: gom nhiều dòng paste nhanh thành 1 tin nhắn
|
|
661
|
+
this._multilineBuffer = [];
|
|
662
|
+
this._pasteBuffer = [];
|
|
663
|
+
this._pasteTimer = null;
|
|
664
|
+
this._isPasteChunk = false;
|
|
665
|
+
this._pasteChunkTimer = null;
|
|
666
|
+
const PASTE_DELAY = 80;
|
|
667
|
+
|
|
668
|
+
process.stdin.on('data', (chunk) => {
|
|
669
|
+
// If a large chunk or chunk with newlines arrives, it's definitely a paste.
|
|
670
|
+
if (chunk.length > 3 || chunk.includes('\n')) {
|
|
671
|
+
this._isPasteChunk = true;
|
|
672
|
+
if (this._pasteChunkTimer) clearTimeout(this._pasteChunkTimer);
|
|
673
|
+
this._pasteChunkTimer = setTimeout(() => {
|
|
674
|
+
this._isPasteChunk = false;
|
|
675
|
+
}, 150);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
const flushPasteBuffer = () => {
|
|
680
|
+
this._pasteTimer = null;
|
|
681
|
+
if (this._pasteBuffer.length === 0) return;
|
|
682
|
+
|
|
683
|
+
const isSingleLineInput = this._pasteBuffer.length === 1 && !this._isPasteChunk;
|
|
684
|
+
const isJustEmptyEnter = this._pasteBuffer.length === 1 && this._pasteBuffer[0].trim() === '';
|
|
685
|
+
|
|
686
|
+
// Normal single-line submit
|
|
687
|
+
if (isSingleLineInput && this._multilineBuffer.length === 0) {
|
|
688
|
+
const line = this._pasteBuffer[0].trim();
|
|
689
|
+
this._pasteBuffer = [];
|
|
690
|
+
if (!line) {
|
|
691
|
+
if (this.running && !this.readlineClosed) {
|
|
692
|
+
readline.moveCursor(process.stdout, 0, -1);
|
|
693
|
+
readline.clearLine(process.stdout, 0);
|
|
694
|
+
this.rl.prompt(true);
|
|
695
|
+
}
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Command to enter multiline mode manually
|
|
700
|
+
if (line === '/multi' || line === '/m') {
|
|
701
|
+
this._multilineBuffer.push('');
|
|
702
|
+
console.log(`${colors.cyan}│ ${colors.dim}[ Đã bật chế độ gõ nhiều dòng. Nhấn Enter 2 lần (dòng trống) để gửi. ]${colors.reset}`);
|
|
703
|
+
if (this.running && !this.readlineClosed) this.rl.prompt(true);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
this.submitInputQueue(line);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// We are in multiline/paste mode
|
|
712
|
+
this._multilineBuffer.push(...this._pasteBuffer);
|
|
713
|
+
this._pasteBuffer = [];
|
|
714
|
+
|
|
715
|
+
// If they pressed Enter on an empty line, submit the multiline buffer!
|
|
716
|
+
if (isJustEmptyEnter && this._multilineBuffer.length > 1) {
|
|
717
|
+
// Remove the trailing empty line
|
|
718
|
+
this._multilineBuffer.pop();
|
|
719
|
+
const combined = this._multilineBuffer.join('\n').trim();
|
|
720
|
+
this._multilineBuffer = [];
|
|
721
|
+
this._isPasteChunk = false;
|
|
722
|
+
|
|
723
|
+
if (!combined) {
|
|
724
|
+
if (this.running && !this.readlineClosed) {
|
|
725
|
+
this.closeInputBox();
|
|
726
|
+
this.showInputPrompt();
|
|
727
|
+
}
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
this.submitInputQueue(combined);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Otherwise, we are still collecting! Wait for user to submit.
|
|
736
|
+
readline.clearLine(process.stdout, 0);
|
|
737
|
+
readline.cursorTo(process.stdout, 0);
|
|
738
|
+
const linesCount = this._multilineBuffer.length;
|
|
739
|
+
console.log(`${colors.cyan}│ ${colors.dim}[ Đang nhập nhiều dòng (${linesCount} dòng)... Nhấn Enter ở dòng trống để gửi ]${colors.reset}`);
|
|
740
|
+
if (this.running && !this.readlineClosed) this.rl.prompt(true);
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
this.submitInputQueue = (combined) => {
|
|
658
744
|
this.inputQueue = this.inputQueue
|
|
659
745
|
.then(async () => {
|
|
660
746
|
this.closeInputBox();
|
|
661
|
-
|
|
662
|
-
if (input) {
|
|
663
|
-
await this.handleInput(input);
|
|
664
|
-
} else {
|
|
665
|
-
if (this.running && !this.readlineClosed) this.showInputPrompt();
|
|
666
|
-
}
|
|
747
|
+
await this.handleInput(combined);
|
|
667
748
|
})
|
|
668
749
|
.catch((error) => {
|
|
669
750
|
this.closeInputBox();
|
|
670
751
|
console.log(`\n${colors.red}? Error: ${error.message}${colors.reset}\n`);
|
|
671
752
|
if (this.running && !this.readlineClosed) this.showInputPrompt();
|
|
672
753
|
});
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
this.rl.on('line', (line) => {
|
|
757
|
+
this._pasteBuffer.push(line);
|
|
758
|
+
if (this._pasteTimer) clearTimeout(this._pasteTimer);
|
|
759
|
+
this._pasteTimer = setTimeout(flushPasteBuffer, PASTE_DELAY);
|
|
673
760
|
});
|
|
674
761
|
|
|
675
762
|
this.rl.on('close', async () => {
|
|
@@ -936,9 +1023,12 @@ export class WinterREPL {
|
|
|
936
1023
|
const preview = input.length > 40 ? input.slice(0, 37) + '...' : input;
|
|
937
1024
|
console.log(`${colors.yellow}⧗${colors.reset} ${colors.bright}Queued #${pos}${colors.reset} ${colors.dim}› ${preview}${colors.reset}`);
|
|
938
1025
|
this.taskQueue.push(input);
|
|
1026
|
+
if (!this.readlineClosed) this.showInputPrompt();
|
|
939
1027
|
return;
|
|
940
1028
|
}
|
|
941
|
-
|
|
1029
|
+
this.processInputTask(input).catch(err => {
|
|
1030
|
+
console.log(colors.red + '\nLỗi xử lý hàng đợi: ' + err.message + colors.reset);
|
|
1031
|
+
});
|
|
942
1032
|
}
|
|
943
1033
|
|
|
944
1034
|
async processInputTask(input) {
|
|
@@ -952,6 +1042,12 @@ export class WinterREPL {
|
|
|
952
1042
|
this.history = this.history.slice(-this.maxHistory);
|
|
953
1043
|
}
|
|
954
1044
|
|
|
1045
|
+
// Echo tin nhắn user để xác nhận đã nhận
|
|
1046
|
+
if (!input.startsWith('/') && !input.startsWith('!')) {
|
|
1047
|
+
const preview = input.length > 120 ? input.slice(0, 117) + '...' : input;
|
|
1048
|
+
console.log(`\n${colors.bright}${colors.green}You${colors.reset} ${colors.dim}›${colors.reset} ${colors.white}${preview}${colors.reset}`);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
955
1051
|
if (input.startsWith('!')) {
|
|
956
1052
|
const command = input.slice(1).trim();
|
|
957
1053
|
if (!command) {
|
|
@@ -1030,6 +1126,7 @@ export class WinterREPL {
|
|
|
1030
1126
|
if (this.spinner) this.spinner.stop();
|
|
1031
1127
|
|
|
1032
1128
|
if (this.taskQueue.length > 0) {
|
|
1129
|
+
this.closeInputBox();
|
|
1033
1130
|
const nextTask = this.taskQueue.shift();
|
|
1034
1131
|
setTimeout(() => this.processInputTask(nextTask), 0);
|
|
1035
1132
|
} else {
|
|
@@ -1748,22 +1845,11 @@ ${colors.reset}
|
|
|
1748
1845
|
}
|
|
1749
1846
|
|
|
1750
1847
|
if (chunk.content) {
|
|
1751
|
-
if (!printed) {
|
|
1752
|
-
if (this.spinner) this.spinner.stop();
|
|
1753
|
-
if (!bufferToolModeContent) {
|
|
1754
|
-
process.stdout.write(`\n${colors.white}`);
|
|
1755
|
-
printed = true;
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
1848
|
content += chunk.content;
|
|
1759
|
-
if (!bufferToolModeContent) {
|
|
1760
|
-
process.stdout.write(chunk.content);
|
|
1761
|
-
}
|
|
1762
1849
|
}
|
|
1763
1850
|
}
|
|
1764
1851
|
|
|
1765
1852
|
if (this.spinner) this.spinner.stop();
|
|
1766
|
-
if (printed) process.stdout.write(colors.reset);
|
|
1767
1853
|
|
|
1768
1854
|
const inlineToolExtraction = this.extractInlineToolCalls(content);
|
|
1769
1855
|
const rawToolCalls = [
|
|
@@ -1776,7 +1862,7 @@ ${colors.reset}
|
|
|
1776
1862
|
const toolCalls = this.normalizeToolCalls(rawToolCalls);
|
|
1777
1863
|
const visibleContent = inlineToolExtraction.content || content;
|
|
1778
1864
|
|
|
1779
|
-
if (
|
|
1865
|
+
if (toolCalls.length === 0 && visibleContent) {
|
|
1780
1866
|
if (options?.requireToolEvidence && this.responseNeedsToolEvidence(visibleContent)) {
|
|
1781
1867
|
return {
|
|
1782
1868
|
assistantMsg: { content: visibleContent },
|
|
@@ -1794,18 +1880,6 @@ ${colors.reset}
|
|
|
1794
1880
|
};
|
|
1795
1881
|
}
|
|
1796
1882
|
|
|
1797
|
-
if (toolCalls.length === 0 && visibleContent) {
|
|
1798
|
-
console.log(`\n${colors.dim}${this.formatAnswerFooter(startedAt, totalUsage)}${colors.reset}\n`);
|
|
1799
|
-
return {
|
|
1800
|
-
assistantMsg: { content: visibleContent },
|
|
1801
|
-
toolCalls,
|
|
1802
|
-
finalContent: visibleContent,
|
|
1803
|
-
finishReason,
|
|
1804
|
-
};
|
|
1805
|
-
} else if (printed && visibleContent) {
|
|
1806
|
-
process.stdout.write('\n');
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
1883
|
return {
|
|
1810
1884
|
assistantMsg: {
|
|
1811
1885
|
content: visibleContent,
|
|
@@ -1974,7 +2048,6 @@ ${colors.reset}
|
|
|
1974
2048
|
const profile = executionProfile || this.selectExecutionProfile(messages, { enableTools: false });
|
|
1975
2049
|
|
|
1976
2050
|
try {
|
|
1977
|
-
process.stdout.write(`\n${colors.white}`);
|
|
1978
2051
|
let isFirst = true;
|
|
1979
2052
|
for await (const chunk of this.ai.streamRequest(messages, {
|
|
1980
2053
|
provider: profile.provider,
|
|
@@ -1982,16 +2055,19 @@ ${colors.reset}
|
|
|
1982
2055
|
enableTools: false,
|
|
1983
2056
|
signal: this.currentAbortController?.signal,
|
|
1984
2057
|
})) {
|
|
2058
|
+
if (isFirst) {
|
|
2059
|
+
isFirst = false;
|
|
2060
|
+
}
|
|
1985
2061
|
if (chunk.usage) this.addUsage(totalUsage, chunk.usage);
|
|
1986
2062
|
if (chunk.content) {
|
|
1987
2063
|
content += chunk.content;
|
|
1988
|
-
process.stdout.write(chunk.content);
|
|
1989
2064
|
}
|
|
1990
2065
|
}
|
|
1991
|
-
|
|
2066
|
+
|
|
2067
|
+
if (this.spinner) this.spinner.stop();
|
|
1992
2068
|
|
|
1993
2069
|
if (content) {
|
|
1994
|
-
|
|
2070
|
+
this.printAssistantAnswer(content, startedAt, totalUsage);
|
|
1995
2071
|
return content;
|
|
1996
2072
|
}
|
|
1997
2073
|
} catch (error) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
|
|
3
|
+
class TerminalManager {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.isPromptVisible = false;
|
|
6
|
+
this.getLinesCountFn = null;
|
|
7
|
+
this.redrawFn = null;
|
|
8
|
+
this.onHideFn = null;
|
|
9
|
+
this._originalLog = console.log;
|
|
10
|
+
this._originalError = console.error;
|
|
11
|
+
this._isIntercepting = false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
install() {
|
|
15
|
+
if (this._isIntercepting) return;
|
|
16
|
+
this._isIntercepting = true;
|
|
17
|
+
|
|
18
|
+
console.log = (...args) => this._interceptLog(this._originalLog, args);
|
|
19
|
+
console.error = (...args) => this._interceptLog(this._originalError, args);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
uninstall() {
|
|
23
|
+
if (!this._isIntercepting) return;
|
|
24
|
+
this._isIntercepting = false;
|
|
25
|
+
console.log = this._originalLog;
|
|
26
|
+
console.error = this._originalError;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setPromptState(isVisible, getLinesCountFn = null, redrawFn = null, onHideFn = null) {
|
|
30
|
+
this.isPromptVisible = isVisible;
|
|
31
|
+
this.getLinesCountFn = getLinesCountFn;
|
|
32
|
+
this.redrawFn = redrawFn;
|
|
33
|
+
this.onHideFn = onHideFn;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
hidePrompt() {
|
|
37
|
+
if (!this.isPromptVisible || !process.stdout.isTTY) return;
|
|
38
|
+
|
|
39
|
+
// Clear the current line first (the readline prompt itself)
|
|
40
|
+
readline.clearLine(process.stdout, 0);
|
|
41
|
+
readline.cursorTo(process.stdout, 0);
|
|
42
|
+
|
|
43
|
+
// If the prompt panel has multiple lines above it, move up and clear
|
|
44
|
+
const linesCount = this.getLinesCountFn ? this.getLinesCountFn() : 0;
|
|
45
|
+
if (linesCount > 1) {
|
|
46
|
+
readline.moveCursor(process.stdout, 0, -(linesCount - 1));
|
|
47
|
+
readline.clearScreenDown(process.stdout);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (this.onHideFn) {
|
|
51
|
+
this.onHideFn();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_interceptLog(originalFn, args) {
|
|
56
|
+
if (!this.isPromptVisible) {
|
|
57
|
+
originalFn.apply(console, args);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Hide prompt
|
|
62
|
+
this.hidePrompt();
|
|
63
|
+
|
|
64
|
+
// Print the actual log
|
|
65
|
+
originalFn.apply(console, args);
|
|
66
|
+
|
|
67
|
+
// Redraw prompt
|
|
68
|
+
if (this.redrawFn) {
|
|
69
|
+
this.redrawFn();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const terminalManager = new TerminalManager();
|
package/src/cli/terminal-ui.js
CHANGED
|
@@ -70,12 +70,11 @@ export function terminalWidth(min = 72, max = 120, fallback = 88) {
|
|
|
70
70
|
|
|
71
71
|
export function supportsUnicodeUi(env = process.env, platform = process.platform) {
|
|
72
72
|
if (env.WINTER_ASCII_UI === '1' || env.WINTER_ASCII_UI === 'true') return false;
|
|
73
|
-
|
|
74
|
-
if (platform !== 'win32') return true;
|
|
75
|
-
return false;
|
|
73
|
+
return true;
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
export function getBoxChars() {
|
|
77
|
+
if (supportsUnicodeUi()) return UNICODE_BOX;
|
|
79
78
|
return ASCII_BOX;
|
|
80
79
|
}
|
|
81
80
|
|
package/src/cli/tui.js
CHANGED
|
@@ -176,6 +176,20 @@ export function renderAssistantPanel({ content = '', footer = '', colors, title
|
|
|
176
176
|
export function renderToolPanel({ toolName = 'Tool', summary = '', success = true, colors } = {}) {
|
|
177
177
|
const c = colors || {};
|
|
178
178
|
const status = success ? `${c.brightGreen}✓${c.reset}` : `${c.red}✖${c.reset}`;
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
|
|
180
|
+
if (!summary.includes('\n')) {
|
|
181
|
+
return `${status} ${c.bright}${c.cyan}${toolName}${c.reset} ${c.dim}· ${summary}${c.reset}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const lines = summary.split('\n');
|
|
185
|
+
const firstLine = lines.shift();
|
|
186
|
+
|
|
187
|
+
const formattedRest = lines.map(line => {
|
|
188
|
+
if (line.startsWith('+')) return ` ${c.green}${line}${c.reset}`;
|
|
189
|
+
if (line.startsWith('-')) return ` ${c.red}${line}${c.reset}`;
|
|
190
|
+
if (line.startsWith('@@')) return ` ${c.cyan}${line}${c.reset}`;
|
|
191
|
+
return ` ${c.dim}${line}${c.reset}`;
|
|
192
|
+
}).join('\n');
|
|
193
|
+
|
|
194
|
+
return `${status} ${c.bright}${c.cyan}${toolName}${c.reset} ${c.dim}· ${firstLine}${c.reset}\n${formattedRest}`;
|
|
181
195
|
}
|