Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/opencode/src/cli/cmd/tui/routes/session/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ export function Header() {
<text fg={theme.text}>
<b>Subagent session</b>
</text>
<text fg={theme.text}>
Parent <span style={{ fg: theme.textMuted }}>{keybind.print("session_parent" as any)}</span>
</text>
<text fg={theme.text}>
Prev <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
</text>
Expand Down
18 changes: 18 additions & 0 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => [
{
Expand Down Expand Up @@ -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)
Expand Down
108 changes: 103 additions & 5 deletions packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -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] ?? [])
Expand All @@ -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<string, ToolPart[]>()
for (const part of taskToolParts()) {
const input = part.state.input as Record<string, unknown>
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", {
Expand All @@ -48,7 +80,6 @@ export function Sidebar(props: { sessionID: string }) {
}
})

const keybind = useKeybind()
const directory = useDirectory()

const hasProviders = createMemo(() =>
Expand Down Expand Up @@ -129,6 +160,73 @@ export function Sidebar(props: { sessionID: string }) {
</Show>
</box>
</Show>
<Show when={subagentGroups().length > 0}>
<box>
<box
flexDirection="row"
gap={1}
onMouseDown={() => subagentGroups().length > 2 && setExpanded("subagents", !expanded.subagents)}
>
<Show when={subagentGroups().length > 2}>
<text fg={theme.text}>{expanded.subagents ? "▼" : "▶"}</text>
</Show>
<text fg={theme.text}>
<b>Subagents</b>
</text>
</box>
<Show when={subagentGroups().length <= 2 || expanded.subagents}>
<For each={subagentGroups()}>
{([agentName, parts]) => {
const hasActive = () =>
parts.some((p) => p.state.status === "running" || p.state.status === "pending")
return (
<box>
<box flexDirection="row" gap={1}>
<text flexShrink={0} style={{ fg: hasActive() ? theme.success : theme.text }}>
</text>
<text fg={theme.text} wrapMode="word">
{agentName}
</text>
</box>
<For each={parts}>
{(part) => {
const isActive = () => part.state.status === "running" || part.state.status === "pending"
const isError = () => part.state.status === "error"
const input = part.state.input as Record<string, unknown>
const description = (input?.description as string) ?? ""
const stateMetadata = (part.state as { metadata?: Record<string, unknown> }).metadata
const sessionId = (part.metadata?.sessionId ?? stateMetadata?.sessionId) as
| string
| undefined
return (
<box
flexDirection="row"
gap={1}
paddingLeft={2}
onMouseUp={(e) => {
if (e.button === 0 && sessionId) {
route.navigate({ type: "session", sessionID: sessionId })
}
}}
>
<text flexShrink={0} fg={isActive() ? theme.success : theme.textMuted}>
{isActive() ? spinnerFrames[spinnerIndex()] : isError() ? "✗" : "✓"}
</text>
<text fg={isActive() ? theme.text : theme.textMuted} wrapMode="word">
{description}
</text>
</box>
)
}}
</For>
</box>
)
}}
</For>
</Show>
</box>
</Show>
<box>
<box
flexDirection="row"
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ export namespace Config {
history_next: z.string().optional().default("down").describe("Next history item"),
session_child_cycle: z.string().optional().default("<leader>right").describe("Next child session"),
session_child_cycle_reverse: z.string().optional().default("<leader>left").describe("Previous child session"),
session_parent: z.string().optional().default("<leader>up").describe("Go to parent session"),
terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"),
})
.strict()
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk/js/src/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,10 @@ export type KeybindsConfig = {
* Previous child session
*/
session_child_cycle_reverse?: string
/**
* Go to parent session
*/
session_parent?: string
/**
* Suspend terminal
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/web/src/content/docs/agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
- **\<Leader>+Up** (or your configured `session_parent` keybind) to go directly to the parent session
- **\<Leader>+Right** (or your configured `session_child_cycle` keybind) to cycle forward through parent → child1 → child2 → ... → parent
- **\<Leader>+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.

---

Expand Down