fix(tui): gate background shortcut by capability (#32837)
This commit is contained in:
parent
62c746f2e8
commit
2892e97c57
@ -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"),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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<ThrowOnError extends boolean = false>(
|
||||
parameters?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
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 }))
|
||||
|
||||
@ -2144,6 +2144,10 @@ export type Provider = {
|
||||
}
|
||||
}
|
||||
|
||||
export type ExperimentalCapabilities = {
|
||||
backgroundSubagents: boolean
|
||||
}
|
||||
|
||||
export type ConsoleState = {
|
||||
consoleManagedProviders: Array<string>
|
||||
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
|
||||
|
||||
@ -65,6 +65,9 @@ export const {
|
||||
provider_default: Record<string, string>
|
||||
provider_next: ProviderListResponse
|
||||
console_state: ConsoleState
|
||||
capabilities: {
|
||||
experimentalBackgroundSubagents: boolean
|
||||
}
|
||||
provider_auth: Record<string, ProviderAuthMethod[]>
|
||||
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))
|
||||
|
||||
@ -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()}
|
||||
<span style={{ fg: theme.textMuted }}> view subagents</span>
|
||||
<Show
|
||||
when={props.parts.some(
|
||||
(x) =>
|
||||
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,
|
||||
)
|
||||
}
|
||||
>
|
||||
<span style={{ fg: theme.textMuted }}> · </span>
|
||||
{backgroundShortcut()}
|
||||
|
||||
@ -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 (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user