Skip to content

TUI: session.child.next/previous keybind defaults are dead commands #56153

Open

Description

@Addin

Summary

session.child.next / session.child.previous keybind defaults (right/left) are dead commands — no command with those IDs is registered anywhere in packages/tui/src.

Root cause

packages/tui/src/config/keybind.ts:139-140 defines the keybinds, but no command with those IDs is registered in packages/tui/src (grep confirms only the definitions exist).

Meanwhile down/up toggle the composer/navigate to parent session (routes/session/index.tsx:1261-1294), which is surprising for plain arrow keys.

Suggested fix

Either register commands for session.child.next/session.child.previous, or remove the dead keybind defaults.

Activity

  1. opencode-agent commented on Oct 16, 2026

    @opencode-agent
    Contributor

    Thanks for the report. I reproduced this on v2.0.31 with two subagent sessions. I opened one child from the subagent picker (down, then ctrl+a to show finished subagents, then return) and pressed right/left. The view stayed on the same child. With the picker open, the arrows only switch its tabs (Subagents/Shell). After closing it with escape, which goes back to the parent, they do nothing. The recording and the script below show this.

    The code matches what you describe: packages/tui/src/config/keybind.ts binds session.child.next/session.child.previous, but no command with those IDs is registered in packages/tui/src. I haven't re-run this on the latest dev build yet; the dev checkout at 582c974 still has the same definitions with no matching commands.

    Related: #56154 covers the up/down behaviour you mention.

    recording.mp4

    Screenshot

    Screenshot

    Reproduction script (drive.ts)
    // Repro for #56153: `right`/`left` (session.child.next/previous) do nothing in a child session.
    import { Effect, Stream } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true, keypressOverlay: true },
      },
      ({ ui, llm }) =>
        Effect.gen(function* () {
          yield* llm.serve((_req, index) =>
            index === 0
              ? Stream.make(
                  Llm.toolCall({ index: 0, id: "call_a", name: "subagent", input: { agent: "general", description: "Alpha child task", prompt: "Say alpha" } }),
                  Llm.toolCall({ index: 1, id: "call_b", name: "subagent", input: { agent: "general", description: "Beta child task", prompt: "Say beta" } }),
                  Llm.finish("tool-calls"),
                )
              : Stream.make(Llm.text(`Done ${index}.`)),
          )
          yield* ui.submit("Run two subagents")
          yield* ui.waitFor("Beta child task", { timeout: 30000 })
          yield* Effect.sleep(3000)
          yield* ui.screenshot("01-parent-with-children")
          // open subagent picker and enter the first child
          yield* ui.press("down")
          yield* Effect.sleep(800)
          yield* ui.press("a", { ctrl: true }) // show inactive (finished) subagents
          yield* Effect.sleep(1000)
          yield* ui.screenshot("02-subagent-picker")
          yield* ui.press("return")
          yield* Effect.sleep(1500)
          const before = yield* ui.capture()
          yield* ui.screenshot("03-in-child")
          // session.child.next default = right: expected to go to the other child (Alpha); instead it only switches the picker tab
          yield* ui.press("right")
          yield* Effect.sleep(1500)
          const afterRight = yield* ui.capture()
          yield* ui.screenshot("04-after-right")
          yield* ui.press("left")
          yield* Effect.sleep(1500)
          const afterLeft = yield* ui.capture()
          yield* ui.screenshot("05-after-left")
          const text = (f: unknown) => JSON.stringify(f)
          yield* Effect.log(`screen unchanged after right: ${text(before) === text(afterRight)}`)
          yield* Effect.log(`screen unchanged after left: ${text(afterRight) === text(afterLeft)}`)
        }),
    )

    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.

  2. Dante-dan commented on Oct 16, 2026

    @Dante-dan
    Contributor

    The child session view keeps the subagent picker open, where left/right switch composer tabs. Adding handlers only to the session’s base keymap would still leave them unreachable there. I’m taking the suggested alternative: disable these two unused defaults and mark them deprecated, while retaining their configuration names for compatibility. The picker continues to provide child-session navigation.

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