winter-super-cli 2026.6.6 → 2026.6.8

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
@@ -6,8 +6,17 @@
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';
19
+ import { terminalManager } from './terminal-manager.js';
11
20
  import { WinterInputController } from './input-controller.js';
12
21
  import { ToolExecutor } from '../tools/executor.js';
13
22
  import { SessionManager } from '../session/manager.js';
@@ -30,7 +39,7 @@ import {
30
39
  buildPromptToolResultWithTokenJuice,
31
40
  } from './tool-runtime.js';
32
41
  import { TokenJuice } from '../context/token-juice.js';
33
- import { classifyModelTier } from '../ai/model-capabilities.js';
42
+ import { classifyModelTier, getModelBudgetMultiplier } from '../ai/model-capabilities.js';
34
43
  import {
35
44
  addUsage as mergeUsage,
36
45
  buildToolCallSignature as buildToolCallSignatureText,
@@ -88,6 +97,7 @@ export class WinterREPL {
88
97
  this.taskQueue = [];
89
98
  this.isProcessing = false;
90
99
  this.isCancelled = false;
100
+ this.currentAbortController = null;
91
101
  this.sessionPermissionGrants = new Set();
92
102
  this.permissionManager = new PermissionManager(this.config, this.session);
93
103
  this.contextLoader = new ContextLoader({ projectPath: this.projectPath, session: this.session, tools: this.tools });
@@ -114,6 +124,10 @@ export class WinterREPL {
114
124
  this.useUnicodeUi = supportsUnicodeUi();
115
125
  this.inputController = new WinterInputController(this);
116
126
  this.watchers = [];
127
+ this.startupNotices = [];
128
+ this._fixedPanel = Boolean(process.stdout.isTTY) && process.env.WINTER_FIXED_PANEL_TUI !== '0';
129
+
130
+ terminalManager.install();
117
131
  }
118
132
 
119
133
  async initCodebaseSearch() {
@@ -147,6 +161,15 @@ export class WinterREPL {
147
161
  });
148
162
  }
149
163
 
164
+ startupNotice(message) {
165
+ const text = String(message || '').trim();
166
+ if (!text) return;
167
+ this.startupNotices.push(text);
168
+ if (this.startupNotices.length > 6) {
169
+ this.startupNotices = this.startupNotices.slice(-6);
170
+ }
171
+ }
172
+
150
173
  async startCodebaseWatcher() {
151
174
  if (this.codebaseWatcher) return;
152
175
  await this.initCodebaseSearch();
@@ -240,6 +263,7 @@ export class WinterREPL {
240
263
 
241
264
  async buildCodebaseContext(task = '') {
242
265
  try {
266
+ const modelTier = this.getActiveModelTier();
243
267
  const stats = await this.ensureCodebaseIndex({ verbose: false });
244
268
  if (!stats.totalChunks) return '';
245
269
 
@@ -278,7 +302,7 @@ export class WinterREPL {
278
302
  }
279
303
  }
280
304
 
281
- return this.compactText(lines.join('\n'), 4200, 'codebase context');
305
+ return this.compactText(lines.join('\n'), this.getCodebaseContextBudget(modelTier), 'codebase context');
282
306
  } catch (error) {
283
307
  return `[Codebase Index]\nUnavailable: ${error.message}`;
284
308
  }
@@ -326,18 +350,22 @@ export class WinterREPL {
326
350
  toolName,
327
351
  result,
328
352
  compact: this.shouldUseCompactPrompt(),
353
+ modelTier: this.getActiveModelTier(),
329
354
  compactText: (text, maxChars, label) => this.compactText(text, maxChars, label),
330
355
  summarizeToolResult: value => this.tools?.summarizeToolResult?.(value) || { ...value },
331
356
  });
332
357
  }
333
358
 
334
359
  async buildPromptToolResultForModel(toolName, result) {
360
+ const modelTier = this.getActiveModelTier();
361
+ const tokenJuice = this.getTokenJuiceForModelTier(modelTier);
335
362
  return buildPromptToolResultWithTokenJuice({
336
363
  toolName,
337
364
  result,
338
365
  projectPath: this.projectPath,
339
- tokenJuice: this.tokenJuice,
366
+ tokenJuice,
340
367
  compact: this.shouldUseCompactPrompt(),
368
+ modelTier,
341
369
  compactText: (text, maxChars, label) => this.compactText(text, maxChars, label),
342
370
  summarizeToolResult: value => this.tools?.summarizeToolResult?.(value) || { ...value },
343
371
  });
@@ -471,7 +499,7 @@ export class WinterREPL {
471
499
  const fileName = path.basename(targetPath);
472
500
  const memoryKey = `[Tự động ghi nhớ file ${fileName}]`;
473
501
  await this.session.replaceMemory(memoryKey, content);
474
- console.log(`${colors.dim}✓ Đã tự động nạp và ghi nhớ file ${fileName}${colors.reset}`);
502
+ this.startupNotice(`loaded ${fileName}`);
475
503
  continue;
476
504
  }
477
505
 
@@ -487,7 +515,7 @@ export class WinterREPL {
487
515
  const content = await fsPromises.readFile(p, 'utf8');
488
516
  const memoryKey = `[Tự động ghi nhớ file ${path.basename(targetPath)}/${c}]`;
489
517
  await this.session.replaceMemory(memoryKey, content);
490
- console.log(`${colors.dim}✓ Đã tự động nạp và ghi nhớ ${path.basename(targetPath)}/${c}${colors.reset}`);
518
+ this.startupNotice(`loaded ${path.basename(targetPath)}/${c}`);
491
519
  loaded = true;
492
520
  break;
493
521
  }
@@ -512,7 +540,7 @@ export class WinterREPL {
512
540
  for (const file of projectInstructionFiles) {
513
541
  const memoryKey = `[Quy tắc dự án từ ${file.relativePath}]`;
514
542
  await this.session.replaceMemory(memoryKey, file.content);
515
- console.log(`${colors.dim}✓ Đã nạp quy tắc dự án từ ${file.relativePath}${colors.reset}`);
543
+ this.startupNotice(`rules ${file.relativePath}`);
516
544
  }
517
545
  }
518
546
  } catch (e) {
@@ -551,8 +579,8 @@ export class WinterREPL {
551
579
  try {
552
580
  const projectWinterMd = path.join(this.projectPath, 'winter.md');
553
581
  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`);
582
+ this.startupNotice('created winter.md');
583
+
556
584
 
557
585
  // Nạp luôn vào memory.
558
586
  await this.session.replaceMemory(`[Quy tắc dự án từ winter.md]`, template);
@@ -577,13 +605,13 @@ export class WinterREPL {
577
605
  if (!isWinterGeneratedProjectDoc(existing)) continue;
578
606
 
579
607
  await fsPromises.writeFile(filePath, doc.content, 'utf8');
580
- console.log(`${colors.green}✓ Đã nâng cấp file ${doc.filename} từ local resources.${colors.reset}`);
608
+ this.startupNotice(`updated ${doc.filename}`);
581
609
  const memoryKey = `[Quy tắc dự án từ ${doc.filename}]`;
582
610
  await this.session.replaceMemory(memoryKey, doc.content);
583
611
  } catch {
584
612
  try {
585
613
  await fsPromises.writeFile(filePath, doc.content, 'utf8');
586
- console.log(`${colors.green}✓ Đã tự động tạo file ${doc.filename} từ local resources!${colors.reset}`);
614
+ this.startupNotice(`created ${doc.filename}`);
587
615
  const memoryKey = `[Quy tắc dự án từ ${doc.filename}]`;
588
616
  await this.session.replaceMemory(memoryKey, doc.content);
589
617
  } catch (err) {
@@ -596,51 +624,19 @@ export class WinterREPL {
596
624
  await this.compactStartupMemories({ projectInstructionFiles, autoCreateDocs });
597
625
 
598
626
  // Codebase Index: warm in background, then inject summaries into model context on demand.
599
- this.codebaseWarmup = this.ensureCodebaseIndex({ verbose: true })
627
+ this.codebaseWarmup = this.ensureCodebaseIndex({ verbose: false })
600
628
  .then(() => this.startCodebaseWatcher())
601
629
  .catch((error) => {
602
- console.log(`${colors.yellow}Codebase index disabled: ${error.message}${colors.reset}`);
630
+ this.startupNotice(`codebase disabled: ${error.message}`);
603
631
  });
604
632
 
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
633
  const sessionHistory = this.session.getHistory(4);
624
634
  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`);
635
+ this.startupNotice(`${sessionHistory.length} recent messages`);
642
636
  }
643
637
 
638
+ this.showStatus();
639
+
644
640
  // Setup readline
645
641
  this.rl = readline.createInterface({
646
642
  input: process.stdin,
@@ -661,22 +657,106 @@ export class WinterREPL {
661
657
  // Hiển thị prompt lần đầu tiên ngay khi khởi động xong.
662
658
  this.showInputPrompt();
663
659
 
664
- this.rl.on('line', (line) => {
660
+ // Paste buffer: gom nhiều dòng paste nhanh thành 1 tin nhắn
661
+ this._multilineBuffer = [];
662
+ this._pasteBuffer = [];
663
+ this._pasteTimer = null;
664
+ this._isPasteChunk = false;
665
+ this._pasteChunkTimer = null;
666
+ const PASTE_DELAY = 80;
667
+
668
+ process.stdin.on('data', (chunk) => {
669
+ // If a large chunk or chunk with newlines arrives, it's definitely a paste.
670
+ if (chunk.length > 3 || chunk.includes('\n')) {
671
+ this._isPasteChunk = true;
672
+ if (this._pasteChunkTimer) clearTimeout(this._pasteChunkTimer);
673
+ this._pasteChunkTimer = setTimeout(() => {
674
+ this._isPasteChunk = false;
675
+ }, 150);
676
+ }
677
+ });
678
+
679
+ const flushPasteBuffer = () => {
680
+ this._pasteTimer = null;
681
+ if (this._pasteBuffer.length === 0) return;
682
+
683
+ const isSingleLineInput = this._pasteBuffer.length === 1 && !this._isPasteChunk;
684
+ const isJustEmptyEnter = this._pasteBuffer.length === 1 && this._pasteBuffer[0].trim() === '';
685
+
686
+ // Normal single-line submit
687
+ if (isSingleLineInput && this._multilineBuffer.length === 0) {
688
+ const line = this._pasteBuffer[0].trim();
689
+ this._pasteBuffer = [];
690
+ if (!line) {
691
+ if (this.running && !this.readlineClosed) {
692
+ readline.moveCursor(process.stdout, 0, -1);
693
+ readline.clearLine(process.stdout, 0);
694
+ this.rl.prompt(true);
695
+ }
696
+ return;
697
+ }
698
+
699
+ // Command to enter multiline mode manually
700
+ if (line === '/multi' || line === '/m') {
701
+ this._multilineBuffer.push('');
702
+ console.log(`${colors.cyan}│ ${colors.dim}[ Đã bật chế độ gõ nhiều dòng. Nhấn Enter 2 lần (dòng trống) để gửi. ]${colors.reset}`);
703
+ if (this.running && !this.readlineClosed) this.rl.prompt(true);
704
+ return;
705
+ }
706
+
707
+ this.submitInputQueue(line);
708
+ return;
709
+ }
710
+
711
+ // We are in multiline/paste mode
712
+ this._multilineBuffer.push(...this._pasteBuffer);
713
+ this._pasteBuffer = [];
714
+
715
+ // If they pressed Enter on an empty line, submit the multiline buffer!
716
+ if (isJustEmptyEnter && this._multilineBuffer.length > 1) {
717
+ // Remove the trailing empty line
718
+ this._multilineBuffer.pop();
719
+ const combined = this._multilineBuffer.join('\n').trim();
720
+ this._multilineBuffer = [];
721
+ this._isPasteChunk = false;
722
+
723
+ if (!combined) {
724
+ if (this.running && !this.readlineClosed) {
725
+ this.closeInputBox();
726
+ this.showInputPrompt();
727
+ }
728
+ return;
729
+ }
730
+
731
+ this.submitInputQueue(combined);
732
+ return;
733
+ }
734
+
735
+ // Otherwise, we are still collecting! Wait for user to submit.
736
+ readline.clearLine(process.stdout, 0);
737
+ readline.cursorTo(process.stdout, 0);
738
+ const linesCount = this._multilineBuffer.length;
739
+ console.log(`${colors.cyan}│ ${colors.dim}[ Đang nhập nhiều dòng (${linesCount} dòng)... Nhấn Enter ở dòng trống để gửi ]${colors.reset}`);
740
+ if (this.running && !this.readlineClosed) this.rl.prompt(true);
741
+ };
742
+
743
+ this.submitInputQueue = (combined) => {
665
744
  this.inputQueue = this.inputQueue
666
745
  .then(async () => {
667
746
  this.closeInputBox();
668
- const input = line.trim();
669
- if (input) {
670
- await this.handleInput(input);
671
- } else {
672
- if (this.running && !this.readlineClosed) this.showInputPrompt();
673
- }
747
+ await this.handleInput(combined);
674
748
  })
675
749
  .catch((error) => {
676
750
  this.closeInputBox();
677
751
  console.log(`\n${colors.red}? Error: ${error.message}${colors.reset}\n`);
678
752
  if (this.running && !this.readlineClosed) this.showInputPrompt();
679
753
  });
754
+ };
755
+
756
+ this.rl.on('line', (line) => {
757
+ this._pasteBuffer.push(line);
758
+ if (this._pasteTimer) clearTimeout(this._pasteTimer);
759
+ this._pasteTimer = setTimeout(flushPasteBuffer, PASTE_DELAY);
680
760
  });
681
761
 
682
762
  this.rl.on('close', async () => {
@@ -711,16 +791,39 @@ export class WinterREPL {
711
791
  return this.inputController.closeInputBox();
712
792
  }
713
793
 
794
+ closeSlashMenu() {
795
+ return this.inputController.closeSlashMenu();
796
+ }
797
+
798
+ handleSlashMenuKey(key = {}) {
799
+ return this.inputController.handleSlashMenuKey(key);
800
+ }
801
+
802
+ handleDirectClipboardPaste() {
803
+ return this.inputController.handleDirectClipboardPaste();
804
+ }
805
+
714
806
  buildInputPanel() {
715
807
  return this.inputController.buildInputPanel();
716
808
  }
717
809
 
718
810
  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('');
811
+ const snapshot = buildTuiSnapshot(this);
812
+ if (process.stdout.isTTY) {
813
+ console.log(`\n${welcomeBanner(this.version, {
814
+ project: snapshot.projectPath,
815
+ session: snapshot.sessionShort,
816
+ provider: snapshot.provider,
817
+ model: snapshot.model,
818
+ })}\n`);
819
+ return;
820
+ }
821
+ console.log(`\n${renderConversationStartup(snapshot, { colors })}\n`);
822
+ }
823
+
824
+ showTuiDashboard() {
825
+ const snapshot = buildTuiSnapshot(this);
826
+ console.log(`\n${renderLandingTui(snapshot, { colors, title: 'Winter Dashboard' })}\n`);
724
827
  }
725
828
 
726
829
  getResourcePaths() {
@@ -917,22 +1020,34 @@ export class WinterREPL {
917
1020
  async handleInput(input) {
918
1021
  if (this.isProcessing) {
919
1022
  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}`);
1023
+ const preview = input.length > 40 ? input.slice(0, 37) + '...' : input;
1024
+ console.log(`${colors.yellow}⧗${colors.reset} ${colors.bright}Queued #${pos}${colors.reset} ${colors.dim}› ${preview}${colors.reset}`);
921
1025
  this.taskQueue.push(input);
1026
+ if (!this.readlineClosed) this.showInputPrompt();
922
1027
  return;
923
1028
  }
924
- await this.processInputTask(input);
1029
+ this.processInputTask(input).catch(err => {
1030
+ console.log(colors.red + '\nLỗi xử lý hàng đợi: ' + err.message + colors.reset);
1031
+ });
925
1032
  }
926
1033
 
927
1034
  async processInputTask(input) {
928
1035
  this.isProcessing = true;
929
1036
  this.isCancelled = false;
1037
+ this.currentAbortController = new AbortController();
930
1038
  try {
1039
+ this.closeSlashMenu();
931
1040
  this.history.push(input);
932
1041
  if (this.history.length > this.maxHistory) {
933
1042
  this.history = this.history.slice(-this.maxHistory);
934
1043
  }
935
1044
 
1045
+ // Echo tin nhắn user để xác nhận đã nhận
1046
+ if (!input.startsWith('/') && !input.startsWith('!')) {
1047
+ const preview = input.length > 120 ? input.slice(0, 117) + '...' : input;
1048
+ console.log(`\n${colors.bright}${colors.green}You${colors.reset} ${colors.dim}›${colors.reset} ${colors.white}${preview}${colors.reset}`);
1049
+ }
1050
+
936
1051
  if (input.startsWith('!')) {
937
1052
  const command = input.slice(1).trim();
938
1053
  if (!command) {
@@ -998,16 +1113,20 @@ export class WinterREPL {
998
1113
  }
999
1114
  } catch (error) {
1000
1115
  if (error.message === 'AbortError') {
1001
- console.log(colors.red + '\nĐã hủy công việc hiện tại.' + colors.reset);
1116
+ if (!this.isCancelled) {
1117
+ console.log(colors.red + '\nĐã hủy công việc hiện tại.' + colors.reset);
1118
+ }
1002
1119
  } else {
1003
1120
  console.log(colors.red + '\nLỗi: ' + error.message + colors.reset);
1004
1121
  }
1005
1122
  } finally {
1006
1123
  this.isProcessing = false;
1007
1124
  this.isCancelled = false;
1125
+ this.currentAbortController = null;
1008
1126
  if (this.spinner) this.spinner.stop();
1009
1127
 
1010
1128
  if (this.taskQueue.length > 0) {
1129
+ this.closeInputBox();
1011
1130
  const nextTask = this.taskQueue.shift();
1012
1131
  setTimeout(() => this.processInputTask(nextTask), 0);
1013
1132
  } else {
@@ -1277,60 +1396,13 @@ CRITICAL DEBUG/AGENT RULES:
1277
1396
  showCommandMenu() {
1278
1397
  const c = colors;
1279
1398
  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}`,
1399
+ const snapshot = buildTuiSnapshot(this);
1400
+ console.log(`\n${renderLandingTui(snapshot, {
1401
+ colors: c,
1402
+ title: 'Winter Agent Console',
1327
1403
  width,
1328
- borderColor: c.magenta,
1329
- titleColor: c.cyan,
1330
- body,
1331
- })}
1332
- ${c.dim}Gửi tin nhắn trực tiếp để chat, ESC để hủy${c.reset}
1333
- `);
1404
+ })}`);
1405
+ 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
1406
  }
1335
1407
 
1336
1408
  showHelp() {
@@ -1500,13 +1572,42 @@ ${colors.reset}
1500
1572
  }
1501
1573
 
1502
1574
  getActiveModelTier() {
1575
+ if (typeof this.ai?._modelTier === 'string' && this.ai._modelTier) {
1576
+ return this.ai._modelTier;
1577
+ }
1503
1578
  const providerName = this.ai?.getActiveProvider?.();
1504
1579
  const model = this.ai?.providers?.[providerName]?.model || '';
1505
1580
  return classifyModelTier(model, providerName);
1506
1581
  }
1507
1582
 
1583
+ getBudgetScale(modelTier = this.getActiveModelTier()) {
1584
+ return getModelBudgetMultiplier(modelTier);
1585
+ }
1586
+
1587
+ getProjectContextBudget(modelTier = this.getActiveModelTier()) {
1588
+ return Math.round(6000 * this.getBudgetScale(modelTier));
1589
+ }
1590
+
1591
+ getCodebaseContextBudget(modelTier = this.getActiveModelTier()) {
1592
+ return Math.round(4200 * this.getBudgetScale(modelTier));
1593
+ }
1594
+
1595
+ getTokenJuiceInlineBudget(modelTier = this.getActiveModelTier()) {
1596
+ return Math.max(800, Math.round(1400 * this.getBudgetScale(modelTier)));
1597
+ }
1598
+
1599
+ getTokenJuiceForModelTier(modelTier = this.getActiveModelTier()) {
1600
+ const tokenJuice = new TokenJuice({
1601
+ projectPath: this.projectPath,
1602
+ inlineBudgetTokens: this.getTokenJuiceInlineBudget(modelTier),
1603
+ });
1604
+ this.tokenJuice = tokenJuice;
1605
+ return tokenJuice;
1606
+ }
1607
+
1508
1608
  shouldUseCompactPrompt() {
1509
- return false;
1609
+ const tier = this.getActiveModelTier();
1610
+ return tier === 'tiny' || tier === 'small';
1510
1611
  }
1511
1612
 
1512
1613
  selectExecutionProfile(messages = [], options = {}) {
@@ -1614,17 +1715,52 @@ ${colors.reset}
1614
1715
  ].join('\n');
1615
1716
  }
1616
1717
 
1718
+ withCurrentAbortSignal(options = {}) {
1719
+ const signal = options.signal || options.abortSignal || this.currentAbortController?.signal;
1720
+ return signal ? { ...options, signal } : options;
1721
+ }
1722
+
1723
+ isAbortError(error) {
1724
+ return error?.name === 'AbortError' || error?.message === 'AbortError';
1725
+ }
1726
+
1727
+ isRateLimitError(error) {
1728
+ const message = String(error?.message || error || '');
1729
+ return error?.status === 429 || /\b429\b|rate[_ -]?limit|tokens per minute|\bTPM\b/i.test(message);
1730
+ }
1731
+
1732
+ isTimeoutError(error) {
1733
+ const message = String(error?.message || error || '');
1734
+ return error?.name === 'TimeoutError'
1735
+ || error?.code === 'ETIMEDOUT'
1736
+ || /timed out|timeout|request aborted/i.test(message);
1737
+ }
1738
+
1739
+ cancelCurrentTask() {
1740
+ if (this.isCancelled) return;
1741
+ this.isCancelled = true;
1742
+ if (this.spinner) this.spinner.stop();
1743
+ if (this.currentAbortController && !this.currentAbortController.signal.aborted) {
1744
+ this.currentAbortController.abort(new DOMException('The operation was aborted.', 'AbortError'));
1745
+ }
1746
+ console.log(`\n\x1b[31m[ Đã hủy công việc hiện tại ]\x1b[0m`);
1747
+ }
1748
+
1617
1749
  async requestAssistantTurn(messages, options, startedAt, totalUsage) {
1750
+ const requestOptions = this.withCurrentAbortSignal(options);
1618
1751
  if (typeof this.ai.streamRequest === 'function') {
1619
1752
  try {
1620
- const streamed = await this.collectAssistantStream(messages, options, startedAt, totalUsage);
1753
+ const streamed = await this.collectAssistantStream(messages, requestOptions, startedAt, totalUsage);
1621
1754
  if (streamed) return streamed;
1622
1755
  } catch (error) {
1756
+ if (this.isAbortError(error)) throw new Error('AbortError');
1757
+ if (this.isRateLimitError(error)) throw error;
1758
+ if (this.isTimeoutError(error)) throw error;
1623
1759
  console.log(`${colors.dim}Streaming failed, retrying normal response: ${error.message}${colors.reset}`);
1624
1760
  }
1625
1761
  }
1626
1762
 
1627
- const response = await this.ai.sendRequest(messages, options);
1763
+ const response = await this.ai.sendRequest(messages, requestOptions);
1628
1764
  this.addUsage(totalUsage, response.usage);
1629
1765
  const assistantMsg = response.choices?.[0]?.message || {};
1630
1766
  const inlineToolExtraction = this.extractInlineToolCalls(assistantMsg.content || '');
@@ -1709,22 +1845,11 @@ ${colors.reset}
1709
1845
  }
1710
1846
 
1711
1847
  if (chunk.content) {
1712
- if (!printed) {
1713
- if (this.spinner) this.spinner.stop();
1714
- if (!bufferToolModeContent) {
1715
- process.stdout.write(`\n${colors.white}`);
1716
- printed = true;
1717
- }
1718
- }
1719
1848
  content += chunk.content;
1720
- if (!bufferToolModeContent) {
1721
- process.stdout.write(chunk.content);
1722
- }
1723
1849
  }
1724
1850
  }
1725
1851
 
1726
1852
  if (this.spinner) this.spinner.stop();
1727
- if (printed) process.stdout.write(colors.reset);
1728
1853
 
1729
1854
  const inlineToolExtraction = this.extractInlineToolCalls(content);
1730
1855
  const rawToolCalls = [
@@ -1737,7 +1862,7 @@ ${colors.reset}
1737
1862
  const toolCalls = this.normalizeToolCalls(rawToolCalls);
1738
1863
  const visibleContent = inlineToolExtraction.content || content;
1739
1864
 
1740
- if (bufferToolModeContent && toolCalls.length === 0 && visibleContent) {
1865
+ if (toolCalls.length === 0 && visibleContent) {
1741
1866
  if (options?.requireToolEvidence && this.responseNeedsToolEvidence(visibleContent)) {
1742
1867
  return {
1743
1868
  assistantMsg: { content: visibleContent },
@@ -1755,18 +1880,6 @@ ${colors.reset}
1755
1880
  };
1756
1881
  }
1757
1882
 
1758
- if (toolCalls.length === 0 && visibleContent) {
1759
- console.log(`\n${colors.dim}${this.formatAnswerFooter(startedAt, totalUsage)}${colors.reset}\n`);
1760
- return {
1761
- assistantMsg: { content: visibleContent },
1762
- toolCalls,
1763
- finalContent: visibleContent,
1764
- finishReason,
1765
- };
1766
- } else if (printed && visibleContent) {
1767
- process.stdout.write('\n');
1768
- }
1769
-
1770
1883
  return {
1771
1884
  assistantMsg: {
1772
1885
  content: visibleContent,
@@ -1783,198 +1896,6 @@ ${colors.reset}
1783
1896
  process.stdout.write(colors.reset);
1784
1897
  }
1785
1898
 
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
1899
 
1979
1900
  getSlashSuggestions(line) {
1980
1901
  const query = String(line || '').trim();
@@ -1984,7 +1905,7 @@ ${colors.reset}
1984
1905
  const preferred = [
1985
1906
  '/help', '/new', '/history', '/exit', '/pwd', '/cd',
1986
1907
  '/provider', '/model', '/models', '/providers',
1987
- '/theme:toggle',
1908
+ '/theme:toggle', '/tui',
1988
1909
  '/auto', '/debug', '/doctor', '/context', '/scorecard', '/swe',
1989
1910
  '/read', '/write', '/glob', '/grep', '/bash',
1990
1911
  '/codex', '/claude', '/karpathy', '/agents',
@@ -2106,6 +2027,7 @@ ${colors.reset}
2106
2027
  provider: executionProfile.provider,
2107
2028
  model: executionProfile.model,
2108
2029
  enableTools: false,
2030
+ signal: this.currentAbortController?.signal,
2109
2031
  });
2110
2032
  this.addUsage(totalUsage, response.usage);
2111
2033
  const content = response.choices?.[0]?.message?.content || '';
@@ -2114,6 +2036,7 @@ ${colors.reset}
2114
2036
  }
2115
2037
  return content;
2116
2038
  } catch (error) {
2039
+ if (this.isAbortError(error)) throw new Error('AbortError');
2117
2040
  const fallback = this.buildToolFallbackAnswer(toolSummaries, error.message);
2118
2041
  console.log(`\n${colors.yellow}${fallback}${colors.reset}\n`);
2119
2042
  return fallback;
@@ -2125,27 +2048,33 @@ ${colors.reset}
2125
2048
  const profile = executionProfile || this.selectExecutionProfile(messages, { enableTools: false });
2126
2049
 
2127
2050
  try {
2128
- process.stdout.write(`\n${colors.white}`);
2129
2051
  let isFirst = true;
2130
2052
  for await (const chunk of this.ai.streamRequest(messages, {
2131
2053
  provider: profile.provider,
2132
2054
  model: profile.model,
2133
2055
  enableTools: false,
2056
+ signal: this.currentAbortController?.signal,
2134
2057
  })) {
2058
+ if (isFirst) {
2059
+ isFirst = false;
2060
+ }
2135
2061
  if (chunk.usage) this.addUsage(totalUsage, chunk.usage);
2136
2062
  if (chunk.content) {
2137
2063
  content += chunk.content;
2138
- process.stdout.write(chunk.content);
2139
2064
  }
2140
2065
  }
2141
- process.stdout.write(colors.reset);
2066
+
2067
+ if (this.spinner) this.spinner.stop();
2142
2068
 
2143
2069
  if (content) {
2144
- console.log(`\n${colors.dim}${this.formatAnswerFooter(startedAt, totalUsage)}${colors.reset}\n`);
2070
+ this.printAssistantAnswer(content, startedAt, totalUsage);
2145
2071
  return content;
2146
2072
  }
2147
2073
  } catch (error) {
2148
2074
  process.stdout.write(colors.reset);
2075
+ if (this.isAbortError(error)) throw new Error('AbortError');
2076
+ if (this.isRateLimitError(error)) throw error;
2077
+ if (this.isTimeoutError(error)) throw error;
2149
2078
  console.log(`${colors.dim}Streaming failed, retrying normal response: ${error.message}${colors.reset}`);
2150
2079
  }
2151
2080
 
@@ -2153,6 +2082,7 @@ ${colors.reset}
2153
2082
  provider: profile.provider,
2154
2083
  model: profile.model,
2155
2084
  enableTools: false,
2085
+ signal: this.currentAbortController?.signal,
2156
2086
  });
2157
2087
  this.addUsage(totalUsage, response.usage);
2158
2088
  content = response.choices?.[0]?.message?.content || '';
@@ -2165,17 +2095,13 @@ ${colors.reset}
2165
2095
  printAssistantAnswer(content, startedAt, usage = {}) {
2166
2096
  const formatted = formatMarkdown(content);
2167
2097
  const footer = this.formatAnswerFooter(startedAt, usage);
2168
- const body = String(formatted || '').split(/\r?\n/);
2169
- console.log(`\n${renderBox({
2170
- title: 'Assistant',
2098
+ console.log(`\n${renderAssistantPanel({
2099
+ content: formatted,
2100
+ footer,
2101
+ colors,
2171
2102
  width: terminalWidth(72, 120, 92),
2172
- borderColor: colors.blue,
2173
- titleColor: colors.cyan,
2174
- body: [...body, '', `${colors.dim}${footer}${colors.reset}`],
2175
2103
  })}\n`);
2176
2104
  return;
2177
- console.log(`${colors.dim}${(this.useUnicodeUi ? '─' : '-').repeat(50)}${colors.reset}`);
2178
- console.log(`${colors.dim}${this.formatAnswerFooter(startedAt, usage)}${colors.reset}\n`);
2179
2105
  }
2180
2106
 
2181
2107
  formatAnswerFooter(startedAt, usage = {}) {
@@ -2223,16 +2149,29 @@ ${colors.reset}
2223
2149
  }
2224
2150
 
2225
2151
  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`);
2152
+ const c = colors;
2153
+ const width = terminalWidth(68, 100, 80);
2154
+ const body = [
2155
+ `${c.yellow}${this.useUnicodeUi ? '⚠' : '!'} AI wants to run${c.reset}`,
2156
+ `${c.bright}${c.white}${commandText}${c.reset}`,
2157
+ '',
2158
+ `${c.cyan}1.${c.reset} Allow once`,
2159
+ `${c.cyan}2.${c.reset} Allow for session`,
2160
+ `${c.cyan}3.${c.reset} Deny`,
2161
+ ];
2162
+
2163
+ console.log(renderBox({
2164
+ title: 'Tool Permission',
2165
+ width,
2166
+ borderColor: c.magenta,
2167
+ titleColor: c.yellow,
2168
+ body,
2169
+ boxChars: { topLeft: '+', topRight: '+', bottomLeft: '+', bottomRight: '+', horizontal: '-', vertical: '|', teeLeft: '+', teeRight: '+' },
2170
+ }));
2232
2171
 
2233
2172
  while (true) {
2234
2173
  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);
2174
+ this.rl.question(`${c.yellow}Choice [1/2/3]: ${c.reset}`, resolve);
2236
2175
  });
2237
2176
 
2238
2177
  const choice = String(answer || '').trim().toLowerCase();
@@ -2246,7 +2185,7 @@ ${colors.reset}
2246
2185
  return false;
2247
2186
  }
2248
2187
 
2249
- process.stdout.write(`${colors.magenta}${side}${colors.reset} ${colors.dim}Vui lòng chọn 1, 2 hoặc 3.${colors.reset}\n`);
2188
+ console.log(`${c.dim}Please choose 1, 2, or 3.${c.reset}`);
2250
2189
  }
2251
2190
  }
2252
2191
 
@@ -2479,6 +2418,7 @@ ${colors.reset}
2479
2418
  }
2480
2419
 
2481
2420
  } catch (error) {
2421
+ if (this.isAbortError(error)) throw error;
2482
2422
  console.log(`\n${colors.red}✖ Error: ${error.message}${colors.reset}\n`);
2483
2423
  }
2484
2424
  }
@@ -2858,6 +2798,7 @@ Do NOT stop until all errors are resolved.`;
2858
2798
  }
2859
2799
 
2860
2800
  async getProjectContext(task = '') {
2801
+ const modelTier = this.getActiveModelTier();
2861
2802
  const context = [];
2862
2803
  const requiredLocalResources = await this.getRequiredLocalResourceSummary();
2863
2804
  if (requiredLocalResources) {
@@ -2924,7 +2865,7 @@ Do NOT stop until all errors are resolved.`;
2924
2865
  // Not a git repo or git not installed
2925
2866
  }
2926
2867
 
2927
- return this.compactText(context.join('\n\n') || 'No project context found.', 14000, 'project context');
2868
+ return this.compactText(context.join('\n\n') || 'No project context found.', this.getProjectContextBudget(modelTier), 'project context');
2928
2869
  }
2929
2870
 
2930
2871
  async getLocalResourceContext() {
@@ -3026,7 +2967,7 @@ Do NOT stop until all errors are resolved.`;
3026
2967
  this.promptBuilder.tools = this.tools;
3027
2968
  this.promptBuilder.sessionPermissionGrants = this.sessionPermissionGrants;
3028
2969
  return this.promptBuilder.buildSystemPrompt(context, {
3029
- projectContextBudget: 5200,
2970
+ modelTier: this.getActiveModelTier(),
3030
2971
  });
3031
2972
  }
3032
2973