Jelajahi Sumber

feat: unwrap ACP, Agent, Workspace, Identifier, Shell namespaces

Kit Langton 2 hari lalu
induk
melakukan
87480f598f
68 mengubah file dengan 2494 tambahan dan 2499 penghapusan
  1. 1522 1524
      packages/opencode/src/acp/agent.ts
  2. 1 0
      packages/opencode/src/acp/index.ts
  3. 357 359
      packages/opencode/src/agent/agent.ts
  4. 1 0
      packages/opencode/src/agent/index.ts
  5. 1 1
      packages/opencode/src/cli/cmd/acp.ts
  6. 1 1
      packages/opencode/src/cli/cmd/agent.ts
  7. 1 1
      packages/opencode/src/cli/cmd/debug/agent.ts
  8. 1 1
      packages/opencode/src/cli/cmd/run.ts
  9. 1 0
      packages/opencode/src/control-plane/index.ts
  10. 1 1
      packages/opencode/src/control-plane/schema.ts
  11. 404 406
      packages/opencode/src/control-plane/workspace.ts
  12. 1 1
      packages/opencode/src/effect/app-runtime.ts
  13. 64 66
      packages/opencode/src/id/id.ts
  14. 1 0
      packages/opencode/src/id/index.ts
  15. 1 1
      packages/opencode/src/permission/schema.ts
  16. 1 1
      packages/opencode/src/pty/schema.ts
  17. 1 1
      packages/opencode/src/pty/service.ts
  18. 1 1
      packages/opencode/src/question/schema.ts
  19. 1 1
      packages/opencode/src/server/fence.ts
  20. 1 1
      packages/opencode/src/server/instance/experimental.ts
  21. 1 1
      packages/opencode/src/server/instance/index.ts
  22. 1 1
      packages/opencode/src/server/instance/middleware.ts
  23. 1 1
      packages/opencode/src/server/instance/session.ts
  24. 1 1
      packages/opencode/src/server/instance/workspace.ts
  25. 1 1
      packages/opencode/src/server/proxy.ts
  26. 1 1
      packages/opencode/src/session/compaction.ts
  27. 1 1
      packages/opencode/src/session/llm.ts
  28. 1 1
      packages/opencode/src/session/processor.ts
  29. 2 2
      packages/opencode/src/session/prompt.ts
  30. 1 1
      packages/opencode/src/session/schema.ts
  31. 1 1
      packages/opencode/src/session/system.ts
  32. 1 0
      packages/opencode/src/shell/index.ts
  33. 79 81
      packages/opencode/src/shell/shell.ts
  34. 1 1
      packages/opencode/src/skill/skill.ts
  35. 1 1
      packages/opencode/src/sync/schema.ts
  36. 1 1
      packages/opencode/src/tool/bash.ts
  37. 1 1
      packages/opencode/src/tool/registry.ts
  38. 1 1
      packages/opencode/src/tool/schema.ts
  39. 1 1
      packages/opencode/src/tool/task.ts
  40. 1 1
      packages/opencode/src/tool/tool.ts
  41. 2 2
      packages/opencode/src/tool/truncate.ts
  42. 1 1
      packages/opencode/src/v2/session-event.ts
  43. 1 1
      packages/opencode/test/acp/agent-interface.test.ts
  44. 1 1
      packages/opencode/test/acp/event-subscription.test.ts
  45. 1 1
      packages/opencode/test/agent/agent.test.ts
  46. 1 1
      packages/opencode/test/config/agent-color.test.ts
  47. 1 1
      packages/opencode/test/plugin/workspace-adaptor.test.ts
  48. 1 1
      packages/opencode/test/pty/pty-shell.test.ts
  49. 1 1
      packages/opencode/test/session/compaction.test.ts
  50. 1 1
      packages/opencode/test/session/llm.test.ts
  51. 2 2
      packages/opencode/test/session/processor-effect.test.ts
  52. 2 2
      packages/opencode/test/session/prompt-effect.test.ts
  53. 1 1
      packages/opencode/test/session/snapshot-tool-race.test.ts
  54. 1 1
      packages/opencode/test/session/system.test.ts
  55. 1 1
      packages/opencode/test/shell/shell.test.ts
  56. 1 1
      packages/opencode/test/sync/index.test.ts
  57. 1 1
      packages/opencode/test/tool/apply_patch.test.ts
  58. 2 2
      packages/opencode/test/tool/bash.test.ts
  59. 1 1
      packages/opencode/test/tool/edit.test.ts
  60. 1 1
      packages/opencode/test/tool/glob.test.ts
  61. 1 1
      packages/opencode/test/tool/grep.test.ts
  62. 1 1
      packages/opencode/test/tool/question.test.ts
  63. 1 1
      packages/opencode/test/tool/read.test.ts
  64. 1 1
      packages/opencode/test/tool/task.test.ts
  65. 1 1
      packages/opencode/test/tool/tool-define.test.ts
  66. 1 1
      packages/opencode/test/tool/truncation.test.ts
  67. 1 1
      packages/opencode/test/tool/webfetch.test.ts
  68. 1 1
      packages/opencode/test/tool/write.test.ts

+ 1522 - 1524
packages/opencode/src/acp/agent.ts

@@ -39,7 +39,7 @@ import { ACPSessionManager } from "./session"
 import type { ACPConfig } from "./types"
 import { Provider } from "../provider"
 import { ModelID, ProviderID } from "../provider/schema"
-import { Agent as AgentModule } from "../agent/agent"
+import { Agent as AgentModule } from "../agent"
 import { AppRuntime } from "@/effect/app-runtime"
 import { Installation } from "@/installation"
 import { MessageV2 } from "@/session/message-v2"
@@ -56,1785 +56,1783 @@ type ModelOption = { modelId: string; name: string }
 
 const DEFAULT_VARIANT_VALUE = "default"
 
