summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJames Long <[email protected]>2026-03-27 16:33:56 -0400
committerGitHub <[email protected]>2026-03-27 16:33:56 -0400
commit4b9660b211aa57477b6baa1848e582d3279f4db7 (patch)
treed87cf614108547fff7608496cae6c3aae3c16494
parente5f0e813b6e2f9305fc27d432689f95a56beea51 (diff)
downloadopencode-4b9660b211aa57477b6baa1848e582d3279f4db7.tar.gz
opencode-4b9660b211aa57477b6baa1848e582d3279f4db7.zip
refactor(core): move more responsibility to workspace routing (#19455)
-rw-r--r--packages/opencode/src/control-plane/adaptors/worktree.ts12
-rw-r--r--packages/opencode/src/control-plane/workspace-router-middleware.ts64
-rw-r--r--packages/opencode/src/server/instance.ts22
-rw-r--r--packages/opencode/src/server/router.ts99
-rw-r--r--packages/opencode/src/server/server.ts2
5 files changed, 102 insertions, 97 deletions
diff --git a/packages/opencode/src/control-plane/adaptors/worktree.ts b/packages/opencode/src/control-plane/adaptors/worktree.ts
index 2a96034d7..719748e3a 100644
--- a/packages/opencode/src/control-plane/adaptors/worktree.ts
+++ b/packages/opencode/src/control-plane/adaptors/worktree.ts
@@ -32,15 +32,7 @@ export const WorktreeAdaptor: Adaptor = {
const config = Config.parse(info)
await Worktree.remove({ directory: config.directory })
},
- async fetch(info, input: RequestInfo | URL, init?: RequestInit) {
- const { Server } = await import("../../server/server")
-
- const config = Config.parse(info)
- 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 Server.Default().fetch(request)
+ async fetch(_info, _input: RequestInfo | URL, _init?: RequestInit) {
+ throw new Error("fetch not implemented")
},
}
diff --git a/packages/opencode/src/control-plane/workspace-router-middleware.ts b/packages/opencode/src/control-plane/workspace-router-middleware.ts
deleted file mode 100644
index 1fc19a22b..000000000
--- a/packages/opencode/src/control-plane/workspace-router-middleware.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import type { MiddlewareHandler } from "hono"
-import { Flag } from "../flag/flag"
-import { getAdaptor } from "./adaptors"
-import { WorkspaceID } from "./schema"
-import { Workspace } from "./workspace"
-import { InstanceRoutes } from "../server/instance"
-import { lazy } from "../util/lazy"
-
-type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" }
-
-const RULES: Array<Rule> = [
- { path: "/session/status", action: "forward" },
- { method: "GET", path: "/session", action: "local" },
-]
-
-function local(method: string, path: string) {
- for (const rule of RULES) {
- if (rule.method && rule.method !== method) continue
- const match = rule.exact ? path === rule.path : path === rule.path || path.startsWith(rule.path + "/")
- if (match) return rule.action === "local"
- }
- return false
-}
-
-const routes = lazy(() => InstanceRoutes())
-
-export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c) => {
- if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
- return routes().fetch(c.req.raw, c.env)
- }
-
- const url = new URL(c.req.url)
- const raw = url.searchParams.get("workspace")
-
- if (!raw) {
- return routes().fetch(c.req.raw, c.env)
- }
-
- if (local(c.req.method, url.pathname)) {
- return routes().fetch(c.req.raw, c.env)
- }
-
- const workspaceID = WorkspaceID.make(raw)
- const workspace = await Workspace.get(workspaceID)
- if (!workspace) {
- return new Response(`Workspace not found: ${workspaceID}`, {
- status: 500,
- headers: {
- "content-type": "text/plain; charset=utf-8",
- },
- })
- }
-
- const adaptor = await getAdaptor(workspace.type)
- const headers = new Headers(c.req.raw.headers)
- headers.delete("x-opencode-workspace")
-
- return adaptor.fetch(workspace, `${url.pathname}${url.search}`, {
- method: c.req.method,
- body: c.req.method === "GET" || c.req.method === "HEAD" ? undefined : await c.req.raw.arrayBuffer(),
- signal: c.req.raw.signal,
- headers,
- })
-}
diff --git a/packages/opencode/src/server/instance.ts b/packages/opencode/src/server/instance.ts
index b99cf3d99..4bb6efaf9 100644
--- a/packages/opencode/src/server/instance.ts
+++ b/packages/opencode/src/server/instance.ts
@@ -14,7 +14,6 @@ import { Global } from "../global"
import { LSP } from "../lsp"
import { Command } from "../command"
import { Flag } from "../flag/flag"
-import { Filesystem } from "@/util/filesystem"
import { QuestionRoutes } from "./routes/question"
import { PermissionRoutes } from "./routes/permission"
import { ProjectRoutes } from "./routes/project"
@@ -26,7 +25,6 @@ import { ConfigRoutes } from "./routes/config"
import { ExperimentalRoutes } from "./routes/experimental"
import { ProviderRoutes } from "./routes/provider"
import { EventRoutes } from "./routes/event"
-import { InstanceBootstrap } from "../project/bootstrap"
import { errorHandler } from "./middleware"
const log = Log.create({ service: "server" })
@@ -45,26 +43,6 @@ const csp = (hash = "") =>
export const InstanceRoutes = (app?: Hono) =>
(app ?? new Hono())
.onError(errorHandler(log))
- .use(async (c, next) => {
- const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
- const directory = Filesystem.resolve(
- (() => {
- try {
- return decodeURIComponent(raw)
- } catch {
- return raw
- }
- })(),
- )
-
- return Instance.provide({
- directory,
- init: InstanceBootstrap,
- async fn() {
- return next()
- },
- })
- })
.route("/project", ProjectRoutes())
.route("/pty", PtyRoutes())
.route("/config", ConfigRoutes())
diff --git a/packages/opencode/src/server/router.ts b/packages/opencode/src/server/router.ts
new file mode 100644
index 000000000..f64180892
--- /dev/null
+++ b/packages/opencode/src/server/router.ts
@@ -0,0 +1,99 @@
+import type { MiddlewareHandler } from "hono"
+import { getAdaptor } from "@/control-plane/adaptors"
+import { WorkspaceID } from "@/control-plane/schema"
+import { Workspace } from "@/control-plane/workspace"
+import { lazy } from "@/util/lazy"
+import { Filesystem } from "@/util/filesystem"
+import { Instance } from "@/project/instance"
+import { InstanceBootstrap } from "@/project/bootstrap"
+import { InstanceRoutes } from "./instance"
+
+type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" }
+
+const RULES: Array<Rule> = [
+ { path: "/session/status", action: "forward" },
+ { method: "GET", path: "/session", action: "local" },
+]
+
+function local(method: string, path: string) {
+ for (const rule of RULES) {
+ if (rule.method && rule.method !== method) continue
+ const match = rule.exact ? path === rule.path : path === rule.path || path.startsWith(rule.path + "/")
+ if (match) return rule.action === "local"
+ }
+ return false
+}
+
+const routes = lazy(() => InstanceRoutes())
+
+export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c) => {
+ const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
+ const directory = Filesystem.resolve(
+ (() => {
+ try {
+ return decodeURIComponent(raw)
+ } catch {
+ return raw
+ }
+ })(),
+ )
+
+ const url = new URL(c.req.url)
+ const workspaceParam = url.searchParams.get("workspace")
+
+ // TODO: If session is being routed, force it to lookup the
+ // project/workspace
+
+ // If no workspace is provided we use the "project" workspace
+ if (!workspaceParam) {
+ return Instance.provide({
+ directory,
+ init: InstanceBootstrap,
+ async fn() {
+ return routes().fetch(c.req.raw, c.env)
+ },
+ })
+ }
+
+ const workspaceID = WorkspaceID.make(workspaceParam)
+ const workspace = await Workspace.get(workspaceID)
+ if (!workspace) {
+ return new Response(`Workspace not found: ${workspaceID}`, {
+ status: 500,
+ headers: {
+ "content-type": "text/plain; charset=utf-8",
+ },
+ })
+ }
+
+ // Handle local workspaces directly so we can pass env to `fetch`,
+ // necessary for websocket upgrades
+ if (workspace.type === "worktree") {
+ return Instance.provide({
+ directory: workspace.directory!,
+ init: InstanceBootstrap,
+ async fn() {
+ return routes().fetch(c.req.raw, c.env)
+ },
+ })
+ }
+
+ // Remote workspaces
+
+ if (local(c.req.method, url.pathname)) {
+ // No instance provided because we are serving cached data; there
+ // is no instance to work with
+ return routes().fetch(c.req.raw, c.env)
+ }
+
+ const adaptor = await getAdaptor(workspace.type)
+ const headers = new Headers(c.req.raw.headers)
+ headers.delete("x-opencode-workspace")
+
+ return adaptor.fetch(workspace, `${url.pathname}${url.search}`, {
+ method: c.req.method,
+ body: c.req.method === "GET" || c.req.method === "HEAD" ? undefined : await c.req.raw.arrayBuffer(),
+ signal: c.req.raw.signal,
+ headers,
+ })
+}
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index cfb22929b..ec245ed59 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -8,7 +8,7 @@ import z from "zod"
import { Auth } from "../auth"
import { Flag } from "../flag/flag"
import { ProviderID } from "../provider/schema"
-import { WorkspaceRouterMiddleware } from "../control-plane/workspace-router-middleware"
+import { WorkspaceRouterMiddleware } from "./router"
import { websocket } from "hono/bun"
import { errors } from "./error"
import { GlobalRoutes } from "./routes/global"