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.
- package/.closer-code.example.json +32 -0
- package/DUAL_OPTIMIZATION_COMPLETE.md +293 -0
- package/README.md +167 -557
- package/README_OPENAI.md +163 -0
- package/THINKING_THROTTLING_OPTIMIZATION.md +244 -0
- package/THROTTLING_1_5S_OPTIMIZATION.md +401 -0
- package/TOOLS_IMPROVEMENTS_SUMMARY.md +273 -0
- package/cloco.md +5 -1
- package/config.example.json +15 -94
- package/config.mcp.example.json +81 -0
- package/dist/bash-runner.js +5 -126
- package/dist/batch-cli.js +286 -20658
- package/dist/closer-cli.js +329 -21135
- package/dist/index.js +308 -31036
- package/docs/ANTHROPIC_TOOL_ERROR_HANDLING.md +220 -0
- package/docs/BUILD_COMMANDS.md +79 -0
- package/docs/CTRL_Z_SUPPORT.md +189 -0
- package/docs/DEEPSEEK_R1_INTEGRATION.md +427 -0
- package/docs/FIX_OPENAI_TOOL_ERROR_HANDLING.md +375 -0
- package/docs/FIX_OPENAI_TOOL_RESULT.md +198 -0
- package/docs/INPUT_ENHANCEMENTS.md +192 -0
- package/docs/MCP_IMPLEMENTATION_SUMMARY.md +428 -0
- package/docs/MCP_INTEGRATION.md +418 -0
- package/docs/MCP_QUICKSTART.md +299 -0
- package/docs/MCP_README.md +166 -0
- package/docs/MINIFY_BUILD.md +180 -0
- package/docs/MULTILINE_INPUT_FEATURE.md +119 -0
- package/docs/OPENAI_CLIENT.md +258 -0
- package/docs/PROJECT_LOCAL_CONFIG.md +471 -0
- package/docs/PROJECT_LOCAL_CONFIG_SUMMARY.md +407 -0
- package/docs/REFACTOR_CONVERSATION.md +306 -0
- package/docs/REGION_EDIT_DESIGN.md +475 -0
- package/docs/SIGNAL_HANDLING.md +171 -0
- package/docs/STREAM_UPDATE_THROTTLE.md +273 -0
- package/docs/TOOLS_REFACTOR_PLAN.md +520 -0
- package/ds_r1.md +249 -0
- package/examples/abort-fence-example.js +294 -0
- package/package.json +18 -4
- package/src/ai-client-legacy.js +6 -1
- package/src/ai-client-openai.js +672 -0
- package/src/ai-client.js +30 -13
- package/src/closer-cli.jsx +450 -162
- package/src/components/fullscreen-conversation.jsx +157 -0
- package/src/components/ink-text-input/index.jsx +324 -0
- package/src/components/multiline-text-input.jsx +614 -0
- package/src/components/progress-bar.jsx +135 -0
- package/src/components/tool-detail-view.jsx +82 -0
- package/src/components/tool-renderers/bash-renderer.jsx +197 -0
- package/src/components/tool-renderers/file-edit-renderer.jsx +247 -0
- package/src/components/tool-renderers/file-read-renderer.jsx +261 -0
- package/src/components/tool-renderers/file-write-renderer.jsx +222 -0
- package/src/components/tool-renderers/index.jsx +178 -0
- package/src/components/tool-renderers/list-renderer.jsx +274 -0
- package/src/components/tool-renderers/search-renderer.jsx +248 -0
- package/src/config.js +182 -20
- package/src/conversation/abort-fence.js +158 -0
- package/src/conversation/core.js +377 -0
- package/src/conversation/index.js +33 -0
- package/src/conversation/mcp-integration.js +96 -0
- package/src/conversation/plan-manager.js +295 -0
- package/src/conversation/stream-handler.js +154 -0
- package/src/conversation/tool-executor.js +264 -0
- package/src/conversation.js +23 -958
- package/src/hooks/use-throttled-state.js +158 -0
- package/src/input/enhanced-input.jsx +268 -0
- package/src/input/history.js +342 -0
- package/src/logger.js +20 -0
- package/src/mcp/client.js +275 -0
- package/src/mcp/tools-adapter.js +149 -0
- package/src/planner.js +18 -5
- package/src/prompt-builder.js +159 -0
- package/src/tools.js +457 -25
- package/src/utils/json-parser.js +231 -0
- package/src/utils/json-repair.js +146 -0
- package/src/utils/platform.js +259 -0
- package/test/test-ctrl-bf.js +121 -0
- package/test/test-deepseek-reasoning.js +118 -0
- package/test/test-history-navigation.js +80 -0
- package/test/test-input-fix.js +105 -0
- package/test/test-input-history.js +98 -0
- package/test/test-mcp.js +115 -0
- package/test/test-openai-client.js +152 -0
- package/test/test-openai-tool-result.js +199 -0
- package/test/test-project-config.js +106 -0
- package/test/test-shortcuts.js +79 -0
- package/test/test-stream-throttle.js +124 -0
- package/test/test-tool-error-handling.js +95 -0
- package/test/verify-input-fix.sh +35 -0
- package/test-abort-fence.js +263 -0
- package/test-abort-fix.js +54 -0
- package/test-abort-new-conversation.js +75 -0
- package/test-ctrl-z.js +54 -0
- package/test-file-read.js +105 -0
- package/test-tool-display.js +127 -0
- package/src/closer-cli.jsx.backup +0 -948
- package/test/workflows/longtalk/cloco.md +0 -19
- package/test/workflows/longtalk/emoji_500.txt +0 -63
- package/test/workflows/longtalk/emoji_list.txt +0 -20
- package/test-ctrl-c.jsx +0 -126
package/src/closer-cli.jsx
CHANGED
|
@@ -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
|
-
|
|
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) -
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 + 方向键 -
|
|
536
|
+
// Shift + 方向键 - 工具详情面板打开时切换工具,否则滚动Thinking区域
|
|
434
537
|
else if (key.shift && key.upArrow) {
|
|
435
|
-
|
|
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
|
-
|
|
438
|
-
|
|
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)
|
|
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
|
-
|
|
518
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
592
|
-
|
|
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
|
-
|
|
596
|
-
|
|
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
|
-
|
|
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
|
-
|
|
603
|
-
|
|
801
|
+
activityUpdate.updateImmediate(`⚡ 执行工具: ${progress.tool}...`);
|
|
802
|
+
thinkingUpdate.updateSmart(prev => [...prev, thinkingMsg], 'tool_start');
|
|
604
803
|
// 存储工具信息,包括 input 和 result 引用(用于生成摘要)
|
|
605
|
-
|
|
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
|
-
|
|
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
|
-
|
|
618
|
-
|
|
823
|
+
activityUpdate.updateImmediate('📊 处理工具结果...');
|
|
824
|
+
thinkingUpdate.updateSmart(prev => [...prev, `📊 [${new Date().toLocaleTimeString()}] 工具执行结果: ${resultMsg}`], 'tool_complete');
|
|
619
825
|
// 更新工具执行结果,并生成摘要
|
|
620
|
-
|
|
826
|
+
toolExecutionsUpdate.updateSmart(prev => {
|
|
621
827
|
if (prev.length === 0) return prev;
|
|
622
828
|
const newExecs = [...prev];
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
|
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"
|
|
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
|
-
{/* 主内容区域 -
|
|
891
|
-
<Box
|
|
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="
|
|
1156
|
+
height="60%"
|
|
902
1157
|
>
|
|
903
1158
|
<Box borderBottom={false} borderColor="blue" paddingBottom={0} marginBottom={1}>
|
|
904
|
-
<Text bold color="blue"
|
|
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%"
|
|
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="
|
|
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="
|
|
936
|
-
{/* 任务进度 - 占
|
|
1193
|
+
<Box flexDirection="column" flexGrow={33} width="33%" height="60%">
|
|
1194
|
+
{/* 任务进度 - 占46%高度(减少4个单位以减少2行高度) */}
|
|
937
1195
|
<Box
|
|
938
|
-
|
|
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={
|
|
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
|
-
{/* 工具执行 - 占
|
|
1217
|
+
{/* 工具执行 - 占54%高度(相应增加以保持平衡) */}
|
|
956
1218
|
<Box
|
|
957
|
-
|
|
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={
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
);
|