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 +1 -0
- package/package.json +1 -1
- package/src/agent/runtime.js +10 -16
- package/src/ai/model-capabilities.js +15 -0
- package/src/ai/prompts/system-prompt.js +26 -4
- package/src/ai/providers.js +132 -55
- package/src/ai/small-model-amplifier.js +2 -2
- package/src/cli/commands.js +12 -0
- package/src/cli/context-loader.js +1 -1
- package/src/cli/input-controller.js +55 -44
- package/src/cli/prompt-builder.js +20 -11
- package/src/cli/repl-commands.js +3 -0
- package/src/cli/repl.js +187 -322
- package/src/cli/slash-commands.js +1 -0
- package/src/cli/snowflake-logo.js +64 -86
- package/src/cli/terminal-ui.js +139 -85
- package/src/cli/tool-runtime.js +8 -3
- package/src/cli/tui.js +181 -0
- package/src/context/token-juice.js +37 -10
- package/src/tools/executor.js +78 -3
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
package/src/agent/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Spinner } from '../cli/spinner.js';
|
|
2
2
|
import { colors } from '../cli/snowflake-logo.js';
|
|
3
|
-
import {
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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,
|
|
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, {
|
|
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
|
|
package/src/ai/providers.js
CHANGED
|
@@ -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
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
530
|
-
|
|
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
|
-
|
|
534
|
-
|
|
601
|
+
if (!response.body) {
|
|
602
|
+
throw new Error(`${provider.name} did not return a stream body`);
|
|
603
|
+
}
|
|
535
604
|
|
|
536
|
-
|
|
537
|
-
buffer
|
|
538
|
-
const lines = buffer.split(/\r?\n/);
|
|
539
|
-
buffer = lines.pop() || '';
|
|
605
|
+
const decoder = new TextDecoder();
|
|
606
|
+
let buffer = '';
|
|
540
607
|
|
|
541
|
-
for (const
|
|
542
|
-
|
|
543
|
-
|
|
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
|
|
546
|
-
|
|
613
|
+
for (const line of lines) {
|
|
614
|
+
const trimmed = line.trim();
|
|
615
|
+
if (!trimmed || !trimmed.startsWith('data:')) continue;
|
|
547
616
|
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
|
630
|
+
content,
|
|
574
631
|
usage: data.usage,
|
|
575
632
|
raw: data,
|
|
576
633
|
};
|
|
577
|
-
}
|
|
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
|
|
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');
|
package/src/cli/commands.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
repl.
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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'
|
|
84
|
-
repl.
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
117
|
+
repl.closeInputBox?.();
|
|
120
118
|
await this.processPastedImageTask(prompt, image);
|
|
121
119
|
})
|
|
122
120
|
.catch((error) => {
|
|
123
|
-
|
|
121
|
+
repl.closeInputBox?.();
|
|
124
122
|
console.log(`\n${colors.red}✖ Paste image error: ${error.message}${colors.reset}\n`);
|
|
125
|
-
if (repl.running && !repl.readlineClosed)
|
|
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
|
-
|
|
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`);
|