winter-super-cli 2026.6.6 → 2026.6.7

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/bin/winter.js CHANGED
@@ -23,6 +23,7 @@ const COMMANDS = new Set([
23
23
  'autopilot', 'plan',
24
24
  'provider', 'providers', 'model', 'models', 'ecc', 'page-agent', 'pageagent',
25
25
  'resources', 'htmlfx', 'memory-vault', 'doctor', 'context', 'scorecard',
26
+ 'tui',
26
27
  ]);
27
28
 
28
29
  function isInteractiveRequest(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "winter-super-cli",
3
- "version": "2026.6.6",
3
+ "version": "2026.6.7",
4
4
  "description": "❄️ AI-Powered Development CLI with Interactive REPL",
5
5
  "type": "module",
6
6
  "main": "bin/winter.js",
@@ -1,6 +1,6 @@
1
1
  import { Spinner } from '../cli/spinner.js';
2
2
  import { colors } from '../cli/snowflake-logo.js';
3
- import { renderBox, terminalWidth, wrapText } from '../cli/terminal-ui.js';
3
+ import { renderToolPanel } from '../cli/tui.js';
4
4
  import { getMutatingToolNames, recordToolCallAdapterStats } from '../cli/tool-runtime.js';
5
5
  import { buildSmallModelAmplification } from '../ai/small-model-amplifier.js';
6
6
 
@@ -38,6 +38,9 @@ export class AgentRuntime {
38
38
  depth,
39
39
  });
40
40
  const maxToolTurns = amplifier.maxToolTurns || 8;
41
+ // Keep self-critique as prompt discipline only. A second runtime model turn
42
+ // duplicates the final answer because the first answer is already rendered.
43
+ amplifier.enforceSelfCritique = false;
41
44
  let forceTextToolFallback = false;
42
45
 
43
46
  try {
@@ -116,7 +119,6 @@ export class AgentRuntime {
116
119
  }
117
120
  }
118
121
 
