From 3607adce1aacdd7fbe65eb71f3070b5a8f6d5adf Mon Sep 17 00:00:00 2001 From: CreatorGhost Date: Sun, 12 Jul 2026 02:18:54 +0530 Subject: [PATCH 1/2] feat(tui): show subagent status in sidebar Ported from upstream anomalyco/opencode#36042. --- packages/tui/src/feature-plugins/builtins.ts | 2 + .../src/feature-plugins/sidebar/subagents.tsx | 102 ++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 packages/tui/src/feature-plugins/sidebar/subagents.tsx diff --git a/packages/tui/src/feature-plugins/builtins.ts b/packages/tui/src/feature-plugins/builtins.ts index b67923f3c5d1..1295062a8ba4 100644 --- a/packages/tui/src/feature-plugins/builtins.ts +++ b/packages/tui/src/feature-plugins/builtins.ts @@ -6,6 +6,7 @@ import SidebarFiles from "./sidebar/files" import SidebarFooter from "./sidebar/footer" import SidebarLsp from "./sidebar/lsp" import SidebarMcp from "./sidebar/mcp" +import SidebarSubagents from "./sidebar/subagents" import SidebarTodo from "./sidebar/todo" import DiffViewer from "./system/diff-viewer" import Notifications from "./system/notifications" @@ -25,6 +26,7 @@ export function createBuiltinPlugins(options: { experimentalEventSystem: boolean SidebarContext, SidebarMcp, SidebarLsp, + SidebarSubagents, SidebarTodo, SidebarFiles, SidebarFooter, diff --git a/packages/tui/src/feature-plugins/sidebar/subagents.tsx b/packages/tui/src/feature-plugins/sidebar/subagents.tsx new file mode 100644 index 000000000000..230f9b14d264 --- /dev/null +++ b/packages/tui/src/feature-plugins/sidebar/subagents.tsx @@ -0,0 +1,102 @@ +import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { Session } from "@opencode-ai/sdk/v2" +import { createMemo, createResource, For, Match, onCleanup, Show, Switch } from "solid-js" +import { useSync } from "../../context/sync" +import type { BuiltinTuiPlugin } from "../builtins" + +const id = "internal:sidebar-subagents" + +function title(item: Session) { + const value = item.title?.replace(/\s*\(@[^)]* subagent\)\s*$/, "").trim() + if (!value) return item.id.slice(0, 8) + return value.length > 26 ? `${value.slice(0, 25)}...` : value +} + +function View(props: { api: TuiPluginApi; sessionID: string }) { + const theme = () => props.api.theme.current + const sync = useSync() + const [loaded, { refetch }] = createResource( + () => props.sessionID, + async (sessionID) => { + const result = await props.api.client.session.children({ sessionID }, { throwOnError: true }) + return result.data ?? [] + }, + ) + const children = createMemo(() => { + const items = new Map() + for (const item of loaded() ?? []) items.set(item.id, item) + for (const item of sync.data.session) { + if (item.parentID === props.sessionID) items.set(item.id, item) + } + return [...items.values()].toSorted((a, b) => a.time.created - b.time.created) + }) + const refresh = () => void refetch() + const stopUpdated = props.api.event.on("session.updated", (event) => { + if (event.properties.info.parentID !== props.sessionID) return + refresh() + }) + const stopDeleted = props.api.event.on("session.deleted", (event) => { + if (!children().some((item) => item.id === event.properties.info.id)) return + refresh() + }) + onCleanup(() => { + stopUpdated() + stopDeleted() + }) + const running = () => children().filter((item) => sync.data.session_status[item.id]?.type !== "idle") + const idle = () => children().length - running().length + + return ( + 0}> + + + Subagents{" "} + 0 ? theme().warning : theme().textMuted }}> + + 0}> + {running().length} running{idle() > 0 ? `, ${idle()} done` : ""} + + 0}>{idle()} done + + + + + {(item) => { + const active = () => sync.data.session_status[item.id]?.type !== "idle" + return ( + + + {active() ? "•" : "✓"} + + + {title(item)} + + + ) + }} + + 5}> + +{children().length - 5} more + + + + ) +} + +const tui: TuiPlugin = async (api) => { + api.slots.register({ + order: 150, + slots: { + sidebar_content(_ctx, props) { + return + }, + }, + }) +} + +const plugin: BuiltinTuiPlugin = { + id, + tui, +} + +export default plugin From b7ecbee1ec49b9d345dd3e3bd13737400ba8b18f Mon Sep 17 00:00:00 2001 From: CreatorGhost Date: Sun, 12 Jul 2026 22:09:34 +0530 Subject: [PATCH 2/2] fix(tui): show nested subagents in sidebar via collectSubtree Addresses Opus review of PR #38: the sidebar only listed direct children, so nested subagents never appeared. Derive the full subagent subtree reactively from the store via the shared collectSubtree helper, dropping the redundant createResource and manual session.updated/deleted subscriptions (which also removes a stale-snapshot window). --- .../src/feature-plugins/sidebar/subagents.tsx | 40 +++++-------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/packages/tui/src/feature-plugins/sidebar/subagents.tsx b/packages/tui/src/feature-plugins/sidebar/subagents.tsx index 230f9b14d264..c39474175282 100644 --- a/packages/tui/src/feature-plugins/sidebar/subagents.tsx +++ b/packages/tui/src/feature-plugins/sidebar/subagents.tsx @@ -1,7 +1,8 @@ import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" import type { Session } from "@opencode-ai/sdk/v2" -import { createMemo, createResource, For, Match, onCleanup, Show, Switch } from "solid-js" +import { createMemo, For, Match, Show, Switch } from "solid-js" import { useSync } from "../../context/sync" +import { collectSubtree } from "../../routes/session" import type { BuiltinTuiPlugin } from "../builtins" const id = "internal:sidebar-subagents" @@ -15,36 +16,15 @@ function title(item: Session) { function View(props: { api: TuiPluginApi; sessionID: string }) { const theme = () => props.api.theme.current const sync = useSync() - const [loaded, { refetch }] = createResource( - () => props.sessionID, - async (sessionID) => { - const result = await props.api.client.session.children({ sessionID }, { throwOnError: true }) - return result.data ?? [] - }, + // The whole subagent subtree (nested subagents included), derived reactively from + // the store — no resource or manual event subscriptions needed. Exclude the root. + const children = createMemo(() => + collectSubtree(sync.data.session, props.sessionID) + .filter((item) => item.id !== props.sessionID) + .toSorted((a, b) => a.time.created - b.time.created), ) - const children = createMemo(() => { - const items = new Map() - for (const item of loaded() ?? []) items.set(item.id, item) - for (const item of sync.data.session) { - if (item.parentID === props.sessionID) items.set(item.id, item) - } - return [...items.values()].toSorted((a, b) => a.time.created - b.time.created) - }) - const refresh = () => void refetch() - const stopUpdated = props.api.event.on("session.updated", (event) => { - if (event.properties.info.parentID !== props.sessionID) return - refresh() - }) - const stopDeleted = props.api.event.on("session.deleted", (event) => { - if (!children().some((item) => item.id === event.properties.info.id)) return - refresh() - }) - onCleanup(() => { - stopUpdated() - stopDeleted() - }) - const running = () => children().filter((item) => sync.data.session_status[item.id]?.type !== "idle") - const idle = () => children().length - running().length + const running = createMemo(() => children().filter((item) => sync.data.session_status[item.id]?.type !== "idle")) + const idle = createMemo(() => children().length - running().length) return ( 0}>