closer-code 1.0.0

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 (100) hide show
  1. package/.env.example +83 -0
  2. package/API_GUIDE.md +1411 -0
  3. package/AUTO_MKDIR_IMPROVEMENT.md +354 -0
  4. package/CLAUDE.md +55 -0
  5. package/CTRL_C_EXPERIMENT.md +90 -0
  6. package/PROJECT_CLEANUP_SUMMARY.md +121 -0
  7. package/README.md +686 -0
  8. package/cloco.md +51 -0
  9. package/config.example.json +116 -0
  10. package/dist/bash-runner.js +128 -0
  11. package/dist/batch-cli.js +20736 -0
  12. package/dist/closer-cli.js +21190 -0
  13. package/dist/index.js +31228 -0
  14. package/docs/EXPORT_COMMAND.md +152 -0
  15. package/docs/FILE_NAMING_IMPROVEMENT.md +168 -0
  16. package/docs/GLOBAL_CONFIG.md +128 -0
  17. package/docs/LONG_MESSAGE_DISPLAY_FIX.md +202 -0
  18. package/docs/PROJECT_HISTORY_ISOLATION.md +315 -0
  19. package/docs/QUICK_START_HISTORY.md +207 -0
  20. package/docs/TASK_PROGRESS_FEATURE.md +190 -0
  21. package/docs/THINKING_CONTENT_RESEARCH.md +267 -0
  22. package/docs/THINKING_FEATURE.md +187 -0
  23. package/docs/THINKING_IMPROVEMENT_COMPARISON.md +193 -0
  24. package/docs/THINKING_OPTIMIZATION_SUMMARY.md +242 -0
  25. package/docs/UI_IMPROVEMENTS_2025-01-18.md +256 -0
  26. package/docs/WHY_THINKING_SHORT.md +201 -0
  27. package/package.json +49 -0
  28. package/scenarios/README.md +234 -0
  29. package/scenarios/run-all-scenarios.js +342 -0
  30. package/scenarios/scenario1-batch-converter.js +247 -0
  31. package/scenarios/scenario2-code-analyzer.js +375 -0
  32. package/scenarios/scenario3-doc-generator.js +371 -0
  33. package/scenarios/scenario4-log-analyzer.js +496 -0
  34. package/scenarios/scenario5-tdd-helper.js +681 -0
  35. package/src/ai-client-legacy.js +171 -0
  36. package/src/ai-client.js +221 -0
  37. package/src/bash-runner.js +148 -0
  38. package/src/batch-cli.js +327 -0
  39. package/src/cli.jsx +166 -0
  40. package/src/closer-cli.jsx +1103 -0
  41. package/src/closer-cli.jsx.backup +948 -0
  42. package/src/commands/batch.js +62 -0
  43. package/src/commands/chat.js +10 -0
  44. package/src/commands/config.js +154 -0
  45. package/src/commands/help.js +76 -0
  46. package/src/commands/history.js +192 -0
  47. package/src/commands/setup.js +17 -0
  48. package/src/commands/upgrade.js +101 -0
  49. package/src/commands/workflow-tests.js +125 -0
  50. package/src/config.js +343 -0
  51. package/src/conversation.js +962 -0
  52. package/src/git-helper.js +349 -0
  53. package/src/index.js +88 -0
  54. package/src/logger.js +347 -0
  55. package/src/plan.js +193 -0
  56. package/src/planner.js +397 -0
  57. package/src/search.js +195 -0
  58. package/src/setup.js +147 -0
  59. package/src/shortcuts.js +269 -0
  60. package/src/snippets.js +430 -0
  61. package/src/test-modules.js +118 -0
  62. package/src/tools.js +398 -0
  63. package/src/utils/cli.js +124 -0
  64. package/src/utils/validator.js +184 -0
  65. package/src/utils/version.js +33 -0
  66. package/src/utils/workflow-test.js +271 -0
  67. package/src/utils/workflow.js +268 -0
  68. package/test/demo-file-naming.js +92 -0
  69. package/test/demo-thinking.js +124 -0
  70. package/test/final-verification-report.md +303 -0
  71. package/test/research-thinking.js +130 -0
  72. package/test/test-auto-mkdir.js +123 -0
  73. package/test/test-e2e-empty-dir.md +108 -0
  74. package/test/test-export-logic.js +119 -0
  75. package/test/test-global-cloco.js +126 -0
  76. package/test/test-history-isolation.js +291 -0
  77. package/test/test-improved-thinking.js +43 -0
  78. package/test/test-long-message.js +65 -0
  79. package/test/test-plan-functionality.js +95 -0
  80. package/test/test-real-scenario.js +216 -0
  81. package/test/test-thinking-display.js +65 -0
  82. package/test/ui-verification-test.js +203 -0
  83. package/test/verify-history-isolation.sh +71 -0
  84. package/test/verify-thinking.js +339 -0
  85. package/test/workflows/empty-dir-creation.md +51 -0
  86. package/test/workflows/inventor/ascii-teacup.js +199 -0
  87. package/test/workflows/inventor/ascii-teacup.mjs +199 -0
  88. package/test/workflows/inventor/ascii_apple.hs +84 -0
  89. package/test/workflows/inventor/ascii_apple.py +91 -0
  90. package/test/workflows/inventor/cloco.md +3 -0
  91. package/test/workflows/longtalk/cloco.md +19 -0
  92. package/test/workflows/longtalk/emoji_500.txt +63 -0
  93. package/test/workflows/longtalk/emoji_list.txt +20 -0
  94. package/test/workflows/programmer/adder.md +33 -0
  95. package/test/workflows/programmer/expect.md +2 -0
  96. package/test/workflows/programmer/prompt.md +3 -0
  97. package/test/workflows/test-empty-dir-creation.js +113 -0
  98. package/test-ctrl-c.jsx +126 -0
  99. package/test-manual-file-creation.js +151 -0
  100. package/winfix.md +3 -0
