winter-super-cli 2026.6.12 → 2026.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agent/runtime.js +16 -3
- package/src/cli/prompt-builder.js +2 -0
- package/src/cli/repl-commands.js +6 -4
- package/src/cli/repl.js +155 -15
- package/src/cli/slash-commands.js +1 -1
- package/src/cli/spinner.js +24 -18
- package/src/design/commands.js +52 -3
package/package.json
CHANGED
package/src/agent/runtime.js
CHANGED
|
@@ -9,7 +9,7 @@ export class AgentRuntime {
|
|
|
9
9
|
this.repl = repl;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
async runConversation(messages, label = 'Thinking', tools = null) {
|
|
12
|
+
async runConversation(messages, label = 'Thinking... (Đang suy nghĩ, tìm cách giải quyết)', tools = null) {
|
|
13
13
|
const repl = this.repl;
|
|
14
14
|
repl.spinner = new Spinner(label + '...');
|
|
15
15
|
repl.spinner.start();
|
|
@@ -46,12 +46,17 @@ export class AgentRuntime {
|
|
|
46
46
|
try {
|
|
47
47
|
for (let i = 0; i < maxToolTurns; i++) {
|
|
48
48
|
if (repl.isCancelled) throw new Error('AbortError');
|
|
49
|
+
if (repl.spinner) {
|
|
50
|
+
repl.spinner.update(`${label}...`);
|
|
51
|
+
repl.spinner.start();
|
|
52
|
+
}
|
|
49
53
|
const turn = await repl.requestAssistantTurn(messages, {
|
|
50
54
|
provider: executionProfile.provider,
|
|
51
55
|
model: executionProfile.model,
|
|
52
56
|
enableTools: true,
|
|
53
57
|
toolPromptOnly: forceTextToolFallback,
|
|
54
58
|
requireToolEvidence: requireToolEvidence && !usedTools,
|
|
59
|
+
usedMutatingTools: usedMutatingTools,
|
|
55
60
|
}, startedAt, totalUsage);
|
|
56
61
|
|
|
57
62
|
const assistantMsg = turn.assistantMsg || {};
|
|
@@ -64,12 +69,15 @@ export class AgentRuntime {
|
|
|
64
69
|
if (toolCalls.length === 0) {
|
|
65
70
|
if (turn.finishReason === 'tool_evidence_required') {
|
|
66
71
|
noToolActionRetries++;
|
|
67
|
-
if (noToolActionRetries >
|
|
68
|
-
finalContent = 'Chưa thực hiện được: model trả lời mà không dùng tool nên Winter đã chặn để tránh báo xạo.';
|
|
72
|
+
if (noToolActionRetries > 3) {
|
|
73
|
+
finalContent = 'Chưa thực hiện được: model trả lời mà không dùng tool nên Winter đã chặn để tránh báo xạo. Hãy thử lại hoặc dùng model mạnh hơn.';
|
|
69
74
|
console.log(`\n${colors.yellow}${finalContent}${colors.reset}\n`);
|
|
70
75
|
reachedToolLimit = false;
|
|
71
76
|
break;
|
|
72
77
|
}
|
|
78
|
+
if (noToolActionRetries >= 2) {
|
|
79
|
+
console.log(`\n${colors.yellow}! Model không chịu dùng tool (lần ${noToolActionRetries}/3). Đang ép buộc lại...${colors.reset}`);
|
|
80
|
+
}
|
|
73
81
|
messages.push({
|
|
74
82
|
role: 'assistant',
|
|
75
83
|
content: assistantMsg.content || '',
|
|
@@ -177,9 +185,14 @@ export class AgentRuntime {
|
|
|
177
185
|
} else if (!proceed) {
|
|
178
186
|
result = { success: false, error: 'User denied permission to execute this command.' };
|
|
179
187
|
} else {
|
|
188
|
+
if (repl.spinner) {
|
|
189
|
+
repl.spinner.update(`Executing ${canonicalToolName}... (Đang chạy lệnh)`);
|
|
190
|
+
repl.spinner.start();
|
|
191
|
+
}
|
|
180
192
|
result = toolName
|
|
181
193
|
? await repl.tools.execute(canonicalToolName, enrichedArgs, { cwd: repl.projectPath })
|
|
182
194
|
: { success: false, error: 'Tool call is missing a tool name' };
|
|
195
|
+
if (repl.spinner) repl.spinner.stop();
|
|
183
196
|
}
|
|
184
197
|
const promptToolResult = await repl.buildPromptToolResultForModel(canonicalToolName, result);
|
|
185
198
|
messages.push({
|
|
@@ -90,6 +90,7 @@ export class PromptBuilder {
|
|
|
90
90
|
`Debug: reproduce or locate the failing path first, read the exact failing file/log, patch the smallest cause, then run the closest test/build/smoke command.`,
|
|
91
91
|
`Design/UI: inspect existing UI and design resources first; deliver polished, responsive, non-generic interfaces, not placeholder layouts.`,
|
|
92
92
|
`Images: if the user attaches or pastes an image, analyze it directly and connect findings to project files when relevant.`,
|
|
93
|
+
`CRITICAL: You MUST call tools (Read/Write/Edit/Bash) to do real work. NEVER write code in markdown and claim done. Winter blocks fake completions.`,
|
|
93
94
|
`Tool fallback when native calls are unavailable: <invoke name="Read"><parameter name="path">README.md</parameter></invoke> OR {"tool":"Read","arguments":{"path":"README.md"}} OR CALL_TOOL Read {"path":"README.md"}.`,
|
|
94
95
|
`Session: cwd=${this.projectPath}; id=${this.session?.getSessionId?.()?.substring(0, 8) || 'unknown'}`,
|
|
95
96
|
`${requiredResourcesStr}${memoryStr}${plansStr}${skillsStr}${workflowStr}${blueprintStr}${startupPlanStr}${sessionSignalsStr}`,
|
|
@@ -114,6 +115,7 @@ export class PromptBuilder {
|
|
|
114
115
|
`## Tool Usage`,
|
|
115
116
|
`Use tools when they materially help. For coding tasks: inspect first, edit second, verify third.`,
|
|
116
117
|
`Prefer Read/Grep/Glob before editing. Use Write/Edit for file changes.`,
|
|
118
|
+
`CRITICAL: When the user asks you to fix/create/edit/run/modify anything, you MUST call tools (Read, Write, Edit, Bash, etc.) to actually do it. NEVER just write code in a markdown code block and claim it is done. Winter will detect and block fake completions. If you say "đã sửa/đã tạo/done/fixed" without a tool call, your response will be rejected.`,
|
|
117
119
|
`Tool call compatibility: if native tool calls are unavailable, output exactly one of these forms and no prose: <invoke name="Read"><parameter name="path">README.md</parameter></invoke> OR {"tool":"Read","arguments":{"path":"README.md"}} OR CALL_TOOL Read {"path":"README.md"}.`,
|
|
118
120
|
`Browser capability: You CAN browse URLs! Use WebFetch to fetch page content (text extraction) or BrowserDebug for Chrome automation (JS rendering, screenshots). If user shares a URL or asks to view a website, use these tools automatically.`,
|
|
119
121
|
`When a task touches coding, agents, UI, brand, or design, inspect the relevant required local resource in depth before deciding.`,
|
package/src/cli/repl-commands.js
CHANGED
|
@@ -3,6 +3,8 @@ import { promises as fs } from 'fs';
|
|
|
3
3
|
import { colors } from './snowflake-logo.js';
|
|
4
4
|
import { SLASH_COMMANDS } from './slash-commands.js';
|
|
5
5
|
|
|
6
|
+
import { DesignCommands } from '../design/commands.js';
|
|
7
|
+
|
|
6
8
|
function getPageAgentRoot(repl) {
|
|
7
9
|
return repl.contextLoader?.getResourcePaths?.()?.pageAgent || path.join(repl.projectPath, 'resources', 'local', 'page-agent');
|
|
8
10
|
}
|
|
@@ -735,11 +737,11 @@ export async function handleSlashCommand(repl, input) {
|
|
|
735
737
|
break;
|
|
736
738
|
|
|
737
739
|
// Design system
|
|
738
|
-
case '/design':
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
console.log(` /design search <query> — Search design systems`);
|
|
740
|
+
case '/design': {
|
|
741
|
+
const designCmd = new DesignCommands(repl);
|
|
742
|
+
await designCmd.execute(args[0], args.slice(1));
|
|
742
743
|
break;
|
|
744
|
+
}
|
|
743
745
|
case '/designs':
|
|
744
746
|
await repl.showDesignSystems();
|
|
745
747
|
break;
|
package/src/cli/repl.js
CHANGED
|
@@ -1706,11 +1706,19 @@ ${colors.reset}
|
|
|
1706
1706
|
const text = this.getLatestUserText(messages).toLowerCase();
|
|
1707
1707
|
if (!text.trim()) return false;
|
|
1708
1708
|
|
|
1709
|
-
|
|
1710
|
-
const
|
|
1711
|
-
|
|
1709
|
+
// Direct action verbs (EN + VN)
|
|
1710
|
+
const actionPattern = /\b(fix|repair|bug|debug|implement|create|write|edit|modify|update|delete|remove|refactor|run|test|build|commit|push|publish|install|check|inspect|read|scan|grep|search|change|apply|patch|add|move|rename|copy|migrate|deploy|setup|configure|generate|format|clean|reset|revert|undo|merge|split|extract|inject|insert|replace|append|prepend|convert|transform|compile|execute|verify|validate|optimize|improve|enhance|upgrade|downgrade|enable|disable|toggle|set|connect|send|fetch|download|upload|open|close|start|stop|restart|sua|lam|tao|ghi|doc|xoa|chay|kiem tra|cai|them|doi|review|tim|viet|trien khai|cap nhat|xay dung|cau hinh|chinh|bo sung|loai bo|sửa|làm|tạo|đọc|xóa|xoá|chạy|kiểm tra|cài|thêm|đổi|tìm|viết|triển khai|cập nhật|xây dựng|cấu hình|chỉnh|bổ sung|loại bỏ|di chuyển|đổi tên|nâng cấp|tối ưu|kết nối|gửi|tải|mở|đóng|khởi động|dừng)\b/i;
|
|
1711
|
+
// Target patterns (filenames, paths, project terms)
|
|
1712
|
+
const targetPattern = /\b(file|repo|project|code|src|test|build|git|npm|node|folder|directory|cli|tool|provider|model|config|readme|package\.json|component|module|class|function|api|endpoint|route|page|style|css|html|template|schema|database|server|client|app|service|hook|context|store|reducer|middleware|controller|handler|view|layout|du an|thu muc|tap tin|loi|chuc nang|dự án|thư mục|tập tin|lỗi|chức năng|giao diện|trang|dịch vụ|thành phần|mô hình)\b|[A-Za-z]:[\\/]|\.jsx?\b|\.tsx?\b|\.json\b|\.md\b|\.py\b|\.css\b|\.html\b|\.vue\b|\.svelte\b|\.rs\b|\.go\b|\.java\b|\.c(pp)?\b|\.rb\b/i;
|
|
1713
|
+
// Pure questions that don't need tool action
|
|
1714
|
+
const pureQuestionPattern = /^(what|why|how|when|where|is|are|can|could|should|would|explain|describe|tell me|compare|giải thích|mô tả|so sánh|tai sao|vi sao|la gi|co nen|co phai|tại sao|vì sao|là gì|có nên|có phải|nhu the nao|như thế nào|khi nào)\b/i;
|
|
1712
1715
|
|
|
1713
1716
|
if (pureQuestionPattern.test(text) && !actionPattern.test(text)) return false;
|
|
1717
|
+
|
|
1718
|
+
// Even without explicit target, some verbs are strong enough on their own
|
|
1719
|
+
const strongActionAlone = /\b(fix|debug|deploy|build|test|commit|install|run|refactor|sửa|chạy|cài|triển khai|xây dựng)\b/i;
|
|
1720
|
+
if (strongActionAlone.test(text)) return true;
|
|
1721
|
+
|
|
1714
1722
|
return actionPattern.test(text) && targetPattern.test(text);
|
|
1715
1723
|
}
|
|
1716
1724
|
|
|
@@ -1728,25 +1736,109 @@ ${colors.reset}
|
|
|
1728
1736
|
const text = String(content || '').toLowerCase();
|
|
1729
1737
|
if (!text.trim()) return false;
|
|
1730
1738
|
|
|
1731
|
-
|
|
1739
|
+
// If model is asking for clarification, that's legitimate - no tool needed
|
|
1740
|
+
const clarification = /(?:cần thêm thông tin|cho mình biết|vui lòng cung cấp|please provide|which file|what file|need more info|clarify|không rõ|chưa rõ|file nào|thư mục nào|bạn muốn|you want me to|could you specify|can you tell)/i;
|
|
1732
1741
|
if (clarification.test(text)) return false;
|
|
1742
|
+
|
|
1733
1743
|
return true;
|
|
1734
1744
|
}
|
|
1735
1745
|
|
|
1746
|
+
detectFakeCompletion(content = '') {
|
|
1747
|
+
const text = String(content || '').toLowerCase();
|
|
1748
|
+
if (!text.trim()) return false;
|
|
1749
|
+
|
|
1750
|
+
// Detect fake completion claims - model says it did something without using tools
|
|
1751
|
+
const fakeCompletionClaims = /(?:đã (?:sửa|tạo|viết|xóa|cập nhật|thêm|chỉnh|xong|hoàn thành|fix|update|edit|write|create|delete|remove|modify|change|apply|deploy|push)|i(?:'ve| have) (?:fixed|created|written|updated|added|modified|changed|edited|applied|deployed|deleted|removed|patched|implemented|refactored)|done!|xong rồi|hoàn thành|đã hoàn tất|hoàn tất|the (?:fix|change|update|edit|modification) (?:has been|is) (?:applied|done|completed|made)|here(?:'s| is) the (?:fix|update|change|solution|implementation|code)|file (?:has been|was) (?:updated|created|modified|written|changed)|changes? (?:have been|has been|were) (?:made|applied|saved)|successfully (?:updated|created|modified|fixed|applied|changed|written))/i;
|
|
1752
|
+
if (fakeCompletionClaims.test(text)) return true;
|
|
1753
|
+
|
|
1754
|
+
// Detect code blocks that pretend to show "changes" without tool use
|
|
1755
|
+
const codeBlockWithFilePath = /```[\s\S]*?(?:[\/\\][\w.-]+\.(?:js|ts|py|css|html|json|md|jsx|tsx|vue|go|rs|java|c|cpp|rb|sh))[\s\S]*?```/i;
|
|
1756
|
+
const claimsFileChange = /(?:here(?:'s| is)|below|sau đây|dưới đây|như sau|updated|modified|changed|new|fixed)/i;
|
|
1757
|
+
if (codeBlockWithFilePath.test(text) && claimsFileChange.test(text)) return true;
|
|
1758
|
+
|
|
1759
|
+
return false;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1736
1762
|
buildToolEvidenceCorrection(messages = []) {
|
|
1737
1763
|
const request = this.getLatestUserText(messages);
|
|
1738
1764
|
return [
|
|
1739
|
-
'
|
|
1740
|
-
'
|
|
1741
|
-
'
|
|
1742
|
-
'
|
|
1765
|
+
'⚠️ RUNTIME ENFORCEMENT: Your previous response was BLOCKED because you did not use any tool.',
|
|
1766
|
+
'',
|
|
1767
|
+
'The user requested an action that requires REAL tool execution. You MUST:',
|
|
1768
|
+
'1. Call Read/Grep/Glob to inspect the relevant files FIRST',
|
|
1769
|
+
'2. Call Write/Edit to make changes',
|
|
1770
|
+
'3. Call Bash to run/test/verify',
|
|
1771
|
+
'',
|
|
1772
|
+
'DO NOT write code in a code block and claim it is done. That is a hallucination.',
|
|
1773
|
+
'DO NOT say "I have updated/created/fixed" without a tool call proving it.',
|
|
1774
|
+
'DO NOT describe what you would do. Actually DO IT with tool calls.',
|
|
1775
|
+
'',
|
|
1776
|
+
'Available tools: Read, Write, Edit, Bash, Glob, Grep, BrowserDebug, WebFetch, WebSearch.',
|
|
1777
|
+
'',
|
|
1778
|
+
'If native tool calls are not supported, output exactly one fallback tool call:',
|
|
1743
1779
|
'<invoke name="Read"><parameter name="path">README.md</parameter></invoke>',
|
|
1744
1780
|
'{"tool":"Read","arguments":{"path":"README.md"}}',
|
|
1745
1781
|
'CALL_TOOL Read {"path":"README.md"}',
|
|
1782
|
+
'',
|
|
1746
1783
|
`Original user request: ${request}`,
|
|
1747
1784
|
].join('\n');
|
|
1748
1785
|
}
|
|
1749
1786
|
|
|
1787
|
+
/**
|
|
1788
|
+
* Smart Tool Routing: phân tích câu lệnh user và gợi ý tool phù hợp.
|
|
1789
|
+
* Giúp model yếu/nhỏ chọn đúng tool thay vì dùng Bash cho mọi thứ.
|
|
1790
|
+
*/
|
|
1791
|
+
buildToolRoutingHint(userMessage = '') {
|
|
1792
|
+
const text = String(userMessage || '').toLowerCase();
|
|
1793
|
+
if (!text.trim()) return null;
|
|
1794
|
+
|
|
1795
|
+
const hints = [];
|
|
1796
|
+
|
|
1797
|
+
// Detect file reading requests
|
|
1798
|
+
const hasPath = /[A-Za-z]:[\\/][\w.\\/\\-]+/i.test(text) || /(?:^|\s)[.~]?\/[\w.\/-]+/i.test(text);
|
|
1799
|
+
const readVerbs = /\b(đọc|doc|read|xem|view|mở|open|show|hiện|hiển thị|cat|type)\b/i;
|
|
1800
|
+
const readFilePatterns = /\b(đọc|doc|read|xem|view|mở|open|show|hiện|cat|type)\b.*\.(?:js|ts|py|json|md|css|html|txt|yaml|yml|toml|cfg|ini|env|sh|bat|ps1|xml|vue|svelte|go|rs|java|c|cpp|rb|php)\b/i;
|
|
1801
|
+
const dirPatterns = /\b(đọc|doc|read|xem|liệt kê|list|ls|dir|show|hiện)\b.*\b(thư mục|folder|directory|dir)\b/i;
|
|
1802
|
+
|
|
1803
|
+
if (readFilePatterns.test(text)) {
|
|
1804
|
+
hints.push('TOOL HINT: To read a file, call tool Read with {"file_path": "<path>"}. Do NOT use Bash with cat/type.');
|
|
1805
|
+
} else if (hasPath && readVerbs.test(text)) {
|
|
1806
|
+
// Path detected + read verb, could be file or directory
|
|
1807
|
+
hints.push('TOOL HINT: To read a file, call tool Read with {"file_path": "<path>"}. To list a directory, call Glob with {"pattern": "*", "cwd": "<path>"}. Do NOT use Bash with ls/dir/cat/type.');
|
|
1808
|
+
} else if (dirPatterns.test(text)) {
|
|
1809
|
+
hints.push('TOOL HINT: To list directory contents, call tool Glob with {"pattern": "*", "cwd": "<path>"}. Do NOT use Bash with ls/dir.');
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// Detect file writing/creating requests
|
|
1813
|
+
if (/\b(tạo|tao|create|viết|viet|write|ghi)\b.*\b(file|tập tin|tap tin)\b/i.test(text)) {
|
|
1814
|
+
hints.push('TOOL HINT: To create/write a file, call tool Write with {"file_path": "<path>", "content": "<content>"}. Do NOT use Bash with echo/cat.');
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// Detect file editing requests
|
|
1818
|
+
if (/\b(sửa|sua|edit|chỉnh|chinh|thay|replace|đổi|doi|modify|update|cập nhật)\b.*(?:file|\.(?:js|ts|py|json|md|css|html)\b)/i.test(text)) {
|
|
1819
|
+
hints.push('TOOL HINT: To edit a file, first Read it, then call Edit with {"file_path": "<path>", "old_string": "<exact text>", "new_string": "<replacement>"}.');
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// Detect search/find requests
|
|
1823
|
+
if (/\b(tìm|tim|find|search|kiếm|kiem|grep|ở đâu|o dau|where)\b/i.test(text) && !(/\b(web|google|online|internet)\b/i.test(text))) {
|
|
1824
|
+
hints.push('TOOL HINT: To search in code, call tool Grep with {"pattern": "<search term>", "path": "<directory>"}. Do NOT use Bash with grep/findstr.');
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// Detect test/run requests
|
|
1828
|
+
if (/\b(chạy|chay|run|test|execute|thực thi|thuc thi|build|compile|npm|node|python|pip)\b/i.test(text) && !readPatterns.test(text)) {
|
|
1829
|
+
hints.push('TOOL HINT: To run commands, call tool Bash with {"command": "<command>"}.');
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// Detect URL/web requests
|
|
1833
|
+
if (/\b(https?:\/\/[^\s]+|url|website|trang web|web page)\b/i.test(text)) {
|
|
1834
|
+
hints.push('TOOL HINT: To fetch a URL, call tool WebFetch with {"url": "<url>"}. For browser debugging, use BrowserDebug.');
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
if (hints.length === 0) return null;
|
|
1838
|
+
|
|
1839
|
+
return `[Tool Router] ${hints.join(' | ')}`;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1750
1842
|
withCurrentAbortSignal(options = {}) {
|
|
1751
1843
|
const signal = options.signal || options.abortSignal || this.currentAbortController?.signal;
|
|
1752
1844
|
return signal ? { ...options, signal } : options;
|
|
@@ -1810,8 +1902,11 @@ ${colors.reset}
|
|
|
1810
1902
|
}
|
|
1811
1903
|
const finishReason = response.choices?.[0]?.finish_reason;
|
|
1812
1904
|
|
|
1905
|
+
if (this.spinner) this.spinner.stop();
|
|
1906
|
+
|
|
1813
1907
|
if (assistantMsg.content && toolCalls.length === 0) {
|
|
1814
|
-
|
|
1908
|
+
const isFakeCompletion = !options?.usedMutatingTools && this.detectFakeCompletion(assistantMsg.content);
|
|
1909
|
+
if ((options?.requireToolEvidence && this.responseNeedsToolEvidence(assistantMsg.content)) || isFakeCompletion) {
|
|
1815
1910
|
return { assistantMsg, toolCalls, finalContent: '', finishReason: 'tool_evidence_required' };
|
|
1816
1911
|
}
|
|
1817
1912
|
this.printAssistantAnswer(assistantMsg.content, startedAt, totalUsage);
|
|
@@ -1823,6 +1918,8 @@ ${colors.reset}
|
|
|
1823
1918
|
|
|
1824
1919
|
async collectAssistantStream(messages, options, startedAt, totalUsage) {
|
|
1825
1920
|
let content = '';
|
|
1921
|
+
let streamBuffer = '';
|
|
1922
|
+
let printedLines = 0;
|
|
1826
1923
|
const toolCallParts = [];
|
|
1827
1924
|
let finishReason = null;
|
|
1828
1925
|
let printed = false;
|
|
@@ -1878,6 +1975,18 @@ ${colors.reset}
|
|
|
1878
1975
|
|
|
1879
1976
|
if (chunk.content) {
|
|
1880
1977
|
content += chunk.content;
|
|
1978
|
+
if (bufferToolModeContent && this.spinner && process.env.NODE_ENV !== 'test') {
|
|
1979
|
+
streamBuffer += chunk.content;
|
|
1980
|
+
let newlineIdx;
|
|
1981
|
+
const terminalCols = process.stdout.columns || 80;
|
|
1982
|
+
while ((newlineIdx = streamBuffer.indexOf('\n')) !== -1) {
|
|
1983
|
+
const line = streamBuffer.slice(0, newlineIdx).replace(/\r$/, '');
|
|
1984
|
+
streamBuffer = streamBuffer.slice(newlineIdx + 1);
|
|
1985
|
+
const visibleLength = line.replace(/\x1b\[[0-9;]*m/g, '').length + 2;
|
|
1986
|
+
printedLines += Math.max(1, Math.ceil(visibleLength / terminalCols));
|
|
1987
|
+
process.stdout.write(`\r\x1b[K${colors.dim}│ ${colors.reset}${colors.dim}${line}${colors.reset}\n`);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1881
1990
|
}
|
|
1882
1991
|
|
|
1883
1992
|
if (this.spinner && printed === false) {
|
|
@@ -1887,16 +1996,20 @@ ${colors.reset}
|
|
|
1887
1996
|
const args = lastCall.function.arguments || '';
|
|
1888
1997
|
let summary = args.replace(/\s+/g, ' ');
|
|
1889
1998
|
if (summary.length > 60) summary = '...' + summary.slice(-60);
|
|
1890
|
-
this.spinner.update(`Calling ${lastCall.function.name}... ${summary}`);
|
|
1999
|
+
this.spinner.update(`Calling ${lastCall.function.name}... (Chuẩn bị gọi tool) ${summary}`);
|
|
1891
2000
|
}
|
|
1892
|
-
} else if (content.length > 0 && bufferToolModeContent) {
|
|
1893
|
-
let summary = content.replace(/\s+/g, ' ');
|
|
1894
|
-
if (summary.length > 60) summary = '...' + summary.slice(-60);
|
|
1895
|
-
this.spinner.update(`Generating... ${summary}`);
|
|
1896
2001
|
}
|
|
1897
2002
|
}
|
|
1898
2003
|
}
|
|
1899
2004
|
|
|
2005
|
+
if (streamBuffer.length > 0 && this.spinner) {
|
|
2006
|
+
const line = streamBuffer.replace(/\r$/, '');
|
|
2007
|
+
const terminalCols = process.stdout.columns || 80;
|
|
2008
|
+
const visibleLength = line.replace(/\x1b\[[0-9;]*m/g, '').length + 2;
|
|
2009
|
+
printedLines += Math.max(1, Math.ceil(visibleLength / terminalCols));
|
|
2010
|
+
process.stdout.write(`\r\x1b[K${colors.dim}│ ${colors.reset}${colors.dim}${line}${colors.reset}\n`);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
1900
2013
|
if (this.spinner) this.spinner.stop();
|
|
1901
2014
|
|
|
1902
2015
|
const inlineToolExtraction = this.extractInlineToolCalls(content);
|
|
@@ -1911,7 +2024,15 @@ ${colors.reset}
|
|
|
1911
2024
|
const visibleContent = inlineToolExtraction.content || content;
|
|
1912
2025
|
|
|
1913
2026
|
if (toolCalls.length === 0 && visibleContent) {
|
|
1914
|
-
if (
|
|
2027
|
+
if (printedLines > 0 && bufferToolModeContent) {
|
|
2028
|
+
const linesToErase = Math.min(printedLines, (process.stdout.rows || 24) - 2);
|
|
2029
|
+
if (linesToErase > 0) {
|
|
2030
|
+
process.stdout.write(`\r\x1b[K\x1b[${linesToErase}A\x1b[J`);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
const isFakeCompletion = !options?.usedMutatingTools && this.detectFakeCompletion(visibleContent);
|
|
2035
|
+
if ((options?.requireToolEvidence && this.responseNeedsToolEvidence(visibleContent)) || isFakeCompletion) {
|
|
1915
2036
|
return {
|
|
1916
2037
|
assistantMsg: { content: visibleContent },
|
|
1917
2038
|
toolCalls,
|
|
@@ -2067,6 +2188,11 @@ ${colors.reset}
|
|
|
2067
2188
|
];
|
|
2068
2189
|
|
|
2069
2190
|
try {
|
|
2191
|
+
if (this.spinner) {
|
|
2192
|
+
this.spinner.update('Thinking (final answer)... (Đang viết câu trả lời cuối)');
|
|
2193
|
+
this.spinner.start();
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2070
2196
|
if (typeof this.ai.streamRequest === 'function') {
|
|
2071
2197
|
return await this.streamFinalAnswer(finalMessages, startedAt, totalUsage, executionProfile);
|
|
2072
2198
|
}
|
|
@@ -2079,11 +2205,15 @@ ${colors.reset}
|
|
|
2079
2205
|
});
|
|
2080
2206
|
this.addUsage(totalUsage, response.usage);
|
|
2081
2207
|
const content = response.choices?.[0]?.message?.content || '';
|
|
2208
|
+
|
|
2209
|
+
if (this.spinner) this.spinner.stop();
|
|
2210
|
+
|
|
2082
2211
|
if (content) {
|
|
2083
2212
|
this.printAssistantAnswer(content, startedAt, totalUsage);
|
|
2084
2213
|
}
|
|
2085
2214
|
return content;
|
|
2086
2215
|
} catch (error) {
|
|
2216
|
+
if (this.spinner) this.spinner.stop();
|
|
2087
2217
|
if (this.isAbortError(error)) throw new Error('AbortError');
|
|
2088
2218
|
const fallback = this.buildToolFallbackAnswer(toolSummaries, error.message);
|
|
2089
2219
|
console.log(`\n${colors.yellow}${fallback}${colors.reset}\n`);
|
|
@@ -2110,6 +2240,9 @@ ${colors.reset}
|
|
|
2110
2240
|
if (chunk.content) {
|
|
2111
2241
|
content += chunk.content;
|
|
2112
2242
|
}
|
|
2243
|
+
if (this.spinner && isFirst === false) {
|
|
2244
|
+
this.spinner.update('Generating...');
|
|
2245
|
+
}
|
|
2113
2246
|
}
|
|
2114
2247
|
|
|
2115
2248
|
if (this.spinner) this.spinner.stop();
|
|
@@ -2450,6 +2583,13 @@ ${colors.reset}
|
|
|
2450
2583
|
}
|
|
2451
2584
|
|
|
2452
2585
|
const tools = this.getAgentTools('general');
|
|
2586
|
+
|
|
2587
|
+
// Smart tool routing: inject a hint for weak models
|
|
2588
|
+
const toolHint = this.buildToolRoutingHint(message);
|
|
2589
|
+
if (toolHint) {
|
|
2590
|
+
messages.push({ role: 'system', content: toolHint });
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2453
2593
|
const { finalContent, usedMutatingTools } = await this.runConversation(messages, 'Thinking', tools);
|
|
2454
2594
|
|
|
2455
2595
|
const allToolCalls = [];
|
|
@@ -35,7 +35,7 @@ export const SLASH_COMMANDS = [
|
|
|
35
35
|
{ cmd: '/grep', desc: 'Search files', usage: '/grep <pattern>' },
|
|
36
36
|
{ cmd: '/bash', desc: 'Run command', usage: '/bash <command>' },
|
|
37
37
|
{ cmd: '/image', desc: 'Analyze image/screenshot or clipboard image', usage: '/image [file] [question]' },
|
|
38
|
-
{ cmd: '/design', desc: 'Design commands', sub: ['search', 'add', 'list', 'preview'] },
|
|
38
|
+
{ cmd: '/design', desc: 'Design commands', sub: ['search', 'add', 'apply', 'list', 'preview'] },
|
|
39
39
|
{ cmd: '/designs', desc: 'List/search awesome-design-md systems', usage: '/designs [query]' },
|
|
40
40
|
{ cmd: '/skill', desc: 'Skills management', sub: ['list', 'enable', 'create'] },
|
|
41
41
|
{ cmd: '/skills', desc: 'List local Winter/Codex/Claude skills' },
|
package/src/cli/spinner.js
CHANGED
|
@@ -14,6 +14,11 @@ export class Spinner {
|
|
|
14
14
|
const reset = this.colors.reset || '';
|
|
15
15
|
const dim = this.colors.dim || '';
|
|
16
16
|
this.startTime = Date.now();
|
|
17
|
+
this.lastLines = 0;
|
|
18
|
+
|
|
19
|
+
// Make sure we're on a clean line
|
|
20
|
+
process.stdout.write('\r\x1b[K');
|
|
21
|
+
|
|
17
22
|
this.interval = setInterval(() => {
|
|
18
23
|
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
|
19
24
|
let timeStr = '';
|
|
@@ -24,25 +29,17 @@ export class Spinner {
|
|
|
24
29
|
const cols = process.stdout.columns || 80;
|
|
25
30
|
let fullStr = `${this.frames[this.frameIndex]} ${this.text}${timeStr}`;
|
|
26
31
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const timeStrLen = timeStr.length;
|
|
33
|
-
const availableSpaceForText = cols - timeStrLen - 6;
|
|
34
|
-
if (availableSpaceForText > 10) {
|
|
35
|
-
const truncText = this.text.length > availableSpaceForText
|
|
36
|
-
? '...' + this.text.slice(-(availableSpaceForText - 3))
|
|
37
|
-
: this.text;
|
|
38
|
-
fullStr = `${this.frames[this.frameIndex]} ${truncText}${timeStr}`;
|
|
39
|
-
} else {
|
|
40
|
-
// Terminal is extremely narrow, just truncate everything
|
|
41
|
-
fullStr = fullStr.slice(0, cols - 4) + '...';
|
|
42
|
-
}
|
|
32
|
+
// Clear previous frame
|
|
33
|
+
if (this.lastLines > 0) {
|
|
34
|
+
process.stdout.write(`\r\x1b[${this.lastLines}A\x1b[J`);
|
|
35
|
+
} else {
|
|
36
|
+
process.stdout.write('\r\x1b[K');
|
|
43
37
|
}
|
|
38
|
+
|
|
39
|
+
const visibleLen = fullStr.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
40
|
+
this.lastLines = Math.max(0, Math.ceil(visibleLen / cols) - 1);
|
|
44
41
|
|
|
45
|
-
process.stdout.write(
|
|
42
|
+
process.stdout.write(`${cyan}${fullStr.slice(0, 2)}${reset}${dim}${fullStr.slice(2)}${reset}`);
|
|
46
43
|
this.frameIndex = (this.frameIndex + 1) % this.frames.length;
|
|
47
44
|
}, 80);
|
|
48
45
|
}
|
|
@@ -51,7 +48,16 @@ export class Spinner {
|
|
|
51
48
|
if (!this.interval) return;
|
|
52
49
|
clearInterval(this.interval);
|
|
53
50
|
this.interval = null;
|
|
54
|
-
|
|
51
|
+
|
|
52
|
+
if (this.lastLines > 0) {
|
|
53
|
+
process.stdout.write(`\r\x1b[${this.lastLines}A\x1b[J`);
|
|
54
|
+
} else {
|
|
55
|
+
process.stdout.write('\r\x1b[K');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (finalText) {
|
|
59
|
+
process.stdout.write(`${finalText}\n`);
|
|
60
|
+
}
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
update(text) {
|
package/src/design/commands.js
CHANGED
|
@@ -11,9 +11,10 @@ import { colors, statusIcons } from '../cli/snowflake-logo.js';
|
|
|
11
11
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
12
12
|
|
|
13
13
|
export class DesignCommands {
|
|
14
|
-
constructor(
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
14
|
+
constructor(repl) {
|
|
15
|
+
this.repl = repl;
|
|
16
|
+
this.session = repl.session;
|
|
17
|
+
this.config = repl.config;
|
|
17
18
|
this.brandsDir = path.join(packageRoot, 'resources', 'local', 'awesome-design-md', 'design-md');
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -26,6 +27,9 @@ export class DesignCommands {
|
|
|
26
27
|
case 'add':
|
|
27
28
|
await this.addBrand(args[0]);
|
|
28
29
|
break;
|
|
30
|
+
case 'apply':
|
|
31
|
+
await this.applyBrand(args[0]);
|
|
32
|
+
break;
|
|
29
33
|
case 'list':
|
|
30
34
|
await this.listBrands();
|
|
31
35
|
break;
|
|
@@ -109,6 +113,51 @@ export class DesignCommands {
|
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
115
|
|
|
116
|
+
async applyBrand(brand) {
|
|
117
|
+
if (!brand) {
|
|
118
|
+
console.log(`${colors.yellow}Usage: winter design apply <brand>${colors.reset}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const brandDir = path.join(this.brandsDir, brand);
|
|
124
|
+
let fileContent = null;
|
|
125
|
+
let fileName = null;
|
|
126
|
+
|
|
127
|
+
const designPath = path.join(brandDir, 'DESIGN.md');
|
|
128
|
+
const readmePath = path.join(brandDir, 'README.md');
|
|
129
|
+
|
|
130
|
+
if (await fs.access(designPath).then(() => true).catch(() => false)) {
|
|
131
|
+
fileContent = await fs.readFile(designPath, 'utf8');
|
|
132
|
+
fileName = 'DESIGN.md';
|
|
133
|
+
} else if (await fs.access(readmePath).then(() => true).catch(() => false)) {
|
|
134
|
+
fileContent = await fs.readFile(readmePath, 'utf8');
|
|
135
|
+
fileName = 'README.md';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!fileContent) {
|
|
139
|
+
console.log(`${colors.red}${statusIcons.error} Brand "${brand}" not found${colors.reset}`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log(`${colors.cyan}${statusIcons.info} Analyzing and applying ${brand} design system...${colors.reset}`);
|
|
144
|
+
|
|
145
|
+
const prompt = `Please act as a Senior UI/UX Engineer. Analyze the following design system (${brand}) and completely refactor the UI and styles in this project to match its specifications. Focus on colors, typography, border radiuses, interactive states, and overall visual aesthetics as defined in the document.
|
|
146
|
+
|
|
147
|
+
<design_system>
|
|
148
|
+
${fileContent}
|
|
149
|
+
</design_system>
|
|
150
|
+
|
|
151
|
+
Start by reviewing the codebase, especially tailwind configs or global css, then rewrite the main components. Create a plan if needed.`;
|
|
152
|
+
|
|
153
|
+
// Inject the task to the AI REPL loop
|
|
154
|
+
await this.repl.chat(prompt);
|
|
155
|
+
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.log(`${colors.red}${statusIcons.error} Error: ${error.message}${colors.reset}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
112
161
|
async listBrands() {
|
|
113
162
|
try {
|
|
114
163
|
const brands = await fs.readdir(this.brandsDir);
|