Skip to content

Commit 2892e97

Browse files
authored
fix(tui): gate background shortcut by capability (anomalyco#32837)
1 parent 62c746f commit 2892e97

8 files changed

Lines changed: 148 additions & 20 deletions

File tree

packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ const ConsoleStateResponse = Schema.Struct({
2525
switchableOrgCount: NonNegativeInt,
2626
}).annotate({ identifier: "ConsoleState" })
2727

28+
const CapabilitiesResponse = Schema.Struct({
29+
backgroundSubagents: Schema.Boolean,
30+
}).annotate({ identifier: "ExperimentalCapabilities" })
31+
2832
const ConsoleOrgOption = Schema.Struct({
2933
accountID: Schema.String,
3034
accountEmail: Schema.String,
@@ -84,6 +88,7 @@ export const SessionListQuery = Schema.Struct({
8488
})
8589

8690
export const ExperimentalPaths = {
91+
capabilities: "/experimental/capabilities",
8792
console: "/experimental/console",
8893
consoleOrgs: "/experimental/console/orgs",
8994
consoleSwitch: "/experimental/console/switch",
@@ -100,6 +105,16 @@ export const ExperimentalApi = HttpApi.make("experimental")
100105
.add(
101106
HttpApiGroup.make("experimental")
102107
.add(
108+
HttpApiEndpoint.get("capabilities", ExperimentalPaths.capabilities, {
109+
query: WorkspaceRoutingQuery,
110+
success: described(CapabilitiesResponse, "Experimental capabilities"),
111+
}).annotateMerge(
112+
OpenApi.annotations({
113+
identifier: "experimental.capabilities.get",
114+
summary: "Get experimental capabilities",
115+
description: "Get experimental features enabled on the OpenCode server.",
116+
}),
117+
),
103118
HttpApiEndpoint.get("console", ExperimentalPaths.console, {
104119
query: WorkspaceRoutingQuery,
105120
success: described(ConsoleStateResponse, "Active Console provider metadata"),

packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
3636
const background = yield* BackgroundJob.Service
3737
const flags = yield* RuntimeFlags.Service
3838

39+
const capabilities = Effect.fn("ExperimentalHttpApi.capabilities")(function* () {
40+
return { backgroundSubagents: flags.experimentalBackgroundSubagents }
41+
})
42+
3943
const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () {
4044
const [state, groups] = yield* Effect.all(
4145
[
@@ -171,6 +175,7 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
171175
})
172176

173177
return handlers
178+
.handle("capabilities", capabilities)
174179
.handle("console", getConsole)
175180
.handle("consoleOrgs", listConsoleOrgs)
176181
.handle("consoleSwitch", switchConsole)

packages/opencode/test/server/httpapi-exercise/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,12 @@ const scenarios: Scenario[] = [
578578
.get("/experimental/session", "experimental.session.list")
579579
.at((ctx) => ({ path: "/experimental/session?roots=false&archived=false", headers: ctx.headers() }))
580580
.json(200, array),
581+
http.protected
582+
.get("/experimental/capabilities", "experimental.capabilities.get")
583+
.json(200, (body) => {
584+
check(typeof body === "object" && body !== null, "capabilities should be an object")
585+
check("backgroundSubagents" in body, "capabilities should report background subagents")
586+
}),
581587
http.protected
582588
.post("/experimental/session/{sessionID}/background", "experimental.session.background")
583589
.mutating()

packages/sdk/js/src/v2/gen/sdk.gen.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import type {
2929
EventTuiPromptAppend,
3030
EventTuiSessionSelect,
3131
EventTuiToastShow,
32+
ExperimentalCapabilitiesGetErrors,
33+
ExperimentalCapabilitiesGetResponses,
3234
ExperimentalConsoleGetErrors,
3335
ExperimentalConsoleGetResponses,
3436
ExperimentalConsoleListOrgsErrors,
@@ -627,6 +629,42 @@ export class ControlPlane extends HeyApiClient {
627629
}
628630
}
629631

632+
export class Capabilities extends HeyApiClient {
633+
/**
634+
* Get experimental capabilities
635+
*
636+
* Get experimental features enabled on the OpenCode server.
637+
*/
638+
public get<ThrowOnError extends boolean = false>(
639+
parameters?: {
640+
directory?: string
641+
workspace?: string
642+
},
643+
options?: Options<never, ThrowOnError>,
644+
) {
645+
const params = buildClientParams(
646+
[parameters],
647+
[
648+
{
649+
args: [
650+
{ in: "query", key: "directory" },
651+
{ in: "query", key: "workspace" },
652+
],
653+
},
654+
],
655+
)
656+
return (options?.client ?? this.client).get<
657+
ExperimentalCapabilitiesGetResponses,
658+
ExperimentalCapabilitiesGetErrors,
659+
ThrowOnError
660+
>({
661+
url: "/experimental/capabilities",
662+
...options,
663+
...params,
664+
})
665+
}
666+
}
667+
630668
export class Console extends HeyApiClient {
631669
/**
632670
* Get active Console provider metadata
@@ -1180,6 +1218,11 @@ export class Experimental extends HeyApiClient {
11801218
return (this._controlPlane ??= new ControlPlane({ client: this.client }))
11811219
}
11821220

1221+
private _capabilities?: Capabilities
1222+
get capabilities(): Capabilities {
1223+
return (this._capabilities ??= new Capabilities({ client: this.client }))
1224+
}
1225+
11831226
private _console?: Console
11841227
get console(): Console {
11851228
return (this._console ??= new Console({ client: this.client }))

packages/sdk/js/src/v2/gen/types.gen.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2144,6 +2144,10 @@ export type Provider = {
21442144
}
21452145
}
21462146

2147+
export type ExperimentalCapabilities = {
2148+
backgroundSubagents: boolean
2149+
}
2150+
21472151
export type ConsoleState = {
21482152
consoleManagedProviders: Array<string>
21492153
activeOrgName?: string
@@ -5549,6 +5553,36 @@ export type ConfigProvidersResponses = {
55495553

55505554
export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses]
55515555

5556+
export type ExperimentalCapabilitiesGetData = {
5557+
body?: never
5558+
path?: never
5559+
query?: {
5560+
directory?: string
5561+
workspace?: string
5562+
}
5563+
url: "/experimental/capabilities"
5564+
}
5565+
5566+
export type ExperimentalCapabilitiesGetErrors = {
5567+
/**
5568+
* Bad request
5569+
*/
5570+
400: BadRequestError
5571+
}
5572+
5573+
export type ExperimentalCapabilitiesGetError =
5574+
ExperimentalCapabilitiesGetErrors[keyof ExperimentalCapabilitiesGetErrors]
5575+
5576+
export type ExperimentalCapabilitiesGetResponses = {
5577+
/**
5578+
* Experimental capabilities
5579+
*/
5580+
200: ExperimentalCapabilities
5581+
}
5582+
5583+
export type ExperimentalCapabilitiesGetResponse =
5584+
ExperimentalCapabilitiesGetResponses[keyof ExperimentalCapabilitiesGetResponses]
5585+
55525586
export type ExperimentalConsoleGetData = {
55535587
body?: never
55545588
path?: never

packages/tui/src/context/sync.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ export const {
6565
provider_default: Record<string, string>
6666
provider_next: ProviderListResponse
6767
console_state: ConsoleState
68+
capabilities: {
69+
experimentalBackgroundSubagents: boolean
70+
}
6871
provider_auth: Record<string, ProviderAuthMethod[]>
6972
agent: Agent[]
7073
command: Command[]
@@ -107,6 +110,9 @@ export const {
107110
connected: [],
108111
},
109112
console_state: emptyConsoleState,
113+
capabilities: {
114+
experimentalBackgroundSubagents: false,
115+
},
110116
provider_auth: {},
111117
config: {},
112118
status: "loading",
@@ -434,6 +440,10 @@ export const {
434440
// blocking - include session.list when continuing a session
435441
const providersPromise = sdk.client.config.providers({ workspace }, { throwOnError: true })
436442
const providerListPromise = sdk.client.provider.list({ workspace }, { throwOnError: true })
443+
const capabilitiesPromise = sdk.client.experimental.capabilities
444+
.get({ workspace }, { throwOnError: true })
445+
.then((x) => x.data)
446+
.catch(() => undefined)
437447
const consoleStatePromise = sdk.client.experimental.console
438448
.get({ workspace }, { throwOnError: true })
439449
.then((x) => x.data)
@@ -443,6 +453,7 @@ export const {
443453
await Promise.all([
444454
providersPromise,
445455
providerListPromise,
456+
capabilitiesPromise,
446457
agentsPromise,
447458
configPromise,
448459
projectPromise,
@@ -451,6 +462,7 @@ export const {
451462
.then(async () => {
452463
const providersResponse = providersPromise.then((x) => x.data!)
453464
const providerListResponse = providerListPromise.then((x) => x.data!)
465+
const capabilitiesResponse = capabilitiesPromise
454466
const consoleStateResponse = consoleStatePromise
455467
const agentsResponse = agentsPromise.then((x) => x.data ?? [])
456468
const configResponse = configPromise.then((x) => x.data!)
@@ -459,22 +471,29 @@ export const {
459471
return Promise.all([
460472
providersResponse,
461473
providerListResponse,
474+
capabilitiesResponse,
462475
consoleStateResponse,
463476
agentsResponse,
464477
configResponse,
465478
...(sessionListResponse ? [sessionListResponse] : []),
466479
]).then((responses) => {
467480
const providers = responses[0]
468481
const providerList = responses[1]
469-
const consoleState = responses[2]
470-
const agents = responses[3]
471-
const config = responses[4]
472-
const sessions = responses[5]
482+
const capabilities = responses[2]
483+
const consoleState = responses[3]
484+
const agents = responses[4]
485+
const config = responses[5]
486+
const sessions = responses[6]
473487

474488
batch(() => {
475489
setStore("provider", reconcile(providers.providers))
476490
setStore("provider_default", reconcile(providers.default))
477491
setStore("provider_next", reconcile(providerList))
492+
setStore(
493+
"capabilities",
494+
"experimentalBackgroundSubagents",
495+
capabilities?.backgroundSubagents === true,
496+
)
478497
setStore("console_state", reconcile(consoleState))
479498
setStore("agent", reconcile(agents))
480499
setStore("config", reconcile(config))

packages/tui/src/routes/session/index.tsx

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -206,15 +206,17 @@ export function Session() {
206206
})
207207
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
208208
const foregroundTasks = createMemo(() =>
209-
messages().flatMap((message) =>
210-
(sync.data.part[message.id] ?? []).filter(
211-
(part): part is ToolPart =>
212-
part.type === "tool" &&
213-
part.tool === "task" &&
214-
part.state.status === "running" &&
215-
part.state.metadata?.background !== true,
216-
),
217-
),
209+
sync.data.capabilities.experimentalBackgroundSubagents
210+
? messages().flatMap((message) =>
211+
(sync.data.part[message.id] ?? []).filter(
212+
(part): part is ToolPart =>
213+
part.type === "tool" &&
214+
part.tool === "task" &&
215+
part.state.status === "running" &&
216+
part.state.metadata?.background !== true,
217+
),
218+
)
219+
: [],
218220
)
219221
const userMessageIDs = createMemo(
220222
() =>
@@ -1510,13 +1512,16 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
15101512
{childShortcut()}
15111513
<span style={{ fg: theme.textMuted }}> view subagents</span>
15121514
<Show
1513-
when={props.parts.some(
1514-
(x) =>
1515-
x.type === "tool" &&
1516-
x.tool === "task" &&
1517-
x.state.status === "running" &&
1518-
x.state.metadata?.background !== true,
1519-
)}
1515+
when={
1516+
sync.data.capabilities.experimentalBackgroundSubagents &&
1517+
props.parts.some(
1518+
(x) =>
1519+
x.type === "tool" &&
1520+
x.tool === "task" &&
1521+
x.state.status === "running" &&
1522+
x.state.metadata?.background !== true,
1523+
)
1524+
}
15201525
>
15211526
<span style={{ fg: theme.textMuted }}> · </span>
15221527
{backgroundShortcut()}

packages/tui/test/fixture/tui-sdk.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export function createFetch(override?: FetchHandler) {
5858
return json({})
5959
if (url.pathname === "/config/providers") return json({ providers: {}, default: {} })
6060
if (url.pathname === "/experimental/console") return json({ consoleManagedProviders: [], switchableOrgCount: 0 })
61+
if (url.pathname === "/experimental/capabilities") return json({ backgroundSubagents: false })
6162
if (url.pathname === "/path") return json({ home: "", state: "", config: "", worktree, directory })
6263
if (url.pathname === "/api/location") return json({ directory, project: { id: "proj_test", directory: worktree } })
6364
if (

0 commit comments

Comments
 (0)