Skip to content

tui: Subagents tab lists a subagent waiting on its own background shell as inactive #62365

Open

Description

@cevr

Description

The Subagents tab in the composer picker shows a subagent as inactive while it waits on its own background work. A subagent that backgrounds a shell (or a subagent of its own) and ends its turn to wait is idle to the host, so the tab's "active" filter (status === "running") drops it and it shows under inactive with no status, even though its shell is running and it will wake when that ends. Same root cause as #48826, on the TUI side.

Suggested fix: derive activity from live state: running while its turn runs, waiting while a shell it started is still running (the shell registry already has this by session) or a session under it is working. The active filter takes both and the row says Waiting.

Fix with a test on my fork: cevr@799f9b5 (about 50 lines in util/session.ts and subagents-tab.tsx).

Plugins

none

OpenCode version

2.2.5, still on dev @ 885df74

Steps to reproduce

  1. Ask the agent to start a background subagent whose task is: run sleep 120; echo done with the shell tool in the background, then end your turn and wait for it.
  2. Once the subagent says it's waiting, open the composer picker's Subagents tab.
  3. The subagent isn't listed as active (or is listed with no status), although its shell runs for two more minutes and it resumes afterwards.

Operating System

macOS

Terminal

Ghostty

Activity

  1. opencodejr commented on Nov 6, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I reproduced this on 2.2.5, and it still happens on the latest dev (885df74).

    Steps, using a simulated model:

    1. The parent starts a background general subagent.
    2. The subagent starts a background shell (sleep 120; echo done) and ends its turn to wait for it.
    3. With the shell still running, open the composer picker's Subagents tab. The active view shows "No active subagents".
    4. Switch to the inactive view with ctrl+a. The subagent is listed there with no status.

    The recording and screenshots below show this, and the script below reproduces it. The filter in subagents-tab.tsx only treats a subagent as active when its status is running, and an idle subagent waiting on its own background work doesn't match.

    The parent also shows "↳ General finished" while the child's shell is still running. That's the core-side problem tracked in #48826. Related reports, most closely related first:

    recording.mp4

    Screenshot

    Screenshot

    Reproduction script (drive.ts)
    // Repro for #62365: a background subagent waiting on its own background shell
    // is listed as inactive in the composer picker's Subagents tab.
    import { Effect, Stream } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const has = (body: unknown, s: string) => JSON.stringify(body).includes(s)
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true, keypressOverlay: true },
        tools: ["shell"],
      },
      ({ ui, llm, tools }) =>
        Effect.gen(function* () {
          const shells = yield* tools.control("shell")
          yield* llm.serve((req) => {
            const child = has(req.body, "You are a subagent spawned")
            if (child) {
              if (has(req.body, "CHILD_DONE_MARK") || has(req.body, 'state=\\"completed\\"'))
                return Stream.make(Llm.text("Shell finished: done. CHILD_FINAL"))
              if (has(req.body, "call_shell"))
                return Stream.make(Llm.text("Shell is running in the background; waiting for it to finish."))
              return Stream.make(
                Llm.toolCall({
                  index: 0,
                  id: "call_shell",
                  name: "shell",
                  input: { command: "sleep 120; echo done", description: "Sleep then echo", background: true },
                }),
                Llm.finish("tool-calls"),
              )
            }
            if (has(req.body, "call_sub")) return Stream.make(Llm.text("Started the background subagent."))
            return Stream.make(
              Llm.toolCall({
                index: 0,
                id: "call_sub",
                name: "subagent",
                input: {
                  agent: "general",
                  description: "Wait on shell",
                  prompt: "Run `sleep 120; echo done` in the background with the shell tool, then end your turn and wait.",
                  background: true,
                },
              }),
              Llm.finish("tool-calls"),
            )
          })
          yield* ui.submit("Start a background subagent that runs a background shell and waits for it")
          const shell = yield* shells.take("call_shell")
          yield* ui.waitFor("General finished", { timeout: 20000 })
          yield* Effect.sleep(3000)
          yield* ui.screenshot("01-parent-after-spawn")
          // open the subagent picker (composer Subagents tab, active filter)
          yield* ui.press("down")
          yield* Effect.sleep(1500)
          yield* ui.screenshot("02-subagents-active-filter")
          yield* ui.press("a", { ctrl: true })
          yield* Effect.sleep(1500)
          yield* ui.screenshot("03-subagents-inactive-filter")
          yield* ui.press("escape")
          // the child's shell is still running; finish it now
          yield* shell.succeed({ output: "done\n", exit: 0 })
          yield* Effect.sleep(3000)
          yield* ui.screenshot("04-after-shell-finished")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions