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/bin/winter.js +1 -0
- package/package.json +1 -1
- package/src/agent/runtime.js +10 -16
- package/src/ai/model-capabilities.js +15 -0
- package/src/ai/prompts/system-prompt.js +26 -4
- package/src/ai/providers.js +132 -55
- package/src/ai/small-model-amplifier.js +2 -2
- package/src/cli/commands.js +12 -0
- package/src/cli/context-loader.js +1 -1
- package/src/cli/diff-view.js +88 -32
- package/src/cli/input-controller.js +77 -44
- package/src/cli/markdown-format.js +12 -7
- package/src/cli/prompt-builder.js +20 -11
- package/src/cli/repl-commands.js +3 -0
- package/src/cli/repl.js +299 -358
- package/src/cli/slash-commands.js +1 -0
- package/src/cli/snowflake-logo.js +64 -86
- package/src/cli/terminal-manager.js +74 -0
- package/src/cli/terminal-ui.js +139 -86
- package/src/cli/tool-runtime.js +8 -3
- package/src/cli/tui.js +195 -0
- package/src/context/token-juice.js +37 -10
- package/src/tools/executor.js +78 -3
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 {
|
|
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'),
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
627
|
+
this.codebaseWarmup = this.ensureCodebaseIndex({ verbose: false })
|
|
600
628
|
.then(() => this.startCodebaseWatcher())
|
|
601
629
|
.catch((error) => {
|
|
602
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
2066
|
+
|
|
2067
|
+
if (this.spinner) this.spinner.stop();
|
|
2142
2068
|
|
|
2143
2069
|
if (content) {
|
|
2144
|
-
|
|
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
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
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
|
|
2227
|
-
const
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
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(`${
|
|
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
|
-
|
|
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.',
|
|
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
|
-
|
|
2970
|
+
modelTier: this.getActiveModelTier(),
|
|
3030
2971
|
});
|
|
3031
2972
|
}
|
|
3032
2973
|
|