From 3dc10a1c165e0a8c567718c33ddd8a62814e0c14 Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 27 Feb 2026 09:41:23 -0500 Subject: [PATCH] Change keybindings to navigate between child sessions (#14814) --- .../src/cli/cmd/tui/routes/session/index.tsx | 86 +++++++++++++------ packages/opencode/src/config/config.ts | 7 +- 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index f20267e08..314018367 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -48,6 +48,7 @@ import type { SkillTool } from "@/tool/skill" import { useKeyboard, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" import { useSDK } from "@tui/context/sdk" import { useCommandDialog } from "@tui/component/dialog-command" +import type { DialogContext } from "@tui/ui/dialog" import { useKeybind } from "@tui/context/keybind" import { Header } from "./header" import { parsePatch } from "diff" @@ -226,6 +227,8 @@ export function Session() { let scroll: ScrollBoxRenderable let prompt: PromptRef const keybind = useKeybind() + const dialog = useDialog() + const renderer = useRenderer() // Allow exit when in child session (prompt is hidden) const exit = useExit() @@ -312,19 +315,40 @@ export function Session() { const local = useLocal() - function moveChild(direction: number) { + function moveFirstChild() { if (children().length === 1) return - let next = children().findIndex((x) => x.id === session()?.id) + direction - if (next >= children().length) next = 0 - if (next < 0) next = children().length - 1 - if (children()[next]) { + const next = children().find((x) => !!x.parentID) + if (next) { navigate({ type: "session", - sessionID: children()[next].id, + sessionID: next.id, }) } } + function moveChild(direction: number) { + if (children().length === 1) return + + const sessions = children().filter((x) => !!x.parentID) + let next = sessions.findIndex((x) => x.id === session()?.id) + direction + + if (next >= sessions.length) next = 0 + if (next < 0) next = sessions.length - 1 + if (sessions[next]) { + navigate({ + type: "session", + sessionID: sessions[next].id, + }) + } + } + + function childSessionHandler(func: (dialog: DialogContext) => void) { + return (dialog: DialogContext) => { + if (!session()?.parentID || dialog.stack.length > 0) return + func(dialog) + } + } + const command = useCommandDialog() command.register(() => [ { @@ -884,24 +908,13 @@ export function Session() { }, }, { - title: "Next child session", - value: "session.child.next", - keybind: "session_child_cycle", + title: "Go to child session", + value: "session.child.first", + keybind: "session_child_first", category: "Session", hidden: true, onSelect: (dialog) => { - moveChild(1) - dialog.clear() - }, - }, - { - title: "Previous child session", - value: "session.child.previous", - keybind: "session_child_cycle_reverse", - category: "Session", - hidden: true, - onSelect: (dialog) => { - moveChild(-1) + moveFirstChild() dialog.clear() }, }, @@ -911,7 +924,7 @@ export function Session() { keybind: "session_parent", category: "Session", hidden: true, - onSelect: (dialog) => { + onSelect: childSessionHandler((dialog) => { const parentID = session()?.parentID if (parentID) { navigate({ @@ -920,7 +933,29 @@ export function Session() { }) } dialog.clear() - }, + }), + }, + { + title: "Next child session", + value: "session.child.next", + keybind: "session_child_cycle", + category: "Session", + hidden: true, + onSelect: childSessionHandler((dialog) => { + moveChild(1) + dialog.clear() + }), + }, + { + title: "Previous child session", + value: "session.child.previous", + keybind: "session_child_cycle_reverse", + category: "Session", + hidden: true, + onSelect: childSessionHandler((dialog) => { + moveChild(-1) + dialog.clear() + }), }, ]) @@ -971,9 +1006,6 @@ export function Session() { } }) - const dialog = useDialog() - const renderer = useRenderer() - // snap to bottom when session changes createEffect(on(() => route.sessionID, toBottom)) @@ -1933,7 +1965,7 @@ function Task(props: ToolProps) { - {keybind.print("session_child_cycle")} + {keybind.print("session_child_first")} view subagents diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 28aea4d67..141f61569 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -896,9 +896,10 @@ export namespace Config { .describe("Delete word backward in input"), history_previous: z.string().optional().default("up").describe("Previous history item"), history_next: z.string().optional().default("down").describe("Next history item"), - session_child_cycle: z.string().optional().default("right").describe("Next child session"), - session_child_cycle_reverse: z.string().optional().default("left").describe("Previous child session"), - session_parent: z.string().optional().default("up").describe("Go to parent session"), + session_child_first: z.string().optional().default("down").describe("Go to first child session"), + session_child_cycle: z.string().optional().default("right").describe("Go to next child session"), + session_child_cycle_reverse: z.string().optional().default("left").describe("Go to previous child session"), + session_parent: z.string().optional().default("up").describe("Go to parent session"), terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"), terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"), tips_toggle: z.string().optional().default("h").describe("Toggle tips on home screen"),