From 77aabd9c57f21c0cdd288b6eef4e197e927b1179 Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 26 Mar 2026 11:33:46 -0400 Subject: [PATCH] feat(core): remove workspace server and reuse core server in workspaces --- .../opencode/src/cli/cmd/workspace-serve.ts | 16 ----- .../src/control-plane/adaptors/worktree.ts | 5 +- .../control-plane/workspace-server/routes.ts | 33 --------- .../control-plane/workspace-server/server.ts | 65 ----------------- packages/opencode/src/index.ts | 9 +-- .../workspace-server-sse.test.ts | 70 ------------------- 6 files changed, 4 insertions(+), 194 deletions(-) delete mode 100644 packages/opencode/src/cli/cmd/workspace-serve.ts delete mode 100644 packages/opencode/src/control-plane/workspace-server/routes.ts delete mode 100644 packages/opencode/src/control-plane/workspace-server/server.ts delete mode 100644 packages/opencode/test/control-plane/workspace-server-sse.test.ts diff --git a/packages/opencode/src/cli/cmd/workspace-serve.ts b/packages/opencode/src/cli/cmd/workspace-serve.ts deleted file mode 100644 index cb5c304e4b..0000000000 --- a/packages/opencode/src/cli/cmd/workspace-serve.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { cmd } from "./cmd" -import { withNetworkOptions, resolveNetworkOptions } from "../network" -import { WorkspaceServer } from "../../control-plane/workspace-server/server" - -export const WorkspaceServeCommand = cmd({ - command: "workspace-serve", - builder: (yargs) => withNetworkOptions(yargs), - describe: "starts a remote workspace event server", - handler: async (args) => { - const opts = await resolveNetworkOptions(args) - const server = WorkspaceServer.Listen(opts) - console.log(`workspace event server listening on http://${server.hostname}:${server.port}/event`) - await new Promise(() => {}) - await server.stop() - }, -}) diff --git a/packages/opencode/src/control-plane/adaptors/worktree.ts b/packages/opencode/src/control-plane/adaptors/worktree.ts index ff2d92e199..3c107cb838 100644 --- a/packages/opencode/src/control-plane/adaptors/worktree.ts +++ b/packages/opencode/src/control-plane/adaptors/worktree.ts @@ -2,6 +2,8 @@ import z from "zod" import { Worktree } from "@/worktree" import { type Adaptor, WorkspaceInfo } from "../types" +import { Server } from "../../server/server" + const Config = WorkspaceInfo.extend({ name: WorkspaceInfo.shape.name.unwrap(), branch: WorkspaceInfo.shape.branch.unwrap(), @@ -34,12 +36,11 @@ export const WorktreeAdaptor: Adaptor = { }, async fetch(info, input: RequestInfo | URL, init?: RequestInit) { const config = Config.parse(info) - const { WorkspaceServer } = await import("../workspace-server/server") const url = input instanceof Request || input instanceof URL ? input : new URL(input, "http://opencode.internal") const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)) headers.set("x-opencode-directory", config.directory) const request = new Request(url, { ...init, headers }) - return WorkspaceServer.App().fetch(request) + return Server.Default().fetch(request) }, } diff --git a/packages/opencode/src/control-plane/workspace-server/routes.ts b/packages/opencode/src/control-plane/workspace-server/routes.ts deleted file mode 100644 index 353e5d50af..0000000000 --- a/packages/opencode/src/control-plane/workspace-server/routes.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { GlobalBus } from "../../bus/global" -import { Hono } from "hono" -import { streamSSE } from "hono/streaming" - -export function WorkspaceServerRoutes() { - return new Hono().get("/event", async (c) => { - c.header("X-Accel-Buffering", "no") - c.header("X-Content-Type-Options", "nosniff") - return streamSSE(c, async (stream) => { - const send = async (event: unknown) => { - await stream.writeSSE({ - data: JSON.stringify(event), - }) - } - const handler = async (event: { directory?: string; payload: unknown }) => { - await send(event.payload) - } - GlobalBus.on("event", handler) - await send({ type: "server.connected", properties: {} }) - const heartbeat = setInterval(() => { - void send({ type: "server.heartbeat", properties: {} }) - }, 10_000) - - await new Promise((resolve) => { - stream.onAbort(() => { - clearInterval(heartbeat) - GlobalBus.off("event", handler) - resolve() - }) - }) - }) - }) -} diff --git a/packages/opencode/src/control-plane/workspace-server/server.ts b/packages/opencode/src/control-plane/workspace-server/server.ts deleted file mode 100644 index b0744fe025..0000000000 --- a/packages/opencode/src/control-plane/workspace-server/server.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Hono } from "hono" -import { Instance } from "../../project/instance" -import { InstanceBootstrap } from "../../project/bootstrap" -import { SessionRoutes } from "../../server/routes/session" -import { WorkspaceServerRoutes } from "./routes" -import { WorkspaceContext } from "../workspace-context" -import { WorkspaceID } from "../schema" - -export namespace WorkspaceServer { - export function App() { - const session = new Hono() - .use(async (c, next) => { - // Right now, we need handle all requests because we don't - // have syncing. In the future all GET requests will handled - // by the control plane - // - // if (c.req.method === "GET") return c.notFound() - await next() - }) - .route("/", SessionRoutes()) - - return new Hono() - .use(async (c, next) => { - const rawWorkspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace") - const raw = c.req.query("directory") || c.req.header("x-opencode-directory") - if (rawWorkspaceID == null) { - throw new Error("workspaceID parameter is required") - } - if (raw == null) { - throw new Error("directory parameter is required") - } - - const directory = (() => { - try { - return decodeURIComponent(raw) - } catch { - return raw - } - })() - - return WorkspaceContext.provide({ - workspaceID: WorkspaceID.make(rawWorkspaceID), - async fn() { - return Instance.provide({ - directory, - init: InstanceBootstrap, - async fn() { - return next() - }, - }) - }, - }) - }) - .route("/session", session) - .route("/", WorkspaceServerRoutes()) - } - - export function Listen(opts: { hostname: string; port: number }) { - return Bun.serve({ - hostname: opts.hostname, - port: opts.port, - fetch: App().fetch, - }) - } -} diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index b3d1db7eb0..e27471068f 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -14,7 +14,6 @@ import { Installation } from "./installation" import { NamedError } from "@opencode-ai/util/error" import { FormatError } from "./cli/error" import { ServeCommand } from "./cli/cmd/serve" -import { WorkspaceServeCommand } from "./cli/cmd/workspace-serve" import { Filesystem } from "./util/filesystem" import { DebugCommand } from "./cli/cmd/debug" import { StatsCommand } from "./cli/cmd/stats" @@ -47,7 +46,7 @@ process.on("uncaughtException", (e) => { }) }) -let cli = yargs(hideBin(process.argv)) +const cli = yargs(hideBin(process.argv)) .parserConfiguration({ "populate--": true }) .scriptName("opencode") .wrap(100) @@ -145,12 +144,6 @@ let cli = yargs(hideBin(process.argv)) .command(PrCommand) .command(SessionCommand) .command(DbCommand) - -if (Installation.isLocal()) { - cli = cli.command(WorkspaceServeCommand) -} - -cli = cli .fail((msg, err) => { if ( msg?.startsWith("Unknown argument") || diff --git a/packages/opencode/test/control-plane/workspace-server-sse.test.ts b/packages/opencode/test/control-plane/workspace-server-sse.test.ts deleted file mode 100644 index 7e7cddb140..0000000000 --- a/packages/opencode/test/control-plane/workspace-server-sse.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test" -import { Log } from "../../src/util/log" -import { WorkspaceServer } from "../../src/control-plane/workspace-server/server" -import { parseSSE } from "../../src/control-plane/sse" -import { GlobalBus } from "../../src/bus/global" -import { resetDatabase } from "../fixture/db" -import { tmpdir } from "../fixture/fixture" - -afterEach(async () => { - await resetDatabase() -}) - -Log.init({ print: false }) - -describe("control-plane/workspace-server SSE", () => { - test("streams GlobalBus events and parseSSE reads them", async () => { - await using tmp = await tmpdir({ git: true }) - const app = WorkspaceServer.App() - const stop = new AbortController() - const seen: unknown[] = [] - try { - const response = await app.request("/event", { - signal: stop.signal, - headers: { - "x-opencode-workspace": "wrk_test_workspace", - "x-opencode-directory": tmp.path, - }, - }) - - expect(response.status).toBe(200) - expect(response.body).toBeDefined() - - const done = new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error("timed out waiting for workspace.test event")) - }, 3000) - - void parseSSE(response.body!, stop.signal, (event) => { - seen.push(event) - const next = event as { type?: string } - if (next.type === "server.connected") { - GlobalBus.emit("event", { - payload: { - type: "workspace.test", - properties: { ok: true }, - }, - }) - return - } - if (next.type !== "workspace.test") return - clearTimeout(timeout) - resolve() - }).catch((error) => { - clearTimeout(timeout) - reject(error) - }) - }) - - await done - - expect(seen.some((event) => (event as { type?: string }).type === "server.connected")).toBe(true) - expect(seen).toContainEqual({ - type: "workspace.test", - properties: { ok: true }, - }) - } finally { - stop.abort() - } - }) -})