This commit is contained in:
bhaktatejas922 2025-08-22 01:32:13 +01:00 committed by GitHub
commit c69f200452
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 453 additions and 88 deletions

View file

@ -48,6 +48,17 @@ OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bas
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
``` ```
### Morph Fast Apply (Optional)
For faster, more accurate code edits, opencode can integrate with [Morph](https://morphllm.com) - an AI model specialized for code merging at 4500+ tokens/second with 98.8% accuracy.
```bash
# Enable Morph Fast Apply
export MORPH_API_KEY=your_api_key
```
When enabled, opencode's edit tool automatically uses Morph's intelligent code merging instead of search-and-replace, supporting multiple edits and `// ... existing code ...` syntax.
### Documentation ### Documentation
For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs). For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).

View file

@ -9,7 +9,10 @@ import { Tool } from "./tool"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import { createTwoFilesPatch } from "diff" import { createTwoFilesPatch } from "diff"
import { Permission } from "../permission" import { Permission } from "../permission"
// @ts-ignore
import DESCRIPTION from "./edit.txt" import DESCRIPTION from "./edit.txt"
// @ts-ignore
import MORPH_DESCRIPTION from "./edit-morph.txt"
import { App } from "../app/app" import { App } from "../app/app"
import { File } from "../file" import { File } from "../file"
import { Bus } from "../bus" import { Bus } from "../bus"
@ -17,65 +20,165 @@ import { FileTime } from "../file/time"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
import { Agent } from "../agent/agent" import { Agent } from "../agent/agent"
export const EditTool = Tool.define("edit", { // OpenAI-compatible client for Morph API
description: DESCRIPTION, class MorphClient {
parameters: z.object({ private apiKey: string
filePath: z.string().describe("The absolute path to the file to modify"), private baseURL = "https://api.morphllm.com/v1"
oldString: z.string().describe("The text to replace"),
newString: z.string().describe("The text to replace it with (must be different from oldString)"), constructor(apiKey: string) {
replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)"), this.apiKey = apiKey
}), }
async execute(params, ctx) {
if (!params.filePath) { async apply(instruction: string, initialCode: string, codeEdit: string): Promise<string> {
throw new Error("filePath is required") const response = await fetch(`${this.baseURL}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
model: "morph-v3-large",
messages: [
{
role: "user",
content: `<instruction>${instruction}</instruction>\n<code>${initialCode}</code>\n<update>${codeEdit}</update>`,
},
],
}),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Morph API error: ${response.status} ${response.statusText}\n${errorText}`)
} }
if (params.oldString === params.newString) { const result = await response.json()
throw new Error("oldString and newString must be different") return result.choices[0].message.content
}
}
async function executeMorphEdit(
params: { target_file: string; instructions: string; code_edit: string },
ctx: any,
morphApiKey: string
) {
if (!params.target_file) {
throw new Error("target_file is required")
}
if (!params.instructions) {
throw new Error("instructions is required")
}
if (!params.code_edit) {
throw new Error("code_edit is required")
}
const app = App.info()
const filePath = path.isAbsolute(params.target_file) ? params.target_file : path.join(app.path.cwd, params.target_file)
if (!Filesystem.contains(app.path.cwd, filePath)) {
throw new Error(`File ${filePath} is not in the current working directory`)
}
const agent = await Agent.get(ctx.agent)
// Read the existing file
const file = Bun.file(filePath)
const stats = await file.stat().catch(() => {})
if (!stats) throw new Error(`File ${filePath} not found`)
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
await FileTime.assert(ctx.sessionID, filePath)
const initialCode = await file.text()
// Use Morph API to apply the edit
const morphClient = new MorphClient(morphApiKey)
let mergedCode: string
try {
mergedCode = await morphClient.apply(params.instructions, initialCode, params.code_edit)
} catch (error) {
throw new Error(`Failed to apply edit with Morph: ${error instanceof Error ? error.message : String(error)}`)
}
const diff = trimDiff(createTwoFilesPatch(filePath, filePath, initialCode, mergedCode))
// Check permissions if needed
if (agent.permission.edit === "ask") {
await Permission.ask({
type: "edit",
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: "🚀 Morph Fast Apply: " + filePath,
metadata: {
filePath,
diff,
morphApplied: true,
},
})
}
// Write the merged code to file
await Bun.write(filePath, mergedCode)
await Bus.publish(File.Event.Edited, {
file: filePath,
})
FileTime.read(ctx.sessionID, filePath)
let output = ""
await LSP.touchFile(filePath, true)
const diagnostics = await LSP.diagnostics()
for (const [file, issues] of Object.entries(diagnostics)) {
if (issues.length === 0) continue
if (file === filePath) {
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
continue
} }
output += `\n<project_diagnostics>\n${file}\n${issues
.filter((item) => item.severity === 1)
.map(LSP.Diagnostic.pretty)
.join("\n")}\n</project_diagnostics>\n`
}
const app = App.info() return {
const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath) metadata: {
if (!Filesystem.contains(app.path.cwd, filePath)) { diagnostics,
throw new Error(`File ${filePath} is not in the current working directory`) diff,
} morphApplied: true,
},
title: `🚀 ${path.relative(app.path.root, filePath)}`,
output: output || `Successfully applied edit using Morph Fast Apply:\n\n${diff}`,
}
}
const agent = await Agent.get(ctx.agent) async function executeRegularEdit(
let diff = "" params: { filePath: string; oldString: string; newString: string; replaceAll?: boolean },
let contentOld = "" ctx: any
let contentNew = "" ) {
await (async () => { if (!params.filePath) {
if (params.oldString === "") { throw new Error("filePath is required")
contentNew = params.newString }
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
if (agent.permission.edit === "ask") {
await Permission.ask({
type: "edit",
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: "Edit this file: " + filePath,
metadata: {
filePath,
diff,
},
})
}
await Bun.write(filePath, params.newString)
await Bus.publish(File.Event.Edited, {
file: filePath,
})
return
}
const file = Bun.file(filePath) if (params.oldString === params.newString) {
const stats = await file.stat().catch(() => {}) throw new Error("oldString and newString must be different")
if (!stats) throw new Error(`File ${filePath} not found`) }
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
await FileTime.assert(ctx.sessionID, filePath)
contentOld = await file.text()
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
const app = App.info()
const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
if (!Filesystem.contains(app.path.cwd, filePath)) {
throw new Error(`File ${filePath} is not in the current working directory`)
}
const agent = await Agent.get(ctx.agent)
let diff = ""
let contentOld = ""
let contentNew = ""
await (async () => {
if (params.oldString === "") {
contentNew = params.newString
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew)) diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
if (agent.permission.edit === "ask") { if (agent.permission.edit === "ask") {
await Permission.ask({ await Permission.ask({
@ -83,7 +186,6 @@ export const EditTool = Tool.define("edit", {
sessionID: ctx.sessionID, sessionID: ctx.sessionID,
messageID: ctx.messageID, messageID: ctx.messageID,
callID: ctx.callID, callID: ctx.callID,
pattern: filePath,
title: "Edit this file: " + filePath, title: "Edit this file: " + filePath,
metadata: { metadata: {
filePath, filePath,
@ -91,43 +193,105 @@ export const EditTool = Tool.define("edit", {
}, },
}) })
} }
await Bun.write(filePath, params.newString)
await file.write(contentNew)
await Bus.publish(File.Event.Edited, { await Bus.publish(File.Event.Edited, {
file: filePath, file: filePath,
}) })
contentNew = await file.text() return
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
})()
FileTime.read(ctx.sessionID, filePath)
let output = ""
await LSP.touchFile(filePath, true)
const diagnostics = await LSP.diagnostics()
for (const [file, issues] of Object.entries(diagnostics)) {
if (issues.length === 0) continue
if (file === filePath) {
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
continue
}
output += `\n<project_diagnostics>\n${file}\n${issues
// TODO: may want to make more leniant for eslint
.filter((item) => item.severity === 1)
.map(LSP.Diagnostic.pretty)
.join("\n")}\n</project_diagnostics>\n`
} }
const file = Bun.file(filePath)
const stats = await file.stat().catch(() => {})
if (!stats) throw new Error(`File ${filePath} not found`)
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
await FileTime.assert(ctx.sessionID, filePath)
contentOld = await file.text()
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
if (agent.permission.edit === "ask") {
await Permission.ask({
type: "edit",
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
pattern: filePath,
title: "Edit this file: " + filePath,
metadata: {
filePath,
diff,
},
})
}
await file.write(contentNew)
await Bus.publish(File.Event.Edited, {
file: filePath,
})
contentNew = await file.text()
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
})()
FileTime.read(ctx.sessionID, filePath)
let output = ""
await LSP.touchFile(filePath, true)
const diagnostics = await LSP.diagnostics()
for (const [file, issues] of Object.entries(diagnostics)) {
if (issues.length === 0) continue
if (file === filePath) {
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
continue
}
output += `\n<project_diagnostics>\n${file}\n${issues
// TODO: may want to make more leniant for eslint
.filter((item) => item.severity === 1)
.map(LSP.Diagnostic.pretty)
.join("\n")}\n</project_diagnostics>\n`
}
return {
metadata: {
diagnostics,
diff,
},
title: `${path.relative(app.path.root, filePath)}`,
output,
}
}
export const EditTool = Tool.define("edit", (async () => {
const morphApiKey = process.env['MORPH_API_KEY']
if (morphApiKey) {
// Morph Fast Apply mode
return { return {
metadata: { description: MORPH_DESCRIPTION as string,
diagnostics, parameters: z.object({
diff, target_file: z.string().describe("The target file to modify"),
}, instructions: z.string().describe("A single sentence written in the first person describing what you're changing. Used to help disambiguate uncertainty in the edit."),
title: `${path.relative(app.path.root, filePath)}`, code_edit: z.string().describe("Specify ONLY the precise lines of code that you wish to edit. Use `// ... existing code ...` for unchanged sections."),
output, }),
async execute(params: any, ctx: any) {
return executeMorphEdit(params, ctx, morphApiKey)
}
} }
}, } else {
}) // Regular search-and-replace mode
return {
description: DESCRIPTION as string,
parameters: z.object({
filePath: z.string().describe("The absolute path to the file to modify"),
oldString: z.string().describe("The text to replace"),
newString: z.string().describe("The text to replace it with (must be different from oldString)"),
replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)"),
}),
async execute(params: any, ctx: any) {
return executeRegularEdit(params, ctx)
}
}
}
}) as any)
export type Replacer = (content: string, find: string) => Generator<string, void, unknown> export type Replacer = (content: string, find: string) => Generator<string, void, unknown>
@ -606,7 +770,8 @@ export function replace(content: string, oldString: string, newString: string, r
// ContextAwareReplacer, // ContextAwareReplacer,
// MultiOccurrenceReplacer, // MultiOccurrenceReplacer,
]) { ]) {
for (const search of replacer(content, oldString)) { const searches = Array.from(replacer(content, oldString))
for (const search of searches) {
const index = content.indexOf(search) const index = content.indexOf(search)
if (index === -1) continue if (index === -1) continue
if (replaceAll) { if (replaceAll) {

View file

@ -0,0 +1,164 @@
import { z } from "zod"
import * as path from "path"
import { Tool } from "./tool"
import { createTwoFilesPatch } from "diff"
import { Permission } from "../permission"
import DESCRIPTION from "./morphedit.txt"
import { App } from "../app/app"
import { File } from "../file"
import { Bus } from "../bus"
import { FileTime } from "../file/time"
import { Filesystem } from "../util/filesystem"
import { Agent } from "../agent/agent"
import { LSP } from "../lsp"
// OpenAI-compatible client for Morph API
class MorphClient {
private apiKey: string
private baseURL = "https://api.morphllm.com/v1"
constructor(apiKey: string) {
this.apiKey = apiKey
}
async apply(instruction: string, initialCode: string, codeEdit: string): Promise<string> {
const response = await fetch(`${this.baseURL}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
model: "morph-v3-large",
messages: [
{
role: "user",
content: `<instruction>${instruction}</instruction>\n<code>${initialCode}</code>\n<update>${codeEdit}</update>`,
},
],
}),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Morph API error: ${response.status} ${response.statusText}\n${errorText}`)
}
const result = await response.json()
return result.choices[0].message.content
}
}
function trimDiff(diff: string): string {
return diff
.split("\n")
.slice(4) // Remove the header lines
.join("\n")
}
export const MorphEditTool = Tool.define("edit", {
description: DESCRIPTION,
parameters: z.object({
target_file: z.string().describe("The target file to modify"),
instructions: z.string().describe("A single sentence written in the first person describing what you're changing. Used to help disambiguate uncertainty in the edit."),
code_edit: z.string().describe("Specify ONLY the precise lines of code that you wish to edit. Use `// ... existing code ...` for unchanged sections."),
}),
async execute(params, ctx) {
if (!params.target_file) {
throw new Error("target_file is required")
}
if (!params.instructions) {
throw new Error("instructions is required")
}
if (!params.code_edit) {
throw new Error("code_edit is required")
}
// Check for Morph API key
const morphApiKey = process.env['MORPH_API_KEY']
if (!morphApiKey) {
throw new Error("MORPH_API_KEY environment variable is required for morphedit tool")
}
const app = App.info()
const filePath = path.isAbsolute(params.target_file) ? params.target_file : path.join(app.path.cwd, params.target_file)
if (!Filesystem.contains(app.path.cwd, filePath)) {
throw new Error(`File ${filePath} is not in the current working directory`)
}
const agent = await Agent.get(ctx.agent)
// Read the existing file
const file = Bun.file(filePath)
const stats = await file.stat().catch(() => {})
if (!stats) throw new Error(`File ${filePath} not found`)
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
await FileTime.assert(ctx.sessionID, filePath)
const initialCode = await file.text()
// Use Morph API to apply the edit
const morphClient = new MorphClient(morphApiKey)
let mergedCode: string
try {
mergedCode = await morphClient.apply(params.instructions, initialCode, params.code_edit)
} catch (error) {
throw new Error(`Failed to apply edit with Morph: ${error instanceof Error ? error.message : String(error)}`)
}
const diff = trimDiff(createTwoFilesPatch(filePath, filePath, initialCode, mergedCode))
// Check permissions if needed
if (agent.permission.edit === "ask") {
await Permission.ask({
type: "edit",
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: "🚀 Morph Fast Apply: " + filePath,
metadata: {
filePath,
diff,
morphApplied: true,
},
})
}
// Write the merged code to file
await Bun.write(filePath, mergedCode)
await Bus.publish(File.Event.Edited, {
file: filePath,
})
FileTime.read(ctx.sessionID, filePath)
let output = ""
await LSP.touchFile(filePath, true)
const diagnostics = await LSP.diagnostics()
for (const [file, issues] of Object.entries(diagnostics)) {
if (issues.length === 0) continue
if (file === filePath) {
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
continue
}
output += `\n<project_diagnostics>\n${file}\n${issues
.filter((item) => item.severity === 1)
.map(LSP.Diagnostic.pretty)
.join("\n")}\n</project_diagnostics>\n`
}
return {
metadata: {
diagnostics,
diff,
morphApplied: true,
},
title: `🚀 ${path.relative(app.path.root, filePath)}`,
output: output || `Successfully applied edit using Morph Fast Apply:\n\n${diff}`,
}
},
})

View file

@ -0,0 +1,21 @@
MORPH FAST APPLY - Use this tool to make an edit to an existing file.
This will be read by a less intelligent model, which will quickly apply the edit. You should make it clear what the edit is, while also minimizing the unchanged code you write.
When writing the edit, you should specify each edit in sequence, with the special comment // ... existing code ... to represent unchanged code in between edited lines.
For example:
// ... existing code ...
FIRST_EDIT
// ... existing code ...
SECOND_EDIT
// ... existing code ...
THIRD_EDIT
// ... existing code ...
You should still bias towards repeating as few lines of the original file as possible to convey the change.
But, each edit should contain sufficient context of unchanged lines around the code you're editing to resolve ambiguity.
DO NOT omit spans of pre-existing code (or comments) without using the // ... existing code ... comment to indicate its absence. If you omit the existing code comment, the model may inadvertently delete these lines.
If you plan on deleting a section, you must provide context before and after to delete it. If the initial code is ```code \n Block 1 \n Block 2 \n Block 3 \n code```, and you want to remove Block 2, you would output ```// ... existing code ... \n Block 1 \n Block 3 \n // ... existing code ...```.
Make sure it is clear what the edit should be, and where it should be applied.
Make edits to a file in a single edit_file instead of multiple edit_file calls to the same file. The apply model can handle many distinct edits at once.

View file

@ -1,6 +1,7 @@
import z from "zod" import z from "zod"
import { BashTool } from "./bash" import { BashTool } from "./bash"
import { EditTool } from "./edit" import { EditTool } from "./edit"
import { MorphEditTool } from "./morphedit"
import { GlobTool } from "./glob" import { GlobTool } from "./glob"
import { GrepTool } from "./grep" import { GrepTool } from "./grep"
import { ListTool } from "./ls" import { ListTool } from "./ls"
@ -14,10 +15,13 @@ import { InvalidTool } from "./invalid"
import type { Agent } from "../agent/agent" import type { Agent } from "../agent/agent"
export namespace ToolRegistry { export namespace ToolRegistry {
// Use Morph Fast Apply as the default edit tool if MORPH_API_KEY is present
const DefaultEditTool = process.env['MORPH_API_KEY'] ? MorphEditTool : EditTool
const ALL = [ const ALL = [
InvalidTool, InvalidTool,
BashTool, BashTool,
EditTool, DefaultEditTool,
WebFetchTool, WebFetchTool,
GlobTool, GlobTool,
GrepTool, GrepTool,
@ -45,21 +49,21 @@ export namespace ToolRegistry {
if (providerID === "openai") { if (providerID === "openai") {
return result.map((t) => ({ return result.map((t) => ({
...t, ...t,
parameters: optionalToNullable(t.parameters), parameters: optionalToNullable(t.parameters as z.ZodTypeAny),
})) }))
} }
if (providerID === "azure") { if (providerID === "azure") {
return result.map((t) => ({ return result.map((t) => ({
...t, ...t,
parameters: optionalToNullable(t.parameters), parameters: optionalToNullable(t.parameters as z.ZodTypeAny),
})) }))
} }
if (providerID === "google") { if (providerID === "google") {
return result.map((t) => ({ return result.map((t) => ({
...t, ...t,
parameters: sanitizeGeminiParameters(t.parameters), parameters: sanitizeGeminiParameters(t.parameters as z.ZodTypeAny),
})) }))
} }