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..c39474175282 --- /dev/null +++ b/packages/tui/src/feature-plugins/sidebar/subagents.tsx @@ -0,0 +1,82 @@ +import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { Session } from "@opencode-ai/sdk/v2" +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" + +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() + // 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 running = createMemo(() => children().filter((item) => sync.data.session_status[item.id]?.type !== "idle")) + const idle = createMemo(() => 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