Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
eed4bfa
init
haoxiangliew Nov 6, 2025
c368e74
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 6, 2025
f14d314
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 6, 2025
e4f4bbe
refactor ACP tools
haoxiangliew Nov 6, 2025
e0e9072
revert changes to readme
haoxiangliew Nov 6, 2025
8408d94
refactor ACP tools
haoxiangliew Nov 6, 2025
949e6b0
we don't need ACP for "does a file exist"
haoxiangliew Nov 6, 2025
e1b0ab8
remove unused import
haoxiangliew Nov 6, 2025
c10f3f8
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 6, 2025
8e86d9c
assure both capabilities are there
haoxiangliew Nov 6, 2025
a799496
Merge branch 'dev' into zed-acp-fixes
rekram1-node Nov 6, 2025
1b78db4
Merge branch 'dev' into zed-acp-fixes
rekram1-node Nov 6, 2025
399b0cf
address PR
haoxiangliew Nov 6, 2025
b011b6b
address PR
haoxiangliew Nov 6, 2025
07de79e
cleanup
haoxiangliew Nov 6, 2025
c246889
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 6, 2025
70efa29
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 6, 2025
268dd07
cleanup
haoxiangliew Nov 6, 2025
6b0c042
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 6, 2025
c08e60c
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 7, 2025
32524f3
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 7, 2025
425f56f
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 8, 2025
63e85c3
refactor merge after 'dev'
haoxiangliew Nov 8, 2025
c95d2f7
update
haoxiangliew Nov 8, 2025
f076d39
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 9, 2025
cd602ca
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 10, 2025
47a99f6
cleanup
haoxiangliew Nov 10, 2025
cb7923c
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 10, 2025
5e244ce
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 11, 2025
12e3774
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 12, 2025
4e5b577
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 13, 2025
71bb163
chore: format code
actions-user Nov 13, 2025
c93c8f3
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 14, 2025
563e619
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 16, 2025
5a2779c
fix after merge
haoxiangliew Nov 16, 2025
ef2a6de
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 17, 2025
bd1c93c
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 17, 2025
854af11
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 18, 2025
1ff6c62
rework for domain
haoxiangliew Nov 18, 2025
8bdc7fa
chore: format code
actions-user Nov 18, 2025
0761b3a
update
haoxiangliew Nov 18, 2025
054907d
chore: format code
actions-user Nov 18, 2025
8d3b326
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 18, 2025
94c2a58
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 19, 2025
5cc19f0
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 21, 2025
a29e72f
chore: format code
actions-user Nov 21, 2025
e52f502
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 21, 2025
fb691cc
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 23, 2025
8db3929
Update Nix flake.lock and hashes
actions-user Nov 23, 2025
e282fcf
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Nov 24, 2025
ae57cb8
Update Nix flake.lock and hashes
actions-user Nov 24, 2025
21739b6
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Dec 1, 2025
e23f92d
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Dec 3, 2025
650f80b
bump
haoxiangliew Dec 3, 2025
8fa8d50
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Dec 7, 2025
28d0755
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Dec 8, 2025
e94ccd8
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Dec 9, 2025
ae5ba82
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Dec 9, 2025
fa2f417
Merge branch 'dev' into zed-acp-fixes
haoxiangliew Dec 10, 2025
300e4f5
update
haoxiangliew Dec 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "AI-powered development tool",
"private": true,
"type": "module",
"packageManager": "bun@1.3.3",
"packageManager": "bun@1.3.4",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"typecheck": "bun turbo typecheck",
Expand Down
46 changes: 46 additions & 0 deletions packages/opencode/src/acp/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,23 @@ import {
type AuthenticateRequest,
type AuthMethod,
type CancelNotification,
type ClientCapabilities,
type InitializeRequest,
type InitializeResponse,
type LoadSessionRequest,
type NewSessionRequest,
type PermissionOption,
type PlanEntry,
type PromptRequest,
type ReadTextFileRequest,
type ReadTextFileResponse,
type SetSessionModelRequest,
type SetSessionModeRequest,
type SetSessionModeResponse,
type ToolCallContent,
type ToolKind,
type WriteTextFileRequest,
type WriteTextFileResponse,
} from "@agentclientprotocol/sdk"
import { Log } from "../util/log"
import { ACPSessionManager } from "./session"
Expand All @@ -29,6 +34,7 @@ import { Todo } from "@/session/todo"
import { z } from "zod"
import { LoadAPIKeyError } from "ai"
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
import { FileSystemDelegate } from "../tool/delegate"

