From e768f4c19589ca9328a5cd15f12747d7af07a2e0 Mon Sep 17 00:00:00 2001 From: franlol Date: Fri, 28 Nov 2025 14:41:52 +0100 Subject: [PATCH 1/5] feat: add subagents sidebar with clickable navigation and parent keybind --- .../src/cli/cmd/tui/routes/session/header.tsx | 3 + .../src/cli/cmd/tui/routes/session/index.tsx | 18 +++ .../cli/cmd/tui/routes/session/sidebar.tsx | 106 +++++++++++++++++- packages/opencode/src/config/config.ts | 1 + packages/web/src/content/docs/agents.mdx | 3 +- 5 files changed, 125 insertions(+), 6 deletions(-) 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 eb780f521bda..cc58f0f30e21 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 fdbcb34f9df9..06d7117b4a9e 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(() => [ { @@ -675,6 +682,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 c63f5116ab25..010f79278276 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,71 @@ 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 (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/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index f63457cc026a..ffe712ccd418 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. --- From ef953819e3fc7c4e14d8db9c215b97577bc5dd7a Mon Sep 17 00:00:00 2001 From: franlol Date: Sat, 29 Nov 2025 01:03:32 +0100 Subject: [PATCH 2/5] fix: fix sdk --- packages/sdk/js/src/gen/types.gen.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index bf23f77ecadf..b3c16f605bb8 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 */ From 62e6469db8549b2b3f6b8428e76d868ecc3cd373 Mon Sep 17 00:00:00 2001 From: franlol Date: Sat, 29 Nov 2025 01:40:04 +0100 Subject: [PATCH 3/5] fix: navigation click propagation --- .../opencode/src/cli/cmd/tui/routes/session/sidebar.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 010f79278276..2e5e6787f3d0 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -204,8 +204,10 @@ export function Sidebar(props: { sessionID: string }) { flexDirection="row" gap={1} paddingLeft={2} - onMouseDown={() => { - if (sessionId) route.navigate({ type: "session", sessionID: sessionId }) + onMouseUp={(e) => { + if (e.button === 0 && sessionId) { + route.navigate({ type: "session", sessionID: sessionId }) + } }} > From a07bc1b11d41eb6237e26558edcf29383869c271 Mon Sep 17 00:00:00 2001 From: Kyle Crommett Date: Sun, 30 Nov 2025 16:47:14 -0800 Subject: [PATCH 4/5] docs: add PR #4865 to merged PRs table --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index acbe7b2a4d67..cab67fe0d0e1 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,16 @@ This fork serves as an integration testing ground for upstream PRs before they a The following PRs have been merged into this fork and are awaiting merge into upstream: -| PR | Title | Status | Description | -| -------------------------------------------------- | --------------------------------- | ------ | ------------------------------------------------------------------ | -| [#4898](https://github.com/sst/opencode/pull/4898) | Search in messages | Open | Ctrl+F to search through session messages with highlighting | -| [#4791](https://github.com/sst/opencode/pull/4791) | Bash output with ANSI | Open | Full terminal emulation for bash output with color support | -| [#4900](https://github.com/sst/opencode/pull/4900) | Double Ctrl+C to exit | Open | Require double Ctrl+C within 2 seconds to prevent accidental exits | -| [#4709](https://github.com/sst/opencode/pull/4709) | Live token usage during streaming | Open | Real-time token tracking and display during model responses | -| [#4773](https://github.com/sst/opencode/pull/4773) | Configurable subagent visibility | Open | Allow agents to restrict which subagents they can invoke | - -_Last updated: 2025-11-29_ +| PR | Title | Status | Description | +| -------------------------------------------------- | ------------------------------------------- | ------ | ------------------------------------------------------------------- | +| [#4898](https://github.com/sst/opencode/pull/4898) | Search in messages | Open | Ctrl+F to search through session messages with highlighting | +| [#4791](https://github.com/sst/opencode/pull/4791) | Bash output with ANSI | Open | Full terminal emulation for bash output with color support | +| [#4900](https://github.com/sst/opencode/pull/4900) | Double Ctrl+C to exit | Open | Require double Ctrl+C within 2 seconds to prevent accidental exits | +| [#4709](https://github.com/sst/opencode/pull/4709) | Live token usage during streaming | Open | Real-time token tracking and display during model responses | +| [#4773](https://github.com/sst/opencode/pull/4773) | Configurable subagent visibility | Open | Allow agents to restrict which subagents they can invoke | +| [#4865](https://github.com/sst/opencode/pull/4865) | Subagents sidebar with clickable navigation | Open | Show subagents in sidebar with click-to-navigate and parent keybind | + +_Last updated: 2025-11-30_ --- From ba0dccec71ebb7b556171904221e93c504471b19 Mon Sep 17 00:00:00 2001 From: Github Action Date: Mon, 1 Dec 2025 00:47:23 +0000 Subject: [PATCH 5/5] Update Nix flake.lock and hashes --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 211be53aa99b..3a6b887186db 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1764445028, - "narHash": "sha256-ik6H/0Zl+qHYDKTXFPpzuVHSZE+uvVz2XQuQd1IVXzo=", + "lastModified": 1764527385, + "narHash": "sha256-nA5ywiGKl76atrbdZ5Aucd8SjF/v8ew9b9QsC+MKL14=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a09378c0108815dbf3961a0e085936f4146ec415", + "rev": "23258e03aaa49b3a68597e3e50eb0cbce7e42e9d", "type": "github" }, "original": {