-export namespace ACP {
-  const log = Log.create({ service: "acp-agent" })
-
-  async function getContextLimit(
-    sdk: OpencodeClient,
-    providerID: ProviderID,
-    modelID: ModelID,
-    directory: string,
-  ): Promise<number | null> {
-    const providers = await sdk.config
-      .providers({ directory })
-      .then((x) => x.data?.providers ?? [])
-      .catch((error) => {
-        log.error("failed to get providers for context limit", { error })
-        return []
-      })
+const log = Log.create({ service: "acp-agent" })
+
+async function getContextLimit(
+  sdk: OpencodeClient,
+  providerID: ProviderID,
+  modelID: ModelID,
+  directory: string,
+): Promise<number | null> {
+  const providers = await sdk.config
+    .providers({ directory })
+    .then((x) => x.data?.providers ?? [])
+    .catch((error) => {
+      log.error("failed to get providers for context limit", { error })
+      return []
+    })
 
-    const provider = providers.find((p) => p.id === providerID)
-    const model = provider?.models[modelID]
-    return model?.limit.context ?? null
-  }
+  const provider = providers.find((p) => p.id === providerID)
+  const model = provider?.models[modelID]
+  return model?.limit.context ?? null
+}
 
-  async function sendUsageUpdate(
-    connection: AgentSideConnection,
-    sdk: OpencodeClient,
-    sessionID: string,
-    directory: string,
-  ): Promise<void> {
-    const messages = await sdk.session
-      .messages({ sessionID, directory }, { throwOnError: true })
-      .then((x) => x.data)
-      .catch((error) => {
-        log.error("failed to fetch messages for usage update", { error })
-        return undefined
-      })
+async function sendUsageUpdate(
+  connection: AgentSideConnection,
+  sdk: OpencodeClient,
+  sessionID: string,
+  directory: string,
+): Promise<void> {
+  const messages = await sdk.session
+    .messages({ sessionID, directory }, { throwOnError: true })
+    .then((x) => x.data)
+    .catch((error) => {
+      log.error("failed to fetch messages for usage update", { error })
+      return undefined
+    })
 
-    if (!messages) return
+  if (!messages) return
 
-    const assistantMessages = messages.filter(
-      (m): m is { info: AssistantMessage; parts: SessionMessageResponse["parts"] } => m.info.role === "assistant",
-    )
+  const assistantMessages = messages.filter(
+    (m): m is { info: AssistantMessage; parts: SessionMessageResponse["parts"] } => m.info.role === "assistant",
+  )
 
-    const lastAssistant = assistantMessages[assistantMessages.length - 1]
-    if (!lastAssistant) return
+  const lastAssistant = assistantMessages[assistantMessages.length - 1]
+  if (!lastAssistant) return
 
-    const msg = lastAssistant.info
-    if (!msg.providerID || !msg.modelID) return
-    const size = await getContextLimit(sdk, ProviderID.make(msg.providerID), ModelID.make(msg.modelID), directory)
+  const msg = lastAssistant.info
+  if (!msg.providerID || !msg.modelID) return
+  const size = await getContextLimit(sdk, ProviderID.make(msg.providerID), ModelID.make(msg.modelID), directory)
 
-    if (!size) {
-      // Cannot calculate usage without known context size
-      return
-    }
+  if (!size) {
+    // Cannot calculate usage without known context size
+    return
+  }
 
-    const used = msg.tokens.input + (msg.tokens.cache?.read ?? 0)
-    const totalCost = assistantMessages.reduce((sum, m) => sum + m.info.cost, 0)
+  const used = msg.tokens.input + (msg.tokens.cache?.read ?? 0)
+  const totalCost = assistantMessages.reduce((sum, m) => sum + m.info.cost, 0)
+
+  await connection
+    .sessionUpdate({
+      sessionId: sessionID,
+      update: {
+        sessionUpdate: "usage_update",
+        used,
+        size,
+        cost: { amount: totalCost, currency: "USD" },
+      },
+    })
+    .catch((error) => {
+      log.error("failed to send usage update", { error })
+    })
+}
 
-    await connection
-      .sessionUpdate({
-        sessionId: sessionID,
-        update: {
-          sessionUpdate: "usage_update",
-          used,
-          size,
-          cost: { amount: totalCost, currency: "USD" },
-        },
-      })
-      .catch((error) => {
-        log.error("failed to send usage update", { error })
-      })
+export async function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
+  return {
+    create: (connection: AgentSideConnection, fullConfig: ACPConfig) => {
+      return new Agent(connection, fullConfig)
+    },
   }
+}
 
-  export async function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
-    return {
-      create: (connection: AgentSideConnection, fullConfig: ACPConfig) => {
-        return new Agent(connection, fullConfig)
-      },
-    }
+export class Agent implements ACPAgent {
+  private connection: AgentSideConnection
+  private config: ACPConfig
+  private sdk: OpencodeClient
+  private sessionManager: ACPSessionManager
+  private eventAbort = new AbortController()
+  private eventStarted = false
+  private bashSnapshots = new Map<string, string>()
+  private toolStarts = new Set<string>()
+  private permissionQueues = new Map<string, Promise<void>>()
+  private permissionOptions: PermissionOption[] = [
+    { optionId: "once", kind: "allow_once", name: "Allow once" },
+    { optionId: "always", kind: "allow_always", name: "Always allow" },
+    { optionId: "reject", kind: "reject_once", name: "Reject" },
+  ]
+
+  constructor(connection: AgentSideConnection, config: ACPConfig) {
+    this.connection = connection
+    this.config = config
+    this.sdk = config.sdk
+    this.sessionManager = new ACPSessionManager(this.sdk)
+    this.startEventSubscription()
   }
 
-  export class Agent implements ACPAgent {
-    private connection: AgentSideConnection
-    private config: ACPConfig
-    private sdk: OpencodeClient
-    private sessionManager: ACPSessionManager
-    private eventAbort = new AbortController()
-    private eventStarted = false
-    private bashSnapshots = new Map<string, string>()
-    private toolStarts = new Set<string>()
-    private permissionQueues = new Map<string, Promise<void>>()
-    private permissionOptions: PermissionOption[] = [
-      { optionId: "once", kind: "allow_once", name: "Allow once" },
-      { optionId: "always", kind: "allow_always", name: "Always allow" },
-      { optionId: "reject", kind: "reject_once", name: "Reject" },
-    ]
-
-    constructor(connection: AgentSideConnection, config: ACPConfig) {
-      this.connection = connection
-      this.config = config
-      this.sdk = config.sdk
-      this.sessionManager = new ACPSessionManager(this.sdk)
-      this.startEventSubscription()
-    }
+  private startEventSubscription() {
+    if (this.eventStarted) return
+    this.eventStarted = true
+    this.runEventSubscription().catch((error) => {
+      if (this.eventAbort.signal.aborted) return
+      log.error("event subscription failed", { error })
+    })
+  }
 
-    private startEventSubscription() {
-      if (this.eventStarted) return
-      this.eventStarted = true
-      this.runEventSubscription().catch((error) => {
-        if (this.eventAbort.signal.aborted) return
-        log.error("event subscription failed", { error })
+  private async runEventSubscription() {
+    while (true) {
+      if (this.eventAbort.signal.aborted) return
+      const events = await this.sdk.global.event({
+        signal: this.eventAbort.signal,
       })
-    }
-
-    private async runEventSubscription() {
-      while (true) {
+      for await (const event of events.stream) {
         if (this.eventAbort.signal.aborted) return
-        const events = await this.sdk.global.event({
-          signal: this.eventAbort.signal,
+        const payload = (event as any)?.payload
+        if (!payload) continue
+        await this.handleEvent(payload as Event).catch((error) => {
+          log.error("failed to handle event", { error, type: payload.type })
         })
-        for await (const event of events.stream) {
-          if (this.eventAbort.signal.aborted) return
-          const payload = (event as any)?.payload
-          if (!payload) continue
-          await this.handleEvent(payload as Event).catch((error) => {
-            log.error("failed to handle event", { error, type: payload.type })
-          })
-        }
       }
     }
+  }
 
-    private async handleEvent(event: Event) {
-      switch (event.type) {
-        case "permission.asked": {
-          const permission = event.properties
-          const session = this.sessionManager.tryGet(permission.sessionID)
-          if (!session) return
-
-          const prev = this.permissionQueues.get(permission.sessionID) ?? Promise.resolve()
-          const next = prev
-            .then(async () => {
-              const directory = session.cwd
-
-              const res = await this.connection
-                .requestPermission({
-                  sessionId: permission.sessionID,
-                  toolCall: {
-                    toolCallId: permission.tool?.callID ?? permission.id,
-                    status: "pending",
-                    title: permission.permission,
-                    rawInput: permission.metadata,
-                    kind: toToolKind(permission.permission),
-                    locations: toLocations(permission.permission, permission.metadata),
-                  },
-                  options: this.permissionOptions,
-                })
-                .catch(async (error) => {
-                  log.error("failed to request permission from ACP", {
-                    error,
-                    permissionID: permission.id,
-                    sessionID: permission.sessionID,
-                  })
-                  await this.sdk.permission.reply({
-                    requestID: permission.id,
-                    reply: "reject",
-                    directory,
-                  })
-                  return undefined
+  private async handleEvent(event: Event) {
+    switch (event.type) {
+      case "permission.asked": {
+        const permission = event.properties
+        const session = this.sessionManager.tryGet(permission.sessionID)
+        if (!session) return
+
+        const prev = this.permissionQueues.get(permission.sessionID) ?? Promise.resolve()
+        const next = prev
+          .then(async () => {
+            const directory = session.cwd
+
+            const res = await this.connection
+              .requestPermission({
+                sessionId: permission.sessionID,
+                toolCall: {
+                  toolCallId: permission.tool?.callID ?? permission.id,
+                  status: "pending",
+                  title: permission.permission,
+                  rawInput: permission.metadata,
+                  kind: toToolKind(permission.permission),
+                  locations: toLocations(permission.permission, permission.metadata),
+                },
+                options: this.permissionOptions,
+              })
+              .catch(async (error) => {
+                log.error("failed to request permission from ACP", {
+                  error,
+                  permissionID: permission.id,
+                  sessionID: permission.sessionID,
                 })
-
-              if (!res) return
-              if (res.outcome.outcome !== "selected") {
                 await this.sdk.permission.reply({
                   requestID: permission.id,
                   reply: "reject",
                   directory,
                 })
-                return
-              }
-
-              if (res.outcome.optionId !== "reject" && permission.permission == "edit") {
-                const metadata = permission.metadata || {}
-                const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : ""
-                const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : ""
-                const content = (await Filesystem.exists(filepath)) ? await Filesystem.readText(filepath) : ""
-                const newContent = getNewContent(content, diff)
-
-                if (newContent) {
-                  void this.connection.writeTextFile({
-                    sessionId: session.id,
-                    path: filepath,
-                    content: newContent,
-                  })
-                }
-              }
+                return undefined
+              })
 
+            if (!res) return
+            if (res.outcome.outcome !== "selected") {
               await this.sdk.permission.reply({
                 requestID: permission.id,
-                reply: res.outcome.optionId as "once" | "always" | "reject",
+                reply: "reject",
                 directory,
               })
-            })
-            .catch((error) => {
-              log.error("failed to handle permission", { error, permissionID: permission.id })
-            })
-            .finally(() => {
-              if (this.permissionQueues.get(permission.sessionID) === next) {
-                this.permissionQueues.delete(permission.sessionID)
+              return
+            }
+
+            if (res.outcome.optionId !== "reject" && permission.permission == "edit") {
+              const metadata = permission.metadata || {}
+              const filepath = typeof metadata["filepath"] === "string" ? metadata["filepath"] : ""
+              const diff = typeof metadata["diff"] === "string" ? metadata["diff"] : ""
+              const content = (await Filesystem.exists(filepath)) ? await Filesystem.readText(filepath) : ""
+              const newContent = getNewContent(content, diff)
+
+              if (newContent) {
+                void this.connection.writeTextFile({
+                  sessionId: session.id,
+                  path: filepath,
+                  content: newContent,
+                })
               }
+            }
+
+            await this.sdk.permission.reply({
+              requestID: permission.id,
+              reply: res.outcome.optionId as "once" | "always" | "reject",
+              directory,
             })
-          this.permissionQueues.set(permission.sessionID, next)
-          return
-        }
+          })
+          .catch((error) => {
+            log.error("failed to handle permission", { error, permissionID: permission.id })
+          })
+          .finally(() => {
+            if (this.permissionQueues.get(permission.sessionID) === next) {
+              this.permissionQueues.delete(permission.sessionID)
+            }
+          })
+        this.permissionQueues.set(permission.sessionID, next)
+        return
+      }
 
-        case "message.part.updated": {
-          log.info("message part updated", { event: event.properties })
-          const props = event.properties
-          const part = props.part
-          const session = this.sessionManager.tryGet(part.sessionID)
-          if (!session) return
-          const sessionId = session.id
-
-          if (part.type === "tool") {
-            await this.toolStart(sessionId, part)
-
-            switch (part.state.status) {
-              case "pending":
-                this.bashSnapshots.delete(part.callID)
-                return
-
-              case "running":
-                const output = this.bashOutput(part)
-                const content: ToolCallContent[] = []
-                if (output) {
-                  const hash = Hash.fast(output)
-                  if (part.tool === "bash") {
-                    if (this.bashSnapshots.get(part.callID) === hash) {
-                      await this.connection
-                        .sessionUpdate({
-                          sessionId,
-                          update: {
-                            sessionUpdate: "tool_call_update",
-                            toolCallId: part.callID,
-                            status: "in_progress",
-                            kind: toToolKind(part.tool),
-                            title: part.tool,
-                            locations: toLocations(part.tool, part.state.input),
-                            rawInput: part.state.input,
-                          },
-                        })
-                        .catch((error) => {
-                          log.error("failed to send tool in_progress to ACP", { error })
-                        })
-                      return
-                    }
-                    this.bashSnapshots.set(part.callID, hash)
-                  }
-                  content.push({
-                    type: "content",
-                    content: {
-                      type: "text",
-                      text: output,
-                    },
-                  })
-                }
-                await this.connection
-                  .sessionUpdate({
-                    sessionId,
-                    update: {
-                      sessionUpdate: "tool_call_update",
-                      toolCallId: part.callID,
-                      status: "in_progress",
-                      kind: toToolKind(part.tool),
-                      title: part.tool,
-                      locations: toLocations(part.tool, part.state.input),
-                      rawInput: part.state.input,
-                      ...(content.length > 0 && { content }),
-                    },
-                  })
-                  .catch((error) => {
-                    log.error("failed to send tool in_progress to ACP", { error })
-                  })
-                return
-
-              case "completed": {
-                this.toolStarts.delete(part.callID)
-                this.bashSnapshots.delete(part.callID)
-                const kind = toToolKind(part.tool)
-                const content: ToolCallContent[] = [
-                  {
-                    type: "content",
-                    content: {
-                      type: "text",
-                      text: part.state.output,
-                    },
-                  },
-                ]
-
-                if (kind === "edit") {
-                  const input = part.state.input
-                  const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
-                  const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
-                  const newText =
-                    typeof input["newString"] === "string"
-                      ? input["newString"]
-                      : typeof input["content"] === "string"
-                        ? input["content"]
-                        : ""
-                  content.push({
-                    type: "diff",
-                    path: filePath,
-                    oldText,
-                    newText,
-                  })
-                }
+      case "message.part.updated": {
+        log.info("message part updated", { event: event.properties })
+        const props = event.properties
+        const part = props.part
+        const session = this.sessionManager.tryGet(part.sessionID)
+        if (!session) return
+        const sessionId = session.id
+
+        if (part.type === "tool") {
+          await this.toolStart(sessionId, part)
+
+          switch (part.state.status) {
+            case "pending":
+              this.bashSnapshots.delete(part.callID)
+              return
 
-                if (part.tool === "todowrite") {
-                  const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
-                  if (parsedTodos.success) {
+            case "running":
+              const output = this.bashOutput(part)
+              const content: ToolCallContent[] = []
+              if (output) {
+                const hash = Hash.fast(output)
+                if (part.tool === "bash") {
+                  if (this.bashSnapshots.get(part.callID) === hash) {
                     await this.connection
                       .sessionUpdate({
                         sessionId,
                         update: {
-                          sessionUpdate: "plan",
-                          entries: parsedTodos.data.map((todo) => {
-                            const status: PlanEntry["status"] =
-                              todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
-                            return {
-                              priority: "medium",
-                              status,
-                              content: todo.content,
-                            }
-                          }),
+                          sessionUpdate: "tool_call_update",
+                          toolCallId: part.callID,
+                          status: "in_progress",
+                          kind: toToolKind(part.tool),
+                          title: part.tool,
+                          locations: toLocations(part.tool, part.state.input),
+                          rawInput: part.state.input,
                         },
                       })
                       .catch((error) => {
-                        log.error("failed to send session update for todo", { error })
+                        log.error("failed to send tool in_progress to ACP", { error })
                       })
-                  } else {
-                    log.error("failed to parse todo output", { error: parsedTodos.error })
+                    return
                   }
+                  this.bashSnapshots.set(part.callID, hash)
                 }
+                content.push({
+                  type: "content",
+                  content: {
+                    type: "text",
+                    text: output,
+                  },
+                })
+              }
+              await this.connection
+                .sessionUpdate({
+                  sessionId,
+                  update: {
+                    sessionUpdate: "tool_call_update",
+                    toolCallId: part.callID,
+                    status: "in_progress",
+                    kind: toToolKind(part.tool),
+                    title: part.tool,
+                    locations: toLocations(part.tool, part.state.input),
+                    rawInput: part.state.input,
+                    ...(content.length > 0 && { content }),
+                  },
+                })
+                .catch((error) => {
+                  log.error("failed to send tool in_progress to ACP", { error })
+                })
+              return
 
-                await this.connection
-                  .sessionUpdate({
-                    sessionId,
-                    update: {
-                      sessionUpdate: "tool_call_update",
-                      toolCallId: part.callID,
-                      status: "completed",
-                      kind,
-                      content,
-                      title: part.state.title,
-                      rawInput: part.state.input,
-                      rawOutput: {
-                        output: part.state.output,
-                        metadata: part.state.metadata,
+            case "completed": {
+              this.toolStarts.delete(part.callID)
+              this.bashSnapshots.delete(part.callID)
+              const kind = toToolKind(part.tool)
+              const content: ToolCallContent[] = [
+                {
+                  type: "content",
+                  content: {
+                    type: "text",
+                    text: part.state.output,
+                  },
+                },
+              ]
+
+              if (kind === "edit") {
+                const input = part.state.input
+                const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
+                const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
+                const newText =
+                  typeof input["newString"] === "string"
+                    ? input["newString"]
+                    : typeof input["content"] === "string"
+                      ? input["content"]
+                      : ""
+                content.push({
+                  type: "diff",
+                  path: filePath,
+                  oldText,
+                  newText,
+                })
+              }
+
+              if (part.tool === "todowrite") {
+                const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
+                if (parsedTodos.success) {
+                  await this.connection
+                    .sessionUpdate({
+                      sessionId,
+                      update: {
+                        sessionUpdate: "plan",
+                        entries: parsedTodos.data.map((todo) => {
+                          const status: PlanEntry["status"] =
+                            todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
+                          return {
+                            priority: "medium",
+                            status,
+                            content: todo.content,
+                          }
+                        }),
                       },
-                    },
-                  })
-                  .catch((error) => {
-                    log.error("failed to send tool completed to ACP", { error })
-                  })
-                return
+                    })
+                    .catch((error) => {
+                      log.error("failed to send session update for todo", { error })
+                    })
+                } else {
+                  log.error("failed to parse todo output", { error: parsedTodos.error })
+                }
               }
-              case "error":
-                this.toolStarts.delete(part.callID)
-                this.bashSnapshots.delete(part.callID)
-                await this.connection
-                  .sessionUpdate({
-                    sessionId,
-                    update: {
-                      sessionUpdate: "tool_call_update",
-                      toolCallId: part.callID,
-                      status: "failed",
-                      kind: toToolKind(part.tool),
-                      title: part.tool,
-                      rawInput: part.state.input,
-                      content: [
-                        {
-                          type: "content",
-                          content: {
-                            type: "text",
-                            text: part.state.error,
-                          },
+
+              await this.connection
+                .sessionUpdate({
+                  sessionId,
+                  update: {
+                    sessionUpdate: "tool_call_update",
+                    toolCallId: part.callID,
+                    status: "completed",
+                    kind,
+                    content,
+                    title: part.state.title,
+                    rawInput: part.state.input,
+                    rawOutput: {
+                      output: part.state.output,
+                      metadata: part.state.metadata,
+                    },
+                  },
+                })
+                .catch((error) => {
+                  log.error("failed to send tool completed to ACP", { error })
+                })
+              return
+            }
+            case "error":
+              this.toolStarts.delete(part.callID)
+              this.bashSnapshots.delete(part.callID)
+              await this.connection
+                .sessionUpdate({
+                  sessionId,
+                  update: {
+                    sessionUpdate: "tool_call_update",
+                    toolCallId: part.callID,
+                    status: "failed",
+                    kind: toToolKind(part.tool),
+                    title: part.tool,
+                    rawInput: part.state.input,
+                    content: [
+                      {
+                        type: "content",
+                        content: {
+                          type: "text",
+                          text: part.state.error,
                         },
-                      ],
-                      rawOutput: {
-                        error: part.state.error,
-                        metadata: part.state.metadata,
                       },
+                    ],
+                    rawOutput: {
+                      error: part.state.error,
+                      metadata: part.state.metadata,
                     },
-                  })
-                  .catch((error) => {
-                    log.error("failed to send tool error to ACP", { error })
-                  })
-                return
-            }
+                  },
+                })
+                .catch((error) => {
+                  log.error("failed to send tool error to ACP", { error })
+                })
+              return
           }
+        }
+
+        // ACP clients already know the prompt they just submitted, so replaying
+        // live user parts duplicates the message. We still replay user history in
+        // loadSession() and forkSession() via processMessage().
+        if (part.type !== "text" && part.type !== "file") return
+
+        return
+      }
+
+      case "message.part.delta": {
+        const props = event.properties
+        const session = this.sessionManager.tryGet(props.sessionID)
+        if (!session) return
+        const sessionId = session.id
 
-          // ACP clients already know the prompt they just submitted, so replaying
-          // live user parts duplicates the message. We still replay user history in
-          // loadSession() and forkSession() via processMessage().
-          if (part.type !== "text" && part.type !== "file") return
+        const message = await this.sdk.session
+          .message(
+            {
+              sessionID: props.sessionID,
+              messageID: props.messageID,
+              directory: session.cwd,
+            },
+            { throwOnError: true },
+          )
+          .then((x) => x.data)
+          .catch((error) => {
+            log.error("unexpected error when fetching message", { error })
+            return undefined
+          })
 
+        if (!message || message.info.role !== "assistant") return
+
+        const part = message.parts.find((p) => p.id === props.partID)
+        if (!part) return
+
+        if (part.type === "text" && props.field === "text" && part.ignored !== true) {
+          await this.connection
+            .sessionUpdate({
+              sessionId,
+              update: {
+                sessionUpdate: "agent_message_chunk",
+                messageId: props.messageID,
+                content: {
+                  type: "text",
+                  text: props.delta,
+                },
+              },
+            })
+            .catch((error) => {
+              log.error("failed to send text delta to ACP", { error })
+            })
           return
         }
 
-        case "message.part.delta": {
-          const props = event.properties
-          const session = this.sessionManager.tryGet(props.sessionID)
-          if (!session) return
-          const sessionId = session.id
-
-          const message = await this.sdk.session
-            .message(
-              {
-                sessionID: props.sessionID,
-                messageID: props.messageID,
-                directory: session.cwd,
+        if (part.type === "reasoning" && props.field === "text") {
+          await this.connection
+            .sessionUpdate({
+              sessionId,
+              update: {
+                sessionUpdate: "agent_thought_chunk",
+                messageId: props.messageID,
+                content: {
+                  type: "text",
+                  text: props.delta,
+                },
               },
-              { throwOnError: true },
-            )
-            .then((x) => x.data)
+            })
             .catch((error) => {
-              log.error("unexpected error when fetching message", { error })
-              return undefined
+              log.error("failed to send reasoning delta to ACP", { error })
             })
+        }
+        return
+      }
+    }
+  }
 
-          if (!message || message.info.role !== "assistant") return
+  async initialize(params: InitializeRequest): Promise<InitializeResponse> {
+    log.info("initialize", { protocolVersion: params.protocolVersion })
 
-          const part = message.parts.find((p) => p.id === props.partID)
-          if (!part) return
+    const authMethod: AuthMethod = {
+      description: "Run `opencode auth login` in the terminal",
+      name: "Login with opencode",
+      id: "opencode-login",
+    }
 
-          if (part.type === "text" && props.field === "text" && part.ignored !== true) {
-            await this.connection
-              .sessionUpdate({
-                sessionId,
-                update: {
-                  sessionUpdate: "agent_message_chunk",
-                  messageId: props.messageID,
-                  content: {
-                    type: "text",
-                    text: props.delta,
-                  },
-                },
-              })
-              .catch((error) => {
-                log.error("failed to send text delta to ACP", { error })
-              })
-            return
-          }
-
-          if (part.type === "reasoning" && props.field === "text") {
-            await this.connection
-              .sessionUpdate({
-                sessionId,
-                update: {
-                  sessionUpdate: "agent_thought_chunk",
-                  messageId: props.messageID,
-                  content: {
-                    type: "text",
-                    text: props.delta,
-                  },
-                },
-              })
-              .catch((error) => {
-                log.error("failed to send reasoning delta to ACP", { error })
-              })
-          }
-          return
-        }
+    // If client supports terminal-auth capability, use that instead.
+    if (params.clientCapabilities?._meta?.["terminal-auth"] === true) {
+      authMethod._meta = {
+        "terminal-auth": {
+          command: "opencode",
+          args: ["auth", "login"],
+          label: "OpenCode Login",
+        },
       }
     }
 
-    async initialize(params: InitializeRequest): Promise<InitializeResponse> {
-      log.info("initialize", { protocolVersion: params.protocolVersion })
-
-      const authMethod: AuthMethod = {
-        description: "Run `opencode auth login` in the terminal",
-        name: "Login with opencode",
-        id: "opencode-login",
-      }
-
-      // If client supports terminal-auth capability, use that instead.
-      if (params.clientCapabilities?._meta?.["terminal-auth"] === true) {
-        authMethod._meta = {
-          "terminal-auth": {
-            command: "opencode",
-            args: ["auth", "login"],
-            label: "OpenCode Login",
-          },
-        }
-      }
-
-      return {
-        protocolVersion: 1,
-        agentCapabilities: {
-          loadSession: true,
-          mcpCapabilities: {
-            http: true,
-            sse: true,
-          },
-          promptCapabilities: {
-            embeddedContext: true,
-            image: true,
-          },
-          sessionCapabilities: {
-            fork: {},
-            list: {},
-            resume: {},
-          },
+    return {
+      protocolVersion: 1,
+      agentCapabilities: {
+        loadSession: true,
+        mcpCapabilities: {
+          http: true,
+          sse: true,
         },
-        authMethods: [authMethod],
-        agentInfo: {
-          name: "OpenCode",
-          version: InstallationVersion,
+        promptCapabilities: {
+          embeddedContext: true,
+          image: true,
         },
-      }
+        sessionCapabilities: {
+          fork: {},
+          list: {},
+          resume: {},
+        },
+      },
+      authMethods: [authMethod],
+      agentInfo: {
+        name: "OpenCode",
+        version: InstallationVersion,
+      },
     }
+  }
 
-    async authenticate(_params: AuthenticateRequest) {
-      throw new Error("Authentication not implemented")
-    }
+  async authenticate(_params: AuthenticateRequest) {
+    throw new Error("Authentication not implemented")
+  }
 
-    async newSession(params: NewSessionRequest) {
-      const directory = params.cwd
-      try {
-        const model = await defaultModel(this.config, directory)
+  async newSession(params: NewSessionRequest) {
+    const directory = params.cwd
+    try {
+      const model = await defaultModel(this.config, directory)
 
-        // Store ACP session state
-        const state = await this.sessionManager.create(params.cwd, params.mcpServers, model)
-        const sessionId = state.id
+      // Store ACP session state
+      const state = await this.sessionManager.create(params.cwd, params.mcpServers, model)
+      const sessionId = state.id
 
-        log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length })
+      log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length })
 
-        const load = await this.loadSessionMode({
-          cwd: directory,
-          mcpServers: params.mcpServers,
-          sessionId,
-        })
+      const load = await this.loadSessionMode({
+        cwd: directory,
+        mcpServers: params.mcpServers,
+        sessionId,
+      })
 
-        return {
-          sessionId,
-          configOptions: load.configOptions,
-          models: load.models,
-          modes: load.modes,
-          _meta: load._meta,
-        }
-      } catch (e) {
-        const error = MessageV2.fromError(e, {
-          providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
-        })
-        if (LoadAPIKeyError.isInstance(error)) {
-          throw RequestError.authRequired()
-        }
-        throw e
+      return {
+        sessionId,
+        configOptions: load.configOptions,
+        models: load.models,
+        modes: load.modes,
+        _meta: load._meta,
+      }
+    } catch (e) {
+      const error = MessageV2.fromError(e, {
+        providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
+      })
+      if (LoadAPIKeyError.isInstance(error)) {
+        throw RequestError.authRequired()
       }
+      throw e
     }
+  }
 
-    async loadSession(params: LoadSessionRequest) {
-      const directory = params.cwd
-      const sessionId = params.sessionId
+  async loadSession(params: LoadSessionRequest) {
+    const directory = params.cwd
+    const sessionId = params.sessionId
 
-      try {
-        const model = await defaultModel(this.config, directory)
+    try {
+      const model = await defaultModel(this.config, directory)
 
-        // Store ACP session state
-        await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model)
+      // Store ACP session state
+      await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model)
 
-        log.info("load_session", { sessionId, mcpServers: params.mcpServers.length })
+      log.info("load_session", { sessionId, mcpServers: params.mcpServers.length })
 
-        const result = await this.loadSessionMode({
-          cwd: directory,
-          mcpServers: params.mcpServers,
-          sessionId,
-        })
+      const result = await this.loadSessionMode({
+        cwd: directory,
+        mcpServers: params.mcpServers,
+        sessionId,
+      })
 
-        // Replay session history
-        const messages = await this.sdk.session
-          .messages(
-            {
-              sessionID: sessionId,
-              directory,
-            },
-            { throwOnError: true },
-          )
-          .then((x) => x.data)
-          .catch((err) => {
-            log.error("unexpected error when fetching message", { error: err })
-            return undefined
-          })
+      // Replay session history
+      const messages = await this.sdk.session
+        .messages(
+          {
+            sessionID: sessionId,
+            directory,
+          },
+          { throwOnError: true },
+        )
+        .then((x) => x.data)
+        .catch((err) => {
+          log.error("unexpected error when fetching message", { error: err })
+          return undefined
+        })
 
-        const lastUser = messages?.findLast((m) => m.info.role === "user")?.info
-        if (lastUser?.role === "user") {
-          result.models.currentModelId = `${lastUser.model.providerID}/${lastUser.model.modelID}`
-          this.sessionManager.setModel(sessionId, {
-            providerID: ProviderID.make(lastUser.model.providerID),
-            modelID: ModelID.make(lastUser.model.modelID),
-          })
-          if (result.modes?.availableModes.some((m) => m.id === lastUser.agent)) {
-            result.modes.currentModeId = lastUser.agent
-            this.sessionManager.setMode(sessionId, lastUser.agent)
-          }
-          result.configOptions = buildConfigOptions({
-            currentModelId: result.models.currentModelId,
-            availableModels: result.models.availableModels,
-            modes: result.modes,
-          })
+      const lastUser = messages?.findLast((m) => m.info.role === "user")?.info
+      if (lastUser?.role === "user") {
+        result.models.currentModelId = `${lastUser.model.providerID}/${lastUser.model.modelID}`
+        this.sessionManager.setModel(sessionId, {
+          providerID: ProviderID.make(lastUser.model.providerID),
+          modelID: ModelID.make(lastUser.model.modelID),
+        })
+        if (result.modes?.availableModes.some((m) => m.id === lastUser.agent)) {
+          result.modes.currentModeId = lastUser.agent
+          this.sessionManager.setMode(sessionId, lastUser.agent)
         }
+        result.configOptions = buildConfigOptions({
+          currentModelId: result.models.currentModelId,
+          availableModels: result.models.availableModels,
+          modes: result.modes,
+        })
+      }
 
-        for (const msg of messages ?? []) {
-          log.debug("replay message", msg)
-          await this.processMessage(msg)
-        }
+      for (const msg of messages ?? []) {
+        log.debug("replay message", msg)
+        await this.processMessage(msg)
+      }
 
-        await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
+      await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
 
-        return result
-      } catch (e) {
-        const error = MessageV2.fromError(e, {
-          providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
-        })
-        if (LoadAPIKeyError.isInstance(error)) {
-          throw RequestError.authRequired()
-        }
-        throw e
+      return result
+    } catch (e) {
+      const error = MessageV2.fromError(e, {
+        providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
+      })
+      if (LoadAPIKeyError.isInstance(error)) {
+        throw RequestError.authRequired()
       }
+      throw e
     }
+  }
 
-    async listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse> {
-      try {
-        const cursor = params.cursor ? Number(params.cursor) : undefined
-        const limit = 100
+  async listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse> {
+    try {
+      const cursor = params.cursor ? Number(params.cursor) : undefined
+      const limit = 100
 
-        const sessions = await this.sdk.session
-          .list(
-            {
-              directory: params.cwd ?? undefined,
-              roots: true,
-            },
-            { throwOnError: true },
-          )
-          .then((x) => x.data ?? [])
+      const sessions = await this.sdk.session
+        .list(
+          {
+            directory: params.cwd ?? undefined,
+            roots: true,
+          },
+          { throwOnError: true },
+        )
+        .then((x) => x.data ?? [])
 
-        const sorted = sessions.toSorted((a, b) => b.time.updated - a.time.updated)
-        const filtered = cursor ? sorted.filter((s) => s.time.updated < cursor) : sorted
-        const page = filtered.slice(0, limit)
+      const sorted = sessions.toSorted((a, b) => b.time.updated - a.time.updated)
+      const filtered = cursor ? sorted.filter((s) => s.time.updated < cursor) : sorted
+      const page = filtered.slice(0, limit)
 
-        const entries: SessionInfo[] = page.map((session) => ({
-          sessionId: session.id,
-          cwd: session.directory,
-          title: session.title,
-          updatedAt: new Date(session.time.updated).toISOString(),
-        }))
+      const entries: SessionInfo[] = page.map((session) => ({
+        sessionId: session.id,
+        cwd: session.directory,
+        title: session.title,
+        updatedAt: new Date(session.time.updated).toISOString(),
+      }))
 
-        const last = page[page.length - 1]
-        const next = filtered.length > limit && last ? String(last.time.updated) : undefined
+      const last = page[page.length - 1]
+      const next = filtered.length > limit && last ? String(last.time.updated) : undefined
 
-        const response: ListSessionsResponse = {
-          sessions: entries,
-        }
-        if (next) response.nextCursor = next
-        return response
-      } catch (e) {
-        const error = MessageV2.fromError(e, {
-          providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
-        })
-        if (LoadAPIKeyError.isInstance(error)) {
-          throw RequestError.authRequired()
-        }
-        throw e
+      const response: ListSessionsResponse = {
+        sessions: entries,
+      }
+      if (next) response.nextCursor = next
+      return response
+    } catch (e) {
+      const error = MessageV2.fromError(e, {
+        providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
+      })
+      if (LoadAPIKeyError.isInstance(error)) {
+        throw RequestError.authRequired()
       }
+      throw e
     }
+  }
 
-    async unstable_forkSession(params: ForkSessionRequest): Promise<ForkSessionResponse> {
-      const directory = params.cwd
-      const mcpServers = params.mcpServers ?? []
+  async unstable_forkSession(params: ForkSessionRequest): Promise<ForkSessionResponse> {
+    const directory = params.cwd
+    const mcpServers = params.mcpServers ?? []
 
-      try {
-        const model = await defaultModel(this.config, directory)
+    try {
+      const model = await defaultModel(this.config, directory)
 
-        const forked = await this.sdk.session
-          .fork(
-            {
-              sessionID: params.sessionId,
-              directory,
-            },
-            { throwOnError: true },
-          )
-          .then((x) => x.data)
+      const forked = await this.sdk.session
+        .fork(
+          {
+            sessionID: params.sessionId,
+            directory,
+          },
+          { throwOnError: true },
+        )
+        .then((x) => x.data)
 
-        if (!forked) {
-          throw new Error("Fork session returned no data")
-        }
+      if (!forked) {
+        throw new Error("Fork session returned no data")
+      }
 
-        const sessionId = forked.id
-        await this.sessionManager.load(sessionId, directory, mcpServers, model)
+      const sessionId = forked.id
+      await this.sessionManager.load(sessionId, directory, mcpServers, model)
 
-        log.info("fork_session", { sessionId, mcpServers: mcpServers.length })
+      log.info("fork_session", { sessionId, mcpServers: mcpServers.length })
 
-        const mode = await this.loadSessionMode({
-          cwd: directory,
-          mcpServers,
-          sessionId,
-        })
+      const mode = await this.loadSessionMode({
+        cwd: directory,
+        mcpServers,
+        sessionId,
+      })
 
-        const messages = await this.sdk.session
-          .messages(
-            {
-              sessionID: sessionId,
-              directory,
-            },
-            { throwOnError: true },
-          )
-          .then((x) => x.data)
-          .catch((err) => {
-            log.error("unexpected error when fetching message", { error: err })
-            return undefined
-          })
+      const messages = await this.sdk.session
+        .messages(
+          {
+            sessionID: sessionId,
+            directory,
+          },
+          { throwOnError: true },
+        )
+        .then((x) => x.data)
+        .catch((err) => {
+          log.error("unexpected error when fetching message", { error: err })
+          return undefined
+        })
 
-        for (const msg of messages ?? []) {
-          log.debug("replay message", msg)
-          await this.processMessage(msg)
-        }
+      for (const msg of messages ?? []) {
+        log.debug("replay message", msg)
+        await this.processMessage(msg)
+      }
 
-        await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
+      await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
 
-        return mode
-      } catch (e) {
-        const error = MessageV2.fromError(e, {
-          providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
-        })
-        if (LoadAPIKeyError.isInstance(error)) {
-          throw RequestError.authRequired()
-        }
-        throw e
+      return mode
+    } catch (e) {
+      const error = MessageV2.fromError(e, {
+        providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
+      })
+      if (LoadAPIKeyError.isInstance(error)) {
+        throw RequestError.authRequired()
       }
+      throw e
     }
+  }
 
-    async unstable_resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
-      const directory = params.cwd
-      const sessionId = params.sessionId
-      const mcpServers = params.mcpServers ?? []
+  async unstable_resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
+    const directory = params.cwd
+    const sessionId = params.sessionId
+    const mcpServers = params.mcpServers ?? []
 
-      try {
-        const model = await defaultModel(this.config, directory)
-        await this.sessionManager.load(sessionId, directory, mcpServers, model)
+    try {
+      const model = await defaultModel(this.config, directory)
+      await this.sessionManager.load(sessionId, directory, mcpServers, model)
 
-        log.info("resume_session", { sessionId, mcpServers: mcpServers.length })
+      log.info("resume_session", { sessionId, mcpServers: mcpServers.length })
 
-        const result = await this.loadSessionMode({
-          cwd: directory,
-          mcpServers,
-          sessionId,
-        })
+      const result = await this.loadSessionMode({
+        cwd: directory,
+        mcpServers,
+        sessionId,
+      })
 
-        await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
+      await sendUsageUpdate(this.connection, this.sdk, sessionId, directory)
 
-        return result
-      } catch (e) {
-        const error = MessageV2.fromError(e, {
-          providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
-        })
-        if (LoadAPIKeyError.isInstance(error)) {
-          throw RequestError.authRequired()
-        }
-        throw e
+      return result
+    } catch (e) {
+      const error = MessageV2.fromError(e, {
+        providerID: ProviderID.make(this.config.defaultModel?.providerID ?? "unknown"),
+      })
+      if (LoadAPIKeyError.isInstance(error)) {
+        throw RequestError.authRequired()
       }
+      throw e
     }
+  }
 
-    private async processMessage(message: SessionMessageResponse) {
-      log.debug("process message", message)
-      if (message.info.role !== "assistant" && message.info.role !== "user") return
-      const sessionId = message.info.sessionID
-
-      for (const part of message.parts) {
-        if (part.type === "tool") {
-          await this.toolStart(sessionId, part)
-          switch (part.state.status) {
-            case "pending":
-              this.bashSnapshots.delete(part.callID)
-              break
-            case "running":
-              const output = this.bashOutput(part)
-              const runningContent: ToolCallContent[] = []
-              if (output) {
-                runningContent.push({
-                  type: "content",
-                  content: {
-                    type: "text",
-                    text: output,
-                  },
-                })
-              }
-              await this.connection
-                .sessionUpdate({
-                  sessionId,
-                  update: {
-                    sessionUpdate: "tool_call_update",
-                    toolCallId: part.callID,
-                    status: "in_progress",
-                    kind: toToolKind(part.tool),
-                    title: part.tool,
-                    locations: toLocations(part.tool, part.state.input),
-                    rawInput: part.state.input,
-                    ...(runningContent.length > 0 && { content: runningContent }),
-                  },
-                })
-                .catch((err) => {
-                  log.error("failed to send tool in_progress to ACP", { error: err })
-                })
-              break
-            case "completed":
-              this.toolStarts.delete(part.callID)
-              this.bashSnapshots.delete(part.callID)
-              const kind = toToolKind(part.tool)
-              const content: ToolCallContent[] = [
-                {
-                  type: "content",
-                  content: {
-                    type: "text",
-                    text: part.state.output,
-                  },
+  private async processMessage(message: SessionMessageResponse) {
+    log.debug("process message", message)
+    if (message.info.role !== "assistant" && message.info.role !== "user") return
+    const sessionId = message.info.sessionID
+
+    for (const part of message.parts) {
+      if (part.type === "tool") {
+        await this.toolStart(sessionId, part)
+        switch (part.state.status) {
+          case "pending":
+            this.bashSnapshots.delete(part.callID)
+            break
+          case "running":
+            const output = this.bashOutput(part)
+            const runningContent: ToolCallContent[] = []
+            if (output) {
+              runningContent.push({
+                type: "content",
+                content: {
+                  type: "text",
+                  text: output,
                 },
-              ]
-
-              if (kind === "edit") {
-                const input = part.state.input
-                const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
-                const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
-                const newText =
-                  typeof input["newString"] === "string"
-                    ? input["newString"]
-                    : typeof input["content"] === "string"
-                      ? input["content"]
-                      : ""
-                content.push({
-                  type: "diff",
-                  path: filePath,
-                  oldText,
-                  newText,
-                })
-              }
+              })
+            }
+            await this.connection
+              .sessionUpdate({
+                sessionId,
+                update: {
+                  sessionUpdate: "tool_call_update",
+                  toolCallId: part.callID,
+                  status: "in_progress",
+                  kind: toToolKind(part.tool),
+                  title: part.tool,
+                  locations: toLocations(part.tool, part.state.input),
+                  rawInput: part.state.input,
+                  ...(runningContent.length > 0 && { content: runningContent }),
+                },
+              })
+              .catch((err) => {
+                log.error("failed to send tool in_progress to ACP", { error: err })
+              })
+            break
+          case "completed":
+            this.toolStarts.delete(part.callID)
+            this.bashSnapshots.delete(part.callID)
+            const kind = toToolKind(part.tool)
+            const content: ToolCallContent[] = [
+              {
+                type: "content",
+                content: {
+                  type: "text",
+                  text: part.state.output,
+                },
+              },
+            ]
+
+            if (kind === "edit") {
+              const input = part.state.input
+              const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
+              const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
+              const newText =
+                typeof input["newString"] === "string"
+                  ? input["newString"]
+                  : typeof input["content"] === "string"
+                    ? input["content"]
+                    : ""
+              content.push({
+                type: "diff",
+                path: filePath,
+                oldText,
+                newText,
+              })
+            }
 
-              if (part.tool === "todowrite") {
-                const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
-                if (parsedTodos.success) {
-                  await this.connection
-                    .sessionUpdate({
-                      sessionId,
-                      update: {
-                        sessionUpdate: "plan",
-                        entries: parsedTodos.data.map((todo) => {
-                          const status: PlanEntry["status"] =
-                            todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
-                          return {
-                            priority: "medium",
-                            status,
-                            content: todo.content,
-                          }
-                        }),
-                      },
-                    })
-                    .catch((err) => {
-                      log.error("failed to send session update for todo", { error: err })
-                    })
-                } else {
-                  log.error("failed to parse todo output", { error: parsedTodos.error })
-                }
+            if (part.tool === "todowrite") {
+              const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
+              if (parsedTodos.success) {
+                await this.connection
+                  .sessionUpdate({
+                    sessionId,
+                    update: {
+                      sessionUpdate: "plan",
+                      entries: parsedTodos.data.map((todo) => {
+                        const status: PlanEntry["status"] =
+                          todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
+                        return {
+                          priority: "medium",
+                          status,
+                          content: todo.content,
+                        }
+                      }),
+                    },
+                  })
+                  .catch((err) => {
+                    log.error("failed to send session update for todo", { error: err })
+                  })
+              } else {
+                log.error("failed to parse todo output", { error: parsedTodos.error })
               }
+            }
 
-              await this.connection
-                .sessionUpdate({
-                  sessionId,
-                  update: {
-                    sessionUpdate: "tool_call_update",
-                    toolCallId: part.callID,
-                    status: "completed",
-                    kind,
-                    content,
-                    title: part.state.title,
-                    rawInput: part.state.input,
-                    rawOutput: {
-                      output: part.state.output,
-                      metadata: part.state.metadata,
-                    },
-                  },
-                })
-                .catch((err) => {
-                  log.error("failed to send tool completed to ACP", { error: err })
-                })
-              break
-            case "error":
-              this.toolStarts.delete(part.callID)
-              this.bashSnapshots.delete(part.callID)
-              await this.connection
-                .sessionUpdate({
-                  sessionId,
-                  update: {
-                    sessionUpdate: "tool_call_update",
-                    toolCallId: part.callID,
-                    status: "failed",
-                    kind: toToolKind(part.tool),
-                    title: part.tool,
-                    rawInput: part.state.input,
-                    content: [
-                      {
-                        type: "content",
-                        content: {
-                          type: "text",
-                          text: part.state.error,
-                        },
-                      },
-                    ],
-                    rawOutput: {
-                      error: part.state.error,
-                      metadata: part.state.metadata,
-                    },
-                  },
-                })
-                .catch((err) => {
-                  log.error("failed to send tool error to ACP", { error: err })
-                })
-              break
-          }
-        } else if (part.type === "text") {
-          if (part.text) {
-            const audience: Role[] | undefined = part.synthetic ? ["assistant"] : part.ignored ? ["user"] : undefined
             await this.connection
               .sessionUpdate({
                 sessionId,
                 update: {
-                  sessionUpdate: message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk",
-                  messageId: message.info.id,
-                  content: {
-                    type: "text",
-                    text: part.text,
-                    ...(audience && { annotations: { audience } }),
+                  sessionUpdate: "tool_call_update",
+                  toolCallId: part.callID,
+                  status: "completed",
+                  kind,
+                  content,
+                  title: part.state.title,
+                  rawInput: part.state.input,
+                  rawOutput: {
+                    output: part.state.output,
+                    metadata: part.state.metadata,
                   },
                 },
               })
               .catch((err) => {
-                log.error("failed to send text to ACP", { error: err })
+                log.error("failed to send tool completed to ACP", { error: err })
               })
-          }
-        } else if (part.type === "file") {
-          // Replay file attachments as appropriate ACP content blocks.
-          // OpenCode stores files internally as { type: "file", url, filename, mime }.
-          // We convert these back to ACP blocks based on the URL scheme and MIME type:
-          // - file:// URLs → resource_link
-          // - data: URLs with image/* → image block
-          // - data: URLs with text/* or application/json → resource with text
-          // - data: URLs with other types → resource with blob
-          const url = part.url
-          const filename = part.filename ?? "file"
-          const mime = part.mime || "application/octet-stream"
-          const messageChunk = message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk"
-
-          if (url.startsWith("file://")) {
-            // Local file reference - send as resource_link
+            break
+          case "error":
+            this.toolStarts.delete(part.callID)
+            this.bashSnapshots.delete(part.callID)
             await this.connection
               .sessionUpdate({
                 sessionId,
                 update: {
-                  sessionUpdate: messageChunk,
-                  messageId: message.info.id,
-                  content: { type: "resource_link", uri: url, name: filename, mimeType: mime },
+                  sessionUpdate: "tool_call_update",
+                  toolCallId: part.callID,
+                  status: "failed",
+                  kind: toToolKind(part.tool),
+                  title: part.tool,
+                  rawInput: part.state.input,
+                  content: [
+                    {
+                      type: "content",
+                      content: {
+                        type: "text",
+                        text: part.state.error,
+                      },
+                    },
+                  ],
+                  rawOutput: {
+                    error: part.state.error,
+                    metadata: part.state.metadata,
+                  },
                 },
               })
               .catch((err) => {
-                log.error("failed to send resource_link to ACP", { error: err })
+                log.error("failed to send tool error to ACP", { error: err })
               })
-          } else if (url.startsWith("data:")) {
-            // Embedded content - parse data URL and send as appropriate block type
-            const base64Match = url.match(/^data:([^;]+);base64,(.*)$/)
-            const dataMime = base64Match?.[1]
-            const base64Data = base64Match?.[2] ?? ""
+            break
+        }
+      } else if (part.type === "text") {
+        if (part.text) {
+          const audience: Role[] | undefined = part.synthetic ? ["assistant"] : part.ignored ? ["user"] : undefined
+          await this.connection
+            .sessionUpdate({
+              sessionId,
+              update: {
+                sessionUpdate: message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk",
+                messageId: message.info.id,
+                content: {
+                  type: "text",
+                  text: part.text,
+                  ...(audience && { annotations: { audience } }),
+                },
+              },
+            })
+            .catch((err) => {
+              log.error("failed to send text to ACP", { error: err })
+            })
+        }
+      } else if (part.type === "file") {
+        // Replay file attachments as appropriate ACP content blocks.
+        // OpenCode stores files internally as { type: "file", url, filename, mime }.
+        // We convert these back to ACP blocks based on the URL scheme and MIME type:
+        // - file:// URLs → resource_link
+        // - data: URLs with image/* → image block
+        // - data: URLs with text/* or application/json → resource with text
+        // - data: URLs with other types → resource with blob
+        const url = part.url
+        const filename = part.filename ?? "file"
+        const mime = part.mime || "application/octet-stream"
+        const messageChunk = message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk"
+
+        if (url.startsWith("file://")) {
+          // Local file reference - send as resource_link
+          await this.connection
+            .sessionUpdate({
+              sessionId,
+              update: {
+                sessionUpdate: messageChunk,
+                messageId: message.info.id,
+                content: { type: "resource_link", uri: url, name: filename, mimeType: mime },
+              },
+            })
+            .catch((err) => {
+              log.error("failed to send resource_link to ACP", { error: err })
+            })
+        } else if (url.startsWith("data:")) {
+          // Embedded content - parse data URL and send as appropriate block type
+          const base64Match = url.match(/^data:([^;]+);base64,(.*)$/)
+          const dataMime = base64Match?.[1]
+          const base64Data = base64Match?.[2] ?? ""
 
-            const effectiveMime = dataMime || mime
+          const effectiveMime = dataMime || mime
 
-            if (effectiveMime.startsWith("image/")) {
-              // Image - send as image block
-              await this.connection
-                .sessionUpdate({
-                  sessionId,
-                  update: {
-                    sessionUpdate: messageChunk,
-                    messageId: message.info.id,
-                    content: {
-                      type: "image",
-                      mimeType: effectiveMime,
-                      data: base64Data,
-                      uri: pathToFileURL(filename).href,
-                    },
-                  },
-                })
-                .catch((err) => {
-                  log.error("failed to send image to ACP", { error: err })
-                })
-            } else {
-              // Non-image: text types get decoded, binary types stay as blob
-              const isText = effectiveMime.startsWith("text/") || effectiveMime === "application/json"
-              const fileUri = pathToFileURL(filename).href
-              const resource = isText
-                ? {
-                    uri: fileUri,
-                    mimeType: effectiveMime,
-                    text: Buffer.from(base64Data, "base64").toString("utf-8"),
-                  }
-                : { uri: fileUri, mimeType: effectiveMime, blob: base64Data }
-
-              await this.connection
-                .sessionUpdate({
-                  sessionId,
-                  update: {
-                    sessionUpdate: messageChunk,
-                    messageId: message.info.id,
-                    content: { type: "resource", resource },
-                  },
-                })
-                .catch((err) => {
-                  log.error("failed to send resource to ACP", { error: err })
-                })
-            }
-          }
-          // URLs that don't match file:// or data: are skipped (unsupported)
-        } else if (part.type === "reasoning") {
-          if (part.text) {
+          if (effectiveMime.startsWith("image/")) {
+            // Image - send as image block
             await this.connection
               .sessionUpdate({
                 sessionId,
                 update: {
-                  sessionUpdate: "agent_thought_chunk",
+                  sessionUpdate: messageChunk,
                   messageId: message.info.id,
                   content: {
-                    type: "text",
-                    text: part.text,
+                    type: "image",
+                    mimeType: effectiveMime,
+                    data: base64Data,
+                    uri: pathToFileURL(filename).href,
                   },
                 },
               })
               .catch((err) => {
-                log.error("failed to send reasoning to ACP", { error: err })
+                log.error("failed to send image to ACP", { error: err })
+              })
+          } else {
+            // Non-image: text types get decoded, binary types stay as blob
+            const isText = effectiveMime.startsWith("text/") || effectiveMime === "application/json"
+            const fileUri = pathToFileURL(filename).href
+            const resource = isText
+              ? {
+                  uri: fileUri,
+                  mimeType: effectiveMime,
+                  text: Buffer.from(base64Data, "base64").toString("utf-8"),
+                }
+              : { uri: fileUri, mimeType: effectiveMime, blob: base64Data }
+
+            await this.connection
+              .sessionUpdate({
+                sessionId,
+                update: {
+                  sessionUpdate: messageChunk,
+                  messageId: message.info.id,
+                  content: { type: "resource", resource },
+                },
+              })
+              .catch((err) => {
+                log.error("failed to send resource to ACP", { error: err })
               })
           }
         }
-      }
-    }
-
-    private bashOutput(part: ToolPart) {
-      if (part.tool !== "bash") return
-      if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return
-      const output = part.state.metadata["output"]
-      if (typeof output !== "string") return
-      return output
-    }
-
-    private async toolStart(sessionId: string, part: ToolPart) {
-      if (this.toolStarts.has(part.callID)) return
-      this.toolStarts.add(part.callID)
-      await this.connection
-        .sessionUpdate({
-          sessionId,
-          update: {
-            sessionUpdate: "tool_call",
-            toolCallId: part.callID,
-            title: part.tool,
-            kind: toToolKind(part.tool),
-            status: "pending",
-            locations: [],
-            rawInput: {},
-          },
-        })
-        .catch((error) => {
-          log.error("failed to send tool pending to ACP", { error })
-        })
-    }
-
-    private async loadAvailableModes(directory: string): Promise<ModeOption[]> {
-      const agents = await this.config.sdk.app
-        .agents(
-          {
-            directory,
-          },
-          { throwOnError: true },
-        )
-        .then((resp) => resp.data!)
-
-      return agents
-        .filter((agent) => agent.mode !== "subagent" && !agent.hidden)
-        .map((agent) => ({
-          id: agent.name,
-          name: agent.name,
-          description: agent.description,
-        }))
-    }
-
-    private async resolveModeState(
-      directory: string,
-      sessionId: string,
-    ): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> {
-      const availableModes = await this.loadAvailableModes(directory)
-      const currentModeId =
-        this.sessionManager.get(sessionId).modeId ||
-        (await (async () => {
-          if (!availableModes.length) return undefined
-          const defaultAgentName = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent()))
-          const resolvedModeId =
-            availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
-          this.sessionManager.setMode(sessionId, resolvedModeId)
-          return resolvedModeId
-        })())
-
-      return { availableModes, currentModeId }
+        // URLs that don't match file:// or data: are skipped (unsupported)
+      } else if (part.type === "reasoning") {
+        if (part.text) {
+          await this.connection
+            .sessionUpdate({
+              sessionId,
+              update: {
+                sessionUpdate: "agent_thought_chunk",
+                messageId: message.info.id,
+                content: {
+                  type: "text",
+                  text: part.text,
+                },
+              },
+            })
+            .catch((err) => {
+              log.error("failed to send reasoning to ACP", { error: err })
+            })
+        }
+      }
     }
+  }
 
-    private async loadSessionMode(params: LoadSessionRequest) {
-      const directory = params.cwd
-      const model = await defaultModel(this.config, directory)
-      const sessionId = params.sessionId
-
-      const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
-      const entries = sortProvidersByName(providers)
-      const availableVariants = modelVariantsFromProviders(entries, model)
-      const currentVariant = this.sessionManager.getVariant(sessionId)
-      if (currentVariant && !availableVariants.includes(currentVariant)) {
-        this.sessionManager.setVariant(sessionId, undefined)
-      }
-      const availableModels = buildAvailableModels(entries, { includeVariants: true })
-      const modeState = await this.resolveModeState(directory, sessionId)
-      const currentModeId = modeState.currentModeId
-      const modes = currentModeId
-        ? {
-            availableModes: modeState.availableModes,
-            currentModeId,
-          }
-        : undefined
+  private bashOutput(part: ToolPart) {
+    if (part.tool !== "bash") return
+    if (!("metadata" in part.state) || !part.state.metadata || typeof part.state.metadata !== "object") return
+    const output = part.state.metadata["output"]
+    if (typeof output !== "string") return
+    return output
+  }
 
-      const commands = await this.config.sdk.command
-        .list(
-          {
-            directory,
-          },
-          { throwOnError: true },
-        )
-        .then((resp) => resp.data!)
+  private async toolStart(sessionId: string, part: ToolPart) {
+    if (this.toolStarts.has(part.callID)) return
+    this.toolStarts.add(part.callID)
+    await this.connection
+      .sessionUpdate({
+        sessionId,
+        update: {
+          sessionUpdate: "tool_call",
+          toolCallId: part.callID,
+          title: part.tool,
+          kind: toToolKind(part.tool),
+          status: "pending",
+          locations: [],
+          rawInput: {},
+        },
+      })
+      .catch((error) => {
+        log.error("failed to send tool pending to ACP", { error })
+      })
+  }
 
-      const availableCommands = commands.map((command) => ({
-        name: command.name,
-        description: command.description ?? "",
+  private async loadAvailableModes(directory: string): Promise<ModeOption[]> {
+    const agents = await this.config.sdk.app
+      .agents(
+        {
+          directory,
+        },
+        { throwOnError: true },
+      )
+      .then((resp) => resp.data!)
+
+    return agents
+      .filter((agent) => agent.mode !== "subagent" && !agent.hidden)
+      .map((agent) => ({
+        id: agent.name,
+        name: agent.name,
+        description: agent.description,
       }))
-      const names = new Set(availableCommands.map((c) => c.name))
-      if (!names.has("compact"))
-        availableCommands.push({
-          name: "compact",
-          description: "compact the session",
-        })
+  }
 
-      const mcpServers: Record<string, Config.Mcp> = {}
-      for (const server of params.mcpServers) {
-        if ("type" in server) {
-          mcpServers[server.name] = {
-            url: server.url,
-            headers: server.headers.reduce<Record<string, string>>((acc, { name, value }) => {
-              acc[name] = value
-              return acc
-            }, {}),
-            type: "remote",
-          }
-        } else {
-          mcpServers[server.name] = {
-            type: "local",
-            command: [server.command, ...server.args],
-            environment: server.env.reduce<Record<string, string>>((acc, { name, value }) => {
-              acc[name] = value
-              return acc
-            }, {}),
-          }
+  private async resolveModeState(
+    directory: string,
+    sessionId: string,
+  ): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> {
+    const availableModes = await this.loadAvailableModes(directory)
+    const currentModeId =
+      this.sessionManager.get(sessionId).modeId ||
+      (await (async () => {
+        if (!availableModes.length) return undefined
+        const defaultAgentName = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent()))
+        const resolvedModeId =
+          availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id
+        this.sessionManager.setMode(sessionId, resolvedModeId)
+        return resolvedModeId
+      })())
+
+    return { availableModes, currentModeId }
+  }
+
+  private async loadSessionMode(params: LoadSessionRequest) {
+    const directory = params.cwd
+    const model = await defaultModel(this.config, directory)
+    const sessionId = params.sessionId
+
+    const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers)
+    const entries = sortProvidersByName(providers)
+    const availableVariants = modelVariantsFromProviders(entries, model)
+    const currentVariant = this.sessionManager.getVariant(sessionId)
+    if (currentVariant && !availableVariants.includes(currentVariant)) {
+      this.sessionManager.setVariant(sessionId, undefined)
+    }
+    const availableModels = buildAvailableModels(entries, { includeVariants: true })
+    const modeState = await this.resolveModeState(directory, sessionId)
+    const currentModeId = modeState.currentModeId
+    const modes = currentModeId
+      ? {
+          availableModes: modeState.availableModes,
+          currentModeId,
         }
-      }
+      : undefined
 
-      await Promise.all(
-        Object.entries(mcpServers).map(async ([key, mcp]) => {
-          await this.sdk.mcp
-            .add(
-              {
-                directory,
-                name: key,
-                config: mcp,
-              },
-              { throwOnError: true },
-            )
-            .catch((error) => {
-              log.error("failed to add mcp server", { name: key, error })
-            })
-        }),
+    const commands = await this.config.sdk.command
+      .list(
+        {
+          directory,
+        },
+        { throwOnError: true },
       )
+      .then((resp) => resp.data!)
+
+    const availableCommands = commands.map((command) => ({
+      name: command.name,
+      description: command.description ?? "",
+    }))
+    const names = new Set(availableCommands.map((c) => c.name))
+    if (!names.has("compact"))
+      availableCommands.push({
+        name: "compact",
+        description: "compact the session",
+      })
 
-      setTimeout(() => {
-        void this.connection.sessionUpdate({
-          sessionId,
-          update: {
-            sessionUpdate: "available_commands_update",
-            availableCommands,
-          },
-        })
-      }, 0)
+    const mcpServers: Record<string, Config.Mcp> = {}
+    for (const server of params.mcpServers) {
+      if ("type" in server) {
+        mcpServers[server.name] = {
+          url: server.url,
+          headers: server.headers.reduce<Record<string, string>>((acc, { name, value }) => {
+            acc[name] = value
+            return acc
+          }, {}),
+          type: "remote",
+        }
+      } else {
+        mcpServers[server.name] = {
+          type: "local",
+          command: [server.command, ...server.args],
+          environment: server.env.reduce<Record<string, string>>((acc, { name, value }) => {
+            acc[name] = value
+            return acc
+          }, {}),
+        }
+      }
+    }
 
-      return {
+    await Promise.all(
+      Object.entries(mcpServers).map(async ([key, mcp]) => {
+        await this.sdk.mcp
+          .add(
+            {
+              directory,
+              name: key,
+              config: mcp,
+            },
+            { throwOnError: true },
+          )
+          .catch((error) => {
+            log.error("failed to add mcp server", { name: key, error })
+          })
+      }),
+    )
+
+    setTimeout(() => {
+      void this.connection.sessionUpdate({
         sessionId,
-        models: {
-          currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
-          availableModels,
+        update: {
+          sessionUpdate: "available_commands_update",
+          availableCommands,
         },
+      })
+    }, 0)
+
+    return {
+      sessionId,
+      models: {
+        currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
+        availableModels,
+      },
+      modes,
+      configOptions: buildConfigOptions({
+        currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
+        availableModels,
         modes,
-        configOptions: buildConfigOptions({
-          currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true),
-          availableModels,
-          modes,
-        }),
-        _meta: buildVariantMeta({
-          model,
-          variant: this.sessionManager.getVariant(sessionId),
-          availableVariants,
-        }),
-      }
+      }),
+      _meta: buildVariantMeta({
+        model,
+        variant: this.sessionManager.getVariant(sessionId),
+        availableVariants,
+      }),
     }
+  }
 
-    async unstable_setSessionModel(params: SetSessionModelRequest) {
-      const session = this.sessionManager.get(params.sessionId)
-      const providers = await this.sdk.config
-        .providers({ directory: session.cwd }, { throwOnError: true })
-        .then((x) => x.data!.providers)
+  async unstable_setSessionModel(params: SetSessionModelRequest) {
+    const session = this.sessionManager.get(params.sessionId)
+    const providers = await this.sdk.config
+      .providers({ directory: session.cwd }, { throwOnError: true })
+      .then((x) => x.data!.providers)
 
-      const selection = parseModelSelection(params.modelId, providers)
-      this.sessionManager.setModel(session.id, selection.model)
-      this.sessionManager.setVariant(session.id, selection.variant)
+    const selection = parseModelSelection(params.modelId, providers)
+    this.sessionManager.setModel(session.id, selection.model)
+    this.sessionManager.setVariant(session.id, selection.variant)
 
-      const entries = sortProvidersByName(providers)
-      const availableVariants = modelVariantsFromProviders(entries, selection.model)
+    const entries = sortProvidersByName(providers)
+    const availableVariants = modelVariantsFromProviders(entries, selection.model)
 
-      return {
-        _meta: buildVariantMeta({
-          model: selection.model,
-          variant: selection.variant,
-          availableVariants,
-        }),
-      }
+    return {
+      _meta: buildVariantMeta({
+        model: selection.model,
+        variant: selection.variant,
+        availableVariants,
+      }),
     }
+  }
 
-    async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> {
-      const session = this.sessionManager.get(params.sessionId)
-      const availableModes = await this.loadAvailableModes(session.cwd)
-      if (!availableModes.some((mode) => mode.id === params.modeId)) {
-        throw new Error(`Agent not found: ${params.modeId}`)
-      }
-      this.sessionManager.setMode(params.sessionId, params.modeId)
+  async setSessionMode(params: SetSessionModeRequest): Promise<SetSessionModeResponse | void> {
+    const session = this.sessionManager.get(params.sessionId)
+    const availableModes = await this.loadAvailableModes(session.cwd)
+    if (!availableModes.some((mode) => mode.id === params.modeId)) {
+      throw new Error(`Agent not found: ${params.modeId}`)
     }
+    this.sessionManager.setMode(params.sessionId, params.modeId)
+  }
 
-    async setSessionConfigOption(params: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse> {
-      const session = this.sessionManager.get(params.sessionId)
-      const providers = await this.sdk.config
-        .providers({ directory: session.cwd }, { throwOnError: true })
-        .then((x) => x.data!.providers)
-      const entries = sortProvidersByName(providers)
-
-      if (params.configId === "model") {
-        if (typeof params.value !== "string") throw RequestError.invalidParams("model value must be a string")
-        const selection = parseModelSelection(params.value, providers)
-        this.sessionManager.setModel(session.id, selection.model)
-        this.sessionManager.setVariant(session.id, selection.variant)
-      } else if (params.configId === "mode") {
-        if (typeof params.value !== "string") throw RequestError.invalidParams("mode value must be a string")
-        const availableModes = await this.loadAvailableModes(session.cwd)
-        if (!availableModes.some((mode) => mode.id === params.value)) {
-          throw RequestError.invalidParams(JSON.stringify({ error: `Mode not found: ${params.value}` }))
-        }
-        this.sessionManager.setMode(session.id, params.value)
-      } else {
-        throw RequestError.invalidParams(JSON.stringify({ error: `Unknown config option: ${params.configId}` }))
+  async setSessionConfigOption(params: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse> {
+    const session = this.sessionManager.get(params.sessionId)
+    const providers = await this.sdk.config
+      .providers({ directory: session.cwd }, { throwOnError: true })
+      .then((x) => x.data!.providers)
+    const entries = sortProvidersByName(providers)
+
+    if (params.configId === "model") {
+      if (typeof params.value !== "string") throw RequestError.invalidParams("model value must be a string")
+      const selection = parseModelSelection(params.value, providers)
+      this.sessionManager.setModel(session.id, selection.model)
+      this.sessionManager.setVariant(session.id, selection.variant)
+    } else if (params.configId === "mode") {
+      if (typeof params.value !== "string") throw RequestError.invalidParams("mode value must be a string")
+      const availableModes = await this.loadAvailableModes(session.cwd)
+      if (!availableModes.some((mode) => mode.id === params.value)) {
+        throw RequestError.invalidParams(JSON.stringify({ error: `Mode not found: ${params.value}` }))
       }
+      this.sessionManager.setMode(session.id, params.value)
+    } else {
+      throw RequestError.invalidParams(JSON.stringify({ error: `Unknown config option: ${params.configId}` }))
+    }
 
-      const updatedSession = this.sessionManager.get(session.id)
-      const model = updatedSession.model ?? (await defaultModel(this.config, session.cwd))
-      const availableVariants = modelVariantsFromProviders(entries, model)
-      const currentModelId = formatModelIdWithVariant(model, updatedSession.variant, availableVariants, true)
-      const availableModels = buildAvailableModels(entries, { includeVariants: true })
-      const modeState = await this.resolveModeState(session.cwd, session.id)
-      const modes = modeState.currentModeId
-        ? { availableModes: modeState.availableModes, currentModeId: modeState.currentModeId }
-        : undefined
+    const updatedSession = this.sessionManager.get(session.id)
+    const model = updatedSession.model ?? (await defaultModel(this.config, session.cwd))
+    const availableVariants = modelVariantsFromProviders(entries, model)
+    const currentModelId = formatModelIdWithVariant(model, updatedSession.variant, availableVariants, true)
+    const availableModels = buildAvailableModels(entries, { includeVariants: true })
+    const modeState = await this.resolveModeState(session.cwd, session.id)
+    const modes = modeState.currentModeId
+      ? { availableModes: modeState.availableModes, currentModeId: modeState.currentModeId }
+      : undefined
 
-      return {
-        configOptions: buildConfigOptions({ currentModelId, availableModels, modes }),
-      }
+    return {
+      configOptions: buildConfigOptions({ currentModelId, availableModels, modes }),
     }
+  }
 
-    async prompt(params: PromptRequest) {
-      const sessionID = params.sessionId
-      const session = this.sessionManager.get(sessionID)
-      const directory = session.cwd
+  async prompt(params: PromptRequest) {
+    const sessionID = params.sessionId
+    const session = this.sessionManager.get(sessionID)
+    const directory = session.cwd
 
-      const current = session.model
-      const model = current ?? (await defaultModel(this.config, directory))
-      if (!current) {
-        this.sessionManager.setModel(session.id, model)
-      }
-      const agent =
-        session.modeId ?? (await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent())))
-
-      const parts: Array<
-        | { type: "text"; text: string; synthetic?: boolean; ignored?: boolean }
-        | { type: "file"; url: string; filename: string; mime: string }
-      > = []
-      for (const part of params.prompt) {
-        switch (part.type) {
-          case "text":
-            const audience = part.annotations?.audience
-            const forAssistant = audience?.length === 1 && audience[0] === "assistant"
-            const forUser = audience?.length === 1 && audience[0] === "user"
+    const current = session.model
+    const model = current ?? (await defaultModel(this.config, directory))
+    if (!current) {
+      this.sessionManager.setModel(session.id, model)
+    }
+    const agent =
+      session.modeId ?? (await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent())))
+
+    const parts: Array<
+      | { type: "text"; text: string; synthetic?: boolean; ignored?: boolean }
+      | { type: "file"; url: string; filename: string; mime: string }
+    > = []
+    for (const part of params.prompt) {
+      switch (part.type) {
+        case "text":
+          const audience = part.annotations?.audience
+          const forAssistant = audience?.length === 1 && audience[0] === "assistant"
+          const forUser = audience?.length === 1 && audience[0] === "user"
+          parts.push({
+            type: "text" as const,
+            text: part.text,
+            ...(forAssistant && { synthetic: true }),
+            ...(forUser && { ignored: true }),
+          })
+          break
+        case "image": {
+          const parsed = parseUri(part.uri ?? "")
+          const filename = parsed.type === "file" ? parsed.filename : "image"
+          if (part.data) {
             parts.push({
-              type: "text" as const,
-              text: part.text,
-              ...(forAssistant && { synthetic: true }),
-              ...(forUser && { ignored: true }),
+              type: "file",
+              url: `data:${part.mimeType};base64,${part.data}`,
+              filename,
+              mime: part.mimeType,
+            })
+          } else if (part.uri && part.uri.startsWith("http:")) {
+            parts.push({
+              type: "file",
+              url: part.uri,
+              filename,
+              mime: part.mimeType,
             })
-            break
-          case "image": {
-            const parsed = parseUri(part.uri ?? "")
-            const filename = parsed.type === "file" ? parsed.filename : "image"
-            if (part.data) {
-              parts.push({
-                type: "file",
-                url: `data:${part.mimeType};base64,${part.data}`,
-                filename,
-                mime: part.mimeType,
-              })
-            } else if (part.uri && part.uri.startsWith("http:")) {
-              parts.push({
-                type: "file",
-                url: part.uri,
-                filename,
-                mime: part.mimeType,
-              })
-            }
-            break
           }
+          break
+        }
 
-          case "resource_link":
-            const parsed = parseUri(part.uri)
-            // Use the name from resource_link if available
-            if (part.name && parsed.type === "file") {
-              parsed.filename = part.name
-            }
-            parts.push(parsed)
+        case "resource_link":
+          const parsed = parseUri(part.uri)
+          // Use the name from resource_link if available
+          if (part.name && parsed.type === "file") {
+            parsed.filename = part.name
+          }
+          parts.push(parsed)
 
-            break
+          break
 
-          case "resource": {
-            const resource = part.resource
-            if ("text" in resource && resource.text) {
-              parts.push({
-                type: "text",
-                text: resource.text,
-              })
-            } else if ("blob" in resource && resource.blob && resource.mimeType) {
-              // Binary resource (PDFs, etc.): store as file part with data URL
-              const parsed = parseUri(resource.uri ?? "")
-              const filename = parsed.type === "file" ? parsed.filename : "file"
-              parts.push({
-                type: "file",
-                url: `data:${resource.mimeType};base64,${resource.blob}`,
-                filename,
-                mime: resource.mimeType,
-              })
-            }
-            break
+        case "resource": {
+          const resource = part.resource
+          if ("text" in resource && resource.text) {
+            parts.push({
+              type: "text",
+              text: resource.text,
+            })
+          } else if ("blob" in resource && resource.blob && resource.mimeType) {
+            // Binary resource (PDFs, etc.): store as file part with data URL
+            const parsed = parseUri(resource.uri ?? "")
+            const filename = parsed.type === "file" ? parsed.filename : "file"
+            parts.push({
+              type: "file",
+              url: `data:${resource.mimeType};base64,${resource.blob}`,
+              filename,
+              mime: resource.mimeType,
+            })
           }
-
-          default:
-            break
+          break
         }
-      }
-
-      log.info("parts", { parts })
-
-      const cmd = (() => {
-        const text = parts
-          .filter((p): p is { type: "text"; text: string } => p.type === "text")
-          .map((p) => p.text)
-          .join("")
-          .trim()
-
-        if (!text.startsWith("/")) return
-
-        const [name, ...rest] = text.slice(1).split(/\s+/)
-        return { name, args: rest.join(" ").trim() }
-      })()
-
-      const buildUsage = (msg: AssistantMessage): Usage => ({
-        totalTokens:
-          msg.tokens.input +
-          msg.tokens.output +
-          msg.tokens.reasoning +
-          (msg.tokens.cache?.read ?? 0) +
-          (msg.tokens.cache?.write ?? 0),
-        inputTokens: msg.tokens.input,
-        outputTokens: msg.tokens.output,
-        thoughtTokens: msg.tokens.reasoning || undefined,
-        cachedReadTokens: msg.tokens.cache?.read || undefined,
-        cachedWriteTokens: msg.tokens.cache?.write || undefined,
-      })
 
-      if (!cmd) {
-        const response = await this.sdk.session.prompt({
-          sessionID,
-          model: {
-            providerID: model.providerID,
-            modelID: model.modelID,
-          },
-          variant: this.sessionManager.getVariant(sessionID),
-          parts,
-          agent,
-          directory,
-        })
-        const msg = response.data?.info
-
-        await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
-
-        return {
-          stopReason: "end_turn" as const,
-          usage: msg ? buildUsage(msg) : undefined,
-          _meta: {},
-        }
+        default:
+          break
       }
+    }
 
-      const command = await this.config.sdk.command
-        .list({ directory }, { throwOnError: true })
-        .then((x) => x.data!.find((c) => c.name === cmd.name))
-      if (command) {
-        const response = await this.sdk.session.command({
-          sessionID,
-          command: command.name,
-          arguments: cmd.args,
-          model: model.providerID + "/" + model.modelID,
-          agent,
-          directory,
-        })
-        const msg = response.data?.info
+    log.info("parts", { parts })
+
+    const cmd = (() => {
+      const text = parts
+        .filter((p): p is { type: "text"; text: string } => p.type === "text")
+        .map((p) => p.text)
+        .join("")
+        .trim()
+
+      if (!text.startsWith("/")) return
+
+      const [name, ...rest] = text.slice(1).split(/\s+/)
+      return { name, args: rest.join(" ").trim() }
+    })()
+
+    const buildUsage = (msg: AssistantMessage): Usage => ({
+      totalTokens:
+        msg.tokens.input +
+        msg.tokens.output +
+        msg.tokens.reasoning +
+        (msg.tokens.cache?.read ?? 0) +
+        (msg.tokens.cache?.write ?? 0),
+      inputTokens: msg.tokens.input,
+      outputTokens: msg.tokens.output,
+      thoughtTokens: msg.tokens.reasoning || undefined,
+      cachedReadTokens: msg.tokens.cache?.read || undefined,
+      cachedWriteTokens: msg.tokens.cache?.write || undefined,
+    })
 
-        await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
+    if (!cmd) {
+      const response = await this.sdk.session.prompt({
+        sessionID,
+        model: {
+          providerID: model.providerID,
+          modelID: model.modelID,
+        },
+        variant: this.sessionManager.getVariant(sessionID),
+        parts,
+        agent,
+        directory,
+      })
+      const msg = response.data?.info
 
-        return {
-          stopReason: "end_turn" as const,
-          usage: msg ? buildUsage(msg) : undefined,
-          _meta: {},
-        }
-      }
+      await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
 
-      switch (cmd.name) {
-        case "compact":
-          await this.config.sdk.session.summarize(
-            {
-              sessionID,
-              directory,
-              providerID: model.providerID,
-              modelID: model.modelID,
-            },
-            { throwOnError: true },
-          )
-          break
+      return {
+        stopReason: "end_turn" as const,
+        usage: msg ? buildUsage(msg) : undefined,
+        _meta: {},
       }
+    }
+
+    const command = await this.config.sdk.command
+      .list({ directory }, { throwOnError: true })
+      .then((x) => x.data!.find((c) => c.name === cmd.name))
+    if (command) {
+      const response = await this.sdk.session.command({
+        sessionID,
+        command: command.name,
+        arguments: cmd.args,
+        model: model.providerID + "/" + model.modelID,
+        agent,
+        directory,
+      })
+      const msg = response.data?.info
 
       await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
 
       return {
         stopReason: "end_turn" as const,
+        usage: msg ? buildUsage(msg) : undefined,
         _meta: {},
       }
     }
 
-    async cancel(params: CancelNotification) {
-      const session = this.sessionManager.get(params.sessionId)
-      await this.config.sdk.session.abort(
-        {
-          sessionID: params.sessionId,
-          directory: session.cwd,
-        },
-        { throwOnError: true },
-      )
+    switch (cmd.name) {
+      case "compact":
+        await this.config.sdk.session.summarize(
+          {
+            sessionID,
+            directory,
+            providerID: model.providerID,
+            modelID: model.modelID,
+          },
+          { throwOnError: true },
+        )
+        break
     }
-  }
 
-  function toToolKind(toolName: string): ToolKind {
-    const tool = toolName.toLocaleLowerCase()
-    switch (tool) {
-      case "bash":
-        return "execute"
-      case "webfetch":
-        return "fetch"
-
-      case "edit":
-      case "patch":
-      case "write":
-        return "edit"
-
-      case "grep":
-      case "glob":
-      case "context7_resolve_library_id":
-      case "context7_get_library_docs":
-        return "search"
-
-      case "read":
-        return "read"
-
-      default:
-        return "other"
+    await sendUsageUpdate(this.connection, this.sdk, sessionID, directory)
+
+    return {
+      stopReason: "end_turn" as const,
+      _meta: {},
     }
   }
 
-  function toLocations(toolName: string, input: Record<string, any>): { path: string }[] {
-    const tool = toolName.toLocaleLowerCase()
-    switch (tool) {
-      case "read":
-      case "edit":
-      case "write":
-        return input["filePath"] ? [{ path: input["filePath"] }] : []
-      case "glob":
-      case "grep":
-        return input["path"] ? [{ path: input["path"] }] : []
-      case "bash":
-        return []
-      default:
-        return []
-    }
+  async cancel(params: CancelNotification) {
+    const session = this.sessionManager.get(params.sessionId)
+    await this.config.sdk.session.abort(
+      {
+        sessionID: params.sessionId,
+        directory: session.cwd,
+      },
+      { throwOnError: true },
+    )
   }
+}
 
-  async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ providerID: ProviderID; modelID: ModelID }> {
-    const sdk = config.sdk
-    const configured = config.defaultModel
-    if (configured) return configured
+function toToolKind(toolName: string): ToolKind {
+  const tool = toolName.toLocaleLowerCase()
+  switch (tool) {
+    case "bash":
+      return "execute"
+    case "webfetch":
+      return "fetch"
+
+    case "edit":
+    case "patch":
+    case "write":
+      return "edit"
+
+    case "grep":
+    case "glob":
+    case "context7_resolve_library_id":
+    case "context7_get_library_docs":
+      return "search"
+
+    case "read":
+      return "read"
+
+    default:
+      return "other"
+  }
+}
 
-    const directory = cwd ?? process.cwd()
+function toLocations(toolName: string, input: Record<string, any>): { path: string }[] {
+  const tool = toolName.toLocaleLowerCase()
+  switch (tool) {
+    case "read":
+    case "edit":
+    case "write":
+      return input["filePath"] ? [{ path: input["filePath"] }] : []
+    case "glob":
+    case "grep":
+      return input["path"] ? [{ path: input["path"] }] : []
+    case "bash":
+      return []
+    default:
+      return []
+  }
+}
 
-    const specified = await sdk.config
-      .get({ directory }, { throwOnError: true })
-      .then((resp) => {
-        const cfg = resp.data
-        if (!cfg || !cfg.model) return undefined
-        return Provider.parseModel(cfg.model)
-      })
-      .catch((error) => {
-        log.error("failed to load user config for default model", { error })
-        return undefined
-      })
+async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ providerID: ProviderID; modelID: ModelID }> {
+  const sdk = config.sdk
+  const configured = config.defaultModel
+  if (configured) return configured
 
-    const providers = await sdk.config
-      .providers({ directory }, { throwOnError: true })
-      .then((x) => x.data?.providers ?? [])
-      .catch((error) => {
-        log.error("failed to list providers for default model", { error })
-        return []
-      })
+  const directory = cwd ?? process.cwd()
 
-    if (specified && providers.length) {
-      const provider = providers.find((p) => p.id === specified.providerID)
-      if (provider && provider.models[specified.modelID]) return specified
-    }
+  const specified = await sdk.config
+    .get({ directory }, { throwOnError: true })
+    .then((resp) => {
+      const cfg = resp.data
+      if (!cfg || !cfg.model) return undefined
+      return Provider.parseModel(cfg.model)
+    })
+    .catch((error) => {
+      log.error("failed to load user config for default model", { error })
+      return undefined
+    })
 
-    if (specified && !providers.length) return specified
+  const providers = await sdk.config
+    .providers({ directory }, { throwOnError: true })
+    .then((x) => x.data?.providers ?? [])
+    .catch((error) => {
+      log.error("failed to list providers for default model", { error })
+      return []
+    })
 
-    const opencodeProvider = providers.find((p) => p.id === "opencode")
-    if (opencodeProvider) {
-      if (opencodeProvider.models["big-pickle"]) {
-        return { providerID: ProviderID.opencode, modelID: ModelID.make("big-pickle") }
-      }
-      const [best] = Provider.sort(Object.values(opencodeProvider.models))
-      if (best) {
-        return {
-          providerID: ProviderID.make(best.providerID),
-          modelID: ModelID.make(best.id),
-        }
-      }
-    }
+  if (specified && providers.length) {
+    const provider = providers.find((p) => p.id === specified.providerID)
+    if (provider && provider.models[specified.modelID]) return specified
+  }
+
+  if (specified && !providers.length) return specified
 
-    const models = providers.flatMap((p) => Object.values(p.models))
-    const [best] = Provider.sort(models)
+  const opencodeProvider = providers.find((p) => p.id === "opencode")
+  if (opencodeProvider) {
+    if (opencodeProvider.models["big-pickle"]) {
+      return { providerID: ProviderID.opencode, modelID: ModelID.make("big-pickle") }
+    }
+    const [best] = Provider.sort(Object.values(opencodeProvider.models))
     if (best) {
       return {
         providerID: ProviderID.make(best.providerID),
         modelID: ModelID.make(best.id),
       }
     }
+  }
 
-    if (specified) return specified
-
-    return { providerID: ProviderID.opencode, modelID: ModelID.make("big-pickle") }
+  const models = providers.flatMap((p) => Object.values(p.models))
+  const [best] = Provider.sort(models)
+  if (best) {
+    return {
+      providerID: ProviderID.make(best.providerID),
+      modelID: ModelID.make(best.id),
+    }
   }
 
-  function parseUri(
-    uri: string,
-  ): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } {
-    try {
-      if (uri.startsWith("file://")) {
-        const path = uri.slice(7)
+  if (specified) return specified
+
+  return { providerID: ProviderID.opencode, modelID: ModelID.make("big-pickle") }
+}
+
+function parseUri(
+  uri: string,
+): { type: "file"; url: string; filename: string; mime: string } | { type: "text"; text: string } {
+  try {
+    if (uri.startsWith("file://")) {
+      const path = uri.slice(7)
+      const name = path.split("/").pop() || path
+      return {
+        type: "file",
+        url: uri,
+        filename: name,
+        mime: "text/plain",
+      }
+    }
+    if (uri.startsWith("zed://")) {
+      const url = new URL(uri)
+      const path = url.searchParams.get("path")
+      if (path) {
         const name = path.split("/").pop() || path
         return {
           type: "file",
-          url: uri,
+          url: pathToFileURL(path).href,
           filename: name,
           mime: "text/plain",
         }
       }
-      if (uri.startsWith("zed://")) {
-        const url = new URL(uri)
-        const path = url.searchParams.get("path")
-        if (path) {
-          const name = path.split("/").pop() || path
-          return {
-            type: "file",
-            url: pathToFileURL(path).href,
-            filename: name,
-            mime: "text/plain",
-          }
-        }
-      }
-      return {
-        type: "text",
-        text: uri,
-      }
-    } catch {
-      return {
-        type: "text",
-        text: uri,
-      }
     }
-  }
-
-  function getNewContent(fileOriginal: string, unifiedDiff: string): string | undefined {
-    const result = applyPatch(fileOriginal, unifiedDiff)
-    if (result === false) {
-      log.error("Failed to apply unified diff (context mismatch)")
-      return undefined
+    return {
+      type: "text",
+      text: uri,
+    }
+  } catch {
+    return {
+      type: "text",
+      text: uri,
     }
-    return result
   }
+}
 
-  function sortProvidersByName<T extends { name: string }>(providers: T[]): T[] {
-    return [...providers].sort((a, b) => {
-      const nameA = a.name.toLowerCase()
-      const nameB = b.name.toLowerCase()
-      if (nameA < nameB) return -1
-      if (nameA > nameB) return 1
-      return 0
-    })
+function getNewContent(fileOriginal: string, unifiedDiff: string): string | undefined {
+  const result = applyPatch(fileOriginal, unifiedDiff)
+  if (result === false) {
+    log.error("Failed to apply unified diff (context mismatch)")
+    return undefined
   }
+  return result
+}
 
-  function modelVariantsFromProviders(
-    providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
-    model: { providerID: ProviderID; modelID: ModelID },
-  ): string[] {
-    const provider = providers.find((entry) => entry.id === model.providerID)
-    if (!provider) return []
-    const modelInfo = provider.models[model.modelID]
-    if (!modelInfo?.variants) return []
-    return Object.keys(modelInfo.variants)
-  }
+function sortProvidersByName<T extends { name: string }>(providers: T[]): T[] {
+  return [...providers].sort((a, b) => {
+    const nameA = a.name.toLowerCase()
+    const nameB = b.name.toLowerCase()
+    if (nameA < nameB) return -1
+    if (nameA > nameB) return 1
+    return 0
+  })
+}
 
-  function buildAvailableModels(
-    providers: Array<{ id: string; name: string; models: Record<string, any> }>,
-    options: { includeVariants?: boolean } = {},
-  ): ModelOption[] {
-    const includeVariants = options.includeVariants ?? false
-    return providers.flatMap((provider) => {
-      const unsorted: Array<{ id: string; name: string; variants?: Record<string, any> }> = Object.values(
-        provider.models,
-      )
-      const models = Provider.sort(unsorted)
-      return models.flatMap((model) => {
-        const base: ModelOption = {
-          modelId: `${provider.id}/${model.id}`,
-          name: `${provider.name}/${model.name}`,
-        }
-        if (!includeVariants || !model.variants) return [base]
-        const variants = Object.keys(model.variants).filter((variant) => variant !== DEFAULT_VARIANT_VALUE)
-        const variantOptions = variants.map((variant) => ({
-          modelId: `${provider.id}/${model.id}/${variant}`,
-          name: `${provider.name}/${model.name} (${variant})`,
-        }))
-        return [base, ...variantOptions]
-      })
+function modelVariantsFromProviders(
+  providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
+  model: { providerID: ProviderID; modelID: ModelID },
+): string[] {
+  const provider = providers.find((entry) => entry.id === model.providerID)
+  if (!provider) return []
+  const modelInfo = provider.models[model.modelID]
+  if (!modelInfo?.variants) return []
+  return Object.keys(modelInfo.variants)
+}
+
+function buildAvailableModels(
+  providers: Array<{ id: string; name: string; models: Record<string, any> }>,
+  options: { includeVariants?: boolean } = {},
+): ModelOption[] {
+  const includeVariants = options.includeVariants ?? false
+  return providers.flatMap((provider) => {
+    const unsorted: Array<{ id: string; name: string; variants?: Record<string, any> }> = Object.values(
+      provider.models,
+    )
+    const models = Provider.sort(unsorted)
+    return models.flatMap((model) => {
+      const base: ModelOption = {
+        modelId: `${provider.id}/${model.id}`,
+        name: `${provider.name}/${model.name}`,
+      }
+      if (!includeVariants || !model.variants) return [base]
+      const variants = Object.keys(model.variants).filter((variant) => variant !== DEFAULT_VARIANT_VALUE)
+      const variantOptions = variants.map((variant) => ({
+        modelId: `${provider.id}/${model.id}/${variant}`,
+        name: `${provider.name}/${model.name} (${variant})`,
+      }))
+      return [base, ...variantOptions]
     })
-  }
+  })
+}
 
-  function formatModelIdWithVariant(
-    model: { providerID: ProviderID; modelID: ModelID },
-    variant: string | undefined,
-    availableVariants: string[],
-    includeVariant: boolean,
-  ) {
-    const base = `${model.providerID}/${model.modelID}`
-    if (!includeVariant || !variant || !availableVariants.includes(variant)) return base
-    return `${base}/${variant}`
-  }
+function formatModelIdWithVariant(
+  model: { providerID: ProviderID; modelID: ModelID },
+  variant: string | undefined,
+  availableVariants: string[],
+  includeVariant: boolean,
+) {
+  const base = `${model.providerID}/${model.modelID}`
+  if (!includeVariant || !variant || !availableVariants.includes(variant)) return base
+  return `${base}/${variant}`
+}
 
-  function buildVariantMeta(input: {
-    model: { providerID: ProviderID; modelID: ModelID }
-    variant?: string
-    availableVariants: string[]
-  }) {
-    return {
-      opencode: {
-        modelId: `${input.model.providerID}/${input.model.modelID}`,
-        variant: input.variant ?? null,
-        availableVariants: input.availableVariants,
-      },
-    }
+function buildVariantMeta(input: {
+  model: { providerID: ProviderID; modelID: ModelID }
+  variant?: string
+  availableVariants: string[]
+}) {
+  return {
+    opencode: {
+      modelId: `${input.model.providerID}/${input.model.modelID}`,
+      variant: input.variant ?? null,
+      availableVariants: input.availableVariants,
+    },
   }
+}
 
-  function parseModelSelection(
-    modelId: string,
-    providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
-  ): { model: { providerID: ProviderID; modelID: ModelID }; variant?: string } {
-    const parsed = Provider.parseModel(modelId)
-    const provider = providers.find((p) => p.id === parsed.providerID)
-    if (!provider) {
-      return { model: parsed, variant: undefined }
-    }
+function parseModelSelection(
+  modelId: string,
+  providers: Array<{ id: string; models: Record<string, { variants?: Record<string, any> }> }>,
+): { model: { providerID: ProviderID; modelID: ModelID }; variant?: string } {
+  const parsed = Provider.parseModel(modelId)
+  const provider = providers.find((p) => p.id === parsed.providerID)
+  if (!provider) {
+    return { model: parsed, variant: undefined }
+  }
 
-    // Check if modelID exists directly
-    if (provider.models[parsed.modelID]) {
-      return { model: parsed, variant: undefined }
-    }
+  // Check if modelID exists directly
+  if (provider.models[parsed.modelID]) {
+    return { model: parsed, variant: undefined }
+  }
 
-    // Try to extract variant from end of modelID (e.g., "claude-sonnet-4/high" -> model: "claude-sonnet-4", variant: "high")
-    const segments = parsed.modelID.split("/")
-    if (segments.length > 1) {
-      const candidateVariant = segments[segments.length - 1]
-      const baseModelId = segments.slice(0, -1).join("/")
-      const baseModelInfo = provider.models[baseModelId]
-      if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) {
-        return {
-          model: { providerID: parsed.providerID, modelID: ModelID.make(baseModelId) },
-          variant: candidateVariant,
-        }
+  // Try to extract variant from end of modelID (e.g., "claude-sonnet-4/high" -> model: "claude-sonnet-4", variant: "high")
+  const segments = parsed.modelID.split("/")
+  if (segments.length > 1) {
+    const candidateVariant = segments[segments.length - 1]
+    const baseModelId = segments.slice(0, -1).join("/")
+    const baseModelInfo = provider.models[baseModelId]
+    if (baseModelInfo?.variants && candidateVariant in baseModelInfo.variants) {
+      return {
+        model: { providerID: parsed.providerID, modelID: ModelID.make(baseModelId) },
+        variant: candidateVariant,
       }
     }
-
-    return { model: parsed, variant: undefined }
   }
 
-  function buildConfigOptions(input: {
-    currentModelId: string
-    availableModels: ModelOption[]
-    modes?: { availableModes: ModeOption[]; currentModeId: string } | undefined
-  }): SessionConfigOption[] {
-    const options: SessionConfigOption[] = [
-      {
-        id: "model",
-        name: "Model",
-        category: "model",
-        type: "select",
-        currentValue: input.currentModelId,
-        options: input.availableModels.map((m) => ({ value: m.modelId, name: m.name })),
-      },
-    ]
-    if (input.modes) {
-      options.push({
-        id: "mode",
-        name: "Session Mode",
-        category: "mode",
-        type: "select",
-        currentValue: input.modes.currentModeId,
-        options: input.modes.availableModes.map((m) => ({
-          value: m.id,
-          name: m.name,
-          ...(m.description ? { description: m.description } : {}),
-        })),
-      })
-    }
-    return options
+  return { model: parsed, variant: undefined }
+}
+
+function buildConfigOptions(input: {
+  currentModelId: string
+  availableModels: ModelOption[]
+  modes?: { availableModes: ModeOption[]; currentModeId: string } | undefined
+}): SessionConfigOption[] {
+  const options: SessionConfigOption[] = [
+    {
+      id: "model",
+      name: "Model",
+      category: "model",
+      type: "select",
+      currentValue: input.currentModelId,
+      options: input.availableModels.map((m) => ({ value: m.modelId, name: m.name })),
+    },
+  ]
+  if (input.modes) {
+    options.push({
+      id: "mode",
+      name: "Session Mode",
+      category: "mode",
+      type: "select",
+      currentValue: input.modes.currentModeId,
+      options: input.modes.availableModes.map((m) => ({
+        value: m.id,
+        name: m.name,
+        ...(m.description ? { description: m.description } : {}),
+      })),
+    })
   }
+  return options
 }

+ 1 - 0
packages/opencode/src/acp/index.ts

@@ -0,0 +1 @@
+export * as ACP from "./agent"

+ 357 - 359
packages/opencode/src/agent/agent.ts

@@ -24,389 +24,387 @@ import { InstanceState } from "@/effect"
 import * as Option from "effect/Option"
 import * as OtelTracer from "@effect/opentelemetry/Tracer"
 
-export namespace Agent {
-  export const Info = z
-    .object({
-      name: z.string(),
-      description: z.string().optional(),
-      mode: z.enum(["subagent", "primary", "all"]),
-      native: z.boolean().optional(),
-      hidden: z.boolean().optional(),
-      topP: z.number().optional(),
-      temperature: z.number().optional(),
-      color: z.string().optional(),
-      permission: Permission.Ruleset.zod,
-      model: z
-        .object({
-          modelID: ModelID.zod,
-          providerID: ProviderID.zod,
-        })
-        .optional(),
-      variant: z.string().optional(),
-      prompt: z.string().optional(),
-      options: z.record(z.string(), z.any()),
-      steps: z.number().int().positive().optional(),
-    })
-    .meta({
-      ref: "Agent",
-    })
-  export type Info = z.infer<typeof Info>
+export const Info = z
+  .object({
+    name: z.string(),
+    description: z.string().optional(),
+    mode: z.enum(["subagent", "primary", "all"]),
+    native: z.boolean().optional(),
+    hidden: z.boolean().optional(),
+    topP: z.number().optional(),
+    temperature: z.number().optional(),
+    color: z.string().optional(),
+    permission: Permission.Ruleset.zod,
+    model: z
+      .object({
+        modelID: ModelID.zod,
+        providerID: ProviderID.zod,
+      })
+      .optional(),
+    variant: z.string().optional(),
+    prompt: z.string().optional(),
+    options: z.record(z.string(), z.any()),
+    steps: z.number().int().positive().optional(),
+  })
+  .meta({
+    ref: "Agent",
+  })
+export type Info = z.infer<typeof Info>
 
-  export interface Interface {
-    readonly get: (agent: string) => Effect.Effect<Agent.Info>
-    readonly list: () => Effect.Effect<Agent.Info[]>
-    readonly defaultAgent: () => Effect.Effect<string>
-    readonly generate: (input: {
-      description: string
-      model?: { providerID: ProviderID; modelID: ModelID }
-    }) => Effect.Effect<{
-      identifier: string
-      whenToUse: string
-      systemPrompt: string
-    }>
-  }
+export interface Interface {
+  readonly get: (agent: string) => Effect.Effect<Info>
+  readonly list: () => Effect.Effect<Info[]>
+  readonly defaultAgent: () => Effect.Effect<string>
+  readonly generate: (input: {
+    description: string
+    model?: { providerID: ProviderID; modelID: ModelID }
+  }) => Effect.Effect<{
+    identifier: string
+    whenToUse: string
+    systemPrompt: string
+  }>
+}
 
-  type State = Omit<Interface, "generate">
+type State = Omit<Interface, "generate">
 
-  export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
+export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
 
-  export const layer = Layer.effect(
-    Service,
-    Effect.gen(function* () {
-      const config = yield* Config.Service
-      const auth = yield* Auth.Service
-      const plugin = yield* Plugin.Service
-      const skill = yield* Skill.Service
-      const provider = yield* Provider.Service
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const config = yield* Config.Service
+    const auth = yield* Auth.Service
+    const plugin = yield* Plugin.Service
+    const skill = yield* Skill.Service
+    const provider = yield* Provider.Service
 
-      const state = yield* InstanceState.make<State>(
-        Effect.fn("Agent.state")(function* (_ctx) {
-          const cfg = yield* config.get()
-          const skillDirs = yield* skill.dirs()
-          const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))]
+    const state = yield* InstanceState.make<State>(
+      Effect.fn("Agent.state")(function* (_ctx) {
+        const cfg = yield* config.get()
+        const skillDirs = yield* skill.dirs()
+        const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))]
 
-          const defaults = Permission.fromConfig({
+        const defaults = Permission.fromConfig({
+          "*": "allow",
+          doom_loop: "ask",
+          external_directory: {
+            "*": "ask",
+            ...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
+          },
+          question: "deny",
+          plan_enter: "deny",
+          plan_exit: "deny",
+          // mirrors github.com/github/gitignore Node.gitignore pattern for .env files
+          read: {
             "*": "allow",
-            doom_loop: "ask",
-            external_directory: {
-              "*": "ask",
-              ...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
-            },
-            question: "deny",
-            plan_enter: "deny",
-            plan_exit: "deny",
-            // mirrors github.com/github/gitignore Node.gitignore pattern for .env files
-            read: {
-              "*": "allow",
-              "*.env": "ask",
-              "*.env.*": "ask",
-              "*.env.example": "allow",
-            },
-          })
+            "*.env": "ask",
+            "*.env.*": "ask",
+            "*.env.example": "allow",
+          },
+        })
 
-          const user = Permission.fromConfig(cfg.permission ?? {})
+        const user = Permission.fromConfig(cfg.permission ?? {})
 
-          const agents: Record<string, Info> = {
-            build: {
-              name: "build",
-              description: "The default agent. Executes tools based on configured permissions.",
-              options: {},
-              permission: Permission.merge(
-                defaults,
-                Permission.fromConfig({
-                  question: "allow",
-                  plan_enter: "allow",
-                }),
-                user,
-              ),
-              mode: "primary",
-              native: true,
-            },
-            plan: {
-              name: "plan",
-              description: "Plan mode. Disallows all edit tools.",
-              options: {},
-              permission: Permission.merge(
-                defaults,
-                Permission.fromConfig({
-                  question: "allow",
-                  plan_exit: "allow",
-                  external_directory: {
-                    [path.join(Global.Path.data, "plans", "*")]: "allow",
-                  },
-                  edit: {
-                    "*": "deny",
-                    [path.join(".opencode", "plans", "*.md")]: "allow",
-                    [path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]:
-                      "allow",
-                  },
-                }),
-                user,
-              ),
-              mode: "primary",
-              native: true,
-            },
-            general: {
-              name: "general",
-              description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
-              permission: Permission.merge(
-                defaults,
-                Permission.fromConfig({
-                  todowrite: "deny",
-                }),
-                user,
-              ),
-              options: {},
-              mode: "subagent",
-              native: true,
-            },
-            explore: {
-              name: "explore",
-              permission: Permission.merge(
-                defaults,
-                Permission.fromConfig({
-                  "*": "deny",
-                  grep: "allow",
-                  glob: "allow",
-                  list: "allow",
-                  bash: "allow",
-                  webfetch: "allow",
-                  websearch: "allow",
-                  codesearch: "allow",
-                  read: "allow",
-                  external_directory: {
-                    "*": "ask",
-                    ...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
-                  },
-                }),
-                user,
-              ),
-              description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`,
-              prompt: PROMPT_EXPLORE,
-              options: {},
-              mode: "subagent",
-              native: true,
-            },
-            compaction: {
-              name: "compaction",
-              mode: "primary",
-              native: true,
-              hidden: true,
-              prompt: PROMPT_COMPACTION,
-              permission: Permission.merge(
-                defaults,
-                Permission.fromConfig({
-                  "*": "deny",
-                }),
-                user,
-              ),
-              options: {},
-            },
-            title: {
-              name: "title",
-              mode: "primary",
-              options: {},
-              native: true,
-              hidden: true,
-              temperature: 0.5,
-              permission: Permission.merge(
-                defaults,
-                Permission.fromConfig({
-                  "*": "deny",
-                }),
-                user,
-              ),
-              prompt: PROMPT_TITLE,
-            },
-            summary: {
-              name: "summary",
-              mode: "primary",
-              options: {},
-              native: true,
-              hidden: true,
-              permission: Permission.merge(
-                defaults,
-                Permission.fromConfig({
+        const agents: Record<string, Info> = {
+          build: {
+            name: "build",
+            description: "The default agent. Executes tools based on configured permissions.",
+            options: {},
+            permission: Permission.merge(
+              defaults,
+              Permission.fromConfig({
+                question: "allow",
+                plan_enter: "allow",
+              }),
+              user,
+            ),
+            mode: "primary",
+            native: true,
+          },
+          plan: {
+            name: "plan",
+            description: "Plan mode. Disallows all edit tools.",
+            options: {},
+            permission: Permission.merge(
+              defaults,
+              Permission.fromConfig({
+                question: "allow",
+                plan_exit: "allow",
+                external_directory: {
+                  [path.join(Global.Path.data, "plans", "*")]: "allow",
+                },
+                edit: {
                   "*": "deny",
-                }),
-                user,
-              ),
-              prompt: PROMPT_SUMMARY,
-            },
-          }
+                  [path.join(".opencode", "plans", "*.md")]: "allow",
+                  [path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]:
+                    "allow",
+                },
+              }),
+              user,
+            ),
+            mode: "primary",
+            native: true,
+          },
+          general: {
+            name: "general",
+            description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
+            permission: Permission.merge(
+              defaults,
+              Permission.fromConfig({
+                todowrite: "deny",
+              }),
+              user,
+            ),
+            options: {},
+            mode: "subagent",
+            native: true,
+          },
+          explore: {
+            name: "explore",
+            permission: Permission.merge(
+              defaults,
+              Permission.fromConfig({
+                "*": "deny",
+                grep: "allow",
+                glob: "allow",
+                list: "allow",
+                bash: "allow",
+                webfetch: "allow",
+                websearch: "allow",
+                codesearch: "allow",
+                read: "allow",
+                external_directory: {
+                  "*": "ask",
+                  ...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
+                },
+              }),
+              user,
+            ),
+            description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`,
+            prompt: PROMPT_EXPLORE,
+            options: {},
+            mode: "subagent",
+            native: true,
+          },
+          compaction: {
+            name: "compaction",
+            mode: "primary",
+            native: true,
+            hidden: true,
+            prompt: PROMPT_COMPACTION,
+            permission: Permission.merge(
+              defaults,
+              Permission.fromConfig({
+                "*": "deny",
+              }),
+              user,
+            ),
+            options: {},
+          },
+          title: {
+            name: "title",
+            mode: "primary",
+            options: {},
+            native: true,
+            hidden: true,
+            temperature: 0.5,
+            permission: Permission.merge(
+              defaults,
+              Permission.fromConfig({
+                "*": "deny",
+              }),
+              user,
+            ),
+            prompt: PROMPT_TITLE,
+          },
+          summary: {
+            name: "summary",
+            mode: "primary",
+            options: {},
+            native: true,
+            hidden: true,
+            permission: Permission.merge(
+              defaults,
+              Permission.fromConfig({
+                "*": "deny",
+              }),
+              user,
+            ),
+            prompt: PROMPT_SUMMARY,
+          },
+        }
 
-          for (const [key, value] of Object.entries(cfg.agent ?? {})) {
-            if (value.disable) {
-              delete agents[key]
-              continue
-            }
-            let item = agents[key]
-            if (!item)
-              item = agents[key] = {
-                name: key,
-                mode: "all",
-                permission: Permission.merge(defaults, user),
-                options: {},
-                native: false,
-              }
-            if (value.model) item.model = Provider.parseModel(value.model)
-            item.variant = value.variant ?? item.variant
-            item.prompt = value.prompt ?? item.prompt
-            item.description = value.description ?? item.description
-            item.temperature = value.temperature ?? item.temperature
-            item.topP = value.top_p ?? item.topP
-            item.mode = value.mode ?? item.mode
-            item.color = value.color ?? item.color
-            item.hidden = value.hidden ?? item.hidden
-            item.name = value.name ?? item.name
-            item.steps = value.steps ?? item.steps
-            item.options = mergeDeep(item.options, value.options ?? {})
-            item.permission = Permission.merge(item.permission, Permission.fromConfig(value.permission ?? {}))
+        for (const [key, value] of Object.entries(cfg.agent ?? {})) {
+          if (value.disable) {
+            delete agents[key]
+            continue
           }
+          let item = agents[key]
+          if (!item)
+            item = agents[key] = {
+              name: key,
+              mode: "all",
+              permission: Permission.merge(defaults, user),
+              options: {},
+              native: false,
+            }
+          if (value.model) item.model = Provider.parseModel(value.model)
+          item.variant = value.variant ?? item.variant
+          item.prompt = value.prompt ?? item.prompt
+          item.description = value.description ?? item.description
+          item.temperature = value.temperature ?? item.temperature
+          item.topP = value.top_p ?? item.topP
+          item.mode = value.mode ?? item.mode
+          item.color = value.color ?? item.color
+          item.hidden = value.hidden ?? item.hidden
+          item.name = value.name ?? item.name
+          item.steps = value.steps ?? item.steps
+          item.options = mergeDeep(item.options, value.options ?? {})
+          item.permission = Permission.merge(item.permission, Permission.fromConfig(value.permission ?? {}))
+        }
 
-          // Ensure Truncate.GLOB is allowed unless explicitly configured
-          for (const name in agents) {
-            const agent = agents[name]
-            const explicit = agent.permission.some((r) => {
-              if (r.permission !== "external_directory") return false
-              if (r.action !== "deny") return false
-              return r.pattern === Truncate.GLOB
-            })
-            if (explicit) continue
+        // Ensure Truncate.GLOB is allowed unless explicitly configured
+        for (const name in agents) {
+          const agent = agents[name]
+          const explicit = agent.permission.some((r) => {
+            if (r.permission !== "external_directory") return false
+            if (r.action !== "deny") return false
+            return r.pattern === Truncate.GLOB
+          })
+          if (explicit) continue
 
-            agents[name].permission = Permission.merge(
-              agents[name].permission,
-              Permission.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }),
-            )
-          }
+          agents[name].permission = Permission.merge(
+            agents[name].permission,
+            Permission.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }),
+          )
+        }
 
-          const get = Effect.fnUntraced(function* (agent: string) {
-            return agents[agent]
-          })
+        const get = Effect.fnUntraced(function* (agent: string) {
+          return agents[agent]
+        })
 
-          const list = Effect.fnUntraced(function* () {
-            const cfg = yield* config.get()
-            return pipe(
-              agents,
-              values(),
-              sortBy(
-                [(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"],
-                [(x) => x.name, "asc"],
-              ),
-            )
-          })
+        const list = Effect.fnUntraced(function* () {
+          const cfg = yield* config.get()
+          return pipe(
+            agents,
+            values(),
+            sortBy(
+              [(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"],
+              [(x) => x.name, "asc"],
+            ),
+          )
+        })
 
-          const defaultAgent = Effect.fnUntraced(function* () {
-            const c = yield* config.get()
-            if (c.default_agent) {
-              const agent = agents[c.default_agent]
-              if (!agent) throw new Error(`default agent "${c.default_agent}" not found`)
-              if (agent.mode === "subagent") throw new Error(`default agent "${c.default_agent}" is a subagent`)
-              if (agent.hidden === true) throw new Error(`default agent "${c.default_agent}" is hidden`)
-              return agent.name
-            }
-            const visible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true)
-            if (!visible) throw new Error("no primary visible agent found")
-            return visible.name
-          })
+        const defaultAgent = Effect.fnUntraced(function* () {
+          const c = yield* config.get()
+          if (c.default_agent) {
+            const agent = agents[c.default_agent]
+            if (!agent) throw new Error(`default agent "${c.default_agent}" not found`)
+            if (agent.mode === "subagent") throw new Error(`default agent "${c.default_agent}" is a subagent`)
+            if (agent.hidden === true) throw new Error(`default agent "${c.default_agent}" is hidden`)
+            return agent.name
+          }
+          const visible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true)
+          if (!visible) throw new Error("no primary visible agent found")
+          return visible.name
+        })
 
-          return {
-            get,
-            list,
-            defaultAgent,
-          } satisfies State
-        }),
-      )
+        return {
+          get,
+          list,
+          defaultAgent,
+        } satisfies State
+      }),
+    )
 
-      return Service.of({
-        get: Effect.fn("Agent.get")(function* (agent: string) {
-          return yield* InstanceState.useEffect(state, (s) => s.get(agent))
-        }),
-        list: Effect.fn("Agent.list")(function* () {
-          return yield* InstanceState.useEffect(state, (s) => s.list())
-        }),
-        defaultAgent: Effect.fn("Agent.defaultAgent")(function* () {
-          return yield* InstanceState.useEffect(state, (s) => s.defaultAgent())
-        }),
-        generate: Effect.fn("Agent.generate")(function* (input: {
-          description: string
-          model?: { providerID: ProviderID; modelID: ModelID }
-        }) {
-          const cfg = yield* config.get()
-          const model = input.model ?? (yield* provider.defaultModel())
-          const resolved = yield* provider.getModel(model.providerID, model.modelID)
-          const language = yield* provider.getLanguage(resolved)
-          const tracer = cfg.experimental?.openTelemetry
-            ? Option.getOrUndefined(yield* Effect.serviceOption(OtelTracer.OtelTracer))
-            : undefined
+    return Service.of({
+      get: Effect.fn("Agent.get")(function* (agent: string) {
+        return yield* InstanceState.useEffect(state, (s) => s.get(agent))
+      }),
+      list: Effect.fn("Agent.list")(function* () {
+        return yield* InstanceState.useEffect(state, (s) => s.list())
+      }),
+      defaultAgent: Effect.fn("Agent.defaultAgent")(function* () {
+        return yield* InstanceState.useEffect(state, (s) => s.defaultAgent())
+      }),
+      generate: Effect.fn("Agent.generate")(function* (input: {
+        description: string
+        model?: { providerID: ProviderID; modelID: ModelID }
+      }) {
+        const cfg = yield* config.get()
+        const model = input.model ?? (yield* provider.defaultModel())
+        const resolved = yield* provider.getModel(model.providerID, model.modelID)
+        const language = yield* provider.getLanguage(resolved)
+        const tracer = cfg.experimental?.openTelemetry
+          ? Option.getOrUndefined(yield* Effect.serviceOption(OtelTracer.OtelTracer))
+          : undefined
 
-          const system = [PROMPT_GENERATE]
-          yield* plugin.trigger("experimental.chat.system.transform", { model: resolved }, { system })
-          const existing = yield* InstanceState.useEffect(state, (s) => s.list())
+        const system = [PROMPT_GENERATE]
+        yield* plugin.trigger("experimental.chat.system.transform", { model: resolved }, { system })
+        const existing = yield* InstanceState.useEffect(state, (s) => s.list())
 
-          // TODO: clean this up so provider specific logic doesnt bleed over
-          const authInfo = yield* auth.get(model.providerID).pipe(Effect.orDie)
-          const isOpenaiOauth = model.providerID === "openai" && authInfo?.type === "oauth"
+        // TODO: clean this up so provider specific logic doesnt bleed over
+        const authInfo = yield* auth.get(model.providerID).pipe(Effect.orDie)
+        const isOpenaiOauth = model.providerID === "openai" && authInfo?.type === "oauth"
 
-          const params = {
-            experimental_telemetry: {
-              isEnabled: cfg.experimental?.openTelemetry,
-              tracer,
-              metadata: {
-                userId: cfg.username ?? "unknown",
-              },
+        const params = {
+          experimental_telemetry: {
+            isEnabled: cfg.experimental?.openTelemetry,
+            tracer,
+            metadata: {
+              userId: cfg.username ?? "unknown",
+            },
+          },
+          temperature: 0.3,
+          messages: [
+            ...(isOpenaiOauth
+              ? []
+              : system.map(
+                  (item): ModelMessage => ({
+                    role: "system",
+                    content: item,
+                  }),
+                )),
+            {
+              role: "user",
+              content: `Create an agent configuration based on this request: "${input.description}".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n  Return ONLY the JSON object, no other text, do not wrap in backticks`,
             },
-            temperature: 0.3,
-            messages: [
-              ...(isOpenaiOauth
-                ? []
-                : system.map(
-                    (item): ModelMessage => ({
-                      role: "system",
-                      content: item,
-                    }),
-                  )),
-              {
-                role: "user",
-                content: `Create an agent configuration based on this request: "${input.description}".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n  Return ONLY the JSON object, no other text, do not wrap in backticks`,
-              },
-            ],
-            model: language,
-            schema: z.object({
-              identifier: z.string(),
-              whenToUse: z.string(),
-              systemPrompt: z.string(),
-            }),
-          } satisfies Parameters<typeof generateObject>[0]
+          ],
+          model: language,
+          schema: z.object({
+            identifier: z.string(),
+            whenToUse: z.string(),
+            systemPrompt: z.string(),
+          }),
+        } satisfies Parameters<typeof generateObject>[0]
 
-          if (isOpenaiOauth) {
-            return yield* Effect.promise(async () => {
-              const result = streamObject({
-                ...params,
-                providerOptions: ProviderTransform.providerOptions(resolved, {
-                  instructions: system.join("\n"),
-                  store: false,
-                }),
-                onError: () => {},
-              })
-              for await (const part of result.fullStream) {
-                if (part.type === "error") throw part.error
-              }
-              return result.object
+        if (isOpenaiOauth) {
+          return yield* Effect.promise(async () => {
+            const result = streamObject({
+              ...params,
+              providerOptions: ProviderTransform.providerOptions(resolved, {
+                instructions: system.join("\n"),
+                store: false,
+              }),
+              onError: () => {},
             })
-          }
+            for await (const part of result.fullStream) {
+              if (part.type === "error") throw part.error
+            }
+            return result.object
+          })
+        }
 
-          return yield* Effect.promise(() => generateObject(params).then((r) => r.object))
-        }),
-      })
-    }),
-  )
+        return yield* Effect.promise(() => generateObject(params).then((r) => r.object))
+      }),
+    })
+  }),
+)
 
-  export const defaultLayer = layer.pipe(
-    Layer.provide(Plugin.defaultLayer),
-    Layer.provide(Provider.defaultLayer),
-    Layer.provide(Auth.defaultLayer),
-    Layer.provide(Config.defaultLayer),
-    Layer.provide(Skill.defaultLayer),
-  )
-}
+export const defaultLayer = layer.pipe(
+  Layer.provide(Plugin.defaultLayer),
+  Layer.provide(Provider.defaultLayer),
+  Layer.provide(Auth.defaultLayer),
+  Layer.provide(Config.defaultLayer),
+  Layer.provide(Skill.defaultLayer),
+)

+ 1 - 0
packages/opencode/src/agent/index.ts

@@ -0,0 +1 @@
+export * as Agent from "./agent"

+ 1 - 1
packages/opencode/src/cli/cmd/acp.ts

@@ -2,7 +2,7 @@ import { Log } from "@/util"
 import { bootstrap } from "../bootstrap"
 import { cmd } from "./cmd"
 import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
-import { ACP } from "@/acp/agent"
+import { ACP } from "@/acp"
 import { Server } from "@/server/server"
 import { createOpencodeClient } from "@opencode-ai/sdk/v2"
 import { withNetworkOptions, resolveNetworkOptions } from "../network"

+ 1 - 1
packages/opencode/src/cli/cmd/agent.ts

@@ -3,7 +3,7 @@ import * as prompts from "@clack/prompts"
 import { AppRuntime } from "@/effect/app-runtime"
 import { UI } from "../ui"
 import { Global } from "../../global"
-import { Agent } from "../../agent/agent"
+import { Agent } from "../../agent"
 import { Provider } from "../../provider"
 import path from "path"
 import fs from "fs/promises"

+ 1 - 1
packages/opencode/src/cli/cmd/debug/agent.ts

@@ -1,7 +1,7 @@
 import { EOL } from "os"
 import { basename } from "path"
 import { Effect } from "effect"
-import { Agent } from "../../../agent/agent"
+import { Agent } from "../../../agent"
 import { Provider } from "../../../provider"
 import { Session } from "../../../session"
 import type { MessageV2 } from "../../../session/message-v2"

+ 1 - 1
packages/opencode/src/cli/cmd/run.ts

@@ -10,7 +10,7 @@ import { Filesystem } from "../../util"
 import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
 import { Server } from "../../server/server"
 import { Provider } from "../../provider"
-import { Agent } from "../../agent/agent"
+import { Agent } from "../../agent"
 import { Permission } from "../../permission"
 import { Tool } from "../../tool"
 import { GlobTool } from "../../tool/glob"

+ 1 - 0
packages/opencode/src/control-plane/index.ts

@@ -0,0 +1 @@
+export * as Workspace from "./workspace"

+ 1 - 1
packages/opencode/src/control-plane/schema.ts

@@ -1,7 +1,7 @@
 import { Schema } from "effect"
 import z from "zod"
 
-import { Identifier } from "@/id/id"
+import { Identifier } from "@/id"
 import { ZodOverride } from "@/util/effect-zod"
 import { withStatics } from "@/util/schema"
 

+ 404 - 406
packages/opencode/src/control-plane/workspace.ts

@@ -25,501 +25,499 @@ import { AppRuntime } from "@/effect/app-runtime"
 import { EventSequenceTable } from "@/sync/event.sql"
 import { waitEvent } from "./util"
 
-export namespace Workspace {
-  export const Info = WorkspaceInfo.meta({
-    ref: "Workspace",
-  })
-  export type Info = z.infer<typeof Info>
-
-  export const ConnectionStatus = z.object({
-    workspaceID: WorkspaceID.zod,
-    status: z.enum(["connected", "connecting", "disconnected", "error"]),
-    error: z.string().optional(),
-  })
-  export type ConnectionStatus = z.infer<typeof ConnectionStatus>
-
-  const Restore = z.object({
-    workspaceID: WorkspaceID.zod,
-    sessionID: SessionID.zod,
-    total: z.number().int().min(0),
-    step: z.number().int().min(0),
-  })
+export const Info = WorkspaceInfo.meta({
+  ref: "Workspace",
+})
+export type Info = z.infer<typeof Info>
+
+export const ConnectionStatus = z.object({
+  workspaceID: WorkspaceID.zod,
+  status: z.enum(["connected", "connecting", "disconnected", "error"]),
+  error: z.string().optional(),
+})
+export type ConnectionStatus = z.infer<typeof ConnectionStatus>
+
+const Restore = z.object({
+  workspaceID: WorkspaceID.zod,
+  sessionID: SessionID.zod,
+  total: z.number().int().min(0),
+  step: z.number().int().min(0),
+})
+
+export const Event = {
+  Ready: BusEvent.define(
+    "workspace.ready",
+    z.object({
+      name: z.string(),
+    }),
+  ),
+  Failed: BusEvent.define(
+    "workspace.failed",
+    z.object({
+      message: z.string(),
+    }),
+  ),
+  Restore: BusEvent.define("workspace.restore", Restore),
+  Status: BusEvent.define("workspace.status", ConnectionStatus),
+}
 
-  export const Event = {
-    Ready: BusEvent.define(
-      "workspace.ready",
-      z.object({
-        name: z.string(),
-      }),
-    ),
-    Failed: BusEvent.define(
-      "workspace.failed",
-      z.object({
-        message: z.string(),
-      }),
-    ),
-    Restore: BusEvent.define("workspace.restore", Restore),
-    Status: BusEvent.define("workspace.status", ConnectionStatus),
+function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
+  return {
+    id: row.id,
+    type: row.type,
+    branch: row.branch,
+    name: row.name,
+    directory: row.directory,
+    extra: row.extra,
+    projectID: row.project_id,
   }
+}
 
-  function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
-    return {
-      id: row.id,
-      type: row.type,
-      branch: row.branch,
-      name: row.name,
-      directory: row.directory,
-      extra: row.extra,
-      projectID: row.project_id,
-    }
+const CreateInput = z.object({
+  id: WorkspaceID.zod.optional(),
+  type: Info.shape.type,
+  branch: Info.shape.branch,
+  projectID: ProjectID.zod,
+  extra: Info.shape.extra,
+})
+
+export const create = fn(CreateInput, async (input) => {
+  const id = WorkspaceID.ascending(input.id)
+  const adaptor = await getAdaptor(input.projectID, input.type)
+
+  const config = await adaptor.configure({ ...input, id, name: Slug.create(), directory: null })
+
+  const info: Info = {
+    id,
+    type: config.type,
+    branch: config.branch ?? null,
+    name: config.name ?? null,
+    directory: config.directory ?? null,
+    extra: config.extra ?? null,
+    projectID: input.projectID,
   }
 
-  const CreateInput = z.object({
-    id: WorkspaceID.zod.optional(),
-    type: Info.shape.type,
-    branch: Info.shape.branch,
-    projectID: ProjectID.zod,
-    extra: Info.shape.extra,
+  Database.use((db) => {
+    db.insert(WorkspaceTable)
+      .values({
+        id: info.id,
+        type: info.type,
+        branch: info.branch,
+        name: info.name,
+        directory: info.directory,
+        extra: info.extra,
+        project_id: info.projectID,
+      })
+      .run()
   })
 
-  export const create = fn(CreateInput, async (input) => {
-    const id = WorkspaceID.ascending(input.id)
-    const adaptor = await getAdaptor(input.projectID, input.type)
+  await adaptor.create(config)
 
-    const config = await adaptor.configure({ ...input, id, name: Slug.create(), directory: null })
+  startSync(info)
 
-    const info: Info = {
-      id,
-      type: config.type,
-      branch: config.branch ?? null,
-      name: config.name ?? null,
-      directory: config.directory ?? null,
-      extra: config.extra ?? null,
-      projectID: input.projectID,
-    }
+  await waitEvent({
+    timeout: TIMEOUT,
+    fn(event) {
+      if (event.workspace === info.id && event.payload.type === Event.Status.type) {
+        const { status } = event.payload.properties
+        return status === "error" || status === "connected"
+      }
+      return false
+    },
+  })
 
-    Database.use((db) => {
-      db.insert(WorkspaceTable)
-        .values({
-          id: info.id,
-          type: info.type,
-          branch: info.branch,
-          name: info.name,
-          directory: info.directory,
-          extra: info.extra,
-          project_id: info.projectID,
-        })
-        .run()
-    })
+  return info
+})
 
-    await adaptor.create(config)
+const SessionRestoreInput = z.object({
+  workspaceID: WorkspaceID.zod,
+  sessionID: SessionID.zod,
+})
 
-    startSync(info)
+export const sessionRestore = fn(SessionRestoreInput, async (input) => {
+  log.info("session restore requested", {
+    workspaceID: input.workspaceID,
+    sessionID: input.sessionID,
+  })
+  try {
+    const space = await get(input.workspaceID)
+    if (!space) throw new Error(`Workspace not found: ${input.workspaceID}`)
 
-    await waitEvent({
-      timeout: TIMEOUT,
-      fn(event) {
-        if (event.workspace === info.id && event.payload.type === Event.Status.type) {
-          const { status } = event.payload.properties
-          return status === "error" || status === "connected"
-        }
-        return false
+    const adaptor = await getAdaptor(space.projectID, space.type)
+    const target = await adaptor.target(space)
+
+    // Need to switch the workspace of the session
+    SyncEvent.run(Session.Event.Updated, {
+      sessionID: input.sessionID,
+      info: {
+        workspaceID: input.workspaceID,
       },
     })
 
-    return info
-  })
+    const rows = Database.use((db) =>
+      db
+        .select({
+          id: EventTable.id,
+          aggregateID: EventTable.aggregate_id,
+          seq: EventTable.seq,
+          type: EventTable.type,
+          data: EventTable.data,
+        })
+        .from(EventTable)
+        .where(eq(EventTable.aggregate_id, input.sessionID))
+        .orderBy(asc(EventTable.seq))
+        .all(),
+    )
+    if (rows.length === 0) throw new Error(`No events found for session: ${input.sessionID}`)
 
-  const SessionRestoreInput = z.object({
-    workspaceID: WorkspaceID.zod,
-    sessionID: SessionID.zod,
-  })
+    const all = rows
 
-  export const sessionRestore = fn(SessionRestoreInput, async (input) => {
-    log.info("session restore requested", {
+    const size = 10
+    const sets = Array.from({ length: Math.ceil(all.length / size) }, (_, i) => all.slice(i * size, (i + 1) * size))
+    const total = sets.length
+    log.info("session restore prepared", {
       workspaceID: input.workspaceID,
       sessionID: input.sessionID,
+      workspaceType: space.type,
+      directory: space.directory,
+      target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory,
+      events: all.length,
+      batches: total,
+      first: all[0]?.seq,
+      last: all.at(-1)?.seq,
     })
-    try {
-      const space = await get(input.workspaceID)
-      if (!space) throw new Error(`Workspace not found: ${input.workspaceID}`)
-
-      const adaptor = await getAdaptor(space.projectID, space.type)
-      const target = await adaptor.target(space)
-
-      // Need to switch the workspace of the session
-      SyncEvent.run(Session.Event.Updated, {
-        sessionID: input.sessionID,
-        info: {
+    GlobalBus.emit("event", {
+      directory: "global",
+      workspace: input.workspaceID,
+      payload: {
+        type: Event.Restore.type,
+        properties: {
           workspaceID: input.workspaceID,
+          sessionID: input.sessionID,
+          total,
+          step: 0,
         },
-      })
-
-      const rows = Database.use((db) =>
-        db
-          .select({
-            id: EventTable.id,
-            aggregateID: EventTable.aggregate_id,
-            seq: EventTable.seq,
-            type: EventTable.type,
-            data: EventTable.data,
-          })
-          .from(EventTable)
-          .where(eq(EventTable.aggregate_id, input.sessionID))
-          .orderBy(asc(EventTable.seq))
-          .all(),
-      )
-      if (rows.length === 0) throw new Error(`No events found for session: ${input.sessionID}`)
-
-      const all = rows
-
-      const size = 10
-      const sets = Array.from({ length: Math.ceil(all.length / size) }, (_, i) => all.slice(i * size, (i + 1) * size))
-      const total = sets.length
-      log.info("session restore prepared", {
+      },
+    })
+    for (const [i, events] of sets.entries()) {
+      log.info("session restore batch starting", {
         workspaceID: input.workspaceID,
         sessionID: input.sessionID,
-        workspaceType: space.type,
-        directory: space.directory,
+        step: i + 1,
+        total,
+        events: events.length,
+        first: events[0]?.seq,
+        last: events.at(-1)?.seq,
         target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory,
-        events: all.length,
-        batches: total,
-        first: all[0]?.seq,
-        last: all.at(-1)?.seq,
       })
-      GlobalBus.emit("event", {
-        directory: "global",
-        workspace: input.workspaceID,
-        payload: {
-          type: Event.Restore.type,
-          properties: {
-            workspaceID: input.workspaceID,
-            sessionID: input.sessionID,
-            total,
-            step: 0,
-          },
-        },
-      })
-      for (const [i, events] of sets.entries()) {
-        log.info("session restore batch starting", {
+      if (target.type === "local") {
+        SyncEvent.replayAll(events)
+        log.info("session restore batch replayed locally", {
           workspaceID: input.workspaceID,
           sessionID: input.sessionID,
           step: i + 1,
           total,
           events: events.length,
-          first: events[0]?.seq,
-          last: events.at(-1)?.seq,
-          target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory,
         })
-        if (target.type === "local") {
-          SyncEvent.replayAll(events)
-          log.info("session restore batch replayed locally", {
-            workspaceID: input.workspaceID,
-            sessionID: input.sessionID,
-            step: i + 1,
-            total,
-            events: events.length,
-          })
-        } else {
-          const url = route(target.url, "/sync/replay")
-          const headers = new Headers(target.headers)
-          headers.set("content-type", "application/json")
-          const res = await fetch(url, {
-            method: "POST",
-            headers,
-            body: JSON.stringify({
-              directory: space.directory ?? "",
-              events,
-            }),
-          })
-          if (!res.ok) {
-            const body = await res.text()
-            log.error("session restore batch failed", {
-              workspaceID: input.workspaceID,
-              sessionID: input.sessionID,
-              step: i + 1,
-              total,
-              status: res.status,
-              body,
-            })
-            throw new Error(
-              `Failed to replay session ${input.sessionID} into workspace ${input.workspaceID}: HTTP ${res.status} ${body}`,
-            )
-          }
-          log.info("session restore batch posted", {
+      } else {
+        const url = route(target.url, "/sync/replay")
+        const headers = new Headers(target.headers)
+        headers.set("content-type", "application/json")
+        const res = await fetch(url, {
+          method: "POST",
+          headers,
+          body: JSON.stringify({
+            directory: space.directory ?? "",
+            events,
+          }),
+        })
+        if (!res.ok) {
+          const body = await res.text()
+          log.error("session restore batch failed", {
             workspaceID: input.workspaceID,
             sessionID: input.sessionID,
             step: i + 1,
             total,
             status: res.status,
+            body,
           })
+          throw new Error(
+            `Failed to replay session ${input.sessionID} into workspace ${input.workspaceID}: HTTP ${res.status} ${body}`,
+          )
         }
-        GlobalBus.emit("event", {
-          directory: "global",
-          workspace: input.workspaceID,
-          payload: {
-            type: Event.Restore.type,
-            properties: {
-              workspaceID: input.workspaceID,
-              sessionID: input.sessionID,
-              total,
-              step: i + 1,
-            },
-          },
+        log.info("session restore batch posted", {
+          workspaceID: input.workspaceID,
+          sessionID: input.sessionID,
+          step: i + 1,
+          total,
+          status: res.status,
         })
       }
-
-      log.info("session restore complete", {
-        workspaceID: input.workspaceID,
-        sessionID: input.sessionID,
-        batches: total,
-      })
-
-      return {
-        total,
-      }
-    } catch (err) {
-      log.error("session restore failed", {
-        workspaceID: input.workspaceID,
-        sessionID: input.sessionID,
-        error: errorData(err),
+      GlobalBus.emit("event", {
+        directory: "global",
+        workspace: input.workspaceID,
+        payload: {
+          type: Event.Restore.type,
+          properties: {
+            workspaceID: input.workspaceID,
+            sessionID: input.sessionID,
+            total,
+            step: i + 1,
+          },
+        },
       })
-      throw err
     }
-  })
 
-  export function list(project: Project.Info) {
-    const rows = Database.use((db) =>
-      db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(),
-    )
-    const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id))
+    log.info("session restore complete", {
+      workspaceID: input.workspaceID,
+      sessionID: input.sessionID,
+      batches: total,
+    })
 
-    for (const space of spaces) startSync(space)
-    return spaces
+    return {
+      total,
+    }
+  } catch (err) {
+    log.error("session restore failed", {
+      workspaceID: input.workspaceID,
+      sessionID: input.sessionID,
+      error: errorData(err),
+    })
+    throw err
   }
+})
 
-  function lookup(id: WorkspaceID) {
-    const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
-    if (!row) return
-    return fromRow(row)
-  }
+export function list(project: Project.Info) {
+  const rows = Database.use((db) =>
+    db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(),
+  )
+  const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id))
 
-  export const get = fn(WorkspaceID.zod, async (id) => {
-    const space = lookup(id)
-    if (!space) return
-    startSync(space)
-    return space
-  })
+  for (const space of spaces) startSync(space)
+  return spaces
+}
 
-  export const remove = fn(WorkspaceID.zod, async (id) => {
-    const sessions = Database.use((db) =>
-      db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.workspace_id, id)).all(),
-    )
-    for (const session of sessions) {
-      await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(session.id)))
-    }
+function lookup(id: WorkspaceID) {
+  const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
+  if (!row) return
+  return fromRow(row)
+}
 
-    const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
+export const get = fn(WorkspaceID.zod, async (id) => {
+  const space = lookup(id)
+  if (!space) return
+  startSync(space)
+  return space
+})
+
+export const remove = fn(WorkspaceID.zod, async (id) => {
+  const sessions = Database.use((db) =>
+    db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.workspace_id, id)).all(),
+  )
+  for (const session of sessions) {
+    await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(session.id)))
+  }
 
-    if (row) {
-      stopSync(id)
+  const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
 
-      const info = fromRow(row)
-      try {
-        const adaptor = await getAdaptor(info.projectID, row.type)
-        await adaptor.remove(info)
-      } catch {
-        log.error("adaptor not available when removing workspace", { type: row.type })
-      }
-      Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
-      return info
-    }
-  })
+  if (row) {
+    stopSync(id)
 
-  const connections = new Map<WorkspaceID, ConnectionStatus>()
-  const aborts = new Map<WorkspaceID, AbortController>()
-  const TIMEOUT = 5000
+    const info = fromRow(row)
+    try {
+      const adaptor = await getAdaptor(info.projectID, row.type)
+      await adaptor.remove(info)
+    } catch {
+      log.error("adaptor not available when removing workspace", { type: row.type })
+    }
+    Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
+    return info
+  }
+})
 
-  function setStatus(id: WorkspaceID, status: ConnectionStatus["status"], error?: string) {
-    const prev = connections.get(id)
-    if (prev?.status === status && prev?.error === error) return
-    const next = { workspaceID: id, status, error }
-    connections.set(id, next)
+const connections = new Map<WorkspaceID, ConnectionStatus>()
+const aborts = new Map<WorkspaceID, AbortController>()
+const TIMEOUT = 5000
 
-    if (status === "error") {
-      aborts.delete(id)
-    }
+function setStatus(id: WorkspaceID, status: ConnectionStatus["status"], error?: string) {
+  const prev = connections.get(id)
+  if (prev?.status === status && prev?.error === error) return
+  const next = { workspaceID: id, status, error }
+  connections.set(id, next)
 
-    GlobalBus.emit("event", {
-      directory: "global",
-      workspace: id,
-      payload: {
-        type: Event.Status.type,
-        properties: next,
-      },
-    })
+  if (status === "error") {
+    aborts.delete(id)
   }
 
-  export function status(): ConnectionStatus[] {
-    return [...connections.values()]
-  }
+  GlobalBus.emit("event", {
+    directory: "global",
+    workspace: id,
+    payload: {
+      type: Event.Status.type,
+      properties: next,
+    },
+  })
+}
 
-  function synced(state: Record<string, number>) {
-    const ids = Object.keys(state)
-    if (ids.length === 0) return true
+export function status(): ConnectionStatus[] {
+  return [...connections.values()]
+}
 
-    const done = Object.fromEntries(
-      Database.use((db) =>
-        db
-          .select({
-            id: EventSequenceTable.aggregate_id,
-            seq: EventSequenceTable.seq,
-          })
-          .from(EventSequenceTable)
-          .where(inArray(EventSequenceTable.aggregate_id, ids))
-          .all(),
-      ).map((row) => [row.id, row.seq]),
-    ) as Record<string, number>
-
-    return ids.every((id) => {
-      return (done[id] ?? -1) >= state[id]
-    })
-  }
+function synced(state: Record<string, number>) {
+  const ids = Object.keys(state)
+  if (ids.length === 0) return true
 
-  export async function isSyncing(workspaceID: WorkspaceID) {
-    return aborts.has(workspaceID)
-  }
+  const done = Object.fromEntries(
+    Database.use((db) =>
+      db
+        .select({
+          id: EventSequenceTable.aggregate_id,
+          seq: EventSequenceTable.seq,
+        })
+        .from(EventSequenceTable)
+        .where(inArray(EventSequenceTable.aggregate_id, ids))
+        .all(),
+    ).map((row) => [row.id, row.seq]),
+  ) as Record<string, number>
+
+  return ids.every((id) => {
+    return (done[id] ?? -1) >= state[id]
+  })
+}
 
-  export async function waitForSync(workspaceID: WorkspaceID, state: Record<string, number>, signal?: AbortSignal) {
-    if (synced(state)) return
+export async function isSyncing(workspaceID: WorkspaceID) {
+  return aborts.has(workspaceID)
+}
 
-    try {
-      await waitEvent({
-        timeout: TIMEOUT,
-        signal,
-        fn(event) {
-          if (event.workspace !== workspaceID && event.payload.type !== "sync") {
-            return false
-          }
-          return synced(state)
-        },
-      })
-    } catch {
-      if (signal?.aborted) throw signal.reason ?? new Error("Request aborted")
-      throw new Error(`Timed out waiting for sync fence: ${JSON.stringify(state)}`)
-    }
+export async function waitForSync(workspaceID: WorkspaceID, state: Record<string, number>, signal?: AbortSignal) {
+  if (synced(state)) return
+
+  try {
+    await waitEvent({
+      timeout: TIMEOUT,
+      signal,
+      fn(event) {
+        if (event.workspace !== workspaceID && event.payload.type !== "sync") {
+          return false
+        }
+        return synced(state)
+      },
+    })
+  } catch {
+    if (signal?.aborted) throw signal.reason ?? new Error("Request aborted")
+    throw new Error(`Timed out waiting for sync fence: ${JSON.stringify(state)}`)
   }
+}
 
-  const log = Log.create({ service: "workspace-sync" })
+const log = Log.create({ service: "workspace-sync" })
 
-  function route(url: string | URL, path: string) {
-    const next = new URL(url)
-    next.pathname = `${next.pathname.replace(/\/$/, "")}${path}`
-    next.search = ""
-    next.hash = ""
-    return next
-  }
+function route(url: string | URL, path: string) {
+  const next = new URL(url)
+  next.pathname = `${next.pathname.replace(/\/$/, "")}${path}`
+  next.search = ""
+  next.hash = ""
+  return next
+}
 
-  async function syncWorkspace(space: Info, signal: AbortSignal) {
-    while (!signal.aborted) {
-      log.info("connecting to global sync", { workspace: space.name })
-      setStatus(space.id, "connecting")
+async function syncWorkspace(space: Info, signal: AbortSignal) {
+  while (!signal.aborted) {
+    log.info("connecting to global sync", { workspace: space.name })
+    setStatus(space.id, "connecting")
 
-      const adaptor = await getAdaptor(space.projectID, space.type)
-      const target = await adaptor.target(space)
+    const adaptor = await getAdaptor(space.projectID, space.type)
+    const target = await adaptor.target(space)
 
-      if (target.type === "local") return
+    if (target.type === "local") return
 
-      const res = await fetch(route(target.url, "/global/event"), {
-        method: "GET",
-        headers: target.headers,
-        signal,
-      }).catch((err: unknown) => {
-        setStatus(space.id, "error", err instanceof Error ? err.message : String(err))
+    const res = await fetch(route(target.url, "/global/event"), {
+      method: "GET",
+      headers: target.headers,
+      signal,
+    }).catch((err: unknown) => {
+      setStatus(space.id, "error", err instanceof Error ? err.message : String(err))
 
-        log.info("failed to connect to global sync", {
-          workspace: space.name,
-          error: err,
-        })
-        return undefined
+      log.info("failed to connect to global sync", {
+        workspace: space.name,
+        error: err,
       })
+      return undefined
+    })
 
-      if (!res || !res.ok || !res.body) {
-        const error = !res ? "No response from global sync" : `Global sync HTTP ${res.status}`
-        log.info("failed to connect to global sync", { workspace: space.name, error })
-        setStatus(space.id, "error", error)
-        await sleep(1000)
-        continue
-      }
-
-      log.info("global sync connected", { workspace: space.name })
-      setStatus(space.id, "connected")
+    if (!res || !res.ok || !res.body) {
+      const error = !res ? "No response from global sync" : `Global sync HTTP ${res.status}`
+      log.info("failed to connect to global sync", { workspace: space.name, error })
+      setStatus(space.id, "error", error)
+      await sleep(1000)
+      continue
+    }
 
-      await parseSSE(res.body, signal, (evt: any) => {
-        try {
-          if (!("payload" in evt)) return
+    log.info("global sync connected", { workspace: space.name })
+    setStatus(space.id, "connected")
 
-          if (evt.payload.type === "sync") {
-            // This name -> type is temporary
-            SyncEvent.replay({ ...evt.payload, type: evt.payload.name } as SyncEvent.SerializedEvent)
-          }
+    await parseSSE(res.body, signal, (evt: any) => {
+      try {
+        if (!("payload" in evt)) return
 
-          GlobalBus.emit("event", {
-            directory: evt.directory,
-            project: evt.project,
-            workspace: space.id,
-            payload: evt.payload,
-          })
-        } catch (err) {
-          log.info("failed to replay global event", {
-            workspaceID: space.id,
-            error: err,
-          })
+        if (evt.payload.type === "sync") {
+          // This name -> type is temporary
+          SyncEvent.replay({ ...evt.payload, type: evt.payload.name } as SyncEvent.SerializedEvent)
         }
-      })
 
-      log.info("disconnected from global sync: " + space.id)
-      setStatus(space.id, "disconnected")
+        GlobalBus.emit("event", {
+          directory: evt.directory,
+          project: evt.project,
+          workspace: space.id,
+          payload: evt.payload,
+        })
+      } catch (err) {
+        log.info("failed to replay global event", {
+          workspaceID: space.id,
+          error: err,
+        })
+      }
+    })
 
-      // TODO: Implement exponential backoff
-      await sleep(1000)
-    }
+    log.info("disconnected from global sync: " + space.id)
+    setStatus(space.id, "disconnected")
+
+    // TODO: Implement exponential backoff
+    await sleep(1000)
   }
+}
 
-  async function startSync(space: Info) {
-    if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
+async function startSync(space: Info) {
+  if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
 
-    const adaptor = await getAdaptor(space.projectID, space.type)
-    const target = await adaptor.target(space)
+  const adaptor = await getAdaptor(space.projectID, space.type)
+  const target = await adaptor.target(space)
 
-    if (target.type === "local") {
-      void Filesystem.exists(target.directory).then((exists) => {
-        setStatus(space.id, exists ? "connected" : "error", exists ? undefined : "directory does not exist")
-      })
-      return
-    }
+  if (target.type === "local") {
+    void Filesystem.exists(target.directory).then((exists) => {
+      setStatus(space.id, exists ? "connected" : "error", exists ? undefined : "directory does not exist")
+    })
+    return
+  }
 
-    if (aborts.has(space.id)) return true
+  if (aborts.has(space.id)) return true
 
-    setStatus(space.id, "disconnected")
+  setStatus(space.id, "disconnected")
 
-    const abort = new AbortController()
-    aborts.set(space.id, abort)
+  const abort = new AbortController()
+  aborts.set(space.id, abort)
 
-    void syncWorkspace(space, abort.signal).catch((error) => {
-      aborts.delete(space.id)
+  void syncWorkspace(space, abort.signal).catch((error) => {
+    aborts.delete(space.id)
 
-      setStatus(space.id, "error", String(error))
-      log.warn("workspace listener failed", {
-        workspaceID: space.id,
-        error,
-      })
+    setStatus(space.id, "error", String(error))
+    log.warn("workspace listener failed", {
+      workspaceID: space.id,
+      error,
     })
-  }
+  })
+}
 
-  function stopSync(id: WorkspaceID) {
-    aborts.get(id)?.abort()
-    aborts.delete(id)
-    connections.delete(id)
-  }
+function stopSync(id: WorkspaceID) {
+  aborts.get(id)?.abort()
+  aborts.delete(id)
+  connections.delete(id)
 }

+ 1 - 1
packages/opencode/src/effect/app-runtime.ts

@@ -17,7 +17,7 @@ import { Snapshot } from "@/snapshot"
 import { Plugin } from "@/plugin"
 import { Provider } from "@/provider"
 import { ProviderAuth } from "@/provider"
-import { Agent } from "@/agent/agent"
+import { Agent } from "@/agent"
 import { Skill } from "@/skill"
 import { Discovery } from "@/skill/discovery"
 import { Question } from "@/question"

+ 64 - 66
packages/opencode/src/id/id.ts

@@ -1,86 +1,84 @@
 import z from "zod"
 import { randomBytes } from "crypto"
 
-export namespace Identifier {
-  const prefixes = {
-    event: "evt",
-    session: "ses",
-    message: "msg",
-    permission: "per",
-    question: "que",
-    user: "usr",
-    part: "prt",
-    pty: "pty",
-    tool: "tool",
-    workspace: "wrk",
-    entry: "ent",
-  } as const
-
-  export function schema(prefix: keyof typeof prefixes) {
-    return z.string().startsWith(prefixes[prefix])
-  }
-
-  const LENGTH = 26
+const prefixes = {
+  event: "evt",
+  session: "ses",
+  message: "msg",
+  permission: "per",
+  question: "que",
+  user: "usr",
+  part: "prt",
+  pty: "pty",
+  tool: "tool",
+  workspace: "wrk",
+  entry: "ent",
+} as const
+
+export function schema(prefix: keyof typeof prefixes) {
+  return z.string().startsWith(prefixes[prefix])
+}
 
-  // State for monotonic ID generation
-  let lastTimestamp = 0
-  let counter = 0
+const LENGTH = 26
 
-  export function ascending(prefix: keyof typeof prefixes, given?: string) {
-    return generateID(prefix, "ascending", given)
-  }
+// State for monotonic ID generation
+let lastTimestamp = 0
+let counter = 0
 
-  export function descending(prefix: keyof typeof prefixes, given?: string) {
-    return generateID(prefix, "descending", given)
-  }
+export function ascending(prefix: keyof typeof prefixes, given?: string) {
+  return generateID(prefix, "ascending", given)
+}
 
-  function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string {
-    if (!given) {
-      return create(prefixes[prefix], direction)
-    }
+export function descending(prefix: keyof typeof prefixes, given?: string) {
+  return generateID(prefix, "descending", given)
+}
 
-    if (!given.startsWith(prefixes[prefix])) {
-      throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
-    }
-    return given
+function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string {
+  if (!given) {
+    return create(prefixes[prefix], direction)
   }
 
-  function randomBase62(length: number): string {
-    const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
-    let result = ""
-    const bytes = randomBytes(length)
-    for (let i = 0; i < length; i++) {
-      result += chars[bytes[i] % 62]
-    }
-    return result
+  if (!given.startsWith(prefixes[prefix])) {
+    throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
   }
+  return given
+}
 
-  export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string {
-    const currentTimestamp = timestamp ?? Date.now()
+function randomBase62(length: number): string {
+  const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+  let result = ""
+  const bytes = randomBytes(length)
+  for (let i = 0; i < length; i++) {
+    result += chars[bytes[i] % 62]
+  }
+  return result
+}
 
-    if (currentTimestamp !== lastTimestamp) {
-      lastTimestamp = currentTimestamp
-      counter = 0
-    }
-    counter++
+export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string {
+  const currentTimestamp = timestamp ?? Date.now()
 
-    let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter)
+  if (currentTimestamp !== lastTimestamp) {
+    lastTimestamp = currentTimestamp
+    counter = 0
+  }
+  counter++
 
-    now = direction === "descending" ? ~now : now
+  let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter)
 
-    const timeBytes = Buffer.alloc(6)
-    for (let i = 0; i < 6; i++) {
-      timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
-    }
+  now = direction === "descending" ? ~now : now
 
-    return prefix + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12)
+  const timeBytes = Buffer.alloc(6)
+  for (let i = 0; i < 6; i++) {
+    timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
   }
 
-  /** Extract timestamp from an ascending ID. Does not work with descending IDs. */
-  export function timestamp(id: string): number {
-    const prefix = id.split("_")[0]
-    const hex = id.slice(prefix.length + 1, prefix.length + 13)
-    const encoded = BigInt("0x" + hex)
-    return Number(encoded / BigInt(0x1000))
-  }
+  return prefix + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12)
+}
+
+/** Extract timestamp from an ascending ID. Does not work with descending IDs. */
+export function timestamp(id: string): number {
+  const prefix = id.split("_")[0]
+  const hex = id.slice(prefix.length + 1, prefix.length + 13)
+  const encoded = BigInt("0x" + hex)
+  return Number(encoded / BigInt(0x1000))
 }

+ 1 - 0
packages/opencode/src/id/index.ts

@@ -0,0 +1 @@
+export * as Identifier from "./id"

+ 1 - 1
packages/opencode/src/permission/schema.ts

@@ -1,7 +1,7 @@
 import { Schema } from "effect"
 import z from "zod"
 
-import { Identifier } from "@/id/id"
+import { Identifier } from "@/id"
 import { ZodOverride } from "@/util/effect-zod"
 import { Newtype } from "@/util/schema"
 

+ 1 - 1
packages/opencode/src/pty/schema.ts

@@ -1,7 +1,7 @@
 import { Schema } from "effect"
 import z from "zod"
 
-import { Identifier } from "@/id/id"
+import { Identifier } from "@/id"
 import { ZodOverride } from "@/util/effect-zod"
 import { withStatics } from "@/util/schema"
 

+ 1 - 1
packages/opencode/src/pty/service.ts

@@ -6,7 +6,7 @@ import type { Proc } from "#pty"
 import z from "zod"
 import { Log } from "../util"
 import { lazy } from "@opencode-ai/shared/util/lazy"
-import { Shell } from "@/shell/shell"
+import { Shell } from "@/shell"
 import { Plugin } from "@/plugin"
 import { PtyID } from "./schema"
 import { Effect, Layer, Context } from "effect"

+ 1 - 1
packages/opencode/src/question/schema.ts

@@ -1,7 +1,7 @@
 import { Schema } from "effect"
 import z from "zod"
 
-import { Identifier } from "@/id/id"
+import { Identifier } from "@/id"
 import { ZodOverride } from "@/util/effect-zod"
 import { Newtype } from "@/util/schema"
 

+ 1 - 1
packages/opencode/src/server/fence.ts

@@ -1,7 +1,7 @@
 import type { MiddlewareHandler } from "hono"
 import { Database, inArray } from "@/storage"
 import { EventSequenceTable } from "@/sync/event.sql"
-import { Workspace } from "@/control-plane/workspace"
+import { Workspace } from "@/control-plane"
 import type { WorkspaceID } from "@/control-plane/schema"
 import { Log } from "@/util"
 

+ 1 - 1
packages/opencode/src/server/instance/experimental.ts

@@ -17,7 +17,7 @@ import { errors } from "../error"
 import { lazy } from "../../util/lazy"
 import { Effect, Option } from "effect"
 import { WorkspaceRoutes } from "./workspace"
-import { Agent } from "@/agent/agent"
+import { Agent } from "@/agent"
 
 const ConsoleOrgOption = z.object({
   accountID: z.string(),

+ 1 - 1
packages/opencode/src/server/instance/index.ts

@@ -7,7 +7,7 @@ import { Format } from "../../format"
 import { TuiRoutes } from "./tui"
 import { Instance } from "../../project/instance"
 import { Vcs } from "../../project"
-import { Agent } from "../../agent/agent"
+import { Agent } from "../../agent"
 import { Skill } from "../../skill"
 import { Global } from "../../global"
 import { LSP } from "../../lsp"

+ 1 - 1
packages/opencode/src/server/instance/middleware.ts

@@ -2,7 +2,7 @@ import type { MiddlewareHandler } from "hono"
 import type { UpgradeWebSocket } from "hono/ws"
 import { getAdaptor } from "@/control-plane/adaptors"
 import { WorkspaceID } from "@/control-plane/schema"
-import { Workspace } from "@/control-plane/workspace"
+import { Workspace } from "@/control-plane"
 import { ServerProxy } from "../proxy"
 import { Instance } from "@/project/instance"
 import { InstanceBootstrap } from "@/project/bootstrap"

+ 1 - 1
packages/opencode/src/server/instance/session.ts

@@ -15,7 +15,7 @@ import { SessionSummary } from "@/session/summary"
 import { Todo } from "../../session/todo"
 import { Effect } from "effect"
 import { AppRuntime } from "../../effect/app-runtime"
-import { Agent } from "../../agent/agent"
+import { Agent } from "../../agent"
 import { Snapshot } from "@/snapshot"
 import { Command } from "../../command"
 import { Log } from "../../util"

+ 1 - 1
packages/opencode/src/server/instance/workspace.ts

@@ -2,7 +2,7 @@ import { Hono } from "hono"
 import { describeRoute, resolver, validator } from "hono-openapi"
 import z from "zod"
 import { listAdaptors } from "../../control-plane/adaptors"
-import { Workspace } from "../../control-plane/workspace"
+import { Workspace } from "../../control-plane"
 import { Instance } from "../../project/instance"
 import { errors } from "../error"
 import { lazy } from "../../util/lazy"

+ 1 - 1
packages/opencode/src/server/proxy.ts

@@ -3,7 +3,7 @@ import type { UpgradeWebSocket } from "hono/ws"
 import { Log } from "@/util"
 import * as Fence from "./fence"
 import type { WorkspaceID } from "@/control-plane/schema"
-import { Workspace } from "@/control-plane/workspace"
+import { Workspace } from "@/control-plane"
 
 const hop = new Set([
   "connection",

+ 1 - 1
packages/opencode/src/session/compaction.ts

@@ -8,7 +8,7 @@ import z from "zod"
 import { Token } from "../util"
 import { Log } from "../util"
 import { SessionProcessor } from "./processor"
-import { Agent } from "@/agent/agent"
+import { Agent } from "@/agent"
 import { Plugin } from "@/plugin"
 import { Config } from "@/config"
 import { NotFoundError } from "@/storage"

+ 1 - 1
packages/opencode/src/session/llm.ts

@@ -8,7 +8,7 @@ import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
 import { ProviderTransform } from "@/provider"
 import { Config } from "@/config"
 import { Instance } from "@/project/instance"
-import type { Agent } from "@/agent/agent"
+import type { Agent } from "@/agent"
 import type { MessageV2 } from "./message-v2"
 import { Plugin } from "@/plugin"
 import { SystemPrompt } from "./system"

+ 1 - 1
packages/opencode/src/session/processor.ts

@@ -1,6 +1,6 @@
 import { Cause, Deferred, Effect, Layer, Context, Scope } from "effect"
 import * as Stream from "effect/Stream"
-import { Agent } from "@/agent/agent"
+import { Agent } from "@/agent"
 import { Bus } from "@/bus"
 import { Config } from "@/config"
 import { Permission } from "@/permission"

+ 2 - 2
packages/opencode/src/session/prompt.ts

@@ -6,7 +6,7 @@ import { MessageV2 } from "./message-v2"
 import { Log } from "../util"
 import { SessionRevert } from "./revert"
 import * as Session from "./session"
-import { Agent } from "../agent/agent"
+import { Agent } from "../agent"
 import { Provider } from "../provider"
 import { ModelID, ProviderID } from "../provider/schema"
 import { type Tool as AITool, tool, jsonSchema, type ToolExecutionOptions, asSchema } from "ai"
@@ -38,7 +38,7 @@ import { Tool } from "@/tool"
 import { Permission } from "@/permission"
 import { SessionStatus } from "./status"
 import { LLM } from "./llm"
-import { Shell } from "@/shell/shell"
+import { Shell } from "@/shell"
 import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { Truncate } from "@/tool"
 import { decodeDataUrl } from "@/util/data-url"

+ 1 - 1
packages/opencode/src/session/schema.ts

@@ -1,7 +1,7 @@
 import { Schema } from "effect"
 import z from "zod"
 
-import { Identifier } from "@/id/id"
+import { Identifier } from "@/id"
 import { ZodOverride } from "@/util/effect-zod"
 import { withStatics } from "@/util/schema"
 

+ 1 - 1
packages/opencode/src/session/system.ts

@@ -12,7 +12,7 @@ import PROMPT_KIMI from "./prompt/kimi.txt"
 import PROMPT_CODEX from "./prompt/codex.txt"
 import PROMPT_TRINITY from "./prompt/trinity.txt"
 import type { Provider } from "@/provider"
-import type { Agent } from "@/agent/agent"
+import type { Agent } from "@/agent"
 import { Permission } from "@/permission"
 import { Skill } from "@/skill"
 

+ 1 - 0
packages/opencode/src/shell/index.ts

@@ -0,0 +1 @@
+export * as Shell from "./shell"

+ 79 - 81
packages/opencode/src/shell/shell.ts

@@ -8,103 +8,101 @@ import { setTimeout as sleep } from "node:timers/promises"
 
 const SIGKILL_TIMEOUT_MS = 200
 
-export namespace Shell {
-  const BLACKLIST = new Set(["fish", "nu"])
-  const LOGIN = new Set(["bash", "dash", "fish", "ksh", "sh", "zsh"])
-  const POSIX = new Set(["bash", "dash", "ksh", "sh", "zsh"])
+const BLACKLIST = new Set(["fish", "nu"])
+const LOGIN = new Set(["bash", "dash", "fish", "ksh", "sh", "zsh"])
+const POSIX = new Set(["bash", "dash", "ksh", "sh", "zsh"])
 
-  export async function killTree(proc: ChildProcess, opts?: { exited?: () => boolean }): Promise<void> {
-    const pid = proc.pid
-    if (!pid || opts?.exited?.()) return
+export async function killTree(proc: ChildProcess, opts?: { exited?: () => boolean }): Promise<void> {
+  const pid = proc.pid
+  if (!pid || opts?.exited?.()) return
 
-    if (process.platform === "win32") {
-      await new Promise<void>((resolve) => {
-        const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], {
-          stdio: "ignore",
-          windowsHide: true,
-        })
-        killer.once("exit", () => resolve())
-        killer.once("error", () => resolve())
+  if (process.platform === "win32") {
+    await new Promise<void>((resolve) => {
+      const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], {
+        stdio: "ignore",
+        windowsHide: true,
       })
-      return
-    }
-
-    try {
-      process.kill(-pid, "SIGTERM")
-      await sleep(SIGKILL_TIMEOUT_MS)
-      if (!opts?.exited?.()) {
-        process.kill(-pid, "SIGKILL")
-      }
-    } catch (_e) {
-      proc.kill("SIGTERM")
-      await sleep(SIGKILL_TIMEOUT_MS)
-      if (!opts?.exited?.()) {
-        proc.kill("SIGKILL")
-      }
-    }
+      killer.once("exit", () => resolve())
+      killer.once("error", () => resolve())
+    })
+    return
   }
 
-  function full(file: string) {
-    if (process.platform !== "win32") return file
-    const shell = Filesystem.windowsPath(file)
-    if (path.win32.dirname(shell) !== ".") {
-      if (shell.startsWith("/") && name(shell) === "bash") return gitbash() || shell
-      return shell
+  try {
+    process.kill(-pid, "SIGTERM")
+    await sleep(SIGKILL_TIMEOUT_MS)
+    if (!opts?.exited?.()) {
+      process.kill(-pid, "SIGKILL")
     }
-    return which(shell) || shell
-  }
-
-  function pick() {
-    const pwsh = which("pwsh.exe")
-    if (pwsh) return pwsh
-    const powershell = which("powershell.exe")
-    if (powershell) return powershell
-  }
-
-  function select(file: string | undefined, opts?: { acceptable?: boolean }) {
-    if (file && (!opts?.acceptable || !BLACKLIST.has(name(file)))) return full(file)
-    if (process.platform === "win32") {
-      const shell = pick()
-      if (shell) return shell
+  } catch (_e) {
+    proc.kill("SIGTERM")
+    await sleep(SIGKILL_TIMEOUT_MS)
+    if (!opts?.exited?.()) {
+      proc.kill("SIGKILL")
     }
-    return fallback()
   }
+}
 
-  export function gitbash() {
-    if (process.platform !== "win32") return
-    if (Flag.OPENCODE_GIT_BASH_PATH) return Flag.OPENCODE_GIT_BASH_PATH
-    const git = which("git")
-    if (!git) return
-    const file = path.join(git, "..", "..", "bin", "bash.exe")
-    if (Filesystem.stat(file)?.size) return file
+function full(file: string) {
+  if (process.platform !== "win32") return file
+  const shell = Filesystem.windowsPath(file)
+  if (path.win32.dirname(shell) !== ".") {
+    if (shell.startsWith("/") && name(shell) === "bash") return gitbash() || shell
+    return shell
   }
+  return which(shell) || shell
+}
 
-  function fallback() {
-    if (process.platform === "win32") {
-      const file = gitbash()
-      if (file) return file
-      return process.env.COMSPEC || "cmd.exe"
-    }
-    if (process.platform === "darwin") return "/bin/zsh"
-    const bash = which("bash")
-    if (bash) return bash
-    return "/bin/sh"
-  }
+function pick() {
+  const pwsh = which("pwsh.exe")
+  if (pwsh) return pwsh
+  const powershell = which("powershell.exe")
+  if (powershell) return powershell
+}
 
-  export function name(file: string) {
-    if (process.platform === "win32") return path.win32.parse(Filesystem.windowsPath(file)).name.toLowerCase()
-    return path.basename(file).toLowerCase()
+function select(file: string | undefined, opts?: { acceptable?: boolean }) {
+  if (file && (!opts?.acceptable || !BLACKLIST.has(name(file)))) return full(file)
+  if (process.platform === "win32") {
+    const shell = pick()
+    if (shell) return shell
   }
+  return fallback()
+}
 
-  export function login(file: string) {
-    return LOGIN.has(name(file))
-  }
+export function gitbash() {
+  if (process.platform !== "win32") return
+  if (Flag.OPENCODE_GIT_BASH_PATH) return Flag.OPENCODE_GIT_BASH_PATH
+  const git = which("git")
+  if (!git) return
+  const file = path.join(git, "..", "..", "bin", "bash.exe")
+  if (Filesystem.stat(file)?.size) return file
+}
 
-  export function posix(file: string) {
-    return POSIX.has(name(file))
+function fallback() {
+  if (process.platform === "win32") {
+    const file = gitbash()
+    if (file) return file
+    return process.env.COMSPEC || "cmd.exe"
   }
+  if (process.platform === "darwin") return "/bin/zsh"
+  const bash = which("bash")
+  if (bash) return bash
+  return "/bin/sh"
+}
 
-  export const preferred = lazy(() => select(process.env.SHELL))
+export function name(file: string) {
+  if (process.platform === "win32") return path.win32.parse(Filesystem.windowsPath(file)).name.toLowerCase()
+  return path.basename(file).toLowerCase()
+}
+
+export function login(file: string) {
+  return LOGIN.has(name(file))
+}
 
-  export const acceptable = lazy(() => select(process.env.SHELL, { acceptable: true }))
+export function posix(file: string) {
+  return POSIX.has(name(file))
 }
+
+export const preferred = lazy(() => select(process.env.SHELL))
+
+export const acceptable = lazy(() => select(process.env.SHELL, { acceptable: true }))

+ 1 - 1
packages/opencode/src/skill/skill.ts

@@ -4,7 +4,7 @@ import { pathToFileURL } from "url"
 import z from "zod"
 import { Effect, Layer, Context } from "effect"
 import { NamedError } from "@opencode-ai/shared/util/error"
-import type { Agent } from "@/agent/agent"
+import type { Agent } from "@/agent"
 import { Bus } from "@/bus"
 import { InstanceState } from "@/effect"
 import { Flag } from "@/flag/flag"

+ 1 - 1
packages/opencode/src/sync/schema.ts

@@ -1,7 +1,7 @@
 import { Schema } from "effect"
 import z from "zod"
 
-import { Identifier } from "@/id/id"
+import { Identifier } from "@/id"
 import { ZodOverride } from "@/util/effect-zod"
 import { withStatics } from "@/util/schema"
 

+ 1 - 1
packages/opencode/src/tool/bash.ts

@@ -12,7 +12,7 @@ import { Language, type Node } from "web-tree-sitter"
 import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { fileURLToPath } from "url"
 import { Flag } from "@/flag/flag"
-import { Shell } from "@/shell/shell"
+import { Shell } from "@/shell"
 
 import { BashArity } from "@/permission/arity"
 import * as Truncate from "./truncate"

+ 1 - 1
packages/opencode/src/tool/registry.ts

@@ -43,7 +43,7 @@ import { FileTime } from "../file/time"
 import { Instruction } from "../session/instruction"
 import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { Bus } from "../bus"
-import { Agent } from "../agent/agent"
+import { Agent } from "../agent"
 import { Skill } from "../skill"
 import { Permission } from "@/permission"
 

+ 1 - 1
packages/opencode/src/tool/schema.ts

@@ -1,7 +1,7 @@
 import { Schema } from "effect"
 import z from "zod"
 
-import { Identifier } from "@/id/id"
+import { Identifier } from "@/id"
 import { ZodOverride } from "@/util/effect-zod"
 import { withStatics } from "@/util/schema"
 

+ 1 - 1
packages/opencode/src/tool/task.ts

@@ -4,7 +4,7 @@ import z from "zod"
 import { Session } from "../session"
 import { SessionID, MessageID } from "../session/schema"
 import { MessageV2 } from "../session/message-v2"
-import { Agent } from "../agent/agent"
+import { Agent } from "../agent"
 import type { SessionPrompt } from "../session/prompt"
 import { Config } from "../config"
 import { Effect } from "effect"

+ 1 - 1
packages/opencode/src/tool/tool.ts

@@ -4,7 +4,7 @@ import type { MessageV2 } from "../session/message-v2"
 import type { Permission } from "../permission"
 import type { SessionID, MessageID } from "../session/schema"
 import * as Truncate from "./truncate"
-import { Agent } from "@/agent/agent"
+import { Agent } from "@/agent"
 
 interface Metadata {
   [key: string]: any

+ 2 - 2
packages/opencode/src/tool/truncate.ts

@@ -1,10 +1,10 @@
 import { NodePath } from "@effect/platform-node"
 import { Cause, Duration, Effect, Layer, Schedule, Context } from "effect"
 import path from "path"
-import type { Agent } from "../agent/agent"
+import type { Agent } from "../agent"
 import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { evaluate } from "@/permission/evaluate"
-import { Identifier } from "../id/id"
+import { Identifier } from "../id"
 import { Log } from "../util"
 import { ToolID } from "./schema"
 import { TRUNCATION_DIR } from "./truncation-dir"

+ 1 - 1
packages/opencode/src/v2/session-event.ts

@@ -1,4 +1,4 @@
-import { Identifier } from "@/id/id"
+import { Identifier } from "@/id"
 import { withStatics } from "@/util/schema"
 import * as DateTime from "effect/DateTime"
 import { Schema } from "effect"

+ 1 - 1
packages/opencode/test/acp/agent-interface.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, test } from "bun:test"
-import { ACP } from "../../src/acp/agent"
+import { ACP } from "../../src/acp"
 import type { Agent as ACPAgent } from "@agentclientprotocol/sdk"
 
 /**

+ 1 - 1
packages/opencode/test/acp/event-subscription.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, test } from "bun:test"
-import { ACP } from "../../src/acp/agent"
+import { ACP } from "../../src/acp"
 import type { AgentSideConnection } from "@agentclientprotocol/sdk"
 import type { Event, EventMessagePartUpdated, ToolStatePending, ToolStateRunning } from "@opencode-ai/sdk/v2"
 import { Instance } from "../../src/project/instance"

+ 1 - 1
packages/opencode/test/agent/agent.test.ts

@@ -3,7 +3,7 @@ import { Effect } from "effect"
 import path from "path"
 import { provideInstance, tmpdir } from "../fixture/fixture"
 import { Instance } from "../../src/project/instance"
-import { Agent } from "../../src/agent/agent"
+import { Agent } from "../../src/agent"
 import { Permission } from "../../src/permission"
 
 // Helper to evaluate permission for a tool with wildcard pattern

+ 1 - 1
packages/opencode/test/config/agent-color.test.ts

@@ -4,7 +4,7 @@ import path from "path"
 import { provideInstance, tmpdir } from "../fixture/fixture"
 import { Instance } from "../../src/project/instance"
 import { Config } from "../../src/config"
-import { Agent as AgentSvc } from "../../src/agent/agent"
+import { Agent as AgentSvc } from "../../src/agent"
 import { Color } from "../../src/util"
 import { AppRuntime } from "../../src/effect/app-runtime"
 

+ 1 - 1
packages/opencode/test/plugin/workspace-adaptor.test.ts

@@ -9,7 +9,7 @@ process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1"
 
 const { Flag } = await import("../../src/flag/flag")
 const { Plugin } = await import("../../src/plugin/index")
-const { Workspace } = await import("../../src/control-plane/workspace")
+const { Workspace } = await import("../../src/control-plane")
 const { Instance } = await import("../../src/project/instance")
 
 const experimental = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES

+ 1 - 1
packages/opencode/test/pty/pty-shell.test.ts

@@ -3,7 +3,7 @@ import { AppRuntime } from "../../src/effect/app-runtime"
 import { Effect } from "effect"
 import { Instance } from "../../src/project/instance"
 import { Pty } from "../../src/pty"
-import { Shell } from "../../src/shell/shell"
+import { Shell } from "../../src/shell"
 import { tmpdir } from "../fixture/fixture"
 
 Shell.preferred.reset()

+ 1 - 1
packages/opencode/test/session/compaction.test.ts

@@ -5,7 +5,7 @@ import * as Stream from "effect/Stream"
 import z from "zod"
 import { Bus } from "../../src/bus"
 import { Config } from "../../src/config"
-import { Agent } from "../../src/agent/agent"
+import { Agent } from "../../src/agent"
 import { LLM } from "../../src/session/llm"
 import { SessionCompaction } from "../../src/session/compaction"
 import { Token } from "../../src/util"

+ 1 - 1
packages/opencode/test/session/llm.test.ts

@@ -12,7 +12,7 @@ import { ModelsDev } from "../../src/provider"
 import { ProviderID, ModelID } from "../../src/provider/schema"
 import { Filesystem } from "../../src/util"
 import { tmpdir } from "../fixture/fixture"
-import type { Agent } from "../../src/agent/agent"
+import type { Agent } from "../../src/agent"
 import { MessageV2 } from "../../src/session/message-v2"
 import { SessionID, MessageID } from "../../src/session/schema"
 import { AppRuntime } from "../../src/effect/app-runtime"

+ 2 - 2
packages/opencode/test/session/processor-effect.test.ts

@@ -2,8 +2,8 @@ import { NodeFileSystem } from "@effect/platform-node"
 import { expect } from "bun:test"
 import { Cause, Effect, Exit, Fiber, Layer } from "effect"
 import path from "path"
-import type { Agent } from "../../src/agent/agent"
-import { Agent as AgentSvc } from "../../src/agent/agent"
+import type { Agent } from "../../src/agent"
+import { Agent as AgentSvc } from "../../src/agent"
 import { Bus } from "../../src/bus"
 import { Config } from "../../src/config"
 import { Permission } from "../../src/permission"

+ 2 - 2
packages/opencode/test/session/prompt-effect.test.ts

@@ -3,7 +3,7 @@ import { FetchHttpClient } from "effect/unstable/http"
 import { expect } from "bun:test"
 import { Cause, Effect, Exit, Fiber, Layer } from "effect"
 import path from "path"
-import { Agent as AgentSvc } from "../../src/agent/agent"
+import { Agent as AgentSvc } from "../../src/agent"
 import { Bus } from "../../src/bus"
 import { Command } from "../../src/command"
 import { Config } from "../../src/config"
@@ -32,7 +32,7 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema"
 import { SessionStatus } from "../../src/session/status"
 import { Skill } from "../../src/skill"
 import { SystemPrompt } from "../../src/session/system"
-import { Shell } from "../../src/shell/shell"
+import { Shell } from "../../src/shell"
 import { Snapshot } from "../../src/snapshot"
 import { ToolRegistry } from "../../src/tool"
 import { Truncate } from "../../src/tool"

+ 1 - 1
packages/opencode/test/session/snapshot-tool-race.test.ts

@@ -29,7 +29,7 @@ import { TestLLMServer } from "../lib/llm-server"
 
 // Same layer setup as prompt-effect.test.ts
 import { NodeFileSystem } from "@effect/platform-node"
-import { Agent as AgentSvc } from "../../src/agent/agent"
+import { Agent as AgentSvc } from "../../src/agent"
 import { Bus } from "../../src/bus"
 import { Command } from "../../src/command"
 import { Config } from "../../src/config"

+ 1 - 1
packages/opencode/test/session/system.test.ts

@@ -1,7 +1,7 @@
 import { describe, expect, test } from "bun:test"
 import path from "path"
 import { Effect } from "effect"
-import { Agent } from "../../src/agent/agent"
+import { Agent } from "../../src/agent"
 import { Instance } from "../../src/project/instance"
 import { SystemPrompt } from "../../src/session/system"
 import { provideInstance, tmpdir } from "../fixture/fixture"

+ 1 - 1
packages/opencode/test/shell/shell.test.ts

@@ -1,6 +1,6 @@
 import { describe, expect, test } from "bun:test"
 import path from "path"
-import { Shell } from "../../src/shell/shell"
+import { Shell } from "../../src/shell"
 import { Filesystem } from "../../src/util"
 
 const withShell = async (shell: string | undefined, fn: () => void | Promise<void>) => {

+ 1 - 1
packages/opencode/test/sync/index.test.ts

@@ -6,7 +6,7 @@ import { Instance } from "../../src/project/instance"
 import { SyncEvent } from "../../src/sync"
 import { Database } from "../../src/storage"
 import { EventTable } from "../../src/sync/event.sql"
-import { Identifier } from "../../src/id/id"
+import { Identifier } from "../../src/id"
 import { Flag } from "../../src/flag/flag"
 import { initProjectors } from "../../src/server/projectors"
 

+ 1 - 1
packages/opencode/test/tool/apply_patch.test.ts

@@ -7,7 +7,7 @@ import { Instance } from "../../src/project/instance"
 import { LSP } from "../../src/lsp"
 import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { Format } from "../../src/format"
-import { Agent } from "../../src/agent/agent"
+import { Agent } from "../../src/agent"
 import { Bus } from "../../src/bus"
 import { Truncate } from "../../src/tool"
 import { tmpdir } from "../fixture/fixture"

+ 2 - 2
packages/opencode/test/tool/bash.test.ts

@@ -2,13 +2,13 @@ import { describe, expect, test } from "bun:test"
 import { Effect, Layer, ManagedRuntime } from "effect"
 import os from "os"
 import path from "path"
-import { Shell } from "../../src/shell/shell"
+import { Shell } from "../../src/shell"
 import { BashTool } from "../../src/tool/bash"
 import { Instance } from "../../src/project/instance"
 import { Filesystem } from "../../src/util"
 import { tmpdir } from "../fixture/fixture"
 import type { Permission } from "../../src/permission"
-import { Agent } from "../../src/agent/agent"
+import { Agent } from "../../src/agent"
 import { Truncate } from "../../src/tool"
 import { SessionID, MessageID } from "../../src/session/schema"
 import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"

+ 1 - 1
packages/opencode/test/tool/edit.test.ts

@@ -9,7 +9,7 @@ import { FileTime } from "../../src/file/time"
 import { LSP } from "../../src/lsp"
 import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { Format } from "../../src/format"
-import { Agent } from "../../src/agent/agent"
+import { Agent } from "../../src/agent"
 import { Bus } from "../../src/bus"
 import { BusEvent } from "../../src/bus/bus-event"
 import { Truncate } from "../../src/tool"

+ 1 - 1
packages/opencode/test/tool/glob.test.ts

@@ -7,7 +7,7 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
 import { Ripgrep } from "../../src/file/ripgrep"
 import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { Truncate } from "../../src/tool"
-import { Agent } from "../../src/agent/agent"
+import { Agent } from "../../src/agent"
 import { provideTmpdirInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 

+ 1 - 1
packages/opencode/test/tool/grep.test.ts

@@ -6,7 +6,7 @@ import { provideInstance, provideTmpdirInstance } from "../fixture/fixture"
 import { SessionID, MessageID } from "../../src/session/schema"
 import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
 import { Truncate } from "../../src/tool"
-import { Agent } from "../../src/agent/agent"
+import { Agent } from "../../src/agent"
 import { Ripgrep } from "../../src/file/ripgrep"
 import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { testEffect } from "../lib/effect"

+ 1 - 1
packages/opencode/test/tool/question.test.ts

@@ -3,7 +3,7 @@ import { Effect, Fiber, Layer } from "effect"
 import { QuestionTool } from "../../src/tool/question"
 import { Question } from "../../src/question"
 import { SessionID, MessageID } from "../../src/session/schema"
-import { Agent } from "../../src/agent/agent"
+import { Agent } from "../../src/agent"
 import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
 import { Truncate } from "../../src/tool"
 import { provideTmpdirInstance } from "../fixture/fixture"

+ 1 - 1
packages/opencode/test/tool/read.test.ts

@@ -1,7 +1,7 @@
 import { afterEach, describe, expect } from "bun:test"
 import { Cause, Effect, Exit, Layer } from "effect"
 import path from "path"
-import { Agent } from "../../src/agent/agent"
+import { Agent } from "../../src/agent"
 import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
 import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { FileTime } from "../../src/file/time"

+ 1 - 1
packages/opencode/test/tool/task.test.ts

@@ -1,6 +1,6 @@
 import { afterEach, describe, expect } from "bun:test"
 import { Effect, Layer } from "effect"
-import { Agent } from "../../src/agent/agent"
+import { Agent } from "../../src/agent"
 import { Config } from "../../src/config"
 import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
 import { Instance } from "../../src/project/instance"

+ 1 - 1
packages/opencode/test/tool/tool-define.test.ts

@@ -1,7 +1,7 @@
 import { describe, test, expect } from "bun:test"
 import { Effect, Layer, ManagedRuntime } from "effect"
 import z from "zod"
-import { Agent } from "../../src/agent/agent"
+import { Agent } from "../../src/agent"
 import { Tool } from "../../src/tool"
 import { Truncate } from "../../src/tool"
 

+ 1 - 1
packages/opencode/test/tool/truncation.test.ts

@@ -2,7 +2,7 @@ import { describe, test, expect } from "bun:test"
 import { NodeFileSystem } from "@effect/platform-node"
 import { Effect, FileSystem, Layer } from "effect"
 import { Truncate } from "../../src/tool"
-import { Identifier } from "../../src/id/id"
+import { Identifier } from "../../src/id"
 import { Process } from "../../src/util"
 import { Filesystem } from "../../src/util"
 import path from "path"

+ 1 - 1
packages/opencode/test/tool/webfetch.test.ts

@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
 import path from "path"
 import { Effect, Layer } from "effect"
 import { FetchHttpClient } from "effect/unstable/http"
-import { Agent } from "../../src/agent/agent"
+import { Agent } from "../../src/agent"
 import { Truncate } from "../../src/tool"
 import { Instance } from "../../src/project/instance"
 import { WebFetchTool } from "../../src/tool/webfetch"

+ 1 - 1
packages/opencode/test/tool/write.test.ts

@@ -11,7 +11,7 @@ import { Bus } from "../../src/bus"
 import { Format } from "../../src/format"
 import { Truncate } from "../../src/tool"
 import { Tool } from "../../src/tool"
-import { Agent } from "../../src/agent/agent"
+import { Agent } from "../../src/agent"
 import { SessionID, MessageID } from "../../src/session/schema"
 import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
 import { provideTmpdirInstance } from "../fixture/fixture"