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
@@ -0,0 +1,261 @@
1
+ /**
2
+ * 文件读取渲染器
3
+ *
4
+ * 支持工具:readFile, readFileLines, readFileTail
5
+ *
6
+ * 特点:
7
+ * - 显示文件路径和大小
8
+ * - 内容预览(带行号)
9
+ * - 行范围信息
10
+ */
11
+
12
+ import React from 'react';
13
+ import { Box, Text } from 'ink';
14
+ import { ProgressBar, Spinner } from '../progress-bar.jsx';
15
+
16
+ /**
17
+ * 解析文件读取工具的输入参数
18
+ */
19
+ function parseInput(input, toolName) {
20
+ if (!input) return { path: '' };
21
+
22
+ if (typeof input === 'string') {
23
+ return { path: input };
24
+ }
25
+
26
+ const path = input.path || input.filePath || input.file || '';
27
+
28
+ // readFileLines 特有参数
29
+ if (toolName === 'readFileLines') {
30
+ return {
31
+ path,
32
+ startLine: input.startLine || input.start || 1,
33
+ endLine: input.endLine || input.end
34
+ };
35
+ }
36
+
37
+ // readFileTail 特有参数
38
+ if (toolName === 'readFileTail') {
39
+ return {
40
+ path,
41
+ lines: input.lines || input.n || 10
42
+ };
43
+ }
44
+
45
+ return { path };
46
+ }
47
+
48
+ /**
49
+ * 解析文件读取工具的输出结果
50
+ */
51
+ function parseResult(result) {
52
+ if (!result) return null;
53
+
54
+ if (typeof result === 'string') {
55
+ return { content: result, success: true };
56
+ }
57
+
58
+ return {
59
+ content: result.content || result.text || result.data || '',
60
+ size: result.size || result.bytes,
61
+ lines: result.lines || result.lineCount,
62
+ success: result.success !== false,
63
+ error: result.error || result.message
64
+ };
65
+ }
66
+
67
+ /**
68
+ * 格式化文件大小
69
+ */
70
+ function formatSize(bytes) {
71
+ if (!bytes) return '';
72
+ if (bytes < 1024) return `${bytes} B`;
73
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
74
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
75
+ }
76
+
77
+ /**
78
+ * 从路径中提取文件名
79
+ */
80
+ function getFileName(path) {
81
+ if (!path) return '';
82
+ const parts = path.replace(/\\/g, '/').split('/');
83
+ return parts[parts.length - 1] || path;
84
+ }
85
+
86
+ /**
87
+ * 从路径中提取目录
88
+ */
89
+ function getDirectory(path) {
90
+ if (!path) return '';
91
+ const normalized = path.replace(/\\/g, '/');
92
+ const lastSlash = normalized.lastIndexOf('/');
93
+ return lastSlash > 0 ? normalized.substring(0, lastSlash + 1) : '';
94
+ }
95
+
96
+ /**
97
+ * 内容预览组件(带行号)
98
+ */
99
+ function ContentPreview({ content, maxLines = 6, startLine = 1 }) {
100
+ if (!content) {
101
+ return <Text dim italic>(无内容)</Text>;
102
+ }
103
+
104
+ const lines = String(content).split('\n');
105
+ const displayLines = lines.slice(0, maxLines);
106
+ const hasMore = lines.length > maxLines;
107
+
108
+ // 计算行号宽度
109
+ const maxLineNum = startLine + displayLines.length - 1;
110
+ const lineNumWidth = String(maxLineNum).length;
111
+
112
+ return (
113
+ <Box flexDirection="column">
114
+ {displayLines.map((line, i) => {
115
+ const lineNum = String(startLine + i).padStart(lineNumWidth, ' ');
116
+ const displayLine = line.length > 70 ? line.substring(0, 67) + '...' : line;
117
+
118
+ return (
119
+ <Box key={i}>
120
+ <Text dim>{lineNum} │ </Text>
121
+ <Text>{displayLine}</Text>
122
+ </Box>
123
+ );
124
+ })}
125
+ {hasMore && (
126
+ <Text dim italic>... 还有 {lines.length - maxLines} 行</Text>
127
+ )}
128
+ </Box>
129
+ );
130
+ }
131
+
132
+ /**
133
+ * 文件读取渲染器组件
134
+ *
135
+ * @param {Object} props
136
+ * @param {Object} props.tool - 工具执行数据
137
+ * @param {number} props.maxHeight - 最大高度
138
+ */
139
+ export function FileReadRenderer({ tool, maxHeight = 10 }) {
140
+ const { input, result, status, duration, startTime } = tool;
141
+ const toolName = tool.tool || 'readFile';
142
+
143
+ const parsedInput = parseInput(input, toolName);
144
+ const parsedResult = parseResult(result);
145
+
146
+ const isRunning = status === 'running';
147
+ const isSuccess = status === 'success' || parsedResult?.success;
148
+ const isError = status === 'error' || parsedResult?.success === false;
149
+
150
+ const fileName = getFileName(parsedInput.path);
151
+ const directory = getDirectory(parsedInput.path);
152
+
153
+ // 计算内容预览可用行数
154
+ const previewMaxLines = Math.max(3, maxHeight - 6);
155
+
156
+ // 工具图标
157
+ const toolIcons = {
158
+ readFile: '📖',
159
+ readFileLines: '📄',
160
+ readFileTail: '📜'
161
+ };
162
+ const icon = toolIcons[toolName] || '📖';
163
+
164
+ return (
165
+ <Box flexDirection="column">
166
+ {/* 文件信息 */}
167
+ <Box>
168
+ <Text>{icon} </Text>
169
+ <Text bold color="cyan">{fileName}</Text>
170
+ {parsedResult?.size && (
171
+ <Text dim> ({formatSize(parsedResult.size)})</Text>
172
+ )}
173
+ </Box>
174
+
175
+ {/* 目录路径 */}
176
+ {directory && (
177
+ <Box>
178
+ <Text dim>📂 {directory}</Text>
179
+ </Box>
180
+ )}
181
+
182
+ {/* 行范围信息 */}
183
+ {toolName === 'readFileLines' && parsedInput.startLine && (
184
+ <Box>
185
+ <Text dim>📍 行 {parsedInput.startLine}</Text>
186
+ {parsedInput.endLine && <Text dim> - {parsedInput.endLine}</Text>}
187
+ {parsedResult?.lines && <Text dim> / 共 {parsedResult.lines} 行</Text>}
188
+ </Box>
189
+ )}
190
+
191
+ {toolName === 'readFileTail' && (
192
+ <Box>
193
+ <Text dim>📍 最后 {parsedInput.lines} 行</Text>
194
+ </Box>
195
+ )}
196
+
197
+ {/* 分隔线 */}
198
+ <Text dim>{'─'.repeat(60)}</Text>
199
+
200
+ {/* 执行中状态 */}
201
+ {isRunning && (
202
+ <Box flexDirection="column">
203
+ <Box>
204
+ <Spinner color="cyan" />
205
+ <Text color="cyan"> 读取中...</Text>
206
+ {startTime && (
207
+ <Text dim> ({((Date.now() - startTime) / 1000).toFixed(1)}s)</Text>
208
+ )}
209
+ </Box>
210
+ <Box marginTop={1}>
211
+ <ProgressBar width={40} color="cyan" label="读取中..." />
212
+ </Box>
213
+ </Box>
214
+ )}
215
+
216
+ {/* 完成状态 - 显示内容预览 */}
217
+ {!isRunning && isSuccess && parsedResult && (
218
+ <Box flexDirection="column">
219
+ <Text bold color="cyan">📝 内容预览</Text>
220
+ <Box
221
+ borderStyle="single"
222
+ borderColor="gray"
223
+ paddingX={1}
224
+ >
225
+ <ContentPreview
226
+ content={parsedResult.content}
227
+ maxLines={previewMaxLines}
228
+ startLine={parsedInput.startLine || 1}
229
+ />
230
+ </Box>
231
+ </Box>
232
+ )}
233
+
234
+ {/* 错误状态 */}
235
+ {!isRunning && isError && (
236
+ <Box flexDirection="column">
237
+ <Text color="red">❌ 读取失败</Text>
238
+ {parsedResult?.error && (
239
+ <Text color="red" dim>{parsedResult.error}</Text>
240
+ )}
241
+ </Box>
242
+ )}
243
+
244
+ {/* 底部状态栏 */}
245
+ {!isRunning && (
246
+ <Box marginTop={1}>
247
+ {isSuccess ? (
248
+ <Text color="green">✓ 读取完成</Text>
249
+ ) : (
250
+ <Text color="red">✗ 读取失败</Text>
251
+ )}
252
+ {duration && (
253
+ <Text dim> | {duration}ms</Text>
254
+ )}
255
+ </Box>
256
+ )}
257
+ </Box>
258
+ );
259
+ }
260
+
261
+ export default FileReadRenderer;
@@ -0,0 +1,222 @@
1
+ /**
2
+ * 文件写入渲染器
3
+ *
4
+ * 支持工具:writeFile
5
+ *
6
+ * 特点:
7
+ * - 显示文件路径
8
+ * - 写入大小
9
+ * - 新建/覆盖标识
10
+ */
11
+
12
+ import React from 'react';
13
+ import { Box, Text } from 'ink';
14
+ import { ProgressBar, Spinner } from '../progress-bar.jsx';
15
+
16
+ /**
17
+ * 解析文件写入工具的输入参数
18
+ */
19
+ function parseInput(input) {
20
+ if (!input) return { path: '', content: '' };
21
+
22
+ if (typeof input === 'string') {
23
+ return { path: input, content: '' };
24
+ }
25
+
26
+ return {
27
+ path: input.path || input.filePath || input.file || '',
28
+ content: input.content || input.text || input.data || ''
29
+ };
30
+ }
31
+
32
+ /**
33
+ * 解析文件写入工具的输出结果
34
+ */
35
+ function parseResult(result) {
36
+ if (!result) return null;
37
+
38
+ if (typeof result === 'string') {
39
+ return { success: true, message: result };
40
+ }
41
+
42
+ return {
43
+ success: result.success !== false,
44
+ created: result.created || result.isNew,
45
+ size: result.size || result.bytes,
46
+ error: result.error || result.message
47
+ };
48
+ }
49
+
50
+ /**
51
+ * 格式化文件大小
52
+ */
53
+ function formatSize(bytes) {
54
+ if (!bytes && bytes !== 0) return '';
55
+ if (bytes < 1024) return `${bytes} B`;
56
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
57
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
58
+ }
59
+
60
+ /**
61
+ * 从路径中提取文件名
62
+ */
63
+ function getFileName(path) {
64
+ if (!path) return '';
65
+ const parts = path.replace(/\\/g, '/').split('/');
66
+ return parts[parts.length - 1] || path;
67
+ }
68
+
69
+ /**
70
+ * 从路径中提取目录
71
+ */
72
+ function getDirectory(path) {
73
+ if (!path) return '';
74
+ const normalized = path.replace(/\\/g, '/');
75
+ const lastSlash = normalized.lastIndexOf('/');
76
+ return lastSlash > 0 ? normalized.substring(0, lastSlash + 1) : '';
77
+ }
78
+
79
+ /**
80
+ * 内容预览组件
81
+ */
82
+ function ContentPreview({ content, maxLines = 4 }) {
83
+ if (!content) {
84
+ return <Text dim italic>(无内容)</Text>;
85
+ }
86
+
87
+ const lines = String(content).split('\n');
88
+ const displayLines = lines.slice(0, maxLines);
89
+ const hasMore = lines.length > maxLines;
90
+
91
+ return (
92
+ <Box flexDirection="column">
93
+ {displayLines.map((line, i) => {
94
+ const displayLine = line.length > 60 ? line.substring(0, 57) + '...' : line;
95
+ return (
96
+ <Text key={i} dim>{displayLine}</Text>
97
+ );
98
+ })}
99
+ {hasMore && (
100
+ <Text dim italic>... 还有 {lines.length - maxLines} 行</Text>
101
+ )}
102
+ </Box>
103
+ );
104
+ }
105
+
106
+ /**
107
+ * 文件写入渲染器组件
108
+ *
109
+ * @param {Object} props
110
+ * @param {Object} props.tool - 工具执行数据
111
+ * @param {number} props.maxHeight - 最大高度
112
+ */
113
+ export function FileWriteRenderer({ tool, maxHeight = 10 }) {
114
+ const { input, result, status, duration, startTime } = tool;
115
+
116
+ const parsedInput = parseInput(input);
117
+ const parsedResult = parseResult(result);
118
+
119
+ const isRunning = status === 'running';
120
+ const isSuccess = status === 'success' || parsedResult?.success;
121
+ const isError = status === 'error' || parsedResult?.success === false;
122
+
123
+ const fileName = getFileName(parsedInput.path);
124
+ const directory = getDirectory(parsedInput.path);
125
+
126
+ // 计算写入大小
127
+ const contentSize = parsedInput.content ?
128
+ new TextEncoder().encode(parsedInput.content).length : 0;
129
+ const displaySize = parsedResult?.size || contentSize;
130
+
131
+ return (
132
+ <Box flexDirection="column">
133
+ {/* 文件信息 */}
134
+ <Box>
135
+ <Text>✍️ </Text>
136
+ <Text bold color="magenta">{fileName}</Text>
137
+ {parsedResult?.created && (
138
+ <Text color="green"> ✨ 新建</Text>
139
+ )}
140
+ {!parsedResult?.created && isSuccess && (
141
+ <Text color="yellow"> 📝 覆盖</Text>
142
+ )}
143
+ </Box>
144
+
145
+ {/* 目录路径 */}
146
+ {directory && (
147
+ <Box>
148
+ <Text dim>📂 {directory}</Text>
149
+ </Box>
150
+ )}
151
+
152
+ {/* 写入大小 */}
153
+ {displaySize > 0 && (
154
+ <Box>
155
+ <Text dim>📏 写入 {formatSize(displaySize)}</Text>
156
+ </Box>
157
+ )}
158
+
159
+ {/* 分隔线 */}
160
+ <Text dim>{'─'.repeat(60)}</Text>
161
+
162
+ {/* 执行中状态 */}
163
+ {isRunning && (
164
+ <Box flexDirection="column">
165
+ <Box>
166
+ <Spinner color="magenta" />
167
+ <Text color="magenta"> 写入中...</Text>
168
+ {startTime && (
169
+ <Text dim> ({((Date.now() - startTime) / 1000).toFixed(1)}s)</Text>
170
+ )}
171
+ </Box>
172
+ <Box marginTop={1}>
173
+ <ProgressBar width={40} color="magenta" label="写入中..." />
174
+ </Box>
175
+ </Box>
176
+ )}
177
+
178
+ {/* 完成状态 - 显示内容预览 */}
179
+ {!isRunning && isSuccess && (
180
+ <Box flexDirection="column">
181
+ <Text bold color="cyan">📝 写入内容预览</Text>
182
+ <Box
183
+ borderStyle="single"
184
+ borderColor="gray"
185
+ paddingX={1}
186
+ >
187
+ <ContentPreview
188
+ content={parsedInput.content}
189
+ maxLines={Math.max(3, maxHeight - 7)}
190
+ />
191
+ </Box>
192
+ </Box>
193
+ )}
194
+
195
+ {/* 错误状态 */}
196
+ {!isRunning && isError && (
197
+ <Box flexDirection="column">
198
+ <Text color="red">❌ 写入失败</Text>
199
+ {parsedResult?.error && (
200
+ <Text color="red" dim>{parsedResult.error}</Text>
201
+ )}
202
+ </Box>
203
+ )}
204
+
205
+ {/* 底部状态栏 */}
206
+ {!isRunning && (
207
+ <Box marginTop={1}>
208
+ {isSuccess ? (
209
+ <Text color="green">✓ 写入完成</Text>
210
+ ) : (
211
+ <Text color="red">✗ 写入失败</Text>
212
+ )}
213
+ {duration && (
214
+ <Text dim> | {duration}ms</Text>
215
+ )}
216
+ </Box>
217
+ )}
218
+ </Box>
219
+ );
220
+ }
221
+
222
+ export default FileWriteRenderer;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * 工具渲染器统一导出
3
+ *
4
+ * 根据工具名称选择对应的渲染器
5
+ */
6
+
7
+ import React from 'react';
8
+ import { Box, Text } from 'ink';
9
+
10
+ import { BashRenderer } from './bash-renderer.jsx';
11
+ import { FileReadRenderer } from './file-read-renderer.jsx';
12
+ import { FileWriteRenderer } from './file-write-renderer.jsx';
13
+ import { FileEditRenderer } from './file-edit-renderer.jsx';
14
+ import { SearchRenderer } from './search-renderer.jsx';
15
+ import { ListRenderer } from './list-renderer.jsx';
16
+
17
+ /**
18
+ * 工具名称到渲染器的映射
19
+ */
20
+ const TOOL_RENDERER_MAP = {
21
+ // Bash 命令
22
+ bash: BashRenderer,
23
+
24
+ // 文件读取
25
+ readFile: FileReadRenderer,
26
+ readFileLines: FileReadRenderer,
27
+ readFileTail: FileReadRenderer,
28
+
29
+ // 文件写入
30
+ writeFile: FileWriteRenderer,
31
+
32
+ // 文件编辑
33
+ editFile: FileEditRenderer,
34
+ regionConstrainedEdit: FileEditRenderer,
35
+
36
+ // 搜索
37
+ searchFiles: SearchRenderer,
38
+ searchCode: SearchRenderer,
39
+
40
+ // 目录列表
41
+ listFiles: ListRenderer
42
+ };
43
+
44
+ /**
45
+ * 默认渲染器(用于未知工具)
46
+ */
47
+ function DefaultRenderer({ tool, maxHeight = 10, renderMode = 'split' }) {
48
+ if ( tool === 'executed' ) { // fix by joyer handle coded
49
+ return (
50
+ <Box>
51
+ </Box>
52
+ );
53
+ }
54
+
55
+ const { input, result, status, duration } = tool;
56
+
57
+ // 全屏模式:简化显示,不显示详细UI
58
+ if (renderMode === 'fullscreen') {
59
+ return (
60
+ <Box>
61
+ <Text dim>[{tool.tool}]</Text>
62
+ </Box>
63
+ );
64
+ }
65
+
66
+
67
+ // 分屏模式:完整显示
68
+ const statusConfig = {
69
+ pending: { icon: '⏳', color: 'gray', label: 'x等待x' },
70
+ running: { icon: '⚡', color: 'yellow', label: '执行中' },
71
+ success: { icon: '✓', color: 'green', label: '成功' },
72
+ error: { icon: '✗', color: 'red', label: '失败' }
73
+ };
74
+
75
+ const { icon, color, label } = statusConfig[status] || statusConfig.pending;
76
+
77
+ return (
78
+ <Box flexDirection="column">
79
+ {/* 工具信息 */}
80
+ <Box>
81
+ <Text color={color}>{icon} </Text>
82
+ <Text bold>tool name:{tool.tool}</Text>
83
+ <Text dim> | </Text>
84
+ <Text color={color}>{label}</Text>
85
+ {duration && <Text dim> | {duration}ms</Text>}
86
+ </Box>
87
+
88
+ {/* 分隔线 */}
89
+ <Text dim>{'─'.repeat(60)}</Text>
90
+
91
+ {/* 输入参数 */}
92
+ <Box flexDirection="column" marginTop={1}>
93
+ <Text bold color="cyan">📥 输入</Text>
94
+ <Box borderStyle="single" borderColor="gray" paddingX={1}>
95
+ <Text>{formatJson(input, maxHeight - 6)}</Text>
96
+ </Box>
97
+ </Box>
98
+
99
+ {/* 输出结果 */}
100
+ {result && (
101
+ <Box flexDirection="column" marginTop={1}>
102
+ <Text bold color={result?.success !== false ? 'green' : 'red'}>
103
+ 📤 输出 {result?.success !== false ? '✓' : '✗'}
104
+ </Text>
105
+ <Box borderStyle="single" borderColor="gray" paddingX={1}>
106
+ <Text>{formatJson(result, maxHeight - 6)}</Text>
107
+ </Box>
108
+ </Box>
109
+ )}
110
+ </Box>
111
+ );
112
+ }
113
+
114
+ /**
115
+ * 格式化 JSON 为字符串
116
+ */
117
+ function formatJson(data, maxLines = 5) {
118
+ if (data === null || data === undefined) {
119
+ return 'null';
120
+ }
121
+
122
+ try {
123
+ const str = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
124
+ const lines = str.split('\n');
125
+
126
+ if (lines.length <= maxLines) {
127
+ return str;
128
+ }
129
+
130
+ return lines.slice(0, maxLines).join('\n') + `\n... 还有 ${lines.length - maxLines} 行`;
131
+ } catch {
132
+ return String(data);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * 获取工具对应的渲染器
138
+ *
139
+ * @param {string} toolName - 工具名称
140
+ * @returns {React.Component} 渲染器组件
141
+ */
142
+ export function getToolRenderer(toolName) {
143
+ return TOOL_RENDERER_MAP[toolName] || DefaultRenderer;
144
+ }
145
+
146
+ /**
147
+ * 工具渲染器包装组件
148
+ *
149
+ * 自动根据工具名称选择对应的渲染器
150
+ *
151
+ * @param {Object} props
152
+ * @param {Object} props.tool - 工具执行数据
153
+ * @param {number} props.maxHeight - 最大高度
154
+ * @param {string} props.renderMode - 渲染模式 ('split' | 'fullscreen')
155
+ */
156
+ export function ToolRenderer({ tool, maxHeight = 10, renderMode = 'split' }) {
157
+ if (!tool) {
158
+ return (
159
+ <Box justifyContent="center" alignItems="center">
160
+ <Text dim>暂无工具数据</Text>
161
+ </Box>
162
+ );
163
+ }
164
+
165
+ const Renderer = getToolRenderer(tool.tool);
166
+
167
+ return <Renderer tool={tool} maxHeight={maxHeight} renderMode={renderMode} />;
168
+ }
169
+
170
+ // 导出所有渲染器
171
+ export { BashRenderer } from './bash-renderer.jsx';
172
+ export { FileReadRenderer } from './file-read-renderer.jsx';
173
+ export { FileWriteRenderer } from './file-write-renderer.jsx';
174
+ export { FileEditRenderer } from './file-edit-renderer.jsx';
175
+ export { SearchRenderer } from './search-renderer.jsx';
176
+ export { ListRenderer } from './list-renderer.jsx';
177
+
178
+ export default ToolRenderer;