winter-super-cli 2026.6.5 → 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/src/cli/repl.js CHANGED
@@ -1,13 +1,21 @@
1
1
  /**
2
- * ?? WINTER REPL ??
2
+ * WINTER REPL
3
3
  * Claude Code / Codex style interactive REPL
4
4
  */
5
5
 
6
6
  import readline from 'readline';
7
7
  import { promises as fs, watch as fsWatch } from 'fs';
8
8
  import { homedir } from 'os';
9
- import { welcomeBanner, colors, applyColorTheme } from './snowflake-logo.js';
9
+ import { colors, applyColorTheme, welcomeBanner } from './snowflake-logo.js';
10
10
  import { renderBox, supportsUnicodeUi, terminalWidth, stripAnsi, wrapText, padVisible } from './terminal-ui.js';
11
+ import {
12
+ buildTuiSnapshot,
13
+ renderAssistantPanel,
14
+ renderConversationStartup,
15
+ renderLandingTui,
16
+ renderStartupTui,
17
+ renderStatusPanel,
18
+ } from './tui.js';
11
19
  import { WinterInputController } from './input-controller.js';
12
20
  import { ToolExecutor } from '../tools/executor.js';
13
21
  import { SessionManager } from '../session/manager.js';
@@ -30,7 +38,7 @@ import {
30
38
  buildPromptToolResultWithTokenJuice,
31
39
  } from './tool-runtime.js';
32
40
  import { TokenJuice } from '../context/token-juice.js';
