diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/tui.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/tui.ts index cb12ccb7a7..c7c447ce85 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/tui.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/tui.ts @@ -28,8 +28,8 @@ const commandAliases = { export const tuiHandlers = HttpApiBuilder.group(InstanceHttpApi, "tui", (handlers) => Effect.gen(function* () { const bus = yield* Bus.Service - const publishCommand = (command: typeof TuiEvent.CommandExecute.properties.Type.command) => - bus.publish(TuiEvent.CommandExecute, { command }) + const publishCommand = (command: typeof TuiEvent.CommandExecute.properties.Type.command | undefined) => + bus.publish(TuiEvent.CommandExecute, { command } as typeof TuiEvent.CommandExecute.properties.Type) const appendPrompt = Effect.fn("TuiHttpApi.appendPrompt")(function* (ctx: { payload: typeof TuiEvent.PromptAppend.properties.Type @@ -71,7 +71,8 @@ export const tuiHandlers = HttpApiBuilder.group(InstanceHttpApi, "tui", (handler const executeCommand = Effect.fn("TuiHttpApi.executeCommand")(function* (ctx: { payload: typeof CommandPayload.Type }) { - yield* publishCommand(commandAliases[ctx.payload.command as keyof typeof commandAliases] ?? ctx.payload.command) + // Legacy only publishes known aliases; unknown commands become undefined. + yield* publishCommand(commandAliases[ctx.payload.command as keyof typeof commandAliases]) return true }) diff --git a/packages/opencode/test/server/httpapi-tui.test.ts b/packages/opencode/test/server/httpapi-tui.test.ts index 9f7c8e9e89..3e844fad02 100644 --- a/packages/opencode/test/server/httpapi-tui.test.ts +++ b/packages/opencode/test/server/httpapi-tui.test.ts @@ -1,6 +1,8 @@ import { afterEach, describe, expect, test } from "bun:test" import type { Context } from "hono" import { Flag } from "@opencode-ai/core/flag/flag" +import { GlobalBus } from "../../src/bus/global" +import { TuiEvent } from "../../src/cli/cmd/tui/event" import { SessionID } from "../../src/session/schema" import { Instance } from "../../src/project/instance" import { TuiApi, TuiPaths } from "../../src/server/routes/instance/httpapi/groups/tui" @@ -15,9 +17,20 @@ void Log.init({ print: false }) const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI -function app() { - Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true - return Server.Default().app +function app(experimental = true) { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental + return experimental ? Server.Default().app : Server.Legacy().app +} + +function nextCommandExecute() { + return new Promise((resolve) => { + const listener = (event: { payload: { type?: string; properties?: { command?: unknown } } }) => { + if (event.payload.type !== TuiEvent.CommandExecute.type) return + GlobalBus.off("event", listener) + resolve(event.payload.properties?.command) + } + GlobalBus.on("event", listener) + }) } async function expectTrue(path: string, headers: Record, body?: unknown) { @@ -72,6 +85,27 @@ describe("tui HttpApi bridge", () => { expect(missing.status).toBe(404) }) + test("matches legacy unknown execute command behavior", async () => { + await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) + const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" } + const body = JSON.stringify({ command: "unknown_command" }) + + const legacyCommand = nextCommandExecute() + const legacy = await app(false).request(TuiPaths.executeCommand, { method: "POST", headers, body }) + expect(legacy.status).toBe(200) + expect(await legacy.json()).toBe(true) + + const effectCommand = nextCommandExecute() + const effect = await app().request(TuiPaths.executeCommand, { method: "POST", headers, body }) + expect(effect.status).toBe(200) + expect(await effect.json()).toBe(true) + + const legacyPublished = await legacyCommand + const effectPublished = await effectCommand + expect(effectPublished).toBe(legacyPublished) + expect(legacyPublished).toBeUndefined() + }) + test("serves TUI control queue through experimental Effect routes", async () => { await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) const pending = callTui({ req: { json: async () => ({ value: 1 }), path: "/demo" } } as unknown as Context)