opencode-fast-apply 2.1.7 → 2.2.0
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/README.md +52 -114
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +236 -146
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,176 +1,114 @@
|
|
|
1
1
|
# opencode-fast-apply
|
|
2
2
|
|
|
3
|
-
OpenCode plugin for Fast Apply - High-performance code editing with OpenAI-compatible APIs
|
|
3
|
+
OpenCode plugin for Fast Apply - High-performance code editing with OpenAI-compatible APIs.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **Multi-backend support** - LM Studio, Ollama, OpenAI,
|
|
12
|
-
- **
|
|
13
|
-
- **Zero escaping overhead** - no XML tag processing needed, handles all code patterns safely
|
|
14
|
-
- **Collision-resistant parsing** - 1.6M+ unique delimiter combinations prevent conflicts with file content
|
|
7
|
+
- **Partial file editing** - Only send relevant sections (50-500 lines), not entire files
|
|
8
|
+
- **Smart context matching** - Automatically finds and replaces code sections
|
|
9
|
+
- **Token efficient** - Save 80-98% tokens compared to full-file edits
|
|
10
|
+
- **Lazy edit markers** - Use `// ... existing code ...` for unchanged sections
|
|
11
|
+
- **Multi-backend support** - LM Studio, Ollama, OpenAI, any OpenAI-compatible endpoint
|
|
12
|
+
- **Robust XML handling** - Safely handles special characters and XML tags in code
|
|
15
13
|
|
|
16
14
|
## Installation
|
|
17
15
|
|
|
18
|
-
### 1. Install from npm (Recommended)
|
|
19
|
-
|
|
20
16
|
```bash
|
|
21
17
|
npm install -g opencode-fast-apply
|
|
22
18
|
```
|
|
23
19
|
|
|
24
|
-
|
|
20
|
+
Configure your API endpoint:
|
|
25
21
|
|
|
26
|
-
For **LM Studio** (default):
|
|
27
22
|
```bash
|
|
23
|
+
# LM Studio (default)
|
|
28
24
|
export FAST_APPLY_URL="http://localhost:1234"
|
|
29
25
|
export FAST_APPLY_MODEL="fastapply-1.5b"
|
|
30
26
|
export FAST_APPLY_API_KEY="optional-api-key"
|
|
31
|
-
```
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
```bash
|
|
28
|
+
# Ollama
|
|
35
29
|
export FAST_APPLY_URL="http://localhost:11434"
|
|
36
30
|
export FAST_APPLY_MODEL="codellama:7b"
|
|
37
|
-
export FAST_APPLY_API_KEY="optional-api-key"
|
|
38
|
-
```
|
|
39
31
|
|
|
40
|
-
|
|
41
|
-
```bash
|
|
32
|
+
# OpenAI
|
|
42
33
|
export FAST_APPLY_URL="https://api.openai.com"
|
|
43
34
|
export FAST_APPLY_MODEL="gpt-4"
|
|
44
|
-
export FAST_APPLY_API_KEY="sk-your-
|
|
35
|
+
export FAST_APPLY_API_KEY="sk-your-key"
|
|
45
36
|
```
|
|
46
37
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
### 3. Add the plugin to your OpenCode config
|
|
50
|
-
|
|
51
|
-
Add to your global config (`~/.config/opencode/opencode.json` or `opencode.jsonc`):
|
|
38
|
+
Add to OpenCode config (`~/.config/opencode/opencode.json`):
|
|
52
39
|
|
|
53
40
|
```json
|
|
54
41
|
{
|
|
55
|
-
"plugin": [
|
|
56
|
-
"opencode-fast-apply"
|
|
57
|
-
]
|
|
42
|
+
"plugin": ["opencode-fast-apply"]
|
|
58
43
|
}
|
|
59
44
|
```
|
|
60
45
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
### 4. Restart OpenCode
|
|
64
|
-
|
|
65
|
-
The `fast_apply_edit` tool will now be available and configured as the default editing tool.
|
|
46
|
+
Restart OpenCode. The `fast_apply_edit` tool is now available.
|
|
66
47
|
|
|
67
48
|
## Usage
|
|
68
49
|
|
|
69
|
-
|
|
50
|
+
```typescript
|
|
51
|
+
// 1. Read relevant section (not entire file)
|
|
52
|
+
const content = await read("src/app.ts", { offset: 100, limit: 50 })
|
|
70
53
|
|
|
71
|
-
|
|
54
|
+
// 2. Edit with partial context
|
|
72
55
|
fast_apply_edit({
|
|
73
|
-
target_filepath: "
|
|
74
|
-
|
|
56
|
+
target_filepath: "src/app.ts",
|
|
57
|
+
original_code: content, // Just 50 lines!
|
|
75
58
|
code_edit: `// ... existing code ...
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
throw new Error("Token is required");
|
|
79
|
-
}
|
|
80
|
-
// ... existing code ...
|
|
59
|
+
function updated() {
|
|
60
|
+
return "modified";
|
|
81
61
|
}
|
|
82
62
|
// ... existing code ...`
|
|
83
63
|
})
|
|
84
64
|
```
|
|
85
65
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
| Large file (500+ lines) | `fast_apply_edit` | Handles partial snippets |
|
|
92
|
-
| Multiple scattered changes | `fast_apply_edit` | Batch efficiently |
|
|
93
|
-
| Whitespace-sensitive | `fast_apply_edit` | Forgiving with formatting |
|
|
66
|
+
**Key points:**
|
|
67
|
+
- Provide 50-500 lines of context around the area you want to change
|
|
68
|
+
- Include 2-5 lines before/after the target section
|
|
69
|
+
- Tool automatically finds and replaces that section in the full file
|
|
70
|
+
- No need to send entire file (saves tokens and improves speed)
|
|
94
71
|
|
|
95
72
|
## Configuration
|
|
96
73
|
|
|
97
74
|
| Variable | Default | Description |
|
|
98
75
|
|----------|---------|-------------|
|
|
99
|
-
| `FAST_APPLY_API_KEY` | `optional-api-key` | API key (optional for local
|
|
100
|
-
| `FAST_APPLY_URL` | `http://localhost:1234
|
|
76
|
+
| `FAST_APPLY_API_KEY` | `optional-api-key` | API key (optional for local) |
|
|
77
|
+
| `FAST_APPLY_URL` | `http://localhost:1234` | API endpoint |
|
|
101
78
|
| `FAST_APPLY_MODEL` | `fastapply-1.5b` | Model name |
|
|
102
|
-
| `FAST_APPLY_TEMPERATURE` | `0.05` | Temperature (0.0-2.0) |
|
|
103
79
|
|
|
104
80
|
## How It Works
|
|
105
81
|
|
|
106
|
-
1.
|
|
107
|
-
2.
|
|
108
|
-
3.
|
|
109
|
-
4.
|
|
110
|
-
5.
|
|
111
|
-
6.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
|
121
|
-
|-------|----------------|---------------------|
|
|
122
|
-
| fastapply-1.5b (Q4) + RTX 4090 | 10,000-15,000 tok/s | High-end GPU |
|
|
123
|
-
| codellama:7b (Q4) + RTX 3060 | 3,000-5,000 tok/s | Mid-range GPU |
|
|
124
|
-
| codellama:7b (Q4) + CPU only | 50-200 tok/s | Modern CPU |
|
|
125
|
-
| OpenAI GPT-4 API | 100-500 tok/s | Network dependent |
|
|
126
|
-
|
|
127
|
-
**Factors affecting performance:**
|
|
128
|
-
- Model size (1.5B vs 7B vs 13B+ parameters)
|
|
129
|
-
- Quantization level (Q4 vs Q5 vs Q8 vs FP16)
|
|
130
|
-
- Hardware (GPU VRAM, CPU cores, RAM)
|
|
131
|
-
- Backend optimization (LM Studio vs Ollama)
|
|
132
|
-
|
|
133
|
-
## Supported Backends
|
|
134
|
-
|
|
135
|
-
- **LM Studio** - Local inference server with GPU acceleration
|
|
136
|
-
- **Ollama** - Local LLM runtime with easy model management
|
|
137
|
-
- **OpenAI** - Cloud API with high reliability
|
|
138
|
-
- **Any OpenAI-compatible endpoint** - Custom servers and providers
|
|
139
|
-
|
|
140
|
-
## Edge Cases Handled
|
|
141
|
-
|
|
142
|
-
- ✅ Code containing XML-like tags (`<update>`, `<code>`, `<result>`) in strings
|
|
143
|
-
- ✅ Code containing triple-angle-bracket patterns (`<<<RESULT>>>`) in comments or strings
|
|
144
|
-
- ✅ Multiple XML-like tags in regex patterns
|
|
145
|
-
- ✅ Special characters (quotes, backslashes, unicode, SQL, HTML entities)
|
|
146
|
-
- ✅ Large files (500+ lines)
|
|
147
|
-
- ✅ Multiple scattered changes in single edit
|
|
148
|
-
- ✅ Complex nested structures
|
|
149
|
-
- ✅ Template strings with `${variable}`
|
|
150
|
-
- ✅ Whitespace and indentation preservation
|
|
151
|
-
- ✅ Unique delimiters per request prevent all parsing conflicts (1.6M+ combinations)
|
|
152
|
-
- ✅ Fallback pattern detection for robust delimiter parsing
|
|
82
|
+
1. AI reads relevant file section (50-500 lines)
|
|
83
|
+
2. AI provides `original_code` (partial context) and `code_edit`
|
|
84
|
+
3. Tool sends to Fast Apply API for merging
|
|
85
|
+
4. Tool finds where `original_code` appears in full file
|
|
86
|
+
5. Tool replaces that section with merged code
|
|
87
|
+
6. Tool writes full file back and shows diff
|
|
88
|
+
|
|
89
|
+
## Token Savings
|
|
90
|
+
|
|
91
|
+
| File Size | Before (Full) | After (Partial) | Savings |
|
|
92
|
+
|-----------|--------------|-----------------|---------|
|
|
93
|
+
| 100 lines | 2,500 tokens | 500 tokens | -80% |
|
|
94
|
+
| 500 lines | 12,500 tokens | 1,000 tokens | -92% |
|
|
95
|
+
| 1000 lines | 25,000 tokens | 1,500 tokens | -94% |
|
|
96
|
+
| 5000 lines | 125,000 tokens | 2,000 tokens | -98% |
|
|
153
97
|
|
|
154
98
|
## Troubleshooting
|
|
155
99
|
|
|
156
|
-
|
|
100
|
+
**"Cannot locate original_code in file"**
|
|
101
|
+
- File was modified since you read it - re-read and try again
|
|
102
|
+
- Whitespace differs - tool tries normalized matching automatically
|
|
103
|
+
- Wrong section provided - verify you extracted the correct lines
|
|
104
|
+
|
|
105
|
+
**API connection issues**
|
|
157
106
|
```bash
|
|
158
|
-
# Test your endpoint
|
|
159
107
|
curl -X POST http://localhost:1234/v1/chat/completions \
|
|
160
108
|
-H "Content-Type: application/json" \
|
|
161
109
|
-d '{"model":"fastapply-1.5b","messages":[{"role":"user","content":"test"}]}'
|
|
162
110
|
```
|
|
163
111
|
|
|
164
|
-
### Slow Performance
|
|
165
|
-
- Use smaller models (1.5B-3B parameters)
|
|
166
|
-
- Enable GPU acceleration in LM Studio/Ollama
|
|
167
|
-
- Use Q4 quantization for faster inference
|
|
168
|
-
- Increase `FAST_APPLY_MAX_TOKENS` if responses are truncated
|
|
169
|
-
|
|
170
|
-
## Contributing
|
|
171
|
-
|
|
172
|
-
Contributions welcome! This plugin could potentially be integrated into OpenCode core.
|
|
173
|
-
|
|
174
112
|
## License
|
|
175
113
|
|
|
176
|
-
|
|
114
|
+
MIT
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,qBAAqB,CAAA;AAshBvD,eAAO,MAAM,eAAe,EAAE,MAgL7B,CAAA;AAGD,eAAe,eAAe,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -16,138 +16,127 @@ const sessionParamsCache = new Map();
|
|
|
16
16
|
const FAST_APPLY_API_KEY = process.env.FAST_APPLY_API_KEY || "optional-api-key";
|
|
17
17
|
const FAST_APPLY_URL = (process.env.FAST_APPLY_URL || "http://localhost:1234/v1").replace(/\/v1\/?$/, "");
|
|
18
18
|
const FAST_APPLY_MODEL = process.env.FAST_APPLY_MODEL || "fastapply-1.5b";
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const FAST_APPLY_SYSTEM_PROMPT = `You are a precise code merging assistant. Your role:
|
|
20
|
+
1. Merge updates while preserving all original structure and formatting
|
|
21
|
+
2. Output ONLY valid code within specified XML tags
|
|
22
|
+
3. Never add explanations or text outside tags
|
|
23
|
+
4. Preserve all escaped XML entities exactly as provided
|
|
24
|
+
5. Ensure output is syntactically valid and complete`;
|
|
25
|
+
const FAST_APPLY_USER_PROMPT = `Merge <update> into <code> and output the complete merged file.
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
RULES:
|
|
28
|
+
- Preserve: structure, order, comments, indentation, escaped entities
|
|
29
|
+
- Output: entire file within <updated-code></updated-code> tags only
|
|
30
|
+
- Forbidden: explanations, placeholders, ellipses, code fences, nested tags
|
|
31
|
+
- Validation: ensure syntactically valid, complete code
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
1. \`fast_apply_edit\` - ALL file edits (default)
|
|
27
|
-
2. \`edit\` - Fallback if API fails
|
|
28
|
-
3. \`write\` - NEW files only
|
|
33
|
+
<code>{original_code}</code>
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
Use \`// ... existing code ...\` for unchanged sections:
|
|
35
|
+
<update>{update_snippet}</update>
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
function updated() {
|
|
36
|
-
return "modified";
|
|
37
|
-
}
|
|
38
|
-
// ... existing code ...
|
|
39
|
-
\`\`\`
|
|
37
|
+
Output complete merged code:`;
|
|
38
|
+
const TOOL_INSTRUCTIONS = `DEFAULT tool for editing existing files. Use INSTEAD of native 'edit' tool.
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
- MANDATORY: Use \`// ... existing code ...\` markers
|
|
43
|
-
- Include 2-3 lines context before/after edits
|
|
44
|
-
- Preserve exact indentation
|
|
45
|
-
- ONE edit block per call (multiple blocks = suboptimal results)
|
|
46
|
-
- Deletions: show context, omit deleted lines
|
|
47
|
-
- NEVER for new files
|
|
40
|
+
CRITICAL: For EXISTING files ONLY. Use 'write' for new files.
|
|
48
41
|
|
|
49
|
-
|
|
42
|
+
WORKFLOW:
|
|
43
|
+
1. Read the file to understand current content
|
|
44
|
+
2. Extract relevant section (50-500 lines with context)
|
|
45
|
+
3. Call fast_apply_edit with original_code (partial) and code_edit
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
PARTIAL EDITING:
|
|
48
|
+
- You DON'T need to provide the entire file
|
|
49
|
+
- Provide 50-500 lines of context around the area you want to change
|
|
50
|
+
- Include 2-5 lines before and after the target section
|
|
51
|
+
- Tool will automatically find and replace that section in the file
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
\`\`\`
|
|
53
|
+
PRIORITY:
|
|
54
|
+
1. fast_apply_edit - ALL file edits (default, 10x faster)
|
|
55
|
+
2. edit - Fallback if API fails
|
|
56
|
+
3. write - NEW files only
|
|
62
57
|
|
|
63
|
-
|
|
58
|
+
FORMAT:
|
|
59
|
+
Use \`// ... existing code ...\` markers for unchanged sections:
|
|
64
60
|
\`\`\`
|
|
65
61
|
// ... existing code ...
|
|
66
|
-
function
|
|
67
|
-
const result = param * 2;
|
|
68
|
-
return result;
|
|
69
|
-
}
|
|
62
|
+
function updated() { return "modified"; }
|
|
70
63
|
// ... existing code ...
|
|
71
64
|
\`\`\`
|
|
72
65
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
RULES:
|
|
67
|
+
- MANDATORY: Read file first to get original_code
|
|
68
|
+
- Provide 50-500 lines of context (not entire file unless small)
|
|
69
|
+
- Use \`// ... existing code ...\` markers in code_edit
|
|
70
|
+
- Include 2-5 lines context before/after edits
|
|
71
|
+
- Preserve exact indentation and whitespace
|
|
72
|
+
- ONE edit block per call (multiple blocks = suboptimal)
|
|
79
73
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
EXAMPLE:
|
|
75
|
+
\`\`\`typescript
|
|
76
|
+
// 1. Read file
|
|
77
|
+
const content = await read("src/app.ts", { offset: 100, limit: 50 })
|
|
78
|
+
|
|
79
|
+
// 2. Call fast_apply_edit with partial context
|
|
80
|
+
fast_apply_edit({
|
|
81
|
+
target_filepath: "src/app.ts",
|
|
82
|
+
original_code: content, // Just 50 lines, not entire file!
|
|
83
|
+
code_edit: "... updated code ..."
|
|
84
|
+
})
|
|
84
85
|
\`\`\`
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return result;
|
|
87
|
+
FALLBACK: If API fails, use native 'edit' tool.`;
|
|
88
|
+
function escapeXmlTags(text) {
|
|
89
|
+
return text
|
|
90
|
+
.replace(/&/g, "&")
|
|
91
|
+
.replace(/</g, "<")
|
|
92
|
+
.replace(/>/g, ">")
|
|
93
|
+
.replace(/"/g, """)
|
|
94
|
+
.replace(/'/g, "'");
|
|
95
95
|
}
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
RESULT_START: `<<<RESULT_${id}>>>`,
|
|
104
|
-
RESULT_END: `<<<END_RESULT_${id}>>>`
|
|
105
|
-
};
|
|
96
|
+
function unescapeXmlTags(text) {
|
|
97
|
+
return text
|
|
98
|
+
.replace(/</g, "<")
|
|
99
|
+
.replace(/>/g, ">")
|
|
100
|
+
.replace(/"/g, '"')
|
|
101
|
+
.replace(/'/g, "'")
|
|
102
|
+
.replace(/&/g, "&");
|
|
106
103
|
}
|
|
107
|
-
function
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const closeTagIdx = stripped.indexOf(">>>", genericStart);
|
|
114
|
-
if (closeTagIdx !== -1) {
|
|
115
|
-
startIdx = closeTagIdx + 3;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
104
|
+
function validateNoNestedTags(content) {
|
|
105
|
+
const unescaped = content
|
|
106
|
+
.replace(/</g, "<")
|
|
107
|
+
.replace(/>/g, ">");
|
|
108
|
+
if (unescaped.includes("<updated-code>") || unescaped.includes("</updated-code>")) {
|
|
109
|
+
throw new Error("Content contains unescaped tag-like sequences that could break parsing");
|
|
118
110
|
}
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
if (unescaped.includes("<code>") || unescaped.includes("</code>")) {
|
|
112
|
+
throw new Error("Content contains unescaped <code> tags that could break prompt structure");
|
|
121
113
|
}
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
const lines = stripped.split("\n");
|
|
125
|
-
if (lines.length >= 2) {
|
|
126
|
-
return lines.slice(1, -1).join("\n");
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return stripped;
|
|
114
|
+
if (unescaped.includes("<update>") || unescaped.includes("</update>")) {
|
|
115
|
+
throw new Error("Content contains unescaped <update> tags that could break prompt structure");
|
|
130
116
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
117
|
+
}
|
|
118
|
+
function extractUpdatedCode(raw) {
|
|
119
|
+
const stripped = raw.trim();
|
|
120
|
+
const startRegex = /<updated-code\s*>/i;
|
|
121
|
+
const endRegex = /<\/updated-code\s*>/i;
|
|
122
|
+
const startMatch = stripped.match(startRegex);
|
|
123
|
+
if (!startMatch || startMatch.index === undefined) {
|
|
124
|
+
throw new Error("Missing or malformed <updated-code> start tag in AI response");
|
|
137
125
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
return extracted;
|
|
126
|
+
const startIdx = startMatch.index + startMatch[0].length;
|
|
127
|
+
const remaining = stripped.slice(startIdx);
|
|
128
|
+
const endMatch = remaining.match(endRegex);
|
|
129
|
+
if (!endMatch || endMatch.index === undefined) {
|
|
130
|
+
throw new Error("Missing or malformed </updated-code> end tag in AI response");
|
|
145
131
|
}
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
132
|
+
const endIdx = endMatch.index;
|
|
133
|
+
const inner = remaining.slice(0, endIdx);
|
|
134
|
+
if (!inner.trim()) {
|
|
135
|
+
throw new Error("Empty updated-code block in AI response");
|
|
149
136
|
}
|
|
150
|
-
|
|
137
|
+
const unescaped = unescapeXmlTags(inner);
|
|
138
|
+
validateNoNestedTags(unescaped);
|
|
139
|
+
return unescaped;
|
|
151
140
|
}
|
|
152
141
|
function generateUnifiedDiff(filepath, original, modified) {
|
|
153
142
|
const patch = createTwoFilesPatch(`a/${filepath}`, `b/${filepath}`, original, modified, "", "", { context: 3 });
|
|
@@ -191,6 +180,94 @@ function shortenPath(filePath, workingDir) {
|
|
|
191
180
|
function estimateTokens(text) {
|
|
192
181
|
return Math.ceil(text.length / 4);
|
|
193
182
|
}
|
|
183
|
+
function normalizeWhitespace(text) {
|
|
184
|
+
return text
|
|
185
|
+
.split('\n')
|
|
186
|
+
.map(line => line.trimEnd())
|
|
187
|
+
.join('\n')
|
|
188
|
+
.replace(/\r\n/g, '\n')
|
|
189
|
+
.trim();
|
|
190
|
+
}
|
|
191
|
+
function findExactMatch(haystack, needle) {
|
|
192
|
+
return haystack.indexOf(needle);
|
|
193
|
+
}
|
|
194
|
+
function findNormalizedMatch(haystack, needle) {
|
|
195
|
+
const normalizedHaystack = normalizeWhitespace(haystack);
|
|
196
|
+
const normalizedNeedle = normalizeWhitespace(needle);
|
|
197
|
+
const index = normalizedHaystack.indexOf(normalizedNeedle);
|
|
198
|
+
if (index === -1)
|
|
199
|
+
return -1;
|
|
200
|
+
let actualIndex = 0;
|
|
201
|
+
let normalizedIndex = 0;
|
|
202
|
+
while (normalizedIndex < index && actualIndex < haystack.length) {
|
|
203
|
+
const char = haystack[actualIndex];
|
|
204
|
+
const normalizedChar = normalizedHaystack[normalizedIndex];
|
|
205
|
+
if (char === '\r' && haystack[actualIndex + 1] === '\n') {
|
|
206
|
+
actualIndex += 2;
|
|
207
|
+
normalizedIndex += 1;
|
|
208
|
+
}
|
|
209
|
+
else if (char === normalizedChar) {
|
|
210
|
+
actualIndex++;
|
|
211
|
+
normalizedIndex++;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
actualIndex++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return actualIndex;
|
|
218
|
+
}
|
|
219
|
+
async function applyPartialEdit(filepath, original_code, merged_code) {
|
|
220
|
+
const currentFile = await readFile(filepath, "utf-8");
|
|
221
|
+
if (currentFile.includes('\0')) {
|
|
222
|
+
return {
|
|
223
|
+
success: false,
|
|
224
|
+
error: "Cannot edit binary files"
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
let index = findExactMatch(currentFile, original_code);
|
|
228
|
+
let matchType = "exact";
|
|
229
|
+
if (index === -1) {
|
|
230
|
+
index = findNormalizedMatch(currentFile, original_code);
|
|
231
|
+
matchType = "normalized";
|
|
232
|
+
}
|
|
233
|
+
if (index === -1) {
|
|
234
|
+
return {
|
|
235
|
+
success: false,
|
|
236
|
+
error: `Cannot locate original_code in ${filepath}.
|
|
237
|
+
|
|
238
|
+
The content you provided doesn't match the current file.
|
|
239
|
+
|
|
240
|
+
POSSIBLE CAUSES:
|
|
241
|
+
- File was modified since you read it
|
|
242
|
+
- Whitespace or indentation differs
|
|
243
|
+
- Wrong section provided
|
|
244
|
+
- File encoding issues
|
|
245
|
+
|
|
246
|
+
SOLUTIONS:
|
|
247
|
+
1. Re-read the file to get current content
|
|
248
|
+
2. Verify exact whitespace and indentation
|
|
249
|
+
3. Provide more context (more surrounding lines)
|
|
250
|
+
4. Use native 'edit' tool for exact string matching`
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const occurrences = currentFile.split(original_code).length - 1;
|
|
254
|
+
if (occurrences > 1) {
|
|
255
|
+
return {
|
|
256
|
+
success: false,
|
|
257
|
+
error: `original_code appears ${occurrences} times in ${filepath}.
|
|
258
|
+
|
|
259
|
+
Please provide more context (more surrounding lines) to uniquely identify the section you want to edit.`
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const newFileContent = currentFile.substring(0, index) +
|
|
263
|
+
merged_code +
|
|
264
|
+
currentFile.substring(index + original_code.length);
|
|
265
|
+
console.log(`[fast-apply] Applied ${matchType} match at position ${index}`);
|
|
266
|
+
return {
|
|
267
|
+
success: true,
|
|
268
|
+
newFileContent
|
|
269
|
+
};
|
|
270
|
+
}
|
|
194
271
|
function formatFastApplyResult(filePath, workingDir, insertions, deletions, diffPreview, modifiedTokens) {
|
|
195
272
|
const shortPath = shortenPath(filePath, workingDir);
|
|
196
273
|
const tokenStr = formatTokenCount(modifiedTokens);
|
|
@@ -219,7 +296,7 @@ function formatErrorOutput(error, filePath, workingDir) {
|
|
|
219
296
|
/**
|
|
220
297
|
* Call OpenAI's Fast Apply API to merge code edits
|
|
221
298
|
*/
|
|
222
|
-
async function callFastApply(originalCode, codeEdit
|
|
299
|
+
async function callFastApply(originalCode, codeEdit) {
|
|
223
300
|
if (!FAST_APPLY_API_KEY) {
|
|
224
301
|
return {
|
|
225
302
|
success: false,
|
|
@@ -227,21 +304,11 @@ async function callFastApply(originalCode, codeEdit, instructions) {
|
|
|
227
304
|
};
|
|
228
305
|
}
|
|
229
306
|
try {
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
${delimiters.ORIGINAL_START}
|
|
237
|
-
${originalCode}
|
|
238
|
-
${delimiters.ORIGINAL_END}
|
|
239
|
-
|
|
240
|
-
${delimiters.UPDATE_START}
|
|
241
|
-
${codeEdit}
|
|
242
|
-
${delimiters.UPDATE_END}
|
|
243
|
-
|
|
244
|
-
Provide the complete updated code wrapped in ${delimiters.RESULT_START} and ${delimiters.RESULT_END}.`;
|
|
307
|
+
const escapedOriginalCode = escapeXmlTags(originalCode);
|
|
308
|
+
const escapedCodeEdit = escapeXmlTags(codeEdit);
|
|
309
|
+
const userContent = FAST_APPLY_USER_PROMPT
|
|
310
|
+
.replace("{original_code}", escapedOriginalCode)
|
|
311
|
+
.replace("{update_snippet}", escapedCodeEdit);
|
|
245
312
|
const response = await fetch(`${FAST_APPLY_URL}/v1/chat/completions`, {
|
|
246
313
|
method: "POST",
|
|
247
314
|
headers: {
|
|
@@ -260,7 +327,7 @@ Provide the complete updated code wrapped in ${delimiters.RESULT_START} and ${de
|
|
|
260
327
|
content: userContent,
|
|
261
328
|
},
|
|
262
329
|
],
|
|
263
|
-
temperature:
|
|
330
|
+
temperature: 0,
|
|
264
331
|
}),
|
|
265
332
|
});
|
|
266
333
|
if (!response.ok) {
|
|
@@ -278,11 +345,20 @@ Provide the complete updated code wrapped in ${delimiters.RESULT_START} and ${de
|
|
|
278
345
|
error: "Fast Apply API returned empty response",
|
|
279
346
|
};
|
|
280
347
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
348
|
+
try {
|
|
349
|
+
const mergedCode = extractUpdatedCode(rawResponse);
|
|
350
|
+
return {
|
|
351
|
+
success: true,
|
|
352
|
+
content: mergedCode,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
catch (parseError) {
|
|
356
|
+
const error = parseError;
|
|
357
|
+
return {
|
|
358
|
+
success: false,
|
|
359
|
+
error: `Failed to parse AI response: ${error.message}`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
286
362
|
}
|
|
287
363
|
catch (err) {
|
|
288
364
|
const error = err;
|
|
@@ -369,15 +445,26 @@ export const FastApplyPlugin = async ({ directory, client }) => {
|
|
|
369
445
|
target_filepath: tool.schema
|
|
370
446
|
.string()
|
|
371
447
|
.describe("Path of the file to modify (relative to project root)"),
|
|
372
|
-
|
|
448
|
+
original_code: tool.schema
|
|
373
449
|
.string()
|
|
374
|
-
.describe(
|
|
450
|
+
.describe(`The original code section to be modified.
|
|
451
|
+
|
|
452
|
+
IMPORTANT:
|
|
453
|
+
- Provide 50-500 lines of context around the area you want to change
|
|
454
|
+
- Include 2-5 lines before and after the target section
|
|
455
|
+
- Must match the current file content exactly (whitespace matters)
|
|
456
|
+
- Can be partial (doesn't need to be entire file)
|
|
457
|
+
|
|
458
|
+
WORKFLOW:
|
|
459
|
+
1. Read the file first to get current content
|
|
460
|
+
2. Extract the relevant section with context
|
|
461
|
+
3. Provide that section as original_code`),
|
|
375
462
|
code_edit: tool.schema
|
|
376
463
|
.string()
|
|
377
|
-
.describe('The code changes
|
|
464
|
+
.describe('The updated code with changes applied. Use "// ... existing code ..." markers for unchanged sections within this context.'),
|
|
378
465
|
},
|
|
379
466
|
async execute(args, toolCtx) {
|
|
380
|
-
const { target_filepath,
|
|
467
|
+
const { target_filepath, original_code, code_edit } = args;
|
|
381
468
|
const params = sessionParamsCache.get(toolCtx.sessionID) || {};
|
|
382
469
|
// Resolve file path relative to project directory
|
|
383
470
|
const filepath = target_filepath.startsWith("/")
|
|
@@ -392,16 +479,12 @@ Get your API key at: https://openai.com/api
|
|
|
392
479
|
|
|
393
480
|
Alternatively, use the native 'edit' tool for this change.`;
|
|
394
481
|
}
|
|
395
|
-
//
|
|
396
|
-
let originalCode;
|
|
482
|
+
// Check if file exists and is writable
|
|
397
483
|
try {
|
|
398
|
-
await access(filepath, constants.R_OK);
|
|
399
|
-
originalCode = await readFile(filepath, "utf-8");
|
|
484
|
+
await access(filepath, constants.R_OK | constants.W_OK);
|
|
400
485
|
}
|
|
401
486
|
catch (err) {
|
|
402
|
-
|
|
403
|
-
if (error.message.includes("ENOENT") || error.message.includes("no such file")) {
|
|
404
|
-
return `Error: File not found: ${target_filepath}
|
|
487
|
+
return `Error: File not found or not writable: ${target_filepath}
|
|
405
488
|
|
|
406
489
|
This tool is for EDITING EXISTING FILES ONLY.
|
|
407
490
|
For new file creation, use the 'write' tool instead.
|
|
@@ -411,26 +494,33 @@ write({
|
|
|
411
494
|
filePath: "${target_filepath}",
|
|
412
495
|
content: "your file content here"
|
|
413
496
|
})`;
|
|
414
|
-
}
|
|
415
|
-
return `Error reading file ${target_filepath}: ${error.message}`;
|
|
416
497
|
}
|
|
417
|
-
// Call
|
|
418
|
-
const result = await callFastApply(
|
|
498
|
+
// Call Fast Apply API to merge the edit
|
|
499
|
+
const result = await callFastApply(original_code, code_edit);
|
|
419
500
|
if (!result.success || !result.content) {
|
|
420
501
|
const errorMsg = result.error || "Unknown error";
|
|
421
502
|
await sendTUIErrorNotification(client, toolCtx.sessionID, target_filepath, directory, errorMsg, params);
|
|
422
503
|
return formatErrorOutput(errorMsg, target_filepath, directory);
|
|
423
504
|
}
|
|
424
505
|
const mergedCode = result.content;
|
|
506
|
+
// Apply partial edit with smart matching
|
|
507
|
+
const applyResult = await applyPartialEdit(filepath, original_code, mergedCode);
|
|
508
|
+
if (!applyResult.success) {
|
|
509
|
+
await sendTUIErrorNotification(client, toolCtx.sessionID, target_filepath, directory, applyResult.error, params);
|
|
510
|
+
return formatErrorOutput(applyResult.error, target_filepath, directory);
|
|
511
|
+
}
|
|
512
|
+
// Write merged file back
|
|
425
513
|
try {
|
|
426
|
-
await writeFile(filepath,
|
|
514
|
+
await writeFile(filepath, applyResult.newFileContent, "utf-8");
|
|
427
515
|
}
|
|
428
516
|
catch (err) {
|
|
429
517
|
const error = err;
|
|
430
518
|
await sendTUIErrorNotification(client, toolCtx.sessionID, target_filepath, directory, error.message, params);
|
|
431
519
|
return formatErrorOutput(error.message, target_filepath, directory);
|
|
432
520
|
}
|
|
433
|
-
|
|
521
|
+
// Read origfile for diff comparison
|
|
522
|
+
const originalFileContent = await readFile(filepath, "utf-8");
|
|
523
|
+
const diff = generateUnifiedDiff(target_filepath, originalFileContent, applyResult.newFileContent);
|
|
434
524
|
const { added, removed } = countChanges(diff);
|
|
435
525
|
const modifiedTokens = estimateTokens(diff);
|
|
436
526
|
await sendTUINotification(client, toolCtx.sessionID, target_filepath, directory, added, removed, modifiedTokens, params);
|
package/package.json
CHANGED