119
- const BOX_WIDTH = terminalWidth(76, 116, 92);
120
122
  messages.push({
121
123
  role: 'assistant',
122
124
  content: assistantMsg.content || '',
@@ -189,20 +191,12 @@ export class AgentRuntime {
189
191
  const summary = repl.formatToolResultForConsole(canonicalToolName, result);
190
192
  if (summary) {
191
193
  toolSummaries.push(`${canonicalToolName}: ${summary}`);
192
- const statusIcon = result.success === false
193
- ? `${colors.red}${repl.useUnicodeUi ? '✖' : 'x'}${colors.reset}`
194
- : `${colors.green}${repl.useUnicodeUi ? '✓' : 'ok'}${colors.reset}`;
195
- const toolLine = `${icon} ${colors.cyan}${colors.bright}${toolName}${colors.reset}`;
196
- const summaryLines = summary.split('\n').flatMap(line => wrapText(line, BOX_WIDTH - 8));
197
- console.log(renderBox({
198
- title: 'AGENT TOOLS EXECUTION',
199
- width: BOX_WIDTH,
200
- borderColor: colors.magenta,
201
- titleColor: colors.bright,
202
- body: [
203
- toolLine,
204
- ...summaryLines.map((line, index) => index === 0 ? `${statusIcon} ${colors.dim}${line}${colors.reset}` : `${colors.dim}${line}${colors.reset}`),
205
- ],
194
+ console.log(renderToolPanel({
195
+ toolName: `${icon} ${toolName}`,
196
+ summary,
197
+ success: result.success !== false,
198
+ colors,
199
+ title: 'Agent Tools',
206
200
  }));
207
201
  }
208
202
  }
@@ -171,6 +171,21 @@ export function getReasoningBump(tier) {
171
171
  }
172
172
  }
173
173
 
174
+ /**
175
+ * Get a budget multiplier for prompt/context sizing.
176
+ * Bigger models can safely absorb more context and larger tool outputs.
177
+ */
178
+ export function getModelBudgetMultiplier(tier) {
179
+ switch (tier) {
180
+ case MODEL_TIERS.TINY: return 0.5;
181
+ case MODEL_TIERS.SMALL: return 0.75;
182
+ case MODEL_TIERS.MEDIUM: return 1;
183
+ case MODEL_TIERS.LARGE: return 2;
184
+ case MODEL_TIERS.FLAGSHIP: return 4;
185
+ default: return 1;
186
+ }
187
+ }
188
+
174
189
  /**
175
190
  * Build a short string describing model capability for system prompt injection.
176
191
  */
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { formatRuntimeEnvironmentSummary, getRuntimeEnvironment } from '../../cli/runtime-env.js';
8
+ import { getModelBudgetMultiplier } from '../model-capabilities.js';
8
9
 
9
10
  const BASE_PRINCIPLES = [
10
11
  'Execute, don\'t describe - Do the work, don\'t write plans about doing the work',
@@ -34,11 +35,22 @@ function buildEnvironmentSummary() {
34
35
  ].join('\n');
35
36
  }
36
37
 
38
+ function getPromptBudgets(modelTier = '') {
39
+ const scale = getModelBudgetMultiplier(modelTier);
40
+ const compactSystemPrompt = scale <= 0.75;
41
+
42
+ return {
43
+ compactSystemPrompt,
44
+ projectContextBudget: Math.round(3200 * scale),
45
+ resourceContextBudget: Math.round(1200 * scale),
46
+ };
47
+ }
48
+
37
49
  function formatToolList(tools = []) {
38
50
  return tools.length > 0 ? tools.slice(0, 10).join(', ') : '';
39
51
  }
40
52
 
41
- function appendSharedContext(parts, { environment, session, design, resourceContext, context, includeResources = false } = {}) {
53
+ function appendSharedContext(parts, { environment, session, design, resourceContext, context, includeResources = false, resourceContextBudget = 1200 } = {}) {
42
54
  parts.push('## Runtime Environment', environment || buildEnvironmentSummary(), '');
43
55
 
44
56
  if (session?.memory?.length) {
@@ -65,7 +77,7 @@ function appendSharedContext(parts, { environment, session, design, resourceCont
65
77
  }
66
78
 
67
79
  if (includeResources && resourceContext) {
68
- parts.push(resourceContext.trim().slice(0, 1200), '');
80
+ parts.push(resourceContext.trim().slice(0, resourceContextBudget), '');
69
81
  }
70
82
 
71
83
  if (context && typeof context === 'object') {
@@ -74,7 +86,10 @@ function appendSharedContext(parts, { environment, session, design, resourceCont
74
86
  }
75
87
 
76
88
  function buildStandardSystemPrompt(options = {}) {
77
- const { role = 'coding', tools = [], resourceContext } = options;
89
+ const { role = 'coding', tools = [], resourceContext, modelTier = '' } = options;
90
+ const budgets = getPromptBudgets(modelTier);
91
+ const projectContextBudget = options.projectContextBudget ?? budgets.projectContextBudget;
92
+ const compactSystemPrompt = options.compactSystemPrompt ?? budgets.compactSystemPrompt;
78
93
  const parts = [
79
94
  'You are Winter, an expert AI coding assistant.',
80
95
  '',
@@ -93,7 +108,11 @@ function buildStandardSystemPrompt(options = {}) {
93
108
 
94
109
  const toolList = formatToolList(tools);
95
110
  if (toolList) parts.push('## Tools', toolList, '');
96
- appendSharedContext(parts, { ...options, includeResources: Boolean(resourceContext) && (role === 'design' || role === 'ui') });
111
+ appendSharedContext(parts, {
112
+ ...options,
113
+ includeResources: Boolean(resourceContext) && (role === 'design' || role === 'ui'),
114
+ resourceContextBudget: budgets.resourceContextBudget,
115
+ });
97
116
 
98
117
  parts.push('Always respond in Vietnamese.');
99
118
  return parts.filter(Boolean).join('\n');
@@ -109,7 +128,10 @@ export function buildSystemPrompt({
109
128
  resourceContext,
110
129
  modelTier,
111
130
  } = {}) {
131
+ const budgets = getPromptBudgets(modelTier);
112
132
  const options = { role, context, tools, session, environment, design, resourceContext, modelTier };
133
+ options.projectContextBudget = options.projectContextBudget ?? budgets.projectContextBudget;
134
+ options.compactSystemPrompt = options.compactSystemPrompt ?? budgets.compactSystemPrompt;
113
135
  return buildStandardSystemPrompt(options);
114
136
  }
115
137
 
@@ -25,11 +25,64 @@ const RESERVED_CONFIG_SECTIONS = new Set([
25
25
  'ui',
26
26
  ]);
27
27
 
28
+ const DEFAULT_REQUEST_TIMEOUT_MS = 120000;
29
+
28
30
  function isAuthError(error) {
29
31
  const msg = String(error?.message || error || '');
30
32
  return /\b(401|403)\b/.test(msg) || /authentication_error|invalid_api_key|unauthorized|auth\s*failed/i.test(msg);
31
33
  }
32
34
 
35
+ function isRateLimitError(error) {
36
+ const msg = String(error?.message || error || '');
37
+ return error?.status === 429 || /\b429\b|rate[_ -]?limit|tokens per minute|\bTPM\b/i.test(msg);
38
+ }
39
+
40
+ function getRequestTimeoutMs(options = {}) {
41
+ const raw = options.timeoutMs ?? process.env.WINTER_REQUEST_TIMEOUT_MS;
42
+ const value = Number(raw);
43
+ if (Number.isFinite(value) && value > 0) return value;
44
+ return DEFAULT_REQUEST_TIMEOUT_MS;
45
+ }
46
+
47
+ function createTimeoutSignal(timeoutMs, externalSignal = null) {
48
+ const controller = new AbortController();
49
+ let timedOut = false;
50
+ const onAbort = () => {
51
+ controller.abort(externalSignal?.reason || new DOMException('The operation was aborted.', 'AbortError'));
52
+ };
53
+ if (externalSignal?.aborted) {
54
+ onAbort();
55
+ } else if (externalSignal) {
56
+ externalSignal.addEventListener('abort', onAbort, { once: true });
57
+ }
58
+ const timer = setTimeout(() => {
59
+ timedOut = true;
60
+ controller.abort(new Error(`Winter request timed out after ${timeoutMs}ms`));
61
+ }, timeoutMs);
62
+ if (typeof timer.unref === 'function') timer.unref();
63
+ return {
64
+ signal: controller.signal,
65
+ timedOut: () => timedOut,
66
+ cleanup: () => {
67
+ clearTimeout(timer);
68
+ if (externalSignal) externalSignal.removeEventListener('abort', onAbort);
69
+ },
70
+ };
71
+ }
72
+
73
+ function normalizeFetchError(error, provider, timeoutMs, stream = false, timedOut = false) {
74
+ if (timedOut || /timed out/i.test(String(error?.message || ''))) {
75
+ const label = stream ? 'stream' : 'request';
76
+ return new Error(`${provider?.name || 'Provider'} ${label} timed out after ${Math.ceil(timeoutMs / 1000)}s`);
77
+ }
78
+ if (error?.name === 'AbortError' || /abort/i.test(String(error?.message || ''))) {
79
+ const abortError = new Error('AbortError');
80
+ abortError.name = 'AbortError';
81
+ return abortError;
82
+ }
83
+ return error;
84
+ }
85
+
33
86
  export class AIProviderManager {
34
87
  constructor(config) {
35
88
  this.config = config;
@@ -365,7 +418,7 @@ export class AIProviderManager {
365
418
  model: routingModel,
366
419
  reasoning: routingReasoning,
367
420
  reasoningLevel: options.reasoningLevel || executionProfile.reasoningLevel,
368
- }), { maxAttempts: 3, baseDelayMs: 150 });
421
+ }), { maxAttempts: 3, baseDelayMs: 150, retryable: error => !isRateLimitError(error) && !/\b(400|404)\b/.test(String(error?.message || error || '')) });
369
422
  } catch (error) {
370
423
  if (isAuthError(error) && routedProvider !== defaultProvider && defaultProvider) {
371
424
  if (!this._fallbackWarned) {
@@ -377,7 +430,7 @@ export class AIProviderManager {
377
430
  model: options.model || defaultProvider.model,
378
431
  reasoning: routingReasoning,
379
432
  reasoningLevel: options.reasoningLevel || executionProfile.reasoningLevel,
380
- }), { maxAttempts: 1, baseDelayMs: 0 });
433
+ }), { maxAttempts: 1, baseDelayMs: 0, retryable: error => !isRateLimitError(error) && !/\b(400|404)\b/.test(String(error?.message || error || '')) });
381
434
  }
382
435
  throw error;
383
436
  }
@@ -426,6 +479,7 @@ export class AIProviderManager {
426
479
  if (!provider) {
427
480
  throw new Error('No active provider is configured');
428
481
  }
482
+ const timeoutMs = getRequestTimeoutMs(options);
429
483
 
430
484
  const body = {
431
485
  model: options.model || provider.model,
@@ -459,15 +513,26 @@ export class AIProviderManager {
459
513
  headers['Authorization'] = `Bearer ${provider.apiKey}`;
460
514
  }
461
515
 
462
- const response = await fetch(`${provider.baseURL}/chat/completions`, {
463
- method: 'POST',
464
- headers,
465
- body: JSON.stringify(body),
466
- });
516
+ const timeout = createTimeoutSignal(timeoutMs, options.signal || options.abortSignal);
517
+ let response;
518
+ try {
519
+ response = await fetch(`${provider.baseURL}/chat/completions`, {
520
+ method: 'POST',
521
+ headers,
522
+ body: JSON.stringify(body),
523
+ signal: timeout.signal,
524
+ });
525
+ } catch (error) {
526
+ throw normalizeFetchError(error, provider, timeoutMs, false, timeout.timedOut());
527
+ } finally {
528
+ timeout.cleanup();
529
+ }
467
530
 
468
531
  if (!response.ok) {
469
532
  const error = await response.text();
470
- throw new Error(`${provider.name} error (${response.status}): ${error}`);
533
+ const requestError = new Error(`${provider.name} error (${response.status}): ${error}`);
534
+ requestError.status = response.status;
535
+ throw requestError;
471
536
  }
472
537
 
473
538
  return await response.json();
@@ -477,6 +542,7 @@ export class AIProviderManager {
477
542
  if (!provider) {
478
543
  throw new Error('No active provider is configured');
479
544
  }
545
+ const timeoutMs = getRequestTimeoutMs(options);
480
546
 
481
547
  const body = {
482
548
  model: options.model || provider.model,
@@ -515,67 +581,78 @@ export class AIProviderManager {
515
581
  headers['Authorization'] = `Bearer ${provider.apiKey}`;
516
582
  }
517
583
 
518
- const response = await fetch(`${provider.baseURL}/chat/completions`, {
519
- method: 'POST',
520
- headers,
521
- body: JSON.stringify(body),
522
- });
523
-
524
- if (!response.ok) {
525
- const error = await response.text();
526
- throw new Error(`${provider.name} stream error (${response.status}): ${error}`);
527
- }
584
+ const timeout = createTimeoutSignal(timeoutMs, options.signal || options.abortSignal);
585
+ let response;
586
+ try {
587
+ response = await fetch(`${provider.baseURL}/chat/completions`, {
588
+ method: 'POST',
589
+ headers,
590
+ body: JSON.stringify(body),
591
+ signal: timeout.signal,
592
+ });
528
593
 
529
- if (!response.body) {
530
- throw new Error(`${provider.name} did not return a stream body`);
531
- }
594
+ if (!response.ok) {
595
+ const error = await response.text();
596
+ const streamError = new Error(`${provider.name} stream error (${response.status}): ${error}`);
597
+ streamError.status = response.status;
598
+ throw streamError;
599
+ }
532
600
 
533
- const decoder = new TextDecoder();
534
- let buffer = '';
601
+ if (!response.body) {
602
+ throw new Error(`${provider.name} did not return a stream body`);
603
+ }
535
604
 
536
- for await (const chunk of response.body) {
537
- buffer += decoder.decode(chunk, { stream: true });
538
- const lines = buffer.split(/\r?\n/);
539
- buffer = lines.pop() || '';
605
+ const decoder = new TextDecoder();
606
+ let buffer = '';
540
607
 
541
- for (const line of lines) {
542
- const trimmed = line.trim();
543
- if (!trimmed || !trimmed.startsWith('data:')) continue;
608
+ for await (const chunk of response.body) {
609
+ buffer += decoder.decode(chunk, { stream: true });
610
+ const lines = buffer.split(/\r?\n/);
611
+ buffer = lines.pop() || '';
544
612
 
545
- const payload = trimmed.slice(5).trim();
546
- if (!payload || payload === '[DONE]') continue;
613
+ for (const line of lines) {
614
+ const trimmed = line.trim();
615
+ if (!trimmed || !trimmed.startsWith('data:')) continue;
547
616
 
548
- let data;
549
- try {
550
- data = JSON.parse(payload);
551
- } catch {
552
- continue;
553
- }
617
+ const payload = trimmed.slice(5).trim();
618
+ if (!payload || payload === '[DONE]') continue;
554
619
 
555
- const choice = data.choices?.[0] || {};
556
- const content = choice.delta?.content ?? choice.message?.content ?? choice.text ?? '';
557
- yield {
558
- content,
559
- usage: data.usage,
560
- raw: data,
561
- };
562
- }
563
- }
620
+ let data;
621
+ try {
622
+ data = JSON.parse(payload);
623
+ } catch {
624
+ continue;
625
+ }
564
626
 
565
- const tail = buffer.trim();
566
- if (tail.startsWith('data:')) {
567
- const payload = tail.slice(5).trim();
568
- if (payload && payload !== '[DONE]') {
569
- try {
570
- const data = JSON.parse(payload);
571
627
  const choice = data.choices?.[0] || {};
628
+ const content = choice.delta?.content ?? choice.message?.content ?? choice.text ?? '';
572
629
  yield {
573
- content: choice.delta?.content ?? choice.message?.content ?? choice.text ?? '',
630
+ content,
574
631
  usage: data.usage,
575
632
  raw: data,
576
633
  };
577
- } catch {}
634
+ }
635
+ }
636
+
637
+ const tail = buffer.trim();
638
+ if (tail.startsWith('data:')) {
639
+ const payload = tail.slice(5).trim();
640
+ if (payload && payload !== '[DONE]') {
641
+ try {
642
+ const data = JSON.parse(payload);
643
+ const choice = data.choices?.[0] || {};
644
+ yield {
645
+ content: choice.delta?.content ?? choice.message?.content ?? choice.text ?? '',
646
+ usage: data.usage,
647
+ raw: data,
648
+ };
649
+ } catch {}
650
+ }
578
651
  }
652
+ } catch (error) {
653
+ throw normalizeFetchError(error, provider, timeoutMs, true, timeout.timedOut());
654
+ } finally {
655
+ timeout.cleanup();
579
656
  }
580
657
  }
581
658
 
@@ -9,9 +9,9 @@ export function buildSmallModelAmplification({ modelTier = '', workflowProfile =
9
9
  const hint = [
10
10
  '[Winter Strength Amplifier]',
11
11
  '- Every model, including tiny/local/free models, must run at Winter maximum capability.',
12
- '- Mandatory loop: PLAN (requirements + files + risks) -> TOOL ACTIONS -> VERIFY -> SELF-CHECK -> FINAL.',
12
+ '- Mandatory internal loop: PLAN (requirements + files + risks) -> TOOL ACTIONS -> VERIFY -> PRIVATE SELF-CHECK -> FINAL.',
13
13
  '- Do not skip verification. If verification fails, iterate until max loops.',
14
- '- Before final answer, run a private self-critique: missing edge cases, missing tests, over-claims, and incorrect assumptions.',
14
+ '- Before final answer, run a private self-critique silently; do not print the self-check or an improved-answer preface.',
15
15
  '- Prefer concrete evidence from tool outputs over reasoning guesses.',
16
16
  '- Use CodeGraph/codebase index context before broad file reads when available.',
17
17
  ].join('\n');
@@ -15,6 +15,7 @@ import { redactSecrets } from './secret-env.js';
15
15
  import { formatRuntimeEnvironmentSummary, getRuntimeEnvironment } from './runtime-env.js';
16
16
  import { ContextLoader } from './context-loader.js';
17
17
  import { ECCManager } from './ecc.js';
18
+ import { buildTuiSnapshot, renderLandingTui } from './tui.js';
18
19
  import { HtmlFxManager } from '../integrations/htmlfx-manager.js';
19
20
  import { selectWorkflow } from '../ai/workflow-selector.js';
20
21
  import { getProfileBlueprint } from '../ai/profile-blueprints.js';
@@ -73,6 +74,7 @@ export class CommandParser {
73
74
  resources: this.handleResources.bind(this),
74
75
  htmlfx: this.handleHtmlFx.bind(this),
75
76
  'memory-vault': this.handleMemoryVault.bind(this),
77
+ tui: this.handleTui.bind(this),
76
78
  provider: this.handleProvider.bind(this),
77
79
  providers: this.showProviders.bind(this),
78
80
  model: this.handleModel.bind(this),
@@ -121,6 +123,7 @@ export class CommandParser {
121
123
  '/forget': (args) => this.session.clearMemory(args.length > 0 ? args.join(' ') : null),
122
124
  '/memories': () => this.showMemories(),
123
125
  '/memory-vault': () => this.handleMemoryVault(args),
126
+ '/tui': () => this.handleTui(args),
124
127
  '/plans': () => this.showPlans(),
125
128
  '/plan': () => this.handlePlan(args),
126
129
  '/cache': () => this.handleCache(args),
@@ -220,6 +223,15 @@ export class CommandParser {
220
223
  }
221
224
  }
222
225
 
226
+ async handleTui() {
227
+ await this.ai.init?.();
228
+ const snapshot = buildTuiSnapshot(this);
229
+ console.log(`\n${renderLandingTui(snapshot, {
230
+ colors,
231
+ title: 'Winter Dashboard',
232
+ })}\n`);
233
+ }
234
+
223
235
  async searchResourceFiles(root, query, limit = 30) {
224
236
  const matches = [];
225
237
  const needle = String(query || '').toLowerCase();
@@ -226,7 +226,7 @@ export class ContextLoader {
226
226
  this.readTextIfExists(pageAgentAgentsPath, 2200),
227
227
  ]);
228
228
 
229
- const hasRequired = Boolean(karpathy || agents || designReadme || designBrands.length > 0 || pageAgentWinter);
229
+ const hasRequired = Boolean(karpathy || agents || designReadme || designBrands.length > 0 || pageAgentWinter || pageAgentAgents);
230
230
  if (!hasRequired) return '';
231
231
 
232
232
  const lines = [];
@@ -1,6 +1,7 @@
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 { drawInFixedArea, enableFixedPanel, moveToPromptRow, moveToScrollRegion, padVisible, renderBox, terminalWidth } from './terminal-ui.js';
4
+ import { buildTuiSnapshot, renderInputPanel } from './tui.js';
4
5
 
5
6
  export class WinterInputController {
6
7
  constructor(repl) {
@@ -11,9 +12,20 @@ export class WinterInputController {
11
12
  const repl = this.repl;
12
13
  if (!repl.running || repl.readlineClosed) return;
13
14
  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();
15
+
16
+ // Queue indicator
17
+ const queueCount = repl.taskQueue?.length || 0;
18
+ const queueTag = queueCount > 0
19
+ ? ` ${colors.yellow}⧗ ${queueCount} pending${colors.reset}`
20
+ : '';
21
+
22
+ const lines = [panel.top + queueTag, panel.status, panel.hint].filter(l => l && l.trim() !== '');
23
+ process.stdout.write('\n' + lines.join('\n') + '\n');
24
+
25
+ if (typeof repl.rl?.setPrompt === 'function') {
26
+ repl.rl.setPrompt(panel.prompt);
27
+ }
28
+ repl.rl?.prompt?.();
17
29
  }
18
30
 
19
31
  closeInputBox() {
@@ -25,38 +37,10 @@ export class WinterInputController {
25
37
 
26
38
  buildInputPanel() {
27
39
  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
- };
40
+ return renderInputPanel(buildTuiSnapshot(repl), {
41
+ colors,
42
+ width: terminalWidth(66, 124),
43
+ });
60
44
  }
61
45
 
62
46
  installSlashSuggestions() {
@@ -80,10 +64,24 @@ export class WinterInputController {
80
64
  return;
81
65
  }
82
66
 
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}`);
67
+ if (key.name === 'escape') {
68
+ if (repl.isProcessing) {
69
+ // Cancel current AI turn
70
+ repl.isCancelled = true;
71
+ if (repl.spinner) repl.spinner.stop();
72
+ 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}`);
73
+ } else {
74
+ // Double-ESC to end session
75
+ const now = Date.now();
76
+ if (this._lastEscTime && (now - this._lastEscTime) < 500) {
77
+ console.log(`\n\n${colors.cyan}Cảm ơn đã sử dụng Winter!${colors.reset}`);
78
+ console.log(`${colors.yellow}Tiếp tục phiên làm việc:${colors.reset}`);
79
+ console.log(`${colors.bright}${colors.green}winter --session ${repl.session?.getSessionId?.() || ''}${colors.reset}\n`);
80
+ process.exit(0);
81
+ }
82
+ this._lastEscTime = now;
83
+ console.log(`${colors.dim}Press ESC again to end session${colors.reset}`);
84
+ }
87
85
  return;
88
86
  }
89
87
 
@@ -116,13 +114,13 @@ export class WinterInputController {
116
114
 
117
115
  repl.inputQueue = repl.inputQueue
118
116
  .then(async () => {
119
- this.closeInputBox();
117
+ repl.closeInputBox?.();
120
118
  await this.processPastedImageTask(prompt, image);
121
119
  })
122
120
  .catch((error) => {
123
- this.closeInputBox();
121
+ repl.closeInputBox?.();
124
122
  console.log(`\n${colors.red}✖ Paste image error: ${error.message}${colors.reset}\n`);
125
- if (repl.running && !repl.readlineClosed) this.showInputPrompt();
123
+ if (repl.running && !repl.readlineClosed) repl.showInputPrompt?.();
126
124
  });
127
125
  return true;
128
126
  } finally {
@@ -134,15 +132,17 @@ export class WinterInputController {
134
132
  const repl = this.repl;
135
133
  repl.isProcessing = true;
136
134
  repl.isCancelled = false;
135
+ repl.currentAbortController = new AbortController();
137
136
  try {
138
137
  await repl.chat(prompt, [image]);
139
138
  } finally {
140
139
  repl.isProcessing = false;
140
+ repl.currentAbortController = null;
141
141
  if (repl.taskQueue.length > 0) {
142
142
  const nextTask = repl.taskQueue.shift();
143
143
  setTimeout(() => repl.processInputTask(nextTask), 0);
144
144
  } else if (!repl.readlineClosed) {
145
- this.showInputPrompt();
145
+ repl.showInputPrompt?.();
146
146
  }
147
147
  }
148
148
  }
@@ -247,12 +247,23 @@ export class WinterInputController {
247
247
 
248
248
  this.clearSlashMenuRender();
249
249
 
250
+ const ASCII_BOX = {
251
+ topLeft: '+',
252
+ topRight: '+',
253
+ bottomLeft: '+',
254
+ bottomRight: '+',
255
+ horizontal: '-',
256
+ vertical: '|',
257
+ teeLeft: '+',
258
+ teeRight: '+',
259
+ };
250
260
  const rendered = renderBox({
251
261
  title: 'Command Palette',
252
262
  width: terminalWidth(66, 110, 88),
253
263
  borderColor: colors.magenta,
254
264
  titleColor: colors.cyan,
255
265
  body,
266
+ boxChars: ASCII_BOX,
256
267
  });
257
268
 
258
269
  process.stdout.write(`\n${rendered}\n`);