@@ -0,0 +1,948 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Closer Code - AI 编程助理 CLI
4
+ */
5
+
6
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
7
+ import { render, Box, Text } from 'ink';
8
+ import TextInput from 'ink-text-input';
9
+ import { useInput } from 'ink';
10
+ import { createConversation } from './conversation.js';
11
+ import { getConfig, updateConfig } from './config.js';
12
+ import { createShortcutManager } from './shortcuts.js';
13
+ import { createSnippetManager, SNIPPET_TEMPLATES } from './snippets.js';
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+
17
+ // 面板组件
18
+ function Panel({ title, children, borderColor = 'gray', flex = 1 }) {
19
+ return (
20
+ <Box
21
+ borderStyle="round"
22
+ borderColor={borderColor}
23
+ flexDirection="column"
24
+ flexGrow={flex}
25
+ paddingX={1}
26
+ marginRight={1}
27
+ marginBottom={1}
28
+ >
29
+ <Box borderBottom={false} borderColor={borderColor} paddingBottom={0} marginBottom={1}>
30
+ <Text bold color={borderColor}>{title}</Text>
31
+ </Box>
32
+ <Box flexGrow={1} flexDirection="column">
33
+ {children}
34
+ </Box>
35
+ </Box>
36
+ );
37
+ }
38
+
39
+ // 消息显示组件
40
+ // 任务进度组件
41
+ function TaskProgress({ plan }) {
42
+ if (!plan) return null;
43
+
44
+ const { completed, total, percentage } = plan.getProgress ? plan.getProgress() : { completed: 0, total: 0, percentage: 0 };
45
+ const statusColor = plan.status === 'completed' ? 'green' :
46
+ plan.status === 'failed' ? 'red' :
47
+ plan.status === 'in_progress' ? 'yellow' : 'gray';
48
+
49
+ // 进度条
50
+ const barWidth = 30;
51
+ const filled = Math.round((percentage / 100) * barWidth);
52
+ const empty = barWidth - filled;
53
+ const bar = '█'.repeat(filled) + '░'.repeat(empty);
54
+
55
+ // Plan 类型标识
56
+ const typeLabel = plan.type === 'auto' ? '🤖 AI' : '📋';
57
+
58
+ return (
59
+ <Box flexDirection="column" marginBottom={1}>
60
+ <Box>
61
+ <Text bold color={statusColor}>
62
+ {typeLabel} {plan.description}
63
+ </Text>
64
+ </Box>
65
+ <Box>
66
+ <Text color="cyan">[{bar}]</Text>
67
+ <Text dim> {completed}/{total} ({Math.round(percentage)}%)</Text>
68
+ </Box>
69
+ {plan.steps && plan.steps.length > 0 && (
70
+ <Box flexDirection="column" marginTop={1}>
71
+ {plan.steps.slice(-3).map((step, i) => {
72
+ const stepIcon = step.status === 'completed' ? '✓' :
73
+ step.status === 'in_progress' ? '→' :
74
+ step.status === 'failed' ? '✗' : '○';
75
+ const stepColor = step.status === 'completed' ? 'green' :
76
+ step.status === 'in_progress' ? 'yellow' :
77
+ step.status === 'failed' ? 'red' : 'gray';
78
+ return (
79
+ <Box key={step.id || i}>
80
+ <Text color={stepColor}>{stepIcon} {step.description}</Text>
81
+ </Box>
82
+ );
83
+ })}
84
+ </Box>
85
+ )}
86
+ {plan.metadata?.error && (
87
+ <Box>
88
+ <Text color="red">Error: {plan.metadata.error}</Text>
89
+ </Box>
90
+ )}
91
+ </Box>
92
+ );
93
+ }
94
+
95
+ // 工具执行显示组件
96
+ function ToolExecution({ tool, timestamp, success, error }) {
97
+ return (
98
+ <Box flexDirection="column" marginBottom={1} paddingX={1} borderStyle="single" borderColor="gray" width="100%">
99
+ <Box width="100%">
100
+ <Text bold color="yellow">⚡ {tool}</Text>
101
+ <Text dim> [{timestamp}]</Text>
102
+ </Box>
103
+ {success !== undefined && (
104
+ <Box width="100%">
105
+ <Text color={success ? 'green' : 'red'}>
106
+ {success ? '✓ 成功' : `✗ ${error || '失败'}`}
107
+ </Text>
108
+ </Box>
109
+ )}
110
+ </Box>
111
+ );
112
+ }
113
+
114
+ /**
115
+ * 将消息格式化为行数组
116
+ * @param {Object} message - 消息对象
117
+ * @param {number} maxWidth - 最大宽度(字符数)
118
+ * @returns {Array} - 行数组
119
+ */
120
+ function formatMessageAsLines(message, maxWidth = 80) {
121
+ const isUser = message.role === 'user';
122
+ const isSystem = message.role === 'system';
123
+ const isError = message.role === 'error';
124
+
125
+ const color = isUser ? 'cyan' : isError ? 'red' : isSystem ? 'yellow' : 'white';
126
+ const prefix = isUser ? '👤 ' : isError ? '❌ ' : isSystem ? 'ℹ️ ' : '🤖 ';
127
+ const content = typeof message.content === 'string'
128
+ ? message.content
129
+ : JSON.stringify(message.content);
130
+
131
+ const lines = [];
132
+ const prefixLength = 4; // emoji + space
133
+ const contentWidth = maxWidth - prefixLength - 2; // 留出边距
134
+
135
+ // 添加前缀行
136
+ lines.push({
137
+ text: prefix,
138
+ color: color,
139
+ type: 'prefix'
140
+ });
141
+
142
+ // 分割内容为行
143
+ const contentLines = content.split('\n');
144
+
145
+ for (let line of contentLines) {
146
+ // 如果行太长,需要分割
147
+ if (line.length > contentWidth) {
148
+ // 分割长行
149
+ for (let i = 0; i < line.length; i += contentWidth) {
150
+ const chunk = line.slice(i, i + contentWidth);
151
+ lines.push({
152
+ text: ' ' + chunk, // 缩进
153
+ color: color,
154
+ type: 'content'
155
+ });
156
+ }
157
+ } else {
158
+ lines.push({
159
+ text: ' ' + (line || ''), // 缩进,空行也显示
160
+ color: color,
161
+ type: 'content'
162
+ });
163
+ }
164
+ }
165
+
166
+ // 添加消息间分隔
167
+ lines.push({
168
+ text: '',
169
+ color: 'gray',
170
+ type: 'separator'
171
+ });
172
+
173
+ return lines;
174
+ }
175
+
176
+ /**
177
+ * 滚动容器组件
178
+ */
179
+ function ScrollContainer({ items, height, scrollPosition }) {
180
+ // 计算可见范围
181
+ const startIndex = Math.max(0, Math.floor(scrollPosition));
182
+ const endIndex = Math.min(items.length, startIndex + height);
183
+ const visibleItems = items.slice(startIndex, endIndex);
184
+
185
+ return (
186
+ <Box flexDirection="column" width="100%">
187
+ {visibleItems.map((item, index) => (
188
+ <Box key={startIndex + index} width="100%">
189
+ <Text color={item.color}>{item.text}</Text>
190
+ </Box>
191
+ ))}
192
+ </Box>
193
+ );
194
+ }
195
+
196
+ // 主应用组件
197
+ function App() {
198
+ const [config, setConfig] = useState(null);
199
+ const [conversation, setConversation] = useState(null);
200
+ const [messages, setMessages] = useState([]);
201
+ const [input, setInput] = useState('');
202
+ const [isProcessing, setIsProcessing] = useState(false);
203
+ const [currentPlan, setCurrentPlan] = useState(null);
204
+ const [toolExecutions, setToolExecutions] = useState([]);
205
+ const [status, setStatus] = useState('Initializing...');
206
+ const [messageCounter, setMessageCounter] = useState(0);
207
+ const [activity, setActivity] = useState(null); // 当前活动描述
208
+ const [thinking, setThinking] = useState([]); // AI 思考过程
209
+ const [thinkingEnabled, setThinkingEnabled] = useState(true); // Thinking 开关状态
210
+
211
+ // 终端尺寸状态
212
+ const [terminalSize, setTerminalSize] = useState({
213
+ columns: process.stdout.columns || 80,
214
+ rows: process.stdout.rows || 30
215
+ });
216
+
217
+ // 监听终端尺寸变化
218
+ useEffect(() => {
219
+ const handleResize = () => {
220
+ setTerminalSize({
221
+ columns: process.stdout.columns || 80,
222
+ rows: process.stdout.rows || 30
223
+ });
224
+ };
225
+
226
+ process.stdout.on('resize', handleResize);
227
+ return () => {
228
+ process.stdout.off('resize', handleResize);
229
+ };
230
+ }, []);
231
+
232
+ // 自定义滚动管理
233
+ const [messageLines, setMessageLines] = useState([]); // 所有消息行
234
+ const [scrollPosition, setScrollPosition] = useState(0); // 当前滚动位置
235
+ const conversationHeight = Math.floor(terminalSize.rows * 0.65) - 2; // Conversation区域高度(行数)
236
+
237
+ // Ctrl+C 退出控制
238
+ const [lastCtrlC, setLastCtrlC] = useState(0);
239
+ const [showExitHint, setShowExitHint] = useState(false);
240
+ const [abortMessage, setAbortMessage] = useState(null); // 中止任务的提示
241
+ const abortControllerRef = useRef(null);
242
+ const inputRef = useRef(''); // 用于在 useInput 中获取最新的 input 值
243
+
244
+ // 当消息更新时,重新计算行
245
+ useEffect(() => {
246
+ const allLines = [];
247
+ const maxWidth = Math.floor(terminalSize.columns * 0.7) - 4; // Conversation区域宽度
248
+
249
+ for (const message of messages) {
250
+ const lines = formatMessageAsLines(message, maxWidth);
251
+ allLines.push(...lines);
252
+ }
253
+
254
+ setMessageLines(allLines);
255
+
256
+ // 自动滚动到底部(如果不是用户主动滚动)
257
+ const maxPosition = Math.max(0, allLines.length - conversationHeight);
258
+ // 只有在处理新消息时才自动滚动
259
+ if (isProcessing || allLines.length < 100) {
260
+ setScrollPosition(maxPosition);
261
+ }
262
+ }, [messages, terminalSize.columns, conversationHeight, isProcessing]);
263
+
264
+ // 键盘输入处理(用于滚动和 Ctrl+C)
265
+ useInput((input, key) => {
266
+ // 处理 Ctrl+C 和 ESC(始终有效,即使在输入模式)
267
+ if ((key.ctrl && input === 'c') || key.escape) {
268
+ const now = Date.now();
269
+
270
+ if (isProcessing) {
271
+ // 如果 AI 正在执行,中止对话
272
+ if (abortControllerRef.current) {
273
+ abortControllerRef.current.abort();
274
+ abortControllerRef.current = null;
275
+ }
276
+ setIsProcessing(false);
277
+ setActivity('❌ 用户中止了 AI 执行');
278
+ setAbortMessage('❌ AI 执行已被中止');
279
+ setThinking(prev => [...prev, `❌ [${new Date().toLocaleTimeString()}] 用户中止了 AI 执行`]);
280
+
281
+ // 3秒后清除中止提示
282
+ setTimeout(() => {
283
+ setAbortMessage(null);
284
+ setActivity(null);
285
+ }, 3000);
286
+
287
+ return;
288
+ }
289
+
290
+ // 没有任务在执行时的退出逻辑
291
+ if (now - lastCtrlC < 1500) {
292
+ // 1.5秒内再次按下,退出程序
293
+ console.log('\n👋 再见!\n');
294
+ process.exit(0);
295
+ } else {
296
+ // 第一次按下,显示提示
297
+ setShowExitHint(true);
298
+ setLastCtrlC(now);
299
+ setTimeout(() => setShowExitHint(false), 1500);
300
+ }
301
+ return;
302
+ }
303
+
304
+ // 处理 Tab 键 - 切换 Thinking 开关
305
+ if (key.tab) {
306
+ setThinkingEnabled(prev => {
307
+ const newValue = !prev;
308
+ process.env.CLOSER_THINKING_ENABLED = newValue ? '1' : '0';
309
+ setActivity(newValue ? '✅ Thinking 已启用' : '🚫 Thinking 已禁用');
310
+ setTimeout(() => setActivity(null), 2000);
311
+ return newValue;
312
+ });
313
+ return;
314
+ }
315
+
316
+ // 处理滚动(使用自定义滚动系统)
317
+ // PageUp/PageDown - 始终可用于滚动
318
+ if (key.pageUp) {
319
+ const delta = Math.min(10, conversationHeight);
320
+ setScrollPosition(prev => Math.max(0, prev - delta));
321
+ } else if (key.pageDown) {
322
+ const delta = Math.min(10, conversationHeight);
323
+ const maxPosition = Math.max(0, messageLines.length - conversationHeight);
324
+ setScrollPosition(prev => Math.min(maxPosition, prev + delta));
325
+ }
326
+ // 方向键 - 仅在输入框为空时滚动(避免与光标移动冲突)
327
+ else if (inputRef.current.length === 0) {
328
+ if (key.upArrow) {
329
+ setScrollPosition(prev => Math.max(0, prev - 1));
330
+ } else if (key.downArrow) {
331
+ const maxPosition = Math.max(0, messageLines.length - conversationHeight);
332
+ setScrollPosition(prev => Math.min(maxPosition, prev + 1));
333
+ }
334
+ }
335
+ // Alt 键组合 - 始终可用
336
+ else if (key.alt && key.upArrow) {
337
+ setScrollPosition(prev => Math.max(0, prev - 1));
338
+ } else if (key.alt && key.downArrow) {
339
+ const maxPosition = Math.max(0, messageLines.length - conversationHeight);
340
+ setScrollPosition(prev => Math.min(maxPosition, prev + 1));
341
+ }
342
+ }, { capture: true }); // capture: true 确保能捕获按键
343
+
344
+ // 初始化
345
+ useEffect(() => {
346
+ async function init() {
347
+ try {
348
+ const cfg = getConfig();
349
+ setConfig(cfg);
350
+
351
+ const conv = await createConversation(cfg);
352
+ setConversation(conv);
353
+
354
+ // 欢迎消息
355
+ const welcomeMsg = {
356
+ role: 'assistant',
357
+ content: `Welcome to Closer Code! 🚀
358
+
359
+ I'm your AI programming assistant. I can help you with:
360
+ • Writing and editing code
361
+ • Debugging and fixing errors
362
+ • Planning and executing complex tasks
363
+ • Searching through codebases
364
+ • Running tests and commands
365
+
366
+ Current directory: ${cfg.behavior.workingDir}
367
+
368
+ Type your message or command to get started.`
369
+ };
370
+
371
+ setMessages([welcomeMsg]);
372
+ setStatus('Ready');
373
+ } catch (error) {
374
+ setStatus(`Error: ${error.message}`);
375
+ console.error('Init error:', error);
376
+ }
377
+ }
378
+
379
+ init();
380
+ }, []);
381
+
382
+
383
+ // 处理用户输入
384
+ const handleSubmit = useCallback(async (value) => {
385
+ if (!conversation || isProcessing) return;
386
+
387
+ setInput('');
388
+ setIsProcessing(true);
389
+ setActivity('📤 发送消息到 AI...');
390
+
391
+ // 添加用户消息
392
+ const userMsg = { role: 'user', content: value };
393
+ setMessages(prev => [...prev, userMsg]);
394
+
395
+ // 处理特殊命令
396
+ if (value.startsWith('/')) {
397
+ await handleCommand(value);
398
+ setIsProcessing(false);
399
+ setActivity(null);
400
+ return;
401
+ }
402
+
403
+ try {
404
+ // 创建 AbortController 用于中止
405
+ const abortController = new AbortController();
406
+ abortControllerRef.current = abortController;
407
+
408
+ // 记录思考开始
409
+ setThinking(prev => [...prev, `🤔 [${new Date().toLocaleTimeString()}] 开始分析用户请求...`]);
410
+
411
+ // 发送到 AI
412
+ setActivity('🤔 AI 正在思考...');
413
+ const response = await conversation.sendMessage(
414
+ value,
415
+ (progress) => {
416
+ // 处理流式响应(使用 SDK 事件监听器 API)
417
+ if (progress.type === 'thinking') {
418
+ // AI thinking 内容 - 逐字显示
419
+ setActivity('🤔 AI 正在深度思考...');
420
+ setThinking(prev => {
421
+ const newThinking = [...prev];
422
+
423
+ // 检查最后一条是否是当前 thinking(未完成的)
424
+ const lastEntry = newThinking[newThinking.length - 1];
425
+ const thinkingDelta = progress.delta || '';
426
+
427
+ if (lastEntry && lastEntry.startsWith('🤔') && !lastEntry.includes('✅')) {
428
+ // 追加增量到当前的 thinking 条目
429
+ const timestamp = lastEntry.match(/\[.*?\]/)[0];
430
+ const currentContent = lastEntry.substring(lastEntry.indexOf('] ') + 2);
431
+ newThinking[newThinking.length - 1] = `🤔 ${timestamp} ${currentContent}${thinkingDelta}`;
432
+ } else {
433
+ // 创建新的 thinking 条目
434
+ const timestamp = `[${new Date().toLocaleTimeString()}]`;
435
+ newThinking.push(`🤔 ${timestamp} ${thinkingDelta}`);
436
+ }
437
+
438
+ return newThinking.slice(-30); // 保留最后 30 条thinking记录
439
+ });
440
+ } else if (progress.type === 'thinking_signature') {
441
+ // Thinking 签名
442
+ setThinking(prev => {
443
+ const newThinking = [...prev];
444
+ // 计算累计的thinking内容长度
445
+ const totalThinkingLength = prev
446
+ .filter(entry => entry.startsWith('🤔'))
447
+ .reduce((sum, entry) => {
448
+ // 提取thinking内容(去掉时间戳前缀)
449
+ const content = entry.replace(/^🤔 \[.*?\] /, '');
450
+ return sum + content.length;
451
+ }, 0);
452
+
453
+ newThinking.push(`✅ [${new Date().toLocaleTimeString()}] Thinking 完成 (${totalThinkingLength} 字符, ~${Math.ceil(totalThinkingLength/4)} tokens)`);
454
+ return newThinking.slice(-30);
455
+ });
456
+ } else if (progress.type === 'thinking_redacted') {
457
+ // Redacted thinking(被编辑的思考内容)
458
+ setThinking(prev => {
459
+ const newThinking = [...prev];
460
+ newThinking.push(`🔒 [${new Date().toLocaleTimeString()}] Redacted thinking: ${progress.content}`);
461
+ return newThinking.slice(-10);
462
+ });
463
+ } else if (progress.type === 'token') {
464
+ // 真正的流式文本
465
+ setActivity('✍️ AI 正在输入...');
466
+ setThinking(prev => [...prev, `✍️ [${new Date().toLocaleTimeString()}] 生成响应中...`]);
467
+ setMessages(prev => {
468
+ const lastMsg = prev[prev.length - 1];
469
+
470
+ if (lastMsg && lastMsg.role === 'assistant' && !lastMsg.complete) {
471
+ // 追加内容
472
+ return [
473
+ ...prev.slice(0, -1),
474
+ {
475
+ ...lastMsg,
476
+ content: lastMsg.content + progress.content,
477
+ key: Date.now()
478
+ }
479
+ ];
480
+ } else {
481
+ // 创建新消息
482
+ return [...prev, {
483
+ role: 'assistant',
484
+ content: progress.content,
485
+ complete: false,
486
+ key: Date.now()
487
+ }];
488
+ }
489
+ });
490
+ } else if (progress.type === 'tool_use_start') {
491
+ // 检测到工具调用
492
+ const thinkingMsg = `⚡ [${new Date().toLocaleTimeString()}] 检测到工具调用: ${progress.toolName}`;
493
+ setActivity(`⚡ 准备执行工具: ${progress.toolName}...`);
494
+ setThinking(prev => [...prev, thinkingMsg]);
495
+ } else if (progress.type === 'plan_created') {
496
+ // AI Planning 被创建
497
+ setCurrentPlan(progress.plan);
498
+ setThinking(prev => [...prev, `📋 [${new Date().toLocaleTimeString()}] AI Planning 已创建`]);
499
+ } else if (progress.type === 'plan_progress') {
500
+ // AI Planning 进度更新
501
+ setCurrentPlan(progress.plan);
502
+ } else if (progress.type === 'tool_start') {
503
+ const thinkingMsg = `⚡ [${new Date().toLocaleTimeString()}] 调用工具: ${progress.tool}`;
504
+ setActivity(`⚡ 执行工具: ${progress.tool}...`);
505
+ setThinking(prev => [...prev, thinkingMsg]);
506
+ // 只存储必要信息,限制历史记录数量
507
+ setToolExecutions(prev => {
508
+ const newExecs = [...prev, {
509
+ tool: progress.tool,
510
+ timestamp: new Date().toLocaleTimeString(),
511
+ result: null
512
+ }];
513
+ // 只保留最近 10 个
514
+ return newExecs.slice(-10);
515
+ });
516
+ } else if (progress.type === 'tool_complete') {
517
+ const resultMsg = progress.result.success ? '✓ 成功' : '✗ 失败';
518
+ setActivity('📊 处理工具结果...');
519
+ setThinking(prev => [...prev, `📊 [${new Date().toLocaleTimeString()}] 工具执行结果: ${resultMsg}`]);
520
+ // 只更新状态,不存储完整数据
521
+ setToolExecutions(prev => {
522
+ if (prev.length === 0) return prev;
523
+ const newExecs = [...prev];
524
+ newExecs[newExecs.length - 1] = {
525
+ ...newExecs[newExecs.length - 1],
526
+ success: progress.result.success,
527
+ error: progress.result.success ? null : (progress.result.error?.substring(0, 50) || 'Failed')
528
+ };
529
+ return newExecs;
530
+ });
531
+ }
532
+ }
533
+ );
534
+
535
+ abortControllerRef.current = null;
536
+
537
+ // 更新最后的消息为完整响应
538
+ setMessages(prev => {
539
+ const lastIdx = prev.findIndex(m => m.role === 'assistant' && !m.complete);
540
+ if (lastIdx >= 0) {
541
+ return [
542
+ ...prev.slice(0, lastIdx),
543
+ {
544
+ role: 'assistant',
545
+ content: response.content,
546
+ complete: true,
547
+ toolCalls: response.toolCalls,
548
+ key: Date.now()
549
+ }
550
+ ];
551
+ } else {
552
+ return [...prev, {
553
+ role: 'assistant',
554
+ content: response.content,
555
+ complete: true,
556
+ toolCalls: response.toolCalls,
557
+ key: Date.now()
558
+ }];
559
+ }
560
+ });
561
+
562
+ } catch (error) {
563
+ setMessages(prev => [...prev, {
564
+ role: 'error',
565
+ content: `Error: ${error.message}`,
566
+ key: Date.now()
567
+ }]);
568
+ } finally {
569
+ setIsProcessing(false);
570
+ setActivity(null);
571
+ }
572
+ }, [conversation, isProcessing]);
573
+
574
+ // 处理命令
575
+ const handleCommand = async (cmd) => {
576
+ const parts = cmd.split(' ');
577
+ const command = parts[0];
578
+ const args = parts.slice(1);
579
+
580
+ switch (command) {
581
+ case '/clear':
582
+ setActivity('🗑️ 清除对话历史...');
583
+ setMessages([]);
584
+ conversation.clearHistory();
585
+ setActivity(null);
586
+ break;
587
+
588
+ case '/plan':
589
+ if (args.length === 0) {
590
+ setMessages(prev => [...prev, {
591
+ role: 'system',
592
+ content: 'Usage: /plan <task description>'
593
+ }]);
594
+ return;
595
+ }
596
+ setActivity('📋 规划任务...');
597
+ setStatus('Planning...');
598
+ const planResult = await conversation.planAndExecute(args.join(' '), (progress) => {
599
+ if (progress.type === 'plan_created' || progress.type === 'plan_ready') {
600
+ setActivity('📋 任务计划已创建');
601
+ setCurrentPlan(progress.plan);
602
+ } else if (progress.type === 'step_start') {
603
+ setActivity(`⚙️ ${progress.step.description}`);
604
+ setCurrentPlan(progress.plan);
605
+ } else if (progress.type === 'step_complete' || progress.type === 'step_failed') {
606
+ setCurrentPlan(progress.plan);
607
+ }
608
+ });
609
+ setStatus('Ready');
610
+ setActivity(null);
611
+ setMessages(prev => [...prev, {
612
+ role: 'system',
613
+ content: `✅ Plan ${planResult.success ? 'completed' : 'failed'}`
614
+ }]);
615
+ break;
616
+
617
+ case '/learn':
618
+ setActivity('🧠 学习项目模式...');
619
+ setStatus('Learning...');
620
+ await conversation.learnProject();
621
+ setMessages(prev => [...prev, {
622
+ role: 'system',
623
+ content: 'Project patterns learned successfully!'
624
+ }]);
625
+ setStatus('Ready');
626
+ setActivity(null);
627
+ break;
628
+
629
+ case '/status':
630
+ setActivity('📊 获取统计信息...');
631
+ const summary = conversation.getSummary();
632
+ setMessages(prev => [...prev, {
633
+ role: 'system',
634
+ content: JSON.stringify(summary, null, 2)
635
+ }]);
636
+ setActivity(null);
637
+ break;
638
+
639
+ case '/export':
640
+ if (args.length === 0) {
641
+ setMessages(prev => [...prev, {
642
+ role: 'system',
643
+ content: 'Usage: /export <filename> - Export conversation to a text file'
644
+ }]);
645
+ return;
646
+ }
647
+ setActivity('📤 导出对话...');
648
+ const filename = args.join(' ');
649
+ await exportConversation(conversation, filename);
650
+ setMessages(prev => [...prev, {
651
+ role: 'system',
652
+ content: `✅ Conversation exported to: ${filename}`
653
+ }]);
654
+ setActivity(null);
655
+ break;
656
+
657
+ case '/help':
658
+ setMessages(prev => [...prev, {
659
+ role: 'system',
660
+ content: `Available commands:
661
+ /clear - Clear conversation history
662
+ /export <filename> - Export conversation to a text file
663
+ /plan <task> - Create and execute a task plan
664
+ /learn - Learn project patterns
665
+ /status - Show conversation summary
666
+ /help - Show this help message`
667
+ }]);
668
+ break;
669
+
670
+ default:
671
+ setMessages(prev => [...prev, {
672
+ role: 'system',
673
+ content: `Unknown command: ${command}. Type /help for available commands.`
674
+ }]);
675
+ }
676
+ };
677
+
678
+ if (!config) {
679
+ return (
680
+ <Box padding={1}>
681
+ <Text>Loading Closer Code...</Text>
682
+ </Box>
683
+ );
684
+ }
685
+
686
+ return (
687
+ <Box flexDirection="column" padding={1} height="100%">
688
+ {/* 顶部状态栏 */}
689
+ <Box
690
+ borderStyle="bold"
691
+ borderColor="magenta"
692
+ paddingX={1}
693
+ marginBottom={1}
694
+ >
695
+ <Box flexGrow={1}>
696
+ <Text bold color="magenta">Closer Code</Text>
697
+ <Text dim> - AI Programming Assistant</Text>
698
+ </Box>
699
+ <Text dim color="gray">|</Text>
700
+ <Box marginLeft={1}>
701
+ <Text color={isProcessing ? 'yellow' : 'green'}>
702
+ {isProcessing ? '● Processing' : '● ' + status}
703
+ </Text>
704
+ </Box>
705
+ </Box>
706
+
707
+ {/* Thinking 区域 - 占17.5%高度 */}
708
+ <Box
709
+ borderStyle="round"
710
+ borderColor="cyan"
711
+ flexDirection="column"
712
+ flexGrow={17.5}
713
+ marginBottom={1}
714
+ width="100%"
715
+ >
716
+ <Box borderBottom={false} borderColor="cyan" paddingBottom={0} marginBottom={1}>
717
+ <Text bold color="cyan">
718
+ 🧠 AI Thinking Process
719
+ <Text dim color="gray"> [Tab: {thinkingEnabled ? 'ON ✅' : 'OFF ❌'}]</Text>
720
+ </Text>
721
+ </Box>
722
+ <Box flexGrow={1} flexDirection="column" overflow="hidden" width="100%">
723
+ {thinking.length > 0 ? (
724
+ thinking.slice(-10).map((thought, i) => (
725
+ <Box key={i} width="100%">
726
+ <Text dim color="cyan" wrap="truncate">{thought}</Text>
727
+ </Box>
728
+ ))
729
+ ) : (
730
+ <Text dim color="gray">No thinking messages yet</Text>
731
+ )}
732
+ </Box>
733
+ </Box>
734
+
735
+ {/* 主内容区域 - 占剩余65% */}
736
+ <Box flexGrow={65} flexDirection="row">
737
+ <Box width="100%">
738
+ {/* 左侧:对话面板 - 占67%宽度 */}
739
+ <Box
740
+ borderStyle="round"
741
+ borderColor="blue"
742
+ flexDirection="column"
743
+ flexGrow={67}
744
+ marginRight={1}
745
+ width="67%"
746
+ height="100%"
747
+ >
748
+ <Box borderBottom={false} borderColor="blue" paddingBottom={0} marginBottom={1}>
749
+ <Text bold color="blue">💬 Conversation</Text>
750
+ </Box>
751
+ <Box flexDirection="column" overflow="hidden" width="100%" height="100%">
752
+ {messageLines.length > 0 ? (
753
+ <>
754
+ {/* 滚动提示 */}
755
+ {scrollPosition > 0 && (
756
+ <Box marginBottom={1} width="100%">
757
+ <Text dim color="blue">↑ Line {scrollPosition + 1} of {messageLines.length} - Press Alt+↓ or PageDown to scroll</Text>
758
+ </Box>
759
+ )}
760
+ {scrollPosition + conversationHeight < messageLines.length && (
761
+ <Box marginBottom={1} width="100%">
762
+ <Text dim color="blue">↓ {messageLines.length - scrollPosition - conversationHeight} more lines below</Text>
763
+ </Box>
764
+ )}
765
+ <ScrollContainer
766
+ items={messageLines}
767
+ height={conversationHeight}
768
+ scrollPosition={scrollPosition}
769
+ />
770
+ </>
771
+ ) : (
772
+ <Box justifyContent="center" alignItems="center" height="100%">
773
+ <Text dim>No messages yet</Text>
774
+ </Box>
775
+ )}
776
+ </Box>
777
+ </Box>
778
+
779
+ {/* 右侧:任务和工具面板 - 占33%宽度 */}
780
+ <Box flexDirection="column" flexGrow={33} width="33%" height="100%">
781
+ {/* 任务进度 - 占50%高度 */}
782
+ <Box
783
+ borderStyle="round"
784
+ borderColor="yellow"
785
+ flexDirection="column"
786
+ flexGrow={50}
787
+ marginBottom={1}
788
+ width="100%"
789
+ >
790
+ <Box borderBottom={false} borderColor="yellow" paddingBottom={0} marginBottom={1}>
791
+ <Text bold color="yellow">📋 Task Progress</Text>
792
+ </Box>
793
+ {currentPlan ? (
794
+ <TaskProgress plan={currentPlan} />
795
+ ) : (
796
+ <Text dim>No active task</Text>
797
+ )}
798
+ </Box>
799
+
800
+ {/* 工具执行 - 占50%高度 */}
801
+ <Box
802
+ borderStyle="round"
803
+ borderColor="green"
804
+ flexDirection="column"
805
+ flexGrow={50}
806
+ width="100%"
807
+ >
808
+ <Box borderBottom={false} borderColor="green" paddingBottom={0} marginBottom={1}>
809
+ <Text bold color="green">🔧 Tool Execution</Text>
810
+ </Box>
811
+ <Box flexDirection="column" overflow="hidden" width="100%">
812
+ {toolExecutions.slice(-10).map((exec, i) => (
813
+ <ToolExecution key={i} {...exec} />
814
+ ))}
815
+ {toolExecutions.length === 0 && (
816
+ <Text dim>No tools executed yet</Text>
817
+ )}
818
+ </Box>
819
+ </Box>
820
+ </Box>
821
+ </Box>
822
+ </Box>
823
+
824
+ {/* 活动提示 */}
825
+ {activity && (
826
+ <Box
827
+ borderStyle="round"
828
+ borderColor={abortMessage ? "red" : "yellow"}
829
+ paddingX={1}
830
+ marginTop={1}
831
+ marginBottom={1}
832
+ >
833
+ <Text bold color={abortMessage ? "red" : "yellow"}>{activity}</Text>
834
+ </Box>
835
+ )}
836
+
837
+ {/* 退出提示 */}
838
+ {showExitHint && (
839
+ <Box
840
+ borderStyle="round"
841
+ borderColor="red"
842
+ paddingX={1}
843
+ marginTop={1}
844
+ marginBottom={1}
845
+ >
846
+ <Text bold color="red">⚠️ 再次按 Ctrl+C 或 ESC 退出程序 (1.5秒内)</Text>
847
+ </Box>
848
+ )}
849
+
850
+ {/* 输入区域 */}
851
+ <Box
852
+ borderStyle="double"
853
+ borderColor="cyan"
854
+ paddingX={1}
855
+ marginTop={activity ? 0 : 1}
856
+ >
857
+ <Box marginRight={1}>
858
+ <Text bold color="cyan">❯</Text>
859
+ </Box>
860
+ <TextInput
861
+ value={input}
862
+ onChange={(value) => {
863
+ setInput(value);
864
+ inputRef.current = value;
865
+ }}
866
+ onSubmit={handleSubmit}
867
+ placeholder="Type a message or /help for commands..."
868
+ disabled={isProcessing}
869
+ />
870
+ </Box>
871
+ </Box>
872
+ );
873
+ }
874
+
875
+ // 导出对话到文本文件
876
+ export async function exportConversation(conversation, filename) {
877
+ try {
878
+ const exportData = conversation.export();
879
+ const messages = exportData.messages || [];
880
+
881
+ // 生成文本格式
882
+ let textContent = '';
883
+ textContent += '='.repeat(80) + '\n';
884
+ textContent += 'Closer Code - Conversation Export\n';
885
+ textContent += '='.repeat(80) + '\n';
886
+ textContent += `Export Date: ${new Date().toLocaleString('zh-CN')}\n`;
887
+ textContent += `Total Messages: ${messages.length}\n`;
888
+ textContent += '='.repeat(80) + '\n\n';
889
+
890
+ messages.forEach((msg, index) => {
891
+ const role = msg.role || 'unknown';
892
+ const roleLabel = {
893
+ 'user': '👤 User',
894
+ 'assistant': '🤖 Assistant',
895
+ 'system': 'ℹ️ System',
896
+ 'error': '❌ Error'
897
+ }[role] || role;
898
+
899
+ textContent += `[${index + 1}] ${roleLabel}\n`;
900
+ textContent += '-'.repeat(80) + '\n';
901
+
902
+ const content = msg.content;
903
+ if (typeof content === 'string') {
904
+ textContent += content + '\n';
905
+ } else if (Array.isArray(content)) {
906
+ // 处理工具调用等复杂内容
907
+ content.forEach(block => {
908
+ if (block.type === 'text') {
909
+ textContent += block.text + '\n';
910
+ } else if (block.type === 'tool_use') {
911
+ textContent += `[Tool: ${block.name}]\n`;
912
+ textContent += JSON.stringify(block.input, null, 2) + '\n';
913
+ } else if (block.type === 'tool_result') {
914
+ textContent += `[Tool Result]\n`;
915
+ textContent += block.content + '\n';
916
+ }
917
+ });
918
+ } else {
919
+ textContent += JSON.stringify(content, null, 2) + '\n';
920
+ }
921
+
922
+ textContent += '\n';
923
+ });
924
+
925
+ textContent += '='.repeat(80) + '\n';
926
+ textContent += 'End of Export\n';
927
+ textContent += '='.repeat(80) + '\n';
928
+
929
+ // 确保文件名有 .txt 扩展名
930
+ let finalFilename = filename;
931
+ if (!filename.endsWith('.txt')) {
932
+ finalFilename = filename + '.txt';
933
+ }
934
+
935
+ // 写入文件
936
+ fs.writeFileSync(finalFilename, textContent, 'utf-8');
937
+
938
+ return { success: true, path: finalFilename };
939
+ } catch (error) {
940
+ return { success: false, error: error.message };
941
+ }
942
+ }
943
+
944
+ // 启动应用
945
+ render(<App />, {exitOnCtrlC: false});
946
+
947
+ // 注意:不在这里设置 SIGINT 处理器,因为 useInput 会处理 Ctrl+C
948
+ // 如果在这里设置,会导致第一次 Ctrl+C 就直接退出