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 b6bc424ba14e..6134a40c93f8 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -762,7 +762,7 @@ export function Session() { value: "session.parent", keybind: "session_parent", category: "Session", - disabled: true, + disabled: !session()?.parentID, onSelect: (dialog) => { const parentID = session()?.parentID if (parentID) { 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 508d10838eda..1facc54eb576 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/v2" -import { Global } from "@/global" +import type { AssistantMessage, ToolPart } from "@opencode-ai/sdk/v2" 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,42 @@ export function Sidebar(props: { sessionID: string }) { diff: true, todo: true, lsp: true, + subagents: true, }) + 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 +79,6 @@ export function Sidebar(props: { sessionID: string }) { } }) - const keybind = useKeybind() const directory = useDirectory() const hasProviders = createMemo(() => @@ -176,6 +206,68 @@ 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 sessionId = part.sessionID + return ( + { + route.navigate({ type: "session", sessionID: sessionId }) + }} + > + + {isActive() ? spinnerFrames[spinnerIndex()] : isError() ? "✗" : "✓"} + + + {description} + + + ) + }} + + + ) + }} + + + + 0 && todo().some((t) => t.status !== "completed")}> {directory()} - Open + shuv - Code + code {" "} {Installation.VERSION}