diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 5214b0c1a9a..7b6ffe621f4 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -19,6 +19,7 @@ import { DialogHelp } from "./ui/dialog-help"
import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command"
import { DialogAgent } from "@tui/component/dialog-agent"
import { DialogSessionList } from "@tui/component/dialog-session-list"
+import { DialogSubagentList } from "@tui/component/dialog-subagent-list"
import { KeybindProvider } from "@tui/context/keybind"
import { ThemeProvider, useTheme } from "@tui/context/theme"
import { Home } from "@tui/routes/home"
@@ -297,6 +298,19 @@ function App() {
dialog.clear()
},
},
+ {
+ title: "List subagents",
+ value: "session.subagents",
+ keybind: "session_subagents",
+ category: "Session",
+ disabled: route.data.type !== "session",
+ onSelect: () => {
+ const data = route.data
+ if (data.type === "session") {
+ dialog.replace(() => )
+ }
+ },
+ },
{
title: "Switch model",
value: "model.list",
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-subagent-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-subagent-list.tsx
new file mode 100644
index 00000000000..0e4e51571f0
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-subagent-list.tsx
@@ -0,0 +1,144 @@
+import { useDialog } from "@tui/ui/dialog"
+import { DialogSelect } from "@tui/ui/dialog-select"
+import { useRoute } from "@tui/context/route"
+import { useSync } from "@tui/context/sync"
+import { createMemo, createSignal, onMount, Show } from "solid-js"
+import { useTheme } from "../context/theme"
+import { useKV } from "../context/kv"
+import { useSDK } from "../context/sdk"
+import { Keybind } from "@/util/keybind"
+import { DialogSessionRename } from "./dialog-session-rename"
+import { buildSubagentOptions, findRootSession, getStatusIndicator, formatTimeAgo } from "../util/subagent-tree"
+import "opentui-spinner/solid"
+
+function getTreePrefix(depth: number): string {
+ if (depth === 0) return ""
+ return " ".repeat(depth - 1) + "└─ "
+}
+
+export function DialogSubagentList(props: { sessionID: string }) {
+ const dialog = useDialog()
+ const sync = useSync()
+ const { theme } = useTheme()
+ const route = useRoute()
+ const kv = useKV()
+ const sdk = useSDK()
+
+ const [toDelete, setToDelete] = createSignal()
+ const deleteKeybind = "ctrl+d"
+
+ const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
+
+ const rootSession = createMemo(() => findRootSession(sync.data.session, props.sessionID))
+
+ const options = createMemo(() => {
+ const root = rootSession()
+ const subagents = buildSubagentOptions(sync.data.session, sync.data.session_status ?? {}, props.sessionID)
+
+ const rootOption = root
+ ? (() => {
+ const statusInfo = getStatusIndicator(sync.data.session_status?.[root.id])
+ const isRunning = statusInfo.status === "busy"
+ const isDeleting = toDelete() === root.id
+ const statusColor =
+ statusInfo.status === "busy" ? theme.primary : statusInfo.status === "retry" ? theme.warning : theme.success
+ return {
+ title: isDeleting ? `Press ${deleteKeybind} again to confirm` : root.title,
+ value: root.id,
+ description: "(main)",
+ bg: isDeleting ? theme.error : undefined,
+ gutter: isRunning ? (
+ [⋯]}>
+
+
+ ) : (
+ {statusInfo.icon}
+ ),
+ }
+ })()
+ : null
+
+ const subagentOptions = subagents.map((sub) => {
+ const isRunning = sub.status === "busy"
+ const isDeleting = toDelete() === sub.id
+ const statusColor = sub.status === "busy" ? theme.primary : sub.status === "retry" ? theme.warning : theme.success
+ const prefix = getTreePrefix(sub.depth + 1)
+
+ return {
+ title: isDeleting ? `Press ${deleteKeybind} again to confirm` : prefix + sub.title,
+ value: sub.id,
+ description: sub.timeAgo,
+ bg: isDeleting ? theme.error : undefined,
+ gutter: isRunning ? (
+ [⋯]}>
+
+
+ ) : (
+ {sub.statusIcon}
+ ),
+ }
+ })
+
+ return rootOption ? [rootOption, ...subagentOptions] : subagentOptions
+ })
+
+ const hasTree = createMemo(() => options().length > 0)
+
+ onMount(() => {
+ dialog.setSize("large")
+ })
+
+ return (
+
+
+ Subagent Tree
+
+ No subagents in this session tree
+ Subagents are created when the agent delegates tasks
+
+ }
+ >
+ {
+ setToDelete(undefined)
+ }}
+ onSelect={(option) => {
+ route.navigate({
+ type: "session",
+ sessionID: option.value,
+ })
+ dialog.clear()
+ }}
+ keybind={[
+ {
+ keybind: Keybind.parse(deleteKeybind)[0],
+ title: "delete",
+ onTrigger: async (option) => {
+ if (toDelete() === option.value) {
+ sdk.client.session.delete({
+ sessionID: option.value,
+ })
+ setToDelete(undefined)
+ return
+ }
+ setToDelete(option.value)
+ },
+ },
+ {
+ keybind: Keybind.parse("ctrl+r")[0],
+ title: "rename",
+ onTrigger: async (option) => {
+ dialog.replace(() => )
+ },
+ },
+ ]}
+ />
+
+ )
+}
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
index a5823289505..a14b3a7a9ca 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
@@ -326,6 +326,11 @@ export function Autocomplete(props: {
description: "toggle thinking visibility",
onSelect: () => command.trigger("session.toggle.thinking"),
},
+ {
+ display: "/subagents",
+ description: "list subagents in this session",
+ onSelect: () => command.trigger("session.subagents"),
+ },
)
if (sync.data.config.share !== "disabled") {
results.push({
diff --git a/packages/opencode/src/cli/cmd/tui/util/subagent-tree.ts b/packages/opencode/src/cli/cmd/tui/util/subagent-tree.ts
new file mode 100644
index 00000000000..176432567df
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/util/subagent-tree.ts
@@ -0,0 +1,131 @@
+import type { Session, SessionStatus } from "@opencode-ai/sdk/v2"
+
+export type SubagentOption = {
+ id: string
+ title: string
+ agentType: string
+ status: "idle" | "busy" | "retry"
+ statusIcon: string
+ timeAgo: string
+ depth: number
+ isCurrent: boolean
+}
+
+export function filterSubagents(sessions: Session[], parentID: string): Session[] {
+ return sessions.filter((s) => s.parentID === parentID)
+}
+
+export function getStatusIndicator(status: SessionStatus | undefined): {
+ icon: string
+ status: "idle" | "busy" | "retry"
+} {
+ if (!status) return { icon: "●", status: "idle" }
+ if (status.type === "busy") return { icon: "◐", status: "busy" }
+ if (status.type === "retry") return { icon: "↻", status: "retry" }
+ return { icon: "●", status: "idle" }
+}
+
+export function formatTimeAgo(timestamp: number): string {
+ const now = Date.now()
+ const diff = now - timestamp
+ const seconds = Math.floor(diff / 1000)
+ const minutes = Math.floor(seconds / 60)
+ const hours = Math.floor(minutes / 60)
+
+ if (hours > 0) return `${hours}h ago`
+ if (minutes > 0) return `${minutes}m ago`
+ if (seconds > 10) return `${seconds}s ago`
+ return "now"
+}
+
+export function extractAgentType(session: Session): string {
+ const colonIndex = session.title.indexOf(":")
+ if (colonIndex > 0 && colonIndex < 20) {
+ return session.title.slice(0, colonIndex).trim()
+ }
+ return "subagent"
+}
+
+export function findRootSession(sessions: Session[], sessionID: string): Session | undefined {
+ const sessionMap = new Map(sessions.map((s) => [s.id, s]))
+ let current = sessionMap.get(sessionID)
+
+ while (current?.parentID) {
+ const parent = sessionMap.get(current.parentID)
+ if (!parent) break
+ current = parent
+ }
+
+ return current
+}
+
+function buildTreeRecursive(
+ sessions: Session[],
+ statuses: Record,
+ parentID: string,
+ depth: number,
+ currentSessionID: string,
+): SubagentOption[] {
+ const children = filterSubagents(sessions, parentID)
+ const sorted = [...children].sort((a, b) => a.time.created - b.time.created)
+ const result: SubagentOption[] = []
+
+ for (const session of sorted) {
+ const statusInfo = getStatusIndicator(statuses[session.id])
+ result.push({
+ id: session.id,
+ title: session.title,
+ agentType: extractAgentType(session),
+ status: statusInfo.status,
+ statusIcon: statusInfo.icon,
+ timeAgo: formatTimeAgo(session.time.updated),
+ depth,
+ isCurrent: session.id === currentSessionID,
+ })
+ const childOptions = buildTreeRecursive(sessions, statuses, session.id, depth + 1, currentSessionID)
+ result.push(...childOptions)
+ }
+
+ return result
+}
+
+export function buildSubagentOptions(
+ sessions: Session[],
+ statuses: Record,
+ currentSessionID: string,
+): SubagentOption[] {
+ const root = findRootSession(sessions, currentSessionID)
+ if (!root) return []
+
+ return buildTreeRecursive(sessions, statuses, root.id, 0, currentSessionID)
+}
+
+export function hasSubagents(sessions: Session[], parentID: string): boolean {
+ return sessions.some((s) => s.parentID === parentID)
+}
+
+export function countSubagents(sessions: Session[], parentID: string): number {
+ return sessions.filter((s) => s.parentID === parentID).length
+}
+
+export function countAllDescendants(sessions: Session[], parentID: string): number {
+ const direct = filterSubagents(sessions, parentID)
+ let count = direct.length
+ for (const child of direct) {
+ count += countAllDescendants(sessions, child.id)
+ }
+ return count
+}
+
+export function isPartOfTree(sessions: Session[], rootID: string, sessionID: string): boolean {
+ if (rootID === sessionID) return true
+ const sessionMap = new Map(sessions.map((s) => [s.id, s]))
+ let current = sessionMap.get(sessionID)
+
+ while (current?.parentID) {
+ if (current.parentID === rootID) return true
+ current = sessionMap.get(current.parentID)
+ }
+
+ return false
+}
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 807cd46fd26..bf83679372e 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -572,6 +572,7 @@ export namespace Config {
session_child_cycle: z.string().optional().default("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"),
+ session_subagents: z.string().optional().default("none").describe("List subagents in current session"),
terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"),
terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"),
tips_toggle: z.string().optional().default("h").describe("Toggle tips on home screen"),
diff --git a/packages/opencode/test/cli/subagent-tree.test.ts b/packages/opencode/test/cli/subagent-tree.test.ts
new file mode 100644
index 00000000000..b78f67d6f8c
--- /dev/null
+++ b/packages/opencode/test/cli/subagent-tree.test.ts
@@ -0,0 +1,505 @@
+import { describe, expect, test } from "bun:test"
+import {
+ filterSubagents,
+ getStatusIndicator,
+ formatTimeAgo,
+ extractAgentType,
+ buildSubagentOptions,
+ hasSubagents,
+ countSubagents,
+ countAllDescendants,
+ findRootSession,
+} from "../../src/cli/cmd/tui/util/subagent-tree"
+import type { Session, SessionStatus } from "@opencode-ai/sdk/v2"
+
+const createMockSession = (overrides: Partial = {}): Session => ({
+ id: "ses_test",
+ projectID: "proj_test",
+ directory: "/test",
+ title: "Test Session",
+ version: "1.0.0",
+ time: {
+ created: Date.now() - 60000,
+ updated: Date.now(),
+ },
+ ...overrides,
+})
+
+describe("filterSubagents", () => {
+ test("returns only direct children of specified parent", () => {
+ const sessions = [
+ createMockSession({ id: "root", parentID: undefined }),
+ createMockSession({ id: "child1", parentID: "root" }),
+ createMockSession({ id: "child2", parentID: "root" }),
+ createMockSession({ id: "grandchild", parentID: "child1" }),
+ ]
+
+ const result = filterSubagents(sessions, "root")
+
+ expect(result.map((x) => x.id)).toEqual(["child1", "child2"])
+ })
+
+ test("returns empty array when no children exist", () => {
+ const sessions = [createMockSession({ id: "root", parentID: undefined })]
+
+ const result = filterSubagents(sessions, "root")
+
+ expect(result).toEqual([])
+ })
+
+ test("returns empty array when parent does not exist", () => {
+ const sessions = [
+ createMockSession({ id: "a", parentID: undefined }),
+ createMockSession({ id: "b", parentID: "a" }),
+ ]
+
+ const result = filterSubagents(sessions, "nonexistent")
+
+ expect(result).toEqual([])
+ })
+})
+
+describe("getStatusIndicator", () => {
+ test("returns idle indicator when status is undefined", () => {
+ const result = getStatusIndicator(undefined)
+
+ expect(result).toEqual({ icon: "●", status: "idle" })
+ })
+
+ test("returns busy indicator for busy status", () => {
+ const result = getStatusIndicator({ type: "busy" })
+
+ expect(result).toEqual({ icon: "◐", status: "busy" })
+ })
+
+ test("returns retry indicator for retry status", () => {
+ const result = getStatusIndicator({ type: "retry", attempt: 1, message: "test", next: Date.now() })
+
+ expect(result).toEqual({ icon: "↻", status: "retry" })
+ })
+
+ test("returns idle indicator for idle status", () => {
+ const result = getStatusIndicator({ type: "idle" })
+
+ expect(result).toEqual({ icon: "●", status: "idle" })
+ })
+})
+
+describe("formatTimeAgo", () => {
+ test("returns 'now' for timestamps within 10 seconds", () => {
+ const result = formatTimeAgo(Date.now() - 5000)
+
+ expect(result).toBe("now")
+ })
+
+ test("returns seconds for timestamps between 10s and 1m", () => {
+ const result = formatTimeAgo(Date.now() - 30000)
+
+ expect(result).toBe("30s ago")
+ })
+
+ test("returns minutes for timestamps between 1m and 1h", () => {
+ const result = formatTimeAgo(Date.now() - 5 * 60 * 1000)
+
+ expect(result).toBe("5m ago")
+ })
+
+ test("returns hours for timestamps over 1h", () => {
+ const result = formatTimeAgo(Date.now() - 2 * 60 * 60 * 1000)
+
+ expect(result).toBe("2h ago")
+ })
+})
+
+describe("extractAgentType", () => {
+ test("extracts agent type from colon-prefixed title", () => {
+ const session = createMockSession({ title: "explore: Find authentication patterns" })
+
+ const result = extractAgentType(session)
+
+ expect(result).toBe("explore")
+ })
+
+ test("returns 'subagent' when no colon in title", () => {
+ const session = createMockSession({ title: "Some random task" })
+
+ const result = extractAgentType(session)
+
+ expect(result).toBe("subagent")
+ })
+
+ test("returns 'subagent' when colon is too far in title", () => {
+ const session = createMockSession({ title: "This is a very long prefix that exceeds limit: task" })
+
+ const result = extractAgentType(session)
+
+ expect(result).toBe("subagent")
+ })
+
+ test("handles librarian agent type", () => {
+ const session = createMockSession({ title: "librarian: JWT documentation" })
+
+ const result = extractAgentType(session)
+
+ expect(result).toBe("librarian")
+ })
+})
+
+describe("buildSubagentOptions", () => {
+ test("returns empty array when no subagents", () => {
+ const sessions = [createMockSession({ id: "root", parentID: undefined })]
+ const statuses: Record = {}
+
+ const result = buildSubagentOptions(sessions, statuses, "root")
+
+ expect(result).toEqual([])
+ })
+
+ test("builds options sorted by creation time", () => {
+ const now = Date.now()
+ const sessions = [
+ createMockSession({ id: "root", parentID: undefined }),
+ createMockSession({
+ id: "child2",
+ parentID: "root",
+ title: "Second",
+ time: { created: now - 1000, updated: now },
+ }),
+ createMockSession({
+ id: "child1",
+ parentID: "root",
+ title: "First",
+ time: { created: now - 2000, updated: now },
+ }),
+ ]
+ const statuses: Record = {}
+
+ const result = buildSubagentOptions(sessions, statuses, "root")
+
+ expect(result.map((x) => x.id)).toEqual(["child1", "child2"])
+ })
+
+ test("includes status from statuses map", () => {
+ const sessions = [
+ createMockSession({ id: "root", parentID: undefined }),
+ createMockSession({ id: "child", parentID: "root", title: "explore: Task" }),
+ ]
+ const statuses: Record = {
+ child: { type: "busy" },
+ }
+
+ const result = buildSubagentOptions(sessions, statuses, "root")
+
+ expect(result[0].status).toBe("busy")
+ expect(result[0].statusIcon).toBe("◐")
+ })
+})
+
+describe("hasSubagents", () => {
+ test("returns true when session has children", () => {
+ const sessions = [
+ createMockSession({ id: "root", parentID: undefined }),
+ createMockSession({ id: "child", parentID: "root" }),
+ ]
+
+ expect(hasSubagents(sessions, "root")).toBe(true)
+ })
+
+ test("returns false when session has no children", () => {
+ const sessions = [createMockSession({ id: "root", parentID: undefined })]
+
+ expect(hasSubagents(sessions, "root")).toBe(false)
+ })
+})
+
+describe("countSubagents", () => {
+ test("returns correct count of children", () => {
+ const sessions = [
+ createMockSession({ id: "root", parentID: undefined }),
+ createMockSession({ id: "child1", parentID: "root" }),
+ createMockSession({ id: "child2", parentID: "root" }),
+ createMockSession({ id: "child3", parentID: "root" }),
+ ]
+
+ expect(countSubagents(sessions, "root")).toBe(3)
+ })
+
+ test("returns 0 when no children", () => {
+ const sessions = [createMockSession({ id: "root", parentID: undefined })]
+
+ expect(countSubagents(sessions, "root")).toBe(0)
+ })
+})
+
+describe("buildSubagentOptions - tree structure", () => {
+ test("builds recursive tree with correct depths", () => {
+ const now = Date.now()
+ const sessions = [
+ createMockSession({ id: "root", parentID: undefined }),
+ createMockSession({
+ id: "child1",
+ parentID: "root",
+ title: "explore: Task 1",
+ time: { created: now - 3000, updated: now },
+ }),
+ createMockSession({
+ id: "child2",
+ parentID: "root",
+ title: "librarian: Task 2",
+ time: { created: now - 2000, updated: now },
+ }),
+ createMockSession({
+ id: "grandchild1",
+ parentID: "child1",
+ title: "oracle: Subtask",
+ time: { created: now - 1000, updated: now },
+ }),
+ ]
+ const statuses: Record = {}
+
+ const result = buildSubagentOptions(sessions, statuses, "root")
+
+ expect(result.map((x) => ({ id: x.id, depth: x.depth }))).toEqual([
+ { id: "child1", depth: 0 },
+ { id: "grandchild1", depth: 1 },
+ { id: "child2", depth: 0 },
+ ])
+ })
+
+ test("builds deeply nested tree", () => {
+ const now = Date.now()
+ const sessions = [
+ createMockSession({ id: "root", parentID: undefined }),
+ createMockSession({
+ id: "level1",
+ parentID: "root",
+ title: "Level 1",
+ time: { created: now - 4000, updated: now },
+ }),
+ createMockSession({
+ id: "level2",
+ parentID: "level1",
+ title: "Level 2",
+ time: { created: now - 3000, updated: now },
+ }),
+ createMockSession({
+ id: "level3",
+ parentID: "level2",
+ title: "Level 3",
+ time: { created: now - 2000, updated: now },
+ }),
+ createMockSession({
+ id: "level4",
+ parentID: "level3",
+ title: "Level 4",
+ time: { created: now - 1000, updated: now },
+ }),
+ ]
+ const statuses: Record = {}
+
+ const result = buildSubagentOptions(sessions, statuses, "root")
+
+ expect(result.map((x) => ({ id: x.id, depth: x.depth }))).toEqual([
+ { id: "level1", depth: 0 },
+ { id: "level2", depth: 1 },
+ { id: "level3", depth: 2 },
+ { id: "level4", depth: 3 },
+ ])
+ })
+
+ test("handles multiple branches at same level", () => {
+ const now = Date.now()
+ const sessions = [
+ createMockSession({ id: "root", parentID: undefined }),
+ createMockSession({
+ id: "branch1",
+ parentID: "root",
+ title: "Branch 1",
+ time: { created: now - 5000, updated: now },
+ }),
+ createMockSession({
+ id: "branch2",
+ parentID: "root",
+ title: "Branch 2",
+ time: { created: now - 4000, updated: now },
+ }),
+ createMockSession({
+ id: "branch1_child",
+ parentID: "branch1",
+ title: "Branch 1 Child",
+ time: { created: now - 3000, updated: now },
+ }),
+ createMockSession({
+ id: "branch2_child",
+ parentID: "branch2",
+ title: "Branch 2 Child",
+ time: { created: now - 2000, updated: now },
+ }),
+ ]
+ const statuses: Record = {}
+
+ const result = buildSubagentOptions(sessions, statuses, "root")
+
+ expect(result.map((x) => ({ id: x.id, depth: x.depth }))).toEqual([
+ { id: "branch1", depth: 0 },
+ { id: "branch1_child", depth: 1 },
+ { id: "branch2", depth: 0 },
+ { id: "branch2_child", depth: 1 },
+ ])
+ })
+})
+
+describe("countAllDescendants", () => {
+ test("counts all descendants recursively", () => {
+ const sessions = [
+ createMockSession({ id: "root", parentID: undefined }),
+ createMockSession({ id: "child1", parentID: "root" }),
+ createMockSession({ id: "child2", parentID: "root" }),
+ createMockSession({ id: "grandchild1", parentID: "child1" }),
+ createMockSession({ id: "grandchild2", parentID: "child1" }),
+ createMockSession({ id: "greatgrandchild", parentID: "grandchild1" }),
+ ]
+
+ expect(countAllDescendants(sessions, "root")).toBe(5)
+ })
+
+ test("returns 0 when no descendants", () => {
+ const sessions = [createMockSession({ id: "root", parentID: undefined })]
+
+ expect(countAllDescendants(sessions, "root")).toBe(0)
+ })
+
+ test("counts only direct children when no grandchildren", () => {
+ const sessions = [
+ createMockSession({ id: "root", parentID: undefined }),
+ createMockSession({ id: "child1", parentID: "root" }),
+ createMockSession({ id: "child2", parentID: "root" }),
+ ]
+
+ expect(countAllDescendants(sessions, "root")).toBe(2)
+ })
+})
+
+describe("findRootSession", () => {
+ test("returns the root when given a deeply nested session", () => {
+ const sessions = [
+ createMockSession({ id: "root", parentID: undefined }),
+ createMockSession({ id: "child", parentID: "root" }),
+ createMockSession({ id: "grandchild", parentID: "child" }),
+ createMockSession({ id: "greatgrandchild", parentID: "grandchild" }),
+ ]
+
+ const result = findRootSession(sessions, "greatgrandchild")
+
+ expect(result?.id).toBe("root")
+ })
+
+ test("returns the session itself when it has no parent", () => {
+ const sessions = [createMockSession({ id: "root", parentID: undefined })]
+
+ const result = findRootSession(sessions, "root")
+
+ expect(result?.id).toBe("root")
+ })
+
+ test("returns undefined when session not found", () => {
+ const sessions = [createMockSession({ id: "root", parentID: undefined })]
+
+ const result = findRootSession(sessions, "nonexistent")
+
+ expect(result).toBeUndefined()
+ })
+
+ test("returns the session when parent is not in sessions list", () => {
+ const sessions = [createMockSession({ id: "orphan", parentID: "missing_parent" })]
+
+ const result = findRootSession(sessions, "orphan")
+
+ expect(result?.id).toBe("orphan")
+ })
+})
+
+describe("buildSubagentOptions - isCurrent marking", () => {
+ test("marks current session with isCurrent true", () => {
+ const now = Date.now()
+ const sessions = [
+ createMockSession({ id: "root", parentID: undefined }),
+ createMockSession({
+ id: "child1",
+ parentID: "root",
+ title: "Child 1",
+ time: { created: now - 2000, updated: now },
+ }),
+ createMockSession({
+ id: "child2",
+ parentID: "root",
+ title: "Child 2",
+ time: { created: now - 1000, updated: now },
+ }),
+ ]
+ const statuses: Record = {}
+
+ const result = buildSubagentOptions(sessions, statuses, "child1")
+
+ expect(result.find((x) => x.id === "child1")?.isCurrent).toBe(true)
+ expect(result.find((x) => x.id === "child2")?.isCurrent).toBe(false)
+ })
+
+ test("shows full tree from root when called from nested session", () => {
+ const now = Date.now()
+ const sessions = [
+ createMockSession({ id: "root", parentID: undefined }),
+ createMockSession({
+ id: "child",
+ parentID: "root",
+ title: "Child",
+ time: { created: now - 3000, updated: now },
+ }),
+ createMockSession({
+ id: "grandchild",
+ parentID: "child",
+ title: "Grandchild",
+ time: { created: now - 2000, updated: now },
+ }),
+ createMockSession({
+ id: "greatgrandchild",
+ parentID: "grandchild",
+ title: "Great Grandchild",
+ time: { created: now - 1000, updated: now },
+ }),
+ ]
+ const statuses: Record = {}
+
+ const result = buildSubagentOptions(sessions, statuses, "greatgrandchild")
+
+ expect(result.map((x) => x.id)).toEqual(["child", "grandchild", "greatgrandchild"])
+ expect(result.find((x) => x.id === "greatgrandchild")?.isCurrent).toBe(true)
+ expect(result.find((x) => x.id === "child")?.isCurrent).toBe(false)
+ expect(result.find((x) => x.id === "grandchild")?.isCurrent).toBe(false)
+ })
+
+ test("marks correct session when in middle of tree", () => {
+ const now = Date.now()
+ const sessions = [
+ createMockSession({ id: "root", parentID: undefined }),
+ createMockSession({
+ id: "child",
+ parentID: "root",
+ title: "Child",
+ time: { created: now - 2000, updated: now },
+ }),
+ createMockSession({
+ id: "grandchild",
+ parentID: "child",
+ title: "Grandchild",
+ time: { created: now - 1000, updated: now },
+ }),
+ ]
+ const statuses: Record = {}
+
+ const result = buildSubagentOptions(sessions, statuses, "child")
+
+ expect(result.find((x) => x.id === "child")?.isCurrent).toBe(true)
+ expect(result.find((x) => x.id === "grandchild")?.isCurrent).toBe(false)
+ })
+})