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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "winter-super-cli",
3
- "version": "2026.6.7",
3
+ "version": "2026.6.8",
4
4
  "description": "❄️ AI-Powered Development CLI with Interactive REPL",
5
5
  "type": "module",
6
6
  "main": "bin/winter.js",
@@ -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 body = [];
128
- const maxLines = Math.min(30, diff.additionsList.length + diff.removalsList.length);
129
- let added = 0;
130
- let removed = 0;
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
- body.push(` ${colors.dim}${path.basename(title)} ${diff.additions} additions, ${diff.removals} deletions${colors.reset}`);
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
- if (added + removed >= maxLines) {
136
- body.push(` ${colors.dim}... and ${diff.changes - maxLines} more changes${colors.reset}`);
137
- break;
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
- const lines = part.value.split('\n').filter(Boolean);
141
- const isAdded = part.added;
142
- const isRemoved = part.removed;
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
- for (const line of lines) {
145
- if (added + removed >= maxLines) break;
146
- if (isAdded) {
147
- added++;
148
- body.push(` ${colors.green}+ ${line}${colors.reset}`);
149
- } else if (isRemoved) {
150
- removed++;
151
- body.push(` ${colors.red}- ${line}${colors.reset}`);
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(`\n${renderBox({
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} — Accept all (apply full diff)\n` +
241
- ` ${colors.red}[r]${colors.reset} — Reject all\n` +
242
- ` ${colors.yellow}[m]${colors.reset} — Manual edit (opens file in $EDITOR)\n` +
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 { drawInFixedArea, enableFixedPanel, moveToPromptRow, moveToScrollRegion, padVisible, renderBox, terminalWidth } from './terminal-ui.js';
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
- if (typeof repl.rl?.setPrompt === 'function') {
26
- repl.rl.setPrompt(panel.prompt);
27
- }
28
- repl.rl?.prompt?.();
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
- this.rl.on('line', (line) => {
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
- const input = line.trim();
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
- await this.processInputTask(input);
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 (bufferToolModeContent && toolCalls.length === 0 && visibleContent) {
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
- process.stdout.write(colors.reset);
2066
+
2067
+ if (this.spinner) this.spinner.stop();
1992
2068
 
1993
2069
  if (content) {
1994
- console.log(`\n${colors.dim}${this.formatAnswerFooter(startedAt, totalUsage)}${colors.reset}\n`);
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();
@@ -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
- if (env.WINTER_UNICODE_UI === '1' || env.WINTER_UNICODE_UI === 'true') return platform !== 'win32';
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
- const plainSummary = String(summary || '').replace(/\n/g, ' ');
180
- return `${status} ${c.bright}${c.cyan}${toolName}${c.reset} ${c.dim}· ${plainSummary}${c.reset}`;
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
  }