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 fb62de9acf5f..d26706cf9687 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -80,6 +80,7 @@ import { DialogExportOptions } from "../../ui/dialog-export-options"
import { formatTranscript } from "../../util/transcript"
import { UI } from "@/cli/ui.ts"
import { useTuiConfig } from "../../context/tui-config"
+import { getDirectChildSessions, getSiblingSessions, shouldRenderSessionPrompt } from "./navigation"
addDefaultParsers(parsers.parsers)
@@ -121,20 +122,16 @@ export function Session() {
const { theme } = useTheme()
const promptRef = usePromptRef()
const session = createMemo(() => sync.session.get(route.sessionID))
- const children = createMemo(() => {
- const parentID = session()?.parentID ?? session()?.id
- return sync.data.session
- .filter((x) => x.parentID === parentID || x.id === parentID)
- .toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
- })
+ const directChildren = createMemo(() => getDirectChildSessions(sync.data.session, session()?.id))
+ const siblingSessions = createMemo(() => getSiblingSessions(sync.data.session, session()))
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
const permissions = createMemo(() => {
if (session()?.parentID) return []
- return children().flatMap((x) => sync.data.permission[x.id] ?? [])
+ return directChildren().flatMap((x) => sync.data.permission[x.id] ?? [])
})
const questions = createMemo(() => {
if (session()?.parentID) return []
- return children().flatMap((x) => sync.data.question[x.id] ?? [])
+ return directChildren().flatMap((x) => sync.data.question[x.id] ?? [])
})
const pending = createMemo(() => {
@@ -320,8 +317,7 @@ export function Session() {
const local = useLocal()
function moveFirstChild() {
- if (children().length === 1) return
- const next = children().find((x) => !!x.parentID)
+ const next = directChildren()[0]
if (next) {
navigate({
type: "session",
@@ -331,9 +327,8 @@ export function Session() {
}
function moveChild(direction: number) {
- if (children().length === 1) return
-
- const sessions = children().filter((x) => !!x.parentID)
+ const sessions = siblingSessions()
+ if (sessions.length <= 1) return
let next = sessions.findIndex((x) => x.id === session()?.id) - direction
if (next >= sessions.length) next = 0
@@ -967,6 +962,19 @@ export function Session() {
},
])
+ createEffect(() => {
+ if (
+ shouldRenderSessionPrompt({
+ session: session(),
+ permissionCount: permissions().length,
+ questionCount: questions().length,
+ })
+ ) {
+ return
+ }
+ promptRef.set(undefined)
+ })
+
const revertInfo = createMemo(() => session()?.revert)
const revertMessageID = createMemo(() => revertInfo()?.messageID)
@@ -1162,22 +1170,28 @@ export function Session() {
- {
- prompt = r
- promptRef.set(r)
- // Apply initial prompt when prompt component mounts (e.g., from fork)
- if (route.initialPrompt) {
- r.set(route.initialPrompt)
- }
- }}
- disabled={permissions().length > 0 || questions().length > 0}
- onSubmit={() => {
- toBottom()
- }}
- sessionID={route.sessionID}
- />
+
+ {
+ prompt = r
+ promptRef.set(r)
+ if (route.initialPrompt) {
+ r.set(route.initialPrompt)
+ }
+ }}
+ disabled={permissions().length > 0 || questions().length > 0}
+ onSubmit={() => {
+ toBottom()
+ }}
+ sessionID={route.sessionID}
+ />
+
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/navigation.ts b/packages/opencode/src/cli/cmd/tui/routes/session/navigation.ts
new file mode 100644
index 000000000000..ae37002cdc3b
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/navigation.ts
@@ -0,0 +1,26 @@
+type SessionNode = {
+ id: string
+ parentID?: string | null
+}
+
+function byID(a: SessionNode, b: SessionNode) {
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
+}
+
+export function getDirectChildSessions(sessions: readonly T[], sessionID?: string): T[] {
+ if (!sessionID) return []
+ return sessions.filter((session) => session.parentID === sessionID).toSorted(byID)
+}
+
+export function getSiblingSessions(sessions: readonly T[], session?: T): T[] {
+ if (!session?.parentID) return []
+ return sessions.filter((candidate) => candidate.parentID === session.parentID).toSorted(byID)
+}
+
+export function shouldRenderSessionPrompt(input: {
+ session?: SessionNode
+ permissionCount: number
+ questionCount: number
+}) {
+ return !input.session?.parentID && input.permissionCount === 0 && input.questionCount === 0
+}
diff --git a/packages/opencode/test/cli/tui/session-navigation.test.ts b/packages/opencode/test/cli/tui/session-navigation.test.ts
new file mode 100644
index 000000000000..3fc2f1b38b7f
--- /dev/null
+++ b/packages/opencode/test/cli/tui/session-navigation.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, test } from "bun:test"
+
+const { getDirectChildSessions, getSiblingSessions, shouldRenderSessionPrompt } = await import(
+ "../../../src/cli/cmd/tui/routes/session/navigation"
+)
+
+describe("session navigation helpers", () => {
+ const sessions = [
+ { id: "root" },
+ { id: "child-a", parentID: "root" },
+ { id: "child-b", parentID: "root" },
+ { id: "grandchild-a", parentID: "child-a" },
+ ]
+
+ test("collects only direct children for the active session", () => {
+ expect(getDirectChildSessions(sessions, "root").map((session) => session.id)).toEqual(["child-a", "child-b"])
+ expect(getDirectChildSessions(sessions, "child-a").map((session) => session.id)).toEqual(["grandchild-a"])
+ })
+
+ test("collects sibling sessions for subagent cycling", () => {
+ expect(getSiblingSessions(sessions, sessions[1]).map((session) => session.id)).toEqual(["child-a", "child-b"])
+ expect(getSiblingSessions(sessions, sessions[0])).toEqual([])
+ })
+
+ test("only renders the prompt for root sessions without blocking UI", () => {
+ expect(shouldRenderSessionPrompt({ session: sessions[0], permissionCount: 0, questionCount: 0 })).toBe(true)
+ expect(shouldRenderSessionPrompt({ session: sessions[1], permissionCount: 0, questionCount: 0 })).toBe(false)
+ expect(shouldRenderSessionPrompt({ session: sessions[0], permissionCount: 1, questionCount: 0 })).toBe(false)
+ })
+})