33
- import { classifyModelTier, isSmallModel } from '../ai/model-capabilities.js';
41
+ import { classifyModelTier, getModelBudgetMultiplier } from '../ai/model-capabilities.js';
34
42
  import {
35
43
  addUsage as mergeUsage,
36
44
  buildToolCallSignature as buildToolCallSignatureText,
@@ -77,8 +85,8 @@ export class WinterREPL {
77
85
  this.ai = new AIProviderManager(this.config);
78
86
  this.tools = new ToolExecutor(this);
79
87
  this.projectPath = options.projectPath || process.cwd();
80
- this.sessionId = options.sessionId || null; // Nh?n sessionId t? bin
81
- this.version = options.version || '1.0.0'; // Nh?n version t? bin
88
+ this.sessionId = options.sessionId || null; // Nhận sessionId từ bin
89
+ this.version = options.version || '1.0.0'; // Nhận version từ bin
82
90
  this.running = true;
83
91
  this.history = [];
84
92
  this.maxHistory = 500;
@@ -88,6 +96,7 @@ export class WinterREPL {
88
96
  this.taskQueue = [];
89
97
  this.isProcessing = false;
90
98
  this.isCancelled = false;
99
+ this.currentAbortController = null;
91
100
  this.sessionPermissionGrants = new Set();
92
101
  this.permissionManager = new PermissionManager(this.config, this.session);
93
102
  this.contextLoader = new ContextLoader({ projectPath: this.projectPath, session: this.session, tools: this.tools });
@@ -114,11 +123,13 @@ export class WinterREPL {
114
123
  this.useUnicodeUi = supportsUnicodeUi();
115
124
  this.inputController = new WinterInputController(this);
116
125
  this.watchers = [];
126
+ this.startupNotices = [];
127
+ this._fixedPanel = Boolean(process.stdout.isTTY) && process.env.WINTER_FIXED_PANEL_TUI !== '0';
117
128
  }
118
129
 
119
130
  async initCodebaseSearch() {
120
131
  if (this.codebaseSearcher) return;
121
- this.codebaseSearcher = new CodebaseSearch({ projectPath: this.projectPath });
132
+ this.codebaseSearcher = new CodebaseSearch({ projectPath: this.projectPath, enableCodeGraph: true });
122
133
  await this.codebaseSearcher.init();
123
134
  this.atContext = new AtContextResolver({
124
135
  projectPath: this.projectPath,
@@ -147,6 +158,15 @@ export class WinterREPL {
147
158
  });
148
159
  }
149
160
 
161
+ startupNotice(message) {
162
+ const text = String(message || '').trim();
163
+ if (!text) return;
164
+ this.startupNotices.push(text);
165
+ if (this.startupNotices.length > 6) {
166
+ this.startupNotices = this.startupNotices.slice(-6);
167
+ }
168
+ }
169
+
150
170
  async startCodebaseWatcher() {
151
171
  if (this.codebaseWatcher) return;
152
172
  await this.initCodebaseSearch();
@@ -166,7 +186,7 @@ export class WinterREPL {
166
186
  await this.codebaseSearcher.clear();
167
187
  }
168
188
  const stats = await this.codebaseSearcher.reindex();
169
- console.log(`${colors.green}? Codebase indexed:${colors.reset}`);
189
+ console.log(`${colors.green} Codebase indexed:${colors.reset}`);
170
190
  console.log(` ${colors.dim}Files: ${stats.totalFiles}, Chunks: ${stats.totalChunks}, Indexed: ${stats.indexedFiles}, Skipped: ${stats.skipped}${colors.reset}`);
171
191
  }
172
192
 
@@ -229,17 +249,18 @@ export class WinterREPL {
229
249
  if (before.totalChunks > 0) return before;
230
250
 
231
251
  if (verbose) {
232
- console.log(`${colors.dim}? Indexing codebase for semantic search...${colors.reset}`);
252
+ console.log(`${colors.dim}Indexing codebase for semantic search...${colors.reset}`);
233
253
  }
234
254
  const indexedStats = await this.codebaseSearcher.reindex();
235
255
  if (verbose) {
236
- console.log(`${colors.green}? Codebase indexed: ${indexedStats.totalFiles} files, ${indexedStats.totalChunks} chunks${colors.reset}`);
256
+ console.log(`${colors.green} Codebase indexed: ${indexedStats.totalFiles} files, ${indexedStats.totalChunks} chunks${colors.reset}`);
237
257
  }
238
258
  return this.codebaseSearcher.indexer.getStats();
239
259
  }
240
260
 
241
261
  async buildCodebaseContext(task = '') {
242
262
  try {
263
+ const modelTier = this.getActiveModelTier();
243
264
  const stats = await this.ensureCodebaseIndex({ verbose: false });
244
265
  if (!stats.totalChunks) return '';
245
266
 
@@ -278,7 +299,7 @@ export class WinterREPL {
278
299
  }
279
300
  }
280
301
 
281
- return this.compactText(lines.join('\n'), this.shouldUseCompactPrompt() ? 1800 : 3200, 'codebase context');
302
+ return this.compactText(lines.join('\n'), this.getCodebaseContextBudget(modelTier), 'codebase context');
282
303
  } catch (error) {
283
304
  return `[Codebase Index]\nUnavailable: ${error.message}`;
284
305
  }
@@ -312,7 +333,7 @@ export class WinterREPL {
312
333
  if (ans.trim().toLowerCase() === 'y') {
313
334
  const ok = await this.diffView.restoreFromBackup(latest.backup, latest.original);
314
335
  if (ok) {
315
- console.log(`${colors.green}? Restored ${latest.original} from backup${colors.reset}`);
336
+ console.log(`${colors.green} Restored ${latest.original} from backup${colors.reset}`);
316
337
  } else {
317
338
  console.log(`${colors.red}Failed to restore${colors.reset}`);
318
339
  }
@@ -326,18 +347,22 @@ export class WinterREPL {
326
347
  toolName,
327
348
  result,
328
349
  compact: this.shouldUseCompactPrompt(),
350
+ modelTier: this.getActiveModelTier(),
329
351
  compactText: (text, maxChars, label) => this.compactText(text, maxChars, label),
330
352
  summarizeToolResult: value => this.tools?.summarizeToolResult?.(value) || { ...value },
331
353
  });
332
354
  }
333
355
 
334
356
  async buildPromptToolResultForModel(toolName, result) {
357
+ const modelTier = this.getActiveModelTier();
358
+ const tokenJuice = this.getTokenJuiceForModelTier(modelTier);
335
359
  return buildPromptToolResultWithTokenJuice({
336
360
  toolName,
337
361
  result,
338
362
  projectPath: this.projectPath,
339
- tokenJuice: this.tokenJuice,
363
+ tokenJuice,
340
364
  compact: this.shouldUseCompactPrompt(),
365
+ modelTier,
341
366
  compactText: (text, maxChars, label) => this.compactText(text, maxChars, label),
342
367
  summarizeToolResult: value => this.tools?.summarizeToolResult?.(value) || { ...value },
343
368
  });
@@ -456,7 +481,7 @@ export class WinterREPL {
456
481
  });
457
482
  await this.session.replaceMemory('[Project Anchor]', `Current project is ${this.projectPath}. Treat this path as the canonical working directory for the session.`, 'info');
458
483
 
459
- // T? ??ng ??c v? ghi nh? m?t s? t?i nguy?n c?c b? (an to?n): ch? n?p file ho?c README trong th? m?c
484
+ // Tự động đọc ghi nhớ một số tài nguyên cục bộ an toàn.
460
485
  const fsPromises = await import('fs/promises');
461
486
  const resourcePaths = this.getResourcePaths();
462
487
  const autoLoadTargets = [resourcePaths.agents, resourcePaths.designs, resourcePaths.karpathy, resourcePaths.pageAgent];
@@ -469,9 +494,9 @@ export class WinterREPL {
469
494
  if (stat.isFile()) {
470
495
  const content = await fsPromises.readFile(targetPath, 'utf8');
471
496
  const fileName = path.basename(targetPath);
472
- const memoryKey = `[T? ??ng ghi nh? file ${fileName}]`;
497
+ const memoryKey = `[Tự động ghi nhớ file ${fileName}]`;
473
498
  await this.session.replaceMemory(memoryKey, content);
474
- console.log(`${colors.dim}? ?? t? ??ng n?p v? ghi nh? file ${fileName}${colors.reset}`);
499
+ this.startupNotice(`loaded ${fileName}`);
475
500
  continue;
476
501
  }
477
502
 
@@ -485,9 +510,9 @@ export class WinterREPL {
485
510
  const cstat = await fsPromises.stat(p).catch(() => null);
486
511
  if (cstat && cstat.isFile()) {
487
512
  const content = await fsPromises.readFile(p, 'utf8');
488
- const memoryKey = `[T? ??ng ghi nh? file ${path.basename(targetPath)}/${c}]`;
513
+ const memoryKey = `[Tự động ghi nhớ file ${path.basename(targetPath)}/${c}]`;
489
514
  await this.session.replaceMemory(memoryKey, content);
490
- console.log(`${colors.dim}? ?? t? ??ng n?p v? ghi nh? ${path.basename(targetPath)}/${c}${colors.reset}`);
515
+ this.startupNotice(`loaded ${path.basename(targetPath)}/${c}`);
491
516
  loaded = true;
492
517
  break;
493
518
  }
@@ -505,63 +530,63 @@ export class WinterREPL {
505
530
  }
506
531
  }
507
532
 
508
- // N?p c?c file quy t?c d? ?n theo th? t? ?u ti?n.
533
+ // Nạp các file quy tắc dự án theo thứ tự ưu tiên.
509
534
  const projectInstructionFiles = await this.readProjectInstructionFiles();
510
535
  try {
511
536
  if (projectInstructionFiles.length > 0) {
512
537
  for (const file of projectInstructionFiles) {
513
- const memoryKey = `[Quy t?c d? ?n t? ${file.relativePath}]`;
538
+ const memoryKey = `[Quy tắc dự án từ ${file.relativePath}]`;
514
539
  await this.session.replaceMemory(memoryKey, file.content);
515
- console.log(`${colors.dim}? ?? n?p quy t?c d? ?n t? ${file.relativePath}${colors.reset}`);
540
+ this.startupNotice(`rules ${file.relativePath}`);
516
541
  }
517
542
  }
518
543
  } catch (e) {
519
- // N?u kh?ng c?, t? ??ng t?o file m?u!
544
+ // Nếu không có, tự động tạo file mẫu.
520
545
  const template = `# Winter Project Rules
521
546
 
522
- ## ?? Project Overview
523
- - **Name**: [T?n d? ?n]
524
- - **Description**: [M? t? ng?n v? d? ?n]
547
+ ## Project Overview
548
+ - **Name**: [Tên dự án]
549
+ - **Description**: [ tả ngắn về dự án]
525
550
 
526
- ## ?? Tech Stack
551
+ ## Tech Stack
527
552
  - **Languages**: JavaScript / TypeScript
528
553
  - **Runtime**: Node.js
529
- - **Frameworks**: [T? ?i?n n?u c?, VD: Express, React...]
554
+ - **Frameworks**: [Tự điền nếu có, VD: Express, React...]
530
555
 
531
- ## ?? AI Behavior & Coding Guidelines
556
+ ## AI Behavior & Coding Guidelines
532
557
 
533
- ### 1. Nguy?n T?c Code (Coding Standards)
534
- - Lu?n ?u ti?n vi?t code s?ch (Clean Code), d? ??c v? d? b?o tr?.
535
- - S? d?ng ES Modules (\`import/export\`) thay v? CommonJS (\`require\`) tr? khi c? l? do ??c bi?t.
536
- - Gi? nguy?n c?c comment v? JSDoc hi?n c? trong file tr? khi ???c y?u c?u s?a.
558
+ ### 1. Nguyên tắc Code (Coding Standards)
559
+ - Luôn ưu tiên viết code sạch (Clean Code), dễ đọc dễ bảo trì.
560
+ - Sử dụng ES Modules (\`import/export\`) thay CommonJS (\`require\`) trừ khi do đặc biệt.
561
+ - Giữ nguyên các comment JSDoc hiện trong file trừ khi được yêu cầu sửa.
537
562
 
538
- ### 2. T??ng T?c V?i Ng??i D?ng (User Interaction)
539
- - Lu?n gi?i th?ch NG?N G?N l? do th?c hi?n thay ??i tr??c khi s?a file.
540
- - Khi g?p l?i, h?y ?? xu?t gi?i ph?p thay v? ch? b?o l?i.
541
- - KH?NG t? ti?n x?a code c? c?a user tr? khi ch?c ch?n n? kh?ng c?n d?ng ho?c ???c y?u c?u.
563
+ ### 2. Tương tác với người dùng (User Interaction)
564
+ - Luôn giải thích ngắn gọn do thực hiện thay đổi trước khi sửa file.
565
+ - Khi gặp lỗi, hãy đề xuất giải pháp thay chỉ báo lỗi.
566
+ - Không tự tiện xóa code của user trừ khi chắc chắn không còn dùng hoặc được yêu cầu.
542
567
 
543
568
  ### 3. Git & Commits
544
- - Vi?t commit message theo chu?n Conventional Commits (VD: \`feat:\`, \`fix:\`, \`docs:\`).
545
- - Lu?n ki?m tra \`git status\` tr??c khi th?c hi?n thay ??i l?n.
569
+ - Viết commit message theo chuẩn Conventional Commits (VD: \`feat:\`, \`fix:\`, \`docs:\`).
570
+ - Luôn kiểm tra \`git status\` trước khi thực hiện thay đổi lớn.
546
571
 
547
- ### 4. X? L? File (File Operations)
548
- - Ch? s?a nh?ng d?ng c?n thi?t, tr?nh vi?t l?i to?n b? file n?u kh?ng c?n.
549
- - Lu?n ??m b?o file kh?ng b? l?i c? ph?p sau khi s?a.
572
+ ### 4. Xử File (File Operations)
573
+ - Chỉ sửa những dòng cần thiết, tránh viết lại toàn bộ file nếu không cần.
574
+ - Luôn đảm bảo file không bị lỗi pháp sau khi sửa.
550
575
  `;
551
576
  try {
552
577
  const projectWinterMd = path.join(this.projectPath, 'winter.md');
553
578
  await fsPromises.writeFile(projectWinterMd, template, 'utf8');
554
- console.log(`\n${colors.green}? ?? t? ??ng t?o file winter.md m?u cho d? ?n m?i!${colors.reset}`);
555
- console.log(`${colors.dim}B?n c? th? ch?nh s?a file n?y ?? d?y AI c?c quy t?c ri?ng c?a d? ?n.${colors.reset}\n`);
579
+ this.startupNotice('created winter.md');
580
+
556
581
 
557
- // N?p lu?n v?o memory
558
- await this.session.replaceMemory(`[Quy t?c d? ?n t? winter.md]`, template);
582
+ // Nạp luôn vào memory.
583
+ await this.session.replaceMemory(`[Quy tắc dự án từ winter.md]`, template);
559
584
  } catch (err) {
560
- // B? qua n?u kh?ng ghi ???c file
585
+ // Bỏ qua nếu không ghi được file.
561
586
  }
562
587
  }
563
588
 
564
- // ?? T? ??ng t?o design.md, skill.md, rule.md n?u ch?a c? ??????????????
589
+ // Tự động tạo design.md, skill.md, rule.md nếu chưa có.
565
590
  const autoCreateDocs = await buildProjectDocs({
566
591
  projectPath: this.projectPath,
567
592
  resourcePaths: this.getResourcePaths(),
@@ -577,17 +602,17 @@ export class WinterREPL {
577
602
  if (!isWinterGeneratedProjectDoc(existing)) continue;
578
603
 
579
604
  await fsPromises.writeFile(filePath, doc.content, 'utf8');
580
- console.log(`${colors.green}? ?? n?ng c?p file ${doc.filename} t? local resources.${colors.reset}`);
581
- const memoryKey = `[Quy t?c d? ?n t? ${doc.filename}]`;
605
+ this.startupNotice(`updated ${doc.filename}`);
606
+ const memoryKey = `[Quy tắc dự án từ ${doc.filename}]`;
582
607
  await this.session.replaceMemory(memoryKey, doc.content);
583
608
  } catch {
584
609
  try {
585
610
  await fsPromises.writeFile(filePath, doc.content, 'utf8');
586
- console.log(`${colors.green}? ?? t? ??ng t?o file ${doc.filename} t? local resources!${colors.reset}`);
587
- const memoryKey = `[Quy t?c d? ?n t? ${doc.filename}]`;
611
+ this.startupNotice(`created ${doc.filename}`);
612
+ const memoryKey = `[Quy tắc dự án từ ${doc.filename}]`;
588
613
  await this.session.replaceMemory(memoryKey, doc.content);
589
614
  } catch (err) {
590
- // B? qua n?u kh?ng t?o ???c
615
+ // Bỏ qua nếu không tạo được.
591
616
  }
592
617
  }
593
618
  }
@@ -596,61 +621,29 @@ export class WinterREPL {
596
621
  await this.compactStartupMemories({ projectInstructionFiles, autoCreateDocs });
597
622
 
598
623
  // Codebase Index: warm in background, then inject summaries into model context on demand.
599
- this.codebaseWarmup = this.ensureCodebaseIndex({ verbose: true })
624
+ this.codebaseWarmup = this.ensureCodebaseIndex({ verbose: false })
600
625
  .then(() => this.startCodebaseWatcher())
601
626
  .catch((error) => {
602
- console.log(`${colors.yellow}Codebase index disabled: ${error.message}${colors.reset}`);
627
+ this.startupNotice(`codebase disabled: ${error.message}`);
603
628
  });
604
629
 
605
- const activeProvider = this.ai.getActiveProvider();
606
- const info = {
607
- project: this.projectPath,
608
- provider: activeProvider,
609
- model: this.ai.providers[activeProvider]?.model,
610
- session: this.session.getSessionId().substring(0, 8)
611
- };
612
-
613
- // Show banner only if not already shown
614
- if (!process.env.WINTER_BANNER_SHOWN) {
615
- console.log(welcomeBanner(this.version, info));
616
- this.showCommandMenu();
617
- process.env.WINTER_BANNER_SHOWN = '1';
618
- } else {
619
- this.showStatus();
620
- }
621
-
622
- // Hi?n th? l?ch s? chat n?u ?ang load l?i session c?, nh?ng ch? replay b?n t?m t?t.
623
630
  const sessionHistory = this.session.getHistory(4);
624
631
  if (sessionHistory.length > 0) {
625
- const columns = process.stdout.columns || 80;
626
- const W = Math.max(60, Math.min(Math.floor(columns * 0.95), 100));
627
- const titleStr = ' L?ch s? phi?n l?m vi?c ';
628
- const ruleChar = this.useUnicodeUi ? '?' : '-';
629
- const sideLine = ruleChar.repeat(Math.max(0, Math.floor((W - titleStr.length) / 2)));
630
- const bottomLine = ruleChar.repeat(W);
631
-
632
- console.log(`\n${colors.dim}${sideLine}${titleStr}${sideLine}${colors.reset}`);
633
- for (const msg of sessionHistory) {
634
- const text = this.formatStartupHistoryEntry(msg.content);
635
- if (msg.role === 'user') {
636
- console.log(`\n${colors.cyan}B?n:${colors.reset} ${text}`);
637
- } else if (msg.role === 'assistant') {
638
- console.log(`\n${colors.bright}${colors.magenta}Winter:${colors.reset} ${text}`);
639
- }
640
- }
641
- console.log(`\n${colors.dim}${bottomLine}${colors.reset}\n`);
632
+ this.startupNotice(`${sessionHistory.length} recent messages`);
642
633
  }
643
634
 
635
+ this.showStatus();
636
+
644
637
  // Setup readline
645
638
  this.rl = readline.createInterface({
646
639
  input: process.stdin,
647
640
  output: process.stdout,
648
- prompt: `${colors.bright}${colors.cyan}${this.useUnicodeUi ? 'winter??:' : 'winter>'} ${colors.reset}`,
641
+ prompt: `${colors.bright}${colors.cyan}winter > ${colors.reset}`,
649
642
  completer: this.completer.bind(this),
650
643
  });
651
644
  this.inputController.installSlashSuggestions();
652
645
 
653
- // B?t s? ki?n Ctrl+C ?? in ra l?nh ti?p t?c session
646
+ // Bắt sự kiện Ctrl+C để in ra lệnh tiếp tục session.
654
647
  this.rl.on('SIGINT', () => {
655
648
  console.log(`\n\n${colors.cyan}Cảm ơn đã sử dụng Winter!${colors.reset}`);
656
649
  console.log(`${colors.yellow}Tiếp tục phiên làm việc:${colors.reset}`);
@@ -658,7 +651,7 @@ export class WinterREPL {
658
651
  process.exit(0);
659
652
  });
660
653
 
661
- // Hi?n th? prompt l?n ??u ti?n ngay khi kh?i ??ng xong
654
+ // Hiển thị prompt lần đầu tiên ngay khi khởi động xong.
662
655
  this.showInputPrompt();
663
656
 
664
657
  this.rl.on('line', (line) => {
@@ -695,7 +688,7 @@ export class WinterREPL {
695
688
  if (!this.useUnicodeUi) {
696
689
  text = text
697
690
  .replace(/\p{Extended_Pictographic}/gu, '')
698
- .replace(/[????????]/g, '-')
691
+ .replace(/[─━—–]/g, '-')
699
692
  .replace(/\s+/g, ' ')
700
693
  .trim();
701
694
  }
@@ -711,16 +704,39 @@ export class WinterREPL {
711
704
  return this.inputController.closeInputBox();
712
705
  }
713
706
 
707
+ closeSlashMenu() {
708
+ return this.inputController.closeSlashMenu();
709
+ }
710
+
711
+ handleSlashMenuKey(key = {}) {
712
+ return this.inputController.handleSlashMenuKey(key);
713
+ }
714
+
715
+ handleDirectClipboardPaste() {
716
+ return this.inputController.handleDirectClipboardPaste();
717
+ }
718
+
714
719
  buildInputPanel() {
715
720
  return this.inputController.buildInputPanel();
716
721
  }
717
722
 
718
723
  showStatus() {
719
- console.log(`${colors.dim}Project: ${this.projectPath}${colors.reset}`);
720
- console.log(`${colors.dim}Provider: ${this.ai.getActiveProvider()}${colors.reset}`);
721
- console.log(`${colors.dim}Session: ${this.session.getSessionId().substring(0, 8)}${colors.reset}`);
722
- console.log(`${colors.dim}Type ${colors.cyan}/help${colors.dim} for commands or ${colors.cyan}/${colors.dim} for menu${colors.reset}`);
723
- console.log('');
724
+ const snapshot = buildTuiSnapshot(this);
725
+ if (process.stdout.isTTY) {
726
+ console.log(`\n${welcomeBanner(this.version, {
727
+ project: snapshot.projectPath,
728
+ session: snapshot.sessionShort,
729
+ provider: snapshot.provider,
730
+ model: snapshot.model,
731
+ })}\n`);
732
+ return;
733
+ }
734
+ console.log(`\n${renderConversationStartup(snapshot, { colors })}\n`);
735
+ }
736
+
737
+ showTuiDashboard() {
738
+ const snapshot = buildTuiSnapshot(this);
739
+ console.log(`\n${renderLandingTui(snapshot, { colors, title: 'Winter Dashboard' })}\n`);
724
740
  }
725
741
 
726
742
  getResourcePaths() {
@@ -917,7 +933,8 @@ export class WinterREPL {
917
933
  async handleInput(input) {
918
934
  if (this.isProcessing) {
919
935
  const pos = this.taskQueue.length + 1;
920
- console.log(`${colors.magenta}?${colors.reset} ${colors.dim}?? x?p h?ng ch? (v? tr? #${pos})${colors.reset}`);
936
+ const preview = input.length > 40 ? input.slice(0, 37) + '...' : input;
937
+ console.log(`${colors.yellow}⧗${colors.reset} ${colors.bright}Queued #${pos}${colors.reset} ${colors.dim}› ${preview}${colors.reset}`);
921
938
  this.taskQueue.push(input);
922
939
  return;
923
940
  }
@@ -927,7 +944,9 @@ export class WinterREPL {
927
944
  async processInputTask(input) {
928
945
  this.isProcessing = true;
929
946
  this.isCancelled = false;
947
+ this.currentAbortController = new AbortController();
930
948
  try {
949
+ this.closeSlashMenu();
931
950
  this.history.push(input);
932
951
  if (this.history.length > this.maxHistory) {
933
952
  this.history = this.history.slice(-this.maxHistory);
@@ -998,13 +1017,16 @@ export class WinterREPL {
998
1017
  }
999
1018
  } catch (error) {
1000
1019
  if (error.message === 'AbortError') {
1001
- console.log(colors.red + '\n?? h?y c?ng vi?c hi?n t?i.' + colors.reset);
1020
+ if (!this.isCancelled) {
1021
+ console.log(colors.red + '\nĐã hủy công việc hiện tại.' + colors.reset);
1022
+ }
1002
1023
  } else {
1003
- console.log(colors.red + '\nL?i: ' + error.message + colors.reset);
1024
+ console.log(colors.red + '\nLỗi: ' + error.message + colors.reset);
1004
1025
  }
1005
1026
  } finally {
1006
1027
  this.isProcessing = false;
1007
1028
  this.isCancelled = false;
1029
+ this.currentAbortController = null;
1008
1030
  if (this.spinner) this.spinner.stop();
1009
1031
 
1010
1032
  if (this.taskQueue.length > 0) {
@@ -1063,13 +1085,13 @@ export class WinterREPL {
1063
1085
 
1064
1086
  let printedLines = 0;
1065
1087
  const render = () => {
1066
- // X?a nh?ng d?ng ?? in tr??c ??
1088
+ // Xóa những dòng đã in trước đó.
1067
1089
  if (printedLines > 0) {
1068
1090
  process.stdout.write('\x1b[' + printedLines + 'A\x1b[J');
1069
1091
  }
1070
1092
 
1071
1093
  let out = `\n${title}\n`;
1072
- out += `D?ng m?i t?n (?/?) ?? di chuy?n, [Space] ?? ch?n/b? ch?n, [Enter] ?? x?c nh?n\n\n`;
1094
+ out += `Dùng mũi tên (↑/↓) để di chuyển, [Space] để chọn/bỏ chọn, [Enter] để xác nhận\n\n`;
1073
1095
 
1074
1096
  for (let i = 0; i < items.length; i++) {
1075
1097
  const isHover = i === cursor;
@@ -1119,11 +1141,11 @@ export class WinterREPL {
1119
1141
  }
1120
1142
 
1121
1143
  async generateInteractivePlan(task) {
1122
- this.spinner = new Spinner('?ang ph?n t?ch v? chia nh? y?u c?u...');
1144
+ this.spinner = new Spinner('Đang phân tích chia nhỏ yêu cầu...');
1123
1145
  this.spinner.start();
1124
1146
 
1125
1147
  const messages = [
1126
- { role: 'system', content: 'B?n l? chuy?n gia l?p k? ho?ch. H?y chia nh? y?u c?u c?a ng??i d?ng th?nh c?c b??c c? th?, h?nh ??ng ???c, r?t ng?n g?n (d??i 15 ch? m?i b??c). CH? TR? V? M?T M?NG JSON C?C CHU?I, KH?NG GI?I TH?CH G? TH?M. V? d?: ["T?o file index.html", "Th?m CSS styling", "Vi?t script.js"]' },
1148
+ { role: 'system', content: 'Bạn chuyên gia lập kế hoạch. Hãy chia nhỏ yêu cầu của người dùng thành các bước cụ thể, hành động được, rất ngắn gọn (dưới 15 chữ mỗi bước). Chỉ trả về một mảng JSON các chuỗi, không giải thích thêm. dụ: ["Tạo file index.html", "Thêm CSS styling", "Viết script.js"]' },
1127
1149
  { role: 'user', content: task }
1128
1150
  ];
1129
1151
 
@@ -1146,40 +1168,40 @@ export class WinterREPL {
1146
1168
  }
1147
1169
 
1148
1170
  if (!Array.isArray(items) || items.length === 0) {
1149
- console.log(`\x1b[33mKh?ng th? parse k? ho?ch. AI ph?n h?i: ${text}\x1b[0m`);
1171
+ console.log(`\x1b[33mKhông thể parse kế hoạch. AI phản hồi: ${text}\x1b[0m`);
1150
1172
  return;
1151
1173
  }
1152
1174
 
1153
- const selectedSteps = await this.showInteractiveChecklist('K? HO?CH TH?C HI?N:', items);
1175
+ const selectedSteps = await this.showInteractiveChecklist('KẾ HOẠCH THỰC HIỆN:', items);
1154
1176
 
1155
1177
  if (selectedSteps.length > 0) {
1156
1178
  for (const step of selectedSteps) {
1157
1179
  await this.session.createPlan(step, task);
1158
1180
  }
1159
- console.log(`\x1b[32m? ?? th?m ${selectedSteps.length} c?ng vi?c v?o Memory (g? /plans ?? xem).\x1b[0m`);
1181
+ console.log(`\x1b[32m Đã thêm ${selectedSteps.length} công việc vào Memory ( /plans để xem).\x1b[0m`);
1160
1182
 
1161
- this.rl.question(`\n\x1b[36mB?n c? mu?n AI b?t tay l?m c?ng vi?c ??U TI?N ngay b?y gi? kh?ng? [y/N]: \x1b[0m`, async (answer) => {
1183
+ this.rl.question(`\n\x1b[36mBạn muốn AI bắt tay làm công việc ĐẦU TIÊN ngay bây giờ không? [y/N]: \x1b[0m`, async (answer) => {
1162
1184
  if (answer.toLowerCase() === 'y') {
1163
- await this.chat(`H?y b?t ??u th?c hi?n b??c ??u ti?n: ${selectedSteps[0]}`);
1185
+ await this.chat(`Hãy bắt đầu thực hiện bước đầu tiên: ${selectedSteps[0]}`);
1164
1186
  }
1165
1187
  });
1166
1188
  } else {
1167
- console.log(`\n\x1b[2m?? hu? k? ho?ch.\x1b[0m`);
1189
+ console.log(`\n\x1b[2mĐã huỷ kế hoạch.\x1b[0m`);
1168
1190
  }
1169
1191
  } catch (e) {
1170
1192
  if (this.spinner) this.spinner.stop();
1171
- console.log(`\x1b[31mL?i: ${e.message}\x1b[0m`);
1193
+ console.log(`\x1b[31mLỗi: ${e.message}\x1b[0m`);
1172
1194
  }
1173
1195
  }
1174
1196
 
1175
1197
 
1176
1198
  async runAutoHealing(task) {
1177
- console.log(`\n\x1b[35m[ TDD AUTO-HEALING MODE ]\x1b[0m K?ch ho?t v?ng l?p ch?a l?nh l?i t? ??ng.`);
1199
+ console.log(`\n\x1b[35m[ TDD AUTO-HEALING MODE ]\x1b[0m Kích hoạt vòng lặp tự sửa lỗi.`);
1178
1200
 
1179
- // T?ng gi?i h?n loop l?n 15 ?? cho ph?p AI t? s?a l?i nhi?u l?n
1201
+ // Tăng giới hạn loop lên 15 để cho phép AI tự sửa lỗi nhiều lần.
1180
1202
  const originalRequestAssistantTurn = this.requestAssistantTurn;
1181
1203
 
1182
- // Ch?n prompt ??c bi?t ?p AI ph?i verify
1204
+ // Chèn prompt đặc biệt ép AI phải verify.
1183
1205
  const verifyCommands = await this.inferVerificationCommands(task);
1184
1206
  const autoPrompt = `TASK: ${task}
1185
1207
 
@@ -1195,7 +1217,7 @@ CRITICAL DEBUG/AGENT RULES:
1195
1217
  }
1196
1218
 
1197
1219
  async runAutoCommit(context = '') {
1198
- console.log(`\n${colors.cyan}? T?o commit message t? ??ng...${colors.reset}`);
1220
+ console.log(`\n${colors.cyan} Tạo commit message tự động...${colors.reset}`);
1199
1221
  const diffResult = await this.tools.execute('Bash', { command: 'git diff --cached' }, { cwd: this.projectPath });
1200
1222
 
1201
1223
  let diff = diffResult.stdout;
@@ -1207,14 +1229,14 @@ CRITICAL DEBUG/AGENT RULES:
1207
1229
  }
1208
1230
 
1209
1231
  if (!diff || diff.trim() === '') {
1210
- console.log(`${colors.yellow}? Kh?ng c? thay ??i n?o ?? commit.${colors.reset}`);
1232
+ console.log(`${colors.yellow}Không thay đổi nào để commit.${colors.reset}`);
1211
1233
  if (this.running && !this.readlineClosed) this.rl.prompt();
1212
1234
  return;
1213
1235
  }
1214
1236
 
1215
- const prompt = `B?n l? chuy?n gia Git. H?y vi?t M?T commit message duy nh?t (kh?ng gi?i th?ch th?m) cho nh?ng thay ??i sau ??y. D?ng format chu?n Conventional Commits (feat/fix/chore/refactor: ...). ${context ? `\nNg? c?nh: ${context}` : ''}\n\nDiff:\n${diff.slice(0, 6000)}`;
1237
+ const prompt = `Bạn chuyên gia Git. Hãy viết MỘT commit message duy nhất (không giải thích thêm) cho những thay đổi sau đây. Dùng format chuẩn Conventional Commits (feat/fix/chore/refactor: ...). ${context ? `\nNgữ cảnh: ${context}` : ''}\n\nDiff:\n${diff.slice(0, 6000)}`;
1216
1238
 
1217
- this.spinner = new Spinner('?ang sinh commit message...');
1239
+ this.spinner = new Spinner('Đang sinh commit message...');
1218
1240
  this.spinner.start();
1219
1241
  try {
1220
1242
  const response = await this.ai.sendRequest([{ role: 'user', content: prompt }], {
@@ -1222,19 +1244,19 @@ CRITICAL DEBUG/AGENT RULES:
1222
1244
  });
1223
1245
  if (this.spinner) this.spinner.stop();
1224
1246
 
1225
- const message = response.choices?.[0]?.message?.content?.trim().replace(/^["']|["']$/g, '') || 'C?p nh?t m? ngu?n';
1247
+ const message = response.choices?.[0]?.message?.content?.trim().replace(/^["']|["']$/g, '') || 'Cập nhật nguồn';
1226
1248
 
1227
- console.log(`\n${colors.green}?? xu?t commit message:${colors.reset}`);
1249
+ console.log(`\n${colors.green}Đề xuất commit message:${colors.reset}`);
1228
1250
  console.log(`${colors.bright}${message}${colors.reset}\n`);
1229
1251
 
1230
- this.rl.question(`${colors.yellow}B?n c? mu?n commit v?i message n?y kh?ng? [y/N/e (t? s?a)]: ${colors.reset}`, async (ans) => {
1252
+ this.rl.question(`${colors.yellow}Bạn muốn commit với message này không? [y/N/e (tự sửa)]: ${colors.reset}`, async (ans) => {
1231
1253
  const choice = ans.trim().toLowerCase();
1232
1254
  if (choice === 'y') {
1233
1255
  if (!isStaged) await this.tools.execute('Bash', { command: 'git add .' }, { cwd: this.projectPath });
1234
1256
  const res = await this.tools.execute('Bash', { command: `git commit -m "${message.replace(/"/g, '\\"')}"` }, { cwd: this.projectPath });
1235
1257
  console.log(res.stdout);
1236
1258
  } else if (choice === 'e') {
1237
- this.rl.question(`${colors.cyan}Nh?p commit message: ${colors.reset}`, async (customMsg) => {
1259
+ this.rl.question(`${colors.cyan}Nhập commit message: ${colors.reset}`, async (customMsg) => {
1238
1260
  if (customMsg.trim()) {
1239
1261
  if (!isStaged) await this.tools.execute('Bash', { command: 'git add .' }, { cwd: this.projectPath });
1240
1262
  const res = await this.tools.execute('Bash', { command: `git commit -m "${customMsg.replace(/"/g, '\\"')}"` }, { cwd: this.projectPath });
@@ -1244,28 +1266,28 @@ CRITICAL DEBUG/AGENT RULES:
1244
1266
  });
1245
1267
  return;
1246
1268
  } else {
1247
- console.log(`${colors.dim}?? hu? commit.${colors.reset}`);
1269
+ console.log(`${colors.dim}Đã huỷ commit.${colors.reset}`);
1248
1270
  }
1249
1271
  if (this.running && !this.readlineClosed) this.rl.prompt();
1250
1272
  });
1251
1273
  } catch (e) {
1252
1274
  if (this.spinner) this.spinner.stop();
1253
- console.log(`${colors.red}L?i: ${e.message}${colors.reset}`);
1275
+ console.log(`${colors.red}Lỗi: ${e.message}${colors.reset}`);
1254
1276
  if (this.running && !this.readlineClosed) this.rl.prompt();
1255
1277
  }
1256
1278
  }
1257
1279
 
1258
1280
  async runCodeReview(context = '') {
1259
- console.log(`\n${colors.cyan}? AI ?ang soi code c?a b?n...${colors.reset}`);
1281
+ console.log(`\n${colors.cyan}AI đang soi code của bạn...${colors.reset}`);
1260
1282
  const diffResult = await this.tools.execute('Bash', { command: 'git diff HEAD' }, { cwd: this.projectPath });
1261
1283
  const diff = diffResult.stdout;
1262
1284
 
1263
1285
  if (!diff || diff.trim() === '') {
1264
- console.log(`${colors.yellow}? Kh?ng c? thay ??i n?o ?? review.${colors.reset}`);
1286
+ console.log(`${colors.yellow}Không thay đổi nào để review.${colors.reset}`);
1265
1287
  return;
1266
1288
  }
1267
1289
 
1268
- const prompt = `H?y ??ng vai m?t Senior Developer kh? t?nh. Review nhanh c?c thay ??i sau ??y. Ch? ra bug (n?u c?), v?n ?? b?o m?t, ho?c nh?ng ch? c?n clean code. Kh?ng c?n khen ng?i, h?y n?i th?ng v?o v?n ??. Tr?nh b?y d?ng Markdown ??p m?t. \n\n${context ? `Ng? c?nh: ${context}\n` : ''}Diff:\n${diff.slice(0, 8000)}`;
1290
+ const prompt = `Hãy đóng vai một Senior Developer khó tính. Review nhanh các thay đổi sau đây. Chỉ ra bug nếu có, vấn đề bảo mật, hoặc những chỗ cần clean code. Không cần khen ngợi, hãy nói thẳng vào vấn đề. Trình bày dạng Markdown đẹp mắt.\n\n${context ? `Ngữ cảnh: ${context}\n` : ''}Diff:\n${diff.slice(0, 8000)}`;
1269
1291
 
1270
1292
  await this.chat(prompt);
1271
1293
  }
@@ -1277,66 +1299,19 @@ CRITICAL DEBUG/AGENT RULES:
1277
1299
  showCommandMenu() {
1278
1300
  const c = colors;
1279
1301
  const width = terminalWidth(72, 112, 92);
1280
- const innerWidth = width - 4;
1281
- const split = Math.floor(innerWidth * 0.54);
1282
- const rightWidth = innerWidth - split - 1;
1283
- const row = (left, right = '') => {
1284
- if (!right) return left;
1285
- return `${padVisible(left, split)} ${padVisible(right, rightWidth)}`;
1286
- };
1287
-
1288
- const body = [
1289
- `${c.bright}${c.cyan}${this.useUnicodeUi ? '? ' : ''}WINTER COMMANDS${c.reset}`,
1290
- `${c.dim}@file context | @Agent task | !cmd bash | /theme:toggle${c.reset}`,
1291
- '',
1292
- `${c.bright}D? ?n & Phi?n l?m vi?c${c.reset}`,
1293
- row(`${c.yellow}/pwd${c.reset} Th? m?c hi?n t?i`, `${c.yellow}/session${c.reset} Phi?n l?m vi?c`),
1294
- row(`${c.yellow}/cd${c.reset} ??i th? m?c`, `${c.yellow}/clear${c.reset} X?a m?n h?nh`),
1295
- row(`${c.yellow}/config${c.reset} Xem c?u h?nh`, `${c.yellow}/exit${c.reset} Tho?t`),
1296
- '',
1297
- `${c.bright}AI & C?ng c?${c.reset}`,
1298
- row(`${c.yellow}/auto${c.reset} TDD t? s?a l?i`, `${c.yellow}/debug${c.reset} Auto debug l?i`),
1299
- row(`${c.yellow}/doctor${c.reset} Ki?m tra tool-call`, `${c.yellow}/agent${c.reset} Ch?y sub-agent`),
1300
- row(`${c.yellow}/swe${c.reset} SWE workflow`, `${c.yellow}/plan${c.reset} L?p k? ho?ch`),
1301
- row(`${c.yellow}/read${c.reset} ??c file`, `${c.yellow}/write${c.reset} Ghi file`),
1302
- row(`${c.yellow}/bash${c.reset} Ch?y l?nh terminal`, `${c.yellow}/grep${c.reset} T?m trong file`),
1303
- row(`${c.yellow}/glob${c.reset} T?m file theo pattern`, `${c.yellow}/image${c.reset} ?nh/file/clipboard`),
1304
- row(`${c.yellow}/paste${c.reset} D?n text/?nh clipboard`, `${c.yellow}/composer${c.reset} Multi-file edit`),
1305
- row(`${c.yellow}/complete${c.reset} G?i ? code`, `${c.yellow}/search${c.reset} T?m ki?m code`),
1306
- row(`${c.yellow}/browse${c.reset} M? URL trong tr?nh duy?t`, `${c.yellow}/page-agent${c.reset} GUI Agent resources`),
1307
- row(`${c.yellow}/ensemble${c.reset} Ch?y nhi?u AI`, `${c.yellow}/vote${c.reset} B?nh ch?n hay nh?t`),
1308
- row(`${c.yellow}/orchestrate${c.reset} Pipeline ?a model`, `${c.yellow}/undo${c.reset} Undo backup`),
1309
- row(`${c.yellow}/ecc${c.reset} ECC resource browser`, `${c.yellow}/codex${c.reset} Codex resources`),
1310
- '',
1311
- `${c.bright}Git Auto-Pilot${c.reset}`,
1312
- row(`${c.yellow}/commit${c.reset} AI t? vi?t commit`, `${c.yellow}/review${c.reset} AI review code thay ??i`),
1313
- '',
1314
- `${c.bright}C?u h?nh Model${c.reset}`,
1315
- row(`${c.yellow}/provider${c.reset} ??i provider AI`, `${c.yellow}/model${c.reset} ??i model`),
1316
- row(`${c.yellow}/providers${c.reset} Danh s?ch provider`, `${c.yellow}/models${c.reset} Danh s?ch model`),
1317
- row(`${c.yellow}/mcp${c.reset} MCP server mgmt`, `${c.yellow}/permissions${c.reset} Quy?n/allowlist`),
1318
- '',
1319
- `${c.bright}B? nh? & K? n?ng${c.reset}`,
1320
- row(`${c.yellow}/remember${c.reset} L?u v?o b? nh?`, `${c.yellow}/memories${c.reset} Xem b? nh?`),
1321
- row(`${c.yellow}/skills${c.reset} Danh s?ch k? n?ng`, `${c.yellow}/designs${c.reset} H? th?ng thi?t k?`),
1322
- ];
1323
-
1324
- console.log(`
1325
- ${renderBox({
1326
- title: `${c.bright}${c.cyan}WINTER COMMANDS${c.reset}`,
1302
+ const snapshot = buildTuiSnapshot(this);
1303
+ console.log(`\n${renderLandingTui(snapshot, {
1304
+ colors: c,
1305
+ title: 'Winter Agent Console',
1327
1306
  width,
1328
- borderColor: c.magenta,
1329
- titleColor: c.cyan,
1330
- body,
1331
- })}
1332
- ${c.dim}G? tin nh?n tr?c ti?p ?? chat ? ESC ?? h?y ? Prompt t? x?p h?ng ch?${c.reset}
1333
- `);
1307
+ })}`);
1308
+ console.log(`${c.dim}Type ${c.cyan}/${c.dim} for palette, ${c.cyan}/help${c.dim} for the full command list.${c.reset}\n`);
1334
1309
  }
1335
1310
 
1336
1311
  showHelp() {
1337
1312
  console.log(`
1338
- ${colors.cyan}? WINTER COMMANDS${colors.reset}
1339
- ${colors.dim}${''.padEnd(50, '?')}${colors.reset}
1313
+ ${colors.cyan}${this.useUnicodeUi ? '❄ ' : ''}WINTER COMMANDS${colors.reset}
1314
+ ${colors.dim}${''.padEnd(50, this.useUnicodeUi ? '─' : '-')}${colors.reset}
1340
1315
 
1341
1316
  ${colors.white}Project:${colors.reset}
1342
1317
  /project, /pwd Show current project
@@ -1417,7 +1392,7 @@ ${colors.reset}
1417
1392
  }
1418
1393
  console.log(`${colors.cyan}Design Systems:${colors.reset}`);
1419
1394
  filtered.forEach(e => {
1420
- const icon = e.isDirectory ? '??' : '??';
1395
+ const icon = e.isDirectory ? '[dir]' : '[file]';
1421
1396
  console.log(` ${icon} ${e.name}`);
1422
1397
  });
1423
1398
  } catch (error) {
@@ -1437,7 +1412,7 @@ ${colors.reset}
1437
1412
  const entries = await this.listPathEntries(section.path, 50);
1438
1413
  if (entries.length > 0) {
1439
1414
  console.log(`${colors.cyan}${section.label}:${colors.reset}`);
1440
- entries.forEach(e => console.log(` ${e.isDirectory ? '??' : '??'} ${e.name}`));
1415
+ entries.forEach(e => console.log(` ${e.isDirectory ? '[dir]' : '[file]'} ${e.name}`));
1441
1416
  }
1442
1417
  } catch { }
1443
1418
  }
@@ -1451,8 +1426,8 @@ ${colors.reset}
1451
1426
  const providers = this.ai.listProviders();
1452
1427
  console.log(`${colors.cyan}Configured Models:${colors.reset}`);
1453
1428
  providers.forEach(p => {
1454
- const active = p.name === this.ai.getActiveProvider() ? ` ${colors.green}? active${colors.reset}` : '';
1455
- const status = p.ready ? `${colors.green}?${colors.reset}` : `${colors.red}?${colors.reset}`;
1429
+ const active = p.name === this.ai.getActiveProvider() ? ` ${colors.green}< active${colors.reset}` : '';
1430
+ const status = p.ready ? `${colors.green}ok${colors.reset}` : `${colors.red}off${colors.reset}`;
1456
1431
  console.log(` ${status} ${colors.bright}${p.name}${colors.reset}: ${p.model}${active}`);
1457
1432
  });
1458
1433
 
@@ -1500,13 +1475,42 @@ ${colors.reset}
1500
1475
  }
1501
1476
 
1502
1477
  getActiveModelTier() {
1478
+ if (typeof this.ai?._modelTier === 'string' && this.ai._modelTier) {
1479
+ return this.ai._modelTier;
1480
+ }
1503
1481
  const providerName = this.ai?.getActiveProvider?.();
1504
1482
  const model = this.ai?.providers?.[providerName]?.model || '';
1505
1483
  return classifyModelTier(model, providerName);
1506
1484
  }
1507
1485
 
1486
+ getBudgetScale(modelTier = this.getActiveModelTier()) {
1487
+ return getModelBudgetMultiplier(modelTier);
1488
+ }
1489
+
1490
+ getProjectContextBudget(modelTier = this.getActiveModelTier()) {
1491
+ return Math.round(6000 * this.getBudgetScale(modelTier));
1492
+ }
1493
+
1494
+ getCodebaseContextBudget(modelTier = this.getActiveModelTier()) {
1495
+ return Math.round(4200 * this.getBudgetScale(modelTier));
1496
+ }
1497
+
1498
+ getTokenJuiceInlineBudget(modelTier = this.getActiveModelTier()) {
1499
+ return Math.max(800, Math.round(1400 * this.getBudgetScale(modelTier)));
1500
+ }
1501
+
1502
+ getTokenJuiceForModelTier(modelTier = this.getActiveModelTier()) {
1503
+ const tokenJuice = new TokenJuice({
1504
+ projectPath: this.projectPath,
1505
+ inlineBudgetTokens: this.getTokenJuiceInlineBudget(modelTier),
1506
+ });
1507
+ this.tokenJuice = tokenJuice;
1508
+ return tokenJuice;
1509
+ }
1510
+
1508
1511
  shouldUseCompactPrompt() {
1509
- return isSmallModel(this.getActiveModelTier());
1512
+ const tier = this.getActiveModelTier();
1513
+ return tier === 'tiny' || tier === 'small';
1510
1514
  }
1511
1515
 
1512
1516
  selectExecutionProfile(messages = [], options = {}) {
@@ -1573,9 +1577,9 @@ ${colors.reset}
1573
1577
  const text = this.getLatestUserText(messages).toLowerCase();
1574
1578
  if (!text.trim()) return false;
1575
1579
 
1576
- 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)\b/i;
1577
- 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)\b|[A-Za-z]:[\\/]|\.js\b|\.ts\b|\.tsx\b|\.json\b|\.md\b/i;
1578
- const pureQuestionPattern = /^(what|why|how|when|where|is|are|can|could|should|would|tai sao|vi sao|la gi|co nen|co phai)\b/i;
1580
+ 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;
1581
+ 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;
1582
+ 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;
1579
1583
 
1580
1584
  if (pureQuestionPattern.test(text) && !actionPattern.test(text)) return false;
1581
1585
  return actionPattern.test(text) && targetPattern.test(text);
@@ -1585,17 +1589,17 @@ ${colors.reset}
1585
1589
  if (!usedMutatingTools) return false;
1586
1590
  const text = String(originalMessage || '').toLowerCase();
1587
1591
  if (!text.trim()) return false;
1588
- if (/\b(skip tests?|no verify|don't verify|khong test|khong verify|bo qua test)\b/i.test(text)) {
1592
+ if (/\b(skip tests?|no verify|don't verify|khong test|khong verify|bo qua test|không test|không verify|bỏ qua test)\b/i.test(text)) {
1589
1593
  return false;
1590
1594
  }
1591
- return /\b(fix|bug|error|test|build|lint|typecheck|compile|refactor|implement|edit|write|change|patch|debug|sua|loi|kiem tra|bien dich|trien khai|lam|doi|viet)\b/i.test(text);
1595
+ return /\b(fix|bug|error|test|build|lint|typecheck|compile|refactor|implement|edit|write|change|patch|debug|sua|loi|kiem tra|bien dich|trien khai|lam|doi|viet|sửa|lỗi|kiểm tra|biên dịch|triển khai|làm|đổi|viết)\b/i.test(text);
1592
1596
  }
1593
1597
 
1594
1598
  responseNeedsToolEvidence(content = '') {
1595
1599
  const text = String(content || '').toLowerCase();
1596
1600
  if (!text.trim()) return false;
1597
1601
 
1598
- 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)/i;
1602
+ 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;
1599
1603
  if (clarification.test(text)) return false;
1600
1604
  return true;
1601
1605
  }
@@ -1614,17 +1618,52 @@ ${colors.reset}
1614
1618
  ].join('\n');
1615
1619
  }
1616
1620
 
1621
+ withCurrentAbortSignal(options = {}) {
1622
+ const signal = options.signal || options.abortSignal || this.currentAbortController?.signal;
1623
+ return signal ? { ...options, signal } : options;
1624
+ }
1625
+
1626
+ isAbortError(error) {
1627
+ return error?.name === 'AbortError' || error?.message === 'AbortError';
1628
+ }
1629
+
1630
+ isRateLimitError(error) {
1631
+ const message = String(error?.message || error || '');
1632
+ return error?.status === 429 || /\b429\b|rate[_ -]?limit|tokens per minute|\bTPM\b/i.test(message);
1633
+ }
1634
+
1635
+ isTimeoutError(error) {
1636
+ const message = String(error?.message || error || '');
1637
+ return error?.name === 'TimeoutError'
1638
+ || error?.code === 'ETIMEDOUT'
1639
+ || /timed out|timeout|request aborted/i.test(message);
1640
+ }
1641
+
1642
+ cancelCurrentTask() {
1643
+ if (this.isCancelled) return;
1644
+ this.isCancelled = true;
1645
+ if (this.spinner) this.spinner.stop();
1646
+ if (this.currentAbortController && !this.currentAbortController.signal.aborted) {
1647
+ this.currentAbortController.abort(new DOMException('The operation was aborted.', 'AbortError'));
1648
+ }
1649
+ console.log(`\n\x1b[31m[ Đã hủy công việc hiện tại ]\x1b[0m`);
1650
+ }
1651
+
1617
1652
  async requestAssistantTurn(messages, options, startedAt, totalUsage) {
1653
+ const requestOptions = this.withCurrentAbortSignal(options);
1618
1654
  if (typeof this.ai.streamRequest === 'function') {
1619
1655
  try {
1620
- const streamed = await this.collectAssistantStream(messages, options, startedAt, totalUsage);
1656
+ const streamed = await this.collectAssistantStream(messages, requestOptions, startedAt, totalUsage);
1621
1657
  if (streamed) return streamed;
1622
1658
  } catch (error) {
1659
+ if (this.isAbortError(error)) throw new Error('AbortError');
1660
+ if (this.isRateLimitError(error)) throw error;
1661
+ if (this.isTimeoutError(error)) throw error;
1623
1662
  console.log(`${colors.dim}Streaming failed, retrying normal response: ${error.message}${colors.reset}`);
1624
1663
  }
1625
1664
  }
1626
1665
 
1627
- const response = await this.ai.sendRequest(messages, options);
1666
+ const response = await this.ai.sendRequest(messages, requestOptions);
1628
1667
  this.addUsage(totalUsage, response.usage);
1629
1668
  const assistantMsg = response.choices?.[0]?.message || {};
1630
1669
  const inlineToolExtraction = this.extractInlineToolCalls(assistantMsg.content || '');
@@ -1783,198 +1822,6 @@ ${colors.reset}
1783
1822
  process.stdout.write(colors.reset);
1784
1823
  }
1785
1824
 
1786
- installSlashSuggestions() {
1787
- if (!process.stdin.isTTY) return;
1788
-
1789
- readline.emitKeypressEvents(process.stdin, this.rl);
1790
-
1791
- process.stdin.on('keypress', (str, key = {}) => {
1792
- if (key.ctrl && key.name === 'v') {
1793
- void this.handleDirectClipboardPaste();
1794
- return;
1795
- }
1796
- if (key.ctrl || key.meta) return;
1797
-
1798
- if (typeof str === 'string' && str.length > 1) {
1799
- return;
1800
- }
1801
-
1802
- if (this.slashMenu.open && this.handleSlashMenuKey(key)) {
1803
- return;
1804
- }
1805
-
1806
- if (key.name === 'escape' && this.isProcessing) {
1807
- this.isCancelled = true;
1808
- if (this.spinner) this.spinner.stop();
1809
- console.log(`\n\x1b[31m[ ?? nh?n l?nh H?Y... AI s? k?t th?c ? thao t?c ti?p theo ]\x1b[0m`);
1810
- return;
1811
- }
1812
-
1813
- queueMicrotask(() => {
1814
- const line = this.rl?.line || '';
1815
- if (!line.startsWith('/')) {
1816
- this.closeSlashMenu();
1817
- return;
1818
- }
1819
-
1820
- this.openSlashMenu(line);
1821
- });
1822
- });
1823
- }
1824
-
1825
- async handleDirectClipboardPaste() {
1826
- if (this._handlingDirectClipboardPaste || this.readlineClosed || !this.running) return false;
1827
- this._handlingDirectClipboardPaste = true;
1828
- try {
1829
- const image = await this.getClipboardImage();
1830
- if (!image) return false;
1831
-
1832
- const prompt = (this.rl?.line || '').trim() || 'Analyze this pasted clipboard image.';
1833
- this.closeSlashMenu();
1834
- if (this.rl?.write) {
1835
- this.rl.write(null, { ctrl: true, name: 'u' });
1836
- }
1837
-
1838
- this.inputQueue = this.inputQueue
1839
- .then(async () => {
1840
- this.closeInputBox();
1841
- await this.processPastedImageTask(prompt, image);
1842
- })
1843
- .catch((error) => {
1844
- this.closeInputBox();
1845
- console.log(`\n${colors.red}? Paste image error: ${error.message}${colors.reset}\n`);
1846
- if (this.running && !this.readlineClosed) this.showInputPrompt();
1847
- });
1848
- return true;
1849
- } finally {
1850
- this._handlingDirectClipboardPaste = false;
1851
- }
1852
- }
1853
-
1854
- async processPastedImageTask(prompt, image) {
1855
- this.isProcessing = true;
1856
- this.isCancelled = false;
1857
- try {
1858
- await this.chat(prompt, [image]);
1859
- } finally {
1860
- this.isProcessing = false;
1861
- if (this.taskQueue.length > 0) {
1862
- const nextTask = this.taskQueue.shift();
1863
- setTimeout(() => this.processInputTask(nextTask), 0);
1864
- } else if (!this.readlineClosed) {
1865
- this.showInputPrompt();
1866
- }
1867
- }
1868
- }
1869
-
1870
- openSlashMenu(line) {
1871
- const matches = this.getSlashSuggestions(line);
1872
- if (matches.length === 0) {
1873
- this.closeSlashMenu();
1874
- return;
1875
- }
1876
- if (this.slashMenu.open && this.slashMenu.line === line) return;
1877
-
1878
- this.slashMenu = { open: true, line, items: matches, selected: 0, printedLines: this.slashMenu?.printedLines || 0 };
1879
- this.renderSlashMenu();
1880
- }
1881
-
1882
- closeSlashMenu() {
1883
- if (this.slashMenu && this.slashMenu.printedLines) {
1884
- readline.moveCursor(process.stdout, 0, -this.slashMenu.printedLines);
1885
- readline.clearScreenDown(process.stdout);
1886
- }
1887
- this.slashMenu = { open: false, line: '', items: [], selected: 0, printedLines: 0 };
1888
- }
1889
-
1890
- handleSlashMenuKey(key = {}) {
1891
- if (key.name === 'up') {
1892
- this.moveSlashSelection(-1);
1893
- return true;
1894
- }
1895
- if (key.name === 'down') {
1896
- this.moveSlashSelection(1);
1897
- return true;
1898
- }
1899
- if (key.name === 'tab') {
1900
- this.acceptSlashSelection();
1901
- return true;
1902
- }
1903
- if (key.name === 'escape') {
1904
- this.closeSlashMenu();
1905
- this.rl.prompt(true);
1906
- return true;
1907
- }
1908
- return false;
1909
- }
1910
-
1911
- moveSlashSelection(delta) {
1912
- if (!this.slashMenu.items.length) return;
1913
- const count = this.slashMenu.items.length;
1914
- this.slashMenu.selected = (this.slashMenu.selected + delta + count) % count;
1915
- this.renderSlashMenu();
1916
- }
1917
-
1918
- acceptSlashSelection() {
1919
- const item = this.slashMenu.items[this.slashMenu.selected];
1920
- if (!item) return;
1921
-
1922
- const currentLine = String(this.rl?.line ?? this.slashMenu.line ?? '');
1923
- const slashPrefixMatch = currentLine.match(/^\s*(\/\S*)(.*)$/);
1924
- const prefix = slashPrefixMatch ? slashPrefixMatch[1] : currentLine.trim();
1925
- const suffixText = slashPrefixMatch ? slashPrefixMatch[2] : '';
1926
- const needsSpace = item.usage && suffixText && !/^\s/.test(suffixText);
1927
- const replacement = `${item.cmd}${needsSpace ? ' ' : ''}${suffixText}`.trimEnd();
1928
-
1929
- this.rl.write(null, { ctrl: true, name: 'u' });
1930
- this.rl.write(replacement || prefix || item.cmd);
1931
- this.closeSlashMenu();
1932
- this.rl.prompt(true);
1933
- }
1934
-
1935
- renderSlashMenu() {
1936
- const matches = this.slashMenu.items;
1937
- if (!matches.length) return;
1938
-
1939
- if (this.slashMenu.printedLines) {
1940
- readline.moveCursor(process.stdout, 0, -this.slashMenu.printedLines);
1941
- }
1942
-
1943
- process.stdout.write('\n');
1944
- readline.clearLine(process.stdout, 1);
1945
- process.stdout.write(`${colors.dim}Commands${colors.reset}\n`);
1946
-
1947
- const maxDisplay = 5;
1948
- const displayedMatches = matches.slice(0, maxDisplay);
1949
-
1950
- displayedMatches.forEach((item, index) => {
1951
- readline.clearLine(process.stdout, 1);
1952
- const usage = item.usage ? ` ${colors.dim}${item.usage}${colors.reset}` : '';
1953
- const pointer = index === this.slashMenu.selected ? `${colors.green}>${colors.reset}` : ' ';
1954
- process.stdout.write(`${pointer} ${colors.cyan}${item.cmd}${colors.reset} ${colors.dim}${item.desc}${colors.reset}${usage}\n`);
1955
- });
1956
-
1957
- if (matches.length > maxDisplay) {
1958
- readline.clearLine(process.stdout, 1);
1959
- process.stdout.write(` ${colors.dim}... v? ${matches.length - maxDisplay} l?nh kh?c (g? ti?p ?? l?c)${colors.reset}\n`);
1960
- }
1961
-
1962
- readline.clearLine(process.stdout, 1);
1963
- process.stdout.write(`${colors.dim}?/? ch?n ? Enter/Tab d?ng ? Esc ??ng${colors.reset}\n`);
1964
-
1965
- // X?a c?c d?ng th?a n?u s? l??ng d?ng m?i ?t h?n s? l??ng d?ng c?
1966
- const currentLines = Math.min(matches.length, maxDisplay) + 3 + (matches.length > maxDisplay ? 1 : 0);
1967
- if (this.slashMenu.printedLines > currentLines) {
1968
- for (let i = 0; i < this.slashMenu.printedLines - currentLines; i++) {
1969
- readline.clearLine(process.stdout, 1);
1970
- process.stdout.write('\n');
1971
- }
1972
- readline.moveCursor(process.stdout, 0, -(this.slashMenu.printedLines - currentLines));
1973
- }
1974
-
1975
- this.slashMenu.printedLines = currentLines;
1976
- this.rl.prompt(true);
1977
- }
1978
1825
 
1979
1826
  getSlashSuggestions(line) {
1980
1827
  const query = String(line || '').trim();
@@ -1984,7 +1831,7 @@ ${colors.reset}
1984
1831
  const preferred = [
1985
1832
  '/help', '/new', '/history', '/exit', '/pwd', '/cd',
1986
1833
  '/provider', '/model', '/models', '/providers',
1987
- '/theme:toggle',
1834
+ '/theme:toggle', '/tui',
1988
1835
  '/auto', '/debug', '/doctor', '/context', '/scorecard', '/swe',
1989
1836
  '/read', '/write', '/glob', '/grep', '/bash',
1990
1837
  '/codex', '/claude', '/karpathy', '/agents',
@@ -2106,6 +1953,7 @@ ${colors.reset}
2106
1953
  provider: executionProfile.provider,
2107
1954
  model: executionProfile.model,
2108
1955
  enableTools: false,
1956
+ signal: this.currentAbortController?.signal,
2109
1957
  });
2110
1958
  this.addUsage(totalUsage, response.usage);
2111
1959
  const content = response.choices?.[0]?.message?.content || '';
@@ -2114,6 +1962,7 @@ ${colors.reset}
2114
1962
  }
2115
1963
  return content;
2116
1964
  } catch (error) {
1965
+ if (this.isAbortError(error)) throw new Error('AbortError');
2117
1966
  const fallback = this.buildToolFallbackAnswer(toolSummaries, error.message);
2118
1967
  console.log(`\n${colors.yellow}${fallback}${colors.reset}\n`);
2119
1968
  return fallback;
@@ -2131,6 +1980,7 @@ ${colors.reset}
2131
1980
  provider: profile.provider,
2132
1981
  model: profile.model,
2133
1982
  enableTools: false,
1983
+ signal: this.currentAbortController?.signal,
2134
1984
  })) {
2135
1985
  if (chunk.usage) this.addUsage(totalUsage, chunk.usage);
2136
1986
  if (chunk.content) {
@@ -2146,6 +1996,9 @@ ${colors.reset}
2146
1996
  }
2147
1997
  } catch (error) {
2148
1998
  process.stdout.write(colors.reset);
1999
+ if (this.isAbortError(error)) throw new Error('AbortError');
2000
+ if (this.isRateLimitError(error)) throw error;
2001
+ if (this.isTimeoutError(error)) throw error;
2149
2002
  console.log(`${colors.dim}Streaming failed, retrying normal response: ${error.message}${colors.reset}`);
2150
2003
  }
2151
2004
 
@@ -2153,6 +2006,7 @@ ${colors.reset}
2153
2006
  provider: profile.provider,
2154
2007
  model: profile.model,
2155
2008
  enableTools: false,
2009
+ signal: this.currentAbortController?.signal,
2156
2010
  });
2157
2011
  this.addUsage(totalUsage, response.usage);
2158
2012
  content = response.choices?.[0]?.message?.content || '';
@@ -2165,17 +2019,13 @@ ${colors.reset}
2165
2019
  printAssistantAnswer(content, startedAt, usage = {}) {
2166
2020
  const formatted = formatMarkdown(content);
2167
2021
  const footer = this.formatAnswerFooter(startedAt, usage);
2168
- const body = String(formatted || '').split(/\r?\n/);
2169
- console.log(`\n${renderBox({
2170
- title: 'Assistant',
2022
+ console.log(`\n${renderAssistantPanel({
2023
+ content: formatted,
2024
+ footer,
2025
+ colors,
2171
2026
  width: terminalWidth(72, 120, 92),
2172
- borderColor: colors.blue,
2173
- titleColor: colors.cyan,
2174
- body: [...body, '', `${colors.dim}${footer}${colors.reset}`],
2175
2027
  })}\n`);
2176
2028
  return;
2177
- console.log(`${colors.dim}${'?'.repeat(50)}${colors.reset}`);
2178
- console.log(`${colors.dim}${this.formatAnswerFooter(startedAt, usage)}${colors.reset}\n`);
2179
2029
  }
2180
2030
 
2181
2031
  formatAnswerFooter(startedAt, usage = {}) {
@@ -2223,16 +2073,29 @@ ${colors.reset}
2223
2073
  }
2224
2074
 
2225
2075
  async promptToolPermission(commandText) {
2226
- const side = this.useUnicodeUi ? '?' : '|';
2227
- const warn = this.useUnicodeUi ? '?' : '!';
2228
- process.stdout.write(`${colors.magenta}${side}${colors.reset} ${colors.yellow}${warn} AI mu?n ch?y: ${colors.bright}${commandText}${colors.reset}\n`);
2229
- process.stdout.write(`${colors.magenta}${side}${colors.reset} ${colors.cyan}1.${colors.reset} Cho ph?p\n`);
2230
- process.stdout.write(`${colors.magenta}${side}${colors.reset} ${colors.cyan}2.${colors.reset} Cho ph?p trong phi?n\n`);
2231
- process.stdout.write(`${colors.magenta}${side}${colors.reset} ${colors.cyan}3.${colors.reset} Kh?ng cho ph?p\n`);
2076
+ const c = colors;
2077
+ const width = terminalWidth(68, 100, 80);
2078
+ const body = [
2079
+ `${c.yellow}${this.useUnicodeUi ? '⚠' : '!'} AI wants to run${c.reset}`,
2080
+ `${c.bright}${c.white}${commandText}${c.reset}`,
2081
+ '',
2082
+ `${c.cyan}1.${c.reset} Allow once`,
2083
+ `${c.cyan}2.${c.reset} Allow for session`,
2084
+ `${c.cyan}3.${c.reset} Deny`,
2085
+ ];
2086
+
2087
+ console.log(renderBox({
2088
+ title: 'Tool Permission',
2089
+ width,
2090
+ borderColor: c.magenta,
2091
+ titleColor: c.yellow,
2092
+ body,
2093
+ boxChars: { topLeft: '+', topRight: '+', bottomLeft: '+', bottomRight: '+', horizontal: '-', vertical: '|', teeLeft: '+', teeRight: '+' },
2094
+ }));
2232
2095
 
2233
2096
  while (true) {
2234
2097
  const answer = await new Promise(resolve => {
2235
- this.rl.question(`${colors.magenta}${side}${colors.reset} ${colors.yellow}Ch?n [1/2/3]: ${colors.reset}`, resolve);
2098
+ this.rl.question(`${c.yellow}Choice [1/2/3]: ${c.reset}`, resolve);
2236
2099
  });
2237
2100
 
2238
2101
  const choice = String(answer || '').trim().toLowerCase();
@@ -2246,7 +2109,7 @@ ${colors.reset}
2246
2109
  return false;
2247
2110
  }
2248
2111
 
2249
- process.stdout.write(`${colors.magenta}${side}${colors.reset} ${colors.dim}Vui l?ng ch?n 1, 2 ho?c 3.${colors.reset}\n`);
2112
+ console.log(`${c.dim}Please choose 1, 2, or 3.${c.reset}`);
2250
2113
  }
2251
2114
  }
2252
2115
 
@@ -2297,7 +2160,7 @@ ${colors.reset}
2297
2160
  await this.session.replaceMemory('[Conversation Summary]', compressed.summary, 'summary');
2298
2161
 
2299
2162
  if (verbose) {
2300
- console.log(`${colors.green}? Compressed ${compressed.omittedCount} old message(s) into session summary.${colors.reset}`);
2163
+ console.log(`${colors.green} Compressed ${compressed.omittedCount} old message(s) into session summary.${colors.reset}`);
2301
2164
  }
2302
2165
  return compressed;
2303
2166
  }
@@ -2371,7 +2234,7 @@ ${colors.reset}
2371
2234
  this.rl.question(`${colors.yellow}Apply is not automatic here. Continue? [y/N]: ${colors.reset}`, resolve);
2372
2235
  });
2373
2236
  console.log(/^y(es)?$/i.test(String(answer || '').trim())
2374
- ? `${colors.green}? Confirmed${colors.reset}`
2237
+ ? `${colors.green} Confirmed${colors.reset}`
2375
2238
  : `${colors.dim}Cancelled${colors.reset}`);
2376
2239
  }
2377
2240
  }
@@ -2380,7 +2243,7 @@ ${colors.reset}
2380
2243
  const action = args[0];
2381
2244
  if (action === 'stop') {
2382
2245
  this.stopWatchers();
2383
- console.log(`${colors.green}? Watcher stopped${colors.reset}`);
2246
+ console.log(`${colors.green} Watcher stopped${colors.reset}`);
2384
2247
  return;
2385
2248
  }
2386
2249
 
@@ -2405,7 +2268,7 @@ ${colors.reset}
2405
2268
  run(name);
2406
2269
  });
2407
2270
  this.watchers.push(watcher);
2408
- console.log(`${colors.green}? Watching ${this.projectPath}${colors.reset}`);
2271
+ console.log(`${colors.green} Watching ${this.projectPath}${colors.reset}`);
2409
2272
  console.log(`${colors.dim}Command: ${command}. Use /watch stop to stop.${colors.reset}`);
2410
2273
  }
2411
2274
 
@@ -2438,7 +2301,7 @@ ${colors.reset}
2438
2301
  const promptHistory = this.getCompressedPromptHistory({
2439
2302
  limit: 20,
2440
2303
  keepRecent: 14,
2441
- maxTotalChars: this.shouldUseCompactPrompt() ? 5000 : 12000,
2304
+ maxTotalChars: 16000,
2442
2305
  });
2443
2306
  if (promptHistory.summary) {
2444
2307
  messages.push({ role: 'system', content: `Compressed prior conversation:\n${promptHistory.summary}` });
@@ -2468,7 +2331,7 @@ ${colors.reset}
2468
2331
  await this.session.addToHistory({ role: 'user', content: message });
2469
2332
  await this.session.addToHistory({ role: 'assistant', content: finalContent });
2470
2333
 
2471
- // T? ??ng verify: n?u AI ?? d?ng tools (s?a code), ch?y test/build
2334
+ // Tự động verify: nếu AI đã dùng tools (sửa code), chạy test/build.
2472
2335
  if (finalContent && this.shouldAutoVerifyAfterTools(message, usedMutatingTools)) {
2473
2336
  const sessionContext = this.session?.getContext?.() || {};
2474
2337
  const profile = String(sessionContext.workflowProfile || 'general');
@@ -2479,7 +2342,8 @@ ${colors.reset}
2479
2342
  }
2480
2343
 
2481
2344
  } catch (error) {
2482
- console.log(`\n${colors.red}? Error: ${error.message}${colors.reset}\n`);
2345
+ if (this.isAbortError(error)) throw error;
2346
+ console.log(`\n${colors.red}✖ Error: ${error.message}${colors.reset}\n`);
2483
2347
  }
2484
2348
  }
2485
2349
 
@@ -2541,7 +2405,7 @@ ${colors.reset}
2541
2405
  }
2542
2406
 
2543
2407
  /**
2544
- * Ch?y verification commands (test, build) v? tr? v? k?t qu?
2408
+ * Chạy verification commands (test, build) trả về kết quả.
2545
2409
  */
2546
2410
  async inferVerificationCommands(task = '') {
2547
2411
  const fs = await import('fs/promises');
@@ -2551,7 +2415,7 @@ ${colors.reset}
2551
2415
  const pkg = JSON.parse(await fs.readFile(packagePath, 'utf8'));
2552
2416
  const scripts = pkg.scripts || {};
2553
2417
  if (scripts.test) candidates.push('npm test');
2554
- if (scripts.build && /\b(build|compile|type|typescript|tsc|frontend|ui|design|next|vite|react|debug|fix|bug|error|l?i)\b/i.test(task)) {
2418
+ if (scripts.build && /\b(build|compile|type|typescript|tsc|frontend|ui|design|next|vite|react|debug|fix|bug|error|loi|lỗi)\b/i.test(task)) {
2555
2419
  candidates.push('npm run build');
2556
2420
  }
2557
2421
  if (scripts.lint && /\b(lint|style|eslint|quality|review)\b/i.test(task)) candidates.push('npm run lint');
@@ -2593,10 +2457,10 @@ ${colors.reset}
2593
2457
  }
2594
2458
 
2595
2459
  /**
2596
- * V?ng l?p t? ??ng verify + s?a l?i:
2597
- * - Ch?y test/build
2598
- * - N?u fail, g?i l?i cho AI fix
2599
- * - L?p ??n khi pass h?t ho?c h?t s? l?n th?
2460
+ * Vòng lặp tự động verify + sửa lỗi:
2461
+ * - Chạy test/build
2462
+ * - Nếu fail, gửi lại cho AI fix
2463
+ * - Lặp đến khi pass hết hoặc hết số lần thử
2600
2464
  */
2601
2465
  async verifyAndHeal(messages, tools, maxAttempts = 5) {
2602
2466
  const verifCommands = await this.inferVerificationCommands(this.getLatestUserText(messages));
@@ -2607,7 +2471,7 @@ ${colors.reset}
2607
2471
  const result = await this.runVerification(verifCommands);
2608
2472
 
2609
2473
  if (result.passed) {
2610
- console.log(`\n${colors.green}? All verifications passed!${colors.reset}\n`);
2474
+ console.log(`\n${colors.green} All verifications passed!${colors.reset}\n`);
2611
2475
  return;
2612
2476
  }
2613
2477
 
@@ -2617,7 +2481,7 @@ ${colors.reset}
2617
2481
  .map(r => `Command: ${r.cmd}\n${r.output}`)
2618
2482
  .join('\n\n---\n\n');
2619
2483
 
2620
- console.log(`\n${colors.yellow}? Verification failed. Sending errors back to AI for fix...${colors.reset}\n`);
2484
+ console.log(`\n${colors.yellow}Verification failed. Sending errors back to AI for fix...${colors.reset}\n`);
2621
2485
 
2622
2486
  // Push error output as user message for AI to fix
2623
2487
  const fixPrompt = `VERIFICATION FAILED (attempt ${attempt}/${maxAttempts}):
@@ -2635,12 +2499,12 @@ Do NOT stop until all errors are resolved.`;
2635
2499
  const { usedTools: fixUsedTools } = await this.runConversation(messages, 'Fixing', tools);
2636
2500
 
2637
2501
  if (!fixUsedTools) {
2638
- console.log(`\n${colors.red}? AI did not attempt to fix the errors. Stopping.${colors.reset}\n`);
2502
+ console.log(`\n${colors.red}AI did not attempt to fix the errors. Stopping.${colors.reset}\n`);
2639
2503
  break;
2640
2504
  }
2641
2505
  }
2642
2506
 
2643
- console.log(`\n${colors.red}? Max verification attempts (${maxAttempts}) reached. Some issues may remain.${colors.reset}\n`);
2507
+ console.log(`\n${colors.red}Max verification attempts (${maxAttempts}) reached. Some issues may remain.${colors.reset}\n`);
2644
2508
  }
2645
2509
 
2646
2510
  shouldUseTools(message = '', imageAttachments = []) {
@@ -2676,9 +2540,9 @@ Do NOT stop until all errors are resolved.`;
2676
2540
  ];
2677
2541
 
2678
2542
  const promptHistory = this.getCompressedPromptHistory({
2679
- limit: this.shouldUseCompactPrompt() ? 14 : 30,
2680
- keepRecent: this.shouldUseCompactPrompt() ? 8 : 14,
2681
- maxTotalChars: this.shouldUseCompactPrompt() ? 5000 : 12000,
2543
+ limit: 40,
2544
+ keepRecent: 16,
2545
+ maxTotalChars: 16000,
2682
2546
  });
2683
2547
  if (promptHistory.summary) {
2684
2548
  messages.push({ role: 'system', content: `Compressed prior conversation:\n${promptHistory.summary}` });
@@ -2770,14 +2634,14 @@ Do NOT stop until all errors are resolved.`;
2770
2634
  const passed = result.usedTools && Boolean(readEvent || /readme\.md/i.test(result.finalContent || ''));
2771
2635
 
2772
2636
  if (passed) {
2773
- console.log(`${colors.green}? Tool calling works for ${provider}/${model}.${colors.reset}`);
2637
+ console.log(`${colors.green} Tool calling works for ${provider}/${model}.${colors.reset}`);
2774
2638
  if (readEvent) {
2775
2639
  console.log(`${colors.dim} Last Read result: ${readEvent.result?.path || probePath}${colors.reset}`);
2776
2640
  }
2777
2641
  return { success: true, provider, model, usedTools: result.usedTools, beforeEvents };
2778
2642
  }
2779
2643
 
2780
- console.log(`${colors.red}? Tool calling did not execute for ${provider}/${model}.${colors.reset}`);
2644
+ console.log(`${colors.red} Tool calling did not execute for ${provider}/${model}.${colors.reset}`);
2781
2645
  console.log(`${colors.yellow} Try a stronger model or use a provider that supports OpenAI-compatible tools/fallback text output.${colors.reset}`);
2782
2646
  return { success: false, provider, model, usedTools: result.usedTools, beforeEvents };
2783
2647
  }
@@ -2858,17 +2722,24 @@ Do NOT stop until all errors are resolved.`;
2858
2722
  }
2859
2723
 
2860
2724
  async getProjectContext(task = '') {
2725
+ const modelTier = this.getActiveModelTier();
2861
2726
  const context = [];
2862
2727
  const requiredLocalResources = await this.getRequiredLocalResourceSummary();
2863
2728
  if (requiredLocalResources) {
2864
2729
  context.push(requiredLocalResources);
2865
2730
  }
2866
2731
 
2732
+ const shouldIncludeResources = /\b(resource|resources|skill|skills|plugin|plugins|claude|codex|agent|agents|design|ui|figma|brand|mcp)\b/i.test(String(task || ''));
2733
+ const localResources = shouldIncludeResources ? await this.getLocalResourceContext() : '';
2734
+ if (localResources) {
2735
+ context.push(localResources);
2736
+ }
2737
+
2867
2738
  const projectInstructionFiles = await this.readProjectInstructionFiles();
2868
2739
 
2869
2740
  for (const file of projectInstructionFiles) {
2870
2741
  try {
2871
- const preview = this.compactText(file.content, this.shouldUseCompactPrompt() ? 450 : 900, 'project instruction');
2742
+ const preview = this.compactText(file.content, 1200, 'project instruction');
2872
2743
  context.push(`[${file.relativePath}]\n${preview}`);
2873
2744
  } catch { }
2874
2745
  }
@@ -2878,21 +2749,24 @@ Do NOT stop until all errors are resolved.`;
2878
2749
  const stat = await fs.stat(packageJsonPath);
2879
2750
  if (stat.isFile()) {
2880
2751
  const content = await fs.readFile(packageJsonPath, 'utf-8');
2881
- context.push(`[package.json]\n${this.compactText(content, this.shouldUseCompactPrompt() ? 650 : 1200, 'package.json')}`);
2752
+ context.push(`[package.json]\n${this.compactText(content, 1600, 'package.json')}`);
2882
2753
  }
2883
2754
  } catch { }
2884
2755
 
2885
- const shouldIncludeResources = /\b(resource|resources|skill|skills|plugin|plugins|claude|codex|agent|agents|design|ui|figma|brand|mcp)\b/i.test(String(task || ''));
2886
- const localResources = shouldIncludeResources ? await this.getLocalResourceContext() : '';
2887
- if (localResources) {
2888
- context.push(localResources);
2889
- }
2890
-
2891
2756
  const codebaseContext = await this.buildCodebaseContext(task);
2892
2757
  if (codebaseContext) {
2893
2758
  context.push(codebaseContext);
2894
2759
  }
2895
2760
 
2761
+ const graphContext = await this.codebaseSearcher?.buildGraphContext?.(task, {
2762
+ maxNodes: 24,
2763
+ maxCodeBlocks: 8,
2764
+ maxCodeBlockSize: 1800,
2765
+ });
2766
+ if (graphContext) {
2767
+ context.push(`[CodeGraph Context]\n${this.compactText(graphContext, 5200, 'codegraph context')}`);
2768
+ }
2769
+
2896
2770
  // Git Context
2897
2771
  try {
2898
2772
  const { execSync } = await import('child_process');
@@ -2902,20 +2776,20 @@ Do NOT stop until all errors are resolved.`;
2902
2776
 
2903
2777
  const gitSummary = execSync('git diff --stat --summary', { cwd: this.projectPath, encoding: 'utf8', stdio: 'pipe', maxBuffer: 1024 * 50 }).trim();
2904
2778
  if (gitSummary) {
2905
- context.push(`[Git Summary]\n${this.compactText(gitSummary, this.shouldUseCompactPrompt() ? 650 : 1200, 'git summary')}`);
2779
+ context.push(`[Git Summary]\n${this.compactText(gitSummary, 1200, 'git summary')}`);
2906
2780
  }
2907
2781
 
2908
2782
  // Get brief git diff for context
2909
2783
  const gitDiff = execSync('git diff', { cwd: this.projectPath, encoding: 'utf8', stdio: 'pipe', maxBuffer: 1024 * 50 }).trim().split('\n').slice(0, 30).join('\n');
2910
2784
  if (gitDiff) {
2911
- context.push(`[Git Diff]\n${this.compactText(gitDiff, this.shouldUseCompactPrompt() ? 900 : 1800, 'git diff')}`);
2785
+ context.push(`[Git Diff]\n${this.compactText(gitDiff, 2200, 'git diff')}`);
2912
2786
  }
2913
2787
  }
2914
2788
  } catch (e) {
2915
2789
  // Not a git repo or git not installed
2916
2790
  }
2917
2791
 
2918
- return this.compactText(context.join('\n\n') || 'No project context found.', this.shouldUseCompactPrompt() ? 4200 : 9000, 'project context');
2792
+ return this.compactText(context.join('\n\n') || 'No project context found.', this.getProjectContextBudget(modelTier), 'project context');
2919
2793
  }
2920
2794
 
2921
2795
  async getLocalResourceContext() {
@@ -3017,7 +2891,7 @@ Do NOT stop until all errors are resolved.`;
3017
2891
  this.promptBuilder.tools = this.tools;
3018
2892
  this.promptBuilder.sessionPermissionGrants = this.sessionPermissionGrants;
3019
2893
  return this.promptBuilder.buildSystemPrompt(context, {
3020
- projectContextBudget: this.shouldUseCompactPrompt() ? 2200 : 3200,
2894
+ modelTier: this.getActiveModelTier(),
3021
2895
  });
3022
2896
  }
3023
2897
 
@@ -3064,7 +2938,7 @@ Do NOT stop until all errors are resolved.`;
3064
2938
  }
3065
2939
  config.mcp.servers.forEach(server => {
3066
2940
  const enabled = server.enabled === false ? `${colors.red}disabled${colors.reset}` : `${colors.green}enabled${colors.reset}`;
3067
- console.log(` ? ${server.name} (${enabled}) -> ${server.command}${server.args?.length ? ` ${server.args.join(' ')}` : ''}`);
2941
+ console.log(` - ${server.name} (${enabled}) -> ${server.command}${server.args?.length ? ` ${server.args.join(' ')}` : ''}`);
3068
2942
  });
3069
2943
  break;
3070
2944
  case 'add': {
@@ -3086,7 +2960,7 @@ Do NOT stop until all errors are resolved.`;
3086
2960
  config.mcp.servers = (config.mcp.servers || []).filter(server => server.name !== name);
3087
2961
  config.mcp.servers.push({ name, command, args: parsedArgs, enabled: true });
3088
2962
  await this.config.save(config);
3089
- console.log(`${colors.green}? Added MCP server: ${name}${colors.reset}`);
2963
+ console.log(`${colors.green} Added MCP server: ${name}${colors.reset}`);
3090
2964
  break;
3091
2965
  }
3092
2966
  case 'remove': {
@@ -3097,7 +2971,7 @@ Do NOT stop until all errors are resolved.`;
3097
2971
  }
3098
2972
  config.mcp.servers = (config.mcp.servers || []).filter(server => server.name !== name);
3099
2973
  await this.config.save(config);
3100
- console.log(`${colors.green}? Removed MCP server: ${name}${colors.reset}`);
2974
+ console.log(`${colors.green} Removed MCP server: ${name}${colors.reset}`);
3101
2975
  break;
3102
2976
  }
3103
2977
  case 'allow': {
@@ -3107,7 +2981,7 @@ Do NOT stop until all errors are resolved.`;
3107
2981
  break;
3108
2982
  }
3109
2983
  await this.config.setPermissionAllowlist({ mcpServers: [name] });
3110
- console.log(`${colors.green}? MCP server allowed: ${name}${colors.reset}`);
2984
+ console.log(`${colors.green} MCP server allowed: ${name}${colors.reset}`);
3111
2985
  break;
3112
2986
  }
3113
2987
  default:
@@ -3142,13 +3016,13 @@ Do NOT stop until all errors are resolved.`;
3142
3016
  break;
3143
3017
  }
3144
3018
  await this.config.setPermissionAllowlist({ [field]: [value] });
3145
- console.log(`${colors.green}? Allowed ${kind}: ${value}${colors.reset}`);
3019
+ console.log(`${colors.green} Allowed ${kind}: ${value}${colors.reset}`);
3146
3020
  break;
3147
3021
  }
3148
3022
  case 'prompt': {
3149
3023
  const value = String(rest[0] || '').toLowerCase();
3150
3024
  await this.config.setPermissionAllowlist({ promptByDefault: !(value === 'off' || value === 'false' || value === '0' || value === 'no') });
3151
- console.log(`${colors.green}? Updated prompt policy${colors.reset}`);
3025
+ console.log(`${colors.green} Updated prompt policy${colors.reset}`);
3152
3026
  break;
3153
3027
  }
3154
3028
  default: