From b749866f0b21328919e0c525109c5b2f3e4e0e24 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 25 Apr 2026 14:55:29 -0400 Subject: [PATCH] feat(httpapi): bridge worktree read endpoint (#24366) --- packages/opencode/specs/effect/http-api.md | 2 +- .../routes/instance/httpapi/experimental.ts | 22 +++++++++++++++++++ .../src/server/routes/instance/index.ts | 1 + .../test/server/httpapi-experimental.test.ts | 6 ++++- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index e32a1dd7ef..3e4f30d02f 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -164,7 +164,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho | `mcp` | `bridged` partial | status only | | `workspace` | `bridged` | list, get, enter | | top-level instance reads | `bridged` | path, vcs, command, agent, skill, lsp, formatter | -| experimental JSON routes | `bridged` partial | console reads, tool ids, resource list; worktree and global session list remain later | +| experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list, resource list; global session list remains later | | `session` | `later/special` | large stateful surface plus streaming | | `sync` | `later` | process/control side effects | | `event` | `special` | SSE | diff --git a/packages/opencode/src/server/routes/instance/httpapi/experimental.ts b/packages/opencode/src/server/routes/instance/httpapi/experimental.ts index 993971202f..4bd9ba30aa 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/experimental.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/experimental.ts @@ -1,6 +1,8 @@ import { Account } from "@/account/account" import { Config } from "@/config" +import { InstanceState } from "@/effect" import { MCP } from "@/mcp" +import { Project } from "@/project" import { ToolRegistry } from "@/tool" import { Effect, Layer, Option, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" @@ -27,10 +29,13 @@ const ConsoleOrgList = Schema.Struct({ const ToolIDs = Schema.Array(Schema.String).annotate({ identifier: "ToolIDs" }) +const WorktreeList = Schema.Array(Schema.String).annotate({ identifier: "WorktreeList" }) + export const ExperimentalPaths = { console: "/experimental/console", consoleOrgs: "/experimental/console/orgs", toolIDs: "/experimental/tool/ids", + worktree: "/experimental/worktree", resource: "/experimental/resource", } as const @@ -66,6 +71,15 @@ export const ExperimentalApi = HttpApi.make("experimental") "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.", }), ), + HttpApiEndpoint.get("worktree", ExperimentalPaths.worktree, { + success: WorktreeList, + }).annotateMerge( + OpenApi.annotations({ + identifier: "worktree.list", + summary: "List worktrees", + description: "List all sandbox worktrees for the current project.", + }), + ), HttpApiEndpoint.get("resource", ExperimentalPaths.resource, { success: Schema.Record(Schema.String, MCP.Resource), }).annotateMerge( @@ -97,6 +111,7 @@ export const experimentalHandlers = Layer.unwrap( const account = yield* Account.Service const config = yield* Config.Service const mcp = yield* MCP.Service + const project = yield* Project.Service const registry = yield* ToolRegistry.Service const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () { @@ -139,6 +154,11 @@ export const experimentalHandlers = Layer.unwrap( return yield* registry.ids() }) + const worktree = Effect.fn("ExperimentalHttpApi.worktree")(function* () { + const ctx = yield* InstanceState.context + return yield* project.sandboxes(ctx.project.id) + }) + const resource = Effect.fn("ExperimentalHttpApi.resource")(function* () { return yield* mcp.resources() }) @@ -148,6 +168,7 @@ export const experimentalHandlers = Layer.unwrap( .handle("console", getConsole) .handle("consoleOrgs", listConsoleOrgs) .handle("toolIDs", toolIDs) + .handle("worktree", worktree) .handle("resource", resource), ) }), @@ -155,5 +176,6 @@ export const experimentalHandlers = Layer.unwrap( Layer.provide(Account.defaultLayer), Layer.provide(Config.defaultLayer), Layer.provide(MCP.defaultLayer), + Layer.provide(Project.defaultLayer), Layer.provide(ToolRegistry.defaultLayer), ) diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index 65dd417051..8e4c497bd7 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -49,6 +49,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.get(ExperimentalPaths.console, (c) => handler(c.req.raw, context)) app.get(ExperimentalPaths.consoleOrgs, (c) => handler(c.req.raw, context)) app.get(ExperimentalPaths.toolIDs, (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context)) app.get(ExperimentalPaths.resource, (c) => handler(c.req.raw, context)) app.get("/provider", (c) => handler(c.req.raw, context)) app.get("/provider/auth", (c) => handler(c.req.raw, context)) diff --git a/packages/opencode/test/server/httpapi-experimental.test.ts b/packages/opencode/test/server/httpapi-experimental.test.ts index 00d1fefb88..38f43e68fd 100644 --- a/packages/opencode/test/server/httpapi-experimental.test.ts +++ b/packages/opencode/test/server/httpapi-experimental.test.ts @@ -41,10 +41,11 @@ describe("experimental HttpApi", () => { }) const headers = { "x-opencode-directory": tmp.path } - const [consoleState, consoleOrgs, toolIDs, resources] = await Promise.all([ + const [consoleState, consoleOrgs, toolIDs, worktrees, resources] = await Promise.all([ app().request(ExperimentalPaths.console, { headers }), app().request(ExperimentalPaths.consoleOrgs, { headers }), app().request(ExperimentalPaths.toolIDs, { headers }), + app().request(ExperimentalPaths.worktree, { headers }), app().request(ExperimentalPaths.resource, { headers }), ]) @@ -60,6 +61,9 @@ describe("experimental HttpApi", () => { expect(toolIDs.status).toBe(200) expect(await toolIDs.json()).toContain("bash") + expect(worktrees.status).toBe(200) + expect(await worktrees.json()).toEqual([]) + expect(resources.status).toBe(200) expect(await resources.json()).toEqual({}) })