winter-super-cli 2026.6.13 → 2026.6.15

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.13",
3
+ "version": "2026.6.15",
4
4
  "description": "❄️ AI-Powered Development CLI with Interactive REPL",
5
5
  "type": "module",
6
6
  "main": "bin/winter.js",
@@ -614,11 +614,15 @@ export class AIProviderManager {
614
614
 
615
615
  const decoder = new TextDecoder();
616
616
  let buffer = '';
617
+ let rawText = '';
618
+ let yieldedAny = false;
617
619
 
618
620
  for await (const chunk of response.body) {
619
621
  if (timeout.resetTimer) timeout.resetTimer();
620
622
 
621
- buffer += decoder.decode(chunk, { stream: true });
623
+ const decoded = decoder.decode(chunk, { stream: true });
624
+ rawText += decoded;
625
+ buffer += decoded;
622
626
  const lines = buffer.split(/\r?\n/);
623
627
  buffer = lines.pop() || '';
624
628
 
@@ -638,6 +642,7 @@ export class AIProviderManager {
638
642
 
639
643
  const choice = data.choices?.[0] || {};
640
644
  const content = choice.delta?.content ?? choice.message?.content ?? choice.text ?? '';
645
+ yieldedAny = true;
641
646
  yield {
642
647
  content,
643
648
  usage: data.usage,
@@ -653,6 +658,7 @@ export class AIProviderManager {
653
658
  try {
654
659
  const data = JSON.parse(payload);
655
660
  const choice = data.choices?.[0] || {};
661
+ yieldedAny = true;
656
662
  yield {
657
663
  content: choice.delta?.content ?? choice.message?.content ?? choice.text ?? '',
658
664
  usage: data.usage,
@@ -661,6 +667,24 @@ export class AIProviderManager {
661
667
  } catch {}
662
668
  }
663
669
  }
670
+
671
+ if (!yieldedAny) {
672
+ try {
673
+ const data = JSON.parse(rawText.trim());
674
+ const choice = data.choices?.[0] || {};
675
+ const content = choice.delta?.content ?? choice.message?.content ?? choice.text ?? '';
676
+ if (content || data.usage || choice.finish_reason) {
677
+ yield {
678
+ content,
679
+ usage: data.usage,
680
+ raw: data,
681
+ };
682
+ }
683
+ } catch {
684
+ // Some OpenAI-compatible servers return 200 with a non-SSE/non-JSON body
685
+ // when streaming is requested. Let the caller fall back to non-streaming.
686
+ }
687
+ }
664
688
  } catch (error) {
665
689
  throw normalizeFetchError(error, provider, timeoutMs, true, timeout.timedOut());
666
690
  } finally {
package/src/cli/repl.js CHANGED
@@ -16,7 +16,6 @@ import {
16
16
  renderStartupTui,
17
17
  renderStatusPanel,
18
18
  } from './tui.js';
19
- import { terminalManager } from './terminal-manager.js';
20
19
  import { WinterInputController } from './input-controller.js';
21
20
  import { ToolExecutor } from '../tools/executor.js';
22
21
  import { SessionManager } from '../session/manager.js';
@@ -125,9 +124,6 @@ export class WinterREPL {
125
124
  this.inputController = new WinterInputController(this);
126
125
  this.watchers = [];
127
126
  this.startupNotices = [];
128
- this._fixedPanel = Boolean(process.stdout.isTTY) && process.env.WINTER_FIXED_PANEL_TUI !== '0';
129
-
130
- terminalManager.install();
131
127
  }
132
128
 
133
129
  async initCodebaseSearch() {
@@ -1798,6 +1794,7 @@ ${colors.reset}
1798
1794
  const hasPath = /[A-Za-z]:[\\/][\w.\\/\\-]+/i.test(text) || /(?:^|\s)[.~]?\/[\w.\/-]+/i.test(text);
1799
1795
  const readVerbs = /\b(đọc|doc|read|xem|view|mở|open|show|hiện|hiển thị|cat|type)\b/i;
1800
1796
  const readFilePatterns = /\b(đọc|doc|read|xem|view|mở|open|show|hiện|cat|type)\b.*\.(?:js|ts|py|json|md|css|html|txt|yaml|yml|toml|cfg|ini|env|sh|bat|ps1|xml|vue|svelte|go|rs|java|c|cpp|rb|php)\b/i;
1797
+ const readPatterns = readFilePatterns;
1801
1798
  const dirPatterns = /\b(đọc|doc|read|xem|liệt kê|list|ls|dir|show|hiện)\b.*\b(thư mục|folder|directory|dir)\b/i;
1802
1799
 
1803
1800
  if (readFilePatterns.test(text)) {
@@ -118,7 +118,20 @@ export function formatRuntimeEnvironmentSummary(profile = getRuntimeEnvironment(
118
118
  ].join('\n')
119
119
  : 'Bash tool shell rule: use the native POSIX shell; leave shell unspecified unless a specific shell is required.';
120
120
 
121
+ const now = new Date();
122
+ const timeFormatter = new Intl.DateTimeFormat('vi-VN', {
123
+ weekday: 'long',
124
+ year: 'numeric',
125
+ month: '2-digit',
126
+ day: '2-digit',
127
+ hour: '2-digit',
128
+ minute: '2-digit',
129
+ second: '2-digit',
130
+ timeZoneName: 'short'
131
+ });
132
+
121
133
  return [
134
+ `Current Local Time: ${timeFormatter.format(now)}`,
122
135
  `Host OS: ${profile.hostOs}`,
123
136
  `Node platform: ${profile.platform}`,
124
137
  `CPU arch: ${profile.arch}`,
@@ -1,73 +1,73 @@
1
- const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
2
- const ZERO_WIDTH_PATTERN = /[\u200B-\u200D\u2060\uFE0E\uFE0F]/u;
3
- const WIDE_CODE_POINT_RANGES = [
4
- [0x1100, 0x115f],
5
- [0x2329, 0x232a],
6
- [0x2e80, 0x303e],
7
- [0x3040, 0xa4cf],
8
- [0xac00, 0xd7a3],
9
- [0xf900, 0xfaff],
10
- [0xfe10, 0xfe19],
11
- [0xfe30, 0xfe6f],
12
- [0xff00, 0xff60],
13
- [0xffe0, 0xffe6],
14
- [0x1f300, 0x1f64f],
15
- [0x1f680, 0x1f6ff],
16
- [0x1f900, 0x1f9ff],
17
- [0x1fa70, 0x1faff],
18
- ];
19
-
20
- const UNICODE_BOX = {
21
- topLeft: '╭',
22
- topRight: '╮',
23
- bottomLeft: '╰',
24
- bottomRight: '╯',
25
- horizontal: '─',
26
- vertical: '│',
27
- teeLeft: '├',
28
- teeRight: '┤',
29
- };
30
-
31
- const ASCII_BOX = {
32
- topLeft: '+',
33
- topRight: '+',
34
- bottomLeft: '+',
35
- bottomRight: '+',
36
- horizontal: '-',
37
- vertical: '|',
38
- teeLeft: '+',
39
- teeRight: '+',
40
- };
41
-
42
- export function stripAnsi(text) {
43
- return String(text ?? '').replace(ANSI_PATTERN, '');
44
- }
45
-
46
- export function visibleWidth(text) {
47
- let width = 0;
48
- for (const char of Array.from(stripAnsi(text))) {
49
- width += charDisplayWidth(char);
50
- }
51
- return width;
52
- }
53
-
54
- export function charDisplayWidth(char) {
55
- if (!char || ZERO_WIDTH_PATTERN.test(char) || /\p{Mark}/u.test(char)) return 0;
56
-
57
- const codePoint = char.codePointAt(0);
58
- if (codePoint === undefined) return 0;
59
- if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) return 0;
60
- if (codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff) return 2;
61
- if (/\p{Extended_Pictographic}/u.test(char)) return 2;
62
- if (WIDE_CODE_POINT_RANGES.some(([start, end]) => codePoint >= start && codePoint <= end)) return 2;
63
- return 1;
64
- }
65
-
66
- export function terminalWidth(min = 72, max = 120, fallback = 88) {
67
- const columns = process.stdout.columns || fallback;
68
- return Math.max(min, Math.min(columns - 2, max));
69
- }
70
-
1
+ const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
2
+ const ZERO_WIDTH_PATTERN = /[\u200B-\u200D\u2060\uFE0E\uFE0F]/u;
3
+ const WIDE_CODE_POINT_RANGES = [
4
+ [0x1100, 0x115f],
5
+ [0x2329, 0x232a],
6
+ [0x2e80, 0x303e],
7
+ [0x3040, 0xa4cf],
8
+ [0xac00, 0xd7a3],
9
+ [0xf900, 0xfaff],
10
+ [0xfe10, 0xfe19],
11
+ [0xfe30, 0xfe6f],
12
+ [0xff00, 0xff60],
13
+ [0xffe0, 0xffe6],
14
+ [0x1f300, 0x1f64f],
15
+ [0x1f680, 0x1f6ff],
16
+ [0x1f900, 0x1f9ff],
17
+ [0x1fa70, 0x1faff],
18
+ ];
19
+
20
+ const UNICODE_BOX = {
21
+ topLeft: '╭',
22
+ topRight: '╮',
23
+ bottomLeft: '╰',
24
+ bottomRight: '╯',
25
+ horizontal: '─',
26
+ vertical: '│',
27
+ teeLeft: '├',
28
+ teeRight: '┤',
29
+ };
30
+
31
+ const ASCII_BOX = {
32
+ topLeft: '+',
33
+ topRight: '+',
34
+ bottomLeft: '+',
35
+ bottomRight: '+',
36
+ horizontal: '-',
37
+ vertical: '|',
38
+ teeLeft: '+',
39
+ teeRight: '+',
40
+ };
41
+
42
+ export function stripAnsi(text) {
43
+ return String(text ?? '').replace(ANSI_PATTERN, '');
44
+ }
45
+
46
+ export function visibleWidth(text) {
47
+ let width = 0;
48
+ for (const char of Array.from(stripAnsi(text))) {
49
+ width += charDisplayWidth(char);
50
+ }
51
+ return width;
52
+ }
53
+
54
+ export function charDisplayWidth(char) {
55
+ if (!char || ZERO_WIDTH_PATTERN.test(char) || /\p{Mark}/u.test(char)) return 0;
56
+
57
+ const codePoint = char.codePointAt(0);
58
+ if (codePoint === undefined) return 0;
59
+ if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) return 0;
60
+ if (codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff) return 2;
61
+ if (/\p{Extended_Pictographic}/u.test(char)) return 2;
62
+ if (WIDE_CODE_POINT_RANGES.some(([start, end]) => codePoint >= start && codePoint <= end)) return 2;
63
+ return 1;
64
+ }
65
+
66
+ export function terminalWidth(min = 72, max = 120, fallback = 88) {
67
+ const columns = process.stdout.columns || fallback;
68
+ return Math.max(min, Math.min(columns - 2, max));
69
+ }
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
73
  return true;
@@ -77,187 +77,187 @@ export function getBoxChars() {
77
77
  if (supportsUnicodeUi()) return UNICODE_BOX;
78
78
  return ASCII_BOX;
79
79
  }
80
-
81
- export function padVisible(text, width, fill = ' ') {
82
- const visible = visibleWidth(text);
83
- const padCount = Math.max(0, width - visible);
84
- return `${text}${fill.repeat(padCount)}`;
85
- }
86
-
87
- export function wrapText(text, width) {
88
- const output = [];
89
- const lines = String(text ?? '').split(/\r?\n/);
90
-
91
- for (const line of lines) {
92
- const plain = stripAnsi(line);
93
- if (visibleWidth(plain) <= width) {
94
- output.push(line);
95
- continue;
96
- }
97
-
98
- const words = plain.split(/\s+/).filter(Boolean);
99
- if (words.length === 0) {
100
- output.push(plain.slice(0, width));
101
- continue;
102
- }
103
-
104
- let current = '';
105
- for (const word of words) {
106
- const candidate = current ? `${current} ${word}` : word;
107
- if (visibleWidth(candidate) <= width) {
108
- current = candidate;
109
- } else {
110
- if (current) output.push(current);
111
- if (visibleWidth(word) > width) {
112
- const chunks = chunkText(word, width);
113
- output.push(...chunks.slice(0, -1));
114
- current = chunks[chunks.length - 1] || '';
115
- } else {
116
- current = word;
117
- }
118
- }
119
- }
120
- if (current) output.push(current);
121
- }
122
-
123
- return output;
124
- }
125
-
126
- export function chunkText(text, width) {
127
- const chars = Array.from(stripAnsi(text));
128
- const chunks = [];
129
- let current = '';
130
- let currentWidth = 0;
131
- for (const char of chars) {
132
- const charWidth = charDisplayWidth(char);
133
- if (current && currentWidth + charWidth > width) {
134
- chunks.push(current);
135
- current = '';
136
- currentWidth = 0;
137
- }
138
- current += char;
139
- currentWidth += charWidth;
140
- }
141
- if (current) chunks.push(current);
142
- return chunks.length > 0 ? chunks : [''];
143
- }
144
-
145
- export function renderBox({
146
- title = '',
147
- body = [],
148
- width,
149
- borderColor = '\x1b[35m',
150
- titleColor = '\x1b[36m',
151
- reset = '\x1b[0m',
152
- boxChars = getBoxChars(),
153
- } = {}) {
154
- const innerWidth = Math.max(28, (width || terminalWidth()) - 4);
155
- const top = `${borderColor}${boxChars.topLeft}${boxChars.horizontal.repeat(innerWidth)}${boxChars.topRight}${reset}`;
156
- const bottom = `${borderColor}${boxChars.bottomLeft}${boxChars.horizontal.repeat(innerWidth)}${boxChars.bottomRight}${reset}`;
157
- const lines = [];
158
- const titleText = title ? ` ${title} ` : '';
159
-
160
- if (titleText) {
161
- const wrappedTitle = wrapText(titleText, innerWidth);
162
- wrappedTitle.forEach((segment, index) => {
163
- const plainSegment = stripAnsi(segment);
164
- const visible = visibleWidth(plainSegment);
165
- const padding = Math.max(0, innerWidth - visible);
166
- const left = index === 0 ? Math.floor(padding / 2) : 0;
167
- const right = index === 0 ? padding - left : padding;
168
- lines.push(`${borderColor}${boxChars.vertical}${reset}${' '.repeat(left)}${titleColor}${plainSegment}${reset}${' '.repeat(right)}${borderColor}${boxChars.vertical}${reset}`);
169
- });
170
- lines.push(`${borderColor}${boxChars.teeLeft}${boxChars.horizontal.repeat(innerWidth)}${boxChars.teeRight}${reset}`);
171
- }
172
-
173
- for (const item of body) {
174
- const rawText = String(item ?? '');
175
- if (visibleWidth(rawText) <= innerWidth) {
176
- const visible = visibleWidth(rawText);
177
- const padding = Math.max(0, innerWidth - visible);
178
- lines.push(`${borderColor}${boxChars.vertical}${reset} ${rawText}${' '.repeat(Math.max(0, padding - 1))}${borderColor}${boxChars.vertical}${reset}`);
179
- continue;
180
- }
181
-
182
- const wrapped = wrapText(rawText, innerWidth);
183
- if (wrapped.length === 0) {
184
- lines.push(`${borderColor}${boxChars.vertical}${reset} ${' '.repeat(Math.max(0, innerWidth - 1))}${borderColor}${boxChars.vertical}${reset}`);
185
- continue;
186
- }
187
-
188
- for (const segment of wrapped) {
189
- const text = stripAnsi(segment);
190
- const visible = visibleWidth(text);
191
- const padding = Math.max(0, innerWidth - visible);
192
- lines.push(`${borderColor}${boxChars.vertical}${reset} ${text}${' '.repeat(Math.max(0, padding - 1))}${borderColor}${boxChars.vertical}${reset}`);
193
- }
194
- }
195
-
196
- return [top, ...lines, bottom].join('\n');
197
- }
198
-
199
- export function renderKeyValueRows(rows, width, colors) {
200
- const innerWidth = Math.max(28, (width || terminalWidth()) - 4);
201
- const boxChars = getBoxChars();
202
- return rows.map(([left, right]) => {
203
- const leftWidth = Math.floor(innerWidth * 0.5);
204
- const rightWidth = innerWidth - leftWidth - 1;
205
- const leftText = padVisible(left, leftWidth);
206
- const rightText = padVisible(right, rightWidth);
207
- return `${colors.border}${boxChars.vertical}${colors.reset} ${leftText}${colors.spacer}${rightText} ${colors.border}${boxChars.vertical}${colors.reset}`;
208
- });
209
- }
210
-
211
-
212
-
213
- export const PANEL_HEIGHT = 5;
214
-
215
- let _fixedEnabled = false;
216
-
217
- export function enableFixedPanel() {
218
- if (!process.stdout.isTTY) return false;
219
- _fixedEnabled = true;
220
- const rows = process.stdout.rows || 24;
221
- const scrollBottom = Math.max(1, rows - PANEL_HEIGHT);
222
- process.stdout.write("\x1b[1;" + scrollBottom + "r");
223
- return true;
224
- }
225
-
226
- export function disableFixedPanel() {
227
- _fixedEnabled = false;
228
- process.stdout.write("\x1b[r");
229
- }
230
-
231
- export function refreshFixedPanel() {
232
- if (!_fixedEnabled || !process.stdout.isTTY) return;
233
- const rows = process.stdout.rows || 24;
234
- const scrollBottom = Math.max(1, rows - PANEL_HEIGHT);
235
- process.stdout.write("\x1b[1;" + scrollBottom + "r");
236
- }
237
-
238
- export function drawInFixedArea(content) {
239
- if (!_fixedEnabled || !process.stdout.isTTY) return;
240
- const rows = process.stdout.rows || 24;
241
- const startRow = Math.max(1, rows - PANEL_HEIGHT + 1);
242
- process.stdout.write("\x1b7");
243
- process.stdout.write("\x1b[" + startRow + ";1H");
244
- process.stdout.write("\x1b[J");
245
- process.stdout.write(String(content ?? ""));
246
- process.stdout.write("\x1b8");
247
- }
248
-
249
- export function moveToScrollRegion() {
250
- if (!process.stdout.isTTY) return;
251
- const rows = process.stdout.rows || 24;
252
- const scrollBottom = Math.max(1, rows - PANEL_HEIGHT);
253
- process.stdout.write("\x1b[" + scrollBottom + ";1H");
254
- }
255
-
256
- export function moveToPromptRow() {
257
- if (!process.stdout.isTTY) return;
258
- const rows = process.stdout.rows || 24;
259
- // Position prompt at last scrollable row (just above the fixed panel)
260
- const promptRow = Math.max(1, rows - PANEL_HEIGHT - 1);
261
- process.stdout.write("\x1b[" + promptRow + ";1H");
262
- }
263
-
80
+
81
+ export function padVisible(text, width, fill = ' ') {
82
+ const visible = visibleWidth(text);
83
+ const padCount = Math.max(0, width - visible);
84
+ return `${text}${fill.repeat(padCount)}`;
85
+ }
86
+
87
+ export function wrapText(text, width) {
88
+ const output = [];
89
+ const lines = String(text ?? '').split(/\r?\n/);
90
+
91
+ for (const line of lines) {
92
+ const plain = stripAnsi(line);
93
+ if (visibleWidth(plain) <= width) {
94
+ output.push(line);
95
+ continue;
96
+ }
97
+
98
+ const words = plain.split(/\s+/).filter(Boolean);
99
+ if (words.length === 0) {
100
+ output.push(plain.slice(0, width));
101
+ continue;
102
+ }
103
+
104
+ let current = '';
105
+ for (const word of words) {
106
+ const candidate = current ? `${current} ${word}` : word;
107
+ if (visibleWidth(candidate) <= width) {
108
+ current = candidate;
109
+ } else {
110
+ if (current) output.push(current);
111
+ if (visibleWidth(word) > width) {
112
+ const chunks = chunkText(word, width);
113
+ output.push(...chunks.slice(0, -1));
114
+ current = chunks[chunks.length - 1] || '';
115
+ } else {
116
+ current = word;
117
+ }
118
+ }
119
+ }
120
+ if (current) output.push(current);
121
+ }
122
+
123
+ return output;
124
+ }
125
+
126
+ export function chunkText(text, width) {
127
+ const chars = Array.from(stripAnsi(text));
128
+ const chunks = [];
129
+ let current = '';
130
+ let currentWidth = 0;
131
+ for (const char of chars) {
132
+ const charWidth = charDisplayWidth(char);
133
+ if (current && currentWidth + charWidth > width) {
134
+ chunks.push(current);
135
+ current = '';
136
+ currentWidth = 0;
137
+ }
138
+ current += char;
139
+ currentWidth += charWidth;
140
+ }
141
+ if (current) chunks.push(current);
142
+ return chunks.length > 0 ? chunks : [''];
143
+ }
144
+
145
+ export function renderBox({
146
+ title = '',
147
+ body = [],
148
+ width,
149
+ borderColor = '\x1b[35m',
150
+ titleColor = '\x1b[36m',
151
+ reset = '\x1b[0m',
152
+ boxChars = getBoxChars(),
153
+ } = {}) {
154
+ const innerWidth = Math.max(28, (width || terminalWidth()) - 4);
155
+ const top = `${borderColor}${boxChars.topLeft}${boxChars.horizontal.repeat(innerWidth)}${boxChars.topRight}${reset}`;
156
+ const bottom = `${borderColor}${boxChars.bottomLeft}${boxChars.horizontal.repeat(innerWidth)}${boxChars.bottomRight}${reset}`;
157
+ const lines = [];
158
+ const titleText = title ? ` ${title} ` : '';
159
+
160
+ if (titleText) {
161
+ const wrappedTitle = wrapText(titleText, innerWidth);
162
+ wrappedTitle.forEach((segment, index) => {
163
+ const plainSegment = stripAnsi(segment);
164
+ const visible = visibleWidth(plainSegment);
165
+ const padding = Math.max(0, innerWidth - visible);
166
+ const left = index === 0 ? Math.floor(padding / 2) : 0;
167
+ const right = index === 0 ? padding - left : padding;
168
+ lines.push(`${borderColor}${boxChars.vertical}${reset}${' '.repeat(left)}${titleColor}${plainSegment}${reset}${' '.repeat(right)}${borderColor}${boxChars.vertical}${reset}`);
169
+ });
170
+ lines.push(`${borderColor}${boxChars.teeLeft}${boxChars.horizontal.repeat(innerWidth)}${boxChars.teeRight}${reset}`);
171
+ }
172
+
173
+ for (const item of body) {
174
+ const rawText = String(item ?? '');
175
+ if (visibleWidth(rawText) <= innerWidth) {
176
+ const visible = visibleWidth(rawText);
177
+ const padding = Math.max(0, innerWidth - visible);
178
+ lines.push(`${borderColor}${boxChars.vertical}${reset} ${rawText}${' '.repeat(Math.max(0, padding - 1))}${borderColor}${boxChars.vertical}${reset}`);
179
+ continue;
180
+ }
181
+
182
+ const wrapped = wrapText(rawText, innerWidth);
183
+ if (wrapped.length === 0) {
184
+ lines.push(`${borderColor}${boxChars.vertical}${reset} ${' '.repeat(Math.max(0, innerWidth - 1))}${borderColor}${boxChars.vertical}${reset}`);
185
+ continue;
186
+ }
187
+
188
+ for (const segment of wrapped) {
189
+ const text = stripAnsi(segment);
190
+ const visible = visibleWidth(text);
191
+ const padding = Math.max(0, innerWidth - visible);
192
+ lines.push(`${borderColor}${boxChars.vertical}${reset} ${text}${' '.repeat(Math.max(0, padding - 1))}${borderColor}${boxChars.vertical}${reset}`);
193
+ }
194
+ }
195
+
196
+ return [top, ...lines, bottom].join('\n');
197
+ }
198
+
199
+ export function renderKeyValueRows(rows, width, colors) {
200
+ const innerWidth = Math.max(28, (width || terminalWidth()) - 4);
201
+ const boxChars = getBoxChars();
202
+ return rows.map(([left, right]) => {
203
+ const leftWidth = Math.floor(innerWidth * 0.5);
204
+ const rightWidth = innerWidth - leftWidth - 1;
205
+ const leftText = padVisible(left, leftWidth);
206
+ const rightText = padVisible(right, rightWidth);
207
+ return `${colors.border}${boxChars.vertical}${colors.reset} ${leftText}${colors.spacer}${rightText} ${colors.border}${boxChars.vertical}${colors.reset}`;
208
+ });
209
+ }
210
+
211
+
212
+
213
+ export const PANEL_HEIGHT = 5;
214
+
215
+ let _fixedEnabled = false;
216
+
217
+ export function enableFixedPanel() {
218
+ if (!process.stdout.isTTY) return false;
219
+ _fixedEnabled = true;
220
+ const rows = process.stdout.rows || 24;
221
+ const scrollBottom = Math.max(1, rows - PANEL_HEIGHT);
222
+ process.stdout.write("\x1b[1;" + scrollBottom + "r");
223
+ return true;
224
+ }
225
+
226
+ export function disableFixedPanel() {
227
+ _fixedEnabled = false;
228
+ process.stdout.write("\x1b[r");
229
+ }
230
+
231
+ export function refreshFixedPanel() {
232
+ if (!_fixedEnabled || !process.stdout.isTTY) return;
233
+ const rows = process.stdout.rows || 24;
234
+ const scrollBottom = Math.max(1, rows - PANEL_HEIGHT);
235
+ process.stdout.write("\x1b[1;" + scrollBottom + "r");
236
+ }
237
+
238
+ export function drawInFixedArea(content) {
239
+ if (!_fixedEnabled || !process.stdout.isTTY) return;
240
+ const rows = process.stdout.rows || 24;
241
+ const startRow = Math.max(1, rows - PANEL_HEIGHT + 1);
242
+ process.stdout.write("\x1b7");
243
+ process.stdout.write("\x1b[" + startRow + ";1H");
244
+ process.stdout.write("\x1b[J");
245
+ process.stdout.write(String(content ?? ""));
246
+ process.stdout.write("\x1b8");
247
+ }
248
+
249
+ export function moveToScrollRegion() {
250
+ if (!process.stdout.isTTY) return;
251
+ const rows = process.stdout.rows || 24;
252
+ const scrollBottom = Math.max(1, rows - PANEL_HEIGHT);
253
+ process.stdout.write("\x1b[" + scrollBottom + ";1H");
254
+ }
255
+
256
+ export function moveToPromptRow() {
257
+ if (!process.stdout.isTTY) return;
258
+ const rows = process.stdout.rows || 24;
259
+ // Position prompt at last scrollable row (just above the fixed panel)
260
+ const promptRow = Math.max(1, rows - PANEL_HEIGHT - 1);
261
+ process.stdout.write("\x1b[" + promptRow + ";1H");
262
+ }
263
+
package/src/cli/tui.js CHANGED
@@ -65,8 +65,6 @@ export function renderLandingTui(snapshot, { colors } = {}) {
65
65
  const padding = Math.max(0, W - leftStatus.length - rightStatus.length);
66
66
  const statusBar = `${bgBlue}${white}${leftStatus}${' '.repeat(padding)}${rightStatus}${reset}`;
67
67
 
68
- const dock = renderInputPanel(snapshot, { colors });
69
-
70
68
  return [
71
69
  ...logoLines,
72
70
  '',
@@ -74,8 +72,7 @@ export function renderLandingTui(snapshot, { colors } = {}) {
74
72
  '',
75
73
  `${white}Directory${reset} ${dim}${snapshot.projectPath}${reset}`,
76
74
  '',
77
- statusBar,
78
- dock.top
75
+ statusBar
79
76
  ].join('\n');
80
77
  }
81
78