Project workspaces overview

/home/jm/data/code/opencode/packages/opencode · 13 messages · 62 parts · · 60,863 tokens
Created Mar 12, 04:50:54 AM · Updated Mar 12, 08:52:31 AM · 1 syncs
userMar 12, 04:55:19 AM

Describe the workspaces feature recently introduced in this project

assistantMar 12, 04:55:19 AM
openrouter/healer-alpha · openrouter
The user wants to understand the workspaces feature recently introduced in this project. I need to explore the codebase to find information about workspaces. Let me start by searching for relevant files and code patterns. I'll use parallel tools to gather information efficiently. First, I'll search for workspace-related files and code. Let me start with a glob search for workspace files and a grep for workspace-related terms.
globcompleted
packages/opencode
Output
/home/jm/data/code/opencode/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-list.tsx
/home/jm/data/code/opencode/packages/opencode/test/control-plane/workspace-sync.test.ts
/home/jm/data/code/opencode/packages/opencode/src/control-plane/workspace.ts
/home/jm/data/code/opencode/packages/opencode/src/control-plane/workspace.sql.ts
/home/jm/data/code/opencode/packages/opencode/src/control-plane/workspace-router-middleware.ts
/home/jm/data/code/opencode/packages/opencode/src/control-plane/workspace-context.ts
/home/jm/data/code/opencode/packages/opencode/test/control-plane/workspace-server-sse.test.ts
/home/jm/data/code/opencode/packages/opencode/src/server/routes/workspace.ts
/home/jm/data/code/opencode/packages/opencode/src/cli/cmd/workspace-serve.ts
grepcompleted
workspace
Output
Found 131 matches (showing first 100)
/home/jm/data/code/opencode/packages/opencode/src/project/project.ts:
  Line 49:           start: z.string().optional().describe("Startup script to run when creating a new workspace (worktree)"),

