feat(tui): allow backgrounding synchronous subagents (#30488)

This commit is contained in:
Kit Langton
2026-06-04 23:40:52 -04:00
committed by GitHub
parent 8c0edca175
commit 3003867c25
26 changed files with 527 additions and 35 deletions
@@ -2,6 +2,7 @@ import { AccountID, OrgID } from "@/account/schema"
import { MCP } from "@/mcp"
import { Session } from "@/session/session"
import { SessionID } from "@/session/schema"
import { Worktree } from "@/worktree"
import { NonNegativeInt } from "@opencode-ai/core/schema"
import { Schema } from "effect"
@@ -91,6 +92,7 @@ export const ExperimentalPaths = {
worktree: "/experimental/worktree",
worktreeReset: "/experimental/worktree/reset",
session: "/experimental/session",
sessionBackground: "/experimental/session/:sessionID/background",
resource: "/experimental/resource",
} as const
@@ -215,6 +217,19 @@ export const ExperimentalApi = HttpApi.make("experimental")
"Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.",
}),
),
HttpApiEndpoint.post("sessionBackground", ExperimentalPaths.sessionBackground, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Backgrounded subagents"),
error: HttpApiError.BadRequest,
}).annotateMerge(
OpenApi.annotations({
identifier: "experimental.session.background",
summary: "Background subagents",
description:
"Detach any synchronous subagents currently blocking the session and continue them in the background.",
}),
),
HttpApiEndpoint.get("resource", ExperimentalPaths.resource, {
query: WorkspaceRoutingQuery,
success: described(Schema.Record(Schema.String, MCP.Resource), "MCP resources"),
@@ -1,10 +1,13 @@
import { Account } from "@/account/account"
import { Agent } from "@/agent/agent"
import { BackgroundJob } from "@/background/job"
import { Config } from "@/config/config"
import { InstanceState } from "@/effect/instance-state"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { MCP } from "@/mcp"
import { Project } from "@/project/project"
import { Session } from "@/session/session"
import type { SessionID } from "@/session/schema"
import { ToolJsonSchema } from "@/tool/json-schema"
import { ToolRegistry } from "@/tool/registry"
import { Worktree } from "@/worktree"
@@ -30,6 +33,8 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
const registry = yield* ToolRegistry.Service
const worktreeSvc = yield* Worktree.Service
const sessions = yield* Session.Service
const background = yield* BackgroundJob.Service
const flags = yield* RuntimeFlags.Service
const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () {
const [state, groups] = yield* Effect.all(
@@ -146,6 +151,21 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
})
})
const sessionBackground = Effect.fn("ExperimentalHttpApi.sessionBackground")(function* (ctx: {
params: { sessionID: SessionID }
}) {
if (!flags.experimentalBackgroundSubagents) return false
const jobs = (yield* background.list()).filter(
(job) =>
job.type === "task" &&
job.status === "running" &&
job.metadata?.parentSessionId === ctx.params.sessionID &&
job.metadata.background !== true,
)
const promoted = yield* Effect.forEach(jobs, (job) => background.promote(job.id), { concurrency: "unbounded" })
return promoted.some((job) => job !== undefined)
})
const resource = Effect.fn("ExperimentalHttpApi.resource")(function* () {
return yield* mcp.resources()
})
@@ -161,6 +181,7 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
.handle("worktreeRemove", worktreeRemove)
.handle("worktreeReset", worktreeReset)
.handle("session", session)
.handle("sessionBackground", sessionBackground)
.handle("resource", resource)
}),
)
@@ -13,6 +13,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { Account } from "@/account/account"
import { Agent } from "@/agent/agent"
import { Auth } from "@/auth"
import { BackgroundJob } from "@/background/job"
import { Config } from "@/config/config"
import { Command } from "@/command"
import * as Observability from "@opencode-ai/core/effect/observability"
@@ -214,6 +215,7 @@ export function createRoutes(
Account.defaultLayer,
Agent.defaultLayer,
Auth.defaultLayer,
BackgroundJob.defaultLayer,
Command.defaultLayer,
Config.defaultLayer,
Format.defaultLayer,
@@ -20,7 +20,9 @@ export function isLocalWorkspaceRoute(method: string, path: string) {
export function getWorkspaceRouteSessionID(url: URL) {
if (url.pathname === "/session/status") return null
const id = url.pathname.match(/^\/session\/([^/]+)(?:\/|$)/)?.[1]
const id =
url.pathname.match(/^\/session\/([^/]+)(?:\/|$)/)?.[1] ??
url.pathname.match(/^\/experimental\/session\/([^/]+)\/background$/)?.[1]
if (!id) return null
return SessionID.make(id)