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,475 @@
1
+ # Region Constrained Edit 工具设计
2
+
3
+ ## 📋 需求分析
4
+
5
+ ### 当前问题
6
+
7
+ AI 在编辑文件时经常遇到:
8
+ - ❌ 相同文本在多处出现,难以精确定位
9
+ - ❌ 担心误替换其他位置的内容
10
+ - ❌ 需要反复确认修改位置
11
+ - ❌ 对大文件编辑时缺乏信心
12
+
13
+ ### 解决方案
14
+
15
+ **`regionConstrainedEdit`** - 限定区域的精确编辑工具
16
+
17
+ **优势**:
18
+ - ✅ 精确定位修改范围
19
+ - ✅ 避免误替换
20
+ - ✅ AI 编辑更自信
21
+ - ✅ 减少来回确认
22
+
23
+ ---
24
+
25
+ ## 🎯 工具设计
26
+
27
+ ### 基本签名
28
+
29
+ ```javascript
30
+ regionConstrainedEdit({
31
+ filePath: string, // 文件路径
32
+ begin: number, // 起始行号(1-based,包含)
33
+ end?: number, // 结束行号(1-based,不包含,可选)
34
+ oldText: string, // 要替换的文本
35
+ newText: string, // 新文本
36
+ isRegex?: boolean, // 是否使用正则表达式
37
+ replaceAll?: boolean // 在区域内是否全部替换
38
+ })
39
+ ```
40
+
41
+ ### 参数说明
42
+
43
+ | 参数 | 类型 | 必需 | 说明 |
44
+ |------|------|------|------|
45
+ | `filePath` | string | ✅ | 文件路径(相对或绝对) |
46
+ | `begin` | number | ✅ | 起始行号(1-based,从1开始) |
47
+ | `end` | number | ❌ | 结束行号(1-based,不包含,默认到文件末尾) |
48
+ | `oldText` | string | ✅ | 要查找的文本(或正则表达式) |
49
+ | `newText` | string | ✅ | 替换的文本 |
50
+ | `isRegex` | boolean | ❌ | 是否将 oldText 视为正则表达式(默认 false) |
51
+ | `replaceAll` | boolean | ❌ | 在区域内是否全部替换(默认 false) |
52
+
53
+ ### 返回值
54
+
55
+ ```javascript
56
+ {
57
+ success: boolean,
58
+ filePath: string,
59
+ region: {
60
+ begin: number,
61
+ end: number,
62
+ lines: number
63
+ },
64
+ replacements: number,
65
+ preview: {
66
+ before: string, // 替换前的片段
67
+ after: string // 替换后的片段
68
+ },
69
+ warning?: string // 警告信息
70
+ }
71
+ ```
72
+
73
+ ---
74
+
75
+ ## 💡 使用场景
76
+
77
+ ### 场景 1:精确替换函数
78
+
79
+ ```javascript
80
+ // 文件内容(100行)
81
+ function oldFunction() {
82
+ // ... 50行代码 ...
83
+ }
84
+
85
+ function oldFunction() { // 第80行,同名函数
86
+ // ... 代码 ...
87
+ }
88
+
89
+ // AI 只想修改第一个函数
90
+ regionConstrainedEdit({
91
+ filePath: 'src/app.js',
92
+ begin: 1,
93
+ end: 60,
94
+ oldText: 'function oldFunction()',
95
+ newText: 'function newFunction()',
96
+ replaceAll: false
97
+ })
98
+
99
+ // 结果:
100
+ // ✅ 第1行的函数被替换
101
+ // ✅ 第80行的函数不受影响
102
+ ```
103
+
104
+ ### 场景 2:修改配置文件特定段
105
+
106
+ ```javascript
107
+ // package.json(50行)
108
+ {
109
+ "name": "my-app",
110
+ "scripts": {
111
+ "build": "webpack",
112
+ "test": "jest"
113
+ },
114
+ "dependencies": { ... },
115
+ "devDependencies": { ... }
116
+ }
117
+
118
+ // 只修改 scripts 部分(第3-6行)
119
+ regionConstrainedEdit({
120
+ filePath: 'package.json',
121
+ begin: 3,
122
+ end: 7,
123
+ oldText: '"build": "webpack"',
124
+ newText: '"build": "webpack --mode production"',
125
+ replaceAll: false
126
+ })
127
+ ```
128
+
129
+ ### 场景 3:使用正则表达式
130
+
131
+ ```javascript
132
+ // 在第100-200行内,替换所有 console.log
133
+ regionConstrainedEdit({
134
+ filePath: 'src/app.js',
135
+ begin: 100,
136
+ end: 200,
137
+ oldText: 'console\\.log\\(.+?\\)',
138
+ newText: '// console.log removed',
139
+ isRegex: true,
140
+ replaceAll: true
141
+ })
142
+ ```
143
+
144
+ ### 场景 4:负数行号(从末尾)
145
+
146
+ ```javascript
147
+ // 修改最后10行
148
+ regionConstrainedEdit({
149
+ filePath: 'src/app.js',
150
+ begin: -10, // 倒数第10行
151
+ end: null, // 到文件末尾
152
+ oldText: 'TODO',
153
+ newText: 'DONE',
154
+ replaceAll: true
155
+ })
156
+ ```
157
+
158
+ ---
159
+
160
+ ## 🔧 实现方案
161
+
162
+ ### 方案 A:纯字符串操作
163
+
164
+ ```javascript
165
+ export const regionConstrainedEditTool = betaZodTool({
166
+ name: 'regionConstrainedEdit',
167
+ description: `Edit a file within a specific line range. Perfect for precise edits.
168
+
169
+ Use cases:
170
+ - Replace text in a specific function
171
+ - Modify configuration sections
172
+ - Edit code blocks without affecting other parts
173
+
174
+ Line numbers are 1-based. Negative numbers count from the end (-1 = last line).`,
175
+ inputSchema: z.object({
176
+ filePath: z.string().describe('File path'),
177
+ begin: z.number().describe('Start line (1-based, negative for from end)'),
178
+ end: z.number().optional().describe('End line (exclusive, default: end of file)'),
179
+ oldText: z.string().describe('Text to find (or regex pattern)'),
180
+ newText: z.string().describe('Replacement text'),
181
+ isRegex: z.boolean().optional().describe('Treat oldText as regex pattern'),
182
+ replaceAll: z.boolean().optional().describe('Replace all occurrences in region')
183
+ }),
184
+ run: async (input) => {
185
+ const fullPath = path.resolve(toolExecutorContext.workingDir, input.filePath);
186
+ const content = await fs.readFile(fullPath, 'utf-8');
187
+
188
+ // 分割为行数组
189
+ const lines = content.split('\n');
190
+ const totalLines = lines.length;
191
+
192
+ // 计算实际行号(处理负数)
193
+ const startLine = input.begin < 0
194
+ ? totalLines + input.begin + 1
195
+ : input.begin;
196
+ const endLine = input.end === undefined
197
+ ? totalLines
198
+ : (input.end < 0 ? totalLines + input.end + 1 : input.end);
199
+
200
+ // 验证行号
201
+ if (startLine < 1 || startLine > totalLines) {
202
+ return JSON.stringify({
203
+ success: false,
204
+ error: `Invalid start line: ${startLine}. File has ${totalLines} lines.`
205
+ });
206
+ }
207
+
208
+ if (endLine < startLine || endLine > totalLines) {
209
+ return JSON.stringify({
210
+ success: false,
211
+ error: `Invalid end line: ${endLine}. Must be between ${startLine} and ${totalLines}.`
212
+ });
213
+ }
214
+
215
+ // 提取区域内容(转换为 0-based)
216
+ const beforeRegion = lines.slice(0, startLine - 1).join('\n');
217
+ const regionLines = lines.slice(startLine - 1, endLine - 1);
218
+ const afterRegion = lines.slice(endLine - 1).join('\n');
219
+ let regionContent = regionLines.join('\n');
220
+
221
+ // 保存替换前内容(用于预览)
222
+ const beforePreview = regionContent.substring(0, 200);
223
+
224
+ // 在区域内执行替换
225
+ let replacements = 0;
226
+ if (input.isRegex) {
227
+ const regex = new RegExp(input.oldText, input.replaceAll ? 'g' : '');
228
+ const matches = regionContent.match(regex);
229
+ replacements = matches ? matches.length : 0;
230
+ regionContent = regionContent.replace(regex, input.newText);
231
+ } else {
232
+ if (input.replaceAll) {
233
+ const parts = regionContent.split(input.oldText);
234
+ replacements = parts.length - 1;
235
+ regionContent = parts.join(input.newText);
236
+ } else {
237
+ if (!regionContent.includes(input.oldText)) {
238
+ return JSON.stringify({
239
+ success: false,
240
+ error: 'Text not found in region',
241
+ region: { begin: startLine, end: endLine },
242
+ hint: 'Check if the text exists in the specified line range.'
243
+ });
244
+ }
245
+ replacements = 1;
246
+ regionContent = regionContent.replace(input.oldText, input.newText);
247
+ }
248
+ }
249
+
250
+ // 重组文件内容
251
+ const newContent = [beforeRegion, regionContent, afterRegion].join('\n');
252
+
253
+ // 写入文件
254
+ await fs.writeFile(fullPath, newContent, 'utf-8');
255
+
256
+ // 生成预览(替换后)
257
+ const afterPreview = regionContent.substring(0, 200);
258
+
259
+ return JSON.stringify({
260
+ success: true,
261
+ filePath: fullPath,
262
+ region: {
263
+ begin: startLine,
264
+ end: endLine,
265
+ lines: endLine - startLine + 1
266
+ },
267
+ replacements,
268
+ preview: {
269
+ before: beforePreview + (beforePreview.length >= 200 ? '...' : ''),
270
+ after: afterPreview + (afterPreview.length >= 200 ? '...' : '')
271
+ }
272
+ });
273
+ }
274
+ });
275
+ ```
276
+
277
+ ### 方案 B:使用 replace-in-file(推荐)
278
+
279
+ ```javascript
280
+ import { replaceInFile } from 'replace-in-file';
281
+
282
+ export const regionConstrainedEditTool = betaZodTool({
283
+ // ... 同上 ...
284
+
285
+ run: async (input) => {
286
+ const fullPath = path.resolve(toolExecutorContext.workingDir, input.filePath);
287
+ const content = await fs.readFile(fullPath, 'utf-8');
288
+ const lines = content.split('\n');
289
+ const totalLines = lines.length;
290
+
291
+ // 计算行号
292
+ const startLine = input.begin < 0 ? totalLines + input.begin + 1 : input.begin;
293
+ const endLine = input.end === undefined ? totalLines : input.end;
294
+
295
+ // 读取文件
296
+ const fileContent = await fs.readFile(fullPath, 'utf-8');
297
+ const fileLines = fileContent.split('\n');
298
+
299
+ // 提取区域
300
+ const beforeLines = fileLines.slice(0, startLine - 1);
301
+ const regionLines = fileLines.slice(startLine - 1, endLine - 1);
302
+ const afterLines = fileLines.slice(endLine - 1);
303
+
304
+ // 执行替换
305
+ let regionContent = regionLines.join('\n');
306
+ const beforePreview = regionContent;
307
+
308
+ if (input.isRegex) {
309
+ const flags = input.replaceAll ? 'g' : '';
310
+ const regex = new RegExp(input.oldText, flags);
311
+ const matches = regionContent.match(regex);
312
+ const replacements = matches ? matches.length : 0;
313
+
314
+ regionContent = regionContent.replace(regex, input.newText);
315
+
316
+ // 重组并写入
317
+ const newContent = [...beforeLines, ...regionContent.split('\n'), ...afterLines].join('\n');
318
+ await fs.writeFile(fullPath, newContent, 'utf-8');
319
+
320
+ return JSON.stringify({
321
+ success: true,
322
+ filePath: fullPath,
323
+ region: { begin: startLine, end: endLine },
324
+ replacements,
325
+ preview: {
326
+ before: beforePreview.substring(0, 200),
327
+ after: regionContent.substring(0, 200)
328
+ }
329
+ });
330
+ } else {
331
+ // 非正则表达式替换
332
+ // ... 类似方案 A ...
333
+ }
334
+ }
335
+ });
336
+ ```
337
+
338
+ ---
339
+
340
+ ## 📊 与其他工具对比
341
+
342
+ | 工具 | 精确度 | 适用场景 | 限制 |
343
+ |------|--------|---------|------|
344
+ | `editFile` | 低 | 简单替换,文件较小 | 可能误替换 |
345
+ | `regionConstrainedEdit` | **高** | 精确定位,大文件 | 需要知道行号 |
346
+ | `readFileLines` + `editFile` | 中 | 两步操作 | 需要两次调用 |
347
+
348
+ **推荐**:
349
+ - 简单替换 → `editFile`
350
+ - 精确替换 → `regionConstrainedEdit`
351
+ - 复杂多步操作 → `readFileLines` + `editFile`
352
+
353
+ ---
354
+
355
+ ## 🎯 Prompt 引导
356
+
357
+ ```markdown
358
+ ## 精确文件编辑
359
+
360
+ ### regionConstrainedEdit - 限定区域编辑
361
+
362
+ 当你需要精确修改文件特定区域时,使用此工具:
363
+
364
+ **优势**:
365
+ - ✅ 只在指定行范围内替换
366
+ - ✅ 避免影响文件其他部分
367
+ - ✅ 适合大文件编辑
368
+
369
+ **使用流程**:
370
+ 1. 先用 `readFileLines` 查看文件结构
371
+ 2. 确定要修改的行号范围
372
+ 3. 使用 `regionConstrainedEdit` 精确修改
373
+
374
+ **示例**:
375
+ ```javascript
376
+ // 1. 查看第1-50行
377
+ readFileLines({filePath: "src/app.js", startLine: 1, endLine: 50})
378
+
379
+ // 2. 修改第10-20行内的函数
380
+ regionConstrainedEdit({
381
+ filePath: "src/app.js",
382
+ begin: 10,
383
+ end: 20,
384
+ oldText: "function oldName()",
385
+ newText: "function newName()",
386
+ replaceAll: false
387
+ })
388
+ ```
389
+
390
+ **行号规则**:
391
+ - 行号从 1 开始(不是 0)
392
+ - 负数从末尾计数(-1 = 最后一行)
393
+ - `end` 不包含(end=20 表示到第19行)
394
+
395
+ **何时使用**:
396
+ - ✅ 文件中有多个相同的函数/变量名
397
+ - ✅ 只想修改特定代码块
398
+ - ✅ 需要精确控制修改范围
399
+ - ✅ 文件很大,不想全部读取
400
+ ```
401
+
402
+ ---
403
+
404
+ ## 🧪 测试用例
405
+
406
+ ### 测试 1:基本替换
407
+
408
+ ```javascript
409
+ // 测试文件(test.js)
410
+ const a = 1;
411
+ const b = 2;
412
+ const a = 3; // 第3行
413
+
414
+ regionConstrainedEdit({
415
+ filePath: 'test.js',
416
+ begin: 1,
417
+ end: 2,
418
+ oldText: 'const a = 1',
419
+ newText: 'const x = 1'
420
+ })
421
+
422
+ // 预期结果:
423
+ // const x = 1; ← 第1行被替换
424
+ // const b = 2;
425
+ // const a = 3; ← 第3行不受影响
426
+ ```
427
+
428
+ ### 测试 2:正则表达式
429
+
430
+ ```javascript
431
+ regionConstrainedEdit({
432
+ filePath: 'test.js',
433
+ begin: 1,
434
+ end: 10,
435
+ oldText: 'const\\s+\\w+\\s*=\\s*\\d+',
436
+ newText: '// removed',
437
+ isRegex: true,
438
+ replaceAll: true
439
+ })
440
+ ```
441
+
442
+ ### 测试 3:负数行号
443
+
444
+ ```javascript
445
+ // 修改最后5行
446
+ regionConstrainedEdit({
447
+ filePath: 'test.js',
448
+ begin: -5,
449
+ oldText: 'TODO',
450
+ newText: 'DONE',
451
+ replaceAll: true
452
+ })
453
+ ```
454
+
455
+ ---
456
+
457
+ ## ✅ 实施检查清单
458
+
459
+ - [ ] 实现 `regionConstrainedEditTool`
460
+ - [ ] 添加到 `TOOLS_MAP`
461
+ - [ ] 更新 Prompt 引导
462
+ - [ ] 添加单元测试
463
+ - [ ] 更新文档
464
+ - [ ] 添加使用示例
465
+
466
+ ---
467
+
468
+ ## 🚀 下一步
469
+
470
+ 1. **实现工具**:使用方案 A(纯字符串)或方案 B(replace-in-file)
471
+ 2. **添加测试**:覆盖各种场景
472
+ 3. **更新 Prompt**:引导 AI 使用新工具
473
+ 4. **文档完善**:添加示例和最佳实践
474
+
475
+ **准备开始实施吗?** 🚀
@@ -0,0 +1,171 @@
1
+ # 信号处理快捷键参考
2
+
3
+ ## ⌨️ Cloco 支持的快捷键
4
+
5
+ ### 程序控制
6
+
7
+ | 快捷键 | 功能 | 说明 |
8
+ |--------|------|------|
9
+ | `Ctrl+C` | 中止任务/退出 | 第一次按中止任务,1.5秒内再按退出 |
10
+ | `Ctrl+Z` | 挂起程序 | 挂起程序,可用 `fg` 恢复 |
11
+ | `ESC` | 退出程序 | 同 Ctrl+C |
12
+
13
+ ### 编辑控制
14
+
15
+ | 快捷键 | 功能 | 说明 |
16
+ |--------|------|------|
17
+ | `Ctrl+D` | 退出输入 | 结束当前输入(如果输入为空) |
18
+ | `Tab` | 切换 Thinking | 开关 AI Thinking 显示 |
19
+
20
+ ### 导航控制
21
+
22
+ | 快捷键 | 功能 | 说明 |
23
+ |--------|------|------|
24
+ | `↑/↓` | 历史记录/滚动 | 输入为空时滚动,否则浏览历史 |
25
+ | `PageUp/PageDown` | 滚动对话 | 快速滚动对话区域 |
26
+ | `Alt+↑/↓` | 滚动对话 | 精确滚动对话区域 |
27
+ | `Shift+↑/↓` | 滚动 Thinking | 滚动 AI Thinking 区域 |
28
+
29
+ ## 📋 信号处理详解
30
+
31
+ ### SIGINT (Ctrl+C)
32
+ ```javascript
33
+ // 处理逻辑:
34
+ if (isProcessing) {
35
+ // 中止当前任务
36
+ abortCurrentPhase();
37
+ } else {
38
+ if (两次按键间隔 < 1.5秒) {
39
+ // 退出程序
40
+ process.exit(0);
41
+ } else {
42
+ // 显示提示
43
+ showExitHint();
44
+ }
45
+ }
46
+ ```
47
+
48
+ ### SIGTSTP (Ctrl+Z)
49
+ ```javascript
50
+ // 处理逻辑:
51
+ console.log('⏸️ 程序已挂起');
52
+ process.kill(process.pid, 'SIGTSTP');
53
+ // 程序暂停,等待 SIGCONT 信号恢复
54
+ ```
55
+
56
+ ### SIGCONT (fg 命令)
57
+ ```javascript
58
+ // 程序恢复后自动继续执行
59
+ // 所有状态都会保留
60
+ ```
61
+
62
+ ## 🎯 使用示例
63
+
64
+ ### 场景 1:中止 AI 任务
65
+ ```
66
+ 用户: 请帮我分析整个项目...
67
+ AI: [开始分析...]
68
+ 用户: [按 Ctrl+C]
69
+ 系统: ⚠️ 正在中止任务...
70
+ ❌ 任务已中止
71
+ ```
72
+
73
+ ### 场景 2:挂起程序
74
+ ```
75
+ 用户: [使用 Cloco 中...]
76
+ 用户: [按 Ctrl+Z]
77
+ 系统: ⏸️ 程序已挂起 (按 fg 命令恢复)
78
+
79
+ [1]+ Stopped npm start
80
+
81
+ $ fg
82
+ [程序恢复,所有状态保留]
83
+ ```
84
+
85
+ ### 场景 3:退出程序
86
+ ```
87
+ 用户: [按 Ctrl+C]
88
+ 系统: ⚠️ 再次按 Ctrl+C 或 ESC 退出程序 (1.5秒内)
89
+
90
+ 用户: [再按 Ctrl+C]
91
+ 系统: 👋 再见!
92
+ ```
93
+
94
+ ## 🔧 技术实现
95
+
96
+ ### useInput 配置
97
+ ```javascript
98
+ useInput((input, key) => {
99
+ // 处理所有快捷键
100
+ }, { capture: true }); // 捕获所有键盘输入
101
+ ```
102
+
103
+ ### 信号处理
104
+ ```javascript
105
+ // Ctrl+Z - 手动发送 SIGTSTP
106
+ process.kill(process.pid, 'SIGTSTP');
107
+
108
+ // Ctrl+C - 处理在 useInput 中
109
+ // ESC - 处理在 useInput 中
110
+ ```
111
+
112
+ ## ⚠️ 注意事项
113
+
114
+ ### Ctrl+C vs Ctrl+Z
115
+
116
+ | 特性 | Ctrl+C | Ctrl+Z |
117
+ |------|--------|--------|
118
+ | 功能 | 中止/退出 | 挂起 |
119
+ | 可恢复 | ❌ | ✅ |
120
+ | 状态保留 | ❌ | ✅ |
121
+ | AI 任务 | 中止 | 可能中断 |
122
+
123
+ ### 最佳实践
124
+
125
+ 1. **使用 Ctrl+Z 当**:
126
+ - ✅ 需要临时切换到其他任务
127
+ - ✅ 想要保留当前状态
128
+ - ✅ 计划稍后继续
129
+
130
+ 2. **使用 Ctrl+C 当**:
131
+ - ✅ 想要中止当前 AI 任务
132
+ - ✅ 想要完全退出程序
133
+ - ✅ 不需要保留状态
134
+
135
+ 3. **避免**:
136
+ - ❌ 在 AI 处理时按 Ctrl+Z(可能导致超时)
137
+ - ❌ 长时间挂起程序(网络可能超时)
138
+
139
+ ## 📚 相关文档
140
+
141
+ - **`CTRL_Z_SUPPORT.md`** - Ctrl+Z 详细说明
142
+ - **`CTRL_Z_CHANGELOG.md`** - Ctrl+Z 实现文档
143
+ - **`test-ctrl-z.js`** - Ctrl+Z 测试脚本
144
+
145
+ ## 🧪 测试
146
+
147
+ ### 测试 Ctrl+Z
148
+ ```bash
149
+ node test-ctrl-z.js
150
+ # 按 Ctrl+Z 挂起
151
+ # 输入 fg 恢复
152
+ # 按 Ctrl+C 退出
153
+ ```
154
+
155
+ ### 测试完整功能
156
+ ```bash
157
+ npm start
158
+ # 测试所有快捷键
159
+ ```
160
+
161
+ ## 🎉 总结
162
+
163
+ Cloco 现在支持完整的 Linux 信号处理:
164
+
165
+ - ✅ Ctrl+C - 中止/退出
166
+ - ✅ Ctrl+Z - 挂起/恢复
167
+ - ✅ ESC - 退出
168
+ - ✅ 所有状态保留
169
+ - ✅ 符合 Linux 惯例
170
+
171
+ **提供了更好的用户体验!** 🚀