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.
@@ -1,6 +1,8 @@
1
1
  import readline from 'readline';
2
2
  import { colors } from './snowflake-logo.js';
3
- import { padVisible, renderBox, terminalWidth, visibleWidth } from './terminal-ui.js';
3
+ import { padVisible, renderBox, terminalWidth } from './terminal-ui.js';
4
+ import { buildTuiSnapshot, renderInputPanel } from './tui.js';
5
+ import { terminalManager } from './terminal-manager.js';
4
6
 
5
7
  export class WinterInputController {
6
8
  constructor(repl) {
@@ -11,52 +13,66 @@ export class WinterInputController {
11
13
  const repl = this.repl;
12
14
  if (!repl.running || repl.readlineClosed) return;
13
15
  const panel = this.buildInputPanel();
14
- process.stdout.write(`\n${panel.top}\n${panel.status}\n${panel.hint}\n`);
15
- repl.rl.setPrompt(panel.prompt);
16
- repl.rl.prompt();
16
+
17
+ // Queue indicator
18
+ const queueCount = repl.taskQueue?.length || 0;
19
+ const queueTag = queueCount > 0
20
+ ? ` ${colors.yellow}⧗ ${queueCount} pending${colors.reset}`
21
+ : '';
22
+
23
+ const lines = [panel.top + queueTag, panel.status, panel.hint].filter(l => l && l.trim() !== '');
24
+
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();
17
57
  }
18
58
 
19
59
  closeInputBox() {
20
60
  const repl = this.repl;
21
61
  if (!repl.running || repl.readlineClosed) return;
62
+
63
+ terminalManager.hidePrompt();
64
+ terminalManager.setPromptState(false);
65
+
22
66
  const panel = this.buildInputPanel();
23
67
  process.stdout.write(`${panel.bottom}\n`);
24
68
  }
25
69
 
26
70
  buildInputPanel() {
27
71
  const repl = this.repl;
28
- const width = Math.max(64, terminalWidth(66, 124) - 2);
29
- const box = repl.useUnicodeUi
30
- ? { topLeft: '╭', topRight: '╮', bottomLeft: '╰', bottomRight: '╯', horizontal: '─', vertical: '│' }
31
- : { topLeft: '+', topRight: '+', bottomLeft: '+', bottomRight: '+', horizontal: '-', vertical: '|' };
32
- const provider = repl.ai?.getActiveProvider?.() || 'provider';
33
- const model = repl.ai?.providers?.[provider]?.model || 'model';
34
- const sessionId = repl.session?.getSessionId?.()?.slice(0, 8) || 'session';
35
- const projectName = repl.projectPath ? repl.projectPath.split(/[\\/]/).filter(Boolean).pop() : 'project';
36
- const queueText = repl.taskQueue?.length > 0 ? `queue:${repl.taskQueue.length}` : 'ready';
37
- const title = ' Winter CLI ';
38
- const titleWidth = visibleWidth(title);
39
- const topFill = Math.max(0, width - titleWidth);
40
- const leftFill = Math.floor(topFill / 2);
41
- const rightFill = topFill - leftFill;
42
- const statusText = [
43
- `model ${provider}/${model}`,
44
- `project ${projectName}`,
45
- `session ${sessionId}`,
46
- queueText,
47
- ].join(' ');
48
- const hintText = '@file @Agent task !cmd Ctrl+V image /context /doctor full';
49
- const hintInnerWidth = Math.max(20, width - 2);
50
- const status = `${colors.magenta}${box.vertical}${colors.reset} ${colors.dim}${padVisible(statusText, hintInnerWidth)}${colors.reset} ${colors.magenta}${box.vertical}${colors.reset}`;
51
- const hint = `${colors.magenta}${box.vertical}${colors.reset} ${colors.dim}${padVisible(hintText, hintInnerWidth)}${colors.reset} ${colors.magenta}${box.vertical}${colors.reset}`;
52
- const prompt = `${colors.magenta}${box.vertical}${colors.reset} ${colors.bright}${colors.cyan}winter${colors.reset}${colors.dim} > ${colors.reset}`;
53
- return {
54
- top: `${colors.magenta}${box.topLeft}${box.horizontal.repeat(leftFill)}${title}${box.horizontal.repeat(rightFill)}${box.topRight}${colors.reset}`,
55
- status,
56
- hint,
57
- prompt,
58
- bottom: `${colors.magenta}${box.bottomLeft}${box.horizontal.repeat(width)}${box.bottomRight}${colors.reset}`,
59
- };
72
+ return renderInputPanel(buildTuiSnapshot(repl), {
73
+ colors,
74
+ width: terminalWidth(66, 124),
75
+ });
60
76
  }
61
77
 
62
78
  installSlashSuggestions() {
@@ -80,10 +96,24 @@ export class WinterInputController {
80
96
  return;
81
97
  }
82
98
 
83
- if (key.name === 'escape' && repl.isProcessing) {
84
- repl.isCancelled = true;
85
- if (repl.spinner) repl.spinner.stop();
86
- console.log(`\n${colors.red}[ Đã nhận lệnh HỦY... AI sẽ kết thúc ở thao tác tiếp theo ]${colors.reset}`);
99
+ if (key.name === 'escape') {
100
+ if (repl.isProcessing) {
101
+ // Cancel current AI turn
102
+ repl.isCancelled = true;
103
+ if (repl.spinner) repl.spinner.stop();
104
+ console.log(`\n${colors.red}[ Đã nhận lệnh HỦY... AI sẽ kết thúc ở thao tác tiếp theo ]${colors.reset}`);
105
+ } else {
106
+ // Double-ESC to end session
107
+ const now = Date.now();
108
+ if (this._lastEscTime && (now - this._lastEscTime) < 500) {
109
+ console.log(`\n\n${colors.cyan}Cảm ơn đã sử dụng Winter!${colors.reset}`);
110
+ console.log(`${colors.yellow}Tiếp tục phiên làm việc:${colors.reset}`);
111
+ console.log(`${colors.bright}${colors.green}winter --session ${repl.session?.getSessionId?.() || ''}${colors.reset}\n`);
112
+ process.exit(0);
113
+ }
114
+ this._lastEscTime = now;
115
+ console.log(`${colors.dim}Press ESC again to end session${colors.reset}`);
116
+ }
87
117
  return;
88
118
  }
89
119
 
@@ -116,13 +146,13 @@ export class WinterInputController {
116
146
 
117
147
  repl.inputQueue = repl.inputQueue
118
148
  .then(async () => {
119
- this.closeInputBox();
149
+ repl.closeInputBox?.();
120
150
  await this.processPastedImageTask(prompt, image);
121
151
  })
122
152
  .catch((error) => {
123
- this.closeInputBox();
153
+ repl.closeInputBox?.();
124
154
  console.log(`\n${colors.red}✖ Paste image error: ${error.message}${colors.reset}\n`);
125
- if (repl.running && !repl.readlineClosed) this.showInputPrompt();
155
+ if (repl.running && !repl.readlineClosed) repl.showInputPrompt?.();
126
156
  });
127
157
  return true;
128
158
  } finally {
@@ -134,15 +164,17 @@ export class WinterInputController {
134
164
  const repl = this.repl;
135
165
  repl.isProcessing = true;
136
166
  repl.isCancelled = false;
167
+ repl.currentAbortController = new AbortController();
137
168
  try {
138
169
  await repl.chat(prompt, [image]);
139
170
  } finally {
140
171
  repl.isProcessing = false;
172
+ repl.currentAbortController = null;
141
173
  if (repl.taskQueue.length > 0) {
142
174
  const nextTask = repl.taskQueue.shift();
143
175
  setTimeout(() => repl.processInputTask(nextTask), 0);
144
176
  } else if (!repl.readlineClosed) {
145
- this.showInputPrompt();
177
+ repl.showInputPrompt?.();
146
178
  }
147
179
  }
148
180
  }
@@ -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 = {}) {
@@ -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;
@@ -1,4 +1,5 @@
1
1
  import { formatRuntimeEnvironmentSummary, getRuntimeEnvironment } from './runtime-env.js';
2
+ import { getModelBudgetMultiplier } from '../ai/model-capabilities.js';
2
3
 
3
4
  /**
4
5
  * PromptBuilder — Builds system prompts for Winter CLI agents.
@@ -49,26 +50,32 @@ export class PromptBuilder {
49
50
  const sessionContext = this.session?.getContext?.() || {};
50
51
  const environmentSummary = this.tools?.getRuntimeEnvironmentSummary?.() || this._defaultEnvironmentSummary();
51
52
  const requiredLocalResources = this.getRequiredLocalResources();
52
- const projectContextBudget = options.projectContextBudget || 3200;
53
- const compactSystemPrompt = options.compactSystemPrompt || projectContextBudget <= 1600;
53
+ const scale = getModelBudgetMultiplier(options.modelTier);
54
+ const projectContextBudget = options.projectContextBudget || Math.round(3200 * scale);
55
+ const compactSystemPrompt = options.compactSystemPrompt ?? (scale <= 0.75);
56
+ const memoryBudget = Math.round(1200 * scale);
57
+ const planBudget = Math.round(1200 * scale);
58
+ const requiredResourcesBudget = Math.round((compactSystemPrompt ? 1200 : 1600) * scale);
59
+ const workflowBudget = Math.round(900 * scale);
60
+ const blueprintBudget = Math.round(700 * scale);
54
61
 
55
62
  const memoryStr = memories.length > 0
56
- ? this._formatMemories(memories)
63
+ ? this._formatMemories(memories, { maxTotalChars: memoryBudget })
57
64
  : '';
58
65
  const requiredResourcesStr = requiredLocalResources
59
- ? `\n## Required Local Resource Rules\n${this._compactText(requiredLocalResources, 1800, 'required local resources')}`
66
+ ? `\n## Required Local Resource Rules\n${this._compactText(requiredLocalResources, requiredResourcesBudget, 'required local resources')}`
60
67
  : '';
61
68
  const plansStr = plans.length > 0
62
- ? this._formatPlans(plans)
69
+ ? this._formatPlans(plans, { maxTotalChars: planBudget })
63
70
  : '';
64
71
  const skillsStr = Array.isArray(sessionContext.activeSkills) && sessionContext.activeSkills.length > 0
65
72
  ? `\n## Auto-applied Skills\n${sessionContext.activeSkills.slice(0, 12).map(skill => `- ${skill}`).join('\n')}${sessionContext.activeSkills.length > 12 ? '\n- ...' : ''}`
66
73
  : '';
67
74
  const workflowStr = sessionContext.workflowHints
68
- ? `\n## Workflow Auto-Selection\n${this._compactText(sessionContext.workflowHints, 900, 'workflow hints')}`
75
+ ? `\n## Workflow Auto-Selection\n${this._compactText(sessionContext.workflowHints, workflowBudget, 'workflow hints')}`
69
76
  : '';
70
77
  const blueprintStr = sessionContext.workflowBlueprint
71
- ? `\n## Profile Blueprint\n${this._compactText(sessionContext.workflowBlueprint, 700, 'workflow blueprint')}`
78
+ ? `\n## Profile Blueprint\n${this._compactText(sessionContext.workflowBlueprint, blueprintBudget, 'workflow blueprint')}`
72
79
  : '';
73
80
  const startupPlanStr = sessionContext.bootstrapPlan?.title
74
81
  ? `\n## Startup Plan\n- ${sessionContext.bootstrapPlan.title}: ${sessionContext.bootstrapPlan.description}`
@@ -164,12 +171,14 @@ export class PromptBuilder {
164
171
  const plans = this.session?.getPlans?.() || [];
165
172
  const sessionContext = this.session?.getContext?.() || {};
166
173
  const requiredLocalResources = this.getRequiredLocalResources();
174
+ const scale = getModelBudgetMultiplier(this.ai?._modelTier || '');
175
+ const projectContextBudget = Math.round(3200 * (scale || 1));
167
176
 
168
- const memoryStr = memories.length > 0 ? this._formatMemories(memories, { maxTotalChars: 900 }) : '';
177
+ const memoryStr = memories.length > 0 ? this._formatMemories(memories, { maxTotalChars: Math.round(900 * (scale || 1)) }) : '';
169
178
  const requiredResourcesStr = requiredLocalResources
170
- ? `\n## Required Local Resource Rules\n${this._compactText(requiredLocalResources, 1600, 'required local resources')}`
179
+ ? `\n## Required Local Resource Rules\n${this._compactText(requiredLocalResources, Math.round(1600 * (scale || 1)), 'required local resources')}`
171
180
  : '';
172
- const plansStr = plans.length > 0 ? this._formatPlans(plans, { maxTotalChars: 900 }) : '';
181
+ const plansStr = plans.length > 0 ? this._formatPlans(plans, { maxTotalChars: Math.round(900 * (scale || 1)) }) : '';
173
182
  const skillsStr = Array.isArray(sessionContext.activeSkills) && sessionContext.activeSkills.length > 0
174
183
  ? `\n## Auto-applied Skills\n${sessionContext.activeSkills.slice(0, 12).map(skill => `- ${skill}`).join('\n')}${sessionContext.activeSkills.length > 12 ? '\n- ...' : ''}`
175
184
  : '';
@@ -217,7 +226,7 @@ export class PromptBuilder {
217
226
  `Working directory: ${this.projectPath}`,
218
227
  `Current session: ${this.session?.getSessionId?.()?.substring(0, 8) || 'unknown'}`,
219
228
  `${requiredResourcesStr}${memoryStr}${plansStr}${skillsStr}${startupPlanStr}`,
220
- context ? `\n## Project Context\n${this._compactText(context, 3200, 'project context')}` : '',
229
+ context ? `\n## Project Context\n${this._compactText(context, projectContextBudget, 'project context')}` : '',
221
230
  ].join('\n');
222
231
  }
223
232
 
@@ -354,6 +354,9 @@ export async function handleSlashCommand(repl, input) {
354
354
  case '/scorecard':
355
355
  await repl.showCapabilityScorecard();
356
356
  return;
357
+ case '/tui':
358
+ repl.showTuiDashboard();
359
+ return;
357
360
 
358
361
  // Git Auto-Pilot
359
362
  case '/commit':