opencode-fast-apply 2.1.6 → 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 -108
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +224 -126
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,170 +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
|
-
- **Robust XML
|
|
13
|
-
- **Special character support** - preserves all string literals, regex patterns, and escape sequences
|
|
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
|
|
14
13
|
|
|
15
14
|
## Installation
|
|
16
15
|
|
|
17
|
-
### 1. Install from npm (Recommended)
|
|
18
|
-
|
|
19
16
|
```bash
|
|
20
17
|
npm install -g opencode-fast-apply
|
|
21
18
|
```
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
Configure your API endpoint:
|
|
24
21
|
|
|
25
|
-
For **LM Studio** (default):
|
|
26
22
|
```bash
|
|
23
|
+
# LM Studio (default)
|
|
27
24
|
export FAST_APPLY_URL="http://localhost:1234"
|
|
28
25
|
export FAST_APPLY_MODEL="fastapply-1.5b"
|
|
29
26
|
export FAST_APPLY_API_KEY="optional-api-key"
|
|
30
|
-
```
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
```bash
|
|
28
|
+
# Ollama
|
|
34
29
|
export FAST_APPLY_URL="http://localhost:11434"
|
|
35
30
|
export FAST_APPLY_MODEL="codellama:7b"
|
|
36
|
-
export FAST_APPLY_API_KEY="optional-api-key"
|
|
37
|
-
```
|
|
38
31
|
|
|
39
|
-
|
|
40
|
-
```bash
|
|
32
|
+
# OpenAI
|
|
41
33
|
export FAST_APPLY_URL="https://api.openai.com"
|
|
42
34
|
export FAST_APPLY_MODEL="gpt-4"
|
|
43
|
-
export FAST_APPLY_API_KEY="sk-your-
|
|
35
|
+
export FAST_APPLY_API_KEY="sk-your-key"
|
|
44
36
|
```
|
|
45
37
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
### 3. Add the plugin to your OpenCode config
|
|
49
|
-
|
|
50
|
-
Add to your global config (`~/.config/opencode/opencode.json` or `opencode.jsonc`):
|
|
38
|
+
Add to OpenCode config (`~/.config/opencode/opencode.json`):
|
|
51
39
|
|
|
52
40
|
```json
|
|
53
41
|
{
|
|
54
|
-
"plugin": [
|
|
55
|
-
"opencode-fast-apply"
|
|
56
|
-
]
|
|
42
|
+
"plugin": ["opencode-fast-apply"]
|
|
57
43
|
}
|
|
58
44
|
```
|
|
59
45
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
### 4. Restart OpenCode
|
|
63
|
-
|
|
64
|
-
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.
|
|
65
47
|
|
|
66
48
|
## Usage
|
|
67
49
|
|
|
68
|
-
|
|
50
|
+
```typescript
|
|
51
|
+
// 1. Read relevant section (not entire file)
|
|
52
|
+
const content = await read("src/app.ts", { offset: 100, limit: 50 })
|
|
69
53
|
|
|
70
|
-
|
|
54
|
+
// 2. Edit with partial context
|
|
71
55
|
fast_apply_edit({
|
|
72
|
-
target_filepath: "
|
|
73
|
-
|
|
56
|
+
target_filepath: "src/app.ts",
|
|
57
|
+
original_code: content, // Just 50 lines!
|
|
74
58
|
code_edit: `// ... existing code ...
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
throw new Error("Token is required");
|
|
78
|
-
}
|
|
79
|
-
// ... existing code ...
|
|
59
|
+
function updated() {
|
|
60
|
+
return "modified";
|
|
80
61
|
}
|
|
81
62
|
// ... existing code ...`
|
|
82
63
|
})
|
|
83
64
|
```
|
|
84
65
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
| Large file (500+ lines) | `fast_apply_edit` | Handles partial snippets |
|
|
91
|
-
| Multiple scattered changes | `fast_apply_edit` | Batch efficiently |
|
|
92
|
-
| 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)
|
|
93
71
|
|
|
94
72
|
## Configuration
|
|
95
73
|
|
|
96
74
|
| Variable | Default | Description |
|
|
97
75
|
|----------|---------|-------------|
|
|
98
|
-
| `FAST_APPLY_API_KEY` | `optional-api-key` | API key (optional for local
|
|
99
|
-
| `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 |
|
|
100
78
|
| `FAST_APPLY_MODEL` | `fastapply-1.5b` | Model name |
|
|
101
|
-
| `FAST_APPLY_TEMPERATURE` | `0.05` | Temperature (0.0-2.0) |
|
|
102
79
|
|
|
103
80
|
## How It Works
|
|
104
81
|
|
|
105
|
-
1.
|
|
106
|
-
2.
|
|
107
|
-
3.
|
|
108
|
-
4.
|
|
109
|
-
5.
|
|
110
|
-
6.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
|
118
|
-
|
|
119
|
-
|
|
|
120
|
-
| codellama:7b (Q4) + RTX 3060 | 3,000-5,000 tok/s | Mid-range GPU |
|
|
121
|
-
| codellama:7b (Q4) + CPU only | 50-200 tok/s | Modern CPU |
|
|
122
|
-
| OpenAI GPT-4 API | 100-500 tok/s | Network dependent |
|
|
123
|
-
|
|
124
|
-
**Factors affecting performance:**
|
|
125
|
-
- Model size (1.5B vs 7B vs 13B+ parameters)
|
|
126
|
-
- Quantization level (Q4 vs Q5 vs Q8 vs FP16)
|
|
127
|
-
- Hardware (GPU VRAM, CPU cores, RAM)
|
|
128
|
-
- Backend optimization (LM Studio vs Ollama)
|
|
129
|
-
|
|
130
|
-
## Supported Backends
|
|
131
|
-
|
|
132
|
-
- **LM Studio** - Local inference server with GPU acceleration
|
|
133
|
-
- **Ollama** - Local LLM runtime with easy model management
|
|
134
|
-
- **OpenAI** - Cloud API with high reliability
|
|
135
|
-
- **Any OpenAI-compatible endpoint** - Custom servers and providers
|
|
136
|
-
|
|
137
|
-
## Edge Cases Handled
|
|
138
|
-
|
|
139
|
-
- ✅ String literals containing `<updated-code>` tags
|
|
140
|
-
- ✅ Multiple XML-like tags in regex patterns
|
|
141
|
-
- ✅ Special characters (quotes, backslashes, unicode, SQL, HTML entities)
|
|
142
|
-
- ✅ Large files (500+ lines)
|
|
143
|
-
- ✅ Multiple scattered changes in single edit
|
|
144
|
-
- ✅ Complex nested structures
|
|
145
|
-
- ✅ Template strings with `${variable}`
|
|
146
|
-
- ✅ Whitespace and indentation preservation
|
|
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% |
|
|
147
97
|
|
|
148
98
|
## Troubleshooting
|
|
149
99
|
|
|
150
|
-
|
|
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**
|
|
151
106
|
```bash
|
|
152
|
-
# Test your endpoint
|
|
153
107
|
curl -X POST http://localhost:1234/v1/chat/completions \
|
|
154
108
|
-H "Content-Type: application/json" \
|
|
155
109
|
-d '{"model":"fastapply-1.5b","messages":[{"role":"user","content":"test"}]}'
|
|
156
110
|
```
|
|
157
111
|
|
|
158
|
-
### Slow Performance
|
|
159
|
-
- Use smaller models (1.5B-3B parameters)
|
|
160
|
-
- Enable GPU acceleration in LM Studio/Ollama
|
|
161
|
-
- Use Q4 quantization for faster inference
|
|
162
|
-
- Increase `FAST_APPLY_MAX_TOKENS` if responses are truncated
|
|
163
|
-
|
|
164
|
-
## Contributing
|
|
165
|
-
|
|
166
|
-
Contributions welcome! This plugin could potentially be integrated into OpenCode core.
|
|
167
|
-
|
|
168
112
|
## License
|
|
169
113
|
|
|
170
|
-
|
|
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,140 +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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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.
|
|
26
|
+
|
|
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
|
|
25
32
|
|
|
26
33
|
<code>{original_code}</code>
|
|
27
34
|
|
|
28
35
|
<update>{update_snippet}</update>
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const UPDATED_CODE_END = "</updated-code>";
|
|
33
|
-
const TOOL_INSTRUCTIONS = `**DEFAULT tool for editing existing files. Use INSTEAD of native 'edit' tool.**
|
|
37
|
+
Output complete merged code:`;
|
|
38
|
+
const TOOL_INSTRUCTIONS = `DEFAULT tool for editing existing files. Use INSTEAD of native 'edit' tool.
|
|
34
39
|
|
|
35
40
|
CRITICAL: For EXISTING files ONLY. Use 'write' for new files.
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
1.
|
|
39
|
-
2.
|
|
40
|
-
3.
|
|
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
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
// ... existing code ...
|
|
51
|
-
\`\`\`
|
|
52
|
-
|
|
53
|
-
## Rules
|
|
54
|
-
- MANDATORY: Use \`// ... existing code ...\` markers
|
|
55
|
-
- Include 2-3 lines context before/after edits
|
|
56
|
-
- Preserve exact indentation
|
|
57
|
-
- ONE edit block per call (multiple blocks = suboptimal results)
|
|
58
|
-
- Deletions: show context, omit deleted lines
|
|
59
|
-
- NEVER for new files
|
|
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
|
|
60
57
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
**Add function:**
|
|
58
|
+
FORMAT:
|
|
59
|
+
Use \`// ... existing code ...\` markers for unchanged sections:
|
|
64
60
|
\`\`\`
|
|
65
61
|
// ... existing code ...
|
|
66
|
-
|
|
67
|
-
// ... existing code ...
|
|
68
|
-
|
|
69
|
-
function newFeature() {
|
|
70
|
-
return newDep.process();
|
|
71
|
-
}
|
|
62
|
+
function updated() { return "modified"; }
|
|
72
63
|
// ... existing code ...
|
|
73
64
|
\`\`\`
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// ... existing code ...
|
|
83
|
-
\`\`\`
|
|
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)
|
|
84
73
|
|
|
85
|
-
|
|
86
|
-
\`\`\`
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
return "stays";
|
|
90
|
-
}
|
|
74
|
+
EXAMPLE:
|
|
75
|
+
\`\`\`typescript
|
|
76
|
+
// 1. Read file
|
|
77
|
+
const content = await read("src/app.ts", { offset: 100, limit: 50 })
|
|
91
78
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//
|
|
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
|
+
})
|
|
96
85
|
\`\`\`
|
|
97
86
|
|
|
98
|
-
|
|
99
|
-
If API fails, use native \`edit\` tool with exact string matching.`;
|
|
87
|
+
FALLBACK: If API fails, use native 'edit' tool.`;
|
|
100
88
|
function escapeXmlTags(text) {
|
|
101
89
|
return text
|
|
102
|
-
.replace(
|
|
103
|
-
.replace(
|
|
90
|
+
.replace(/&/g, "&")
|
|
91
|
+
.replace(/</g, "<")
|
|
92
|
+
.replace(/>/g, ">")
|
|
93
|
+
.replace(/"/g, """)
|
|
94
|
+
.replace(/'/g, "'");
|
|
104
95
|
}
|
|
105
96
|
function unescapeXmlTags(text) {
|
|
106
97
|
return text
|
|
107
|
-
.replace(/<
|
|
108
|
-
.replace(/&
|
|
98
|
+
.replace(/</g, "<")
|
|
99
|
+
.replace(/>/g, ">")
|
|
100
|
+
.replace(/"/g, '"')
|
|
101
|
+
.replace(/'/g, "'")
|
|
102
|
+
.replace(/&/g, "&");
|
|
109
103
|
}
|
|
110
|
-
function
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
startIdx = stripped.indexOf("<updated-code");
|
|
117
|
-
if (startIdx !== -1) {
|
|
118
|
-
const closeTagIdx = stripped.indexOf(">", startIdx);
|
|
119
|
-
if (closeTagIdx !== -1) {
|
|
120
|
-
startIdx = closeTagIdx + 1;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
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");
|
|
123
110
|
}
|
|
124
|
-
|
|
125
|
-
|
|
111
|
+
if (unescaped.includes("<code>") || unescaped.includes("</code>")) {
|
|
112
|
+
throw new Error("Content contains unescaped <code> tags that could break prompt structure");
|
|
126
113
|
}
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
const lines = stripped.split("\n");
|
|
130
|
-
if (lines.length >= 2) {
|
|
131
|
-
return unescapeXmlTags(lines.slice(1, -1).join("\n"));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return unescapeXmlTags(stripped);
|
|
114
|
+
if (unescaped.includes("<update>") || unescaped.includes("</update>")) {
|
|
115
|
+
throw new Error("Content contains unescaped <update> tags that could break prompt structure");
|
|
135
116
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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");
|
|
139
125
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
return unescapeXmlTags(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");
|
|
147
131
|
}
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
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");
|
|
151
136
|
}
|
|
152
|
-
|
|
137
|
+
const unescaped = unescapeXmlTags(inner);
|
|
138
|
+
validateNoNestedTags(unescaped);
|
|
139
|
+
return unescaped;
|
|
153
140
|
}
|
|
154
141
|
function generateUnifiedDiff(filepath, original, modified) {
|
|
155
142
|
const patch = createTwoFilesPatch(`a/${filepath}`, `b/${filepath}`, original, modified, "", "", { context: 3 });
|
|
@@ -193,6 +180,94 @@ function shortenPath(filePath, workingDir) {
|
|
|
193
180
|
function estimateTokens(text) {
|
|
194
181
|
return Math.ceil(text.length / 4);
|
|
195
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
|
+
}
|
|
196
271
|
function formatFastApplyResult(filePath, workingDir, insertions, deletions, diffPreview, modifiedTokens) {
|
|
197
272
|
const shortPath = shortenPath(filePath, workingDir);
|
|
198
273
|
const tokenStr = formatTokenCount(modifiedTokens);
|
|
@@ -221,7 +296,7 @@ function formatErrorOutput(error, filePath, workingDir) {
|
|
|
221
296
|
/**
|
|
222
297
|
* Call OpenAI's Fast Apply API to merge code edits
|
|
223
298
|
*/
|
|
224
|
-
async function callFastApply(originalCode, codeEdit
|
|
299
|
+
async function callFastApply(originalCode, codeEdit) {
|
|
225
300
|
if (!FAST_APPLY_API_KEY) {
|
|
226
301
|
return {
|
|
227
302
|
success: false,
|
|
@@ -252,7 +327,7 @@ async function callFastApply(originalCode, codeEdit, instructions) {
|
|
|
252
327
|
content: userContent,
|
|
253
328
|
},
|
|
254
329
|
],
|
|
255
|
-
temperature:
|
|
330
|
+
temperature: 0,
|
|
256
331
|
}),
|
|
257
332
|
});
|
|
258
333
|
if (!response.ok) {
|
|
@@ -270,11 +345,20 @@ async function callFastApply(originalCode, codeEdit, instructions) {
|
|
|
270
345
|
error: "Fast Apply API returned empty response",
|
|
271
346
|
};
|
|
272
347
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
}
|
|
278
362
|
}
|
|
279
363
|
catch (err) {
|
|
280
364
|
const error = err;
|
|
@@ -361,15 +445,26 @@ export const FastApplyPlugin = async ({ directory, client }) => {
|
|
|
361
445
|
target_filepath: tool.schema
|
|
362
446
|
.string()
|
|
363
447
|
.describe("Path of the file to modify (relative to project root)"),
|
|
364
|
-
|
|
448
|
+
original_code: tool.schema
|
|
365
449
|
.string()
|
|
366
|
-
.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`),
|
|
367
462
|
code_edit: tool.schema
|
|
368
463
|
.string()
|
|
369
|
-
.describe('The code changes
|
|
464
|
+
.describe('The updated code with changes applied. Use "// ... existing code ..." markers for unchanged sections within this context.'),
|
|
370
465
|
},
|
|
371
466
|
async execute(args, toolCtx) {
|
|
372
|
-
const { target_filepath,
|
|
467
|
+
const { target_filepath, original_code, code_edit } = args;
|
|
373
468
|
const params = sessionParamsCache.get(toolCtx.sessionID) || {};
|
|
374
469
|
// Resolve file path relative to project directory
|
|
375
470
|
const filepath = target_filepath.startsWith("/")
|
|
@@ -384,16 +479,12 @@ Get your API key at: https://openai.com/api
|
|
|
384
479
|
|
|
385
480
|
Alternatively, use the native 'edit' tool for this change.`;
|
|
386
481
|
}
|
|
387
|
-
//
|
|
388
|
-
let originalCode;
|
|
482
|
+
// Check if file exists and is writable
|
|
389
483
|
try {
|
|
390
|
-
await access(filepath, constants.R_OK);
|
|
391
|
-
originalCode = await readFile(filepath, "utf-8");
|
|
484
|
+
await access(filepath, constants.R_OK | constants.W_OK);
|
|
392
485
|
}
|
|
393
486
|
catch (err) {
|
|
394
|
-
|
|
395
|
-
if (error.message.includes("ENOENT") || error.message.includes("no such file")) {
|
|
396
|
-
return `Error: File not found: ${target_filepath}
|
|
487
|
+
return `Error: File not found or not writable: ${target_filepath}
|
|
397
488
|
|
|
398
489
|
This tool is for EDITING EXISTING FILES ONLY.
|
|
399
490
|
For new file creation, use the 'write' tool instead.
|
|
@@ -403,26 +494,33 @@ write({
|
|
|
403
494
|
filePath: "${target_filepath}",
|
|
404
495
|
content: "your file content here"
|
|
405
496
|
})`;
|
|
406
|
-
}
|
|
407
|
-
return `Error reading file ${target_filepath}: ${error.message}`;
|
|
408
497
|
}
|
|
409
|
-
// Call
|
|
410
|
-
const result = await callFastApply(
|
|
498
|
+
// Call Fast Apply API to merge the edit
|
|
499
|
+
const result = await callFastApply(original_code, code_edit);
|
|
411
500
|
if (!result.success || !result.content) {
|
|
412
501
|
const errorMsg = result.error || "Unknown error";
|
|
413
502
|
await sendTUIErrorNotification(client, toolCtx.sessionID, target_filepath, directory, errorMsg, params);
|
|
414
503
|
return formatErrorOutput(errorMsg, target_filepath, directory);
|
|
415
504
|
}
|
|
416
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
|
|
417
513
|
try {
|
|
418
|
-
await writeFile(filepath,
|
|
514
|
+
await writeFile(filepath, applyResult.newFileContent, "utf-8");
|
|
419
515
|
}
|
|
420
516
|
catch (err) {
|
|
421
517
|
const error = err;
|
|
422
518
|
await sendTUIErrorNotification(client, toolCtx.sessionID, target_filepath, directory, error.message, params);
|
|
423
519
|
return formatErrorOutput(error.message, target_filepath, directory);
|
|
424
520
|
}
|
|
425
|
-
|
|
521
|
+
// Read origfile for diff comparison
|
|
522
|
+
const originalFileContent = await readFile(filepath, "utf-8");
|
|
523
|
+
const diff = generateUnifiedDiff(target_filepath, originalFileContent, applyResult.newFileContent);
|
|
426
524
|
const { added, removed } = countChanges(diff);
|
|
427
525
|
const modifiedTokens = estimateTokens(diff);
|
|
428
526
|
await sendTUINotification(client, toolCtx.sessionID, target_filepath, directory, added, removed, modifiedTokens, params);
|
package/package.json
CHANGED