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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "winter-super-cli",
3
- "version": "2026.6.12",
3
+ "version": "2026.6.13",
4
4
  "description": "❄️ AI-Powered Development CLI with Interactive REPL",
5
5
  "type": "module",
6
6
  "main": "bin/winter.js",
@@ -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 > 2) {
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.`,
@@ -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
- console.log(`${colors.yellow}Design commands:${colors.reset}`);
740
- console.log(` /design add <name> — Add a design system`);
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
- 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|sua|lam|tao|ghi|doc|xoa|chay|kiem tra|cai|them|doi|review|tim|sửa|làm|tạo|đọc|xóa|xoá|chạy|kiểm tra|cài|thêm|đổi|tìm)\b/i;
1710
- const targetPattern = /\b(file|repo|project|code|src|test|build|git|npm|node|folder|directory|cli|tool|provider|model|config|readme|package\.json|du an|thu muc|tap tin|loi|chuc nang|dự án|thư mục|tập tin|lỗi|chức năng)\b|[A-Za-z]:[\\/]|\.js\b|\.ts\b|\.tsx\b|\.json\b|\.md\b/i;
1711
- const pureQuestionPattern = /^(what|why|how|when|where|is|are|can|could|should|would|tai sao|vi sao|la gi|co nen|co phai|tại sao|vì sao|là gì|có nên|có phải)\b/i;
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
- const clarification = /(?:cần thêm|cho mình|vui lòng|please provide|which file|what file|need more|clarify|không rõ|chưa rõ|file nào|thư mục nào|c?n th?m|cho m?nh|vui l?ng|kh?ng r?|ch?a r?|file n?o|th? m?c n?o)/i;
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
- 'Runtime correction: the user requested an action that requires tool evidence.',
1740
- 'Your previous response did not use any tool, so it was blocked to avoid falsely claiming completion.',
1741
- 'Now use the available tools to inspect/edit/run/check as needed. Do not say the task is done until a tool result proves it.',
1742
- 'If native tool calls are not supported by this model/provider, output exactly one fallback tool call and no prose, for example:',
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
- if (options?.requireToolEvidence && this.responseNeedsToolEvidence(assistantMsg.content)) {
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 (options?.requireToolEvidence && this.responseNeedsToolEvidence(visibleContent)) {
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' },
@@ -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
- // Clean ANSI for length calculation (approximate)
28
- const visibleLen = fullStr.replace(/\x1b\[[0-9;]*m/g, '').length;
29
-
30
- if (visibleLen >= cols - 1) {
31
- // Truncate the text part, leave room for timeStr and frame
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(`\r\x1b[K${cyan}${fullStr.slice(0, 2)}${reset}${dim}${fullStr.slice(2)}${reset}`);
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
- process.stdout.write(`\r\x1b[K${finalText ? `${finalText}\n` : ''}`);
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) {
@@ -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(session, config) {
15
- this.session = session;
16
- this.config = config;
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);