thatgfsj-code 0.2.1

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.
Files changed (179) hide show
  1. package/.patches/0001-fix-repl-replace-readline-with-inquirer-input-for-ke.patch +279 -0
  2. package/.patches/0002-fix-repl-stream-AI-output-directly-to-stdout-so-term.patch +564 -0
  3. package/.patches/0003-fix-session-break-the-hallucination-loop-in-assistan.patch +194 -0
  4. package/.patches/0004-chore-release-bump-version-to-0.2.1.patch +24 -0
  5. package/LICENSE +21 -0
  6. package/README.md +100 -0
  7. package/ROADMAP.md +107 -0
  8. package/dist/agent/core.d.ts +56 -0
  9. package/dist/agent/core.d.ts.map +1 -0
  10. package/dist/agent/core.js +142 -0
  11. package/dist/agent/core.js.map +1 -0
  12. package/dist/agent/index.d.ts +8 -0
  13. package/dist/agent/index.d.ts.map +1 -0
  14. package/dist/agent/index.js +8 -0
  15. package/dist/agent/index.js.map +1 -0
  16. package/dist/agent/intent.d.ts +36 -0
  17. package/dist/agent/intent.d.ts.map +1 -0
  18. package/dist/agent/intent.js +146 -0
  19. package/dist/agent/intent.js.map +1 -0
  20. package/dist/agent/streaming.d.ts +50 -0
  21. package/dist/agent/streaming.d.ts.map +1 -0
  22. package/dist/agent/streaming.js +110 -0
  23. package/dist/agent/streaming.js.map +1 -0
  24. package/dist/core/ai-engine.d.ts +59 -0
  25. package/dist/core/ai-engine.d.ts.map +1 -0
  26. package/dist/core/ai-engine.js +378 -0
  27. package/dist/core/ai-engine.js.map +1 -0
  28. package/dist/core/cli.d.ts +21 -0
  29. package/dist/core/cli.d.ts.map +1 -0
  30. package/dist/core/cli.js +143 -0
  31. package/dist/core/cli.js.map +1 -0
  32. package/dist/core/config.d.ts +38 -0
  33. package/dist/core/config.d.ts.map +1 -0
  34. package/dist/core/config.js +129 -0
  35. package/dist/core/config.js.map +1 -0
  36. package/dist/core/context-compactor.d.ts +54 -0
  37. package/dist/core/context-compactor.d.ts.map +1 -0
  38. package/dist/core/context-compactor.js +197 -0
  39. package/dist/core/context-compactor.js.map +1 -0
  40. package/dist/core/hooks.d.ts +75 -0
  41. package/dist/core/hooks.d.ts.map +1 -0
  42. package/dist/core/hooks.js +146 -0
  43. package/dist/core/hooks.js.map +1 -0
  44. package/dist/core/permissions.d.ts +52 -0
  45. package/dist/core/permissions.d.ts.map +1 -0
  46. package/dist/core/permissions.js +237 -0
  47. package/dist/core/permissions.js.map +1 -0
  48. package/dist/core/session.d.ts +54 -0
  49. package/dist/core/session.d.ts.map +1 -0
  50. package/dist/core/session.js +147 -0
  51. package/dist/core/session.js.map +1 -0
  52. package/dist/core/skills.d.ts +60 -0
  53. package/dist/core/skills.d.ts.map +1 -0
  54. package/dist/core/skills.js +175 -0
  55. package/dist/core/skills.js.map +1 -0
  56. package/dist/core/state.d.ts +60 -0
  57. package/dist/core/state.d.ts.map +1 -0
  58. package/dist/core/state.js +106 -0
  59. package/dist/core/state.js.map +1 -0
  60. package/dist/core/subagent.d.ts +68 -0
  61. package/dist/core/subagent.d.ts.map +1 -0
  62. package/dist/core/subagent.js +142 -0
  63. package/dist/core/subagent.js.map +1 -0
  64. package/dist/core/system-prompt.d.ts +40 -0
  65. package/dist/core/system-prompt.d.ts.map +1 -0
  66. package/dist/core/system-prompt.js +138 -0
  67. package/dist/core/system-prompt.js.map +1 -0
  68. package/dist/core/tool-registry.d.ts +53 -0
  69. package/dist/core/tool-registry.d.ts.map +1 -0
  70. package/dist/core/tool-registry.js +138 -0
  71. package/dist/core/tool-registry.js.map +1 -0
  72. package/dist/core/types.d.ts +206 -0
  73. package/dist/core/types.d.ts.map +1 -0
  74. package/dist/core/types.js +127 -0
  75. package/dist/core/types.js.map +1 -0
  76. package/dist/index.d.ts +7 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.js +481 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/mcp/client.d.ts +83 -0
  81. package/dist/mcp/client.d.ts.map +1 -0
  82. package/dist/mcp/client.js +267 -0
  83. package/dist/mcp/client.js.map +1 -0
  84. package/dist/repl/index.d.ts +8 -0
  85. package/dist/repl/index.d.ts.map +1 -0
  86. package/dist/repl/index.js +8 -0
  87. package/dist/repl/index.js.map +1 -0
  88. package/dist/repl/input.d.ts +61 -0
  89. package/dist/repl/input.d.ts.map +1 -0
  90. package/dist/repl/input.js +127 -0
  91. package/dist/repl/input.js.map +1 -0
  92. package/dist/repl/loop.d.ts +53 -0
  93. package/dist/repl/loop.d.ts.map +1 -0
  94. package/dist/repl/loop.js +241 -0
  95. package/dist/repl/loop.js.map +1 -0
  96. package/dist/repl/output.d.ts +105 -0
  97. package/dist/repl/output.d.ts.map +1 -0
  98. package/dist/repl/output.js +197 -0
  99. package/dist/repl/output.js.map +1 -0
  100. package/dist/repl/welcome.d.ts +68 -0
  101. package/dist/repl/welcome.d.ts.map +1 -0
  102. package/dist/repl/welcome.js +249 -0
  103. package/dist/repl/welcome.js.map +1 -0
  104. package/dist/tools/file.d.ts +46 -0
  105. package/dist/tools/file.d.ts.map +1 -0
  106. package/dist/tools/file.js +100 -0
  107. package/dist/tools/file.js.map +1 -0
  108. package/dist/tools/git.d.ts +52 -0
  109. package/dist/tools/git.d.ts.map +1 -0
  110. package/dist/tools/git.js +112 -0
  111. package/dist/tools/git.js.map +1 -0
  112. package/dist/tools/index.d.ts +22 -0
  113. package/dist/tools/index.d.ts.map +1 -0
  114. package/dist/tools/index.js +43 -0
  115. package/dist/tools/index.js.map +1 -0
  116. package/dist/tools/search.d.ts +36 -0
  117. package/dist/tools/search.d.ts.map +1 -0
  118. package/dist/tools/search.js +223 -0
  119. package/dist/tools/search.js.map +1 -0
  120. package/dist/tools/shell.d.ts +57 -0
  121. package/dist/tools/shell.d.ts.map +1 -0
  122. package/dist/tools/shell.js +153 -0
  123. package/dist/tools/shell.js.map +1 -0
  124. package/dist/utils/diff-preview.d.ts +42 -0
  125. package/dist/utils/diff-preview.d.ts.map +1 -0
  126. package/dist/utils/diff-preview.js +174 -0
  127. package/dist/utils/diff-preview.js.map +1 -0
  128. package/dist/utils/index.d.ts +8 -0
  129. package/dist/utils/index.d.ts.map +1 -0
  130. package/dist/utils/index.js +8 -0
  131. package/dist/utils/index.js.map +1 -0
  132. package/dist/utils/memory.d.ts +81 -0
  133. package/dist/utils/memory.d.ts.map +1 -0
  134. package/dist/utils/memory.js +186 -0
  135. package/dist/utils/memory.js.map +1 -0
  136. package/dist/utils/project-context.d.ts +62 -0
  137. package/dist/utils/project-context.d.ts.map +1 -0
  138. package/dist/utils/project-context.js +197 -0
  139. package/dist/utils/project-context.js.map +1 -0
  140. package/docs/API_KEY_GUIDE.md +236 -0
  141. package/docs/FAQ.md +182 -0
  142. package/install.bat +63 -0
  143. package/install.ps1 +238 -0
  144. package/install.sh +113 -0
  145. package/package.json +36 -0
  146. package/src/agent/core.ts +179 -0
  147. package/src/agent/index.ts +8 -0
  148. package/src/agent/intent.ts +181 -0
  149. package/src/agent/streaming.ts +132 -0
  150. package/src/core/ai-engine.ts +437 -0
  151. package/src/core/cli.ts +171 -0
  152. package/src/core/config.ts +147 -0
  153. package/src/core/context-compactor.ts +245 -0
  154. package/src/core/hooks.ts +196 -0
  155. package/src/core/permissions.ts +308 -0
  156. package/src/core/session.ts +165 -0
  157. package/src/core/skills.ts +208 -0
  158. package/src/core/state.ts +120 -0
  159. package/src/core/subagent.ts +195 -0
  160. package/src/core/system-prompt.ts +163 -0
  161. package/src/core/tool-registry.ts +157 -0
  162. package/src/core/types.ts +280 -0
  163. package/src/index.ts +544 -0
  164. package/src/mcp/client.ts +330 -0
  165. package/src/repl/index.ts +8 -0
  166. package/src/repl/input.ts +139 -0
  167. package/src/repl/loop.ts +280 -0
  168. package/src/repl/output.ts +222 -0
  169. package/src/repl/welcome.ts +296 -0
  170. package/src/tools/file.ts +117 -0
  171. package/src/tools/git.ts +132 -0
  172. package/src/tools/index.ts +48 -0
  173. package/src/tools/search.ts +263 -0
  174. package/src/tools/shell.ts +181 -0
  175. package/src/utils/diff-preview.ts +202 -0
  176. package/src/utils/index.ts +8 -0
  177. package/src/utils/memory.ts +223 -0
  178. package/src/utils/project-context.ts +207 -0
  179. package/tsconfig.json +19 -0
@@ -0,0 +1,564 @@
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
+