winter-super-cli 2026.6.6 → 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.
@@ -18,6 +18,7 @@ export const SLASH_COMMANDS = [
18
18
  { cmd: '/compress', desc: 'Compress old conversation context' },
19
19
  { cmd: '/context', desc: 'Inspect what Winter sends to the model', usage: '/context [task]' },
20
20
  { cmd: '/scorecard', desc: 'Score Winter against Codebuff/Codex/Claude capability gates' },
21
+ { cmd: '/tui', desc: 'Show Winter TUI dashboard' },
21
22
  { cmd: '/plan', desc: 'Create/view plans' },
22
23
  { cmd: '/plans', desc: 'List active plans' },
23
24
  { cmd: '/tasks', desc: 'List tasks' },
@@ -6,6 +6,11 @@ export const colors = {
6
6
  dim: '\x1b[2m',
7
7
  italic: '\x1b[3m',
8
8
  underline: '\x1b[4m',
9
+ bgBlack: '\x1b[40m',
10
+ bgBlue: '\x1b[44m',
11
+ bgCyan: '\x1b[46m',
12
+ bgWhite: '\x1b[47m',
13
+ bgBrightBlue: '\x1b[104m',
9
14
  cyan: '\x1b[36m',
10
15
  blue: '\x1b[34m',
11
16
  magenta: '\x1b[35m',
@@ -13,112 +18,85 @@ export const colors = {
13
18
  red: '\x1b[31m',
14
19
  yellow: '\x1b[33m',
15
20
  green: '\x1b[32m',
16
- bgCyan: '\x1b[46m',
21
+ brightGreen: '\x1b[92m',
17
22
  bgMagenta: '\x1b[45m',
18
23
  };
19
24
 
20
- const DARK_THEME = { cyan: '\x1b[36m', blue: '\x1b[34m', magenta: '\x1b[35m', white: '\x1b[37m', red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', dim: '\x1b[2m' };
21
- const LIGHT_THEME = { cyan: '\x1b[96m', blue: '\x1b[94m', magenta: '\x1b[95m', white: '\x1b[97m', red: '\x1b[91m', yellow: '\x1b[93m', green: '\x1b[92m', dim: '\x1b[2m' };
25
+ const DARK_THEME = { ...colors };
26
+ const LIGHT_THEME = { ...colors };
22
27
 
23
28
  export function applyColorTheme(theme = 'dark') {
24
- const selected = String(theme || '').toLowerCase() === 'light' ? LIGHT_THEME : DARK_THEME;
25
- Object.assign(colors, selected, { theme: String(theme || 'dark').toLowerCase() === 'light' ? 'light' : 'dark' });
29
+ colors.theme = theme;
26
30
  return colors.theme;
27
31
  }
28
32
 
29
- const snowflakeArt = String.raw`
30
- ii
31
- .i;
32
- , ;i ,
33
- ;i::;:i;
34
- ,i1:
35
- :,, ,, ;:,::i,;:i: , i ii;
36
- ,ii;;: ii ,1 :;iiiii: ;1 .1, i;:,
37
- :::ii,,;1. ;iii; :1:,:i1;,
38
- ,;i:,i:i1:i, ,;i; ;i;1i;; :;:
39
- ,. ,i1iii11, ,;, ,11ii;ii;
40
- :;: ::,,:i111i: ,, ,:
41
- :: ,,, :i111i:,,,: :;:
42
- :ii;ii11: ,;, 11iii1i:
43
- :i: ;;i1;;; ;i;, ,i:1i:i,:i;:
44
- ,;1i;,:1; ;iiii 1;,,i1::,
45
- ,,;i, 1, i; :iiiii;: 1: ii ,;;ii,
46
- ;ii i, , :i::,;:,::;. ,: ,,:
47
- :ii:
48
- ;i:;;:i;
49
- ;i
50
- ;i,
51
- ;i.
52
- `;
53
-
54
- export const miniLogo = `${colors.cyan}${supportsUnicodeUi() ? '❄' : '*'}${colors.reset}`;
33
+ export const miniLogo = `${colors.cyan}❄${colors.reset}`;
55
34
 
56
35
  export function welcomeBanner(version, info = {}) {
57
- const pPath = info.project || 'Unknown';
58
- const displayPath = pPath; // Hiển thị đầy đủ đường dẫn chính xác
59
- const pId = info.session || 'New';
36
+ const displayPath = info.project || 'Unknown';
60
37
  const provider = info.provider || 'default';
61
38
  const model = info.model || 'unknown';
62
39
 
63
- // Tính toán chiều rộng động (nhỏ hơn 5% cửa sổ, tối thiểu 60, tối đa 100 cho đẹp)
64
- const columns = process.stdout.columns || 80;
65
- const W = Math.max(60, Math.min(Math.floor(columns * 0.95), 100));
66
- const unicode = supportsUnicodeUi();
67
- const dot = `${colors.green}${unicode ? '●' : '*'}${colors.reset}`;
68
-
69
- // Căn giữa Snowflake Art
70
- const artLines = snowflakeArt.split('\n').filter(l => l.trim() !== '' || l.length > 0);
71
- // Tìm chiều dài thực tế lớn nhất của art (bỏ qua khoảng trắng thừa ở cuối)
72
- const maxArtWidth = Math.max(...artLines.map(l => l.trimEnd().length));
73
- const artPadding = Math.max(0, Math.floor((W - maxArtWidth) / 2));
74
- const centeredArt = artLines.map(l => ' '.repeat(artPadding) + l.trimEnd()).join('\n');
75
-
76
- // Căn giữa tiêu đề
77
- const title = `W I N T E R v${version}`;
78
- const titlePadding = Math.max(0, Math.floor((W - title.length) / 2));
79
-
80
- const subtitle = `Build by Atus | fb: iam.anhtu | github: anhtu1707`;
81
- const subPadding = Math.max(0, Math.floor((W - subtitle.length) / 2));
82
-
83
- const infoWidth = Math.max(60, Math.min(terminalWidth(60, 100, 80), 100));
84
- const banner = `${colors.cyan}${centeredArt}${colors.reset}
85
-
86
- ${' '.repeat(titlePadding)}${colors.bright}${colors.magenta}W I N T E R${colors.reset} ${colors.dim}v${version}${colors.reset}
87
- ${' '.repeat(subPadding)}${colors.dim}${subtitle}${colors.reset}
88
- ${renderBox({
89
- title: '',
90
- width: infoWidth,
91
- borderColor: colors.blue,
92
- titleColor: colors.blue,
93
- body: [
94
- `${dot} ${colors.cyan}Project:${colors.reset} ${colors.green}${displayPath}${colors.reset}`,
95
- `${dot} ${colors.cyan}Model: ${colors.reset} ${model} ${colors.dim}(${provider})${colors.reset}`,
96
- `${dot} ${colors.cyan}Session:${colors.reset} ${colors.yellow}${pId}${colors.reset}`,
97
- `${colors.dim}Gõ ${colors.cyan}/help${colors.dim} để xem lệnh · ${colors.cyan}/auto${colors.dim} chế độ tự sửa · ${colors.cyan}ESC${colors.dim} để hủy${colors.reset}`,
98
- ],
99
- })}
100
- `;
40
+ const W = Math.max(60, Math.min(process.stdout.columns || 80, 100));
41
+ const white = colors.white;
42
+ const dim = colors.dim;
43
+ const bright = colors.bright;
44
+ const reset = colors.reset;
45
+ const green = '\x1b[92m';
46
+ const cyan = '\x1b[36m';
47
+ const bgBrightBlue = '\x1b[104m';
48
+ const bgBlue = '\x1b[48;5;236m';
49
+
50
+ const logo = [
51
+ '╔══════════════════════════════════════════════════════════════════╗',
52
+ '║ ║',
53
+ '║ ██╗ ██╗██╗███╗ ██╗████████╗███████╗██████╗ ║',
54
+ '║ ██║ ██║██║████╗ ██║╚══██╔══╝██╔════╝██╔══██╗ ║',
55
+ '║ ██║ █╗ ██║██║██╔██╗██║ ██║ █████╗ ██████╔╝ ║',
56
+ '║ ██║███╗██║██║██║╚████║ ██║ ██╔══╝ ██╔══██╗ ║',
57
+ '║ ╚███╔███╔╝██║██║ ╚███║ ██║ ███████╗██║ ██║ ║',
58
+ '║ ╚══╝╚══╝ ╚═╝╚═╝ ╚══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ║',
59
+ '║ ║',
60
+ '║ A I C o d i n g A s s i s t a n t ║',
61
+ '║══════════════════════════════════════════════════════════════════║',
62
+ '║ fb.com/iam.anhtu ║ github.com/anhtu1707 ║',
63
+ '╚══════════════════════════════════════════════════════════════════╝',
64
+ ];
65
+ const logoLines = logo.map(line => `${bright}${cyan}${line}${reset}`);
66
+
67
+ const leftStatus = ` ${provider} · ${model} `;
68
+ const rightStatus = ` ESC×2 exit · /help `;
69
+ const padding = Math.max(0, W - leftStatus.length - rightStatus.length);
70
+ const statusBar = `${bgBlue}${white}${leftStatus}${' '.repeat(padding)}${rightStatus}${reset}`;
71
+
72
+ const banner = [
73
+ ...logoLines,
74
+ '',
75
+ `${white}Winter will run commands on your behalf to help you build.${reset}`,
76
+ '',
77
+ `${white}Directory${reset} ${dim}${displayPath}${reset}`,
78
+ '',
79
+ statusBar
80
+ ].join('\n');
101
81
  return banner;
102
82
  }
103
83
 
104
84
  export const statusIcons = {
105
- online: `${colors.green}${supportsUnicodeUi() ? '●' : 'on'}${colors.reset}`,
106
- offline: `${colors.dim}${supportsUnicodeUi() ? '○' : 'off'}${colors.reset}`,
107
- warning: `${colors.yellow}${supportsUnicodeUi() ? '◆' : '!'}${colors.reset}`,
108
- error: `${colors.red}${supportsUnicodeUi() ? '✖' : 'x'}${colors.reset}`,
109
- success: `${colors.green}${supportsUnicodeUi() ? '✓' : 'ok'}${colors.reset}`,
110
- thinking: `${colors.cyan}${supportsUnicodeUi() ? '◉' : '...'}${colors.reset}`,
111
- queue: `${colors.magenta}${supportsUnicodeUi() ? '◎' : 'queue'}${colors.reset}`,
85
+ online: `${colors.green}●${colors.reset}`,
86
+ offline: `${colors.dim}○${colors.reset}`,
87
+ warning: `${colors.yellow}◆${colors.reset}`,
88
+ error: `${colors.red}✖${colors.reset}`,
89
+ success: `${colors.green}✓${colors.reset}`,
90
+ thinking: `${colors.cyan}◉${colors.reset}`,
91
+ queue: `${colors.magenta}◎${colors.reset}`,
112
92
  };
113
93
 
114
- export function sessionIndicator(sessionId) {
115
- const id = sessionId ? sessionId.substring(0, 8) : 'none';
116
- return `${colors.dim}[${colors.cyan}session:${id}${colors.dim}]${colors.reset}`;
94
+ export function providerStatus(name, status) {
95
+ return `${statusIcons[status] || statusIcons.offline} ${name}`;
117
96
  }
118
97
 
119
- export function providerStatus(name, status) {
120
- const icon = status === 'ready' ? statusIcons.online : statusIcons.offline;
121
- return `${icon} ${name}`;
98
+ export function sessionIndicator(sessionId) {
99
+ return `session:${sessionId}`;
122
100
  }
123
101
 
124
- export const snowflake = snowflakeArt;
102
+ export const snowflake = '';
@@ -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();
@@ -1,6 +1,6 @@
1
1
  const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
2
2
  const ZERO_WIDTH_PATTERN = /[\u200B-\u200D\u2060\uFE0E\uFE0F]/u;
3
- const WIDE_CODE_POINT_RANGES = [
3
+ const WIDE_CODE_POINT_RANGES = [
4
4
  [0x1100, 0x115f],
5
5
  [0x2329, 0x232a],
6
6
  [0x2e80, 0x303e],
@@ -14,30 +14,30 @@ const WIDE_CODE_POINT_RANGES = [
14
14
  [0x1f300, 0x1f64f],
15
15
  [0x1f680, 0x1f6ff],
16
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
- };
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
41
 
42
42
  export function stripAnsi(text) {
43
43
  return String(text ?? '').replace(ANSI_PATTERN, '');
@@ -63,20 +63,19 @@ export function charDisplayWidth(char) {
63
63
  return 1;
64
64
  }
65
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
-
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
- if (env.WINTER_UNICODE_UI === '1' || env.WINTER_UNICODE_UI === 'true') return true;
74
- if (platform !== 'win32') return true;
75
- return Boolean(env.WT_SESSION || env.TERM_PROGRAM || env.TERM || env.ConEmuANSI === 'ON');
73
+ return true;
76
74
  }
77
75
 
78
76
  export function getBoxChars() {
79
- return supportsUnicodeUi() ? UNICODE_BOX : ASCII_BOX;
77
+ if (supportsUnicodeUi()) return UNICODE_BOX;
78
+ return ASCII_BOX;
80
79
  }
81
80
 
82
81
  export function padVisible(text, width, fill = ' ') {
@@ -124,38 +123,38 @@ export function wrapText(text, width) {
124
123
  return output;
125
124
  }
126
125
 
127
- export function chunkText(text, width) {
128
- const chars = Array.from(stripAnsi(text));
129
- const chunks = [];
130
- let current = '';
131
- let currentWidth = 0;
132
- for (const char of chars) {
133
- const charWidth = charDisplayWidth(char);
134
- if (current && currentWidth + charWidth > width) {
135
- chunks.push(current);
136
- current = '';
137
- currentWidth = 0;
138
- }
139
- current += char;
140
- currentWidth += charWidth;
141
- }
142
- if (current) chunks.push(current);
143
- return chunks.length > 0 ? chunks : [''];
144
- }
145
-
146
- export function renderBox({
147
- title = '',
148
- body = [],
149
- width,
150
- borderColor = '\x1b[35m',
151
- titleColor = '\x1b[36m',
152
- reset = '\x1b[0m',
153
- boxChars = getBoxChars(),
154
- } = {}) {
155
- const innerWidth = Math.max(28, (width || terminalWidth()) - 4);
156
- const top = `${borderColor}${boxChars.topLeft}${boxChars.horizontal.repeat(innerWidth)}${boxChars.topRight}${reset}`;
157
- const bottom = `${borderColor}${boxChars.bottomLeft}${boxChars.horizontal.repeat(innerWidth)}${boxChars.bottomRight}${reset}`;
158
- const lines = [];
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 = [];
159
158
  const titleText = title ? ` ${title} ` : '';
160
159
 
161
160
  if (titleText) {
@@ -166,45 +165,99 @@ export function renderBox({
166
165
  const padding = Math.max(0, innerWidth - visible);
167
166
  const left = index === 0 ? Math.floor(padding / 2) : 0;
168
167
  const right = index === 0 ? padding - left : padding;
169
- lines.push(`${borderColor}${boxChars.vertical}${reset}${' '.repeat(left)}${titleColor}${plainSegment}${reset}${' '.repeat(right)}${borderColor}${boxChars.vertical}${reset}`);
170
- });
171
- lines.push(`${borderColor}${boxChars.teeLeft}${boxChars.horizontal.repeat(innerWidth)}${boxChars.teeRight}${reset}`);
172
- }
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
+ }
173
172
 
174
173
  for (const item of body) {
175
174
  const rawText = String(item ?? '');
176
175
  if (visibleWidth(rawText) <= innerWidth) {
177
176
  const visible = visibleWidth(rawText);
178
177
  const padding = Math.max(0, innerWidth - visible);
179
- lines.push(`${borderColor}${boxChars.vertical}${reset} ${rawText}${' '.repeat(Math.max(0, padding - 1))}${borderColor}${boxChars.vertical}${reset}`);
180
- continue;
181
- }
178
+ lines.push(`${borderColor}${boxChars.vertical}${reset} ${rawText}${' '.repeat(Math.max(0, padding - 1))}${borderColor}${boxChars.vertical}${reset}`);
179
+ continue;
180
+ }
182
181
 
183
182
  const wrapped = wrapText(rawText, innerWidth);
184
183
  if (wrapped.length === 0) {
185
- lines.push(`${borderColor}${boxChars.vertical}${reset} ${' '.repeat(Math.max(0, innerWidth - 1))}${borderColor}${boxChars.vertical}${reset}`);
186
- continue;
187
- }
184
+ lines.push(`${borderColor}${boxChars.vertical}${reset} ${' '.repeat(Math.max(0, innerWidth - 1))}${borderColor}${boxChars.vertical}${reset}`);
185
+ continue;
186
+ }
188
187
 
189
188
  for (const segment of wrapped) {
190
189
  const text = stripAnsi(segment);
191
190
  const visible = visibleWidth(text);
192
191
  const padding = Math.max(0, innerWidth - visible);
193
- lines.push(`${borderColor}${boxChars.vertical}${reset} ${text}${' '.repeat(Math.max(0, padding - 1))}${borderColor}${boxChars.vertical}${reset}`);
194
- }
195
- }
192
+ lines.push(`${borderColor}${boxChars.vertical}${reset} ${text}${' '.repeat(Math.max(0, padding - 1))}${borderColor}${boxChars.vertical}${reset}`);
193
+ }
194
+ }
196
195
 
197
196
  return [top, ...lines, bottom].join('\n');
198
197
  }
199
198
 
200
- export function renderKeyValueRows(rows, width, colors) {
201
- const innerWidth = Math.max(28, (width || terminalWidth()) - 4);
202
- const boxChars = getBoxChars();
203
- return rows.map(([left, right]) => {
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]) => {
204
203
  const leftWidth = Math.floor(innerWidth * 0.5);
205
204
  const rightWidth = innerWidth - leftWidth - 1;
206
205
  const leftText = padVisible(left, leftWidth);
207
206
  const rightText = padVisible(right, rightWidth);
208
- return `${colors.border}${boxChars.vertical}${colors.reset} ${leftText}${colors.spacer}${rightText} ${colors.border}${boxChars.vertical}${colors.reset}`;
209
- });
210
- }
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
+
@@ -1,4 +1,5 @@
1
1
  import { TokenJuice } from '../context/token-juice.js';
2
+ import { getModelBudgetMultiplier } from '../ai/model-capabilities.js';
2
3
 
3
4
  export function getMutatingToolNames() {
4
5
  return new Set([
@@ -18,7 +19,8 @@ export function getMutatingToolNames() {
18
19
  ]);
19
20
  }
20
21
 
21
- export function getToolResultPromptBudget(toolName, compact = false) {
22
+ export function getToolResultPromptBudget(toolName, compact = false, modelTier = '') {
23
+ const scale = getModelBudgetMultiplier(modelTier);
22
24
  const budgets = {
23
25
  Read: compact ? 2800 : 5000,
24
26
  Grep: compact ? 2200 : 4200,
@@ -29,19 +31,20 @@ export function getToolResultPromptBudget(toolName, compact = false) {
29
31
  BrowserDebug: compact ? 2200 : 4200,
30
32
  NotebookRead: compact ? 2600 : 5000,
31
33
  };
32
- return budgets[toolName] || (compact ? 1800 : 3200);
34
+ return Math.max(400, Math.round((budgets[toolName] || (compact ? 1800 : 3200)) * scale));
33
35
  }
34
36
 
35
37
  export function buildPromptToolResult({
36
38
  toolName,
37
39
  result,
38
40
  compact = false,
41
+ modelTier = '',
39
42
  compactText = (value, maxChars) => String(value || '').slice(0, maxChars),
40
43
  summarizeToolResult = value => ({ ...value }),
41
44
  } = {}) {
42
45
  if (!result || typeof result !== 'object') return result;
43
46
 
44
- const budget = getToolResultPromptBudget(toolName, compact);
47
+ const budget = getToolResultPromptBudget(toolName, compact, modelTier);
45
48
  const copy = summarizeToolResult(result);
46
49
  const preserveKeys = ['content', 'stdout', 'stderr', 'diff', 'matches', 'files', 'cells'];
47
50
  let remaining = budget;
@@ -78,6 +81,7 @@ export async function buildPromptToolResultWithTokenJuice({
78
81
  projectPath = process.cwd(),
79
82
  tokenJuice,
80
83
  compact = false,
84
+ modelTier = '',
81
85
  compactText = (value, maxChars) => String(value || '').slice(0, maxChars),
82
86
  summarizeToolResult = value => ({ ...value }),
83
87
  } = {}) {
@@ -85,6 +89,7 @@ export async function buildPromptToolResultWithTokenJuice({
85
89
  toolName,
86
90
  result,
87
91
  compact,
92
+ modelTier,
88
93
  compactText,
89
94
  summarizeToolResult,
90
95
  });