closer-code 1.0.0 → 1.0.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 (99) hide show
  1. package/.closer-code.example.json +32 -0
  2. package/DUAL_OPTIMIZATION_COMPLETE.md +293 -0
  3. package/README.md +167 -557
  4. package/README_OPENAI.md +163 -0
  5. package/THINKING_THROTTLING_OPTIMIZATION.md +244 -0
  6. package/THROTTLING_1_5S_OPTIMIZATION.md +401 -0
  7. package/TOOLS_IMPROVEMENTS_SUMMARY.md +273 -0
  8. package/cloco.md +5 -1
  9. package/config.example.json +15 -94
  10. package/config.mcp.example.json +81 -0
  11. package/dist/bash-runner.js +5 -126
  12. package/dist/batch-cli.js +286 -20658
  13. package/dist/closer-cli.js +329 -21135
  14. package/dist/index.js +308 -31036
  15. package/docs/ANTHROPIC_TOOL_ERROR_HANDLING.md +220 -0
  16. package/docs/BUILD_COMMANDS.md +79 -0
  17. package/docs/CTRL_Z_SUPPORT.md +189 -0
  18. package/docs/DEEPSEEK_R1_INTEGRATION.md +427 -0
  19. package/docs/FIX_OPENAI_TOOL_ERROR_HANDLING.md +375 -0
  20. package/docs/FIX_OPENAI_TOOL_RESULT.md +198 -0
  21. package/docs/INPUT_ENHANCEMENTS.md +192 -0
  22. package/docs/MCP_IMPLEMENTATION_SUMMARY.md +428 -0
  23. package/docs/MCP_INTEGRATION.md +418 -0
  24. package/docs/MCP_QUICKSTART.md +299 -0
  25. package/docs/MCP_README.md +166 -0
  26. package/docs/MINIFY_BUILD.md +180 -0
  27. package/docs/MULTILINE_INPUT_FEATURE.md +119 -0
  28. package/docs/OPENAI_CLIENT.md +258 -0
  29. package/docs/PROJECT_LOCAL_CONFIG.md +471 -0
  30. package/docs/PROJECT_LOCAL_CONFIG_SUMMARY.md +407 -0
  31. package/docs/REFACTOR_CONVERSATION.md +306 -0
  32. package/docs/REGION_EDIT_DESIGN.md +475 -0
  33. package/docs/SIGNAL_HANDLING.md +171 -0
  34. package/docs/STREAM_UPDATE_THROTTLE.md +273 -0
  35. package/docs/TOOLS_REFACTOR_PLAN.md +520 -0
  36. package/ds_r1.md +249 -0
  37. package/examples/abort-fence-example.js +294 -0
  38. package/package.json +18 -4
  39. package/src/ai-client-legacy.js +6 -1
  40. package/src/ai-client-openai.js +672 -0
  41. package/src/ai-client.js +30 -13
  42. package/src/closer-cli.jsx +450 -162
  43. package/src/components/fullscreen-conversation.jsx +157 -0
  44. package/src/components/ink-text-input/index.jsx +324 -0
  45. package/src/components/multiline-text-input.jsx +614 -0
  46. package/src/components/progress-bar.jsx +135 -0
  47. package/src/components/tool-detail-view.jsx +82 -0
  48. package/src/components/tool-renderers/bash-renderer.jsx +197 -0
  49. package/src/components/tool-renderers/file-edit-renderer.jsx +247 -0
  50. package/src/components/tool-renderers/file-read-renderer.jsx +261 -0
  51. package/src/components/tool-renderers/file-write-renderer.jsx +222 -0
  52. package/src/components/tool-renderers/index.jsx +178 -0
  53. package/src/components/tool-renderers/list-renderer.jsx +274 -0
  54. package/src/components/tool-renderers/search-renderer.jsx +248 -0
  55. package/src/config.js +182 -20
  56. package/src/conversation/abort-fence.js +158 -0
  57. package/src/conversation/core.js +377 -0
  58. package/src/conversation/index.js +33 -0
  59. package/src/conversation/mcp-integration.js +96 -0
  60. package/src/conversation/plan-manager.js +295 -0
  61. package/src/conversation/stream-handler.js +154 -0
  62. package/src/conversation/tool-executor.js +264 -0
  63. package/src/conversation.js +23 -958
  64. package/src/hooks/use-throttled-state.js +158 -0
  65. package/src/input/enhanced-input.jsx +268 -0
  66. package/src/input/history.js +342 -0
  67. package/src/logger.js +20 -0
  68. package/src/mcp/client.js +275 -0
  69. package/src/mcp/tools-adapter.js +149 -0
  70. package/src/planner.js +18 -5
  71. package/src/prompt-builder.js +159 -0
  72. package/src/tools.js +457 -25
  73. package/src/utils/json-parser.js +231 -0
  74. package/src/utils/json-repair.js +146 -0
  75. package/src/utils/platform.js +259 -0
  76. package/test/test-ctrl-bf.js +121 -0
  77. package/test/test-deepseek-reasoning.js +118 -0
  78. package/test/test-history-navigation.js +80 -0
  79. package/test/test-input-fix.js +105 -0
  80. package/test/test-input-history.js +98 -0
  81. package/test/test-mcp.js +115 -0
  82. package/test/test-openai-client.js +152 -0
  83. package/test/test-openai-tool-result.js +199 -0
  84. package/test/test-project-config.js +106 -0
  85. package/test/test-shortcuts.js +79 -0
  86. package/test/test-stream-throttle.js +124 -0
  87. package/test/test-tool-error-handling.js +95 -0
  88. package/test/verify-input-fix.sh +35 -0
  89. package/test-abort-fence.js +263 -0
  90. package/test-abort-fix.js +54 -0
  91. package/test-abort-new-conversation.js +75 -0
  92. package/test-ctrl-z.js +54 -0
  93. package/test-file-read.js +105 -0
  94. package/test-tool-display.js +127 -0
  95. package/src/closer-cli.jsx.backup +0 -948
  96. package/test/workflows/longtalk/cloco.md +0 -19
  97. package/test/workflows/longtalk/emoji_500.txt +0 -63
  98. package/test/workflows/longtalk/emoji_list.txt +0 -20
  99. package/test-ctrl-c.jsx +0 -126
