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,157 @@
1
+ /**
2
+ * 全屏对话组件
3
+ *
4
+ * 特点:
5
+ * - 占满整个屏幕
6
+ * - 不限制高度,自然滚动
7
+ * - 方便查看完整历史和复制内容
8
+ * - 使用终端自带滚动
9
+ * - 工具详情与对话混排显示
10
+ */
11
+
12
+ import React from 'react';
13
+ import { Box, Text } from 'ink';
14
+ import { ToolRenderer } from './tool-renderers/index.jsx';
15
+
16
+ /**
17
+ * 工具执行卡片组件(全屏模式专用 - 简洁版)
18
+ */
19
+ function ToolExecutionCard({ tool }) {
20
+ const statusConfig = {
21
+ pending: { icon: '⏳', color: 'gray' },
22
+ running: { icon: '⚡', color: 'yellow' },
23
+ success: { icon: '✓', color: 'green' },
24
+ error: { icon: '✗', color: 'red' }
25
+ };
26
+
27
+ const status = tool.status || 'pending';
28
+ const { icon, color } = statusConfig[status];
29
+
30
+ return (
31
+ <Box flexDirection="column" marginY={1} width="100%">
32
+ {/* 工具标题 - 简洁样式 */}
33
+ <Box>
34
+ <Text color={color}>{icon} </Text>
35
+ <Text color="gray">🔧 </Text>
36
+ <Text bold>{tool.tool}</Text>
37
+ {tool.duration && <Text dim> ({tool.duration}ms)</Text>}
38
+ </Box>
39
+
40
+ {/* 工具详情 */}
41
+ <Box paddingLeft={2}>
42
+ <ToolRenderer tool={tool} maxHeight={20} />
43
+ </Box>
44
+ </Box>
45
+ );
46
+ }
47
+
48
+ /**
49
+ * 全屏对话组件(使用 React.memo 优化渲染性能)
50
+ */
51
+ const FullscreenConversation = React.memo(function FullscreenConversation({ messages, tokenStats, toolExecutions = [], showTools = true }) {
52
+
53
+ // 计算token使用颜色
54
+ const getTokenColor = () => {
55
+ const percentage = tokenStats.percentage || 0;
56
+ if (percentage < 70) return 'green';
57
+ if (percentage < 90) return 'yellow';
58
+ return 'red';
59
+ };
60
+
61
+ // 将消息和工具执行按时间顺序混合
62
+ const getMixedContent = () => {
63
+ const content = [];
64
+
65
+ // 添加消息
66
+ messages.forEach((message, index) => {
67
+ content.push({
68
+ type: 'message',
69
+ data: message,
70
+ key: message.key || `msg-${index}`,
71
+ timestamp: message.timestamp || index
72
+ });
73
+ });
74
+
75
+ // 添加工具执行(仅在 showTools 为 true 时)
76
+ if (showTools) {
77
+ toolExecutions.forEach((tool, index) => {
78
+ content.push({
79
+ type: 'tool',
80
+ data: tool,
81
+ key: tool.id || `tool-${index}`,
82
+ timestamp: tool.id || Date.now() + index
83
+ });
84
+ });
85
+ }
86
+
87
+ // 按时间戳排序(保持原有顺序,工具执行插入到对应位置)
88
+ // 由于消息没有精确时间戳,我们保持消息在前,工具在后的顺序
89
+ // 实际上工具执行是在 AI 响应过程中发生的,所以放在消息之后
90
+ return content;
91
+ };
92
+
93
+ const mixedContent = getMixedContent();
94
+
95
+ return (
96
+ <Box flexDirection="column" height="100%" paddingX={1}>
97
+ {/* 顶部状态栏 - 简洁无边框 */}
98
+ <Box paddingX={1} marginBottom={1} justifyContent="space-between">
99
+ <Text dim>📺 全屏模式 (Ctrl+G 退出)</Text>
100
+ <Text color={getTokenColor()} dim>
101
+ {tokenStats.total?.toLocaleString() || 0}/{tokenStats.limit?.toLocaleString() || 4096}
102
+ </Text>
103
+ </Box>
104
+
105
+ {/* 混合内容列表 - 不限制高度 */}
106
+ <Box flexDirection="column" flexGrow={1}>
107
+ {mixedContent.length === 0 ? (
108
+ <Box justifyContent="center" alignItems="center">
109
+ <Text dim>暂无消息</Text>
110
+ </Box>
111
+ ) : (
112
+ mixedContent.map((item, index) => {
113
+ if (item.type === 'message') {
114
+ const message = item.data;
115
+ const isUser = message.role === 'user';
116
+ const isSystem = message.role === 'system';
117
+ const isError = message.role === 'error';
118
+
119
+ const color = isUser ? 'cyan' : isError ? 'red' : isSystem ? 'yellow' : 'white';
120
+
121
+ return (
122
+ <Box key={item.key} flexDirection="column" marginBottom={1} width="100%">
123
+ <Box width="100%">
124
+ <Text bold color={color}>
125
+ {isUser ? '👤 用户' : isError ? '❌ 错误' : isSystem ? 'ℹ️ 系统' : '🤖 助手'}
126
+ </Text>
127
+ </Box>
128
+ <Box width="100%">
129
+ <Text color={color}>
130
+ {typeof message.content === 'string'
131
+ ? message.content
132
+ : JSON.stringify(message.content, null, 2)}
133
+ </Text>
134
+ </Box>
135
+ {/* 简洁分隔 */}
136
+ <Box marginTop={1} />
137
+ </Box>
138
+ );
139
+ } else if (item.type === 'tool') {
140
+ return (
141
+ <ToolExecutionCard key={item.key} tool={item.data} />
142
+ );
143
+ }
144
+ return null;
145
+ })
146
+ )}
147
+ </Box>
148
+
149
+ {/* 底部提示 - 简洁无边框 */}
150
+ <Box paddingX={1} marginTop={1}>
151
+ <Text dim>💡 鼠标选择复制 | 滚轮滚动 | Ctrl+G 返回 | Ctrl+T {showTools ? '隐藏' : '显示'}工具</Text>
152
+ </Box>
153
+ </Box>
154
+ );
155
+ });
156
+
157
+ export default FullscreenConversation;
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Enhanced TextInput Component
3
+ *
4
+ * Forked from ink-text-input with the following enhancements:
5
+ * - Added cursorPosition prop for external cursor control
6
+ * - Added onCursorChange callback to notify cursor position changes
7
+ * - Implemented Ctrl+A (jump to start)
8
+ * - Implemented Ctrl+E (jump to end)
9
+ * - Implemented Ctrl+B (move back one character)
10
+ * - Implemented Ctrl+F (move forward one character)
11
+ * - Implemented Ctrl+U (delete to start)
12
+ * - Implemented Ctrl+K (delete to end)
13
+ * - Implemented Ctrl+W (delete previous word)
14
+ */
15
+
16
+ import React, { useState, useEffect } from 'react';
17
+ import { Text, useInput } from 'ink';
18
+ import chalk from 'chalk';
19
+ import { supportsJobControl } from '../../utils/platform.js';
20
+
21
+ export function EnhancedTextInput({
22
+ value: originalValue = '',
23
+ placeholder = '',
24
+ focus = true,
25
+ mask,
26
+ highlightPastedText = false,
27
+ showCursor = true,
28
+ cursorPosition: externalCursorPosition,
29
+ onCursorChange,
30
+ onChange,
31
+ onSubmit,
32
+ onEnterMultiline,
33
+ enableShortcuts = true
34
+ }) {
35
+ // 初始化光标位置
36
+ const getInitialCursorOffset = () => {
37
+ if (externalCursorPosition !== undefined) {
38
+ return externalCursorPosition;
39
+ }
40
+ return (originalValue || '').length;
41
+ };
42
+
43
+ const [state, setState] = useState({
44
+ cursorOffset: getInitialCursorOffset(),
45
+ cursorWidth: 0
46
+ });
47
+
48
+ const { cursorOffset, cursorWidth } = state;
49
+
50
+ // 同步外部 cursorPosition 变化
51
+ useEffect(() => {
52
+ if (externalCursorPosition !== undefined && externalCursorPosition !== cursorOffset) {
53
+ setState({
54
+ cursorOffset: externalCursorPosition,
55
+ cursorWidth: 0
56
+ });
57
+ }
58
+ }, [externalCursorPosition]);
59
+
60
+ // 当 value 变化时,调整光标位置
61
+ useEffect(() => {
62
+ setState(previousState => {
63
+ if (!focus || !showCursor) {
64
+ return previousState;
65
+ }
66
+
67
+ const newValue = originalValue || '';
68
+
69
+ // 如果光标超出范围,调整到末尾
70
+ if (previousState.cursorOffset > newValue.length) {
71
+ const newOffset = newValue.length;
72
+ // 通知父组件
73
+ if (onCursorChange) {
74
+ onCursorChange(newOffset);
75
+ }
76
+ return {
77
+ cursorOffset: newOffset,
78
+ cursorWidth: 0
79
+ };
80
+ }
81
+
82
+ return previousState;
83
+ });
84
+ }, [originalValue, focus, showCursor, onCursorChange]);
85
+
86
+ const cursorActualWidth = highlightPastedText ? cursorWidth : 0;
87
+ const value = mask ? mask.repeat(originalValue.length) : originalValue;
88
+
89
+ let renderedValue = value;
90
+ let renderedPlaceholder = placeholder ? chalk.grey(placeholder) : undefined;
91
+
92
+ // 渲染光标(fake cursor)
93
+ if (showCursor && focus) {
94
+ renderedPlaceholder =
95
+ placeholder.length > 0
96
+ ? chalk.inverse(placeholder[0]) + chalk.grey(placeholder.slice(1))
97
+ : chalk.inverse(' ');
98
+
99
+ renderedValue = value.length > 0 ? '' : chalk.inverse(' ');
100
+
101
+ let i = 0;
102
+ for (const char of value) {
103
+ renderedValue +=
104
+ i >= cursorOffset - cursorActualWidth && i <= cursorOffset
105
+ ? chalk.inverse(char)
106
+ : char;
107
+ i++;
108
+ }
109
+
110
+ if (value.length > 0 && cursorOffset === value.length) {
111
+ renderedValue += chalk.inverse(' ');
112
+ }
113
+ }
114
+
115
+ useInput((input, key) => {
116
+ // 不处理的按键(由主组件处理)
117
+ if (key.upArrow ||
118
+ key.downArrow ||
119
+ (key.ctrl && input === 'c') ||
120
+ (key.ctrl && input === 'z') ||
121
+ (key.ctrl && input === 't') ||
122
+ (key.ctrl && input === 'g') ||
123
+ key.tab ||
124
+ (key.shift && key.tab)) {
125
+ return;
126
+ }
127
+
128
+ // Ctrl+Enter: 进入多行模式
129
+ if (key.ctrl && key.return && onEnterMultiline) {
130
+ onEnterMultiline();
131
+ return;
132
+ }
133
+
134
+ // Enter 键
135
+ if (key.return) {
136
+ if (onSubmit) {
137
+ onSubmit(originalValue);
138
+ }
139
+ return;
140
+ }
141
+
142
+ let nextCursorOffset = cursorOffset;
143
+ let nextValue = originalValue;
144
+ let nextCursorWidth = 0;
145
+
146
+ // 快捷键处理
147
+ if (enableShortcuts && key.ctrl) {
148
+ // Ctrl+A: 跳到行首
149
+ if (input === 'a') {
150
+ nextCursorOffset = 0;
151
+ setState({
152
+ cursorOffset: nextCursorOffset,
153
+ cursorWidth: 0
154
+ });
155
+ if (onCursorChange) {
156
+ onCursorChange(nextCursorOffset);
157
+ }
158
+ return;
159
+ }
160
+
161
+ // Ctrl+E: 跳到行尾
162
+ if (input === 'e') {
163
+ nextCursorOffset = originalValue.length;
164
+ setState({
165
+ cursorOffset: nextCursorOffset,
166
+ cursorWidth: 0
167
+ });
168
+ if (onCursorChange) {
169
+ onCursorChange(nextCursorOffset);
170
+ }
171
+ return;
172
+ }
173
+
174
+ // Ctrl+B: 向后移动一个字符(相当于左箭头)
175
+ if (input === 'b') {
176
+ nextCursorOffset = Math.max(0, cursorOffset - 1);
177
+ setState({
178
+ cursorOffset: nextCursorOffset,
179
+ cursorWidth: 0
180
+ });
181
+ if (onCursorChange) {
182
+ onCursorChange(nextCursorOffset);
183
+ }
184
+ return;
185
+ }
186
+
187
+ // Ctrl+F: 向前移动一个字符(相当于右箭头)
188
+ if (input === 'f') {
189
+ nextCursorOffset = Math.min(originalValue.length, cursorOffset + 1);
190
+ setState({
191
+ cursorOffset: nextCursorOffset,
192
+ cursorWidth: 0
193
+ });
194
+ if (onCursorChange) {
195
+ onCursorChange(nextCursorOffset);
196
+ }
197
+ return;
198
+ }
199
+
200
+ // Ctrl+U: 删除到行首
201
+ if (input === 'u') {
202
+ if (cursorOffset > 0) {
203
+ nextValue = originalValue.slice(cursorOffset);
204
+ onChange(nextValue);
205
+ nextCursorOffset = 0;
206
+ setState({
207
+ cursorOffset: nextCursorOffset,
208
+ cursorWidth: 0
209
+ });
210
+ if (onCursorChange) {
211
+ onCursorChange(nextCursorOffset);
212
+ }
213
+ }
214
+ return;
215
+ }
216
+
217
+ // Ctrl+K: 删除到行尾
218
+ if (input === 'k') {
219
+ if (cursorOffset < originalValue.length) {
220
+ nextValue = originalValue.slice(0, cursorOffset);
221
+ onChange(nextValue);
222
+ setState({
223
+ cursorOffset: nextCursorOffset,
224
+ cursorWidth: 0
225
+ });
226
+ }
227
+ return;
228
+ }
229
+
230
+ // Ctrl+W: 删除前一个单词
231
+ if (input === 'w') {
232
+ if (cursorOffset > 0) {
233
+ const beforeCursor = originalValue.slice(0, cursorOffset);
234
+ const afterCursor = originalValue.slice(cursorOffset);
235
+
236
+ // 找到前一个单词的边界
237
+ const newBefore = beforeCursor.replace(/\S+\s*$/, '');
238
+ nextCursorOffset = newBefore.length;
239
+ nextValue = newBefore + afterCursor;
240
+
241
+ onChange(nextValue);
242
+ setState({
243
+ cursorOffset: nextCursorOffset,
244
+ cursorWidth: 0
245
+ });
246
+ if (onCursorChange) {
247
+ onCursorChange(nextCursorOffset);
248
+ }
249
+ }
250
+ return;
251
+ }
252
+ }
253
+
254
+ // 左箭头
255
+ if (key.leftArrow) {
256
+ if (showCursor) {
257
+ nextCursorOffset--;
258
+ }
259
+ }
260
+ // 右箭头
261
+ else if (key.rightArrow) {
262
+ if (showCursor) {
263
+ nextCursorOffset++;
264
+ }
265
+ }
266
+ // Backspace 或 Delete
267
+ else if (key.backspace || key.delete) {
268
+ if (cursorOffset > 0) {
269
+ nextValue =
270
+ originalValue.slice(0, cursorOffset - 1) +
271
+ originalValue.slice(cursorOffset, originalValue.length);
272
+ nextCursorOffset--;
273
+ }
274
+ }
275
+ // 普通字符输入
276
+ else {
277
+ nextValue =
278
+ originalValue.slice(0, cursorOffset) +
279
+ input +
280
+ originalValue.slice(cursorOffset, originalValue.length);
281
+ nextCursorOffset += input.length;
282
+
283
+ if (input.length > 1) {
284
+ nextCursorWidth = input.length;
285
+ }
286
+ }
287
+
288
+ // 边界检查
289
+ if (nextCursorOffset < 0) {
290
+ nextCursorOffset = 0;
291
+ }
292
+
293
+ if (nextCursorOffset > nextValue.length) {
294
+ nextCursorOffset = nextValue.length;
295
+ }
296
+
297
+ setState({
298
+ cursorOffset: nextCursorOffset,
299
+ cursorWidth: nextCursorWidth
300
+ });
301
+
302
+ // 通知父组件光标位置变化
303
+ if (onCursorChange && nextCursorOffset !== cursorOffset) {
304
+ onCursorChange(nextCursorOffset);
305
+ }
306
+
307
+ // 通知父组件值变化
308
+ if (nextValue !== originalValue) {
309
+ onChange(nextValue);
310
+ }
311
+ }, { isActive: focus });
312
+
313
+ return (
314
+ <Text>
315
+ {placeholder
316
+ ? value.length > 0
317
+ ? renderedValue
318
+ : renderedPlaceholder
319
+ : renderedValue}
320
+ </Text>
321
+ );
322
+ }
323
+
324
+ export default EnhancedTextInput;