opencode-fast-apply 2.1.7 → 2.2.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/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 (LM Studio, Ollama).
3
+ OpenCode plugin for Fast Apply - High-performance code editing with OpenAI-compatible APIs.
4
4
 
5
5
  ## Features
6
6
 
7
- - **High-speed code editing** via OpenAI-compatible Fast Apply API (speed depends on your hardware and model)
8
- - **Lazy edit markers** (`// ... existing code ...`) - no exact string matching needed
9
- - **Unified diff output** with context for easy review
10
- - **Graceful fallback** - suggests native `edit` tool on API failure
11
- - **Multi-backend support** - LM Studio, Ollama, OpenAI, and any OpenAI-compatible endpoint
12
- - **Unique delimiter system** - generates random delimiters per request (`<<<RESULT_xxxx>>>`) to eliminate parsing conflicts
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
- ### 2. Configure your API endpoint
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
- For **Ollama**:
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
- For **OpenAI**:
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-openai-key"
35
+ export FAST_APPLY_API_KEY="sk-your-key"
45
36
  ```
46
37
 
47
- **Note:** The plugin automatically handles URLs with or without `/v1` suffix.
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
- **That's it!** The plugin automatically embeds all instructions - no additional configuration needed.
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
- The LLM can use `fast_apply_edit` for efficient partial file edits:
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: "sth.ts",
74
- instructions: "Add error handling for invalid tokens",
56
+ target_filepath: "src/app.ts",
57
+ original_code: content, // Just 50 lines!
75
58
  code_edit: `// ... existing code ...
