diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx
index eb780f521bd..cc58f0f30e2 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx
@@ -81,6 +81,9 @@ export function Header() {
Subagent session
+
+ Parent {keybind.print("session_parent" as any)}
+
Prev {keybind.print("session_child_cycle_reverse")}
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 fcf562782a4..3bd4ca4fc59 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -231,6 +231,13 @@ export function Session() {
}
}
+ function goToParent() {
+ const parentID = session()?.parentID
+ if (parentID) {
+ navigate({ type: "session", sessionID: parentID })
+ }
+ }
+
const command = useCommandDialog()
command.register(() => [
{
@@ -679,6 +686,17 @@ export function Session() {
dialog.clear()
},
},
+ {
+ title: "Go to parent session",
+ value: "session.parent",
+ keybind: "session_parent" as any,
+ category: "Session",
+ disabled: !session()?.parentID,
+ onSelect: (dialog) => {
+ goToParent()
+ dialog.clear()
+ },
+ },
])
const revertInfo = createMemo(() => session()?.revert)
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
index c63f5116ab2..2e5e6787f3d 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
@@ -1,17 +1,17 @@
import { useSync } from "@tui/context/sync"
-import { createMemo, For, Show, Switch, Match } from "solid-js"
+import { createMemo, For, Show, Switch, Match, createSignal, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { useTheme } from "../../context/theme"
+import { useRoute } from "../../context/route"
import { Locale } from "@/util/locale"
import path from "path"
-import type { AssistantMessage } from "@opencode-ai/sdk"
-import { Global } from "@/global"
+import type { AssistantMessage, ToolPart } from "@opencode-ai/sdk"
import { Installation } from "@/installation"
-import { useKeybind } from "../../context/keybind"
import { useDirectory } from "../../context/directory"
export function Sidebar(props: { sessionID: string }) {
const sync = useSync()
+ const route = useRoute()
const { theme } = useTheme()
const session = createMemo(() => sync.session.get(props.sessionID)!)
const diff = createMemo(() => sync.data.session_diff[props.sessionID] ?? [])
@@ -23,11 +23,43 @@ export function Sidebar(props: { sessionID: string }) {
diff: true,
todo: true,
lsp: true,
+ subagents: true,
})
+ // Animated spinner
+ const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
+ const [spinnerIndex, setSpinnerIndex] = createSignal(0)
+
+ const intervalId = setInterval(() => {
+ setSpinnerIndex((prev) => (prev + 1) % spinnerFrames.length)
+ }, 100)
+ onCleanup(() => clearInterval(intervalId))
+
// Sort MCP servers alphabetically for consistent display order
const mcpEntries = createMemo(() => Object.entries(sync.data.mcp).sort(([a], [b]) => a.localeCompare(b)))
+ const taskToolParts = createMemo(() => {
+ const parts: ToolPart[] = []
+ for (const message of messages()) {
+ for (const part of sync.data.part[message.id] ?? []) {
+ if (part.type === "tool" && part.tool === "task") parts.push(part)
+ }
+ }
+ return parts
+ })
+
+ const subagentGroups = createMemo(() => {
+ const groups = new Map()
+ for (const part of taskToolParts()) {
+ const input = part.state.input as Record
+ const agentName = input?.subagent_type as string
+ if (!agentName) continue
+ if (!groups.has(agentName)) groups.set(agentName, [])
+ groups.get(agentName)!.push(part)
+ }
+ return Array.from(groups.entries())
+ })
+
const cost = createMemo(() => {
const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
return new Intl.NumberFormat("en-US", {
@@ -48,7 +80,6 @@ export function Sidebar(props: { sessionID: string }) {
}
})
- const keybind = useKeybind()
const directory = useDirectory()
const hasProviders = createMemo(() =>
@@ -129,6 +160,73 @@ export function Sidebar(props: { sessionID: string }) {
+ 0}>
+
+ subagentGroups().length > 2 && setExpanded("subagents", !expanded.subagents)}
+ >
+ 2}>
+ {expanded.subagents ? "▼" : "▶"}
+
+
+ Subagents
+
+
+
+
+ {([agentName, parts]) => {
+ const hasActive = () =>
+ parts.some((p) => p.state.status === "running" || p.state.status === "pending")
+ return (
+
+
+
+ •
+
+
+ {agentName}
+
+
+
+ {(part) => {
+ const isActive = () => part.state.status === "running" || part.state.status === "pending"
+ const isError = () => part.state.status === "error"
+ const input = part.state.input as Record
+ const description = (input?.description as string) ?? ""
+ const stateMetadata = (part.state as { metadata?: Record }).metadata
+ const sessionId = (part.metadata?.sessionId ?? stateMetadata?.sessionId) as
+ | string
+ | undefined
+ return (
+ {
+ if (e.button === 0 && sessionId) {
+ route.navigate({ type: "session", sessionID: sessionId })
+ }
+ }}
+ >
+
+ {isActive() ? spinnerFrames[spinnerIndex()] : isError() ? "✗" : "✓"}
+
+
+ {description}
+
+
+ )
+ }}
+
+
+ )
+ }}
+
+
+
+
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"),
terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"),
})
.strict()
diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts
index bf23f77ecad..b3c16f605bb 100644
--- a/packages/sdk/js/src/gen/types.gen.ts
+++ b/packages/sdk/js/src/gen/types.gen.ts
@@ -864,6 +864,10 @@ export type KeybindsConfig = {
* Previous child session
*/
session_child_cycle_reverse?: string
+ /**
+ * Go to parent session
+ */
+ session_parent?: string
/**
* Suspend terminal
*/
diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx
index f63457cc026..ffe712ccd41 100644
--- a/packages/web/src/content/docs/agents.mdx
+++ b/packages/web/src/content/docs/agents.mdx
@@ -89,10 +89,11 @@ A general-purpose agent for researching complex questions, searching for code, a
```
3. **Navigation between sessions**: When subagents create their own child sessions, you can navigate between the parent session and all child sessions using:
+ - **\+Up** (or your configured `session_parent` keybind) to go directly to the parent session
- **\+Right** (or your configured `session_child_cycle` keybind) to cycle forward through parent → child1 → child2 → ... → parent
- **\+Left** (or your configured `session_child_cycle_reverse` keybind) to cycle backward through parent ← child1 ← child2 ← ... ← parent
- This allows you to seamlessly switch between the main conversation and specialized subagent work.
+ You can also click on any subagent task in the sidebar to navigate directly to that subagent's session.
---