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 +1 -1
- package/src/ai/providers.js +25 -1
- package/src/cli/repl.js +1 -4
- package/src/cli/runtime-env.js +13 -0
- package/src/cli/terminal-ui.js +254 -254
- package/src/cli/tui.js +1 -4
package/package.json
CHANGED
package/src/ai/providers.js
CHANGED
|
@@ -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
|
-
|
|
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)) {
|
package/src/cli/runtime-env.js
CHANGED
|
@@ -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}`,
|
package/src/cli/terminal-ui.js
CHANGED
|
@@ -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
|
|