@@ -5,12 +5,18 @@
5
5
 
6
6
  import React, { useState, useEffect, useCallback, useRef } from 'react';
7
7
  import { render, Box, Text } from 'ink';
8
- import TextInput from 'ink-text-input';
9
8
  import { useInput } from 'ink';
10
9
  import { createConversation } from './conversation.js';
11
10
  import { getConfig, updateConfig } from './config.js';
11
+ import { generateToolSummary } from './tools.js';
12
12
  import { createShortcutManager } from './shortcuts.js';
13
13
  import { createSnippetManager, SNIPPET_TEMPLATES } from './snippets.js';
14
+ import { createHistoryManager } from './input/history.js';
15
+ import { EnhancedTextInputWithShortcuts } from './input/enhanced-input.jsx';
16
+ import FullscreenConversation from './components/fullscreen-conversation.jsx';
17
+ import { ToolDetailPanel } from './components/tool-detail-view.jsx';
18
+ import { safeSuspend, getPlatformName } from './utils/platform.js';
19
+ import { useSmartThrottledState } from './hooks/use-throttled-state.js';
14
20
  import fs from 'fs';
15
21
  import path from 'path';
16
22
 
@@ -18,7 +24,10 @@ import path from 'path';
18
24
  function Panel({ title, children, borderColor = 'gray', flex = 1 }) {
19
25
  return (
20
26
  <Box
21
- borderStyle="round"
27
+ borderTop={true}
28
+ borderBottom={true}
29
+ borderLeft={false}
30
+ borderRight={false}
22
31
  borderColor={borderColor}
23
32
  flexDirection="column"
24
33
  flexGrow={flex}
@@ -106,13 +115,20 @@ function TaskProgress({ plan }) {
106
115
  }
107
116
 
108
117
  // 工具执行显示组件
109
- function ToolExecution({ summary, timestamp }) {
118
+ function ToolExecution({ summary, detailInfo, timestamp }) {
110
119
  return (
111
120
  <Box flexDirection="column" marginBottom={1} paddingX={1} borderStyle="single" borderColor="gray" width="100%">
121
+ {/* 第一行:简短摘要 */}
112
122
  <Box width="100%">
113
123
  <Text>{summary}</Text>
114
124
  <Text dim> [{timestamp}]</Text>
115
125
  </Box>
126
+ {/* 第二行:详细信息 */}
127
+ {detailInfo && (
128
+ <Box width="100%">
129
+ <Text dim color="gray">{detailInfo}</Text>
130
+ </Box>
131
+ )}
116
132
  </Box>
117
133
  );
118
134
  }
@@ -269,6 +285,8 @@ function App() {
269
285
  const [messages, setMessages] = useState([]);
270
286
  const [input, setInput] = useState('');
271
287
  const [isProcessing, setIsProcessing] = useState(false);
288
+ const [fullscreenMode, setFullscreenMode] = useState(false);
289
+ const [showToolsInFullscreen, setShowToolsInFullscreen] = useState(true); // 全屏模式下是否显示工具
272
290
  const [currentPlan, setCurrentPlan] = useState(null);
273
291
  const [toolExecutions, setToolExecutions] = useState([]);
274
292
  const [status, setStatus] = useState('Initializing...');
@@ -277,6 +295,35 @@ function App() {
277
295
  const [thinking, setThinking] = useState([]); // AI 思考过程
278
296
  const [thinkingEnabled, setThinkingEnabled] = useState(true); // Thinking 开关状态
279
297
  const [thinkingScrollPosition, setThinkingScrollPosition] = useState(0); // Thinking滚动位置
298
+ const [showToolDetail, setShowToolDetail] = useState(false); // 工具详情面板开关
299
+ const [toolDetailIndex, setToolDetailIndex] = useState(0); // 工具详情面板选中索引
300
+
301
+ // 节流更新 Hook
302
+ const activityUpdate = useSmartThrottledState(setActivity, {
303
+ throttleDelay: 1500,
304
+ immediateTypes: ['error', 'abort']
305
+ });
306
+
307
+ const thinkingUpdate = useSmartThrottledState(setThinking, {
308
+ throttleDelay: 1500,
309
+ immediateTypes: ['tool_start', 'tool_complete', 'thinking_signature',
310
+ 'thinking_redacted', 'abort']
311
+ });
312
+
313
+ const messagesUpdate = useSmartThrottledState(setMessages, {
314
+ throttleDelay: 1500,
315
+ immediateTypes: ['user', 'error', 'system']
316
+ });
317
+
318
+ const toolExecutionsUpdate = useSmartThrottledState(setToolExecutions, {
319
+ throttleDelay: 1500,
320
+ immediateTypes: ['complete', 'error']
321
+ });
322
+
323
+ const planUpdate = useSmartThrottledState(setCurrentPlan, {
324
+ throttleDelay: 1500,
325
+ immediateTypes: ['completed', 'failed']
326
+ });
280
327
 
281
328
  // 终端尺寸状态
282
329
  const [terminalSize, setTerminalSize] = useState({
@@ -299,21 +346,50 @@ function App() {
299
346
  };
300
347
  }, []);
301
348
 
349
+ // 监听 SIGCONT (fg 恢复信号)
350
+ useEffect(() => {
351
+ const handleContinue = () => {
352
+ // 恢复 raw mode
353
+ if (process.stdin.isTTY) {
354
+ process.stdin.setRawMode(true);
355
+ process.stdin.resume();
356
+ }
357
+
358
+ // 清屏重绘
359
+ console.clear();
360
+
361
+ // 强制刷新终端尺寸,触发重新渲染
362
+ setTerminalSize({
363
+ columns: process.stdout.columns || 80,
364
+ rows: process.stdout.rows || 30
365
+ });
366
+ };
367
+
368
+ process.on('SIGCONT', handleContinue);
369
+
370
+ return () => {
371
+ process.off('SIGCONT', handleContinue);
372
+ };
373
+ }, []);
374
+
302
375
  // 自定义滚动管理
303
376
  const [messageLines, setMessageLines] = useState([]); // 所有消息行
304
377
  const [scrollPosition, setScrollPosition] = useState(0); // 当前滚动位置
305
- const conversationHeight = Math.floor(terminalSize.rows * 0.65) - 2; // Conversation区域高度(行数)
378
+ const conversationHeight = Math.floor(terminalSize.rows * 0.65) - 4; // Conversation区域高度(行数)
306
379
 
307
380
  // Thinking区域滚动管理
308
381
  const [thinkingLines, setThinkingLines] = useState([]); // 所有thinking行
309
- const thinkingHeight = 3; // Thinking区域固定显示3行
310
-
382
+ const thinkingHeight = 4; // Thinking区域固定显示4行(增加1行以便更好查看标题)
383
+
311
384
  // Ctrl+C 退出控制
312
385
  const [lastCtrlC, setLastCtrlC] = useState(0);
313
386
  const [showExitHint, setShowExitHint] = useState(false);
314
387
  const [abortMessage, setAbortMessage] = useState(null); // 中止任务的提示
315
- const abortControllerRef = useRef(null);
388
+ const [aborting, setAborting] = useState(false); // 是否正在中止
389
+ const conversationRef = useRef(null); // Conversation 对象引用
316
390
  const inputRef = useRef(''); // 用于在 useInput 中获取最新的 input 值
391
+ // 历史记录管理器
392
+ const [inputHistory] = useState(() => createHistoryManager({ maxSize: 100 }));
317
393
 
318
394
  // 当消息更新时,重新计算行
319
395
  useEffect(() => {
@@ -354,27 +430,56 @@ function App() {
354
430
 
355
431
  // 键盘输入处理(用于滚动和 Ctrl+C)
356
432
  useInput((input, key) => {
433
+ // 处理 Ctrl+Z - 挂起程序(发送 SIGTSTP 信号)
434
+ if (key.ctrl && input === 'z') {
435
+ // 使用安全的挂起函数
436
+ const success = safeSuspend();
437
+
438
+ if (!success) {
439
+ // 挂起失败或不支持,显示提示
440
+ console.log(`\n⚠️ ${getPlatformName()} 不支持 Ctrl+Z 挂起\n`);
441
+ console.log('替代方案:');
442
+ console.log(' - 使用 Ctrl+C 退出程序');
443
+ console.log(' - 或使用 Ctrl+G 切换全屏模式\n');
444
+ }
445
+ return;
446
+ }
447
+
448
+ // 处理 Ctrl+G - 切换全屏模式
449
+ if (key.ctrl && input === 'g') {
450
+ setFullscreenMode(prev => !prev);
451
+ return;
452
+ }
453
+
454
+ // 处理 Ctrl+T - 全屏模式下切换工具显示,普通模式下切换工具详情面板
455
+ if (key.ctrl && input === 't') {
456
+ if (fullscreenMode) {
457
+ // 全屏模式:切换工具显示
458
+ setShowToolsInFullscreen(prev => {
459
+ const newValue = !prev;
460
+ setActivity(newValue ? '🔧 工具显示已开启' : '🚫 工具显示已关闭');
461
+ setTimeout(() => setActivity(null), 1500);
462
+ return newValue;
463
+ });
464
+ } else {
465
+ // 普通模式:切换工具详情面板
466
+ setShowToolDetail(prev => {
467
+ const newValue = !prev;
468
+ setActivity(newValue ? '📋 工具详情面板已打开' : '📋 工具详情面板已关闭');
469
+ setTimeout(() => setActivity(null), 1500);
470
+ return newValue;
471
+ });
472
+ }
473
+ return;
474
+ }
475
+
357
476
  // 处理 Ctrl+C 和 ESC(始终有效,即使在输入模式)
358
477
  if ((key.ctrl && input === 'c') || key.escape) {
359
478
  const now = Date.now();
360
479
 
361
480
  if (isProcessing) {
362
481
  // 如果 AI 正在执行,中止对话
363
- if (abortControllerRef.current) {
364
- abortControllerRef.current.abort();
365
- abortControllerRef.current = null;
366
- }
367
- setIsProcessing(false);
368
- setActivity('❌ 用户中止了 AI 执行');
369
- setAbortMessage('❌ AI 执行已被中止');
370
- setThinking(prev => [...prev, `❌ [${new Date().toLocaleTimeString()}] 用户中止了 AI 执行`]);
371
-
372
- // 3秒后清除中止提示
373
- setTimeout(() => {
374
- setAbortMessage(null);
375
- setActivity(null);
376
- }, 3000);
377
-
482
+ handleAbort();
378
483
  return;
379
484
  }
380
485
 
@@ -404,9 +509,16 @@ function App() {
404
509
  return;
405
510
  }
406
511
 
512
+ // 方向键:让 MultilineTextInput 处理历史记录导航和多行光标移动
513
+ // 父组件不再拦截这些事件
514
+ if (key.upArrow || key.downArrow) {
515
+ // 始终让子组件处理
516
+ return false;
517
+ }
518
+
407
519
  // 处理滚动(使用自定义滚动系统)
408
520
  // PageUp/PageDown - 始终可用于滚动
409
- if (key.pageUp) {
521
+ else if (key.pageUp) {
410
522
  const delta = Math.min(10, conversationHeight);
411
523
  setScrollPosition(prev => Math.max(0, prev - delta));
412
524
  } else if (key.pageDown) {
@@ -414,15 +526,6 @@ function App() {
414
526
  const maxPosition = Math.max(0, messageLines.length - conversationHeight);
415
527
  setScrollPosition(prev => Math.min(maxPosition, prev + delta));
416
528
  }
417
- // 方向键 - 仅在输入框为空时滚动(避免与光标移动冲突)
418
- else if (inputRef.current.length === 0) {
419
- if (key.upArrow) {
420
- setScrollPosition(prev => Math.max(0, prev - 1));
421
- } else if (key.downArrow) {
422
- const maxPosition = Math.max(0, messageLines.length - conversationHeight);
423
- setScrollPosition(prev => Math.min(maxPosition, prev + 1));
424
- }
425
- }
426
529
  // Alt 键组合 - 始终可用
427
530
  else if (key.alt && key.upArrow) {
428
531
  setScrollPosition(prev => Math.max(0, prev - 1));
@@ -430,15 +533,32 @@ function App() {
430
533
  const maxPosition = Math.max(0, messageLines.length - conversationHeight);
431
534
  setScrollPosition(prev => Math.min(maxPosition, prev + 1));
432
535
  }
433
- // Shift + 方向键 - 滚动Thinking区域
536
+ // Shift + 方向键 - 工具详情面板打开时切换工具,否则滚动Thinking区域
434
537
  else if (key.shift && key.upArrow) {
435
- setThinkingScrollPosition(prev => Math.max(0, prev - 1));
538
+ if (showToolDetail && toolExecutions.length > 0) {
539
+ setToolDetailIndex(prev => Math.max(0, prev - 1));
540
+ } else {
541
+ setThinkingScrollPosition(prev => Math.max(0, prev - 1));
542
+ }
436
543
  } else if (key.shift && key.downArrow) {
437
- const maxPosition = Math.max(0, thinkingLines.length - thinkingHeight);
438
- setThinkingScrollPosition(prev => Math.min(maxPosition, prev + 1));
544
+ if (showToolDetail && toolExecutions.length > 0) {
545
+ setToolDetailIndex(prev => Math.min(toolExecutions.length - 1, prev + 1));
546
+ } else {
547
+ const maxPosition = Math.max(0, thinkingLines.length - thinkingHeight);
548
+ setThinkingScrollPosition(prev => Math.min(maxPosition, prev + 1));
549
+ }
439
550
  }
440
551
  }, { capture: true }); // capture: true 确保能捕获按键
441
552
 
553
+ // Token 统计状态
554
+ const [tokenStats, setTokenStats] = useState({
555
+ total: 0,
556
+ limit: 4096, // 默认token限制
557
+ input: 0,
558
+ output: 0,
559
+ percentage: 0
560
+ });
561
+
442
562
  // 初始化
443
563
  useEffect(() => {
444
564
  async function init() {
@@ -446,8 +566,19 @@ function App() {
446
566
  const cfg = getConfig();
447
567
  setConfig(cfg);
448
568
 
569
+ // 从配置中获取token限制
570
+ const provider = cfg.ai?.provider || 'anthropic';
571
+ const tokenLimit = cfg.ai?.[provider]?.maxTokens || 4096;
572
+
573
+ // 更新token统计状态
574
+ setTokenStats(prev => ({
575
+ ...prev,
576
+ limit: tokenLimit
577
+ }));
578
+
449
579
  const conv = await createConversation(cfg);
450
580
  setConversation(conv);
581
+ conversationRef.current = conv; // 保存引用
451
582
 
452
583
  // 欢迎消息
453
584
  const welcomeMsg = {
@@ -478,9 +609,77 @@ Type your message or command to get started.`
478
609
  }, []);
479
610
 
480
611
 
612
+ // 更新token统计信息
613
+ const updateTokenStats = useCallback((newInputTokens = 0, newOutputTokens = 0) => {
614
+ setTokenStats(prev => {
615
+ const newInput = prev.input + newInputTokens;
616
+ const newOutput = prev.output + newOutputTokens;
617
+ const newTotal = newInput + newOutput;
618
+ const newPercentage = Math.round((newTotal / prev.limit) * 100);
619
+
620
+ return {
621
+ ...prev,
622
+ input: newInput,
623
+ output: newOutput,
624
+ total: newTotal,
625
+ percentage: newPercentage
626
+ };
627
+ });
628
+ }, []);
629
+
630
+ // 估算用户消息的token数(在没有API计数的情况下使用)
631
+ const estimateUserTokens = useCallback((text) => {
632
+ // 简单估算:中文字符和英文字符分别处理
633
+ const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
634
+ const englishChars = text.length - chineseChars;
635
+ // 中文字符约2-3个token,英文字符约0.25个token(按单词计算)
636
+ return Math.ceil(chineseChars * 2.5 + englishChars * 0.25);
637
+ }, []);
638
+
639
+ /**
640
+ * 处理 Ctrl+C 中止
641
+ * 使用 Abort Fence 机制中止当前对话
642
+ */
643
+ const handleAbort = useCallback(async () => {
644
+ if (!isProcessing || aborting) {
645
+ return;
646
+ }
647
+
648
+ setAborting(true);
649
+ setActivity('⚠️ 正在中止任务...');
650
+ setThinking(prev => [...prev, `⚠️ [${new Date().toLocaleTimeString()}] 正在中止当前任务...`]);
651
+
652
+ try {
653
+ if (conversationRef.current) {
654
+ // 调用 Conversation 的 abortCurrentPhase 方法
655
+ await conversationRef.current.abortCurrentPhase();
656
+ }
657
+ } catch (error) {
658
+ console.error('[Abort Error]', error.message);
659
+ setThinking(prev => [...prev, `❌ [${new Date().toLocaleTimeString()}] 中止失败: ${error.message}`]);
660
+ } finally {
661
+ setAborting(false);
662
+ setIsProcessing(false);
663
+ setActivity('❌ 任务已中止');
664
+ setAbortMessage('❌ AI 执行已被中止');
665
+
666
+ // 3秒后清除中止提示
667
+ setTimeout(() => {
668
+ setAbortMessage(null);
669
+ setActivity(null);
670
+ }, 3000);
671
+ }
672
+ }, [isProcessing, aborting]);
673
+
481
674
  // 处理用户输入
482
675
  const handleSubmit = useCallback(async (value) => {
483
- if (!conversation || isProcessing) return;
676
+ if (!conversation || isProcessing) {
677
+ // 如果正在中止,等待
678
+ if (aborting) {
679
+ setActivity('⏳ 等待中止完成...');
680
+ }
681
+ return;
682
+ }
484
683
 
485
684
  setInput('');
486
685
  setIsProcessing(true);
@@ -490,6 +689,10 @@ Type your message or command to get started.`
490
689
  const userMsg = { role: 'user', content: value };
491
690
  setMessages(prev => [...prev, userMsg]);
492
691
 
692
+ // 估算用户消息的token数(在没有API计数的情况下使用)
693
+ const userTokens = estimateUserTokens(value);
694
+ updateTokenStats(userTokens, 0);
695
+
493
696
  // 处理特殊命令
494
697
  if (value.startsWith('/')) {
495
698
  await handleCommand(value);
@@ -499,23 +702,19 @@ Type your message or command to get started.`
499
702
  }
500
703
 
501
704
  try {
502
- // 创建 AbortController 用于中止
503
- const abortController = new AbortController();
504
- abortControllerRef.current = abortController;
505
-
506
705
  // 记录思考开始
507
706
  setThinking(prev => [...prev, `🤔 [${new Date().toLocaleTimeString()}] 开始分析用户请求...`]);
508
-
509
- // 发送到 AI
707
+
708
+ // 发送到 AI(Conversation 内部会管理 AbortController)
510
709
  setActivity('🤔 AI 正在思考...');
511
710
  const response = await conversation.sendMessage(
512
711
  value,
513
712
  (progress) => {
514
713
  // 处理流式响应(使用 SDK 事件监听器 API)
515
714
  if (progress.type === 'thinking') {
516
- // AI thinking 内容 - 逐字显示
517
- setActivity('🤔 AI 正在深度思考...');
518
- setThinking(prev => {
715
+ // AI thinking 内容 - 逐字显示(使用节流更新)
716
+ activityUpdate.updateImmediate('🤔 AI 正在深度思考...');
717
+ thinkingUpdate.updateSmart(prev => {
519
718
  const newThinking = [...prev];
520
719
 
521
720
  // 检查最后一条是否是当前 thinking(未完成的)
@@ -534,10 +733,10 @@ Type your message or command to get started.`
534
733
  }
535
734
 
536
735
  return newThinking.slice(-30); // 保留最后 30 条thinking记录
537
- });
736
+ }, 'thinking');
538
737
  } else if (progress.type === 'thinking_signature') {
539
- // Thinking 签名
540
- setThinking(prev => {
738
+ // Thinking 签名(立即更新)
739
+ thinkingUpdate.updateSmart(prev => {
541
740
  const newThinking = [...prev];
542
741
  // 计算累计的thinking内容长度
543
742
  const totalThinkingLength = prev
@@ -547,22 +746,22 @@ Type your message or command to get started.`
547
746
  const content = entry.replace(/^🤔 \[.*?\] /, '');
548
747
  return sum + content.length;
549
748
  }, 0);
550
-
749
+
551
750
  newThinking.push(`✅ [${new Date().toLocaleTimeString()}] Thinking 完成 (${totalThinkingLength} 字符, ~${Math.ceil(totalThinkingLength/4)} tokens)`);
552
751
  return newThinking.slice(-30);
553
- });
752
+ }, 'thinking_signature');
554
753
  } else if (progress.type === 'thinking_redacted') {
555
- // Redacted thinking(被编辑的思考内容)
556
- setThinking(prev => {
754
+ // Redacted thinking(立即更新)
755
+ thinkingUpdate.updateSmart(prev => {
557
756
  const newThinking = [...prev];
558
757
  newThinking.push(`🔒 [${new Date().toLocaleTimeString()}] Redacted thinking: ${progress.content}`);
559
758
  return newThinking.slice(-10);
560
- });
759
+ }, 'thinking_redacted');
561
760
  } else if (progress.type === 'token') {
562
- // 真正的流式文本
563
- setActivity('✍️ AI 正在输入...');
564
- setThinking(prev => [...prev, `✍️ [${new Date().toLocaleTimeString()}] 生成响应中...`]);
565
- setMessages(prev => {
761
+ // 真正的流式文本(使用节流更新)
762
+ activityUpdate.updateImmediate('✍️ AI 正在输入...');
763
+ thinkingUpdate.updateThrottled(prev => [...prev, `✍️ [${new Date().toLocaleTimeString()}] 生成响应中...`]);
764
+ messagesUpdate.updateSmart(prev => {
566
765
  const lastMsg = prev[prev.length - 1];
567
766
 
568
767
  if (lastMsg && lastMsg.role === 'assistant' && !lastMsg.complete) {
@@ -588,94 +787,92 @@ Type your message or command to get started.`
588
787
  } else if (progress.type === 'tool_use_start') {
589
788
  // 检测到工具调用
590
789
  const thinkingMsg = `⚡ [${new Date().toLocaleTimeString()}] 检测到工具调用: ${progress.toolName}`;
591
- setActivity(`⚡ 准备执行工具: ${progress.toolName}...`);
592
- setThinking(prev => [...prev, thinkingMsg]);
790
+ activityUpdate.updateImmediate(`⚡ 准备执行工具: ${progress.toolName}...`);
791
+ thinkingUpdate.updateSmart(prev => [...prev, thinkingMsg], 'tool_start');
593
792
  } else if (progress.type === 'plan_created') {
594
793
  // AI Planning 被创建
595
- setCurrentPlan(progress.plan);
596
- setThinking(prev => [...prev, `📋 [${new Date().toLocaleTimeString()}] AI Planning 已创建`]);
794
+ planUpdate.updateImmediate(progress.plan);
795
+ thinkingUpdate.updateThrottled(prev => [...prev, `📋 [${new Date().toLocaleTimeString()}] AI Planning 已创建`]);
597
796
  } else if (progress.type === 'plan_progress') {
598
- // AI Planning 进度更新
599
- setCurrentPlan(progress.plan);
797
+ // AI Planning 进度更新(节流更新)
798
+ planUpdate.updateThrottled(progress.plan);
600
799
  } else if (progress.type === 'tool_start') {
601
800
  const thinkingMsg = `⚡ [${new Date().toLocaleTimeString()}] 调用工具: ${progress.tool}`;
602
- setActivity(`⚡ 执行工具: ${progress.tool}...`);
603
- setThinking(prev => [...prev, thinkingMsg]);
801
+ activityUpdate.updateImmediate(`⚡ 执行工具: ${progress.tool}...`);
802
+ thinkingUpdate.updateSmart(prev => [...prev, thinkingMsg], 'tool_start');
604
803
  // 存储工具信息,包括 input 和 result 引用(用于生成摘要)
605
- setToolExecutions(prev => {
804
+ toolExecutionsUpdate.updateSmart(prev => {
606
805
  const newExecs = [...prev, {
806
+ id: Date.now(),
607
807
  tool: progress.tool,
608
808
  timestamp: new Date().toLocaleTimeString(),
609
809
  input: progress.input,
610
- result: null
810
+ result: null,
811
+ status: 'running',
812
+ startTime: Date.now(),
813
+ duration: null
611
814
  }];
612
815
  // 只保留最近 10 个
613
- return newExecs.slice(-10);
614
- });
816
+ const sliced = newExecs.slice(-10);
817
+ // 自动将索引指向最新的工具
818
+ setToolDetailIndex(sliced.length - 1);
819
+ return sliced;
820
+ }, 'tool_start');
615
821
  } else if (progress.type === 'tool_complete') {
616
822
  const resultMsg = progress.result.success ? '✓ 成功' : '✗ 失败';
617
- setActivity('📊 处理工具结果...');
618
- setThinking(prev => [...prev, `📊 [${new Date().toLocaleTimeString()}] 工具执行结果: ${resultMsg}`]);
823
+ activityUpdate.updateImmediate('📊 处理工具结果...');
824
+ thinkingUpdate.updateSmart(prev => [...prev, `📊 [${new Date().toLocaleTimeString()}] 工具执行结果: ${resultMsg}`], 'tool_complete');
619
825
  // 更新工具执行结果,并生成摘要
620
- setToolExecutions(prev => {
826
+ toolExecutionsUpdate.updateSmart(prev => {
621
827
  if (prev.length === 0) return prev;
622
828
  const newExecs = [...prev];
623
- const lastExec = newExecs[newExecs.length - 1];
624
-
625
- // 生成简短摘要(不使用完整数据)
626
- let summary = '';
627
- const tool = lastExec.tool;
628
- const input = lastExec.input || {};
629
- const result = progress.result;
630
-
631
- // 根据工具类型生成摘要
632
- if (tool === 'bash') {
633
- const cmd = input.command || '';
634
- const parts = cmd.trim().split(/\s+/);
635
- const command = parts[0] || 'bash';
636
- const arg1 = parts[1] ? parts[1].substring(0, 20) : '';
637
- summary = result.success
638
- ? `✓ ${command} ${arg1}`
639
- : `✗ ${command}`;
640
- } else if (tool === 'readFile') {
641
- const filePath = input.filePath || '';
642
- const fileName = filePath.split('/').pop().substring(0, 20);
643
- summary = result.success
644
- ? `📖 ${fileName}`
645
- : `✗ ${fileName}`;
646
- } else if (tool === 'writeFile') {
647
- const filePath = input.filePath || '';
648
- const fileName = filePath.split('/').pop().substring(0, 20);
649
- summary = result.success
650
- ? `✍️ ${fileName}`
651
- : `✗ ${fileName}`;
652
- } else if (tool === 'editFile') {
653
- const filePath = input.filePath || '';
654
- const fileName = filePath.split('/').pop().substring(0, 20);
655
- summary = result.success
656
- ? `✏️ ${fileName}`
657
- : `✗ ${fileName}`;
658
- } else {
659
- summary = result.success
660
- ? `✓ ${tool}`
661
- : `✗ ${tool}`;
662
- }
663
-
664
- newExecs[newExecs.length - 1] = {
829
+ // 找到最后一个 running 状态的工具
830
+ const lastRunningIdx = newExecs.findLastIndex(e => e.status === 'running');
831
+ if (lastRunningIdx === -1) return prev;
832
+ const lastExec = newExecs[lastRunningIdx];
833
+
834
+ // 使用 generateToolSummary 生成双行显示
835
+ const { summary, detailInfo } = generateToolSummary(
836
+ lastExec.tool,
837
+ lastExec.input || {},
838
+ progress.result
839
+ );
840
+
841
+ newExecs[lastRunningIdx] = {
665
842
  ...lastExec,
666
843
  result: progress.result,
667
- summary: summary
844
+ status: progress.result.success ? 'success' : 'error',
845
+ duration: Date.now() - lastExec.startTime,
846
+ summary,
847
+ detailInfo
668
848
  };
669
849
  return newExecs;
670
850
  });
671
851
  }
672
852
  }
673
853
  );
674
-
675
- abortControllerRef.current = null;
854
+
855
+ // 检查是否是 abort 结果
856
+ if (response.aborted) {
857
+ thinkingUpdate.updateSmart(prev => [...prev, `❌ [${new Date().toLocaleTimeString()}] 对话已中止: ${response.abortReason}`], 'abort');
858
+ return;
859
+ }
860
+
861
+ // 使用API返回的准确token使用信息
862
+ if (response.usage) {
863
+ const { input_tokens = 0, output_tokens = 0 } = response.usage;
864
+ updateTokenStats(input_tokens, output_tokens);
865
+
866
+ // 在thinking中记录准确的token使用
867
+ thinkingUpdate.updateSmart(prev => [...prev, `📊 [${new Date().toLocaleTimeString()}] API Token使用: 输入=${input_tokens}, 输出=${output_tokens}`], 'thinking');
868
+ } else {
869
+ // 如果没有usage信息,使用估算
870
+ const aiTokens = Math.ceil(response.content.length / 4);
871
+ updateTokenStats(0, aiTokens);
872
+ }
676
873
 
677
874
  // 更新最后的消息为完整响应
678
- setMessages(prev => {
875
+ messagesUpdate.updateSmart(prev => {
679
876
  const lastIdx = prev.findIndex(m => m.role === 'assistant' && !m.complete);
680
877
  if (lastIdx >= 0) {
681
878
  return [
@@ -685,6 +882,7 @@ Type your message or command to get started.`
685
882
  content: response.content,
686
883
  complete: true,
687
884
  toolCalls: response.toolCalls,
885
+ usage: response.usage,
688
886
  key: Date.now()
689
887
  }
690
888
  ];
@@ -694,22 +892,23 @@ Type your message or command to get started.`
694
892
  content: response.content,
695
893
  complete: true,
696
894
  toolCalls: response.toolCalls,
895
+ usage: response.usage,
697
896
  key: Date.now()
698
897
  }];
699
898
  }
700
899
  });
701
900
 
702
901
  } catch (error) {
703
- setMessages(prev => [...prev, {
902
+ messagesUpdate.updateSmart(prev => [...prev, {
704
903
  role: 'error',
705
904
  content: `Error: ${error.message}`,
706
905
  key: Date.now()
707
- }]);
906
+ }], 'error');
708
907
  } finally {
709
908
  setIsProcessing(false);
710
- setActivity(null);
909
+ activityUpdate.updateImmediate(null);
711
910
  }
712
- }, [conversation, isProcessing]);
911
+ }, [conversation, isProcessing, updateTokenStats]);
713
912
 
714
913
  // 处理命令
715
914
  const handleCommand = async (cmd) => {
@@ -722,6 +921,14 @@ Type your message or command to get started.`
722
921
  setActivity('🗑️ 清除对话历史...');
723
922
  setMessages([]);
724
923
  conversation.clearHistory();
924
+ // 重置token统计
925
+ setTokenStats({
926
+ total: 0,
927
+ limit: tokenStats.limit, // 保持原有的限制
928
+ input: 0,
929
+ output: 0,
930
+ percentage: 0
931
+ });
725
932
  setActivity(null);
726
933
  break;
727
934
 
@@ -803,10 +1010,31 @@ Type your message or command to get started.`
803
1010
  /plan <task> - Create and execute a task plan
804
1011
  /learn - Learn project patterns
805
1012
  /status - Show conversation summary
1013
+ /history - Show input history statistics
806
1014
  /help - Show this help message`
807
1015
  }]);
808
1016
  break;
809
1017
 
1018
+ case '/history':
1019
+ setActivity('📊 获取历史记录统计...');
1020
+ const historyStats = inputHistory.getStats();
1021
+ const historyInfo = {
1022
+ role: 'system',
1023
+ content: `Input History Statistics:
1024
+ • Total entries: ${historyStats.total}
1025
+ • Current index: ${historyStats.currentIndex}
1026
+ • Search results: ${historyStats.searchResults}
1027
+ • History file: ~/.closer-code/closer-input-history
1028
+
1029
+ Tips:
1030
+ • Use ↑/↓ arrows to browse history
1031
+ • History is automatically saved
1032
+ • Duplicate entries are filtered`
1033
+ };
1034
+ setMessages(prev => [...prev, historyInfo]);
1035
+ setActivity(null);
1036
+ break;
1037
+
810
1038
  default:
811
1039
  setMessages(prev => [...prev, {
812
1040
  role: 'system',
@@ -815,6 +1043,26 @@ Type your message or command to get started.`
815
1043
  }
816
1044
  };
817
1045
 
1046
+ // 根据token使用情况确定颜色
1047
+ const getTokenColor = () => {
1048
+ const percentage = tokenStats.percentage;
1049
+ if (percentage < 70) return 'green';
1050
+ if (percentage < 90) return 'yellow';
1051
+ return 'red';
1052
+ };
1053
+
1054
+
1055
+ // 全屏模式:显示完整对话历史(包含工具详情)
1056
+ if (fullscreenMode) {
1057
+ return (
1058
+ <FullscreenConversation
1059
+ messages={messages}
1060
+ tokenStats={tokenStats}
1061
+ toolExecutions={toolExecutions}
1062
+ showTools={showToolsInFullscreen}
1063
+ />
1064
+ );
1065
+ }
818
1066
  if (!config) {
819
1067
  return (
820
1068
  <Box padding={1}>
@@ -824,7 +1072,7 @@ Type your message or command to get started.`
824
1072
  }
825
1073
 
826
1074
  return (
827
- <Box flexDirection="column" padding={1} height="100%">
1075
+ <Box flexDirection="column" padding={1}>
828
1076
  {/* 顶部状态栏 */}
829
1077
  <Box
830
1078
  borderStyle="bold"
@@ -846,10 +1094,13 @@ Type your message or command to get started.`
846
1094
 
847
1095
  {/* Thinking 区域 - 占17.5%高度 */}
848
1096
  <Box
849
- borderStyle="round"
1097
+ borderTop={true}
1098
+ borderBottom={true}
1099
+ borderLeft={false}
1100
+ borderRight={false}
1101
+ borderStyle="single"
850
1102
  borderColor="cyan"
851
1103
  flexDirection="column"
852
- flexGrow={17.5}
853
1104
  marginBottom={1}
854
1105
  width="100%"
855
1106
  >
@@ -859,7 +1110,7 @@ Type your message or command to get started.`
859
1110
  <Text dim color="gray"> [Tab: {thinkingEnabled ? 'ON ✅' : 'OFF ❌'}] [Shift+↑/↓: Scroll]</Text>
860
1111
  </Text>
861
1112
  </Box>
862
- <Box flexGrow={1} flexDirection="column" overflow="hidden" width="100%">
1113
+ <Box flexDirection="column" width="100%">
863
1114
  {thinkingLines.length > 0 ? (
864
1115
  <>
865
1116
  {/* 滚动提示 */}
@@ -880,30 +1131,37 @@ Type your message or command to get started.`
880
1131
  />
881
1132
  </>
882
1133
  ) : (
883
- <Box justifyContent="center" alignItems="center" height="100%">
1134
+ <Box justifyContent="center" alignItems="center">
884
1135
  <Text dim color="gray">No thinking messages yet</Text>
885
1136
  </Box>
886
1137
  )}
887
1138
  </Box>
888
1139
  </Box>
889
1140
 
890
- {/* 主内容区域 - 占剩余65% */}
891
- <Box flexGrow={65} flexDirection="row">
1141
+ {/* 主内容区域 - 工具详情面板打开时占50%,否则占65% */}
1142
+ <Box flexDirection="row">
892
1143
  <Box width="100%">
893
1144
  {/* 左侧:对话面板 - 占67%宽度 */}
894
1145
  <Box
895
- borderStyle="round"
896
1146
  borderColor="blue"
1147
+ borderTop={true}
1148
+ borderBottom={true}
1149
+ borderLeft={false}
1150
+ borderRight={false}
1151
+ borderStyle="single"
897
1152
  flexDirection="column"
898
1153
  flexGrow={67}
899
1154
  marginRight={1}
900
1155
  width="67%"
901
- height="100%"
1156
+ height="60%"
902
1157
  >
903
1158
  <Box borderBottom={false} borderColor="blue" paddingBottom={0} marginBottom={1}>
904
- <Text bold color="blue">💬 Conversation</Text>
1159
+ <Text bold color="blue">
1160
+ 💬 Conversation
1161
+ <Text color={getTokenColor()}> [Tokens: {tokenStats.total.toLocaleString()}/{tokenStats.limit.toLocaleString()}]</Text>
1162
+ </Text>
905
1163
  </Box>
906
- <Box flexDirection="column" overflow="hidden" width="100%" height="100%">
1164
+ <Box flexDirection="column" overflow="hidden" width="100%">
907
1165
  {messageLines.length > 0 ? (
908
1166
  <>
909
1167
  {/* 滚动提示 */}
@@ -924,7 +1182,7 @@ Type your message or command to get started.`
924
1182
  />
925
1183
  </>
926
1184
  ) : (
927
- <Box justifyContent="center" alignItems="center" height="100%">
1185
+ <Box justifyContent="center" alignItems="center" height="60%">
928
1186
  <Text dim>No messages yet</Text>
929
1187
  </Box>
930
1188
  )}
@@ -932,13 +1190,17 @@ Type your message or command to get started.`
932
1190
  </Box>
933
1191
 
934
1192
  {/* 右侧:任务和工具面板 - 占33%宽度 */}
935
- <Box flexDirection="column" flexGrow={33} width="33%" height="100%">
936
- {/* 任务进度 - 占50%高度 */}
1193
+ <Box flexDirection="column" flexGrow={33} width="33%" height="60%">
1194
+ {/* 任务进度 - 占46%高度(减少4个单位以减少2行高度) */}
937
1195
  <Box
938
- borderStyle="round"
1196
+ borderTop={true}
1197
+ borderBottom={true}
1198
+ borderLeft={false}
1199
+ borderRight={false}
1200
+ borderStyle="single"
939
1201
  borderColor="yellow"
940
1202
  flexDirection="column"
941
- flexGrow={50}
1203
+ flexGrow={46}
942
1204
  marginBottom={1}
943
1205
  width="100%"
944
1206
  >
@@ -952,12 +1214,16 @@ Type your message or command to get started.`
952
1214
  )}
953
1215
  </Box>
954
1216
 
955
- {/* 工具执行 - 占50%高度 */}
1217
+ {/* 工具执行 - 占54%高度(相应增加以保持平衡) */}
956
1218
  <Box
957
- borderStyle="round"
1219
+ borderTop={true}
1220
+ borderBottom={true}
1221
+ borderLeft={false}
1222
+ borderRight={false}
1223
+ borderStyle="single"
958
1224
  borderColor="green"
959
1225
  flexDirection="column"
960
- flexGrow={50}
1226
+ flexGrow={54}
961
1227
  width="100%"
962
1228
  >
963
1229
  <Box borderBottom={false} borderColor="green" paddingBottom={0} marginBottom={1}>
@@ -979,7 +1245,9 @@ Type your message or command to get started.`
979
1245
  {/* 活动提示 */}
980
1246
  {activity && (
981
1247
  <Box
982
- borderStyle="round"
1248
+ borderTop={true}
1249
+ borderBottom={true}
1250
+ borderStyle="single"
983
1251
  borderColor={abortMessage ? "red" : "yellow"}
984
1252
  paddingX={1}
985
1253
  marginTop={1}
@@ -992,7 +1260,9 @@ Type your message or command to get started.`
992
1260
  {/* 退出提示 */}
993
1261
  {showExitHint && (
994
1262
  <Box
995
- borderStyle="round"
1263
+ borderTop={true}
1264
+ borderBottom={true}
1265
+ borderStyle="single"
996
1266
  borderColor="red"
997
1267
  paddingX={1}
998
1268
  marginTop={1}
@@ -1002,26 +1272,44 @@ Type your message or command to get started.`
1002
1272
  </Box>
1003
1273
  )}
1004
1274
 
1275
+ {/* 工具详情面板 - 当打开时占用较大空间 */}
1276
+ {showToolDetail && (
1277
+ <ToolDetailPanel
1278
+ tools={toolExecutions}
1279
+ visible={showToolDetail}
1280
+ onClose={() => setShowToolDetail(false)}
1281
+ height={Math.floor(terminalSize.rows * 0.45)}
1282
+ selectedIndex={toolDetailIndex}
1283
+ />
1284
+ )}
1285
+
1005
1286
  {/* 输入区域 */}
1006
- <Box
1007
- borderStyle="double"
1008
- borderColor="cyan"
1009
- paddingX={1}
1010
- marginTop={activity ? 0 : 1}
1011
- >
1012
- <Box marginRight={1}>
1013
- <Text bold color="cyan">❯</Text>
1287
+ <Box flexDirection="column" marginTop={activity ? 0 : 1}>
1288
+ {/* 输入区域标记 */}
1289
+ <Box marginBottom={1} paddingLeft={1}>
1290
+ <Text dim color="cyan">
1291
+ {isProcessing ? '⏳ 处理中...' : '▶ 输入消息'}
1292
+ </Text>
1293
+ <Text dim color="gray">
1294
+ {' '}(Enter发送, Ctrl+Enter换行)
1295
+ </Text>
1296
+ </Box>
1297
+
1298
+ {/* 输入框 - 无边框,方便鼠标复制 */}
1299
+ <Box paddingLeft={1} paddingRight={1}>
1300
+ <EnhancedTextInputWithShortcuts
1301
+ value={input}
1302
+ onChange={(value) => {
1303
+ setInput(value);
1304
+ inputRef.current = value;
1305
+ }}
1306
+ onSubmit={handleSubmit}
1307
+ placeholder="在这里输入..."
1308
+ disabled={isProcessing}
1309
+ history={inputHistory}
1310
+ showHistoryIndicator={true}
1311
+ />
1014
1312
  </Box>
1015
- <TextInput
1016
- value={input}
1017
- onChange={(value) => {
1018
- setInput(value);
1019
- inputRef.current = value;
1020
- }}
1021
- onSubmit={handleSubmit}
1022
- placeholder="Type a message or /help for commands..."
1023
- disabled={isProcessing}
1024
- />
1025
1313
  </Box>
1026
1314
  </Box>
1027
1315
  );