export namespace ACP {
const log = Log.create({ service: "acp-agent" })
Expand All @@ -50,6 +56,7 @@ export namespace ACP {
private config: ACPConfig
private sdk: OpencodeClient
private sessionManager
private clientCapabilities?: ClientCapabilities

constructor(connection: AgentSideConnection, config: ACPConfig) {
this.connection = connection
Expand All @@ -58,6 +65,33 @@ export namespace ACP {
this.sessionManager = new ACPSessionManager(this.sdk)
}

private registerACPTools(sessionID: string) {
const { readTextFile, writeTextFile } = this.clientCapabilities?.fs ?? {}
const delegate: FileSystemDelegate = {
...(readTextFile && {
read: async (path, options) => {
const res = await this.readTextFile({
sessionId: sessionID,
path,
line: options?.offset,
limit: options?.limit,
})
return res.content
},
}),
...(writeTextFile && {
write: async (path, content) => {
await this.writeTextFile({
sessionId: sessionID,
path,
content,
})
},
}),
}
FileSystemDelegate.register(sessionID, delegate)
}

private setupEventSubscriptions(session: ACPSessionState) {
const sessionId = session.id
const directory = session.cwd
Expand Down Expand Up @@ -332,6 +366,7 @@ export namespace ACP {

async initialize(params: InitializeRequest): Promise<InitializeResponse> {
log.info("initialize", { protocolVersion: params.protocolVersion })
this.clientCapabilities = params.clientCapabilities

const authMethod: AuthMethod = {
description: "Run `opencode auth login` in the terminal",
Expand Down Expand Up @@ -393,6 +428,7 @@ export namespace ACP {
})

this.setupEventSubscriptions(state)
this.registerACPTools(sessionId)

return {
sessionId,
Expand Down Expand Up @@ -697,6 +733,16 @@ export namespace ACP {
{ throwOnError: true },
)
}

async readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
log.debug("readTextFile", { path: params.path, sessionId: params.sessionId })
return await this.connection.readTextFile(params)
}

async writeTextFile(params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
log.debug("writeTextFile", { path: params.path, sessionId: params.sessionId })
return await this.connection.writeTextFile(params)
}
}

function toToolKind(toolName: string): ToolKind {
Expand Down
20 changes: 20 additions & 0 deletions packages/opencode/src/tool/delegate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export interface FileSystemDelegate {
read?(path: string, options?: { offset?: number; limit?: number }): Promise<string>
write?(path: string, content: string): Promise<void>
}

export namespace FileSystemDelegate {
const registry = new Map<string, FileSystemDelegate>()

export function register(sessionID: string, delegate: FileSystemDelegate) {
registry.set(sessionID, delegate)
}

export function get(sessionID: string): FileSystemDelegate | undefined {
return registry.get(sessionID)
}

export function unregister(sessionID: string) {
registry.delete(sessionID)
}
}
10 changes: 6 additions & 4 deletions packages/opencode/src/tool/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance"
import { Agent } from "../agent/agent"
import { Snapshot } from "@/snapshot"
import { FileSystemDelegate } from "./delegate"

function normalizeLineEndings(text: string): string {
return text.replaceAll("\r\n", "\n")
Expand All @@ -40,6 +41,7 @@ export const EditTool = Tool.define("edit", {
}

const agent = await Agent.get(ctx.agent)
const delegate = FileSystemDelegate.get(ctx.sessionID)

const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
if (!Filesystem.contains(Instance.directory, filePath)) {
Expand Down Expand Up @@ -91,7 +93,7 @@ export const EditTool = Tool.define("edit", {
},
})
}
await Bun.write(filePath, params.newString)
delegate?.write ? await delegate.write(filePath, params.newString) : await Bun.write(filePath, params.newString)
await Bus.publish(File.Event.Edited, {
file: filePath,
})
Expand All @@ -103,7 +105,7 @@ export const EditTool = Tool.define("edit", {
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()
contentOld = delegate?.read ? await delegate.read(filePath) : await file.text()
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)

diff = trimDiff(
Expand All @@ -123,11 +125,11 @@ export const EditTool = Tool.define("edit", {
})
}

await file.write(contentNew)
delegate?.write ? await delegate.write(filePath, contentNew) : await Bun.write(filePath, contentNew)
await Bus.publish(File.Event.Edited, {
file: filePath,
})
contentNew = await file.text()
contentNew = delegate?.read ? await delegate.read(filePath) : await file.text()
diff = trimDiff(
createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)),
)
Expand Down
7 changes: 5 additions & 2 deletions packages/opencode/src/tool/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance"
import { Identifier } from "../id/id"
import { Permission } from "../permission"
import { FileSystemDelegate } from "./delegate"
import { Agent } from "@/agent/agent"
import { iife } from "@/util/iife"

Expand All @@ -29,6 +30,7 @@ export const ReadTool = Tool.define("read", {
}
const title = path.relative(Instance.worktree, filepath)
const agent = await Agent.get(ctx.agent)
const delegate = FileSystemDelegate.get(ctx.sessionID)

if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(Instance.directory, filepath)) {
const parentDir = path.dirname(filepath)
Expand Down Expand Up @@ -122,8 +124,9 @@ export const ReadTool = Tool.define("read", {

const limit = params.limit ?? DEFAULT_READ_LIMIT
const offset = params.offset || 0
const lines = await file.text().then((text) => text.split("\n"))
const raw = lines.slice(offset, offset + limit).map((line) => {
let lines = (delegate?.read ? await delegate.read(filepath, { offset, limit }) : await file.text()).split("\n")
if (!delegate?.read) lines = lines.slice(offset, offset + limit)
const raw = lines.map((line) => {
return line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line
})
const content = raw.map((line, index) => {
Expand Down
4 changes: 3 additions & 1 deletion packages/opencode/src/tool/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FileTime } from "../file/time"
import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance"
import { Agent } from "../agent/agent"
import { FileSystemDelegate } from "./delegate"

export const WriteTool = Tool.define("write", {
description: DESCRIPTION,
Expand All @@ -19,6 +20,7 @@ export const WriteTool = Tool.define("write", {
}),
async execute(params, ctx) {
const agent = await Agent.get(ctx.agent)
const delegate = FileSystemDelegate.get(ctx.sessionID)

const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
if (!Filesystem.contains(Instance.directory, filepath)) {
Expand Down Expand Up @@ -68,7 +70,7 @@ export const WriteTool = Tool.define("write", {
},
})

await Bun.write(filepath, params.content)
delegate?.write ? await delegate.write(filepath, params.content) : await Bun.write(filepath, params.content)
await Bus.publish(File.Event.Edited, {
file: filepath,
})
Expand Down