/home/jm/data/code/opencode/packages/opencode/src/cli/cmd/tui/worker.ts:
  Line 47: const startEventStream = (input: { directory: string; workspaceID?: string }) => {
  Line 63:     experimental_workspaceID: input.workspaceID,
  Line 139:   async setWorkspace(input: { workspaceID?: string }) {
  Line 140:     startEventStream({ directory: process.cwd(), workspaceID: input.workspaceID })

/home/jm/data/code/opencode/packages/opencode/src/cli/cmd/tui/thread.ts:
  Line 45:     setWorkspace: (workspaceID) => {
  Line 46:       void client.call("setWorkspace", { workspaceID })

/home/jm/data/code/opencode/packages/opencode/test/control-plane/workspace-sync.test.ts:
  Line 7: import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
  Line 49: describe("control-plane/workspace.startSyncing", () => {
  Line 50:   test("syncs only remote workspaces and emits remote SSE events", async () => {
  Line 51:     const { Workspace } = await import("../../src/control-plane/workspace")
  Line 55:     const id1 = Identifier.descending("workspace")
  Line 56:     const id2 = Identifier.descending("workspace")

/home/jm/data/code/opencode/packages/opencode/test/control-plane/session-proxy-middleware.test.ts:
  Line 6: import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
  Line 8: import { WorkspaceContext } from "../../src/control-plane/workspace-context"
  Line 20:   workspace?: "first" | "second"
  Line 40:           : new URL(input, "http://workspace.test").toString()
  Line 57:   const id1 = Identifier.descending("workspace")
  Line 58:   const id2 = Identifier.descending("workspace")
  Line 83:   const { WorkspaceRouterMiddleware } = await import("../../src/control-plane/workspace-router-middleware")
  Line 95:         ...
grepcompleted
Workspace
Output
Found 81 matches
/home/jm/data/code/opencode/packages/opencode/src/cli/cmd/tui/thread.ts:
  Line 45:     setWorkspace: (workspaceID) => {
  Line 46:       void client.call("setWorkspace", { workspaceID })

/home/jm/data/code/opencode/packages/opencode/src/cli/cmd/tui/worker.ts:
  Line 139:   async setWorkspace(input: { workspaceID?: string }) {

/home/jm/data/code/opencode/packages/opencode/test/control-plane/workspace-sync.test.ts:
  Line 7: import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
  Line 20: const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert
  Line 51:     const { Workspace } = await import("../../src/control-plane/workspace")
  Line 60:         .insert(WorkspaceTable)
  Line 91:     const sync = Workspace.startSyncing(project)

/home/jm/data/code/opencode/packages/opencode/test/control-plane/session-proxy-middleware.test.ts:
  Line 6: import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
  Line 8: import { WorkspaceContext } from "../../src/control-plane/workspace-context"
  Line 24: const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert
  Line 62:       .insert(WorkspaceTable)
  Line 83:   const { WorkspaceRouterMiddleware } = await import("../../src/control-plane/workspace-router-middleware")
  Line 84:   const app = new Hono().use(WorkspaceRouterMiddleware)
  Line 94:           WorkspaceContext.provide({

/home/jm/data/code/opencode/packages/opencode/src/index.ts:
  Line 16: import { WorkspaceServeCommand } from "./cli/cmd/workspace-serve"
  Line 153:   cli = cli.command(WorkspaceServeCommand)

/home/jm/data/code/opencode/packages/opencode/src/storage/schema.ts:
  Line 5: export { WorkspaceTable } from "../control-plane/workspace.sql"

/home/jm/data/code/opencode/packages/opencode/src/session/index.ts:
  Line 25: import { WorkspaceContext } from "../control-plane/workspace-context"
  Line 304:       workspaceID: Works...
13,116in / 166out / 109reasoning
assistantMar 12, 04:55:29 AM
openrouter/healer-alpha · openrouter
I see there are workspace-related files. Let me read some of the key files to understand the feature.
readcompleted
packages/opencode/src/control-plane/workspace.ts
Output
<path>/home/jm/data/code/opencode/packages/opencode/src/control-plane/workspace.ts</path>
<type>file</type>
<content>1: import z from "zod"
2: import { Identifier } from "@/id/id"
3: import { fn } from "@/util/fn"
4: import { Database, eq } from "@/storage/db"
5: import { Project } from "@/project/project"
6: import { BusEvent } from "@/bus/bus-event"
7: import { GlobalBus } from "@/bus/global"
8: import { Log } from "@/util/log"
9: import { WorkspaceTable } from "./workspace.sql"
10: import { getAdaptor } from "./adaptors"
11: import { WorkspaceInfo } from "./types"
12: import { parseSSE } from "./sse"
13: 
14: export namespace Workspace {
15:   export const Event = {
16:     Ready: BusEvent.define(
17:       "workspace.ready",
18:       z.object({
19:         name: z.string(),
20:       }),
21:     ),
22:     Failed: BusEvent.define(
23:       "workspace.failed",
24:       z.object({
25:         message: z.string(),
26:       }),
27:     ),
28:   }
29: 
30:   export const Info = WorkspaceInfo.meta({
31:     ref: "Workspace",
32:   })
33:   export type Info = z.infer<typeof Info>
34: 
35:   function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
36:     return {
37:       id: row.id,
38:       type: row.type,
39:       branch: row.branch,
40:       name: row.name,
41:       directory: row.directory,
42:       extra: row.extra,
43:       projectID: row.project_id,
44:     }
45:   }
46: 
47:   const CreateInput = z.object({
48:     id: Identifier.schema("workspace").optional(),
49:     type: Info.shape.type,
50:     branch: Info.shape.branch,
51:     projectID: Info.shape.projectID,
52:     extra: Info.shape.extra,
53:   })
54: 
55:   export const create = fn(CreateInput, async (input) => {
56:     const id = Identifier.ascending("workspace", input.id)
57:     const adaptor = await getAdaptor(input.type)
58: 
59:     const config = await adaptor.configure({ ...input, id, name: null, directory: null })
60: 
61:     const info: Info = {
62:       id,
63:     ...
readcompleted
packages/opencode/src/control-plane/types.ts
Output
<path>/home/jm/data/code/opencode/packages/opencode/src/control-plane/types.ts</path>
<type>file</type>
<content>1: import z from "zod"
2: import { Identifier } from "@/id/id"
3: 
4: export const WorkspaceInfo = z.object({
5:   id: Identifier.schema("workspace"),
6:   type: z.string(),
7:   branch: z.string().nullable(),
8:   name: z.string().nullable(),
9:   directory: z.string().nullable(),
10:   extra: z.unknown().nullable(),
11:   projectID: z.string(),
12: })
13: export type WorkspaceInfo = z.infer<typeof WorkspaceInfo>
14: 
15: export type Adaptor = {
16:   configure(input: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
17:   create(input: WorkspaceInfo, from?: WorkspaceInfo): Promise<void>
18:   remove(config: WorkspaceInfo): Promise<void>
19:   fetch(config: WorkspaceInfo, input: RequestInfo | URL, init?: RequestInit): Promise<Response>
20: }

(End of file - total 20 lines)
</content>
readcompleted
packages/opencode/src/control-plane/workspace.sql.ts
Output
<path>/home/jm/data/code/opencode/packages/opencode/src/control-plane/workspace.sql.ts</path>
<type>file</type>
<content>1: import { sqliteTable, text } from "drizzle-orm/sqlite-core"
2: import { ProjectTable } from "@/project/project.sql"
3: 
4: export const WorkspaceTable = sqliteTable("workspace", {
5:   id: text().primaryKey(),
6:   type: text().notNull(),
7:   branch: text(),
8:   name: text(),
9:   directory: text(),
10:   extra: text({ mode: "json" }),
11:   project_id: text()
12:     .notNull()
13:     .references(() => ProjectTable.id, { onDelete: "cascade" }),
14: })

(End of file - total 14 lines)
</content>
readcompleted
packages/opencode/src/server/routes/workspace.ts
Output
<path>/home/jm/data/code/opencode/packages/opencode/src/server/routes/workspace.ts</path>
<type>file</type>
<content>1: import { Hono } from "hono"
2: import { describeRoute, resolver, validator } from "hono-openapi"
3: import z from "zod"
4: import { Workspace } from "../../control-plane/workspace"
5: import { Instance } from "../../project/instance"
6: import { errors } from "../error"
7: import { lazy } from "../../util/lazy"
8: 
9: export const WorkspaceRoutes = lazy(() =>
10:   new Hono()
11:     .post(
12:       "/",
13:       describeRoute({
14:         summary: "Create workspace",
15:         description: "Create a workspace for the current project.",
16:         operationId: "experimental.workspace.create",
17:         responses: {
18:           200: {
19:             description: "Workspace created",
20:             content: {
21:               "application/json": {
22:                 schema: resolver(Workspace.Info),
23:               },
24:             },
25:           },
26:           ...errors(400),
27:         },
28:       }),
29:       validator(
30:         "json",
31:         Workspace.create.schema.omit({
32:           projectID: true,
33:         }),
34:       ),
35:       async (c) => {
36:         const body = c.req.valid("json")
37:         const workspace = await Workspace.create({
38:           projectID: Instance.project.id,
39:           ...body,
40:         })
41:         return c.json(workspace)
42:       },
43:     )
44:     .get(
45:       "/",
46:       describeRoute({
47:         summary: "List workspaces",
48:         description: "List all workspaces.",
49:         operationId: "experimental.workspace.list",
50:         responses: {
51:           200: {
52:             description: "Workspaces",
53:             content: {
54:               "application/json": {
55:                 schema: resolver(z.array(Workspace.Info)),
56:               },
57:             },
58:           },
59:         },
60:       }),
61:       async (c) => ...
4,259in / 157out / 26reasoning
assistantMar 12, 04:55:38 AM
openrouter/healer-alpha · openrouter
Now I have a good understanding of the workspaces feature. Let me also look at the adaptors to understand the different workspace types, and the workspace server to understand the remote workspace functionality.
readcompleted
packages/opencode/src/control-plane/adaptors
Output
<path>/home/jm/data/code/opencode/packages/opencode/src/control-plane/adaptors</path>
<type>directory</type>
<entries>
index.ts
worktree.ts

(2 entries)
</entries>
readcompleted
packages/opencode/src/control-plane/workspace-server/server.ts
Output
<path>/home/jm/data/code/opencode/packages/opencode/src/control-plane/workspace-server/server.ts</path>
<type>file</type>
<content>1: import { Hono } from "hono"
2: import { Instance } from "../../project/instance"
3: import { InstanceBootstrap } from "../../project/bootstrap"
4: import { SessionRoutes } from "../../server/routes/session"
5: import { WorkspaceServerRoutes } from "./routes"
6: import { WorkspaceContext } from "../workspace-context"
7: 
8: export namespace WorkspaceServer {
9:   export function App() {
10:     const session = new Hono()
11:       .use(async (c, next) => {
12:         // Right now, we need handle all requests because we don't
13:         // have syncing. In the future all GET requests will handled
14:         // by the control plane
15:         //
16:         // if (c.req.method === "GET") return c.notFound()
17:         await next()
18:       })
19:       .route("/", SessionRoutes())
20: 
21:     return new Hono()
22:       .use(async (c, next) => {
23:         const workspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace")
24:         const raw = c.req.query("directory") || c.req.header("x-opencode-directory")
25:         if (workspaceID == null) {
26:           throw new Error("workspaceID parameter is required")
27:         }
28:         if (raw == null) {
29:           throw new Error("directory parameter is required")
30:         }
31: 
32:         const directory = (() => {
33:           try {
34:             return decodeURIComponent(raw)
35:           } catch {
36:             return raw
37:           }
38:         })()
39: 
40:         return WorkspaceContext.provide({
41:           workspaceID,
42:           async fn() {
43:             return Instance.provide({
44:               directory,
45:               init: InstanceBootstrap,
46:               async fn() {
47:                 return next()
48:               },
49:             })
50:           },
51:         })
52:       })
53:       .route("/sessi...
readcompleted
packages/opencode/src/control-plane/workspace-router-middleware.ts
Output
<path>/home/jm/data/code/opencode/packages/opencode/src/control-plane/workspace-router-middleware.ts</path>
<type>file</type>
<content>1: import { Instance } from "@/project/instance"
2: import type { MiddlewareHandler } from "hono"
3: import { Installation } from "../installation"
4: import { getAdaptor } from "./adaptors"
5: import { Workspace } from "./workspace"
6: import { WorkspaceContext } from "./workspace-context"
7: 
8: // This middleware forwards all non-GET requests if the workspace is a
9: // remote. The remote workspace needs to handle session mutations
10: async function routeRequest(req: Request) {
11:   // Right now, we need to forward all requests to the workspace
12:   // because we don't have syncing. In the future all GET requests
13:   // which don't mutate anything will be handled locally
14:   //
15:   // if (req.method === "GET") return
16: 
17:   if (!WorkspaceContext.workspaceID) return
18: 
19:   const workspace = await Workspace.get(WorkspaceContext.workspaceID)
20:   if (!workspace) {
21:     return new Response(`Workspace not found: ${WorkspaceContext.workspaceID}`, {
22:       status: 500,
23:       headers: {
24:         "content-type": "text/plain; charset=utf-8",
25:       },
26:     })
27:   }
28: 
29:   const adaptor = await getAdaptor(workspace.type)
30: 
31:   return adaptor.fetch(workspace, `${new URL(req.url).pathname}${new URL(req.url).search}`, {
32:     method: req.method,
33:     body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer(),
34:     signal: req.signal,
35:     headers: req.headers,
36:   })
37: }
38: 
39: export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c, next) => {
40:   // Only available in development for now
41:   if (!Installation.isLocal()) {
42:     return next()
43:   }
44: 
45:   const response = await routeRequest(c.req.raw)
46:   if (response) {
47:     return response
48:   }
49:   return next()
50: }

(End of file - total 50 lines)
</content>
3,030in / 145out / 53reasoning
assistantMar 12, 04:55:50 AM
openrouter/healer-alpha · openrouter
readcompleted
packages/opencode/src/control-plane/adaptors/worktree.ts
Output
<path>/home/jm/data/code/opencode/packages/opencode/src/control-plane/adaptors/worktree.ts</path>
<type>file</type>
<content>1: import z from "zod"
2: import { Worktree } from "@/worktree"
3: import { type Adaptor, WorkspaceInfo } from "../types"
4: 
5: const Config = WorkspaceInfo.extend({
6:   name: WorkspaceInfo.shape.name.unwrap(),
7:   branch: WorkspaceInfo.shape.branch.unwrap(),
8:   directory: WorkspaceInfo.shape.directory.unwrap(),
9: })
10: 
11: type Config = z.infer<typeof Config>
12: 
13: export const WorktreeAdaptor: Adaptor = {
14:   async configure(info) {
15:     const worktree = await Worktree.makeWorktreeInfo(info.name ?? undefined)
16:     return {
17:       ...info,
18:       name: worktree.name,
19:       branch: worktree.branch,
20:       directory: worktree.directory,
21:     }
22:   },
23:   async create(info) {
24:     const config = Config.parse(info)
25:     const bootstrap = await Worktree.createFromInfo({
26:       name: config.name,
27:       directory: config.directory,
28:       branch: config.branch,
29:     })
30:     return bootstrap()
31:   },
32:   async remove(info) {
33:     const config = Config.parse(info)
34:     await Worktree.remove({ directory: config.directory })
35:   },
36:   async fetch(info, input: RequestInfo | URL, init?: RequestInit) {
37:     const config = Config.parse(info)
38:     const { WorkspaceServer } = await import("../workspace-server/server")
39:     const url = input instanceof Request || input instanceof URL ? input : new URL(input, "http://opencode.internal")
40:     const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined))
41:     headers.set("x-opencode-directory", config.directory)
42: 
43:     const request = new Request(url, { ...init, headers })
44:     return WorkspaceServer.App().fetch(request)
45:   },
46: }

(End of file - total 46 lines)
</content>
readcompleted
packages/opencode/src/control-plane/adaptors/index.ts
Output
<path>/home/jm/data/code/opencode/packages/opencode/src/control-plane/adaptors/index.ts</path>
<type>file</type>
<content>1: import { lazy } from "@/util/lazy"
2: import type { Adaptor } from "../types"
3: 
4: const ADAPTORS: Record<string, () => Promise<Adaptor>> = {
5:   worktree: lazy(async () => (await import("./worktree")).WorktreeAdaptor),
6: }
7: 
8: export function getAdaptor(type: string): Promise<Adaptor> {
9:   return ADAPTORS[type]()
10: }
11: 
12: export function installAdaptor(type: string, adaptor: Adaptor) {
13:   // This is experimental: mostly used for testing right now, but we
14:   // will likely allow this in the future. Need to figure out the
15:   // TypeScript story
16: 
17:   // @ts-expect-error we force the builtin types right now, but we
18:   // will implement a way to extend the types for custom adaptors
19:   ADAPTORS[type] = () => adaptor
20: }

(End of file - total 20 lines)
</content>
readcompleted
packages/opencode/src/control-plane/workspace-server/routes.ts
Output
<path>/home/jm/data/code/opencode/packages/opencode/src/control-plane/workspace-server/routes.ts</path>
<type>file</type>
<content>1: import { GlobalBus } from "../../bus/global"
2: import { Hono } from "hono"
3: import { streamSSE } from "hono/streaming"
4: 
5: export function WorkspaceServerRoutes() {
6:   return new Hono().get("/event", async (c) => {
7:     c.header("X-Accel-Buffering", "no")
8:     c.header("X-Content-Type-Options", "nosniff")
9:     return streamSSE(c, async (stream) => {
10:       const send = async (event: unknown) => {
11:         await stream.writeSSE({
12:           data: JSON.stringify(event),
13:         })
14:       }
15:       const handler = async (event: { directory?: string; payload: unknown }) => {
16:         await send(event.payload)
17:       }
18:       GlobalBus.on("event", handler)
19:       await send({ type: "server.connected", properties: {} })
20:       const heartbeat = setInterval(() => {
21:         void send({ type: "server.heartbeat", properties: {} })
22:       }, 10_000)
23: 
24:       await new Promise<void>((resolve) => {
25:         stream.onAbort(() => {
26:           clearInterval(heartbeat)
27:           GlobalBus.off("event", handler)
28:           resolve()
29:         })
30:       })
31:     })
32:   })
33: }

(End of file - total 33 lines)
</content>
21,778in / 108out
assistantMar 12, 04:55:52 AM
openrouter/healer-alpha · openrouter
现在我已经收集了足够的信息来描述工作空间功能。让我整理一下: 1. 工作空间是一个实验性功能,位于控制平面(control-plane)模块中 2. 主要文件: - `src/control-plane/workspace.ts` - 核心工作空间逻辑 - `src/control-plane/workspace.sql.ts` - 数据库表定义 - `src/control-plane/types.ts` - 类型定义 - `src/control-plane/workspace-context.ts` - 上下文管理 - `src/control-plane/workspace-router-middleware.ts` - 请求路由中间件 - `src/control-plane/workspace-server/server.ts` - 工作空间服务器 - `src/control-plane/adaptors/` - 适配器(目前只有worktree适配器) 3. API路由:`src/server/routes/workspace.ts` 提供了创建、列表、删除工作空间的REST端点 4. CLI命令:`src/cli/cmd/workspace-serve.ts` 提供了工作空间服务命令 让我再查看一下工作空间的上下文和测试文件来了解更多细节。
readcompleted
packages/opencode/src/control-plane/workspace-context.ts
Output
<path>/home/jm/data/code/opencode/packages/opencode/src/control-plane/workspace-context.ts</path>
<type>file</type>
<content>1: import { Context } from "../util/context"
2: 
3: interface Context {
4:   workspaceID?: string
5: }
6: 
7: const context = Context.create<Context>("workspace")
8: 
9: export const WorkspaceContext = {
10:   async provide<R>(input: { workspaceID?: string; fn: () => R }): Promise<R> {
11:     return context.provide({ workspaceID: input.workspaceID }, async () => {
12:       return input.fn()
13:     })
14:   },
15: 
16:   get workspaceID() {
17:     try {
18:       return context.use().workspaceID
19:     } catch (e) {
20:       return undefined
21:     }
22:   },
23: }

(End of file - total 23 lines)
</content>
readcompleted
packages/opencode/test/control-plane/workspace-sync.test.ts
Output
<path>/home/jm/data/code/opencode/packages/opencode/test/control-plane/workspace-sync.test.ts</path>
<type>file</type>
<content>1: import { afterEach, describe, expect, mock, test } from "bun:test"
2: import { Identifier } from "../../src/id/id"
3: import { Log } from "../../src/util/log"
4: import { tmpdir } from "../fixture/fixture"
5: import { Project } from "../../src/project/project"
6: import { Database } from "../../src/storage/db"
7: import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
8: import { GlobalBus } from "../../src/bus/global"
9: import { resetDatabase } from "../fixture/db"
10: import * as adaptors from "../../src/control-plane/adaptors"
11: import type { Adaptor } from "../../src/control-plane/types"
12: 
13: afterEach(async () => {
14:   mock.restore()
15:   await resetDatabase()
16: })
17: 
18: Log.init({ print: false })
19: 
20: const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert
21: 
22: const TestAdaptor: Adaptor = {
23:   configure(config) {
24:     return config
25:   },
26:   async create() {
27:     throw new Error("not used")
28:   },
29:   async remove() {},
30:   async fetch(_config: unknown, _input: RequestInfo | URL, _init?: RequestInit) {
31:     const body = new ReadableStream<Uint8Array>({
32:       start(controller) {
33:         const encoder = new TextEncoder()
34:         controller.enqueue(encoder.encode('data: {"type":"remote.ready","properties":{}}\n\n'))
35:         controller.close()
36:       },
37:     })
38:     return new Response(body, {
39:       status: 200,
40:       headers: {
41:         "content-type": "text/event-stream",
42:       },
43:     })
44:   },
45: }
46: 
47: adaptors.installAdaptor("testing", TestAdaptor)
48: 
49: describe("control-plane/workspace.startSyncing", () => {
50:   test("syncs only remote workspaces and emits remote SSE events", async () => {
51:     const { Workspace } = await import("../../src/control-plane/workspace")...
readcompleted
packages/opencode/src/cli/cmd/workspace-serve.ts
Output
<path>/home/jm/data/code/opencode/packages/opencode/src/cli/cmd/workspace-serve.ts</path>
<type>file</type>
<content>1: import { cmd } from "./cmd"
2: import { withNetworkOptions, resolveNetworkOptions } from "../network"
3: import { WorkspaceServer } from "../../control-plane/workspace-server/server"
4: 
5: export const WorkspaceServeCommand = cmd({
6:   command: "workspace-serve",
7:   builder: (yargs) => withNetworkOptions(yargs),
8:   describe: "starts a remote workspace event server",
9:   handler: async (args) => {
10:     const opts = await resolveNetworkOptions(args)
11:     const server = WorkspaceServer.Listen(opts)
12:     console.log(`workspace event server listening on http://${server.hostname}:${server.port}/event`)
13:     await new Promise(() => {})
14:     await server.stop()
15:   },
16: })

(End of file - total 16 lines)
</content>
2,697in / 338out / 294reasoning
assistantMar 12, 04:55:59 AM
openrouter/healer-alpha · openrouter
Now I have a good understanding of the workspaces feature. Let me also check for any documentation or README files that might mention workspaces.
grepcompleted
workspace
Output
No files found
readcompleted
packages/opencode/src/session/index.ts
Output
<path>/home/jm/data/code/opencode/packages/opencode/src/session/index.ts</path>
<type>file</type>
<content>530:     },
531:   )
532: 
533:   export function* list(input?: {
534:     directory?: string
535:     workspaceID?: string
536:     roots?: boolean
537:     start?: number
538:     search?: string
539:     limit?: number
540:   }) {
541:     const project = Instance.project
542:     const conditions = [eq(SessionTable.project_id, project.id)]
543: 
544:     if (WorkspaceContext.workspaceID) {
545:       conditions.push(eq(SessionTable.workspace_id, WorkspaceContext.workspaceID))
546:     }
547:     if (input?.directory) {
548:       conditions.push(eq(SessionTable.directory, input.directory))
549:     }
550:     if (input?.roots) {
551:       conditions.push(isNull(SessionTable.parent_id))
552:     }
553:     if (input?.start) {
554:       conditions.push(gte(SessionTable.time_updated, input.start))
555:     }
556:     if (input?.search) {
557:       conditions.push(like(SessionTable.title, `%${input.search}%`))
558:     }
559: 

(Showing lines 530-559 of 886. Use offset=560 to continue.)
</content>
2,141in / 114out / 37reasoning
assistantMar 12, 04:56:05 AM
openrouter/healer-alpha · openrouter
现在我需要整理关于工作空间功能的描述。让我先总结一下工作空间功能的各个方面: 1. **数据库模型**:工作空间存储在 `workspace` 表中,包含 id、type、branch、name、directory、extra 和 project_id 字段。 2. **核心功能**: - 创建工作空间(`Workspace.create`) - 列出项目中的所有工作空间(`Workspace.list`) - 获取单个工作空间信息(`Workspace.get`) - 删除工作空间(`Workspace.remove`) - 同步工作空间事件(`Workspace.startSyncing`) 3. **适配器系统**:工作空间有适配器接口,目前实现了工作树(worktree)适配器。 4. **API 端点**:在 `/experimental/workspace` 路由下提供 POST、GET、DELETE 端点。 5. **事件同步**:工作空间可以同步远程事件,通过 SSE(Server-Sent Events)进行实时更新。 6. **上下文管理**:工作空间上下文在请求处理中传递工作空间 ID。 7. **路由中间件**:工作空间路由中间件可以将请求转发到远程工作空间。 让我再看看是否有更多细节:
readcompleted
packages/opencode/src/worktree/index.ts
Output
<path>/home/jm/data/code/opencode/packages/opencode/src/worktree/index.ts</path>
<type>file</type>
<content>530:     }
531: 
532:     const directory = await canonical(input.directory)
533:     const primary = await canonical(Instance.worktree)
534:     if (directory === primary) {
535:       throw new ResetFailedError({ message: "Cannot reset the primary workspace" })
536:     }
537: 
538:     const list = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree)
539:     if (list.exitCode !== 0) {
540:       throw new ResetFailedError({ message: errorText(list) || "Failed to read git worktrees" })
541:     }
542: 
543:     const lines = outputText(list.stdout)
544:       .split("\n")
545:       .map((line) => line.trim())
546:     const entries = lines.reduce<{ path?: string; branch?: string }[]>((acc, line) => {
547:       if (!line) return acc
548:       if (line.startsWith("worktree ")) {
549:         acc.push({ path: line.slice("worktree ".length).trim() })
550:         return acc
551:       }
552:       const current = acc[acc.length - 1]
553:       if (!current) return acc
554:       if (line.startsWith("branch ")) {
555:         current.branch = line.slice("branch ".length).trim()
556:       }
557:       return acc
558:     }, [])
559: 
560:     const entry = await (async () => {
561:       for (const item of entries) {
562:         if (!item.path) continue
563:         const key = await canonical(item.path)
564:         if (key === directory) return item
565:       }
566:     })()
567:     if (!entry?.path) {
568:       throw new ResetFailedError({ message: "Worktree not found" })
569:     }
570: 
571:     const remoteList = await $`git remote`.quiet().nothrow().cwd(Instance.worktree)
572:     if (remoteList.exitCode !== 0) {
573:       throw new ResetFailedError({ message: errorText(remoteList) || "Failed to list git remotes" })
574:     }
575: 
576:     const remotes = outputText(remoteList.stdout)
577:       .split("\n")
578:      ...
469in / 323out / 362reasoning
assistantMar 12, 04:56:11 AM
openrouter/healer-alpha · openrouter
Let me also check if there's any documentation or changelog that mentions workspaces.

Based on my analysis of the codebase, here's a description of the workspaces feature recently introduced in the OpenCode project:


Workspaces Feature Overview

Workspaces are an experimental feature that enables creating isolated environments (currently using Git worktrees) for a project. Each workspace has its own session context, allowing multiple concurrent development contexts within the same project.

Core Architecture

The feature is organized under src/control-plane/ with these key components:

Component Purpose
workspace.ts CRUD operations, event syncing, lifecycle management
workspace.sql.ts Drizzle schema (workspace table)
workspace-context.ts Async context for workspace-scoped operations
workspace-router-middleware.ts Forwards requests to remote workspaces
workspace-server/ Standalone SSE server for remote workspace events
adaptors/ Pluggable backend implementations (currently worktree)
types.ts WorkspaceInfo schema and Adaptor interface

Data Model

// workspace.sql.ts
WorkspaceTable = sqliteTable("workspace", {
  id: text().primaryKey(),
  type: text().notNull(),           // "worktree" currently
  branch: text(),
  name: text(),
  directory: text(),
  extra: text({ mode: "json" }),    // extensible metadata
  project_id: text().references(() => ProjectTable.id, { onDelete: "cascade" }),
})

API Routes (experimental)

Method Path Operation ID
POST /experimental/workspace/ experimental.workspace.create
GET /experimental/workspace/ experimental.workspace.list
DELETE /experimental/workspace/:id experimental.workspace.remove

Adaptor System

The Adaptor interface allows pluggable workspace backends:

type Adaptor = {
  configure(input: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
  create(input: WorkspaceInfo, from?: WorkspaceInfo): Promise<void>
  remove(config: WorkspaceInfo): Promise<void>
  fetch(config: WorkspaceInfo, input: RequestInfo | URL, init?: RequestInit): Promise<Response>
}

Currently only worktree is implemented, which uses git worktree to create isolated directory checkouts. The system supports installing custom adaptors via installAdaptor() (used in testing).

Key Behaviors

  1. Session Scoping: Sessions can be filtered by workspace_id; the WorkspaceContext provides the current workspace ID within request scope.

  2. Remote Syncing: Workspace.startSyncing() connects to remote workspace servers via SSE to receive events. Only non-worktree workspaces are synced (line 135: .filter((space) => space.type !== "worktree")).

  3. Request Forwarding: WorkspaceRouterMiddleware forwards all requests to remote workspaces (currently both GET and mutating; comments indicate future plan to handle GET locally).

  4. Workspace Server: A standalone server (workspace-serve CLI command) that streams events via SSE at /event, used by remote workspaces to push state changes back to the control plane.

Integration Points

  • TUI: setWorkspace() method in the worker allows switching workspace context
  • Sessions: Session.list() filters by WorkspaceContext.workspaceID
  • Worktree module: Validates against resetting the primary workspace

Current Limitations (noted in code comments)

  • All requests forwarded to remotes (future: GET handled locally)
  • Only worktree adaptor implemented
  • Experimental API (marked in operation IDs)
  • Development-only middleware (Installation.isLocal() check)

CLI Command

opencode workspace-serve --hostname <host> --port <port>
# Starts the remote workspace event server
752in / 847out / 22reasoning
userMar 12, 04:59:16 AM

Can you use the @researcher, @explore, and/or any other agents nessecary to describe how the git workspace and changes could be managed from a cloudflare worker/durable object type configuration, performing or tracking these workspace diffs dynamically against a remote self hosted git repository and git daemon

Use the above message and context to generate a prompt and call the task tool with subagent: researcher

Use the above message and context to generate a prompt and call the task tool with subagent: explore

assistantMar 12, 04:59:16 AM
openrouter/healer-alpha · openrouter
The user wants me to use the researcher and explore agents to investigate how git workspaces could be managed from a Cloudflare Worker/Durable Object configuration, tracking workspace diffs dynamically against a remote self-hosted git repository and git daemon. First, I need to understand the context better by looking at the existing codebase structure, especially the cloudflare-related code and any existing git/workspace integration. Then I'll craft prompts for the agents. Let me start by exploring the codebase to understand the current architecture and then launch the agents with appropriate prompts.
taskcompleted
Explore Cloudflare and git workspace integration
Output
task_id: ses_31f95d8f5ffedxP36XPbbRdOvB (for resuming to continue this task if needed)

<task_result>
基于对 OpenCode 代码库的深入探索,我为您提供一个全面的架构分析摘要:

## 1. 当前 Cloudflare 集成点

**已存在的集成:**
- **Cloudflare AI Gateway**:在 `/src/provider/provider.ts` 中实现了 AI 提供商集成,支持通过环境变量配置 `CLOUDFLARE_GATEWAY_ID`、`CLOUDFLARE_ACCOUNT_ID` 和 `CLOUDFLARE_API_TOKEN`
- **Cloudflare Workers AI**:同一文件中集成了 Cloudflare 的 AI 推理服务

**缺失的集成:**
- 没有 Cloudflare Workers 或 Durable Objects 的实现
- 没有 `wrangler.toml` 配置文件
- 没有 Cloudflare Pages 部署配置
- 没有边缘存储(KV、R2)或边缘数据库(D1)的使用

## 2. Git 工作区管理模式

**当前架构:**
- **工作区管理系统**(`src/worktree/index.ts`):
  - 支持创建、删除和重置 git 工作区
  - 自动生成随机工作区名称(形容词-名词组合)
  - 支持启动脚本执行
  - 与项目实例和引导系统集成

- **工作区适配器**(`src/control-plane/adaptors/worktree.ts`):
  - 实现 `Adaptor` 接口,支持 `configure`、`create`、`remove`、`fetch` 方法
  - 将工作区请求路由到本地工作区服务器
  - 支持远程工作区事件服务器

- **工作区路由中间件**(`src/control-plane/workspace-router-middleware.ts`):
  - 自动转发非 GET 请求到远程工作区
  - 处理工作区上下文和会话管理

- **远程工作区事件服务器**(`src/cli/cmd/workspace-serve.ts`):
  - 基于 Hono 的 HTTP 服务器
  - 支持网络配置选项
  - 提供工作区事件流

## 3. 实时同步机制

**SSE(Server-Sent Events)实现:**
- **SSE 解析器**(`src/control-plane/sse.ts`):
  - 解析 SSE 数据流,支持 `data`、`id`、`retry` 字段
  - 处理 JSON 和文本事件
  - 支持中断信号和流控制

- **全局事件总线**(`src/bus/global.ts`):
  - 基于 EventEmitter 的事件发布/订阅系统
  - 支持跨模块事件通信
  - 集成到工作区同步系统

- **工作区事件循环**(`src/control-plane/workspace.ts`):
  - 持续轮询远程工作区事件端点
  - 处理事件重连和错误恢复
  - 将事件路由到全局事件总线

- **实时心跳机制**:
  - 每 10 秒发送心跳事件
  - 每 30 秒主事件流心跳
  - 防止超时和连接中断

## 4. 会话和工作区生命周期管理

**会话管理**(`src/session/index.ts`):
- 完整的会话 CRUD 操作
- 支持父/子会话关系
- 会话状态跟踪(创建、更新、归档)
- 会话分享和权限控制

**工作区生命周期**(`src/control-plane/workspace.ts`):
- 工作区创建、列出、删除操作
- 工作区类型适配器模式
- 工作区事件同步
- 工作区配置管理

**项目管理**(`src/project/project.ts`):
- 项目发现和初始化
- Git VCS 集成
- 项目配置和命令管理
- 沙箱环境支持

## 5. 关键架构模式和抽象

**适配器模式**:
- 工作区类型通过适配器接口抽象
- 支持动态工作区类型扩展
- 隔离具体实现细节

**上下文模式**:
- 工作区上下文提供请求级状态
- 支持嵌套上下文提供
- 线程安全的状态管理

**事件驱动架构**:
- 全局事件总线实现松耦合通信
- 支持跨模块事件传播
- 工作区事件同步基于此模式

**数据库抽象**:
- SQLite 数据库(Drizzle ORM)
- 模式迁移管理
- 事...
938in / 344out / 156reasoning
assistantMar 12, 05:02:51 AM
openrouter/healer-alpha · openrouter
Now I need to call the researcher agent to do a deep dive on how to implement Cloudflare Workers/Durable Objects for dynamic git workspace management. Let me craft a detailed prompt based on the context we have.
taskcompleted
Research Cloudflare Workers git workspace
Output
task_id: ses_31f92c889ffe7Ok4yRZedhtn5i (for resuming to continue this task if needed)

<task_result>
Now let me provide a summary of my findings and recommendations:

# Dynamic Git Workspace Management with Cloudflare Workers + Durable Objects - Technical Analysis

Based on my deep research of the OpenCode codebase and Cloudflare platform capabilities, here's my comprehensive technical analysis:

## Key Findings

### 1. **Current OpenCode Architecture**
- Uses a pluggable `Adaptor` interface for workspace implementations
- Has `WorktreeAdaptor` for local git worktrees
- Uses SSE for real-time workspace events
- Has `WorkspaceRouterMiddleware` for routing requests to appropriate adaptors
- Simple database schema for workspace metadata

### 2. **Cloudflare Platform Capabilities**
- **Durable Objects**: Perfect for per-workspace state coordination (single-threaded, strongly consistent)
- **R2 Storage**: Ideal for git objects and large files (S3-compatible, no egress fees)
- **D1 Database**: Good for workspace metadata and diff tracking
- **Workers**: Edge compute with WebSocket hibernation for real-time updates
- **git-on-cloudflare**: Existing open-source implementation of Git Smart HTTP v2 on Cloudflare

## Recommended Architecture

### Hybrid Approach: Edge Coordination + Optional Git Daemon

```
┌─────────────────┐    ┌─────────────────────────────────┐
│   OpenCode      │    │      Cloudflare Platform        │
│   Client        │    │                                 │
│                 │    │  ┌─────────────────────────┐    │
│  ┌──────────┐   │    │  │    Entry Worker        │    │
│  │Workspace │   │    │  │  (Route/Proxy/Auth)    │    │
│  │Context   │───┼───►│  └──────────┬────────────┘    │
│  └──────────┘   │    │             │                  │
│                 │    │             ▼                  │
│  ┌──────────┐   │    │  ┌─────────────────────────┐    │
│  │Adaptor   │   │    │  │  Durable Object         │    │
│  │Interface │   │    │  │ (Per-works...
1,754in / 578out / 53reasoning
assistantMar 12, 05:09:43 AM
openrouter/healer-alpha · openrouter
Now I have comprehensive information from both the explore and researcher agents. Let me synthesize this into a clear, actionable design specification for implementing dynamic git workspace management using Cloudflare Workers and Durable Objects, building on the existing OpenCode architecture. Let me create a comprehensive technical design document that: 1. Builds on the existing workspace adaptor pattern 2. Proposes Cloudflare Workers/Durable Objects integration 3. Addresses dynamic git workspace management 4. Covers diff tracking against a remote git repository 5. Provides implementation guidance This should be a design spec that can be used to plan the implementation.

Based on my research of the codebase and Cloudflare platform capabilities, here's a comprehensive technical analysis for implementing dynamic git workspace management using Cloudflare Workers and Durable Objects.


Dynamic Git Workspace Management with Cloudflare Workers + Durable Objects

Architecture Overview

The proposed architecture extends OpenCode's existing Adaptor pattern to support cloud-native git workspace management while maintaining compatibility with the current local worktree implementation.

┌─────────────────────────────────────────────────────────────────────┐
│                        OpenCode Client                              │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  WorkspaceContext  │  Adaptor Interface  │  SSE Events        │  │
│  └────────────────────┴─────────────────────┴────────────────────┘  │
└──────────────────────────────┬──────────────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────────────┐
│                      Cloudflare Edge                                │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │              Entry Worker (Router/Auth)                       │  │
│  │  • Route requests to appropriate Durable Object              │  │
│  │  • Handle authentication and rate limiting                    │  │
│  │  • WebSocket upgrade for real-time updates                    │  │
│  └──────────────────────────────┬───────────────────────────────┘  │
│                                 │                                   │
│                                 ▼                                   │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │           Durable Object (Per-Workspace)                     │  │
│  │  • Strong consistency for workspace state                    │  │
│  │  • Git operation coordination                                │  │
│  │  • Real-time event broadcasting via WebSocket                │  │
│  │  • Diff computation and caching                              │  │
│  └──────────────────────────────┬───────────────────────────────┘  │
│                                 │                                   │
│  ┌──────────────────────────────┴───────────────────────────────┐  │
│  │                    Storage Layer                              │  │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │  │
│  │  │     R2      │  │     D1      │  │         KV          │  │  │
│  │  │ Git Objects │  │   Metadata  │  │  Config/Cache       │  │  │
│  │  │   & Files   │  │    & Diffs  │  │                     │  │  │
│  │  └─────────────┘  └─────────────┘  └─────────────────────┘  │  │
│  └──────────────────────────────────────────────────────────────┘  │
└──────────────────────────────┬──────────────────────────────────────┘
                               │
                               ▼ (optional)
┌─────────────────────────────────────────────────────────────────────┐
│                  Self-Hosted Git Daemon                             │
│  • Smart HTTP v2 protocol                                           │
│  • Large repository handling                                        │
│  • Advanced git operations (rebase, merge, etc.)                    │
└─────────────────────────────────────────────────────────────────────┘

Data Models and Types

Extended Adaptor Interface

// src/control-plane/types.ts - Extended

import { z } from "zod"

export const CloudflareWorkspaceInfo = z.object({
  // Base workspace info
  id: Identifier.schema("workspace"),
  type: z.literal("cloudflare-worker"),
  branch: z.string().nullable(),
  name: z.string().nullable(),
  directory: z.string().nullable(),
  extra: z.unknown().nullable(),
  projectID: z.string(),
  
  // Cloudflare-specific fields
  cfAccountId: z.string(),
  cfWorkerName: z.string(),
  cfDurableObjectId: z.string(),
  gitRemoteUrl: z.string(),
  lastSyncAt: z.number().optional(),
  diffHash: z.string().optional(),
})

export type CloudflareWorkspaceInfo = z.infer<typeof CloudflareWorkspaceInfo>

export interface CloudflareAdaptor extends Adaptor {
  // Git operations
  getGitUrl(config: CloudflareWorkspaceInfo): Promise<string>
  fetchDiff(config: CloudflareWorkspaceInfo, baseRef: string, headRef: string): Promise<GitDiff>
  createBranch(config: CloudflareWorkspaceInfo, branchName: string): Promise<void>
  commitChanges(config: CloudflareWorkspaceInfo, message: string, files: FileChange[]): Promise<string>
  
  // Real-time updates
  subscribeToChanges(config: CloudflareWorkspaceInfo): WebSocket
  getWorkspaceEvents(config: CloudflareWorkspaceInfo): ReadableStream<WorkspaceEvent>
  
  // State management
  getWorkspaceState(config: CloudflareWorkspaceInfo): Promise<WorkspaceState>
  updateWorkspaceState(config: CloudflareWorkspaceInfo, state: Partial<WorkspaceState>): Promise<void>
}

export interface GitDiff {
  baseRef: string
  headRef: string
  files: DiffFile[]
  summary: DiffSummary
  computedAt: number
}

export interface DiffFile {
  path: string
  status: 'added' | 'modified' | 'deleted' | 'renamed'
  additions: number
  deletions: number
  patch?: string
}

export interface DiffSummary {
  filesChanged: number
  additions: number
  deletions: number
}

export interface WorkspaceState {
  currentBranch: string
  lastCommit: string
  isDirty: boolean
  stagedFiles: string[]
  untrackedFiles: string[]
  lastSyncAt: number
}

Database Schema Extensions

-- Add to workspace table or create new table
CREATE TABLE workspace_diff (
  id TEXT PRIMARY KEY,
  workspace_id TEXT NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
  base_ref TEXT NOT NULL,
  head_ref TEXT NOT NULL,
  diff_hash TEXT NOT NULL,
  files_changed INTEGER NOT NULL,
  additions INTEGER NOT NULL,
  deletions INTEGER NOT NULL,
  computed_at INTEGER NOT NULL,
  expires_at INTEGER,
  INDEX workspace_diff_workspace_idx ON workspace_diff(workspace_id),
  INDEX workspace_diff_hash_idx ON workspace_diff(diff_hash)
);

CREATE TABLE workspace_event (
  id TEXT PRIMARY KEY,
  workspace_id TEXT NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
  event_type TEXT NOT NULL,
  payload TEXT NOT NULL,
  created_at INTEGER NOT NULL,
  INDEX workspace_event_workspace_idx ON workspace_event(workspace_id),
  INDEX workspace_event_created_idx ON workspace_event(created_at)
);

Implementation Strategy

Phase 1: Cloudflare Adaptor Foundation

Objective: Basic integration with Cloudflare Workers API

// src/control-plane/adaptors/cloudflare.ts

import { type Adaptor, CloudflareWorkspaceInfo } from "../types"

export const CloudflareAdaptor: CloudflareAdaptor = {
  async configure(info) {
    // Validate Cloudflare credentials
    // Generate unique workspace identifiers
    // Set up Durable Object binding
    return {
      ...info,
      cfDurableObjectId: generateDurableObjectId(info.id),
      lastSyncAt: Date.now(),
    }
  },

  async create(info) {
    const config = CloudflareWorkspaceInfo.parse(info)
    
    // 1. Create Durable Object for workspace
    await createDurableObject(config.cfAccountId, config.cfDurableObjectId)
    
    // 2. Initialize git repository in R2
    await initializeGitRepo(config)
    
    // 3. Set up event streaming
    await setupEventStreaming(config)
    
    // 4. Store initial state
    await storeWorkspaceState(config, {
      currentBranch: config.branch || 'main',
      lastCommit: '',
      isDirty: false,
      stagedFiles: [],
      untrackedFiles: [],
      lastSyncAt: Date.now(),
    })
  },

  async remove(config) {
    // 1. Clean up Durable Object
    await deleteDurableObject(config.cfAccountId, config.cfDurableObjectId)
    
    // 2. Remove git data from R2
    await cleanupGitData(config)
    
    // 3. Remove metadata from D1
    await removeWorkspaceMetadata(config.id)
  },

  async fetch(config, input, init) {
    // Route request to Durable Object
    const durableObjectUrl = getDurableObjectUrl(config)
    const request = new Request(durableObjectUrl + input, init)
    return fetch(request)
  },

  // Cloudflare-specific methods
  async getGitUrl(config) {
    return `https://${config.cfWorkerName}.${config.cfAccountId}.workers.dev/git`
  },

  async fetchDiff(config, baseRef, headRef) {
    // Use Durable Object to compute diff
    const durableObject = getDurableObjectStub(config.cfDurableObjectId)
    return durableObject.fetch('/diff', {
      method: 'POST',
      body: JSON.stringify({ baseRef, headRef }),
    }).then(r => r.json())
  },

  async subscribeToChanges(config) {
    // WebSocket connection to Durable Object
    const wsUrl = `wss://${config.cfWorkerName}.${config.cfAccountId}.workers.dev/ws?workspace=${config.id}`
    return new WebSocket(wsUrl)
  },
}

Phase 2: Durable Object Implementation

Objective: Per-workspace state coordination and git operations

// src/control-plane/workspace-server/cloudflare-do.ts

import { DurableObject } from 'cloudflare:workers'

export class WorkspaceDurableObject extends DurableObject {
  private workspaceId: string
  private state: WorkspaceState
  private connections: Set<WebSocket>

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env)
    this.workspaceId = ctx.id.toString()
    this.connections = new Set()
    this.state = this.loadState()
  }

  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url)
    
    // WebSocket upgrade for real-time updates
    if (url.pathname === '/ws') {
      return this.handleWebSocket(request)
    }
    
    // Git operations
    if (url.pathname.startsWith('/git/')) {
      return this.handleGitOperation(request)
    }
    
    // Diff computation
    if (url.pathname === '/diff') {
      return this.handleDiffComputation(request)
    }
    
    // State management
    if (url.pathname === '/state') {
      return this.handleStateManagement(request)
    }
    
    return new Response('Not Found', { status: 404 })
  }

  private async handleWebSocket(request: Request): Promise<Response> {
    const pair = new WebSocketPair()
    const [client, server] = Object.values(pair)
    
    server.accept()
    this.connections.add(server)
    
    server.addEventListener('close', () => {
      this.connections.delete(server)
    })
    
    // Send current state on connect
    server.send(JSON.stringify({
      type: 'state',
      payload: this.state,
    }))
    
    return new Response(null, {
      status: 101,
      webSocket: client,
    })
  }

  private async handleGitOperation(request: Request): Promise<Response> {
    const operation = new URL(request.url).pathname.split('/git/')[1]
    
    switch (operation) {
      case 'clone':
        return this.handleClone(request)
      case 'fetch':
        return this.handleFetch(request)
      case 'diff':
        return this.handleDiff(request)
      case 'commit':
        return this.handleCommit(request)
      case 'branch':
        return this.handleBranch(request)
      default:
        return new Response('Unknown git operation', { status: 400 })
    }
  }

  private async handleClone(request: Request): Promise<Response> {
    const { remoteUrl, branch } = await request.json()
    
    // Clone repository to R2
    const repo = await this.cloneToR2(remoteUrl, branch)
    
    // Update state
    this.state = {
      ...this.state,
      currentBranch: branch || 'main',
      lastCommit: repo.headCommit,
      lastSyncAt: Date.now(),
    }
    
    // Broadcast update
    this.broadcastState()
    
    return Response.json({ success: true, commit: repo.headCommit })
  }

  private async handleDiffComputation(request: Request): Promise<Response> {
    const { baseRef, headRef } = await request.json()
    
    // Check cache first
    const cacheKey = `diff:${baseRef}:${headRef}`
    const cached = await this.ctx.storage.get(cacheKey)
    if (cached) {
      return Response.json(cached)
    }
    
    // Compute diff
    const diff = await this.computeDiff(baseRef, headRef)
    
    // Cache result
    await this.ctx.storage.put(cacheKey, diff, {
      expirationTtl: 3600, // 1 hour
    })
    
    return Response.json(diff)
  }

  private async broadcastState() {
    const message = JSON.stringify({
      type: 'state',
      payload: this.state,
    })
    
    for (const ws of this.connections) {
      try {
        ws.send(message)
      } catch {
        this.connections.delete(ws)
      }
    }
  }

  private loadState(): WorkspaceState {
    // Load from Durable Object storage
    return this.ctx.storage.get('state') || {
      currentBranch: 'main',
      lastCommit: '',
      isDirty: false,
      stagedFiles: [],
      untrackedFiles: [],
      lastSyncAt: 0,
    }
  }

  private async saveState() {
    await this.ctx.storage.put('state', this.state)
  }
}

Phase 3: Git Operations Implementation

Objective: Edge-native git operations using R2 storage

// src/control-plane/workspace-server/git-operations.ts

import { R2Bucket } from '@cloudflare/workers-types'

export class EdgeGitOperations {
  constructor(
    private r2: R2Bucket,
    private workspaceId: string,
  ) {}

  async clone(remoteUrl: string, branch: string = 'main'): Promise<GitRepo> {
    // 1. Fetch git objects from remote
    const objects = await this.fetchGitObjects(remoteUrl)
    
    // 2. Store objects in R2
    await this.storeGitObjects(objects)
    
    // 3. Create refs
    await this.createRefs(branch, objects.headCommit)
    
    // 4. Checkout working directory
    await this.checkoutWorkingDirectory(branch)
    
    return {
      headCommit: objects.headCommit,
      branch,
      lastSync: Date.now(),
    }
  }

  async diff(baseRef: string, headRef: string): Promise<GitDiff> {
    // 1. Get commit objects
    const baseCommit = await this.getCommit(baseRef)
    const headCommit = await this.getCommit(headRef)
    
    // 2. Compare trees
    const baseTree = await this.getTree(baseCommit.tree)
    const headTree = await this.getTree(headCommit.tree)
    
    // 3. Compute diff
    const diff = await this.computeTreeDiff(baseTree, headTree)
    
    return {
      baseRef,
      headRef,
      files: diff.files,
      summary: diff.summary,
      computedAt: Date.now(),
    }
  }

  async commit(message: string, files: FileChange[]): Promise<string> {
    // 1. Update working directory files in R2
    await this.updateWorkingDirectory(files)
    
    // 2. Create tree object
    const tree = await this.createTree(files)
    
    // 3. Create commit object
    const commit = await this.createCommit(tree, message)
    
    // 4. Update ref
    await this.updateRef('HEAD', commit.hash)
    
    return commit.hash
  }

  private async fetchGitObjects(remoteUrl: string): Promise<GitObjects> {
    // Implement Smart HTTP v2 protocol
    // This is complex - consider using git-on-cloudflare or delegating to self-hosted daemon
    throw new Error('Not implemented - requires git protocol implementation')
  }

  private async computeTreeDiff(baseTree: GitTree, headTree: GitTree): Promise<GitDiff> {
    // Compare trees and compute file-level diffs
    const files: DiffFile[] = []
    let additions = 0
    let deletions = 0
    
    // Implementation depends on git object format
    // This is a simplified version
    
    return {
      files,
      summary: {
        filesChanged: files.length,
        additions,
        deletions,
      },
    }
  }
}

Phase 4: Integration with OpenCode

Objective: Seamless integration with existing workspace system

// src/control-plane/workspace-router-middleware.ts - Extended

import { WorkspaceRouterMiddleware as BaseMiddleware } from "./workspace-router-middleware"

export const CloudflareWorkspaceRouterMiddleware: MiddlewareHandler = async (c, next) => {
  // Check if current workspace is cloudflare type
  const workspaceID = WorkspaceContext.workspaceID
  if (!workspaceID) return next()
  
  const workspace = await Workspace.get(workspaceID)
  if (!workspace || workspace.type !== 'cloudflare-worker') {
    return next()
  }
  
  // Route to Cloudflare adaptor
  const adaptor = await getAdaptor('cloudflare-worker')
  const response = await adaptor.fetch(workspace, c.req.url, {
    method: c.req.method,
    headers: c.req.raw.headers,
    body: c.req.method === 'GET' ? undefined : await c.req.arrayBuffer(),
  })
  
  return new Response(response.body, {
    status: response.status,
    headers: response.headers,
  })
}

Key Technical Considerations

1. Git Protocol Implementation

Challenge: Implementing Smart HTTP v2 in Cloudflare Workers is complex due to CPU time limits.

Solutions:

  • Option A: Use existing git-on-cloudflare open-source implementation
  • Option B: Delegate heavy git operations to self-hosted git daemon
  • Option C: Implement subset of git protocol for common operations only

Recommendation: Start with Option B (self-hosted daemon) for full git support, then gradually implement edge-native operations for common use cases.

2. Storage Architecture

R2 Storage Structure:

/workspace-{id}/
├── objects/           # Git objects (packfiles)
├── refs/             # Branch references
├── working/          # Working directory files
└── cache/            # Computed diffs and metadata

D1 Tables:

  • workspace_metadata: Workspace configuration
  • workspace_diffs: Computed diff cache
  • workspace_events: Event log for sync

3. Real-time Synchronization

WebSocket Hibernation Pattern:

  • Durable Object maintains WebSocket connections
  • Broadcasts state changes to all connected clients
  • Handles reconnection and offline scenarios
  • Uses hibernation API for cost efficiency

4. Performance Optimization

Caching Strategy:

  • L1: In-memory cache in Durable Object (per-request)
  • L2: Durable Object storage (persistent)
  • L3: KV for frequently accessed metadata
  • L4: R2 for git objects and large files

Diff Computation:

  • Cache computed diffs with 1-hour TTL
  • Incremental diff updates
  • Background computation for large repositories

5. Cost Management

Estimated Costs (per 1000 workspaces):

  • Workers: ~$0.30/million requests
  • Durable Objects: ~$0.15/million requests + storage
  • R2: ~$0.015/GB-month storage
  • D1: ~$0.20/GB-month + query costs

Optimization:

  • Implement usage quotas per workspace
  • Use background processing for non-critical operations
  • Compress git objects efficiently

Implementation Roadmap

Phase 1: Foundation (2-3 weeks)

  1. Extend Adaptor interface with Cloudflare-specific methods
  2. Implement basic CloudflareAdaptor with proxy to existing workspace server
  3. Set up Cloudflare Workers project with Durable Object bindings
  4. Basic R2 storage for git metadata

Phase 2: Core Features (3-4 weeks)

  1. Implement Durable Object for workspace state management
  2. Add WebSocket support for real-time updates
  3. Implement diff computation and caching
  4. Integrate with OpenCode's session management

Phase 3: Git Operations (4-6 weeks)

  1. Implement Smart HTTP v2 client for git operations
  2. Add edge-native git clone/fetch for small repositories
  3. Implement branch and commit operations
  4. Add self-hosted git daemon integration as fallback

Phase 4: Advanced Features (3-4 weeks)

  1. Real-time collaboration via WebSocket
  2. Offline sync and conflict resolution
  3. Advanced diff features (syntax highlighting, inline comments)
  4. Performance optimization and monitoring

Migration Strategy

Backward Compatibility

  • Existing WorktreeAdaptor remains unchanged
  • New CloudflareAdaptor implements same Adaptor interface
  • WorkspaceRouterMiddleware routes based on workspace type
  • Gradual migration path for existing workspaces

Data Migration

  1. Export existing workspace metadata
  2. Clone repositories to R2 storage
  3. Update workspace type to cloudflare-worker
  4. Verify functionality before removing local worktree

Security Considerations

Authentication

  • Use Cloudflare Access for API authentication
  • Implement workspace-level permissions
  • Secure git operations with token-based auth

Data Protection

  • Encrypt sensitive data in R2 at rest
  • Use D1 for access-controlled metadata
  • Implement audit logging for workspace operations

Network Security

  • Use Cloudflare's DDoS protection
  • Implement rate limiting per workspace
  • Secure WebSocket connections with authentication

This architecture provides a scalable, cost-effective solution for dynamic git workspace management while maintaining compatibility with OpenCode's existing workspace system. The incremental implementation approach allows for gradual rollout and validation at each phase.

1,940in / 4,869out / 177reasoning