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
|
@@ -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
|
+
**提供了更好的用户体验!** 🚀
|