76
- function validateToken(token) {
77
- if (!token) {
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
- ### When to use `fast_apply_edit` vs `edit`
87
-
88
- | Situation | Tool | Reason |
89
- |-----------|------|--------|
90
- | Small, exact replacement | `edit` | Fast, no API call |
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 servers) |
100
- | `FAST_APPLY_URL` | `http://localhost:1234/v1` | OpenAI-compatible API endpoint |
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. Reads the original file content
107
- 2. Generates unique random delimiters for this request (e.g., `<<<RESULT_a3f9>>>`)
108
- 3. Sends system prompt + user prompt with unique delimiters to OpenAI-compatible API
109
- 4. API intelligently merges the lazy edit markers with original code
110
- 5. Extracts result using the unique delimiters with fallback pattern detection
111
- 6. Writes the merged result back to the file
112
- 7. Returns a unified diff showing what changed
113
-
114
- **Delimiter Design:** Each request generates unique 4-character alphanumeric IDs (1,679,616 combinations) for delimiters like `<<<RESULT_xxxx>>>` and `<<<END_RESULT_xxxx>>>`. This eliminates parsing conflicts even if your code contains similar patterns. Fallback logic detects any `<<<RESULT_*>>>` pattern for maximum robustness.
115
-
116
- ## Performance
117
-
118
- Performance varies based on your setup:
119
-
120
- | Setup | Estimated Speed | Hardware Requirement |
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
- ### API Connection Issues
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
- [MIT](LICENSE)
114
+ MIT
@@ -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;AAgbvD,eAAO,MAAM,eAAe,EAAE,MA4J7B,CAAA;AAGD,eAAe,eAAe,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;AA4hBvD,eAAO,MAAM,eAAe,EAAE,MAiL7B,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 FAST_APPLY_TEMPERATURE = parseFloat(process.env.FAST_APPLY_TEMPERATURE || "0.05");
20
- const FAST_APPLY_SYSTEM_PROMPT = "You are a coding assistant that helps merge code updates, ensuring every modification is fully integrated.";
21
- const TOOL_INSTRUCTIONS = `**DEFAULT tool for editing existing files. Use INSTEAD of native 'edit' tool.**
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
- CRITICAL: For EXISTING files ONLY. Use 'write' for new files.
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
- ## Priority
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
- ## Format
31
- Use \`// ... existing code ...\` for unchanged sections:
35
+ <update>{update_snippet}</update>
32
36
 
33
- \`\`\`
34
- // ... existing code ...
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
- ## Rules
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
- ## Examples
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
- **Add function:**
52
- \`\`\`
53
- // ... existing code ...
54
- import { newDep } from './newDep';
55
- // ... existing code ...
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
- function newFeature() {
58
- return newDep.process();
59
- }
60
- // ... existing code ...
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
- **Modify:**
58
+ FORMAT:
59
+ Use \`// ... existing code ...\` markers for unchanged sections:
64
60
  \`\`\`
65
61
  // ... existing code ...
66
- function existingFunc(param) {
67
- const result = param * 2;
68
- return result;
69
- }
62
+ function updated() { return "modified"; }
70
63
  // ... existing code ...
71
64
  \`\`\`
72
65
 
73
- **Delete:**
74
- \`\`\`
75
- // ... existing code ...
76
- function keepThis() {
77
- return "stays";
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
- function alsoKeepThis() {
81
- return "stays";
82
- }
83
- // ... existing code ...
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
- ## Fallback
87
- If API fails, use native \`edit\` tool with exact string matching.`;
88
- function generateRandomId() {
89
- const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
90
- let result = '';
91
- for (let i = 0; i < 4; i++) {
92
- result += chars.charAt(Math.floor(Math.random() * chars.length));
93
- }
94
- return result;
87
+ FALLBACK: If API fails, use native 'edit' tool.`;
88
+ function escapeXmlTags(text) {
89
+ return text
90
+ .replace(/&/g, "&amp;")
91
+ .replace(/</g, "&lt;")
92
+ .replace(/>/g, "&gt;")
93
+ .replace(/"/g, "&quot;")
94
+ .replace(/'/g, "&apos;");
95
95
  }
96
- function generateUniqueDelimiters() {
97
- const id = generateRandomId();
98
- return {
99
- ORIGINAL_START: `<<<ORIGINAL_${id}>>>`,
100
- ORIGINAL_END: `<<<END_ORIGINAL_${id}>>>`,
101
- UPDATE_START: `<<<UPDATE_${id}>>>`,
102
- UPDATE_END: `<<<END_UPDATE_${id}>>>`,
103
- RESULT_START: `<<<RESULT_${id}>>>`,
104
- RESULT_END: `<<<END_RESULT_${id}>>>`
105
- };
96
+ function unescapeXmlTags(text) {
97
+ return text
98
+ .replace(/&lt;/g, "<")
99
+ .replace(/&gt;/g, ">")
100
+ .replace(/&quot;/g, '"')
101
+ .replace(/&apos;/g, "'")
102
+ .replace(/&amp;/g, "&");
106
103
  }
107
- function extractUpdatedCode(raw, resultStart, resultEnd) {
108
- const stripped = raw.trim();
109
- let startIdx = stripped.indexOf(resultStart);
110
- if (startIdx === -1) {
111
- const genericStart = stripped.indexOf("<<<RESULT_");
112
- if (genericStart !== -1) {
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(/&lt;/g, "<")
107
+ .replace(/&gt;/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
- else {
120
- startIdx += resultStart.length;
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 (startIdx === -1 || startIdx === resultStart.length - 1) {
123
- if (stripped.startsWith("```") && stripped.endsWith("```")) {
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
- let endIdx = stripped.indexOf(resultEnd, startIdx);
132
- if (endIdx === -1) {
133
- const genericEnd = stripped.indexOf("<<<END_RESULT_", startIdx);
134
- if (genericEnd !== -1) {
135
- endIdx = genericEnd;
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
- if (endIdx === -1) {
139
- const extracted = stripped.slice(startIdx).trim();
140
- const lastCloseTag = extracted.lastIndexOf("<<<");
141
- if (lastCloseTag !== -1 && extracted.slice(lastCloseTag).toLowerCase().includes("end")) {
142
- return extracted.slice(0, lastCloseTag).trim();
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 inner = stripped.substring(startIdx, endIdx);
147
- if (!inner || inner.trim().length === 0) {
148
- throw new Error("Empty result block");
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
- return inner;
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,97 @@ function shortenPath(filePath, workingDir) {
191
180
  function estimateTokens(text) {
192
181
  return Math.ceil(text.length / 4);
193
182
  }
183
+ function formatDiffForMarkdown(diff) {
184
+ return "```diff\n" + diff + "\n```";
185
+ }
186
+ function normalizeWhitespace(text) {
187
+ return text
188
+ .split('\n')
189
+ .map(line => line.trimEnd())
190
+ .join('\n')
191
+ .replace(/\r\n/g, '\n')
192
+ .trim();
193
+ }
194
+ function findExactMatch(haystack, needle) {
195
+ return haystack.indexOf(needle);
196
+ }
197
+ function findNormalizedMatch(haystack, needle) {
198
+ const normalizedHaystack = normalizeWhitespace(haystack);
199
+ const normalizedNeedle = normalizeWhitespace(needle);
200
+ const index = normalizedHaystack.indexOf(normalizedNeedle);
201
+ if (index === -1)
202
+ return -1;
203
+ let actualIndex = 0;
204
+ let normalizedIndex = 0;
205
+ while (normalizedIndex < index && actualIndex < haystack.length) {
206
+ const char = haystack[actualIndex];
207
+ const normalizedChar = normalizedHaystack[normalizedIndex];
208
+ if (char === '\r' && haystack[actualIndex + 1] === '\n') {
209
+ actualIndex += 2;
210
+ normalizedIndex += 1;
211
+ }
212
+ else if (char === normalizedChar) {
213
+ actualIndex++;
214
+ normalizedIndex++;
215
+ }
216
+ else {
217
+ actualIndex++;
218
+ }
219
+ }
220
+ return actualIndex;
221
+ }
222
+ async function applyPartialEdit(filepath, original_code, merged_code) {
223
+ const currentFile = await readFile(filepath, "utf-8");
224
+ if (currentFile.includes('\0')) {
225
+ return {
226
+ success: false,
227
+ error: "Cannot edit binary files"
228
+ };
229
+ }
230
+ let index = findExactMatch(currentFile, original_code);
231
+ let matchType = "exact";
232
+ if (index === -1) {
233
+ index = findNormalizedMatch(currentFile, original_code);
234
+ matchType = "normalized";
235
+ }
236
+ if (index === -1) {
237
+ return {
238
+ success: false,
239
+ error: `Cannot locate original_code in ${filepath}.
240
+
241
+ The content you provided doesn't match the current file.
242
+
243
+ POSSIBLE CAUSES:
244
+ - File was modified since you read it
245
+ - Whitespace or indentation differs
246
+ - Wrong section provided
247
+ - File encoding issues
248
+
249
+ SOLUTIONS:
250
+ 1. Re-read the file to get current content
251
+ 2. Verify exact whitespace and indentation
252
+ 3. Provide more context (more surrounding lines)
253
+ 4. Use native 'edit' tool for exact string matching`
254
+ };
255
+ }
256
+ const occurrences = currentFile.split(original_code).length - 1;
257
+ if (occurrences > 1) {
258
+ return {
259
+ success: false,
260
+ error: `original_code appears ${occurrences} times in ${filepath}.
261
+
262
+ Please provide more context (more surrounding lines) to uniquely identify the section you want to edit.`
263
+ };
264
+ }
265
+ const newFileContent = currentFile.substring(0, index) +
266
+ merged_code +
267
+ currentFile.substring(index + original_code.length);
268
+ console.log(`[fast-apply] Applied ${matchType} match at position ${index}`);
269
+ return {
270
+ success: true,
271
+ newFileContent
272
+ };
273
+ }
194
274
  function formatFastApplyResult(filePath, workingDir, insertions, deletions, diffPreview, modifiedTokens) {
195
275
  const shortPath = shortenPath(filePath, workingDir);
196
276
  const tokenStr = formatTokenCount(modifiedTokens);
@@ -219,7 +299,7 @@ function formatErrorOutput(error, filePath, workingDir) {
219
299
  /**
220
300
  * Call OpenAI's Fast Apply API to merge code edits
221
301
  */
222
- async function callFastApply(originalCode, codeEdit, instructions) {
302
+ async function callFastApply(originalCode, codeEdit) {
223
303
  if (!FAST_APPLY_API_KEY) {
224
304
  return {
225
305
  success: false,
@@ -227,21 +307,11 @@ async function callFastApply(originalCode, codeEdit, instructions) {
227
307
  };
228
308
  }
229
309
  try {
230
- const delimiters = generateUniqueDelimiters();
231
- const userContent = `Merge all changes from the UPDATE_BLOCK into the ORIGINAL_BLOCK below.
232
- - Preserve the code's structure, order, comments, and indentation exactly.
233
- - Output only the updated code, enclosed within ${delimiters.RESULT_START} and ${delimiters.RESULT_END} delimiters.
234
- - Do not include any additional text, explanations, placeholders, ellipses, or code fences.
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}.`;
310
+ const escapedOriginalCode = escapeXmlTags(originalCode);
311
+ const escapedCodeEdit = escapeXmlTags(codeEdit);
312
+ const userContent = FAST_APPLY_USER_PROMPT
313
+ .replace("{original_code}", escapedOriginalCode)
314
+ .replace("{update_snippet}", escapedCodeEdit);
245
315
  const response = await fetch(`${FAST_APPLY_URL}/v1/chat/completions`, {
246
316
  method: "POST",
247
317
  headers: {
@@ -260,7 +330,7 @@ Provide the complete updated code wrapped in ${delimiters.RESULT_START} and ${de
260
330
  content: userContent,
261
331
  },
262
332
  ],
263
- temperature: FAST_APPLY_TEMPERATURE,
333
+ temperature: 0,
264
334
  }),
265
335
  });
266
336
  if (!response.ok) {
@@ -278,11 +348,20 @@ Provide the complete updated code wrapped in ${delimiters.RESULT_START} and ${de
278
348
  error: "Fast Apply API returned empty response",
279
349
  };
280
350
  }
281
- const mergedCode = extractUpdatedCode(rawResponse, delimiters.RESULT_START, delimiters.RESULT_END);
282
- return {
283
- success: true,
284
- content: mergedCode,
285
- };
351
+ try {
352
+ const mergedCode = extractUpdatedCode(rawResponse);
353
+ return {
354
+ success: true,
355
+ content: mergedCode,
356
+ };
357
+ }
358
+ catch (parseError) {
359
+ const error = parseError;
360
+ return {
361
+ success: false,
362
+ error: `Failed to parse AI response: ${error.message}`,
363
+ };
364
+ }
286
365
  }
287
366
  catch (err) {
288
367
  const error = err;
@@ -323,14 +402,15 @@ async function sendTUIMessage(client, sessionID, message, params) {
323
402
  console.error("[fast-apply] Failed to send TUI notification:", error.message);
324
403
  }
325
404
  }
326
- async function sendTUINotification(client, sessionID, filePath, workingDir, insertions, deletions, modifiedTokens, params) {
405
+ async function sendTUINotification(client, sessionID, filePath, workingDir, insertions, deletions, modifiedTokens, diff, params) {
327
406
  const shortPath = shortenPath(filePath, workingDir);
328
407
  const tokenStr = formatTokenCount(modifiedTokens);
329
408
  const message = [
330
409
  `▣ Fast Apply | ~${tokenStr} tokens modified`,
331
410
  "",
332
- "Applied changes:",
333
- `→ ${shortPath}: +${insertions} -${deletions}`
411
+ `Applied changes to ${shortPath} (+${insertions} -${deletions}):`,
412
+ "",
413
+ formatDiffForMarkdown(diff)
334
414
  ].join("\n");
335
415
  await sendTUIMessage(client, sessionID, message, params);
336
416
  }
@@ -369,15 +449,26 @@ export const FastApplyPlugin = async ({ directory, client }) => {
369
449
  target_filepath: tool.schema
370
450
  .string()
371
451
  .describe("Path of the file to modify (relative to project root)"),
372
- instructions: tool.schema
452
+ original_code: tool.schema
373
453
  .string()
374
- .describe("Brief first-person description of what you're changing (helps disambiguate)"),
454
+ .describe(`The original code section to be modified.
455
+
456
+ IMPORTANT:
457
+ - Provide 50-500 lines of context around the area you want to change
458
+ - Include 2-5 lines before and after the target section
459
+ - Must match the current file content exactly (whitespace matters)
460
+ - Can be partial (doesn't need to be entire file)
461
+
462
+ WORKFLOW:
463
+ 1. Read the file first to get current content
464
+ 2. Extract the relevant section with context
465
+ 3. Provide that section as original_code`),
375
466
  code_edit: tool.schema
376
467
  .string()
377
- .describe('The code changes with "// ... existing code ..." markers for unchanged sections'),
468
+ .describe('The updated code with changes applied. Use "// ... existing code ..." markers for unchanged sections within this context.'),
378
469
  },
379
470
  async execute(args, toolCtx) {
380
- const { target_filepath, instructions, code_edit } = args;
471
+ const { target_filepath, original_code, code_edit } = args;
381
472
  const params = sessionParamsCache.get(toolCtx.sessionID) || {};
382
473
  // Resolve file path relative to project directory
383
474
  const filepath = target_filepath.startsWith("/")
@@ -392,16 +483,12 @@ Get your API key at: https://openai.com/api
392
483
 
393
484
  Alternatively, use the native 'edit' tool for this change.`;
394
485
  }
395
- // Read the original file
396
- let originalCode;
486
+ // Check if file exists and is writable
397
487
  try {
398
- await access(filepath, constants.R_OK);
399
- originalCode = await readFile(filepath, "utf-8");
488
+ await access(filepath, constants.R_OK | constants.W_OK);
400
489
  }
401
490
  catch (err) {
402
- const error = err;
403
- if (error.message.includes("ENOENT") || error.message.includes("no such file")) {
404
- return `Error: File not found: ${target_filepath}
491
+ return `Error: File not found or not writable: ${target_filepath}
405
492
 
406
493
  This tool is for EDITING EXISTING FILES ONLY.
407
494
  For new file creation, use the 'write' tool instead.
@@ -411,29 +498,36 @@ write({
411
498
  filePath: "${target_filepath}",
412
499
  content: "your file content here"
413
500
  })`;
414
- }
415
- return `Error reading file ${target_filepath}: ${error.message}`;
416
501
  }
417
- // Call OpenAI API to merge the edit
418
- const result = await callFastApply(originalCode, code_edit, instructions);
502
+ // Call Fast Apply API to merge the edit
503
+ const result = await callFastApply(original_code, code_edit);
419
504
  if (!result.success || !result.content) {
420
505
  const errorMsg = result.error || "Unknown error";
421
506
  await sendTUIErrorNotification(client, toolCtx.sessionID, target_filepath, directory, errorMsg, params);
422
507
  return formatErrorOutput(errorMsg, target_filepath, directory);
423
508
  }
424
509
  const mergedCode = result.content;
510
+ // Apply partial edit with smart matching
511
+ const applyResult = await applyPartialEdit(filepath, original_code, mergedCode);
512
+ if (!applyResult.success) {
513
+ await sendTUIErrorNotification(client, toolCtx.sessionID, target_filepath, directory, applyResult.error, params);
514
+ return formatErrorOutput(applyResult.error, target_filepath, directory);
515
+ }
516
+ // Write merged file back
425
517
  try {
426
- await writeFile(filepath, mergedCode, "utf-8");
518
+ await writeFile(filepath, applyResult.newFileContent, "utf-8");
427
519
  }
428
520
  catch (err) {
429
521
  const error = err;
430
522
  await sendTUIErrorNotification(client, toolCtx.sessionID, target_filepath, directory, error.message, params);
431
523
  return formatErrorOutput(error.message, target_filepath, directory);
432
524
  }
433
- const diff = generateUnifiedDiff(target_filepath, originalCode, mergedCode);
525
+ // Read origfile for diff comparison
526
+ const originalFileContent = await readFile(filepath, "utf-8");
527
+ const diff = generateUnifiedDiff(target_filepath, originalFileContent, applyResult.newFileContent);
434
528
  const { added, removed } = countChanges(diff);
435
529
  const modifiedTokens = estimateTokens(diff);
436
- await sendTUINotification(client, toolCtx.sessionID, target_filepath, directory, added, removed, modifiedTokens, params);
530
+ await sendTUINotification(client, toolCtx.sessionID, target_filepath, directory, added, removed, modifiedTokens, diff, params);
437
531
  return formatFastApplyResult(target_filepath, directory, added, removed, diff, modifiedTokens);
438
532
  },
439
533
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-fast-apply",
3
- "version": "2.1.7",
3
+ "version": "2.2.1",
4
4
  "description": "OpenCode plugin for Fast Apply - High-performance code editing with OpenAI-compatible APIs (LM Studio, Ollama)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",