winter-super-cli 2026.6.6 → 2026.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli/repl.js CHANGED
@@ -6,8 +6,16 @@
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 } 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,
@@ -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,6 +123,8 @@ 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() {
@@ -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();
@@ -240,6 +260,7 @@ export class WinterREPL {
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'), 4200, '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
  }
@@ -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
  });
@@ -471,7 +496,7 @@ export class WinterREPL {
471
496
  const fileName = path.basename(targetPath);
472
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
 
@@ -487,7 +512,7 @@ export class WinterREPL {
487
512
  const content = await fsPromises.readFile(p, 'utf8');
488
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
  }
@@ -512,7 +537,7 @@ export class WinterREPL {
512
537
  for (const file of projectInstructionFiles) {
513
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) {
@@ -551,8 +576,8 @@ export class WinterREPL {
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
582
  // Nạp luôn vào memory.
558
583
  await this.session.replaceMemory(`[Quy tắc dự án từ winter.md]`, template);
@@ -577,13 +602,13 @@ 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}`);
605
+ this.startupNotice(`updated ${doc.filename}`);
581
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}`);
611
+ this.startupNotice(`created ${doc.filename}`);
587
612
  const memoryKey = `[Quy tắc dự án từ ${doc.filename}]`;
588
613
  await this.session.replaceMemory(memoryKey, doc.content);
589
614
  } catch (err) {
@@ -596,51 +621,19 @@ 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,
@@ -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
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) {
@@ -1277,60 +1299,13 @@ 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ửi tin nhắn trực tiếp để chat, ESC để hủy${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() {
@@ -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 false;
1512
+ const tier = this.getActiveModelTier();
1513
+ return tier === 'tiny' || tier === 'small';
1510
1514
  }
1511
1515
 
1512
1516
  selectExecutionProfile(messages = [], options = {}) {
@@ -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}${(this.useUnicodeUi ? '─' : '-').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
 
@@ -2479,6 +2342,7 @@ ${colors.reset}
2479
2342
  }
2480
2343
 
2481
2344
  } catch (error) {
2345
+ if (this.isAbortError(error)) throw error;
2482
2346
  console.log(`\n${colors.red}✖ Error: ${error.message}${colors.reset}\n`);
2483
2347
  }
2484
2348
  }
@@ -2858,6 +2722,7 @@ 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) {
@@ -2924,7 +2789,7 @@ Do NOT stop until all errors are resolved.`;
2924
2789
  // Not a git repo or git not installed
2925
2790
  }
2926
2791
 
2927
- return this.compactText(context.join('\n\n') || 'No project context found.', 14000, 'project context');
2792
+ return this.compactText(context.join('\n\n') || 'No project context found.', this.getProjectContextBudget(modelTier), 'project context');
2928
2793
  }
2929
2794
 
2930
2795
  async getLocalResourceContext() {
@@ -3026,7 +2891,7 @@ Do NOT stop until all errors are resolved.`;
3026
2891
  this.promptBuilder.tools = this.tools;
3027
2892
  this.promptBuilder.sessionPermissionGrants = this.sessionPermissionGrants;
3028
2893
  return this.promptBuilder.buildSystemPrompt(context, {
3029
- projectContextBudget: 5200,
2894
+ modelTier: this.getActiveModelTier(),
3030
2895
  });
3031
2896
  }
3032
2897