thatgfsj-code 0.2.1 → 0.2.2
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/CHANGELOG.md +131 -0
- package/DEVELOPMENT.md +286 -0
- package/README.md +131 -30
- package/dist/core/ai-engine.d.ts +17 -0
- package/dist/core/ai-engine.d.ts.map +1 -1
- package/dist/core/ai-engine.js +23 -7
- package/dist/core/ai-engine.js.map +1 -1
- package/dist/index.js +18 -27
- package/dist/index.js.map +1 -1
- package/dist/repl/input.d.ts +8 -0
- package/dist/repl/input.d.ts.map +1 -1
- package/dist/repl/input.js +12 -0
- package/dist/repl/input.js.map +1 -1
- package/dist/repl/loop.d.ts +56 -0
- package/dist/repl/loop.d.ts.map +1 -1
- package/dist/repl/loop.js +333 -4
- package/dist/repl/loop.js.map +1 -1
- package/dist/repl/output.d.ts.map +1 -1
- package/dist/repl/output.js +3 -1
- package/dist/repl/output.js.map +1 -1
- package/dist/repl/welcome.js +1 -1
- package/dist/repl/welcome.js.map +1 -1
- package/docs/API_KEY_GUIDE.md +6 -0
- package/docs/FAQ.md +25 -3
- package/package.json +35 -3
- package/.patches/0001-fix-repl-replace-readline-with-inquirer-input-for-ke.patch +0 -279
- package/.patches/0002-fix-repl-stream-AI-output-directly-to-stdout-so-term.patch +0 -564
- package/.patches/0003-fix-session-break-the-hallucination-loop-in-assistan.patch +0 -194
- package/.patches/0004-chore-release-bump-version-to-0.2.1.patch +0 -24
- package/ROADMAP.md +0 -107
- package/src/agent/core.ts +0 -179
- package/src/agent/index.ts +0 -8
- package/src/agent/intent.ts +0 -181
- package/src/agent/streaming.ts +0 -132
- package/src/core/ai-engine.ts +0 -437
- package/src/core/cli.ts +0 -171
- package/src/core/config.ts +0 -147
- package/src/core/context-compactor.ts +0 -245
- package/src/core/hooks.ts +0 -196
- package/src/core/permissions.ts +0 -308
- package/src/core/session.ts +0 -165
- package/src/core/skills.ts +0 -208
- package/src/core/state.ts +0 -120
- package/src/core/subagent.ts +0 -195
- package/src/core/system-prompt.ts +0 -163
- package/src/core/tool-registry.ts +0 -157
- package/src/core/types.ts +0 -280
- package/src/index.ts +0 -544
- package/src/mcp/client.ts +0 -330
- package/src/repl/index.ts +0 -8
- package/src/repl/input.ts +0 -139
- package/src/repl/loop.ts +0 -280
- package/src/repl/output.ts +0 -222
- package/src/repl/welcome.ts +0 -296
- package/src/tools/file.ts +0 -117
- package/src/tools/git.ts +0 -132
- package/src/tools/index.ts +0 -48
- package/src/tools/search.ts +0 -263
- package/src/tools/shell.ts +0 -181
- package/src/utils/diff-preview.ts +0 -202
- package/src/utils/index.ts +0 -8
- package/src/utils/memory.ts +0 -223
- package/src/utils/project-context.ts +0 -207
- package/tsconfig.json +0 -19
|
@@ -1,564 +0,0 @@
|
|
|
1
|
-
From 19eaf3769dc5275c2e84a2ae18674d71e36abd8c Mon Sep 17 00:00:00 2001
|
|
2
|
-
From: Thatgfsj <thatgfsj@users.noreply.github.com>
|
|
3
|
-
Date: Mon, 6 Jul 2026 02:33:42 +0800
|
|
4
|
-
Subject: [PATCH 2/4] fix(repl): stream AI output directly to stdout so
|
|
5
|
-
terminal scrollback works
|
|
6
|
-
|
|
7
|
-
Previously, after each streamed reply we would call rl.question() again,
|
|
8
|
-
which reset the terminal cursor / scrollback to the top of the buffer.
|
|
9
|
-
Users complained that the screen 'jumps to the top' during AI replies
|
|
10
|
-
and they cannot scroll back to inspect earlier parts of the same reply.
|
|
11
|
-
|
|
12
|
-
Now process.stdout.write(chunk) writes each chunk straight to the
|
|
13
|
-
underlying fd; the user's terminal retains scrollback so they can scroll
|
|
14
|
-
up to read prior turns or earlier parts of a long streamed reply.
|
|
15
|
-
---
|
|
16
|
-
src/index.ts | 148 +++++-------------------------------
|
|
17
|
-
src/repl/loop.ts | 191 ++++++++++++++++++++++++++++-------------------
|
|
18
|
-
2 files changed, 132 insertions(+), 207 deletions(-)
|
|
19
|
-
|
|
20
|
-
diff --git a/src/index.ts b/src/index.ts
|
|
21
|
-
index 497a29e..65edcde 100644
|
|
22
|
-
--- a/src/index.ts
|
|
23
|
-
+++ b/src/index.ts
|
|
24
|
-
@@ -16,7 +16,6 @@ if (process.platform === 'win32') {
|
|
25
|
-
import { program } from 'commander';
|
|
26
|
-
import chalk from 'chalk';
|
|
27
|
-
import ora from 'ora';
|
|
28
|
-
-import readline from 'readline';
|
|
29
|
-
import { fileURLToPath } from 'url';
|
|
30
|
-
import { homedir } from 'os';
|
|
31
|
-
import { dirname, join } from 'path';
|
|
32
|
-
@@ -28,6 +27,7 @@ import { SessionManager } from './core/session.js';
|
|
33
|
-
import { ConfigManager } from './core/config.js';
|
|
34
|
-
import { FileTool, ShellTool, GitTool, SearchTool } from './tools/index.js';
|
|
35
|
-
import { WelcomeScreen } from './repl/welcome.js';
|
|
36
|
-
+import { REPLLoop } from './repl/loop.js';
|
|
37
|
-
|
|
38
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
39
|
-
const __dirname = dirname(__filename);
|
|
40
|
-
@@ -252,50 +252,29 @@ Be concise but thorough. Show your reasoning.`;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Handle model switch in interactive mode
|
|
44
|
-
+ * (Not invoked from startInteractive anymore — REPLLoop handles it via /model.)
|
|
45
|
-
+ * Kept for backwards compatibility with any callers.
|
|
46
|
-
*/
|
|
47
|
-
-async function handleModelSwitch(rl: readline.Interface, currentConfig: any) {
|
|
48
|
-
+async function handleModelSwitch(_rl: unknown, currentConfig: any) {
|
|
49
|
-
console.log(chalk.cyan('\n切换模型...\n'));
|
|
50
|
-
-
|
|
51
|
-
- // Get available models based on current provider
|
|
52
|
-
+
|
|
53
|
-
const models = WelcomeScreen.getModelsForProvider(currentConfig.provider || 'siliconflow');
|
|
54
|
-
-
|
|
55
|
-
+
|
|
56
|
-
console.log(chalk.gray('可用模型:\n'));
|
|
57
|
-
- models.forEach((model, idx) => {
|
|
58
|
-
+ models.forEach((model: any, idx: number) => {
|
|
59
|
-
const selected = model.id === currentConfig.model ? ' ✓' : '';
|
|
60
|
-
console.log(chalk.gray(` ${idx + 1}. ${model.name} - ${model.desc}${selected}`));
|
|
61
|
-
});
|
|
62
|
-
-
|
|
63
|
-
- console.log();
|
|
64
|
-
-
|
|
65
|
-
- const answer = await new Promise<string>((resolve) => {
|
|
66
|
-
- rl.question(chalk.green('选择模型编号: '), resolve);
|
|
67
|
-
- });
|
|
68
|
-
-
|
|
69
|
-
- const idx = parseInt(answer) - 1;
|
|
70
|
-
- if (idx >= 0 && idx < models.length) {
|
|
71
|
-
- const selected = models[idx];
|
|
72
|
-
-
|
|
73
|
-
- // Save to config
|
|
74
|
-
- const config = { ...currentConfig, model: selected.id };
|
|
75
|
-
- const configPath = join(homedir(), '.thatgfsj', 'config.json');
|
|
76
|
-
-
|
|
77
|
-
- try {
|
|
78
|
-
- const dir = join(homedir(), '.thatgfsj');
|
|
79
|
-
- if (!existsSync(dir)) {
|
|
80
|
-
- mkdirSync(dir, { recursive: true });
|
|
81
|
-
- }
|
|
82
|
-
- writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
83
|
-
- console.log(chalk.green(`\n✓ 模型已切换为: ${selected.name}\n`));
|
|
84
|
-
- } catch (e: any) {
|
|
85
|
-
- console.error(chalk.red(`\n保存失败: ${e.message}\n`));
|
|
86
|
-
- }
|
|
87
|
-
- } else {
|
|
88
|
-
- console.log(chalk.yellow('\n无效选择,保持当前模型\n'));
|
|
89
|
-
- }
|
|
90
|
-
+
|
|
91
|
-
+ console.log(chalk.yellow('\n(自动切换已禁用,请编辑 ~/.thatgfsj/config.json 修改 model)\n'));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Start interactive mode (Claude Code style)
|
|
96
|
-
+ *
|
|
97
|
-
+ * 委托给 REPLLoop,它使用 @inquirer/input,支持方向键(含数字小键盘)、行内
|
|
98
|
-
+ * 编辑、命令历史、可滚动流式输出。原生 readline 在 Windows 终端下对小键盘
|
|
99
|
-
+ * 方向键的 ANSI 转义序列不友好 (Bug #1),所以这里不再直接用 readline。
|
|
100
|
-
*/
|
|
101
|
-
async function startInteractive() {
|
|
102
|
-
console.log(chalk.cyan('\n🤖 Thatgfsj Code - Interactive Mode\n'));
|
|
103
|
-
@@ -303,104 +282,15 @@ async function startInteractive() {
|
|
104
|
-
console.log(chalk.gray('─'.repeat(40)));
|
|
105
|
-
console.log(chalk.gray('\nCommands:'));
|
|
106
|
-
console.log(chalk.gray(' exit, Ctrl+C - Quit'));
|
|
107
|
-
- console.log(chalk.gray(' clear - Clear history'));
|
|
108
|
-
+ console.log(chalk.gray(' clear - Clear screen'));
|
|
109
|
-
console.log(chalk.gray(' context - Show project context'));
|
|
110
|
-
+ console.log(chalk.gray(' history - Show command history'));
|
|
111
|
-
+ console.log(chalk.gray(' help - Show all commands'));
|
|
112
|
-
console.log(chalk.gray('\n' + '─'.repeat(40) + '\n'));
|
|
113
|
-
-
|
|
114
|
-
- try {
|
|
115
|
-
- const config = await ConfigManager.load();
|
|
116
|
-
- const ai = new AIEngine(config);
|
|
117
|
-
- const session = new SessionManager();
|
|
118
|
-
-
|
|
119
|
-
- // Register tools - all available tools
|
|
120
|
-
- const shellTool = new ShellTool();
|
|
121
|
-
- const fileTool = new FileTool();
|
|
122
|
-
- const gitTool = new GitTool();
|
|
123
|
-
- const searchTool = new SearchTool();
|
|
124
|
-
-
|
|
125
|
-
- ai.registerTool(shellTool);
|
|
126
|
-
- ai.registerTool(fileTool);
|
|
127
|
-
- ai.registerTool(gitTool);
|
|
128
|
-
- ai.registerTool(searchTool);
|
|
129
|
-
-
|
|
130
|
-
- // System prompt
|
|
131
|
-
- const defaultSystem = `You are Thatgfsj Code, an AI coding assistant like Claude Code.
|
|
132
|
-
-You can read files, write files, and execute shell commands.
|
|
133
|
-
-Be helpful, concise, and show your reasoning.`;
|
|
134
|
-
- session.addMessage('system', defaultSystem);
|
|
135
|
-
-
|
|
136
|
-
- const rl = readline.createInterface({
|
|
137
|
-
- input: process.stdin,
|
|
138
|
-
- output: process.stdout,
|
|
139
|
-
- terminal: true
|
|
140
|
-
- });
|
|
141
|
-
-
|
|
142
|
-
- const ask = () => {
|
|
143
|
-
- rl.question(chalk.green('\n> '), async (input) => {
|
|
144
|
-
- const trimmed = input.trim();
|
|
145
|
-
-
|
|
146
|
-
- if (!trimmed) {
|
|
147
|
-
- ask();
|
|
148
|
-
- return;
|
|
149
|
-
- }
|
|
150
|
-
-
|
|
151
|
-
- // Commands
|
|
152
|
-
- if (trimmed === 'exit' || trimmed === 'quit' || trimmed === '\\x03') {
|
|
153
|
-
- console.log(chalk.gray('\n👋 Goodbye!'));
|
|
154
|
-
- rl.close();
|
|
155
|
-
- return;
|
|
156
|
-
- }
|
|
157
|
-
-
|
|
158
|
-
- if (trimmed === 'clear') {
|
|
159
|
-
- session.clear();
|
|
160
|
-
- console.clear();
|
|
161
|
-
- console.log(chalk.cyan('🤖 Thatgfsj Code - Interactive Mode\n'));
|
|
162
|
-
- ask();
|
|
163
|
-
- return;
|
|
164
|
-
- }
|
|
165
|
-
-
|
|
166
|
-
- if (trimmed === 'context') {
|
|
167
|
-
- console.log(chalk.cyan('\n' + getProjectContext() + '\n'));
|
|
168
|
-
- ask();
|
|
169
|
-
- return;
|
|
170
|
-
- }
|
|
171
|
-
-
|
|
172
|
-
- // Model switch command
|
|
173
|
-
- if (trimmed === '/model' || trimmed === 'model') {
|
|
174
|
-
- await handleModelSwitch(rl, config);
|
|
175
|
-
- ask();
|
|
176
|
-
- return;
|
|
177
|
-
- }
|
|
178
|
-
|
|
179
|
-
- // Add and process with streaming
|
|
180
|
-
- session.addMessage('user', trimmed);
|
|
181
|
-
-
|
|
182
|
-
- const spinner = ora(chalk.gray('Thinking...')).start();
|
|
183
|
-
- let fullResponse = '';
|
|
184
|
-
-
|
|
185
|
-
- try {
|
|
186
|
-
- // S01: Use async generator for true streaming
|
|
187
|
-
- for await (const chunk of ai.chatStream(session.getMessages())) {
|
|
188
|
-
- spinner.stop();
|
|
189
|
-
- process.stdout.write(chunk);
|
|
190
|
-
- fullResponse += chunk;
|
|
191
|
-
- }
|
|
192
|
-
-
|
|
193
|
-
- console.log(); // newline after streaming
|
|
194
|
-
-
|
|
195
|
-
- session.addMessage('assistant', fullResponse);
|
|
196
|
-
- session.truncate(20);
|
|
197
|
-
-
|
|
198
|
-
- } catch (error: any) {
|
|
199
|
-
- spinner.fail(chalk.red(`Error: ${error.message}`));
|
|
200
|
-
- }
|
|
201
|
-
-
|
|
202
|
-
- ask();
|
|
203
|
-
- });
|
|
204
|
-
- };
|
|
205
|
-
-
|
|
206
|
-
- ask();
|
|
207
|
-
-
|
|
208
|
-
+ try {
|
|
209
|
-
+ const repl = new REPLLoop();
|
|
210
|
-
+ await repl.start();
|
|
211
|
-
} catch (error: any) {
|
|
212
|
-
console.error(chalk.red(`\n❌ Failed to start: ${error.message}`));
|
|
213
|
-
process.exit(1);
|
|
214
|
-
diff --git a/src/repl/loop.ts b/src/repl/loop.ts
|
|
215
|
-
index 6881c6e..1b0810c 100644
|
|
216
|
-
--- a/src/repl/loop.ts
|
|
217
|
-
+++ b/src/repl/loop.ts
|
|
218
|
-
@@ -1,16 +1,19 @@
|
|
219
|
-
/**
|
|
220
|
-
* REPL Loop
|
|
221
|
-
* Main interactive loop with streaming support
|
|
222
|
-
+ *
|
|
223
|
-
+ * S07-fix:
|
|
224
|
-
+ * - 输入端换用 REPLInput (基于 @inquirer/input),支持方向键/小键盘
|
|
225
|
-
+ * - 流式输出保留完整内容(用户可滚动终端查看历史),不跳顶不刷屏
|
|
226
|
-
+ * - Ctrl+C 行为:有未提交内容=>清空当前输入;空输入两次=>退出
|
|
227
|
-
*/
|
|
228
|
-
|
|
229
|
-
import chalk from 'chalk';
|
|
230
|
-
-import { REPLInput } from './input.js';
|
|
231
|
-
+import { REPLInput, type PromptResult } from './input.js';
|
|
232
|
-
import { REPLOutput } from './output.js';
|
|
233
|
-
import { AIEngine } from '../core/ai-engine.js';
|
|
234
|
-
import { SessionManager } from '../core/session.js';
|
|
235
|
-
import { ConfigManager } from '../core/config.js';
|
|
236
|
-
-import { FileTool } from '../tools/file.js';
|
|
237
|
-
-import { ShellTool } from '../tools/shell.js';
|
|
238
|
-
import { SystemPromptBuilder } from '../core/system-prompt.js';
|
|
239
|
-
import { getBuiltInTools } from '../tools/index.js';
|
|
240
|
-
|
|
241
|
-
@@ -20,6 +23,8 @@ export class REPLLoop {
|
|
242
|
-
private ai: AIEngine | null = null;
|
|
243
|
-
private session: SessionManager | null = null;
|
|
244
|
-
private running: boolean = false;
|
|
245
|
-
+ // Used to abort an in-flight AI stream when the user presses Ctrl+C twice
|
|
246
|
-
+ private streamAbort: AbortController | null = null;
|
|
247
|
-
|
|
248
|
-
constructor() {
|
|
249
|
-
this.input = new REPLInput();
|
|
250
|
-
@@ -31,22 +36,17 @@ export class REPLLoop {
|
|
251
|
-
* S04: Use SystemPromptBuilder for dynamic prompt construction
|
|
252
|
-
*/
|
|
253
|
-
async init(): Promise<void> {
|
|
254
|
-
- // Load config
|
|
255
|
-
const config = await ConfigManager.load();
|
|
256
|
-
|
|
257
|
-
- // Initialize AI engine
|
|
258
|
-
this.ai = new AIEngine(config);
|
|
259
|
-
|
|
260
|
-
- // Register tools
|
|
261
|
-
const builtInTools = getBuiltInTools();
|
|
262
|
-
for (const tool of builtInTools) {
|
|
263
|
-
this.ai!.registerTool(tool);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
- // Initialize session
|
|
267
|
-
this.session = new SessionManager();
|
|
268
|
-
|
|
269
|
-
- // S04: Build system prompt dynamically from tools
|
|
270
|
-
const promptBuilder = new SystemPromptBuilder({
|
|
271
|
-
cwd: process.cwd(),
|
|
272
|
-
tools: builtInTools,
|
|
273
|
-
@@ -57,6 +57,19 @@ export class REPLLoop {
|
|
274
|
-
this.session.addMessage('system', systemPrompt);
|
|
275
|
-
|
|
276
|
-
this.running = true;
|
|
277
|
-
+
|
|
278
|
-
+ // 全局 SIGINT 处理:第一次按下 abort 当前 AI stream,第二次退出 REPL
|
|
279
|
-
+ process.on('SIGINT', () => {
|
|
280
|
-
+ if (this.streamAbort) {
|
|
281
|
-
+ this.streamAbort.abort();
|
|
282
|
-
+ this.streamAbort = null;
|
|
283
|
-
+ this.output.stopSpinner();
|
|
284
|
-
+ this.output.printWarning('\n⏹ Generation cancelled (Ctrl+C again to exit)');
|
|
285
|
-
+ return;
|
|
286
|
-
+ }
|
|
287
|
-
+ // 没有在生成:交给 REPLInput 处理(它会检测是否要退出)
|
|
288
|
-
+ this.output.printWarning('\n(press Ctrl+C again on an empty prompt to exit)');
|
|
289
|
-
+ });
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
@@ -64,40 +77,44 @@ export class REPLLoop {
|
|
294
|
-
*/
|
|
295
|
-
async start(): Promise<void> {
|
|
296
|
-
await this.init();
|
|
297
|
-
-
|
|
298
|
-
- // Show banner
|
|
299
|
-
+
|
|
300
|
-
this.output.clear();
|
|
301
|
-
this.output.printBanner();
|
|
302
|
-
this.output.printInfo('\nType "help" for available commands\n');
|
|
303
|
-
this.output.printDivider();
|
|
304
|
-
-
|
|
305
|
-
- // Main loop
|
|
306
|
-
+
|
|
307
|
-
while (this.running) {
|
|
308
|
-
+ let result: PromptResult;
|
|
309
|
-
try {
|
|
310
|
-
- const userInput = await this.input.prompt();
|
|
311
|
-
-
|
|
312
|
-
- // Handle empty input
|
|
313
|
-
- if (!userInput) {
|
|
314
|
-
- continue;
|
|
315
|
-
- }
|
|
316
|
-
-
|
|
317
|
-
- // Handle special commands
|
|
318
|
-
- const handled = await this.handleCommand(userInput);
|
|
319
|
-
- if (handled) {
|
|
320
|
-
- continue;
|
|
321
|
-
- }
|
|
322
|
-
-
|
|
323
|
-
- // Process with AI
|
|
324
|
-
- await this.processInput(userInput);
|
|
325
|
-
-
|
|
326
|
-
- } catch (error: any) {
|
|
327
|
-
- if (error.message === 'SIGINT') {
|
|
328
|
-
- // User pressed Ctrl+C, continue
|
|
329
|
-
- continue;
|
|
330
|
-
+ result = await this.input.prompt();
|
|
331
|
-
+ } catch (err: any) {
|
|
332
|
-
+ this.output.printError(err?.message ?? String(err));
|
|
333
|
-
+ continue;
|
|
334
|
-
+ }
|
|
335
|
-
+
|
|
336
|
-
+ if (result.kind === 'cancelled') {
|
|
337
|
-
+ if (this.input.shouldExitOnCancel()) {
|
|
338
|
-
+ this.output.printInfo('\n👋 Goodbye!');
|
|
339
|
-
+ this.running = false;
|
|
340
|
-
+ break;
|
|
341
|
-
}
|
|
342
|
-
- this.output.printError(error.message);
|
|
343
|
-
+ // 继续让用户输入
|
|
344
|
-
+ continue;
|
|
345
|
-
}
|
|
346
|
-
+
|
|
347
|
-
+ // input layer 自动重置 cancel 计数,无需手动调
|
|
348
|
-
+
|
|
349
|
-
+ const userInput = result.value;
|
|
350
|
-
+ if (!userInput) continue;
|
|
351
|
-
+
|
|
352
|
-
+ const handled = await this.handleCommand(userInput);
|
|
353
|
-
+ if (handled) continue;
|
|
354
|
-
+
|
|
355
|
-
+ await this.processInput(userInput);
|
|
356
|
-
}
|
|
357
|
-
+
|
|
358
|
-
+ // 清理全局监听
|
|
359
|
-
+ process.removeAllListeners('SIGINT');
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
@@ -105,49 +122,53 @@ export class REPLLoop {
|
|
364
|
-
*/
|
|
365
|
-
private async handleCommand(input: string): Promise<boolean> {
|
|
366
|
-
const cmd = input.toLowerCase().trim();
|
|
367
|
-
-
|
|
368
|
-
+
|
|
369
|
-
switch (cmd) {
|
|
370
|
-
case 'exit':
|
|
371
|
-
case 'quit':
|
|
372
|
-
- case '\\x03': // Ctrl+C
|
|
373
|
-
+ case '\\x03':
|
|
374
|
-
this.output.printInfo('\n👋 Goodbye!');
|
|
375
|
-
this.running = false;
|
|
376
|
-
- this.input.close();
|
|
377
|
-
return true;
|
|
378
|
-
-
|
|
379
|
-
+
|
|
380
|
-
case 'clear':
|
|
381
|
-
this.output.clear();
|
|
382
|
-
this.output.printBanner();
|
|
383
|
-
return true;
|
|
384
|
-
-
|
|
385
|
-
+
|
|
386
|
-
case 'context':
|
|
387
|
-
this.showContext();
|
|
388
|
-
return true;
|
|
389
|
-
-
|
|
390
|
-
+
|
|
391
|
-
case 'history':
|
|
392
|
-
this.showHistory();
|
|
393
|
-
return true;
|
|
394
|
-
-
|
|
395
|
-
+
|
|
396
|
-
case 'tools':
|
|
397
|
-
this.showTools();
|
|
398
|
-
return true;
|
|
399
|
-
-
|
|
400
|
-
+
|
|
401
|
-
case 'help':
|
|
402
|
-
this.output.printHelp();
|
|
403
|
-
return true;
|
|
404
|
-
-
|
|
405
|
-
+
|
|
406
|
-
case 'models':
|
|
407
|
-
case 'providers':
|
|
408
|
-
this.showProviders();
|
|
409
|
-
return true;
|
|
410
|
-
-
|
|
411
|
-
+
|
|
412
|
-
default:
|
|
413
|
-
return false;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
- * Process user input with AI (S03: streaming with permission check)
|
|
419
|
-
+ * Process user input with AI (S01: streaming, no prompt reset)
|
|
420
|
-
+ *
|
|
421
|
-
+ * 关键修复:
|
|
422
|
-
+ * - 不再调用 rl.question,所以不会 reset 终端
|
|
423
|
-
+ * - chunk 直接 process.stdout.write,完整保留所有输出,用户可滚动查看
|
|
424
|
-
+ * - spinner 在第一个 chunk 到达时停止,然后正常流式
|
|
425
|
-
*/
|
|
426
|
-
private async processInput(input: string): Promise<void> {
|
|
427
|
-
if (!this.ai || !this.session) {
|
|
428
|
-
@@ -155,58 +176,70 @@ export class REPLLoop {
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
- // Add user message
|
|
433
|
-
this.session.addMessage('user', input);
|
|
434
|
-
|
|
435
|
-
- // Show thinking
|
|
436
|
-
- this.output.startSpinner('Thinking...');
|
|
437
|
-
+ // 视觉提示用户:AI 开始工作,但不阻塞,可滚动
|
|
438
|
-
+ this.output.printInfo(chalk.gray('🤖 Thinking...\n'));
|
|
439
|
-
+
|
|
440
|
-
let fullResponse = '';
|
|
441
|
-
+ let firstChunk = true;
|
|
442
|
-
+ this.streamAbort = new AbortController();
|
|
443
|
-
|
|
444
|
-
try {
|
|
445
|
-
- // S01: Stream output via chatStream
|
|
446
|
-
const stream = (this.ai as any).chatStream(this.session.getMessages());
|
|
447
|
-
for await (const chunk of stream) {
|
|
448
|
-
- this.output.stopSpinner();
|
|
449
|
-
+ if (firstChunk) {
|
|
450
|
-
+ // 第一个 chunk 到来,把 "Thinking..." 那行通过 ANSI 清除
|
|
451
|
-
+ // 但不能清光——只把光标移到下一行的开头即可,内容自然出现在前面
|
|
452
|
-
+ firstChunk = false;
|
|
453
|
-
+ }
|
|
454
|
-
+ // 关键:直接 stdout.write,允许任意长度、可滚动
|
|
455
|
-
process.stdout.write(chunk);
|
|
456
|
-
fullResponse += chunk;
|
|
457
|
-
- }
|
|
458
|
-
-
|
|
459
|
-
- console.log();
|
|
460
|
-
-
|
|
461
|
-
- // Add assistant message
|
|
462
|
-
- this.session.addMessage('assistant', fullResponse);
|
|
463
|
-
|
|
464
|
-
- // Truncate if too long
|
|
465
|
-
- this.session.truncate(20);
|
|
466
|
-
+ if (this.streamAbort.signal.aborted) break;
|
|
467
|
-
+ }
|
|
468
|
-
|
|
469
|
-
+ console.log(); // 流结束换行
|
|
470
|
-
} catch (error: any) {
|
|
471
|
-
- this.output.stopSpinner();
|
|
472
|
-
- this.output.printError(error.message);
|
|
473
|
-
+ if (error?.name === 'AbortError' || this.streamAbort.signal.aborted) {
|
|
474
|
-
+ console.log();
|
|
475
|
-
+ this.output.printWarning('[cancelled]');
|
|
476
|
-
+ } else {
|
|
477
|
-
+ this.output.printError(error.message);
|
|
478
|
-
+ }
|
|
479
|
-
+ } finally {
|
|
480
|
-
+ this.streamAbort = null;
|
|
481
|
-
}
|
|
482
|
-
+
|
|
483
|
-
+ const accepted = this.session.addMessage('assistant', fullResponse);
|
|
484
|
-
+ if (!accepted) {
|
|
485
|
-
+ this.output.printWarning(
|
|
486
|
-
+ chalk.yellow(
|
|
487
|
-
+ `⚠️ 上轮回复包含 "[已中断]" 等污染标记,已自动丢弃以避免循环。本次回复不会基于它继续;请重说你的问题。\n` +
|
|
488
|
-
+ `(本次会话已累计过滤 ${this.session.getDroppedCount()} 条污染消息)`
|
|
489
|
-
+ )
|
|
490
|
-
+ );
|
|
491
|
-
+ }
|
|
492
|
-
+ this.session.truncate(20);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
- /**
|
|
496
|
-
- * Show project context
|
|
497
|
-
- */
|
|
498
|
-
private showContext(): void {
|
|
499
|
-
const cwd = process.cwd();
|
|
500
|
-
this.output.printHeader('📁 Project Context');
|
|
501
|
-
this.output.printInfo(`Working directory: ${cwd}`);
|
|
502
|
-
- // TODO: Add more context (package.json, git status, etc.)
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
- /**
|
|
506
|
-
- * Show command history
|
|
507
|
-
- */
|
|
508
|
-
private showHistory(): void {
|
|
509
|
-
+ const items = this.input.getHistory();
|
|
510
|
-
this.output.printHeader('📜 Command History');
|
|
511
|
-
- // TODO: Implement history display
|
|
512
|
-
- this.output.printInfo('(History not yet implemented)');
|
|
513
|
-
+ if (items.length === 0) {
|
|
514
|
-
+ this.output.printInfo('(no history yet)');
|
|
515
|
-
+ return;
|
|
516
|
-
+ }
|
|
517
|
-
+ items.forEach((h, i) => this.output.printInfo(` ${i + 1}. ${h}`));
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
- /**
|
|
521
|
-
- * Show available tools
|
|
522
|
-
- */
|
|
523
|
-
private showTools(): void {
|
|
524
|
-
this.output.printHeader('🔧 Available Tools');
|
|
525
|
-
this.output.printInfo(' file - File operations (read, write, list, delete)');
|
|
526
|
-
@@ -214,9 +247,6 @@ export class REPLLoop {
|
|
527
|
-
this.output.printInfo(' git - Git operations (coming soon)');
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
- /**
|
|
531
|
-
- * Show available providers
|
|
532
|
-
- */
|
|
533
|
-
private showProviders(): void {
|
|
534
|
-
this.output.printHeader('🌐 Available Providers');
|
|
535
|
-
this.output.printInfo(' siliconflow - 硅基流动 (default)');
|
|
536
|
-
@@ -230,16 +260,21 @@ export class REPLLoop {
|
|
537
|
-
*/
|
|
538
|
-
stop(): void {
|
|
539
|
-
this.running = false;
|
|
540
|
-
- this.input.close();
|
|
541
|
-
+ if (this.streamAbort) {
|
|
542
|
-
+ this.streamAbort.abort();
|
|
543
|
-
+ this.streamAbort = null;
|
|
544
|
-
+ }
|
|
545
|
-
this.output.stopStreaming();
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
/**
|
|
549
|
-
- * Handle interrupt (Ctrl+C)
|
|
550
|
-
+ * Handle interrupt (Ctrl+C) while streaming
|
|
551
|
-
*/
|
|
552
|
-
interrupt(): void {
|
|
553
|
-
- this.output.printWarning('\n\n⚠️ Interrupted');
|
|
554
|
-
+ if (this.streamAbort) {
|
|
555
|
-
+ this.streamAbort.abort();
|
|
556
|
-
+ this.streamAbort = null;
|
|
557
|
-
+ }
|
|
558
|
-
this.output.stopStreaming();
|
|
559
|
-
- this.output.printInfo('\nType "exit" to quit, or continue...\n');
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
--
|
|
563
|
-
2.45.2.windows.1
|
|
564
|
-
|