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_ --- 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": { 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 8e4447364dab..bdc543f8f6fb 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -379,6 +379,13 @@ export function Session() { } } + function goToParent() { + const parentID = session()?.parentID + if (parentID) { + navigate({ type: "session", sessionID: parentID }) + } + } + const command = useCommandDialog() command.register(() => [ { @@ -849,6 +856,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 51e8acf2fa68..facdc41005ed 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,73 @@ 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 (e.button === 0 && 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/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 1a3a7fd9e65b..9c8141bafc4a 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -870,6 +870,10 @@ export type KeybindsConfig = { * Previous child session */ session_child_cycle_reverse?: string + /** + * Go to parent session + */ + session_parent?: string /** * Suspend terminal */ diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index d6cd7131a319..0e48c1ede6b5 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. ---