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) + }) +})