diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts index 378e0b721..52c714a5a 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts @@ -25,6 +25,10 @@ const ConsoleStateResponse = Schema.Struct({ switchableOrgCount: NonNegativeInt, }).annotate({ identifier: "ConsoleState" }) +const CapabilitiesResponse = Schema.Struct({ + backgroundSubagents: Schema.Boolean, +}).annotate({ identifier: "ExperimentalCapabilities" }) + const ConsoleOrgOption = Schema.Struct({ accountID: Schema.String, accountEmail: Schema.String, @@ -84,6 +88,7 @@ export const SessionListQuery = Schema.Struct({ }) export const ExperimentalPaths = { + capabilities: "/experimental/capabilities", console: "/experimental/console", consoleOrgs: "/experimental/console/orgs", consoleSwitch: "/experimental/console/switch", @@ -100,6 +105,16 @@ export const ExperimentalApi = HttpApi.make("experimental") .add( HttpApiGroup.make("experimental") .add( + HttpApiEndpoint.get("capabilities", ExperimentalPaths.capabilities, { + query: WorkspaceRoutingQuery, + success: described(CapabilitiesResponse, "Experimental capabilities"), + }).annotateMerge( + OpenApi.annotations({ + identifier: "experimental.capabilities.get", + summary: "Get experimental capabilities", + description: "Get experimental features enabled on the OpenCode server.", + }), + ), HttpApiEndpoint.get("console", ExperimentalPaths.console, { query: WorkspaceRoutingQuery, success: described(ConsoleStateResponse, "Active Console provider metadata"), diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts index 57d6464d9..caed54400 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts @@ -36,6 +36,10 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper const background = yield* BackgroundJob.Service const flags = yield* RuntimeFlags.Service + const capabilities = Effect.fn("ExperimentalHttpApi.capabilities")(function* () { + return { backgroundSubagents: flags.experimentalBackgroundSubagents } + }) + const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () { const [state, groups] = yield* Effect.all( [ @@ -171,6 +175,7 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper }) return handlers + .handle("capabilities", capabilities) .handle("console", getConsole) .handle("consoleOrgs", listConsoleOrgs) .handle("consoleSwitch", switchConsole) diff --git a/packages/opencode/test/server/httpapi-exercise/index.ts b/packages/opencode/test/server/httpapi-exercise/index.ts index 3860d742d..e8fdadd8e 100644 --- a/packages/opencode/test/server/httpapi-exercise/index.ts +++ b/packages/opencode/test/server/httpapi-exercise/index.ts @@ -578,6 +578,12 @@ const scenarios: Scenario[] = [ .get("/experimental/session", "experimental.session.list") .at((ctx) => ({ path: "/experimental/session?roots=false&archived=false", headers: ctx.headers() })) .json(200, array), + http.protected + .get("/experimental/capabilities", "experimental.capabilities.get") + .json(200, (body) => { + check(typeof body === "object" && body !== null, "capabilities should be an object") + check("backgroundSubagents" in body, "capabilities should report background subagents") + }), http.protected .post("/experimental/session/{sessionID}/background", "experimental.session.background") .mutating() diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 0b72e620a..7c1c9108d 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -29,6 +29,8 @@ import type { EventTuiPromptAppend, EventTuiSessionSelect, EventTuiToastShow, + ExperimentalCapabilitiesGetErrors, + ExperimentalCapabilitiesGetResponses, ExperimentalConsoleGetErrors, ExperimentalConsoleGetResponses, ExperimentalConsoleListOrgsErrors, @@ -627,6 +629,42 @@ export class ControlPlane extends HeyApiClient { } } +export class Capabilities extends HeyApiClient { + /** + * Get experimental capabilities + * + * Get experimental features enabled on the OpenCode server. + */ + public get( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get< + ExperimentalCapabilitiesGetResponses, + ExperimentalCapabilitiesGetErrors, + ThrowOnError + >({ + url: "/experimental/capabilities", + ...options, + ...params, + }) + } +} + export class Console extends HeyApiClient { /** * Get active Console provider metadata @@ -1180,6 +1218,11 @@ export class Experimental extends HeyApiClient { return (this._controlPlane ??= new ControlPlane({ client: this.client })) } + private _capabilities?: Capabilities + get capabilities(): Capabilities { + return (this._capabilities ??= new Capabilities({ client: this.client })) + } + private _console?: Console get console(): Console { return (this._console ??= new Console({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 093c1894a..e2add116e 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2144,6 +2144,10 @@ export type Provider = { } } +export type ExperimentalCapabilities = { + backgroundSubagents: boolean +} + export type ConsoleState = { consoleManagedProviders: Array activeOrgName?: string @@ -5549,6 +5553,36 @@ export type ConfigProvidersResponses = { export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses] +export type ExperimentalCapabilitiesGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/capabilities" +} + +export type ExperimentalCapabilitiesGetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalCapabilitiesGetError = + ExperimentalCapabilitiesGetErrors[keyof ExperimentalCapabilitiesGetErrors] + +export type ExperimentalCapabilitiesGetResponses = { + /** + * Experimental capabilities + */ + 200: ExperimentalCapabilities +} + +export type ExperimentalCapabilitiesGetResponse = + ExperimentalCapabilitiesGetResponses[keyof ExperimentalCapabilitiesGetResponses] + export type ExperimentalConsoleGetData = { body?: never path?: never diff --git a/packages/tui/src/context/sync.tsx b/packages/tui/src/context/sync.tsx index 4882c1392..d6c6a40eb 100644 --- a/packages/tui/src/context/sync.tsx +++ b/packages/tui/src/context/sync.tsx @@ -65,6 +65,9 @@ export const { provider_default: Record provider_next: ProviderListResponse console_state: ConsoleState + capabilities: { + experimentalBackgroundSubagents: boolean + } provider_auth: Record agent: Agent[] command: Command[] @@ -107,6 +110,9 @@ export const { connected: [], }, console_state: emptyConsoleState, + capabilities: { + experimentalBackgroundSubagents: false, + }, provider_auth: {}, config: {}, status: "loading", @@ -434,6 +440,10 @@ export const { // blocking - include session.list when continuing a session const providersPromise = sdk.client.config.providers({ workspace }, { throwOnError: true }) const providerListPromise = sdk.client.provider.list({ workspace }, { throwOnError: true }) + const capabilitiesPromise = sdk.client.experimental.capabilities + .get({ workspace }, { throwOnError: true }) + .then((x) => x.data) + .catch(() => undefined) const consoleStatePromise = sdk.client.experimental.console .get({ workspace }, { throwOnError: true }) .then((x) => x.data) @@ -443,6 +453,7 @@ export const { await Promise.all([ providersPromise, providerListPromise, + capabilitiesPromise, agentsPromise, configPromise, projectPromise, @@ -451,6 +462,7 @@ export const { .then(async () => { const providersResponse = providersPromise.then((x) => x.data!) const providerListResponse = providerListPromise.then((x) => x.data!) + const capabilitiesResponse = capabilitiesPromise const consoleStateResponse = consoleStatePromise const agentsResponse = agentsPromise.then((x) => x.data ?? []) const configResponse = configPromise.then((x) => x.data!) @@ -459,6 +471,7 @@ export const { return Promise.all([ providersResponse, providerListResponse, + capabilitiesResponse, consoleStateResponse, agentsResponse, configResponse, @@ -466,15 +479,21 @@ export const { ]).then((responses) => { const providers = responses[0] const providerList = responses[1] - const consoleState = responses[2] - const agents = responses[3] - const config = responses[4] - const sessions = responses[5] + const capabilities = responses[2] + const consoleState = responses[3] + const agents = responses[4] + const config = responses[5] + const sessions = responses[6] batch(() => { setStore("provider", reconcile(providers.providers)) setStore("provider_default", reconcile(providers.default)) setStore("provider_next", reconcile(providerList)) + setStore( + "capabilities", + "experimentalBackgroundSubagents", + capabilities?.backgroundSubagents === true, + ) setStore("console_state", reconcile(consoleState)) setStore("agent", reconcile(agents)) setStore("config", reconcile(config)) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 1a1eb0fcc..4d983ee99 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -206,15 +206,17 @@ export function Session() { }) const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) const foregroundTasks = createMemo(() => - messages().flatMap((message) => - (sync.data.part[message.id] ?? []).filter( - (part): part is ToolPart => - part.type === "tool" && - part.tool === "task" && - part.state.status === "running" && - part.state.metadata?.background !== true, - ), - ), + sync.data.capabilities.experimentalBackgroundSubagents + ? messages().flatMap((message) => + (sync.data.part[message.id] ?? []).filter( + (part): part is ToolPart => + part.type === "tool" && + part.tool === "task" && + part.state.status === "running" && + part.state.metadata?.background !== true, + ), + ) + : [], ) const userMessageIDs = createMemo( () => @@ -1510,13 +1512,16 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las {childShortcut()} view subagents - x.type === "tool" && - x.tool === "task" && - x.state.status === "running" && - x.state.metadata?.background !== true, - )} + when={ + sync.data.capabilities.experimentalBackgroundSubagents && + props.parts.some( + (x) => + x.type === "tool" && + x.tool === "task" && + x.state.status === "running" && + x.state.metadata?.background !== true, + ) + } > ยท {backgroundShortcut()} diff --git a/packages/tui/test/fixture/tui-sdk.ts b/packages/tui/test/fixture/tui-sdk.ts index 1d3bdd575..ed18b7acd 100644 --- a/packages/tui/test/fixture/tui-sdk.ts +++ b/packages/tui/test/fixture/tui-sdk.ts @@ -58,6 +58,7 @@ export function createFetch(override?: FetchHandler) { return json({}) if (url.pathname === "/config/providers") return json({ providers: {}, default: {} }) if (url.pathname === "/experimental/console") return json({ consoleManagedProviders: [], switchableOrgCount: 0 }) + if (url.pathname === "/experimental/capabilities") return json({ backgroundSubagents: false }) if (url.pathname === "/path") return json({ home: "", state: "", config: "", worktree, directory }) if (url.pathname === "/api/location") return json({ directory, project: { id: "proj_test", directory: worktree } }) if (