From 075f876e6fbaf3e02223e1add69d8b8e2901d5af Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 2 May 2026 09:35:39 -0400 Subject: [PATCH] fix(httpapi): re-land workspace create payload accepts missing extra (#25412) --- .../instance/httpapi/groups/workspace.ts | 5 +- .../instance/httpapi/handlers/workspace.ts | 1 + .../test/server/httpapi-workspace.test.ts | 53 ++++++++++++++++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts index 112b8a3298..08e9e044bb 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts @@ -9,7 +9,10 @@ import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" import { described } from "./metadata" const root = "/experimental/workspace" -export const CreatePayload = Schema.Struct(Struct.omit(Workspace.CreateInput.fields, ["projectID"])) +export const CreatePayload = Schema.Struct({ + ...Struct.omit(Workspace.CreateInput.fields, ["projectID", "extra"]), + extra: Schema.optional(Workspace.CreateInput.fields.extra), +}) export const SessionRestorePayload = Schema.Struct(Struct.omit(Workspace.SessionRestoreInput.fields, ["workspaceID"])) export const SessionRestoreResponse = Schema.Struct({ total: NonNegativeInt, diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts index 03e8ee74b7..570f355e57 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts @@ -24,6 +24,7 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac return yield* workspace .create({ ...ctx.payload, + extra: ctx.payload.extra ?? null, projectID: instance.project.id, }) .pipe(Effect.mapError(() => new HttpApiError.BadRequest({}))) diff --git a/packages/opencode/test/server/httpapi-workspace.test.ts b/packages/opencode/test/server/httpapi-workspace.test.ts index e44a5ee3cd..48dcd885b2 100644 --- a/packages/opencode/test/server/httpapi-workspace.test.ts +++ b/packages/opencode/test/server/httpapi-workspace.test.ts @@ -27,9 +27,9 @@ const it = testEffect( Layer.mergeAll(NodeServices.layer, Project.defaultLayer, Session.defaultLayer, Workspace.defaultLayer), ) -function request(path: string, directory: string, init: RequestInit = {}) { +function request(path: string, directory: string, init: RequestInit = {}, httpApi = true) { return Effect.promise(() => { - Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = httpApi const headers = new Headers(init.headers) headers.set("x-opencode-directory", directory) return Promise.resolve(Server.Default().app.request(path, { ...init, headers })) @@ -195,6 +195,55 @@ describe("workspace HttpApi", () => { }), ) + it.live("creates workspace with the TUI payload shape", () => + Effect.gen(function* () { + Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true + const dir = yield* tmpdirScoped({ git: true }) + const project = yield* Project.use.fromDirectory(dir) + registerAdapter(project.project.id, "local-test", localAdapter(path.join(dir, ".workspace"))) + + const created = yield* request(WorkspacePaths.list, dir, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ type: "local-test", branch: null }), + }) + + expect(created.status).toBe(200) + expect((yield* Effect.promise(() => created.json())) as Workspace.Info).toMatchObject({ + type: "local-test", + name: "local-test", + extra: null, + }) + }), + ) + + it.live("documents legacy Hono accepting the TUI payload shape", () => + Effect.gen(function* () { + Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true + const dir = yield* tmpdirScoped({ git: true }) + const project = yield* Project.use.fromDirectory(dir) + registerAdapter(project.project.id, "local-test", localAdapter(path.join(dir, ".workspace"))) + + const created = yield* request( + WorkspacePaths.list, + dir, + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ type: "local-test", branch: null }), + }, + false, + ) + + expect(created.status).toBe(200) + expect((yield* Effect.promise(() => created.json())) as Workspace.Info).toMatchObject({ + type: "local-test", + name: "local-test", + extra: null, + }) + }), + ) + it.live("routes local workspace requests through the workspace target directory", () => Effect.gen(function* () { Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true